From f2338e44f30479a461c81165b9983e323f266878 Mon Sep 17 00:00:00 2001 From: Aleksandr Kuperman Date: Tue, 14 Nov 2023 19:00:09 +0000 Subject: [PATCH 01/16] feat: ERC20 token bridging --- .gitmodules | 3 ++ contracts/bridges/FxERC20ChildTunnel.sol | 53 ++++++++++++++++++++++++ contracts/bridges/FxERC20RootTunnel.sol | 48 +++++++++++++++++++++ contracts/interfaces/IERC20.sol | 48 +++++++++++++++++++++ lib/fx-portal | 1 + 5 files changed, 153 insertions(+) create mode 100644 contracts/bridges/FxERC20ChildTunnel.sol create mode 100755 contracts/bridges/FxERC20RootTunnel.sol create mode 100644 contracts/interfaces/IERC20.sol create mode 160000 lib/fx-portal diff --git a/.gitmodules b/.gitmodules index 03d3342..afe574f 100644 --- a/.gitmodules +++ b/.gitmodules @@ -2,3 +2,6 @@ path = lib/solmate url = https://github.com/rari-capital/solmate branch = v7 +[submodule "lib/fx-portal"] + path = lib/fx-portal + url = https://github.com/0xPolygon/fx-portal diff --git a/contracts/bridges/FxERC20ChildTunnel.sol b/contracts/bridges/FxERC20ChildTunnel.sol new file mode 100644 index 0000000..ce5dcac --- /dev/null +++ b/contracts/bridges/FxERC20ChildTunnel.sol @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.21; + +import {FxBaseChildTunnel} from "../../lib/fx-portal/contracts/tunnel/FxBaseChildTunnel.sol"; +import {IERC20} from "../interfaces/IERC20.sol"; + +/** + * @title FxERC20ChildTunnel + */ +contract FxERC20ChildTunnel is FxBaseChildTunnel { + event FxDepositERC20(address indexed childToken, address indexed rootToken, address from, address indexed to, uint256 amount); + event FxWithdrawERC20(address indexed rootToken, address indexed childToken, address from, address indexed to, uint256 amount); + + // Child token + address public immutable childToken; + // Root token + address public immutable rootToken; + + // slither-disable-next-line missing-zero-check + constructor(address _fxChild, address _childToken, address _rootToken) FxBaseChildTunnel(_fxChild) { + childToken = _childToken; + rootToken = _rootToken; + } + + function deposit(uint256 amount) public { + _deposit(msg.sender, amount); + } + + function depositTo(address to, uint256 amount) public { + _deposit(to, amount); + } + + function _processMessageFromRoot( + uint256 /* stateId */, + address sender, + bytes memory data + ) internal override validateSender(sender) { + // Decode incoming data + (address from, address to, uint256 amount) = abi.decode(data, (address, address, uint256)); + + IERC20(childToken).transfer(to, amount); + emit FxWithdrawERC20(rootToken, childToken, from, to, amount); + } + + function _deposit(address to, uint256 amount) internal { + // Deposit tokens + IERC20(childToken).transferFrom(msg.sender, address(this), amount); + + // Send message to root + _sendMessageToRoot(abi.encode(msg.sender, to, amount)); + emit FxDepositERC20(childToken, rootToken, msg.sender, to, amount); + } +} diff --git a/contracts/bridges/FxERC20RootTunnel.sol b/contracts/bridges/FxERC20RootTunnel.sol new file mode 100755 index 0000000..c75b97b --- /dev/null +++ b/contracts/bridges/FxERC20RootTunnel.sol @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.21; + +import {FxBaseRootTunnel} from "../../lib/fx-portal/contracts/tunnel/FxBaseRootTunnel.sol"; +import {IERC20} from "../interfaces/IERC20.sol"; + +/** + * @title FxERC20RootTunnel + */ +contract FxERC20RootTunnel is FxBaseRootTunnel { + event FxDepositERC20(address indexed childToken, address indexed rootToken, address from, address indexed to, uint256 amount); + event FxWithdrawERC20(address indexed rootToken, address indexed childToken, address from, address indexed to, uint256 amount); + + // Child token + address public immutable childToken; + // Root token + address public immutable rootToken; + + constructor(address _checkpointManager, address _fxRoot, address _childToken, address _rootToken) + FxBaseRootTunnel(_checkpointManager, _fxRoot) + { + childToken = _childToken; + rootToken = _rootToken; + } + + function withdraw(address to, uint256 amount) external { + // Transfer from sender to this contract + IERC20(rootToken).transferFrom(msg.sender, address(this), amount); + + // Burn tokens + IERC20(rootToken).burn(amount); + + // Send message to child + bytes memory message = abi.encode(msg.sender, to, amount); + _sendMessageToChild(message); + emit FxWithdrawERC20(rootToken, childToken, msg.sender, to, amount); + } + + // exit processor + function _processMessageFromChild(bytes memory data) internal override { + // Decode message from child + (address from, address to, uint256 amount) = abi.decode(data, (address, address, uint256)); + + // transfer from tokens to + IERC20(rootToken).mint(to, amount); + emit FxDepositERC20(childToken, rootToken, from, to, amount); + } +} diff --git a/contracts/interfaces/IERC20.sol b/contracts/interfaces/IERC20.sol new file mode 100644 index 0000000..b24d880 --- /dev/null +++ b/contracts/interfaces/IERC20.sol @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.21; + +/// @dev ERC20 token interface. +interface IERC20 { + /// @dev Gets the amount of tokens owned by a specified account. + /// @param account Account address. + /// @return Amount of tokens owned. + function balanceOf(address account) external view returns (uint256); + + /// @dev Gets the total amount of tokens stored by the contract. + /// @return Amount of tokens. + function totalSupply() external view returns (uint256); + + /// @dev Gets remaining number of tokens that the `spender` can transfer on behalf of `owner`. + /// @param owner Token owner. + /// @param spender Account address that is able to transfer tokens on behalf of the owner. + /// @return Token amount allowed to be transferred. + function allowance(address owner, address spender) external view returns (uint256); + + /// @dev Sets `amount` as the allowance of `spender` over the caller's tokens. + /// @param spender Account address that will be able to transfer tokens on behalf of the caller. + /// @param amount Token amount. + /// @return True if the function execution is successful. + function approve(address spender, uint256 amount) external returns (bool); + + /// @dev Transfers the token amount. + /// @param to Address to transfer to. + /// @param amount The amount to transfer. + /// @return True if the function execution is successful. + function transfer(address to, uint256 amount) external returns (bool); + + /// @dev Transfers the token amount that was previously approved up until the maximum allowance. + /// @param from Account address to transfer from. + /// @param to Account address to transfer to. + /// @param amount Amount to transfer to. + /// @return True if the function execution is successful. + function transferFrom(address from, address to, uint256 amount) external returns (bool); + + /// @dev Mints tokens. + /// @param account Account address. + /// @param amount Token amount. + function mint(address account, uint256 amount) external; + + /// @dev Burns tokens. + /// @param amount Token amount to burn. + function burn(uint256 amount) external; +} diff --git a/lib/fx-portal b/lib/fx-portal new file mode 160000 index 0000000..296ac8d --- /dev/null +++ b/lib/fx-portal @@ -0,0 +1 @@ +Subproject commit 296ac8d41579f98d3a4dfb6d41737fae272a30ba From d5e581a7365454da7c69ceff969b03eaf3865c81 Mon Sep 17 00:00:00 2001 From: Aleksandr Kuperman Date: Tue, 14 Nov 2023 19:05:44 +0000 Subject: [PATCH 02/16] chore: linter --- .eslintignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.eslintignore b/.eslintignore index 3f2f531..c6af692 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,2 +1,3 @@ coverage* audit/* +lib/* From c50e829476a5328d5983747ce38320489347b7cc Mon Sep 17 00:00:00 2001 From: Aleksandr Kuperman Date: Wed, 15 Nov 2023 11:10:06 +0000 Subject: [PATCH 03/16] refactor: adding simmetrical withdraw functions --- contracts/bridges/FxERC20ChildTunnel.sol | 10 +++++--- contracts/bridges/FxERC20RootTunnel.sol | 30 ++++++++++++++++-------- 2 files changed, 27 insertions(+), 13 deletions(-) diff --git a/contracts/bridges/FxERC20ChildTunnel.sol b/contracts/bridges/FxERC20ChildTunnel.sol index ce5dcac..24d46f9 100644 --- a/contracts/bridges/FxERC20ChildTunnel.sol +++ b/contracts/bridges/FxERC20ChildTunnel.sol @@ -22,11 +22,11 @@ contract FxERC20ChildTunnel is FxBaseChildTunnel { rootToken = _rootToken; } - function deposit(uint256 amount) public { + function deposit(uint256 amount) external { _deposit(msg.sender, amount); } - function depositTo(address to, uint256 amount) public { + function depositTo(address to, uint256 amount) external { _deposit(to, amount); } @@ -38,7 +38,9 @@ contract FxERC20ChildTunnel is FxBaseChildTunnel { // Decode incoming data (address from, address to, uint256 amount) = abi.decode(data, (address, address, uint256)); + // Transfer tokens IERC20(childToken).transfer(to, amount); + emit FxWithdrawERC20(rootToken, childToken, from, to, amount); } @@ -47,7 +49,9 @@ contract FxERC20ChildTunnel is FxBaseChildTunnel { IERC20(childToken).transferFrom(msg.sender, address(this), amount); // Send message to root - _sendMessageToRoot(abi.encode(msg.sender, to, amount)); + bytes memory message = abi.encode(msg.sender, to, amount); + _sendMessageToRoot(message); + emit FxDepositERC20(childToken, rootToken, msg.sender, to, amount); } } diff --git a/contracts/bridges/FxERC20RootTunnel.sol b/contracts/bridges/FxERC20RootTunnel.sol index c75b97b..63ee682 100755 --- a/contracts/bridges/FxERC20RootTunnel.sol +++ b/contracts/bridges/FxERC20RootTunnel.sol @@ -23,17 +23,12 @@ contract FxERC20RootTunnel is FxBaseRootTunnel { rootToken = _rootToken; } - function withdraw(address to, uint256 amount) external { - // Transfer from sender to this contract - IERC20(rootToken).transferFrom(msg.sender, address(this), amount); - - // Burn tokens - IERC20(rootToken).burn(amount); + function withdraw(uint256 amount) external { + _withdraw(msg.sender, amount); + } - // Send message to child - bytes memory message = abi.encode(msg.sender, to, amount); - _sendMessageToChild(message); - emit FxWithdrawERC20(rootToken, childToken, msg.sender, to, amount); + function withdrawTo(address to, uint256 amount) external { + _withdraw(to, amount); } // exit processor @@ -43,6 +38,21 @@ contract FxERC20RootTunnel is FxBaseRootTunnel { // transfer from tokens to IERC20(rootToken).mint(to, amount); + emit FxDepositERC20(childToken, rootToken, from, to, amount); } + + function _withdraw(address to, uint256 amount) internal { + // Transfer from sender to this contract + IERC20(rootToken).transferFrom(msg.sender, address(this), amount); + + // Burn tokens + IERC20(rootToken).burn(amount); + + // Send message to child + bytes memory message = abi.encode(msg.sender, to, amount); + _sendMessageToChild(message); + + emit FxWithdrawERC20(rootToken, childToken, msg.sender, to, amount); + } } From 1f3a9deea388a55014ca6d39fb67d9d932e9a5fa Mon Sep 17 00:00:00 2001 From: Aleksandr Kuperman Date: Wed, 15 Nov 2023 20:15:12 +0000 Subject: [PATCH 04/16] chore: polygon-eth ERC20 token transfer mechanism test --- contracts/bridges/BridgedERC20.sol | 64 ++++++++++ .../polygon/deploy_03_erc20_child_tunnel.js | 67 ++++++++++ .../test/fx_mumbai_goerli_token_transfer.js | 119 ++++++++++++++++++ .../bridges/polygon/test/globals.json | 2 +- .../polygon/verify_03_erc20_child_tunnel.js | 10 ++ scripts/deployment/deploy_19_bridged_erc20.js | 59 +++++++++ .../deployment/deploy_20_erc20_root_tunnel.js | 60 +++++++++ .../deploy_21_bridged_erc20_change_owner.js | 47 +++++++ scripts/deployment/globals_goerli.json | 2 +- .../deployment/verify_20_erc20_root_tunnel.js | 11 ++ 10 files changed, 439 insertions(+), 2 deletions(-) create mode 100644 contracts/bridges/BridgedERC20.sol create mode 100644 scripts/deployment/bridges/polygon/deploy_03_erc20_child_tunnel.js create mode 100644 scripts/deployment/bridges/polygon/test/fx_mumbai_goerli_token_transfer.js create mode 100644 scripts/deployment/bridges/polygon/verify_03_erc20_child_tunnel.js create mode 100644 scripts/deployment/deploy_19_bridged_erc20.js create mode 100644 scripts/deployment/deploy_20_erc20_root_tunnel.js create mode 100644 scripts/deployment/deploy_21_bridged_erc20_change_owner.js create mode 100644 scripts/deployment/verify_20_erc20_root_tunnel.js diff --git a/contracts/bridges/BridgedERC20.sol b/contracts/bridges/BridgedERC20.sol new file mode 100644 index 0000000..c30b32c --- /dev/null +++ b/contracts/bridges/BridgedERC20.sol @@ -0,0 +1,64 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.21; + +import {ERC20} from "../../lib/solmate/src/tokens/ERC20.sol"; + +/// @dev Only `owner` has a privilege, but the `sender` was provided. +/// @param sender Sender address. +/// @param owner Required sender address as an owner. +error OwnerOnly(address sender, address owner); + +/// @dev Provided zero address. +error ZeroAddress(); + + +/// @title BridgedERC20 - Smart contract for bridged ERC20 token +contract BridgedERC20 is ERC20 { + event OwnerUpdated(address indexed owner); + + address public owner; + + constructor() ERC20("ERC20 bridged token", "BridgedERC20", 18) { + owner = msg.sender; + } + + /// @dev Changes the owner address. + /// @param newOwner Address of a new owner. + function changeOwner(address newOwner) external { + // Only the contract owner is allowed to change the owner + if (msg.sender != owner) { + revert OwnerOnly(msg.sender, owner); + } + + // Zero address check + if (newOwner == address(0)) { + revert ZeroAddress(); + } + + owner = newOwner; + emit OwnerUpdated(newOwner); + } + + /// @dev Mints bridged tokens. + /// @param account Account address. + /// @param amount Bridged token amount. + function mint(address account, uint256 amount) external { + // Only the contract owner is allowed to mint + if (msg.sender != owner) { + revert OwnerOnly(msg.sender, owner); + } + + _mint(account, amount); + } + + /// @dev Burns bridged tokens. + /// @param amount Bridged token amount to burn. + function burn(uint256 amount) external { + // Only the contract owner is allowed to burn + if (msg.sender != owner) { + revert OwnerOnly(msg.sender, owner); + } + + _burn(msg.sender, amount); + } +} \ No newline at end of file diff --git a/scripts/deployment/bridges/polygon/deploy_03_erc20_child_tunnel.js b/scripts/deployment/bridges/polygon/deploy_03_erc20_child_tunnel.js new file mode 100644 index 0000000..166b188 --- /dev/null +++ b/scripts/deployment/bridges/polygon/deploy_03_erc20_child_tunnel.js @@ -0,0 +1,67 @@ +/*global process*/ + +const { ethers } = require("hardhat"); +const { LedgerSigner } = require("@anders-t/ethers-ledger"); + +async function main() { + const fs = require("fs"); + const globalsFile = "globals.json"; + const dataFromJSON = fs.readFileSync(globalsFile, "utf8"); + let parsedData = JSON.parse(dataFromJSON); + const useLedger = parsedData.useLedger; + const derivationPath = parsedData.derivationPath; + const providerName = parsedData.providerName; + let EOA; + + let provider; + if (providerName == "polygon") { + provider = await ethers.providers.getDefaultProvider("matic"); + } else { + const mumbaiURL = "https://polygon-mumbai.g.alchemy.com/v2/" + process.env.ALCHEMY_API_KEY_MUMBAI; + provider = new ethers.providers.JsonRpcProvider(mumbaiURL); + } + const signers = await ethers.getSigners(); + + if (useLedger) { + EOA = new LedgerSigner(provider, derivationPath); + } else { + EOA = signers[0]; + } + // EOA address + const deployer = await EOA.getAddress(); + console.log("EOA is:", deployer); + + // Transaction signing and execution + console.log("3. EOA to deploy ERC20 child tunnel contract"); + const FxERC20ChildTunnel = await ethers.getContractFactory("FxERC20ChildTunnel"); + console.log("You are signing the following transaction: FxERC20ChildTunnel.connect(EOA).deploy(fxChild, childToken, rootToken)"); + const gasPriceInGwei = "230"; + const gasPrice = ethers.utils.parseUnits(gasPriceInGwei, "gwei"); + const fxERC20ChildTunnel = await FxERC20ChildTunnel.connect(EOA).deploy(parsedData.fxChildAddress, parsedData.childTokenAddress, parsedData.bridgedERC20Address, { gasPrice }); + const result = await fxERC20ChildTunnel.deployed(); + + // Transaction details + console.log("Contract deployment: FxERC20ChildTunnel"); + console.log("Contract address:", fxERC20ChildTunnel.address); + console.log("Transaction:", result.deployTransaction.hash); + + // Wait half a minute for the transaction completion + await new Promise(r => setTimeout(r, 30000)); + + // Writing updated parameters back to the JSON file + parsedData.fxERC20ChildTunnelAddress = fxERC20ChildTunnel.address; + fs.writeFileSync(globalsFile, JSON.stringify(parsedData)); + + // Contract verification + if (parsedData.contractVerification) { + const execSync = require("child_process").execSync; + execSync("npx hardhat verify --constructor-args scripts/deployment/bridges/polygon/verify_03_erc20_child_tunnel.js --network " + providerName + " " + fxERC20ChildTunnel.address, { encoding: "utf-8" }); + } +} + +main() + .then(() => process.exit(0)) + .catch((error) => { + console.error(error); + process.exit(1); + }); diff --git a/scripts/deployment/bridges/polygon/test/fx_mumbai_goerli_token_transfer.js b/scripts/deployment/bridges/polygon/test/fx_mumbai_goerli_token_transfer.js new file mode 100644 index 0000000..f620b60 --- /dev/null +++ b/scripts/deployment/bridges/polygon/test/fx_mumbai_goerli_token_transfer.js @@ -0,0 +1,119 @@ +/*global process*/ + +const { ethers } = require("ethers"); + +async function main() { + const ALCHEMY_API_KEY_GOERLI = process.env.ALCHEMY_API_KEY_GOERLI; + const goerliURL = "https://eth-goerli.g.alchemy.com/v2/" + ALCHEMY_API_KEY_GOERLI; + const goerliProvider = new ethers.providers.JsonRpcProvider(goerliURL); + await goerliProvider.getBlockNumber().then((result) => { + console.log("Current block number goerli: " + result); + }); + + const ALCHEMY_API_KEY_MUMBAI = process.env.ALCHEMY_API_KEY_MUMBAI; + const mumbaiURL = "https://polygon-mumbai.g.alchemy.com/v2/" + ALCHEMY_API_KEY_MUMBAI; + const mumbaiProvider = new ethers.providers.JsonRpcProvider(mumbaiURL); + await mumbaiProvider.getBlockNumber().then((result) => { + console.log("Current block number mumbai: " + result); + }); + + const fs = require("fs"); + // FxRoot address on goerli + const fxRootAddress = "0x3d1d3E34f7fB6D26245E6640E1c50710eFFf15bA"; + const fxRootJSON = "artifacts/lib/fx-portal/contracts/FxRoot.sol/FxRoot.json"; + let contractFromJSON = fs.readFileSync(fxRootJSON, "utf8"); + let parsedFile = JSON.parse(contractFromJSON); + const fxRootABI = parsedFile["abi"]; + const fxRoot = new ethers.Contract(fxRootAddress, fxRootABI, goerliProvider); + + // FxChild address on mumbai + const fxChildAddress = "0xCf73231F28B7331BBe3124B907840A94851f9f11"; + const fxChildJSON = "artifacts/lib/fx-portal/contracts/FxChild.sol/FxChild.json"; + contractFromJSON = fs.readFileSync(fxChildJSON, "utf8"); + parsedFile = JSON.parse(contractFromJSON); + const fxChildABI = parsedFile["abi"]; + const fxChild = new ethers.Contract(fxChildAddress, fxChildABI, mumbaiProvider); + + // ChildMockERC20 address on mumbai + const mockChildERC20Address = "0xeB49bE5DF00F74bd240DE4535DDe6Bc89CEfb994"; + const mockChildERC20JSON = "artifacts/contracts/bridges/test/ChildMockERC20.sol/ChildMockERC20.json"; + contractFromJSON = fs.readFileSync(mockChildERC20JSON, "utf8"); + parsedFile = JSON.parse(contractFromJSON); + const mockChildERC20ABI = parsedFile["abi"]; + const mockChildERC20 = new ethers.Contract(mockChildERC20Address, mockChildERC20ABI, mumbaiProvider); + + // BridgedERC20 address on goerli + const bridgedERC20Address = "0x88e4ad16Bd4953Bbe74589942b368969037a7d81"; + const bridgedERC20JSON = "artifacts/contracts/bridges/BridgedERC20.sol/BridgedERC20.json"; + contractFromJSON = fs.readFileSync(bridgedERC20JSON, "utf8"); + parsedFile = JSON.parse(contractFromJSON); + const bridgedERC20ABI = parsedFile["abi"]; + const bridgedERC20 = new ethers.Contract(bridgedERC20Address, bridgedERC20ABI, goerliProvider); + + // Test deployed FxERC20ChildTunnel address on mumbai + const fxERC20ChildTunnelAddress = "0x1d333b46dB6e8FFd271b6C2D2B254868BD9A2dbd"; + const fxERC20ChildTunnelJSON = "artifacts/contracts/bridges/FxERC20ChildTunnel.sol/FxERC20ChildTunnel.json"; + contractFromJSON = fs.readFileSync(fxERC20ChildTunnelJSON, "utf8"); + parsedFile = JSON.parse(contractFromJSON); + const fxERC20ChildTunnelABI = parsedFile["abi"]; + const fxERC20ChildTunnel = new ethers.Contract(fxERC20ChildTunnelAddress, fxERC20ChildTunnelABI, mumbaiProvider); + const verifyFxChildAddress = await fxERC20ChildTunnel.fxChild(); + if (fxChildAddress == verifyFxChildAddress) { + console.log("Successfully connected to the test fxERC20ChildTunnel contract"); + } + + // Test deployed FxERC20RootTunnel address on goerli + const fxERC20RootTunnelAddress = "0x1479f01AbdC9b33e6a40F4b03dD38521e3feF98e"; + const fxERC20RootTunnelJSON = "artifacts/contracts/bridges/FxERC20RootTunnel.sol/FxERC20RootTunnel.json"; + contractFromJSON = fs.readFileSync(fxERC20RootTunnelJSON, "utf8"); + parsedFile = JSON.parse(contractFromJSON); + const fxERC20RootTunnelABI = parsedFile["abi"]; + const fxERC20RootTunnel = new ethers.Contract(fxERC20RootTunnelAddress, fxERC20RootTunnelABI, goerliProvider); + const verifyFxRootAddress = await fxERC20RootTunnel.fxRoot(); + if (fxRootAddress == verifyFxRootAddress) { + console.log("Successfully connected to the test fxERC20RootTunnel contract"); + } + + // Get the EOA + const account = ethers.utils.HDNode.fromMnemonic(process.env.TESTNET_MNEMONIC).derivePath("m/44'/60'/0'/0/0"); + const EOAgoerli = new ethers.Wallet(account, goerliProvider); + const EOAmumbai = new ethers.Wallet(account, mumbaiProvider); + console.log("EOA", EOAgoerli.address); + if (EOAmumbai.address == EOAgoerli.address) { + console.log("Correct wallet setup"); + } + +// const balance = await mockChildERC20.balanceOf(fxERC20ChildTunnel.address); +// console.log(balance); +// return; + + // Deposit tokens for goerli bridged ERC20 ones + await mockChildERC20.connect(EOAmumbai).approve(fxERC20ChildTunnel.address, 10); + const tx = await fxERC20ChildTunnel.connect(EOAmumbai).deposit(10); + console.log(tx.hash); + await tx.wait(); + + // Wait for the event of a processed data on mumbai and then goerli + let waitForEvent = true; + while (waitForEvent) { + // Check for the last 100 blocks in order to catch the event + const events = await fxERC20RootTunnel.queryFilter("FxDepositERC20", -100); + events.forEach((item) => { + console.log("Catch event FxDepositERC20:", item); + waitForEvent = false; + }); + // Continue waiting for the event if none was received + if (waitForEvent) { + console.log("Waiting for the receive event, next update in 300 seconds ..."); + // Sleep for a minute + await new Promise(r => setTimeout(r, 300000)); + } + } +} + +main() + .then(() => process.exit(0)) + .catch((error) => { + console.error(error); + process.exit(1); + }); diff --git a/scripts/deployment/bridges/polygon/test/globals.json b/scripts/deployment/bridges/polygon/test/globals.json index 506e318..8589f3a 100644 --- a/scripts/deployment/bridges/polygon/test/globals.json +++ b/scripts/deployment/bridges/polygon/test/globals.json @@ -1 +1 @@ -{"contractVerification":true,"useLedger":false,"derivationPath":"m/44'/60'/2'/0/0","fxRootAddress":"0x3d1d3E34f7fB6D26245E6640E1c50710eFFf15bA","fxChildAddress":"0xCf73231F28B7331BBe3124B907840A94851f9f11","timelockAddress":"0x95dA0F8C3eC5D40209f0EF1ED5E61deD28307d8d","fxGovernorTunnelAddress":"0x29086141ecdc310058fc23273F8ef7881d20C2f7","childMockERC20Address":"0xeB49bE5DF00F74bd240DE4535DDe6Bc89CEfb994"} \ No newline at end of file +{"contractVerification":true,"useLedger":false,"providerName":"polygonMumbai","derivationPath":"m/44'/60'/2'/0/0","fxRootAddress":"0x3d1d3E34f7fB6D26245E6640E1c50710eFFf15bA","fxChildAddress":"0xCf73231F28B7331BBe3124B907840A94851f9f11","timelockAddress":"0x95dA0F8C3eC5D40209f0EF1ED5E61deD28307d8d","fxGovernorTunnelAddress":"0x29086141ecdc310058fc23273F8ef7881d20C2f7","childTokenAddress":"0xeB49bE5DF00F74bd240DE4535DDe6Bc89CEfb994","checkpointManagerAddress":"0x2890bA17EfE978480615e330ecB65333b880928e","bridgedERC20Address":"0x88e4ad16Bd4953Bbe74589942b368969037a7d81","fxERC20ChildTunnelAddress":"0x1d333b46dB6e8FFd271b6C2D2B254868BD9A2dbd","fxERC20RootTunnelAddress":"0x1479f01AbdC9b33e6a40F4b03dD38521e3feF98e"} \ No newline at end of file diff --git a/scripts/deployment/bridges/polygon/verify_03_erc20_child_tunnel.js b/scripts/deployment/bridges/polygon/verify_03_erc20_child_tunnel.js new file mode 100644 index 0000000..af78daa --- /dev/null +++ b/scripts/deployment/bridges/polygon/verify_03_erc20_child_tunnel.js @@ -0,0 +1,10 @@ +const fs = require("fs"); +const globalsFile = "globals.json"; +const dataFromJSON = fs.readFileSync(globalsFile, "utf8"); +const parsedData = JSON.parse(dataFromJSON); + +module.exports = [ + parsedData.fxChildAddress, + parsedData.childTokenAddress, + parsedData.bridgedERC20Address +]; \ No newline at end of file diff --git a/scripts/deployment/deploy_19_bridged_erc20.js b/scripts/deployment/deploy_19_bridged_erc20.js new file mode 100644 index 0000000..ef2cc48 --- /dev/null +++ b/scripts/deployment/deploy_19_bridged_erc20.js @@ -0,0 +1,59 @@ +/*global process*/ + +const { ethers } = require("hardhat"); +const { LedgerSigner } = require("@anders-t/ethers-ledger"); + +async function main() { + const fs = require("fs"); + const globalsFile = "globals.json"; + const dataFromJSON = fs.readFileSync(globalsFile, "utf8"); + let parsedData = JSON.parse(dataFromJSON); + const useLedger = parsedData.useLedger; + const derivationPath = parsedData.derivationPath; + const providerName = parsedData.providerName; + let EOA; + + const provider = await ethers.providers.getDefaultProvider(providerName); + const signers = await ethers.getSigners(); + + if (useLedger) { + EOA = new LedgerSigner(provider, derivationPath); + } else { + EOA = signers[0]; + } + // EOA address + const deployer = await EOA.getAddress(); + console.log("EOA is:", deployer); + + // Transaction signing and execution + console.log("19. EOA to deploy bridged ERC20 contract"); + const BridgedERC20 = await ethers.getContractFactory("BridgedERC20"); + console.log("You are signing the following transaction: BridgedERC20.connect(EOA).deploy()"); + const bridgedERC20 = await BridgedERC20.connect(EOA).deploy(); + let result = await bridgedERC20.deployed(); + + // Transaction details + console.log("Contract deployment: BridgedERC20"); + console.log("Contract address:", bridgedERC20.address); + console.log("Transaction:", result.deployTransaction.hash); + + // Wait half a minute for the transaction completion + await new Promise(r => setTimeout(r, 30000)); + + // Writing updated parameters back to the JSON file + parsedData.bridgedERC20Address = bridgedERC20.address; + fs.writeFileSync(globalsFile, JSON.stringify(parsedData)); + + // Contract verification + if (parsedData.contractVerification) { + const execSync = require("child_process").execSync; + execSync("npx hardhat verify --network " + providerName + " " + bridgedERC20.address, { encoding: "utf-8" }); + } +} + +main() + .then(() => process.exit(0)) + .catch((error) => { + console.error(error); + process.exit(1); + }); diff --git a/scripts/deployment/deploy_20_erc20_root_tunnel.js b/scripts/deployment/deploy_20_erc20_root_tunnel.js new file mode 100644 index 0000000..943a418 --- /dev/null +++ b/scripts/deployment/deploy_20_erc20_root_tunnel.js @@ -0,0 +1,60 @@ +/*global process*/ + +const { ethers } = require("hardhat"); +const { LedgerSigner } = require("@anders-t/ethers-ledger"); + +async function main() { + const fs = require("fs"); + const globalsFile = "globals.json"; + const dataFromJSON = fs.readFileSync(globalsFile, "utf8"); + let parsedData = JSON.parse(dataFromJSON); + const useLedger = parsedData.useLedger; + const derivationPath = parsedData.derivationPath; + const providerName = parsedData.providerName; + let EOA; + + const provider = await ethers.providers.getDefaultProvider(providerName); + const signers = await ethers.getSigners(); + + if (useLedger) { + EOA = new LedgerSigner(provider, derivationPath); + } else { + EOA = signers[0]; + } + // EOA address + const deployer = await EOA.getAddress(); + console.log("EOA is:", deployer); + + // Transaction signing and execution + console.log("20. EOA to deploy ERC20 root tunnel contract"); + const FxERC20RootTunnel = await ethers.getContractFactory("FxERC20RootTunnel"); + console.log("You are signing the following transaction: FxERC20RootTunnel.connect(EOA).deploy(checkpointManager, fxRoot, childToken, rootToken)"); + const fxERC20RootTunnel = await FxERC20RootTunnel.connect(EOA).deploy(parsedData.checkpointManagerAddress, + parsedData.fxRootAddress, parsedData.childTokenAddress, parsedData.bridgedERC20Address); + const result = await fxERC20RootTunnel.deployed(); + + // Transaction details + console.log("Contract deployment: FxERC20RootTunnel"); + console.log("Contract address:", fxERC20RootTunnel.address); + console.log("Transaction:", result.deployTransaction.hash); + + // Wait half a minute for the transaction completion + await new Promise(r => setTimeout(r, 30000)); + + // Writing updated parameters back to the JSON file + parsedData.fxERC20RootTunnelAddress = fxERC20RootTunnel.address; + fs.writeFileSync(globalsFile, JSON.stringify(parsedData)); + + // Contract verification + if (parsedData.contractVerification) { + const execSync = require("child_process").execSync; + execSync("npx hardhat verify --constructor-args scripts/deployment/verify_20_erc20_root_tunnel.js --network " + providerName + " " + fxERC20RootTunnel.address, { encoding: "utf-8" }); + } +} + +main() + .then(() => process.exit(0)) + .catch((error) => { + console.error(error); + process.exit(1); + }); diff --git a/scripts/deployment/deploy_21_bridged_erc20_change_owner.js b/scripts/deployment/deploy_21_bridged_erc20_change_owner.js new file mode 100644 index 0000000..612d849 --- /dev/null +++ b/scripts/deployment/deploy_21_bridged_erc20_change_owner.js @@ -0,0 +1,47 @@ +/*global process*/ + +const { ethers } = require("hardhat"); +const { LedgerSigner } = require("@anders-t/ethers-ledger"); + +async function main() { + const fs = require("fs"); + const globalsFile = "globals.json"; + const dataFromJSON = fs.readFileSync(globalsFile, "utf8"); + let parsedData = JSON.parse(dataFromJSON); + const useLedger = parsedData.useLedger; + const derivationPath = parsedData.derivationPath; + const providerName = parsedData.providerName; + const fxERC20RootTunnelAddress = parsedData.fxERC20RootTunnelAddress; + const bridgedERC20Address = parsedData.bridgedERC20Address; + let EOA; + + const provider = await ethers.providers.getDefaultProvider(providerName); + const signers = await ethers.getSigners(); + + if (useLedger) { + EOA = new LedgerSigner(provider, derivationPath); + } else { + EOA = signers[0]; + } + // EOA address + const deployer = await EOA.getAddress(); + console.log("EOA is:", deployer); + + // Transaction signing and execution + console.log("21. EOA to change owner of BridgedERC20 to FxERC20RootTunnel"); + const bridgedERC20 = await ethers.getContractAt("BridgedERC20", bridgedERC20Address); + console.log("You are signing the following transaction: bridgedERC20.connect(EOA).changeOwner(FxERC20RootTunnel)"); + const result = await bridgedERC20.changeOwner(fxERC20RootTunnelAddress); + + // Transaction details + console.log("Contract deployment: BridgedERC20"); + console.log("Contract address:", bridgedERC20.address); + console.log("Transaction:", result.hash); +} + +main() + .then(() => process.exit(0)) + .catch((error) => { + console.error(error); + process.exit(1); + }); diff --git a/scripts/deployment/globals_goerli.json b/scripts/deployment/globals_goerli.json index 04d76f8..6874568 100644 --- a/scripts/deployment/globals_goerli.json +++ b/scripts/deployment/globals_goerli.json @@ -1 +1 @@ -{"contractVerification":true,"useLedger":false,"valoryMultisig":"0x87cc0d34f6111c8A7A4Bdf758a9a715A3675f941","derivationPath":"m/44'/60'/2'/0/0","CM":"0x04C06323Fe3D53Deb7364c0055E1F68458Cc2570","providerName":"goerli","olasSaltString":"0x0001a5","timelockMinDelay":"5","veOlasSaltString":"0x7e01a5","initialVotingDelay":"20","initialVotingPeriod":"80","initialProposalThreshold":"1000000000000000000000","quorum":"1","initSupply":"526500000000000000000000000","timelockSupply":"100000000000000000000000000","saleSupply":"301500000000000000000000000","valorySupply":"125000000000000000000000000","GnosisSafe":"0xd9Db270c1B5E3Bd161E8c8503c55cEABeE709552","GnosisSafeProxyFactory":"0xa6B71E26C5e0845f74c812102Ca7114b6a896AB2","MultiSendCallOnly":"0x40A2aCCbd92BCA938b02010E17A5b8929b49130D","deploymentFactory":"0x3C1fF68f5aa342D296d4DEe4Bb1cACCA912D95fE","olasAddress":"0xEdfc28215B1Eb6eb0be426f1f529cf691A5C2400","timelockAddress":"0x34C895f302D0b5cf52ec0Edd3945321EB0f83dd5","veOLASAddress":"0xf8B20e160557c747E8640CdcE77E1dd44bCaAfbB","governorAddress":"0xBb7e1D6Cb6F243D6bdE81CE92a9f2aFF7Fbe7eac","buOLASAddress":"0x397125902ED2cA2d42104F621f448A2cE1bC8Fb7","wveOLASAddress":"0xa2AA89938805836077aB0724f335142da7A27085","governorTwoAddress":"0x63b0f322837a7160B7E3d95C60aAaeB4EF1aECcb","treasuryAddress":"0x7bedCA17D29e53C8062d10902a6219F8d1E3B9B5","guardCMAddress":"0x1F4C3134aFB97DE2BfBBF28894d24F58D0A95eFC"} \ No newline at end of file +{"contractVerification":true,"useLedger":false,"valoryMultisig":"0x87cc0d34f6111c8A7A4Bdf758a9a715A3675f941","derivationPath":"m/44'/60'/2'/0/0","CM":"0x04C06323Fe3D53Deb7364c0055E1F68458Cc2570","providerName":"goerli","olasSaltString":"0x0001a5","timelockMinDelay":"5","veOlasSaltString":"0x7e01a5","initialVotingDelay":"20","initialVotingPeriod":"80","initialProposalThreshold":"1000000000000000000000","quorum":"1","initSupply":"526500000000000000000000000","timelockSupply":"100000000000000000000000000","saleSupply":"301500000000000000000000000","valorySupply":"125000000000000000000000000","GnosisSafe":"0xd9Db270c1B5E3Bd161E8c8503c55cEABeE709552","GnosisSafeProxyFactory":"0xa6B71E26C5e0845f74c812102Ca7114b6a896AB2","MultiSendCallOnly":"0x40A2aCCbd92BCA938b02010E17A5b8929b49130D","deploymentFactory":"0x3C1fF68f5aa342D296d4DEe4Bb1cACCA912D95fE","olasAddress":"0xEdfc28215B1Eb6eb0be426f1f529cf691A5C2400","timelockAddress":"0x34C895f302D0b5cf52ec0Edd3945321EB0f83dd5","veOLASAddress":"0xf8B20e160557c747E8640CdcE77E1dd44bCaAfbB","governorAddress":"0xBb7e1D6Cb6F243D6bdE81CE92a9f2aFF7Fbe7eac","buOLASAddress":"0x397125902ED2cA2d42104F621f448A2cE1bC8Fb7","wveOLASAddress":"0xa2AA89938805836077aB0724f335142da7A27085","governorTwoAddress":"0x63b0f322837a7160B7E3d95C60aAaeB4EF1aECcb","treasuryAddress":"0x7bedCA17D29e53C8062d10902a6219F8d1E3B9B5","guardCMAddress":"0x1F4C3134aFB97DE2BfBBF28894d24F58D0A95eFC","fxRootAddress":"0x3d1d3E34f7fB6D26245E6640E1c50710eFFf15bA","checkpointManagerAddress":"0x2890bA17EfE978480615e330ecB65333b880928e"} \ No newline at end of file diff --git a/scripts/deployment/verify_20_erc20_root_tunnel.js b/scripts/deployment/verify_20_erc20_root_tunnel.js new file mode 100644 index 0000000..cf32f0f --- /dev/null +++ b/scripts/deployment/verify_20_erc20_root_tunnel.js @@ -0,0 +1,11 @@ +const fs = require("fs"); +const globalsFile = "globals.json"; +const dataFromJSON = fs.readFileSync(globalsFile, "utf8"); +const parsedData = JSON.parse(dataFromJSON); + +module.exports = [ + parsedData.checkpointManagerAddress, + parsedData.fxRootAddress, + parsedData.childTokenAddress, + parsedData.bridgedERC20Address +]; \ No newline at end of file From 29f2810dcaa217ce56576b73836ecc1d20da9721 Mon Sep 17 00:00:00 2001 From: Aleksandr Kuperman Date: Wed, 15 Nov 2023 20:18:04 +0000 Subject: [PATCH 05/16] chore: linters --- .gitleaksignore | 4 ++++ .../bridges/polygon/test/fx_mumbai_goerli_token_transfer.js | 6 +++--- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/.gitleaksignore b/.gitleaksignore index cf731e4..202d85f 100644 --- a/.gitleaksignore +++ b/.gitleaksignore @@ -6,3 +6,7 @@ c4f2bb1997ed9f4689a807d494e7d29ed3c6bd2f:scripts/lbp/fjord_enable_swap.js:generi 260edea55dc19cebf830f7a68fa34c72685523f9:scripts/deployment/globals_mainnet.json:generic-api-key:2 4aec13ea3e18189b9f647459b84758efcea40785:scripts/deployment/globals_mainnet.json:generic-api-key:2 672f4a7e75e45bb8903e61a88246f735ba3f3b91:scripts/deployment/globals_mainnet.json:generic-api-key:2 +1f3a9deea388a55014ca6d39fb67d9d932e9a5fa:scripts/deployment/bridges/polygon/test/globals.json:generic-api-key:1 +1f3a9deea388a55014ca6d39fb67d9d932e9a5fa:scripts/deployment/bridges/polygon/test/globals.json:generic-api-key:2 +4b70a4ad9afdd4a2b965795fc580697cb9a282f8:scripts/deployment/bridges/polygon/test/globals.json:generic-api-key:1 +4b70a4ad9afdd4a2b965795fc580697cb9a282f8:scripts/deployment/bridges/polygon/test/globals.json:generic-api-key:2 diff --git a/scripts/deployment/bridges/polygon/test/fx_mumbai_goerli_token_transfer.js b/scripts/deployment/bridges/polygon/test/fx_mumbai_goerli_token_transfer.js index f620b60..a63cf42 100644 --- a/scripts/deployment/bridges/polygon/test/fx_mumbai_goerli_token_transfer.js +++ b/scripts/deployment/bridges/polygon/test/fx_mumbai_goerli_token_transfer.js @@ -83,9 +83,9 @@ async function main() { console.log("Correct wallet setup"); } -// const balance = await mockChildERC20.balanceOf(fxERC20ChildTunnel.address); -// console.log(balance); -// return; + // const balance = await mockChildERC20.balanceOf(fxERC20ChildTunnel.address); + // console.log(balance); + // return; // Deposit tokens for goerli bridged ERC20 ones await mockChildERC20.connect(EOAmumbai).approve(fxERC20ChildTunnel.address, 10); From a74dadd288a6c13df6851a984cf4cfa134c92483 Mon Sep 17 00:00:00 2001 From: Aleksandr Kuperman Date: Thu, 23 Nov 2023 18:07:32 +0000 Subject: [PATCH 06/16] chore: adding natspec --- contracts/bridges/BridgedERC20.sol | 5 +- contracts/bridges/FxERC20ChildTunnel.sol | 62 +++++++++++++++---- contracts/bridges/FxERC20RootTunnel.sol | 60 ++++++++++++++---- scripts/deployment/deploy_19_bridged_erc20.js | 1 + 4 files changed, 102 insertions(+), 26 deletions(-) diff --git a/contracts/bridges/BridgedERC20.sol b/contracts/bridges/BridgedERC20.sol index c30b32c..459fe98 100644 --- a/contracts/bridges/BridgedERC20.sol +++ b/contracts/bridges/BridgedERC20.sol @@ -13,12 +13,15 @@ error ZeroAddress(); /// @title BridgedERC20 - Smart contract for bridged ERC20 token +/// @dev Bridged token contract is owned by the bridge mediator contract, and thus the token representation from +/// another chain must be minted and burned solely by the bridge mediator contract. contract BridgedERC20 is ERC20 { event OwnerUpdated(address indexed owner); + // Bridged token owner address public owner; - constructor() ERC20("ERC20 bridged token", "BridgedERC20", 18) { + constructor(string memory _name, string memory _symbol) ERC20(_name, symbol, 18) { owner = msg.sender; } diff --git a/contracts/bridges/FxERC20ChildTunnel.sol b/contracts/bridges/FxERC20ChildTunnel.sol index 24d46f9..787c0a9 100644 --- a/contracts/bridges/FxERC20ChildTunnel.sol +++ b/contracts/bridges/FxERC20ChildTunnel.sol @@ -4,52 +4,90 @@ pragma solidity ^0.8.21; import {FxBaseChildTunnel} from "../../lib/fx-portal/contracts/tunnel/FxBaseChildTunnel.sol"; import {IERC20} from "../interfaces/IERC20.sol"; -/** - * @title FxERC20ChildTunnel - */ +/// @dev Provided zero address. +error ZeroAddress(); + +/// @dev Zero value when it has to be different from zero. +error ZeroValue(); + +/// @title FxERC20ChildTunnel - Smart contract for the L2 token management part +/// @author Aleksandr Kuperman - +/// @author Andrey Lebedev - +/// @author Mariapia Moscatiello - contract FxERC20ChildTunnel is FxBaseChildTunnel { event FxDepositERC20(address indexed childToken, address indexed rootToken, address from, address indexed to, uint256 amount); event FxWithdrawERC20(address indexed rootToken, address indexed childToken, address from, address indexed to, uint256 amount); - // Child token + // Child token address address public immutable childToken; - // Root token + // Root token address address public immutable rootToken; - // slither-disable-next-line missing-zero-check + /// @dev FxERC20ChildTunnel constructor. + /// @param _fxChild Fx Child contract address. + /// @param _childToken L2 token address. + /// @param _rootToken Corresponding L1 token address. constructor(address _fxChild, address _childToken, address _rootToken) FxBaseChildTunnel(_fxChild) { + // Check for zero addresses + if (_fxChild == address(0) || _childToken == address(0) || _rootToken == address(0)) { + revert ZeroAddress(); + } + childToken = _childToken; rootToken = _rootToken; } + /// @dev Deposits tokens on L2 in order to obtain their corresponding bridged version on L1. + /// @notice Destination address is the same as the sender address. + /// @param amount Token amount to be deposited. function deposit(uint256 amount) external { _deposit(msg.sender, amount); } + /// @dev Deposits tokens on L2 in order to obtain their corresponding bridged version on L1 by a specified address. + /// @param to Destination address on L1. + /// @param amount Token amount to be deposited. function depositTo(address to, uint256 amount) external { + // Check for the address to send tokens to + if (to == address(0)) { + revert ZeroAddress(); + } + _deposit(to, amount); } + /// @dev Receives the token message from L1 and transfers L2 tokens to a specified address. + /// @param sender FxERC20RootTunnel contract address from L1. + /// @param message Incoming bridge data. function _processMessageFromRoot( uint256 /* stateId */, address sender, - bytes memory data + bytes memory message ) internal override validateSender(sender) { - // Decode incoming data - (address from, address to, uint256 amount) = abi.decode(data, (address, address, uint256)); + // Decode incoming message from root: (address, address, uint256) + (address from, address to, uint256 amount) = abi.decode(message, (address, address, uint256)); - // Transfer tokens + // Transfer decoded amount of tokens to a specified address IERC20(childToken).transfer(to, amount); emit FxWithdrawERC20(rootToken, childToken, from, to, amount); } + /// @dev Deposits tokens on L2 to get their representation on L1 by a specified address. + /// @param to Destination address on L1. + /// @param amount Token amount to be deposited. function _deposit(address to, uint256 amount) internal { - // Deposit tokens + // Check for the non-zero amount + if (amount == 0) { + revert ZeroValue(); + } + + // Deposit tokens on an L2 bridge contract (lock) IERC20(childToken).transferFrom(msg.sender, address(this), amount); - // Send message to root + // Encode message for root: (address, address, uint256) bytes memory message = abi.encode(msg.sender, to, amount); + // Send message to root _sendMessageToRoot(message); emit FxDepositERC20(childToken, rootToken, msg.sender, to, amount); diff --git a/contracts/bridges/FxERC20RootTunnel.sol b/contracts/bridges/FxERC20RootTunnel.sol index 63ee682..a3abb14 100755 --- a/contracts/bridges/FxERC20RootTunnel.sol +++ b/contracts/bridges/FxERC20RootTunnel.sol @@ -4,53 +4,87 @@ pragma solidity ^0.8.21; import {FxBaseRootTunnel} from "../../lib/fx-portal/contracts/tunnel/FxBaseRootTunnel.sol"; import {IERC20} from "../interfaces/IERC20.sol"; -/** - * @title FxERC20RootTunnel - */ +/// @dev Provided zero address. +error ZeroAddress(); + +/// @dev Zero value when it has to be different from zero. +error ZeroValue(); + +/// @title FxERC20RootTunnel - Smart contract for the L1 token management part +/// @author Aleksandr Kuperman - +/// @author Andrey Lebedev - +/// @author Mariapia Moscatiello - contract FxERC20RootTunnel is FxBaseRootTunnel { event FxDepositERC20(address indexed childToken, address indexed rootToken, address from, address indexed to, uint256 amount); event FxWithdrawERC20(address indexed rootToken, address indexed childToken, address from, address indexed to, uint256 amount); - // Child token + // Child token address address public immutable childToken; - // Root token + // Root token address address public immutable rootToken; + /// @dev FxERC20RootTunnel constructor. + /// @param _checkpointManager Checkpoint manager contract. + /// @param _fxRoot Fx Root contract address. + /// @param _childToken L2 token address. + /// @param _rootToken Corresponding L1 token address. constructor(address _checkpointManager, address _fxRoot, address _childToken, address _rootToken) FxBaseRootTunnel(_checkpointManager, _fxRoot) { + // Check for zero addresses + if (_checkpointManager == address(0) || _fxRoot == address(0) || _childToken == address(0) || + _rootToken == address(0)) { + revert ZeroAddress(); + } + childToken = _childToken; rootToken = _rootToken; } + /// @dev Withdraws bridged tokens on L1 in order to obtain their original version on L2. + /// @notice Destination address is the same as the sender address. + /// @param amount Token amount to be withdrawn. function withdraw(uint256 amount) external { _withdraw(msg.sender, amount); } + /// @dev Withdraws bridged tokens on L1 in order to obtain their original version on L2 by a specified address. + /// @param to Destination address on L2. + /// @param amount Token amount to be withdrawn. function withdrawTo(address to, uint256 amount) external { _withdraw(to, amount); } - // exit processor - function _processMessageFromChild(bytes memory data) internal override { - // Decode message from child - (address from, address to, uint256 amount) = abi.decode(data, (address, address, uint256)); + /// @dev Receives the token message from L1 and transfers L2 tokens to a specified address. + /// @param message Incoming bridge data. + function _processMessageFromChild(bytes memory message) internal override { + // Decode incoming message from child: (address, address, uint256) + (address from, address to, uint256 amount) = abi.decode(message, (address, address, uint256)); - // transfer from tokens to + // Mints bridged amount of tokens to a specified address IERC20(rootToken).mint(to, amount); emit FxDepositERC20(childToken, rootToken, from, to, amount); } + /// @dev Withdraws bridged tokens from L1 to get their original tokens on L1 by a specified address. + /// @param to Destination address on L2. + /// @param amount Token amount to be withdrawn. function _withdraw(address to, uint256 amount) internal { - // Transfer from sender to this contract + // Check for the non-zero amount + if (amount == 0) { + revert ZeroValue(); + } + + // Transfer tokens from sender to this contract address IERC20(rootToken).transferFrom(msg.sender, address(this), amount); - // Burn tokens + // Burn bridged tokens IERC20(rootToken).burn(amount); - // Send message to child + // Encode message for child: (address, address, uint256) bytes memory message = abi.encode(msg.sender, to, amount); + // Send message to child _sendMessageToChild(message); emit FxWithdrawERC20(rootToken, childToken, msg.sender, to, amount); diff --git a/scripts/deployment/deploy_19_bridged_erc20.js b/scripts/deployment/deploy_19_bridged_erc20.js index ef2cc48..1aad9a4 100644 --- a/scripts/deployment/deploy_19_bridged_erc20.js +++ b/scripts/deployment/deploy_19_bridged_erc20.js @@ -30,6 +30,7 @@ async function main() { const BridgedERC20 = await ethers.getContractFactory("BridgedERC20"); console.log("You are signing the following transaction: BridgedERC20.connect(EOA).deploy()"); const bridgedERC20 = await BridgedERC20.connect(EOA).deploy(); + //const bridgedERC20 = await BridgedERC20.connect(EOA).deploy("ERC20 bridged token", "BridgedERC20"); let result = await bridgedERC20.deployed(); // Transaction details From c7cef3636adcb513a2461b6cc435baacee3c6c32 Mon Sep 17 00:00:00 2001 From: Aleksandr Kuperman Date: Thu, 23 Nov 2023 20:34:07 +0000 Subject: [PATCH 07/16] test: adding tests --- contracts/bridges/FxERC20ChildTunnel.sol | 2 +- contracts/bridges/FxERC20RootTunnel.sol | 2 +- test/FxERC20.js | 217 +++++++++++++++++++++++ 3 files changed, 219 insertions(+), 2 deletions(-) create mode 100644 test/FxERC20.js diff --git a/contracts/bridges/FxERC20ChildTunnel.sol b/contracts/bridges/FxERC20ChildTunnel.sol index 787c0a9..ea1ba71 100644 --- a/contracts/bridges/FxERC20ChildTunnel.sol +++ b/contracts/bridges/FxERC20ChildTunnel.sol @@ -58,7 +58,7 @@ contract FxERC20ChildTunnel is FxBaseChildTunnel { /// @dev Receives the token message from L1 and transfers L2 tokens to a specified address. /// @param sender FxERC20RootTunnel contract address from L1. - /// @param message Incoming bridge data. + /// @param message Incoming bridge message. function _processMessageFromRoot( uint256 /* stateId */, address sender, diff --git a/contracts/bridges/FxERC20RootTunnel.sol b/contracts/bridges/FxERC20RootTunnel.sol index a3abb14..d29c75d 100755 --- a/contracts/bridges/FxERC20RootTunnel.sol +++ b/contracts/bridges/FxERC20RootTunnel.sol @@ -56,7 +56,7 @@ contract FxERC20RootTunnel is FxBaseRootTunnel { } /// @dev Receives the token message from L1 and transfers L2 tokens to a specified address. - /// @param message Incoming bridge data. + /// @param message Incoming bridge message. function _processMessageFromChild(bytes memory message) internal override { // Decode incoming message from child: (address, address, uint256) (address from, address to, uint256 amount) = abi.decode(message, (address, address, uint256)); diff --git a/test/FxERC20.js b/test/FxERC20.js new file mode 100644 index 0000000..35f9770 --- /dev/null +++ b/test/FxERC20.js @@ -0,0 +1,217 @@ +/*global describe, context, beforeEach, it*/ + +const { expect } = require("chai"); +const { ethers } = require("hardhat"); + +describe("FxERC20", function () { + let fxERC20RootTunnel; + let fxERC20ChildTunnel; + let childToken; + let rootToken; + let signers; + let deployer; + const AddressZero = ethers.constants.AddressZero; + const stateId = 0; + const initMint = "1" + "0".repeat(20); + const amount = 1000; + + beforeEach(async function () { + signers = await ethers.getSigners(); + deployer = signers[0]; + + // Child token on L2 + const ChildMockERC20 = await ethers.getContractFactory("ChildMockERC20"); + childToken = await ChildMockERC20.deploy(); + await childToken.deployed(); + + // Root token is a bridged ERC20 token + const BridgedToken = await ethers.getContractFactory("BridgedERC20"); + rootToken = await BridgedToken.deploy("Bridged token", "BERC20"); + await rootToken.deployed(); + + // ERC20 tunnels + const FxERC20RootTunnel = await ethers.getContractFactory("FxERC20RootTunnel"); + fxERC20RootTunnel = await FxERC20RootTunnel.deploy(deployer.address, deployer.address, childToken.address, + rootToken.address); + await fxERC20RootTunnel.deployed(); + + const FxERC20ChildTunnel = await ethers.getContractFactory("FxERC20ChildTunnel"); + fxERC20ChildTunnel = await FxERC20ChildTunnel.deploy(deployer.address, childToken.address, rootToken.address); + await fxERC20ChildTunnel.deployed(); + + // Set child and root tunnels accordingly + await fxERC20RootTunnel.setFxChildTunnel(fxERC20ChildTunnel.address); + await fxERC20ChildTunnel.setFxRootTunnel(fxERC20RootTunnel.address); + + // Mint tokens + await childToken.mint(deployer.address, initMint); + }); + + context("Initialization", async function () { + it("Deploying with zero addresses", async function () { + const FxERC20RootTunnel = await ethers.getContractFactory("FxERC20RootTunnel"); + await expect( + FxERC20RootTunnel.deploy(AddressZero, AddressZero, AddressZero, AddressZero) + ).to.be.revertedWithCustomError(fxERC20RootTunnel, "ZeroAddress"); + + await expect( + FxERC20RootTunnel.deploy(signers[1].address, AddressZero, AddressZero, AddressZero) + ).to.be.revertedWithCustomError(fxERC20RootTunnel, "ZeroAddress"); + + await expect( + FxERC20RootTunnel.deploy(signers[1].address, signers[1].address, AddressZero, AddressZero) + ).to.be.revertedWithCustomError(fxERC20RootTunnel, "ZeroAddress"); + + await expect( + FxERC20RootTunnel.deploy(signers[1].address, signers[1].address, signers[1].address, AddressZero) + ).to.be.revertedWithCustomError(fxERC20RootTunnel, "ZeroAddress"); + + const FxERC20ChildTunnel = await ethers.getContractFactory("FxERC20ChildTunnel"); + await expect( + FxERC20ChildTunnel.deploy(AddressZero, AddressZero, AddressZero) + ).to.be.revertedWithCustomError(fxERC20ChildTunnel, "ZeroAddress"); + + await expect( + FxERC20ChildTunnel.deploy(signers[1].address, AddressZero, AddressZero) + ).to.be.revertedWithCustomError(fxERC20ChildTunnel, "ZeroAddress"); + + await expect( + FxERC20ChildTunnel.deploy(signers[1].address, signers[1].address, AddressZero) + ).to.be.revertedWithCustomError(fxERC20ChildTunnel, "ZeroAddress"); + }); + }); + + context("Deposit and withdraw ERC20 tokens", async function () { + it("Should fail when trying to call from incorrect contract addresses", async function () { + // FxChild as a sender is incorrect + await expect( + fxERC20ChildTunnel.connect(signers[1]).processMessageFromRoot(stateId, deployer.address, "0x") + ).to.be.revertedWith("FxBaseChildTunnel: INVALID_SENDER"); + + // FxRoot as a sender is incorrect + await expect( + fxERC20ChildTunnel.connect(deployer).processMessageFromRoot(stateId, signers[1].address, "0x") + ).to.be.revertedWith("FxBaseChildTunnel: INVALID_SENDER_FROM_ROOT"); + }); + + it("Deposit tokens", async function () { + // Approve tokens + await childToken.approve(fxERC20ChildTunnel.address, amount); + + // Send tokens to L1 + await fxERC20ChildTunnel.connect(deployer).deposit(amount); + + // Tokens must be locked on the FxERC20ChildTunnel contract address + const balance = await childToken.balanceOf(fxERC20ChildTunnel.address); + expect(balance).to.equal(amount); + + // On L1 fxERC20RootTunnel will be passed a proof validation data that the tx has happened on L2 + await rootToken.mint(deployer.address, amount); + }); + + it("Deposit tokens to a different address", async function () { + const account = signers[1].address; + + // Approve tokens + await childToken.approve(fxERC20ChildTunnel.address, amount); + + // Send tokens to L1 + await fxERC20ChildTunnel.connect(deployer).depositTo(account, amount); + + // Tokens must be locked on the FxERC20ChildTunnel contract address + const balance = await childToken.balanceOf(fxERC20ChildTunnel.address); + expect(balance).to.equal(amount); + + // On L1 fxERC20RootTunnel will be passed a proof validation data that the tx has happened on L2 + await rootToken.mint(account, amount); + }); + + it("Withdraw tokens", async function () { + // Approve tokens + await childToken.connect(deployer).approve(fxERC20ChildTunnel.address, amount); + + // Send tokens to L1 + await fxERC20ChildTunnel.connect(deployer).deposit(amount); + + // Tokens must be locked on the FxERC20ChildTunnel contract address + let balance = await childToken.balanceOf(fxERC20ChildTunnel.address); + expect(balance).to.equal(amount); + + // On L1 fxERC20RootTunnel will be passed a proof validation data that the tx has happened on L2 + await rootToken.mint(deployer.address, amount); + + // Withdraw tokens + await rootToken.connect(deployer).approve(fxERC20RootTunnel.address, amount); + + // Root token must be owned by the FxERC20RootTunnel contract + await rootToken.changeOwner(fxERC20RootTunnel.address); + + // Burn tokens on L1 and send message to L2 to retrieve them there + await expect( + fxERC20RootTunnel.withdraw(amount) + ).to.be.reverted; + + // Get the message on L2 + // const data = ethers.utils.solidityPack( + // ["address", "address", "uint256"], + // [deployer.address, deployer.address, amount] + // ); + const data = ethers.utils.defaultAbiCoder.encode( + ["address", "address", "uint256"], + [deployer.address, deployer.address, amount] + ); + + // Upon message receive, tokens on L2 are transferred to the destination account (deployer) + await fxERC20ChildTunnel.connect(deployer).processMessageFromRoot(stateId, fxERC20RootTunnel.address, data); + + // There must be no balance left locked on the FxERC20ChildTunnel contract + balance = await childToken.balanceOf(fxERC20ChildTunnel.address); + expect(balance).to.equal(0); + }); + + it("Withdraw tokens to a different address", async function () { + const account = signers[1]; + + // Approve tokens + await childToken.connect(deployer).approve(fxERC20ChildTunnel.address, amount); + + // Send tokens to L1 + await fxERC20ChildTunnel.connect(deployer).depositTo(account.address, amount); + + // Tokens must be locked on the FxERC20ChildTunnel contract address + let balance = await childToken.balanceOf(fxERC20ChildTunnel.address); + expect(balance).to.equal(amount); + + // On L1 fxERC20RootTunnel will be passed a proof validation data that the tx has happened on L2 + await rootToken.mint(account.address, amount); + + // Withdraw tokens + await rootToken.connect(deployer).approve(fxERC20RootTunnel.address, amount); + + // Root token must be owned by the FxERC20RootTunnel contract + await rootToken.changeOwner(fxERC20RootTunnel.address); + + // Burn tokens on L1 and send message to L2 to retrieve them there + await expect( + fxERC20RootTunnel.withdrawTo(deployer.address, amount) + ).to.be.reverted; + + // Get the message on L2 + // const data = ethers.utils.solidityPack( + // ["address", "address", "uint256"], + // [deployer.address, deployer.address, amount] + // ); + const data = ethers.utils.defaultAbiCoder.encode( + ["address", "address", "uint256"], + [account.address, deployer.address, amount] + ); + + // Upon message receive, tokens on L2 are transferred to the destination account (deployer) + await fxERC20ChildTunnel.connect(deployer).processMessageFromRoot(stateId, fxERC20RootTunnel.address, data); + + // There must be no balance left locked on the FxERC20ChildTunnel contract + balance = await childToken.balanceOf(fxERC20ChildTunnel.address); + expect(balance).to.equal(0); + }); + }); +}); From cf5e70a14b24a2a17ab21aee201aee6b514286dd Mon Sep 17 00:00:00 2001 From: Aleksandr Kuperman Date: Thu, 23 Nov 2023 20:41:47 +0000 Subject: [PATCH 08/16] refactor: encode to encodePacked --- contracts/bridges/FxERC20ChildTunnel.sol | 18 ++++++++++++++++-- contracts/bridges/FxERC20RootTunnel.sol | 18 ++++++++++++++++-- test/FxERC20.js | 16 ++++------------ 3 files changed, 36 insertions(+), 16 deletions(-) diff --git a/contracts/bridges/FxERC20ChildTunnel.sol b/contracts/bridges/FxERC20ChildTunnel.sol index ea1ba71..b7b9219 100644 --- a/contracts/bridges/FxERC20ChildTunnel.sol +++ b/contracts/bridges/FxERC20ChildTunnel.sol @@ -65,7 +65,21 @@ contract FxERC20ChildTunnel is FxBaseChildTunnel { bytes memory message ) internal override validateSender(sender) { // Decode incoming message from root: (address, address, uint256) - (address from, address to, uint256 amount) = abi.decode(message, (address, address, uint256)); + address from; + address to; + uint256 amount; + // solhint-disable-next-line no-inline-assembly + assembly { + // Offset 20 bytes for the address from (160 bits) + let i := 20 + from := mload(add(message, i)) + // Offset 20 bytes for the address to (160 bits) + i := add(i, 20) + to := mload(add(message, i)) + // Offset the data by32 bytes of amount (256 bits) + i := add(i, 32) + amount := mload(add(message, i)) + } // Transfer decoded amount of tokens to a specified address IERC20(childToken).transfer(to, amount); @@ -86,7 +100,7 @@ contract FxERC20ChildTunnel is FxBaseChildTunnel { IERC20(childToken).transferFrom(msg.sender, address(this), amount); // Encode message for root: (address, address, uint256) - bytes memory message = abi.encode(msg.sender, to, amount); + bytes memory message = abi.encodePacked(msg.sender, to, amount); // Send message to root _sendMessageToRoot(message); diff --git a/contracts/bridges/FxERC20RootTunnel.sol b/contracts/bridges/FxERC20RootTunnel.sol index d29c75d..4b3ea8d 100755 --- a/contracts/bridges/FxERC20RootTunnel.sol +++ b/contracts/bridges/FxERC20RootTunnel.sol @@ -59,7 +59,21 @@ contract FxERC20RootTunnel is FxBaseRootTunnel { /// @param message Incoming bridge message. function _processMessageFromChild(bytes memory message) internal override { // Decode incoming message from child: (address, address, uint256) - (address from, address to, uint256 amount) = abi.decode(message, (address, address, uint256)); + address from; + address to; + uint256 amount; + // solhint-disable-next-line no-inline-assembly + assembly { + // Offset 20 bytes for the address from (160 bits) + let i := 20 + from := mload(add(message, i)) + // Offset 20 bytes for the address to (160 bits) + i := add(i, 20) + to := mload(add(message, i)) + // Offset the data by32 bytes of amount (256 bits) + i := add(i, 32) + amount := mload(add(message, i)) + } // Mints bridged amount of tokens to a specified address IERC20(rootToken).mint(to, amount); @@ -83,7 +97,7 @@ contract FxERC20RootTunnel is FxBaseRootTunnel { IERC20(rootToken).burn(amount); // Encode message for child: (address, address, uint256) - bytes memory message = abi.encode(msg.sender, to, amount); + bytes memory message = abi.encodePacked(msg.sender, to, amount); // Send message to child _sendMessageToChild(message); diff --git a/test/FxERC20.js b/test/FxERC20.js index 35f9770..fd5f1f9 100644 --- a/test/FxERC20.js +++ b/test/FxERC20.js @@ -152,11 +152,7 @@ describe("FxERC20", function () { ).to.be.reverted; // Get the message on L2 - // const data = ethers.utils.solidityPack( - // ["address", "address", "uint256"], - // [deployer.address, deployer.address, amount] - // ); - const data = ethers.utils.defaultAbiCoder.encode( + const data = ethers.utils.solidityPack( ["address", "address", "uint256"], [deployer.address, deployer.address, amount] ); @@ -169,7 +165,7 @@ describe("FxERC20", function () { expect(balance).to.equal(0); }); - it("Withdraw tokens to a different address", async function () { + it.only("Withdraw tokens to a different address", async function () { const account = signers[1]; // Approve tokens @@ -197,13 +193,9 @@ describe("FxERC20", function () { ).to.be.reverted; // Get the message on L2 - // const data = ethers.utils.solidityPack( - // ["address", "address", "uint256"], - // [deployer.address, deployer.address, amount] - // ); - const data = ethers.utils.defaultAbiCoder.encode( + const data = ethers.utils.solidityPack( ["address", "address", "uint256"], - [account.address, deployer.address, amount] + [deployer.address, deployer.address, amount] ); // Upon message receive, tokens on L2 are transferred to the destination account (deployer) From 6d1e4dae2999e2650f7fd5096838bc6a9b2f7501 Mon Sep 17 00:00:00 2001 From: Aleksandr Kuperman Date: Thu, 23 Nov 2023 20:51:17 +0000 Subject: [PATCH 09/16] refactor: encode to encodePacked --- contracts/bridges/FxERC20ChildTunnel.sol | 9 +++------ contracts/bridges/FxERC20RootTunnel.sol | 9 +++------ test/FxERC20.js | 2 +- 3 files changed, 7 insertions(+), 13 deletions(-) diff --git a/contracts/bridges/FxERC20ChildTunnel.sol b/contracts/bridges/FxERC20ChildTunnel.sol index b7b9219..16d085a 100644 --- a/contracts/bridges/FxERC20ChildTunnel.sol +++ b/contracts/bridges/FxERC20ChildTunnel.sol @@ -71,14 +71,11 @@ contract FxERC20ChildTunnel is FxBaseChildTunnel { // solhint-disable-next-line no-inline-assembly assembly { // Offset 20 bytes for the address from (160 bits) - let i := 20 - from := mload(add(message, i)) + from := mload(add(message, 20)) // Offset 20 bytes for the address to (160 bits) - i := add(i, 20) - to := mload(add(message, i)) + to := mload(add(message, 40)) // Offset the data by32 bytes of amount (256 bits) - i := add(i, 32) - amount := mload(add(message, i)) + amount := mload(add(message, 72)) } // Transfer decoded amount of tokens to a specified address diff --git a/contracts/bridges/FxERC20RootTunnel.sol b/contracts/bridges/FxERC20RootTunnel.sol index 4b3ea8d..d103c92 100755 --- a/contracts/bridges/FxERC20RootTunnel.sol +++ b/contracts/bridges/FxERC20RootTunnel.sol @@ -65,14 +65,11 @@ contract FxERC20RootTunnel is FxBaseRootTunnel { // solhint-disable-next-line no-inline-assembly assembly { // Offset 20 bytes for the address from (160 bits) - let i := 20 - from := mload(add(message, i)) + from := mload(add(message, 20)) // Offset 20 bytes for the address to (160 bits) - i := add(i, 20) - to := mload(add(message, i)) + to := mload(add(message, 40)) // Offset the data by32 bytes of amount (256 bits) - i := add(i, 32) - amount := mload(add(message, i)) + amount := mload(add(message, 72)) } // Mints bridged amount of tokens to a specified address diff --git a/test/FxERC20.js b/test/FxERC20.js index fd5f1f9..a52ba9f 100644 --- a/test/FxERC20.js +++ b/test/FxERC20.js @@ -165,7 +165,7 @@ describe("FxERC20", function () { expect(balance).to.equal(0); }); - it.only("Withdraw tokens to a different address", async function () { + it("Withdraw tokens to a different address", async function () { const account = signers[1]; // Approve tokens From ad70c18418fcef8b750593ce850b0f43ffe83fd8 Mon Sep 17 00:00:00 2001 From: Aleksandr Kuperman <95347597+kupermind@users.noreply.github.com> Date: Fri, 24 Nov 2023 10:53:46 +0000 Subject: [PATCH 10/16] Update contracts/bridges/FxERC20RootTunnel.sol Co-authored-by: mariapiamo <93650028+mariapiamo@users.noreply.github.com> --- contracts/bridges/FxERC20RootTunnel.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/bridges/FxERC20RootTunnel.sol b/contracts/bridges/FxERC20RootTunnel.sol index d103c92..7750e72 100755 --- a/contracts/bridges/FxERC20RootTunnel.sol +++ b/contracts/bridges/FxERC20RootTunnel.sol @@ -55,7 +55,7 @@ contract FxERC20RootTunnel is FxBaseRootTunnel { _withdraw(to, amount); } - /// @dev Receives the token message from L1 and transfers L2 tokens to a specified address. + /// @dev Receives the token message from L2 and transfers bridged tokens to a specified address. /// @param message Incoming bridge message. function _processMessageFromChild(bytes memory message) internal override { // Decode incoming message from child: (address, address, uint256) From 10e7a749138c7aa01d76603a77502419c6d6b6fc Mon Sep 17 00:00:00 2001 From: Aleksandr Kuperman Date: Fri, 24 Nov 2023 11:15:24 +0000 Subject: [PATCH 11/16] test: adding to tests --- test/FxERC20.js | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/test/FxERC20.js b/test/FxERC20.js index a52ba9f..9dfe1e0 100644 --- a/test/FxERC20.js +++ b/test/FxERC20.js @@ -83,14 +83,14 @@ describe("FxERC20", function () { context("Deposit and withdraw ERC20 tokens", async function () { it("Should fail when trying to call from incorrect contract addresses", async function () { - // FxChild as a sender is incorrect + // signers[1].address as a sender is incorrect, must be deployer.address (aka FxChild in the setup) await expect( fxERC20ChildTunnel.connect(signers[1]).processMessageFromRoot(stateId, deployer.address, "0x") ).to.be.revertedWith("FxBaseChildTunnel: INVALID_SENDER"); - // FxRoot as a sender is incorrect + // deployer.address as an FxERC20RootTunnel is incorrect await expect( - fxERC20ChildTunnel.connect(deployer).processMessageFromRoot(stateId, signers[1].address, "0x") + fxERC20ChildTunnel.connect(deployer).processMessageFromRoot(stateId, deployer.address, "0x") ).to.be.revertedWith("FxBaseChildTunnel: INVALID_SENDER_FROM_ROOT"); }); @@ -157,12 +157,19 @@ describe("FxERC20", function () { [deployer.address, deployer.address, amount] ); + const balanceBefore = await childToken.balanceOf(deployer.address); + // Upon message receive, tokens on L2 are transferred to the destination account (deployer) await fxERC20ChildTunnel.connect(deployer).processMessageFromRoot(stateId, fxERC20RootTunnel.address, data); // There must be no balance left locked on the FxERC20ChildTunnel contract balance = await childToken.balanceOf(fxERC20ChildTunnel.address); expect(balance).to.equal(0); + + // The receiver balance must increase for the amount sent + const balanceAfter = await childToken.balanceOf(deployer.address); + const balanceDiff = Number(balanceAfter.sub(balanceBefore)); + expect(balanceDiff).to.equal(amount); }); it("Withdraw tokens to a different address", async function () { @@ -195,15 +202,22 @@ describe("FxERC20", function () { // Get the message on L2 const data = ethers.utils.solidityPack( ["address", "address", "uint256"], - [deployer.address, deployer.address, amount] + [deployer.address, account.address, amount] ); + const balanceBefore = await childToken.balanceOf(account.address); + // Upon message receive, tokens on L2 are transferred to the destination account (deployer) await fxERC20ChildTunnel.connect(deployer).processMessageFromRoot(stateId, fxERC20RootTunnel.address, data); // There must be no balance left locked on the FxERC20ChildTunnel contract balance = await childToken.balanceOf(fxERC20ChildTunnel.address); expect(balance).to.equal(0); + + // The receiver balance must increase for the amount sent + const balanceAfter = await childToken.balanceOf(account.address); + const balanceDiff = Number(balanceAfter.sub(balanceBefore)); + expect(balanceDiff).to.equal(amount); }); }); }); From bb050c242cd1b376825af509773f49a266c935db Mon Sep 17 00:00:00 2001 From: Aleksandr Kuperman Date: Fri, 24 Nov 2023 12:11:04 +0000 Subject: [PATCH 12/16] feat: introducing FxRootMock contract to simulate cross-chain --- test/FxERC20.js | 62 ++++++++++++++++++++++++------------------------- 1 file changed, 31 insertions(+), 31 deletions(-) diff --git a/test/FxERC20.js b/test/FxERC20.js index 9dfe1e0..554da0c 100644 --- a/test/FxERC20.js +++ b/test/FxERC20.js @@ -30,13 +30,22 @@ describe("FxERC20", function () { await rootToken.deployed(); // ERC20 tunnels + const FxRootMock = await ethers.getContractFactory("FxRootMock"); + const fxRootMock = await FxRootMock.deploy(); + await fxRootMock.deployed(); + const FxERC20RootTunnel = await ethers.getContractFactory("FxERC20RootTunnel"); - fxERC20RootTunnel = await FxERC20RootTunnel.deploy(deployer.address, deployer.address, childToken.address, + fxERC20RootTunnel = await FxERC20RootTunnel.deploy(deployer.address, fxRootMock.address, childToken.address, rootToken.address); await fxERC20RootTunnel.deployed(); + // Set the FxERC20RootTunnel address such that the FxRootMock routes the call from the FxERC20RootTunnel + // directly to the FxERC20ChildTunnel to simulate the cross-chain message sending + await fxRootMock.setRootTunnel(fxERC20RootTunnel.address); + const FxERC20ChildTunnel = await ethers.getContractFactory("FxERC20ChildTunnel"); - fxERC20ChildTunnel = await FxERC20ChildTunnel.deploy(deployer.address, childToken.address, rootToken.address); + // FxRootMock is the mock for FxChild contract address in order to re-route message sending for testing purposes + fxERC20ChildTunnel = await FxERC20ChildTunnel.deploy(fxRootMock.address, childToken.address, rootToken.address); await fxERC20ChildTunnel.deployed(); // Set child and root tunnels accordingly @@ -88,9 +97,14 @@ describe("FxERC20", function () { fxERC20ChildTunnel.connect(signers[1]).processMessageFromRoot(stateId, deployer.address, "0x") ).to.be.revertedWith("FxBaseChildTunnel: INVALID_SENDER"); + // deployer is the FxChild for testing purposes + const FxERC20ChildTunnel = await ethers.getContractFactory("FxERC20ChildTunnel"); + const testChildTunnel = await FxERC20ChildTunnel.deploy(deployer.address, childToken.address, rootToken.address); + await testChildTunnel.deployed(); + // deployer.address as an FxERC20RootTunnel is incorrect await expect( - fxERC20ChildTunnel.connect(deployer).processMessageFromRoot(stateId, deployer.address, "0x") + testChildTunnel.connect(deployer).processMessageFromRoot(stateId, deployer.address, "0x") ).to.be.revertedWith("FxBaseChildTunnel: INVALID_SENDER_FROM_ROOT"); }); @@ -146,21 +160,14 @@ describe("FxERC20", function () { // Root token must be owned by the FxERC20RootTunnel contract await rootToken.changeOwner(fxERC20RootTunnel.address); - // Burn tokens on L1 and send message to L2 to retrieve them there - await expect( - fxERC20RootTunnel.withdraw(amount) - ).to.be.reverted; - - // Get the message on L2 - const data = ethers.utils.solidityPack( - ["address", "address", "uint256"], - [deployer.address, deployer.address, amount] - ); - const balanceBefore = await childToken.balanceOf(deployer.address); - // Upon message receive, tokens on L2 are transferred to the destination account (deployer) - await fxERC20ChildTunnel.connect(deployer).processMessageFromRoot(stateId, fxERC20RootTunnel.address, data); + // Burn tokens on L1 and send message to L2 to retrieve them there + await fxERC20RootTunnel.withdraw(amount); + + // Check that bridged tokens were burned + balance = await rootToken.balanceOf(deployer.address); + expect(balance).to.equal(0); // There must be no balance left locked on the FxERC20ChildTunnel contract balance = await childToken.balanceOf(fxERC20ChildTunnel.address); @@ -189,33 +196,26 @@ describe("FxERC20", function () { await rootToken.mint(account.address, amount); // Withdraw tokens - await rootToken.connect(deployer).approve(fxERC20RootTunnel.address, amount); + await rootToken.connect(account).approve(fxERC20RootTunnel.address, amount); // Root token must be owned by the FxERC20RootTunnel contract await rootToken.changeOwner(fxERC20RootTunnel.address); - // Burn tokens on L1 and send message to L2 to retrieve them there - await expect( - fxERC20RootTunnel.withdrawTo(deployer.address, amount) - ).to.be.reverted; - - // Get the message on L2 - const data = ethers.utils.solidityPack( - ["address", "address", "uint256"], - [deployer.address, account.address, amount] - ); + const balanceBefore = await childToken.balanceOf(deployer.address); - const balanceBefore = await childToken.balanceOf(account.address); + // Burn tokens on L1 and send message to L2 to retrieve them there + await fxERC20RootTunnel.connect(account).withdrawTo(deployer.address, amount); - // Upon message receive, tokens on L2 are transferred to the destination account (deployer) - await fxERC20ChildTunnel.connect(deployer).processMessageFromRoot(stateId, fxERC20RootTunnel.address, data); + // Check that bridged tokens were burned + balance = await rootToken.balanceOf(account.address); + expect(balance).to.equal(0); // There must be no balance left locked on the FxERC20ChildTunnel contract balance = await childToken.balanceOf(fxERC20ChildTunnel.address); expect(balance).to.equal(0); // The receiver balance must increase for the amount sent - const balanceAfter = await childToken.balanceOf(account.address); + const balanceAfter = await childToken.balanceOf(deployer.address); const balanceDiff = Number(balanceAfter.sub(balanceBefore)); expect(balanceDiff).to.equal(amount); }); From 630696e29bd31a2df3f13003da805238699f1c0c Mon Sep 17 00:00:00 2001 From: Aleksandr Kuperman Date: Fri, 24 Nov 2023 12:59:29 +0000 Subject: [PATCH 13/16] feat: adding root mock and decreasing the token amount size in the message --- contracts/bridges/FxERC20ChildTunnel.sol | 10 +++++----- contracts/bridges/FxERC20RootTunnel.sol | 10 +++++----- contracts/bridges/test/FxRootMock.sol | 22 ++++++++++++++++++++++ test/FxERC20.js | 2 +- 4 files changed, 33 insertions(+), 11 deletions(-) create mode 100644 contracts/bridges/test/FxRootMock.sol diff --git a/contracts/bridges/FxERC20ChildTunnel.sol b/contracts/bridges/FxERC20ChildTunnel.sol index 16d085a..1a3a233 100644 --- a/contracts/bridges/FxERC20ChildTunnel.sol +++ b/contracts/bridges/FxERC20ChildTunnel.sol @@ -67,15 +67,15 @@ contract FxERC20ChildTunnel is FxBaseChildTunnel { // Decode incoming message from root: (address, address, uint256) address from; address to; - uint256 amount; + uint96 amount; // solhint-disable-next-line no-inline-assembly assembly { // Offset 20 bytes for the address from (160 bits) from := mload(add(message, 20)) // Offset 20 bytes for the address to (160 bits) to := mload(add(message, 40)) - // Offset the data by32 bytes of amount (256 bits) - amount := mload(add(message, 72)) + // Offset 12 bytes of amount (96 bits) + amount := mload(add(message, 52)) } // Transfer decoded amount of tokens to a specified address @@ -96,8 +96,8 @@ contract FxERC20ChildTunnel is FxBaseChildTunnel { // Deposit tokens on an L2 bridge contract (lock) IERC20(childToken).transferFrom(msg.sender, address(this), amount); - // Encode message for root: (address, address, uint256) - bytes memory message = abi.encodePacked(msg.sender, to, amount); + // Encode message for root: (address, address, uint96) + bytes memory message = abi.encodePacked(msg.sender, to, uint96(amount)); // Send message to root _sendMessageToRoot(message); diff --git a/contracts/bridges/FxERC20RootTunnel.sol b/contracts/bridges/FxERC20RootTunnel.sol index 7750e72..3f0b53e 100755 --- a/contracts/bridges/FxERC20RootTunnel.sol +++ b/contracts/bridges/FxERC20RootTunnel.sol @@ -61,15 +61,15 @@ contract FxERC20RootTunnel is FxBaseRootTunnel { // Decode incoming message from child: (address, address, uint256) address from; address to; - uint256 amount; + uint96 amount; // solhint-disable-next-line no-inline-assembly assembly { // Offset 20 bytes for the address from (160 bits) from := mload(add(message, 20)) // Offset 20 bytes for the address to (160 bits) to := mload(add(message, 40)) - // Offset the data by32 bytes of amount (256 bits) - amount := mload(add(message, 72)) + // Offset 12 bytes of amount (96 bits) + amount := mload(add(message, 52)) } // Mints bridged amount of tokens to a specified address @@ -93,8 +93,8 @@ contract FxERC20RootTunnel is FxBaseRootTunnel { // Burn bridged tokens IERC20(rootToken).burn(amount); - // Encode message for child: (address, address, uint256) - bytes memory message = abi.encodePacked(msg.sender, to, amount); + // Encode message for child: (address, address, uint96) + bytes memory message = abi.encodePacked(msg.sender, to, uint96(amount)); // Send message to child _sendMessageToChild(message); diff --git a/contracts/bridges/test/FxRootMock.sol b/contracts/bridges/test/FxRootMock.sol new file mode 100644 index 0000000..c0177c0 --- /dev/null +++ b/contracts/bridges/test/FxRootMock.sol @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.21; + +interface IFxERC20ChildTunnel { + function processMessageFromRoot(uint256 stateId, address rootMessageSender, bytes calldata data) external; +} + +/// @title FxRootMock - Root mock contract for fx-portal +contract FxRootMock { + address public fxERC20RootTunnel; + + function setRootTunnel(address _fxERC20RootTunnel) external { + fxERC20RootTunnel = _fxERC20RootTunnel; + } + + /// @dev Mock of the send message to child. + /// @param _receiver FxERC20RootTunnel contract address. + /// @param _data Message to send to L2. + function sendMessageToChild(address _receiver, bytes calldata _data) external { + IFxERC20ChildTunnel(_receiver).processMessageFromRoot(0, fxERC20RootTunnel, _data); + } +} diff --git a/test/FxERC20.js b/test/FxERC20.js index 554da0c..0d4116e 100644 --- a/test/FxERC20.js +++ b/test/FxERC20.js @@ -3,7 +3,7 @@ const { expect } = require("chai"); const { ethers } = require("hardhat"); -describe("FxERC20", function () { +describe.("FxERC20", function () { let fxERC20RootTunnel; let fxERC20ChildTunnel; let childToken; From 63820a9fc4a287359d7c1c1a443a5b47e3932b2f Mon Sep 17 00:00:00 2001 From: Aleksandr Kuperman Date: Fri, 24 Nov 2023 13:03:13 +0000 Subject: [PATCH 14/16] chore: adding comments --- contracts/bridges/FxERC20ChildTunnel.sol | 3 ++- contracts/bridges/FxERC20RootTunnel.sol | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/contracts/bridges/FxERC20ChildTunnel.sol b/contracts/bridges/FxERC20ChildTunnel.sol index 1a3a233..68bb5f1 100644 --- a/contracts/bridges/FxERC20ChildTunnel.sol +++ b/contracts/bridges/FxERC20ChildTunnel.sol @@ -64,9 +64,10 @@ contract FxERC20ChildTunnel is FxBaseChildTunnel { address sender, bytes memory message ) internal override validateSender(sender) { - // Decode incoming message from root: (address, address, uint256) + // Decode incoming message from root: (address, address, uint96) address from; address to; + // The token amount is limited to be no bigger than 2^96 - 1 uint96 amount; // solhint-disable-next-line no-inline-assembly assembly { diff --git a/contracts/bridges/FxERC20RootTunnel.sol b/contracts/bridges/FxERC20RootTunnel.sol index 3f0b53e..f482e7c 100755 --- a/contracts/bridges/FxERC20RootTunnel.sol +++ b/contracts/bridges/FxERC20RootTunnel.sol @@ -58,9 +58,10 @@ contract FxERC20RootTunnel is FxBaseRootTunnel { /// @dev Receives the token message from L2 and transfers bridged tokens to a specified address. /// @param message Incoming bridge message. function _processMessageFromChild(bytes memory message) internal override { - // Decode incoming message from child: (address, address, uint256) + // Decode incoming message from child: (address, address, uint96) address from; address to; + // The token amount is limited to be no bigger than 2^96 - 1 uint96 amount; // solhint-disable-next-line no-inline-assembly assembly { From 0d2c8b2b709202187ad188f03449dc7fe504b085 Mon Sep 17 00:00:00 2001 From: Aleksandr Kuperman Date: Fri, 24 Nov 2023 13:13:38 +0000 Subject: [PATCH 15/16] chore: linter --- test/FxERC20.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/FxERC20.js b/test/FxERC20.js index 0d4116e..554da0c 100644 --- a/test/FxERC20.js +++ b/test/FxERC20.js @@ -3,7 +3,7 @@ const { expect } = require("chai"); const { ethers } = require("hardhat"); -describe.("FxERC20", function () { +describe("FxERC20", function () { let fxERC20RootTunnel; let fxERC20ChildTunnel; let childToken; From ad52fe3e52c52431723df3120312f4b90712bc86 Mon Sep 17 00:00:00 2001 From: Aleksandr Kuperman Date: Fri, 24 Nov 2023 18:05:40 +0000 Subject: [PATCH 16/16] test: adding to coverage --- contracts/bridges/FxERC20ChildTunnel.sol | 2 +- contracts/bridges/FxERC20RootTunnel.sol | 5 +++ test/FxERC20.js | 46 ++++++++++++++++++++++++ 3 files changed, 52 insertions(+), 1 deletion(-) diff --git a/contracts/bridges/FxERC20ChildTunnel.sol b/contracts/bridges/FxERC20ChildTunnel.sol index 68bb5f1..82a48fc 100644 --- a/contracts/bridges/FxERC20ChildTunnel.sol +++ b/contracts/bridges/FxERC20ChildTunnel.sol @@ -48,7 +48,7 @@ contract FxERC20ChildTunnel is FxBaseChildTunnel { /// @param to Destination address on L1. /// @param amount Token amount to be deposited. function depositTo(address to, uint256 amount) external { - // Check for the address to send tokens to + // Check for the address to deposit tokens to if (to == address(0)) { revert ZeroAddress(); } diff --git a/contracts/bridges/FxERC20RootTunnel.sol b/contracts/bridges/FxERC20RootTunnel.sol index f482e7c..6fd8724 100755 --- a/contracts/bridges/FxERC20RootTunnel.sol +++ b/contracts/bridges/FxERC20RootTunnel.sol @@ -52,6 +52,11 @@ contract FxERC20RootTunnel is FxBaseRootTunnel { /// @param to Destination address on L2. /// @param amount Token amount to be withdrawn. function withdrawTo(address to, uint256 amount) external { + // Check for the address to withdraw tokens to + if (to == address(0)) { + revert ZeroAddress(); + } + _withdraw(to, amount); } diff --git a/test/FxERC20.js b/test/FxERC20.js index 554da0c..f26451b 100644 --- a/test/FxERC20.js +++ b/test/FxERC20.js @@ -88,6 +88,30 @@ describe("FxERC20", function () { FxERC20ChildTunnel.deploy(signers[1].address, signers[1].address, AddressZero) ).to.be.revertedWithCustomError(fxERC20ChildTunnel, "ZeroAddress"); }); + + it("Bridging ownership", async function () { + const account = signers[1]; + + // Try to change the owner not by the owner + await expect( + rootToken.connect(account).changeOwner(account.address) + ).to.be.revertedWithCustomError(rootToken, "OwnerOnly"); + + // Try to change the owner with a zero address + await expect( + rootToken.connect(deployer).changeOwner(AddressZero) + ).to.be.revertedWithCustomError(rootToken, "ZeroAddress"); + + // Try to mint not by the owner + await expect( + rootToken.connect(account).mint(account.address, initMint) + ).to.be.revertedWithCustomError(rootToken, "OwnerOnly"); + + // Try to burn not by the owner + await expect( + rootToken.connect(account).burn(initMint) + ).to.be.revertedWithCustomError(rootToken, "OwnerOnly"); + }); }); context("Deposit and withdraw ERC20 tokens", async function () { @@ -108,6 +132,28 @@ describe("FxERC20", function () { ).to.be.revertedWith("FxBaseChildTunnel: INVALID_SENDER_FROM_ROOT"); }); + it("Deposit or withdraw zero amount or to zero addresses", async function () { + // Try to deposit zero amounts + await expect( + fxERC20ChildTunnel.connect(deployer).deposit(0) + ).to.be.revertedWithCustomError(fxERC20ChildTunnel, "ZeroValue"); + + // Try to deposit to a zero address + await expect( + fxERC20ChildTunnel.connect(deployer).depositTo(AddressZero, amount) + ).to.be.revertedWithCustomError(fxERC20ChildTunnel, "ZeroAddress"); + + // Try to withdraw a zero amount + await expect( + fxERC20RootTunnel.connect(deployer).withdraw(0) + ).to.be.revertedWithCustomError(fxERC20RootTunnel, "ZeroValue"); + + // Try to deposit to a zero address + await expect( + fxERC20RootTunnel.connect(deployer).withdrawTo(AddressZero, amount) + ).to.be.revertedWithCustomError(fxERC20RootTunnel, "ZeroAddress"); + }); + it("Deposit tokens", async function () { // Approve tokens await childToken.approve(fxERC20ChildTunnel.address, amount);