diff --git a/.gitignore b/.gitignore index ff8a655..9054933 100644 --- a/.gitignore +++ b/.gitignore @@ -132,3 +132,6 @@ dist # foundry cache/ out/ + +# Mac +.DS_Store diff --git a/community-transaction-2.json b/community-transaction-2.json new file mode 100644 index 0000000..699d6e4 --- /dev/null +++ b/community-transaction-2.json @@ -0,0 +1,93 @@ +{ + "version": "1.0", + "chainId": "1", + "createdAt": 1706621962084, + "meta": { + "name": "Transactions Batch", + "description": "", + "txBuilderVersion": "1.16.3", + "createdFromSafeAddress": "0xf2964cCcB7CDA9e808aaBe8DB0DDDAF7890dd378", + "createdFromOwnerAddress": "", + "checksum": "0x5e9c0d22f677244dbc303f72f2485be95afe55381eb19bf2216e88ff73faf553" + }, + "transactions": [ + { + "to": "0xFE67A4450907459c3e1FFf623aA927dD4e28c67a", + "value": "0", + "data": "0x095ea7b300000000000000000000000022f424bca11fe154c403c277b5f8dab54a4ba29b000000000000000000000000000000000000000000005db52a6a91f5b2280000", + "contractMethod": { + "inputs": [ + { "internalType": "address", "name": "spender", "type": "address" }, + { "internalType": "uint256", "name": "amount", "type": "uint256" } + ], + "name": "approve", + "payable": false + }, + "contractInputsValues": { + "spender": "0x22f424Bca11FE154c403c277b5F8dAb54a4bA29b", + "amount": "442522000000000000000000" + } + }, + { + "to": "0x22f424Bca11FE154c403c277b5F8dAb54a4bA29b", + "value": "0", + "data": "0xb6b55f25000000000000000000000000000000000000000000005db52a6a91f5b2280000", + "contractMethod": { + "inputs": [ + { "internalType": "uint256", "name": "_amount", "type": "uint256" } + ], + "name": "deposit", + "payable": false + }, + "contractInputsValues": { "_amount": "442522000000000000000000" } + }, + { + "to": "0x58b9cB810A68a7f3e1E4f8Cb45D1B9B3c79705E8", + "value": "0", + "data": "0x095ea7b30000000000000000000000008898b472c54c31894e3b9bb83cea802a5d0e63c6000000000000000000000000000000000000000000005db52a6a91f5b2280000", + "contractMethod": { + "inputs": [ + { "internalType": "address", "name": "spender", "type": "address" }, + { "internalType": "uint256", "name": "amount", "type": "uint256" } + ], + "name": "approve", + "payable": false + }, + "contractInputsValues": { + "spender": "0x8898B472C54c31894e3B9bb83cEA802a5d0e63C6", + "amount": "442522000000000000000000" + } + }, + { + "to": "0x8898B472C54c31894e3B9bb83cEA802a5d0e63C6", + "value": "0", + "data": "0x8aac16ba000000000000000000000000000000000000000000000000000000006172626f000000000000000000000000409b0031aaad2ed5fc3ec1db7cef7dc92672483200000000000000000000000058b9cb810a68a7f3e1e4f8cb45d1b9b3c79705e8000000000000000000000000409b0031aaad2ed5fc3ec1db7cef7dc926724832000000000000000000000000000000000000000000005db52a6a91f5b2280000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000e0000000000000000000000000000000000000000000000000000000000000000f0000000000000000000000000000000000000000000000000000000000000000", + "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": "1634886255", + "_to": "0x409b0031AaAd2Ed5FC3Ec1dB7CEF7Dc926724832", + "_asset": "0x58b9cB810A68a7f3e1E4f8Cb45D1B9B3c79705E8", + "_delegate": "0x409b0031AaAd2Ed5FC3Ec1dB7CEF7Dc926724832", + "_amount": "442522000000000000000000", + "_slippage": "0", + "_callData": "0x" + } + } + ] +} diff --git a/test/community-proposal-2.t.sol b/test/community-proposal-2.t.sol new file mode 100644 index 0000000..dc90440 --- /dev/null +++ b/test/community-proposal-2.t.sol @@ -0,0 +1,199 @@ +// 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 - Arb NEXT token (token0 on arb vault) + +// -------- + +contract CommunityProposal2 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 = "/community-transaction-2.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 POL multisig + uint256 public LIQUIDITY_AMOUNT_ARB = 442522 ether; // used in transactions + + // ================== Setup ================== + + function setUp() public { + // Create the fork helper for mainnet and Arbitrum + uint256[] memory chains = new uint256[](2); + chains[0] = 1; + chains[1] = 42161; + + 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 Arbitrum fork + vm.selectFork(FORK_HELPER.forkIdsByChain(42161)); + caller = AddressLookup.getConnext(42161); + vm.makePersistent(caller); + + // Process Arbitrum xcall for `approval` by transferring to `to` + address to = utils_getXCallTo(3); + address asset = AddressLookup.getNEXTAddress(42161); + vm.startPrank(caller); + uint256 initialbalance = IERC20(AddressLookup.getNEXTAddress(42161)) + .balanceOf(to); + + // Mint on NEXT to caller + IXERC20(asset).mint(to, LIQUIDITY_AMOUNT_ARB); + // No calldata on the xcall + vm.stopPrank(); + + // Ensure the aritrum balance increased + uint256 balance = IERC20(AddressLookup.getNEXTAddress(42161)).balanceOf( + to + ); + assertEq(balance, LIQUIDITY_AMOUNT_ARB + 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_ARB, + "!balance" + ); + } +}