From 23778750fb0b8734e70368aaaa6e6cf5927d409e Mon Sep 17 00:00:00 2001 From: Matt Rice Date: Fri, 25 Feb 2022 17:53:26 -0500 Subject: [PATCH 01/11] WIP Signed-off-by: Matt Rice --- contracts/Polygon_SpokePool.sol | 35 ++++++++++++++++++++ contracts/chain-adapters/Polygon_Adapter.sol | 0 2 files changed, 35 insertions(+) create mode 100644 contracts/Polygon_SpokePool.sol create mode 100644 contracts/chain-adapters/Polygon_Adapter.sol diff --git a/contracts/Polygon_SpokePool.sol b/contracts/Polygon_SpokePool.sol new file mode 100644 index 000000000..ef288ce8a --- /dev/null +++ b/contracts/Polygon_SpokePool.sol @@ -0,0 +1,35 @@ +//SPDX-License-Identifier: Unlicense +pragma solidity ^0.8.0; + +import "./interfaces/WETH9.sol"; + +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "./SpokePool.sol"; +import "./SpokePoolInterface.sol"; + +interface PolygonIERC20 is IERC20 { + function withdraw(uint256 amount) external; +} + +/** + * @notice Polygon specific SpokePool. + */ +contract Polygon_SpokePool is SpokePoolInterface, SpokePool { + event PolygonTokensBridged(address indexed token, address indexed receiver, uint256 amount); + + constructor( + address _crossDomainAdmin, + address _hubPool, + address timerAddress + ) SpokePool(_crossDomainAdmin, _hubPool, 0x4200000000000000000000000000000000000006, timerAddress) {} + + /************************************** + * INTERNAL FUNCTIONS * + **************************************/ + + function _bridgeTokensToHubPool(RelayerRefundLeaf memory relayerRefundLeaf) internal override { + PolygonIERC20(relayerRefundLeaf.l2TokenAddress).withdraw(relayerRefundLeaf.amountToReturn); + + emit OptimismTokensBridged(relayerRefundLeaf.l2TokenAddress, address(this), relayerRefundLeaf.amountToReturn); + } +} diff --git a/contracts/chain-adapters/Polygon_Adapter.sol b/contracts/chain-adapters/Polygon_Adapter.sol new file mode 100644 index 000000000..e69de29bb From 945e063c21d3d764d635de76760b0f22643abcd6 Mon Sep 17 00:00:00 2001 From: Matt Rice Date: Sun, 27 Feb 2022 16:31:56 -0500 Subject: [PATCH 02/11] WIP Signed-off-by: Matt Rice --- contracts/FxBaseChildTunnel.sol | 75 ++++++++ contracts/Polygon_SpokePool.sol | 22 ++- .../chain-adapters/PolygonTokenBridger.sol | 38 ++++ contracts/chain-adapters/Polygon_Adapter.sol | 70 +++++++ contracts/polygon/FxBaseChildTunnel.sol | 75 ++++++++ contracts/polygon/FxBaseRootTunnel.sol | 180 ++++++++++++++++++ 6 files changed, 457 insertions(+), 3 deletions(-) create mode 100644 contracts/FxBaseChildTunnel.sol create mode 100644 contracts/chain-adapters/PolygonTokenBridger.sol create mode 100644 contracts/polygon/FxBaseChildTunnel.sol create mode 100644 contracts/polygon/FxBaseRootTunnel.sol diff --git a/contracts/FxBaseChildTunnel.sol b/contracts/FxBaseChildTunnel.sol new file mode 100644 index 000000000..a23608b6f --- /dev/null +++ b/contracts/FxBaseChildTunnel.sol @@ -0,0 +1,75 @@ +// SPDX-License-Identifier: MIT +// Copied with no modifications from Polygon demo FxTunnel repo: https://github.com/jdkanani/fx-portal +// except bumping version from 0.7.3 --> 0.8 +pragma solidity ^0.8.0; + +// IFxMessageProcessor represents interface to process message +interface IFxMessageProcessor { + function processMessageFromRoot( + uint256 stateId, + address rootMessageSender, + bytes calldata data + ) external; +} + +/** + * @notice Mock child tunnel contract to receive and send message from L2 + */ +abstract contract FxBaseChildTunnel is IFxMessageProcessor { + // MessageTunnel on L1 will get data from this event + event MessageSent(bytes message); + + // fx child + address public immutable fxChild; + + // fx root tunnel + address public immutable fxRootTunnel; + + constructor(address _fxChild, address _fxRootTunnel) { + fxChild = _fxChild; + fxRootTunnel = _fxRootTunnel; + } + + // Sender must be fxRootTunnel. + modifier validateSender(address sender) { + require(sender == fxRootTunnel, "FxBaseChildTunnel: INVALID_SENDER_FROM_ROOT"); + _; + } + + function processMessageFromRoot( + uint256 stateId, + address rootMessageSender, + bytes calldata data + ) public override { + require(msg.sender == fxChild, "FxBaseChildTunnel: INVALID_SENDER"); + _processMessageFromRoot(stateId, rootMessageSender, data); + } + + /** + * @notice Emit message that can be received on Root Tunnel + * @dev Call the internal function when need to emit message + * @param message bytes message that will be sent to Root Tunnel + * some message examples - + * abi.encode(tokenId); + * abi.encode(tokenId, tokenMetadata); + * abi.encode(messageType, messageData); + */ + function _sendMessageToRoot(bytes memory message) internal { + emit MessageSent(message); + } + + /** + * @notice Process message received from Root Tunnel + * @dev function needs to be implemented to handle message as per requirement + * This is called by onStateReceive function. + * Since it is called via a system call, any event will not be emitted during its execution. + * @param stateId unique state id + * @param sender root message sender + * @param message bytes message that was sent from Root Tunnel + */ + function _processMessageFromRoot( + uint256 stateId, + address sender, + bytes memory message + ) internal virtual; +} diff --git a/contracts/Polygon_SpokePool.sol b/contracts/Polygon_SpokePool.sol index ef288ce8a..01c3f75d0 100644 --- a/contracts/Polygon_SpokePool.sol +++ b/contracts/Polygon_SpokePool.sol @@ -6,6 +6,7 @@ import "./interfaces/WETH9.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "./SpokePool.sol"; import "./SpokePoolInterface.sol"; +import "./FxBaseChildTunnel.sol"; interface PolygonIERC20 is IERC20 { function withdraw(uint256 amount) external; @@ -14,14 +15,27 @@ interface PolygonIERC20 is IERC20 { /** * @notice Polygon specific SpokePool. */ -contract Polygon_SpokePool is SpokePoolInterface, SpokePool { +contract Polygon_SpokePool is SpokePoolInterface, SpokePool, FxBaseChildTunnel { + + address public fxChild; + event PolygonTokensBridged(address indexed token, address indexed receiver, uint256 amount); constructor( address _crossDomainAdmin, address _hubPool, address timerAddress - ) SpokePool(_crossDomainAdmin, _hubPool, 0x4200000000000000000000000000000000000006, timerAddress) {} + ) SpokePool(_crossDomainAdmin, _hubPool, 0x4200000000000000000000000000000000000006, timerAddress) FxBaseChildTunnel(, _crossDomainAdmin) {} + + + function processMessageFromRoot( + uint256 stateId, + address rootMessageSender, + bytes calldata data + ) public override { + require(msg.sender == fxChild, "FxBaseChildTunnel: INVALID_SENDER"); + _processMessageFromRoot(stateId, rootMessageSender, data); + } /************************************** * INTERNAL FUNCTIONS * @@ -30,6 +44,8 @@ contract Polygon_SpokePool is SpokePoolInterface, SpokePool { function _bridgeTokensToHubPool(RelayerRefundLeaf memory relayerRefundLeaf) internal override { PolygonIERC20(relayerRefundLeaf.l2TokenAddress).withdraw(relayerRefundLeaf.amountToReturn); - emit OptimismTokensBridged(relayerRefundLeaf.l2TokenAddress, address(this), relayerRefundLeaf.amountToReturn); + emit PolygonTokensBridged(relayerRefundLeaf.l2TokenAddress, address(this), relayerRefundLeaf.amountToReturn); } + + function } diff --git a/contracts/chain-adapters/PolygonTokenBridger.sol b/contracts/chain-adapters/PolygonTokenBridger.sol new file mode 100644 index 000000000..51d09a7d5 --- /dev/null +++ b/contracts/chain-adapters/PolygonTokenBridger.sol @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity ^0.8.0; + +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import "../Lockable.sol"; + + +interface PolygonIERC20 is IERC20 { + function withdraw(uint256 amount) external; +} + +// Because Polygon only allows withdrawals from a particular address to go to that same address on mainnet, we need to +// have some sort of contract that can guarantee identical addresses on Polygon and Ethereum. +// Note: this contract is intended to be completely immutable, so it's guaranteed that the contract on each side is +// configured identically as long as it is created via create2. +contract PolygonTokenBridger is Lockable { + using SafeERC20 for PolygonIERC20; + using SafeERC20 for IERC20; + + address public immutable destination; + constructor( + address _destination + ) { + destination = _destination; + } + + // Polygon side. + function send(PolygonIERC20 token, uint256 amount) public nonReentrant { + token.safeTransferFrom(msg.sender, address(this), amount); + token.withdraw(amount); + } + + // Mainnet side. + function retrieve(IERC20 token) public nonReentrant { + token.safeTransfer(destination, token.balanceOf(address(this))); + } +} diff --git a/contracts/chain-adapters/Polygon_Adapter.sol b/contracts/chain-adapters/Polygon_Adapter.sol index e69de29bb..a0e1e078c 100644 --- a/contracts/chain-adapters/Polygon_Adapter.sol +++ b/contracts/chain-adapters/Polygon_Adapter.sol @@ -0,0 +1,70 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity ^0.8.0; + +import "./Base_Adapter.sol"; +import "../interfaces/AdapterInterface.sol"; +import "../interfaces/WETH9.sol"; + +import "@eth-optimism/contracts/libraries/bridge/CrossDomainEnabled.sol"; +import "@eth-optimism/contracts/L1/messaging/IL1StandardBridge.sol"; +import "@uma/core/contracts/common/implementation/Lockable.sol"; +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; + +interface IRootChainManager { + function depositEtherFor(address user) external payable; + function depositFor(address user,address rootToken,bytes calldata depositData) external; +} + +interface IFxStateSender { + function sendMessageToChild(address _receiver, bytes calldata _data) external; +} + +/** + * @notice Sends cross chain messages Optimism L2 network. + * @dev This contract's owner should be set to the some multisig or admin contract. The Owner can simply set the L2 gas + * and the HubPool. The HubPool is the only contract that can relay tokens and messages over the bridge. + */ +contract Polygon_Adapter is Base_Adapter, Lockable { + using SafeERC20 for IERC20; + IRootChainManager public rootChainManager; + IFxStateSender public fxStateSender; + WETH9 public l1Weth; + + event TokensRelayedToPolygon(address indexed l1Token, address indexed l2Token, uint256 amount, address indexed to); + + constructor( + address _hubPool, + IRootChainManager _rootChainManager, + IFxStateSender _fxStateSender, + WETH9 _l1Weth + ) Base_Adapter(_hubPool) { + rootChainManager = _rootChainManager; + fxStateSender = _fxStateSender; + l1Weth = _l1Weth; + } + + function relayMessage(address target, bytes memory message) external payable override nonReentrant onlyHubPool { + fxStateSender.sendMessageToChild(target, message); + } + + function relayTokens( + address l1Token, + address l2Token, + uint256 amount, + address to + ) external payable override nonReentrant onlyHubPool { + // If the l1Token is weth then unwrap it to ETH then send the ETH to the standard bridge. + if (l1Token == address(l1Weth)) { + l1Weth.withdraw(amount); + rootChainManager.depositEtherFor{ value: amount }(to); + } else { + IERC20(l1Token).safeIncreaseAllowance(address(rootChainManager), amount); + rootChainManager.depositFor(to, l1Token, abi.encode(amount)); + } + emit TokensRelayedToPolygon(l1Token, l2Token, amount, to); + } + + // Added to enable the Optimism_Adapter to receive ETH. used when unwrapping WETH. + receive() external payable {} +} diff --git a/contracts/polygon/FxBaseChildTunnel.sol b/contracts/polygon/FxBaseChildTunnel.sol new file mode 100644 index 000000000..a23608b6f --- /dev/null +++ b/contracts/polygon/FxBaseChildTunnel.sol @@ -0,0 +1,75 @@ +// SPDX-License-Identifier: MIT +// Copied with no modifications from Polygon demo FxTunnel repo: https://github.com/jdkanani/fx-portal +// except bumping version from 0.7.3 --> 0.8 +pragma solidity ^0.8.0; + +// IFxMessageProcessor represents interface to process message +interface IFxMessageProcessor { + function processMessageFromRoot( + uint256 stateId, + address rootMessageSender, + bytes calldata data + ) external; +} + +/** + * @notice Mock child tunnel contract to receive and send message from L2 + */ +abstract contract FxBaseChildTunnel is IFxMessageProcessor { + // MessageTunnel on L1 will get data from this event + event MessageSent(bytes message); + + // fx child + address public immutable fxChild; + + // fx root tunnel + address public immutable fxRootTunnel; + + constructor(address _fxChild, address _fxRootTunnel) { + fxChild = _fxChild; + fxRootTunnel = _fxRootTunnel; + } + + // Sender must be fxRootTunnel. + modifier validateSender(address sender) { + require(sender == fxRootTunnel, "FxBaseChildTunnel: INVALID_SENDER_FROM_ROOT"); + _; + } + + function processMessageFromRoot( + uint256 stateId, + address rootMessageSender, + bytes calldata data + ) public override { + require(msg.sender == fxChild, "FxBaseChildTunnel: INVALID_SENDER"); + _processMessageFromRoot(stateId, rootMessageSender, data); + } + + /** + * @notice Emit message that can be received on Root Tunnel + * @dev Call the internal function when need to emit message + * @param message bytes message that will be sent to Root Tunnel + * some message examples - + * abi.encode(tokenId); + * abi.encode(tokenId, tokenMetadata); + * abi.encode(messageType, messageData); + */ + function _sendMessageToRoot(bytes memory message) internal { + emit MessageSent(message); + } + + /** + * @notice Process message received from Root Tunnel + * @dev function needs to be implemented to handle message as per requirement + * This is called by onStateReceive function. + * Since it is called via a system call, any event will not be emitted during its execution. + * @param stateId unique state id + * @param sender root message sender + * @param message bytes message that was sent from Root Tunnel + */ + function _processMessageFromRoot( + uint256 stateId, + address sender, + bytes memory message + ) internal virtual; +} diff --git a/contracts/polygon/FxBaseRootTunnel.sol b/contracts/polygon/FxBaseRootTunnel.sol new file mode 100644 index 000000000..b2cfb7fdc --- /dev/null +++ b/contracts/polygon/FxBaseRootTunnel.sol @@ -0,0 +1,180 @@ +// SPDX-License-Identifier: MIT +// Copied with no modifications from Polygon demo FxTunnel repo: https://github.com/jdkanani/fx-portal +// except bumping version from 0.7.3 --> 0.8 +pragma solidity ^0.8.0; + +import "@uma/core/contracts/external/polygon/lib/RLPReader.sol"; +import "@uma/core/contracts/external/polygon/lib/MerklePatriciaProof.sol"; +import "@uma/core/contracts/external/polygon/lib/Merkle.sol"; + +interface IFxStateSender { + function sendMessageToChild(address _receiver, bytes calldata _data) external; +} + +contract ICheckpointManager { + struct HeaderBlock { + bytes32 root; + uint256 start; + uint256 end; + uint256 createdAt; + address proposer; + } + + /** + * @notice mapping of checkpoint header numbers to block details + * @dev These checkpoints are submited by plasma contracts + */ + mapping(uint256 => HeaderBlock) public headerBlocks; +} + +abstract contract FxBaseRootTunnel { + using RLPReader for bytes; + using RLPReader for RLPReader.RLPItem; + using Merkle for bytes32; + + // keccak256(MessageSent(bytes)) + bytes32 public constant SEND_MESSAGE_EVENT_SIG = 0x8c5261668696ce22758910d05bab8f186d6eb247ceac2af2e82c7dc17669b036; + + // state sender contract + IFxStateSender public immutable fxRoot; + // root chain manager + ICheckpointManager public immutable checkpointManager; + // child tunnel contract which receives and sends messages + address public immutable fxChildTunnel; + + // storage to avoid duplicate exits + mapping(bytes32 => bool) public processedExits; + + constructor(address _checkpointManager, address _fxRoot, address _fxChildTunnel) { + checkpointManager = ICheckpointManager(_checkpointManager); + fxRoot = IFxStateSender(_fxRoot); + fxChildTunnel = _fxChildTunnel; + } + + /** + * @notice Send bytes message to Child Tunnel + * @param message bytes message that will be sent to Child Tunnel + * some message examples - + * abi.encode(tokenId); + * abi.encode(tokenId, tokenMetadata); + * abi.encode(messageType, messageData); + */ + function _sendMessageToChild(bytes memory message) internal { + fxRoot.sendMessageToChild(fxChildTunnel, message); + } + + function _validateAndExtractMessage(bytes memory inputData) internal returns (bytes memory) { + RLPReader.RLPItem[] memory inputDataRLPList = inputData.toRlpItem().toList(); + + // checking if exit has already been processed + // unique exit is identified using hash of (blockNumber, branchMask, receiptLogIndex) + bytes32 exitHash = + keccak256( + abi.encodePacked( + inputDataRLPList[2].toUint(), // blockNumber + // first 2 nibbles are dropped while generating nibble array + // this allows branch masks that are valid but bypass exitHash check (changing first 2 nibbles only) + // so converting to nibble array and then hashing it + MerklePatriciaProof._getNibbleArray(inputDataRLPList[8].toBytes()), // branchMask + inputDataRLPList[9].toUint() // receiptLogIndex + ) + ); + require(processedExits[exitHash] == false, "FxRootTunnel: EXIT_ALREADY_PROCESSED"); + processedExits[exitHash] = true; + + RLPReader.RLPItem[] memory receiptRLPList = inputDataRLPList[6].toBytes().toRlpItem().toList(); + RLPReader.RLPItem memory logRLP = + receiptRLPList[3].toList()[ + inputDataRLPList[9].toUint() // receiptLogIndex + ]; + + RLPReader.RLPItem[] memory logRLPList = logRLP.toList(); + + // check child tunnel + require(fxChildTunnel == RLPReader.toAddress(logRLPList[0]), "FxRootTunnel: INVALID_FX_CHILD_TUNNEL"); + + // verify receipt inclusion + require( + MerklePatriciaProof.verify( + inputDataRLPList[6].toBytes(), // receipt + inputDataRLPList[8].toBytes(), // branchMask + inputDataRLPList[7].toBytes(), // receiptProof + bytes32(inputDataRLPList[5].toUint()) // receiptRoot + ), + "FxRootTunnel: INVALID_RECEIPT_PROOF" + ); + + // verify checkpoint inclusion + _checkBlockMembershipInCheckpoint( + inputDataRLPList[2].toUint(), // blockNumber + inputDataRLPList[3].toUint(), // blockTime + bytes32(inputDataRLPList[4].toUint()), // txRoot + bytes32(inputDataRLPList[5].toUint()), // receiptRoot + inputDataRLPList[0].toUint(), // headerNumber + inputDataRLPList[1].toBytes() // blockProof + ); + + RLPReader.RLPItem[] memory logTopicRLPList = logRLPList[1].toList(); // topics + + require( + bytes32(logTopicRLPList[0].toUint()) == SEND_MESSAGE_EVENT_SIG, // topic0 is event sig + "FxRootTunnel: INVALID_SIGNATURE" + ); + + // received message data + bytes memory receivedData = logRLPList[2].toBytes(); + bytes memory message = abi.decode(receivedData, (bytes)); // event decodes params again, so decoding bytes to get message + return message; + } + + function _checkBlockMembershipInCheckpoint( + uint256 blockNumber, + uint256 blockTime, + bytes32 txRoot, + bytes32 receiptRoot, + uint256 headerNumber, + bytes memory blockProof + ) private view returns (uint256) { + (bytes32 headerRoot, uint256 startBlock, , uint256 createdAt, ) = checkpointManager.headerBlocks(headerNumber); + + require( + keccak256(abi.encodePacked(blockNumber, blockTime, txRoot, receiptRoot)).checkMembership( + blockNumber - startBlock, + headerRoot, + blockProof + ), + "FxRootTunnel: INVALID_HEADER" + ); + return createdAt; + } + + /** + * @notice receive message from L2 to L1, validated by proof + * @dev This function verifies if the transaction actually happened on child chain + * + * @param inputData RLP encoded data of the reference tx containing following list of fields + * 0 - headerNumber - Checkpoint header block number containing the reference tx + * 1 - blockProof - Proof that the block header (in the child chain) is a leaf in the submitted merkle root + * 2 - blockNumber - Block number containing the reference tx on child chain + * 3 - blockTime - Reference tx block time + * 4 - txRoot - Transactions root of block + * 5 - receiptRoot - Receipts root of block + * 6 - receipt - Receipt of the reference transaction + * 7 - receiptProof - Merkle proof of the reference receipt + * 8 - branchMask - 32 bits denoting the path of receipt in merkle tree + * 9 - receiptLogIndex - Log Index to read from the receipt + */ + function receiveMessage(bytes memory inputData) public virtual { + bytes memory message = _validateAndExtractMessage(inputData); + _processMessageFromChild(message); + } + + /** + * @notice Process message received from Child Tunnel + * @dev function needs to be implemented to handle message as per requirement + * This is called by onStateReceive function. + * Since it is called via a system call, any event will not be emitted during its execution. + * @param message bytes message that was sent from Child Tunnel + */ + function _processMessageFromChild(bytes memory message) internal virtual; +} From f9ce434528bd15a0649bcc271622f8213344ea04 Mon Sep 17 00:00:00 2001 From: Matt Rice Date: Sun, 27 Feb 2022 17:02:06 -0500 Subject: [PATCH 03/11] WIP Signed-off-by: Matt Rice --- contracts/FxBaseChildTunnel.sol | 75 -------- .../PolygonTokenBridger.sol | 6 +- contracts/Polygon_SpokePool.sol | 54 ++++-- contracts/chain-adapters/Polygon_Adapter.sol | 12 +- contracts/polygon/FxBaseChildTunnel.sol | 75 -------- contracts/polygon/FxBaseRootTunnel.sol | 180 ------------------ 6 files changed, 52 insertions(+), 350 deletions(-) delete mode 100644 contracts/FxBaseChildTunnel.sol rename contracts/{chain-adapters => }/PolygonTokenBridger.sol (95%) delete mode 100644 contracts/polygon/FxBaseChildTunnel.sol delete mode 100644 contracts/polygon/FxBaseRootTunnel.sol diff --git a/contracts/FxBaseChildTunnel.sol b/contracts/FxBaseChildTunnel.sol deleted file mode 100644 index a23608b6f..000000000 --- a/contracts/FxBaseChildTunnel.sol +++ /dev/null @@ -1,75 +0,0 @@ -// SPDX-License-Identifier: MIT -// Copied with no modifications from Polygon demo FxTunnel repo: https://github.com/jdkanani/fx-portal -// except bumping version from 0.7.3 --> 0.8 -pragma solidity ^0.8.0; - -// IFxMessageProcessor represents interface to process message -interface IFxMessageProcessor { - function processMessageFromRoot( - uint256 stateId, - address rootMessageSender, - bytes calldata data - ) external; -} - -/** - * @notice Mock child tunnel contract to receive and send message from L2 - */ -abstract contract FxBaseChildTunnel is IFxMessageProcessor { - // MessageTunnel on L1 will get data from this event - event MessageSent(bytes message); - - // fx child - address public immutable fxChild; - - // fx root tunnel - address public immutable fxRootTunnel; - - constructor(address _fxChild, address _fxRootTunnel) { - fxChild = _fxChild; - fxRootTunnel = _fxRootTunnel; - } - - // Sender must be fxRootTunnel. - modifier validateSender(address sender) { - require(sender == fxRootTunnel, "FxBaseChildTunnel: INVALID_SENDER_FROM_ROOT"); - _; - } - - function processMessageFromRoot( - uint256 stateId, - address rootMessageSender, - bytes calldata data - ) public override { - require(msg.sender == fxChild, "FxBaseChildTunnel: INVALID_SENDER"); - _processMessageFromRoot(stateId, rootMessageSender, data); - } - - /** - * @notice Emit message that can be received on Root Tunnel - * @dev Call the internal function when need to emit message - * @param message bytes message that will be sent to Root Tunnel - * some message examples - - * abi.encode(tokenId); - * abi.encode(tokenId, tokenMetadata); - * abi.encode(messageType, messageData); - */ - function _sendMessageToRoot(bytes memory message) internal { - emit MessageSent(message); - } - - /** - * @notice Process message received from Root Tunnel - * @dev function needs to be implemented to handle message as per requirement - * This is called by onStateReceive function. - * Since it is called via a system call, any event will not be emitted during its execution. - * @param stateId unique state id - * @param sender root message sender - * @param message bytes message that was sent from Root Tunnel - */ - function _processMessageFromRoot( - uint256 stateId, - address sender, - bytes memory message - ) internal virtual; -} diff --git a/contracts/chain-adapters/PolygonTokenBridger.sol b/contracts/PolygonTokenBridger.sol similarity index 95% rename from contracts/chain-adapters/PolygonTokenBridger.sol rename to contracts/PolygonTokenBridger.sol index 51d09a7d5..9ab9ab757 100644 --- a/contracts/chain-adapters/PolygonTokenBridger.sol +++ b/contracts/PolygonTokenBridger.sol @@ -5,7 +5,6 @@ import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import "../Lockable.sol"; - interface PolygonIERC20 is IERC20 { function withdraw(uint256 amount) external; } @@ -19,9 +18,8 @@ contract PolygonTokenBridger is Lockable { using SafeERC20 for IERC20; address public immutable destination; - constructor( - address _destination - ) { + + constructor(address _destination) { destination = _destination; } diff --git a/contracts/Polygon_SpokePool.sol b/contracts/Polygon_SpokePool.sol index 01c3f75d0..44a9c946e 100644 --- a/contracts/Polygon_SpokePool.sol +++ b/contracts/Polygon_SpokePool.sol @@ -2,39 +2,66 @@ pragma solidity ^0.8.0; import "./interfaces/WETH9.sol"; - import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import "./SpokePool.sol"; import "./SpokePoolInterface.sol"; -import "./FxBaseChildTunnel.sol"; +import "./PolygonTokenBridger.sol"; +// ERC20s (on polygon) compatible with polygon's bridge have a withdraw method. interface PolygonIERC20 is IERC20 { function withdraw(uint256 amount) external; } +// IFxMessageProcessor represents interface to process messages. +interface IFxMessageProcessor { + function processMessageFromRoot( + uint256 stateId, + address rootMessageSender, + bytes calldata data + ) external; +} + /** * @notice Polygon specific SpokePool. */ -contract Polygon_SpokePool is SpokePoolInterface, SpokePool, FxBaseChildTunnel { - +contract Polygon_SpokePool is SpokePoolInterface, IFxMessageProcessor, SpokePool { + using SafeERC20 for PolygonIERC20; address public fxChild; + PolygonTokenBridger public polygonTokenBridger; + bool private callValidated = false; event PolygonTokensBridged(address indexed token, address indexed receiver, uint256 amount); constructor( + PolygonTokenBridger _polygonTokenBridger, address _crossDomainAdmin, address _hubPool, address timerAddress - ) SpokePool(_crossDomainAdmin, _hubPool, 0x4200000000000000000000000000000000000006, timerAddress) FxBaseChildTunnel(, _crossDomainAdmin) {} - + ) SpokePool(_crossDomainAdmin, _hubPool, 0x4200000000000000000000000000000000000006, timerAddress) { + polygonTokenBridger = _polygonTokenBridger; + } function processMessageFromRoot( - uint256 stateId, + uint256, address rootMessageSender, bytes calldata data - ) public override { - require(msg.sender == fxChild, "FxBaseChildTunnel: INVALID_SENDER"); - _processMessageFromRoot(stateId, rootMessageSender, data); + ) public { + // Validation logic. + require(msg.sender == fxChild, "Not from fxChild"); + require(rootMessageSender == hubPool, "Not from mainnet HubPool"); + + // This sets a variable indicating that we're now inside a validated call. + // Note: this is used by other methods to ensure that this call has been validated by this method and is not + // spoofed. + callValidated = true; + + // This uses delegatecall to take the information in the message and process it as a function call on this contract. + (bool success, ) = address(this).delegatecall(data); + require(success, "delegatecall failed"); + + // Reset callValidated to false to disallow admin calls after this method exits. + callValidated = false; } /************************************** @@ -42,10 +69,13 @@ contract Polygon_SpokePool is SpokePoolInterface, SpokePool, FxBaseChildTunnel { **************************************/ function _bridgeTokensToHubPool(RelayerRefundLeaf memory relayerRefundLeaf) internal override { - PolygonIERC20(relayerRefundLeaf.l2TokenAddress).withdraw(relayerRefundLeaf.amountToReturn); + PolygonIERC20(relayerRefundLeaf.l2TokenAddress).safeIncreaseAllowance(relayerRefundLeaf.amountToReturn); + polygonTokenBridger.send(relayerRefundLeaf.l2TokenAddress, relayerRefundLeaf.amountToReturn); emit PolygonTokensBridged(relayerRefundLeaf.l2TokenAddress, address(this), relayerRefundLeaf.amountToReturn); } - function + function _requireAdminSender() internal view override { + require(callValidated, "Must call processMessageFromRoot"); + } } diff --git a/contracts/chain-adapters/Polygon_Adapter.sol b/contracts/chain-adapters/Polygon_Adapter.sol index a0e1e078c..b517105a9 100644 --- a/contracts/chain-adapters/Polygon_Adapter.sol +++ b/contracts/chain-adapters/Polygon_Adapter.sol @@ -13,7 +13,12 @@ import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; interface IRootChainManager { function depositEtherFor(address user) external payable; - function depositFor(address user,address rootToken,bytes calldata depositData) external; + + function depositFor( + address user, + address rootToken, + bytes calldata depositData + ) external; } interface IFxStateSender { @@ -31,8 +36,6 @@ contract Polygon_Adapter is Base_Adapter, Lockable { IFxStateSender public fxStateSender; WETH9 public l1Weth; - event TokensRelayedToPolygon(address indexed l1Token, address indexed l2Token, uint256 amount, address indexed to); - constructor( address _hubPool, IRootChainManager _rootChainManager, @@ -46,6 +49,7 @@ contract Polygon_Adapter is Base_Adapter, Lockable { function relayMessage(address target, bytes memory message) external payable override nonReentrant onlyHubPool { fxStateSender.sendMessageToChild(target, message); + emit MessageRelayed(target, message); } function relayTokens( @@ -62,7 +66,7 @@ contract Polygon_Adapter is Base_Adapter, Lockable { IERC20(l1Token).safeIncreaseAllowance(address(rootChainManager), amount); rootChainManager.depositFor(to, l1Token, abi.encode(amount)); } - emit TokensRelayedToPolygon(l1Token, l2Token, amount, to); + emit TokensRelayed(l1Token, l2Token, amount, to); } // Added to enable the Optimism_Adapter to receive ETH. used when unwrapping WETH. diff --git a/contracts/polygon/FxBaseChildTunnel.sol b/contracts/polygon/FxBaseChildTunnel.sol deleted file mode 100644 index a23608b6f..000000000 --- a/contracts/polygon/FxBaseChildTunnel.sol +++ /dev/null @@ -1,75 +0,0 @@ -// SPDX-License-Identifier: MIT -// Copied with no modifications from Polygon demo FxTunnel repo: https://github.com/jdkanani/fx-portal -// except bumping version from 0.7.3 --> 0.8 -pragma solidity ^0.8.0; - -// IFxMessageProcessor represents interface to process message -interface IFxMessageProcessor { - function processMessageFromRoot( - uint256 stateId, - address rootMessageSender, - bytes calldata data - ) external; -} - -/** - * @notice Mock child tunnel contract to receive and send message from L2 - */ -abstract contract FxBaseChildTunnel is IFxMessageProcessor { - // MessageTunnel on L1 will get data from this event - event MessageSent(bytes message); - - // fx child - address public immutable fxChild; - - // fx root tunnel - address public immutable fxRootTunnel; - - constructor(address _fxChild, address _fxRootTunnel) { - fxChild = _fxChild; - fxRootTunnel = _fxRootTunnel; - } - - // Sender must be fxRootTunnel. - modifier validateSender(address sender) { - require(sender == fxRootTunnel, "FxBaseChildTunnel: INVALID_SENDER_FROM_ROOT"); - _; - } - - function processMessageFromRoot( - uint256 stateId, - address rootMessageSender, - bytes calldata data - ) public override { - require(msg.sender == fxChild, "FxBaseChildTunnel: INVALID_SENDER"); - _processMessageFromRoot(stateId, rootMessageSender, data); - } - - /** - * @notice Emit message that can be received on Root Tunnel - * @dev Call the internal function when need to emit message - * @param message bytes message that will be sent to Root Tunnel - * some message examples - - * abi.encode(tokenId); - * abi.encode(tokenId, tokenMetadata); - * abi.encode(messageType, messageData); - */ - function _sendMessageToRoot(bytes memory message) internal { - emit MessageSent(message); - } - - /** - * @notice Process message received from Root Tunnel - * @dev function needs to be implemented to handle message as per requirement - * This is called by onStateReceive function. - * Since it is called via a system call, any event will not be emitted during its execution. - * @param stateId unique state id - * @param sender root message sender - * @param message bytes message that was sent from Root Tunnel - */ - function _processMessageFromRoot( - uint256 stateId, - address sender, - bytes memory message - ) internal virtual; -} diff --git a/contracts/polygon/FxBaseRootTunnel.sol b/contracts/polygon/FxBaseRootTunnel.sol deleted file mode 100644 index b2cfb7fdc..000000000 --- a/contracts/polygon/FxBaseRootTunnel.sol +++ /dev/null @@ -1,180 +0,0 @@ -// SPDX-License-Identifier: MIT -// Copied with no modifications from Polygon demo FxTunnel repo: https://github.com/jdkanani/fx-portal -// except bumping version from 0.7.3 --> 0.8 -pragma solidity ^0.8.0; - -import "@uma/core/contracts/external/polygon/lib/RLPReader.sol"; -import "@uma/core/contracts/external/polygon/lib/MerklePatriciaProof.sol"; -import "@uma/core/contracts/external/polygon/lib/Merkle.sol"; - -interface IFxStateSender { - function sendMessageToChild(address _receiver, bytes calldata _data) external; -} - -contract ICheckpointManager { - struct HeaderBlock { - bytes32 root; - uint256 start; - uint256 end; - uint256 createdAt; - address proposer; - } - - /** - * @notice mapping of checkpoint header numbers to block details - * @dev These checkpoints are submited by plasma contracts - */ - mapping(uint256 => HeaderBlock) public headerBlocks; -} - -abstract contract FxBaseRootTunnel { - using RLPReader for bytes; - using RLPReader for RLPReader.RLPItem; - using Merkle for bytes32; - - // keccak256(MessageSent(bytes)) - bytes32 public constant SEND_MESSAGE_EVENT_SIG = 0x8c5261668696ce22758910d05bab8f186d6eb247ceac2af2e82c7dc17669b036; - - // state sender contract - IFxStateSender public immutable fxRoot; - // root chain manager - ICheckpointManager public immutable checkpointManager; - // child tunnel contract which receives and sends messages - address public immutable fxChildTunnel; - - // storage to avoid duplicate exits - mapping(bytes32 => bool) public processedExits; - - constructor(address _checkpointManager, address _fxRoot, address _fxChildTunnel) { - checkpointManager = ICheckpointManager(_checkpointManager); - fxRoot = IFxStateSender(_fxRoot); - fxChildTunnel = _fxChildTunnel; - } - - /** - * @notice Send bytes message to Child Tunnel - * @param message bytes message that will be sent to Child Tunnel - * some message examples - - * abi.encode(tokenId); - * abi.encode(tokenId, tokenMetadata); - * abi.encode(messageType, messageData); - */ - function _sendMessageToChild(bytes memory message) internal { - fxRoot.sendMessageToChild(fxChildTunnel, message); - } - - function _validateAndExtractMessage(bytes memory inputData) internal returns (bytes memory) { - RLPReader.RLPItem[] memory inputDataRLPList = inputData.toRlpItem().toList(); - - // checking if exit has already been processed - // unique exit is identified using hash of (blockNumber, branchMask, receiptLogIndex) - bytes32 exitHash = - keccak256( - abi.encodePacked( - inputDataRLPList[2].toUint(), // blockNumber - // first 2 nibbles are dropped while generating nibble array - // this allows branch masks that are valid but bypass exitHash check (changing first 2 nibbles only) - // so converting to nibble array and then hashing it - MerklePatriciaProof._getNibbleArray(inputDataRLPList[8].toBytes()), // branchMask - inputDataRLPList[9].toUint() // receiptLogIndex - ) - ); - require(processedExits[exitHash] == false, "FxRootTunnel: EXIT_ALREADY_PROCESSED"); - processedExits[exitHash] = true; - - RLPReader.RLPItem[] memory receiptRLPList = inputDataRLPList[6].toBytes().toRlpItem().toList(); - RLPReader.RLPItem memory logRLP = - receiptRLPList[3].toList()[ - inputDataRLPList[9].toUint() // receiptLogIndex - ]; - - RLPReader.RLPItem[] memory logRLPList = logRLP.toList(); - - // check child tunnel - require(fxChildTunnel == RLPReader.toAddress(logRLPList[0]), "FxRootTunnel: INVALID_FX_CHILD_TUNNEL"); - - // verify receipt inclusion - require( - MerklePatriciaProof.verify( - inputDataRLPList[6].toBytes(), // receipt - inputDataRLPList[8].toBytes(), // branchMask - inputDataRLPList[7].toBytes(), // receiptProof - bytes32(inputDataRLPList[5].toUint()) // receiptRoot - ), - "FxRootTunnel: INVALID_RECEIPT_PROOF" - ); - - // verify checkpoint inclusion - _checkBlockMembershipInCheckpoint( - inputDataRLPList[2].toUint(), // blockNumber - inputDataRLPList[3].toUint(), // blockTime - bytes32(inputDataRLPList[4].toUint()), // txRoot - bytes32(inputDataRLPList[5].toUint()), // receiptRoot - inputDataRLPList[0].toUint(), // headerNumber - inputDataRLPList[1].toBytes() // blockProof - ); - - RLPReader.RLPItem[] memory logTopicRLPList = logRLPList[1].toList(); // topics - - require( - bytes32(logTopicRLPList[0].toUint()) == SEND_MESSAGE_EVENT_SIG, // topic0 is event sig - "FxRootTunnel: INVALID_SIGNATURE" - ); - - // received message data - bytes memory receivedData = logRLPList[2].toBytes(); - bytes memory message = abi.decode(receivedData, (bytes)); // event decodes params again, so decoding bytes to get message - return message; - } - - function _checkBlockMembershipInCheckpoint( - uint256 blockNumber, - uint256 blockTime, - bytes32 txRoot, - bytes32 receiptRoot, - uint256 headerNumber, - bytes memory blockProof - ) private view returns (uint256) { - (bytes32 headerRoot, uint256 startBlock, , uint256 createdAt, ) = checkpointManager.headerBlocks(headerNumber); - - require( - keccak256(abi.encodePacked(blockNumber, blockTime, txRoot, receiptRoot)).checkMembership( - blockNumber - startBlock, - headerRoot, - blockProof - ), - "FxRootTunnel: INVALID_HEADER" - ); - return createdAt; - } - - /** - * @notice receive message from L2 to L1, validated by proof - * @dev This function verifies if the transaction actually happened on child chain - * - * @param inputData RLP encoded data of the reference tx containing following list of fields - * 0 - headerNumber - Checkpoint header block number containing the reference tx - * 1 - blockProof - Proof that the block header (in the child chain) is a leaf in the submitted merkle root - * 2 - blockNumber - Block number containing the reference tx on child chain - * 3 - blockTime - Reference tx block time - * 4 - txRoot - Transactions root of block - * 5 - receiptRoot - Receipts root of block - * 6 - receipt - Receipt of the reference transaction - * 7 - receiptProof - Merkle proof of the reference receipt - * 8 - branchMask - 32 bits denoting the path of receipt in merkle tree - * 9 - receiptLogIndex - Log Index to read from the receipt - */ - function receiveMessage(bytes memory inputData) public virtual { - bytes memory message = _validateAndExtractMessage(inputData); - _processMessageFromChild(message); - } - - /** - * @notice Process message received from Child Tunnel - * @dev function needs to be implemented to handle message as per requirement - * This is called by onStateReceive function. - * Since it is called via a system call, any event will not be emitted during its execution. - * @param message bytes message that was sent from Child Tunnel - */ - function _processMessageFromChild(bytes memory message) internal virtual; -} From 02a86cf730566db7a2d25231d40db4f3550f417a Mon Sep 17 00:00:00 2001 From: Matt Rice Date: Sun, 27 Feb 2022 17:40:34 -0500 Subject: [PATCH 04/11] WIP Signed-off-by: Matt Rice --- contracts/Polygon_SpokePool.sol | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/contracts/Polygon_SpokePool.sol b/contracts/Polygon_SpokePool.sol index 44a9c946e..f889a9194 100644 --- a/contracts/Polygon_SpokePool.sol +++ b/contracts/Polygon_SpokePool.sol @@ -37,8 +37,9 @@ contract Polygon_SpokePool is SpokePoolInterface, IFxMessageProcessor, SpokePool PolygonTokenBridger _polygonTokenBridger, address _crossDomainAdmin, address _hubPool, + address _wmaticAddress, // Note: wmatic is used here since it is the token sent via msg.value on polygon. address timerAddress - ) SpokePool(_crossDomainAdmin, _hubPool, 0x4200000000000000000000000000000000000006, timerAddress) { + ) SpokePool(_crossDomainAdmin, _hubPool, _wmaticAddress, timerAddress) { polygonTokenBridger = _polygonTokenBridger; } From 5e9d482425cfd427e396e51b459b21980f38204a Mon Sep 17 00:00:00 2001 From: Matt Rice Date: Sun, 27 Feb 2022 17:46:35 -0500 Subject: [PATCH 05/11] WIP Signed-off-by: Matt Rice --- contracts/Polygon_SpokePool.sol | 4 +++- contracts/chain-adapters/Optimism_Adapter.sol | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/contracts/Polygon_SpokePool.sol b/contracts/Polygon_SpokePool.sol index f889a9194..9ec7c5015 100644 --- a/contracts/Polygon_SpokePool.sol +++ b/contracts/Polygon_SpokePool.sol @@ -43,8 +43,10 @@ contract Polygon_SpokePool is SpokePoolInterface, IFxMessageProcessor, SpokePool polygonTokenBridger = _polygonTokenBridger; } + // Note: stateId value isn't used because it isn't relevant for this method. It doesn't care what state sync + // triggered this call. function processMessageFromRoot( - uint256, + uint256, /*stateId*/ address rootMessageSender, bytes calldata data ) public { diff --git a/contracts/chain-adapters/Optimism_Adapter.sol b/contracts/chain-adapters/Optimism_Adapter.sol index 484efd70a..f2ae0a221 100644 --- a/contracts/chain-adapters/Optimism_Adapter.sol +++ b/contracts/chain-adapters/Optimism_Adapter.sol @@ -11,6 +11,7 @@ import "@eth-optimism/contracts/L1/messaging/IL1StandardBridge.sol"; import "@uma/core/contracts/common/implementation/Lockable.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; /** * @notice Sends cross chain messages Optimism L2 network. @@ -18,6 +19,7 @@ import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; * and the HubPool. The HubPool is the only contract that can relay tokens and messages over the bridge. */ contract Optimism_Adapter is Base_Adapter, CrossDomainEnabled, Lockable { + using SafeERC20 for IERC20; uint32 public l2GasLimit = 5_000_000; WETH9 public l1Weth; @@ -57,7 +59,7 @@ contract Optimism_Adapter is Base_Adapter, CrossDomainEnabled, Lockable { l1Weth.withdraw(amount); l1StandardBridge.depositETHTo{ value: amount }(to, l2GasLimit, ""); } else { - IERC20(l1Token).approve(address(l1StandardBridge), amount); + IERC20(l1Token).safeIncreaseAllowance(address(l1StandardBridge), amount); l1StandardBridge.depositERC20To(l1Token, l2Token, to, amount, l2GasLimit, ""); } emit TokensRelayed(l1Token, l2Token, amount, to); From 6a515fbc9b8ec0e3fee5981c062635e5ed62dede Mon Sep 17 00:00:00 2001 From: Matt Rice Date: Sun, 27 Feb 2022 19:17:41 -0500 Subject: [PATCH 06/11] WIP Signed-off-by: Matt Rice --- contracts/PolygonTokenBridger.sol | 9 +++++++-- contracts/chain-adapters/Polygon_Adapter.sol | 6 ++---- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/contracts/PolygonTokenBridger.sol b/contracts/PolygonTokenBridger.sol index 9ab9ab757..9f6fc5d44 100644 --- a/contracts/PolygonTokenBridger.sol +++ b/contracts/PolygonTokenBridger.sol @@ -3,7 +3,7 @@ pragma solidity ^0.8.0; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; -import "../Lockable.sol"; +import "./Lockable.sol"; interface PolygonIERC20 is IERC20 { function withdraw(uint256 amount) external; @@ -12,7 +12,12 @@ interface PolygonIERC20 is IERC20 { // Because Polygon only allows withdrawals from a particular address to go to that same address on mainnet, we need to // have some sort of contract that can guarantee identical addresses on Polygon and Ethereum. // Note: this contract is intended to be completely immutable, so it's guaranteed that the contract on each side is -// configured identically as long as it is created via create2. +// configured identically as long as it is created via create2. create2 is an alternative creation method that uses +// a different address determination mechanism from normal create. +// Normal create: address = hash(deployer_address, deployer_nonce) +// create2: address = hash(0xFF, sender, salt, bytecode) +// This ultimately allows create2 to generate deterministic addresses that don't depend on the transaction count of the +// sender. contract PolygonTokenBridger is Lockable { using SafeERC20 for PolygonIERC20; using SafeERC20 for IERC20; diff --git a/contracts/chain-adapters/Polygon_Adapter.sol b/contracts/chain-adapters/Polygon_Adapter.sol index b517105a9..4ffe85c98 100644 --- a/contracts/chain-adapters/Polygon_Adapter.sol +++ b/contracts/chain-adapters/Polygon_Adapter.sol @@ -26,9 +26,7 @@ interface IFxStateSender { } /** - * @notice Sends cross chain messages Optimism L2 network. - * @dev This contract's owner should be set to the some multisig or admin contract. The Owner can simply set the L2 gas - * and the HubPool. The HubPool is the only contract that can relay tokens and messages over the bridge. + * @notice Sends cross chain messages Polygon L2 network. */ contract Polygon_Adapter is Base_Adapter, Lockable { using SafeERC20 for IERC20; @@ -69,6 +67,6 @@ contract Polygon_Adapter is Base_Adapter, Lockable { emit TokensRelayed(l1Token, l2Token, amount, to); } - // Added to enable the Optimism_Adapter to receive ETH. used when unwrapping WETH. + // Added to enable the Polygon_Adapter to receive ETH. used when unwrapping WETH. receive() external payable {} } From 97ba5ff8fad63d43b8a5816af3fce41704a52d5a Mon Sep 17 00:00:00 2001 From: Matt Rice Date: Sun, 27 Feb 2022 20:18:49 -0500 Subject: [PATCH 07/11] WIP Signed-off-by: Matt Rice --- contracts/PolygonTokenBridger.sol | 1 + contracts/Polygon_SpokePool.sol | 12 +++++------- contracts/SpokePool.sol | 2 +- 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/contracts/PolygonTokenBridger.sol b/contracts/PolygonTokenBridger.sol index 9f6fc5d44..e5017af7f 100644 --- a/contracts/PolygonTokenBridger.sol +++ b/contracts/PolygonTokenBridger.sol @@ -5,6 +5,7 @@ import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import "./Lockable.sol"; +// ERC20s (on polygon) compatible with polygon's bridge have a withdraw method. interface PolygonIERC20 is IERC20 { function withdraw(uint256 amount) external; } diff --git a/contracts/Polygon_SpokePool.sol b/contracts/Polygon_SpokePool.sol index 9ec7c5015..6fe8540af 100644 --- a/contracts/Polygon_SpokePool.sol +++ b/contracts/Polygon_SpokePool.sol @@ -8,11 +8,6 @@ import "./SpokePool.sol"; import "./SpokePoolInterface.sol"; import "./PolygonTokenBridger.sol"; -// ERC20s (on polygon) compatible with polygon's bridge have a withdraw method. -interface PolygonIERC20 is IERC20 { - function withdraw(uint256 amount) external; -} - // IFxMessageProcessor represents interface to process messages. interface IFxMessageProcessor { function processMessageFromRoot( @@ -72,8 +67,11 @@ contract Polygon_SpokePool is SpokePoolInterface, IFxMessageProcessor, SpokePool **************************************/ function _bridgeTokensToHubPool(RelayerRefundLeaf memory relayerRefundLeaf) internal override { - PolygonIERC20(relayerRefundLeaf.l2TokenAddress).safeIncreaseAllowance(relayerRefundLeaf.amountToReturn); - polygonTokenBridger.send(relayerRefundLeaf.l2TokenAddress, relayerRefundLeaf.amountToReturn); + PolygonIERC20(relayerRefundLeaf.l2TokenAddress).safeIncreaseAllowance( + address(polygonTokenBridger), + relayerRefundLeaf.amountToReturn + ); + polygonTokenBridger.send(PolygonIERC20(relayerRefundLeaf.l2TokenAddress), relayerRefundLeaf.amountToReturn); emit PolygonTokensBridged(relayerRefundLeaf.l2TokenAddress, address(this), relayerRefundLeaf.amountToReturn); } diff --git a/contracts/SpokePool.sol b/contracts/SpokePool.sol index 545d4d617..3dd45c8f8 100644 --- a/contracts/SpokePool.sol +++ b/contracts/SpokePool.sol @@ -12,8 +12,8 @@ import "@openzeppelin/contracts/utils/cryptography/SignatureChecker.sol"; import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; import "@uma/core/contracts/common/implementation/Testable.sol"; -import "@uma/core/contracts/common/implementation/Lockable.sol"; import "@uma/core/contracts/common/implementation/MultiCaller.sol"; +import "./Lockable.sol"; import "./MerkleLib.sol"; import "./SpokePoolInterface.sol"; From 081ed3d52512b01a965d782fe435598be7d3b6dd Mon Sep 17 00:00:00 2001 From: Matt Rice Date: Mon, 28 Feb 2022 01:38:57 -0500 Subject: [PATCH 08/11] WIP Signed-off-by: Matt Rice --- contracts/PolygonTokenBridger.sol | 19 ++- contracts/Polygon_SpokePool.sol | 12 +- contracts/test/PolygonERC20Test.sol | 13 ++ test/chain-adapters/Polygon_Adapter.ts | 0 .../Polygon_SpokePool.ts | 129 ++++++++++++++++++ 5 files changed, 170 insertions(+), 3 deletions(-) create mode 100644 contracts/test/PolygonERC20Test.sol create mode 100644 test/chain-adapters/Polygon_Adapter.ts create mode 100644 test/chain-specific-spokepools/Polygon_SpokePool.ts diff --git a/contracts/PolygonTokenBridger.sol b/contracts/PolygonTokenBridger.sol index e5017af7f..16ac29e50 100644 --- a/contracts/PolygonTokenBridger.sol +++ b/contracts/PolygonTokenBridger.sol @@ -10,6 +10,10 @@ interface PolygonIERC20 is IERC20 { function withdraw(uint256 amount) external; } +interface MaticToken { + function withdraw(uint256 amount) external payable; +} + // Because Polygon only allows withdrawals from a particular address to go to that same address on mainnet, we need to // have some sort of contract that can guarantee identical addresses on Polygon and Ethereum. // Note: this contract is intended to be completely immutable, so it's guaranteed that the contract on each side is @@ -23,6 +27,7 @@ contract PolygonTokenBridger is Lockable { using SafeERC20 for PolygonIERC20; using SafeERC20 for IERC20; + MaticToken public constant maticToken = MaticToken(0x0000000000000000000000000000000000001010); address public immutable destination; constructor(address _destination) { @@ -30,13 +35,25 @@ contract PolygonTokenBridger is Lockable { } // Polygon side. - function send(PolygonIERC20 token, uint256 amount) public nonReentrant { + function send( + PolygonIERC20 token, + uint256 amount, + bool isMatic + ) public nonReentrant { token.safeTransferFrom(msg.sender, address(this), amount); + + // In the wMatic case, this unwraps. For other ERC20s, this is the burn/send action. token.withdraw(amount); + + // This takes the token that was withdrawn and calls withdraw on the "native" ERC20. + if (isMatic) maticToken.withdraw{ value: amount }(amount); } // Mainnet side. function retrieve(IERC20 token) public nonReentrant { token.safeTransfer(destination, token.balanceOf(address(this))); } + + // Added to enable the this contract to receive ETH. Used when unwrapping Weth. + receive() external payable {} } diff --git a/contracts/Polygon_SpokePool.sol b/contracts/Polygon_SpokePool.sol index 6fe8540af..5321ffa1c 100644 --- a/contracts/Polygon_SpokePool.sol +++ b/contracts/Polygon_SpokePool.sol @@ -33,9 +33,11 @@ contract Polygon_SpokePool is SpokePoolInterface, IFxMessageProcessor, SpokePool address _crossDomainAdmin, address _hubPool, address _wmaticAddress, // Note: wmatic is used here since it is the token sent via msg.value on polygon. + address _fxChild, address timerAddress ) SpokePool(_crossDomainAdmin, _hubPool, _wmaticAddress, timerAddress) { polygonTokenBridger = _polygonTokenBridger; + fxChild = _fxChild; } // Note: stateId value isn't used because it isn't relevant for this method. It doesn't care what state sync @@ -47,7 +49,7 @@ contract Polygon_SpokePool is SpokePoolInterface, IFxMessageProcessor, SpokePool ) public { // Validation logic. require(msg.sender == fxChild, "Not from fxChild"); - require(rootMessageSender == hubPool, "Not from mainnet HubPool"); + require(rootMessageSender == crossDomainAdmin, "Not from mainnet admmin"); // This sets a variable indicating that we're now inside a validated call. // Note: this is used by other methods to ensure that this call has been validated by this method and is not @@ -71,7 +73,13 @@ contract Polygon_SpokePool is SpokePoolInterface, IFxMessageProcessor, SpokePool address(polygonTokenBridger), relayerRefundLeaf.amountToReturn ); - polygonTokenBridger.send(PolygonIERC20(relayerRefundLeaf.l2TokenAddress), relayerRefundLeaf.amountToReturn); + + // Note: WETH is WMATIC on matic, so this tells the tokenbridger that this is an unwrappable native token. + polygonTokenBridger.send( + PolygonIERC20(relayerRefundLeaf.l2TokenAddress), + relayerRefundLeaf.amountToReturn, + address(weth) == relayerRefundLeaf.l2TokenAddress + ); emit PolygonTokensBridged(relayerRefundLeaf.l2TokenAddress, address(this), relayerRefundLeaf.amountToReturn); } diff --git a/contracts/test/PolygonERC20Test.sol b/contracts/test/PolygonERC20Test.sol new file mode 100644 index 000000000..e9e29e236 --- /dev/null +++ b/contracts/test/PolygonERC20Test.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity ^0.8.0; + +import "@uma/core/contracts/common/implementation/ExpandedERC20.sol"; +import "../PolygonTokenBridger.sol"; + +contract PolygonERC20Test is ExpandedERC20, PolygonIERC20 { + constructor() ExpandedERC20("Polygon Test", "POLY_TEST", 18) {} + + function withdraw(uint256 amount) public { + _burn(msg.sender, amount); + } +} diff --git a/test/chain-adapters/Polygon_Adapter.ts b/test/chain-adapters/Polygon_Adapter.ts new file mode 100644 index 000000000..e69de29bb diff --git a/test/chain-specific-spokepools/Polygon_SpokePool.ts b/test/chain-specific-spokepools/Polygon_SpokePool.ts new file mode 100644 index 000000000..541fa2555 --- /dev/null +++ b/test/chain-specific-spokepools/Polygon_SpokePool.ts @@ -0,0 +1,129 @@ +import { TokenRolesEnum, ZERO_ADDRESS } from "@uma/common"; +import { mockTreeRoot, amountToReturn, amountHeldByPool } from "../constants"; +import { ethers, expect, Contract, SignerWithAddress, getContractFactory, seedContract } from "../utils"; +import { hubPoolFixture } from "../HubPool.Fixture"; +import { buildRelayerRefundLeafs, buildRelayerRefundTree } from "../MerkleLib.utils"; + +let hubPool: Contract, polygonSpokePool: Contract, timer: Contract, dai: Contract, weth: Contract; + +let owner: SignerWithAddress, relayer: SignerWithAddress, rando: SignerWithAddress, fxChild: SignerWithAddress; + +async function constructSimpleTree(l2Token: Contract | string, destinationChainId: number) { + const leafs = buildRelayerRefundLeafs( + [destinationChainId], // Destination chain ID. + [amountToReturn], // amountToReturn. + [l2Token as string], // l2Token. + [[]], // refundAddresses. + [[]] // refundAmounts. + ); + + const tree = await buildRelayerRefundTree(leafs); + + return { leafs, tree }; +} +describe("Polygon Spoke Pool", function () { + beforeEach(async function () { + [owner, relayer, fxChild, rando] = await ethers.getSigners(); + ({ weth, hubPool, timer } = await hubPoolFixture()); + + const polygonTokenBridger = await ( + await getContractFactory("PolygonTokenBridger", { signer: owner }) + ).deploy(hubPool.address); + + dai = await (await getContractFactory("PolygonERC20Test", owner)).deploy(); + await dai.addMember(TokenRolesEnum.MINTER, owner.address); + + polygonSpokePool = await ( + await getContractFactory("Polygon_SpokePool", { signer: owner }) + ).deploy(polygonTokenBridger.address, owner.address, hubPool.address, weth.address, fxChild.address, timer.address); + + await seedContract(polygonSpokePool, relayer, [dai], weth, amountHeldByPool); + }); + + it("Only correct caller can set the cross domain admin", async function () { + const setCrossDomainAdminData = polygonSpokePool.interface.encodeFunctionData("setCrossDomainAdmin", [ + rando.address, + ]); + + // Wrong rootMessageSender address. + await expect(polygonSpokePool.connect(fxChild).processMessageFromRoot(0, rando.address, setCrossDomainAdminData)).to + .be.reverted; + + // Wrong calling address. + await expect(polygonSpokePool.connect(rando).processMessageFromRoot(0, owner.address, setCrossDomainAdminData)).to + .be.reverted; + + await polygonSpokePool.connect(fxChild).processMessageFromRoot(0, owner.address, setCrossDomainAdminData); + expect(await polygonSpokePool.crossDomainAdmin()).to.equal(rando.address); + }); + + it("Only correct caller can set the hub pool address", async function () { + const setHubPoolData = polygonSpokePool.interface.encodeFunctionData("setHubPool", [rando.address]); + + // Wrong rootMessageSender address. + await expect(polygonSpokePool.connect(fxChild).processMessageFromRoot(0, rando.address, setHubPoolData)).to.be + .reverted; + + // Wrong calling address. + await expect(polygonSpokePool.connect(rando).processMessageFromRoot(0, owner.address, setHubPoolData)).to.be + .reverted; + + await polygonSpokePool.connect(fxChild).processMessageFromRoot(0, owner.address, setHubPoolData); + expect(await polygonSpokePool.hubPool()).to.equal(rando.address); + }); + + it("Only correct caller can set the quote time buffer", async function () { + const setDepositQuoteTimeBufferData = polygonSpokePool.interface.encodeFunctionData("setDepositQuoteTimeBuffer", [ + 12345, + ]); + + // Wrong rootMessageSender address. + await expect( + polygonSpokePool.connect(fxChild).processMessageFromRoot(0, rando.address, setDepositQuoteTimeBufferData) + ).to.be.reverted; + + // Wrong calling address. + await expect( + polygonSpokePool.connect(rando).processMessageFromRoot(0, owner.address, setDepositQuoteTimeBufferData) + ).to.be.reverted; + + await polygonSpokePool.connect(fxChild).processMessageFromRoot(0, owner.address, setDepositQuoteTimeBufferData); + expect(await polygonSpokePool.depositQuoteTimeBuffer()).to.equal(12345); + }); + + it("Only correct caller can initialize a relayer refund", async function () { + const relayRootBundleData = polygonSpokePool.interface.encodeFunctionData("relayRootBundle", [ + mockTreeRoot, + mockTreeRoot, + ]); + + // Wrong rootMessageSender address. + await expect(polygonSpokePool.connect(fxChild).processMessageFromRoot(0, rando.address, relayRootBundleData)).to.be + .reverted; + + // Wrong calling address. + await expect(polygonSpokePool.connect(rando).processMessageFromRoot(0, owner.address, relayRootBundleData)).to.be + .reverted; + + await polygonSpokePool.connect(fxChild).processMessageFromRoot(0, owner.address, relayRootBundleData); + + expect((await polygonSpokePool.rootBundles(0)).slowRelayRoot).to.equal(mockTreeRoot); + expect((await polygonSpokePool.rootBundles(0)).relayerRefundRoot).to.equal(mockTreeRoot); + }); + + it("Bridge tokens to hub pool correctly sends tokens through the PolygonTokenBridger", async function () { + const { leafs, tree } = await constructSimpleTree(dai.address, await polygonSpokePool.callStatic.chainId()); + const relayRootBundleData = polygonSpokePool.interface.encodeFunctionData("relayRootBundle", [ + tree.getHexRoot(), + mockTreeRoot, + ]); + + await polygonSpokePool.connect(fxChild).processMessageFromRoot(0, owner.address, relayRootBundleData); + const bridger = await polygonSpokePool.polygonTokenBridger(); + + // Checks that there's a burn event from the bridger. + await expect(polygonSpokePool.connect(relayer).executeRelayerRefundRoot(0, leafs[0], tree.getHexProof(leafs[0]))) + .to.emit(dai, "Transfer") + .withArgs(bridger, ZERO_ADDRESS, amountToReturn); + }); +}); From 7ee138b1251ae98822dbba25a466bbf7d79b38c9 Mon Sep 17 00:00:00 2001 From: Matt Rice Date: Mon, 28 Feb 2022 04:12:19 -0500 Subject: [PATCH 09/11] finish tests Signed-off-by: Matt Rice --- contracts/PolygonTokenBridger.sol | 11 +- contracts/chain-adapters/Polygon_Adapter.sol | 2 +- contracts/test/PolygonMocks.sol | 16 +++ test/chain-adapters/Polygon_Adapter.ts | 118 ++++++++++++++++++ .../Polygon_SpokePool.ts | 2 +- 5 files changed, 144 insertions(+), 5 deletions(-) create mode 100644 contracts/test/PolygonMocks.sol diff --git a/contracts/PolygonTokenBridger.sol b/contracts/PolygonTokenBridger.sol index 16ac29e50..87d184c07 100644 --- a/contracts/PolygonTokenBridger.sol +++ b/contracts/PolygonTokenBridger.sol @@ -4,6 +4,7 @@ pragma solidity ^0.8.0; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import "./Lockable.sol"; +import "./interfaces/WETH9.sol"; // ERC20s (on polygon) compatible with polygon's bridge have a withdraw method. interface PolygonIERC20 is IERC20 { @@ -29,9 +30,11 @@ contract PolygonTokenBridger is Lockable { MaticToken public constant maticToken = MaticToken(0x0000000000000000000000000000000000001010); address public immutable destination; + WETH9 public immutable l1Weth; - constructor(address _destination) { + constructor(address _destination, WETH9 _l1Weth) { destination = _destination; + l1Weth = _l1Weth; } // Polygon side. @@ -54,6 +57,8 @@ contract PolygonTokenBridger is Lockable { token.safeTransfer(destination, token.balanceOf(address(this))); } - // Added to enable the this contract to receive ETH. Used when unwrapping Weth. - receive() external payable {} + receive() external payable { + // Note: this should only happen on the mainnet side where ETH is sent to the contract directly by the bridge. + if (functionCallStackOriginatesFromOutsideThisContract()) l1Weth.deposit{ value: address(this).balance }(); + } } diff --git a/contracts/chain-adapters/Polygon_Adapter.sol b/contracts/chain-adapters/Polygon_Adapter.sol index 4ffe85c98..a32cf8603 100644 --- a/contracts/chain-adapters/Polygon_Adapter.sol +++ b/contracts/chain-adapters/Polygon_Adapter.sol @@ -7,7 +7,7 @@ import "../interfaces/WETH9.sol"; import "@eth-optimism/contracts/libraries/bridge/CrossDomainEnabled.sol"; import "@eth-optimism/contracts/L1/messaging/IL1StandardBridge.sol"; -import "@uma/core/contracts/common/implementation/Lockable.sol"; +import "../Lockable.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; diff --git a/contracts/test/PolygonMocks.sol b/contracts/test/PolygonMocks.sol new file mode 100644 index 000000000..d788455d7 --- /dev/null +++ b/contracts/test/PolygonMocks.sol @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity ^0.8.0; + +contract RootChainManagerMock { + function depositEtherFor(address user) external payable {} + + function depositFor( + address user, + address rootToken, + bytes calldata depositData + ) external {} +} + +contract FxStateSenderMock { + function sendMessageToChild(address _receiver, bytes calldata _data) external {} +} diff --git a/test/chain-adapters/Polygon_Adapter.ts b/test/chain-adapters/Polygon_Adapter.ts index e69de29bb..ed63bb8ac 100644 --- a/test/chain-adapters/Polygon_Adapter.ts +++ b/test/chain-adapters/Polygon_Adapter.ts @@ -0,0 +1,118 @@ +import { + sampleL2Gas, + amountToLp, + mockTreeRoot, + refundProposalLiveness, + bondAmount, + mockSlowRelayRoot, +} from "./../constants"; +import { + ethers, + expect, + Contract, + FakeContract, + SignerWithAddress, + createFake, + getContractFactory, + seedWallet, + randomAddress, +} from "../utils"; +import { hubPoolFixture, enableTokensForLP } from "../HubPool.Fixture"; +import { constructSingleChainTree } from "../MerkleLib.utils"; + +let hubPool: Contract, + polygonAdapter: Contract, + mockAdapter: Contract, + weth: Contract, + dai: Contract, + timer: Contract, + mockSpoke: Contract; +let l2Weth: string, l2Dai: string; +let owner: SignerWithAddress, dataWorker: SignerWithAddress, liquidityProvider: SignerWithAddress; +let rootChainManager: FakeContract, fxStateSender: FakeContract; + +const polygonChainId = 137; +const l1ChainId = 1; + +describe("Polygon Chain Adapter", function () { + beforeEach(async function () { + [owner, dataWorker, liquidityProvider] = await ethers.getSigners(); + ({ weth, dai, l2Weth, l2Dai, hubPool, mockSpoke, timer, mockAdapter } = await hubPoolFixture()); + await seedWallet(dataWorker, [dai], weth, amountToLp); + await seedWallet(liquidityProvider, [dai], weth, amountToLp.mul(10)); + + await enableTokensForLP(owner, hubPool, weth, [weth, dai]); + await weth.connect(liquidityProvider).approve(hubPool.address, amountToLp); + await hubPool.connect(liquidityProvider).addLiquidity(weth.address, amountToLp); + await weth.connect(dataWorker).approve(hubPool.address, bondAmount.mul(10)); + await dai.connect(liquidityProvider).approve(hubPool.address, amountToLp); + await hubPool.connect(liquidityProvider).addLiquidity(dai.address, amountToLp); + await dai.connect(dataWorker).approve(hubPool.address, bondAmount.mul(10)); + + rootChainManager = await createFake("RootChainManagerMock"); + fxStateSender = await createFake("FxStateSenderMock"); + + polygonAdapter = await ( + await getContractFactory("Polygon_Adapter", owner) + ).deploy(hubPool.address, rootChainManager.address, fxStateSender.address, weth.address); + + await hubPool.setCrossChainContracts(polygonChainId, polygonAdapter.address, mockSpoke.address); + await hubPool.whitelistRoute(polygonChainId, l1ChainId, l2Weth, weth.address); + await hubPool.whitelistRoute(polygonChainId, l1ChainId, l2Dai, dai.address); + + await hubPool.setCrossChainContracts(l1ChainId, mockAdapter.address, mockSpoke.address); + await hubPool.whitelistRoute(l1ChainId, polygonChainId, weth.address, l2Weth); + await hubPool.whitelistRoute(l1ChainId, polygonChainId, dai.address, l2Dai); + }); + + it("relayMessage calls spoke pool functions", async function () { + const newAdmin = randomAddress(); + const functionCallData = mockSpoke.interface.encodeFunctionData("setCrossDomainAdmin", [newAdmin]); + expect(await hubPool.relaySpokePoolAdminFunction(polygonChainId, functionCallData)) + .to.emit(polygonAdapter, "MessageRelayed") + .withArgs(mockSpoke.address, functionCallData); + + expect(fxStateSender.sendMessageToChild).to.have.been.calledWith(mockSpoke.address, functionCallData); + }); + it("Correctly calls appropriate Polygon bridge functions when making ERC20 cross chain calls", async function () { + // Create an action that will send an L1->L2 tokens transfer and bundle. For this, create a relayer repayment bundle + // and check that at it's finalization the L2 bridge contracts are called as expected. + const { leafs, tree, tokensSendToL2 } = await constructSingleChainTree(dai.address, 1, polygonChainId); + await hubPool.connect(dataWorker).proposeRootBundle([3117], 1, tree.getHexRoot(), mockTreeRoot, mockSlowRelayRoot); + await timer.setCurrentTime(Number(await timer.getCurrentTime()) + refundProposalLiveness + 1); + await hubPool.connect(dataWorker).executeRootBundle(leafs[0], tree.getHexProof(leafs[0])); + + // The correct functions should have been called on the polygon contracts. + expect(rootChainManager.depositFor).to.have.been.calledOnce; // One token transfer over the bridge. + expect(rootChainManager.depositEtherFor).to.have.callCount(0); // No ETH transfers over the bridge. + + const expectedErc20L1ToL2BridgeParams = [ + mockSpoke.address, + dai.address, + ethers.utils.defaultAbiCoder.encode(["uint256"], [tokensSendToL2]), + ]; + expect(rootChainManager.depositFor).to.have.been.calledWith(...expectedErc20L1ToL2BridgeParams); + const expectedL1ToL2FunctionCallParams = [ + mockSpoke.address, + mockSpoke.interface.encodeFunctionData("relayRootBundle", [mockTreeRoot, mockSlowRelayRoot]), + ]; + expect(fxStateSender.sendMessageToChild).to.have.been.calledWith(...expectedL1ToL2FunctionCallParams); + }); + it("Correctly unwraps WETH and bridges ETH", async function () { + // Cant bridge WETH on polygon. Rather, unwrap WETH to ETH then bridge it. Validate the adapter does this. + const { leafs, tree } = await constructSingleChainTree(weth.address, 1, polygonChainId); + await hubPool.connect(dataWorker).proposeRootBundle([3117], 1, tree.getHexRoot(), mockTreeRoot, mockSlowRelayRoot); + await timer.setCurrentTime(Number(await timer.getCurrentTime()) + refundProposalLiveness + 1); + await hubPool.connect(dataWorker).executeRootBundle(leafs[0], tree.getHexProof(leafs[0])); + + // The correct functions should have been called on the polygon contracts. + expect(rootChainManager.depositEtherFor).to.have.been.calledOnce; // One eth transfer over the bridge. + expect(rootChainManager.depositFor).to.have.callCount(0); // No Token transfers over the bridge. + expect(rootChainManager.depositEtherFor).to.have.been.calledWith(mockSpoke.address); + const expectedL2ToL1FunctionCallParams = [ + mockSpoke.address, + mockSpoke.interface.encodeFunctionData("relayRootBundle", [mockTreeRoot, mockSlowRelayRoot]), + ]; + expect(fxStateSender.sendMessageToChild).to.have.been.calledWith(...expectedL2ToL1FunctionCallParams); + }); +}); diff --git a/test/chain-specific-spokepools/Polygon_SpokePool.ts b/test/chain-specific-spokepools/Polygon_SpokePool.ts index 541fa2555..f0cfba605 100644 --- a/test/chain-specific-spokepools/Polygon_SpokePool.ts +++ b/test/chain-specific-spokepools/Polygon_SpokePool.ts @@ -28,7 +28,7 @@ describe("Polygon Spoke Pool", function () { const polygonTokenBridger = await ( await getContractFactory("PolygonTokenBridger", { signer: owner }) - ).deploy(hubPool.address); + ).deploy(hubPool.address, weth.address); dai = await (await getContractFactory("PolygonERC20Test", owner)).deploy(); await dai.addMember(TokenRolesEnum.MINTER, owner.address); From 8ee7d6b08fea1dd73aeb0e1af9f4c108a757cedf Mon Sep 17 00:00:00 2001 From: Matt Rice Date: Mon, 28 Feb 2022 04:20:58 -0500 Subject: [PATCH 10/11] WIP Signed-off-by: Matt Rice --- .../Polygon_SpokePool.ts | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/test/chain-specific-spokepools/Polygon_SpokePool.ts b/test/chain-specific-spokepools/Polygon_SpokePool.ts index f0cfba605..e3a3b14fe 100644 --- a/test/chain-specific-spokepools/Polygon_SpokePool.ts +++ b/test/chain-specific-spokepools/Polygon_SpokePool.ts @@ -1,6 +1,6 @@ import { TokenRolesEnum, ZERO_ADDRESS } from "@uma/common"; import { mockTreeRoot, amountToReturn, amountHeldByPool } from "../constants"; -import { ethers, expect, Contract, SignerWithAddress, getContractFactory, seedContract } from "../utils"; +import { ethers, expect, Contract, SignerWithAddress, getContractFactory, seedContract, toWei } from "../utils"; import { hubPoolFixture } from "../HubPool.Fixture"; import { buildRelayerRefundLeafs, buildRelayerRefundTree } from "../MerkleLib.utils"; @@ -126,4 +126,20 @@ describe("Polygon Spoke Pool", function () { .to.emit(dai, "Transfer") .withArgs(bridger, ZERO_ADDRESS, amountToReturn); }); + + it("PolygonTokenBridger retrieves and unwraps tokens correctly", async function () { + const polygonTokenBridger = await ( + await getContractFactory("PolygonTokenBridger", { signer: owner }) + ).deploy(hubPool.address, weth.address); + + await expect(() => + owner.sendTransaction({ to: polygonTokenBridger.address, value: toWei("1") }) + ).to.changeTokenBalance(weth, polygonTokenBridger, toWei("1")); + + await expect(() => polygonTokenBridger.connect(owner).retrieve(weth.address)).to.changeTokenBalances( + weth, + [polygonTokenBridger, hubPool], + [toWei("1").mul(-1), toWei("1")] + ); + }); }); From fed3d2bb50fcfd12df0b693b58d040d643aa4d0c Mon Sep 17 00:00:00 2001 From: Matt Rice Date: Mon, 28 Feb 2022 04:31:12 -0500 Subject: [PATCH 11/11] WIP Signed-off-by: Matt Rice --- contracts/Polygon_SpokePool.sol | 27 ++++++++++++++++++--------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/contracts/Polygon_SpokePool.sol b/contracts/Polygon_SpokePool.sol index 5321ffa1c..301d49dd6 100644 --- a/contracts/Polygon_SpokePool.sol +++ b/contracts/Polygon_SpokePool.sol @@ -28,6 +28,23 @@ contract Polygon_SpokePool is SpokePoolInterface, IFxMessageProcessor, SpokePool event PolygonTokensBridged(address indexed token, address indexed receiver, uint256 amount); + // Note: validating calls this way ensures that strange calls coming from the fxChild won't be misinterpreted. + // Put differently, just checking that msg.sender == fxChild is not sufficient. + // All calls that have admin priviledges must be fired from within the processMessageFromRoot method that's gone + // through validation where the sender is checked and the root (mainnet) sender is also validated. + // This modifier sets the callValidated variable so this condition can be checked in _requireAdminSender(). + modifier validateInternalCalls() { + // This sets a variable indicating that we're now inside a validated call. + // Note: this is used by other methods to ensure that this call has been validated by this method and is not + // spoofed. See + callValidated = true; + + _; + + // Reset callValidated to false to disallow admin calls after this method exits. + callValidated = false; + } + constructor( PolygonTokenBridger _polygonTokenBridger, address _crossDomainAdmin, @@ -46,22 +63,14 @@ contract Polygon_SpokePool is SpokePoolInterface, IFxMessageProcessor, SpokePool uint256, /*stateId*/ address rootMessageSender, bytes calldata data - ) public { + ) public validateInternalCalls { // Validation logic. require(msg.sender == fxChild, "Not from fxChild"); require(rootMessageSender == crossDomainAdmin, "Not from mainnet admmin"); - // This sets a variable indicating that we're now inside a validated call. - // Note: this is used by other methods to ensure that this call has been validated by this method and is not - // spoofed. - callValidated = true; - // This uses delegatecall to take the information in the message and process it as a function call on this contract. (bool success, ) = address(this).delegatecall(data); require(success, "delegatecall failed"); - - // Reset callValidated to false to disallow admin calls after this method exits. - callValidated = false; } /**************************************