From 067d158bd3e16228d1c16c6d33d4048c3d4efe63 Mon Sep 17 00:00:00 2001 From: byshape Date: Tue, 2 Jan 2024 18:00:42 +0000 Subject: [PATCH] Added some foundry tests for dst chain --- .gitignore | 4 + .gitmodules | 3 + contracts/EscrowFactory.sol | 12 +-- foundry.toml | 6 ++ package.json | 2 + remappings.txt | 11 +++ test/EscrowFactory.js | 5 ++ test/foundry/Escrow.t.sol | 73 ++++++++++++++++++ test/foundry/EscrowFactory.t.sol | 50 +++++++++++++ test/utils/BaseSetup.sol | 125 +++++++++++++++++++++++++++++++ test/utils/Utils.sol | 36 +++++++++ 11 files changed, 322 insertions(+), 5 deletions(-) create mode 100644 .gitmodules create mode 100644 foundry.toml create mode 100644 remappings.txt create mode 100644 test/foundry/Escrow.t.sol create mode 100644 test/foundry/EscrowFactory.t.sol create mode 100644 test/utils/BaseSetup.sol create mode 100644 test/utils/Utils.sol diff --git a/.gitignore b/.gitignore index c396a48..61b4981 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,7 @@ build .coverage_artifacts .idea .env + +# foundry +cache_forge +out diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..888d42d --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "lib/forge-std"] + path = lib/forge-std + url = https://github.com/foundry-rs/forge-std diff --git a/contracts/EscrowFactory.sol b/contracts/EscrowFactory.sol index 78e21d5..74fdf0c 100644 --- a/contracts/EscrowFactory.sol +++ b/contracts/EscrowFactory.sol @@ -10,7 +10,6 @@ import { SafeERC20 } from "@1inch/solidity-utils/contracts/libraries/SafeERC20.s import { ClonesWithImmutableArgs } from "clones-with-immutable-args/ClonesWithImmutableArgs.sol"; import { IEscrowFactory } from "./interfaces/IEscrowFactory.sol"; -import { Escrow } from "./Escrow.sol"; contract EscrowFactory is IEscrowFactory { using AddressLib for Address; @@ -67,6 +66,7 @@ contract EscrowFactory is IEscrowFactory { * @dev Creates a new escrow contract for taker. */ function createEscrow(DstEscrowImmutablesCreation calldata dstEscrowImmutables) external { + // Check that the escrow cancellation will start not later than the cancellation time on the source chain. if ( block.timestamp + dstEscrowImmutables.timelocks.finality + @@ -88,9 +88,9 @@ contract EscrowFactory is IEscrowFactory { dstEscrowImmutables.timelocks.publicUnlock ); bytes32 salt = keccak256(abi.encodePacked(data, msg.sender)); - Escrow escrow = _createEscrow(data, salt); + address escrow = _createEscrow(data, salt); IERC20(dstEscrowImmutables.token).safeTransferFrom( - msg.sender, address(escrow), dstEscrowImmutables.amount + dstEscrowImmutables.safetyDeposit + msg.sender, escrow, dstEscrowImmutables.amount + dstEscrowImmutables.safetyDeposit ); } @@ -101,7 +101,9 @@ contract EscrowFactory is IEscrowFactory { function _createEscrow( bytes memory data, bytes32 salt - ) private returns (Escrow clone) { - clone = Escrow(IMPLEMENTATION.clone3(data, salt)); + ) private returns (address clone) { + clone = address( + uint160(IMPLEMENTATION.clone3(data, salt)) & 0x000000000000000000000000ffffffffffffffffffffffffffffffffffffffff + ); } } diff --git a/foundry.toml b/foundry.toml new file mode 100644 index 0000000..5d07aa8 --- /dev/null +++ b/foundry.toml @@ -0,0 +1,6 @@ +[profile.default] +src = 'contracts' +out = 'out' +libs = ['node_modules', 'lib'] +test = 'test' +cache_path = 'cache_forge' diff --git a/package.json b/package.json index b277c34..5d1135a 100644 --- a/package.json +++ b/package.json @@ -48,6 +48,8 @@ "lint:js:fix": "eslint . --fix", "lint:sol": "solhint --max-warnings 0 \"contracts/**/*.sol\"", "lint:sol:fix": "solhint --max-warnings 0 \"contracts/**/*.sol\" --fix", + "lint:foundry": "solhint --max-warnings 0 \"test/**/*.sol\"", + "lint:foundry:fix": "solhint --max-warnings 0 \"test/**/*.sol\" --fix", "test": "hardhat test --parallel", "test:ci": "hardhat test" } diff --git a/remappings.txt b/remappings.txt new file mode 100644 index 0000000..875b194 --- /dev/null +++ b/remappings.txt @@ -0,0 +1,11 @@ +@1inch/=node_modules/@1inch/ +@chainlink/=node_modules/@chainlink/ +@eth-optimism/=node_modules/@eth-optimism/ +@openzeppelin/=node_modules/@openzeppelin/ +clones-with-immutable-args/=node_modules/clones-with-immutable-args/ +ds-test/=lib/forge-std/lib/ds-test/src/ +eth-gas-reporter/=node_modules/eth-gas-reporter/ +forge-std/=lib/forge-std/src/ +hardhat-deploy/=node_modules/hardhat-deploy/ +hardhat/=node_modules/hardhat/ +solidity-utils/=node_modules/@1inch/solidity-utils/ diff --git a/test/EscrowFactory.js b/test/EscrowFactory.js index 159d8fa..3cf19a2 100644 --- a/test/EscrowFactory.js +++ b/test/EscrowFactory.js @@ -125,6 +125,7 @@ describe('EscrowFactory', async function () { } }); + // Migrated it('should deploy clones for taker', async function () { for (let i = 0; i < 3; i++) { const { accounts, tokens, dstClone, tx, escrowImmutables } = await deployCloneDst(); @@ -142,6 +143,7 @@ describe('EscrowFactory', async function () { } }); + // Migrated it('should not deploy clone for taker when it is unsafe', async function () { const { contracts, tx, escrowImmutables } = await deployCloneDst(); await time.setNextBlockTimestamp(escrowImmutables.srcCancellationTimestamp + 1n); @@ -156,6 +158,7 @@ describe('EscrowFactory', async function () { ).to.be.revertedWithCustomError(srcClone, 'InvalidWithdrawalTime'); }); + // Migrated it('should not withdraw tokens on the destination chain during finality lock', async function () { const { accounts, tokens, dstClone, tx, escrowImmutables, secret } = await deployCloneDst(); await expect(tx).to.changeTokenBalances( @@ -195,6 +198,7 @@ describe('EscrowFactory', async function () { ).to.be.revertedWithCustomError(srcClone, 'InvalidSecret'); }); + // Migrated it('should withdraw tokens on the destination chain by resolver', async function () { const { accounts, tokens, dstClone, tx, escrowImmutables, secret } = await deployCloneDst(); await expect(tx).to.changeTokenBalances( @@ -220,6 +224,7 @@ describe('EscrowFactory', async function () { ); }); + // Migrated it('should not withdraw tokens on the destination chain with the wrong secret', async function () { const { accounts, tokens, dstClone, tx, escrowImmutables } = await deployCloneDst(); await expect(tx).to.changeTokenBalances( diff --git a/test/foundry/Escrow.t.sol b/test/foundry/Escrow.t.sol new file mode 100644 index 0000000..913baa6 --- /dev/null +++ b/test/foundry/Escrow.t.sol @@ -0,0 +1,73 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.23; + +import { Escrow, IEscrow } from "../../contracts/Escrow.sol"; +import { IEscrowFactory } from "../../contracts/EscrowFactory.sol"; + +import { BaseSetup } from "../utils/BaseSetup.sol"; + +contract EscrowTest is BaseSetup { + function setUp() public virtual override { + BaseSetup.setUp(); + } + + /* solhint-disable func-name-mixedcase */ + + function test_NoWithdrawalDuringFinalityLockDst() public { + ( + IEscrowFactory.DstEscrowImmutablesCreation memory immutables, + Escrow dstClone + ) = _prepareDataDst(SECRET, TAKING_AMOUNT); + + // deploy escrow + vm.startPrank(bob); + escrowFactory.createEscrow(immutables); + + // withdraw + vm.expectRevert(IEscrow.InvalidWithdrawalTime.selector); + dstClone.withdrawDst(SECRET); + } + + function test_WithdrawByResolverDst() public { + ( + IEscrowFactory.DstEscrowImmutablesCreation memory immutables, + Escrow dstClone + ) = _prepareDataDst(SECRET, TAKING_AMOUNT); + + // deploy escrow + vm.startPrank(bob); + escrowFactory.createEscrow(immutables); + + uint256 balanceAlice = dai.balanceOf(alice); + uint256 balanceBob = dai.balanceOf(bob); + uint256 balanceEscrow = dai.balanceOf(address(dstClone)); + + // withdraw + vm.warp(block.timestamp + dstTimelocks.finality + 10); + dstClone.withdrawDst(SECRET); + + assertEq(dai.balanceOf(alice), balanceAlice + TAKING_AMOUNT); + assertEq(dai.balanceOf(bob), balanceBob + SAFETY_DEPOSIT); + assertEq(dai.balanceOf(address(dstClone)), balanceEscrow - (TAKING_AMOUNT + SAFETY_DEPOSIT)); + } + + function test_NoWithdrawalWithWrongSecretDst() public { + bytes32 wrongSecret = keccak256(abi.encodePacked("wrong secret")); + + ( + IEscrowFactory.DstEscrowImmutablesCreation memory immutables, + Escrow dstClone + ) = _prepareDataDst(SECRET, TAKING_AMOUNT); + + // deploy escrow + vm.startPrank(bob); + escrowFactory.createEscrow(immutables); + + // withdraw + vm.warp(block.timestamp + dstTimelocks.finality + 100); + vm.expectRevert(IEscrow.InvalidSecret.selector); + dstClone.withdrawDst(wrongSecret); + } + + /* solhint-enable func-name-mixedcase */ +} diff --git a/test/foundry/EscrowFactory.t.sol b/test/foundry/EscrowFactory.t.sol new file mode 100644 index 0000000..18cce54 --- /dev/null +++ b/test/foundry/EscrowFactory.t.sol @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.23; + +import { Escrow, IEscrow } from "../../contracts/Escrow.sol"; +import { IEscrowFactory } from "../../contracts/EscrowFactory.sol"; + +import { BaseSetup } from "../utils/BaseSetup.sol"; + +contract EscrowFactoryTest is BaseSetup { + function setUp() public virtual override { + BaseSetup.setUp(); + } + + /* solhint-disable func-name-mixedcase */ + + function testFuzz_DeployCloneForTaker(bytes32 secret, uint256 amount) public { + vm.assume(amount > 0.1 ether && amount < 1 ether); + ( + IEscrowFactory.DstEscrowImmutablesCreation memory immutables, + Escrow dstClone + ) = _prepareDataDst(secret, amount); + uint256 balanceBob = dai.balanceOf(bob); + uint256 balanceEscrow = dai.balanceOf(address(dstClone)); + + // deploy escrow + vm.prank(bob); + escrowFactory.createEscrow(immutables); + + assertEq(dai.balanceOf(bob), balanceBob - (amount + immutables.safetyDeposit)); + assertEq(dai.balanceOf(address(dstClone)), balanceEscrow + amount + immutables.safetyDeposit); + + IEscrow.DstEscrowImmutables memory returnedImmutables = dstClone.dstEscrowImmutables(); + assertEq(returnedImmutables.hashlock, uint256(keccak256(abi.encodePacked(secret)))); + assertEq(returnedImmutables.amount, amount); + } + + function test_NoUnsafeDeploymentForTaker() public { + + (IEscrowFactory.DstEscrowImmutablesCreation memory immutables,) = _prepareDataDst(SECRET, TAKING_AMOUNT); + + vm.warp(immutables.srcCancellationTimestamp + 1); + + // deploy escrow + vm.prank(bob); + vm.expectRevert(IEscrowFactory.InvalidCreationTime.selector); + escrowFactory.createEscrow(immutables); + } + + /* solhint-enable func-name-mixedcase */ +} diff --git a/test/utils/BaseSetup.sol b/test/utils/BaseSetup.sol new file mode 100644 index 0000000..c1adb5a --- /dev/null +++ b/test/utils/BaseSetup.sol @@ -0,0 +1,125 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.23; + +import { Test } from "forge-std/Test.sol"; + +import { TokenCustomDecimalsMock } from "solidity-utils/contracts/mocks/TokenCustomDecimalsMock.sol"; +import { TokenMock } from "solidity-utils/contracts/mocks/TokenMock.sol"; + +import { Escrow, IEscrow } from "../../contracts/Escrow.sol"; +import { EscrowFactory, IEscrowFactory } from "../../contracts/EscrowFactory.sol"; + +import { Utils } from "./Utils.sol"; + +contract BaseSetup is Test { + /* solhint-disable private-vars-leading-underscore */ + bytes32 internal constant SECRET = keccak256(abi.encodePacked("secret")); + uint256 internal constant TAKING_AMOUNT = 0.5 ether; + uint256 internal constant SAFETY_DEPOSIT = 0.05 ether; + + Utils internal utils; + address payable[] internal users; + + address internal alice; + address internal bob; + + TokenMock internal dai; + TokenCustomDecimalsMock internal usdc; + + address internal limitOrderProtocol; + EscrowFactory internal escrowFactory; + Escrow internal escrow; + + IEscrow.SrcTimelocks internal srcTimelocks = IEscrow.SrcTimelocks({ + finality: 120, + publicUnlock: 900 + }); + IEscrow.DstTimelocks internal dstTimelocks = IEscrow.DstTimelocks({ + finality: 300, + unlock: 240, + publicUnlock: 360 + }); + /* solhint-enable private-vars-leading-underscore */ + + function setUp() public virtual { + utils = new Utils(); + users = utils.createUsers(2); + + alice = users[0]; + vm.label(alice, "Alice"); + bob = users[1]; + vm.label(bob, "Bob"); + + _deployTokens(); + dai.mint(bob, 1000 ether); + usdc.mint(alice, 1000 ether); + + _deployContracts(); + + vm.prank(bob); + dai.approve(address(escrowFactory), 1000 ether); + vm.prank(alice); + usdc.approve(address(escrowFactory), 1000 ether); + } + + function _deployTokens() internal { + dai = new TokenMock("DAI", "DAI"); + vm.label(address(dai), "DAI"); + usdc = new TokenCustomDecimalsMock("USDC", "USDC", 1000 ether, 6); + vm.label(address(usdc), "USDC"); + } + + function _deployContracts() internal { + limitOrderProtocol = address(this); + escrow = new Escrow(); + vm.label(address(escrow), "Escrow"); + escrowFactory = new EscrowFactory(address(escrow), limitOrderProtocol); + vm.label(address(escrowFactory), "EscrowFactory"); + } + + function _prepareDataDst(bytes32 secret, uint256 amount) internal view returns ( + IEscrowFactory.DstEscrowImmutablesCreation memory, + Escrow + ) { + ( + IEscrowFactory.DstEscrowImmutablesCreation memory escrowImmutables, + bytes memory data + ) = _buildDstEscrowImmutables(secret, amount); + address msgSender = bob; + uint256 deployedAt = block.timestamp; + bytes32 salt = keccak256(abi.encodePacked(deployedAt, data, msgSender)); + Escrow dstClone = Escrow(escrowFactory.addressOfEscrow(salt)); + return (escrowImmutables, dstClone); + } + + function _buildDstEscrowImmutables(bytes32 secret, uint256 amount) internal view returns( + IEscrowFactory.DstEscrowImmutablesCreation memory immutables, + bytes memory data + ) { + uint256 hashlock = uint256(keccak256(abi.encodePacked(secret))); + uint256 safetyDeposit = amount * 10 / 100; + uint256 srcCancellationTimestamp = block.timestamp + srcTimelocks.finality + srcTimelocks.publicUnlock; + immutables = IEscrowFactory.DstEscrowImmutablesCreation({ + hashlock: hashlock, + maker: alice, + taker: bob, + token: address(dai), + amount: amount, + safetyDeposit: safetyDeposit, + timelocks: dstTimelocks, + srcCancellationTimestamp: srcCancellationTimestamp + }); + data = abi.encode( + hashlock, + alice, + bob, + block.chainid, + address(dai), + amount, + safetyDeposit, + dstTimelocks.finality, + dstTimelocks.unlock, + dstTimelocks.publicUnlock + ); + } +} diff --git a/test/utils/Utils.sol b/test/utils/Utils.sol new file mode 100644 index 0000000..0f3add0 --- /dev/null +++ b/test/utils/Utils.sol @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: Unlicense +pragma solidity 0.8.23; + +import { Test } from "forge-std/Test.sol"; + +contract Utils is Test { + // solhint-disable private-vars-leading-underscore + bytes32 internal nextUser = keccak256(abi.encodePacked("user address")); + + function getNextUserAddress() external returns (address payable) { + address payable user = payable(address(uint160(uint256(nextUser)))); + nextUser = keccak256(abi.encodePacked(nextUser)); + return user; + } + + // create users with 100 ETH balance each + function createUsers(uint256 userNum) + external + returns (address payable[] memory) + { + address payable[] memory users = new address payable[](userNum); + for (uint256 i = 0; i < userNum; i++) { + address payable user = this.getNextUserAddress(); + vm.deal(user, 100 ether); + users[i] = user; + } + + return users; + } + + // move block.number forward by a given number of blocks + function mineBlocks(uint256 numBlocks) external { + uint256 targetBlock = block.number + numBlocks; + vm.roll(targetBlock); + } +}