diff --git a/contracts/PolygonTokenBridger.sol b/contracts/PolygonTokenBridger.sol new file mode 100644 index 000000000..87d184c07 --- /dev/null +++ b/contracts/PolygonTokenBridger.sol @@ -0,0 +1,64 @@ +// 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"; +import "./interfaces/WETH9.sol"; + +// ERC20s (on polygon) compatible with polygon's bridge have a withdraw method. +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 +// 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; + + MaticToken public constant maticToken = MaticToken(0x0000000000000000000000000000000000001010); + address public immutable destination; + WETH9 public immutable l1Weth; + + constructor(address _destination, WETH9 _l1Weth) { + destination = _destination; + l1Weth = _l1Weth; + } + + // Polygon side. + 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))); + } + + 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/Polygon_SpokePool.sol b/contracts/Polygon_SpokePool.sol new file mode 100644 index 000000000..301d49dd6 --- /dev/null +++ b/contracts/Polygon_SpokePool.sol @@ -0,0 +1,99 @@ +//SPDX-License-Identifier: Unlicense +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 "./PolygonTokenBridger.sol"; + +// 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, 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); + + // 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, + 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 + // triggered this call. + function processMessageFromRoot( + uint256, /*stateId*/ + address rootMessageSender, + bytes calldata data + ) public validateInternalCalls { + // Validation logic. + require(msg.sender == fxChild, "Not from fxChild"); + require(rootMessageSender == crossDomainAdmin, "Not from mainnet admmin"); + + // 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"); + } + + /************************************** + * INTERNAL FUNCTIONS * + **************************************/ + + function _bridgeTokensToHubPool(RelayerRefundLeaf memory relayerRefundLeaf) internal override { + PolygonIERC20(relayerRefundLeaf.l2TokenAddress).safeIncreaseAllowance( + address(polygonTokenBridger), + 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); + } + + function _requireAdminSender() internal view override { + require(callValidated, "Must call processMessageFromRoot"); + } +} 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"; 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); diff --git a/contracts/chain-adapters/Polygon_Adapter.sol b/contracts/chain-adapters/Polygon_Adapter.sol new file mode 100644 index 000000000..a32cf8603 --- /dev/null +++ b/contracts/chain-adapters/Polygon_Adapter.sol @@ -0,0 +1,72 @@ +// 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 "../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 Polygon L2 network. + */ +contract Polygon_Adapter is Base_Adapter, Lockable { + using SafeERC20 for IERC20; + IRootChainManager public rootChainManager; + IFxStateSender public fxStateSender; + WETH9 public l1Weth; + + 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); + emit MessageRelayed(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 TokensRelayed(l1Token, l2Token, amount, to); + } + + // Added to enable the Polygon_Adapter to receive ETH. used when unwrapping WETH. + receive() external payable {} +} 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/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 new file mode 100644 index 000000000..ed63bb8ac --- /dev/null +++ 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 new file mode 100644 index 000000000..e3a3b14fe --- /dev/null +++ b/test/chain-specific-spokepools/Polygon_SpokePool.ts @@ -0,0 +1,145 @@ +import { TokenRolesEnum, ZERO_ADDRESS } from "@uma/common"; +import { mockTreeRoot, amountToReturn, amountHeldByPool } from "../constants"; +import { ethers, expect, Contract, SignerWithAddress, getContractFactory, seedContract, toWei } 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, weth.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); + }); + + 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")] + ); + }); +});