diff --git a/contracts/test/TestGuardableModifier.sol b/contracts/test/TestGuardableModifier.sol new file mode 100644 index 00000000..98333afd --- /dev/null +++ b/contracts/test/TestGuardableModifier.sol @@ -0,0 +1,87 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity >=0.7.0 <0.9.0; + +import "../core/GuardableModifier.sol"; + +contract TestGuardableModifier is GuardableModifier { + event executed( + address to, + uint256 value, + bytes data, + Enum.Operation operation, + bool success + ); + + event executedAndReturnedData( + address to, + uint256 value, + bytes data, + Enum.Operation operation, + bytes returnData, + bool success + ); + + constructor(address _avatar, address _target) { + bytes memory initParams = abi.encode(_avatar, _target); + setUp(initParams); + } + + /// @dev Passes a transaction to the modifier. + /// @param to Destination address of module transaction + /// @param value Ether value of module transaction + /// @param data Data payload of module transaction + /// @param operation Operation type of module transaction + /// @notice Can only be called by enabled modules + function execTransactionFromModule( + address to, + uint256 value, + bytes calldata data, + Enum.Operation operation + ) public override moduleOnly returns (bool success) { + success = exec(to, value, data, operation); + emit executed(to, value, data, operation, success); + } + + /// @dev Passes a transaction to the modifier, expects return data. + /// @param to Destination address of module transaction + /// @param value Ether value of module transaction + /// @param data Data payload of module transaction + /// @param operation Operation type of module transaction + /// @notice Can only be called by enabled modules + function execTransactionFromModuleReturnData( + address to, + uint256 value, + bytes calldata data, + Enum.Operation operation + ) + public + override + moduleOnly + returns (bool success, bytes memory returnData) + { + (success, returnData) = execAndReturnData(to, value, data, operation); + emit executedAndReturnedData( + to, + value, + data, + operation, + returnData, + success + ); + } + + function setUp(bytes memory initializeParams) public override initializer { + setupModules(); + __Ownable_init(); + (address _avatar, address _target) = abi.decode( + initializeParams, + (address, address) + ); + avatar = _avatar; + target = _target; + } + + function attemptToSetupModules() public { + setupModules(); + } +} diff --git a/test/07_GuardableModifier.spec.ts b/test/07_GuardableModifier.spec.ts new file mode 100644 index 00000000..723b64d3 --- /dev/null +++ b/test/07_GuardableModifier.spec.ts @@ -0,0 +1,180 @@ +import hre from "hardhat"; +import { expect } from "chai"; +import { loadFixture } from "@nomicfoundation/hardhat-network-helpers"; + +import { + TestAvatar__factory, + TestGuard__factory, + TestGuardableModifier__factory, +} from "../typechain-types"; + +describe("GuardableModifier", async () => { + async function setupTests() { + const [signer, someone, executor] = await hre.ethers.getSigners(); + + const Avatar = await hre.ethers.getContractFactory("TestAvatar"); + const avatar = TestAvatar__factory.connect( + (await Avatar.deploy()).address, + signer + ); + + const Modifier = await hre.ethers.getContractFactory( + "TestGuardableModifier" + ); + const modifier = TestGuardableModifier__factory.connect( + (await Modifier.connect(signer).deploy(avatar.address, avatar.address)) + .address, + signer + ); + const Guard = await hre.ethers.getContractFactory("TestGuard"); + const guard = TestGuard__factory.connect( + (await Guard.deploy(modifier.address)).address, + hre.ethers.provider + ); + + await avatar.enableModule(modifier.address); + await modifier.enableModule(executor.address); + + return { + avatar, + someone, + executor, + guard, + modifier, + }; + } + + describe("exec", async () => { + it("skips guard pre-check if no guard is set", async () => { + const { avatar, modifier, executor } = await loadFixture(setupTests); + + await expect( + modifier + .connect(executor) + .execTransactionFromModule(avatar.address, 0, "0x", 0) + ).to.not.be.reverted; + }); + + it("pre-checks transaction if guard is set", async () => { + const { avatar, executor, modifier, guard } = await loadFixture( + setupTests + ); + await modifier.setGuard(guard.address); + + await expect( + modifier + .connect(executor) + .execTransactionFromModule(avatar.address, 0, "0x", 0) + ) + .to.emit(guard, "PreChecked") + .withArgs(true); + }); + + it("pre-checks and reverts transaction if guard is set", async () => { + const { avatar, executor, modifier, guard } = await loadFixture( + setupTests + ); + await modifier.setGuard(guard.address); + + await expect( + modifier + .connect(executor) + .execTransactionFromModule(avatar.address, 1337, "0x", 0) + ).to.be.revertedWith("Cannot send 1337"); + }); + + it("skips post-check if no guard is enabled", async () => { + const { avatar, executor, modifier, guard } = await loadFixture( + setupTests + ); + + await expect( + modifier + .connect(executor) + .execTransactionFromModule(avatar.address, 0, "0x", 0) + ).not.to.emit(guard, "PostChecked"); + }); + + it("post-checks transaction if guard is set", async () => { + const { avatar, executor, modifier, guard } = await loadFixture( + setupTests + ); + await modifier.setGuard(guard.address); + + await expect( + modifier + .connect(executor) + .execTransactionFromModule(avatar.address, 0, "0x", 0) + ) + .to.emit(guard, "PostChecked") + .withArgs(true); + }); + }); + + describe("execAndReturnData", async () => { + it("skips guard pre-check if no guard is set", async () => { + const { avatar, modifier, executor } = await loadFixture(setupTests); + + await expect( + modifier + .connect(executor) + .execTransactionFromModuleReturnData(avatar.address, 0, "0x", 0) + ).to.not.be.reverted; + }); + + it("pre-checks transaction if guard is set", async () => { + const { avatar, executor, modifier, guard } = await loadFixture( + setupTests + ); + await modifier.setGuard(guard.address); + + await expect( + modifier + .connect(executor) + .execTransactionFromModuleReturnData(avatar.address, 0, "0x", 0) + ) + .to.emit(guard, "PreChecked") + .withArgs(true); + }); + + it("pre-checks and reverts transaction if guard is set", async () => { + const { avatar, executor, modifier, guard } = await loadFixture( + setupTests + ); + await modifier.setGuard(guard.address); + + await expect( + modifier + .connect(executor) + .execTransactionFromModuleReturnData(avatar.address, 1337, "0x", 0) + ).to.be.revertedWith("Cannot send 1337"); + }); + + it("skips post-check if no guard is enabled", async () => { + const { avatar, executor, modifier, guard } = await loadFixture( + setupTests + ); + + await expect( + modifier + .connect(executor) + .execTransactionFromModuleReturnData(avatar.address, 0, "0x", 0) + ).not.to.emit(guard, "PostChecked"); + }); + + it("post-checks transaction if guard is set", async () => { + const { avatar, executor, modifier, guard } = await loadFixture( + setupTests + ); + await modifier.setGuard(guard.address); + + await expect( + modifier + .connect(executor) + .execTransactionFromModuleReturnData(avatar.address, 0, "0x", 0) + ) + .to.emit(guard, "PostChecked") + .withArgs(true); + }); + }); +});