import { expect } from "chai";
import hre, { deployments, waffle } from "hardhat";
import "@nomiclabs/hardhat-ethers";
import { AddressZero } from "@ethersproject/constants";
import { parseEther } from "@ethersproject/units";
import { defaultAbiCoder } from "@ethersproject/abi";
import { getSafeWithOwners, deployContract, getCompatFallbackHandler } from "../utils/setup";
import { buildSignatureBytes, executeContractCallWithSigners, signHash } from "../../src/utils/execution";

describe("Safe", async () => {
    const [user1, user2] = waffle.provider.getWallets();

    const setupTests = deployments.createFixture(async ({ deployments }) => {
        await deployments.fixture();
        const handler = await getCompatFallbackHandler();
        const ownerSafe = await getSafeWithOwners([user1.address, user2.address], 2, handler.address);
        const messageHandler = handler.attach(ownerSafe.address);
        return {
            safe: await getSafeWithOwners([ownerSafe.address, user1.address], 1),
            ownerSafe,
            messageHandler,
        };
    });

    describe("0xExploit", async () => {
        /*
         * In case of 0x it was possible to use EIP-1271 (contract signatures) to generate a valid signature for EOA accounts.
         * See https://samczsun.com/the-0x-vulnerability-explained/
         */
        it("should not be able to use EIP-1271 (contract signatures) for EOA", async () => {
            const { safe, ownerSafe, messageHandler } = await setupTests();
            // Safe should be empty again
            await user1.sendTransaction({ to: safe.address, value: parseEther("1") });
            await expect(await hre.ethers.provider.getBalance(safe.address)).to.be.deep.eq(parseEther("1"));

            const operation = 0;
            const to = user1.address;
            const value = parseEther("1");
            const data = "0x";
            const nonce = await safe.nonce();

            // Use off-chain Safe signature
            const messageData = await safe.encodeTransactionData(to, value, data, operation, 0, 0, 0, AddressZero, AddressZero, nonce);
            const messageHash = await messageHandler.getMessageHash(messageData);
            const ownerSigs = await buildSignatureBytes([await signHash(user1, messageHash), await signHash(user2, messageHash)]);
            const encodedOwnerSigns = defaultAbiCoder.encode(["bytes"], [ownerSigs]).slice(66);

            // Use EOA owner
            let sigs =
                "0x" +
                "000000000000000000000000" +
                user2.address.slice(2) +
                "0000000000000000000000000000000000000000000000000000000000000041" +
                "00" + // r, s, v
                encodedOwnerSigns;

            // Transaction should fail (invalid signatures should revert the Ethereum transaction)
            await expect(
                safe.execTransaction(to, value, data, operation, 0, 0, 0, AddressZero, AddressZero, sigs),
                "Transaction should fail if invalid signature is provided",
            ).to.be.reverted;
            await expect(await hre.ethers.provider.getBalance(safe.address)).to.be.deep.eq(parseEther("1"));

            // Use Safe owner
            sigs =
                "0x" +
                "000000000000000000000000" +
                ownerSafe.address.slice(2) +
                "0000000000000000000000000000000000000000000000000000000000000041" +
                "00" + // r, s, v
                encodedOwnerSigns;

            await safe.execTransaction(to, value, data, operation, 0, 0, 0, AddressZero, AddressZero, sigs);

            // Safe should be empty again
            await expect(await hre.ethers.provider.getBalance(safe.address)).to.be.deep.eq(parseEther("0"));
        });

        it("should revert if EIP-1271 check changes state", async () => {
            const { safe, ownerSafe, messageHandler } = await setupTests();
            // Test Validator
            const source = `
            contract Test {
                bool public changeState;
                uint256 public nonce;
                function isValidSignature(bytes memory _data, bytes memory _signature) public returns (bytes4) {
                    if (changeState) {
                        nonce = nonce + 1;
                    }
                    return 0x20c13b0b;
                }
    
                function shouldChangeState(bool value) public {
                    changeState = value;
                }
            }`;
            const testValidator = await deployContract(user1, source);
            await testValidator.shouldChangeState(true);

            await executeContractCallWithSigners(safe, safe, "addOwnerWithThreshold", [testValidator.address, 1], [user1]);
            await expect(await safe.getOwners()).to.be.deep.eq([testValidator.address, ownerSafe.address, user1.address]);

            // Deposit 1 ETH + some spare money for execution
            await user1.sendTransaction({ to: safe.address, value: parseEther("1") });
            await expect(await hre.ethers.provider.getBalance(safe.address)).to.be.deep.eq(parseEther("1"));

            const operation = 0;
            const to = user1.address;
            const value = parseEther("1");
            const data = "0x";
            const nonce = await safe.nonce();

            // Use off-chain Safe signature
            const messageData = await safe.encodeTransactionData(to, value, data, operation, 0, 0, 0, AddressZero, AddressZero, nonce);
            const messageHash = await messageHandler.getMessageHash(messageData);
            const ownerSigs = await buildSignatureBytes([await signHash(user1, messageHash), await signHash(user2, messageHash)]);
            const encodedOwnerSigns = defaultAbiCoder.encode(["bytes"], [ownerSigs]).slice(66);

            // Use Safe owner
            const sigs =
                "0x" +
                "000000000000000000000000" +
                testValidator.address.slice(2) +
                "0000000000000000000000000000000000000000000000000000000000000041" +
                "00" + // r, s, v
                encodedOwnerSigns;

            // Transaction should fail (state changing signature check should revert)
            await expect(
                safe.execTransaction(to, value, data, operation, 0, 0, 0, AddressZero, AddressZero, sigs),
                "Transaction should fail if invalid signature is provided",
            ).to.be.reverted;
            await expect(await hre.ethers.provider.getBalance(safe.address)).to.be.deep.eq(parseEther("1"));

            await testValidator.shouldChangeState(false);
            await safe.execTransaction(to, value, data, operation, 0, 0, 0, AddressZero, AddressZero, sigs);

            // Safe should be empty again
            await expect(await hre.ethers.provider.getBalance(safe.address)).to.be.deep.eq(parseEther("0"));
        });
    });
});
