Commit 9a693575 authored by Antonios Kogias's avatar Antonios Kogias Committed by GitHub

feat(ctp): add solidity tests with foundry (#2585)

This commits adds solidity tests for contracts-periphery package. More specific
    it covers AssetReceiver, TeleportrWithdrawer & Transactor
parent 65fa9177
......@@ -25,6 +25,7 @@ packages/contracts/hardhat*
packages/contracts-periphery/coverage*
packages/contracts-periphery/@openzeppelin*
packages/contracts-periphery/hardhat*
packages/contracts-periphery/forge-artifacts*
packages/data-transport-layer/db
......
......@@ -27,7 +27,10 @@
"@eth-optimism/contracts-bedrock/ds-test",
"@eth-optimism/contracts-bedrock/forge-std",
"@eth-optimism/contracts-bedrock/@rari-capital/solmate",
"@eth-optimism/contracts-bedrock/excessively-safe-call"
"@eth-optimism/contracts-bedrock/excessively-safe-call",
"@eth-optimism/contracts-periphery/ds-test",
"@eth-optimism/contracts-periphery/forge-std",
"@eth-optimism/contracts-periphery/@rari-capital/solmate"
]
},
"private": true,
......
......@@ -20,5 +20,7 @@ ignores: [
"eslint-config-prettier",
"eslint-plugin-prettier",
"chai",
"babel-eslint"
"babel-eslint",
"ds-test",
"forge-std"
]
AssetReceiverTest:testFail_withdrawERC20() (gas: 199441)
AssetReceiverTest:testFail_withdrawERC20withAmount() (gas: 199389)
AssetReceiverTest:testFail_withdrawERC721() (gas: 55930)
AssetReceiverTest:testFail_withdrawETH() (gas: 10523)
AssetReceiverTest:testFail_withdrawETHwithAmount() (gas: 10639)
AssetReceiverTest:test_constructor() (gas: 9845)
AssetReceiverTest:test_receive() (gas: 18860)
AssetReceiverTest:test_withdrawERC20() (gas: 183388)
AssetReceiverTest:test_withdrawERC20withAmount() (gas: 182436)
AssetReceiverTest:test_withdrawERC721() (gas: 49149)
AssetReceiverTest:test_withdrawETH() (gas: 26121)
AssetReceiverTest:test_withdrawETHwithAmount() (gas: 26161)
TeleportrWithdrawerTest:testFail_setData() (gas: 8546)
TeleportrWithdrawerTest:testFail_setRecipient() (gas: 9952)
TeleportrWithdrawerTest:testFail_setTeleportr() (gas: 9918)
TeleportrWithdrawerTest:test_constructor() (gas: 9790)
TeleportrWithdrawerTest:test_setData() (gas: 41835)
TeleportrWithdrawerTest:test_setRecipient() (gas: 36176)
TeleportrWithdrawerTest:test_setTeleportr() (gas: 38023)
TeleportrWithdrawerTest:test_withdrawFromTeleportrToContract() (gas: 191517)
TeleportrWithdrawerTest:test_withdrawFromTeleportrToEOA() (gas: 78597)
TransactorTest:testFail_CALL() (gas: 15737)
TransactorTest:testFail_DELEGATECALLL() (gas: 15704)
TransactorTest:test_CALL() (gas: 27132)
TransactorTest:test_DELEGATECALL() (gas: 21266)
TransactorTest:test_constructor() (gas: 9823)
module.exports = {
skipFiles: [
'./test-libraries',
'./foundry-tests'
],
mocha: {
grep: "@skip-on-coverage",
......
node_modules
contracts/foundry-tests/*.t.sol
//SPDX-License-Identifier: MIT
pragma solidity 0.8.10;
/* Testing utilities */
import { Test } from "forge-std/Test.sol";
import { TestERC20 } from "../testing/helpers/TestERC20.sol";
import { TestERC721 } from "../testing/helpers/TestERC721.sol";
import { AssetReceiver } from "../universal/AssetReceiver.sol";
contract AssetReceiver_Initializer is Test {
address alice = address(128);
address bob = address(256);
uint8 immutable DEFAULT_TOKEN_ID = 0;
TestERC20 testERC20;
TestERC721 testERC721;
AssetReceiver assetReceiver;
function _setUp() public {
// Deploy ERC20 and ERC721 tokens
testERC20 = new TestERC20();
testERC721 = new TestERC721();
// Deploy AssetReceiver contract
assetReceiver = new AssetReceiver(address(alice));
vm.label(address(assetReceiver), "AssetReceiver");
// Give alice and bob some ETH
vm.deal(alice, 1 ether);
vm.deal(bob, 1 ether);
testERC721.mint(alice, DEFAULT_TOKEN_ID);
vm.label(alice, "alice");
vm.label(bob, "bob");
}
}
contract AssetReceiverTest is AssetReceiver_Initializer {
function setUp() public {
super._setUp();
}
// Tests if the owner was set correctly during deploy
function test_constructor() external {
assertEq(address(alice), assetReceiver.owner());
}
// Tests that receive works as inteded
function test_receive() external {
// Check that contract balance is 0 initially
assertEq(address(assetReceiver).balance, 0);
// Send funds
vm.prank(alice);
(bool success, ) = address(assetReceiver).call{ value: 100 }(hex"");
// Compare balance after the tx sent
assertTrue(success);
assertEq(address(assetReceiver).balance, 100);
}
// Tests withdrawETH function with only an address as argument, called by owner
function test_withdrawETH() external {
// Check contract initial balance
assertEq(address(assetReceiver).balance, 0);
// Fund contract with 1 eth and check caller and contract balances
vm.deal(address(assetReceiver), 1 ether);
assertEq(address(assetReceiver).balance, 1 ether);
assertEq(address(alice).balance, 1 ether);
// call withdrawETH
vm.prank(alice);
assetReceiver.withdrawETH(payable(alice));
// check balances after the call
assertEq(address(assetReceiver).balance, 0);
assertEq(address(alice).balance, 2 ether);
}
// withdrawETH should fail if called by non-owner
function testFail_withdrawETH() external {
vm.deal(address(assetReceiver), 1 ether);
assetReceiver.withdrawETH(payable(alice));
vm.expectRevert("UNAUTHORIZED");
}
// Similar as withdrawETH but specify amount to withdraw
function test_withdrawETHwithAmount() external {
assertEq(address(assetReceiver).balance, 0);
vm.deal(address(assetReceiver), 1 ether);
assertEq(address(assetReceiver).balance, 1 ether);
assertEq(address(alice).balance, 1 ether);
// call withdrawETH
vm.prank(alice);
assetReceiver.withdrawETH(payable(alice), 0.5 ether);
// check balances after the call
assertEq(address(assetReceiver).balance, 0.5 ether);
assertEq(address(alice).balance, 1.5 ether);
}
// withdrawETH with address and amount as arguments called by non-owner
function testFail_withdrawETHwithAmount() external {
vm.deal(address(assetReceiver), 1 ether);
assetReceiver.withdrawETH(payable(alice), 0.5 ether);
vm.expectRevert("UNAUTHORIZED");
}
// Test withdrawERC20 with token and address arguments, from owner
function test_withdrawERC20() external {
// check balances before the call
assertEq(testERC20.balanceOf(address(assetReceiver)), 0);
deal(address(testERC20), address(assetReceiver), 100_000);
assertEq(testERC20.balanceOf(address(assetReceiver)), 100_000);
assertEq(testERC20.balanceOf(alice), 0);
// call withdrawERC20
vm.prank(alice);
assetReceiver.withdrawERC20(testERC20, alice);
// check balances after the call
assertEq(testERC20.balanceOf(alice), 100_000);
assertEq(testERC20.balanceOf(address(assetReceiver)), 0);
}
// Same as withdrawERC20 but call from non-owner
function testFail_withdrawERC20() external {
deal(address(testERC20), address(assetReceiver), 100_000);
assetReceiver.withdrawERC20(testERC20, alice);
vm.expectRevert("UNAUTHORIZED");
}
// Similar as withdrawERC20 but specify amount to withdraw
function test_withdrawERC20withAmount() external {
// check balances before the call
assertEq(testERC20.balanceOf(address(assetReceiver)), 0);
deal(address(testERC20), address(assetReceiver), 100_000);
assertEq(testERC20.balanceOf(address(assetReceiver)), 100_000);
assertEq(testERC20.balanceOf(alice), 0);
// call withdrawERC20
vm.prank(alice);
assetReceiver.withdrawERC20(testERC20, alice, 50_000);
// check balances after the call
assertEq(testERC20.balanceOf(alice), 50_000);
assertEq(testERC20.balanceOf(address(assetReceiver)), 50_000);
}
// Similar as withdrawERC20 with amount but call from non-owner
function testFail_withdrawERC20withAmount() external {
deal(address(testERC20), address(assetReceiver), 100_000);
assetReceiver.withdrawERC20(testERC20, alice, 50_000);
vm.expectRevert("UNAUTHORIZED");
}
// Test withdrawERC721 from owner
function test_withdrawERC721() external {
// Check owner of the token before calling withdrawERC721
assertEq(testERC721.ownerOf(DEFAULT_TOKEN_ID), alice);
// Send the token from alice to the contract
vm.prank(alice);
testERC721.transferFrom(alice, address(assetReceiver), DEFAULT_TOKEN_ID);
assertEq(testERC721.ownerOf(DEFAULT_TOKEN_ID), address(assetReceiver));
// Call withdrawERC721
vm.prank(alice);
assetReceiver.withdrawERC721(testERC721, alice, DEFAULT_TOKEN_ID);
// Check the owner after the call
assertEq(testERC721.ownerOf(DEFAULT_TOKEN_ID), alice);
}
// Similar as withdrawERC721 but call from non-owner
function testFail_withdrawERC721() external {
vm.prank(alice);
testERC721.transferFrom(alice, address(assetReceiver), DEFAULT_TOKEN_ID);
assetReceiver.withdrawERC721(testERC721, alice, DEFAULT_TOKEN_ID);
vm.expectRevert("UNAUTHORIZED");
}
}
//SPDX-License-Identifier: MIT
pragma solidity 0.8.10;
/* Testing utilities */
import { Test } from "forge-std/Test.sol";
import { SimpleStorage } from "../testing/helpers/SimpleStorage.sol";
import { MockTeleportr } from "../testing/helpers/MockTeleportr.sol";
import { TeleportrWithdrawer } from "../universal/TeleportrWithdrawer.sol";
contract TeleportrWithdrawer_Initializer is Test {
address alice = address(128);
address bob = address(256);
TeleportrWithdrawer teleportrWithdrawer;
MockTeleportr mockTeleportr;
SimpleStorage simpleStorage;
function _setUp() public {
// Deploy MockTeleportr and SimpleStorage helper contracts
mockTeleportr = new MockTeleportr();
simpleStorage = new SimpleStorage();
// Deploy Transactor contract
teleportrWithdrawer = new TeleportrWithdrawer(address(alice));
vm.label(address(teleportrWithdrawer), "TeleportrWithdrawer");
// Give alice and bob some ETH
vm.deal(alice, 1 ether);
vm.deal(bob, 1 ether);
vm.label(alice, "alice");
vm.label(bob, "bob");
}
}
contract TeleportrWithdrawerTest is TeleportrWithdrawer_Initializer {
function setUp() public {
super._setUp();
}
// Tests if the owner was set correctly during deploy
function test_constructor() external {
assertEq(address(alice), teleportrWithdrawer.owner());
}
// Tests setRecipient function when called by authorized address
function test_setRecipient() external {
// Call setRecipient from alice
vm.prank(alice);
teleportrWithdrawer.setRecipient(address(alice));
assertEq(teleportrWithdrawer.recipient(), address(alice));
}
// setRecipient should fail if called by unauthorized address
function testFail_setRecipient() external {
teleportrWithdrawer.setRecipient(address(alice));
vm.expectRevert("UNAUTHORIZED");
}
// Tests setTeleportr function when called by authorized address
function test_setTeleportr() external {
// Call setRecipient from alice
vm.prank(alice);
teleportrWithdrawer.setTeleportr(address(mockTeleportr));
assertEq(teleportrWithdrawer.teleportr(), address(mockTeleportr));
}
// setTeleportr should fail if called by unauthorized address
function testFail_setTeleportr() external {
teleportrWithdrawer.setTeleportr(address(bob));
vm.expectRevert("UNAUTHORIZED");
}
// Tests setData function when called by authorized address
function test_setData() external {
bytes memory data = "0x1234567890";
// Call setData from alice
vm.prank(alice);
teleportrWithdrawer.setData(data);
assertEq(teleportrWithdrawer.data(), data);
}
// setData should fail if called by unauthorized address
function testFail_setData() external {
bytes memory data = "0x1234567890";
teleportrWithdrawer.setData(data);
vm.expectRevert("UNAUTHORIZED");
}
// Tests withdrawFromTeleportr, when called expected to withdraw the balance
// to the recipient address when the target is an EOA
function test_withdrawFromTeleportrToEOA() external {
// Fund the Teleportr contract with 1 ETH
vm.deal(address(teleportrWithdrawer), 1 ether);
// Set target address and Teleportr
vm.startPrank(alice);
teleportrWithdrawer.setRecipient(address(bob));
teleportrWithdrawer.setTeleportr(address(mockTeleportr));
vm.stopPrank();
// Run withdrawFromTeleportr
assertEq(address(bob).balance, 1 ether);
teleportrWithdrawer.withdrawFromTeleportr();
assertEq(address(bob).balance, 2 ether);
}
// When called from a contract account it should withdraw the balance and trigger the code
function test_withdrawFromTeleportrToContract() external {
bytes32 key = 0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa;
bytes32 value = 0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb;
bytes memory data = abi.encodeWithSelector(simpleStorage.set.selector, key, value);
// Fund the Teleportr contract with 1 ETH
vm.deal(address(teleportrWithdrawer), 1 ether);
// Set target address and Teleportr
vm.startPrank(alice);
teleportrWithdrawer.setRecipient(address(simpleStorage));
teleportrWithdrawer.setTeleportr(address(mockTeleportr));
teleportrWithdrawer.setData(data);
vm.stopPrank();
// Run withdrawFromTeleportr
assertEq(address(simpleStorage).balance, 0);
teleportrWithdrawer.withdrawFromTeleportr();
assertEq(address(simpleStorage).balance, 1 ether);
assertEq(simpleStorage.get(key), value);
}
}
//SPDX-License-Identifier: MIT
pragma solidity 0.8.10;
/* Testing utilities */
import { Test } from "forge-std/Test.sol";
import { CallRecorder } from "../testing/helpers/CallRecorder.sol";
import { Reverter } from "../testing/helpers/Reverter.sol";
import { Transactor } from "../universal/Transactor.sol";
contract Transactor_Initializer is Test {
address alice = address(128);
address bob = address(256);
Transactor transactor;
Reverter reverter;
CallRecorder callRecorded;
function _setUp() public {
// Deploy Reverter and CallRecorder helper contracts
reverter = new Reverter();
callRecorded = new CallRecorder();
// Deploy Transactor contract
transactor = new Transactor(address(alice));
vm.label(address(transactor), "Transactor");
// Give alice and bob some ETH
vm.deal(alice, 1 ether);
vm.deal(bob, 1 ether);
vm.label(alice, "alice");
vm.label(bob, "bob");
}
}
contract TransactorTest is Transactor_Initializer {
function setUp() public {
super._setUp();
}
// Tests if the owner was set correctly during deploy
function test_constructor() external {
assertEq(address(alice), transactor.owner());
}
// Tests CALL, should do a call to target
function test_CALL() external {
// Initialize call data
bytes memory data = abi.encodeWithSelector(callRecorded.record.selector);
// Run CALL
vm.prank(alice);
vm.expectCall(address(callRecorded), data);
transactor.CALL(address(callRecorded), data, 200_000 wei, 420);
}
// It should revert if called by non-owner
function testFail_CALL() external {
// Initialize call data
bytes memory data = abi.encodeWithSelector(callRecorded.record.selector);
// Run CALL
vm.prank(bob);
transactor.CALL(address(callRecorded), data, 200_000 wei, 420);
vm.expectRevert("UNAUTHORIZED");
}
function test_DELEGATECALL() external {
// Initialize call data
bytes memory data = abi.encodeWithSelector(reverter.doRevert.selector);
// Run CALL
vm.prank(alice);
vm.expectCall(address(reverter), data);
transactor.DELEGATECALL(address(reverter), data, 200_000 wei);
}
// It should revert if called by non-owner
function testFail_DELEGATECALLL() external {
// Initialize call data
bytes memory data = abi.encodeWithSelector(reverter.doRevert.selector);
// Run CALL
vm.prank(bob);
transactor.DELEGATECALL(address(reverter), data, 200_000 wei);
vm.expectRevert("UNAUTHORIZED");
}
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.9;
import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import { ERC20 } from "@rari-capital/solmate/src/tokens/ERC20.sol";
contract TestERC20 is ERC20 {
constructor() ERC20("TEST", "TST") {}
constructor() ERC20("TEST", "TST", 18) {}
function mint(address to, uint256 value) public {
_mint(to, value);
......
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.9;
import { ERC721 } from "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import { ERC721 } from "@rari-capital/solmate/src/tokens/ERC721.sol";
contract TestERC721 is ERC721 {
constructor() ERC721("TEST", "TST") {}
......@@ -9,4 +9,6 @@ contract TestERC721 is ERC721 {
function mint(address to, uint256 tokenId) public {
_mint(to, tokenId);
}
function tokenURI(uint256) public pure virtual override returns (string memory) {}
}
[default]
# The source directory
src = 'contracts/universal'
# The test directory
test = 'contracts/foundry-tests'
# We need to build seperate artifacts for forge and hh, because they each expect a different
# structure for the artifacts directory.
# The artifact directory
out = 'forge-artifacts'
# Enables or disables the optimizer
optimizer = true
# The number of optimizer runs
optimizer_runs = 200
# A list of remappings
remappings = [
'@rari-capital/solmate/=node_modules/@rari-capital/solmate',
'forge-std/=node_modules/forge-std/src',
'ds-test/=node_modules/ds-test/src'
]
# The metadata hash can be removed from the bytecode by setting "none"
bytecode_hash = "none"
import { HardhatUserConfig } from 'hardhat/types'
import { HardhatUserConfig, subtask } from 'hardhat/config'
import { TASK_COMPILE_SOLIDITY_GET_SOURCE_PATHS } from 'hardhat/builtin-tasks/task-names'
import { getenv } from '@eth-optimism/core-utils'
import * as dotenv from 'dotenv'
......@@ -19,6 +20,14 @@ import './tasks'
// Load environment variables from .env
dotenv.config()
subtask(TASK_COMPILE_SOLIDITY_GET_SOURCE_PATHS).setAction(
async (_, __, runSuper) => {
const paths = await runSuper()
return paths.filter((p: string) => !p.endsWith('.t.sol'))
}
)
const config: HardhatUserConfig = {
networks: {
optimism: {
......
......@@ -16,12 +16,15 @@
"standards"
],
"scripts": {
"build": "yarn build:contracts",
"build:contracts": "hardhat compile --show-stack-traces",
"build": "yarn build:hh",
"build:hh": "hardhat compile --show-stack-traces",
"build:forge": "forge build",
"test": "yarn test:contracts",
"test:contracts": "hardhat test --show-stack-traces",
"test:forge": "forge test",
"test:coverage": "NODE_OPTIONS=--max_old_space_size=8192 hardhat coverage && istanbul check-coverage --statements 90 --branches 84 --functions 88 --lines 90",
"test:slither": "slither .",
"gas-snapshot": "forge snapshot",
"pretest:slither": "rm -f @openzeppelin && rm -f hardhat && ln -s node_modules/@openzeppelin @openzeppelin && ln -s ../../node_modules/hardhat hardhat",
"posttest:slither": "rm -f @openzeppelin && rm -f hardhat",
"lint:ts:check": "eslint . --max-warnings=0",
......@@ -31,7 +34,7 @@
"lint:contracts:fix": "yarn prettier --write 'contracts/**/*.sol'",
"lint:fix": "yarn lint:contracts:fix && yarn lint:ts:fix",
"lint": "yarn lint:fix && yarn lint:check",
"clean": "rm -rf ./dist ./artifacts ./cache ./coverage ./tsconfig.tsbuildinfo",
"clean": "rm -rf ./dist ./artifacts ./forge-artifacts ./cache ./coverage ./tsconfig.tsbuildinfo",
"prepublishOnly": "yarn copyfiles -u 1 -e \"**/test-*/**/*\" \"contracts/**/*\" ./",
"postpublish": "rimraf chugsplash L1 L2 libraries standards",
"prepack": "yarn prepublishOnly",
......@@ -62,9 +65,9 @@
"@nomiclabs/hardhat-ethers": "^2.0.2",
"@nomiclabs/hardhat-etherscan": "^3.0.3",
"@nomiclabs/hardhat-waffle": "^2.0.1",
"@rari-capital/solmate": "https://github.com/rari-capital/solmate.git#eaaccf88ac5290299884437e1aee098a96583d54",
"@openzeppelin/contracts": "4.6.0",
"@openzeppelin/contracts-upgradeable": "4.6.0",
"@rari-capital/solmate": "^6.3.0",
"@types/chai": "^4.2.18",
"@types/mocha": "^8.2.2",
"@types/node": "^17.0.21",
......@@ -72,8 +75,10 @@
"chai": "^4.3.4",
"copyfiles": "^2.3.0",
"dotenv": "^10.0.0",
"ds-test": "https://github.com/dapphub/ds-test.git#9310e879db8ba3ea6d5c6489a579118fd264a3f5",
"ethereum-waffle": "^3.3.0",
"ethers": "^5.6.8",
"forge-std": "https://github.com/foundry-rs/forge-std.git#564510058ab3db01577b772c275e081e678373f2",
"hardhat": "^2.9.6",
"hardhat-deploy": "^0.11.10",
"hardhat-gas-reporter": "^1.0.8",
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment