diff --git a/foundry.toml b/foundry.toml index acb113d..7d7a4c9 100644 --- a/foundry.toml +++ b/foundry.toml @@ -6,9 +6,18 @@ libs = ["node_modules", "lib"] # Compiler auto_detect_solc = true +via_ir = true optimizer = true -optimizer-runs = 1_000_000 +optimizer-runs = 1 # Network chain_id = 1 -rpc_endpoints = { mainnet = "${MAINNET_RPC_URL}" } \ No newline at end of file +rpc_endpoints = { mainnet = "${MAINNET_RPC_URL}" } + +# Tests +verbosity = 2 + +# Formatting +line_length = 110 +number_underscore = 'thousands' +bracket_spacing = true \ No newline at end of file diff --git a/remappings.txt b/remappings.txt index 055d9d8..a979aeb 100644 --- a/remappings.txt +++ b/remappings.txt @@ -1,4 +1,5 @@ src/=src/ +base/=src/base/ common/=src/common/ forge-std/=lib/forge-std/src/ diff --git a/src/ExampleProposal.sol b/src/ExampleProposal.sol new file mode 100644 index 0000000..b93bf25 --- /dev/null +++ b/src/ExampleProposal.sol @@ -0,0 +1,9 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.19; + +import {TornadoProposal} from "base/TornadoProposal.sol"; + +contract ExampleProposal is TornadoProposal { + function executeProposal() public virtual override { /* ... */ } +} diff --git a/src/base/TornadoProposal.sol b/src/base/TornadoProposal.sol index b0c0bad..04b9161 100644 --- a/src/base/TornadoProposal.sol +++ b/src/base/TornadoProposal.sol @@ -8,34 +8,24 @@ import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {IGovernance} from "common/interfaces/IGovernance.sol"; import {IGnosisSafe} from "common/interfaces/IGnosisSafe.sol"; -abstract contract TornadoProposal { +import {TornadoAddresses} from "common/TornadoAddresses.sol"; + +abstract contract TornadoProposal is TornadoAddresses { function executeProposal() public virtual; function getMultisig() internal pure returns (IGnosisSafe) { - return IGnosisSafe(0xb04E030140b30C27bcdfaafFFA98C57d80eDa7B4); + return IGnosisSafe(getMultisigAddress()); } - function getEns() internal pure returns (ENS) { - return ENS(0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e); + function getENS() internal pure returns (ENS) { + return ENS(getENSAddress()); } function getTornToken() internal pure returns (IERC20) { - return IERC20(0x77777FeDdddFfC19Ff86DB637967013e6C6A116C); + return IERC20(getTornTokenAddress()); } function getGovernance() internal pure returns (IGovernance) { return IGovernance(getGovernanceProxyAddress()); } - - function getGovernanceProxyAddress() internal pure returns (address) { - return 0x5efda50f22d34F262c29268506C5Fa42cB56A1Ce; - } - - function getStakingProxyAddress() internal pure returns (address) { - return 0x2FC93484614a34f26F7970CBB94615bA109BB4bf; - } - - function getRegistryProxyAddress() internal pure returns (address) { - return 0x58E8dCC13BE9780fC42E8723D8EaD4CF46943dF2; - } } diff --git a/src/common/TornadoAddresses.sol b/src/common/TornadoAddresses.sol new file mode 100644 index 0000000..d4fa917 --- /dev/null +++ b/src/common/TornadoAddresses.sol @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.19; + +contract TornadoAddresses { + function getENSAddress() internal pure returns (address) { + return 0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e; + } + + function getTornTokenAddress() internal pure returns (address) { + return 0x77777FeDdddFfC19Ff86DB637967013e6C6A116C; + } + + function getMultisigAddress() internal pure returns (address) { + return 0xb04E030140b30C27bcdfaafFFA98C57d80eDa7B4; + } + + function getGovernanceProxyAddress() internal pure returns (address) { + return 0x5efda50f22d34F262c29268506C5Fa42cB56A1Ce; + } + + function getStakingProxyAddress() internal pure returns (address) { + return 0x2FC93484614a34f26F7970CBB94615bA109BB4bf; + } + + function getRegistryProxyAddress() internal pure returns (address) { + return 0x58E8dCC13BE9780fC42E8723D8EaD4CF46943dF2; + } +} diff --git a/src/common/interfaces/IGnosisSafe.sol b/src/common/interfaces/IGnosisSafe.sol index 43b5a54..4a11ce3 100644 --- a/src/common/interfaces/IGnosisSafe.sol +++ b/src/common/interfaces/IGnosisSafe.sol @@ -6,6 +6,18 @@ interface IGnosisSafe { DelegateCall } + function NAME() external view returns (string memory); + + function VERSION() external view returns (string memory); + + function nonce() external view returns (uint256); + + function domainSeparator() external view returns (bytes32); + + function signedMessages(bytes32) external view returns (uint256); + + function approvedHashes(address, bytes32) external view returns (uint256); + function setup( address[] calldata _owners, uint256 _threshold, diff --git a/src/common/test/Common.sol b/src/common/test/Common.sol deleted file mode 100644 index 9df7357..0000000 --- a/src/common/test/Common.sol +++ /dev/null @@ -1,33 +0,0 @@ -pragma solidity ^0.8.19; - -contract Common { - uint256 constant TEST_PRIVATE_KEY_ONE = 0x66ddbd7cbe4a566df405f6ded0b908c669f88cdb1656380c050e3a457bd21df0; - uint256 constant TEST_PRIVATE_KEY_TWO = 0xa4c8c98120e77741a87a116074a2df4ddb20d1149069290fd4a3d7ee65c55064; - address constant TEST_ADDRESS_ONE = 0x118251976c65AFAf291f5255450ddb5b6A4d8B88; - address constant TEST_ADDRESS_TWO = 0x63aE7d90Eb37ca39FC62dD9991DbEfeE70673a20; - - address constant ADDRESS_TO_STAKE = 0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045; - uint256 constant STAKE_AMOUNT = 100_000 ether; - - uint256 constant PROPOSAL_DURATION = 7 days; - uint256 constant PROPOSAL_THRESHOLD = 25000 ether; - string constant PROPOSAL_DESCRIPTION = - "{title:'Proposal #22: Test clone of 21 proposal: change locked stake balance directly',description:''}"; - - address constant VERIFIER_ADDRESS = 0x77777FeDdddFfC19Ff86DB637967013e6C6A116C; - - bytes32 constant PERMIT_TYPEHASH = - keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"); - - bytes32 constant EIP712_DOMAIN = keccak256( - abi.encode( - keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"), - keccak256(bytes("TornadoCash")), - keccak256(bytes("1")), - 1, - VERIFIER_ADDRESS - ) - ); - - uint16 constant PERMIT_FUNC_SELECTOR = uint16(0x1901); -} diff --git a/test/TornadoProposalTest.sol b/test/TornadoProposalTest.sol new file mode 100644 index 0000000..f7ef544 --- /dev/null +++ b/test/TornadoProposalTest.sol @@ -0,0 +1,148 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import {IGovernance, Proposal} from "common/interfaces/IGovernance.sol"; + +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {ERC20Permit} from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol"; + +import {Test} from "forge-std/Test.sol"; + +import {TornadoAddresses} from "common/TornadoAddresses.sol"; + +contract TornadoProposalTest is Test, TornadoAddresses { + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ PERMIT ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + bytes32 public constant PERMIT_TYPEHASH = + keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"); + + bytes32 public constant EIP712_DOMAIN = keccak256( + abi.encode( + keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"), + keccak256(bytes("TornadoCash")), + keccak256(bytes("1")), + 1, + VERIFIER_ADDRESS + ) + ); + + uint16 public constant PERMIT_FUNC_SELECTOR = uint16(0x1901); + + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ TEST DUMMIES ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + address public constant TEST_REAL_ADDRESS_WITH_BALANCE = 0x9Ff3C1Bea9ffB56a78824FE29f457F066257DD58; + + address public constant TEST_RELAYER_ADDRESS = 0x30F96AEF199B399B722F8819c9b0723016CEAe6C; // moon-relayer.eth (just for testing) + + uint256 public constant TEST_PRIVATE_KEY_ONE = 0x66ddbd7cbe4a566df405f6ded0b908c669f88cdb1656380c050e3a457bd21df0; + uint256 public constant TEST_PRIVATE_KEY_TWO = 0xa4c8c98120e77741a87a116074a2df4ddb20d1149069290fd4a3d7ee65c55064; + + address public constant TEST_ADDRESS_ONE = 0x118251976c65AFAf291f5255450ddb5b6A4d8B88; + address public constant TEST_ADDRESS_TWO = 0x63aE7d90Eb37ca39FC62dD9991DbEfeE70673a20; + + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ADDRESSES ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + address public constant VERIFIER_ADDRESS = 0x77777FeDdddFfC19Ff86DB637967013e6C6A116C; + + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ GOVERNANCE ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + string public constant PROPOSAL_DESCRIPTION = "{title:'Some proposal',description:''}"; + + uint256 public EXECUTION_DELAY; + uint256 public EXECUTION_EXPIRATION; + uint256 public QUORUM_VOTES; + uint256 public PROPOSAL_THRESHOLD; + uint256 public VOTING_DELAY; + uint256 public VOTING_PERIOD; + uint256 public CLOSING_PERIOD; + uint256 public VOTE_EXTEND_TIME; + + IGovernance public immutable governance = IGovernance(getGovernanceProxyAddress()); + + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ TEST UTILS ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + function setUp() public virtual { + _fetchConfiguration(); + } + + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ HELPERS ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + function waitUntilExecutable(uint256 proposalId) internal { + uint256 proposalExecutableTime = getProposalExecutableTime(proposalId); + require(block.timestamp < proposalExecutableTime, "Too late to execute proposal"); + vm.warp(proposalExecutableTime); + } + + function easyPropose(address proposalAddress) public returns (uint256) { + retrieveAndLockBalance(TEST_PRIVATE_KEY_ONE, TEST_ADDRESS_ONE, PROPOSAL_THRESHOLD); + retrieveAndLockBalance(TEST_PRIVATE_KEY_TWO, TEST_ADDRESS_TWO, 1 ether); + + /* ----------PROPOSER------------ */ + vm.startPrank(TEST_ADDRESS_ONE); + + uint256 proposalId = governance.propose(proposalAddress, PROPOSAL_DESCRIPTION); + + // TIME-TRAVEL + vm.warp(block.timestamp + 6 hours); + + governance.castVote(proposalId, true); + + vm.stopPrank(); + /* ------------------------------ */ + + /* -------------VOTER-------------*/ + vm.startPrank(TEST_ADDRESS_TWO); + governance.castVote(proposalId, true); + vm.stopPrank(); + /* ------------------------------ */ + + return proposalId; + } + + function retrieveAndLockBalance(uint256 privateKey, address voter, uint256 amount) internal { + uint256 lockTimestamp = block.timestamp + VOTING_PERIOD + EXECUTION_DELAY; + uint256 accountNonce = ERC20Permit(getTornTokenAddress()).nonces(voter); + + bytes32 messageHash = keccak256( + abi.encodePacked( + PERMIT_FUNC_SELECTOR, + EIP712_DOMAIN, + keccak256( + abi.encode(PERMIT_TYPEHASH, voter, getGovernanceProxyAddress(), amount, accountNonce, lockTimestamp) + ) + ) + ); + + /* ----------GOVERNANCE------- */ + vm.startPrank(getGovernanceProxyAddress()); + IERC20(getTornTokenAddress()).transfer(voter, amount); + vm.stopPrank(); + /* ----------------------------*/ + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(privateKey, messageHash); + + /* ----------VOTER------------ */ + vm.startPrank(voter); + governance.lock(voter, amount, lockTimestamp, v, r, s); + vm.stopPrank(); + /* ----------------------------*/ + } + + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ GETTERS ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + function getProposalExecutableTime(uint256 proposalId) internal view returns (uint256) { + Proposal memory proposal = IGovernance(getGovernanceProxyAddress()).proposals(proposalId); + return proposal.endTime + VOTING_DELAY + 1 hours; + } + + function _fetchConfiguration() internal { + EXECUTION_DELAY = governance.EXECUTION_DELAY(); + EXECUTION_EXPIRATION = governance.EXECUTION_EXPIRATION(); + QUORUM_VOTES = governance.QUORUM_VOTES(); + PROPOSAL_THRESHOLD = governance.PROPOSAL_THRESHOLD(); + VOTING_DELAY = governance.VOTING_DELAY(); + VOTING_PERIOD = governance.VOTING_PERIOD(); + CLOSING_PERIOD = governance.CLOSING_PERIOD(); + VOTE_EXTEND_TIME = governance.VOTE_EXTEND_TIME(); + } +}