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)); }) }) })