diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fbe7307..5a3ff96 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -22,8 +22,5 @@ jobs: - name: Install deps run: forge install - - name: Check gas snapshots - run: forge snapshot --check - - - name: Run tests - run: forge test + - name: Check gas snapshots && Run tests + run: ./main.sh diff --git a/README.md b/README.md index 3ab0303..55f0c5e 100644 --- a/README.md +++ b/README.md @@ -101,6 +101,27 @@ contract ContractTest is Test { } ``` +### Running the test + +- Ensure that Foundry is installed in your system. +- Create a `.env` file in the root of the repository and copy the content of `.env.example` in it. Make sure to add RPCs of the networks you are going cross-chain + + ``` + MAINNET_RPC_URL= + OPTIMISM_RPC_URL= + BNB_RPC_URL= + GNOSIS_RPC_URL= + POLYGON_RPC_URL= + ARBITRUM_ONE_RPC_URL= + ``` + +- Run forge test to run all the tests. In case you only need one test to run. Use `--match-contract` flag. Ex. below for VelodromeProposal: + + ```sh + forge test --match-contract VelodromeProposal + ``` + + ### Development This project uses [Foundry](https://getfoundry.sh). See the [book](https://book.getfoundry.sh/getting-started/installation.html) for instructions on how to install and use Foundry. diff --git a/Velodrome-transactions.json b/Velodrome-transactions.json new file mode 100644 index 0000000..3f82819 --- /dev/null +++ b/Velodrome-transactions.json @@ -0,0 +1,93 @@ +{ + "version": "1.0", + "chainId": "1", + "createdAt": 1698843459112, + "meta": { + "name": "Transactions Batch", + "description": "", + "txBuilderVersion": "1.16.3", + "createdFromSafeAddress": "0xf2964cCcB7CDA9e808aaBe8DB0DDDAF7890dd378", + "createdFromOwnerAddress": "", + "checksum": "0xdbc08311d3e010b14c94e185b27ff37d6413e31bf0677df5803dbad54fa3128a" + }, + "transactions": [ + { + "to": "0xFE67A4450907459c3e1FFf623aA927dD4e28c67a", + "value": "0", + "data": "0x095ea7b300000000000000000000000022f424bca11fe154c403c277b5f8dab54a4ba29b000000000000000000000000000000000000000000013da329b6336471800000", + "contractMethod": { + "inputs": [ + { "name": "spender", "type": "address", "internalType": "address" }, + { "name": "amount", "type": "uint256", "internalType": "uint256" } + ], + "name": "approve", + "payable": false + }, + "contractInputsValues": { + "spender": "0x22f424Bca11FE154c403c277b5F8dAb54a4bA29b", + "amount": "1500000000000000000000000" + } + }, + { + "to": "0x22f424Bca11FE154c403c277b5F8dAb54a4bA29b", + "value": "0", + "data": "0xb6b55f25000000000000000000000000000000000000000000013da329b6336471800000", + "contractMethod": { + "inputs": [ + { "internalType": "uint256", "name": "_amount", "type": "uint256" } + ], + "name": "deposit", + "payable": false + }, + "contractInputsValues": { "_amount": "1500000000000000000000000" } + }, + { + "to": "0x58b9cB810A68a7f3e1E4f8Cb45D1B9B3c79705E8", + "value": "0", + "data": "0x095ea7b30000000000000000000000008898b472c54c31894e3b9bb83cea802a5d0e63c6000000000000000000000000000000000000000000013da329b6336471800000", + "contractMethod": { + "inputs": [ + { "internalType": "address", "name": "spender", "type": "address" }, + { "internalType": "uint256", "name": "amount", "type": "uint256" } + ], + "name": "approve", + "payable": false + }, + "contractInputsValues": { + "spender": "0x8898B472C54c31894e3B9bb83cEA802a5d0e63C6", + "amount": "1500000000000000000000000" + } + }, + { + "to": "0x8898B472C54c31894e3B9bb83cEA802a5d0e63C6", + "value": "0", + "data": "0x8aac16ba000000000000000000000000000000000000000000000000000000006f7074690000000000000000000000001a3c9dc0c3fb2d9bc435e28479b3d9b3e334334700000000000000000000000058b9cb810a68a7f3e1e4f8cb45d1b9b3c79705e80000000000000000000000001a3c9dc0c3fb2d9bc435e28479b3d9b3e3343347000000000000000000000000000000000000000000013da329b6336471800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000e0000000000000000000000000000000000000000000000000000000000000000f0000000000000000000000000000000000000000000000000000000000000000", + "contractMethod": { + "inputs": [ + { + "internalType": "uint32", + "name": "_destination", + "type": "uint32" + }, + { "internalType": "address", "name": "_to", "type": "address" }, + { "internalType": "address", "name": "_asset", "type": "address" }, + { "internalType": "address", "name": "_delegate", "type": "address" }, + { "internalType": "uint256", "name": "_amount", "type": "uint256" }, + { "internalType": "uint256", "name": "_slippage", "type": "uint256" }, + { "internalType": "bytes", "name": "_callData", "type": "bytes" } + ], + "name": "xcall", + "payable": true + }, + "contractInputsValues": { + "_destination": "1869640809", + "_to": "0x1a3C9dC0c3fb2D9bC435e28479b3d9b3e3343347", + "_asset": "0x58b9cB810A68a7f3e1E4f8Cb45D1B9B3c79705E8", + "_delegate": "0x1a3C9dC0c3fb2D9bC435e28479b3d9b3e3343347", + "_amount": "1500000000000000000000000", + "_slippage": "0", + "_callData": "0x" + } + } + ] +} diff --git a/main.sh b/main.sh new file mode 100755 index 0000000..439108e --- /dev/null +++ b/main.sh @@ -0,0 +1,16 @@ +#!/bin/bash + +# Exporting values for all mainnet network RPCs. +export MAINNET_RPC_URL="https://eth.llamarpc.com" +export OPTIMISM_RPC_URL="https://optimism.llamarpc.com" +export BNB_RPC_URL="https://binance.llamarpc.com" +export GNOSIS_RPC_URL="https://gnosis-pokt.nodies.app" +export POLYGON_RPC_URL="https://polygon.llamarpc.com" +export ARBITRUM_ONE_RPC_URL="https://arbitrum.llamarpc.com" + + +# Check gas snapshots +forge snapshot --check + +# Run tests +forge test \ No newline at end of file diff --git a/test/VelodromeProposal.sol b/test/VelodromeProposal.sol new file mode 100644 index 0000000..5d081ba --- /dev/null +++ b/test/VelodromeProposal.sol @@ -0,0 +1,203 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {Strings} from "@openzeppelin/utils/Strings.sol"; +import {Ownable} from "@openzeppelin/access/Ownable.sol"; +import {IERC20} from "@openzeppelin/token/ERC20/IERC20.sol"; + +import {MultiSendCallOnly} from "safe-contracts/libraries/MultiSendCallOnly.sol"; + +import {IXReceiver} from "@connext/interfaces/core/IXReceiver.sol"; + +import {IXERC20} from "./interfaces/IXERC20.sol"; + +import {ForgeHelper} from "./utils/ForgeHelper.sol"; +import {ForkHelper} from "./utils/ForkHelper.sol"; +import {AddressLookup} from "./utils/AddressLookup.sol"; +import {ChainLookup} from "./utils/ChainLookup.sol"; + +import "forge-std/StdJson.sol"; +import "forge-std/console.sol"; + +// Addresses ---- + +// 0xFE67A4450907459c3e1FFf623aA927dD4e28c67a - mainnet NEXT token (token1 on mainnet vault) + +// 0x58b9cb810a68a7f3e1e4f8cb45d1b9b3c79705e8 - Optimism NEXT token (token0 on arb vault) + +// -------- + +contract VelodromeProposal is ForgeHelper { + enum Operation { + Call, + DelegateCall + } + + struct Transaction { + address to; + uint256 value; + bytes data; + Operation operation; + } + + // ================== Libraries ================== + using stdJson for string; + using Strings for string; + using Strings for uint256; + + // ================== Events ================== + + // ================== Structs ================== + + // ================== Storage ================== + + // Fork management utilities + ForkHelper public FORK_HELPER; + + // Transactions path + string public TRANSACTIONS_PATH = "/Velodrome-transactions.json"; + + // Number of transactions to execute in multisend data: + // 1. mainnet approval of NEXT to lockbox + // 2. mainnet deposit on lockbox + // 3. mainnet approval of xNEXT to connext + // 4. xcall xNEXT into connext + + uint256 public NUMBER_TRANSACTIONS = 4; + + // Amount to bridge into OP multisig + uint256 public LIQUIDITY_AMOUNT_OPTIMISM = 1500000 ether; // used in transactions + + // ================== Setup ================== + + function setUp() public { + // Create the fork helper for mainnet and optimism + uint256[] memory chains = new uint256[](2); + chains[0] = 1; + chains[1] = 10; + + uint256[] memory blocks = new uint256[](2); + + FORK_HELPER = new ForkHelper(chains, blocks); + vm.makePersistent(address(FORK_HELPER)); + + // Create the forks + FORK_HELPER.utils_createForks(); + assertEq(FORK_HELPER.utils_getNetworksCount(), 2, "!forks"); + } + + function utils_generateTransactions() + public + view + returns (Transaction[] memory _transactions) + { + // Generate executable from `Velodrome-transactions.json` + string memory path = string.concat(vm.projectRoot(), TRANSACTIONS_PATH); + + string memory json = vm.readFile(path); + + // Generate the bytes of the multisend transactions + _transactions = new Transaction[](NUMBER_TRANSACTIONS); + for (uint256 i; i < NUMBER_TRANSACTIONS; i++) { + string memory baseJsonPath = string.concat( + ".transactions[", + i.toString(), + "]" + ); + address to = json.readAddress(string.concat(baseJsonPath, ".to")); + uint256 value = json.readUint( + string.concat(baseJsonPath, ".value") + ); + // No way to check if data is null in json, this will revert if data is null + // TODO: add support to automatically generate data if its null + bytes memory data = json.readBytes( + string.concat(baseJsonPath, ".data") + ); + + // Add to transactions + _transactions[i] = Transaction({ + to: to, + value: value, + data: data, + operation: Operation.Call + }); + } + } + + function utils_getXCallTo( + uint256 transactionIdx + ) public view returns (address _to) { + // Generate executable from `Velodrome-transactions.json` + string memory path = string.concat(vm.projectRoot(), TRANSACTIONS_PATH); + + string memory json = vm.readFile(path); + string memory jsonPath = string.concat( + ".transactions[", + transactionIdx.toString(), + "].contractInputsValues._to" + ); + _to = json.readAddress(jsonPath); + } + + // ================== Tests ================== + function test_executableShouldPass() public { + // Generate the multisend transactions + // bytes memory transactions = utils_generateMultisendTransactions(); + Transaction[] memory transactions = utils_generateTransactions(); + + // Select and prep mainnet fork + vm.selectFork(FORK_HELPER.forkIdsByChain(1)); + address caller = AddressLookup.getConnextDao(1); + uint256 initial = IERC20(AddressLookup.getNEXTAddress(1)).balanceOf( + caller + ); + vm.makePersistent(caller); + + // Submit the transactions + // NOTE: This assumes signatures will be valid, and the batching of these transactions + // will be valid. Simply pranks and calls each function in a loop as DAO. + for (uint256 i; i < transactions.length; i++) { + // Send tx + vm.prank(caller); + (bool success, ) = transactions[i].to.call(transactions[i].data); + assertTrue(success, string.concat("!success @ ", i.toString())); + } + + // Select and prep Optimism fork + vm.selectFork(FORK_HELPER.forkIdsByChain(10)); + caller = AddressLookup.getConnext(10); + vm.makePersistent(caller); + + // Process optimism xcall for `approval` by transferring to `to` + address to = utils_getXCallTo(3); + address asset = AddressLookup.getNEXTAddress(10); + vm.startPrank(caller); + uint256 initialbalance = IERC20(AddressLookup.getNEXTAddress(10)) + .balanceOf(to); + + // Mint on NEXT to caller + IXERC20(asset).mint(to, LIQUIDITY_AMOUNT_OPTIMISM); + // No calldata on the xcall + vm.stopPrank(); + + // Ensure the optimism balance increased + uint256 balance = IERC20(AddressLookup.getNEXTAddress(10)).balanceOf( + to + ); + assertEq( + balance, + LIQUIDITY_AMOUNT_OPTIMISM + initialbalance, + "!balance" + ); + + // Ensure the connext mainnet balance decreased + vm.selectFork(FORK_HELPER.forkIdsByChain(1)); + assertEq( + IERC20(AddressLookup.getNEXTAddress(1)).balanceOf( + AddressLookup.getConnextDao(1) + ), + initial - LIQUIDITY_AMOUNT_OPTIMISM, + "!balance" + ); + } +} diff --git a/test/utils/ForkHelper.sol b/test/utils/ForkHelper.sol index 277de29..e18bd38 100644 --- a/test/utils/ForkHelper.sol +++ b/test/utils/ForkHelper.sol @@ -56,7 +56,12 @@ contract ForkHelper is ForgeHelper { require(NETWORK_IDS.length > 0, "!networks"); for (uint256 i; i < NETWORK_IDS.length; i++) { // create the fork - uint256 forkId = vm.createSelectFork(vm.envString(ChainLookup.getRpcEnvName(NETWORK_IDS[i])), FORK_BLOCKS[i]); + uint256 forkId; + if (FORK_BLOCKS[i] == 0) { + forkId = vm.createSelectFork(vm.envString(ChainLookup.getRpcEnvName(NETWORK_IDS[i]))); + } else { + forkId = vm.createSelectFork(vm.envString(ChainLookup.getRpcEnvName(NETWORK_IDS[i])), FORK_BLOCKS[i]); + } // update the mappings forkIdsByChain[block.chainid] = forkId; chainsByForkId[forkId] = block.chainid;