Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merge Develop into main #344

Merged
merged 9 commits into from
Sep 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 33 additions & 7 deletions hardhat.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,11 @@ import { HardhatUserConfig, task } from 'hardhat/config';

import 'dotenv/config';
import 'hardhat-gas-reporter';
import '@nomiclabs/hardhat-etherscan';
import '@nomicfoundation/hardhat-verify';
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 @@ -80,8 +75,39 @@ const config: HardhatUserConfig = {
enabled: process.env.REPORT_GAS !== undefined,
currency: 'USD',
},
sourcify: {
enabled: true,
},
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
10 changes: 5 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,9 @@
},
"devDependencies": {
"@foundry-rs/hardhat-forge": "^0.1.17",
"@nomicfoundation/hardhat-chai-matchers": "^2.0.2",
"@nomicfoundation/hardhat-ethers": "^3.0.4",
"@nomiclabs/hardhat-etherscan": "^3.1.0",
"@nomicfoundation/hardhat-chai-matchers": "^2.0.7",
"@nomicfoundation/hardhat-ethers": "^3.0.8",
"@nomicfoundation/hardhat-verify": "^2.0.10",
"@typechain/ethers-v6": "^0.5.1",
"@typechain/hardhat": "^9.1.0",
"@types/chai": "^4.3.10",
Expand All @@ -69,14 +69,14 @@
"eslint-plugin-promise": "^6.1.1",
"ethereum-waffle": "^4.0.10",
"ethers": "^6.1.0",
"hardhat": "^2.19.0",
"hardhat": "^2.22.0",
"hardhat-gas-reporter": "^1.0.8",
"npmignore": "^0.3.0",
"prettier": "^3.0.3",
"prettier-plugin-solidity": "^1.2.0",
"solhint": "^4.0.0",
"solidity-coverage": "^0.8.5",
"ts-node": "^10.9.1",
"ts-node": "^10.9.2",
"typechain": "^8.3.2",
"typescript": "^5.2.2"
},
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/PrivateOffer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
module.exports = [
{
currencyPayer: '0xd084260683d0f2ade61c4d71bf38e0d02803e2a9', // currencyPayer
tokenReceiver: '0xd084260683d0f2ade61c4d71bf38e0d02803e2a9', // tokenReceiver
currencyReceiver: '0x81e99b5e64f0bc14b56649575b2347868687ff4d', // currencyReceiver
tokenAmount: '1000000000000000000', // tokenAmount
tokenPrice: '20000000', // tokenPrice
expiration: '1726272000', // expiration
currency: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', // currency
token: '0x82a3f389aa8519ebe78947d4bfd45f4e050a3f2c', // token
},
];
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 && rando != address(timelock));
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
Loading