GnosisSafe.GuardManager.spec.ts 7.74 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 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154
import { expect } from "chai";
import hre, { deployments, waffle, ethers } from "hardhat";
import { BigNumber } from "ethers";
import "@nomiclabs/hardhat-ethers";
import { AddressZero } from "@ethersproject/constants";
import { getMock, getSafeWithOwners } from "../utils/setup";
import { buildContractCall, buildSafeTransaction, buildSignatureBytes, calculateSafeTransactionHash, executeContractCallWithSigners, executeTx, safeApproveHash } from "../../src/utils/execution";
import { chainId } from "../utils/encoding";

describe("GuardManager", async () => {

    const [user1, user2] = waffle.provider.getWallets();

    const setupWithTemplate = deployments.createFixture(async ({ deployments }) => {
        await deployments.fixture();
        const mock = await getMock();
        const safe = await getSafeWithOwners([user2.address])
        await executeContractCallWithSigners(safe, safe, "setGuard", [mock.address], [user2])
        return {
            safe,
            mock
        }
    })

    describe("setGuard", async () => {

        it('is not called when setting initially', async () => {
            const { safe, mock } = await setupWithTemplate()

            const slot = ethers.utils.keccak256(ethers.utils.toUtf8Bytes("guard_manager.guard.address"))

            await executeContractCallWithSigners(safe, safe, "setGuard", [AddressZero], [user2])

            // Check fallback handler
            await expect(
                await hre.ethers.provider.getStorageAt(safe.address, slot)
            ).to.be.eq("0x" + "".padStart(64, "0"))

            await mock.reset()

            await expect(
                await executeContractCallWithSigners(safe, safe, "setGuard", [mock.address], [user2])
            ).to.emit(safe, "ChangedGuard").withArgs(mock.address)

            // Check fallback handler
            await expect(
                await hre.ethers.provider.getStorageAt(safe.address, slot)
            ).to.be.eq("0x" + mock.address.toLowerCase().slice(2).padStart(64, "0"))
            
            // Guard should not be called, as it was not set before the transaction execution
            expect(await mock.callStatic.invocationCount()).to.be.eq(0);
        })

        it('is called when removed', async () => {
            const { safe, mock } = await setupWithTemplate()

            const slot = ethers.utils.keccak256(ethers.utils.toUtf8Bytes("guard_manager.guard.address"))

            // Check fallback handler
            await expect(
                await hre.ethers.provider.getStorageAt(safe.address, slot)
            ).to.be.eq("0x" + mock.address.toLowerCase().slice(2).padStart(64, "0"))

            const safeTx = buildContractCall(safe, "setGuard", [AddressZero], await safe.nonce())
            const signature = await safeApproveHash(user2, safe, safeTx)
            const signatureBytes = buildSignatureBytes([signature])

            await expect(
                executeTx(safe, safeTx, [signature])
            ).to.emit(safe, "ChangedGuard").withArgs(AddressZero)

            // Check fallback handler
            await expect(
                await hre.ethers.provider.getStorageAt(safe.address, slot)
            ).to.be.eq("0x" + "".padStart(64, "0"))
            
            expect(await mock.callStatic.invocationCount()).to.be.eq(2);
            const guardInterface = (await hre.ethers.getContractAt("Guard", mock.address)).interface
            const checkTxData = guardInterface.encodeFunctionData("checkTransaction", [
                safeTx.to, safeTx.value, safeTx.data, safeTx.operation, safeTx.safeTxGas,
                safeTx.baseGas, safeTx.gasPrice, safeTx.gasToken, safeTx.refundReceiver,
                signatureBytes, user1.address
            ])
            expect(await mock.callStatic.invocationCountForCalldata(checkTxData)).to.be.eq(1);
            // Guard should also be called for post exec check, even if it is removed with the Safe tx
            const checkExecData = guardInterface.encodeFunctionData("checkAfterExecution", [calculateSafeTransactionHash(safe, safeTx, await chainId()), true])
            expect(await mock.callStatic.invocationCountForCalldata(checkExecData)).to.be.eq(1);
        })
    })

    describe("execTransaction", async () => {
        it('reverts if the pre hook of the guard reverts', async () => {
            const { safe, mock } = await setupWithTemplate()

            const safeTx = buildSafeTransaction({ to: mock.address, data: "0xbaddad42", nonce: 1 })
            const signature = await safeApproveHash(user2, safe, safeTx)
            const signatureBytes = buildSignatureBytes([signature])
            const guardInterface = (await hre.ethers.getContractAt("Guard", mock.address)).interface
            const checkTxData = guardInterface.encodeFunctionData("checkTransaction", [
                safeTx.to, safeTx.value, safeTx.data, safeTx.operation, safeTx.safeTxGas,
                safeTx.baseGas, safeTx.gasPrice, safeTx.gasToken, safeTx.refundReceiver,
                signatureBytes, user1.address
            ])
            await mock.givenCalldataRevertWithMessage(checkTxData, "Computer says Nah")
            const checkExecData = guardInterface.encodeFunctionData("checkAfterExecution", [calculateSafeTransactionHash(safe, safeTx, await chainId()), true])

            await expect(
                executeTx(safe, safeTx, [signature])
            ).to.be.revertedWith("Computer says Nah")

            await mock.reset()

            await expect(
                executeTx(safe, safeTx, [signature])
            ).to.emit(safe, "ExecutionSuccess")

            expect(await mock.callStatic.invocationCount()).to.be.deep.equals(BigNumber.from(3));
            expect(await mock.callStatic.invocationCountForCalldata(checkTxData)).to.be.deep.equals(BigNumber.from(1));
            expect(await mock.callStatic.invocationCountForCalldata(checkExecData)).to.be.deep.equals(BigNumber.from(1));
            expect(await mock.callStatic.invocationCountForCalldata("0xbaddad42")).to.be.deep.equals(BigNumber.from(1));
        })

        it('reverts if the post hook of the guard reverts', async () => {
            const { safe, mock } = await setupWithTemplate()

            const safeTx = buildSafeTransaction({ to: mock.address, data: "0xbaddad42", nonce: 1 })
            const signature = await safeApproveHash(user2, safe, safeTx)
            const signatureBytes = buildSignatureBytes([signature])
            const guardInterface = (await hre.ethers.getContractAt("Guard", mock.address)).interface
            const checkTxData = guardInterface.encodeFunctionData("checkTransaction", [
                safeTx.to, safeTx.value, safeTx.data, safeTx.operation, safeTx.safeTxGas,
                safeTx.baseGas, safeTx.gasPrice, safeTx.gasToken, safeTx.refundReceiver,
                signatureBytes, user1.address
            ])
            const checkExecData = guardInterface.encodeFunctionData("checkAfterExecution", [calculateSafeTransactionHash(safe, safeTx, await chainId()), true])
            await mock.givenCalldataRevertWithMessage(checkExecData, "Computer says Nah")

            await expect(
                executeTx(safe, safeTx, [signature])
            ).to.be.revertedWith("Computer says Nah")

            await mock.reset()

            await expect(
                executeTx(safe, safeTx, [signature])
            ).to.emit(safe, "ExecutionSuccess")

            expect(await mock.callStatic.invocationCount()).to.be.deep.equals(BigNumber.from(3));
            expect(await mock.callStatic.invocationCountForCalldata(checkTxData)).to.be.deep.equals(BigNumber.from(1));
            expect(await mock.callStatic.invocationCountForCalldata(checkExecData)).to.be.deep.equals(BigNumber.from(1));
            expect(await mock.callStatic.invocationCountForCalldata("0xbaddad42")).to.be.deep.equals(BigNumber.from(1));
        })
    })
})