@safe-global/safe-contracts
Version:
Ethereum multisig contract
193 lines (160 loc) • 9.02 kB
text/typescript
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 guardContract = await hre.ethers.getContractAt("Guard", AddressZero);
const guardEip165Calldata = guardContract.interface.encodeFunctionData("supportsInterface", ["0xe6d7a83a"]);
await mock.givenCalldataReturnBool(guardEip165Calldata, true);
const safe = await getSafeWithOwners([user2.address]);
await executeContractCallWithSigners(safe, safe, "setGuard", [mock.address], [user2]);
return {
safe,
mock,
guardEip165Calldata,
};
});
describe("setGuard", async () => {
it("is not called when setting initially", async () => {
const { safe, mock, guardEip165Calldata } = await setupWithTemplate();
const slot = ethers.utils.keccak256(ethers.utils.toUtf8Bytes("guard_manager.guard.address"));
await executeContractCallWithSigners(safe, safe, "setGuard", [AddressZero], [user2]);
// Check guard
await expect(await hre.ethers.provider.getStorageAt(safe.address, slot)).to.be.eq("0x" + "".padStart(64, "0"));
await mock.reset();
await expect(await hre.ethers.provider.getStorageAt(safe.address, slot)).to.be.eq("0x" + "".padStart(64, "0"));
// Reverts if it doesn't implement ERC165 Guard Interface
await expect(executeContractCallWithSigners(safe, safe, "setGuard", [mock.address], [user2])).to.be.revertedWith("GS013");
await mock.givenCalldataReturnBool(guardEip165Calldata, true);
await expect(executeContractCallWithSigners(safe, safe, "setGuard", [mock.address], [user2]))
.to.emit(safe, "ChangedGuard")
.withArgs(mock.address);
// Check guard
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 guard
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 guard
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));
});
});
});