diff --git a/contracts/HubPool.sol b/contracts/HubPool.sol index 70f58e271..3b01c678c 100644 --- a/contracts/HubPool.sol +++ b/contracts/HubPool.sol @@ -2,6 +2,7 @@ pragma solidity ^0.8.0; import "./MerkleLib.sol"; +import "./chain-adapters/AdapterInterface.sol"; import "@uma/core/contracts/common/implementation/Testable.sol"; import "@uma/core/contracts/common/implementation/Lockable.sol"; @@ -51,6 +52,13 @@ contract HubPool is Testable, Lockable, MultiCaller, Ownable { mapping(address => LPToken) public lpTokens; // Mapping of L1TokenAddress to the associated LPToken. + struct CrossChainContract { + AdapterInterface adapter; + address spokePool; + } + + mapping(uint256 => CrossChainContract) public crossChainContracts; // Mapping of chainId to the associated adapter and spokePool contracts. + FinderInterface public finder; bytes32 public identifier; @@ -88,19 +96,23 @@ contract HubPool is Testable, Lockable, MultiCaller, Ownable { uint64 requestExpirationTimestamp, uint64 unclaimedPoolRebalanceLeafCount, uint256[] bundleEvaluationBlockNumbers, - bytes32 poolRebalanceRoot, - bytes32 destinationDistributionRoot, + bytes32 indexed poolRebalanceRoot, + bytes32 indexed destinationDistributionRoot, address indexed proposer ); - event RelayerRefundExecuted(uint256 relayerRefundId, MerkleLib.PoolRebalance poolRebalance, address caller); - - event RelayerRefundDisputed( - address indexed disputer, - SkinnyOptimisticOracleInterface.Request ooPriceRequest, - RefundRequest refundRequest + event RelayerRefundExecuted( + uint256 indexed leafId, + uint256 indexed chainId, + address[] l1Token, + uint256[] bundleLpFees, + int256[] netSendAmount, + int256[] runningBalance, + address indexed caller ); - modifier onlyIfNoActiveRequest() { + event RelayerRefundDisputed(address indexed disputer, uint256 requestTime, bytes disputedAncillaryData); + + modifier noActiveRequests() { require(refundRequest.unclaimedPoolRebalanceLeafCount == 0, "Active request has unclaimed leafs"); _; } @@ -126,12 +138,21 @@ contract HubPool is Testable, Lockable, MultiCaller, Ownable { * ADMIN FUNCTIONS * *************************************************/ - function setBond(IERC20 newBondToken, uint256 newBondAmount) public onlyOwner onlyIfNoActiveRequest { + function setBond(IERC20 newBondToken, uint256 newBondAmount) public onlyOwner noActiveRequests { bondToken = newBondToken; bondAmount = newBondAmount; emit BondSet(address(newBondToken), newBondAmount); } + function setCrossChainContracts( + uint256 chainId, + AdapterInterface adapter, + address spokePool + ) public onlyOwner noActiveRequests { + require(address(crossChainContracts[chainId].adapter) == address(0), "Contract already set"); + crossChainContracts[chainId] = CrossChainContract(adapter, spokePool); + } + /** * @notice Whitelist an origin token <-> destination token route. */ @@ -220,12 +241,15 @@ contract HubPool is Testable, Lockable, MultiCaller, Ownable { * DATA WORKER FUNCTIONS * *************************************************/ + // After initiateRelayerRefund is called, if the any props are wrong then this proposal can be challenged. Once the + // challenge period passes, then the roots are no longer disputable, and only executeRelayerRefund can be called and + // initiateRelayerRefund can't be called again until all leafs are executed. function initiateRelayerRefund( uint256[] memory bundleEvaluationBlockNumbers, uint64 poolRebalanceLeafCount, bytes32 poolRebalanceRoot, bytes32 destinationDistributionRoot - ) public onlyIfNoActiveRequest { + ) public noActiveRequests { require(poolRebalanceLeafCount > 0, "Bundle must have at least 1 leaf"); uint64 requestExpirationTimestamp = uint64(getCurrentTime() + refundProposalLiveness); @@ -251,38 +275,41 @@ contract HubPool is Testable, Lockable, MultiCaller, Ownable { ); } - function executeRelayerRefund( - uint256 relayerRefundRequestId, - MerkleLib.PoolRebalance memory poolRebalance, - bytes32[] memory proof - ) public { + function executeRelayerRefund(MerkleLib.PoolRebalance memory poolRebalanceLeaf, bytes32[] memory proof) public { require(getCurrentTime() >= refundRequest.requestExpirationTimestamp, "Not passed liveness"); - // Verify the leafId in the poolRebalance has not yet been claimed. - require(!MerkleLib.isClaimed1D(refundRequest.claimedBitMap, poolRebalance.leafId), "Already claimed"); + // Verify the leafId in the poolRebalanceLeaf has not yet been claimed. + require(!MerkleLib.isClaimed1D(refundRequest.claimedBitMap, poolRebalanceLeaf.leafId), "Already claimed"); // Verify the props provided generate a leaf that, along with the proof, are included in the merkle root. - require(MerkleLib.verifyPoolRebalance(refundRequest.poolRebalanceRoot, poolRebalance, proof), "Bad Proof"); + require(MerkleLib.verifyPoolRebalance(refundRequest.poolRebalanceRoot, poolRebalanceLeaf, proof), "Bad Proof"); // Set the leafId in the claimed bitmap. - refundRequest.claimedBitMap = MerkleLib.setClaimed1D(refundRequest.claimedBitMap, poolRebalance.leafId); + refundRequest.claimedBitMap = MerkleLib.setClaimed1D(refundRequest.claimedBitMap, poolRebalanceLeaf.leafId); // Decrement the unclaimedPoolRebalanceLeafCount. refundRequest.unclaimedPoolRebalanceLeafCount--; - // Transfer the bondAmount to back to the proposer, if this was not done before for this refund bundle. - if (!refundRequest.proposerBondRepaid) { - refundRequest.proposerBondRepaid = true; + // Transfer the bondAmount to back to the proposer, if this the last executed leaf. Only sending this once all + // leafs have been executed acts to force the data worker to execute all bundles or they wont receive their bond. + //TODO: consider if we want to reward the proposer. if so, this is where we should do it. + if (refundRequest.unclaimedPoolRebalanceLeafCount == 0) bondToken.safeTransfer(refundRequest.proposer, bondAmount); - } - // TODO call into canonical bridge to send PoolRebalance.netSendAmount for the associated - // PoolRebalance.tokenAddresses, to the target PoolRebalance.chainId. this will likely happen within a - // x_Messenger contract for each chain. these messengers will be registered in a separate process that will follow - // in a later PR. + _sendTokensToChain(poolRebalanceLeaf.chainId, poolRebalanceLeaf.l1Tokens, poolRebalanceLeaf.netSendAmounts); + _executeRelayerRefundOnChain(poolRebalanceLeaf.chainId); + // TODO: modify the associated utilized and pending reserves for each token sent. - emit RelayerRefundExecuted(relayerRefundRequestId, poolRebalance, msg.sender); + emit RelayerRefundExecuted( + poolRebalanceLeaf.leafId, + poolRebalanceLeaf.chainId, + poolRebalanceLeaf.l1Tokens, + poolRebalanceLeaf.bundleLpFees, + poolRebalanceLeaf.netSendAmounts, + poolRebalanceLeaf.runningBalances, + msg.sender + ); } function disputeRelayerRefund() public { @@ -290,13 +317,14 @@ contract HubPool is Testable, Lockable, MultiCaller, Ownable { // Request price from OO and dispute it. uint256 totalBond = _getBondTokenFinalFee() + bondAmount; + bytes memory requestAncillaryData = _getRefundProposalAncillaryData(); bondToken.safeTransferFrom(msg.sender, address(this), totalBond); // This contract needs to approve totalBond*2 against the OO contract. (for the price request and dispute). bondToken.safeApprove(address(_getOptimisticOracle()), totalBond * 2); _getOptimisticOracle().requestAndProposePriceFor( identifier, uint32(getCurrentTime()), - _getRefundProposalAncillaryData(), + requestAncillaryData, bondToken, // Set reward to 0, since we'll settle proposer reward payouts directly from this contract after a relay // proposal has passed the challenge period. @@ -328,13 +356,13 @@ contract HubPool is Testable, Lockable, MultiCaller, Ownable { _getOptimisticOracle().disputePriceFor( identifier, uint32(getCurrentTime()), - _getRefundProposalAncillaryData(), + requestAncillaryData, ooPriceRequest, msg.sender, address(this) ); - emit RelayerRefundDisputed(msg.sender, ooPriceRequest, refundRequest); + emit RelayerRefundDisputed(msg.sender, getCurrentTime(), requestAncillaryData); // Finally, delete the state pertaining to the active refundRequest. delete refundRequest; @@ -405,6 +433,45 @@ contract HubPool is Testable, Lockable, MultiCaller, Ownable { .rawValue; } + function _sendTokensToChain( + uint256 chainId, + address[] memory l1Tokens, + int256[] memory netSendAmounts + ) internal { + AdapterInterface adapter = crossChainContracts[chainId].adapter; + require(address(adapter) != address(0), "Adapter not set for target chain"); + + for (uint32 i = 0; i < l1Tokens.length; i++) { + // Validate the output L2 token is correctly whitelisted. + address l2Token = whitelistedRoutes[l1Tokens[i]][chainId]; + require(l2Token != address(0), "Route not whitelisted"); + + int256 amount = netSendAmounts[i]; + + // TODO: Checking the amount is greater than 0 is not sufficient. we need to build an external library that + // makes the decision on if there should be an L1->L2 token transfer. this should come in a later PR. + if (amount > 0) { + // Send the adapter all the tokens it needs to bridge. This should be refined later to remove the extra + // token transfer through the use of delegate call. + IERC20(l1Tokens[i]).safeApprove(address(adapter), uint256(amount)); + adapter.relayTokens( + l1Tokens[i], // l1Token + l2Token, // l2Token + uint256(amount), // amount + crossChainContracts[chainId].spokePool // to. This should be the spokePool. + ); + } + } + } + + function _executeRelayerRefundOnChain(uint256 chainId) internal { + AdapterInterface adapter = crossChainContracts[chainId].adapter; + adapter.relayMessage( + crossChainContracts[chainId].spokePool, // target. This should be the spokePool on the L2. + abi.encodeWithSignature("initializeRelayerRefund(bytes32)", refundRequest.destinationDistributionRoot) // message + ); + } + // Added to enable the BridgePool to receive ETH. used when unwrapping Weth. receive() external payable {} } diff --git a/contracts/MerkleLib.sol b/contracts/MerkleLib.sol index 8d7d0018a..2aab4d816 100644 --- a/contracts/MerkleLib.sol +++ b/contracts/MerkleLib.sol @@ -15,22 +15,22 @@ library MerkleLib { uint256 leafId; // This is used to know which chain to send cross-chain transactions to (and which SpokePool to sent to). uint256 chainId; - // The following arrays are required to be the same length. They are parallel arrays for the given chainId and should be ordered by the `tokenAddresses` field. + // The following arrays are required to be the same length. They are parallel arrays for the given chainId and should be ordered by the `l1Tokens` field. // All whitelisted tokens with nonzero relays on this chain in this bundle in the order of whitelisting. - address[] tokenAddresses; + address[] l1Tokens; uint256[] bundleLpFees; // Total LP fee amount per token in this bundle, encompassing all associated bundled relays. // This array is grouped with the two above, and it represents the amount to send or request back from the // SpokePool. If positive, the pool will pay the SpokePool. If negative the SpokePool will pay the HubPool. // There can be arbitrarily complex rebalancing rules defined offchain. This number is only nonzero // when the rules indicate that a rebalancing action should occur. When a rebalance does not occur, - // runningBalance for this token should change by the total relays - deposits in this bundle. When a rebalance - // does occur, runningBalance should be set to zero for this token and the netSendAmount should be set to the - // previous runningBalance + relays - deposits in this bundle. - int256[] netSendAmount; + // runningBalances for this token should change by the total relays - deposits in this bundle. When a rebalance + // does occur, runningBalances should be set to zero for this token and the netSendAmounts should be set to the + // previous runningBalances + relays - deposits in this bundle. + int256[] netSendAmounts; // This is only here to be emitted in an event to track a running unpaid balance between the L2 pool and the L1 pool. // A positive number indicates that the HubPool owes the SpokePool funds. A negative number indicates that the - // SpokePool owes the HubPool funds. See the comment above for the dynamics of this and netSendAmount - int256[] runningBalance; + // SpokePool owes the HubPool funds. See the comment above for the dynamics of this and netSendAmounts + int256[] runningBalances; } // This leaf is meant to be decoded in the SpokePool in order to pay out individual relayers for this bundle. diff --git a/contracts/SpokePool.sol b/contracts/SpokePool.sol index fdedfee8e..12dbe89c8 100644 --- a/contracts/SpokePool.sol +++ b/contracts/SpokePool.sol @@ -242,7 +242,9 @@ abstract contract SpokePool is Testable, Lockable, MultiCaller { emit FilledRelay(relayHash, relayFills[relayHash], repaymentChain, amountToSend, msg.sender, relayData); } - function initializeRelayerRefund(bytes32 relayerRepaymentDistributionProof) public {} + function initializeRelayerRefund(bytes32 relayerRepaymentDistributionProof) public virtual { + return; + } function distributeRelayerRefund( uint256 relayerRefundId, diff --git a/contracts/chain-adapters/AdapterInterface.sol b/contracts/chain-adapters/AdapterInterface.sol new file mode 100644 index 000000000..50f6b5a79 --- /dev/null +++ b/contracts/chain-adapters/AdapterInterface.sol @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity ^0.8.0; + +/** + * @notice Sends cross chain messages and tokens to contracts on a specific L2 network. + */ + +interface AdapterInterface { + function relayMessage(address target, bytes memory message) external payable; + + function relayTokens( + address l1Token, + address l2Token, + uint256 amount, + address to + ) external payable; +} diff --git a/contracts/chain-adapters/Mock_Adapter.sol b/contracts/chain-adapters/Mock_Adapter.sol new file mode 100644 index 000000000..ef2a59814 --- /dev/null +++ b/contracts/chain-adapters/Mock_Adapter.sol @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity ^0.8.0; + +import "./AdapterInterface.sol"; +import "@openzeppelin/contracts/access/Ownable.sol"; +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +/** + * @notice Sends cross chain messages Optimism L2 network. + * @dev This contract's owner should be set to the BridgeAdmin deployed on the same L1 network so that only the + * BridgeAdmin can call cross-chain administrative functions on the L2 SpokePool via this messenger. + */ +contract Mock_Adapter is Ownable, AdapterInterface { + event RelayMessageCalled(address target, bytes message, address caller); + + event RelayTokensCalled(address l1Token, address l2Token, uint256 amount, address to, address caller); + + function relayMessage(address target, bytes memory message) external payable override onlyOwner { + emit RelayMessageCalled(target, message, msg.sender); + } + + function relayTokens( + address l1Token, + address l2Token, + uint256 amount, + address to + ) external payable override onlyOwner { + emit RelayTokensCalled(l1Token, l2Token, amount, to, msg.sender); + // Pull the tokens from the caller to mock the actions of an L1 bridge pulling tokens. + IERC20(l1Token).transferFrom(msg.sender, address(this), amount); + } +} diff --git a/contracts/chain-adapters/Optimism_Adapter.sol b/contracts/chain-adapters/Optimism_Adapter.sol new file mode 100644 index 000000000..96e4c6606 --- /dev/null +++ b/contracts/chain-adapters/Optimism_Adapter.sol @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity ^0.8.0; + +import "@eth-optimism/contracts/libraries/bridge/CrossDomainEnabled.sol"; +import "@eth-optimism/contracts/L1/messaging/IL1ERC20Bridge.sol"; +import "./AdapterInterface.sol"; +import "@openzeppelin/contracts/access/Ownable.sol"; + +/** + * @notice Sends cross chain messages Optimism L2 network. + * @dev This contract's owner should be set to the BridgeAdmin deployed on the same L1 network so that only the + * BridgeAdmin can call cross-chain administrative functions on the L2 SpokePool via this messenger. + */ +contract Optimism_Messenger is Ownable, CrossDomainEnabled, AdapterInterface { + uint32 public gasLimit; + + address l1Weth; + + IL1ERC20Bridge l1ERC20Bridge; + + constructor( + uint32 _gasLimit, + address _crossDomainMessenger, + address _IL1ERC20Bridge + ) CrossDomainEnabled(_crossDomainMessenger) { + gasLimit = _gasLimit; + l1ERC20Bridge = IL1ERC20Bridge(_IL1ERC20Bridge); + } + + function relayMessage(address target, bytes memory message) external payable override onlyOwner { + sendCrossDomainMessage(target, uint32(gasLimit), message); + } + + // TODO: we should look into using delegate call as this current implementation assumes the caller + // transfers the tokens first to this contract. + function relayTokens( + address l1Token, + address l2Token, + uint256 amount, + address to + ) external payable override onlyOwner { + //TODO: add weth support. + l1ERC20Bridge.depositERC20To(l1Token, l2Token, to, amount, gasLimit, "0x"); + } +} diff --git a/contracts/test/MockSpokePool.sol b/contracts/test/MockSpokePool.sol index c5f9e5545..aca17af9e 100644 --- a/contracts/test/MockSpokePool.sol +++ b/contracts/test/MockSpokePool.sol @@ -25,4 +25,8 @@ contract MockSpokePool is SpokePool { function setDepositQuoteTimeBuffer(uint64 buffer) public { _setDepositQuoteTimeBuffer(buffer); } + + function initializeRelayerRefund(bytes32 relayerRepaymentDistributionProof) public override { + return; + } } diff --git a/hardhat.config.ts b/hardhat.config.ts index 2b63ed6a6..8e54673f0 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -10,19 +10,6 @@ import "hardhat-deploy"; dotenv.config(); -// This is a sample Hardhat task. To learn how to create your own go to -// https://hardhat.org/guides/create-task.html -task("accounts", "Prints the list of accounts", async (taskArgs, hre) => { - const accounts = await hre.ethers.getSigners(); - - for (const account of accounts) { - console.log(account.address); - } -}); - -// You need to export an object to set up your config -// Go to https://hardhat.org/config/ to learn more - const config: HardhatUserConfig = { solidity: { compilers: [{ version: "0.8.11", settings: { optimizer: { enabled: true, runs: 200 } } }] }, networks: { @@ -31,18 +18,9 @@ const config: HardhatUserConfig = { accountsBalance: "1000000000000000000000000", // 1mil ETH }, }, - ropsten: { - url: process.env.ROPSTEN_URL || "", - accounts: process.env.PRIVATE_KEY !== undefined ? [process.env.PRIVATE_KEY] : [], - }, - }, - gasReporter: { - enabled: process.env.REPORT_GAS !== undefined, - currency: "USD", - }, - etherscan: { - apiKey: process.env.ETHERSCAN_API_KEY, }, + gasReporter: { enabled: process.env.REPORT_GAS !== undefined, currency: "USD" }, + etherscan: { apiKey: process.env.ETHERSCAN_API_KEY }, }; export default config; diff --git a/test/HubPool.Admin.ts b/test/HubPool.Admin.ts index 205c819f8..6605e038e 100644 --- a/test/HubPool.Admin.ts +++ b/test/HubPool.Admin.ts @@ -3,7 +3,7 @@ import { Contract } from "ethers"; import { ethers } from "hardhat"; import { ZERO_ADDRESS } from "@uma/common"; import { getContractFactory, SignerWithAddress, createRandomBytes32, seedWallet } from "./utils"; -import { depositDestinationChainId, bondAmount } from "./constants"; +import { destinationChainId, bondAmount } from "./constants"; import { hubPoolFixture } from "./HubPool.Fixture"; let hubPool: Contract, weth: Contract, usdc: Contract; @@ -38,10 +38,10 @@ describe("HubPool Admin functions", function () { await expect(hubPool.connect(other).disableL1TokenForLiquidityProvision(weth.address)).to.be.reverted; }); it("Can whitelist route for deposits and rebalances", async function () { - await expect(hubPool.whitelistRoute(weth.address, usdc.address, depositDestinationChainId)) + await expect(hubPool.whitelistRoute(weth.address, usdc.address, destinationChainId)) .to.emit(hubPool, "WhitelistRoute") - .withArgs(weth.address, depositDestinationChainId, usdc.address); - expect(await hubPool.whitelistedRoutes(weth.address, depositDestinationChainId)).to.equal(usdc.address); + .withArgs(weth.address, destinationChainId, usdc.address); + expect(await hubPool.whitelistedRoutes(weth.address, destinationChainId)).to.equal(usdc.address); }); it("Can change the bond token and amount", async function () { diff --git a/test/HubPool.Fixture.ts b/test/HubPool.Fixture.ts index 6a193d84f..6734178af 100644 --- a/test/HubPool.Fixture.ts +++ b/test/HubPool.Fixture.ts @@ -1,6 +1,6 @@ import { TokenRolesEnum, interfaceName } from "@uma/common"; -import { getContractFactory, utf8ToHex, toBN, fromWei } from "./utils"; -import { bondAmount, refundProposalLiveness, finalFee, identifier } from "./constants"; +import { getContractFactory, randomAddress, toBN, fromWei } from "./utils"; +import { bondAmount, refundProposalLiveness, finalFee, identifier, repaymentChainId } from "./constants"; import { Contract, Signer } from "ethers"; import hre from "hardhat"; @@ -30,7 +30,7 @@ export const hubPoolFixture = hre.deployments.createFixture(async ({ ethers }) = await parentFixtureOutput.store.setFinalFee(usdc.address, { rawValue: toBN(fromWei(finalFee)).mul(1e6) }); await parentFixtureOutput.store.setFinalFee(dai.address, { rawValue: finalFee }); - // Deploy the hubPool + // Deploy the hubPool. const merkleLib = await (await getContractFactory("MerkleLib", signer)).deploy(); const hubPool = await ( await getContractFactory("HubPool", { signer: signer, libraries: { MerkleLib: merkleLib.address } }) @@ -44,7 +44,23 @@ export const hubPoolFixture = hre.deployments.createFixture(async ({ ethers }) = parentFixtureOutput.timer.address ); - return { weth, usdc, dai, hubPool, ...parentFixtureOutput }; + // Deploy a mock chain adapter and add it as the chainAdapter for the test chainId. Set the SpokePool to address 0. + const mockAdapter = await (await getContractFactory("Mock_Adapter", signer)).deploy(); + await mockAdapter.transferOwnership(hubPool.address); + const mockSpoke = await ( + await getContractFactory("MockSpokePool", signer) + ).deploy(weth.address, 0, parentFixtureOutput.timer.address); + await hubPool.setCrossChainContracts(repaymentChainId, mockAdapter.address, mockSpoke.address); + + // Deploy mock l2 tokens for each token created before and whitelist the routes. + const l2Weth = randomAddress(); + const l2Dai = randomAddress(); + const l2Usdc = randomAddress(); + await hubPool.whitelistRoute(weth.address, l2Weth, repaymentChainId); + await hubPool.whitelistRoute(dai.address, l2Dai, repaymentChainId); + await hubPool.whitelistRoute(usdc.address, l2Usdc, repaymentChainId); + + return { weth, usdc, dai, hubPool, mockAdapter, mockSpoke, l2Weth, l2Dai, l2Usdc, ...parentFixtureOutput }; }); export async function enableTokensForLiquidityProvision(owner: Signer, hubPool: Contract, tokens: Contract[]) { diff --git a/test/HubPool.RefundExecution.ts b/test/HubPool.RefundExecution.ts new file mode 100644 index 000000000..3c8a9f65f --- /dev/null +++ b/test/HubPool.RefundExecution.ts @@ -0,0 +1,106 @@ +import { expect } from "chai"; +import { Contract } from "ethers"; +import { ethers } from "hardhat"; + +import { SignerWithAddress, toBNWei, seedWallet, createRandomBytes32 } from "./utils"; +import * as consts from "./constants"; +import { hubPoolFixture, enableTokensForLiquidityProvision } from "./HubPool.Fixture"; +import { buildPoolRebalanceTree, buildPoolRebalanceLeafs } from "./MerkleLib.utils"; + +let hubPool: Contract, mockAdapter: Contract, weth: Contract, dai: Contract, mockSpoke: Contract, timer: Contract; +let owner: SignerWithAddress, dataWorker: SignerWithAddress, liquidityProvider: SignerWithAddress; +let l2Weth: string, l2Dai: string; + +// Construct the leafs that will go into the merkle tree. For this function create a simple set of leafs that will +// repay two token to one chain Id with simple lpFee, netSend and running balance amounts. +async function constructSimpleTree() { + const wethToSend = toBNWei(100); + const daiToSend = toBNWei(1000); + const leafs = buildPoolRebalanceLeafs( + [consts.repaymentChainId], // repayment chain. In this test we only want to send one token to one chain. + [weth, dai], // l1Token. We will only be sending WETH and DAI to the associated repayment chain. + [[toBNWei(1), toBNWei(10)]], // bundleLpFees. Set to 1 ETH and 10 DAI respectively to attribute to the LPs. + [[wethToSend, daiToSend]], // netSendAmounts. Set to 100 ETH and 1000 DAI as the amount to send from L1->L2. + [[wethToSend, daiToSend]] // runningBalances. Set to 100 ETH and 1000 DAI. + ); + const tree = await buildPoolRebalanceTree(leafs); + + return { wethToSend, daiToSend, leafs, tree }; +} + +describe("HubPool Relayer Refund Execution", function () { + beforeEach(async function () { + [owner, dataWorker, liquidityProvider] = await ethers.getSigners(); + ({ weth, dai, hubPool, mockAdapter, mockSpoke, timer, l2Weth, l2Dai } = await hubPoolFixture()); + await seedWallet(dataWorker, [dai], weth, consts.bondAmount.add(consts.finalFee).mul(2)); + await seedWallet(liquidityProvider, [dai], weth, consts.amountToLp.mul(10)); + + await enableTokensForLiquidityProvision(owner, hubPool, [weth, dai]); + await weth.connect(liquidityProvider).approve(hubPool.address, consts.amountToLp); + await hubPool.connect(liquidityProvider).addLiquidity(weth.address, consts.amountToLp); + await dai.connect(liquidityProvider).approve(hubPool.address, consts.amountToLp.mul(10)); // LP with 10000 DAI. + await hubPool.connect(liquidityProvider).addLiquidity(dai.address, consts.amountToLp.mul(10)); + + await weth.connect(dataWorker).approve(hubPool.address, consts.bondAmount.mul(10)); + }); + + it("Execute relayer refund correctly produces the refund bundle call and sends cross-chain repayment actions", async function () { + const { wethToSend, daiToSend, leafs, tree } = await constructSimpleTree(); + + await hubPool.connect(dataWorker).initiateRelayerRefund( + [3117], // bundleEvaluationBlockNumbers used by bots to construct bundles. Length must equal the number of leafs. + 1, // poolRebalanceLeafCount. There is exactly one leaf in the bundle (just sending WETH to one address). + tree.getHexRoot(), // poolRebalanceRoot. Generated from the merkle tree constructed before. + consts.mockDestinationDistributionRoot // destinationDistributionRoot. Not relevant for this test. + ); + + // Advance time so the request can be executed and execute the request. + await timer.setCurrentTime(Number(await timer.getCurrentTime()) + consts.refundProposalLiveness); + await hubPool.connect(dataWorker).executeRelayerRefund(leafs[0], tree.getHexProof(leafs[0])); + + // Balances should have updated as expected. + expect(await weth.balanceOf(hubPool.address)).to.equal(consts.amountToLp.sub(wethToSend)); + expect(await weth.balanceOf(mockAdapter.address)).to.equal(wethToSend); + expect(await dai.balanceOf(hubPool.address)).to.equal(consts.amountToLp.mul(10).sub(daiToSend)); + expect(await dai.balanceOf(mockAdapter.address)).to.equal(daiToSend); + + // Check the mockAdapter was called with the correct arguments for each method. + const relayMessageEvents = await mockAdapter.queryFilter(mockAdapter.filters.RelayMessageCalled()); + expect(relayMessageEvents.length).to.equal(1); // Exactly one message send from L1->L2. + expect(relayMessageEvents[0].args?.target).to.equal(mockSpoke.address); + expect(relayMessageEvents[0].args?.message).to.equal( + mockSpoke.interface.encodeFunctionData("initializeRelayerRefund", [consts.mockDestinationDistributionRoot]) + ); + + const relayTokensEvents = await mockAdapter.queryFilter(mockAdapter.filters.RelayTokensCalled()); + expect(relayTokensEvents.length).to.equal(2); // Exactly two token transfers from L1->L2. + expect(relayTokensEvents[0].args?.l1Token).to.equal(weth.address); + expect(relayTokensEvents[0].args?.l2Token).to.equal(l2Weth); + expect(relayTokensEvents[0].args?.amount).to.equal(wethToSend); + expect(relayTokensEvents[0].args?.to).to.equal(mockSpoke.address); + expect(relayTokensEvents[1].args?.l1Token).to.equal(dai.address); + expect(relayTokensEvents[1].args?.l2Token).to.equal(l2Dai); + expect(relayTokensEvents[1].args?.amount).to.equal(daiToSend); + expect(relayTokensEvents[1].args?.to).to.equal(mockSpoke.address); + }); + it("Execution rejects invalid leafs", async function () { + const { leafs, tree } = await constructSimpleTree(); + await hubPool.connect(dataWorker).initiateRelayerRefund([3117], 1, tree.getHexRoot(), createRandomBytes32()); + await timer.setCurrentTime(Number(await timer.getCurrentTime()) + consts.refundProposalLiveness); + + // Take the valid root but change some element within it, such as the chainId. This will change the hash of the leaf + // and as such the contract should reject it for not being included within the merkle tree for the valid proof. + const badLeaf = { ...leafs[0], chainId: 13371 }; + await expect(hubPool.connect(dataWorker).executeRelayerRefund(badLeaf, tree.getHexProof(leafs[0]))).to.be.reverted; + }); + + it("Execution rejects double claimed leafs", async function () { + const { leafs, tree } = await constructSimpleTree(); + await hubPool.connect(dataWorker).initiateRelayerRefund([3117], 1, tree.getHexRoot(), createRandomBytes32()); + await timer.setCurrentTime(Number(await timer.getCurrentTime()) + consts.refundProposalLiveness); + + // First claim should be fine. Second claim should be reverted as you cant double claim a leaf. + await hubPool.connect(dataWorker).executeRelayerRefund(leafs[0], tree.getHexProof(leafs[0])); + await expect(hubPool.connect(dataWorker).executeRelayerRefund(leafs[0], tree.getHexProof(leafs[0]))).to.be.reverted; + }); +}); diff --git a/test/HubPool.RefundInitilization.ts b/test/HubPool.RefundInitilization.ts new file mode 100644 index 000000000..61b69f7ba --- /dev/null +++ b/test/HubPool.RefundInitilization.ts @@ -0,0 +1,66 @@ +import { expect } from "chai"; +import { Contract } from "ethers"; +import { ethers } from "hardhat"; +import { SignerWithAddress, seedWallet } from "./utils"; +import * as consts from "./constants"; +import { hubPoolFixture } from "./HubPool.Fixture"; + +let hubPool: Contract, weth: Contract, dataWorker: SignerWithAddress; + +describe("HubPool Relayer Refund Initialization", function () { + beforeEach(async function () { + [dataWorker] = await ethers.getSigners(); + ({ weth, hubPool } = await hubPoolFixture()); + await seedWallet(dataWorker, [], weth, consts.bondAmount.add(consts.finalFee).mul(2)); + }); + + it("Initialization of a relay correctly stores data, emits events and pulls the bond", async function () { + const expectedRequestExpirationTimestamp = Number(await hubPool.getCurrentTime()) + consts.refundProposalLiveness; + await weth.connect(dataWorker).approve(hubPool.address, consts.bondAmount); + const dataWorkerWethBalancerBefore = await weth.callStatic.balanceOf(dataWorker.address); + + await expect( + hubPool + .connect(dataWorker) + .initiateRelayerRefund( + consts.mockBundleEvaluationBlockNumbers, + consts.mockPoolRebalanceLeafCount, + consts.mockPoolRebalanceRoot, + consts.mockDestinationDistributionRoot + ) + ) + .to.emit(hubPool, "InitiateRefundRequested") + .withArgs( + expectedRequestExpirationTimestamp, + consts.mockPoolRebalanceLeafCount, + consts.mockBundleEvaluationBlockNumbers, + consts.mockPoolRebalanceRoot, + consts.mockDestinationDistributionRoot, + dataWorker.address + ); + // Balances of the hubPool should have incremented by the bond and the dataWorker should have decremented by the bond. + expect(await weth.balanceOf(hubPool.address)).to.equal(consts.bondAmount); + expect(await weth.balanceOf(dataWorker.address)).to.equal(dataWorkerWethBalancerBefore.sub(consts.bondAmount)); + + const refundRequest = await hubPool.refundRequest(); + expect(refundRequest.requestExpirationTimestamp).to.equal(expectedRequestExpirationTimestamp); + expect(refundRequest.unclaimedPoolRebalanceLeafCount).to.equal(consts.mockPoolRebalanceLeafCount); + expect(refundRequest.poolRebalanceRoot).to.equal(consts.mockPoolRebalanceRoot); + expect(refundRequest.destinationDistributionRoot).to.equal(consts.mockDestinationDistributionRoot); + expect(refundRequest.claimedBitMap).to.equal(0); // no claims yet so everything should be marked at 0. + expect(refundRequest.proposer).to.equal(dataWorker.address); + expect(refundRequest.proposerBondRepaid).to.equal(false); + + // Can not re-initialize if the previous bundle has unclaimed leaves. + await expect( + hubPool + .connect(dataWorker) + .initiateRelayerRefund( + consts.mockBundleEvaluationBlockNumbers, + consts.mockPoolRebalanceLeafCount, + consts.mockPoolRebalanceRoot, + consts.mockDestinationDistributionRoot + ) + ).to.be.revertedWith("Active request has unclaimed leafs"); + }); +}); diff --git a/test/HubPool.RelayerRefund.ts b/test/HubPool.RelayerDispute.ts similarity index 51% rename from test/HubPool.RelayerRefund.ts rename to test/HubPool.RelayerDispute.ts index 37a08215b..3d2f2c870 100644 --- a/test/HubPool.RelayerRefund.ts +++ b/test/HubPool.RelayerDispute.ts @@ -1,91 +1,35 @@ import { expect } from "chai"; import { Contract } from "ethers"; import { ethers } from "hardhat"; -import { ZERO_ADDRESS, parseAncillaryData } from "@uma/common"; -import { getContractFactory, SignerWithAddress, createRandomBytes32, seedWallet } from "./utils"; +import { parseAncillaryData } from "@uma/common"; +import { SignerWithAddress, seedWallet } from "./utils"; import * as consts from "./constants"; import { hubPoolFixture, enableTokensForLiquidityProvision } from "./HubPool.Fixture"; let hubPool: Contract, weth: Contract, optimisticOracle: Contract; let owner: SignerWithAddress, dataWorker: SignerWithAddress, liquidityProvider: SignerWithAddress; -const mockBundleEvaluationBlockNumbers = [1, 2, 3]; -const mockPoolRebalanceLeafCount = 5; -const mockPoolRebalanceRoot = createRandomBytes32(); -const mockDestinationDistributionRoot = createRandomBytes32(); - -describe("HubPool Relayer Refund", function () { +describe("HubPool Relayer Refund Dispute", function () { beforeEach(async function () { [owner, dataWorker, liquidityProvider] = await ethers.getSigners(); ({ weth, hubPool, optimisticOracle } = await hubPoolFixture()); - await seedWallet(dataWorker, [], weth, consts.bondAmount); - await seedWallet(owner, [], weth, consts.bondAmount); + await enableTokensForLiquidityProvision(owner, hubPool, [weth]); + await seedWallet(dataWorker, [], weth, consts.bondAmount.add(consts.finalFee).mul(2)); await seedWallet(liquidityProvider, [], weth, consts.amountToLp); - - await enableTokensForLiquidityProvision(owner, hubPool, [weth]); await weth.connect(liquidityProvider).approve(hubPool.address, consts.amountToLp); await hubPool.connect(liquidityProvider).addLiquidity(weth.address, consts.amountToLp); }); - it("Initialization of a relay correctly stores data, emits events and pulls the bond", async function () { - const expectedRequestExpirationTimestamp = Number(await hubPool.getCurrentTime()) + consts.refundProposalLiveness; - await weth.connect(dataWorker).approve(hubPool.address, consts.bondAmount); - const dataWorkerWethBalancerBefore = await weth.callStatic.balanceOf(dataWorker.address); - - await expect( - hubPool - .connect(dataWorker) - .initiateRelayerRefund( - mockBundleEvaluationBlockNumbers, - mockPoolRebalanceLeafCount, - mockPoolRebalanceRoot, - mockDestinationDistributionRoot - ) - ) - .to.emit(hubPool, "InitiateRefundRequested") - .withArgs( - expectedRequestExpirationTimestamp, - mockPoolRebalanceLeafCount, - mockBundleEvaluationBlockNumbers, - mockPoolRebalanceRoot, - mockDestinationDistributionRoot, - dataWorker.address - ); - // Balances of the hubPool should have incremented by the bond and the dataWorker should have decremented by the bond. - expect(await weth.balanceOf(hubPool.address)).to.equal(consts.bondAmount.add(consts.amountToLp)); - expect(await weth.balanceOf(dataWorker.address)).to.equal(dataWorkerWethBalancerBefore.sub(consts.bondAmount)); - - const refundRequest = await hubPool.refundRequest(); - expect(refundRequest.requestExpirationTimestamp).to.equal(expectedRequestExpirationTimestamp); - expect(refundRequest.unclaimedPoolRebalanceLeafCount).to.equal(mockPoolRebalanceLeafCount); - expect(refundRequest.poolRebalanceRoot).to.equal(mockPoolRebalanceRoot); - expect(refundRequest.destinationDistributionRoot).to.equal(mockDestinationDistributionRoot); - expect(refundRequest.claimedBitMap).to.equal(0); // no claims yet so everything should be marked at 0. - expect(refundRequest.proposer).to.equal(dataWorker.address); - expect(refundRequest.proposerBondRepaid).to.equal(false); - - // Can not re-initialize if the previous bundle has unclaimed leaves. - await expect( - hubPool - .connect(dataWorker) - .initiateRelayerRefund( - mockBundleEvaluationBlockNumbers, - mockPoolRebalanceLeafCount, - mockPoolRebalanceRoot, - mockDestinationDistributionRoot - ) - ).to.be.revertedWith("Active request has unclaimed leafs"); - }); it("Dispute relayer refund correctly deletes the active request and enqueues a price request with the OO", async function () { await weth.connect(dataWorker).approve(hubPool.address, consts.bondAmount.mul(10)); await hubPool .connect(dataWorker) .initiateRelayerRefund( - mockBundleEvaluationBlockNumbers, - mockPoolRebalanceLeafCount, - mockPoolRebalanceRoot, - mockDestinationDistributionRoot + consts.mockBundleEvaluationBlockNumbers, + consts.mockPoolRebalanceLeafCount, + consts.mockPoolRebalanceRoot, + consts.mockDestinationDistributionRoot ); const preCallAncillaryData = await hubPool._getRefundProposalAncillaryData(); @@ -112,9 +56,9 @@ describe("HubPool Relayer Refund", function () { expect(parsedAncillaryData?.requestExpirationTimestamp).to.equal( Number(await hubPool.getCurrentTime()) + consts.refundProposalLiveness ); - expect(parsedAncillaryData?.unclaimedPoolRebalanceLeafCount).to.equal(mockPoolRebalanceLeafCount); - expect("0x" + parsedAncillaryData?.poolRebalanceRoot).to.equal(mockPoolRebalanceRoot); - expect("0x" + parsedAncillaryData?.destinationDistributionRoot).to.equal(mockDestinationDistributionRoot); + expect(parsedAncillaryData?.unclaimedPoolRebalanceLeafCount).to.equal(consts.mockPoolRebalanceLeafCount); + expect("0x" + parsedAncillaryData?.poolRebalanceRoot).to.equal(consts.mockPoolRebalanceRoot); + expect("0x" + parsedAncillaryData?.destinationDistributionRoot).to.equal(consts.mockDestinationDistributionRoot); expect(parsedAncillaryData?.claimedBitMap).to.equal(0); expect(ethers.utils.getAddress("0x" + parsedAncillaryData?.proposer)).to.equal(dataWorker.address); }); @@ -123,10 +67,10 @@ describe("HubPool Relayer Refund", function () { await hubPool .connect(dataWorker) .initiateRelayerRefund( - mockBundleEvaluationBlockNumbers, - mockPoolRebalanceLeafCount, - mockPoolRebalanceRoot, - mockDestinationDistributionRoot + consts.mockBundleEvaluationBlockNumbers, + consts.mockPoolRebalanceLeafCount, + consts.mockPoolRebalanceRoot, + consts.mockDestinationDistributionRoot ); await hubPool.setCurrentTime(Number(await hubPool.getCurrentTime()) + consts.refundProposalLiveness + 1); diff --git a/test/MerkleLib.Proofs.ts b/test/MerkleLib.Proofs.ts index d857abb12..4089ad153 100644 --- a/test/MerkleLib.Proofs.ts +++ b/test/MerkleLib.Proofs.ts @@ -1,27 +1,11 @@ +import { PoolRebalance, DestinationDistribution } from "./MerkleLib.utils"; import { expect } from "chai"; import { merkleLibFixture } from "./MerkleLib.Fixture"; import { Contract, BigNumber } from "ethers"; import { MerkleTree } from "../utils/MerkleTree"; import { ethers } from "hardhat"; -import { randomBigNumber, randomAddress } from "./utils"; - -interface PoolRebalance { - leafId: BigNumber; - chainId: BigNumber; - tokenAddresses: string[]; - bundleLpFees: BigNumber[]; - netSendAmount: BigNumber[]; - runningBalance: BigNumber[]; -} - -interface DestinationDistribution { - leafId: BigNumber; - chainId: BigNumber; - amountToReturn: BigNumber; - l2TokenAddress: string; - refundAddresses: string[]; - refundAmounts: BigNumber[]; -} +const { defaultAbiCoder, keccak256 } = ethers.utils; +import { randomBigNumber, randomAddress, getParamType } from "./utils"; let merkleLibTest: Contract; @@ -35,34 +19,31 @@ describe("MerkleLib Proofs", async function () { const numRebalances = 101; for (let i = 0; i < numRebalances; i++) { const numTokens = 10; - const tokenAddresses: string[] = []; + const l1Tokens: string[] = []; const bundleLpFees: BigNumber[] = []; - const netSendAmount: BigNumber[] = []; - const runningBalance: BigNumber[] = []; + const netSendAmounts: BigNumber[] = []; + const runningBalances: BigNumber[] = []; for (let j = 0; j < numTokens; j++) { - tokenAddresses.push(randomAddress()); + l1Tokens.push(randomAddress()); bundleLpFees.push(randomBigNumber()); - netSendAmount.push(randomBigNumber()); - runningBalance.push(randomBigNumber()); + netSendAmounts.push(randomBigNumber()); + runningBalances.push(randomBigNumber()); } poolRebalances.push({ leafId: BigNumber.from(i), chainId: randomBigNumber(), - tokenAddresses, + l1Tokens, bundleLpFees, - netSendAmount, - runningBalance, + netSendAmounts, + runningBalances, }); } // Remove the last element. const invalidPoolRebalance = poolRebalances.pop()!; - const fragment = merkleLibTest.interface.fragments.find((fragment) => fragment.name === "verifyPoolRebalance"); - const param = fragment!.inputs.find((input) => input.name === "rebalance"); - - const hashFn = (input: PoolRebalance) => - ethers.utils.keccak256(ethers.utils.defaultAbiCoder.encode([param!], [input])); + const paramType = await getParamType("MerkleLib", "verifyPoolRebalance", "rebalance"); + const hashFn = (input: PoolRebalance) => keccak256(defaultAbiCoder.encode([paramType!], [input])); const merkleTree = new MerkleTree(poolRebalances, hashFn); const root = merkleTree.getHexRoot(); @@ -97,13 +78,8 @@ describe("MerkleLib Proofs", async function () { // Remove the last element. const invalidDestinationDistribution = destinationDistributions.pop()!; - const fragment = merkleLibTest.interface.fragments.find( - (fragment) => fragment.name === "verifyRelayerDistribution" - ); - const param = fragment!.inputs.find((input) => input.name === "distribution"); - - const hashFn = (input: DestinationDistribution) => - ethers.utils.keccak256(ethers.utils.defaultAbiCoder.encode([param!], [input])); + const paramType = await getParamType("MerkleLib", "verifyRelayerDistribution", "distribution"); + const hashFn = (input: DestinationDistribution) => keccak256(defaultAbiCoder.encode([paramType!], [input])); const merkleTree = new MerkleTree(destinationDistributions, hashFn); const root = merkleTree.getHexRoot(); diff --git a/test/MerkleLib.utils.ts b/test/MerkleLib.utils.ts new file mode 100644 index 000000000..951ce05b0 --- /dev/null +++ b/test/MerkleLib.utils.ts @@ -0,0 +1,60 @@ +import { expect } from "chai"; +import { getParamType } from "./utils"; +import { merkleLibFixture } from "./MerkleLib.Fixture"; +import { MerkleTree } from "../utils/MerkleTree"; +import { ethers } from "hardhat"; +const { defaultAbiCoder, keccak256 } = ethers.utils; +import { BigNumber, Signer, Contract } from "ethers"; + +export interface PoolRebalance { + leafId: BigNumber; + chainId: BigNumber; + l1Tokens: string[]; + bundleLpFees: BigNumber[]; + netSendAmounts: BigNumber[]; + runningBalances: BigNumber[]; +} + +export interface DestinationDistribution { + leafId: BigNumber; + chainId: BigNumber; + amountToReturn: BigNumber; + l2TokenAddress: string; + refundAddresses: string[]; + refundAmounts: BigNumber[]; +} + +export async function buildPoolRebalanceTree(poolRebalances: PoolRebalance[]) { + for (let i = 0; i < poolRebalances.length; i++) { + // The 4 provided parallel arrays must be of equal length. + expect(poolRebalances[i].l1Tokens.length) + .to.equal(poolRebalances[i].bundleLpFees.length) + .to.equal(poolRebalances[i].netSendAmounts.length) + .to.equal(poolRebalances[i].runningBalances.length); + } + + const paramType = await getParamType("MerkleLib", "verifyPoolRebalance", "rebalance"); + const hashFn = (input: PoolRebalance) => keccak256(defaultAbiCoder.encode([paramType!], [input])); + return new MerkleTree(poolRebalances, hashFn); +} + +export function buildPoolRebalanceLeafs( + destinationChainIds: number[], + l1Tokens: Contract[], + bundleLpFees: BigNumber[][], + netSendAmounts: BigNumber[][], + runningBalances: BigNumber[][] +): PoolRebalance[] { + return Array(destinationChainIds.length) + .fill(0) + .map((_, i) => { + return { + leafId: BigNumber.from(i), + chainId: BigNumber.from(destinationChainIds[i]), + l1Tokens: l1Tokens.map((token: Contract) => token.address), + bundleLpFees: bundleLpFees[i], + netSendAmounts: netSendAmounts[i], + runningBalances: runningBalances[i], + }; + }); +} diff --git a/test/SpokePool.Admin.ts b/test/SpokePool.Admin.ts index 9ab43d941..490203cae 100644 --- a/test/SpokePool.Admin.ts +++ b/test/SpokePool.Admin.ts @@ -3,7 +3,7 @@ import { Contract } from "ethers"; import { ethers } from "hardhat"; import { SignerWithAddress } from "./utils"; import { spokePoolFixture } from "./SpokePool.Fixture"; -import { depositDestinationChainId } from "./constants"; +import { destinationChainId } from "./constants"; let spokePool: Contract, erc20: Contract; let owner: SignerWithAddress; @@ -14,10 +14,10 @@ describe("SpokePool Admin Functions", async function () { ({ spokePool, erc20 } = await spokePoolFixture()); }); it("Enable token path", async function () { - await expect(spokePool.connect(owner).setEnableRoute(erc20.address, depositDestinationChainId, true)) + await expect(spokePool.connect(owner).setEnableRoute(erc20.address, destinationChainId, true)) .to.emit(spokePool, "EnabledDepositRoute") - .withArgs(erc20.address, depositDestinationChainId, true); - expect(await spokePool.enabledDepositRoutes(erc20.address, depositDestinationChainId)).to.equal(true); + .withArgs(erc20.address, destinationChainId, true); + expect(await spokePool.enabledDepositRoutes(erc20.address, destinationChainId)).to.equal(true); }); it("Change deposit quote buffer", async function () { await expect(spokePool.connect(owner).setDepositQuoteTimeBuffer(60)) diff --git a/test/SpokePool.Deposit.ts b/test/SpokePool.Deposit.ts index c4e963c88..72046c254 100644 --- a/test/SpokePool.Deposit.ts +++ b/test/SpokePool.Deposit.ts @@ -3,7 +3,7 @@ import { Contract } from "ethers"; import { ethers } from "hardhat"; import { SignerWithAddress, seedWallet, toBN, toWei } from "./utils"; import { spokePoolFixture, enableRoutes } from "./SpokePool.Fixture"; -import { amountToSeedWallets, amountToDeposit, depositDestinationChainId, depositRelayerFeePct } from "./constants"; +import { amountToSeedWallets, amountToDeposit, destinationChainId, depositRelayerFeePct } from "./constants"; let spokePool: Contract, weth: Contract, erc20: Contract, unwhitelistedErc20: Contract; let depositor: SignerWithAddress, recipient: SignerWithAddress; @@ -37,7 +37,7 @@ describe("SpokePool Depositor Logic", async function () { .connect(depositor) .deposit( erc20.address, - depositDestinationChainId, + destinationChainId, amountToDeposit, recipient.address, depositRelayerFeePct, @@ -46,7 +46,7 @@ describe("SpokePool Depositor Logic", async function () { ) .to.emit(spokePool, "FundsDeposited") .withArgs( - depositDestinationChainId, + destinationChainId, amountToDeposit, 0, depositRelayerFeePct, @@ -72,7 +72,7 @@ describe("SpokePool Depositor Logic", async function () { .connect(depositor) .deposit( weth.address, - depositDestinationChainId, + destinationChainId, amountToDeposit, recipient.address, depositRelayerFeePct, @@ -86,7 +86,7 @@ describe("SpokePool Depositor Logic", async function () { .connect(depositor) .deposit( weth.address, - depositDestinationChainId, + destinationChainId, amountToDeposit, recipient.address, depositRelayerFeePct, @@ -106,7 +106,7 @@ describe("SpokePool Depositor Logic", async function () { .connect(depositor) .deposit( weth.address, - depositDestinationChainId, + destinationChainId, amountToDeposit, recipient.address, depositRelayerFeePct, @@ -125,7 +125,7 @@ describe("SpokePool Depositor Logic", async function () { .connect(depositor) .deposit( erc20.address, - depositDestinationChainId, + destinationChainId, amountToDeposit, recipient.address, depositRelayerFeePct, @@ -140,7 +140,7 @@ describe("SpokePool Depositor Logic", async function () { .connect(depositor) .deposit( unwhitelistedErc20.address, - depositDestinationChainId, + destinationChainId, amountToDeposit, recipient.address, depositRelayerFeePct, @@ -149,13 +149,13 @@ describe("SpokePool Depositor Logic", async function () { ).to.be.reverted; // Cannot deposit disabled route. - await spokePool.connect(depositor).setEnableRoute(erc20.address, depositDestinationChainId, false); + await spokePool.connect(depositor).setEnableRoute(erc20.address, destinationChainId, false); await expect( spokePool .connect(depositor) .deposit( erc20.address, - depositDestinationChainId, + destinationChainId, amountToDeposit, recipient.address, depositRelayerFeePct, @@ -163,13 +163,13 @@ describe("SpokePool Depositor Logic", async function () { ) ).to.be.reverted; // Re-enable route. - await spokePool.connect(depositor).setEnableRoute(erc20.address, depositDestinationChainId, true); + await spokePool.connect(depositor).setEnableRoute(erc20.address, destinationChainId, true); // Cannot deposit with invalid relayer fee. await expect( spokePool.connect(depositor).deposit( erc20.address, - depositDestinationChainId, + destinationChainId, amountToDeposit, recipient.address, toWei("1"), // Fee > 50% @@ -181,7 +181,7 @@ describe("SpokePool Depositor Logic", async function () { await expect( spokePool.connect(depositor).deposit( erc20.address, - depositDestinationChainId, + destinationChainId, amountToDeposit, recipient.address, depositRelayerFeePct, @@ -191,7 +191,7 @@ describe("SpokePool Depositor Logic", async function () { await expect( spokePool.connect(depositor).deposit( erc20.address, - depositDestinationChainId, + destinationChainId, amountToDeposit, recipient.address, depositRelayerFeePct, diff --git a/test/SpokePool.Fixture.ts b/test/SpokePool.Fixture.ts index 383303161..33ca2004b 100644 --- a/test/SpokePool.Fixture.ts +++ b/test/SpokePool.Fixture.ts @@ -2,7 +2,7 @@ import { TokenRolesEnum } from "@uma/common"; import { Contract, utils } from "ethers"; import { getContractFactory, SignerWithAddress } from "./utils"; import { - depositDestinationChainId, + destinationChainId, depositQuoteTimeBuffer, amountToDeposit, depositRelayerFeePct, @@ -47,7 +47,7 @@ export async function enableRoutes(spokePool: Contract, routes: DepositRoute[]) for (const route of routes) { await spokePool.setEnableRoute( route.originToken, - route.destinationChainId ? route.destinationChainId : depositDestinationChainId, + route.destinationChainId ? route.destinationChainId : destinationChainId, route.enabled !== undefined ? route.enabled : true ); } @@ -64,7 +64,7 @@ export async function deposit( .connect(depositor) .deposit( token.address, - depositDestinationChainId, + destinationChainId, amountToDeposit, recipient.address, depositRelayerFeePct, diff --git a/test/constants.ts b/test/constants.ts index 7474ab5b9..86b2c9192 100644 --- a/test/constants.ts +++ b/test/constants.ts @@ -1,4 +1,4 @@ -import { toWei, utf8ToHex, toBN } from "./utils"; +import { toWei, utf8ToHex, toBN, createRandomBytes32 } from "./utils"; export const amountToSeedWallets = toWei("1500"); @@ -8,8 +8,6 @@ export const amountToDeposit = toWei("100"); export const amountToRelay = toWei("25"); -export const depositDestinationChainId = 10; - export const depositRelayerFeePct = toWei("0.1"); export const realizedLpFeePct = toWei("0.1"); @@ -20,8 +18,12 @@ export const totalPostFeesPct = toBN(oneHundredPct).sub(toBN(depositRelayerFeePc export const amountToRelayPreFees = toBN(amountToRelay).mul(toBN(oneHundredPct)).div(totalPostFeesPct); +export const destinationChainId = 1337; + export const originChainId = 666; + export const repaymentChainId = 777; + export const firstDepositId = 0; export const depositQuoteTimeBuffer = 10 * 60; // 10 minutes @@ -39,3 +41,11 @@ export const zeroBytes32 = "0x00000000000000000000000000000000000000000000000000 export const identifier = utf8ToHex("IS_ACROSS_V2_RELAY_VALID"); export const zeroRawValue = { rawValue: "0" }; + +export const mockBundleEvaluationBlockNumbers = [1, 2, 3]; + +export const mockPoolRebalanceLeafCount = 5; + +export const mockPoolRebalanceRoot = createRandomBytes32(); + +export const mockDestinationDistributionRoot = createRandomBytes32(); diff --git a/test/utils.ts b/test/utils.ts index c64fed50f..cc94e2a44 100644 --- a/test/utils.ts +++ b/test/utils.ts @@ -1,8 +1,8 @@ +import { zeroAddress } from "./constants"; import { getBytecode, getAbi } from "@uma/contracts-node"; import { ethers } from "hardhat"; import { BigNumber, Signer, Contract, ContractFactory } from "ethers"; import { FactoryOptions } from "hardhat/types"; -import hre from "hardhat"; export interface SignerWithAddress extends Signer { address: string; @@ -28,6 +28,8 @@ export async function getContractFactory( export const toWei = (num: string | number | BigNumber) => ethers.utils.parseEther(num.toString()); +export const toBNWei = (num: string | number | BigNumber) => BigNumber.from(toWei(num)); + export const fromWei = (num: string | number | BigNumber) => ethers.utils.formatUnits(num.toString()); export const toBN = (num: string | number | BigNumber) => { @@ -58,5 +60,11 @@ export function randomBigNumber() { } export function randomAddress() { - return ethers.utils.hexlify(ethers.utils.randomBytes(20)); + return ethers.utils.getAddress(ethers.utils.hexlify(ethers.utils.randomBytes(20))); +} + +export async function getParamType(contractName: string, functionName: string, paramName: string) { + const contractFactory = await getContractFactory(contractName, new ethers.VoidSigner(zeroAddress)); + const fragment = contractFactory.interface.fragments.find((fragment) => fragment.name === functionName); + return fragment!.inputs.find((input) => input.name === paramName) || ""; }