Skip to content

Commit

Permalink
Merge pull request #342 from corpus-io/feature/addTimelockController
Browse files Browse the repository at this point in the history
Feature/add timelock controller
  • Loading branch information
malteish authored Jun 13, 2024
2 parents 6619659 + 3041136 commit 37fa8a1
Show file tree
Hide file tree
Showing 6 changed files with 550 additions and 449 deletions.
4 changes: 2 additions & 2 deletions docs/deployments.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ These are the most relevant contracts. Those that we deployed were verified on b
| VestingCloneFactory | | [0xCCC45E788bcf916b3b7cA79c2e1A1fC694aD03F7](https://gnosisscan.io/address/0xccc45e788bcf916b3b7ca79c2e1a1fc694ad03f7) | |
| PrivateOfferFactory | | [0x66330A3718F68c293046d39498EDC6a043CF7190](https://gnosisscan.io/address/0x66330a3718f68c293046d39498edc6a043cf7190) | |
| FeeSettings | | [0xFce9A1e8C063162f4F54f84ab8B2744D3Efc15A2](https://gnosisscan.io/address/0xFce9A1e8C063162f4F54f84ab8B2744D3Efc15A2) | |
| AllowList | | [0xf2c479836b1f23eBE127CFB3B6dabf535d60B6DD](https://gnosisscan.io/address/0xf2c479836b1f23ebe127cfb3b6dabf535d60b6dd) |
| AllowList | | [0x6c6c8fd9629c9bcec625004012dd0aabd89960c8](https://gnosisscan.io/address/0x6c6c8fd9629c9bcec625004012dd0aabd89960c8) |
| CrowdinvestingCloneFactory | | [0x470586e0a7c2E641c39930B96E58E4300Be32cF3](https://gnosisscan.io/address/0x470586e0a7c2e641c39930b96e58e4300be32cf3) | | |
| monerium | | [0xcB444e90D8198415266c6a2724b7900fb12FC56E](https://gnosis.blockscout.com/token/0xcB444e90D8198415266c6a2724b7900fb12FC56E) | these were deployed by the monerium team |

Expand All @@ -38,7 +38,7 @@ The contracts are deployed to these testing networks:
| VestingCloneFactory | | [0x2CC672eac7326DC0c3E19d1B313548346Eb10FD8](https://blockscout.chiadochain.net/address/0x2cc672eac7326dc0c3e19d1b313548346eb10fd8) | |
| PrivateOfferFactory | | [0x994257AcCF99E5995F011AB2A3025063e5367629](https://blockscout.chiadochain.net/address/0x994257accf99e5995f011ab2a3025063e5367629) | |
| FeeSettings | | [0xab32D71F81CB897C17C9474059466bF7e117384c](https://blockscout.chiadochain.net/address/0xab32D71F81CB897C17C9474059466bF7e117384c) | |
| AllowList | | [0x774AE1a25964A0DbA498Ff7b7B59B2877B0F5be6](https://blockscout.chiadochain.net/address/0x774ae1a25964a0dba498ff7b7b59b2877b0f5be6) |
| AllowList | | [0x9372D940798ba7989bd11545B1f7b67Da456bFB2](https://blockscout.chiadochain.net/address/0x9372D940798ba7989bd11545B1f7b67Da456bFB2) |
| CrowdinvestingCloneFactory | | [0x53B5E6Acd59021E61495AbD30796b09A25c880eD](https://blockscout.chiadochain.net/address/0x53b5e6acd59021e61495abd30796b09a25c880ed) | |
| tokenize.it_USDC | | [0xC3Ea9c8BF307c7022670C88dF0357E28DA975267](https://blockscout.chiadochain.net/address/0xc3ea9c8bf307c7022670c88df0357e28da975267) | |
| tokenize.it_EUROC | | [0x730653cEB98334937431e27be111369a90B9aCc7](https://blockscout.chiadochain.net/address/0x730653ceb98334937431e27be111369a90b9acc7) | |
Expand Down
35 changes: 29 additions & 6 deletions hardhat.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,6 @@ import '@nomicfoundation/hardhat-ethers';
import '@nomicfoundation/hardhat-chai-matchers';
import '@typechain/hardhat';

// require("@nomiclabs/hardhat-waffle");
// require("hardhat-gas-reporter");
// require("solidity-coverage");
//require("@foundry-rs/hardhat-forge");

// This is a sample Hardhat task. To learn how to create your own go to
// https://hardhat.org/guides/create-task.html
task('accounts', 'Prints the list of accounts', async (taskArgs, hre) => {
Expand Down Expand Up @@ -81,7 +76,35 @@ const config: HardhatUserConfig = {
currency: 'USD',
},
etherscan: {
apiKey: process.env.ETHERSCAN_API_KEY,
apiKey: {
mainnet: process.env.ETHERSCAN_API_KEY || '',
sepolia: process.env.ETHERSCAN_API_KEY || '',
chiado: process.env.GNOSISSCAN_API_KEY || '',
gnosis: process.env.GNOSISSCAN_API_KEY || '',
},
customChains: [
{
network: `chiado`,
chainId: 10200,
urls: {
apiURL: `https://gnosis-chiado.blockscout.com/api`,
browserURL: `https://blockscout.chiadochain.net`,
},
},
{
network: 'gnosis',
chainId: 100,
urls: {
// 3) Select to what explorer verify the contracts
// Gnosisscan https://gnosis.blockscout.com/api?
apiURL: 'https://api.gnosisscan.io/api',
browserURL: 'https://gnosisscan.io/',
// Blockscout
// apiURL: 'https://blockscout.com/xdai/mainnet/api',
// browserURL: 'https://blockscout.com/xdai/mainnet',
},
},
],
},
typechain: {
outDir: 'types',
Expand Down
1 change: 1 addition & 0 deletions script/verificationArguments/foundry/TimelockController
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
0 [0x24447b86bc86f3ab2ae83c7d9e97e2ff441f23d7,0x197Fc9FEaeFfb0bAB8684Ce15a9987b397E17b06] [0x24447b86bc86f3ab2ae83c7d9e97e2ff441f23d7,0x197Fc9FEaeFfb0bAB8684Ce15a9987b397E17b06] 0x197Fc9FEaeFfb0bAB8684Ce15a9987b397E17b06
12 changes: 12 additions & 0 deletions script/verificationArguments/hardhat/TimelockController.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
module.exports = [
0,
[
'0x24447b86bc86f3ab2ae83c7d9e97e2ff441f23d7',
'0x197Fc9FEaeFfb0bAB8684Ce15a9987b397E17b06',
],
[
'0x24447b86bc86f3ab2ae83c7d9e97e2ff441f23d7',
'0x197Fc9FEaeFfb0bAB8684Ce15a9987b397E17b06',
],
'0x197Fc9FEaeFfb0bAB8684Ce15a9987b397E17b06',
];
240 changes: 240 additions & 0 deletions test/TimeLockController.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,240 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

import {Test, console} from "forge-std/Test.sol";
import "@openzeppelin/contracts/governance/TimelockController.sol";
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";

contract ERC20MintableByAnyone is ERC20 {
constructor(string memory name, string memory symbol) ERC20(name, symbol) {}

function mint(address to, uint256 amount) public {
_mint(to, amount);
}
}

contract EscrowTest is Test {
error TimelockInsufficientDelay(uint256 delay, uint256 minDelay);

ERC20MintableByAnyone token;
TimelockController timelock;

uint256 public roofPK = 1;
uint256 public platformColdPK = 2;
uint256 public emitterPK = 3;

address public roofAccount = vm.addr(roofPK);
address public platformColdAccount = vm.addr(platformColdPK);
address public emitterAccount = vm.addr(emitterPK);
address public platformHotAccount = address(this);

// we re-use these variables for multiple operations
address target;
uint256 value = 0; // no value
bytes payload = abi.encodeWithSignature("approve(address,uint256)", platformColdAccount, type(uint256).max);
bytes32 predecessor = 0x0; // no predecessor
bytes32 salt = 0x0; // no salt
uint256 delay = 1;

function setUp() public {
vm.warp(1); // otherwise, weird stuff happens

// create the erc20 token
token = new ERC20MintableByAnyone("test_token", "TT");
target = address(token);

// create the time lock controller. emitter is proposer and executor, platform is admin
address[] memory roleHolders = new address[](2);
roleHolders[0] = emitterAccount;
roleHolders[1] = platformHotAccount;
timelock = new TimelockController(
0 seconds,
roleHolders,
roleHolders,
platformHotAccount // the executing hot wallet is admin for now
);

// get id
bytes32 id2 = timelock.hashOperation(target, value, payload, predecessor, salt);

// propose operation #1
assertEq(timelock.isOperation(id2), false, "operation should not exist");
timelock.schedule(target, value, payload, predecessor, salt, delay);
assertEq(timelock.isOperation(id2), true, "operation should exist");
assertEq(timelock.isOperationPending(id2), true, "operation should be pending");

// increase time by one second
vm.warp(2);

// execute operation
assertEq(timelock.isOperationReady(id2), true, "operation should be ready");
timelock.execute(target, value, payload, predecessor, salt);
assertEq(timelock.isOperationDone(id2), true, "operation should be done");

// check that the allowance was set
assertEq(
token.allowance(address(timelock), platformColdAccount),
type(uint256).max,
"allowance for platformColdAccount should be max"
);

//propose and execute second operation
payload = abi.encodeWithSignature("approve(address,uint256)", roofAccount, type(uint256).max);
timelock.schedule(target, value, payload, predecessor, salt, delay);
vm.warp(3);
timelock.execute(target, value, payload, predecessor, salt);
assertEq(
token.allowance(address(timelock), roofAccount),
type(uint256).max,
"allowance for roofAccount should be max"
);

console.log("updating delay next");

// update timelock delay to 2 months. This requires a new operation.
payload = abi.encodeWithSignature("updateDelay(uint256)", 2 * 30 days);
timelock.schedule(
address(timelock), // notice how timelok calls itself here
value,
payload,
predecessor,
salt,
delay
);
vm.warp(4);
timelock.execute(address(timelock), value, payload, predecessor, salt);
assertEq(timelock.getMinDelay(), 2 * 30 days, "timelock delay should be 2 months");

console.log("revoking roles next");

// remove platform as admin, executor and proposer
timelock.revokeRole(timelock.PROPOSER_ROLE(), platformHotAccount);
timelock.revokeRole(timelock.CANCELLER_ROLE(), platformHotAccount);
timelock.revokeRole(timelock.EXECUTOR_ROLE(), platformHotAccount);
timelock.revokeRole(timelock.TIMELOCK_ADMIN_ROLE(), platformHotAccount);

// check that platform no longer holds roles
assertEq(
timelock.hasRole(timelock.TIMELOCK_ADMIN_ROLE(), platformHotAccount),
false,
"platform should not be admin"
);
assertEq(
timelock.hasRole(timelock.PROPOSER_ROLE(), platformHotAccount),
false,
"platform should not be proposer"
);
assertEq(
timelock.hasRole(timelock.EXECUTOR_ROLE(), platformHotAccount),
false,
"platform should not be executor"
);
assertEq(
timelock.hasRole(timelock.CANCELLER_ROLE(), platformHotAccount),
false,
"platform should not be canceller"
);

// mint some tokens to the timelock
token.mint(address(timelock), 1000);
}

function test_platformCanTransfer() public {
assertEq(token.balanceOf(platformColdAccount), 0, "platformColdAccount should have 0 tokens");
vm.prank(platformColdAccount);
token.transferFrom(address(timelock), platformColdAccount, 100);
assertEq(token.balanceOf(platformColdAccount), 100, "platformColdAccount should have 100 tokens");
}

function test_roofCanTransfer() public {
assertEq(token.balanceOf(roofAccount), 0, "roofAccount should have 0 tokens");
vm.prank(roofAccount);
token.transferFrom(address(timelock), roofAccount, 100);
assertEq(token.balanceOf(roofAccount), 100, "roofAccount should have 100 tokens");
}

function test_RandoCanNotTransfer(address rando) public {
vm.assume(rando != roofAccount && rando != platformColdAccount);
assertEq(token.balanceOf(rando), 0, "rando should have 0 tokens");
vm.prank(rando);
vm.expectRevert();
token.transferFrom(address(timelock), rando, 100);
assertEq(token.balanceOf(rando), 0, "rando should still have 0 tokens");
}

function test_emitterCanNotTransferImmediately() public {
assertEq(token.balanceOf(emitterAccount), 0, "emitterAccount should have 0 tokens");
vm.prank(emitterAccount);
vm.expectRevert();
token.transferFrom(address(timelock), emitterAccount, 100);
assertEq(token.balanceOf(emitterAccount), 0, "emitterAccount should still have 0 tokens");
}

function test_emitterCanTransferAfterDelay() public {
assertEq(token.balanceOf(emitterAccount), 0, "emitterAccount should have 0 tokens");

payload = abi.encodeWithSignature("transfer(address,uint256)", emitterAccount, 100);
target = address(token);
delay = 2 * 30 days;

vm.prank(emitterAccount);
timelock.schedule(target, value, payload, predecessor, salt, delay);
vm.warp(2 * 30 days + 10); // we did some warping in setup, so we need to add 10 seconds

vm.prank(emitterAccount);
timelock.execute(target, value, payload, predecessor, salt);
assertEq(token.balanceOf(emitterAccount), 100, "emitterAccount should have 100 tokens");
}

function test_emitterCanNotProposeShorterDelay() public {
assertEq(token.balanceOf(emitterAccount), 0, "emitterAccount should have 0 tokens");

payload = abi.encodeWithSignature("transfer(address,uint256)", emitterAccount, 100);
target = address(token);
delay = 2 * 29 days;

vm.prank(emitterAccount);
vm.expectRevert();
timelock.schedule(target, value, payload, predecessor, salt, delay);
}

function test_emitterCanNotExecuteBeforeDelayHasPassed() public {
assertEq(token.balanceOf(emitterAccount), 0, "emitterAccount should have 0 tokens");

payload = abi.encodeWithSignature("transfer(address,uint256)", emitterAccount, 100);
target = address(token);
delay = 2 * 30 days;

vm.prank(emitterAccount);
timelock.schedule(target, value, payload, predecessor, salt, delay);
vm.warp(2 * 30 days - 1);

vm.prank(emitterAccount);
vm.expectRevert(); // not extracting the precise error now
timelock.execute(target, value, payload, predecessor, salt);
}

/**
* BEHOLD! The emitter can approve itself for infinite allowance
* -> this breaks the security model of the timelock in our application
*/
function test_emitterCanApproveSelf() public {
assertEq(token.allowance(address(timelock), emitterAccount), 0, "emitterAccount should have 0 allowance");

payload = abi.encodeWithSignature("approve(address,uint256)", emitterAccount, type(uint256).max);
target = address(token);
delay = 2 * 30 days;

vm.prank(emitterAccount);
timelock.schedule(target, value, payload, predecessor, salt, delay);
vm.warp(2 * 30 days + 10); // we did some warping in setup, so we need to add 10 seconds

vm.prank(emitterAccount);
timelock.execute(target, value, payload, predecessor, salt);
assertEq(
token.allowance(address(timelock), emitterAccount),
type(uint256).max,
"emitterAccount should have infinite allowance"
);
}
}
Loading

0 comments on commit 37fa8a1

Please sign in to comment.