diff --git a/.eslintignore b/.eslintignore index 3f2f531..c6af692 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,2 +1,3 @@ coverage* audit/* +lib/* 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/.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/BridgedERC20.sol b/contracts/bridges/BridgedERC20.sol new file mode 100644 index 0000000..459fe98 --- /dev/null +++ b/contracts/bridges/BridgedERC20.sol @@ -0,0 +1,67 @@ +// 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 +/// @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(string memory _name, string memory _symbol) ERC20(_name, symbol, 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/contracts/bridges/FxERC20ChildTunnel.sol b/contracts/bridges/FxERC20ChildTunnel.sol new file mode 100644 index 0000000..82a48fc --- /dev/null +++ b/contracts/bridges/FxERC20ChildTunnel.sol @@ -0,0 +1,107 @@ +// 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"; + +/// @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 address + address public immutable childToken; + // Root token address + address public immutable rootToken; + + /// @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 deposit 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 message. + function _processMessageFromRoot( + uint256 /* stateId */, + address sender, + bytes memory message + ) internal override validateSender(sender) { + // 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 { + // 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 12 bytes of amount (96 bits) + amount := mload(add(message, 52)) + } + + // 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 { + // 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); + + // Encode message for root: (address, address, uint96) + bytes memory message = abi.encodePacked(msg.sender, to, uint96(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 new file mode 100755 index 0000000..6fd8724 --- /dev/null +++ b/contracts/bridges/FxERC20RootTunnel.sol @@ -0,0 +1,109 @@ +// 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"; + +/// @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 address + address public immutable childToken; + // 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 { + // Check for the address to withdraw tokens to + if (to == address(0)) { + revert ZeroAddress(); + } + + _withdraw(to, amount); + } + + /// @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, 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 { + // 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 12 bytes of amount (96 bits) + amount := mload(add(message, 52)) + } + + // 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 { + // 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 bridged tokens + IERC20(rootToken).burn(amount); + + // Encode message for child: (address, address, uint96) + bytes memory message = abi.encodePacked(msg.sender, to, uint96(amount)); + // Send message to child + _sendMessageToChild(message); + + emit FxWithdrawERC20(rootToken, childToken, msg.sender, to, amount); + } +} 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/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 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..a63cf42 --- /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..1aad9a4 --- /dev/null +++ b/scripts/deployment/deploy_19_bridged_erc20.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("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(); + //const bridgedERC20 = await BridgedERC20.connect(EOA).deploy("ERC20 bridged token", "BridgedERC20"); + 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 diff --git a/test/FxERC20.js b/test/FxERC20.js new file mode 100644 index 0000000..f26451b --- /dev/null +++ b/test/FxERC20.js @@ -0,0 +1,269 @@ +/*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 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, 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"); + // 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 + 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"); + }); + + 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 () { + it("Should fail when trying to call from incorrect contract addresses", async function () { + // 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"); + + // 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( + testChildTunnel.connect(deployer).processMessageFromRoot(stateId, deployer.address, "0x") + ).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); + + // 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); + + const balanceBefore = await childToken.balanceOf(deployer.address); + + // 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); + 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 () { + 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(account).approve(fxERC20RootTunnel.address, amount); + + // Root token must be owned by the FxERC20RootTunnel contract + await rootToken.changeOwner(fxERC20RootTunnel.address); + + const balanceBefore = await childToken.balanceOf(deployer.address); + + // Burn tokens on L1 and send message to L2 to retrieve them there + await fxERC20RootTunnel.connect(account).withdrawTo(deployer.address, amount); + + // 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(deployer.address); + const balanceDiff = Number(balanceAfter.sub(balanceBefore)); + expect(balanceDiff).to.equal(amount); + }); + }); +});