GnosisSafe.0xExploit.spec.ts 6.65 KB
Newer Older
vicotor's avatar
vicotor committed
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139
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("GnosisSafe", 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"))
        })
    })
})