From 6b4d8bc8233560770752e4d624fcc06023cb0b3b Mon Sep 17 00:00:00 2001 From: Reinis Martinsons <77973553+Reinis-FRP@users.noreply.github.com> Date: Fri, 18 Oct 2024 11:58:46 +0300 Subject: [PATCH 01/29] feat(chain-adapters): add solana adapter (#641) * feat(chain-adapters): add solana adapter Signed-off-by: Reinis Martinsons * fix: comments Signed-off-by: Reinis Martinsons * test: solana adapter Signed-off-by: Reinis Martinsons * Update contracts/chain-adapters/Solana_Adapter.sol Co-authored-by: Chris Maree * fix: do not hash bytes32 svm address Signed-off-by: Reinis Martinsons --------- Signed-off-by: Reinis Martinsons Co-authored-by: Chris Maree --- contracts/chain-adapters/Solana_Adapter.sol | 193 ++++++++++++++++++ .../external/interfaces/CCTPInterfaces.sol | 21 ++ contracts/libraries/CircleCCTPAdapter.sol | 14 +- test/evm/hardhat/MerkleLib.utils.ts | 13 +- .../hardhat/chain-adapters/Solana_Adapter.ts | 140 +++++++++++++ utils/abis.ts | 14 ++ utils/utils.ts | 9 + 7 files changed, 398 insertions(+), 6 deletions(-) create mode 100644 contracts/chain-adapters/Solana_Adapter.sol create mode 100644 test/evm/hardhat/chain-adapters/Solana_Adapter.ts diff --git a/contracts/chain-adapters/Solana_Adapter.sol b/contracts/chain-adapters/Solana_Adapter.sol new file mode 100644 index 000000000..8708889e7 --- /dev/null +++ b/contracts/chain-adapters/Solana_Adapter.sol @@ -0,0 +1,193 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +import { IMessageTransmitter, ITokenMessenger } from "../external/interfaces/CCTPInterfaces.sol"; +import { SpokePoolInterface } from "../interfaces/SpokePoolInterface.sol"; +import { AdapterInterface } from "./interfaces/AdapterInterface.sol"; +import { CircleCCTPAdapter, CircleDomainIds } from "../libraries/CircleCCTPAdapter.sol"; + +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +/** + * @notice Contract containing logic to send messages from L1 to Solana via CCTP. + * @dev Public functions calling external contracts do not guard against reentrancy because they are expected to be + * called via delegatecall, which will execute this contract's logic within the context of the originating contract. + * For example, the HubPool will delegatecall these functions, therefore it's only necessary that the HubPool's methods + * that call this contract's logic guard against reentrancy. + * @custom:security-contact bugs@across.to + */ + +// solhint-disable-next-line contract-name-camelcase +contract Solana_Adapter is AdapterInterface, CircleCCTPAdapter { + /** + * @notice The official Circle CCTP MessageTransmitter contract endpoint. + * @dev Posted officially here: https://developers.circle.com/stablecoins/docs/evm-smart-contracts + */ + // solhint-disable-next-line immutable-vars-naming + IMessageTransmitter public immutable cctpMessageTransmitter; + + // Solana spoke pool address, decoded from Base58 to bytes32. + bytes32 public immutable SOLANA_SPOKE_POOL_BYTES32; + + // Solana spoke pool address, mapped to its EVM address representation. + address public immutable SOLANA_SPOKE_POOL_ADDRESS; + + // USDC mint address on Solana, decoded from Base58 to bytes32. + bytes32 public immutable SOLANA_USDC_BYTES32; + + // USDC mint address on Solana, mapped to its EVM address representation. + address public immutable SOLANA_USDC_ADDRESS; + + // USDC token address on Solana for the spoke pool (vault ATA), decoded from Base58 to bytes32. + bytes32 public immutable SOLANA_SPOKE_POOL_USDC_VAULT; + + // Custom errors for constructor argument validation. + error InvalidCctpTokenMessenger(address tokenMessenger); + error InvalidCctpMessageTransmitter(address messageTransmitter); + + // Custom errors for relayMessage validation. + error InvalidRelayMessageTarget(address target); + error InvalidOriginToken(address originToken); + error InvalidDestinationChainId(uint256 destinationChainId); + + // Custom errors for relayTokens validation. + error InvalidL1Token(address l1Token); + error InvalidL2Token(address l2Token); + error InvalidAmount(uint256 amount); + error InvalidTokenRecipient(address to); + + /** + * @notice Constructs new Adapter. + * @param _l1Usdc USDC address on L1. + * @param _cctpTokenMessenger TokenMessenger contract to bridge tokens via CCTP. + * @param _cctpMessageTransmitter MessageTransmitter contract to bridge messages via CCTP. + * @param solanaSpokePool Solana spoke pool address, decoded from Base58 to bytes32. + * @param solanaUsdc USDC mint address on Solana, decoded from Base58 to bytes32. + * @param solanaSpokePoolUsdcVault USDC token address on Solana for the spoke pool, decoded from Base58 to bytes32. + */ + constructor( + IERC20 _l1Usdc, + ITokenMessenger _cctpTokenMessenger, + IMessageTransmitter _cctpMessageTransmitter, + bytes32 solanaSpokePool, + bytes32 solanaUsdc, + bytes32 solanaSpokePoolUsdcVault + ) CircleCCTPAdapter(_l1Usdc, _cctpTokenMessenger, CircleDomainIds.Solana) { + // Solana adapter requires CCTP TokenMessenger and MessageTransmitter contracts to be set. + if (address(_cctpTokenMessenger) == address(0)) { + revert InvalidCctpTokenMessenger(address(_cctpTokenMessenger)); + } + if (address(_cctpMessageTransmitter) == address(0)) { + revert InvalidCctpMessageTransmitter(address(_cctpMessageTransmitter)); + } + + cctpMessageTransmitter = _cctpMessageTransmitter; + + SOLANA_SPOKE_POOL_BYTES32 = solanaSpokePool; + SOLANA_SPOKE_POOL_ADDRESS = _trimSolanaAddress(solanaSpokePool); + + SOLANA_USDC_BYTES32 = solanaUsdc; + SOLANA_USDC_ADDRESS = _trimSolanaAddress(solanaUsdc); + + SOLANA_SPOKE_POOL_USDC_VAULT = solanaSpokePoolUsdcVault; + } + + /** + * @notice Send cross-chain message to target on Solana. + * @dev Only allows sending messages to the Solana spoke pool. + * @param target Program on Solana (translated as EVM address) that will receive message. + * @param message Data to send to target. + */ + function relayMessage(address target, bytes calldata message) external payable override { + if (target != SOLANA_SPOKE_POOL_ADDRESS) { + revert InvalidRelayMessageTarget(target); + } + + bytes4 selector = bytes4(message[:4]); + if (selector == SpokePoolInterface.setEnableRoute.selector) { + cctpMessageTransmitter.sendMessage( + CircleDomainIds.Solana, + SOLANA_SPOKE_POOL_BYTES32, + _translateSetEnableRoute(message) + ); + } else { + cctpMessageTransmitter.sendMessage(CircleDomainIds.Solana, SOLANA_SPOKE_POOL_BYTES32, message); + } + + // TODO: consider if we need also to emit the translated message. + emit MessageRelayed(target, message); + } + + /** + * @notice Bridge tokens to Solana. + * @dev Only allows bridging USDC to Solana spoke pool. + * @param l1Token L1 token to deposit. + * @param l2Token L2 token to receive. + * @param amount Amount of L1 tokens to deposit and L2 tokens to receive. + * @param to Bridge recipient. + */ + function relayTokens( + address l1Token, + address l2Token, + uint256 amount, + address to + ) external payable override { + if (l1Token != address(usdcToken)) { + revert InvalidL1Token(l1Token); + } + if (l2Token != SOLANA_USDC_ADDRESS) { + revert InvalidL2Token(l2Token); + } + if (amount > type(uint64).max) { + revert InvalidAmount(amount); + } + if (to != SOLANA_SPOKE_POOL_ADDRESS) { + revert InvalidTokenRecipient(to); + } + + _transferUsdc(SOLANA_SPOKE_POOL_USDC_VAULT, amount); + + // TODO: consider if we need also to emit the translated addresses. + emit TokensRelayed(l1Token, l2Token, amount, to); + } + + /** + * @notice Helper to map a Solana address to an Ethereum address representation. + * @dev The Ethereum address is derived from the Solana address by truncating it to its lowest 20 bytes. This same + * conversion must be done by the HubPool owner when adding Solana spoke pool and setting the corresponding pool + * rebalance and deposit routes. + * @param solanaAddress Solana address (Base58 decoded to bytes32) to map to its Ethereum address representation. + * @return Ethereum address representation of the Solana address. + */ + function _trimSolanaAddress(bytes32 solanaAddress) internal pure returns (address) { + return address(uint160(uint256(solanaAddress))); + } + + /** + * @notice Translates a message to enable/disable a route on Solana spoke pool. + * @param message Message to translate, expecting setEnableRoute(address,uint256,bool). + * @return Translated message, using setEnableRoute(bytes32,uint64,bool). + */ + function _translateSetEnableRoute(bytes calldata message) internal view returns (bytes memory) { + (address originToken, uint256 destinationChainId, bool enable) = abi.decode( + message[4:], + (address, uint256, bool) + ); + + if (originToken != SOLANA_USDC_ADDRESS) { + revert InvalidOriginToken(originToken); + } + + if (destinationChainId > type(uint64).max) { + revert InvalidDestinationChainId(destinationChainId); + } + + return + abi.encodeWithSignature( + "setEnableRoute(bytes32,uint64,bool)", + SOLANA_USDC_BYTES32, + uint64(destinationChainId), + enable + ); + } +} diff --git a/contracts/external/interfaces/CCTPInterfaces.sol b/contracts/external/interfaces/CCTPInterfaces.sol index 8431bbfdc..e932d943a 100644 --- a/contracts/external/interfaces/CCTPInterfaces.sol +++ b/contracts/external/interfaces/CCTPInterfaces.sol @@ -74,3 +74,24 @@ interface ITokenMinter { */ function burnLimitsPerMessage(address token) external view returns (uint256); } + +/** + * IMessageTransmitter in CCTP inherits IRelayer and IReceiver, but here we only import sendMessage from IRelayer: + * https://github.com/circlefin/evm-cctp-contracts/blob/377c9bd813fb86a42d900ae4003599d82aef635a/src/interfaces/IMessageTransmitter.sol#L25 + * https://github.com/circlefin/evm-cctp-contracts/blob/377c9bd813fb86a42d900ae4003599d82aef635a/src/interfaces/IRelayer.sol#L23-L35 + */ +interface IMessageTransmitter { + /** + * @notice Sends an outgoing message from the source domain. + * @dev Increment nonce, format the message, and emit `MessageSent` event with message information. + * @param destinationDomain Domain of destination chain + * @param recipient Address of message recipient on destination domain as bytes32 + * @param messageBody Raw bytes content of message + * @return nonce reserved by message + */ + function sendMessage( + uint32 destinationDomain, + bytes32 recipient, + bytes calldata messageBody + ) external returns (uint64); +} diff --git a/contracts/libraries/CircleCCTPAdapter.sol b/contracts/libraries/CircleCCTPAdapter.sol index 6403ed4c4..f1193fdb0 100644 --- a/contracts/libraries/CircleCCTPAdapter.sol +++ b/contracts/libraries/CircleCCTPAdapter.sol @@ -9,6 +9,7 @@ library CircleDomainIds { uint32 public constant Ethereum = 0; uint32 public constant Optimism = 2; uint32 public constant Arbitrum = 3; + uint32 public constant Solana = 5; uint32 public constant Base = 6; uint32 public constant Polygon = 7; // Use this value for placeholder purposes only for adapters that extend this adapter but haven't yet been @@ -87,6 +88,16 @@ abstract contract CircleCCTPAdapter { * @param amount Amount of USDC to transfer. */ function _transferUsdc(address to, uint256 amount) internal { + _transferUsdc(_addressToBytes32(to), amount); + } + + /** + * @notice Transfers USDC from the current domain to the given address on the new domain. + * @dev This function will revert if the CCTP bridge is disabled. I.e. if the zero address is passed to the constructor for the cctpTokenMessenger. + * @param to Address to receive USDC on the new domain represented as bytes32. + * @param amount Amount of USDC to transfer. + */ + function _transferUsdc(bytes32 to, uint256 amount) internal { // Only approve the exact amount to be transferred usdcToken.safeIncreaseAllowance(address(cctpTokenMessenger), amount); // Submit the amount to be transferred to bridged via the TokenMessenger. @@ -94,10 +105,9 @@ abstract contract CircleCCTPAdapter { ITokenMinter cctpMinter = cctpTokenMessenger.localMinter(); uint256 burnLimit = cctpMinter.burnLimitsPerMessage(address(usdcToken)); uint256 remainingAmount = amount; - bytes32 recipient = _addressToBytes32(to); while (remainingAmount > 0) { uint256 partAmount = remainingAmount > burnLimit ? burnLimit : remainingAmount; - cctpTokenMessenger.depositForBurn(partAmount, recipientCircleDomainId, recipient, address(usdcToken)); + cctpTokenMessenger.depositForBurn(partAmount, recipientCircleDomainId, to, address(usdcToken)); remainingAmount -= partAmount; } } diff --git a/test/evm/hardhat/MerkleLib.utils.ts b/test/evm/hardhat/MerkleLib.utils.ts index 4886f1cb3..20a0e43d1 100644 --- a/test/evm/hardhat/MerkleLib.utils.ts +++ b/test/evm/hardhat/MerkleLib.utils.ts @@ -4,7 +4,7 @@ import { BigNumber, defaultAbiCoder, keccak256, - toBNWei, + toBNWeiWithDecimals, createRandomBytes32, Contract, } from "../../../utils/utils"; @@ -119,9 +119,14 @@ export async function constructSingleRelayerRefundTree(l2Token: Contract | Strin return { leaves, tree }; } -export async function constructSingleChainTree(token: string, scalingSize = 1, repaymentChain = repaymentChainId) { - const tokensSendToL2 = toBNWei(100 * scalingSize); - const realizedLpFees = toBNWei(10 * scalingSize); +export async function constructSingleChainTree( + token: string, + scalingSize = 1, + repaymentChain = repaymentChainId, + decimals = 18 +) { + const tokensSendToL2 = toBNWeiWithDecimals(100 * scalingSize, decimals); + const realizedLpFees = toBNWeiWithDecimals(10 * scalingSize, decimals); const leaves = buildPoolRebalanceLeaves( [repaymentChain], // repayment chain. In this test we only want to send one token to one chain. [[token]], // l1Token. We will only be sending 1 token to one chain. diff --git a/test/evm/hardhat/chain-adapters/Solana_Adapter.ts b/test/evm/hardhat/chain-adapters/Solana_Adapter.ts new file mode 100644 index 000000000..a86a8fda8 --- /dev/null +++ b/test/evm/hardhat/chain-adapters/Solana_Adapter.ts @@ -0,0 +1,140 @@ +/* eslint-disable no-unused-expressions */ +import { + amountToLp, + refundProposalLiveness, + bondAmount, + mockRelayerRefundRoot, + mockSlowRelayRoot, +} from "./../constants"; +import { + ethers, + expect, + Contract, + createFakeFromABI, + FakeContract, + SignerWithAddress, + getContractFactory, + seedWallet, + randomAddress, + createRandomBytes32, + trimSolanaAddress, + toWeiWithDecimals, +} from "../../../../utils/utils"; +import { hubPoolFixture, enableTokensForLP } from "../fixtures/HubPool.Fixture"; +import { constructSingleChainTree } from "../MerkleLib.utils"; +import { + CCTPTokenMessengerInterface, + CCTPTokenMinterInterface, + CCTPMessageTransmitterInterface, +} from "../../../../utils/abis"; + +let hubPool: Contract, solanaAdapter: Contract, weth: Contract, usdc: Contract, timer: Contract, mockSpoke: Contract; +let owner: SignerWithAddress, dataWorker: SignerWithAddress, liquidityProvider: SignerWithAddress; +let cctpTokenMessenger: FakeContract, cctpMessageTransmitter: FakeContract, cctpTokenMinter: FakeContract; +let solanaSpokePoolBytes32: string, + solanaUsdcBytes32: string, + solanaSpokePoolUsdcVaultBytes32: string, + solanaSpokePoolAddress: string, + solanaUsdcAddress: string; + +const solanaChainId = 1234567890; // TODO: Decide how to represent Solana in Across as it does not have a chainId. +const solanaDomainId = 5; + +describe("Solana Chain Adapter", function () { + beforeEach(async function () { + [owner, dataWorker, liquidityProvider] = await ethers.getSigners(); + ({ weth, hubPool, mockSpoke, timer, usdc } = await hubPoolFixture()); + await seedWallet(dataWorker, [usdc], weth, amountToLp); + await seedWallet(liquidityProvider, [usdc], weth, amountToLp.mul(10)); + + await enableTokensForLP(owner, hubPool, weth, [weth, usdc]); + for (const token of [weth, usdc]) { + await token.connect(liquidityProvider).approve(hubPool.address, amountToLp); + await hubPool.connect(liquidityProvider).addLiquidity(token.address, amountToLp); + await token.connect(dataWorker).approve(hubPool.address, bondAmount.mul(10)); + } + + cctpTokenMessenger = await createFakeFromABI(CCTPTokenMessengerInterface); + cctpMessageTransmitter = await createFakeFromABI(CCTPMessageTransmitterInterface); + cctpTokenMinter = await createFakeFromABI(CCTPTokenMinterInterface); + cctpTokenMessenger.localMinter.returns(cctpTokenMinter.address); + cctpTokenMinter.burnLimitsPerMessage.returns(toWeiWithDecimals("1000000", 6)); + + solanaSpokePoolBytes32 = createRandomBytes32(); + solanaUsdcBytes32 = createRandomBytes32(); + solanaSpokePoolUsdcVaultBytes32 = createRandomBytes32(); + + solanaSpokePoolAddress = trimSolanaAddress(solanaSpokePoolBytes32); + solanaUsdcAddress = trimSolanaAddress(solanaUsdcBytes32); + + solanaAdapter = await ( + await getContractFactory("Solana_Adapter", owner) + ).deploy( + usdc.address, + cctpTokenMessenger.address, + cctpMessageTransmitter.address, + solanaSpokePoolBytes32, + solanaUsdcBytes32, + solanaSpokePoolUsdcVaultBytes32 + ); + + await hubPool.setCrossChainContracts(solanaChainId, solanaAdapter.address, solanaSpokePoolAddress); + await hubPool.setPoolRebalanceRoute(solanaChainId, usdc.address, solanaUsdcAddress); + }); + + it("relayMessage calls spoke pool functions", async function () { + const newAdmin = randomAddress(); + const functionCallData = mockSpoke.interface.encodeFunctionData("setCrossDomainAdmin", [newAdmin]); + expect(await hubPool.relaySpokePoolAdminFunction(solanaChainId, functionCallData)) + .to.emit(solanaAdapter.attach(hubPool.address), "MessageRelayed") + .withArgs(solanaSpokePoolAddress, functionCallData); + expect(cctpMessageTransmitter.sendMessage).to.have.been.calledWith( + solanaDomainId, + solanaSpokePoolBytes32, + functionCallData + ); + }); + + it("Correctly calls the CCTP bridge adapter when attempting to bridge USDC", 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 { leaves, tree, tokensSendToL2 } = await constructSingleChainTree(usdc.address, 1, solanaChainId, 6); + await hubPool + .connect(dataWorker) + .proposeRootBundle([3117], 1, tree.getHexRoot(), mockRelayerRefundRoot, mockSlowRelayRoot); + await timer.setCurrentTime(Number(await timer.getCurrentTime()) + refundProposalLiveness + 1); + await hubPool.connect(dataWorker).executeRootBundle(...Object.values(leaves[0]), tree.getHexProof(leaves[0])); + + // Adapter should have approved CCTP TokenMessenger to spend its ERC20, but the fake instance does not pull them. + expect(await usdc.allowance(hubPool.address, cctpTokenMessenger.address)).to.equal(tokensSendToL2); + + // The correct functions should have been called on the CCTP TokenMessenger contract + expect(cctpTokenMessenger.depositForBurn).to.have.been.calledOnce; + expect(cctpTokenMessenger.depositForBurn).to.have.been.calledWith( + ethers.BigNumber.from(tokensSendToL2), + solanaDomainId, + solanaSpokePoolUsdcVaultBytes32, + usdc.address + ); + }); + + it("Correctly translates setEnableRoute calls to the spoke pool", async function () { + // Enable deposits for USDC on Solana. + const destinationChainId = 1; + const depositsEnabled = true; + await hubPool.setDepositRoute(solanaChainId, destinationChainId, solanaUsdcAddress, depositsEnabled); + + // Solana spoke pool expects to receive full bytes32 token address and uint64 for chainId. + const solanaInterface = new ethers.utils.Interface(["function setEnableRoute(bytes32, uint64, bool)"]); + const solanaMessage = solanaInterface.encodeFunctionData("setEnableRoute", [ + solanaUsdcBytes32, + destinationChainId, + depositsEnabled, + ]); + expect(cctpMessageTransmitter.sendMessage).to.have.been.calledWith( + solanaDomainId, + solanaSpokePoolBytes32, + solanaMessage + ); + }); +}); diff --git a/utils/abis.ts b/utils/abis.ts index 7a52b6f1c..f33c69027 100644 --- a/utils/abis.ts +++ b/utils/abis.ts @@ -51,3 +51,17 @@ export const CCTPTokenMinterInterface = [ type: "function", }, ]; + +export const CCTPMessageTransmitterInterface = [ + { + inputs: [ + { internalType: "uint32", name: "destinationDomain", type: "uint32" }, + { internalType: "bytes32", name: "recipient", type: "bytes32" }, + { internalType: "bytes", name: "messageBody", type: "bytes" }, + ], + name: "sendMessage", + outputs: [{ internalType: "uint64", name: "", type: "uint64" }], + stateMutability: "nonpayable", + type: "function", + }, +]; diff --git a/utils/utils.ts b/utils/utils.ts index 215580421..f526a29c4 100644 --- a/utils/utils.ts +++ b/utils/utils.ts @@ -166,6 +166,15 @@ function avmL1ToL2Alias(l1Address: string) { return ethers.utils.hexlify(l2AddressAsNumber.mod(mask)); } +export function trimSolanaAddress(bytes32Address: string): string { + if (!ethers.utils.isHexString(bytes32Address, 32)) { + throw new Error("Invalid bytes32 address"); + } + + const uint160Address = ethers.BigNumber.from(bytes32Address).mask(160); + return ethers.utils.hexZeroPad(ethers.utils.hexlify(uint160Address), 20); +} + const { defaultAbiCoder, keccak256 } = ethers.utils; export { avmL1ToL2Alias, expect, Contract, ethers, BigNumber, defaultAbiCoder, keccak256, FakeContract, Signer }; From 5a54ddb66ec2e8a6c028d5e5a1719bc89ff0b30a Mon Sep 17 00:00:00 2001 From: Pablo Maldonado Date: Mon, 21 Oct 2024 13:30:58 +0100 Subject: [PATCH 02/29] feat: address to bytes32 contract changes (#650) * feat: add address to bytes32 contract changes Signed-off-by: Pablo Maldonado * refactor: remove todos Signed-off-by: Pablo Maldonado * refactor: imports Signed-off-by: Pablo Maldonado * Update contracts/SpokePool.sol Co-authored-by: Reinis Martinsons <77973553+Reinis-FRP@users.noreply.github.com> * feat: bytes 32 comparisons Signed-off-by: Pablo Maldonado * refactor: format code Signed-off-by: Pablo Maldonado * fix: tests Signed-off-by: Pablo Maldonado * feat: bytes 32 check Signed-off-by: Pablo Maldonado * fix: ts Signed-off-by: Pablo Maldonado * feat: reuse lib in cctp adapter Signed-off-by: Pablo Maldonado * feat: _preExecuteLeafHook Signed-off-by: Pablo Maldonado * refactor: comments Signed-off-by: Pablo Maldonado * feat: _verifyUpdateV3DepositMessage Signed-off-by: Pablo Maldonado * feat: backward compatibility Signed-off-by: Pablo Maldonado * feat: backwards compatibility tests Signed-off-by: Pablo Maldonado * feat: change comparison casting address bytes32 Signed-off-by: Pablo Maldonado * fix: test Signed-off-by: Pablo Maldonado * feat: merkle tree leaf to bytes32 Signed-off-by: Pablo Maldonado * test: leaf type update fixes Signed-off-by: Pablo Maldonado * feat: remove helper Signed-off-by: Pablo Maldonado --------- Signed-off-by: Pablo Maldonado Co-authored-by: Reinis Martinsons <77973553+Reinis-FRP@users.noreply.github.com> --- contracts/Linea_SpokePool.sol | 5 +- contracts/Ovm_SpokePool.sol | 6 +- contracts/PolygonZkEVM_SpokePool.sol | 7 +- contracts/Polygon_SpokePool.sol | 7 +- contracts/SpokePool.sol | 340 ++++++++++++++---- contracts/SpokePoolVerifier.sol | 12 +- contracts/SwapAndBridge.sol | 61 ++-- contracts/ZkSync_SpokePool.sol | 5 +- contracts/erc7683/ERC7683.sol | 6 +- contracts/erc7683/ERC7683Across.sol | 24 +- contracts/erc7683/ERC7683OrderDepositor.sol | 57 +-- .../erc7683/ERC7683OrderDepositorExternal.sol | 27 +- contracts/interfaces/SpokePoolInterface.sol | 4 +- contracts/interfaces/V3SpokePoolInterface.sol | 80 ++--- contracts/libraries/AddressConverters.sol | 15 + contracts/libraries/CircleCCTPAdapter.sol | 15 +- contracts/permit2-order/Permit2Depositor.sol | 12 +- contracts/test/MockSpokePool.sol | 114 ++++-- .../interfaces/MockV2SpokePoolInterface.sol | 8 +- .../local/MultiCallerUpgradeable.t.sol | 13 +- .../evm/foundry/local/SpokePoolVerifier.t.sol | 58 ++- test/evm/hardhat/MerkleLib.Proofs.ts | 17 +- test/evm/hardhat/MerkleLib.utils.ts | 3 +- test/evm/hardhat/SpokePool.Admin.ts | 11 +- test/evm/hardhat/SpokePool.Deposit.ts | 309 +++++++++++----- .../hardhat/SpokePool.ExecuteRootBundle.ts | 45 ++- test/evm/hardhat/SpokePool.Relay.ts | 164 ++++----- test/evm/hardhat/SpokePool.SlowRelay.ts | 49 ++- .../Arbitrum_SpokePool.ts | 3 +- .../Ethereum_SpokePool.ts | 12 +- .../Optimism_SpokePool.ts | 3 +- .../Polygon_SpokePool.ts | 17 +- .../evm/hardhat/fixtures/SpokePool.Fixture.ts | 8 +- utils/utils.ts | 17 + 34 files changed, 1005 insertions(+), 529 deletions(-) create mode 100644 contracts/libraries/AddressConverters.sol diff --git a/contracts/Linea_SpokePool.sol b/contracts/Linea_SpokePool.sol index 4327f40b7..562b6c6df 100644 --- a/contracts/Linea_SpokePool.sol +++ b/contracts/Linea_SpokePool.sol @@ -15,6 +15,7 @@ import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; */ contract Linea_SpokePool is SpokePool { using SafeERC20 for IERC20; + using AddressToBytes32 for address; /** * @notice Address of Linea's Canonical Message Service contract on L2. @@ -122,8 +123,8 @@ contract Linea_SpokePool is SpokePool { * @notice Wraps any ETH into WETH before executing base function. This is necessary because SpokePool receives * ETH over the canonical token bridge instead of WETH. */ - function _preExecuteLeafHook(address l2TokenAddress) internal override { - if (l2TokenAddress == address(wrappedNativeToken)) _depositEthToWeth(); + function _preExecuteLeafHook(bytes32 l2TokenAddress) internal override { + if (l2TokenAddress == address(wrappedNativeToken).toBytes32()) _depositEthToWeth(); } // Wrap any ETH owned by this contract so we can send expected L2 token to recipient. This is necessary because diff --git a/contracts/Ovm_SpokePool.sol b/contracts/Ovm_SpokePool.sol index a3abbfd01..90d709573 100644 --- a/contracts/Ovm_SpokePool.sol +++ b/contracts/Ovm_SpokePool.sol @@ -34,8 +34,10 @@ interface IL2ERC20Bridge { */ contract Ovm_SpokePool is SpokePool, CircleCCTPAdapter { using SafeERC20 for IERC20; + using AddressToBytes32 for address; // "l1Gas" parameter used in call to bridge tokens from this contract back to L1 via IL2ERC20Bridge. Currently // unused by bridge but included for future compatibility. + uint32 public l1Gas; // ETH is an ERC20 on OVM. @@ -133,8 +135,8 @@ contract Ovm_SpokePool is SpokePool, CircleCCTPAdapter { * @notice Wraps any ETH into WETH before executing leaves. This is necessary because SpokePool receives * ETH over the canonical token bridge instead of WETH. */ - function _preExecuteLeafHook(address l2TokenAddress) internal override { - if (l2TokenAddress == address(wrappedNativeToken)) _depositEthToWeth(); + function _preExecuteLeafHook(bytes32 l2TokenAddress) internal override { + if (l2TokenAddress == address(wrappedNativeToken).toBytes32()) _depositEthToWeth(); } // Wrap any ETH owned by this contract so we can send expected L2 token to recipient. This is necessary because diff --git a/contracts/PolygonZkEVM_SpokePool.sol b/contracts/PolygonZkEVM_SpokePool.sol index 54251d88c..1817dba35 100644 --- a/contracts/PolygonZkEVM_SpokePool.sol +++ b/contracts/PolygonZkEVM_SpokePool.sol @@ -31,8 +31,9 @@ interface IBridgeMessageReceiver { */ contract PolygonZkEVM_SpokePool is SpokePool, IBridgeMessageReceiver { using SafeERC20 for IERC20; - + using AddressToBytes32 for address; // Address of Polygon zkEVM's Canonical Bridge on L2. + IPolygonZkEVMBridge public l2PolygonZkEVMBridge; // Polygon zkEVM's internal network id for L1. @@ -157,8 +158,8 @@ contract PolygonZkEVM_SpokePool is SpokePool, IBridgeMessageReceiver { * @notice Wraps any ETH into WETH before executing base function. This is necessary because SpokePool receives * ETH over the canonical token bridge instead of WETH. */ - function _preExecuteLeafHook(address l2TokenAddress) internal override { - if (l2TokenAddress == address(wrappedNativeToken)) _depositEthToWeth(); + function _preExecuteLeafHook(bytes32 l2TokenAddress) internal override { + if (l2TokenAddress == address(wrappedNativeToken).toBytes32()) _depositEthToWeth(); } // Wrap any ETH owned by this contract so we can send expected L2 token to recipient. This is necessary because diff --git a/contracts/Polygon_SpokePool.sol b/contracts/Polygon_SpokePool.sol index a1d630ded..6cebd506c 100644 --- a/contracts/Polygon_SpokePool.sol +++ b/contracts/Polygon_SpokePool.sol @@ -146,7 +146,8 @@ contract Polygon_SpokePool is IFxMessageProcessor, SpokePool, CircleCCTPAdapter * @param data ABI encoded function call to execute on this contract. */ function processMessageFromRoot( - uint256, /*stateId*/ + uint256, + /*stateId*/ address rootMessageSender, bytes calldata data ) public validateInternalCalls { @@ -203,7 +204,6 @@ contract Polygon_SpokePool is IFxMessageProcessor, SpokePool, CircleCCTPAdapter * whereby someone batches this call with a bunch of other calls and produces a very large L2 burn transaction. * This might make the L2 -> L1 message fail due to exceeding the L1 calldata limit. */ - function executeRelayerRefundLeaf( uint32 rootBundleId, SpokePoolInterface.RelayerRefundLeaf memory relayerRefundLeaf, @@ -220,7 +220,6 @@ contract Polygon_SpokePool is IFxMessageProcessor, SpokePool, CircleCCTPAdapter /************************************** * INTERNAL FUNCTIONS * **************************************/ - function _setFxChild(address _fxChild) internal { //slither-disable-next-line missing-zero-check fxChild = _fxChild; @@ -232,7 +231,7 @@ contract Polygon_SpokePool is IFxMessageProcessor, SpokePool, CircleCCTPAdapter emit SetPolygonTokenBridger(address(_polygonTokenBridger)); } - function _preExecuteLeafHook(address) internal override { + function _preExecuteLeafHook(bytes32) internal override { // Wraps MATIC --> WMATIC before distributing tokens from this contract. _wrap(); } diff --git a/contracts/SpokePool.sol b/contracts/SpokePool.sol index a9720518a..c70bd4f5f 100644 --- a/contracts/SpokePool.sol +++ b/contracts/SpokePool.sol @@ -10,6 +10,7 @@ import "./interfaces/V3SpokePoolInterface.sol"; import "./upgradeable/MultiCallerUpgradeable.sol"; import "./upgradeable/EIP712CrossChainUpgradeable.sol"; import "./upgradeable/AddressLibUpgradeable.sol"; +import "./libraries/AddressConverters.sol"; import "@openzeppelin/contracts-upgradeable/token/ERC20/IERC20Upgradeable.sol"; import "@openzeppelin/contracts-upgradeable/token/ERC20/utils/SafeERC20Upgradeable.sol"; @@ -39,6 +40,8 @@ abstract contract SpokePool is { using SafeERC20Upgradeable for IERC20Upgradeable; using AddressLibUpgradeable for address; + using Bytes32ToAddress for bytes32; + using AddressToBytes32 for address; // Address of the L1 contract that acts as the owner of this SpokePool. This should normally be set to the HubPool // address. The crossDomainAdmin address is unused when the SpokePool is deployed to the same chain as the HubPool. @@ -67,7 +70,7 @@ abstract contract SpokePool is RootBundle[] public rootBundles; // Origin token to destination token routings can be turned on or off, which can enable or disable deposits. - mapping(address => mapping(uint256 => bool)) public enabledDepositRoutes; + mapping(bytes32 => mapping(uint256 => bool)) public enabledDepositRoutes; // Each relay is associated with the hash of parameters that uniquely identify the original deposit and a relay // attempt for that deposit. The relay itself is just represented as the amount filled so far. The total amount to @@ -126,6 +129,11 @@ abstract contract SpokePool is uint256 public constant MAX_TRANSFER_SIZE = 1e36; bytes32 public constant UPDATE_V3_DEPOSIT_DETAILS_HASH = + keccak256( + "UpdateDepositDetails(uint32 depositId,uint256 originChainId,uint256 updatedOutputAmount,bytes32 updatedRecipient,bytes updatedMessage)" + ); + + bytes32 public constant UPDATE_V3_DEPOSIT_ADDRESS_OVERLOAD_DETAILS_HASH = keccak256( "UpdateDepositDetails(uint32 depositId,uint256 originChainId,uint256 updatedOutputAmount,address updatedRecipient,bytes updatedMessage)" ); @@ -134,7 +142,7 @@ abstract contract SpokePool is uint256 public constant EMPTY_REPAYMENT_CHAIN_ID = 0; // Default address used to signify that no relayer should be credited with a refund, for example // when executing a slow fill. - address public constant EMPTY_RELAYER = address(0); + bytes32 public constant EMPTY_RELAYER = bytes32(0); // This is the magic value that signals to the off-chain validator // that this deposit can never expire. A deposit with this fill deadline should always be eligible for a // slow fill, meaning that its output token and input token must be "equivalent". Therefore, this value is only @@ -160,15 +168,15 @@ abstract contract SpokePool is uint256[] refundAmounts, uint32 indexed rootBundleId, uint32 indexed leafId, - address l2TokenAddress, - address[] refundAddresses, + bytes32 l2TokenAddress, + bytes32[] refundAddresses, address caller ); event TokensBridged( uint256 amountToReturn, uint256 indexed chainId, uint32 indexed leafId, - address indexed l2TokenAddress, + bytes32 indexed l2TokenAddress, address caller ); event EmergencyDeleteRootBundle(uint256 indexed rootBundleId); @@ -193,8 +201,8 @@ abstract contract SpokePool is event RequestedSpeedUpDeposit( int64 newRelayerFeePct, uint32 indexed depositId, - address indexed depositor, - address updatedRecipient, + bytes32 indexed depositor, + bytes32 updatedRecipient, bytes updatedMessage, bytes depositorSignature ); @@ -209,10 +217,10 @@ abstract contract SpokePool is int64 relayerFeePct, int64 realizedLpFeePct, uint32 indexed depositId, - address destinationToken, - address relayer, - address indexed depositor, - address recipient, + bytes32 destinationToken, + bytes32 relayer, + bytes32 indexed depositor, + bytes32 recipient, bytes message, RelayExecutionInfo updatableRelayData ); @@ -227,8 +235,9 @@ abstract contract SpokePool is * @param payoutAdjustmentPct Adjustment to the payout amount. */ /// @custom:audit FOLLOWING STRUCT TO BE DEPRECATED + struct RelayExecutionInfo { - address recipient; + bytes32 recipient; bytes message; int64 relayerFeePct; bool isSlowRelay; @@ -366,7 +375,7 @@ abstract contract SpokePool is uint256 destinationChainId, bool enabled ) public override onlyAdmin nonReentrant { - enabledDepositRoutes[originToken][destinationChainId] = enabled; + enabledDepositRoutes[originToken.toBytes32()][destinationChainId] = enabled; emit EnabledDepositRoute(originToken, destinationChainId, enabled); } @@ -438,9 +447,9 @@ abstract contract SpokePool is uint256 // maxCount. Deprecated. ) public payable override nonReentrant unpausedDeposits { _deposit( - msg.sender, - recipient, - originToken, + msg.sender.toBytes32(), + recipient.toBytes32(), + originToken.toBytes32(), amount, destinationChainId, relayerFeePct, @@ -480,7 +489,16 @@ abstract contract SpokePool is bytes memory message, uint256 // maxCount. Deprecated. ) public payable nonReentrant unpausedDeposits { - _deposit(depositor, recipient, originToken, amount, destinationChainId, relayerFeePct, quoteTimestamp, message); + _deposit( + depositor.toBytes32(), + recipient.toBytes32(), + originToken.toBytes32(), + amount, + destinationChainId, + relayerFeePct, + quoteTimestamp, + message + ); } /******************************************** @@ -538,14 +556,14 @@ abstract contract SpokePool is * If the message is not empty, the recipient contract must implement handleV3AcrossMessage() or the fill will revert. */ function depositV3( - address depositor, - address recipient, - address inputToken, - address outputToken, + bytes32 depositor, + bytes32 recipient, + bytes32 inputToken, + bytes32 outputToken, uint256 inputAmount, uint256 outputAmount, uint256 destinationChainId, - address exclusiveRelayer, + bytes32 exclusiveRelayer, uint32 quoteTimestamp, uint32 fillDeadline, uint32 exclusivityPeriod, @@ -578,7 +596,7 @@ abstract contract SpokePool is // If the address of the origin token is a wrappedNativeToken contract and there is a msg.value with the // transaction then the user is sending the native token. In this case, the native token should be // wrapped. - if (inputToken == address(wrappedNativeToken) && msg.value > 0) { + if (inputToken == address(wrappedNativeToken).toBytes32() && msg.value > 0) { if (msg.value != inputAmount) revert MsgValueDoesNotMatchInputAmount(); wrappedNativeToken.deposit{ value: msg.value }(); // Else, it is a normal ERC20. In this case pull the token from the caller as per normal. @@ -587,7 +605,7 @@ abstract contract SpokePool is } else { // msg.value should be 0 if input token isn't the wrapped native token. if (msg.value != 0) revert MsgValueDoesNotMatchInputAmount(); - IERC20Upgradeable(inputToken).safeTransferFrom(msg.sender, address(this), inputAmount); + IERC20Upgradeable(inputToken.toAddress()).safeTransferFrom(msg.sender, address(this), inputAmount); } emit V3FundsDeposited( @@ -608,6 +626,72 @@ abstract contract SpokePool is ); } + /** + * @notice An overloaded version of `depositV3` that accepts `address` types for backward compatibility. + * This function allows bridging of input tokens cross-chain to a destination chain, receiving a specified amount of output tokens. + * The relayer is refunded in input tokens on a repayment chain of their choice, minus system fees, after an optimistic challenge + * window. The exclusivity period is specified as an offset from the current block timestamp. + * + * @dev This version mirrors the original `depositV3` function, but uses `address` types for `depositor`, `recipient`, + * `inputToken`, `outputToken`, and `exclusiveRelayer` for compatibility with contracts using the `address` type. + * + * The key functionality and logic remain identical, ensuring interoperability across both versions. + * + * @param depositor The account credited with the deposit who can request to "speed up" this deposit by modifying + * the output amount, recipient, and message. + * @param recipient The account receiving funds on the destination chain. Can be an EOA or a contract. If + * the output token is the wrapped native token for the chain, then the recipient will receive native token if + * an EOA or wrapped native token if a contract. + * @param inputToken The token pulled from the caller's account and locked into this contract to initiate the deposit. + * The equivalent of this token on the relayer's repayment chain of choice will be sent as a refund. If this is equal + * to the wrapped native token, the caller can optionally pass in native token as msg.value, provided msg.value = inputTokenAmount. + * @param outputToken The token that the relayer will send to the recipient on the destination chain. Must be an ERC20. + * @param inputAmount The amount of input tokens pulled from the caller's account and locked into this contract. This + * amount will be sent to the relayer as a refund following an optimistic challenge window in the HubPool, less a system fee. + * @param outputAmount The amount of output tokens that the relayer will send to the recipient on the destination. + * @param destinationChainId The destination chain identifier. Must be enabled along with the input token as a valid + * deposit route from this spoke pool or this transaction will revert. + * @param exclusiveRelayer The relayer exclusively allowed to fill this deposit before the exclusivity deadline. + * @param quoteTimestamp The HubPool timestamp that determines the system fee paid by the depositor. This must be set + * between [currentTime - depositQuoteTimeBuffer, currentTime] where currentTime is block.timestamp on this chain. + * @param fillDeadline The deadline for the relayer to fill the deposit. After this destination chain timestamp, the fill will + * revert on the destination chain. Must be set between [currentTime, currentTime + fillDeadlineBuffer] where currentTime + * is block.timestamp on this chain. + * @param exclusivityPeriod Added to the current time to set the exclusive relayer deadline. After this timestamp, + * anyone can fill the deposit. + * @param message The message to send to the recipient on the destination chain if the recipient is a contract. If the + * message is not empty, the recipient contract must implement `handleV3AcrossMessage()` or the fill will revert. + */ + function depositV3( + address depositor, + address recipient, + address inputToken, + address outputToken, + uint256 inputAmount, + uint256 outputAmount, + uint256 destinationChainId, + address exclusiveRelayer, + uint32 quoteTimestamp, + uint32 fillDeadline, + uint32 exclusivityPeriod, + bytes calldata message + ) public payable { + depositV3( + depositor.toBytes32(), + recipient.toBytes32(), + inputToken.toBytes32(), + outputToken.toBytes32(), + inputAmount, + outputAmount, + destinationChainId, + exclusiveRelayer.toBytes32(), + quoteTimestamp, + fillDeadline, + exclusivityPeriod, + message + ); + } + /** * @notice Submits deposit and sets quoteTimestamp to current Time. Sets fill and exclusivity * deadlines as offsets added to the current time. This function is designed to be called by users @@ -640,6 +724,67 @@ abstract contract SpokePool is * @param message The message to send to the recipient on the destination chain if the recipient is a contract. * If the message is not empty, the recipient contract must implement handleV3AcrossMessage() or the fill will revert. */ + function depositV3Now( + bytes32 depositor, + bytes32 recipient, + bytes32 inputToken, + bytes32 outputToken, + uint256 inputAmount, + uint256 outputAmount, + uint256 destinationChainId, + bytes32 exclusiveRelayer, + uint32 fillDeadlineOffset, + uint32 exclusivityPeriod, + bytes calldata message + ) external payable { + depositV3( + depositor, + recipient, + inputToken, + outputToken, + inputAmount, + outputAmount, + destinationChainId, + exclusiveRelayer, + uint32(getCurrentTime()), + uint32(getCurrentTime()) + fillDeadlineOffset, + exclusivityPeriod, + message + ); + } + + /** + * @notice An overloaded version of `depositV3Now` that supports addresses as input types for backward compatibility. + * This function submits a deposit and sets `quoteTimestamp` to the current time. The `fill` and `exclusivity` deadlines + * are set as offsets added to the current time. It is designed to be called by users, including Multisig contracts, who may + * not have certainty when their transaction will be mined. + * + * @dev This version is identical to the original `depositV3Now` but uses `address` types for `depositor`, `recipient`, + * `inputToken`, `outputToken`, and `exclusiveRelayer` to support compatibility with older systems. + * It maintains the same logic and purpose, ensuring interoperability with both versions. + * + * @param depositor The account credited with the deposit, who can request to "speed up" this deposit by modifying + * the output amount, recipient, and message. + * @param recipient The account receiving funds on the destination chain. Can be an EOA or a contract. If + * the output token is the wrapped native token for the chain, then the recipient will receive the native token if + * an EOA or wrapped native token if a contract. + * @param inputToken The token pulled from the caller's account and locked into this contract to initiate the deposit. + * Equivalent tokens on the relayer's repayment chain will be sent as a refund. If this is the wrapped native token, + * msg.value must equal inputTokenAmount when passed. + * @param outputToken The token the relayer will send to the recipient on the destination chain. Must be an ERC20. + * @param inputAmount The amount of input tokens pulled from the caller's account and locked into this contract. + * This amount will be sent to the relayer as a refund following an optimistic challenge window in the HubPool, plus a system fee. + * @param outputAmount The amount of output tokens the relayer will send to the recipient on the destination. + * @param destinationChainId The destination chain identifier. Must be enabled with the input token as a valid deposit route + * from this spoke pool, or the transaction will revert. + * @param exclusiveRelayer The relayer exclusively allowed to fill the deposit before the exclusivity deadline. + * @param fillDeadlineOffset Added to the current time to set the fill deadline. After this timestamp, fills on the + * destination chain will revert. + * @param exclusivityPeriod Added to the current time to set the exclusive relayer deadline. After this timestamp, + * anyone can fill the deposit until the fill deadline. + * @param message The message to send to the recipient on the destination chain. If the recipient is a contract, it must + * implement `handleV3AcrossMessage()` if the message is not empty, or the fill will revert. + */ function depositV3Now( address depositor, address recipient, @@ -722,14 +867,14 @@ abstract contract SpokePool is bytes calldata message ) public payable { depositV3( - depositor, - recipient, - inputToken, - outputToken, + depositor.toBytes32(), + recipient.toBytes32(), + inputToken.toBytes32(), + outputToken.toBytes32(), inputAmount, outputAmount, destinationChainId, - exclusiveRelayer, + exclusiveRelayer.toBytes32(), quoteTimestamp, fillDeadline, exclusivityPeriod, @@ -756,10 +901,10 @@ abstract contract SpokePool is * _verifyUpdateV3DepositMessage() for more details about how this signature should be constructed. */ function speedUpV3Deposit( - address depositor, + bytes32 depositor, uint32 depositId, uint256 updatedOutputAmount, - address updatedRecipient, + bytes32 updatedRecipient, bytes calldata updatedMessage, bytes calldata depositorSignature ) public override nonReentrant { @@ -770,7 +915,8 @@ abstract contract SpokePool is updatedOutputAmount, updatedRecipient, updatedMessage, - depositorSignature + depositorSignature, + UPDATE_V3_DEPOSIT_DETAILS_HASH ); // Assuming the above checks passed, a relayer can take the signature and the updated deposit information @@ -785,6 +931,60 @@ abstract contract SpokePool is ); } + /** + * @notice An overloaded version of `speedUpV3Deposit` using `address` types for backward compatibility. + * This function allows the depositor to signal to the relayer to use updated output amount, recipient, and/or message + * when filling a deposit. This can be useful when the deposit needs to be modified after the original transaction has + * been mined. + * + * @dev The `depositor` and `depositId` must match the parameters in a `V3FundsDeposited` event that the depositor wants to speed up. + * The relayer is not obligated but has the option to use this updated information when filling the deposit using + * `fillV3RelayWithUpdatedDeposit()`. This version uses `address` types for compatibility with systems relying on + * `address`-based implementations. + * + * @param depositor The depositor that must sign the `depositorSignature` and was the original depositor. + * @param depositId The deposit ID to speed up. + * @param updatedOutputAmount The new output amount to use for this deposit. It should be lower than the previous value, + * otherwise the relayer has no incentive to use this updated value. + * @param updatedRecipient The new recipient for this deposit. Can be modified if the original recipient is a contract that + * expects to receive a message from the relay and needs to be changed. + * @param updatedMessage The new message for this deposit. Can be modified if the recipient is a contract that expects + * to receive a message from the relay and needs to be updated. + * @param depositorSignature The signed EIP712 hashstruct containing the deposit ID. Should be signed by the depositor account. + * If the depositor is a contract, it should implement EIP1271 to sign as a contract. See `_verifyUpdateV3DepositMessage()` + * for more details on how the signature should be constructed. + */ + function speedUpV3Deposit( + address depositor, + uint32 depositId, + uint256 updatedOutputAmount, + address updatedRecipient, + bytes calldata updatedMessage, + bytes calldata depositorSignature + ) public { + _verifyUpdateV3DepositMessage( + depositor.toBytes32(), + depositId, + chainId(), + updatedOutputAmount, + updatedRecipient.toBytes32(), + updatedMessage, + depositorSignature, + UPDATE_V3_DEPOSIT_ADDRESS_OVERLOAD_DETAILS_HASH + ); + + // Assuming the above checks passed, a relayer can take the signature and the updated deposit information + // from the following event to submit a fill with updated relay data. + emit RequestedSpeedUpV3Deposit( + updatedOutputAmount, + depositId, + depositor.toBytes32(), + updatedRecipient.toBytes32(), + updatedMessage, + depositorSignature + ); + } + /************************************** * RELAYER FUNCTIONS * **************************************/ @@ -839,9 +1039,9 @@ abstract contract SpokePool is // Exclusivity deadline is inclusive and is the latest timestamp that the exclusive relayer has sole right // to fill the relay. if ( - relayData.exclusiveRelayer != msg.sender && + relayData.exclusiveRelayer != msg.sender.toBytes32() && relayData.exclusivityDeadline >= getCurrentTime() && - relayData.exclusiveRelayer != address(0) + relayData.exclusiveRelayer != bytes32(0) ) { revert NotExclusiveRelayer(); } @@ -855,7 +1055,7 @@ abstract contract SpokePool is repaymentChainId: repaymentChainId }); - _fillRelayV3(relayExecution, msg.sender, false); + _fillRelayV3(relayExecution, msg.sender.toBytes32(), false); } /** @@ -879,13 +1079,13 @@ abstract contract SpokePool is V3RelayData calldata relayData, uint256 repaymentChainId, uint256 updatedOutputAmount, - address updatedRecipient, + bytes32 updatedRecipient, bytes calldata updatedMessage, bytes calldata depositorSignature ) public override nonReentrant unpausedFills { // Exclusivity deadline is inclusive and is the latest timestamp that the exclusive relayer has sole right // to fill the relay. - if (relayData.exclusivityDeadline >= getCurrentTime() && relayData.exclusiveRelayer != msg.sender) { + if (relayData.exclusivityDeadline >= getCurrentTime() && relayData.exclusiveRelayer != msg.sender.toBytes32()) { revert NotExclusiveRelayer(); } @@ -905,10 +1105,11 @@ abstract contract SpokePool is updatedOutputAmount, updatedRecipient, updatedMessage, - depositorSignature + depositorSignature, + UPDATE_V3_DEPOSIT_DETAILS_HASH ); - _fillRelayV3(relayExecution, msg.sender, false); + _fillRelayV3(relayExecution, msg.sender.toBytes32(), false); } /** @@ -1047,8 +1248,9 @@ abstract contract SpokePool is // Check that proof proves that relayerRefundLeaf is contained within the relayer refund root. // Note: This should revert if the relayerRefundRoot is uninitialized. - if (!MerkleLib.verifyRelayerRefund(rootBundle.relayerRefundRoot, relayerRefundLeaf, proof)) + if (!MerkleLib.verifyRelayerRefund(rootBundle.relayerRefundRoot, relayerRefundLeaf, proof)) { revert InvalidMerkleProof(); + } _setClaimedLeaf(rootBundleId, relayerRefundLeaf.leafId); @@ -1096,11 +1298,10 @@ abstract contract SpokePool is /************************************** * INTERNAL FUNCTIONS * **************************************/ - function _deposit( - address depositor, - address recipient, - address originToken, + bytes32 depositor, + bytes32 recipient, + bytes32 originToken, uint256 amount, uint256 destinationChainId, int64 relayerFeePct, @@ -1128,17 +1329,19 @@ abstract contract SpokePool is // If the address of the origin token is a wrappedNativeToken contract and there is a msg.value with the // transaction then the user is sending ETH. In this case, the ETH should be deposited to wrappedNativeToken. - if (originToken == address(wrappedNativeToken) && msg.value > 0) { + if (originToken == address(wrappedNativeToken).toBytes32() && msg.value > 0) { if (msg.value != amount) revert MsgValueDoesNotMatchInputAmount(); wrappedNativeToken.deposit{ value: msg.value }(); // Else, it is a normal ERC20. In this case pull the token from the user's wallet as per normal. // Note: this includes the case where the L2 user has WETH (already wrapped ETH) and wants to bridge them. // In this case the msg.value will be set to 0, indicating a "normal" ERC20 bridging action. - } else IERC20Upgradeable(originToken).safeTransferFrom(msg.sender, address(this), amount); + } else { + IERC20Upgradeable(originToken.toAddress()).safeTransferFrom(msg.sender, address(this), amount); + } emit V3FundsDeposited( originToken, // inputToken - address(0), // outputToken. Setting this to 0x0 means that the outputToken should be assumed to be the + bytes32(0), // outputToken. Setting this to 0x0 means that the outputToken should be assumed to be the // canonical token for the destination chain matching the inputToken. Therefore, this deposit // can always be slow filled. // - setting token to 0x0 will signal to off-chain validator that the "equivalent" @@ -1157,7 +1360,7 @@ abstract contract SpokePool is // is no exclusive deadline depositor, recipient, - address(0), // exclusiveRelayer. Setting this to 0x0 will signal to off-chain validator that there + bytes32(0), // exclusiveRelayer. Setting this to 0x0 will signal to off-chain validator that there // is no exclusive relayer. message ); @@ -1168,23 +1371,26 @@ abstract contract SpokePool is uint256 amountToReturn, uint256[] memory refundAmounts, uint32 leafId, - address l2TokenAddress, - address[] memory refundAddresses + bytes32 l2TokenAddress, + bytes32[] memory refundAddresses ) internal { if (refundAddresses.length != refundAmounts.length) revert InvalidMerkleLeaf(); + address l2TokenAddressParsed = l2TokenAddress.toAddress(); + // Send each relayer refund address the associated refundAmount for the L2 token address. // Note: Even if the L2 token is not enabled on this spoke pool, we should still refund relayers. uint256 length = refundAmounts.length; for (uint256 i = 0; i < length; ++i) { uint256 amount = refundAmounts[i]; - if (amount > 0) IERC20Upgradeable(l2TokenAddress).safeTransfer(refundAddresses[i], amount); + if (amount > 0) + IERC20Upgradeable(l2TokenAddressParsed).safeTransfer(refundAddresses[i].toAddress(), amount); } // If leaf's amountToReturn is positive, then send L2 --> L1 message to bridge tokens back via // chain-specific bridging method. if (amountToReturn > 0) { - _bridgeTokensToHubPool(amountToReturn, l2TokenAddress); + _bridgeTokensToHubPool(amountToReturn, l2TokenAddressParsed); emit TokensBridged(amountToReturn, _chainId, leafId, l2TokenAddress, msg.sender); } @@ -1202,7 +1408,7 @@ abstract contract SpokePool is emit SetWithdrawalRecipient(newWithdrawalRecipient); } - function _preExecuteLeafHook(address) internal virtual { + function _preExecuteLeafHook(bytes32) internal virtual { // This method by default is a no-op. Different child spoke pools might want to execute functionality here // such as wrapping any native tokens owned by the contract into wrapped tokens before proceeding with // executing the leaf. @@ -1222,25 +1428,22 @@ abstract contract SpokePool is } function _verifyUpdateV3DepositMessage( - address depositor, + bytes32 depositor, uint32 depositId, uint256 originChainId, uint256 updatedOutputAmount, - address updatedRecipient, + bytes32 updatedRecipient, bytes memory updatedMessage, - bytes memory depositorSignature + bytes memory depositorSignature, + bytes32 hashType ) internal view { // A depositor can request to modify an un-relayed deposit by signing a hash containing the updated // details and information uniquely identifying the deposit to relay. This information ensures // that this signature cannot be re-used for other deposits. - // Note: We use the EIP-712 (https://eips.ethereum.org/EIPS/eip-712) standard for hashing and signing typed data. - // Specifically, we use the version of the encoding known as "v4", as implemented by the JSON RPC method - // `eth_signedTypedDataV4` in MetaMask (https://docs.metamask.io/guide/signing-data.html). bytes32 expectedTypedDataV4Hash = _hashTypedDataV4( - // EIP-712 compliant hash struct: https://eips.ethereum.org/EIPS/eip-712#definition-of-hashstruct keccak256( abi.encode( - UPDATE_V3_DEPOSIT_DETAILS_HASH, + hashType, depositId, originChainId, updatedOutputAmount, @@ -1248,10 +1451,9 @@ abstract contract SpokePool is keccak256(updatedMessage) ) ), - // By passing in the origin chain id, we enable the verification of the signature on a different chain originChainId ); - _verifyDepositorSignature(depositor, expectedTypedDataV4Hash, depositorSignature); + _verifyDepositorSignature(depositor.toAddress(), expectedTypedDataV4Hash, depositorSignature); } // This function is isolated and made virtual to allow different L2's to implement chain specific recovery of @@ -1285,8 +1487,9 @@ abstract contract SpokePool is updatedOutputAmount: relayExecution.updatedOutputAmount }); - if (!MerkleLib.verifyV3SlowRelayFulfillment(rootBundles[rootBundleId].slowRelayRoot, slowFill, proof)) + if (!MerkleLib.verifyV3SlowRelayFulfillment(rootBundles[rootBundleId].slowRelayRoot, slowFill, proof)) { revert InvalidMerkleProof(); + } } function _computeAmountPostFees(uint256 amount, int256 feesPct) private pure returns (uint256) { @@ -1311,7 +1514,7 @@ abstract contract SpokePool is // exclusiveRelayer if passed exclusivityDeadline or if slow fill. function _fillRelayV3( V3RelayExecutionParams memory relayExecution, - address relayer, + bytes32 relayer, bool isSlowFill ) internal { V3RelayData memory relayData = relayExecution.relay; @@ -1327,9 +1530,8 @@ abstract contract SpokePool is // event to assist the Dataworker in knowing when to return funds back to the HubPool that can no longer // be used for a slow fill execution. FillType fillType = isSlowFill - ? FillType.SlowFill + ? FillType.SlowFill // The following is true if this is a fast fill that was sent after a slow fill request. : ( - // The following is true if this is a fast fill that was sent after a slow fill request. fillStatuses[relayExecution.relayHash] == uint256(FillStatus.RequestedSlowFill) ? FillType.ReplacedSlowFill : FillType.FastFill @@ -1375,12 +1577,12 @@ abstract contract SpokePool is // way (no need to have funds on the destination). // If this is a slow fill, we can't exit early since we still need to send funds out of this contract // since there is no "relayer". - address recipientToSend = relayExecution.updatedRecipient; + address recipientToSend = relayExecution.updatedRecipient.toAddress(); if (msg.sender == recipientToSend && !isSlowFill) return; // If relay token is wrappedNativeToken then unwrap and send native token. - address outputToken = relayData.outputToken; + address outputToken = relayData.outputToken.toAddress(); uint256 amountToSend = relayExecution.updatedOutputAmount; if (outputToken == address(wrappedNativeToken)) { // Note: useContractFunds is True if we want to send funds to the recipient directly out of this contract, diff --git a/contracts/SpokePoolVerifier.sol b/contracts/SpokePoolVerifier.sol index 5dc333684..2aa038498 100644 --- a/contracts/SpokePoolVerifier.sol +++ b/contracts/SpokePoolVerifier.sol @@ -3,6 +3,7 @@ pragma solidity ^0.8.0; import "@openzeppelin/contracts/utils/Address.sol"; import "./interfaces/V3SpokePoolInterface.sol"; +import { AddressToBytes32 } from "./libraries/AddressConverters.sol"; /** * @notice SpokePoolVerifier is a contract that verifies that the SpokePool exists on this chain before sending ETH to it. @@ -14,6 +15,7 @@ import "./interfaces/V3SpokePoolInterface.sol"; */ contract SpokePoolVerifier { using Address for address; + using AddressToBytes32 for address; error InvalidMsgValue(); error InvalidSpokePool(); @@ -42,12 +44,12 @@ contract SpokePoolVerifier { */ function deposit( V3SpokePoolInterface spokePool, - address recipient, - address inputToken, + bytes32 recipient, + bytes32 inputToken, uint256 inputAmount, uint256 outputAmount, uint256 destinationChainId, - address exclusiveRelayer, + bytes32 exclusiveRelayer, uint32 quoteTimestamp, uint32 fillDeadline, uint32 exclusivityDeadline, @@ -57,12 +59,12 @@ contract SpokePoolVerifier { if (!address(spokePool).isContract()) revert InvalidSpokePool(); // Set msg.sender as the depositor so that msg.sender can speed up the deposit. spokePool.depositV3{ value: msg.value }( - msg.sender, + msg.sender.toBytes32(), recipient, inputToken, // @dev Setting outputToken to 0x0 to instruct fillers to use the equivalent token // as the originToken on the destination chain. - address(0), + bytes32(0), inputAmount, outputAmount, destinationChainId, diff --git a/contracts/SwapAndBridge.sol b/contracts/SwapAndBridge.sol index 787d4bef0..dd4b80f74 100644 --- a/contracts/SwapAndBridge.sol +++ b/contracts/SwapAndBridge.sol @@ -6,6 +6,7 @@ import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import "./Lockable.sol"; import "@uma/core/contracts/common/implementation/MultiCaller.sol"; +import { Bytes32ToAddress } from "./libraries/AddressConverters.sol"; /** * @title SwapAndBridgeBase @@ -14,6 +15,7 @@ import "@uma/core/contracts/common/implementation/MultiCaller.sol"; */ abstract contract SwapAndBridgeBase is Lockable, MultiCaller { using SafeERC20 for IERC20; + using Bytes32ToAddress for bytes32; // This contract performs a low level call with arbirary data to an external contract. This is a large attack // surface and we should whitelist which function selectors are allowed to be called on the exchange. @@ -30,19 +32,19 @@ abstract contract SwapAndBridgeBase is Lockable, MultiCaller { // until after the swap. struct DepositData { // Token received on destination chain. - address outputToken; + bytes32 outputToken; // Amount of output token to be received by recipient. uint256 outputAmount; // The account credited with deposit who can submit speedups to the Across deposit. - address depositor; + bytes32 depositor; // The account that will receive the output token on the destination chain. If the output token is // wrapped native token, then if this is an EOA then they will receive native token on the destination // chain and if this is a contract then they will receive an ERC20. - address recipient; + bytes32 recipient; // The destination chain identifier. uint256 destinationChainid; // The account that can exclusively fill the deposit before the exclusivity deadline. - address exclusiveRelayer; + bytes32 exclusiveRelayer; // Timestamp of the deposit used by system to charge fees. Must be within short window of time into the past // relative to this chain's current time or deposit will revert. uint32 quoteTimestamp; @@ -56,11 +58,11 @@ abstract contract SwapAndBridgeBase is Lockable, MultiCaller { event SwapBeforeBridge( address exchange, - address indexed swapToken, - address indexed acrossInputToken, + bytes32 indexed swapToken, + bytes32 indexed acrossInputToken, uint256 swapTokenAmount, uint256 acrossInputAmount, - address indexed acrossOutputToken, + bytes32 indexed acrossOutputToken, uint256 acrossOutputAmount ); @@ -95,8 +97,8 @@ abstract contract SwapAndBridgeBase is Lockable, MultiCaller { uint256 swapTokenAmount, uint256 minExpectedInputTokenAmount, DepositData calldata depositData, - IERC20 _swapToken, - IERC20 _acrossInputToken + bytes32 _swapToken, + bytes32 _acrossInputToken ) internal { // Note: this check should never be impactful, but is here out of an abundance of caution. // For example, if the exchange address in the contract is also an ERC20 token that is approved by some @@ -104,12 +106,12 @@ abstract contract SwapAndBridgeBase is Lockable, MultiCaller { if (!allowedSelectors[bytes4(routerCalldata)]) revert InvalidFunctionSelector(); // Pull tokens from caller into this contract. - _swapToken.safeTransferFrom(msg.sender, address(this), swapTokenAmount); + IERC20(_swapToken.toAddress()).safeTransferFrom(msg.sender, address(this), swapTokenAmount); // Swap and run safety checks. - uint256 srcBalanceBefore = _swapToken.balanceOf(address(this)); - uint256 dstBalanceBefore = _acrossInputToken.balanceOf(address(this)); + uint256 srcBalanceBefore = IERC20(_swapToken.toAddress()).balanceOf(address(this)); + uint256 dstBalanceBefore = IERC20(_acrossInputToken.toAddress()).balanceOf(address(this)); - _swapToken.safeIncreaseAllowance(EXCHANGE, swapTokenAmount); + IERC20(_swapToken.toAddress()).safeIncreaseAllowance(EXCHANGE, swapTokenAmount); // solhint-disable-next-line avoid-low-level-calls (bool success, bytes memory result) = EXCHANGE.call(routerCalldata); require(success, string(result)); @@ -131,39 +133,42 @@ abstract contract SwapAndBridgeBase is Lockable, MultiCaller { * @param swapTokenBalanceBefore Balance of swapToken before swap. * @param inputTokenBalanceBefore Amount of Across input token we held before swap * @param minExpectedInputTokenAmount Minimum amount of received acrossInputToken that we'll bridge - **/ + * + */ function _checkSwapOutputAndDeposit( uint256 swapTokenAmount, uint256 swapTokenBalanceBefore, uint256 inputTokenBalanceBefore, uint256 minExpectedInputTokenAmount, DepositData calldata depositData, - IERC20 _swapToken, - IERC20 _acrossInputToken + bytes32 _swapToken, + bytes32 _acrossInputToken ) internal { // Sanity check that we received as many tokens as we require: - uint256 returnAmount = _acrossInputToken.balanceOf(address(this)) - inputTokenBalanceBefore; + uint256 returnAmount = IERC20(_acrossInputToken.toAddress()).balanceOf(address(this)) - inputTokenBalanceBefore; // Sanity check that received amount from swap is enough to submit Across deposit with. if (returnAmount < minExpectedInputTokenAmount) revert MinimumExpectedInputAmount(); // Sanity check that we don't have any leftover swap tokens that would be locked in this contract (i.e. check // that we weren't partial filled). - if (swapTokenBalanceBefore - _swapToken.balanceOf(address(this)) != swapTokenAmount) revert LeftoverSrcTokens(); + if (swapTokenBalanceBefore - IERC20(_swapToken.toAddress()).balanceOf(address(this)) != swapTokenAmount) { + revert LeftoverSrcTokens(); + } emit SwapBeforeBridge( EXCHANGE, - address(_swapToken), - address(_acrossInputToken), + _swapToken, + _acrossInputToken, swapTokenAmount, returnAmount, depositData.outputToken, depositData.outputAmount ); // Deposit the swapped tokens into Across and bridge them using remainder of input params. - _acrossInputToken.safeIncreaseAllowance(address(SPOKE_POOL), returnAmount); + IERC20(_acrossInputToken.toAddress()).safeIncreaseAllowance(address(SPOKE_POOL), returnAmount); SPOKE_POOL.depositV3( depositData.depositor, depositData.recipient, - address(_acrossInputToken), // input token + _acrossInputToken, // input token depositData.outputToken, // output token returnAmount, // input amount. depositData.outputAmount, // output amount @@ -189,10 +194,10 @@ contract SwapAndBridge is SwapAndBridgeBase { // This contract simply enables the caller to swap a token on this chain for another specified one // and bridge it as the input token via Across. This simplification is made to make the code // easier to reason about and solve a specific use case for Across. - IERC20 public immutable SWAP_TOKEN; + bytes32 public immutable SWAP_TOKEN; // The token that will be bridged via Across as the inputToken. - IERC20 public immutable ACROSS_INPUT_TOKEN; + bytes32 public immutable ACROSS_INPUT_TOKEN; /** * @notice Construct a new SwapAndBridge contract. @@ -206,8 +211,8 @@ contract SwapAndBridge is SwapAndBridgeBase { V3SpokePoolInterface _spokePool, address _exchange, bytes4[] memory _allowedSelectors, - IERC20 _swapToken, - IERC20 _acrossInputToken + bytes32 _swapToken, + bytes32 _acrossInputToken ) SwapAndBridgeBase(_spokePool, _exchange, _allowedSelectors) { SWAP_TOKEN = _swapToken; ACROSS_INPUT_TOKEN = _acrossInputToken; @@ -275,8 +280,8 @@ contract UniversalSwapAndBridge is SwapAndBridgeBase { * @param depositData Specifies the Across deposit params we'll send after the swap. */ function swapAndBridge( - IERC20 swapToken, - IERC20 acrossInputToken, + bytes32 swapToken, + bytes32 acrossInputToken, bytes calldata routerCalldata, uint256 swapTokenAmount, uint256 minExpectedInputTokenAmount, diff --git a/contracts/ZkSync_SpokePool.sol b/contracts/ZkSync_SpokePool.sol index 3224e4cb3..65fb4b16c 100644 --- a/contracts/ZkSync_SpokePool.sol +++ b/contracts/ZkSync_SpokePool.sol @@ -23,6 +23,7 @@ interface IL2ETH { * @custom:security-contact bugs@across.to */ contract ZkSync_SpokePool is SpokePool { + using AddressToBytes32 for address; // On Ethereum, avoiding constructor parameters and putting them into constants reduces some of the gas cost // upon contract deployment. On zkSync the opposite is true: deploying the same bytecode for contracts, // while changing only constructor parameters can lead to substantial fee savings. So, the following params @@ -88,8 +89,8 @@ contract ZkSync_SpokePool is SpokePool { * @notice Wraps any ETH into WETH before executing base function. This is necessary because SpokePool receives * ETH over the canonical token bridge instead of WETH. */ - function _preExecuteLeafHook(address l2TokenAddress) internal override { - if (l2TokenAddress == address(wrappedNativeToken)) _depositEthToWeth(); + function _preExecuteLeafHook(bytes32 l2TokenAddress) internal override { + if (l2TokenAddress == address(wrappedNativeToken).toBytes32()) _depositEthToWeth(); } // Wrap any ETH owned by this contract so we can send expected L2 token to recipient. This is necessary because diff --git a/contracts/erc7683/ERC7683.sol b/contracts/erc7683/ERC7683.sol index e9764d0e1..c35068055 100644 --- a/contracts/erc7683/ERC7683.sol +++ b/contracts/erc7683/ERC7683.sol @@ -6,10 +6,10 @@ pragma solidity ^0.8.0; struct GaslessCrossChainOrder { /// @dev The contract address that the order is meant to be settled by. /// Fillers send this order to this contract address on the origin chain - address originSettler; + bytes32 originSettler; /// @dev The address of the user who is initiating the swap, /// whose input tokens will be taken and escrowed - address user; + bytes32 user; /// @dev Nonce to be used as replay protection for the order uint256 nonce; /// @dev The chainId of the origin chain @@ -45,7 +45,7 @@ struct OnchainCrossChainOrder { /// @dev Intended to improve integration generalization by allowing fillers to compute the exact input and output information of any order struct ResolvedCrossChainOrder { /// @dev The address of the user who is initiating the transfer - address user; + bytes32 user; /// @dev The chainId of the origin chain uint64 originChainId; /// @dev The timestamp by which the order must be opened diff --git a/contracts/erc7683/ERC7683Across.sol b/contracts/erc7683/ERC7683Across.sol index 87c29c362..2b008ce84 100644 --- a/contracts/erc7683/ERC7683Across.sol +++ b/contracts/erc7683/ERC7683Across.sol @@ -6,19 +6,19 @@ import { GaslessCrossChainOrder } from "./ERC7683.sol"; // Data unique to every CrossChainOrder settled on Across struct AcrossOrderData { - address inputToken; + bytes32 inputToken; uint256 inputAmount; - address outputToken; + bytes32 outputToken; uint256 outputAmount; uint32 destinationChainId; - address recipient; - address exclusiveRelayer; + bytes32 recipient; + bytes32 exclusiveRelayer; uint32 exclusivityPeriod; bytes message; } struct AcrossOriginFillerData { - address exclusiveRelayer; + bytes32 exclusiveRelayer; } struct AcrossDestinationFillerData { @@ -27,13 +27,13 @@ struct AcrossDestinationFillerData { bytes constant ACROSS_ORDER_DATA_TYPE = abi.encodePacked( "AcrossOrderData(", - "address inputToken,", + "bytes32 inputToken,", "uint256 inputAmount,", - "address outputToken,", + "bytes32 outputToken,", "uint256 outputAmount,", "uint32 destinationChainId,", - "address recipient,", - "address exclusiveRelayer," + "bytes32 recipient,", + "bytes32 exclusiveRelayer," "uint32 exclusivityPeriod,", "bytes message)" ); @@ -49,8 +49,8 @@ library ERC7683Permit2Lib { bytes internal constant CROSS_CHAIN_ORDER_TYPE = abi.encodePacked( "GaslessCrossChainOrder(", - "address originSettler,", - "address user,", + "bytes32 originSettler,", + "bytes32 user,", "uint256 nonce,", "uint32 originChainId,", "uint32 openDeadline,", @@ -63,7 +63,7 @@ library ERC7683Permit2Lib { abi.encodePacked(CROSS_CHAIN_ORDER_TYPE, ACROSS_ORDER_DATA_TYPE); bytes32 internal constant CROSS_CHAIN_ORDER_TYPE_HASH = keccak256(CROSS_CHAIN_ORDER_EIP712_TYPE); - string private constant TOKEN_PERMISSIONS_TYPE = "TokenPermissions(address token,uint256 amount)"; + string private constant TOKEN_PERMISSIONS_TYPE = "TokenPermissions(bytes32 token,uint256 amount)"; string internal constant PERMIT2_ORDER_TYPE = string( abi.encodePacked( diff --git a/contracts/erc7683/ERC7683OrderDepositor.sol b/contracts/erc7683/ERC7683OrderDepositor.sol index bd5e7e03c..08c611198 100644 --- a/contracts/erc7683/ERC7683OrderDepositor.sol +++ b/contracts/erc7683/ERC7683OrderDepositor.sol @@ -5,6 +5,7 @@ import "../external/interfaces/IPermit2.sol"; import "@openzeppelin/contracts/utils/math/SafeCast.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import { AddressToBytes32, Bytes32ToAddress } from "../libraries/AddressConverters.sol"; import { Output, GaslessCrossChainOrder, OnchainCrossChainOrder, ResolvedCrossChainOrder, IOriginSettler, FillInstruction } from "./ERC7683.sol"; import { AcrossOrderData, AcrossOriginFillerData, ERC7683Permit2Lib, ACROSS_ORDER_DATA_TYPE_HASH } from "./ERC7683Across.sol"; @@ -17,6 +18,8 @@ import { AcrossOrderData, AcrossOriginFillerData, ERC7683Permit2Lib, ACROSS_ORDE */ abstract contract ERC7683OrderDepositor is IOriginSettler { using SafeERC20 for IERC20; + using AddressToBytes32 for address; + using Bytes32ToAddress for bytes32; error WrongSettlementContract(); error WrongChainId(); @@ -89,10 +92,14 @@ abstract contract ERC7683OrderDepositor is IOriginSettler { function open(OnchainCrossChainOrder calldata order) external { (ResolvedCrossChainOrder memory resolvedOrder, AcrossOrderData memory acrossOrderData) = _resolve(order); - IERC20(acrossOrderData.inputToken).safeTransferFrom(msg.sender, address(this), acrossOrderData.inputAmount); + IERC20(acrossOrderData.inputToken.toAddress()).safeTransferFrom( + msg.sender, + address(this), + acrossOrderData.inputAmount + ); _callDeposit( - msg.sender, + msg.sender.toBytes32(), acrossOrderData.recipient, acrossOrderData.inputToken, acrossOrderData.outputToken, @@ -168,7 +175,7 @@ abstract contract ERC7683OrderDepositor is IOriginSettler { ) { // Ensure that order was intended to be settled by Across. - if (order.originSettler != address(this)) { + if (order.originSettler != address(this).toBytes32()) { revert WrongSettlementContract(); } @@ -184,7 +191,7 @@ abstract contract ERC7683OrderDepositor is IOriginSettler { (acrossOrderData, acrossOriginFillerData) = decode(order.orderData, fillerData); if ( - acrossOrderData.exclusiveRelayer != address(0) && + acrossOrderData.exclusiveRelayer != address(0).toBytes32() && acrossOrderData.exclusiveRelayer != acrossOriginFillerData.exclusiveRelayer ) { revert WrongExclusiveRelayer(); @@ -192,9 +199,9 @@ abstract contract ERC7683OrderDepositor is IOriginSettler { Output[] memory maxSpent = new Output[](1); maxSpent[0] = Output({ - token: _toBytes32(acrossOrderData.outputToken), + token: acrossOrderData.outputToken, amount: acrossOrderData.outputAmount, - recipient: _toBytes32(acrossOrderData.recipient), + recipient: acrossOrderData.recipient, chainId: acrossOrderData.destinationChainId }); @@ -204,16 +211,16 @@ abstract contract ERC7683OrderDepositor is IOriginSettler { // repayment on. Output[] memory minReceived = new Output[](1); minReceived[0] = Output({ - token: _toBytes32(acrossOrderData.inputToken), + token: acrossOrderData.inputToken, amount: acrossOrderData.inputAmount, - recipient: _toBytes32(acrossOriginFillerData.exclusiveRelayer), + recipient: acrossOriginFillerData.exclusiveRelayer, chainId: SafeCast.toUint32(block.chainid) }); FillInstruction[] memory fillInstructions = new FillInstruction[](1); fillInstructions[0] = FillInstruction({ destinationChainId: acrossOrderData.destinationChainId, - destinationSettler: _toBytes32(_destinationSettler(acrossOrderData.destinationChainId)), + destinationSettler: _destinationSettler(acrossOrderData.destinationChainId), originData: abi.encode( order.user, acrossOrderData.recipient, @@ -255,9 +262,9 @@ abstract contract ERC7683OrderDepositor is IOriginSettler { Output[] memory maxSpent = new Output[](1); maxSpent[0] = Output({ - token: _toBytes32(acrossOrderData.outputToken), + token: acrossOrderData.outputToken, amount: acrossOrderData.outputAmount, - recipient: _toBytes32(acrossOrderData.recipient), + recipient: acrossOrderData.recipient, chainId: acrossOrderData.destinationChainId }); @@ -267,16 +274,16 @@ abstract contract ERC7683OrderDepositor is IOriginSettler { // repayment on. Output[] memory minReceived = new Output[](1); minReceived[0] = Output({ - token: _toBytes32(acrossOrderData.inputToken), + token: acrossOrderData.inputToken, amount: acrossOrderData.inputAmount, - recipient: _toBytes32(acrossOrderData.exclusiveRelayer), + recipient: acrossOrderData.exclusiveRelayer, chainId: SafeCast.toUint32(block.chainid) }); FillInstruction[] memory fillInstructions = new FillInstruction[](1); fillInstructions[0] = FillInstruction({ destinationChainId: acrossOrderData.destinationChainId, - destinationSettler: _toBytes32(_destinationSettler(acrossOrderData.destinationChainId)), + destinationSettler: _destinationSettler(acrossOrderData.destinationChainId), originData: abi.encode( msg.sender, acrossOrderData.recipient, @@ -294,7 +301,7 @@ abstract contract ERC7683OrderDepositor is IOriginSettler { }); resolvedOrder = ResolvedCrossChainOrder({ - user: msg.sender, + user: msg.sender.toBytes32(), originChainId: SafeCast.toUint64(block.chainid), openDeadline: type(uint32).max, // no deadline since the user is sending it fillDeadline: order.fillDeadline, @@ -311,7 +318,7 @@ abstract contract ERC7683OrderDepositor is IOriginSettler { ) internal { IPermit2.PermitTransferFrom memory permit = IPermit2.PermitTransferFrom({ permitted: IPermit2.TokenPermissions({ - token: acrossOrderData.inputToken, + token: acrossOrderData.inputToken.toAddress(), amount: acrossOrderData.inputAmount }), nonce: order.nonce, @@ -327,26 +334,22 @@ abstract contract ERC7683OrderDepositor is IOriginSettler { PERMIT2.permitWitnessTransferFrom( permit, signatureTransferDetails, - order.user, + order.user.toAddress(), ERC7683Permit2Lib.hashOrder(order, ERC7683Permit2Lib.hashOrderData(acrossOrderData)), // witness data hash ERC7683Permit2Lib.PERMIT2_ORDER_TYPE, // witness data type string signature ); } - function _toBytes32(address input) internal pure returns (bytes32) { - return bytes32(uint256(uint160(input))); - } - function _callDeposit( - address depositor, - address recipient, - address inputToken, - address outputToken, + bytes32 depositor, + bytes32 recipient, + bytes32 inputToken, + bytes32 outputToken, uint256 inputAmount, uint256 outputAmount, uint256 destinationChainId, - address exclusiveRelayer, + bytes32 exclusiveRelayer, uint32 quoteTimestamp, uint32 fillDeadline, uint32 exclusivityDeadline, @@ -355,5 +358,5 @@ abstract contract ERC7683OrderDepositor is IOriginSettler { function _currentDepositId() internal view virtual returns (uint32); - function _destinationSettler(uint256 chainId) internal view virtual returns (address); + function _destinationSettler(uint256 chainId) internal view virtual returns (bytes32); } diff --git a/contracts/erc7683/ERC7683OrderDepositorExternal.sol b/contracts/erc7683/ERC7683OrderDepositorExternal.sol index 5b34ee74d..5e5b10cca 100644 --- a/contracts/erc7683/ERC7683OrderDepositorExternal.sol +++ b/contracts/erc7683/ERC7683OrderDepositorExternal.sol @@ -7,6 +7,7 @@ import { ERC7683OrderDepositor } from "./ERC7683OrderDepositor.sol"; import "../SpokePool.sol"; import "../external/interfaces/IPermit2.sol"; import "@uma/core/contracts/common/implementation/MultiCaller.sol"; +import { Bytes32ToAddress } from "../libraries/AddressConverters.sol"; /** * @notice ERC7683OrderDepositorExternal processes an external order type and translates it into an AcrossV3Deposit @@ -15,17 +16,17 @@ import "@uma/core/contracts/common/implementation/MultiCaller.sol"; */ contract ERC7683OrderDepositorExternal is ERC7683OrderDepositor, Ownable, MultiCaller { using SafeERC20 for IERC20; - + using Bytes32ToAddress for bytes32; event SetDestinationSettler( uint256 indexed chainId, - address indexed prevDestinationSettler, - address indexed destinationSettler + bytes32 indexed prevDestinationSettler, + bytes32 indexed destinationSettler ); SpokePool public immutable SPOKE_POOL; // Mapping of chainIds to destination settler addresses. - mapping(uint256 => address) public destinationSettlers; + mapping(uint256 => bytes32) public destinationSettlers; constructor( SpokePool _spokePool, @@ -35,27 +36,27 @@ contract ERC7683OrderDepositorExternal is ERC7683OrderDepositor, Ownable, MultiC SPOKE_POOL = _spokePool; } - function setDestinationSettler(uint256 chainId, address destinationSettler) external { - address prevDestinationSettler = destinationSettlers[chainId]; + function setDestinationSettler(uint256 chainId, bytes32 destinationSettler) external { + bytes32 prevDestinationSettler = destinationSettlers[chainId]; destinationSettlers[chainId] = destinationSettler; emit SetDestinationSettler(chainId, prevDestinationSettler, destinationSettler); } function _callDeposit( - address depositor, - address recipient, - address inputToken, - address outputToken, + bytes32 depositor, + bytes32 recipient, + bytes32 inputToken, + bytes32 outputToken, uint256 inputAmount, uint256 outputAmount, uint256 destinationChainId, - address exclusiveRelayer, + bytes32 exclusiveRelayer, uint32 quoteTimestamp, uint32 fillDeadline, uint32 exclusivityDeadline, bytes memory message ) internal override { - IERC20(inputToken).safeIncreaseAllowance(address(SPOKE_POOL), inputAmount); + IERC20(inputToken.toAddress()).safeIncreaseAllowance(address(SPOKE_POOL), inputAmount); SPOKE_POOL.depositV3( depositor, @@ -77,7 +78,7 @@ contract ERC7683OrderDepositorExternal is ERC7683OrderDepositor, Ownable, MultiC return SPOKE_POOL.numberOfDeposits(); } - function _destinationSettler(uint256 chainId) internal view override returns (address) { + function _destinationSettler(uint256 chainId) internal view override returns (bytes32) { return destinationSettlers[chainId]; } } diff --git a/contracts/interfaces/SpokePoolInterface.sol b/contracts/interfaces/SpokePoolInterface.sol index 08bd4aae8..248eee292 100644 --- a/contracts/interfaces/SpokePoolInterface.sol +++ b/contracts/interfaces/SpokePoolInterface.sol @@ -17,9 +17,9 @@ interface SpokePoolInterface { // Used as the index in the bitmap to track whether this leaf has been executed or not. uint32 leafId; // The associated L2TokenAddress that these claims apply to. - address l2TokenAddress; + bytes32 l2TokenAddress; // Must be same length as refundAmounts and designates each address that must be refunded. - address[] refundAddresses; + bytes32[] refundAddresses; } // Stores collection of merkle roots that can be published to this contract from the HubPool, which are referenced diff --git a/contracts/interfaces/V3SpokePoolInterface.sol b/contracts/interfaces/V3SpokePoolInterface.sol index 0be81c630..e63d688a1 100644 --- a/contracts/interfaces/V3SpokePoolInterface.sol +++ b/contracts/interfaces/V3SpokePoolInterface.sol @@ -36,16 +36,16 @@ interface V3SpokePoolInterface { // replay attacks on other chains. If any portion of this data differs, the relay is considered to be // completely distinct. struct V3RelayData { - // The address that made the deposit on the origin chain. - address depositor; - // The recipient address on the destination chain. - address recipient; + // The bytes32 that made the deposit on the origin chain. + bytes32 depositor; + // The recipient bytes32 on the destination chain. + bytes32 recipient; // This is the exclusive relayer who can fill the deposit before the exclusivity deadline. - address exclusiveRelayer; + bytes32 exclusiveRelayer; // Token that is deposited on origin chain by depositor. - address inputToken; + bytes32 inputToken; // Token that is received on destination chain by recipient. - address outputToken; + bytes32 outputToken; // The amount of input token deposited by depositor. uint256 inputAmount; // The amount of output token to be received by recipient. @@ -77,7 +77,7 @@ interface V3SpokePoolInterface { V3RelayData relay; bytes32 relayHash; uint256 updatedOutputAmount; - address updatedRecipient; + bytes32 updatedRecipient; bytes updatedMessage; uint256 repaymentChainId; } @@ -86,7 +86,7 @@ interface V3SpokePoolInterface { // Similar to V3RelayExecutionParams, these parameters are not used to uniquely identify the deposit being // filled so they don't have to be unpacked by all clients. struct V3RelayExecutionEventInfo { - address updatedRecipient; + bytes32 updatedRecipient; bytes updatedMessage; uint256 updatedOutputAmount; FillType fillType; @@ -97,8 +97,8 @@ interface V3SpokePoolInterface { **************************************/ event V3FundsDeposited( - address inputToken, - address outputToken, + bytes32 inputToken, + bytes32 outputToken, uint256 inputAmount, uint256 outputAmount, uint256 indexed destinationChainId, @@ -106,24 +106,24 @@ interface V3SpokePoolInterface { uint32 quoteTimestamp, uint32 fillDeadline, uint32 exclusivityDeadline, - address indexed depositor, - address recipient, - address exclusiveRelayer, + bytes32 indexed depositor, + bytes32 recipient, + bytes32 exclusiveRelayer, bytes message ); event RequestedSpeedUpV3Deposit( uint256 updatedOutputAmount, uint32 indexed depositId, - address indexed depositor, - address updatedRecipient, + bytes32 indexed depositor, + bytes32 updatedRecipient, bytes updatedMessage, bytes depositorSignature ); event FilledV3Relay( - address inputToken, - address outputToken, + bytes32 inputToken, + bytes32 outputToken, uint256 inputAmount, uint256 outputAmount, uint256 repaymentChainId, @@ -131,26 +131,26 @@ interface V3SpokePoolInterface { uint32 indexed depositId, uint32 fillDeadline, uint32 exclusivityDeadline, - address exclusiveRelayer, - address indexed relayer, - address depositor, - address recipient, + bytes32 exclusiveRelayer, + bytes32 indexed relayer, + bytes32 depositor, + bytes32 recipient, bytes message, V3RelayExecutionEventInfo relayExecutionInfo ); event RequestedV3SlowFill( - address inputToken, - address outputToken, + bytes32 inputToken, + bytes32 outputToken, uint256 inputAmount, uint256 outputAmount, uint256 indexed originChainId, uint32 indexed depositId, uint32 fillDeadline, uint32 exclusivityDeadline, - address exclusiveRelayer, - address depositor, - address recipient, + bytes32 exclusiveRelayer, + bytes32 depositor, + bytes32 recipient, bytes message ); @@ -159,14 +159,14 @@ interface V3SpokePoolInterface { **************************************/ function depositV3( - address depositor, - address recipient, - address inputToken, - address outputToken, + bytes32 depositor, + bytes32 recipient, + bytes32 inputToken, + bytes32 outputToken, uint256 inputAmount, uint256 outputAmount, uint256 destinationChainId, - address exclusiveRelayer, + bytes32 exclusiveRelayer, uint32 quoteTimestamp, uint32 fillDeadline, uint32 exclusivityDeadline, @@ -174,24 +174,24 @@ interface V3SpokePoolInterface { ) external payable; function depositV3Now( - address depositor, - address recipient, - address inputToken, - address outputToken, + bytes32 depositor, + bytes32 recipient, + bytes32 inputToken, + bytes32 outputToken, uint256 inputAmount, uint256 outputAmount, uint256 destinationChainId, - address exclusiveRelayer, + bytes32 exclusiveRelayer, uint32 fillDeadlineOffset, uint32 exclusivityDeadline, bytes calldata message ) external payable; function speedUpV3Deposit( - address depositor, + bytes32 depositor, uint32 depositId, uint256 updatedOutputAmount, - address updatedRecipient, + bytes32 updatedRecipient, bytes calldata updatedMessage, bytes calldata depositorSignature ) external; @@ -202,7 +202,7 @@ interface V3SpokePoolInterface { V3RelayData calldata relayData, uint256 repaymentChainId, uint256 updatedOutputAmount, - address updatedRecipient, + bytes32 updatedRecipient, bytes calldata updatedMessage, bytes calldata depositorSignature ) external; diff --git a/contracts/libraries/AddressConverters.sol b/contracts/libraries/AddressConverters.sol new file mode 100644 index 000000000..888629e17 --- /dev/null +++ b/contracts/libraries/AddressConverters.sol @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +library Bytes32ToAddress { + function toAddress(bytes32 _bytes32) internal pure returns (address) { + require(uint256(_bytes32) >> 192 == 0, "Invalid bytes32: highest 12 bytes must be 0"); + return address(uint160(uint256(_bytes32))); + } +} + +library AddressToBytes32 { + function toBytes32(address _address) internal pure returns (bytes32) { + return bytes32(uint256(uint160(_address))); + } +} diff --git a/contracts/libraries/CircleCCTPAdapter.sol b/contracts/libraries/CircleCCTPAdapter.sol index f1193fdb0..1df55ba11 100644 --- a/contracts/libraries/CircleCCTPAdapter.sol +++ b/contracts/libraries/CircleCCTPAdapter.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 "../external/interfaces/CCTPInterfaces.sol"; +import { AddressToBytes32 } from "../libraries/AddressConverters.sol"; library CircleDomainIds { uint32 public constant Ethereum = 0; @@ -24,13 +25,14 @@ library CircleDomainIds { */ abstract contract CircleCCTPAdapter { using SafeERC20 for IERC20; - + using AddressToBytes32 for address; /** * @notice The domain ID that CCTP will transfer funds to. * @dev This identifier is assigned by Circle and is not related to a chain ID. * @dev Official domain list can be found here: https://developers.circle.com/stablecoins/docs/supported-domains */ /// @custom:oz-upgrades-unsafe-allow state-variable-immutable + uint32 public immutable recipientCircleDomainId; /** @@ -64,15 +66,6 @@ abstract contract CircleCCTPAdapter { recipientCircleDomainId = _recipientCircleDomainId; } - /** - * @notice converts address to bytes32 (alignment preserving cast.) - * @param addr the address to convert to bytes32 - * @dev Sourced from the official CCTP repo: https://github.com/walkerq/evm-cctp-contracts/blob/139d8d0ce3b5531d3c7ec284f89d946dfb720016/src/messages/Message.sol#L142C1-L148C6 - */ - function _addressToBytes32(address addr) internal pure returns (bytes32) { - return bytes32(uint256(uint160(addr))); - } - /** * @notice Returns whether or not the CCTP bridge is enabled. * @dev If the CCTPTokenMessenger is the zero address, CCTP bridging is disabled. @@ -88,7 +81,7 @@ abstract contract CircleCCTPAdapter { * @param amount Amount of USDC to transfer. */ function _transferUsdc(address to, uint256 amount) internal { - _transferUsdc(_addressToBytes32(to), amount); + _transferUsdc(to.toBytes32(), amount); } /** diff --git a/contracts/permit2-order/Permit2Depositor.sol b/contracts/permit2-order/Permit2Depositor.sol index 8a72318c9..d265e24e1 100644 --- a/contracts/permit2-order/Permit2Depositor.sol +++ b/contracts/permit2-order/Permit2Depositor.sol @@ -8,12 +8,14 @@ import "../interfaces/V3SpokePoolInterface.sol"; import "@openzeppelin/contracts/utils/math/SafeCast.sol"; import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { AddressToBytes32 } from "../libraries/AddressConverters.sol"; /** * @notice Permit2Depositor processes an external order type and translates it into an AcrossV3 deposit. */ contract Permit2Depositor { using SafeERC20 for IERC20; + using AddressToBytes32 for address; // SpokePool that this contract can deposit to. V3SpokePoolInterface public immutable SPOKE_POOL; @@ -58,15 +60,15 @@ contract Permit2Depositor { IERC20(order.input.token).safeIncreaseAllowance(address(SPOKE_POOL), amountToDeposit); SPOKE_POOL.depositV3( - order.info.offerer, + order.info.offerer.toBytes32(), // Note: Permit2OrderLib checks that order only has a single output. - order.outputs[0].recipient, - order.input.token, - order.outputs[0].token, + order.outputs[0].recipient.toBytes32(), + order.input.token.toBytes32(), + order.outputs[0].token.toBytes32(), amountToDeposit, order.outputs[0].amount, order.outputs[0].chainId, - destinationChainFillerAddress, + destinationChainFillerAddress.toBytes32(), SafeCast.toUint32(order.info.initiateDeadline - QUOTE_BEFORE_DEADLINE), fillDeadline, // The entire fill period is exclusive. diff --git a/contracts/test/MockSpokePool.sol b/contracts/test/MockSpokePool.sol index 6c7ddbe90..33ade9230 100644 --- a/contracts/test/MockSpokePool.sol +++ b/contracts/test/MockSpokePool.sol @@ -5,6 +5,7 @@ import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; import "../SpokePool.sol"; import "./interfaces/MockV2SpokePoolInterface.sol"; import "./V2MerkleLib.sol"; +import { AddressToBytes32, Bytes32ToAddress } from "../libraries/AddressConverters.sol"; /** * @title MockSpokePool @@ -12,6 +13,8 @@ import "./V2MerkleLib.sol"; */ contract MockSpokePool is SpokePool, MockV2SpokePoolInterface, OwnableUpgradeable { using SafeERC20Upgradeable for IERC20Upgradeable; + using AddressToBytes32 for address; + using Bytes32ToAddress for bytes32; uint256 private chainId_; uint256 private currentTime; @@ -25,7 +28,7 @@ contract MockSpokePool is SpokePool, MockV2SpokePoolInterface, OwnableUpgradeabl ); event BridgedToHubPool(uint256 amount, address token); - event PreLeafExecuteHook(address token); + event PreLeafExecuteHook(bytes32 token); /// @custom:oz-upgrades-unsafe-allow constructor constructor(address _wrappedNativeTokenAddress) SpokePool(_wrappedNativeTokenAddress, 1 hours, 9 hours) {} // solhint-disable-line no-empty-blocks @@ -49,8 +52,8 @@ contract MockSpokePool is SpokePool, MockV2SpokePoolInterface, OwnableUpgradeabl uint256 amountToReturn, uint256[] memory refundAmounts, uint32 leafId, - address l2TokenAddress, - address[] memory refundAddresses + bytes32 l2TokenAddress, + bytes32[] memory refundAddresses ) external { _distributeRelayerRefunds(_chainId, amountToReturn, refundAmounts, leafId, l2TokenAddress, refundAddresses); } @@ -60,7 +63,7 @@ contract MockSpokePool is SpokePool, MockV2SpokePoolInterface, OwnableUpgradeabl uint32 depositId, uint256 originChainId, int64 updatedRelayerFeePct, - address updatedRecipient, + bytes32 updatedRecipient, bytes memory updatedMessage, bytes memory depositorSignature ) internal view { @@ -83,11 +86,11 @@ contract MockSpokePool is SpokePool, MockV2SpokePoolInterface, OwnableUpgradeabl } function verifyUpdateV3DepositMessage( - address depositor, + bytes32 depositor, uint32 depositId, uint256 originChainId, uint256 updatedOutputAmount, - address updatedRecipient, + bytes32 updatedRecipient, bytes memory updatedMessage, bytes memory depositorSignature ) public view { @@ -99,13 +102,36 @@ contract MockSpokePool is SpokePool, MockV2SpokePoolInterface, OwnableUpgradeabl updatedOutputAmount, updatedRecipient, updatedMessage, - depositorSignature + depositorSignature, + UPDATE_V3_DEPOSIT_DETAILS_HASH + ); + } + + function verifyUpdateV3DepositMessage( + address depositor, + uint32 depositId, + uint256 originChainId, + uint256 updatedOutputAmount, + address updatedRecipient, + bytes memory updatedMessage, + bytes memory depositorSignature + ) public view { + return + _verifyUpdateV3DepositMessage( + depositor.toBytes32(), + depositId, + originChainId, + updatedOutputAmount, + updatedRecipient.toBytes32(), + updatedMessage, + depositorSignature, + UPDATE_V3_DEPOSIT_ADDRESS_OVERLOAD_DETAILS_HASH ); } function fillRelayV3Internal( V3RelayExecutionParams memory relayExecution, - address relayer, + bytes32 relayer, bool isSlowFill ) external { _fillRelayV3(relayExecution, relayer, isSlowFill); @@ -126,7 +152,7 @@ contract MockSpokePool is SpokePool, MockV2SpokePoolInterface, OwnableUpgradeabl return currentTime; } - function _preExecuteLeafHook(address token) internal override { + function _preExecuteLeafHook(bytes32 token) internal override { emit PreLeafExecuteHook(token); } @@ -161,7 +187,9 @@ contract MockSpokePool is SpokePool, MockV2SpokePoolInterface, OwnableUpgradeabl if (originToken == address(wrappedNativeToken) && msg.value > 0) { require(msg.value == amount); wrappedNativeToken.deposit{ value: msg.value }(); - } else IERC20Upgradeable(originToken).safeTransferFrom(msg.sender, address(this), amount); + } else { + IERC20Upgradeable(originToken).safeTransferFrom(msg.sender, address(this), amount); + } emit FundsDeposited( amount, @@ -178,17 +206,17 @@ contract MockSpokePool is SpokePool, MockV2SpokePoolInterface, OwnableUpgradeabl } function speedUpDeposit( - address depositor, + bytes32 depositor, int64 updatedRelayerFeePct, uint32 depositId, - address updatedRecipient, + bytes32 updatedRecipient, bytes memory updatedMessage, bytes memory depositorSignature ) public nonReentrant { require(SignedMath.abs(updatedRelayerFeePct) < 0.5e18, "Invalid relayer fee"); _verifyUpdateDepositMessage( - depositor, + depositor.toAddress(), depositId, chainId(), updatedRelayerFeePct, @@ -210,9 +238,9 @@ contract MockSpokePool is SpokePool, MockV2SpokePoolInterface, OwnableUpgradeabl } function fillRelay( - address depositor, - address recipient, - address destinationToken, + bytes32 depositor, + bytes32 recipient, + bytes32 destinationToken, uint256 amount, uint256 maxTokensToSend, uint256 repaymentChainId, @@ -253,9 +281,9 @@ contract MockSpokePool is SpokePool, MockV2SpokePoolInterface, OwnableUpgradeabl } function executeSlowRelayLeaf( - address depositor, - address recipient, - address destinationToken, + bytes32 depositor, + bytes32 recipient, + bytes32 destinationToken, uint256 amount, uint256 originChainId, int64 realizedLpFeePct, @@ -284,10 +312,10 @@ contract MockSpokePool is SpokePool, MockV2SpokePoolInterface, OwnableUpgradeabl } function fillRelayWithUpdatedDeposit( - address depositor, - address recipient, - address updatedRecipient, - address destinationToken, + bytes32 depositor, + bytes32 recipient, + bytes32 updatedRecipient, + bytes32 destinationToken, uint256 amount, uint256 maxTokensToSend, uint256 repaymentChainId, @@ -327,7 +355,7 @@ contract MockSpokePool is SpokePool, MockV2SpokePoolInterface, OwnableUpgradeabl relayExecution.relayHash = _getRelayHash(relayExecution.relay); _verifyUpdateDepositMessage( - depositor, + depositor.toAddress(), depositId, originChainId, updatedRelayerFeePct, @@ -340,9 +368,9 @@ contract MockSpokePool is SpokePool, MockV2SpokePoolInterface, OwnableUpgradeabl } function _executeSlowRelayLeaf( - address depositor, - address recipient, - address destinationToken, + bytes32 depositor, + bytes32 recipient, + bytes32 destinationToken, uint256 amount, uint256 originChainId, uint256 destinationChainId, @@ -435,24 +463,32 @@ contract MockSpokePool is SpokePool, MockV2SpokePoolInterface, OwnableUpgradeabl relayFills[relayExecution.relayHash] += fillAmountPreFees; - if (msg.sender == relayExecution.updatedRecipient && !relayExecution.slowFill) return fillAmountPreFees; + if (msg.sender.toBytes32() == relayExecution.updatedRecipient && !relayExecution.slowFill) { + return fillAmountPreFees; + } - if (relayData.destinationToken == address(wrappedNativeToken)) { - if (!relayExecution.slowFill) - IERC20Upgradeable(relayData.destinationToken).safeTransferFrom(msg.sender, address(this), amountToSend); - _unwrapwrappedNativeTokenTo(payable(relayExecution.updatedRecipient), amountToSend); + if (relayData.destinationToken == address(wrappedNativeToken).toBytes32()) { + if (!relayExecution.slowFill) { + IERC20Upgradeable(relayData.destinationToken.toAddress()).safeTransferFrom( + msg.sender, + address(this), + amountToSend + ); + } + _unwrapwrappedNativeTokenTo(payable(relayExecution.updatedRecipient.toAddress()), amountToSend); } else { - if (!relayExecution.slowFill) - IERC20Upgradeable(relayData.destinationToken).safeTransferFrom( + if (!relayExecution.slowFill) { + IERC20Upgradeable(relayData.destinationToken.toAddress()).safeTransferFrom( msg.sender, - relayExecution.updatedRecipient, + relayExecution.updatedRecipient.toAddress(), amountToSend ); - else - IERC20Upgradeable(relayData.destinationToken).safeTransfer( - relayExecution.updatedRecipient, + } else { + IERC20Upgradeable(relayData.destinationToken.toAddress()).safeTransfer( + relayExecution.updatedRecipient.toAddress(), amountToSend ); + } } } @@ -492,7 +528,7 @@ contract MockSpokePool is SpokePool, MockV2SpokePoolInterface, OwnableUpgradeabl relayExecution.relay.realizedLpFeePct, relayExecution.relay.depositId, relayExecution.relay.destinationToken, - msg.sender, + msg.sender.toBytes32(), relayExecution.relay.depositor, relayExecution.relay.recipient, relayExecution.relay.message, diff --git a/contracts/test/interfaces/MockV2SpokePoolInterface.sol b/contracts/test/interfaces/MockV2SpokePoolInterface.sol index 8f2f8acf2..214012865 100644 --- a/contracts/test/interfaces/MockV2SpokePoolInterface.sol +++ b/contracts/test/interfaces/MockV2SpokePoolInterface.sol @@ -6,9 +6,9 @@ pragma solidity ^0.8.0; */ interface MockV2SpokePoolInterface { struct RelayData { - address depositor; - address recipient; - address destinationToken; + bytes32 depositor; + bytes32 recipient; + bytes32 destinationToken; uint256 amount; uint256 originChainId; uint256 destinationChainId; @@ -22,7 +22,7 @@ interface MockV2SpokePoolInterface { RelayData relay; bytes32 relayHash; int64 updatedRelayerFeePct; - address updatedRecipient; + bytes32 updatedRecipient; bytes updatedMessage; uint256 repaymentChainId; uint256 maxTokensToSend; diff --git a/test/evm/foundry/local/MultiCallerUpgradeable.t.sol b/test/evm/foundry/local/MultiCallerUpgradeable.t.sol index 516e0d0a0..c50b87540 100644 --- a/test/evm/foundry/local/MultiCallerUpgradeable.t.sol +++ b/test/evm/foundry/local/MultiCallerUpgradeable.t.sol @@ -8,11 +8,14 @@ import { SpokePool } from "../../../../contracts/SpokePool.sol"; import { Ethereum_SpokePool } from "../../../../contracts/Ethereum_SpokePool.sol"; import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { AddressToBytes32 } from "../../../../contracts/libraries/AddressConverters.sol"; // This test does not require a mainnet fork (since it is testing contracts before deployment). contract MultiCallerUpgradeableTest is Test { Ethereum_SpokePool ethereumSpokePool; + using AddressToBytes32 for address; + ERC20 mockWETH; ERC20 mockL2WETH; @@ -42,11 +45,11 @@ contract MultiCallerUpgradeableTest is Test { uint256 mockRepaymentChainId = 1; uint32 fillDeadline = uint32(ethereumSpokePool.getCurrentTime()) + 1000; - mockRelayData.depositor = rando1; - mockRelayData.recipient = rando2; - mockRelayData.exclusiveRelayer = relayer; - mockRelayData.inputToken = address(mockWETH); - mockRelayData.outputToken = address(mockL2WETH); + mockRelayData.depositor = rando1.toBytes32(); + mockRelayData.recipient = rando2.toBytes32(); + mockRelayData.exclusiveRelayer = relayer.toBytes32(); + mockRelayData.inputToken = address(mockWETH).toBytes32(); + mockRelayData.outputToken = address(mockL2WETH).toBytes32(); mockRelayData.inputAmount = depositAmount; mockRelayData.outputAmount = depositAmount; mockRelayData.originChainId = mockRepaymentChainId; diff --git a/test/evm/foundry/local/SpokePoolVerifier.t.sol b/test/evm/foundry/local/SpokePoolVerifier.t.sol index 391116244..3b36a006f 100644 --- a/test/evm/foundry/local/SpokePoolVerifier.t.sol +++ b/test/evm/foundry/local/SpokePoolVerifier.t.sol @@ -9,11 +9,31 @@ import { V3SpokePoolInterface } from "../../../../contracts/interfaces/V3SpokePo import { WETH9 } from "../../../../contracts/external/WETH9.sol"; import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import { ERC1967Proxy } from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; +import { AddressToBytes32 } from "../../../../contracts/libraries/AddressConverters.sol"; + +interface EthereumSpokePoolOnlyAddressInterface { + function depositV3( + bytes32 depositor, + bytes32 recipient, + bytes32 inputToken, + bytes32 outputToken, + uint256 inputAmount, + uint256 outputAmount, + uint256 destinationChainId, + bytes32 exclusiveRelayer, + uint32 quoteTimestamp, + uint32 fillDeadline, + uint32 exclusivityDeadline, + bytes calldata message + ) external payable; +} contract SpokePoolVerifierTest is Test { Ethereum_SpokePool ethereumSpokePool; SpokePoolVerifier spokePoolVerifier; + using AddressToBytes32 for address; + ERC20 mockWETH; ERC20 mockERC20; @@ -60,12 +80,12 @@ contract SpokePoolVerifierTest is Test { vm.expectRevert(SpokePoolVerifier.InvalidMsgValue.selector); spokePoolVerifier.deposit{ value: 0 }( ethereumSpokePool, // spokePool - depositor, // recipient - address(mockWETH), // inputToken + depositor.toBytes32(), // recipient + address(mockWETH).toBytes32(), // inputToken depositAmount, // inputAmount depositAmount, // outputAmount destinationChainId, // destinationChainId - address(0), // exclusiveRelayer + bytes32(0), // exclusiveRelayer uint32(block.timestamp), // quoteTimestamp uint32(block.timestamp) + fillDeadlineBuffer, // fillDeadline 0, // exclusivityDeadline @@ -76,12 +96,12 @@ contract SpokePoolVerifierTest is Test { vm.expectRevert(V3SpokePoolInterface.MsgValueDoesNotMatchInputAmount.selector); spokePoolVerifier.deposit{ value: depositAmount }( ethereumSpokePool, // spokePool - depositor, // recipient - address(mockERC20), // inputToken + depositor.toBytes32(), // recipient + address(mockERC20).toBytes32(), // inputToken depositAmount, // inputAmount depositAmount, // outputAmount destinationChainId, // destinationChainId - address(0), // exclusiveRelayer + bytes32(0), // exclusiveRelayer uint32(block.timestamp), // quoteTimestamp uint32(block.timestamp) + fillDeadlineBuffer, // fillDeadline 0, // exclusivityDeadline @@ -98,12 +118,12 @@ contract SpokePoolVerifierTest is Test { vm.expectRevert(SpokePoolVerifier.InvalidSpokePool.selector); spokePoolVerifier.deposit{ value: depositAmount }( V3SpokePoolInterface(address(0)), // spokePool - depositor, // recipient - address(mockWETH), // inputToken + depositor.toBytes32(), // recipient + address(mockWETH).toBytes32(), // inputToken depositAmount, // inputAmount depositAmount, // outputAmount destinationChainId, // destinationChainId - address(0), // exclusiveRelayer + bytes32(0), // exclusiveRelayer uint32(block.timestamp), // quoteTimestamp uint32(block.timestamp) + fillDeadlineBuffer, // fillDeadline 0, // exclusivityDeadline @@ -121,31 +141,31 @@ contract SpokePoolVerifierTest is Test { address(ethereumSpokePool), // callee depositAmount, // value abi.encodeCall( // data - ethereumSpokePool.depositV3, + EthereumSpokePoolOnlyAddressInterface.depositV3, ( - depositor, - depositor, - address(mockWETH), - address(0), + depositor.toBytes32(), + depositor.toBytes32(), + address(mockWETH).toBytes32(), + bytes32(0), depositAmount, depositAmount, destinationChainId, - address(0), + bytes32(0), uint32(block.timestamp), uint32(block.timestamp) + fillDeadlineBuffer, - 0, + uint32(0), bytes("") ) ) ); spokePoolVerifier.deposit{ value: depositAmount }( ethereumSpokePool, // spokePool - depositor, // recipient - address(mockWETH), // inputToken + depositor.toBytes32(), // recipient + address(mockWETH).toBytes32(), // inputToken depositAmount, // inputAmount depositAmount, // outputAmount destinationChainId, // destinationChainId - address(0), // exclusiveRelayer + bytes32(0), // exclusiveRelayer uint32(block.timestamp), // quoteTimestamp uint32(block.timestamp) + fillDeadlineBuffer, // fillDeadline 0, // exclusivityDeadline diff --git a/test/evm/hardhat/MerkleLib.Proofs.ts b/test/evm/hardhat/MerkleLib.Proofs.ts index 62ec669c8..ff756e7a8 100644 --- a/test/evm/hardhat/MerkleLib.Proofs.ts +++ b/test/evm/hardhat/MerkleLib.Proofs.ts @@ -4,13 +4,14 @@ import { MerkleTree, EMPTY_MERKLE_ROOT } from "../../../utils/MerkleTree"; import { expect, randomBigNumber, - randomAddress, getParamType, defaultAbiCoder, keccak256, Contract, BigNumber, ethers, + randomBytes32, + randomAddress, } from "../../../utils/utils"; import { V3RelayData, V3SlowFill } from "../../../test-utils"; @@ -80,14 +81,14 @@ describe("MerkleLib Proofs", async function () { const refundAddresses: string[] = []; const refundAmounts: BigNumber[] = []; for (let j = 0; j < numAddresses; j++) { - refundAddresses.push(randomAddress()); + refundAddresses.push(randomBytes32()); refundAmounts.push(randomBigNumber()); } relayerRefundLeaves.push({ leafId: BigNumber.from(i), chainId: randomBigNumber(2), amountToReturn: randomBigNumber(), - l2TokenAddress: randomAddress(), + l2TokenAddress: randomBytes32(), refundAddresses, refundAmounts, }); @@ -113,11 +114,11 @@ describe("MerkleLib Proofs", async function () { const numDistributions = 101; // Create 101 and remove the last to use as the "invalid" one. for (let i = 0; i < numDistributions; i++) { const relayData: V3RelayData = { - depositor: randomAddress(), - recipient: randomAddress(), - exclusiveRelayer: randomAddress(), - inputToken: randomAddress(), - outputToken: randomAddress(), + depositor: randomBytes32(), + recipient: randomBytes32(), + exclusiveRelayer: randomBytes32(), + inputToken: randomBytes32(), + outputToken: randomBytes32(), inputAmount: randomBigNumber(), outputAmount: randomBigNumber(), originChainId: randomBigNumber(2).toNumber(), diff --git a/test/evm/hardhat/MerkleLib.utils.ts b/test/evm/hardhat/MerkleLib.utils.ts index 20a0e43d1..2508d7398 100644 --- a/test/evm/hardhat/MerkleLib.utils.ts +++ b/test/evm/hardhat/MerkleLib.utils.ts @@ -7,6 +7,7 @@ import { toBNWeiWithDecimals, createRandomBytes32, Contract, + hexZeroPadAddress, } from "../../../utils/utils"; import { amountToReturn, repaymentChainId } from "./constants"; import { MerkleTree } from "../../../utils/MerkleTree"; @@ -109,7 +110,7 @@ export async function constructSingleRelayerRefundTree(l2Token: Contract | Strin const leaves = buildRelayerRefundLeaves( [destinationChainId], // Destination chain ID. [amountToReturn], // amountToReturn. - [l2Token as string], // l2Token. + [hexZeroPadAddress(l2Token as string)], // l2Token. [[]], // refundAddresses. [[]] // refundAmounts. ); diff --git a/test/evm/hardhat/SpokePool.Admin.ts b/test/evm/hardhat/SpokePool.Admin.ts index eea2b0667..955c10e13 100644 --- a/test/evm/hardhat/SpokePool.Admin.ts +++ b/test/evm/hardhat/SpokePool.Admin.ts @@ -1,4 +1,11 @@ -import { expect, ethers, Contract, SignerWithAddress, getContractFactory } from "../../../utils/utils"; +import { + expect, + ethers, + Contract, + SignerWithAddress, + getContractFactory, + hexZeroPadAddress, +} from "../../../utils/utils"; import { hre } from "../../../utils/utils.hre"; import { spokePoolFixture } from "./fixtures/SpokePool.Fixture"; import { destinationChainId, mockRelayerRefundRoot, mockSlowRelayRoot } from "./constants"; @@ -23,7 +30,7 @@ describe("SpokePool Admin Functions", async function () { await expect(spokePool.connect(owner).setEnableRoute(erc20.address, destinationChainId, true)) .to.emit(spokePool, "EnabledDepositRoute") .withArgs(erc20.address, destinationChainId, true); - expect(await spokePool.enabledDepositRoutes(erc20.address, destinationChainId)).to.equal(true); + expect(await spokePool.enabledDepositRoutes(hexZeroPadAddress(erc20.address), destinationChainId)).to.equal(true); }); it("Pause deposits", async function () { diff --git a/test/evm/hardhat/SpokePool.Deposit.ts b/test/evm/hardhat/SpokePool.Deposit.ts index 434917ecc..4698dbd3a 100644 --- a/test/evm/hardhat/SpokePool.Deposit.ts +++ b/test/evm/hardhat/SpokePool.Deposit.ts @@ -8,6 +8,9 @@ import { toWei, randomAddress, BigNumber, + hexZeroPadAddressLowercase, + hexZeroPadAddress, + bytes32ToAddress, } from "../../../utils/utils"; import { spokePoolFixture, @@ -30,6 +33,24 @@ import { const { AddressZero: ZERO_ADDRESS } = ethers.constants; +const depositV3Bytes = + "depositV3(bytes32,bytes32,bytes32,bytes32,uint256,uint256,uint256,bytes32,uint32,uint32,uint32,bytes)"; +const depositV3Address = + "depositV3(address,address,address,address,uint256,uint256,uint256,address,uint32,uint32,uint32,bytes)"; + +const depositV3NowBytes = + "depositV3Now(bytes32,bytes32,bytes32,bytes32,uint256,uint256,uint256,bytes32,uint32,uint32,bytes)"; +const depositV3NowAddress = + "depositV3Now(address,address,address,address,uint256,uint256,uint256,address,uint32,uint32,bytes)"; + +const speedUpV3DepositBytes = "speedUpV3Deposit(bytes32,uint32,uint256,bytes32,bytes,bytes)"; +const speedUpV3DepositAddress = "speedUpV3Deposit(address,uint32,uint256,address,bytes,bytes)"; + +const verifyUpdateV3DepositMessageBytes = + "verifyUpdateV3DepositMessage(bytes32,uint32,uint256,uint256,bytes32,bytes,bytes)"; +const verifyUpdateV3DepositMessageAddress = + "verifyUpdateV3DepositMessage(address,uint32,uint256,uint256,address,bytes,bytes)"; + describe("SpokePool Depositor Logic", async function () { let spokePool: Contract, weth: Contract, erc20: Contract, unwhitelistedErc20: Contract; let depositor: SignerWithAddress, recipient: SignerWithAddress; @@ -87,8 +108,8 @@ describe("SpokePool Depositor Logic", async function () { ) .to.emit(spokePool, "V3FundsDeposited") .withArgs( - erc20.address, - ZERO_ADDRESS, + hexZeroPadAddressLowercase(erc20.address), + hexZeroPadAddressLowercase(ZERO_ADDRESS), amountToDeposit, amountReceived, destinationChainId, @@ -96,9 +117,9 @@ describe("SpokePool Depositor Logic", async function () { quoteTimestamp, MAX_UINT32, 0, - depositor.address, - recipient.address, - ZERO_ADDRESS, + hexZeroPadAddressLowercase(depositor.address), + hexZeroPadAddressLowercase(recipient.address), + hexZeroPadAddressLowercase(ZERO_ADDRESS), "0x" ); @@ -127,8 +148,8 @@ describe("SpokePool Depositor Logic", async function () { ) .to.emit(spokePool, "V3FundsDeposited") .withArgs( - erc20.address, - ZERO_ADDRESS, + hexZeroPadAddressLowercase(erc20.address), + hexZeroPadAddressLowercase(ZERO_ADDRESS), amountToDeposit, amountReceived, destinationChainId, @@ -136,9 +157,9 @@ describe("SpokePool Depositor Logic", async function () { quoteTimestamp, BigNumber.from("0xFFFFFFFF"), 0, - newDepositor, // Depositor is overridden. - recipient.address, - ZERO_ADDRESS, + hexZeroPadAddressLowercase(newDepositor), // Depositor is overridden. + hexZeroPadAddressLowercase(recipient.address), + hexZeroPadAddressLowercase(ZERO_ADDRESS), "0x" ); }); @@ -349,17 +370,20 @@ describe("SpokePool Depositor Logic", async function () { function getDepositArgsFromRelayData( _relayData: V3RelayData, _destinationChainId = destinationChainId, - _quoteTimestamp = quoteTimestamp + _quoteTimestamp = quoteTimestamp, + _isAddressOverload = false ) { return [ - _relayData.depositor, - _relayData.recipient, - _relayData.inputToken, - _relayData.outputToken, + _isAddressOverload ? bytes32ToAddress(_relayData.depositor) : hexZeroPadAddress(_relayData.depositor), + _isAddressOverload ? bytes32ToAddress(_relayData.recipient) : hexZeroPadAddress(_relayData.recipient), + _isAddressOverload ? bytes32ToAddress(_relayData.inputToken) : hexZeroPadAddress(_relayData.inputToken), + _isAddressOverload ? bytes32ToAddress(_relayData.outputToken) : hexZeroPadAddress(_relayData.outputToken), _relayData.inputAmount, _relayData.outputAmount, _destinationChainId, - _relayData.exclusiveRelayer, + _isAddressOverload + ? bytes32ToAddress(_relayData.exclusiveRelayer) + : hexZeroPadAddress(_relayData.exclusiveRelayer), _quoteTimestamp, _relayData.fillDeadline, _relayData.exclusivityDeadline, @@ -368,11 +392,11 @@ describe("SpokePool Depositor Logic", async function () { } beforeEach(async function () { relayData = { - depositor: depositor.address, - recipient: recipient.address, - exclusiveRelayer: ZERO_ADDRESS, - inputToken: erc20.address, - outputToken: randomAddress(), + depositor: hexZeroPadAddress(depositor.address), + recipient: hexZeroPadAddress(recipient.address), + exclusiveRelayer: hexZeroPadAddress(ZERO_ADDRESS), + inputToken: hexZeroPadAddress(erc20.address), + outputToken: hexZeroPadAddress(randomAddress()), inputAmount: amountToDeposit, outputAmount: amountToDeposit.sub(19), originChainId: originChainId, @@ -384,29 +408,34 @@ describe("SpokePool Depositor Logic", async function () { depositArgs = getDepositArgsFromRelayData(relayData); }); it("placeholder: gas test", async function () { - await spokePool.connect(depositor).depositV3(...depositArgs); + await spokePool.connect(depositor)[depositV3Bytes](...depositArgs); + }); + it("should allow depositv3 with address overload", async function () { + await spokePool + .connect(depositor) + [depositV3Address](...getDepositArgsFromRelayData(relayData, destinationChainId, quoteTimestamp, true)); }); it("route disabled", async function () { // Verify that routes are disabled by default for a new route const _depositArgs = getDepositArgsFromRelayData(relayData, 999); - await expect(spokePool.connect(depositor).depositV3(..._depositArgs)).to.be.revertedWith("DisabledRoute"); + await expect(spokePool.connect(depositor)[depositV3Bytes](..._depositArgs)).to.be.revertedWith("DisabledRoute"); // Enable the route: await spokePool.connect(depositor).setEnableRoute(erc20.address, 999, true); - await expect(spokePool.connect(depositor).depositV3(..._depositArgs)).to.not.be.reverted; + await expect(spokePool.connect(depositor)[depositV3Bytes](..._depositArgs)).to.not.be.reverted; }); it("invalid quoteTimestamp", async function () { const quoteTimeBuffer = await spokePool.depositQuoteTimeBuffer(); const currentTime = await spokePool.getCurrentTime(); await expect( - spokePool.connect(depositor).depositV3( + spokePool.connect(depositor)[depositV3Bytes]( // quoteTimestamp too far into past (i.e. beyond the buffer) ...getDepositArgsFromRelayData(relayData, destinationChainId, currentTime.sub(quoteTimeBuffer).sub(1)) ) ).to.be.revertedWith("InvalidQuoteTimestamp"); await expect( - spokePool.connect(depositor).depositV3( + spokePool.connect(depositor)[depositV3Bytes]( // quoteTimestamp right at the buffer is OK ...getDepositArgsFromRelayData(relayData, destinationChainId, currentTime.sub(quoteTimeBuffer)) ) @@ -418,19 +447,19 @@ describe("SpokePool Depositor Logic", async function () { const currentTime = await spokePool.getCurrentTime(); await expect( - spokePool.connect(depositor).depositV3( + spokePool.connect(depositor)[depositV3Bytes]( // fillDeadline too far into future (i.e. beyond the buffer) ...getDepositArgsFromRelayData({ ...relayData, fillDeadline: currentTime.add(fillDeadlineBuffer).add(1) }) ) ).to.be.revertedWith("InvalidFillDeadline"); await expect( - spokePool.connect(depositor).depositV3( + spokePool.connect(depositor)[depositV3Bytes]( // fillDeadline in past ...getDepositArgsFromRelayData({ ...relayData, fillDeadline: currentTime.sub(1) }) ) ).to.be.revertedWith("InvalidFillDeadline"); await expect( - spokePool.connect(depositor).depositV3( + spokePool.connect(depositor)[depositV3Bytes]( // fillDeadline right at the buffer is OK ...getDepositArgsFromRelayData({ ...relayData, fillDeadline: currentTime.add(fillDeadlineBuffer) }) ) @@ -440,14 +469,14 @@ describe("SpokePool Depositor Logic", async function () { await expect( spokePool .connect(depositor) - .depositV3(...getDepositArgsFromRelayData({ ...relayData, inputToken: weth.address }), { value: 1 }) + [depositV3Bytes](...getDepositArgsFromRelayData({ ...relayData, inputToken: weth.address }), { value: 1 }) ).to.be.revertedWith("MsgValueDoesNotMatchInputAmount"); // Pulls ETH from depositor and deposits it into WETH via the wrapped contract. await expect(() => spokePool .connect(depositor) - .depositV3(...getDepositArgsFromRelayData({ ...relayData, inputToken: weth.address }), { + [depositV3Bytes](...getDepositArgsFromRelayData({ ...relayData, inputToken: weth.address }), { value: amountToDeposit, }) ).to.changeEtherBalances([depositor, weth], [amountToDeposit.mul(toBN("-1")), amountToDeposit]); // ETH should transfer from depositor to WETH contract. @@ -457,18 +486,18 @@ describe("SpokePool Depositor Logic", async function () { }); it("if input token is not WETH then msg.value must be 0", async function () { await expect( - spokePool.connect(depositor).depositV3(...getDepositArgsFromRelayData(relayData), { value: 1 }) + spokePool.connect(depositor)[depositV3Bytes](...getDepositArgsFromRelayData(relayData), { value: 1 }) ).to.be.revertedWith("MsgValueDoesNotMatchInputAmount"); }); it("if input token is WETH and msg.value = 0, pulls ERC20 from depositor", async function () { await expect(() => spokePool .connect(depositor) - .depositV3(...getDepositArgsFromRelayData({ ...relayData, inputToken: weth.address }), { value: 0 }) + [depositV3Bytes](...getDepositArgsFromRelayData({ ...relayData, inputToken: weth.address }), { value: 0 }) ).to.changeTokenBalances(weth, [depositor, spokePool], [amountToDeposit.mul(toBN("-1")), amountToDeposit]); }); it("pulls input token from caller", async function () { - await expect(() => spokePool.connect(depositor).depositV3(...depositArgs)).to.changeTokenBalances( + await expect(() => spokePool.connect(depositor)[depositV3Bytes](...depositArgs)).to.changeTokenBalances( erc20, [depositor, spokePool], [amountToDeposit.mul(toBN("-1")), amountToDeposit] @@ -481,15 +510,54 @@ describe("SpokePool Depositor Logic", async function () { await expect( spokePool .connect(depositor) - .depositV3Now( - relayData.depositor, - relayData.recipient, - relayData.inputToken, - relayData.outputToken, + [depositV3NowBytes]( + hexZeroPadAddress(relayData.depositor), + hexZeroPadAddress(relayData.recipient), + hexZeroPadAddress(relayData.inputToken), + hexZeroPadAddress(relayData.outputToken), + relayData.inputAmount, + relayData.outputAmount, + destinationChainId, + hexZeroPadAddress(relayData.exclusiveRelayer), + fillDeadlineOffset, + exclusivityDeadline, + relayData.message + ) + ) + .to.emit(spokePool, "V3FundsDeposited") + .withArgs( + hexZeroPadAddressLowercase(relayData.inputToken), + hexZeroPadAddressLowercase(relayData.outputToken), + relayData.inputAmount, + relayData.outputAmount, + destinationChainId, + // deposit ID is 0 for first deposit + 0, + currentTime, // quoteTimestamp should be current time + currentTime + fillDeadlineOffset, // fillDeadline should be current time + offset + currentTime, + hexZeroPadAddressLowercase(relayData.depositor), + hexZeroPadAddressLowercase(relayData.recipient), + hexZeroPadAddressLowercase(relayData.exclusiveRelayer), + relayData.message + ); + }); + it("should allow depositV3Now with address overload", async function () { + const currentTime = (await spokePool.getCurrentTime()).toNumber(); + const fillDeadlineOffset = 1000; + const exclusivityDeadline = 0; + await expect( + spokePool + .connect(depositor) + [depositV3NowAddress]( + bytes32ToAddress(relayData.depositor), + bytes32ToAddress(relayData.recipient), + bytes32ToAddress(relayData.inputToken), + bytes32ToAddress(relayData.outputToken), relayData.inputAmount, relayData.outputAmount, destinationChainId, - relayData.exclusiveRelayer, + bytes32ToAddress(relayData.exclusiveRelayer), fillDeadlineOffset, exclusivityDeadline, relayData.message @@ -497,8 +565,8 @@ describe("SpokePool Depositor Logic", async function () { ) .to.emit(spokePool, "V3FundsDeposited") .withArgs( - relayData.inputToken, - relayData.outputToken, + hexZeroPadAddressLowercase(relayData.inputToken), + hexZeroPadAddressLowercase(relayData.outputToken), relayData.inputAmount, relayData.outputAmount, destinationChainId, @@ -507,19 +575,19 @@ describe("SpokePool Depositor Logic", async function () { currentTime, // quoteTimestamp should be current time currentTime + fillDeadlineOffset, // fillDeadline should be current time + offset currentTime, - relayData.depositor, - relayData.recipient, - relayData.exclusiveRelayer, + hexZeroPadAddressLowercase(relayData.depositor), + hexZeroPadAddressLowercase(relayData.recipient), + hexZeroPadAddressLowercase(relayData.exclusiveRelayer), relayData.message ); }); it("emits V3FundsDeposited event with correct deposit ID", async function () { const currentTime = (await spokePool.getCurrentTime()).toNumber(); - await expect(spokePool.connect(depositor).depositV3(...depositArgs)) + await expect(spokePool.connect(depositor)[depositV3Bytes](...depositArgs)) .to.emit(spokePool, "V3FundsDeposited") .withArgs( - relayData.inputToken, - relayData.outputToken, + hexZeroPadAddressLowercase(relayData.inputToken), + hexZeroPadAddressLowercase(relayData.outputToken), relayData.inputAmount, relayData.outputAmount, destinationChainId, @@ -528,14 +596,14 @@ describe("SpokePool Depositor Logic", async function () { quoteTimestamp, relayData.fillDeadline, currentTime, - relayData.depositor, - relayData.recipient, - relayData.exclusiveRelayer, + hexZeroPadAddressLowercase(relayData.depositor), + hexZeroPadAddressLowercase(relayData.recipient), + hexZeroPadAddressLowercase(relayData.exclusiveRelayer), relayData.message ); }); it("deposit ID state variable incremented", async function () { - await spokePool.connect(depositor).depositV3(...depositArgs); + await spokePool.connect(depositor)[depositV3Bytes](...depositArgs); expect(await spokePool.numberOfDeposits()).to.equal(1); }); it("tokens are always pulled from caller, even if different from specified depositor", async function () { @@ -545,12 +613,12 @@ describe("SpokePool Depositor Logic", async function () { await expect( spokePool .connect(depositor) - .depositV3(...getDepositArgsFromRelayData({ ...relayData, depositor: newDepositor })) + [depositV3Bytes](...getDepositArgsFromRelayData({ ...relayData, depositor: newDepositor })) ) .to.emit(spokePool, "V3FundsDeposited") .withArgs( - relayData.inputToken, - relayData.outputToken, + hexZeroPadAddressLowercase(relayData.inputToken), + hexZeroPadAddressLowercase(relayData.outputToken), relayData.inputAmount, relayData.outputAmount, destinationChainId, @@ -559,19 +627,21 @@ describe("SpokePool Depositor Logic", async function () { relayData.fillDeadline, currentTime, // New depositor - newDepositor, - relayData.recipient, - relayData.exclusiveRelayer, + hexZeroPadAddressLowercase(newDepositor), + hexZeroPadAddressLowercase(relayData.recipient), + hexZeroPadAddressLowercase(relayData.exclusiveRelayer), relayData.message ); expect(await erc20.balanceOf(depositor.address)).to.equal(balanceBefore.sub(amountToDeposit)); }); it("deposits are not paused", async function () { await spokePool.pauseDeposits(true); - await expect(spokePool.connect(depositor).depositV3(...depositArgs)).to.be.revertedWith("DepositsArePaused"); + await expect(spokePool.connect(depositor)[depositV3Bytes](...depositArgs)).to.be.revertedWith( + "DepositsArePaused" + ); }); it("reentrancy protected", async function () { - const functionCalldata = spokePool.interface.encodeFunctionData("depositV3", [...depositArgs]); + const functionCalldata = spokePool.interface.encodeFunctionData(depositV3Bytes, [...depositArgs]); await expect(spokePool.connect(depositor).callback(functionCalldata)).to.be.revertedWith( "ReentrancyGuard: reentrant call" ); @@ -588,27 +658,27 @@ describe("SpokePool Depositor Logic", async function () { depositId, originChainId, updatedOutputAmount, - updatedRecipient, + hexZeroPadAddress(updatedRecipient), updatedMessage ); - await spokePool.verifyUpdateV3DepositMessage( - depositor.address, + await spokePool[verifyUpdateV3DepositMessageBytes]( + hexZeroPadAddress(depositor.address), depositId, originChainId, updatedOutputAmount, - updatedRecipient, + hexZeroPadAddress(updatedRecipient), updatedMessage, signature ); // Reverts if passed in depositor is the signer or if signature is incorrect await expect( - spokePool.verifyUpdateV3DepositMessage( - updatedRecipient, + spokePool[verifyUpdateV3DepositMessageBytes]( + hexZeroPadAddress(updatedRecipient), depositId, originChainId, updatedOutputAmount, - updatedRecipient, + hexZeroPadAddress(updatedRecipient), updatedMessage, signature ) @@ -620,16 +690,16 @@ describe("SpokePool Depositor Logic", async function () { depositId + 1, originChainId, updatedOutputAmount, - updatedRecipient, + hexZeroPadAddress(updatedRecipient), updatedMessage ); await expect( - spokePool.verifyUpdateV3DepositMessage( - depositor.address, + spokePool[verifyUpdateV3DepositMessageBytes]( + hexZeroPadAddress(depositor.address), depositId, originChainId, updatedOutputAmount, - updatedRecipient, + hexZeroPadAddress(updatedRecipient), updatedMessage, invalidSignature ) @@ -643,25 +713,27 @@ describe("SpokePool Depositor Logic", async function () { depositId, spokePoolChainId, updatedOutputAmount, - updatedRecipient, + hexZeroPadAddress(updatedRecipient), updatedMessage ); await expect( - spokePool.speedUpV3Deposit( - depositor.address, - depositId, - updatedOutputAmount, - updatedRecipient, - updatedMessage, - expectedSignature - ) + spokePool + .connect(depositor) + [speedUpV3DepositBytes]( + hexZeroPadAddress(depositor.address), + depositId, + updatedOutputAmount, + hexZeroPadAddress(updatedRecipient), + updatedMessage, + expectedSignature + ) ) .to.emit(spokePool, "RequestedSpeedUpV3Deposit") .withArgs( updatedOutputAmount, depositId, - depositor.address, - updatedRecipient, + hexZeroPadAddressLowercase(depositor.address), + hexZeroPadAddressLowercase(updatedRecipient), updatedMessage, expectedSignature ); @@ -673,30 +745,81 @@ describe("SpokePool Depositor Logic", async function () { depositId, otherChainId, updatedOutputAmount, - updatedRecipient, + hexZeroPadAddress(updatedRecipient), updatedMessage ); await expect( - spokePool.verifyUpdateV3DepositMessage( - depositor.address, + spokePool[verifyUpdateV3DepositMessageBytes]( + hexZeroPadAddress(depositor.address), depositId, otherChainId, updatedOutputAmount, - updatedRecipient, + hexZeroPadAddress(updatedRecipient), updatedMessage, invalidSignatureForChain ) ).to.not.be.reverted; await expect( - spokePool.speedUpV3Deposit( - depositor.address, - depositId, + spokePool + .connect(depositor) + [speedUpV3DepositBytes]( + hexZeroPadAddress(depositor.address), + depositId, + updatedOutputAmount, + hexZeroPadAddress(updatedRecipient), + updatedMessage, + invalidSignatureForChain + ) + ).to.be.revertedWith("InvalidDepositorSignature"); + }); + it("should allow speeding up V3 deposit with address overload", async function () { + const updatedOutputAmount = amountToDeposit.add(1); + const updatedRecipient = randomAddress(); + const updatedMessage = "0x1234"; + const depositId = 100; + const spokePoolChainId = await spokePool.chainId(); + + const signature = await getUpdatedV3DepositSignature( + depositor, + depositId, + spokePoolChainId, + updatedOutputAmount, + updatedRecipient, + updatedMessage, + true + ); + + await spokePool[verifyUpdateV3DepositMessageAddress]( + depositor.address, + depositId, + spokePoolChainId, + updatedOutputAmount, + updatedRecipient, + updatedMessage, + signature + ); + + await expect( + spokePool + .connect(depositor) + [speedUpV3DepositAddress]( + depositor.address, + depositId, + updatedOutputAmount, + updatedRecipient, + updatedMessage, + signature + ) + ) + .to.emit(spokePool, "RequestedSpeedUpV3Deposit") + .withArgs( updatedOutputAmount, - updatedRecipient, + depositId, + hexZeroPadAddressLowercase(depositor.address), + hexZeroPadAddressLowercase(updatedRecipient), updatedMessage, - invalidSignatureForChain - ) - ).to.be.revertedWith("InvalidDepositorSignature"); + signature + ); }); }); }); diff --git a/test/evm/hardhat/SpokePool.ExecuteRootBundle.ts b/test/evm/hardhat/SpokePool.ExecuteRootBundle.ts index 1a2ae5c18..76af268d8 100644 --- a/test/evm/hardhat/SpokePool.ExecuteRootBundle.ts +++ b/test/evm/hardhat/SpokePool.ExecuteRootBundle.ts @@ -1,4 +1,14 @@ -import { SignerWithAddress, seedContract, toBN, expect, Contract, ethers, BigNumber } from "../../../utils/utils"; +import { + SignerWithAddress, + seedContract, + toBN, + expect, + Contract, + ethers, + BigNumber, + hexZeroPadAddress, + hexZeroPadAddressLowercase, +} from "../../../utils/utils"; import * as consts from "./constants"; import { spokePoolFixture } from "./fixtures/SpokePool.Fixture"; import { buildRelayerRefundTree, buildRelayerRefundLeaves } from "./MerkleLib.utils"; @@ -12,8 +22,8 @@ async function constructSimpleTree(l2Token: Contract, destinationChainId: number const leaves = buildRelayerRefundLeaves( [destinationChainId, destinationChainId], // Destination chain ID. [consts.amountToReturn, toBN(0)], // amountToReturn. - [l2Token.address, l2Token.address], // l2Token. - [[relayer.address, rando.address], []], // refundAddresses. + [hexZeroPadAddress(l2Token.address), hexZeroPadAddress(l2Token.address)], // l2Token. + [[hexZeroPadAddress(relayer.address), hexZeroPadAddress(rando.address)], []], // refundAddresses. [[consts.amountToRelay, consts.amountToRelay], []] // refundAmounts. ); const leavesRefundAmount = leaves @@ -53,14 +63,17 @@ describe("SpokePool Root Bundle Execution", function () { // Check events. let relayTokensEvents = await spokePool.queryFilter(spokePool.filters.ExecutedRelayerRefundRoot()); - expect(relayTokensEvents[0].args?.l2TokenAddress).to.equal(destErc20.address); + expect(relayTokensEvents[0].args?.l2TokenAddress).to.equal(hexZeroPadAddressLowercase(destErc20.address)); expect(relayTokensEvents[0].args?.leafId).to.equal(0); expect(relayTokensEvents[0].args?.chainId).to.equal(destinationChainId); expect(relayTokensEvents[0].args?.amountToReturn).to.equal(consts.amountToReturn); expect((relayTokensEvents[0].args?.refundAmounts as BigNumber[]).map((v) => v.toString())).to.deep.equal( [consts.amountToRelay, consts.amountToRelay].map((v) => v.toString()) ); - expect(relayTokensEvents[0].args?.refundAddresses).to.deep.equal([relayer.address, rando.address]); + expect(relayTokensEvents[0].args?.refundAddresses).to.deep.equal([ + hexZeroPadAddressLowercase(relayer.address), + hexZeroPadAddressLowercase(rando.address), + ]); // Should emit TokensBridged event if amountToReturn is positive. let tokensBridgedEvents = await spokePool.queryFilter(spokePool.filters.TokensBridged()); @@ -128,8 +141,8 @@ describe("SpokePool Root Bundle Execution", function () { toBN(1), [consts.amountToRelay, consts.amountToRelay, toBN(0)], 0, - destErc20.address, - [relayer.address, rando.address] + hexZeroPadAddress(destErc20.address), + [hexZeroPadAddress(relayer.address), hexZeroPadAddress(rando.address)] ) ).to.be.revertedWith("InvalidMerkleLeaf"); }); @@ -138,7 +151,7 @@ describe("SpokePool Root Bundle Execution", function () { await expect( spokePool .connect(dataWorker) - .distributeRelayerRefunds(destinationChainId, toBN(1), [], 0, destErc20.address, []) + .distributeRelayerRefunds(destinationChainId, toBN(1), [], 0, hexZeroPadAddress(destErc20.address), []) ) .to.emit(spokePool, "BridgedToHubPool") .withArgs(toBN(1), destErc20.address); @@ -147,10 +160,10 @@ describe("SpokePool Root Bundle Execution", function () { await expect( spokePool .connect(dataWorker) - .distributeRelayerRefunds(destinationChainId, toBN(1), [], 0, destErc20.address, []) + .distributeRelayerRefunds(destinationChainId, toBN(1), [], 0, hexZeroPadAddress(destErc20.address), []) ) .to.emit(spokePool, "TokensBridged") - .withArgs(toBN(1), destinationChainId, 0, destErc20.address, dataWorker.address); + .withArgs(toBN(1), destinationChainId, 0, hexZeroPadAddressLowercase(destErc20.address), dataWorker.address); }); }); describe("amountToReturn = 0", function () { @@ -158,14 +171,14 @@ describe("SpokePool Root Bundle Execution", function () { await expect( spokePool .connect(dataWorker) - .distributeRelayerRefunds(destinationChainId, toBN(0), [], 0, destErc20.address, []) + .distributeRelayerRefunds(destinationChainId, toBN(0), [], 0, hexZeroPadAddress(destErc20.address), []) ).to.not.emit(spokePool, "BridgedToHubPool"); }); it("does not emit TokensBridged", async function () { await expect( spokePool .connect(dataWorker) - .distributeRelayerRefunds(destinationChainId, toBN(0), [], 0, destErc20.address, []) + .distributeRelayerRefunds(destinationChainId, toBN(0), [], 0, hexZeroPadAddress(destErc20.address), []) ).to.not.emit(spokePool, "TokensBridged"); }); }); @@ -179,8 +192,8 @@ describe("SpokePool Root Bundle Execution", function () { toBN(1), [consts.amountToRelay, consts.amountToRelay, toBN(0)], 0, - destErc20.address, - [relayer.address, rando.address, rando.address] + hexZeroPadAddress(destErc20.address), + [hexZeroPadAddress(relayer.address), hexZeroPadAddress(rando.address), hexZeroPadAddress(rando.address)] ) ).to.changeTokenBalances( destErc20, @@ -199,8 +212,8 @@ describe("SpokePool Root Bundle Execution", function () { toBN(1), [consts.amountToRelay, consts.amountToRelay, toBN(0)], 0, - destErc20.address, - [relayer.address, rando.address, rando.address] + hexZeroPadAddress(destErc20.address), + [hexZeroPadAddress(relayer.address), hexZeroPadAddress(rando.address), hexZeroPadAddress(rando.address)] ) ) .to.emit(spokePool, "BridgedToHubPool") diff --git a/test/evm/hardhat/SpokePool.Relay.ts b/test/evm/hardhat/SpokePool.Relay.ts index ca35500d9..813424496 100644 --- a/test/evm/hardhat/SpokePool.Relay.ts +++ b/test/evm/hardhat/SpokePool.Relay.ts @@ -8,6 +8,9 @@ import { randomAddress, createRandomBytes32, BigNumber, + hexZeroPadAddress, + hexZeroPadAddressLowercase, + bytes32ToAddress, } from "../../../utils/utils"; import { spokePoolFixture, @@ -59,11 +62,11 @@ describe("SpokePool Relayer Logic", async function () { beforeEach(async function () { const fillDeadline = (await spokePool.getCurrentTime()).toNumber() + 1000; relayData = { - depositor: depositor.address, - recipient: recipient.address, - exclusiveRelayer: relayer.address, - inputToken: erc20.address, - outputToken: destErc20.address, + depositor: hexZeroPadAddress(depositor.address), + recipient: hexZeroPadAddress(recipient.address), + exclusiveRelayer: hexZeroPadAddress(relayer.address), + inputToken: hexZeroPadAddress(erc20.address), + outputToken: hexZeroPadAddress(destErc20.address), inputAmount: consts.amountToDeposit, outputAmount: consts.amountToDeposit, originChainId: consts.originChainId, @@ -87,7 +90,7 @@ describe("SpokePool Relayer Logic", async function () { await expect( spokePool.connect(relayer).fillRelayV3Internal( relayExecution, - relayer.address, + hexZeroPadAddress(relayer.address), false // isSlowFill ) ).to.be.revertedWith("ExpiredFillDeadline"); @@ -98,7 +101,7 @@ describe("SpokePool Relayer Logic", async function () { await expect( spokePool.connect(relayer).fillRelayV3Internal( relayExecution, - relayer.address, + hexZeroPadAddress(relayer.address), false // isSlowFill ) ).to.be.revertedWith("RelayFilled"); @@ -109,14 +112,14 @@ describe("SpokePool Relayer Logic", async function () { await expect( spokePool.connect(relayer).fillRelayV3Internal( relayExecution, - relayer.address, + hexZeroPadAddress(relayer.address), false // isSlowFill ) ) .to.emit(spokePool, "FilledV3Relay") .withArgs( - relayData.inputToken, - relayData.outputToken, + hexZeroPadAddressLowercase(relayData.inputToken), + hexZeroPadAddressLowercase(relayData.outputToken), relayData.inputAmount, relayData.outputAmount, relayExecution.repaymentChainId, @@ -124,13 +127,13 @@ describe("SpokePool Relayer Logic", async function () { relayData.depositId, relayData.fillDeadline, relayData.exclusivityDeadline, - relayData.exclusiveRelayer, - relayer.address, - relayData.depositor, - relayData.recipient, + hexZeroPadAddressLowercase(relayData.exclusiveRelayer), + hexZeroPadAddressLowercase(relayer.address), + hexZeroPadAddressLowercase(relayData.depositor), + hexZeroPadAddressLowercase(relayData.recipient), relayData.message, [ - relayExecution.updatedRecipient, + hexZeroPadAddressLowercase(relayData.recipient), relayExecution.updatedMessage, relayExecution.updatedOutputAmount, // Testing that this FillType is not "FastFill" @@ -145,14 +148,14 @@ describe("SpokePool Relayer Logic", async function () { await expect( spokePool.connect(relayer).fillRelayV3Internal( relayExecution, - relayer.address, + hexZeroPadAddress(relayer.address), true // isSlowFill ) ) .to.emit(spokePool, "FilledV3Relay") .withArgs( - relayData.inputToken, - relayData.outputToken, + hexZeroPadAddressLowercase(relayData.inputToken), + hexZeroPadAddressLowercase(relayData.outputToken), relayData.inputAmount, relayData.outputAmount, relayExecution.repaymentChainId, @@ -160,13 +163,13 @@ describe("SpokePool Relayer Logic", async function () { relayData.depositId, relayData.fillDeadline, relayData.exclusivityDeadline, - relayData.exclusiveRelayer, - relayer.address, - relayData.depositor, - relayData.recipient, + hexZeroPadAddressLowercase(relayData.exclusiveRelayer), + hexZeroPadAddressLowercase(relayer.address), + hexZeroPadAddressLowercase(relayData.depositor), + hexZeroPadAddressLowercase(relayData.recipient), relayData.message, [ - relayExecution.updatedRecipient, + hexZeroPadAddressLowercase(relayData.recipient), relayExecution.updatedMessage, relayExecution.updatedOutputAmount, // Testing that this FillType is "SlowFill" @@ -180,14 +183,14 @@ describe("SpokePool Relayer Logic", async function () { await expect( spokePool.connect(relayer).fillRelayV3Internal( relayExecution, - relayer.address, + hexZeroPadAddressLowercase(relayer.address), false // isSlowFill ) ) .to.emit(spokePool, "FilledV3Relay") .withArgs( - relayData.inputToken, - relayData.outputToken, + hexZeroPadAddressLowercase(relayData.inputToken), + hexZeroPadAddressLowercase(relayData.outputToken), relayData.inputAmount, relayData.outputAmount, relayExecution.repaymentChainId, @@ -195,13 +198,13 @@ describe("SpokePool Relayer Logic", async function () { relayData.depositId, relayData.fillDeadline, relayData.exclusivityDeadline, - relayData.exclusiveRelayer, - relayer.address, - relayData.depositor, - relayData.recipient, + hexZeroPadAddressLowercase(relayData.exclusiveRelayer), + hexZeroPadAddressLowercase(relayer.address), + hexZeroPadAddressLowercase(relayData.depositor), + hexZeroPadAddressLowercase(relayData.recipient), relayData.message, [ - relayExecution.updatedRecipient, + hexZeroPadAddressLowercase(relayData.recipient), relayExecution.updatedMessage, relayExecution.updatedOutputAmount, FillType.FastFill, @@ -213,13 +216,13 @@ describe("SpokePool Relayer Logic", async function () { const _relayData = { ...relayData, // Set recipient == relayer - recipient: relayer.address, + recipient: hexZeroPadAddress(relayer.address), }; const relayExecution = await getRelayExecutionParams(_relayData, consts.destinationChainId); await expect( spokePool.connect(relayer).fillRelayV3Internal( relayExecution, - relayer.address, + hexZeroPadAddress(relayer.address), false // isSlowFill ) ).to.not.emit(destErc20, "Transfer"); @@ -231,15 +234,15 @@ describe("SpokePool Relayer Logic", async function () { // Overwrite amount to send to be double the original amount updatedOutputAmount: consts.amountToDeposit.mul(2), // Overwrite recipient to depositor which is not the same as the original recipient - updatedRecipient: depositor.address, + updatedRecipient: hexZeroPadAddress(depositor.address), }; - expect(_relayExecution.updatedRecipient).to.not.equal(relayExecution.updatedRecipient); + expect(_relayExecution.updatedRecipient).to.not.equal(hexZeroPadAddress(relayExecution.updatedRecipient)); expect(_relayExecution.updatedOutputAmount).to.not.equal(relayExecution.updatedOutputAmount); await destErc20.connect(relayer).approve(spokePool.address, _relayExecution.updatedOutputAmount); await expect(() => spokePool.connect(relayer).fillRelayV3Internal( _relayExecution, - relayer.address, + hexZeroPadAddress(relayer.address), false // isSlowFill ) ).to.changeTokenBalance(destErc20, depositor, consts.amountToDeposit.mul(2)); @@ -247,13 +250,13 @@ describe("SpokePool Relayer Logic", async function () { it("unwraps native token if sending to EOA", async function () { const _relayData = { ...relayData, - outputToken: weth.address, + outputToken: hexZeroPadAddress(weth.address), }; const relayExecution = await getRelayExecutionParams(_relayData, consts.destinationChainId); await expect(() => spokePool.connect(relayer).fillRelayV3Internal( relayExecution, - relayer.address, + hexZeroPadAddress(relayer.address), false // isSlowFill ) ).to.changeEtherBalance(recipient, relayExecution.updatedOutputAmount); @@ -261,7 +264,7 @@ describe("SpokePool Relayer Logic", async function () { it("slow fills send native token out of spoke pool balance", async function () { const _relayData = { ...relayData, - outputToken: weth.address, + outputToken: hexZeroPadAddress(weth.address), }; const relayExecution = await getRelayExecutionParams(_relayData, consts.destinationChainId); await weth.connect(relayer).transfer(spokePool.address, relayExecution.updatedOutputAmount); @@ -269,7 +272,7 @@ describe("SpokePool Relayer Logic", async function () { await expect(() => spokePool.connect(relayer).fillRelayV3Internal( relayExecution, - relayer.address, + hexZeroPadAddress(relayer.address), true // isSlowFill ) ).to.changeEtherBalance(recipient, relayExecution.updatedOutputAmount); @@ -282,7 +285,7 @@ describe("SpokePool Relayer Logic", async function () { await expect(() => spokePool.connect(relayer).fillRelayV3Internal( relayExecution, - relayer.address, + hexZeroPadAddress(relayer.address), true // isSlowFill ) ).to.changeTokenBalance(destErc20, spokePool, relayExecution.updatedOutputAmount.mul(-1)); @@ -292,7 +295,7 @@ describe("SpokePool Relayer Logic", async function () { const acrossMessageHandler = await createFake("AcrossMessageHandlerMock"); const _relayData = { ...relayData, - recipient: acrossMessageHandler.address, + recipient: hexZeroPadAddress(acrossMessageHandler.address), message: "0x1234", }; const relayExecution = await getRelayExecutionParams(_relayData, consts.destinationChainId); @@ -300,11 +303,12 @@ describe("SpokePool Relayer Logic", async function () { // Handler is called with expected params. await spokePool.connect(relayer).fillRelayV3Internal( relayExecution, - relayer.address, + hexZeroPadAddress(relayer.address), false // isSlowFill ); + const test = bytes32ToAddress(_relayData.outputToken); expect(acrossMessageHandler.handleV3AcrossMessage).to.have.been.calledOnceWith( - _relayData.outputToken, + bytes32ToAddress(_relayData.outputToken), relayExecution.updatedOutputAmount, relayer.address, // Custom relayer _relayData.message @@ -331,7 +335,7 @@ describe("SpokePool Relayer Logic", async function () { const _relayData = { ...relayData, // Overwrite exclusive relayer and exclusivity deadline - exclusiveRelayer: recipient.address, + exclusiveRelayer: hexZeroPadAddress(recipient.address), exclusivityDeadline: relayData.fillDeadline, }; await expect(spokePool.connect(relayer).fillV3Relay(_relayData, consts.repaymentChainId)).to.be.revertedWith( @@ -373,8 +377,8 @@ describe("SpokePool Relayer Logic", async function () { await expect(spokePool.connect(relayer).fillV3Relay(relayData, consts.repaymentChainId)) .to.emit(spokePool, "FilledV3Relay") .withArgs( - relayData.inputToken, - relayData.outputToken, + hexZeroPadAddressLowercase(relayData.inputToken), + hexZeroPadAddressLowercase(relayData.outputToken), relayData.inputAmount, relayData.outputAmount, consts.repaymentChainId, // Should be passed-in repayment chain ID @@ -382,13 +386,13 @@ describe("SpokePool Relayer Logic", async function () { relayData.depositId, relayData.fillDeadline, relayData.exclusivityDeadline, - relayData.exclusiveRelayer, - relayer.address, // Should be equal to msg.sender of fillRelayV3 - relayData.depositor, - relayData.recipient, + hexZeroPadAddressLowercase(relayData.exclusiveRelayer), + hexZeroPadAddressLowercase(relayer.address), // Should be equal to msg.sender of fillRelayV3 + hexZeroPadAddressLowercase(relayData.depositor), + hexZeroPadAddressLowercase(relayData.recipient), relayData.message, [ - relayData.recipient, // updatedRecipient should be equal to recipient + hexZeroPadAddressLowercase(relayData.recipient), // updatedRecipient should be equal to recipient relayData.message, // updatedMessage should be equal to message relayData.outputAmount, // updatedOutputAmount should be equal to outputAmount // Should be FastFill @@ -409,7 +413,7 @@ describe("SpokePool Relayer Logic", async function () { relayData.depositId, relayData.originChainId, updatedOutputAmount, - updatedRecipient, + hexZeroPadAddress(updatedRecipient), updatedMessage ); }); @@ -417,7 +421,7 @@ describe("SpokePool Relayer Logic", async function () { const _relayData = { ...relayData, // Overwrite exclusive relayer and exclusivity deadline - exclusiveRelayer: recipient.address, + exclusiveRelayer: hexZeroPadAddress(recipient.address), exclusivityDeadline: relayData.fillDeadline, }; await expect( @@ -427,7 +431,7 @@ describe("SpokePool Relayer Logic", async function () { _relayData, consts.repaymentChainId, updatedOutputAmount, - updatedRecipient, + hexZeroPadAddress(updatedRecipient), updatedMessage, signature ) @@ -442,7 +446,7 @@ describe("SpokePool Relayer Logic", async function () { }, consts.repaymentChainId, updatedOutputAmount, - updatedRecipient, + hexZeroPadAddress(updatedRecipient), updatedMessage, signature ) @@ -461,15 +465,15 @@ describe("SpokePool Relayer Logic", async function () { relayData, consts.repaymentChainId, updatedOutputAmount, - updatedRecipient, + hexZeroPadAddress(updatedRecipient), updatedMessage, signature ) ) .to.emit(spokePool, "FilledV3Relay") .withArgs( - relayData.inputToken, - relayData.outputToken, + hexZeroPadAddressLowercase(relayData.inputToken), + hexZeroPadAddressLowercase(relayData.outputToken), relayData.inputAmount, relayData.outputAmount, consts.repaymentChainId, // Should be passed-in repayment chain ID @@ -477,14 +481,14 @@ describe("SpokePool Relayer Logic", async function () { relayData.depositId, relayData.fillDeadline, relayData.exclusivityDeadline, - relayData.exclusiveRelayer, - relayer.address, // Should be equal to msg.sender - relayData.depositor, - relayData.recipient, + hexZeroPadAddressLowercase(relayData.exclusiveRelayer), + hexZeroPadAddressLowercase(relayer.address), // Should be equal to msg.sender + hexZeroPadAddressLowercase(relayData.depositor), + hexZeroPadAddressLowercase(relayData.recipient), relayData.message, [ // Should use passed-in updated params: - updatedRecipient, + hexZeroPadAddressLowercase(updatedRecipient), updatedMessage, updatedOutputAmount, // Should be FastFill @@ -502,10 +506,10 @@ describe("SpokePool Relayer Logic", async function () { spokePool .connect(relayer) .fillV3RelayWithUpdatedDeposit( - { ...relayData, depositor: relayer.address }, + { ...relayData, depositor: hexZeroPadAddress(relayer.address) }, consts.repaymentChainId, updatedOutputAmount, - updatedRecipient, + hexZeroPadAddress(updatedRecipient), updatedMessage, signature ) @@ -517,7 +521,7 @@ describe("SpokePool Relayer Logic", async function () { relayData.depositId + 1, relayData.originChainId, updatedOutputAmount, - updatedRecipient, + hexZeroPadAddress(updatedRecipient), updatedMessage ); await expect( @@ -527,7 +531,7 @@ describe("SpokePool Relayer Logic", async function () { relayData, consts.repaymentChainId, updatedOutputAmount, - updatedRecipient, + hexZeroPadAddress(updatedRecipient), updatedMessage, otherSignature ) @@ -541,7 +545,7 @@ describe("SpokePool Relayer Logic", async function () { { ...relayData, originChainId: relayData.originChainId + 1 }, consts.repaymentChainId, updatedOutputAmount, - updatedRecipient, + hexZeroPadAddress(updatedRecipient), updatedMessage, signature ) @@ -555,7 +559,7 @@ describe("SpokePool Relayer Logic", async function () { { ...relayData, depositId: relayData.depositId + 1 }, consts.repaymentChainId, updatedOutputAmount, - updatedRecipient, + hexZeroPadAddress(updatedRecipient), updatedMessage, signature ) @@ -569,7 +573,7 @@ describe("SpokePool Relayer Logic", async function () { relayData, consts.repaymentChainId, updatedOutputAmount.sub(1), - updatedRecipient, + hexZeroPadAddress(updatedRecipient), updatedMessage, signature ) @@ -583,7 +587,7 @@ describe("SpokePool Relayer Logic", async function () { relayData, consts.repaymentChainId, updatedOutputAmount, - randomAddress(), + hexZeroPadAddress(randomAddress()), updatedMessage, signature ) @@ -597,7 +601,7 @@ describe("SpokePool Relayer Logic", async function () { relayData, consts.repaymentChainId, updatedOutputAmount, - updatedRecipient, + hexZeroPadAddress(updatedRecipient), updatedMessage, createRandomBytes32() ) @@ -611,17 +615,17 @@ describe("SpokePool Relayer Logic", async function () { relayData.depositId, relayData.originChainId, updatedOutputAmount, - updatedRecipient, + hexZeroPadAddress(updatedRecipient), updatedMessage ); await expect( spokePool .connect(relayer) .fillV3RelayWithUpdatedDeposit( - { ...relayData, depositor: erc1271.address }, + { ...relayData, depositor: hexZeroPadAddress(erc1271.address) }, consts.repaymentChainId, updatedOutputAmount, - updatedRecipient, + hexZeroPadAddress(updatedRecipient), updatedMessage, incorrectSignature ) @@ -630,10 +634,10 @@ describe("SpokePool Relayer Logic", async function () { spokePool .connect(relayer) .fillV3RelayWithUpdatedDeposit( - { ...relayData, depositor: erc1271.address }, + { ...relayData, depositor: hexZeroPadAddress(erc1271.address) }, consts.repaymentChainId, updatedOutputAmount, - updatedRecipient, + hexZeroPadAddress(updatedRecipient), updatedMessage, signature ) @@ -648,7 +652,7 @@ describe("SpokePool Relayer Logic", async function () { relayData, consts.repaymentChainId, updatedOutputAmount, - updatedRecipient, + hexZeroPadAddress(updatedRecipient), updatedMessage, signature ) @@ -661,7 +665,7 @@ describe("SpokePool Relayer Logic", async function () { relayData, consts.repaymentChainId, updatedOutputAmount, - updatedRecipient, + hexZeroPadAddress(updatedRecipient), updatedMessage, signature ); diff --git a/test/evm/hardhat/SpokePool.SlowRelay.ts b/test/evm/hardhat/SpokePool.SlowRelay.ts index a8da51d20..522f05bb5 100644 --- a/test/evm/hardhat/SpokePool.SlowRelay.ts +++ b/test/evm/hardhat/SpokePool.SlowRelay.ts @@ -1,4 +1,15 @@ -import { expect, Contract, ethers, SignerWithAddress, toBN, seedContract, seedWallet } from "../../../utils/utils"; +import { + expect, + Contract, + ethers, + SignerWithAddress, + toBN, + seedContract, + seedWallet, + hexZeroPadAddress, + hexZeroPadAddressLowercase, + bytes32ToAddress, +} from "../../../utils/utils"; import { spokePoolFixture, V3RelayData, getV3RelayHash, V3SlowFill, FillType } from "./fixtures/SpokePool.Fixture"; import { buildV3SlowRelayTree } from "./MerkleLib.utils"; import * as consts from "./constants"; @@ -31,11 +42,11 @@ describe("SpokePool Slow Relay Logic", async function () { beforeEach(async function () { const fillDeadline = (await spokePool.getCurrentTime()).toNumber() + 1000; relayData = { - depositor: depositor.address, - recipient: recipient.address, - exclusiveRelayer: relayer.address, - inputToken: erc20.address, - outputToken: destErc20.address, + depositor: hexZeroPadAddress(depositor.address), + recipient: hexZeroPadAddress(recipient.address), + exclusiveRelayer: hexZeroPadAddress(relayer.address), + inputToken: hexZeroPadAddress(erc20.address), + outputToken: hexZeroPadAddress(destErc20.address), inputAmount: consts.amountToDeposit, outputAmount: fullRelayAmountPostFees, originChainId: consts.originChainId, @@ -101,11 +112,11 @@ describe("SpokePool Slow Relay Logic", async function () { beforeEach(async function () { const fillDeadline = (await spokePool.getCurrentTime()).toNumber() + 1000; relayData = { - depositor: depositor.address, - recipient: recipient.address, - exclusiveRelayer: relayer.address, - inputToken: erc20.address, - outputToken: destErc20.address, + depositor: hexZeroPadAddress(depositor.address), + recipient: hexZeroPadAddress(recipient.address), + exclusiveRelayer: hexZeroPadAddress(relayer.address), + inputToken: hexZeroPadAddress(erc20.address), + outputToken: hexZeroPadAddress(destErc20.address), inputAmount: consts.amountToDeposit, outputAmount: fullRelayAmountPostFees, originChainId: consts.originChainId, @@ -206,7 +217,7 @@ describe("SpokePool Slow Relay Logic", async function () { ) ) .to.emit(spokePool, "PreLeafExecuteHook") - .withArgs(slowRelayLeaf.relayData.outputToken); + .withArgs(slowRelayLeaf.relayData.outputToken.toLowerCase()); }); it("cannot execute leaves with chain IDs not matching spoke pool's chain ID", async function () { // In this test, the merkle proof is valid for the tree relayed to the spoke pool, but the merkle leaf @@ -275,8 +286,8 @@ describe("SpokePool Slow Relay Logic", async function () { ) .to.emit(spokePool, "FilledV3Relay") .withArgs( - relayData.inputToken, - relayData.outputToken, + hexZeroPadAddressLowercase(relayData.inputToken), + hexZeroPadAddressLowercase(relayData.outputToken), relayData.inputAmount, relayData.outputAmount, // Sets repaymentChainId to 0: @@ -285,15 +296,15 @@ describe("SpokePool Slow Relay Logic", async function () { relayData.depositId, relayData.fillDeadline, relayData.exclusivityDeadline, - relayData.exclusiveRelayer, + hexZeroPadAddressLowercase(relayData.exclusiveRelayer), // Sets relayer address to 0x0 - consts.zeroAddress, - relayData.depositor, - relayData.recipient, + hexZeroPadAddressLowercase(consts.zeroAddress), + hexZeroPadAddressLowercase(relayData.depositor), + hexZeroPadAddressLowercase(relayData.recipient), relayData.message, [ // Uses relayData.recipient - relayData.recipient, + hexZeroPadAddressLowercase(relayData.recipient), // Uses relayData.message relayData.message, // Uses slow fill leaf's updatedOutputAmount diff --git a/test/evm/hardhat/chain-specific-spokepools/Arbitrum_SpokePool.ts b/test/evm/hardhat/chain-specific-spokepools/Arbitrum_SpokePool.ts index 5476a2b43..1377aaed5 100644 --- a/test/evm/hardhat/chain-specific-spokepools/Arbitrum_SpokePool.ts +++ b/test/evm/hardhat/chain-specific-spokepools/Arbitrum_SpokePool.ts @@ -11,6 +11,7 @@ import { seedContract, avmL1ToL2Alias, createFakeFromABI, + hexZeroPadAddress, } from "../../../../utils/utils"; import { hre } from "../../../../utils/utils.hre"; import { hubPoolFixture } from "../fixtures/HubPool.Fixture"; @@ -79,7 +80,7 @@ describe("Arbitrum Spoke Pool", function () { it("Only cross domain owner can enable a route", async function () { await expect(arbitrumSpokePool.setEnableRoute(l2Dai, 1, true)).to.be.reverted; await arbitrumSpokePool.connect(crossDomainAlias).setEnableRoute(l2Dai, 1, true); - expect(await arbitrumSpokePool.enabledDepositRoutes(l2Dai, 1)).to.equal(true); + expect(await arbitrumSpokePool.enabledDepositRoutes(hexZeroPadAddress(l2Dai), 1)).to.equal(true); }); it("Only cross domain owner can whitelist a token pair", async function () { diff --git a/test/evm/hardhat/chain-specific-spokepools/Ethereum_SpokePool.ts b/test/evm/hardhat/chain-specific-spokepools/Ethereum_SpokePool.ts index 3080995ff..04d800d89 100644 --- a/test/evm/hardhat/chain-specific-spokepools/Ethereum_SpokePool.ts +++ b/test/evm/hardhat/chain-specific-spokepools/Ethereum_SpokePool.ts @@ -1,5 +1,13 @@ import { mockTreeRoot, amountToReturn, amountHeldByPool } from "../constants"; -import { ethers, expect, Contract, SignerWithAddress, getContractFactory, seedContract } from "../../../../utils/utils"; +import { + ethers, + expect, + Contract, + SignerWithAddress, + getContractFactory, + seedContract, + hexZeroPadAddress, +} from "../../../../utils/utils"; import { hre } from "../../../../utils/utils.hre"; import { hubPoolFixture } from "../fixtures/HubPool.Fixture"; import { constructSingleRelayerRefundTree } from "../MerkleLib.utils"; @@ -47,7 +55,7 @@ describe("Ethereum Spoke Pool", function () { it("Only owner can enable a route", async function () { await expect(spokePool.connect(rando).setEnableRoute(dai.address, 1, true)).to.be.reverted; await spokePool.connect(owner).setEnableRoute(dai.address, 1, true); - expect(await spokePool.enabledDepositRoutes(dai.address, 1)).to.equal(true); + expect(await spokePool.enabledDepositRoutes(hexZeroPadAddress(dai.address), 1)).to.equal(true); }); it("Only owner can set the hub pool address", async function () { diff --git a/test/evm/hardhat/chain-specific-spokepools/Optimism_SpokePool.ts b/test/evm/hardhat/chain-specific-spokepools/Optimism_SpokePool.ts index 628b1c6bc..f22525538 100644 --- a/test/evm/hardhat/chain-specific-spokepools/Optimism_SpokePool.ts +++ b/test/evm/hardhat/chain-specific-spokepools/Optimism_SpokePool.ts @@ -10,6 +10,7 @@ import { getContractFactory, seedContract, createFakeFromABI, + hexZeroPadAddress, } from "../../../../utils/utils"; import { CCTPTokenMessengerInterface, CCTPTokenMinterInterface } from "../../../../utils/abis"; import { hre } from "../../../../utils/utils.hre"; @@ -93,7 +94,7 @@ describe("Optimism Spoke Pool", function () { await expect(optimismSpokePool.setEnableRoute(l2Dai, 1, true)).to.be.reverted; crossDomainMessenger.xDomainMessageSender.returns(owner.address); await optimismSpokePool.connect(crossDomainMessenger.wallet).setEnableRoute(l2Dai, 1, true); - expect(await optimismSpokePool.enabledDepositRoutes(l2Dai, 1)).to.equal(true); + expect(await optimismSpokePool.enabledDepositRoutes(hexZeroPadAddress(l2Dai), 1)).to.equal(true); }); it("Only cross domain owner can set the cross domain admin", async function () { diff --git a/test/evm/hardhat/chain-specific-spokepools/Polygon_SpokePool.ts b/test/evm/hardhat/chain-specific-spokepools/Polygon_SpokePool.ts index 4a847990e..156cc34d0 100644 --- a/test/evm/hardhat/chain-specific-spokepools/Polygon_SpokePool.ts +++ b/test/evm/hardhat/chain-specific-spokepools/Polygon_SpokePool.ts @@ -20,6 +20,7 @@ import { seedWallet, FakeContract, createFakeFromABI, + hexZeroPadAddress, } from "../../../../utils/utils"; import { hre } from "../../../../utils/utils.hre"; import { hubPoolFixture } from "../fixtures/HubPool.Fixture"; @@ -155,7 +156,7 @@ describe("Polygon Spoke Pool", function () { .reverted; await polygonSpokePool.connect(fxChild).processMessageFromRoot(0, owner.address, setEnableRouteData); - expect(await polygonSpokePool.enabledDepositRoutes(l2Dai, 1)).to.equal(true); + expect(await polygonSpokePool.enabledDepositRoutes(hexZeroPadAddress(l2Dai), 1)).to.equal(true); }); it("Only correct caller can initialize a relayer refund", async function () { @@ -260,7 +261,7 @@ describe("Polygon Spoke Pool", function () { const leaves = buildRelayerRefundLeaves( [l2ChainId, l2ChainId], // Destination chain ID. [amountToReturn, ethers.constants.Zero], // amountToReturn. - [dai.address, dai.address], // l2Token. + [hexZeroPadAddress(dai.address), hexZeroPadAddress(dai.address)], // l2Token. [[], []], // refundAddresses. [[], []] // refundAmounts. ); @@ -288,7 +289,7 @@ describe("Polygon Spoke Pool", function () { const leaves = buildRelayerRefundLeaves( [l2ChainId, l2ChainId], // Destination chain ID. [ethers.constants.Zero, ethers.constants.Zero], // amountToReturn. - [dai.address, dai.address], // l2Token. + [hexZeroPadAddress(dai.address), hexZeroPadAddress(dai.address)], // l2Token. [[], []], // refundAddresses. [[], []] // refundAmounts. ); @@ -320,11 +321,11 @@ describe("Polygon Spoke Pool", function () { ]; const currentTime = (await polygonSpokePool.getCurrentTime()).toNumber(); const relayData: V3RelayData = { - depositor: owner.address, - recipient: acrossMessageHandler.address, - exclusiveRelayer: zeroAddress, - inputToken: dai.address, - outputToken: dai.address, + depositor: hexZeroPadAddress(owner.address), + recipient: hexZeroPadAddress(acrossMessageHandler.address), + exclusiveRelayer: hexZeroPadAddress(zeroAddress), + inputToken: hexZeroPadAddress(dai.address), + outputToken: hexZeroPadAddress(dai.address), inputAmount: toWei("1"), outputAmount: toWei("1"), originChainId: originChainId, diff --git a/test/evm/hardhat/fixtures/SpokePool.Fixture.ts b/test/evm/hardhat/fixtures/SpokePool.Fixture.ts index ac5d1d78d..8ff0b51f6 100644 --- a/test/evm/hardhat/fixtures/SpokePool.Fixture.ts +++ b/test/evm/hardhat/fixtures/SpokePool.Fixture.ts @@ -7,6 +7,7 @@ import { ethers, BigNumber, defaultAbiCoder, + hexZeroPadAddress, } from "../../../../utils/utils"; import * as consts from "../constants"; @@ -281,7 +282,7 @@ export function getV3RelayHash(relayData: V3RelayData, destinationChainId: numbe return ethers.utils.keccak256( defaultAbiCoder.encode( [ - "tuple(address depositor, address recipient, address exclusiveRelayer, address inputToken, address outputToken, uint256 inputAmount, uint256 outputAmount, uint256 originChainId, uint32 depositId, uint32 fillDeadline, uint32 exclusivityDeadline, bytes message)", + "tuple(bytes32 depositor, bytes32 recipient, bytes32 exclusiveRelayer, bytes32 inputToken, bytes32 outputToken, uint256 inputAmount, uint256 outputAmount, uint256 originChainId, uint32 depositId, uint32 fillDeadline, uint32 exclusivityDeadline, bytes message)", "uint256 destinationChainId", ], [relayData, destinationChainId] @@ -441,7 +442,8 @@ export async function getUpdatedV3DepositSignature( originChainId: number, updatedOutputAmount: BigNumber, updatedRecipient: string, - updatedMessage: string + updatedMessage: string, + isAddressOverload: boolean = false ): Promise { const typedData = { types: { @@ -449,7 +451,7 @@ export async function getUpdatedV3DepositSignature( { name: "depositId", type: "uint32" }, { name: "originChainId", type: "uint256" }, { name: "updatedOutputAmount", type: "uint256" }, - { name: "updatedRecipient", type: "address" }, + { name: "updatedRecipient", type: isAddressOverload ? "address" : "bytes32" }, { name: "updatedMessage", type: "bytes" }, ], }, diff --git a/utils/utils.ts b/utils/utils.ts index f526a29c4..61bde6750 100644 --- a/utils/utils.ts +++ b/utils/utils.ts @@ -101,6 +101,19 @@ export const hexToUtf8 = (input: string) => ethers.utils.toUtf8String(input); export const createRandomBytes32 = () => ethers.utils.hexlify(ethers.utils.randomBytes(32)); +export const hexZeroPad = (input: string, length: number) => ethers.utils.hexZeroPad(input, length); + +export const hexZeroPadAddress = (input: string) => hexZeroPad(input, 32); + +export const hexZeroPadAddressLowercase = (input: string) => hexZeroPad(input.toLowerCase(), 32); + +export const bytes32ToAddress = (input: string) => { + if (!/^0x[a-fA-F0-9]{64}$/.test(input)) { + throw new Error("Invalid bytes32 input"); + } + return ethers.utils.getAddress("0x" + input.slice(26)); +}; + export async function seedWallet( walletToFund: Signer, tokens: Contract[], @@ -134,6 +147,10 @@ export function randomAddress() { return ethers.utils.getAddress(ethers.utils.hexlify(ethers.utils.randomBytes(20))); } +export function randomBytes32() { + return ethers.utils.hexlify(ethers.utils.randomBytes(32)); +} + export async function getParamType(contractName: string, functionName: string, paramName: string) { const contractFactory = await getContractFactory(contractName, new ethers.VoidSigner(ethers.constants.AddressZero)); const fragment = contractFactory.interface.fragments.find((fragment) => fragment.name === functionName); From aa36eb606d57e6f84642ba71867e5dd3fafc6adf Mon Sep 17 00:00:00 2001 From: Chris Maree Date: Tue, 22 Oct 2024 12:28:50 +0200 Subject: [PATCH 03/29] feat: Add relayer repayment address (#653) * WIP Signed-off-by: chrismaree * WIP Signed-off-by: chrismaree * WIP Signed-off-by: chrismaree * WIP Signed-off-by: chrismaree * WIP Signed-off-by: chrismaree --------- Signed-off-by: chrismaree --- contracts/SpokePool.sol | 16 ++-- contracts/interfaces/V3SpokePoolInterface.sol | 11 ++- hardhat.config.ts | 4 +- programs/svm-spoke/src/instructions/fill.rs | 3 +- .../svm-spoke/src/instructions/slow_fill.rs | 2 +- programs/svm-spoke/src/lib.rs | 9 ++- scripts/svm/simpleFill.ts | 2 +- test/evm/hardhat/SpokePool.Relay.ts | 75 ++++++++++++++----- test/evm/hardhat/SpokePool.SlowRelay.ts | 19 +++-- .../Polygon_SpokePool.ts | 12 ++- test/svm/SvmSpoke.Fill.ts | 61 +++++++++++---- test/svm/SvmSpoke.SlowFill.ts | 2 +- 12 files changed, 155 insertions(+), 61 deletions(-) diff --git a/contracts/SpokePool.sol b/contracts/SpokePool.sol index c70bd4f5f..d02370a80 100644 --- a/contracts/SpokePool.sol +++ b/contracts/SpokePool.sol @@ -1030,12 +1030,11 @@ abstract contract SpokePool is * @param repaymentChainId Chain of SpokePool where relayer wants to be refunded after the challenge window has * passed. Will receive inputAmount of the equivalent token to inputToken on the repayment chain. */ - function fillV3Relay(V3RelayData calldata relayData, uint256 repaymentChainId) - public - override - nonReentrant - unpausedFills - { + function fillV3Relay( + V3RelayData calldata relayData, + uint256 repaymentChainId, + bytes32 repaymentAddress + ) public override nonReentrant unpausedFills { // Exclusivity deadline is inclusive and is the latest timestamp that the exclusive relayer has sole right // to fill the relay. if ( @@ -1055,7 +1054,7 @@ abstract contract SpokePool is repaymentChainId: repaymentChainId }); - _fillRelayV3(relayExecution, msg.sender.toBytes32(), false); + _fillRelayV3(relayExecution, repaymentAddress, false); } /** @@ -1078,6 +1077,7 @@ abstract contract SpokePool is function fillV3RelayWithUpdatedDeposit( V3RelayData calldata relayData, uint256 repaymentChainId, + bytes32 repaymentAddress, uint256 updatedOutputAmount, bytes32 updatedRecipient, bytes calldata updatedMessage, @@ -1109,7 +1109,7 @@ abstract contract SpokePool is UPDATE_V3_DEPOSIT_DETAILS_HASH ); - _fillRelayV3(relayExecution, msg.sender.toBytes32(), false); + _fillRelayV3(relayExecution, repaymentAddress, false); } /** diff --git a/contracts/interfaces/V3SpokePoolInterface.sol b/contracts/interfaces/V3SpokePoolInterface.sol index e63d688a1..2b01f5322 100644 --- a/contracts/interfaces/V3SpokePoolInterface.sol +++ b/contracts/interfaces/V3SpokePoolInterface.sol @@ -23,9 +23,9 @@ interface V3SpokePoolInterface { // to know when to send excess funds from the SpokePool to the HubPool because they can no longer be used // for a slow fill execution. SlowFill - // Slow fills are requested via requestSlowFill and executed by executeSlowRelayLeaf after a bundle containing - // the slow fill is validated. } + // Slow fills are requested via requestSlowFill and executed by executeSlowRelayLeaf after a bundle containing + // the slow fill is validated. /************************************** * STRUCTS * @@ -196,11 +196,16 @@ interface V3SpokePoolInterface { bytes calldata depositorSignature ) external; - function fillV3Relay(V3RelayData calldata relayData, uint256 repaymentChainId) external; + function fillV3Relay( + V3RelayData calldata relayData, + uint256 repaymentChainId, + bytes32 repaymentAddress + ) external; function fillV3RelayWithUpdatedDeposit( V3RelayData calldata relayData, uint256 repaymentChainId, + bytes32 repaymentAddress, uint256 updatedOutputAmount, bytes32 updatedRecipient, bytes calldata updatedMessage, diff --git a/hardhat.config.ts b/hardhat.config.ts index 09e3d3c18..0e1a876b8 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -40,7 +40,7 @@ const LARGE_CONTRACT_COMPILER_SETTINGS = { settings: { optimizer: { enabled: true, runs: 1000 }, viaIR: true, - debug: { revertStrings: isTest ? "default" : "strip" }, + debug: { revertStrings: isTest ? "debug" : "strip" }, }, }; const DEFAULT_CONTRACT_COMPILER_SETTINGS = { @@ -49,7 +49,7 @@ const DEFAULT_CONTRACT_COMPILER_SETTINGS = { optimizer: { enabled: true, runs: 1000000 }, viaIR: true, // Only strip revert strings if not testing or in ci. - debug: { revertStrings: isTest ? "default" : "strip" }, + debug: { revertStrings: isTest ? "debug" : "strip" }, }, }; diff --git a/programs/svm-spoke/src/instructions/fill.rs b/programs/svm-spoke/src/instructions/fill.rs index 7394cab6f..986e6999f 100644 --- a/programs/svm-spoke/src/instructions/fill.rs +++ b/programs/svm-spoke/src/instructions/fill.rs @@ -99,6 +99,7 @@ pub fn fill_v3_relay( relay_hash: [u8; 32], // include in props, while not using it, to enable us to access it from the #Instruction Attribute within the accounts. This enables us to pass in the relay_hash PDA. relay_data: V3RelayData, repayment_chain_id: u64, + repayment_address: Pubkey, ) -> Result<()> { let state = &mut ctx.accounts.state; let current_time = get_current_time(state)?; @@ -165,7 +166,7 @@ pub fn fill_v3_relay( fill_deadline: relay_data.fill_deadline, exclusivity_deadline: relay_data.exclusivity_deadline, exclusive_relayer: relay_data.exclusive_relayer, - relayer: *ctx.accounts.signer.key, + relayer: repayment_address, depositor: relay_data.depositor, recipient: relay_data.recipient, message: relay_data.message, diff --git a/programs/svm-spoke/src/instructions/slow_fill.rs b/programs/svm-spoke/src/instructions/slow_fill.rs index 94fb0d4bf..0068eed6b 100644 --- a/programs/svm-spoke/src/instructions/slow_fill.rs +++ b/programs/svm-spoke/src/instructions/slow_fill.rs @@ -255,7 +255,7 @@ pub fn execute_v3_slow_relay_leaf( fill_deadline: relay_data.fill_deadline, exclusivity_deadline: relay_data.exclusivity_deadline, exclusive_relayer: relay_data.exclusive_relayer, - relayer: *ctx.accounts.signer.key, + relayer: Pubkey::default(), // There is no repayment address for slow depositor: relay_data.depositor, recipient: relay_data.recipient, message: relay_data.message, diff --git a/programs/svm-spoke/src/lib.rs b/programs/svm-spoke/src/lib.rs index 53113e2b6..979779cff 100644 --- a/programs/svm-spoke/src/lib.rs +++ b/programs/svm-spoke/src/lib.rs @@ -132,8 +132,15 @@ pub mod svm_spoke { relay_hash: [u8; 32], relay_data: V3RelayData, repayment_chain_id: u64, + repayment_address: Pubkey, ) -> Result<()> { - instructions::fill_v3_relay(ctx, relay_hash, relay_data, repayment_chain_id) + instructions::fill_v3_relay( + ctx, + relay_hash, + relay_data, + repayment_chain_id, + repayment_address, + ) } pub fn close_fill_pda( diff --git a/scripts/svm/simpleFill.ts b/scripts/svm/simpleFill.ts index 30271a464..3849ef1ac 100644 --- a/scripts/svm/simpleFill.ts +++ b/scripts/svm/simpleFill.ts @@ -121,7 +121,7 @@ async function fillV3Relay(): Promise { })) ); - const tx = await (program.methods.fillV3Relay(Array.from(relayHashUint8Array), relayData, chainId) as any) + const tx = await (program.methods.fillV3Relay(Array.from(relayHashUint8Array), relayData, chainId, signer) as any) .accounts({ state: statePda, signer: signer, diff --git a/test/evm/hardhat/SpokePool.Relay.ts b/test/evm/hardhat/SpokePool.Relay.ts index 813424496..eca78f472 100644 --- a/test/evm/hardhat/SpokePool.Relay.ts +++ b/test/evm/hardhat/SpokePool.Relay.ts @@ -318,14 +318,17 @@ describe("SpokePool Relayer Logic", async function () { describe("fillV3Relay", function () { it("fills are not paused", async function () { await spokePool.pauseFills(true); - await expect(spokePool.connect(relayer).fillV3Relay(relayData, consts.repaymentChainId)).to.be.revertedWith( - "FillsArePaused" - ); + await expect( + spokePool + .connect(relayer) + .fillV3Relay(relayData, consts.repaymentChainId, hexZeroPadAddressLowercase(relayer.address)) + ).to.be.revertedWith("FillsArePaused"); }); it("reentrancy protected", async function () { const functionCalldata = spokePool.interface.encodeFunctionData("fillV3Relay", [ relayData, consts.repaymentChainId, + hexZeroPadAddressLowercase(relayer.address), ]); await expect(spokePool.connect(relayer).callback(functionCalldata)).to.be.revertedWith( "ReentrancyGuard: reentrant call" @@ -338,19 +341,21 @@ describe("SpokePool Relayer Logic", async function () { exclusiveRelayer: hexZeroPadAddress(recipient.address), exclusivityDeadline: relayData.fillDeadline, }; - await expect(spokePool.connect(relayer).fillV3Relay(_relayData, consts.repaymentChainId)).to.be.revertedWith( - "NotExclusiveRelayer" - ); + await expect( + spokePool + .connect(relayer) + .fillV3Relay(_relayData, consts.repaymentChainId, hexZeroPadAddressLowercase(relayer.address)) + ).to.be.revertedWith("NotExclusiveRelayer"); // Can send it after exclusivity deadline await expect( - spokePool.connect(relayer).fillV3Relay( - { - ..._relayData, - exclusivityDeadline: 0, - }, - consts.repaymentChainId - ) + spokePool + .connect(relayer) + .fillV3Relay( + { ..._relayData, exclusivityDeadline: 0 }, + consts.repaymentChainId, + hexZeroPadAddressLowercase(relayer.address) + ) ).to.not.be.reverted; }); it("if no exclusive relayer is set, exclusivity deadline can be in future", async function () { @@ -361,7 +366,11 @@ describe("SpokePool Relayer Logic", async function () { }; // Can send it after exclusivity deadline - await expect(spokePool.connect(relayer).fillV3Relay(_relayData, consts.repaymentChainId)).to.not.be.reverted; + await expect( + spokePool + .connect(relayer) + .fillV3Relay(_relayData, consts.repaymentChainId, hexZeroPadAddressLowercase(relayer.address)) + ).to.not.be.reverted; }); it("can have empty exclusive relayer before exclusivity deadline", async function () { const _relayData = { @@ -371,10 +380,18 @@ describe("SpokePool Relayer Logic", async function () { }; // Can send it before exclusivity deadline if exclusive relayer is empty - await expect(spokePool.connect(relayer).fillV3Relay(_relayData, consts.repaymentChainId)).to.not.be.reverted; + await expect( + spokePool + .connect(relayer) + .fillV3Relay(_relayData, consts.repaymentChainId, hexZeroPadAddressLowercase(relayer.address)) + ).to.not.be.reverted; }); it("calls _fillRelayV3 with expected params", async function () { - await expect(spokePool.connect(relayer).fillV3Relay(relayData, consts.repaymentChainId)) + await expect( + spokePool + .connect(relayer) + .fillV3Relay(relayData, consts.repaymentChainId, hexZeroPadAddressLowercase(relayer.address)) + ) .to.emit(spokePool, "FilledV3Relay") .withArgs( hexZeroPadAddressLowercase(relayData.inputToken), @@ -430,6 +447,7 @@ describe("SpokePool Relayer Logic", async function () { .fillV3RelayWithUpdatedDeposit( _relayData, consts.repaymentChainId, + hexZeroPadAddressLowercase(relayer.address), updatedOutputAmount, hexZeroPadAddress(updatedRecipient), updatedMessage, @@ -445,6 +463,7 @@ describe("SpokePool Relayer Logic", async function () { exclusivityDeadline: 0, }, consts.repaymentChainId, + hexZeroPadAddressLowercase(relayer.address), updatedOutputAmount, hexZeroPadAddress(updatedRecipient), updatedMessage, @@ -464,6 +483,7 @@ describe("SpokePool Relayer Logic", async function () { .fillV3RelayWithUpdatedDeposit( relayData, consts.repaymentChainId, + hexZeroPadAddressLowercase(relayer.address), updatedOutputAmount, hexZeroPadAddress(updatedRecipient), updatedMessage, @@ -508,6 +528,7 @@ describe("SpokePool Relayer Logic", async function () { .fillV3RelayWithUpdatedDeposit( { ...relayData, depositor: hexZeroPadAddress(relayer.address) }, consts.repaymentChainId, + hexZeroPadAddressLowercase(relayer.address), updatedOutputAmount, hexZeroPadAddress(updatedRecipient), updatedMessage, @@ -530,6 +551,7 @@ describe("SpokePool Relayer Logic", async function () { .fillV3RelayWithUpdatedDeposit( relayData, consts.repaymentChainId, + hexZeroPadAddressLowercase(relayer.address), updatedOutputAmount, hexZeroPadAddress(updatedRecipient), updatedMessage, @@ -544,6 +566,7 @@ describe("SpokePool Relayer Logic", async function () { .fillV3RelayWithUpdatedDeposit( { ...relayData, originChainId: relayData.originChainId + 1 }, consts.repaymentChainId, + hexZeroPadAddressLowercase(relayer.address), updatedOutputAmount, hexZeroPadAddress(updatedRecipient), updatedMessage, @@ -558,6 +581,7 @@ describe("SpokePool Relayer Logic", async function () { .fillV3RelayWithUpdatedDeposit( { ...relayData, depositId: relayData.depositId + 1 }, consts.repaymentChainId, + hexZeroPadAddressLowercase(relayer.address), updatedOutputAmount, hexZeroPadAddress(updatedRecipient), updatedMessage, @@ -572,6 +596,7 @@ describe("SpokePool Relayer Logic", async function () { .fillV3RelayWithUpdatedDeposit( relayData, consts.repaymentChainId, + hexZeroPadAddressLowercase(relayer.address), updatedOutputAmount.sub(1), hexZeroPadAddress(updatedRecipient), updatedMessage, @@ -586,6 +611,7 @@ describe("SpokePool Relayer Logic", async function () { .fillV3RelayWithUpdatedDeposit( relayData, consts.repaymentChainId, + hexZeroPadAddressLowercase(relayer.address), updatedOutputAmount, hexZeroPadAddress(randomAddress()), updatedMessage, @@ -600,6 +626,7 @@ describe("SpokePool Relayer Logic", async function () { .fillV3RelayWithUpdatedDeposit( relayData, consts.repaymentChainId, + hexZeroPadAddressLowercase(relayer.address), updatedOutputAmount, hexZeroPadAddress(updatedRecipient), updatedMessage, @@ -624,6 +651,7 @@ describe("SpokePool Relayer Logic", async function () { .fillV3RelayWithUpdatedDeposit( { ...relayData, depositor: hexZeroPadAddress(erc1271.address) }, consts.repaymentChainId, + hexZeroPadAddressLowercase(relayer.address), updatedOutputAmount, hexZeroPadAddress(updatedRecipient), updatedMessage, @@ -636,6 +664,7 @@ describe("SpokePool Relayer Logic", async function () { .fillV3RelayWithUpdatedDeposit( { ...relayData, depositor: hexZeroPadAddress(erc1271.address) }, consts.repaymentChainId, + hexZeroPadAddressLowercase(relayer.address), updatedOutputAmount, hexZeroPadAddress(updatedRecipient), updatedMessage, @@ -644,13 +673,16 @@ describe("SpokePool Relayer Logic", async function () { ).to.not.be.reverted; }); it("cannot send updated fill after original fill", async function () { - await spokePool.connect(relayer).fillV3Relay(relayData, consts.repaymentChainId); + await spokePool + .connect(relayer) + .fillV3Relay(relayData, consts.repaymentChainId, hexZeroPadAddressLowercase(relayer.address)); await expect( spokePool .connect(relayer) .fillV3RelayWithUpdatedDeposit( relayData, consts.repaymentChainId, + hexZeroPadAddressLowercase(relayer.address), updatedOutputAmount, hexZeroPadAddress(updatedRecipient), updatedMessage, @@ -664,14 +696,17 @@ describe("SpokePool Relayer Logic", async function () { .fillV3RelayWithUpdatedDeposit( relayData, consts.repaymentChainId, + hexZeroPadAddressLowercase(relayer.address), updatedOutputAmount, hexZeroPadAddress(updatedRecipient), updatedMessage, signature ); - await expect(spokePool.connect(relayer).fillV3Relay(relayData, consts.repaymentChainId)).to.be.revertedWith( - "RelayFilled" - ); + await expect( + spokePool + .connect(relayer) + .fillV3Relay(relayData, consts.repaymentChainId, hexZeroPadAddressLowercase(relayer.address)) + ).to.be.revertedWith("RelayFilled"); }); }); }); diff --git a/test/evm/hardhat/SpokePool.SlowRelay.ts b/test/evm/hardhat/SpokePool.SlowRelay.ts index 522f05bb5..cf69e43d8 100644 --- a/test/evm/hardhat/SpokePool.SlowRelay.ts +++ b/test/evm/hardhat/SpokePool.SlowRelay.ts @@ -8,7 +8,6 @@ import { seedWallet, hexZeroPadAddress, hexZeroPadAddressLowercase, - bytes32ToAddress, } from "../../../utils/utils"; import { spokePoolFixture, V3RelayData, getV3RelayHash, V3SlowFill, FillType } from "./fixtures/SpokePool.Fixture"; import { buildV3SlowRelayTree } from "./MerkleLib.utils"; @@ -84,7 +83,9 @@ describe("SpokePool Slow Relay Logic", async function () { ); // Can fast fill after: - await spokePool.connect(relayer).fillV3Relay(relayData, consts.repaymentChainId); + await spokePool + .connect(relayer) + .fillV3Relay(relayData, consts.repaymentChainId, hexZeroPadAddress(relayer.address)); }); it("cannot request if FillStatus is Filled", async function () { const relayHash = getV3RelayHash(relayData, consts.destinationChainId); @@ -166,7 +167,9 @@ describe("SpokePool Slow Relay Logic", async function () { // Cannot fast fill after slow fill await expect( - spokePool.connect(relayer).fillV3Relay(slowRelayLeaf.relayData, consts.repaymentChainId) + spokePool + .connect(relayer) + .fillV3Relay(slowRelayLeaf.relayData, consts.repaymentChainId, hexZeroPadAddress(relayer.address)) ).to.be.revertedWith("RelayFilled"); }); it("cannot be used to double send a fill", async function () { @@ -174,7 +177,9 @@ describe("SpokePool Slow Relay Logic", async function () { await spokePool.connect(depositor).relayRootBundle(consts.mockTreeRoot, tree.getHexRoot()); // Fill before executing slow fill - await spokePool.connect(relayer).fillV3Relay(slowRelayLeaf.relayData, consts.repaymentChainId); + await spokePool + .connect(relayer) + .fillV3Relay(slowRelayLeaf.relayData, consts.repaymentChainId, hexZeroPadAddress(relayer.address)); await expect( spokePool.connect(relayer).executeV3SlowRelayLeaf( slowRelayLeaf, @@ -290,15 +295,13 @@ describe("SpokePool Slow Relay Logic", async function () { hexZeroPadAddressLowercase(relayData.outputToken), relayData.inputAmount, relayData.outputAmount, - // Sets repaymentChainId to 0: - 0, + 0, // Sets repaymentChainId to 0. relayData.originChainId, relayData.depositId, relayData.fillDeadline, relayData.exclusivityDeadline, hexZeroPadAddressLowercase(relayData.exclusiveRelayer), - // Sets relayer address to 0x0 - hexZeroPadAddressLowercase(consts.zeroAddress), + hexZeroPadAddressLowercase(consts.zeroAddress), // Sets relayer address to 0x0 hexZeroPadAddressLowercase(relayData.depositor), hexZeroPadAddressLowercase(relayData.recipient), relayData.message, diff --git a/test/evm/hardhat/chain-specific-spokepools/Polygon_SpokePool.ts b/test/evm/hardhat/chain-specific-spokepools/Polygon_SpokePool.ts index 156cc34d0..df82f23fd 100644 --- a/test/evm/hardhat/chain-specific-spokepools/Polygon_SpokePool.ts +++ b/test/evm/hardhat/chain-specific-spokepools/Polygon_SpokePool.ts @@ -335,8 +335,16 @@ describe("Polygon Spoke Pool", function () { message: "0x1234", }; const fillData = [ - polygonSpokePool.interface.encodeFunctionData("fillV3Relay", [relayData, repaymentChainId]), - polygonSpokePool.interface.encodeFunctionData("fillV3Relay", [{ ...relayData, depositId: 1 }, repaymentChainId]), + polygonSpokePool.interface.encodeFunctionData("fillV3Relay", [ + relayData, + repaymentChainId, + hexZeroPadAddress(relayer.address), + ]), + polygonSpokePool.interface.encodeFunctionData("fillV3Relay", [ + { ...relayData, depositId: 1 }, + repaymentChainId, + hexZeroPadAddress(relayer.address), + ]), ]; const otherData = [polygonSpokePool.interface.encodeFunctionData("wrap", [])]; diff --git a/test/svm/SvmSpoke.Fill.ts b/test/svm/SvmSpoke.Fill.ts index f1649caf7..bf02e7914 100644 --- a/test/svm/SvmSpoke.Fill.ts +++ b/test/svm/SvmSpoke.Fill.ts @@ -93,7 +93,11 @@ describe("svm_spoke.fill", () => { assertSE(relayerAccount.amount, seedBalance, "Relayer's balance should be equal to seed balance before the fill"); const relayHash = Array.from(calculateRelayHashUint8Array(relayData, chainId)); - await program.methods.fillV3Relay(relayHash, relayData, new BN(1)).accounts(accounts).signers([relayer]).rpc(); + await program.methods + .fillV3Relay(relayHash, relayData, new BN(1), relayer.publicKey) + .accounts(accounts) + .signers([relayer]) + .rpc(); // Verify relayer's balance after the fill relayerAccount = await getAccount(connection, relayerTA); @@ -110,7 +114,11 @@ describe("svm_spoke.fill", () => { it("Verifies FilledV3Relay event after filling a relay", async () => { const relayHash = Array.from(calculateRelayHashUint8Array(relayData, chainId)); - await program.methods.fillV3Relay(relayHash, relayData, new BN(1)).accounts(accounts).signers([relayer]).rpc(); + await program.methods + .fillV3Relay(relayHash, relayData, new BN(420), otherRelayer.publicKey) + .accounts(accounts) + .signers([relayer]) + .rpc(); // Fetch and verify the FilledV3Relay event await new Promise((resolve) => setTimeout(resolve, 500)); @@ -122,6 +130,9 @@ describe("svm_spoke.fill", () => { Object.keys(relayData).forEach((key) => { assertSE(event[key], relayData[key], `${key.charAt(0).toUpperCase() + key.slice(1)} should match`); }); + // These props below are not part of relayData. + assertSE(event.repaymentChainId, new BN(420), "Repayment chain id should match"); + assertSE(event.relayer, otherRelayer.publicKey, "Repayment address should match"); }); it("Fails to fill a V3 relay after the fill deadline", async () => { @@ -129,7 +140,11 @@ describe("svm_spoke.fill", () => { const relayHash = Array.from(calculateRelayHashUint8Array(relayData, chainId)); try { - await program.methods.fillV3Relay(relayHash, relayData, new BN(1)).accounts(accounts).signers([relayer]).rpc(); + await program.methods + .fillV3Relay(relayHash, relayData, new BN(1), relayer.publicKey) + .accounts(accounts) + .signers([relayer]) + .rpc(); assert.fail("Fill should have failed due to fill deadline passed"); } catch (err: any) { assert.include(err.toString(), "ExpiredFillDeadline", "Expected ExpiredFillDeadline error"); @@ -144,7 +159,7 @@ describe("svm_spoke.fill", () => { const relayHash = Array.from(calculateRelayHashUint8Array(relayData, chainId)); try { await program.methods - .fillV3Relay(relayHash, relayData, new BN(1)) + .fillV3Relay(relayHash, relayData, new BN(1), relayer.publicKey) .accounts(accounts) .signers([otherRelayer]) .rpc(); @@ -165,7 +180,11 @@ describe("svm_spoke.fill", () => { const relayerAccountBefore = await getAccount(connection, otherRelayerTA); const relayHash = Array.from(calculateRelayHashUint8Array(relayData, chainId)); - await program.methods.fillV3Relay(relayHash, relayData, new BN(1)).accounts(accounts).signers([otherRelayer]).rpc(); + await program.methods + .fillV3Relay(relayHash, relayData, new BN(1), relayer.publicKey) + .accounts(accounts) + .signers([otherRelayer]) + .rpc(); // Verify relayer's balance after the fill const relayerAccountAfter = await getAccount(connection, otherRelayerTA); @@ -188,11 +207,19 @@ describe("svm_spoke.fill", () => { const relayHash = Array.from(calculateRelayHashUint8Array(relayData, chainId)); // First fill attempt - await program.methods.fillV3Relay(relayHash, relayData, new BN(1)).accounts(accounts).signers([relayer]).rpc(); + await program.methods + .fillV3Relay(relayHash, relayData, new BN(1), relayer.publicKey) + .accounts(accounts) + .signers([relayer]) + .rpc(); // Second fill attempt with the same data try { - await program.methods.fillV3Relay(relayHash, relayData, new BN(1)).accounts(accounts).signers([relayer]).rpc(); + await program.methods + .fillV3Relay(relayHash, relayData, new BN(1), relayer.publicKey) + .accounts(accounts) + .signers([relayer]) + .rpc(); assert.fail("Fill should have failed due to RelayFilled error"); } catch (err: any) { assert.include(err.toString(), "RelayFilled", "Expected RelayFilled error"); @@ -210,7 +237,11 @@ describe("svm_spoke.fill", () => { }; // Execute the fill_v3_relay call - await program.methods.fillV3Relay(relayHash, relayData, new BN(1)).accounts(accounts).signers([relayer]).rpc(); + await program.methods + .fillV3Relay(relayHash, relayData, new BN(1), relayer.publicKey) + .accounts(accounts) + .signers([relayer]) + .rpc(); // Verify the fill PDA exists before closing const fillStatusAccountBefore = await connection.getAccountInfo(accounts.fillStatus); @@ -225,7 +256,7 @@ describe("svm_spoke.fill", () => { } // Set the current time to past the fill deadline - await setCurrentTime(program, state, relayer, relayData.fillDeadline.add(new BN(1))); + await setCurrentTime(program, state, relayer, relayData.fillDeadline.add(new BN(1), relayer.publicKey)); // Close the fill PDA await program.methods.closeFillPda(relayHash, relayData).accounts(closeFillPdaAccounts).signers([relayer]).rpc(); @@ -245,7 +276,7 @@ describe("svm_spoke.fill", () => { // Fill the relay await program.methods - .fillV3Relay(Array.from(relayHash), relayData, new BN(1)) + .fillV3Relay(Array.from(relayHash), relayData, new BN(1), relayer.publicKey) .accounts(accounts) .signers([relayer]) .rpc(); @@ -267,7 +298,11 @@ describe("svm_spoke.fill", () => { // Try to fill the relay. This should fail because fills are paused. const relayHash = Array.from(calculateRelayHashUint8Array(relayData, chainId)); try { - await program.methods.fillV3Relay(relayHash, relayData, new BN(1)).accounts(accounts).signers([relayer]).rpc(); + await program.methods + .fillV3Relay(relayHash, relayData, new BN(1), relayer.publicKey) + .accounts(accounts) + .signers([relayer]) + .rpc(); assert.fail("Should not be able to fill relay when fills are paused"); } catch (err: any) { assert.include(err.toString(), "Fills are currently paused!", "Expected fills paused error"); @@ -284,7 +319,7 @@ describe("svm_spoke.fill", () => { try { await program.methods - .fillV3Relay(Array.from(relayHash), relayData, new BN(1)) + .fillV3Relay(Array.from(relayHash), relayData, new BN(1), relayer.publicKey) .accounts({ ...accounts, recipient: wrongRecipient, @@ -312,7 +347,7 @@ describe("svm_spoke.fill", () => { try { await program.methods - .fillV3Relay(Array.from(relayHash), relayData, new BN(1)) + .fillV3Relay(Array.from(relayHash), relayData, new BN(1), relayer.publicKey) .accounts({ ...accounts, mintAccount: wrongMint, diff --git a/test/svm/SvmSpoke.SlowFill.ts b/test/svm/SvmSpoke.SlowFill.ts index b8a81ad5f..24572c65b 100644 --- a/test/svm/SvmSpoke.SlowFill.ts +++ b/test/svm/SvmSpoke.SlowFill.ts @@ -221,7 +221,7 @@ describe("svm_spoke.slow_fill", () => { // Fill the relay first await program.methods - .fillV3Relay(relayHash, formatRelayData(relayData), new BN(1)) + .fillV3Relay(relayHash, formatRelayData(relayData), new BN(1), relayer.publicKey) .accounts(fillAccounts) .signers([relayer]) .rpc(); From 1afdf1ef388ef03a10084ab6bfa3313853a837bf Mon Sep 17 00:00:00 2001 From: Chris Maree Date: Tue, 22 Oct 2024 13:07:56 +0200 Subject: [PATCH 04/29] fix: clean up cast utilities (#676) * WIP Signed-off-by: chrismaree * WIP Signed-off-by: chrismaree * WIP Signed-off-by: chrismaree * WIP Signed-off-by: chrismaree * WIP Signed-off-by: chrismaree * WIP Signed-off-by: chrismaree --------- Signed-off-by: chrismaree --- test/evm/hardhat/MerkleLib.utils.ts | 4 +- test/evm/hardhat/SpokePool.Admin.ts | 11 +- test/evm/hardhat/SpokePool.Deposit.ts | 133 ++++++----- .../hardhat/SpokePool.ExecuteRootBundle.ts | 35 ++- test/evm/hardhat/SpokePool.Relay.ts | 219 ++++++++---------- test/evm/hardhat/SpokePool.SlowRelay.ts | 45 ++-- .../Arbitrum_SpokePool.ts | 4 +- .../Ethereum_SpokePool.ts | 4 +- .../Optimism_SpokePool.ts | 4 +- .../Polygon_SpokePool.ts | 22 +- .../evm/hardhat/fixtures/SpokePool.Fixture.ts | 2 +- utils/utils.ts | 4 +- 12 files changed, 230 insertions(+), 257 deletions(-) diff --git a/test/evm/hardhat/MerkleLib.utils.ts b/test/evm/hardhat/MerkleLib.utils.ts index 2508d7398..39e6aa3a6 100644 --- a/test/evm/hardhat/MerkleLib.utils.ts +++ b/test/evm/hardhat/MerkleLib.utils.ts @@ -7,7 +7,7 @@ import { toBNWeiWithDecimals, createRandomBytes32, Contract, - hexZeroPadAddress, + addressToBytes, } from "../../../utils/utils"; import { amountToReturn, repaymentChainId } from "./constants"; import { MerkleTree } from "../../../utils/MerkleTree"; @@ -110,7 +110,7 @@ export async function constructSingleRelayerRefundTree(l2Token: Contract | Strin const leaves = buildRelayerRefundLeaves( [destinationChainId], // Destination chain ID. [amountToReturn], // amountToReturn. - [hexZeroPadAddress(l2Token as string)], // l2Token. + [addressToBytes(l2Token as string)], // l2Token. [[]], // refundAddresses. [[]] // refundAmounts. ); diff --git a/test/evm/hardhat/SpokePool.Admin.ts b/test/evm/hardhat/SpokePool.Admin.ts index 955c10e13..f51adf376 100644 --- a/test/evm/hardhat/SpokePool.Admin.ts +++ b/test/evm/hardhat/SpokePool.Admin.ts @@ -1,11 +1,4 @@ -import { - expect, - ethers, - Contract, - SignerWithAddress, - getContractFactory, - hexZeroPadAddress, -} from "../../../utils/utils"; +import { expect, ethers, Contract, SignerWithAddress, getContractFactory, addressToBytes } from "../../../utils/utils"; import { hre } from "../../../utils/utils.hre"; import { spokePoolFixture } from "./fixtures/SpokePool.Fixture"; import { destinationChainId, mockRelayerRefundRoot, mockSlowRelayRoot } from "./constants"; @@ -30,7 +23,7 @@ describe("SpokePool Admin Functions", async function () { await expect(spokePool.connect(owner).setEnableRoute(erc20.address, destinationChainId, true)) .to.emit(spokePool, "EnabledDepositRoute") .withArgs(erc20.address, destinationChainId, true); - expect(await spokePool.enabledDepositRoutes(hexZeroPadAddress(erc20.address), destinationChainId)).to.equal(true); + expect(await spokePool.enabledDepositRoutes(addressToBytes(erc20.address), destinationChainId)).to.equal(true); }); it("Pause deposits", async function () { diff --git a/test/evm/hardhat/SpokePool.Deposit.ts b/test/evm/hardhat/SpokePool.Deposit.ts index 4698dbd3a..135744d01 100644 --- a/test/evm/hardhat/SpokePool.Deposit.ts +++ b/test/evm/hardhat/SpokePool.Deposit.ts @@ -8,8 +8,7 @@ import { toWei, randomAddress, BigNumber, - hexZeroPadAddressLowercase, - hexZeroPadAddress, + addressToBytes, bytes32ToAddress, } from "../../../utils/utils"; import { @@ -108,8 +107,8 @@ describe("SpokePool Depositor Logic", async function () { ) .to.emit(spokePool, "V3FundsDeposited") .withArgs( - hexZeroPadAddressLowercase(erc20.address), - hexZeroPadAddressLowercase(ZERO_ADDRESS), + addressToBytes(erc20.address), + addressToBytes(ZERO_ADDRESS), amountToDeposit, amountReceived, destinationChainId, @@ -117,9 +116,9 @@ describe("SpokePool Depositor Logic", async function () { quoteTimestamp, MAX_UINT32, 0, - hexZeroPadAddressLowercase(depositor.address), - hexZeroPadAddressLowercase(recipient.address), - hexZeroPadAddressLowercase(ZERO_ADDRESS), + addressToBytes(depositor.address), + addressToBytes(recipient.address), + addressToBytes(ZERO_ADDRESS), "0x" ); @@ -148,8 +147,8 @@ describe("SpokePool Depositor Logic", async function () { ) .to.emit(spokePool, "V3FundsDeposited") .withArgs( - hexZeroPadAddressLowercase(erc20.address), - hexZeroPadAddressLowercase(ZERO_ADDRESS), + addressToBytes(erc20.address), + addressToBytes(ZERO_ADDRESS), amountToDeposit, amountReceived, destinationChainId, @@ -157,9 +156,9 @@ describe("SpokePool Depositor Logic", async function () { quoteTimestamp, BigNumber.from("0xFFFFFFFF"), 0, - hexZeroPadAddressLowercase(newDepositor), // Depositor is overridden. - hexZeroPadAddressLowercase(recipient.address), - hexZeroPadAddressLowercase(ZERO_ADDRESS), + addressToBytes(newDepositor), // Depositor is overridden. + addressToBytes(recipient.address), + addressToBytes(ZERO_ADDRESS), "0x" ); }); @@ -374,16 +373,16 @@ describe("SpokePool Depositor Logic", async function () { _isAddressOverload = false ) { return [ - _isAddressOverload ? bytes32ToAddress(_relayData.depositor) : hexZeroPadAddress(_relayData.depositor), - _isAddressOverload ? bytes32ToAddress(_relayData.recipient) : hexZeroPadAddress(_relayData.recipient), - _isAddressOverload ? bytes32ToAddress(_relayData.inputToken) : hexZeroPadAddress(_relayData.inputToken), - _isAddressOverload ? bytes32ToAddress(_relayData.outputToken) : hexZeroPadAddress(_relayData.outputToken), + _isAddressOverload ? bytes32ToAddress(_relayData.depositor) : addressToBytes(_relayData.depositor), + _isAddressOverload ? bytes32ToAddress(_relayData.recipient) : addressToBytes(_relayData.recipient), + _isAddressOverload ? bytes32ToAddress(_relayData.inputToken) : addressToBytes(_relayData.inputToken), + _isAddressOverload ? bytes32ToAddress(_relayData.outputToken) : addressToBytes(_relayData.outputToken), _relayData.inputAmount, _relayData.outputAmount, _destinationChainId, _isAddressOverload ? bytes32ToAddress(_relayData.exclusiveRelayer) - : hexZeroPadAddress(_relayData.exclusiveRelayer), + : addressToBytes(_relayData.exclusiveRelayer), _quoteTimestamp, _relayData.fillDeadline, _relayData.exclusivityDeadline, @@ -392,11 +391,11 @@ describe("SpokePool Depositor Logic", async function () { } beforeEach(async function () { relayData = { - depositor: hexZeroPadAddress(depositor.address), - recipient: hexZeroPadAddress(recipient.address), - exclusiveRelayer: hexZeroPadAddress(ZERO_ADDRESS), - inputToken: hexZeroPadAddress(erc20.address), - outputToken: hexZeroPadAddress(randomAddress()), + depositor: addressToBytes(depositor.address), + recipient: addressToBytes(recipient.address), + exclusiveRelayer: addressToBytes(ZERO_ADDRESS), + inputToken: addressToBytes(erc20.address), + outputToken: addressToBytes(randomAddress()), inputAmount: amountToDeposit, outputAmount: amountToDeposit.sub(19), originChainId: originChainId, @@ -511,14 +510,14 @@ describe("SpokePool Depositor Logic", async function () { spokePool .connect(depositor) [depositV3NowBytes]( - hexZeroPadAddress(relayData.depositor), - hexZeroPadAddress(relayData.recipient), - hexZeroPadAddress(relayData.inputToken), - hexZeroPadAddress(relayData.outputToken), + addressToBytes(relayData.depositor), + addressToBytes(relayData.recipient), + addressToBytes(relayData.inputToken), + addressToBytes(relayData.outputToken), relayData.inputAmount, relayData.outputAmount, destinationChainId, - hexZeroPadAddress(relayData.exclusiveRelayer), + addressToBytes(relayData.exclusiveRelayer), fillDeadlineOffset, exclusivityDeadline, relayData.message @@ -526,8 +525,8 @@ describe("SpokePool Depositor Logic", async function () { ) .to.emit(spokePool, "V3FundsDeposited") .withArgs( - hexZeroPadAddressLowercase(relayData.inputToken), - hexZeroPadAddressLowercase(relayData.outputToken), + addressToBytes(relayData.inputToken), + addressToBytes(relayData.outputToken), relayData.inputAmount, relayData.outputAmount, destinationChainId, @@ -536,9 +535,9 @@ describe("SpokePool Depositor Logic", async function () { currentTime, // quoteTimestamp should be current time currentTime + fillDeadlineOffset, // fillDeadline should be current time + offset currentTime, - hexZeroPadAddressLowercase(relayData.depositor), - hexZeroPadAddressLowercase(relayData.recipient), - hexZeroPadAddressLowercase(relayData.exclusiveRelayer), + addressToBytes(relayData.depositor), + addressToBytes(relayData.recipient), + addressToBytes(relayData.exclusiveRelayer), relayData.message ); }); @@ -565,8 +564,8 @@ describe("SpokePool Depositor Logic", async function () { ) .to.emit(spokePool, "V3FundsDeposited") .withArgs( - hexZeroPadAddressLowercase(relayData.inputToken), - hexZeroPadAddressLowercase(relayData.outputToken), + addressToBytes(relayData.inputToken), + addressToBytes(relayData.outputToken), relayData.inputAmount, relayData.outputAmount, destinationChainId, @@ -575,9 +574,9 @@ describe("SpokePool Depositor Logic", async function () { currentTime, // quoteTimestamp should be current time currentTime + fillDeadlineOffset, // fillDeadline should be current time + offset currentTime, - hexZeroPadAddressLowercase(relayData.depositor), - hexZeroPadAddressLowercase(relayData.recipient), - hexZeroPadAddressLowercase(relayData.exclusiveRelayer), + addressToBytes(relayData.depositor), + addressToBytes(relayData.recipient), + addressToBytes(relayData.exclusiveRelayer), relayData.message ); }); @@ -586,8 +585,8 @@ describe("SpokePool Depositor Logic", async function () { await expect(spokePool.connect(depositor)[depositV3Bytes](...depositArgs)) .to.emit(spokePool, "V3FundsDeposited") .withArgs( - hexZeroPadAddressLowercase(relayData.inputToken), - hexZeroPadAddressLowercase(relayData.outputToken), + addressToBytes(relayData.inputToken), + addressToBytes(relayData.outputToken), relayData.inputAmount, relayData.outputAmount, destinationChainId, @@ -596,9 +595,9 @@ describe("SpokePool Depositor Logic", async function () { quoteTimestamp, relayData.fillDeadline, currentTime, - hexZeroPadAddressLowercase(relayData.depositor), - hexZeroPadAddressLowercase(relayData.recipient), - hexZeroPadAddressLowercase(relayData.exclusiveRelayer), + addressToBytes(relayData.depositor), + addressToBytes(relayData.recipient), + addressToBytes(relayData.exclusiveRelayer), relayData.message ); }); @@ -617,8 +616,8 @@ describe("SpokePool Depositor Logic", async function () { ) .to.emit(spokePool, "V3FundsDeposited") .withArgs( - hexZeroPadAddressLowercase(relayData.inputToken), - hexZeroPadAddressLowercase(relayData.outputToken), + addressToBytes(relayData.inputToken), + addressToBytes(relayData.outputToken), relayData.inputAmount, relayData.outputAmount, destinationChainId, @@ -627,9 +626,9 @@ describe("SpokePool Depositor Logic", async function () { relayData.fillDeadline, currentTime, // New depositor - hexZeroPadAddressLowercase(newDepositor), - hexZeroPadAddressLowercase(relayData.recipient), - hexZeroPadAddressLowercase(relayData.exclusiveRelayer), + addressToBytes(newDepositor), + addressToBytes(relayData.recipient), + addressToBytes(relayData.exclusiveRelayer), relayData.message ); expect(await erc20.balanceOf(depositor.address)).to.equal(balanceBefore.sub(amountToDeposit)); @@ -658,15 +657,15 @@ describe("SpokePool Depositor Logic", async function () { depositId, originChainId, updatedOutputAmount, - hexZeroPadAddress(updatedRecipient), + addressToBytes(updatedRecipient), updatedMessage ); await spokePool[verifyUpdateV3DepositMessageBytes]( - hexZeroPadAddress(depositor.address), + addressToBytes(depositor.address), depositId, originChainId, updatedOutputAmount, - hexZeroPadAddress(updatedRecipient), + addressToBytes(updatedRecipient), updatedMessage, signature ); @@ -674,11 +673,11 @@ describe("SpokePool Depositor Logic", async function () { // Reverts if passed in depositor is the signer or if signature is incorrect await expect( spokePool[verifyUpdateV3DepositMessageBytes]( - hexZeroPadAddress(updatedRecipient), + addressToBytes(updatedRecipient), depositId, originChainId, updatedOutputAmount, - hexZeroPadAddress(updatedRecipient), + addressToBytes(updatedRecipient), updatedMessage, signature ) @@ -690,16 +689,16 @@ describe("SpokePool Depositor Logic", async function () { depositId + 1, originChainId, updatedOutputAmount, - hexZeroPadAddress(updatedRecipient), + addressToBytes(updatedRecipient), updatedMessage ); await expect( spokePool[verifyUpdateV3DepositMessageBytes]( - hexZeroPadAddress(depositor.address), + addressToBytes(depositor.address), depositId, originChainId, updatedOutputAmount, - hexZeroPadAddress(updatedRecipient), + addressToBytes(updatedRecipient), updatedMessage, invalidSignature ) @@ -713,17 +712,17 @@ describe("SpokePool Depositor Logic", async function () { depositId, spokePoolChainId, updatedOutputAmount, - hexZeroPadAddress(updatedRecipient), + addressToBytes(updatedRecipient), updatedMessage ); await expect( spokePool .connect(depositor) [speedUpV3DepositBytes]( - hexZeroPadAddress(depositor.address), + addressToBytes(depositor.address), depositId, updatedOutputAmount, - hexZeroPadAddress(updatedRecipient), + addressToBytes(updatedRecipient), updatedMessage, expectedSignature ) @@ -732,8 +731,8 @@ describe("SpokePool Depositor Logic", async function () { .withArgs( updatedOutputAmount, depositId, - hexZeroPadAddressLowercase(depositor.address), - hexZeroPadAddressLowercase(updatedRecipient), + addressToBytes(depositor.address), + addressToBytes(updatedRecipient), updatedMessage, expectedSignature ); @@ -745,16 +744,16 @@ describe("SpokePool Depositor Logic", async function () { depositId, otherChainId, updatedOutputAmount, - hexZeroPadAddress(updatedRecipient), + addressToBytes(updatedRecipient), updatedMessage ); await expect( spokePool[verifyUpdateV3DepositMessageBytes]( - hexZeroPadAddress(depositor.address), + addressToBytes(depositor.address), depositId, otherChainId, updatedOutputAmount, - hexZeroPadAddress(updatedRecipient), + addressToBytes(updatedRecipient), updatedMessage, invalidSignatureForChain ) @@ -763,10 +762,10 @@ describe("SpokePool Depositor Logic", async function () { spokePool .connect(depositor) [speedUpV3DepositBytes]( - hexZeroPadAddress(depositor.address), + addressToBytes(depositor.address), depositId, updatedOutputAmount, - hexZeroPadAddress(updatedRecipient), + addressToBytes(updatedRecipient), updatedMessage, invalidSignatureForChain ) @@ -815,8 +814,8 @@ describe("SpokePool Depositor Logic", async function () { .withArgs( updatedOutputAmount, depositId, - hexZeroPadAddressLowercase(depositor.address), - hexZeroPadAddressLowercase(updatedRecipient), + addressToBytes(depositor.address), + addressToBytes(updatedRecipient), updatedMessage, signature ); diff --git a/test/evm/hardhat/SpokePool.ExecuteRootBundle.ts b/test/evm/hardhat/SpokePool.ExecuteRootBundle.ts index 76af268d8..c0326d1ca 100644 --- a/test/evm/hardhat/SpokePool.ExecuteRootBundle.ts +++ b/test/evm/hardhat/SpokePool.ExecuteRootBundle.ts @@ -6,8 +6,7 @@ import { Contract, ethers, BigNumber, - hexZeroPadAddress, - hexZeroPadAddressLowercase, + addressToBytes, } from "../../../utils/utils"; import * as consts from "./constants"; import { spokePoolFixture } from "./fixtures/SpokePool.Fixture"; @@ -22,8 +21,8 @@ async function constructSimpleTree(l2Token: Contract, destinationChainId: number const leaves = buildRelayerRefundLeaves( [destinationChainId, destinationChainId], // Destination chain ID. [consts.amountToReturn, toBN(0)], // amountToReturn. - [hexZeroPadAddress(l2Token.address), hexZeroPadAddress(l2Token.address)], // l2Token. - [[hexZeroPadAddress(relayer.address), hexZeroPadAddress(rando.address)], []], // refundAddresses. + [addressToBytes(l2Token.address), addressToBytes(l2Token.address)], // l2Token. + [[addressToBytes(relayer.address), addressToBytes(rando.address)], []], // refundAddresses. [[consts.amountToRelay, consts.amountToRelay], []] // refundAmounts. ); const leavesRefundAmount = leaves @@ -63,7 +62,7 @@ describe("SpokePool Root Bundle Execution", function () { // Check events. let relayTokensEvents = await spokePool.queryFilter(spokePool.filters.ExecutedRelayerRefundRoot()); - expect(relayTokensEvents[0].args?.l2TokenAddress).to.equal(hexZeroPadAddressLowercase(destErc20.address)); + expect(relayTokensEvents[0].args?.l2TokenAddress).to.equal(addressToBytes(destErc20.address)); expect(relayTokensEvents[0].args?.leafId).to.equal(0); expect(relayTokensEvents[0].args?.chainId).to.equal(destinationChainId); expect(relayTokensEvents[0].args?.amountToReturn).to.equal(consts.amountToReturn); @@ -71,8 +70,8 @@ describe("SpokePool Root Bundle Execution", function () { [consts.amountToRelay, consts.amountToRelay].map((v) => v.toString()) ); expect(relayTokensEvents[0].args?.refundAddresses).to.deep.equal([ - hexZeroPadAddressLowercase(relayer.address), - hexZeroPadAddressLowercase(rando.address), + addressToBytes(relayer.address), + addressToBytes(rando.address), ]); // Should emit TokensBridged event if amountToReturn is positive. @@ -141,8 +140,8 @@ describe("SpokePool Root Bundle Execution", function () { toBN(1), [consts.amountToRelay, consts.amountToRelay, toBN(0)], 0, - hexZeroPadAddress(destErc20.address), - [hexZeroPadAddress(relayer.address), hexZeroPadAddress(rando.address)] + addressToBytes(destErc20.address), + [addressToBytes(relayer.address), addressToBytes(rando.address)] ) ).to.be.revertedWith("InvalidMerkleLeaf"); }); @@ -151,7 +150,7 @@ describe("SpokePool Root Bundle Execution", function () { await expect( spokePool .connect(dataWorker) - .distributeRelayerRefunds(destinationChainId, toBN(1), [], 0, hexZeroPadAddress(destErc20.address), []) + .distributeRelayerRefunds(destinationChainId, toBN(1), [], 0, addressToBytes(destErc20.address), []) ) .to.emit(spokePool, "BridgedToHubPool") .withArgs(toBN(1), destErc20.address); @@ -160,10 +159,10 @@ describe("SpokePool Root Bundle Execution", function () { await expect( spokePool .connect(dataWorker) - .distributeRelayerRefunds(destinationChainId, toBN(1), [], 0, hexZeroPadAddress(destErc20.address), []) + .distributeRelayerRefunds(destinationChainId, toBN(1), [], 0, addressToBytes(destErc20.address), []) ) .to.emit(spokePool, "TokensBridged") - .withArgs(toBN(1), destinationChainId, 0, hexZeroPadAddressLowercase(destErc20.address), dataWorker.address); + .withArgs(toBN(1), destinationChainId, 0, addressToBytes(destErc20.address), dataWorker.address); }); }); describe("amountToReturn = 0", function () { @@ -171,14 +170,14 @@ describe("SpokePool Root Bundle Execution", function () { await expect( spokePool .connect(dataWorker) - .distributeRelayerRefunds(destinationChainId, toBN(0), [], 0, hexZeroPadAddress(destErc20.address), []) + .distributeRelayerRefunds(destinationChainId, toBN(0), [], 0, addressToBytes(destErc20.address), []) ).to.not.emit(spokePool, "BridgedToHubPool"); }); it("does not emit TokensBridged", async function () { await expect( spokePool .connect(dataWorker) - .distributeRelayerRefunds(destinationChainId, toBN(0), [], 0, hexZeroPadAddress(destErc20.address), []) + .distributeRelayerRefunds(destinationChainId, toBN(0), [], 0, addressToBytes(destErc20.address), []) ).to.not.emit(spokePool, "TokensBridged"); }); }); @@ -192,8 +191,8 @@ describe("SpokePool Root Bundle Execution", function () { toBN(1), [consts.amountToRelay, consts.amountToRelay, toBN(0)], 0, - hexZeroPadAddress(destErc20.address), - [hexZeroPadAddress(relayer.address), hexZeroPadAddress(rando.address), hexZeroPadAddress(rando.address)] + addressToBytes(destErc20.address), + [addressToBytes(relayer.address), addressToBytes(rando.address), addressToBytes(rando.address)] ) ).to.changeTokenBalances( destErc20, @@ -212,8 +211,8 @@ describe("SpokePool Root Bundle Execution", function () { toBN(1), [consts.amountToRelay, consts.amountToRelay, toBN(0)], 0, - hexZeroPadAddress(destErc20.address), - [hexZeroPadAddress(relayer.address), hexZeroPadAddress(rando.address), hexZeroPadAddress(rando.address)] + addressToBytes(destErc20.address), + [addressToBytes(relayer.address), addressToBytes(rando.address), addressToBytes(rando.address)] ) ) .to.emit(spokePool, "BridgedToHubPool") diff --git a/test/evm/hardhat/SpokePool.Relay.ts b/test/evm/hardhat/SpokePool.Relay.ts index eca78f472..3e9e7a980 100644 --- a/test/evm/hardhat/SpokePool.Relay.ts +++ b/test/evm/hardhat/SpokePool.Relay.ts @@ -8,8 +8,7 @@ import { randomAddress, createRandomBytes32, BigNumber, - hexZeroPadAddress, - hexZeroPadAddressLowercase, + addressToBytes, bytes32ToAddress, } from "../../../utils/utils"; import { @@ -62,11 +61,11 @@ describe("SpokePool Relayer Logic", async function () { beforeEach(async function () { const fillDeadline = (await spokePool.getCurrentTime()).toNumber() + 1000; relayData = { - depositor: hexZeroPadAddress(depositor.address), - recipient: hexZeroPadAddress(recipient.address), - exclusiveRelayer: hexZeroPadAddress(relayer.address), - inputToken: hexZeroPadAddress(erc20.address), - outputToken: hexZeroPadAddress(destErc20.address), + depositor: addressToBytes(depositor.address), + recipient: addressToBytes(recipient.address), + exclusiveRelayer: addressToBytes(relayer.address), + inputToken: addressToBytes(erc20.address), + outputToken: addressToBytes(destErc20.address), inputAmount: consts.amountToDeposit, outputAmount: consts.amountToDeposit, originChainId: consts.originChainId, @@ -90,7 +89,7 @@ describe("SpokePool Relayer Logic", async function () { await expect( spokePool.connect(relayer).fillRelayV3Internal( relayExecution, - hexZeroPadAddress(relayer.address), + addressToBytes(relayer.address), false // isSlowFill ) ).to.be.revertedWith("ExpiredFillDeadline"); @@ -101,7 +100,7 @@ describe("SpokePool Relayer Logic", async function () { await expect( spokePool.connect(relayer).fillRelayV3Internal( relayExecution, - hexZeroPadAddress(relayer.address), + addressToBytes(relayer.address), false // isSlowFill ) ).to.be.revertedWith("RelayFilled"); @@ -112,14 +111,14 @@ describe("SpokePool Relayer Logic", async function () { await expect( spokePool.connect(relayer).fillRelayV3Internal( relayExecution, - hexZeroPadAddress(relayer.address), + addressToBytes(relayer.address), false // isSlowFill ) ) .to.emit(spokePool, "FilledV3Relay") .withArgs( - hexZeroPadAddressLowercase(relayData.inputToken), - hexZeroPadAddressLowercase(relayData.outputToken), + addressToBytes(relayData.inputToken), + addressToBytes(relayData.outputToken), relayData.inputAmount, relayData.outputAmount, relayExecution.repaymentChainId, @@ -127,13 +126,13 @@ describe("SpokePool Relayer Logic", async function () { relayData.depositId, relayData.fillDeadline, relayData.exclusivityDeadline, - hexZeroPadAddressLowercase(relayData.exclusiveRelayer), - hexZeroPadAddressLowercase(relayer.address), - hexZeroPadAddressLowercase(relayData.depositor), - hexZeroPadAddressLowercase(relayData.recipient), + addressToBytes(relayData.exclusiveRelayer), + addressToBytes(relayer.address), + addressToBytes(relayData.depositor), + addressToBytes(relayData.recipient), relayData.message, [ - hexZeroPadAddressLowercase(relayData.recipient), + addressToBytes(relayData.recipient), relayExecution.updatedMessage, relayExecution.updatedOutputAmount, // Testing that this FillType is not "FastFill" @@ -148,14 +147,14 @@ describe("SpokePool Relayer Logic", async function () { await expect( spokePool.connect(relayer).fillRelayV3Internal( relayExecution, - hexZeroPadAddress(relayer.address), + addressToBytes(relayer.address), true // isSlowFill ) ) .to.emit(spokePool, "FilledV3Relay") .withArgs( - hexZeroPadAddressLowercase(relayData.inputToken), - hexZeroPadAddressLowercase(relayData.outputToken), + addressToBytes(relayData.inputToken), + addressToBytes(relayData.outputToken), relayData.inputAmount, relayData.outputAmount, relayExecution.repaymentChainId, @@ -163,13 +162,13 @@ describe("SpokePool Relayer Logic", async function () { relayData.depositId, relayData.fillDeadline, relayData.exclusivityDeadline, - hexZeroPadAddressLowercase(relayData.exclusiveRelayer), - hexZeroPadAddressLowercase(relayer.address), - hexZeroPadAddressLowercase(relayData.depositor), - hexZeroPadAddressLowercase(relayData.recipient), + addressToBytes(relayData.exclusiveRelayer), + addressToBytes(relayer.address), + addressToBytes(relayData.depositor), + addressToBytes(relayData.recipient), relayData.message, [ - hexZeroPadAddressLowercase(relayData.recipient), + addressToBytes(relayData.recipient), relayExecution.updatedMessage, relayExecution.updatedOutputAmount, // Testing that this FillType is "SlowFill" @@ -183,14 +182,14 @@ describe("SpokePool Relayer Logic", async function () { await expect( spokePool.connect(relayer).fillRelayV3Internal( relayExecution, - hexZeroPadAddressLowercase(relayer.address), + addressToBytes(relayer.address), false // isSlowFill ) ) .to.emit(spokePool, "FilledV3Relay") .withArgs( - hexZeroPadAddressLowercase(relayData.inputToken), - hexZeroPadAddressLowercase(relayData.outputToken), + addressToBytes(relayData.inputToken), + addressToBytes(relayData.outputToken), relayData.inputAmount, relayData.outputAmount, relayExecution.repaymentChainId, @@ -198,13 +197,13 @@ describe("SpokePool Relayer Logic", async function () { relayData.depositId, relayData.fillDeadline, relayData.exclusivityDeadline, - hexZeroPadAddressLowercase(relayData.exclusiveRelayer), - hexZeroPadAddressLowercase(relayer.address), - hexZeroPadAddressLowercase(relayData.depositor), - hexZeroPadAddressLowercase(relayData.recipient), + addressToBytes(relayData.exclusiveRelayer), + addressToBytes(relayer.address), + addressToBytes(relayData.depositor), + addressToBytes(relayData.recipient), relayData.message, [ - hexZeroPadAddressLowercase(relayData.recipient), + addressToBytes(relayData.recipient), relayExecution.updatedMessage, relayExecution.updatedOutputAmount, FillType.FastFill, @@ -216,13 +215,13 @@ describe("SpokePool Relayer Logic", async function () { const _relayData = { ...relayData, // Set recipient == relayer - recipient: hexZeroPadAddress(relayer.address), + recipient: addressToBytes(relayer.address), }; const relayExecution = await getRelayExecutionParams(_relayData, consts.destinationChainId); await expect( spokePool.connect(relayer).fillRelayV3Internal( relayExecution, - hexZeroPadAddress(relayer.address), + addressToBytes(relayer.address), false // isSlowFill ) ).to.not.emit(destErc20, "Transfer"); @@ -234,15 +233,15 @@ describe("SpokePool Relayer Logic", async function () { // Overwrite amount to send to be double the original amount updatedOutputAmount: consts.amountToDeposit.mul(2), // Overwrite recipient to depositor which is not the same as the original recipient - updatedRecipient: hexZeroPadAddress(depositor.address), + updatedRecipient: addressToBytes(depositor.address), }; - expect(_relayExecution.updatedRecipient).to.not.equal(hexZeroPadAddress(relayExecution.updatedRecipient)); + expect(_relayExecution.updatedRecipient).to.not.equal(addressToBytes(relayExecution.updatedRecipient)); expect(_relayExecution.updatedOutputAmount).to.not.equal(relayExecution.updatedOutputAmount); await destErc20.connect(relayer).approve(spokePool.address, _relayExecution.updatedOutputAmount); await expect(() => spokePool.connect(relayer).fillRelayV3Internal( _relayExecution, - hexZeroPadAddress(relayer.address), + addressToBytes(relayer.address), false // isSlowFill ) ).to.changeTokenBalance(destErc20, depositor, consts.amountToDeposit.mul(2)); @@ -250,13 +249,13 @@ describe("SpokePool Relayer Logic", async function () { it("unwraps native token if sending to EOA", async function () { const _relayData = { ...relayData, - outputToken: hexZeroPadAddress(weth.address), + outputToken: addressToBytes(weth.address), }; const relayExecution = await getRelayExecutionParams(_relayData, consts.destinationChainId); await expect(() => spokePool.connect(relayer).fillRelayV3Internal( relayExecution, - hexZeroPadAddress(relayer.address), + addressToBytes(relayer.address), false // isSlowFill ) ).to.changeEtherBalance(recipient, relayExecution.updatedOutputAmount); @@ -264,7 +263,7 @@ describe("SpokePool Relayer Logic", async function () { it("slow fills send native token out of spoke pool balance", async function () { const _relayData = { ...relayData, - outputToken: hexZeroPadAddress(weth.address), + outputToken: addressToBytes(weth.address), }; const relayExecution = await getRelayExecutionParams(_relayData, consts.destinationChainId); await weth.connect(relayer).transfer(spokePool.address, relayExecution.updatedOutputAmount); @@ -272,7 +271,7 @@ describe("SpokePool Relayer Logic", async function () { await expect(() => spokePool.connect(relayer).fillRelayV3Internal( relayExecution, - hexZeroPadAddress(relayer.address), + addressToBytes(relayer.address), true // isSlowFill ) ).to.changeEtherBalance(recipient, relayExecution.updatedOutputAmount); @@ -285,7 +284,7 @@ describe("SpokePool Relayer Logic", async function () { await expect(() => spokePool.connect(relayer).fillRelayV3Internal( relayExecution, - hexZeroPadAddress(relayer.address), + addressToBytes(relayer.address), true // isSlowFill ) ).to.changeTokenBalance(destErc20, spokePool, relayExecution.updatedOutputAmount.mul(-1)); @@ -295,7 +294,7 @@ describe("SpokePool Relayer Logic", async function () { const acrossMessageHandler = await createFake("AcrossMessageHandlerMock"); const _relayData = { ...relayData, - recipient: hexZeroPadAddress(acrossMessageHandler.address), + recipient: addressToBytes(acrossMessageHandler.address), message: "0x1234", }; const relayExecution = await getRelayExecutionParams(_relayData, consts.destinationChainId); @@ -303,7 +302,7 @@ describe("SpokePool Relayer Logic", async function () { // Handler is called with expected params. await spokePool.connect(relayer).fillRelayV3Internal( relayExecution, - hexZeroPadAddress(relayer.address), + addressToBytes(relayer.address), false // isSlowFill ); const test = bytes32ToAddress(_relayData.outputToken); @@ -319,16 +318,14 @@ describe("SpokePool Relayer Logic", async function () { it("fills are not paused", async function () { await spokePool.pauseFills(true); await expect( - spokePool - .connect(relayer) - .fillV3Relay(relayData, consts.repaymentChainId, hexZeroPadAddressLowercase(relayer.address)) + spokePool.connect(relayer).fillV3Relay(relayData, consts.repaymentChainId, addressToBytes(relayer.address)) ).to.be.revertedWith("FillsArePaused"); }); it("reentrancy protected", async function () { const functionCalldata = spokePool.interface.encodeFunctionData("fillV3Relay", [ relayData, consts.repaymentChainId, - hexZeroPadAddressLowercase(relayer.address), + addressToBytes(relayer.address), ]); await expect(spokePool.connect(relayer).callback(functionCalldata)).to.be.revertedWith( "ReentrancyGuard: reentrant call" @@ -338,13 +335,11 @@ describe("SpokePool Relayer Logic", async function () { const _relayData = { ...relayData, // Overwrite exclusive relayer and exclusivity deadline - exclusiveRelayer: hexZeroPadAddress(recipient.address), + exclusiveRelayer: addressToBytes(recipient.address), exclusivityDeadline: relayData.fillDeadline, }; await expect( - spokePool - .connect(relayer) - .fillV3Relay(_relayData, consts.repaymentChainId, hexZeroPadAddressLowercase(relayer.address)) + spokePool.connect(relayer).fillV3Relay(_relayData, consts.repaymentChainId, addressToBytes(relayer.address)) ).to.be.revertedWith("NotExclusiveRelayer"); // Can send it after exclusivity deadline @@ -354,7 +349,7 @@ describe("SpokePool Relayer Logic", async function () { .fillV3Relay( { ..._relayData, exclusivityDeadline: 0 }, consts.repaymentChainId, - hexZeroPadAddressLowercase(relayer.address) + addressToBytes(relayer.address) ) ).to.not.be.reverted; }); @@ -367,9 +362,7 @@ describe("SpokePool Relayer Logic", async function () { // Can send it after exclusivity deadline await expect( - spokePool - .connect(relayer) - .fillV3Relay(_relayData, consts.repaymentChainId, hexZeroPadAddressLowercase(relayer.address)) + spokePool.connect(relayer).fillV3Relay(_relayData, consts.repaymentChainId, addressToBytes(relayer.address)) ).to.not.be.reverted; }); it("can have empty exclusive relayer before exclusivity deadline", async function () { @@ -381,21 +374,17 @@ describe("SpokePool Relayer Logic", async function () { // Can send it before exclusivity deadline if exclusive relayer is empty await expect( - spokePool - .connect(relayer) - .fillV3Relay(_relayData, consts.repaymentChainId, hexZeroPadAddressLowercase(relayer.address)) + spokePool.connect(relayer).fillV3Relay(_relayData, consts.repaymentChainId, addressToBytes(relayer.address)) ).to.not.be.reverted; }); it("calls _fillRelayV3 with expected params", async function () { await expect( - spokePool - .connect(relayer) - .fillV3Relay(relayData, consts.repaymentChainId, hexZeroPadAddressLowercase(relayer.address)) + spokePool.connect(relayer).fillV3Relay(relayData, consts.repaymentChainId, addressToBytes(relayer.address)) ) .to.emit(spokePool, "FilledV3Relay") .withArgs( - hexZeroPadAddressLowercase(relayData.inputToken), - hexZeroPadAddressLowercase(relayData.outputToken), + addressToBytes(relayData.inputToken), + addressToBytes(relayData.outputToken), relayData.inputAmount, relayData.outputAmount, consts.repaymentChainId, // Should be passed-in repayment chain ID @@ -403,13 +392,13 @@ describe("SpokePool Relayer Logic", async function () { relayData.depositId, relayData.fillDeadline, relayData.exclusivityDeadline, - hexZeroPadAddressLowercase(relayData.exclusiveRelayer), - hexZeroPadAddressLowercase(relayer.address), // Should be equal to msg.sender of fillRelayV3 - hexZeroPadAddressLowercase(relayData.depositor), - hexZeroPadAddressLowercase(relayData.recipient), + addressToBytes(relayData.exclusiveRelayer), + addressToBytes(relayer.address), // Should be equal to msg.sender of fillRelayV3 + addressToBytes(relayData.depositor), + addressToBytes(relayData.recipient), relayData.message, [ - hexZeroPadAddressLowercase(relayData.recipient), // updatedRecipient should be equal to recipient + addressToBytes(relayData.recipient), // updatedRecipient should be equal to recipient relayData.message, // updatedMessage should be equal to message relayData.outputAmount, // updatedOutputAmount should be equal to outputAmount // Should be FastFill @@ -430,7 +419,7 @@ describe("SpokePool Relayer Logic", async function () { relayData.depositId, relayData.originChainId, updatedOutputAmount, - hexZeroPadAddress(updatedRecipient), + addressToBytes(updatedRecipient), updatedMessage ); }); @@ -438,7 +427,7 @@ describe("SpokePool Relayer Logic", async function () { const _relayData = { ...relayData, // Overwrite exclusive relayer and exclusivity deadline - exclusiveRelayer: hexZeroPadAddress(recipient.address), + exclusiveRelayer: addressToBytes(recipient.address), exclusivityDeadline: relayData.fillDeadline, }; await expect( @@ -447,9 +436,9 @@ describe("SpokePool Relayer Logic", async function () { .fillV3RelayWithUpdatedDeposit( _relayData, consts.repaymentChainId, - hexZeroPadAddressLowercase(relayer.address), + addressToBytes(relayer.address), updatedOutputAmount, - hexZeroPadAddress(updatedRecipient), + addressToBytes(updatedRecipient), updatedMessage, signature ) @@ -463,9 +452,9 @@ describe("SpokePool Relayer Logic", async function () { exclusivityDeadline: 0, }, consts.repaymentChainId, - hexZeroPadAddressLowercase(relayer.address), + addressToBytes(relayer.address), updatedOutputAmount, - hexZeroPadAddress(updatedRecipient), + addressToBytes(updatedRecipient), updatedMessage, signature ) @@ -483,17 +472,17 @@ describe("SpokePool Relayer Logic", async function () { .fillV3RelayWithUpdatedDeposit( relayData, consts.repaymentChainId, - hexZeroPadAddressLowercase(relayer.address), + addressToBytes(relayer.address), updatedOutputAmount, - hexZeroPadAddress(updatedRecipient), + addressToBytes(updatedRecipient), updatedMessage, signature ) ) .to.emit(spokePool, "FilledV3Relay") .withArgs( - hexZeroPadAddressLowercase(relayData.inputToken), - hexZeroPadAddressLowercase(relayData.outputToken), + addressToBytes(relayData.inputToken), + addressToBytes(relayData.outputToken), relayData.inputAmount, relayData.outputAmount, consts.repaymentChainId, // Should be passed-in repayment chain ID @@ -501,14 +490,14 @@ describe("SpokePool Relayer Logic", async function () { relayData.depositId, relayData.fillDeadline, relayData.exclusivityDeadline, - hexZeroPadAddressLowercase(relayData.exclusiveRelayer), - hexZeroPadAddressLowercase(relayer.address), // Should be equal to msg.sender - hexZeroPadAddressLowercase(relayData.depositor), - hexZeroPadAddressLowercase(relayData.recipient), + addressToBytes(relayData.exclusiveRelayer), + addressToBytes(relayer.address), // Should be equal to msg.sender + addressToBytes(relayData.depositor), + addressToBytes(relayData.recipient), relayData.message, [ // Should use passed-in updated params: - hexZeroPadAddressLowercase(updatedRecipient), + addressToBytes(updatedRecipient), updatedMessage, updatedOutputAmount, // Should be FastFill @@ -526,11 +515,11 @@ describe("SpokePool Relayer Logic", async function () { spokePool .connect(relayer) .fillV3RelayWithUpdatedDeposit( - { ...relayData, depositor: hexZeroPadAddress(relayer.address) }, + { ...relayData, depositor: addressToBytes(relayer.address) }, consts.repaymentChainId, - hexZeroPadAddressLowercase(relayer.address), + addressToBytes(relayer.address), updatedOutputAmount, - hexZeroPadAddress(updatedRecipient), + addressToBytes(updatedRecipient), updatedMessage, signature ) @@ -542,7 +531,7 @@ describe("SpokePool Relayer Logic", async function () { relayData.depositId + 1, relayData.originChainId, updatedOutputAmount, - hexZeroPadAddress(updatedRecipient), + addressToBytes(updatedRecipient), updatedMessage ); await expect( @@ -551,9 +540,9 @@ describe("SpokePool Relayer Logic", async function () { .fillV3RelayWithUpdatedDeposit( relayData, consts.repaymentChainId, - hexZeroPadAddressLowercase(relayer.address), + addressToBytes(relayer.address), updatedOutputAmount, - hexZeroPadAddress(updatedRecipient), + addressToBytes(updatedRecipient), updatedMessage, otherSignature ) @@ -566,9 +555,9 @@ describe("SpokePool Relayer Logic", async function () { .fillV3RelayWithUpdatedDeposit( { ...relayData, originChainId: relayData.originChainId + 1 }, consts.repaymentChainId, - hexZeroPadAddressLowercase(relayer.address), + addressToBytes(relayer.address), updatedOutputAmount, - hexZeroPadAddress(updatedRecipient), + addressToBytes(updatedRecipient), updatedMessage, signature ) @@ -581,9 +570,9 @@ describe("SpokePool Relayer Logic", async function () { .fillV3RelayWithUpdatedDeposit( { ...relayData, depositId: relayData.depositId + 1 }, consts.repaymentChainId, - hexZeroPadAddressLowercase(relayer.address), + addressToBytes(relayer.address), updatedOutputAmount, - hexZeroPadAddress(updatedRecipient), + addressToBytes(updatedRecipient), updatedMessage, signature ) @@ -596,9 +585,9 @@ describe("SpokePool Relayer Logic", async function () { .fillV3RelayWithUpdatedDeposit( relayData, consts.repaymentChainId, - hexZeroPadAddressLowercase(relayer.address), + addressToBytes(relayer.address), updatedOutputAmount.sub(1), - hexZeroPadAddress(updatedRecipient), + addressToBytes(updatedRecipient), updatedMessage, signature ) @@ -611,9 +600,9 @@ describe("SpokePool Relayer Logic", async function () { .fillV3RelayWithUpdatedDeposit( relayData, consts.repaymentChainId, - hexZeroPadAddressLowercase(relayer.address), + addressToBytes(relayer.address), updatedOutputAmount, - hexZeroPadAddress(randomAddress()), + addressToBytes(randomAddress()), updatedMessage, signature ) @@ -626,9 +615,9 @@ describe("SpokePool Relayer Logic", async function () { .fillV3RelayWithUpdatedDeposit( relayData, consts.repaymentChainId, - hexZeroPadAddressLowercase(relayer.address), + addressToBytes(relayer.address), updatedOutputAmount, - hexZeroPadAddress(updatedRecipient), + addressToBytes(updatedRecipient), updatedMessage, createRandomBytes32() ) @@ -642,18 +631,18 @@ describe("SpokePool Relayer Logic", async function () { relayData.depositId, relayData.originChainId, updatedOutputAmount, - hexZeroPadAddress(updatedRecipient), + addressToBytes(updatedRecipient), updatedMessage ); await expect( spokePool .connect(relayer) .fillV3RelayWithUpdatedDeposit( - { ...relayData, depositor: hexZeroPadAddress(erc1271.address) }, + { ...relayData, depositor: addressToBytes(erc1271.address) }, consts.repaymentChainId, - hexZeroPadAddressLowercase(relayer.address), + addressToBytes(relayer.address), updatedOutputAmount, - hexZeroPadAddress(updatedRecipient), + addressToBytes(updatedRecipient), updatedMessage, incorrectSignature ) @@ -662,11 +651,11 @@ describe("SpokePool Relayer Logic", async function () { spokePool .connect(relayer) .fillV3RelayWithUpdatedDeposit( - { ...relayData, depositor: hexZeroPadAddress(erc1271.address) }, + { ...relayData, depositor: addressToBytes(erc1271.address) }, consts.repaymentChainId, - hexZeroPadAddressLowercase(relayer.address), + addressToBytes(relayer.address), updatedOutputAmount, - hexZeroPadAddress(updatedRecipient), + addressToBytes(updatedRecipient), updatedMessage, signature ) @@ -675,16 +664,16 @@ describe("SpokePool Relayer Logic", async function () { it("cannot send updated fill after original fill", async function () { await spokePool .connect(relayer) - .fillV3Relay(relayData, consts.repaymentChainId, hexZeroPadAddressLowercase(relayer.address)); + .fillV3Relay(relayData, consts.repaymentChainId, addressToBytes(relayer.address)); await expect( spokePool .connect(relayer) .fillV3RelayWithUpdatedDeposit( relayData, consts.repaymentChainId, - hexZeroPadAddressLowercase(relayer.address), + addressToBytes(relayer.address), updatedOutputAmount, - hexZeroPadAddress(updatedRecipient), + addressToBytes(updatedRecipient), updatedMessage, signature ) @@ -696,16 +685,14 @@ describe("SpokePool Relayer Logic", async function () { .fillV3RelayWithUpdatedDeposit( relayData, consts.repaymentChainId, - hexZeroPadAddressLowercase(relayer.address), + addressToBytes(relayer.address), updatedOutputAmount, - hexZeroPadAddress(updatedRecipient), + addressToBytes(updatedRecipient), updatedMessage, signature ); await expect( - spokePool - .connect(relayer) - .fillV3Relay(relayData, consts.repaymentChainId, hexZeroPadAddressLowercase(relayer.address)) + spokePool.connect(relayer).fillV3Relay(relayData, consts.repaymentChainId, addressToBytes(relayer.address)) ).to.be.revertedWith("RelayFilled"); }); }); diff --git a/test/evm/hardhat/SpokePool.SlowRelay.ts b/test/evm/hardhat/SpokePool.SlowRelay.ts index cf69e43d8..b1ef48fc0 100644 --- a/test/evm/hardhat/SpokePool.SlowRelay.ts +++ b/test/evm/hardhat/SpokePool.SlowRelay.ts @@ -6,8 +6,7 @@ import { toBN, seedContract, seedWallet, - hexZeroPadAddress, - hexZeroPadAddressLowercase, + addressToBytes, } from "../../../utils/utils"; import { spokePoolFixture, V3RelayData, getV3RelayHash, V3SlowFill, FillType } from "./fixtures/SpokePool.Fixture"; import { buildV3SlowRelayTree } from "./MerkleLib.utils"; @@ -41,11 +40,11 @@ describe("SpokePool Slow Relay Logic", async function () { beforeEach(async function () { const fillDeadline = (await spokePool.getCurrentTime()).toNumber() + 1000; relayData = { - depositor: hexZeroPadAddress(depositor.address), - recipient: hexZeroPadAddress(recipient.address), - exclusiveRelayer: hexZeroPadAddress(relayer.address), - inputToken: hexZeroPadAddress(erc20.address), - outputToken: hexZeroPadAddress(destErc20.address), + depositor: addressToBytes(depositor.address), + recipient: addressToBytes(recipient.address), + exclusiveRelayer: addressToBytes(relayer.address), + inputToken: addressToBytes(erc20.address), + outputToken: addressToBytes(destErc20.address), inputAmount: consts.amountToDeposit, outputAmount: fullRelayAmountPostFees, originChainId: consts.originChainId, @@ -83,9 +82,7 @@ describe("SpokePool Slow Relay Logic", async function () { ); // Can fast fill after: - await spokePool - .connect(relayer) - .fillV3Relay(relayData, consts.repaymentChainId, hexZeroPadAddress(relayer.address)); + await spokePool.connect(relayer).fillV3Relay(relayData, consts.repaymentChainId, addressToBytes(relayer.address)); }); it("cannot request if FillStatus is Filled", async function () { const relayHash = getV3RelayHash(relayData, consts.destinationChainId); @@ -113,11 +110,11 @@ describe("SpokePool Slow Relay Logic", async function () { beforeEach(async function () { const fillDeadline = (await spokePool.getCurrentTime()).toNumber() + 1000; relayData = { - depositor: hexZeroPadAddress(depositor.address), - recipient: hexZeroPadAddress(recipient.address), - exclusiveRelayer: hexZeroPadAddress(relayer.address), - inputToken: hexZeroPadAddress(erc20.address), - outputToken: hexZeroPadAddress(destErc20.address), + depositor: addressToBytes(depositor.address), + recipient: addressToBytes(recipient.address), + exclusiveRelayer: addressToBytes(relayer.address), + inputToken: addressToBytes(erc20.address), + outputToken: addressToBytes(destErc20.address), inputAmount: consts.amountToDeposit, outputAmount: fullRelayAmountPostFees, originChainId: consts.originChainId, @@ -169,7 +166,7 @@ describe("SpokePool Slow Relay Logic", async function () { await expect( spokePool .connect(relayer) - .fillV3Relay(slowRelayLeaf.relayData, consts.repaymentChainId, hexZeroPadAddress(relayer.address)) + .fillV3Relay(slowRelayLeaf.relayData, consts.repaymentChainId, addressToBytes(relayer.address)) ).to.be.revertedWith("RelayFilled"); }); it("cannot be used to double send a fill", async function () { @@ -179,7 +176,7 @@ describe("SpokePool Slow Relay Logic", async function () { // Fill before executing slow fill await spokePool .connect(relayer) - .fillV3Relay(slowRelayLeaf.relayData, consts.repaymentChainId, hexZeroPadAddress(relayer.address)); + .fillV3Relay(slowRelayLeaf.relayData, consts.repaymentChainId, addressToBytes(relayer.address)); await expect( spokePool.connect(relayer).executeV3SlowRelayLeaf( slowRelayLeaf, @@ -291,8 +288,8 @@ describe("SpokePool Slow Relay Logic", async function () { ) .to.emit(spokePool, "FilledV3Relay") .withArgs( - hexZeroPadAddressLowercase(relayData.inputToken), - hexZeroPadAddressLowercase(relayData.outputToken), + addressToBytes(relayData.inputToken), + addressToBytes(relayData.outputToken), relayData.inputAmount, relayData.outputAmount, 0, // Sets repaymentChainId to 0. @@ -300,14 +297,14 @@ describe("SpokePool Slow Relay Logic", async function () { relayData.depositId, relayData.fillDeadline, relayData.exclusivityDeadline, - hexZeroPadAddressLowercase(relayData.exclusiveRelayer), - hexZeroPadAddressLowercase(consts.zeroAddress), // Sets relayer address to 0x0 - hexZeroPadAddressLowercase(relayData.depositor), - hexZeroPadAddressLowercase(relayData.recipient), + addressToBytes(relayData.exclusiveRelayer), + addressToBytes(consts.zeroAddress), // Sets relayer address to 0x0 + addressToBytes(relayData.depositor), + addressToBytes(relayData.recipient), relayData.message, [ // Uses relayData.recipient - hexZeroPadAddressLowercase(relayData.recipient), + addressToBytes(relayData.recipient), // Uses relayData.message relayData.message, // Uses slow fill leaf's updatedOutputAmount diff --git a/test/evm/hardhat/chain-specific-spokepools/Arbitrum_SpokePool.ts b/test/evm/hardhat/chain-specific-spokepools/Arbitrum_SpokePool.ts index 1377aaed5..c76ffe94c 100644 --- a/test/evm/hardhat/chain-specific-spokepools/Arbitrum_SpokePool.ts +++ b/test/evm/hardhat/chain-specific-spokepools/Arbitrum_SpokePool.ts @@ -11,7 +11,7 @@ import { seedContract, avmL1ToL2Alias, createFakeFromABI, - hexZeroPadAddress, + addressToBytes, } from "../../../../utils/utils"; import { hre } from "../../../../utils/utils.hre"; import { hubPoolFixture } from "../fixtures/HubPool.Fixture"; @@ -80,7 +80,7 @@ describe("Arbitrum Spoke Pool", function () { it("Only cross domain owner can enable a route", async function () { await expect(arbitrumSpokePool.setEnableRoute(l2Dai, 1, true)).to.be.reverted; await arbitrumSpokePool.connect(crossDomainAlias).setEnableRoute(l2Dai, 1, true); - expect(await arbitrumSpokePool.enabledDepositRoutes(hexZeroPadAddress(l2Dai), 1)).to.equal(true); + expect(await arbitrumSpokePool.enabledDepositRoutes(addressToBytes(l2Dai), 1)).to.equal(true); }); it("Only cross domain owner can whitelist a token pair", async function () { diff --git a/test/evm/hardhat/chain-specific-spokepools/Ethereum_SpokePool.ts b/test/evm/hardhat/chain-specific-spokepools/Ethereum_SpokePool.ts index 04d800d89..1b5966135 100644 --- a/test/evm/hardhat/chain-specific-spokepools/Ethereum_SpokePool.ts +++ b/test/evm/hardhat/chain-specific-spokepools/Ethereum_SpokePool.ts @@ -6,7 +6,7 @@ import { SignerWithAddress, getContractFactory, seedContract, - hexZeroPadAddress, + addressToBytes, } from "../../../../utils/utils"; import { hre } from "../../../../utils/utils.hre"; import { hubPoolFixture } from "../fixtures/HubPool.Fixture"; @@ -55,7 +55,7 @@ describe("Ethereum Spoke Pool", function () { it("Only owner can enable a route", async function () { await expect(spokePool.connect(rando).setEnableRoute(dai.address, 1, true)).to.be.reverted; await spokePool.connect(owner).setEnableRoute(dai.address, 1, true); - expect(await spokePool.enabledDepositRoutes(hexZeroPadAddress(dai.address), 1)).to.equal(true); + expect(await spokePool.enabledDepositRoutes(addressToBytes(dai.address), 1)).to.equal(true); }); it("Only owner can set the hub pool address", async function () { diff --git a/test/evm/hardhat/chain-specific-spokepools/Optimism_SpokePool.ts b/test/evm/hardhat/chain-specific-spokepools/Optimism_SpokePool.ts index f22525538..460e19a3d 100644 --- a/test/evm/hardhat/chain-specific-spokepools/Optimism_SpokePool.ts +++ b/test/evm/hardhat/chain-specific-spokepools/Optimism_SpokePool.ts @@ -10,7 +10,7 @@ import { getContractFactory, seedContract, createFakeFromABI, - hexZeroPadAddress, + addressToBytes, } from "../../../../utils/utils"; import { CCTPTokenMessengerInterface, CCTPTokenMinterInterface } from "../../../../utils/abis"; import { hre } from "../../../../utils/utils.hre"; @@ -94,7 +94,7 @@ describe("Optimism Spoke Pool", function () { await expect(optimismSpokePool.setEnableRoute(l2Dai, 1, true)).to.be.reverted; crossDomainMessenger.xDomainMessageSender.returns(owner.address); await optimismSpokePool.connect(crossDomainMessenger.wallet).setEnableRoute(l2Dai, 1, true); - expect(await optimismSpokePool.enabledDepositRoutes(hexZeroPadAddress(l2Dai), 1)).to.equal(true); + expect(await optimismSpokePool.enabledDepositRoutes(addressToBytes(l2Dai), 1)).to.equal(true); }); it("Only cross domain owner can set the cross domain admin", async function () { diff --git a/test/evm/hardhat/chain-specific-spokepools/Polygon_SpokePool.ts b/test/evm/hardhat/chain-specific-spokepools/Polygon_SpokePool.ts index df82f23fd..9ef4bff15 100644 --- a/test/evm/hardhat/chain-specific-spokepools/Polygon_SpokePool.ts +++ b/test/evm/hardhat/chain-specific-spokepools/Polygon_SpokePool.ts @@ -20,7 +20,7 @@ import { seedWallet, FakeContract, createFakeFromABI, - hexZeroPadAddress, + addressToBytes, } from "../../../../utils/utils"; import { hre } from "../../../../utils/utils.hre"; import { hubPoolFixture } from "../fixtures/HubPool.Fixture"; @@ -156,7 +156,7 @@ describe("Polygon Spoke Pool", function () { .reverted; await polygonSpokePool.connect(fxChild).processMessageFromRoot(0, owner.address, setEnableRouteData); - expect(await polygonSpokePool.enabledDepositRoutes(hexZeroPadAddress(l2Dai), 1)).to.equal(true); + expect(await polygonSpokePool.enabledDepositRoutes(addressToBytes(l2Dai), 1)).to.equal(true); }); it("Only correct caller can initialize a relayer refund", async function () { @@ -261,7 +261,7 @@ describe("Polygon Spoke Pool", function () { const leaves = buildRelayerRefundLeaves( [l2ChainId, l2ChainId], // Destination chain ID. [amountToReturn, ethers.constants.Zero], // amountToReturn. - [hexZeroPadAddress(dai.address), hexZeroPadAddress(dai.address)], // l2Token. + [addressToBytes(dai.address), addressToBytes(dai.address)], // l2Token. [[], []], // refundAddresses. [[], []] // refundAmounts. ); @@ -289,7 +289,7 @@ describe("Polygon Spoke Pool", function () { const leaves = buildRelayerRefundLeaves( [l2ChainId, l2ChainId], // Destination chain ID. [ethers.constants.Zero, ethers.constants.Zero], // amountToReturn. - [hexZeroPadAddress(dai.address), hexZeroPadAddress(dai.address)], // l2Token. + [addressToBytes(dai.address), addressToBytes(dai.address)], // l2Token. [[], []], // refundAddresses. [[], []] // refundAmounts. ); @@ -321,11 +321,11 @@ describe("Polygon Spoke Pool", function () { ]; const currentTime = (await polygonSpokePool.getCurrentTime()).toNumber(); const relayData: V3RelayData = { - depositor: hexZeroPadAddress(owner.address), - recipient: hexZeroPadAddress(acrossMessageHandler.address), - exclusiveRelayer: hexZeroPadAddress(zeroAddress), - inputToken: hexZeroPadAddress(dai.address), - outputToken: hexZeroPadAddress(dai.address), + depositor: addressToBytes(owner.address), + recipient: addressToBytes(acrossMessageHandler.address), + exclusiveRelayer: addressToBytes(zeroAddress), + inputToken: addressToBytes(dai.address), + outputToken: addressToBytes(dai.address), inputAmount: toWei("1"), outputAmount: toWei("1"), originChainId: originChainId, @@ -338,12 +338,12 @@ describe("Polygon Spoke Pool", function () { polygonSpokePool.interface.encodeFunctionData("fillV3Relay", [ relayData, repaymentChainId, - hexZeroPadAddress(relayer.address), + addressToBytes(relayer.address), ]), polygonSpokePool.interface.encodeFunctionData("fillV3Relay", [ { ...relayData, depositId: 1 }, repaymentChainId, - hexZeroPadAddress(relayer.address), + addressToBytes(relayer.address), ]), ]; const otherData = [polygonSpokePool.interface.encodeFunctionData("wrap", [])]; diff --git a/test/evm/hardhat/fixtures/SpokePool.Fixture.ts b/test/evm/hardhat/fixtures/SpokePool.Fixture.ts index 8ff0b51f6..b4943e218 100644 --- a/test/evm/hardhat/fixtures/SpokePool.Fixture.ts +++ b/test/evm/hardhat/fixtures/SpokePool.Fixture.ts @@ -7,7 +7,7 @@ import { ethers, BigNumber, defaultAbiCoder, - hexZeroPadAddress, + addressToBytes, } from "../../../../utils/utils"; import * as consts from "../constants"; diff --git a/utils/utils.ts b/utils/utils.ts index 61bde6750..c06b0e22a 100644 --- a/utils/utils.ts +++ b/utils/utils.ts @@ -103,9 +103,7 @@ export const createRandomBytes32 = () => ethers.utils.hexlify(ethers.utils.rando export const hexZeroPad = (input: string, length: number) => ethers.utils.hexZeroPad(input, length); -export const hexZeroPadAddress = (input: string) => hexZeroPad(input, 32); - -export const hexZeroPadAddressLowercase = (input: string) => hexZeroPad(input.toLowerCase(), 32); +export const addressToBytes = (input: string) => hexZeroPad(input.toLowerCase(), 32); export const bytes32ToAddress = (input: string) => { if (!/^0x[a-fA-F0-9]{64}$/.test(input)) { From 7a85081bdac279df11fbad65f90b865525131c49 Mon Sep 17 00:00:00 2001 From: Chris Maree Date: Mon, 28 Oct 2024 11:17:25 +0100 Subject: [PATCH 05/29] feat: update spokepool relayer refund to handle blocked transfers (#675) Co-authored-by: Matt Rice --- contracts/SpokePool.sol | 108 +++++++++--- contracts/interfaces/V3SpokePoolInterface.sol | 9 + contracts/test/ExpandedERC20WithBlacklist.sol | 27 +++ storage-layouts/Arbitrum_SpokePool.json | 8 +- storage-layouts/Base_SpokePool.json | 8 +- storage-layouts/Blast_SpokePool.json | 8 +- storage-layouts/Ethereum_SpokePool.json | 8 +- storage-layouts/Linea_SpokePool.json | 8 +- storage-layouts/Mode_SpokePool.json | 8 +- storage-layouts/Optimism_SpokePool.json | 8 +- storage-layouts/Polygon_SpokePool.json | 8 +- storage-layouts/ZkSync_SpokePool.json | 8 +- .../fork/BlacklistedRelayerRecipient.t.sol | 164 ++++++++++++++++++ .../hardhat/SpokePool.ClaimRelayerRefund.ts | 90 ++++++++++ test/evm/hardhat/SpokePool.Deposit.ts | 1 - .../hardhat/SpokePool.ExecuteRootBundle.ts | 39 +++++ .../evm/hardhat/fixtures/SpokePool.Fixture.ts | 3 +- 17 files changed, 480 insertions(+), 33 deletions(-) create mode 100644 contracts/test/ExpandedERC20WithBlacklist.sol create mode 100644 test/evm/foundry/fork/BlacklistedRelayerRecipient.t.sol create mode 100644 test/evm/hardhat/SpokePool.ClaimRelayerRefund.ts diff --git a/contracts/SpokePool.sol b/contracts/SpokePool.sol index d02370a80..fa9835d0d 100644 --- a/contracts/SpokePool.sol +++ b/contracts/SpokePool.sol @@ -102,6 +102,8 @@ abstract contract SpokePool is // to eliminate any chance of collision between pre and post V3 relay hashes. mapping(bytes32 => uint256) public fillStatuses; + mapping(address => mapping(address => uint256)) public relayerRefund; + /************************************************************** * CONSTANT/IMMUTABLE VARIABLES * **************************************************************/ @@ -149,7 +151,7 @@ abstract contract SpokePool is // used as a fillDeadline in deposit(), a soon to be deprecated function that also hardcodes outputToken to // the zero address, which forces the off-chain validator to replace the output token with the equivalent // token for the input token. By using this magic value, off-chain validators do not have to keep - // this event in their lookback window when querying for expired deposts. + // this event in their lookback window when querying for expired deposits. uint32 public constant INFINITE_FILL_DEADLINE = type(uint32).max; /**************************************** * EVENTS * @@ -170,6 +172,7 @@ abstract contract SpokePool is uint32 indexed leafId, bytes32 l2TokenAddress, bytes32[] refundAddresses, + bool deferredRefunds, address caller ); event TokensBridged( @@ -585,7 +588,7 @@ abstract contract SpokePool is if (currentTime - quoteTimestamp > depositQuoteTimeBuffer) revert InvalidQuoteTimestamp(); // fillDeadline is relative to the destination chain. - // Don’t allow fillDeadline to be more than several bundles into the future. + // Don't allow fillDeadline to be more than several bundles into the future. // This limits the maximum required lookback for dataworker and relayer instances. // Also, don't allow fillDeadline to be in the past. This poses a potential UX issue if the destination // chain time keeping and this chain's time keeping are out of sync but is not really a practical hurdle @@ -846,7 +849,7 @@ abstract contract SpokePool is * @param fillDeadline The deadline for the relayer to fill the deposit. After this destination chain timestamp, * the fill will revert on the destination chain. Must be set between [currentTime, currentTime + fillDeadlineBuffer] * where currentTime is block.timestamp on this chain or this transaction will revert. - * @param exclusivityPeriod Added to the current time to set the exclusive reayer deadline, + * @param exclusivityPeriod Added to the current time to set the exclusive relayer deadline, * which is the deadline for the exclusiveRelayer to fill the deposit. After this destination chain timestamp, * anyone can fill the deposit. * @param message The message to send to the recipient on the destination chain if the recipient is a contract. @@ -1062,7 +1065,7 @@ abstract contract SpokePool is * recipient, and/or message. The relayer should only use this function if they can supply a message signed * by the depositor that contains the fill's matching deposit ID along with updated relay parameters. * If the signature can be verified, then this function will emit a FilledV3Event that will be used by - * the system for refund verification purposes. In otherwords, this function is an alternative way to fill a + * the system for refund verification purposes. In other words, this function is an alternative way to fill a * a deposit than fillV3Relay. * @dev Subject to same exclusivity deadline rules as fillV3Relay(). * @param relayData struct containing all the data needed to identify the deposit to be filled. See fillV3Relay(). @@ -1254,7 +1257,7 @@ abstract contract SpokePool is _setClaimedLeaf(rootBundleId, relayerRefundLeaf.leafId); - _distributeRelayerRefunds( + bool deferredRefunds = _distributeRelayerRefunds( relayerRefundLeaf.chainId, relayerRefundLeaf.amountToReturn, relayerRefundLeaf.refundAmounts, @@ -1271,10 +1274,27 @@ abstract contract SpokePool is relayerRefundLeaf.leafId, relayerRefundLeaf.l2TokenAddress, relayerRefundLeaf.refundAddresses, + deferredRefunds, msg.sender ); } + /** + * @notice Enables a relayer to claim outstanding repayments. Should virtually never be used, unless for some reason + * relayer repayment transfer fails for reasons such as token transfer reverts due to blacklisting. In this case, + * the relayer can still call this method and claim the tokens to a new address. + * @param l2TokenAddress Address of the L2 token to claim refunds for. + * @param refundAddress Address to send the refund to. + */ + function claimRelayerRefund(address l2TokenAddress, address refundAddress) public { + uint256 refund = relayerRefund[l2TokenAddress][msg.sender]; + if (refund == 0) revert NoRelayerRefundToClaim(); + relayerRefund[l2TokenAddress][msg.sender] = 0; + IERC20Upgradeable(l2TokenAddress).safeTransfer(refundAddress, refund); + + emit ClaimedRelayerRefund(l2TokenAddress, msg.sender, refundAddress, refund); + } + /************************************** * VIEW FUNCTIONS * **************************************/ @@ -1295,6 +1315,10 @@ abstract contract SpokePool is return block.timestamp; // solhint-disable-line not-rely-on-time } + function getRelayerRefund(address l2TokenAddress, address refundAddress) public view returns (uint256) { + return relayerRefund[l2TokenAddress][refundAddress]; + } + /************************************** * INTERNAL FUNCTIONS * **************************************/ @@ -1373,29 +1397,71 @@ abstract contract SpokePool is uint32 leafId, bytes32 l2TokenAddress, bytes32[] memory refundAddresses - ) internal { - if (refundAddresses.length != refundAmounts.length) revert InvalidMerkleLeaf(); - - address l2TokenAddressParsed = l2TokenAddress.toAddress(); - - // Send each relayer refund address the associated refundAmount for the L2 token address. - // Note: Even if the L2 token is not enabled on this spoke pool, we should still refund relayers. - uint256 length = refundAmounts.length; - for (uint256 i = 0; i < length; ++i) { - uint256 amount = refundAmounts[i]; - if (amount > 0) - IERC20Upgradeable(l2TokenAddressParsed).safeTransfer(refundAddresses[i].toAddress(), amount); + ) internal returns (bool deferredRefunds) { + uint256 numRefunds = refundAmounts.length; + if (refundAddresses.length != numRefunds) revert InvalidMerkleLeaf(); + + if (numRefunds > 0) { + uint256 spokeStartBalance = IERC20Upgradeable(l2TokenAddress.toAddress()).balanceOf(address(this)); + uint256 totalRefundedAmount = 0; // Track the total amount refunded. + + // Send each relayer refund address the associated refundAmount for the L2 token address. + // Note: Even if the L2 token is not enabled on this spoke pool, we should still refund relayers. + for (uint256 i = 0; i < numRefunds; ++i) { + if (refundAmounts[i] > 0) { + totalRefundedAmount += refundAmounts[i]; + + // Only if the total refunded amount exceeds the spoke starting balance, should we revert. This + // ensures that bundles are atomic, if we have sufficient balance to refund all relayers and + // prevents can only re-pay some of the relayers. + if (totalRefundedAmount > spokeStartBalance) revert InsufficientSpokePoolBalanceToExecuteLeaf(); + + bool success = _noRevertTransfer( + l2TokenAddress.toAddress(), + refundAddresses[i].toAddress(), + refundAmounts[i] + ); + + // If the transfer failed then track a deferred transfer for the relayer. Given this function would + // have revered if there was insufficient balance, this will only happen if the transfer call + // reverts. This will only occur if the underlying transfer method on the l2Token reverts due to + // recipient blacklisting or other related modifications to the l2Token.transfer method. + if (!success) { + relayerRefund[l2TokenAddress.toAddress()][refundAddresses[i].toAddress()] += refundAmounts[i]; + deferredRefunds = true; + } + } + } } - // If leaf's amountToReturn is positive, then send L2 --> L1 message to bridge tokens back via // chain-specific bridging method. if (amountToReturn > 0) { - _bridgeTokensToHubPool(amountToReturn, l2TokenAddressParsed); + _bridgeTokensToHubPool(amountToReturn, l2TokenAddress.toAddress()); emit TokensBridged(amountToReturn, _chainId, leafId, l2TokenAddress, msg.sender); } } + // Re-implementation of OZ _callOptionalReturnBool to use private logic. Function executes a transfer and returns a + // bool indicating if the external call was successful, rather than reverting. Original method: + // https://github.com/OpenZeppelin/openzeppelin-contracts/blob/28aed34dc5e025e61ea0390c18cac875bfde1a78/contracts/token/ERC20/utils/SafeERC20.sol#L188 + function _noRevertTransfer( + address token, + address to, + uint256 amount + ) internal returns (bool) { + bool success; + uint256 returnSize; + uint256 returnValue; + bytes memory data = abi.encodeCall(IERC20Upgradeable.transfer, (to, amount)); + assembly { + success := call(gas(), token, 0, add(data, 0x20), mload(data), 0, 0x20) + returnSize := returndatasize() + returnValue := mload(0) + } + return success && (returnSize == 0 ? address(token).code.length > 0 : returnValue == 1); + } + function _setCrossDomainAdmin(address newCrossDomainAdmin) internal { if (newCrossDomainAdmin == address(0)) revert InvalidCrossDomainAdmin(); crossDomainAdmin = newCrossDomainAdmin; @@ -1466,7 +1532,7 @@ abstract contract SpokePool is bytes memory depositorSignature ) internal view virtual { // Note: - // - We don't need to worry about reentrancy from a contract deployed at the depositor address since the method + // - We don't need to worry about re-entrancy from a contract deployed at the depositor address since the method // `SignatureChecker.isValidSignatureNow` is a view method. Re-entrancy can happen, but it cannot affect state. // - EIP-1271 signatures are supported. This means that a signature valid now, may not be valid later and vice-versa. // - For an EIP-1271 signature to work, the depositor contract address must map to a deployed contract on the destination @@ -1621,5 +1687,5 @@ abstract contract SpokePool is // Reserve storage slots for future versions of this base contract to add state variables without // affecting the storage layout of child contracts. Decrement the size of __gap whenever state variables // are added. This is at bottom of contract to make sure it's always at the end of storage. - uint256[999] private __gap; + uint256[998] private __gap; } diff --git a/contracts/interfaces/V3SpokePoolInterface.sol b/contracts/interfaces/V3SpokePoolInterface.sol index 2b01f5322..49e16f876 100644 --- a/contracts/interfaces/V3SpokePoolInterface.sol +++ b/contracts/interfaces/V3SpokePoolInterface.sol @@ -154,6 +154,13 @@ interface V3SpokePoolInterface { bytes message ); + event ClaimedRelayerRefund( + address indexed l2TokenAddress, + address indexed caller, + address indexed refundAddress, + uint256 amount + ); + /************************************** * FUNCTIONS * **************************************/ @@ -242,4 +249,6 @@ interface V3SpokePoolInterface { error InvalidPayoutAdjustmentPct(); error WrongERC7683OrderId(); error LowLevelCallFailed(bytes data); + error InsufficientSpokePoolBalanceToExecuteLeaf(); + error NoRelayerRefundToClaim(); } diff --git a/contracts/test/ExpandedERC20WithBlacklist.sol b/contracts/test/ExpandedERC20WithBlacklist.sol new file mode 100644 index 000000000..607609eb7 --- /dev/null +++ b/contracts/test/ExpandedERC20WithBlacklist.sol @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import "@uma/core/contracts/common/implementation/ExpandedERC20.sol"; + +contract ExpandedERC20WithBlacklist is ExpandedERC20 { + mapping(address => bool) public isBlackListed; + + constructor( + string memory name, + string memory symbol, + uint8 decimals + ) ExpandedERC20(name, symbol, decimals) {} + + function setBlacklistStatus(address account, bool status) external { + isBlackListed[account] = status; + } + + function _beforeTokenTransfer( + address from, + address to, + uint256 amount + ) internal override { + require(!isBlackListed[to], "Recipient is blacklisted"); + super._beforeTokenTransfer(from, to, amount); + } +} diff --git a/storage-layouts/Arbitrum_SpokePool.json b/storage-layouts/Arbitrum_SpokePool.json index 132a22efe..14bd92871 100644 --- a/storage-layouts/Arbitrum_SpokePool.json +++ b/storage-layouts/Arbitrum_SpokePool.json @@ -146,10 +146,16 @@ }, { "contract": "contracts/Arbitrum_SpokePool.sol:Arbitrum_SpokePool", - "label": "__gap", + "label": "relayerRefund", "offset": 0, "slot": "2163" }, + { + "contract": "contracts/Arbitrum_SpokePool.sol:Arbitrum_SpokePool", + "label": "__gap", + "offset": 0, + "slot": "2164" + }, { "contract": "contracts/Arbitrum_SpokePool.sol:Arbitrum_SpokePool", "label": "l2GatewayRouter", diff --git a/storage-layouts/Base_SpokePool.json b/storage-layouts/Base_SpokePool.json index 925a6ab42..80934a3f8 100644 --- a/storage-layouts/Base_SpokePool.json +++ b/storage-layouts/Base_SpokePool.json @@ -146,10 +146,16 @@ }, { "contract": "contracts/Base_SpokePool.sol:Base_SpokePool", - "label": "__gap", + "label": "relayerRefund", "offset": 0, "slot": "2163" }, + { + "contract": "contracts/Base_SpokePool.sol:Base_SpokePool", + "label": "__gap", + "offset": 0, + "slot": "2164" + }, { "contract": "contracts/Base_SpokePool.sol:Base_SpokePool", "label": "l1Gas", diff --git a/storage-layouts/Blast_SpokePool.json b/storage-layouts/Blast_SpokePool.json index e287f83a7..2be5208f9 100644 --- a/storage-layouts/Blast_SpokePool.json +++ b/storage-layouts/Blast_SpokePool.json @@ -146,10 +146,16 @@ }, { "contract": "contracts/Blast_SpokePool.sol:Blast_SpokePool", - "label": "__gap", + "label": "relayerRefund", "offset": 0, "slot": "2163" }, + { + "contract": "contracts/Blast_SpokePool.sol:Blast_SpokePool", + "label": "__gap", + "offset": 0, + "slot": "2164" + }, { "contract": "contracts/Blast_SpokePool.sol:Blast_SpokePool", "label": "l1Gas", diff --git a/storage-layouts/Ethereum_SpokePool.json b/storage-layouts/Ethereum_SpokePool.json index 73fc3f2e2..416c2be2c 100644 --- a/storage-layouts/Ethereum_SpokePool.json +++ b/storage-layouts/Ethereum_SpokePool.json @@ -146,10 +146,16 @@ }, { "contract": "contracts/Ethereum_SpokePool.sol:Ethereum_SpokePool", - "label": "__gap", + "label": "relayerRefund", "offset": 0, "slot": "2163" }, + { + "contract": "contracts/Ethereum_SpokePool.sol:Ethereum_SpokePool", + "label": "__gap", + "offset": 0, + "slot": "2164" + }, { "contract": "contracts/Ethereum_SpokePool.sol:Ethereum_SpokePool", "label": "__gap", diff --git a/storage-layouts/Linea_SpokePool.json b/storage-layouts/Linea_SpokePool.json index 4a14b02c3..9dae90739 100644 --- a/storage-layouts/Linea_SpokePool.json +++ b/storage-layouts/Linea_SpokePool.json @@ -146,10 +146,16 @@ }, { "contract": "contracts/Linea_SpokePool.sol:Linea_SpokePool", - "label": "__gap", + "label": "relayerRefund", "offset": 0, "slot": "2163" }, + { + "contract": "contracts/Linea_SpokePool.sol:Linea_SpokePool", + "label": "__gap", + "offset": 0, + "slot": "2164" + }, { "contract": "contracts/Linea_SpokePool.sol:Linea_SpokePool", "label": "l2MessageService", diff --git a/storage-layouts/Mode_SpokePool.json b/storage-layouts/Mode_SpokePool.json index 55de1b86b..b0daad98f 100644 --- a/storage-layouts/Mode_SpokePool.json +++ b/storage-layouts/Mode_SpokePool.json @@ -146,10 +146,16 @@ }, { "contract": "contracts/Mode_SpokePool.sol:Mode_SpokePool", - "label": "__gap", + "label": "relayerRefund", "offset": 0, "slot": "2163" }, + { + "contract": "contracts/Mode_SpokePool.sol:Mode_SpokePool", + "label": "__gap", + "offset": 0, + "slot": "2164" + }, { "contract": "contracts/Mode_SpokePool.sol:Mode_SpokePool", "label": "l1Gas", diff --git a/storage-layouts/Optimism_SpokePool.json b/storage-layouts/Optimism_SpokePool.json index 8732f142e..ded4492f7 100644 --- a/storage-layouts/Optimism_SpokePool.json +++ b/storage-layouts/Optimism_SpokePool.json @@ -146,10 +146,16 @@ }, { "contract": "contracts/Optimism_SpokePool.sol:Optimism_SpokePool", - "label": "__gap", + "label": "relayerRefund", "offset": 0, "slot": "2163" }, + { + "contract": "contracts/Optimism_SpokePool.sol:Optimism_SpokePool", + "label": "__gap", + "offset": 0, + "slot": "2164" + }, { "contract": "contracts/Optimism_SpokePool.sol:Optimism_SpokePool", "label": "l1Gas", diff --git a/storage-layouts/Polygon_SpokePool.json b/storage-layouts/Polygon_SpokePool.json index 50c271f81..924750f6e 100644 --- a/storage-layouts/Polygon_SpokePool.json +++ b/storage-layouts/Polygon_SpokePool.json @@ -146,10 +146,16 @@ }, { "contract": "contracts/Polygon_SpokePool.sol:Polygon_SpokePool", - "label": "__gap", + "label": "relayerRefund", "offset": 0, "slot": "2163" }, + { + "contract": "contracts/Polygon_SpokePool.sol:Polygon_SpokePool", + "label": "__gap", + "offset": 0, + "slot": "2164" + }, { "contract": "contracts/Polygon_SpokePool.sol:Polygon_SpokePool", "label": "fxChild", diff --git a/storage-layouts/ZkSync_SpokePool.json b/storage-layouts/ZkSync_SpokePool.json index 7d1a0c5ce..d76f701b5 100644 --- a/storage-layouts/ZkSync_SpokePool.json +++ b/storage-layouts/ZkSync_SpokePool.json @@ -146,10 +146,16 @@ }, { "contract": "contracts/ZkSync_SpokePool.sol:ZkSync_SpokePool", - "label": "__gap", + "label": "relayerRefund", "offset": 0, "slot": "2163" }, + { + "contract": "contracts/ZkSync_SpokePool.sol:ZkSync_SpokePool", + "label": "__gap", + "offset": 0, + "slot": "2164" + }, { "contract": "contracts/ZkSync_SpokePool.sol:ZkSync_SpokePool", "label": "l2Eth", diff --git a/test/evm/foundry/fork/BlacklistedRelayerRecipient.t.sol b/test/evm/foundry/fork/BlacklistedRelayerRecipient.t.sol new file mode 100644 index 000000000..a2be448a8 --- /dev/null +++ b/test/evm/foundry/fork/BlacklistedRelayerRecipient.t.sol @@ -0,0 +1,164 @@ +// SPDX-License-Identifier: Unlicense +pragma solidity ^0.8.0; + +import { Test } from "forge-std/Test.sol"; +import { MockSpokePool } from "../../../../contracts/test/MockSpokePool.sol"; + +import "@openzeppelin/contracts-upgradeable/token/ERC20/IERC20Upgradeable.sol"; + +// Define a minimal interface for USDT. Note USDT does NOT return anything after a transfer. +interface IUSDT { + function balanceOf(address account) external view returns (uint256); + + function transfer(address recipient, uint256 amount) external; + + function transferFrom( + address from, + address to, + uint256 value + ) external; + + function addBlackList(address _evilUser) external; + + function getBlackListStatus(address _evilUser) external view returns (bool); +} + +// Define a minimal interface for USDC. Note USDC returns a boolean after a transfer. +interface IUSDC { + function balanceOf(address account) external view returns (uint256); + + function transfer(address recipient, uint256 amount) external returns (bool); + + function transferFrom( + address from, + address to, + uint256 value + ) external returns (bool); + + function blacklist(address _account) external; + + function isBlacklisted(address _account) external view returns (bool); +} + +contract MockSpokePoolTest is Test { + MockSpokePool spokePool; + IUSDT usdt; + IUSDC usdc; + + address largeUSDTAccount = 0x99C9fc46f92E8a1c0deC1b1747d010903E884bE1; + address largeUSDCAccount = 0x37305B1cD40574E4C5Ce33f8e8306Be057fD7341; + uint256 seedAmount = 10_000 * 10**6; + + address recipient1 = address(0x6969691111111420); + address recipient2 = address(0x6969692222222420); + + function setUp() public { + spokePool = new MockSpokePool(address(0x123)); + // Create an instance of USDT & USDCusing its mainnet address + usdt = IUSDT(address(0xdAC17F958D2ee523a2206206994597C13D831ec7)); + usdc = IUSDC(address(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48)); + + // Impersonate a large USDT & USDC holders and send tokens to spokePool contract. + assertTrue(usdt.balanceOf(largeUSDTAccount) > seedAmount, "Large USDT holder has less USDT than expected"); + assertTrue(usdc.balanceOf(largeUSDCAccount) > seedAmount, "Large USDC holder has less USDC than expected"); + + vm.prank(largeUSDTAccount); + usdt.transfer(address(spokePool), seedAmount); + assertEq(usdt.balanceOf(address(spokePool)), seedAmount, "Seed transfer failed"); + + vm.prank(largeUSDCAccount); + usdc.transfer(address(spokePool), seedAmount); + assertEq(usdc.balanceOf(address(spokePool)), seedAmount, "USDC seed transfer failed"); + } + + function testStandardRefundsWorks() public { + // Test USDT + assertEq(usdt.balanceOf(recipient1), 0, "Recipient should start with 0 USDT balance"); + assertEq(usdt.balanceOf(address(spokePool)), seedAmount, "SpokePool should have seed USDT balance"); + + uint256[] memory refundAmounts = new uint256[](1); + refundAmounts[0] = 420 * 10**6; + + bytes32[] memory refundAddresses = new bytes32[](1); + refundAddresses[0] = toBytes32(recipient1); + spokePool.distributeRelayerRefunds(1, 0, refundAmounts, 0, toBytes32(address(usdt)), refundAddresses); + + assertEq(usdt.balanceOf(recipient1), refundAmounts[0], "Recipient should have received refund"); + assertEq(usdt.balanceOf(address(spokePool)), seedAmount - refundAmounts[0], "SpokePool bal should drop"); + + // Test USDC + assertEq(usdc.balanceOf(recipient1), 0, "Recipient should start with 0 USDC balance"); + assertEq(usdc.balanceOf(address(spokePool)), seedAmount, "SpokePool should have seed USDC balance"); + + spokePool.distributeRelayerRefunds(1, 0, refundAmounts, 0, toBytes32(address(usdc)), refundAddresses); + + assertEq(usdc.balanceOf(recipient1), refundAmounts[0], "Recipient should have received refund"); + assertEq(usdc.balanceOf(address(spokePool)), seedAmount - refundAmounts[0], "SpokePool bal should drop"); + } + + function testSomeRecipientsBlacklistedDoesNotBlockTheWholeRefundUsdt() public { + // Note that USDT does NOT block blacklisted recipients, only blacklisted senders. This means that even + // if a recipient is blacklisted the bundle payment should still work to them, even though they then cant + // send the tokens after the fact. + assertEq(usdt.getBlackListStatus(recipient1), false, "Recipient1 should not be blacklisted"); + vm.prank(0xC6CDE7C39eB2f0F0095F41570af89eFC2C1Ea828); // USDT owner. + usdt.addBlackList(recipient1); + assertEq(usdt.getBlackListStatus(recipient1), true, "Recipient1 should be blacklisted"); + + assertEq(usdt.balanceOf(recipient1), 0, "Recipient1 should start with 0 USDT balance"); + assertEq(usdt.balanceOf(recipient2), 0, "Recipient2 should start with 0 USDT balance"); + + uint256[] memory refundAmounts = new uint256[](2); + refundAmounts[0] = 420 * 10**6; + refundAmounts[1] = 69 * 10**6; + + bytes32[] memory refundAddresses = new bytes32[](2); + refundAddresses[0] = toBytes32(recipient1); + refundAddresses[1] = toBytes32(recipient2); + spokePool.distributeRelayerRefunds(1, 0, refundAmounts, 0, toBytes32(address(usdt)), refundAddresses); + + assertEq(usdt.balanceOf(recipient1), refundAmounts[0], "Recipient1 should have received their refund"); + assertEq(usdt.balanceOf(recipient2), refundAmounts[1], "Recipient2 should have received their refund"); + + assertEq(spokePool.getRelayerRefund(address(usdt), recipient1), 0); + assertEq(spokePool.getRelayerRefund(address(usdt), recipient2), 0); + } + + function testSomeRecipientsBlacklistedDoesNotBlockTheWholeRefundUsdc() public { + // USDC blacklist blocks both the sender and recipient. Therefore if we a recipient within a bundle is + // blacklisted, they should be credited for the refund amount that can be claimed later to a new address. + assertEq(usdc.isBlacklisted(recipient1), false, "Recipient1 should not be blacklisted"); + vm.prank(0x10DF6B6fe66dd319B1f82BaB2d054cbb61cdAD2e); // USDC blacklister + usdc.blacklist(recipient1); + assertEq(usdc.isBlacklisted(recipient1), true, "Recipient1 should be blacklisted"); + + assertEq(usdc.balanceOf(recipient1), 0, "Recipient1 should start with 0 USDc balance"); + assertEq(usdc.balanceOf(recipient2), 0, "Recipient2 should start with 0 USDc balance"); + + uint256[] memory refundAmounts = new uint256[](2); + refundAmounts[0] = 420 * 10**6; + refundAmounts[1] = 69 * 10**6; + + bytes32[] memory refundAddresses = new bytes32[](2); + refundAddresses[0] = toBytes32(recipient1); + refundAddresses[1] = toBytes32(recipient2); + spokePool.distributeRelayerRefunds(1, 0, refundAmounts, 0, toBytes32(address(usdc)), refundAddresses); + + assertEq(usdc.balanceOf(recipient1), 0, "Recipient1 should have 0 refund as blacklisted"); + assertEq(usdc.balanceOf(recipient2), refundAmounts[1], "Recipient2 should have received their refund"); + + assertEq(spokePool.getRelayerRefund(address(usdc), recipient1), refundAmounts[0]); + assertEq(spokePool.getRelayerRefund(address(usdc), recipient2), 0); + + // Now, blacklisted recipient should be able to claim refund to a new address. + address newRecipient = address(0x6969693333333420); + vm.prank(recipient1); + spokePool.claimRelayerRefund(address(usdc), newRecipient); + assertEq(usdc.balanceOf(newRecipient), refundAmounts[0], "New recipient should have received relayer2 refund"); + assertEq(spokePool.getRelayerRefund(address(usdt), recipient1), 0); + } + + function toBytes32(address _address) internal pure returns (bytes32) { + return bytes32(uint256(uint160(_address))); + } +} diff --git a/test/evm/hardhat/SpokePool.ClaimRelayerRefund.ts b/test/evm/hardhat/SpokePool.ClaimRelayerRefund.ts new file mode 100644 index 000000000..84826c640 --- /dev/null +++ b/test/evm/hardhat/SpokePool.ClaimRelayerRefund.ts @@ -0,0 +1,90 @@ +import { + SignerWithAddress, + seedContract, + seedWallet, + expect, + Contract, + ethers, + toBN, + addressToBytes, +} from "../../../utils/utils"; +import * as consts from "./constants"; +import { spokePoolFixture } from "./fixtures/SpokePool.Fixture"; + +let spokePool: Contract, destErc20: Contract, weth: Contract; +let deployerWallet: SignerWithAddress, relayer: SignerWithAddress, rando: SignerWithAddress; + +let destinationChainId: number; + +describe("SpokePool with Blacklisted destErc20", function () { + beforeEach(async function () { + [deployerWallet, relayer, rando] = await ethers.getSigners(); + ({ spokePool, destErc20, weth } = await spokePoolFixture()); + + destinationChainId = Number(await spokePool.chainId()); + await seedContract(spokePool, deployerWallet, [destErc20], weth, consts.amountHeldByPool); + }); + + it("Blacklist destErc20 operates as expected", async function () { + // Transfer tokens to relayer before blacklisting works as expected. + await seedWallet(deployerWallet, [destErc20], weth, consts.amountToRelay); + await destErc20.connect(deployerWallet).transfer(relayer.address, consts.amountToRelay); + expect(await destErc20.balanceOf(relayer.address)).to.equal(consts.amountToRelay); + + await destErc20.setBlacklistStatus(relayer.address, true); // Blacklist the relayer + + // Attempt to transfer tokens to the blacklisted relayer + await expect(destErc20.connect(deployerWallet).transfer(relayer.address, consts.amountToRelay)).to.be.revertedWith( + "Recipient is blacklisted" + ); + }); + + it("Executes repayments and handles blacklisted addresses", async function () { + // No starting relayer liability. + expect(await spokePool.getRelayerRefund(destErc20.address, relayer.address)).to.equal(toBN(0)); + expect(await destErc20.balanceOf(rando.address)).to.equal(toBN(0)); + expect(await destErc20.balanceOf(relayer.address)).to.equal(toBN(0)); + // Blacklist the relayer + await destErc20.setBlacklistStatus(relayer.address, true); + + // Distribute relayer refunds. some refunds go to blacklisted address and some go to non-blacklisted address. + + await spokePool + .connect(deployerWallet) + .distributeRelayerRefunds( + destinationChainId, + consts.amountToReturn, + [consts.amountToRelay, consts.amountToRelay], + 0, + addressToBytes(destErc20.address), + [addressToBytes(relayer.address), addressToBytes(rando.address)] + ); + + // Ensure relayerRepaymentLiability is incremented + expect(await spokePool.getRelayerRefund(destErc20.address, relayer.address)).to.equal(consts.amountToRelay); + expect(await destErc20.balanceOf(rando.address)).to.equal(consts.amountToRelay); + expect(await destErc20.balanceOf(relayer.address)).to.equal(toBN(0)); + }); + it("Relayer with failed repayment can claim their refund", async function () { + await destErc20.setBlacklistStatus(relayer.address, true); + + await spokePool + .connect(deployerWallet) + .distributeRelayerRefunds( + destinationChainId, + consts.amountToReturn, + [consts.amountToRelay], + 0, + addressToBytes(destErc20.address), + [addressToBytes(relayer.address)] + ); + + await expect(spokePool.connect(relayer).claimRelayerRefund(destErc20.address, relayer.address)).to.be.revertedWith( + "Recipient is blacklisted" + ); + + expect(await destErc20.balanceOf(rando.address)).to.equal(toBN(0)); + await spokePool.connect(relayer).claimRelayerRefund(destErc20.address, rando.address); + expect(await destErc20.balanceOf(rando.address)).to.equal(consts.amountToRelay); + }); +}); diff --git a/test/evm/hardhat/SpokePool.Deposit.ts b/test/evm/hardhat/SpokePool.Deposit.ts index 135744d01..82d758eec 100644 --- a/test/evm/hardhat/SpokePool.Deposit.ts +++ b/test/evm/hardhat/SpokePool.Deposit.ts @@ -27,7 +27,6 @@ import { amountReceived, MAX_UINT32, originChainId, - zeroAddress, } from "./constants"; const { AddressZero: ZERO_ADDRESS } = ethers.constants; diff --git a/test/evm/hardhat/SpokePool.ExecuteRootBundle.ts b/test/evm/hardhat/SpokePool.ExecuteRootBundle.ts index c0326d1ca..76d85bca4 100644 --- a/test/evm/hardhat/SpokePool.ExecuteRootBundle.ts +++ b/test/evm/hardhat/SpokePool.ExecuteRootBundle.ts @@ -73,6 +73,7 @@ describe("SpokePool Root Bundle Execution", function () { addressToBytes(relayer.address), addressToBytes(rando.address), ]); + expect(relayTokensEvents[0].args?.deferredRefunds).to.equal(false); // Should emit TokensBridged event if amountToReturn is positive. let tokensBridgedEvents = await spokePool.queryFilter(spokePool.filters.TokensBridged()); @@ -129,6 +130,28 @@ describe("SpokePool Root Bundle Execution", function () { await expect(spokePool.connect(dataWorker).executeRelayerRefundLeaf(0, leaves[0], tree.getHexProof(leaves[0]))).to .be.reverted; }); + it("Execution correctly logs deferred refunds", async function () { + const { leaves, tree } = await constructSimpleTree(destErc20, destinationChainId); + + // Store new tree. + await spokePool.connect(dataWorker).relayRootBundle( + tree.getHexRoot(), // relayer refund root. Generated from the merkle tree constructed before. + consts.mockSlowRelayRoot + ); + + // Blacklist the relayer EOA to prevent it from receiving refunds. The execution should still succeed, though. + await destErc20.setBlacklistStatus(relayer.address, true); + await spokePool.connect(dataWorker).executeRelayerRefundLeaf(0, leaves[0], tree.getHexProof(leaves[0])); + + // Only the non-blacklisted recipient should receive their refund. + expect(await destErc20.balanceOf(spokePool.address)).to.equal(consts.amountHeldByPool.sub(consts.amountToRelay)); + expect(await destErc20.balanceOf(relayer.address)).to.equal(toBN(0)); + expect(await destErc20.balanceOf(rando.address)).to.equal(consts.amountToRelay); + + // Check event that tracks deferred refunds. + let relayTokensEvents = await spokePool.queryFilter(spokePool.filters.ExecutedRelayerRefundRoot()); + expect(relayTokensEvents[0].args?.deferredRefunds).to.equal(true); + }); describe("_distributeRelayerRefunds", function () { it("refund address length mismatch", async function () { @@ -219,5 +242,21 @@ describe("SpokePool Root Bundle Execution", function () { .withArgs(toBN(1), destErc20.address); }); }); + describe("Total refundAmounts > spokePool's balance", function () { + it("Reverts and emits log", async function () { + await expect( + spokePool.connect(dataWorker).distributeRelayerRefunds( + destinationChainId, + toBN(1), + [consts.amountHeldByPool, consts.amountToRelay], // spoke has only amountHeldByPool. + 0, + addressToBytes(destErc20.address), + [addressToBytes(relayer.address), addressToBytes(rando.address)] + ) + ).to.be.revertedWith("InsufficientSpokePoolBalanceToExecuteLeaf"); + + expect((await destErc20.queryFilter(destErc20.filters.Transfer(spokePool.address))).length).to.equal(0); + }); + }); }); }); diff --git a/test/evm/hardhat/fixtures/SpokePool.Fixture.ts b/test/evm/hardhat/fixtures/SpokePool.Fixture.ts index b4943e218..a87f6dd66 100644 --- a/test/evm/hardhat/fixtures/SpokePool.Fixture.ts +++ b/test/evm/hardhat/fixtures/SpokePool.Fixture.ts @@ -7,7 +7,6 @@ import { ethers, BigNumber, defaultAbiCoder, - addressToBytes, } from "../../../../utils/utils"; import * as consts from "../constants"; @@ -41,7 +40,7 @@ export async function deploySpokePool( ).deploy("Unwhitelisted", "UNWHITELISTED", 18); await unwhitelistedErc20.addMember(consts.TokenRolesEnum.MINTER, deployerWallet.address); const destErc20 = await ( - await getContractFactory("ExpandedERC20", deployerWallet) + await getContractFactory("ExpandedERC20WithBlacklist", deployerWallet) ).deploy("L2 USD Coin", "L2 USDC", 18); await destErc20.addMember(consts.TokenRolesEnum.MINTER, deployerWallet.address); From 3e7cb6cf505fbfd9435ea320e7215215adbe3145 Mon Sep 17 00:00:00 2001 From: chrismaree Date: Fri, 1 Nov 2024 12:49:44 +0100 Subject: [PATCH 06/29] WIP Signed-off-by: chrismaree --- test/evm/hardhat/SpokePool.Relay.ts | 22 +++++++++++----------- test/evm/hardhat/SpokePool.SlowRelay.ts | 5 ++++- 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/test/evm/hardhat/SpokePool.Relay.ts b/test/evm/hardhat/SpokePool.Relay.ts index 774de7109..cb5e8204c 100644 --- a/test/evm/hardhat/SpokePool.Relay.ts +++ b/test/evm/hardhat/SpokePool.Relay.ts @@ -427,17 +427,17 @@ describe("SpokePool Relayer Logic", async function () { // Clock drift between spokes can mean exclusivityDeadline is in future even when no exclusivity was applied. await spokePool.setCurrentTime(relayData.exclusivityDeadline - 1); await expect( - spokePool.connect(relayer).fillV3RelayWithUpdatedDeposit( - { - ...relayData, - exclusiveRelayer: consts.zeroAddress, - }, - consts.repaymentChainId, - updatedOutputAmount, - updatedRecipient, - updatedMessage, - signature - ) + spokePool + .connect(relayer) + .fillV3RelayWithUpdatedDeposit( + { ...relayData, exclusiveRelayer: addressToBytes(consts.zeroAddress) }, + consts.repaymentChainId, + addressToBytes(relayer.address), + updatedOutputAmount, + addressToBytes(updatedRecipient), + updatedMessage, + signature + ) ).to.emit(spokePool, "FilledV3Relay"); }); it("must be exclusive relayer before exclusivity deadline", async function () { diff --git a/test/evm/hardhat/SpokePool.SlowRelay.ts b/test/evm/hardhat/SpokePool.SlowRelay.ts index 70c14810d..b82cd0956 100644 --- a/test/evm/hardhat/SpokePool.SlowRelay.ts +++ b/test/evm/hardhat/SpokePool.SlowRelay.ts @@ -64,7 +64,10 @@ describe("SpokePool Slow Relay Logic", async function () { // Clock drift between spokes can mean exclusivityDeadline is in future even when no exclusivity was applied. await spokePool.setCurrentTime(relayData.exclusivityDeadline - 1); await expect( - spokePool.connect(relayer).requestV3SlowFill({ ...relayData, exclusiveRelayer: consts.zeroAddress }) + spokePool.connect(relayer).requestV3SlowFill({ + ...relayData, + exclusiveRelayer: addressToBytes(consts.zeroAddress), + }) ).to.emit(spokePool, "RequestedV3SlowFill"); }); it("during exclusivity deadline", async function () { From 231b5097bed59f17068b4f6fde916f940b423571 Mon Sep 17 00:00:00 2001 From: Pablo Maldonado Date: Thu, 7 Nov 2024 14:12:22 +0000 Subject: [PATCH 07/29] fix(evm): merkle tree tests bytes32 Signed-off-by: Pablo Maldonado --- .../hardhat/SpokePool.ExecuteRootBundle.ts | 8 +++--- test/svm/utils.ts | 27 ++++++++++++------- utils/utils.ts | 2 ++ 3 files changed, 23 insertions(+), 14 deletions(-) diff --git a/test/evm/hardhat/SpokePool.ExecuteRootBundle.ts b/test/evm/hardhat/SpokePool.ExecuteRootBundle.ts index ae13c31d3..4e35aa9e6 100644 --- a/test/evm/hardhat/SpokePool.ExecuteRootBundle.ts +++ b/test/evm/hardhat/SpokePool.ExecuteRootBundle.ts @@ -98,8 +98,8 @@ describe("SpokePool Root Bundle Execution", function () { totalSolanaDistributions: evmDistributions, mixLeaves: true, chainId: destinationChainId, - evmTokenAddress: destErc20.address, - evmRelayers: [relayer.address, rando.address], + evmTokenAddress: addressToBytes(destErc20.address), + evmRelayers: [addressToBytes(relayer.address), addressToBytes(rando.address)], evmRefundAmounts: [consts.amountToRelay.div(evmDistributions), consts.amountToRelay.div(evmDistributions)], }); @@ -134,8 +134,8 @@ describe("SpokePool Root Bundle Execution", function () { totalSolanaDistributions: evmDistributions, mixLeaves: false, chainId: destinationChainId, - evmTokenAddress: destErc20.address, - evmRelayers: [relayer.address, rando.address], + evmTokenAddress: addressToBytes(destErc20.address), + evmRelayers: [addressToBytes(relayer.address), addressToBytes(rando.address)], evmRefundAmounts: [consts.amountToRelay.div(evmDistributions), consts.amountToRelay.div(evmDistributions)], }); diff --git a/test/svm/utils.ts b/test/svm/utils.ts index bdf9827a5..210e940e7 100644 --- a/test/svm/utils.ts +++ b/test/svm/utils.ts @@ -1,21 +1,20 @@ import { BN, Program } from "@coral-xyz/anchor"; import { Keypair, PublicKey } from "@solana/web3.js"; -import { BigNumber, ethers } from "ethers"; import * as crypto from "crypto"; +import { BigNumber, ethers } from "ethers"; import { SvmSpoke } from "../../target/types/svm_spoke"; +import { MerkleTree } from "@uma/common"; import { - readEvents, - readProgramEvents, calculateRelayHashUint8Array, findProgramAddress, LargeAccountsCoder, + readEvents, + readProgramEvents, } from "../../src/SvmUtils"; -import { MerkleTree } from "@uma/common"; -import { getParamType, keccak256 } from "../../test-utils"; -import { ParamType } from "ethers/lib/utils"; +import { addressToBytes, isBytes32 } from "../../test-utils"; -export { readEvents, readProgramEvents, calculateRelayHashUint8Array, findProgramAddress }; +export { calculateRelayHashUint8Array, findProgramAddress, readEvents, readProgramEvents }; export async function printLogs(connection: any, program: any, tx: any) { const latestBlockHash = await connection.getLatestBlockhash(); @@ -102,6 +101,14 @@ export function buildRelayerRefundMerkleTree({ }): { relayerRefundLeaves: RelayerRefundLeafType[]; merkleTree: MerkleTree } { const relayerRefundLeaves: RelayerRefundLeafType[] = []; + if (evmTokenAddress && !isBytes32(evmTokenAddress)) { + throw new Error("EVM token address must be a bytes32 address"); + } + + if (evmRelayers && evmRelayers.some((address) => !isBytes32(address))) { + throw new Error("EVM relayers must be bytes32 addresses"); + } + const createSolanaLeaf = (index: number) => ({ isSolana: true, leafId: new BN(index), @@ -118,8 +125,8 @@ export function buildRelayerRefundMerkleTree({ leafId: BigNumber.from(index), chainId: BigNumber.from(chainId), amountToReturn: BigNumber.from(0), - l2TokenAddress: evmTokenAddress ?? randomAddress(), - refundAddresses: evmRelayers || [randomAddress(), randomAddress()], + l2TokenAddress: evmTokenAddress ?? addressToBytes(randomAddress()), + refundAddresses: evmRelayers || [addressToBytes(randomAddress()), addressToBytes(randomAddress())], refundAmounts: evmRefundAmounts || [BigNumber.from(randomBigInt()), BigNumber.from(randomBigInt())], } as RelayerRefundLeaf); @@ -179,7 +186,7 @@ export const relayerRefundHashFn = (input: RelayerRefundLeaf | RelayerRefundLeaf const abiCoder = new ethers.utils.AbiCoder(); const encodedData = abiCoder.encode( [ - "tuple( uint256 amountToReturn, uint256 chainId, uint256[] refundAmounts, uint256 leafId, address l2TokenAddress, address[] refundAddresses)", + "tuple( uint256 amountToReturn, uint256 chainId, uint256[] refundAmounts, uint256 leafId, bytes32 l2TokenAddress, bytes32[] refundAddresses)", ], [ { diff --git a/utils/utils.ts b/utils/utils.ts index c06b0e22a..9aceac79e 100644 --- a/utils/utils.ts +++ b/utils/utils.ts @@ -112,6 +112,8 @@ export const bytes32ToAddress = (input: string) => { return ethers.utils.getAddress("0x" + input.slice(26)); }; +export const isBytes32 = (input: string) => /^0x[0-9a-fA-F]{64}$/.test(input); + export async function seedWallet( walletToFund: Signer, tokens: Contract[], From 38be8e0d707b1559daff3343dd5213c8ff223e79 Mon Sep 17 00:00:00 2001 From: chrismaree Date: Sat, 9 Nov 2024 17:36:17 +0700 Subject: [PATCH 08/29] WIP Signed-off-by: chrismaree --- contracts/SpokePool.sol | 18 ++++++++++-------- contracts/interfaces/V3SpokePoolInterface.sol | 8 ++++---- .../hardhat/SpokePool.ClaimRelayerRefund.ts | 18 ++++++++++++------ 3 files changed, 26 insertions(+), 18 deletions(-) diff --git a/contracts/SpokePool.sol b/contracts/SpokePool.sol index a52d58844..6cf14f38a 100644 --- a/contracts/SpokePool.sol +++ b/contracts/SpokePool.sol @@ -102,7 +102,9 @@ abstract contract SpokePool is // to eliminate any chance of collision between pre and post V3 relay hashes. mapping(bytes32 => uint256) public fillStatuses; - mapping(address => mapping(address => uint256)) public relayerRefund; + // Mapping of L2TokenAddress to relayer to outstanding refund amount. Used when a relayer repayment fails for some + // reason (eg blacklist) to track their outstanding liability, thereby letting them claim it later. + mapping(bytes32 => mapping(bytes32 => uint256)) public relayerRefund; /************************************************************** * CONSTANT/IMMUTABLE VARIABLES * @@ -1228,13 +1230,13 @@ abstract contract SpokePool is * @param l2TokenAddress Address of the L2 token to claim refunds for. * @param refundAddress Address to send the refund to. */ - function claimRelayerRefund(address l2TokenAddress, address refundAddress) public { - uint256 refund = relayerRefund[l2TokenAddress][msg.sender]; + function claimRelayerRefund(bytes32 l2TokenAddress, bytes32 refundAddress) public { + uint256 refund = relayerRefund[l2TokenAddress][msg.sender.toBytes32()]; if (refund == 0) revert NoRelayerRefundToClaim(); - relayerRefund[l2TokenAddress][msg.sender] = 0; - IERC20Upgradeable(l2TokenAddress).safeTransfer(refundAddress, refund); + relayerRefund[l2TokenAddress][msg.sender.toBytes32()] = 0; + IERC20Upgradeable(l2TokenAddress.toAddress()).safeTransfer(refundAddress.toAddress(), refund); - emit ClaimedRelayerRefund(l2TokenAddress, msg.sender, refundAddress, refund); + emit ClaimedRelayerRefund(l2TokenAddress, refundAddress, refund, msg.sender); } /************************************** @@ -1257,7 +1259,7 @@ abstract contract SpokePool is return block.timestamp; // solhint-disable-line not-rely-on-time } - function getRelayerRefund(address l2TokenAddress, address refundAddress) public view returns (uint256) { + function getRelayerRefund(bytes32 l2TokenAddress, bytes32 refundAddress) public view returns (uint256) { return relayerRefund[l2TokenAddress][refundAddress]; } @@ -1369,7 +1371,7 @@ abstract contract SpokePool is // reverts. This will only occur if the underlying transfer method on the l2Token reverts due to // recipient blacklisting or other related modifications to the l2Token.transfer method. if (!success) { - relayerRefund[l2TokenAddress.toAddress()][refundAddresses[i].toAddress()] += refundAmounts[i]; + relayerRefund[l2TokenAddress][refundAddresses[i]] += refundAmounts[i]; deferredRefunds = true; } } diff --git a/contracts/interfaces/V3SpokePoolInterface.sol b/contracts/interfaces/V3SpokePoolInterface.sol index d1f9f8882..ec031f685 100644 --- a/contracts/interfaces/V3SpokePoolInterface.sol +++ b/contracts/interfaces/V3SpokePoolInterface.sol @@ -155,10 +155,10 @@ interface V3SpokePoolInterface { ); event ClaimedRelayerRefund( - address indexed l2TokenAddress, - address indexed caller, - address indexed refundAddress, - uint256 amount + bytes32 indexed l2TokenAddress, + bytes32 indexed refundAddress, + uint256 amount, + address indexed caller ); /************************************** diff --git a/test/evm/hardhat/SpokePool.ClaimRelayerRefund.ts b/test/evm/hardhat/SpokePool.ClaimRelayerRefund.ts index 84826c640..3248d34fd 100644 --- a/test/evm/hardhat/SpokePool.ClaimRelayerRefund.ts +++ b/test/evm/hardhat/SpokePool.ClaimRelayerRefund.ts @@ -41,7 +41,9 @@ describe("SpokePool with Blacklisted destErc20", function () { it("Executes repayments and handles blacklisted addresses", async function () { // No starting relayer liability. - expect(await spokePool.getRelayerRefund(destErc20.address, relayer.address)).to.equal(toBN(0)); + expect( + await spokePool.getRelayerRefund(addressToBytes(destErc20.address), addressToBytes(relayer.address)) + ).to.equal(toBN(0)); expect(await destErc20.balanceOf(rando.address)).to.equal(toBN(0)); expect(await destErc20.balanceOf(relayer.address)).to.equal(toBN(0)); // Blacklist the relayer @@ -61,7 +63,9 @@ describe("SpokePool with Blacklisted destErc20", function () { ); // Ensure relayerRepaymentLiability is incremented - expect(await spokePool.getRelayerRefund(destErc20.address, relayer.address)).to.equal(consts.amountToRelay); + expect( + await spokePool.getRelayerRefund(addressToBytes(destErc20.address), addressToBytes(relayer.address)) + ).to.equal(consts.amountToRelay); expect(await destErc20.balanceOf(rando.address)).to.equal(consts.amountToRelay); expect(await destErc20.balanceOf(relayer.address)).to.equal(toBN(0)); }); @@ -79,12 +83,14 @@ describe("SpokePool with Blacklisted destErc20", function () { [addressToBytes(relayer.address)] ); - await expect(spokePool.connect(relayer).claimRelayerRefund(destErc20.address, relayer.address)).to.be.revertedWith( - "Recipient is blacklisted" - ); + await expect( + spokePool.connect(relayer).claimRelayerRefund(addressToBytes(destErc20.address), addressToBytes(relayer.address)) + ).to.be.revertedWith("Recipient is blacklisted"); expect(await destErc20.balanceOf(rando.address)).to.equal(toBN(0)); - await spokePool.connect(relayer).claimRelayerRefund(destErc20.address, rando.address); + await spokePool + .connect(relayer) + .claimRelayerRefund(addressToBytes(destErc20.address), addressToBytes(rando.address)); expect(await destErc20.balanceOf(rando.address)).to.equal(consts.amountToRelay); }); }); From b223f6c95e6c217a8695ca368fd1ffefabca4833 Mon Sep 17 00:00:00 2001 From: Pablo Maldonado Date: Tue, 19 Nov 2024 17:22:56 +0000 Subject: [PATCH 09/29] feat(svm): svm-dev fixes from review (#727) * refactor(svm): reuse bytes32 to address lib in svm adapter Signed-off-by: Pablo Maldonado * feat: custom errors Signed-off-by: Pablo Maldonado * feat: fix test Signed-off-by: Pablo Maldonado --------- Signed-off-by: Pablo Maldonado --- contracts/chain-adapters/Solana_Adapter.sol | 25 ++++++++----------- contracts/libraries/AddressConverters.sol | 13 +++++++++- .../hardhat/chain-adapters/Solana_Adapter.ts | 2 +- 3 files changed, 24 insertions(+), 16 deletions(-) diff --git a/contracts/chain-adapters/Solana_Adapter.sol b/contracts/chain-adapters/Solana_Adapter.sol index 8708889e7..429500e83 100644 --- a/contracts/chain-adapters/Solana_Adapter.sol +++ b/contracts/chain-adapters/Solana_Adapter.sol @@ -5,6 +5,7 @@ import { IMessageTransmitter, ITokenMessenger } from "../external/interfaces/CCT import { SpokePoolInterface } from "../interfaces/SpokePoolInterface.sol"; import { AdapterInterface } from "./interfaces/AdapterInterface.sol"; import { CircleCCTPAdapter, CircleDomainIds } from "../libraries/CircleCCTPAdapter.sol"; +import { Bytes32ToAddress } from "../libraries/AddressConverters.sol"; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; @@ -19,6 +20,14 @@ import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; // solhint-disable-next-line contract-name-camelcase contract Solana_Adapter is AdapterInterface, CircleCCTPAdapter { + /** + * @notice We use Bytes32ToAddress library to map a Solana address to an Ethereum address representation. + * @dev The Ethereum address is derived from the Solana address by truncating it to its lowest 20 bytes. This same + * conversion must be done by the HubPool owner when adding Solana spoke pool and setting the corresponding pool + * rebalance and deposit routes. + */ + using Bytes32ToAddress for bytes32; + /** * @notice The official Circle CCTP MessageTransmitter contract endpoint. * @dev Posted officially here: https://developers.circle.com/stablecoins/docs/evm-smart-contracts @@ -84,10 +93,10 @@ contract Solana_Adapter is AdapterInterface, CircleCCTPAdapter { cctpMessageTransmitter = _cctpMessageTransmitter; SOLANA_SPOKE_POOL_BYTES32 = solanaSpokePool; - SOLANA_SPOKE_POOL_ADDRESS = _trimSolanaAddress(solanaSpokePool); + SOLANA_SPOKE_POOL_ADDRESS = solanaSpokePool.toAddressUnchecked(); SOLANA_USDC_BYTES32 = solanaUsdc; - SOLANA_USDC_ADDRESS = _trimSolanaAddress(solanaUsdc); + SOLANA_USDC_ADDRESS = solanaUsdc.toAddressUnchecked(); SOLANA_SPOKE_POOL_USDC_VAULT = solanaSpokePoolUsdcVault; } @@ -151,18 +160,6 @@ contract Solana_Adapter is AdapterInterface, CircleCCTPAdapter { emit TokensRelayed(l1Token, l2Token, amount, to); } - /** - * @notice Helper to map a Solana address to an Ethereum address representation. - * @dev The Ethereum address is derived from the Solana address by truncating it to its lowest 20 bytes. This same - * conversion must be done by the HubPool owner when adding Solana spoke pool and setting the corresponding pool - * rebalance and deposit routes. - * @param solanaAddress Solana address (Base58 decoded to bytes32) to map to its Ethereum address representation. - * @return Ethereum address representation of the Solana address. - */ - function _trimSolanaAddress(bytes32 solanaAddress) internal pure returns (address) { - return address(uint160(uint256(solanaAddress))); - } - /** * @notice Translates a message to enable/disable a route on Solana spoke pool. * @param message Message to translate, expecting setEnableRoute(address,uint256,bool). diff --git a/contracts/libraries/AddressConverters.sol b/contracts/libraries/AddressConverters.sol index 888629e17..624fd9aa3 100644 --- a/contracts/libraries/AddressConverters.sol +++ b/contracts/libraries/AddressConverters.sol @@ -2,8 +2,19 @@ pragma solidity ^0.8.0; library Bytes32ToAddress { + /************************************** + * ERRORS * + **************************************/ + error InvalidBytes32(); + function toAddress(bytes32 _bytes32) internal pure returns (address) { - require(uint256(_bytes32) >> 192 == 0, "Invalid bytes32: highest 12 bytes must be 0"); + if (uint256(_bytes32) >> 192 != 0) { + revert InvalidBytes32(); + } + return address(uint160(uint256(_bytes32))); + } + + function toAddressUnchecked(bytes32 _bytes32) internal pure returns (address) { return address(uint160(uint256(_bytes32))); } } diff --git a/test/evm/hardhat/chain-adapters/Solana_Adapter.ts b/test/evm/hardhat/chain-adapters/Solana_Adapter.ts index a86a8fda8..86dcb576e 100644 --- a/test/evm/hardhat/chain-adapters/Solana_Adapter.ts +++ b/test/evm/hardhat/chain-adapters/Solana_Adapter.ts @@ -87,7 +87,7 @@ describe("Solana Chain Adapter", function () { const functionCallData = mockSpoke.interface.encodeFunctionData("setCrossDomainAdmin", [newAdmin]); expect(await hubPool.relaySpokePoolAdminFunction(solanaChainId, functionCallData)) .to.emit(solanaAdapter.attach(hubPool.address), "MessageRelayed") - .withArgs(solanaSpokePoolAddress, functionCallData); + .withArgs(solanaSpokePoolAddress.toLowerCase(), functionCallData); expect(cctpMessageTransmitter.sendMessage).to.have.been.calledWith( solanaDomainId, solanaSpokePoolBytes32, From 3d0e899b57fecc2905cc90c41b57353afb00fcc7 Mon Sep 17 00:00:00 2001 From: Pablo Maldonado Date: Wed, 20 Nov 2024 11:13:57 +0000 Subject: [PATCH 10/29] test: fix forge tests Signed-off-by: Pablo Maldonado --- .../fork/BlacklistedRelayerRecipient.t.sol | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/test/evm/foundry/fork/BlacklistedRelayerRecipient.t.sol b/test/evm/foundry/fork/BlacklistedRelayerRecipient.t.sol index a2be448a8..c7a5b4831 100644 --- a/test/evm/foundry/fork/BlacklistedRelayerRecipient.t.sol +++ b/test/evm/foundry/fork/BlacklistedRelayerRecipient.t.sol @@ -3,7 +3,7 @@ pragma solidity ^0.8.0; import { Test } from "forge-std/Test.sol"; import { MockSpokePool } from "../../../../contracts/test/MockSpokePool.sol"; - +import { AddressToBytes32 } from "../../../../contracts/libraries/AddressConverters.sol"; import "@openzeppelin/contracts-upgradeable/token/ERC20/IERC20Upgradeable.sol"; // Define a minimal interface for USDT. Note USDT does NOT return anything after a transfer. @@ -44,6 +44,7 @@ contract MockSpokePoolTest is Test { MockSpokePool spokePool; IUSDT usdt; IUSDC usdc; + using AddressToBytes32 for address; address largeUSDTAccount = 0x99C9fc46f92E8a1c0deC1b1747d010903E884bE1; address largeUSDCAccount = 0x37305B1cD40574E4C5Ce33f8e8306Be057fD7341; @@ -120,8 +121,8 @@ contract MockSpokePoolTest is Test { assertEq(usdt.balanceOf(recipient1), refundAmounts[0], "Recipient1 should have received their refund"); assertEq(usdt.balanceOf(recipient2), refundAmounts[1], "Recipient2 should have received their refund"); - assertEq(spokePool.getRelayerRefund(address(usdt), recipient1), 0); - assertEq(spokePool.getRelayerRefund(address(usdt), recipient2), 0); + assertEq(spokePool.getRelayerRefund(address(usdt).toBytes32(), recipient1.toBytes32()), 0); + assertEq(spokePool.getRelayerRefund(address(usdt).toBytes32(), recipient2.toBytes32()), 0); } function testSomeRecipientsBlacklistedDoesNotBlockTheWholeRefundUsdc() public { @@ -147,15 +148,15 @@ contract MockSpokePoolTest is Test { assertEq(usdc.balanceOf(recipient1), 0, "Recipient1 should have 0 refund as blacklisted"); assertEq(usdc.balanceOf(recipient2), refundAmounts[1], "Recipient2 should have received their refund"); - assertEq(spokePool.getRelayerRefund(address(usdc), recipient1), refundAmounts[0]); - assertEq(spokePool.getRelayerRefund(address(usdc), recipient2), 0); + assertEq(spokePool.getRelayerRefund(address(usdc).toBytes32(), recipient1.toBytes32()), refundAmounts[0]); + assertEq(spokePool.getRelayerRefund(address(usdc).toBytes32(), recipient2.toBytes32()), 0); // Now, blacklisted recipient should be able to claim refund to a new address. address newRecipient = address(0x6969693333333420); vm.prank(recipient1); - spokePool.claimRelayerRefund(address(usdc), newRecipient); + spokePool.claimRelayerRefund(address(usdc).toBytes32(), newRecipient.toBytes32()); assertEq(usdc.balanceOf(newRecipient), refundAmounts[0], "New recipient should have received relayer2 refund"); - assertEq(spokePool.getRelayerRefund(address(usdt), recipient1), 0); + assertEq(spokePool.getRelayerRefund(address(usdt).toBytes32(), recipient1.toBytes32()), 0); } function toBytes32(address _address) internal pure returns (bytes32) { From 1ff7bb1e91cd39aed5bedf8c64c981f785a57180 Mon Sep 17 00:00:00 2001 From: Chris Maree Date: Fri, 22 Nov 2024 15:14:44 +0700 Subject: [PATCH 11/29] proposal: ensure that EVM errors are always consistant on underflows (#720) --- contracts/SpokePool.sol | 5 ++--- test/evm/hardhat/SpokePool.Deposit.ts | 6 ++++++ 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/contracts/SpokePool.sol b/contracts/SpokePool.sol index a9c25858a..f8874fa4e 100644 --- a/contracts/SpokePool.sol +++ b/contracts/SpokePool.sol @@ -520,12 +520,11 @@ abstract contract SpokePool is // It is assumed that cross-chain timestamps are normally loosely in-sync, but clock drift can occur. If the // SpokePool time stalls or lags significantly, it is still possible to make deposits by setting quoteTimestamp // within the configured buffer. The owner should pause deposits/fills if this is undesirable. - // This will underflow if quoteTimestamp is more than depositQuoteTimeBuffer; - // this is safe but will throw an unintuitive error. // slither-disable-next-line timestamp uint256 currentTime = getCurrentTime(); - if (currentTime - quoteTimestamp > depositQuoteTimeBuffer) revert InvalidQuoteTimestamp(); + if (quoteTimestamp > currentTime || currentTime - quoteTimestamp > depositQuoteTimeBuffer) + revert InvalidQuoteTimestamp(); // fillDeadline is relative to the destination chain. // Don't allow fillDeadline to be more than several bundles into the future. diff --git a/test/evm/hardhat/SpokePool.Deposit.ts b/test/evm/hardhat/SpokePool.Deposit.ts index 8e22759b8..be18c5bdd 100644 --- a/test/evm/hardhat/SpokePool.Deposit.ts +++ b/test/evm/hardhat/SpokePool.Deposit.ts @@ -431,6 +431,12 @@ describe("SpokePool Depositor Logic", async function () { ...getDepositArgsFromRelayData(relayData, destinationChainId, currentTime.sub(quoteTimeBuffer).sub(1)) ) ).to.be.revertedWith("InvalidQuoteTimestamp"); + await expect( + spokePool.connect(depositor)[depositV3Bytes]( + // quoteTimestamp in the future should also revert with InvalidQuoteTimestamp + ...getDepositArgsFromRelayData(relayData, destinationChainId, currentTime.add(500)) + ) + ).to.be.revertedWith("InvalidQuoteTimestamp"); await expect( spokePool.connect(depositor)[depositV3Bytes]( // quoteTimestamp right at the buffer is OK From d36f9e66b45da60a0dc3202efce5e1e429ddefec Mon Sep 17 00:00:00 2001 From: Pablo Maldonado Date: Fri, 22 Nov 2024 10:02:21 +0000 Subject: [PATCH 12/29] feat: revert bytes32 conversion for internal functions (#755) --- contracts/Linea_SpokePool.sol | 5 +- contracts/Ovm_SpokePool.sol | 6 +- contracts/PolygonZkEVM_SpokePool.sol | 4 +- contracts/Polygon_SpokePool.sol | 2 +- contracts/SpokePool.sol | 91 +++++++++---------- contracts/ZkSync_SpokePool.sol | 5 +- contracts/interfaces/SpokePoolInterface.sol | 4 +- contracts/test/MockSpokePool.sol | 8 +- .../fork/BlacklistedRelayerRecipient.t.sol | 34 +++---- test/evm/hardhat/MerkleLib.Proofs.ts | 6 +- test/evm/hardhat/MerkleLib.utils.ts | 2 +- test/evm/hardhat/SpokePool.Admin.ts | 2 +- .../hardhat/SpokePool.ClaimRelayerRefund.ts | 16 ++-- .../hardhat/SpokePool.ExecuteRootBundle.ts | 44 ++++----- .../Arbitrum_SpokePool.ts | 2 +- .../Ethereum_SpokePool.ts | 2 +- .../Optimism_SpokePool.ts | 2 +- .../Polygon_SpokePool.ts | 6 +- test/svm/utils.ts | 14 +-- 19 files changed, 115 insertions(+), 140 deletions(-) diff --git a/contracts/Linea_SpokePool.sol b/contracts/Linea_SpokePool.sol index 562b6c6df..4327f40b7 100644 --- a/contracts/Linea_SpokePool.sol +++ b/contracts/Linea_SpokePool.sol @@ -15,7 +15,6 @@ import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; */ contract Linea_SpokePool is SpokePool { using SafeERC20 for IERC20; - using AddressToBytes32 for address; /** * @notice Address of Linea's Canonical Message Service contract on L2. @@ -123,8 +122,8 @@ contract Linea_SpokePool is SpokePool { * @notice Wraps any ETH into WETH before executing base function. This is necessary because SpokePool receives * ETH over the canonical token bridge instead of WETH. */ - function _preExecuteLeafHook(bytes32 l2TokenAddress) internal override { - if (l2TokenAddress == address(wrappedNativeToken).toBytes32()) _depositEthToWeth(); + function _preExecuteLeafHook(address l2TokenAddress) internal override { + if (l2TokenAddress == address(wrappedNativeToken)) _depositEthToWeth(); } // Wrap any ETH owned by this contract so we can send expected L2 token to recipient. This is necessary because diff --git a/contracts/Ovm_SpokePool.sol b/contracts/Ovm_SpokePool.sol index 90d709573..d21531e3c 100644 --- a/contracts/Ovm_SpokePool.sol +++ b/contracts/Ovm_SpokePool.sol @@ -34,7 +34,7 @@ interface IL2ERC20Bridge { */ contract Ovm_SpokePool is SpokePool, CircleCCTPAdapter { using SafeERC20 for IERC20; - using AddressToBytes32 for address; + // "l1Gas" parameter used in call to bridge tokens from this contract back to L1 via IL2ERC20Bridge. Currently // unused by bridge but included for future compatibility. @@ -135,8 +135,8 @@ contract Ovm_SpokePool is SpokePool, CircleCCTPAdapter { * @notice Wraps any ETH into WETH before executing leaves. This is necessary because SpokePool receives * ETH over the canonical token bridge instead of WETH. */ - function _preExecuteLeafHook(bytes32 l2TokenAddress) internal override { - if (l2TokenAddress == address(wrappedNativeToken).toBytes32()) _depositEthToWeth(); + function _preExecuteLeafHook(address l2TokenAddress) internal override { + if (l2TokenAddress == address(wrappedNativeToken)) _depositEthToWeth(); } // Wrap any ETH owned by this contract so we can send expected L2 token to recipient. This is necessary because diff --git a/contracts/PolygonZkEVM_SpokePool.sol b/contracts/PolygonZkEVM_SpokePool.sol index 1817dba35..b9367a75d 100644 --- a/contracts/PolygonZkEVM_SpokePool.sol +++ b/contracts/PolygonZkEVM_SpokePool.sol @@ -158,8 +158,8 @@ contract PolygonZkEVM_SpokePool is SpokePool, IBridgeMessageReceiver { * @notice Wraps any ETH into WETH before executing base function. This is necessary because SpokePool receives * ETH over the canonical token bridge instead of WETH. */ - function _preExecuteLeafHook(bytes32 l2TokenAddress) internal override { - if (l2TokenAddress == address(wrappedNativeToken).toBytes32()) _depositEthToWeth(); + function _preExecuteLeafHook(address l2TokenAddress) internal override { + if (l2TokenAddress == address(wrappedNativeToken)) _depositEthToWeth(); } // Wrap any ETH owned by this contract so we can send expected L2 token to recipient. This is necessary because diff --git a/contracts/Polygon_SpokePool.sol b/contracts/Polygon_SpokePool.sol index 6cebd506c..990c987af 100644 --- a/contracts/Polygon_SpokePool.sol +++ b/contracts/Polygon_SpokePool.sol @@ -231,7 +231,7 @@ contract Polygon_SpokePool is IFxMessageProcessor, SpokePool, CircleCCTPAdapter emit SetPolygonTokenBridger(address(_polygonTokenBridger)); } - function _preExecuteLeafHook(bytes32) internal override { + function _preExecuteLeafHook(address) internal override { // Wraps MATIC --> WMATIC before distributing tokens from this contract. _wrap(); } diff --git a/contracts/SpokePool.sol b/contracts/SpokePool.sol index f8874fa4e..e09e64f70 100644 --- a/contracts/SpokePool.sol +++ b/contracts/SpokePool.sol @@ -71,7 +71,7 @@ abstract contract SpokePool is RootBundle[] public rootBundles; // Origin token to destination token routings can be turned on or off, which can enable or disable deposits. - mapping(bytes32 => mapping(uint256 => bool)) public enabledDepositRoutes; + mapping(address => mapping(uint256 => bool)) public enabledDepositRoutes; // Each relay is associated with the hash of parameters that uniquely identify the original deposit and a relay // attempt for that deposit. The relay itself is just represented as the amount filled so far. The total amount to @@ -105,7 +105,7 @@ abstract contract SpokePool is // Mapping of L2TokenAddress to relayer to outstanding refund amount. Used when a relayer repayment fails for some // reason (eg blacklist) to track their outstanding liability, thereby letting them claim it later. - mapping(bytes32 => mapping(bytes32 => uint256)) public relayerRefund; + mapping(address => mapping(address => uint256)) public relayerRefund; /************************************************************** * CONSTANT/IMMUTABLE VARIABLES * @@ -173,8 +173,8 @@ abstract contract SpokePool is uint256[] refundAmounts, uint32 indexed rootBundleId, uint32 indexed leafId, - bytes32 l2TokenAddress, - bytes32[] refundAddresses, + address l2TokenAddress, + address[] refundAddresses, bool deferredRefunds, address caller ); @@ -318,7 +318,7 @@ abstract contract SpokePool is uint256 destinationChainId, bool enabled ) public override onlyAdmin nonReentrant { - enabledDepositRoutes[originToken.toBytes32()][destinationChainId] = enabled; + enabledDepositRoutes[originToken][destinationChainId] = enabled; emit EnabledDepositRoute(originToken, destinationChainId, enabled); } @@ -390,9 +390,9 @@ abstract contract SpokePool is uint256 // maxCount. Deprecated. ) public payable override nonReentrant unpausedDeposits { _deposit( - msg.sender.toBytes32(), - recipient.toBytes32(), - originToken.toBytes32(), + msg.sender, + recipient, + originToken, amount, destinationChainId, relayerFeePct, @@ -432,16 +432,7 @@ abstract contract SpokePool is bytes memory message, uint256 // maxCount. Deprecated. ) public payable nonReentrant unpausedDeposits { - _deposit( - depositor.toBytes32(), - recipient.toBytes32(), - originToken.toBytes32(), - amount, - destinationChainId, - relayerFeePct, - quoteTimestamp, - message - ); + _deposit(depositor, recipient, originToken, amount, destinationChainId, relayerFeePct, quoteTimestamp, message); } /******************************************** @@ -514,7 +505,7 @@ abstract contract SpokePool is ) public payable override nonReentrant unpausedDeposits { // Check that deposit route is enabled for the input token. There are no checks required for the output token // which is pulled from the relayer at fill time and passed through this contract atomically to the recipient. - if (!enabledDepositRoutes[inputToken][destinationChainId]) revert DisabledRoute(); + if (!enabledDepositRoutes[inputToken.toAddress()][destinationChainId]) revert DisabledRoute(); // Require that quoteTimestamp has a maximum age so that depositors pay an LP fee based on recent HubPool usage. // It is assumed that cross-chain timestamps are normally loosely in-sync, but clock drift can occur. If the @@ -980,8 +971,11 @@ abstract contract SpokePool is // Exclusivity deadline is inclusive and is the latest timestamp that the exclusive relayer has sole right // to fill the relay. if ( - _fillIsExclusive(relayData.exclusiveRelayer, relayData.exclusivityDeadline, uint32(getCurrentTime())) && - relayData.exclusiveRelayer.toAddress() != msg.sender + _fillIsExclusive( + relayData.exclusiveRelayer.toAddress(), + relayData.exclusivityDeadline, + uint32(getCurrentTime()) + ) && relayData.exclusiveRelayer.toAddress() != msg.sender ) { revert NotExclusiveRelayer(); } @@ -1027,8 +1021,11 @@ abstract contract SpokePool is // Exclusivity deadline is inclusive and is the latest timestamp that the exclusive relayer has sole right // to fill the relay. if ( - _fillIsExclusive(relayData.exclusiveRelayer, relayData.exclusivityDeadline, uint32(getCurrentTime())) && - relayData.exclusiveRelayer.toAddress() != msg.sender + _fillIsExclusive( + relayData.exclusiveRelayer.toAddress(), + relayData.exclusivityDeadline, + uint32(getCurrentTime()) + ) && relayData.exclusiveRelayer.toAddress() != msg.sender ) { revert NotExclusiveRelayer(); } @@ -1077,7 +1074,7 @@ abstract contract SpokePool is // fast fill within this deadline. Moreover, the depositor should expect to get *fast* filled within // this deadline, not slow filled. As a simplifying assumption, we will not allow slow fills to be requested // during this exclusivity period. - if (_fillIsExclusive(relayData.exclusiveRelayer, relayData.exclusivityDeadline, currentTime)) { + if (_fillIsExclusive(relayData.exclusiveRelayer.toAddress(), relayData.exclusivityDeadline, currentTime)) { revert NoSlowFillsInExclusivityWindow(); } if (relayData.fillDeadline < currentTime) revert ExpiredFillDeadline(); @@ -1165,7 +1162,7 @@ abstract contract SpokePool is ) public override nonReentrant { V3RelayData memory relayData = slowFillLeaf.relayData; - _preExecuteLeafHook(relayData.outputToken); + _preExecuteLeafHook(relayData.outputToken.toAddress()); // @TODO In the future consider allowing way for slow fill leaf to be created with updated // deposit params like outputAmount, message and recipient. @@ -1241,9 +1238,9 @@ abstract contract SpokePool is * @param refundAddress Address to send the refund to. */ function claimRelayerRefund(bytes32 l2TokenAddress, bytes32 refundAddress) public { - uint256 refund = relayerRefund[l2TokenAddress][msg.sender.toBytes32()]; + uint256 refund = relayerRefund[l2TokenAddress.toAddress()][msg.sender]; if (refund == 0) revert NoRelayerRefundToClaim(); - relayerRefund[l2TokenAddress][msg.sender.toBytes32()] = 0; + relayerRefund[l2TokenAddress.toAddress()][refundAddress.toAddress()] = 0; IERC20Upgradeable(l2TokenAddress.toAddress()).safeTransfer(refundAddress.toAddress(), refund); emit ClaimedRelayerRefund(l2TokenAddress, refundAddress, refund, msg.sender); @@ -1269,7 +1266,7 @@ abstract contract SpokePool is return block.timestamp; // solhint-disable-line not-rely-on-time } - function getRelayerRefund(bytes32 l2TokenAddress, bytes32 refundAddress) public view returns (uint256) { + function getRelayerRefund(address l2TokenAddress, address refundAddress) public view returns (uint256) { return relayerRefund[l2TokenAddress][refundAddress]; } @@ -1277,9 +1274,9 @@ abstract contract SpokePool is * INTERNAL FUNCTIONS * **************************************/ function _deposit( - bytes32 depositor, - bytes32 recipient, - bytes32 originToken, + address depositor, + address recipient, + address originToken, uint256 amount, uint256 destinationChainId, int64 relayerFeePct, @@ -1307,18 +1304,18 @@ abstract contract SpokePool is // If the address of the origin token is a wrappedNativeToken contract and there is a msg.value with the // transaction then the user is sending ETH. In this case, the ETH should be deposited to wrappedNativeToken. - if (originToken == address(wrappedNativeToken).toBytes32() && msg.value > 0) { + if (originToken == address(wrappedNativeToken) && msg.value > 0) { if (msg.value != amount) revert MsgValueDoesNotMatchInputAmount(); wrappedNativeToken.deposit{ value: msg.value }(); // Else, it is a normal ERC20. In this case pull the token from the user's wallet as per normal. // Note: this includes the case where the L2 user has WETH (already wrapped ETH) and wants to bridge them. // In this case the msg.value will be set to 0, indicating a "normal" ERC20 bridging action. } else { - IERC20Upgradeable(originToken.toAddress()).safeTransferFrom(msg.sender, address(this), amount); + IERC20Upgradeable(originToken).safeTransferFrom(msg.sender, address(this), amount); } emit V3FundsDeposited( - originToken, // inputToken + originToken.toBytes32(), // inputToken bytes32(0), // outputToken. Setting this to 0x0 means that the outputToken should be assumed to be the // canonical token for the destination chain matching the inputToken. Therefore, this deposit // can always be slow filled. @@ -1336,8 +1333,8 @@ abstract contract SpokePool is // expired deposits refunds could be a breaking change for existing users of this function. 0, // exclusivityDeadline. Setting this to 0 along with the exclusiveRelayer to 0x0 means that there // is no exclusive deadline - depositor, - recipient, + depositor.toBytes32(), + recipient.toBytes32(), bytes32(0), // exclusiveRelayer. Setting this to 0x0 will signal to off-chain validator that there // is no exclusive relayer. message @@ -1349,14 +1346,14 @@ abstract contract SpokePool is uint256 amountToReturn, uint256[] memory refundAmounts, uint32 leafId, - bytes32 l2TokenAddress, - bytes32[] memory refundAddresses + address l2TokenAddress, + address[] memory refundAddresses ) internal returns (bool deferredRefunds) { uint256 numRefunds = refundAmounts.length; if (refundAddresses.length != numRefunds) revert InvalidMerkleLeaf(); if (numRefunds > 0) { - uint256 spokeStartBalance = IERC20Upgradeable(l2TokenAddress.toAddress()).balanceOf(address(this)); + uint256 spokeStartBalance = IERC20Upgradeable(l2TokenAddress).balanceOf(address(this)); uint256 totalRefundedAmount = 0; // Track the total amount refunded. // Send each relayer refund address the associated refundAmount for the L2 token address. @@ -1370,11 +1367,7 @@ abstract contract SpokePool is // prevents can only re-pay some of the relayers. if (totalRefundedAmount > spokeStartBalance) revert InsufficientSpokePoolBalanceToExecuteLeaf(); - bool success = _noRevertTransfer( - l2TokenAddress.toAddress(), - refundAddresses[i].toAddress(), - refundAmounts[i] - ); + bool success = _noRevertTransfer(l2TokenAddress, refundAddresses[i], refundAmounts[i]); // If the transfer failed then track a deferred transfer for the relayer. Given this function would // have revered if there was insufficient balance, this will only happen if the transfer call @@ -1390,9 +1383,9 @@ abstract contract SpokePool is // If leaf's amountToReturn is positive, then send L2 --> L1 message to bridge tokens back via // chain-specific bridging method. if (amountToReturn > 0) { - _bridgeTokensToHubPool(amountToReturn, l2TokenAddress.toAddress()); + _bridgeTokensToHubPool(amountToReturn, l2TokenAddress); - emit TokensBridged(amountToReturn, _chainId, leafId, l2TokenAddress, msg.sender); + emit TokensBridged(amountToReturn, _chainId, leafId, l2TokenAddress.toBytes32(), msg.sender); } } @@ -1428,7 +1421,7 @@ abstract contract SpokePool is emit SetWithdrawalRecipient(newWithdrawalRecipient); } - function _preExecuteLeafHook(bytes32) internal virtual { + function _preExecuteLeafHook(address) internal virtual { // This method by default is a no-op. Different child spoke pools might want to execute functionality here // such as wrapping any native tokens owned by the contract into wrapped tokens before proceeding with // executing the leaf. @@ -1632,11 +1625,11 @@ abstract contract SpokePool is // Determine whether the combination of exlcusiveRelayer and exclusivityDeadline implies active exclusivity. function _fillIsExclusive( - bytes32 exclusiveRelayer, + address exclusiveRelayer, uint32 exclusivityDeadline, uint32 currentTime ) internal pure returns (bool) { - return exclusivityDeadline >= currentTime && exclusiveRelayer != bytes32(0); + return exclusivityDeadline >= currentTime && exclusiveRelayer != address(0); } // Implementing contract needs to override this to ensure that only the appropriate cross chain admin can execute diff --git a/contracts/ZkSync_SpokePool.sol b/contracts/ZkSync_SpokePool.sol index 65fb4b16c..3224e4cb3 100644 --- a/contracts/ZkSync_SpokePool.sol +++ b/contracts/ZkSync_SpokePool.sol @@ -23,7 +23,6 @@ interface IL2ETH { * @custom:security-contact bugs@across.to */ contract ZkSync_SpokePool is SpokePool { - using AddressToBytes32 for address; // On Ethereum, avoiding constructor parameters and putting them into constants reduces some of the gas cost // upon contract deployment. On zkSync the opposite is true: deploying the same bytecode for contracts, // while changing only constructor parameters can lead to substantial fee savings. So, the following params @@ -89,8 +88,8 @@ contract ZkSync_SpokePool is SpokePool { * @notice Wraps any ETH into WETH before executing base function. This is necessary because SpokePool receives * ETH over the canonical token bridge instead of WETH. */ - function _preExecuteLeafHook(bytes32 l2TokenAddress) internal override { - if (l2TokenAddress == address(wrappedNativeToken).toBytes32()) _depositEthToWeth(); + function _preExecuteLeafHook(address l2TokenAddress) internal override { + if (l2TokenAddress == address(wrappedNativeToken)) _depositEthToWeth(); } // Wrap any ETH owned by this contract so we can send expected L2 token to recipient. This is necessary because diff --git a/contracts/interfaces/SpokePoolInterface.sol b/contracts/interfaces/SpokePoolInterface.sol index 248eee292..08bd4aae8 100644 --- a/contracts/interfaces/SpokePoolInterface.sol +++ b/contracts/interfaces/SpokePoolInterface.sol @@ -17,9 +17,9 @@ interface SpokePoolInterface { // Used as the index in the bitmap to track whether this leaf has been executed or not. uint32 leafId; // The associated L2TokenAddress that these claims apply to. - bytes32 l2TokenAddress; + address l2TokenAddress; // Must be same length as refundAmounts and designates each address that must be refunded. - bytes32[] refundAddresses; + address[] refundAddresses; } // Stores collection of merkle roots that can be published to this contract from the HubPool, which are referenced diff --git a/contracts/test/MockSpokePool.sol b/contracts/test/MockSpokePool.sol index 721bad51d..a19a02138 100644 --- a/contracts/test/MockSpokePool.sol +++ b/contracts/test/MockSpokePool.sol @@ -52,8 +52,8 @@ contract MockSpokePool is SpokePool, MockV2SpokePoolInterface, OwnableUpgradeabl uint256 amountToReturn, uint256[] memory refundAmounts, uint32 leafId, - bytes32 l2TokenAddress, - bytes32[] memory refundAddresses + address l2TokenAddress, + address[] memory refundAddresses ) external { _distributeRelayerRefunds(_chainId, amountToReturn, refundAmounts, leafId, l2TokenAddress, refundAddresses); } @@ -152,8 +152,8 @@ contract MockSpokePool is SpokePool, MockV2SpokePoolInterface, OwnableUpgradeabl return currentTime; } - function _preExecuteLeafHook(bytes32 token) internal override { - emit PreLeafExecuteHook(token); + function _preExecuteLeafHook(address token) internal override { + emit PreLeafExecuteHook(token.toBytes32()); } function _bridgeTokensToHubPool(uint256 amount, address token) internal override { diff --git a/test/evm/foundry/fork/BlacklistedRelayerRecipient.t.sol b/test/evm/foundry/fork/BlacklistedRelayerRecipient.t.sol index c7a5b4831..65bbdbdb5 100644 --- a/test/evm/foundry/fork/BlacklistedRelayerRecipient.t.sol +++ b/test/evm/foundry/fork/BlacklistedRelayerRecipient.t.sol @@ -80,9 +80,9 @@ contract MockSpokePoolTest is Test { uint256[] memory refundAmounts = new uint256[](1); refundAmounts[0] = 420 * 10**6; - bytes32[] memory refundAddresses = new bytes32[](1); - refundAddresses[0] = toBytes32(recipient1); - spokePool.distributeRelayerRefunds(1, 0, refundAmounts, 0, toBytes32(address(usdt)), refundAddresses); + address[] memory refundAddresses = new address[](1); + refundAddresses[0] = recipient1; + spokePool.distributeRelayerRefunds(1, 0, refundAmounts, 0, address(usdt), refundAddresses); assertEq(usdt.balanceOf(recipient1), refundAmounts[0], "Recipient should have received refund"); assertEq(usdt.balanceOf(address(spokePool)), seedAmount - refundAmounts[0], "SpokePool bal should drop"); @@ -91,7 +91,7 @@ contract MockSpokePoolTest is Test { assertEq(usdc.balanceOf(recipient1), 0, "Recipient should start with 0 USDC balance"); assertEq(usdc.balanceOf(address(spokePool)), seedAmount, "SpokePool should have seed USDC balance"); - spokePool.distributeRelayerRefunds(1, 0, refundAmounts, 0, toBytes32(address(usdc)), refundAddresses); + spokePool.distributeRelayerRefunds(1, 0, refundAmounts, 0, address(usdc), refundAddresses); assertEq(usdc.balanceOf(recipient1), refundAmounts[0], "Recipient should have received refund"); assertEq(usdc.balanceOf(address(spokePool)), seedAmount - refundAmounts[0], "SpokePool bal should drop"); @@ -113,16 +113,16 @@ contract MockSpokePoolTest is Test { refundAmounts[0] = 420 * 10**6; refundAmounts[1] = 69 * 10**6; - bytes32[] memory refundAddresses = new bytes32[](2); - refundAddresses[0] = toBytes32(recipient1); - refundAddresses[1] = toBytes32(recipient2); - spokePool.distributeRelayerRefunds(1, 0, refundAmounts, 0, toBytes32(address(usdt)), refundAddresses); + address[] memory refundAddresses = new address[](2); + refundAddresses[0] = recipient1; + refundAddresses[1] = recipient2; + spokePool.distributeRelayerRefunds(1, 0, refundAmounts, 0, address(usdt), refundAddresses); assertEq(usdt.balanceOf(recipient1), refundAmounts[0], "Recipient1 should have received their refund"); assertEq(usdt.balanceOf(recipient2), refundAmounts[1], "Recipient2 should have received their refund"); - assertEq(spokePool.getRelayerRefund(address(usdt).toBytes32(), recipient1.toBytes32()), 0); - assertEq(spokePool.getRelayerRefund(address(usdt).toBytes32(), recipient2.toBytes32()), 0); + assertEq(spokePool.getRelayerRefund(address(usdt), recipient1), 0); + assertEq(spokePool.getRelayerRefund(address(usdt), recipient2), 0); } function testSomeRecipientsBlacklistedDoesNotBlockTheWholeRefundUsdc() public { @@ -140,23 +140,23 @@ contract MockSpokePoolTest is Test { refundAmounts[0] = 420 * 10**6; refundAmounts[1] = 69 * 10**6; - bytes32[] memory refundAddresses = new bytes32[](2); - refundAddresses[0] = toBytes32(recipient1); - refundAddresses[1] = toBytes32(recipient2); - spokePool.distributeRelayerRefunds(1, 0, refundAmounts, 0, toBytes32(address(usdc)), refundAddresses); + address[] memory refundAddresses = new address[](2); + refundAddresses[0] = recipient1; + refundAddresses[1] = recipient2; + spokePool.distributeRelayerRefunds(1, 0, refundAmounts, 0, address(usdc), refundAddresses); assertEq(usdc.balanceOf(recipient1), 0, "Recipient1 should have 0 refund as blacklisted"); assertEq(usdc.balanceOf(recipient2), refundAmounts[1], "Recipient2 should have received their refund"); - assertEq(spokePool.getRelayerRefund(address(usdc).toBytes32(), recipient1.toBytes32()), refundAmounts[0]); - assertEq(spokePool.getRelayerRefund(address(usdc).toBytes32(), recipient2.toBytes32()), 0); + assertEq(spokePool.getRelayerRefund(address(usdc), recipient1), refundAmounts[0]); + assertEq(spokePool.getRelayerRefund(address(usdc), recipient2), 0); // Now, blacklisted recipient should be able to claim refund to a new address. address newRecipient = address(0x6969693333333420); vm.prank(recipient1); spokePool.claimRelayerRefund(address(usdc).toBytes32(), newRecipient.toBytes32()); assertEq(usdc.balanceOf(newRecipient), refundAmounts[0], "New recipient should have received relayer2 refund"); - assertEq(spokePool.getRelayerRefund(address(usdt).toBytes32(), recipient1.toBytes32()), 0); + assertEq(spokePool.getRelayerRefund(address(usdt), recipient1), 0); } function toBytes32(address _address) internal pure returns (bytes32) { diff --git a/test/evm/hardhat/MerkleLib.Proofs.ts b/test/evm/hardhat/MerkleLib.Proofs.ts index ff756e7a8..77e88b035 100644 --- a/test/evm/hardhat/MerkleLib.Proofs.ts +++ b/test/evm/hardhat/MerkleLib.Proofs.ts @@ -4,6 +4,7 @@ import { MerkleTree, EMPTY_MERKLE_ROOT } from "../../../utils/MerkleTree"; import { expect, randomBigNumber, + randomAddress, getParamType, defaultAbiCoder, keccak256, @@ -11,7 +12,6 @@ import { BigNumber, ethers, randomBytes32, - randomAddress, } from "../../../utils/utils"; import { V3RelayData, V3SlowFill } from "../../../test-utils"; @@ -81,14 +81,14 @@ describe("MerkleLib Proofs", async function () { const refundAddresses: string[] = []; const refundAmounts: BigNumber[] = []; for (let j = 0; j < numAddresses; j++) { - refundAddresses.push(randomBytes32()); + refundAddresses.push(randomAddress()); refundAmounts.push(randomBigNumber()); } relayerRefundLeaves.push({ leafId: BigNumber.from(i), chainId: randomBigNumber(2), amountToReturn: randomBigNumber(), - l2TokenAddress: randomBytes32(), + l2TokenAddress: randomAddress(), refundAddresses, refundAmounts, }); diff --git a/test/evm/hardhat/MerkleLib.utils.ts b/test/evm/hardhat/MerkleLib.utils.ts index 39e6aa3a6..ff2b605f9 100644 --- a/test/evm/hardhat/MerkleLib.utils.ts +++ b/test/evm/hardhat/MerkleLib.utils.ts @@ -110,7 +110,7 @@ export async function constructSingleRelayerRefundTree(l2Token: Contract | Strin const leaves = buildRelayerRefundLeaves( [destinationChainId], // Destination chain ID. [amountToReturn], // amountToReturn. - [addressToBytes(l2Token as string)], // l2Token. + [l2Token as string], // l2Token. [[]], // refundAddresses. [[]] // refundAmounts. ); diff --git a/test/evm/hardhat/SpokePool.Admin.ts b/test/evm/hardhat/SpokePool.Admin.ts index f51adf376..149a74909 100644 --- a/test/evm/hardhat/SpokePool.Admin.ts +++ b/test/evm/hardhat/SpokePool.Admin.ts @@ -23,7 +23,7 @@ describe("SpokePool Admin Functions", async function () { await expect(spokePool.connect(owner).setEnableRoute(erc20.address, destinationChainId, true)) .to.emit(spokePool, "EnabledDepositRoute") .withArgs(erc20.address, destinationChainId, true); - expect(await spokePool.enabledDepositRoutes(addressToBytes(erc20.address), destinationChainId)).to.equal(true); + expect(await spokePool.enabledDepositRoutes(erc20.address, destinationChainId)).to.equal(true); }); it("Pause deposits", async function () { diff --git a/test/evm/hardhat/SpokePool.ClaimRelayerRefund.ts b/test/evm/hardhat/SpokePool.ClaimRelayerRefund.ts index 3248d34fd..2c9778fd8 100644 --- a/test/evm/hardhat/SpokePool.ClaimRelayerRefund.ts +++ b/test/evm/hardhat/SpokePool.ClaimRelayerRefund.ts @@ -41,9 +41,7 @@ describe("SpokePool with Blacklisted destErc20", function () { it("Executes repayments and handles blacklisted addresses", async function () { // No starting relayer liability. - expect( - await spokePool.getRelayerRefund(addressToBytes(destErc20.address), addressToBytes(relayer.address)) - ).to.equal(toBN(0)); + expect(await spokePool.getRelayerRefund(destErc20.address, relayer.address)).to.equal(toBN(0)); expect(await destErc20.balanceOf(rando.address)).to.equal(toBN(0)); expect(await destErc20.balanceOf(relayer.address)).to.equal(toBN(0)); // Blacklist the relayer @@ -58,14 +56,12 @@ describe("SpokePool with Blacklisted destErc20", function () { consts.amountToReturn, [consts.amountToRelay, consts.amountToRelay], 0, - addressToBytes(destErc20.address), - [addressToBytes(relayer.address), addressToBytes(rando.address)] + destErc20.address, + [relayer.address, rando.address] ); // Ensure relayerRepaymentLiability is incremented - expect( - await spokePool.getRelayerRefund(addressToBytes(destErc20.address), addressToBytes(relayer.address)) - ).to.equal(consts.amountToRelay); + expect(await spokePool.getRelayerRefund(destErc20.address, relayer.address)).to.equal(consts.amountToRelay); expect(await destErc20.balanceOf(rando.address)).to.equal(consts.amountToRelay); expect(await destErc20.balanceOf(relayer.address)).to.equal(toBN(0)); }); @@ -79,8 +75,8 @@ describe("SpokePool with Blacklisted destErc20", function () { consts.amountToReturn, [consts.amountToRelay], 0, - addressToBytes(destErc20.address), - [addressToBytes(relayer.address)] + destErc20.address, + [relayer.address] ); await expect( diff --git a/test/evm/hardhat/SpokePool.ExecuteRootBundle.ts b/test/evm/hardhat/SpokePool.ExecuteRootBundle.ts index 4e35aa9e6..fefc39678 100644 --- a/test/evm/hardhat/SpokePool.ExecuteRootBundle.ts +++ b/test/evm/hardhat/SpokePool.ExecuteRootBundle.ts @@ -22,8 +22,8 @@ async function constructSimpleTree(l2Token: Contract, destinationChainId: number const leaves = buildRelayerRefundLeaves( [destinationChainId, destinationChainId], // Destination chain ID. [consts.amountToReturn, toBN(0)], // amountToReturn. - [addressToBytes(l2Token.address), addressToBytes(l2Token.address)], // l2Token. - [[addressToBytes(relayer.address), addressToBytes(rando.address)], []], // refundAddresses. + [l2Token.address, l2Token.address], // l2Token. + [[relayer.address, rando.address], []], // refundAddresses. [[consts.amountToRelay, consts.amountToRelay], []] // refundAmounts. ); const leavesRefundAmount = leaves @@ -63,18 +63,14 @@ describe("SpokePool Root Bundle Execution", function () { // Check events. let relayTokensEvents = await spokePool.queryFilter(spokePool.filters.ExecutedRelayerRefundRoot()); - expect(relayTokensEvents[0].args?.l2TokenAddress).to.equal(addressToBytes(destErc20.address)); + expect(relayTokensEvents[0].args?.l2TokenAddress).to.equal(destErc20.address); expect(relayTokensEvents[0].args?.leafId).to.equal(0); expect(relayTokensEvents[0].args?.chainId).to.equal(destinationChainId); expect(relayTokensEvents[0].args?.amountToReturn).to.equal(consts.amountToReturn); expect((relayTokensEvents[0].args?.refundAmounts as BigNumber[]).map((v) => v.toString())).to.deep.equal( [consts.amountToRelay, consts.amountToRelay].map((v) => v.toString()) ); - expect(relayTokensEvents[0].args?.refundAddresses).to.deep.equal([ - addressToBytes(relayer.address), - addressToBytes(rando.address), - ]); - expect(relayTokensEvents[0].args?.deferredRefunds).to.equal(false); + expect(relayTokensEvents[0].args?.refundAddresses).to.deep.equal([relayer.address, rando.address]); // Should emit TokensBridged event if amountToReturn is positive. let tokensBridgedEvents = await spokePool.queryFilter(spokePool.filters.TokensBridged()); @@ -98,8 +94,8 @@ describe("SpokePool Root Bundle Execution", function () { totalSolanaDistributions: evmDistributions, mixLeaves: true, chainId: destinationChainId, - evmTokenAddress: addressToBytes(destErc20.address), - evmRelayers: [addressToBytes(relayer.address), addressToBytes(rando.address)], + evmTokenAddress: destErc20.address, + evmRelayers: [relayer.address, rando.address], evmRefundAmounts: [consts.amountToRelay.div(evmDistributions), consts.amountToRelay.div(evmDistributions)], }); @@ -134,8 +130,8 @@ describe("SpokePool Root Bundle Execution", function () { totalSolanaDistributions: evmDistributions, mixLeaves: false, chainId: destinationChainId, - evmTokenAddress: addressToBytes(destErc20.address), - evmRelayers: [addressToBytes(relayer.address), addressToBytes(rando.address)], + evmTokenAddress: destErc20.address, + evmRelayers: [relayer.address, rando.address], evmRefundAmounts: [consts.amountToRelay.div(evmDistributions), consts.amountToRelay.div(evmDistributions)], }); @@ -236,8 +232,8 @@ describe("SpokePool Root Bundle Execution", function () { toBN(1), [consts.amountToRelay, consts.amountToRelay, toBN(0)], 0, - addressToBytes(destErc20.address), - [addressToBytes(relayer.address), addressToBytes(rando.address)] + destErc20.address, + [relayer.address, rando.address] ) ).to.be.revertedWith("InvalidMerkleLeaf"); }); @@ -246,7 +242,7 @@ describe("SpokePool Root Bundle Execution", function () { await expect( spokePool .connect(dataWorker) - .distributeRelayerRefunds(destinationChainId, toBN(1), [], 0, addressToBytes(destErc20.address), []) + .distributeRelayerRefunds(destinationChainId, toBN(1), [], 0, destErc20.address, []) ) .to.emit(spokePool, "BridgedToHubPool") .withArgs(toBN(1), destErc20.address); @@ -255,7 +251,7 @@ describe("SpokePool Root Bundle Execution", function () { await expect( spokePool .connect(dataWorker) - .distributeRelayerRefunds(destinationChainId, toBN(1), [], 0, addressToBytes(destErc20.address), []) + .distributeRelayerRefunds(destinationChainId, toBN(1), [], 0, destErc20.address, []) ) .to.emit(spokePool, "TokensBridged") .withArgs(toBN(1), destinationChainId, 0, addressToBytes(destErc20.address), dataWorker.address); @@ -266,14 +262,14 @@ describe("SpokePool Root Bundle Execution", function () { await expect( spokePool .connect(dataWorker) - .distributeRelayerRefunds(destinationChainId, toBN(0), [], 0, addressToBytes(destErc20.address), []) + .distributeRelayerRefunds(destinationChainId, toBN(0), [], 0, destErc20.address, []) ).to.not.emit(spokePool, "BridgedToHubPool"); }); it("does not emit TokensBridged", async function () { await expect( spokePool .connect(dataWorker) - .distributeRelayerRefunds(destinationChainId, toBN(0), [], 0, addressToBytes(destErc20.address), []) + .distributeRelayerRefunds(destinationChainId, toBN(0), [], 0, destErc20.address, []) ).to.not.emit(spokePool, "TokensBridged"); }); }); @@ -287,8 +283,8 @@ describe("SpokePool Root Bundle Execution", function () { toBN(1), [consts.amountToRelay, consts.amountToRelay, toBN(0)], 0, - addressToBytes(destErc20.address), - [addressToBytes(relayer.address), addressToBytes(rando.address), addressToBytes(rando.address)] + destErc20.address, + [relayer.address, rando.address, rando.address] ) ).to.changeTokenBalances( destErc20, @@ -307,8 +303,8 @@ describe("SpokePool Root Bundle Execution", function () { toBN(1), [consts.amountToRelay, consts.amountToRelay, toBN(0)], 0, - addressToBytes(destErc20.address), - [addressToBytes(relayer.address), addressToBytes(rando.address), addressToBytes(rando.address)] + destErc20.address, + [relayer.address, rando.address, rando.address] ) ) .to.emit(spokePool, "BridgedToHubPool") @@ -323,8 +319,8 @@ describe("SpokePool Root Bundle Execution", function () { toBN(1), [consts.amountHeldByPool, consts.amountToRelay], // spoke has only amountHeldByPool. 0, - addressToBytes(destErc20.address), - [addressToBytes(relayer.address), addressToBytes(rando.address)] + destErc20.address, + [relayer.address, rando.address] ) ).to.be.revertedWith("InsufficientSpokePoolBalanceToExecuteLeaf"); diff --git a/test/evm/hardhat/chain-specific-spokepools/Arbitrum_SpokePool.ts b/test/evm/hardhat/chain-specific-spokepools/Arbitrum_SpokePool.ts index c76ffe94c..723e513e9 100644 --- a/test/evm/hardhat/chain-specific-spokepools/Arbitrum_SpokePool.ts +++ b/test/evm/hardhat/chain-specific-spokepools/Arbitrum_SpokePool.ts @@ -80,7 +80,7 @@ describe("Arbitrum Spoke Pool", function () { it("Only cross domain owner can enable a route", async function () { await expect(arbitrumSpokePool.setEnableRoute(l2Dai, 1, true)).to.be.reverted; await arbitrumSpokePool.connect(crossDomainAlias).setEnableRoute(l2Dai, 1, true); - expect(await arbitrumSpokePool.enabledDepositRoutes(addressToBytes(l2Dai), 1)).to.equal(true); + expect(await arbitrumSpokePool.enabledDepositRoutes(l2Dai, 1)).to.equal(true); }); it("Only cross domain owner can whitelist a token pair", async function () { diff --git a/test/evm/hardhat/chain-specific-spokepools/Ethereum_SpokePool.ts b/test/evm/hardhat/chain-specific-spokepools/Ethereum_SpokePool.ts index 1b5966135..a7b2d0823 100644 --- a/test/evm/hardhat/chain-specific-spokepools/Ethereum_SpokePool.ts +++ b/test/evm/hardhat/chain-specific-spokepools/Ethereum_SpokePool.ts @@ -55,7 +55,7 @@ describe("Ethereum Spoke Pool", function () { it("Only owner can enable a route", async function () { await expect(spokePool.connect(rando).setEnableRoute(dai.address, 1, true)).to.be.reverted; await spokePool.connect(owner).setEnableRoute(dai.address, 1, true); - expect(await spokePool.enabledDepositRoutes(addressToBytes(dai.address), 1)).to.equal(true); + expect(await spokePool.enabledDepositRoutes(dai.address, 1)).to.equal(true); }); it("Only owner can set the hub pool address", async function () { diff --git a/test/evm/hardhat/chain-specific-spokepools/Optimism_SpokePool.ts b/test/evm/hardhat/chain-specific-spokepools/Optimism_SpokePool.ts index 460e19a3d..f629007fb 100644 --- a/test/evm/hardhat/chain-specific-spokepools/Optimism_SpokePool.ts +++ b/test/evm/hardhat/chain-specific-spokepools/Optimism_SpokePool.ts @@ -94,7 +94,7 @@ describe("Optimism Spoke Pool", function () { await expect(optimismSpokePool.setEnableRoute(l2Dai, 1, true)).to.be.reverted; crossDomainMessenger.xDomainMessageSender.returns(owner.address); await optimismSpokePool.connect(crossDomainMessenger.wallet).setEnableRoute(l2Dai, 1, true); - expect(await optimismSpokePool.enabledDepositRoutes(addressToBytes(l2Dai), 1)).to.equal(true); + expect(await optimismSpokePool.enabledDepositRoutes(l2Dai, 1)).to.equal(true); }); it("Only cross domain owner can set the cross domain admin", async function () { diff --git a/test/evm/hardhat/chain-specific-spokepools/Polygon_SpokePool.ts b/test/evm/hardhat/chain-specific-spokepools/Polygon_SpokePool.ts index 9ef4bff15..650024636 100644 --- a/test/evm/hardhat/chain-specific-spokepools/Polygon_SpokePool.ts +++ b/test/evm/hardhat/chain-specific-spokepools/Polygon_SpokePool.ts @@ -156,7 +156,7 @@ describe("Polygon Spoke Pool", function () { .reverted; await polygonSpokePool.connect(fxChild).processMessageFromRoot(0, owner.address, setEnableRouteData); - expect(await polygonSpokePool.enabledDepositRoutes(addressToBytes(l2Dai), 1)).to.equal(true); + expect(await polygonSpokePool.enabledDepositRoutes(l2Dai, 1)).to.equal(true); }); it("Only correct caller can initialize a relayer refund", async function () { @@ -261,7 +261,7 @@ describe("Polygon Spoke Pool", function () { const leaves = buildRelayerRefundLeaves( [l2ChainId, l2ChainId], // Destination chain ID. [amountToReturn, ethers.constants.Zero], // amountToReturn. - [addressToBytes(dai.address), addressToBytes(dai.address)], // l2Token. + [dai.address, dai.address], // l2Token. [[], []], // refundAddresses. [[], []] // refundAmounts. ); @@ -289,7 +289,7 @@ describe("Polygon Spoke Pool", function () { const leaves = buildRelayerRefundLeaves( [l2ChainId, l2ChainId], // Destination chain ID. [ethers.constants.Zero, ethers.constants.Zero], // amountToReturn. - [addressToBytes(dai.address), addressToBytes(dai.address)], // l2Token. + [dai.address, dai.address], // l2Token. [[], []], // refundAddresses. [[], []] // refundAmounts. ); diff --git a/test/svm/utils.ts b/test/svm/utils.ts index 210e940e7..393305bc8 100644 --- a/test/svm/utils.ts +++ b/test/svm/utils.ts @@ -101,14 +101,6 @@ export function buildRelayerRefundMerkleTree({ }): { relayerRefundLeaves: RelayerRefundLeafType[]; merkleTree: MerkleTree } { const relayerRefundLeaves: RelayerRefundLeafType[] = []; - if (evmTokenAddress && !isBytes32(evmTokenAddress)) { - throw new Error("EVM token address must be a bytes32 address"); - } - - if (evmRelayers && evmRelayers.some((address) => !isBytes32(address))) { - throw new Error("EVM relayers must be bytes32 addresses"); - } - const createSolanaLeaf = (index: number) => ({ isSolana: true, leafId: new BN(index), @@ -125,8 +117,8 @@ export function buildRelayerRefundMerkleTree({ leafId: BigNumber.from(index), chainId: BigNumber.from(chainId), amountToReturn: BigNumber.from(0), - l2TokenAddress: evmTokenAddress ?? addressToBytes(randomAddress()), - refundAddresses: evmRelayers || [addressToBytes(randomAddress()), addressToBytes(randomAddress())], + l2TokenAddress: evmTokenAddress ?? randomAddress(), + refundAddresses: evmRelayers || [randomAddress(), randomAddress()], refundAmounts: evmRefundAmounts || [BigNumber.from(randomBigInt()), BigNumber.from(randomBigInt())], } as RelayerRefundLeaf); @@ -186,7 +178,7 @@ export const relayerRefundHashFn = (input: RelayerRefundLeaf | RelayerRefundLeaf const abiCoder = new ethers.utils.AbiCoder(); const encodedData = abiCoder.encode( [ - "tuple( uint256 amountToReturn, uint256 chainId, uint256[] refundAmounts, uint256 leafId, bytes32 l2TokenAddress, bytes32[] refundAddresses)", + "tuple( uint256 amountToReturn, uint256 chainId, uint256[] refundAmounts, uint256 leafId, address l2TokenAddress, address[] refundAddresses)", ], [ { From 3c2ab4031f0894d022f996589a49bec40d53902d Mon Sep 17 00:00:00 2001 From: Chris Maree Date: Sat, 23 Nov 2024 12:26:01 +0700 Subject: [PATCH 13/29] Discard changes to contracts/Ovm_SpokePool.sol --- contracts/Ovm_SpokePool.sol | 2 -- 1 file changed, 2 deletions(-) diff --git a/contracts/Ovm_SpokePool.sol b/contracts/Ovm_SpokePool.sol index d21531e3c..a3abbfd01 100644 --- a/contracts/Ovm_SpokePool.sol +++ b/contracts/Ovm_SpokePool.sol @@ -34,10 +34,8 @@ interface IL2ERC20Bridge { */ contract Ovm_SpokePool is SpokePool, CircleCCTPAdapter { using SafeERC20 for IERC20; - // "l1Gas" parameter used in call to bridge tokens from this contract back to L1 via IL2ERC20Bridge. Currently // unused by bridge but included for future compatibility. - uint32 public l1Gas; // ETH is an ERC20 on OVM. From a89be73cce0e3dbe012b81594b9e43cbc39193f4 Mon Sep 17 00:00:00 2001 From: Pablo Maldonado Date: Mon, 25 Nov 2024 03:04:15 +0000 Subject: [PATCH 14/29] fix: stack too deep (#766) --- contracts/SpokePool.sol | 93 ++++++------- contracts/interfaces/V3SpokePoolInterface.sol | 16 +++ test/evm/hardhat/SpokePool.Deposit.ts | 127 ++++++++---------- test/evm/hardhat/constants.ts | 16 +++ 4 files changed, 134 insertions(+), 118 deletions(-) diff --git a/contracts/SpokePool.sol b/contracts/SpokePool.sol index 342e81112..c5366f682 100644 --- a/contracts/SpokePool.sol +++ b/contracts/SpokePool.sol @@ -514,21 +514,22 @@ abstract contract SpokePool is uint32 exclusivityParameter, bytes calldata message ) public payable override nonReentrant unpausedDeposits { - _depositV3( - depositor, - recipient, - inputToken.toAddress(), // Input token will always be an address when deposits originate from EVM. - outputToken, - inputAmount, - outputAmount, - destinationChainId, - exclusiveRelayer, - numberOfDeposits++, // Increment count of deposits so that deposit ID for this spoke pool is unique. - quoteTimestamp, - fillDeadline, - exclusivityParameter, - message - ); + DepositV3Params memory params = DepositV3Params({ + depositor: depositor, + recipient: recipient, + inputToken: inputToken, + outputToken: outputToken, + inputAmount: inputAmount, + outputAmount: outputAmount, + destinationChainId: destinationChainId, + exclusiveRelayer: exclusiveRelayer, + depositId: numberOfDeposits++, // Increment count of deposits so that deposit ID for this spoke pool is unique. + quoteTimestamp: quoteTimestamp, + fillDeadline: fillDeadline, + exclusivityParameter: exclusivityParameter, + message: message + }); + _depositV3(params); } /** @@ -1242,24 +1243,10 @@ abstract contract SpokePool is * INTERNAL FUNCTIONS * **************************************/ - function _depositV3( - bytes32 depositor, - bytes32 recipient, - address inputToken, - bytes32 outputToken, - uint256 inputAmount, - uint256 outputAmount, - uint256 destinationChainId, - bytes32 exclusiveRelayer, - uint32 depositId, - uint32 quoteTimestamp, - uint32 fillDeadline, - uint32 exclusivityParameter, - bytes calldata message - ) internal { + function _depositV3(DepositV3Params memory params) internal { // Check that deposit route is enabled for the input token. There are no checks required for the output token // which is pulled from the relayer at fill time and passed through this contract atomically to the recipient. - if (!enabledDepositRoutes[inputToken][destinationChainId]) revert DisabledRoute(); + if (!enabledDepositRoutes[params.inputToken.toAddress()][params.destinationChainId]) revert DisabledRoute(); // Require that quoteTimestamp has a maximum age so that depositors pay an LP fee based on recent HubPool usage. // It is assumed that cross-chain timestamps are normally loosely in-sync, but clock drift can occur. If the @@ -1270,7 +1257,8 @@ abstract contract SpokePool is // slither-disable-next-line timestamp uint256 currentTime = getCurrentTime(); - if (currentTime - quoteTimestamp > depositQuoteTimeBuffer) revert InvalidQuoteTimestamp(); + if (currentTime < params.quoteTimestamp || currentTime - params.quoteTimestamp > depositQuoteTimeBuffer) + revert InvalidQuoteTimestamp(); // fillDeadline is relative to the destination chain. // Don’t allow fillDeadline to be more than several bundles into the future. @@ -1279,7 +1267,8 @@ abstract contract SpokePool is // chain time keeping and this chain's time keeping are out of sync but is not really a practical hurdle // unless they are significantly out of sync or the depositor is setting very short fill deadlines. This latter // situation won't be a problem for honest users. - if (fillDeadline < currentTime || fillDeadline > currentTime + fillDeadlineBuffer) revert InvalidFillDeadline(); + if (params.fillDeadline < currentTime || params.fillDeadline > currentTime + fillDeadlineBuffer) + revert InvalidFillDeadline(); // There are three cases for setting the exclusivity deadline using the exclusivity parameter: // 1. If this parameter is 0, then there is no exclusivity period and emit 0 for the deadline. This @@ -1292,7 +1281,7 @@ abstract contract SpokePool is // 3. Otherwise, interpret this parameter as a timestamp and emit it as the exclusivity deadline. This means // that the filler of this deposit will not assume re-org risk related to the block.timestamp of this // event changing. - uint32 exclusivityDeadline = exclusivityParameter; + uint32 exclusivityDeadline = params.exclusivityParameter; if (exclusivityDeadline > 0) { if (exclusivityDeadline <= MAX_EXCLUSIVITY_PERIOD_SECONDS) { exclusivityDeadline += uint32(currentTime); @@ -1300,14 +1289,14 @@ abstract contract SpokePool is // As a safety measure, prevent caller from inadvertently locking funds during exclusivity period // by forcing them to specify an exclusive relayer. - if (exclusiveRelayer == bytes32(0)) revert InvalidExclusiveRelayer(); + if (params.exclusiveRelayer == bytes32(0)) revert InvalidExclusiveRelayer(); } // If the address of the origin token is a wrappedNativeToken contract and there is a msg.value with the // transaction then the user is sending the native token. In this case, the native token should be // wrapped. - if (inputToken == address(wrappedNativeToken) && msg.value > 0) { - if (msg.value != inputAmount) revert MsgValueDoesNotMatchInputAmount(); + if (params.inputToken == address(wrappedNativeToken).toBytes32() && msg.value > 0) { + if (msg.value != params.inputAmount) revert MsgValueDoesNotMatchInputAmount(); wrappedNativeToken.deposit{ value: msg.value }(); // Else, it is a normal ERC20. In this case pull the token from the caller as per normal. // Note: this includes the case where the L2 caller has WETH (already wrapped ETH) and wants to bridge them. @@ -1315,23 +1304,27 @@ abstract contract SpokePool is } else { // msg.value should be 0 if input token isn't the wrapped native token. if (msg.value != 0) revert MsgValueDoesNotMatchInputAmount(); - IERC20Upgradeable(inputToken).safeTransferFrom(msg.sender, address(this), inputAmount); + IERC20Upgradeable(params.inputToken.toAddress()).safeTransferFrom( + msg.sender, + address(this), + params.inputAmount + ); } emit V3FundsDeposited( - inputToken.toBytes32(), - outputToken, - inputAmount, - outputAmount, - destinationChainId, - depositId, - quoteTimestamp, - fillDeadline, + params.inputToken, + params.outputToken, + params.inputAmount, + params.outputAmount, + params.destinationChainId, + params.depositId, + params.quoteTimestamp, + params.fillDeadline, exclusivityDeadline, - depositor, - recipient, - exclusiveRelayer, - message + params.depositor, + params.recipient, + params.exclusiveRelayer, + params.message ); } diff --git a/contracts/interfaces/V3SpokePoolInterface.sol b/contracts/interfaces/V3SpokePoolInterface.sol index e32db5d23..b93115325 100644 --- a/contracts/interfaces/V3SpokePoolInterface.sol +++ b/contracts/interfaces/V3SpokePoolInterface.sol @@ -91,6 +91,22 @@ interface V3SpokePoolInterface { uint256 updatedOutputAmount; FillType fillType; } + // Represents the parameters required for a V3 deposit operation in the SpokePool. + struct DepositV3Params { + bytes32 depositor; + bytes32 recipient; + bytes32 inputToken; + bytes32 outputToken; + uint256 inputAmount; + uint256 outputAmount; + uint256 destinationChainId; + bytes32 exclusiveRelayer; + uint32 depositId; + uint32 quoteTimestamp; + uint32 fillDeadline; + uint32 exclusivityParameter; + bytes message; + } /************************************** * EVENTS * diff --git a/test/evm/hardhat/SpokePool.Deposit.ts b/test/evm/hardhat/SpokePool.Deposit.ts index 1edcb8111..96fdc1c9a 100644 --- a/test/evm/hardhat/SpokePool.Deposit.ts +++ b/test/evm/hardhat/SpokePool.Deposit.ts @@ -29,28 +29,11 @@ import { originChainId, MAX_EXCLUSIVITY_OFFSET_SECONDS, zeroAddress, + SpokePoolFuncs, } from "./constants"; const { AddressZero: ZERO_ADDRESS } = ethers.constants; -const depositV3Bytes = - "depositV3(bytes32,bytes32,bytes32,bytes32,uint256,uint256,uint256,bytes32,uint32,uint32,uint32,bytes)"; -const depositV3Address = - "depositV3(address,address,address,address,uint256,uint256,uint256,address,uint32,uint32,uint32,bytes)"; - -const depositV3NowBytes = - "depositV3Now(bytes32,bytes32,bytes32,bytes32,uint256,uint256,uint256,bytes32,uint32,uint32,bytes)"; -const depositV3NowAddress = - "depositV3Now(address,address,address,address,uint256,uint256,uint256,address,uint32,uint32,bytes)"; - -const speedUpV3DepositBytes = "speedUpV3Deposit(bytes32,uint32,uint256,bytes32,bytes,bytes)"; -const speedUpV3DepositAddress = "speedUpV3Deposit(address,uint32,uint256,address,bytes,bytes)"; - -const verifyUpdateV3DepositMessageBytes = - "verifyUpdateV3DepositMessage(bytes32,uint32,uint256,uint256,bytes32,bytes,bytes)"; -const verifyUpdateV3DepositMessageAddress = - "verifyUpdateV3DepositMessage(address,uint32,uint256,uint256,address,bytes,bytes)"; - describe("SpokePool Depositor Logic", async function () { let spokePool: Contract, weth: Contract, erc20: Contract, unwhitelistedErc20: Contract; let depositor: SignerWithAddress, recipient: SignerWithAddress; @@ -407,40 +390,44 @@ describe("SpokePool Depositor Logic", async function () { depositArgs = getDepositArgsFromRelayData(relayData); }); it("placeholder: gas test", async function () { - await spokePool.connect(depositor)[depositV3Bytes](...depositArgs); + await spokePool.connect(depositor)[SpokePoolFuncs.depositV3Bytes](...depositArgs); }); it("should allow depositv3 with address overload", async function () { await spokePool .connect(depositor) - [depositV3Address](...getDepositArgsFromRelayData(relayData, destinationChainId, quoteTimestamp, true)); + [SpokePoolFuncs.depositV3Address]( + ...getDepositArgsFromRelayData(relayData, destinationChainId, quoteTimestamp, true) + ); }); it("route disabled", async function () { // Verify that routes are disabled by default for a new route const _depositArgs = getDepositArgsFromRelayData(relayData, 999); - await expect(spokePool.connect(depositor)[depositV3Bytes](..._depositArgs)).to.be.revertedWith("DisabledRoute"); + await expect(spokePool.connect(depositor)[SpokePoolFuncs.depositV3Bytes](..._depositArgs)).to.be.revertedWith( + "DisabledRoute" + ); // Enable the route: await spokePool.connect(depositor).setEnableRoute(erc20.address, 999, true); - await expect(spokePool.connect(depositor)[depositV3Bytes](..._depositArgs)).to.not.be.reverted; + await expect(spokePool.connect(depositor)[SpokePoolFuncs.depositV3Bytes](..._depositArgs)).to.not.be.reverted; }); it("invalid quoteTimestamp", async function () { const quoteTimeBuffer = await spokePool.depositQuoteTimeBuffer(); const currentTime = await spokePool.getCurrentTime(); await expect( - spokePool.connect(depositor)[depositV3Bytes]( + spokePool.connect(depositor)[SpokePoolFuncs.depositV3Bytes]( // quoteTimestamp too far into past (i.e. beyond the buffer) ...getDepositArgsFromRelayData(relayData, destinationChainId, currentTime.sub(quoteTimeBuffer).sub(1)) ) ).to.be.revertedWith("InvalidQuoteTimestamp"); await expect( - spokePool.connect(depositor)[depositV3Bytes]( + spokePool.connect(depositor)[SpokePoolFuncs.depositV3Bytes]( // quoteTimestamp in the future should also revert with InvalidQuoteTimestamp ...getDepositArgsFromRelayData(relayData, destinationChainId, currentTime.add(500)) ) ).to.be.revertedWith("InvalidQuoteTimestamp"); await expect( - spokePool.connect(depositor)[depositV3Bytes]( + spokePool.connect(depositor)[SpokePoolFuncs.depositV3Bytes]( // quoteTimestamp right at the buffer is OK ...getDepositArgsFromRelayData(relayData, destinationChainId, currentTime.sub(quoteTimeBuffer)) ) @@ -451,19 +438,19 @@ describe("SpokePool Depositor Logic", async function () { const currentTime = await spokePool.getCurrentTime(); await expect( - spokePool.connect(depositor)[depositV3Bytes]( + spokePool.connect(depositor)[SpokePoolFuncs.depositV3Bytes]( // fillDeadline too far into future (i.e. beyond the buffer) ...getDepositArgsFromRelayData({ ...relayData, fillDeadline: currentTime.add(fillDeadlineBuffer).add(1) }) ) ).to.be.revertedWith("InvalidFillDeadline"); await expect( - spokePool.connect(depositor)[depositV3Bytes]( + spokePool.connect(depositor)[SpokePoolFuncs.depositV3Bytes]( // fillDeadline in past ...getDepositArgsFromRelayData({ ...relayData, fillDeadline: currentTime.sub(1) }) ) ).to.be.revertedWith("InvalidFillDeadline"); await expect( - spokePool.connect(depositor)[depositV3Bytes]( + spokePool.connect(depositor)[SpokePoolFuncs.depositV3Bytes]( // fillDeadline right at the buffer is OK ...getDepositArgsFromRelayData({ ...relayData, fillDeadline: currentTime.add(fillDeadlineBuffer) }) ) @@ -474,7 +461,7 @@ describe("SpokePool Depositor Logic", async function () { // If exclusive deadline is not zero, then exclusive relayer must be set. await expect( - spokePool.connect(depositor).depositV3( + spokePool.connect(depositor)[SpokePoolFuncs.depositV3Bytes]( ...getDepositArgsFromRelayData({ ...relayData, exclusiveRelayer: zeroAddress, @@ -483,7 +470,7 @@ describe("SpokePool Depositor Logic", async function () { ) ).to.be.revertedWith("InvalidExclusiveRelayer"); await expect( - spokePool.connect(depositor).depositV3( + spokePool.connect(depositor)[SpokePoolFuncs.depositV3Bytes]( ...getDepositArgsFromRelayData({ ...relayData, exclusiveRelayer: zeroAddress, @@ -492,7 +479,7 @@ describe("SpokePool Depositor Logic", async function () { ) ).to.be.revertedWith("InvalidExclusiveRelayer"); await expect( - spokePool.connect(depositor).depositV3( + spokePool.connect(depositor)[SpokePoolFuncs.depositV3Bytes]( ...getDepositArgsFromRelayData({ ...relayData, exclusiveRelayer: zeroAddress, @@ -501,7 +488,7 @@ describe("SpokePool Depositor Logic", async function () { ) ).to.be.revertedWith("InvalidExclusiveRelayer"); await expect( - spokePool.connect(depositor).depositV3( + spokePool.connect(depositor)[SpokePoolFuncs.depositV3Bytes]( ...getDepositArgsFromRelayData({ ...relayData, exclusiveRelayer: zeroAddress, @@ -510,7 +497,7 @@ describe("SpokePool Depositor Logic", async function () { ) ).to.be.revertedWith("InvalidExclusiveRelayer"); await expect( - spokePool.connect(depositor).depositV3( + spokePool.connect(depositor)[SpokePoolFuncs.depositV3Bytes]( ...getDepositArgsFromRelayData({ ...relayData, exclusiveRelayer: zeroAddress, @@ -519,7 +506,7 @@ describe("SpokePool Depositor Logic", async function () { ) ).to.be.revertedWith("InvalidExclusiveRelayer"); await expect( - spokePool.connect(depositor).depositV3( + spokePool.connect(depositor)[SpokePoolFuncs.depositV3Bytes]( ...getDepositArgsFromRelayData({ ...relayData, exclusiveRelayer: zeroAddress, @@ -533,7 +520,7 @@ describe("SpokePool Depositor Logic", async function () { const fillDeadlineOffset = 1000; const exclusivityDeadlineOffset = MAX_EXCLUSIVITY_OFFSET_SECONDS; await expect( - spokePool.connect(depositor).depositV3( + spokePool.connect(depositor)[SpokePoolFuncs.depositV3Bytes]( ...getDepositArgsFromRelayData( { ...relayData, @@ -559,7 +546,7 @@ describe("SpokePool Depositor Logic", async function () { currentTime + exclusivityDeadlineOffset, // exclusivityDeadline should be current time + offset relayData.depositor, relayData.recipient, - depositor.address, + addressToBytes(depositor.address), relayData.message ); }); @@ -568,7 +555,7 @@ describe("SpokePool Depositor Logic", async function () { const fillDeadlineOffset = 1000; const exclusivityDeadlineTimestamp = MAX_EXCLUSIVITY_OFFSET_SECONDS + 1; await expect( - spokePool.connect(depositor).depositV3( + spokePool.connect(depositor)[SpokePoolFuncs.depositV3Bytes]( ...getDepositArgsFromRelayData( { ...relayData, @@ -594,7 +581,7 @@ describe("SpokePool Depositor Logic", async function () { exclusivityDeadlineTimestamp, // exclusivityDeadline should be passed in time relayData.depositor, relayData.recipient, - depositor.address, + addressToBytes(depositor.address), relayData.message ); }); @@ -603,7 +590,7 @@ describe("SpokePool Depositor Logic", async function () { const fillDeadlineOffset = 1000; const zeroExclusivity = 0; await expect( - spokePool.connect(depositor).depositV3( + spokePool.connect(depositor)[SpokePoolFuncs.depositV3Bytes]( ...getDepositArgsFromRelayData( { ...relayData, @@ -629,7 +616,7 @@ describe("SpokePool Depositor Logic", async function () { 0, // Exclusivity deadline should always be 0 relayData.depositor, relayData.recipient, - depositor.address, + addressToBytes(depositor.address), relayData.message ); }); @@ -637,14 +624,16 @@ describe("SpokePool Depositor Logic", async function () { await expect( spokePool .connect(depositor) - [depositV3Bytes](...getDepositArgsFromRelayData({ ...relayData, inputToken: weth.address }), { value: 1 }) + [SpokePoolFuncs.depositV3Bytes](...getDepositArgsFromRelayData({ ...relayData, inputToken: weth.address }), { + value: 1, + }) ).to.be.revertedWith("MsgValueDoesNotMatchInputAmount"); // Pulls ETH from depositor and deposits it into WETH via the wrapped contract. await expect(() => spokePool .connect(depositor) - [depositV3Bytes](...getDepositArgsFromRelayData({ ...relayData, inputToken: weth.address }), { + [SpokePoolFuncs.depositV3Bytes](...getDepositArgsFromRelayData({ ...relayData, inputToken: weth.address }), { value: amountToDeposit, }) ).to.changeEtherBalances([depositor, weth], [amountToDeposit.mul(toBN("-1")), amountToDeposit]); // ETH should transfer from depositor to WETH contract. @@ -654,31 +643,34 @@ describe("SpokePool Depositor Logic", async function () { }); it("if input token is not WETH then msg.value must be 0", async function () { await expect( - spokePool.connect(depositor)[depositV3Bytes](...getDepositArgsFromRelayData(relayData), { value: 1 }) + spokePool + .connect(depositor) + [SpokePoolFuncs.depositV3Bytes](...getDepositArgsFromRelayData(relayData), { value: 1 }) ).to.be.revertedWith("MsgValueDoesNotMatchInputAmount"); }); it("if input token is WETH and msg.value = 0, pulls ERC20 from depositor", async function () { await expect(() => spokePool .connect(depositor) - [depositV3Bytes](...getDepositArgsFromRelayData({ ...relayData, inputToken: weth.address }), { value: 0 }) + [SpokePoolFuncs.depositV3Bytes](...getDepositArgsFromRelayData({ ...relayData, inputToken: weth.address }), { + value: 0, + }) ).to.changeTokenBalances(weth, [depositor, spokePool], [amountToDeposit.mul(toBN("-1")), amountToDeposit]); }); it("pulls input token from caller", async function () { - await expect(() => spokePool.connect(depositor)[depositV3Bytes](...depositArgs)).to.changeTokenBalances( - erc20, - [depositor, spokePool], - [amountToDeposit.mul(toBN("-1")), amountToDeposit] - ); + await expect(() => + spokePool.connect(depositor)[SpokePoolFuncs.depositV3Bytes](...depositArgs) + ).to.changeTokenBalances(erc20, [depositor, spokePool], [amountToDeposit.mul(toBN("-1")), amountToDeposit]); }); it("depositV3Now uses current time as quote time", async function () { const currentTime = (await spokePool.getCurrentTime()).toNumber(); const fillDeadlineOffset = 1000; const exclusivityDeadline = 0; + await expect( spokePool .connect(depositor) - [depositV3NowBytes]( + [SpokePoolFuncs.depositV3NowBytes]( addressToBytes(relayData.depositor), addressToBytes(relayData.recipient), addressToBytes(relayData.inputToken), @@ -703,7 +695,7 @@ describe("SpokePool Depositor Logic", async function () { 0, currentTime, // quoteTimestamp should be current time currentTime + fillDeadlineOffset, // fillDeadline should be current time + offset - currentTime, + exclusivityDeadline, addressToBytes(relayData.depositor), addressToBytes(relayData.recipient), addressToBytes(relayData.exclusiveRelayer), @@ -717,7 +709,7 @@ describe("SpokePool Depositor Logic", async function () { await expect( spokePool .connect(depositor) - [depositV3NowAddress]( + [SpokePoolFuncs.depositV3NowAddress]( bytes32ToAddress(relayData.depositor), bytes32ToAddress(relayData.recipient), bytes32ToAddress(relayData.inputToken), @@ -742,7 +734,7 @@ describe("SpokePool Depositor Logic", async function () { 0, currentTime, // quoteTimestamp should be current time currentTime + fillDeadlineOffset, // fillDeadline should be current time + offset - currentTime, + exclusivityDeadline, addressToBytes(relayData.depositor), addressToBytes(relayData.recipient), addressToBytes(relayData.exclusiveRelayer), @@ -750,8 +742,7 @@ describe("SpokePool Depositor Logic", async function () { ); }); it("emits V3FundsDeposited event with correct deposit ID", async function () { - const currentTime = (await spokePool.getCurrentTime()).toNumber(); - await expect(spokePool.connect(depositor)[depositV3Bytes](...depositArgs)) + await expect(spokePool.connect(depositor)[SpokePoolFuncs.depositV3Bytes](...depositArgs)) .to.emit(spokePool, "V3FundsDeposited") .withArgs( addressToBytes(relayData.inputToken), @@ -763,7 +754,7 @@ describe("SpokePool Depositor Logic", async function () { 0, quoteTimestamp, relayData.fillDeadline, - currentTime, + relayData.exclusivityDeadline, addressToBytes(relayData.depositor), addressToBytes(relayData.recipient), addressToBytes(relayData.exclusiveRelayer), @@ -771,7 +762,7 @@ describe("SpokePool Depositor Logic", async function () { ); }); it("deposit ID state variable incremented", async function () { - await spokePool.connect(depositor)[depositV3Bytes](...depositArgs); + await spokePool.connect(depositor)[SpokePoolFuncs.depositV3Bytes](...depositArgs); expect(await spokePool.numberOfDeposits()).to.equal(1); }); it("tokens are always pulled from caller, even if different from specified depositor", async function () { @@ -780,7 +771,7 @@ describe("SpokePool Depositor Logic", async function () { await expect( spokePool .connect(depositor) - [depositV3Bytes](...getDepositArgsFromRelayData({ ...relayData, depositor: newDepositor })) + [SpokePoolFuncs.depositV3Bytes](...getDepositArgsFromRelayData({ ...relayData, depositor: newDepositor })) ) .to.emit(spokePool, "V3FundsDeposited") .withArgs( @@ -803,12 +794,12 @@ describe("SpokePool Depositor Logic", async function () { }); it("deposits are not paused", async function () { await spokePool.pauseDeposits(true); - await expect(spokePool.connect(depositor)[depositV3Bytes](...depositArgs)).to.be.revertedWith( + await expect(spokePool.connect(depositor)[SpokePoolFuncs.depositV3Bytes](...depositArgs)).to.be.revertedWith( "DepositsArePaused" ); }); it("reentrancy protected", async function () { - const functionCalldata = spokePool.interface.encodeFunctionData(depositV3Bytes, [...depositArgs]); + const functionCalldata = spokePool.interface.encodeFunctionData(SpokePoolFuncs.depositV3Bytes, [...depositArgs]); await expect(spokePool.connect(depositor).callback(functionCalldata)).to.be.revertedWith( "ReentrancyGuard: reentrant call" ); @@ -828,7 +819,7 @@ describe("SpokePool Depositor Logic", async function () { addressToBytes(updatedRecipient), updatedMessage ); - await spokePool[verifyUpdateV3DepositMessageBytes]( + await spokePool[SpokePoolFuncs.verifyUpdateV3DepositMessageBytes]( addressToBytes(depositor.address), depositId, originChainId, @@ -840,7 +831,7 @@ describe("SpokePool Depositor Logic", async function () { // Reverts if passed in depositor is the signer or if signature is incorrect await expect( - spokePool[verifyUpdateV3DepositMessageBytes]( + spokePool[SpokePoolFuncs.verifyUpdateV3DepositMessageBytes]( addressToBytes(updatedRecipient), depositId, originChainId, @@ -861,7 +852,7 @@ describe("SpokePool Depositor Logic", async function () { updatedMessage ); await expect( - spokePool[verifyUpdateV3DepositMessageBytes]( + spokePool[SpokePoolFuncs.verifyUpdateV3DepositMessageBytes]( addressToBytes(depositor.address), depositId, originChainId, @@ -886,7 +877,7 @@ describe("SpokePool Depositor Logic", async function () { await expect( spokePool .connect(depositor) - [speedUpV3DepositBytes]( + [SpokePoolFuncs.speedUpV3DepositBytes]( addressToBytes(depositor.address), depositId, updatedOutputAmount, @@ -916,7 +907,7 @@ describe("SpokePool Depositor Logic", async function () { updatedMessage ); await expect( - spokePool[verifyUpdateV3DepositMessageBytes]( + spokePool[SpokePoolFuncs.verifyUpdateV3DepositMessageBytes]( addressToBytes(depositor.address), depositId, otherChainId, @@ -929,7 +920,7 @@ describe("SpokePool Depositor Logic", async function () { await expect( spokePool .connect(depositor) - [speedUpV3DepositBytes]( + [SpokePoolFuncs.speedUpV3DepositBytes]( addressToBytes(depositor.address), depositId, updatedOutputAmount, @@ -956,7 +947,7 @@ describe("SpokePool Depositor Logic", async function () { true ); - await spokePool[verifyUpdateV3DepositMessageAddress]( + await spokePool[SpokePoolFuncs.verifyUpdateV3DepositMessageAddress]( depositor.address, depositId, spokePoolChainId, @@ -969,7 +960,7 @@ describe("SpokePool Depositor Logic", async function () { await expect( spokePool .connect(depositor) - [speedUpV3DepositAddress]( + [SpokePoolFuncs.speedUpV3DepositAddress]( depositor.address, depositId, updatedOutputAmount, diff --git a/test/evm/hardhat/constants.ts b/test/evm/hardhat/constants.ts index 14c2f6075..c32b60979 100644 --- a/test/evm/hardhat/constants.ts +++ b/test/evm/hardhat/constants.ts @@ -109,3 +109,19 @@ export const sampleRateModel = { R1: toWei(0.07).toString(), R2: toWei(0.75).toString(), }; + +export const SpokePoolFuncs = { + depositV3Bytes: + "depositV3(bytes32,bytes32,bytes32,bytes32,uint256,uint256,uint256,bytes32,uint32,uint32,uint32,bytes)", + depositV3Address: + "depositV3(address,address,address,address,uint256,uint256,uint256,address,uint32,uint32,uint32,bytes)", + depositV3NowBytes: + "depositV3Now(bytes32,bytes32,bytes32,bytes32,uint256,uint256,uint256,bytes32,uint32,uint32,bytes)", + depositV3NowAddress: + "depositV3Now(address,address,address,address,uint256,uint256,uint256,address,uint32,uint32,bytes)", + speedUpV3DepositBytes: "speedUpV3Deposit(bytes32,uint32,uint256,bytes32,bytes,bytes)", + speedUpV3DepositAddress: "speedUpV3Deposit(address,uint32,uint256,address,bytes,bytes)", + verifyUpdateV3DepositMessageBytes: "verifyUpdateV3DepositMessage(bytes32,uint32,uint256,uint256,bytes32,bytes,bytes)", + verifyUpdateV3DepositMessageAddress: + "verifyUpdateV3DepositMessage(address,uint32,uint256,uint256,address,bytes,bytes)", +}; From 56dca16d2fa600a6ec7504d3a379c8eef5404f0c Mon Sep 17 00:00:00 2001 From: chrismaree Date: Mon, 25 Nov 2024 10:08:57 +0700 Subject: [PATCH 15/29] WIP Signed-off-by: chrismaree --- contracts/interfaces/V3SpokePoolInterface.sol | 1 + 1 file changed, 1 insertion(+) diff --git a/contracts/interfaces/V3SpokePoolInterface.sol b/contracts/interfaces/V3SpokePoolInterface.sol index b93115325..0564457f0 100644 --- a/contracts/interfaces/V3SpokePoolInterface.sol +++ b/contracts/interfaces/V3SpokePoolInterface.sol @@ -91,6 +91,7 @@ interface V3SpokePoolInterface { uint256 updatedOutputAmount; FillType fillType; } + // Represents the parameters required for a V3 deposit operation in the SpokePool. struct DepositV3Params { bytes32 depositor; From 5c566fb94687ff651d0232dd0b89a26b4e194a7b Mon Sep 17 00:00:00 2001 From: chrismaree Date: Mon, 25 Nov 2024 10:22:23 +0700 Subject: [PATCH 16/29] WIP Signed-off-by: chrismaree --- contracts/SpokePool.sol | 2 +- contracts/interfaces/V3SpokePoolInterface.sol | 15 +++++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/contracts/SpokePool.sol b/contracts/SpokePool.sol index c5366f682..3aee3715f 100644 --- a/contracts/SpokePool.sol +++ b/contracts/SpokePool.sol @@ -581,7 +581,7 @@ abstract contract SpokePool is uint32 fillDeadline, uint32 exclusivityPeriod, bytes calldata message - ) public payable { + ) public payable override { depositV3( depositor.toBytes32(), recipient.toBytes32(), diff --git a/contracts/interfaces/V3SpokePoolInterface.sol b/contracts/interfaces/V3SpokePoolInterface.sol index 0564457f0..15fa4fa8e 100644 --- a/contracts/interfaces/V3SpokePoolInterface.sol +++ b/contracts/interfaces/V3SpokePoolInterface.sol @@ -197,6 +197,21 @@ interface V3SpokePoolInterface { bytes calldata message ) external payable; + function depositV3( + address depositor, + address recipient, + address inputToken, + address outputToken, + uint256 inputAmount, + uint256 outputAmount, + uint256 destinationChainId, + address exclusiveRelayer, + uint32 quoteTimestamp, + uint32 fillDeadline, + uint32 exclusivityDeadline, + bytes calldata message + ) external payable; + function depositV3Now( bytes32 depositor, bytes32 recipient, From f80e188422ebe10ed9dfde93a9337bef2ae6a85e Mon Sep 17 00:00:00 2001 From: Pablo Maldonado Date: Mon, 25 Nov 2024 07:39:44 +0000 Subject: [PATCH 17/29] Revert "feat: update depositor to bytes32" (#764) This reverts commit 85f00011d5360163172295b7f9d08381c1f253b7. --- contracts/SpokePool.sol | 10 +++++----- contracts/test/MockSpokePool.sol | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/contracts/SpokePool.sol b/contracts/SpokePool.sol index 3aee3715f..9933f7fff 100644 --- a/contracts/SpokePool.sol +++ b/contracts/SpokePool.sol @@ -815,7 +815,7 @@ abstract contract SpokePool is bytes calldata depositorSignature ) public override nonReentrant { _verifyUpdateV3DepositMessage( - depositor, + depositor.toAddress(), depositId, chainId(), updatedOutputAmount, @@ -869,7 +869,7 @@ abstract contract SpokePool is bytes calldata depositorSignature ) public { _verifyUpdateV3DepositMessage( - depositor.toBytes32(), + depositor, depositId, chainId(), updatedOutputAmount, @@ -1009,7 +1009,7 @@ abstract contract SpokePool is }); _verifyUpdateV3DepositMessage( - relayData.depositor, + relayData.depositor.toAddress(), relayData.depositId, relayData.originChainId, updatedOutputAmount, @@ -1496,7 +1496,7 @@ abstract contract SpokePool is } function _verifyUpdateV3DepositMessage( - bytes32 depositor, + address depositor, uint32 depositId, uint256 originChainId, uint256 updatedOutputAmount, @@ -1521,7 +1521,7 @@ abstract contract SpokePool is ), originChainId ); - _verifyDepositorSignature(depositor.toAddress(), expectedTypedDataV4Hash, depositorSignature); + _verifyDepositorSignature(depositor, expectedTypedDataV4Hash, depositorSignature); } // This function is isolated and made virtual to allow different L2's to implement chain specific recovery of diff --git a/contracts/test/MockSpokePool.sol b/contracts/test/MockSpokePool.sol index a19a02138..b9efea1b2 100644 --- a/contracts/test/MockSpokePool.sol +++ b/contracts/test/MockSpokePool.sol @@ -96,7 +96,7 @@ contract MockSpokePool is SpokePool, MockV2SpokePoolInterface, OwnableUpgradeabl ) public view { return _verifyUpdateV3DepositMessage( - depositor, + depositor.toAddress(), depositId, originChainId, updatedOutputAmount, @@ -118,7 +118,7 @@ contract MockSpokePool is SpokePool, MockV2SpokePoolInterface, OwnableUpgradeabl ) public view { return _verifyUpdateV3DepositMessage( - depositor.toBytes32(), + depositor, depositId, originChainId, updatedOutputAmount, From 79bf5b83c5fc96c033b10c3ade5c67e79d9f9d2e Mon Sep 17 00:00:00 2001 From: Chris Maree Date: Mon, 25 Nov 2024 15:01:40 +0700 Subject: [PATCH 18/29] Discard changes to contracts/PolygonZkEVM_SpokePool.sol --- contracts/PolygonZkEVM_SpokePool.sol | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/contracts/PolygonZkEVM_SpokePool.sol b/contracts/PolygonZkEVM_SpokePool.sol index b9367a75d..54251d88c 100644 --- a/contracts/PolygonZkEVM_SpokePool.sol +++ b/contracts/PolygonZkEVM_SpokePool.sol @@ -31,9 +31,8 @@ interface IBridgeMessageReceiver { */ contract PolygonZkEVM_SpokePool is SpokePool, IBridgeMessageReceiver { using SafeERC20 for IERC20; - using AddressToBytes32 for address; - // Address of Polygon zkEVM's Canonical Bridge on L2. + // Address of Polygon zkEVM's Canonical Bridge on L2. IPolygonZkEVMBridge public l2PolygonZkEVMBridge; // Polygon zkEVM's internal network id for L1. From aa48a3d8a4d1060d7128bb5a57d07e9afeae64cf Mon Sep 17 00:00:00 2001 From: Chris Maree Date: Mon, 25 Nov 2024 15:01:45 +0700 Subject: [PATCH 19/29] Discard changes to contracts/Polygon_SpokePool.sol --- contracts/Polygon_SpokePool.sol | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/contracts/Polygon_SpokePool.sol b/contracts/Polygon_SpokePool.sol index 990c987af..a1d630ded 100644 --- a/contracts/Polygon_SpokePool.sol +++ b/contracts/Polygon_SpokePool.sol @@ -146,8 +146,7 @@ contract Polygon_SpokePool is IFxMessageProcessor, SpokePool, CircleCCTPAdapter * @param data ABI encoded function call to execute on this contract. */ function processMessageFromRoot( - uint256, - /*stateId*/ + uint256, /*stateId*/ address rootMessageSender, bytes calldata data ) public validateInternalCalls { @@ -204,6 +203,7 @@ contract Polygon_SpokePool is IFxMessageProcessor, SpokePool, CircleCCTPAdapter * whereby someone batches this call with a bunch of other calls and produces a very large L2 burn transaction. * This might make the L2 -> L1 message fail due to exceeding the L1 calldata limit. */ + function executeRelayerRefundLeaf( uint32 rootBundleId, SpokePoolInterface.RelayerRefundLeaf memory relayerRefundLeaf, @@ -220,6 +220,7 @@ contract Polygon_SpokePool is IFxMessageProcessor, SpokePool, CircleCCTPAdapter /************************************** * INTERNAL FUNCTIONS * **************************************/ + function _setFxChild(address _fxChild) internal { //slither-disable-next-line missing-zero-check fxChild = _fxChild; From 44bfd94a3f95b1a165c995f233a83ad6b81086ed Mon Sep 17 00:00:00 2001 From: Chris Maree Date: Mon, 25 Nov 2024 22:29:25 +0700 Subject: [PATCH 20/29] fix: make event case consistant between evm & svm (#760) --- contracts/SpokePool.sol | 4 ++-- test/evm/hardhat/SpokePool.Admin.ts | 2 +- test/svm/SvmSpoke.HandleReceiveMessage.ts | 12 ++++++------ 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/contracts/SpokePool.sol b/contracts/SpokePool.sol index 9933f7fff..96a6c770c 100644 --- a/contracts/SpokePool.sol +++ b/contracts/SpokePool.sol @@ -189,7 +189,7 @@ abstract contract SpokePool is bytes32 indexed l2TokenAddress, address caller ); - event EmergencyDeleteRootBundle(uint256 indexed rootBundleId); + event EmergencyDeletedRootBundle(uint256 indexed rootBundleId); event PausedDeposits(bool isPaused); event PausedFills(bool isPaused); @@ -356,7 +356,7 @@ abstract contract SpokePool is // would require a new list in storage to keep track of keys. //slither-disable-next-line mapping-deletion delete rootBundles[rootBundleId]; - emit EmergencyDeleteRootBundle(rootBundleId); + emit EmergencyDeletedRootBundle(rootBundleId); } /************************************** diff --git a/test/evm/hardhat/SpokePool.Admin.ts b/test/evm/hardhat/SpokePool.Admin.ts index 149a74909..1818e10bd 100644 --- a/test/evm/hardhat/SpokePool.Admin.ts +++ b/test/evm/hardhat/SpokePool.Admin.ts @@ -49,7 +49,7 @@ describe("SpokePool Admin Functions", async function () { expect(await spokePool.rootBundles(0)).has.property("relayerRefundRoot", mockRelayerRefundRoot); await expect(spokePool.connect(owner).emergencyDeleteRootBundle(0)) - .to.emit(spokePool, "EmergencyDeleteRootBundle") + .to.emit(spokePool, "EmergencyDeletedRootBundle") .withArgs(0); expect(await spokePool.rootBundles(0)).has.property("slowRelayRoot", ethers.utils.hexZeroPad("0x0", 32)); diff --git a/test/svm/SvmSpoke.HandleReceiveMessage.ts b/test/svm/SvmSpoke.HandleReceiveMessage.ts index b29fb4f3f..638c0532b 100644 --- a/test/svm/SvmSpoke.HandleReceiveMessage.ts +++ b/test/svm/SvmSpoke.HandleReceiveMessage.ts @@ -521,34 +521,34 @@ describe("svm_spoke.handle_receive_message", () => { messageBody, }); - // Remaining accounts specific to EmergencyDeleteRootBundle. + // Remaining accounts specific to EmergencyDeletedRootBundle. // Same 3 remaining accounts passed for HandleReceiveMessage context. const emergencyDeleteRootBundleRemainingAccounts = remainingAccounts.slice(0, 3); - // closer in self-invoked EmergencyDeleteRootBundle. + // closer in self-invoked EmergencyDeletedRootBundle. emergencyDeleteRootBundleRemainingAccounts.push({ isSigner: true, isWritable: true, pubkey: provider.wallet.publicKey, }); - // state in self-invoked EmergencyDeleteRootBundle. + // state in self-invoked EmergencyDeletedRootBundle. emergencyDeleteRootBundleRemainingAccounts.push({ isSigner: false, isWritable: false, pubkey: state, }); - // root_bundle in self-invoked EmergencyDeleteRootBundle. + // root_bundle in self-invoked EmergencyDeletedRootBundle. emergencyDeleteRootBundleRemainingAccounts.push({ isSigner: false, isWritable: true, pubkey: rootBundle, }); - // event_authority in self-invoked EmergencyDeleteRootBundle (appended by Anchor with event_cpi macro). + // event_authority in self-invoked EmergencyDeletedRootBundle (appended by Anchor with event_cpi macro). emergencyDeleteRootBundleRemainingAccounts.push({ isSigner: false, isWritable: false, pubkey: eventAuthority, }); - // program in self-invoked EmergencyDeleteRootBundle (appended by Anchor with event_cpi macro). + // program in self-invoked EmergencyDeletedRootBundle (appended by Anchor with event_cpi macro). emergencyDeleteRootBundleRemainingAccounts.push({ isSigner: false, isWritable: false, From d1f7a3adbcb3b62165dd72bb1c59b53351fb104a Mon Sep 17 00:00:00 2001 From: nicholaspai <9457025+nicholaspai@users.noreply.github.com> Date: Mon, 25 Nov 2024 12:42:27 -0500 Subject: [PATCH 21/29] feat(SpokePool): Remove depositExclusive (#642) This function was used to express exclusivity as a period but its no longer useful since depositV3 now allows caller to express exclusivityPeriod instead of exclusivityDeadline --- contracts/SpokePool.sol | 68 ----------------------------------------- 1 file changed, 68 deletions(-) diff --git a/contracts/SpokePool.sol b/contracts/SpokePool.sol index 96a6c770c..cdaaec7f7 100644 --- a/contracts/SpokePool.sol +++ b/contracts/SpokePool.sol @@ -720,74 +720,6 @@ abstract contract SpokePool is ); } - /** - * @notice DEPRECATED. Use depositV3() instead. - * @notice Submits deposit and sets exclusivityDeadline to current time plus some offset. This function is - * designed to be called by users who want to set an exclusive relayer for some amount of time after their deposit - * transaction is mined. - * @notice If exclusivtyDeadlineOffset > 0, then exclusiveRelayer must be set to a valid address, which is a - * requirement imposed by depositV3(). - * @param depositor The account credited with the deposit who can request to "speed up" this deposit by modifying - * the output amount, recipient, and message. - * @param recipient The account receiving funds on the destination chain. Can be an EOA or a contract. If - * the output token is the wrapped native token for the chain, then the recipient will receive native token if - * an EOA or wrapped native token if a contract. - * @param inputToken The token pulled from the caller's account and locked into this contract to - * initiate the deposit. The equivalent of this token on the relayer's repayment chain of choice will be sent - * as a refund. If this is equal to the wrapped native token then the caller can optionally pass in native token as - * msg.value, as long as msg.value = inputTokenAmount. - * @param outputToken The token that the relayer will send to the recipient on the destination chain. Must be an - * ERC20. - * @param inputAmount The amount of input tokens to pull from the caller's account and lock into this contract. - * This amount will be sent to the relayer on their repayment chain of choice as a refund following an optimistic - * challenge window in the HubPool, plus a system fee. - * @param outputAmount The amount of output tokens that the relayer will send to the recipient on the destination. - * @param destinationChainId The destination chain identifier. Must be enabled along with the input token - * as a valid deposit route from this spoke pool or this transaction will revert. - * @param exclusiveRelayer The relayer that will be exclusively allowed to fill this deposit before the - * exclusivity deadline timestamp. - * @param quoteTimestamp The HubPool timestamp that is used to determine the system fee paid by the depositor. - * This must be set to some time between [currentTime - depositQuoteTimeBuffer, currentTime] - * where currentTime is block.timestamp on this chain or this transaction will revert. - * @param fillDeadline The deadline for the relayer to fill the deposit. After this destination chain timestamp, - * the fill will revert on the destination chain. Must be set between [currentTime, currentTime + fillDeadlineBuffer] - * where currentTime is block.timestamp on this chain or this transaction will revert. - * @param exclusivityPeriod Added to the current time to set the exclusive relayer deadline, - * which is the deadline for the exclusiveRelayer to fill the deposit. After this destination chain timestamp, - * anyone can fill the deposit. - * @param message The message to send to the recipient on the destination chain if the recipient is a contract. - * If the message is not empty, the recipient contract must implement handleV3AcrossMessage() or the fill will revert. - */ - function depositExclusive( - address depositor, - address recipient, - address inputToken, - address outputToken, - uint256 inputAmount, - uint256 outputAmount, - uint256 destinationChainId, - address exclusiveRelayer, - uint32 quoteTimestamp, - uint32 fillDeadline, - uint32 exclusivityPeriod, - bytes calldata message - ) public payable { - depositV3( - depositor.toBytes32(), - recipient.toBytes32(), - inputToken.toBytes32(), - outputToken.toBytes32(), - inputAmount, - outputAmount, - destinationChainId, - exclusiveRelayer.toBytes32(), - quoteTimestamp, - fillDeadline, - exclusivityPeriod, - message - ); - } - /** * @notice Depositor can use this function to signal to relayer to use updated output amount, recipient, * and/or message. From 9905481d1cca9c57d3dd306a820f4a6f30b9f32c Mon Sep 17 00:00:00 2001 From: Matt Rice Date: Mon, 25 Nov 2024 12:46:14 -0500 Subject: [PATCH 22/29] feat: Introduce opt-in deterministic relay data hashes (again) (#639) * Revert "feat(SpokePool): Introduce opt-in deterministic relay data hashes (#583)" This reverts commit 9d21d1ba12ef10751e1b33e09556b6c0b10883cc. * Reapply "feat(SpokePool): Introduce opt-in deterministic relay data hashes (#583)" This reverts commit d363bf0e671082303f3a79cf8998848c766bd8dc. * add deposit nonces to 7683 Signed-off-by: Matt Rice * fix Signed-off-by: Matt Rice * WIP Signed-off-by: Matt Rice * feat(SpokePool): Introduce opt-in deterministic relay data hashes (#583) * fix(SpokePool): Apply exclusivity consistently The new relative exclusivity check has not been propagated to fillV3RelayWithUpdatedDeposit(). Identified via test case failures in the relayer. Signed-off-by: Paul <108695806+pxrl@users.noreply.github.com> * Also check on slow fill requests * Update contracts/SpokePool.sol * lint * Update * Add pure * Fix * Add tests * improve(SpokePool): _depositV3 interprets `exclusivityParameter` as 0, an offset, or a timestamp There should be a way for the deposit transaction to remove chain re-org risk affecting the block.timestamp by allowing the caller to set a fixed `exclusivityDeadline` value. This supports the existing behavior where the `exclusivityDeadline` is always emitted as its passed in. The new behavior is that if the `exclusivityParameter`, which replaces the `exclusivityDeadlineOffset` parameter, is 0 or greater than 1 year in seconds, then the `exclusivityDeadline` is equal to this parameter. Otherwise, its interpreted by `_depositV3()` as an offset. The offset would be useful in cases where the origin chain will not re-org, for example. * Update SpokePool.sol * Update SpokePool.Relay.ts * Update SpokePool.SlowRelay.ts * Update contracts/SpokePool.sol Co-authored-by: Paul <108695806+pxrl@users.noreply.github.com> * Update SpokePool.sol * Update contracts/SpokePool.sol * rebase * Update SpokePool.sol * Revert "Merge branch 'npai/exclusivity-switch' into mrice32/deterministic-new" This reverts commit 24329445ad3612efdc11be0b2dd66f753602cef0, reversing changes made to 6fe353460cd3152f95756d68e83e0514b56acd4f. * Revert "Merge branch 'npai/exclusivity-switch' into mrice32/deterministic-new" This reverts commit 24329445ad3612efdc11be0b2dd66f753602cef0, reversing changes made to 6fe353460cd3152f95756d68e83e0514b56acd4f. * revert * Update SpokePool.sol * Fix * Update SpokePool.sol Co-authored-by: Chris Maree * WIP * WIP * wip * Update SpokePool.Relay.ts * Fix * Update SpokePool.sol * Update SpokePool.sol --------- Signed-off-by: Matt Rice Signed-off-by: Paul <108695806+pxrl@users.noreply.github.com> Co-authored-by: nicholaspai Co-authored-by: nicholaspai <9457025+nicholaspai@users.noreply.github.com> Co-authored-by: Paul <108695806+pxrl@users.noreply.github.com> Co-authored-by: Chris Maree --- contracts/SpokePool.sol | 173 ++++++++++++++++-- contracts/erc7683/ERC7683OrderDepositor.sol | 20 +- .../erc7683/ERC7683OrderDepositorExternal.sol | 55 ++++-- contracts/erc7683/ERC7683Permit2Lib.sol | 2 + contracts/interfaces/V3SpokePoolInterface.sol | 14 +- contracts/test/MockSpokePool.sol | 8 +- test/evm/hardhat/SpokePool.Deposit.ts | 60 ++++++ test/evm/hardhat/constants.ts | 11 +- .../evm/hardhat/fixtures/SpokePool.Fixture.ts | 2 +- 9 files changed, 297 insertions(+), 48 deletions(-) diff --git a/contracts/SpokePool.sol b/contracts/SpokePool.sol index cdaaec7f7..362ef30af 100644 --- a/contracts/SpokePool.sol +++ b/contracts/SpokePool.sol @@ -60,7 +60,12 @@ abstract contract SpokePool is WETH9Interface private DEPRECATED_wrappedNativeToken; uint32 private DEPRECATED_depositQuoteTimeBuffer; - // Count of deposits is used to construct a unique deposit identifier for this spoke pool. + // Count of deposits is used to construct a unique deposit identifier for this spoke pool. This value + // gets emitted and incremented on each depositV3 call. Because its a uint32, it will get implicitly cast to + // uint256 in the emitted V3FundsDeposited event by setting its most significant bits to 0. + // This variable name `numberOfDeposits` should ideally be re-named to + // depositNonceCounter or something similar because its not a true representation of the number of deposits + // because `unsafeDepositV3` can be called directly and bypass this increment. uint32 public numberOfDeposits; // Whether deposits and fills are disabled. @@ -135,12 +140,12 @@ abstract contract SpokePool is bytes32 public constant UPDATE_V3_DEPOSIT_DETAILS_HASH = keccak256( - "UpdateDepositDetails(uint32 depositId,uint256 originChainId,uint256 updatedOutputAmount,bytes32 updatedRecipient,bytes updatedMessage)" + "UpdateDepositDetails(uint256 depositId,uint256 originChainId,uint256 updatedOutputAmount,bytes32 updatedRecipient,bytes updatedMessage)" ); bytes32 public constant UPDATE_V3_DEPOSIT_ADDRESS_OVERLOAD_DETAILS_HASH = keccak256( - "UpdateDepositDetails(uint32 depositId,uint256 originChainId,uint256 updatedOutputAmount,address updatedRecipient,bytes updatedMessage)" + "UpdateDepositDetails(uint256 depositId,uint256 originChainId,uint256 updatedOutputAmount,address updatedRecipient,bytes updatedMessage)" ); // Default chain Id used to signify that no repayment is requested, for example when executing a slow fill. @@ -487,7 +492,7 @@ abstract contract SpokePool is * the fill will revert on the destination chain. Must be set between [currentTime, currentTime + fillDeadlineBuffer] * where currentTime is block.timestamp on this chain or this transaction will revert. * @param exclusivityParameter This value is used to set the exclusivity deadline timestamp in the emitted deposit - * event. Before this destinationchain timestamp, only the exclusiveRelayer (if set to a non-zero address), + * event. Before this destination chain timestamp, only the exclusiveRelayer (if set to a non-zero address), * can fill this deposit. There are three ways to use this parameter: * 1. NO EXCLUSIVITY: If this value is set to 0, then a timestamp of 0 will be emitted, * meaning that there is no exclusivity period. @@ -514,6 +519,13 @@ abstract contract SpokePool is uint32 exclusivityParameter, bytes calldata message ) public payable override nonReentrant unpausedDeposits { + // Increment deposit nonce variable `numberOfDeposits` so that deposit ID for this deposit on this + // spoke pool is unique. This variable `numberOfDeposits` should ideally be re-named to + // depositNonceCounter or something similar because its not a true representation of the number of deposits + // because `unsafeDepositV3` can be called directly and bypass this increment. + // The `numberOfDeposits` is a uint32 that will get implicitly cast to uint256 by setting the + // most significant bits to 0, which creates very little chance this an unsafe deposit ID collides + // with a safe deposit ID. DepositV3Params memory params = DepositV3Params({ depositor: depositor, recipient: recipient, @@ -523,7 +535,7 @@ abstract contract SpokePool is outputAmount: outputAmount, destinationChainId: destinationChainId, exclusiveRelayer: exclusiveRelayer, - depositId: numberOfDeposits++, // Increment count of deposits so that deposit ID for this spoke pool is unique. + depositId: numberOfDeposits++, quoteTimestamp: quoteTimestamp, fillDeadline: fillDeadline, exclusivityParameter: exclusivityParameter, @@ -563,8 +575,17 @@ abstract contract SpokePool is * @param fillDeadline The deadline for the relayer to fill the deposit. After this destination chain timestamp, the fill will * revert on the destination chain. Must be set between [currentTime, currentTime + fillDeadlineBuffer] where currentTime * is block.timestamp on this chain. - * @param exclusivityPeriod Added to the current time to set the exclusive relayer deadline. After this timestamp, - * anyone can fill the deposit. + * @param exclusivityParameter This value is used to set the exclusivity deadline timestamp in the emitted deposit + * event. Before this destination chain timestamp, only the exclusiveRelayer (if set to a non-zero address), + * can fill this deposit. There are three ways to use this parameter: + * 1. NO EXCLUSIVITY: If this value is set to 0, then a timestamp of 0 will be emitted, + * meaning that there is no exclusivity period. + * 2. OFFSET: If this value is less than MAX_EXCLUSIVITY_PERIOD_SECONDS, then add this value to + * the block.timestamp to derive the exclusive relayer deadline. Note that using the parameter in this way + * will expose the filler of the deposit to the risk that the block.timestamp of this event gets changed + * due to a chain-reorg, which would also change the exclusivity timestamp. + * 3. TIMESTAMP: Otherwise, set this value as the exclusivity deadline timestamp. + * which is the deadline for the exclusiveRelayer to fill the deposit. * @param message The message to send to the recipient on the destination chain if the recipient is a contract. If the * message is not empty, the recipient contract must implement `handleV3AcrossMessage()` or the fill will revert. */ @@ -579,7 +600,7 @@ abstract contract SpokePool is address exclusiveRelayer, uint32 quoteTimestamp, uint32 fillDeadline, - uint32 exclusivityPeriod, + uint32 exclusivityParameter, bytes calldata message ) public payable override { depositV3( @@ -593,11 +614,121 @@ abstract contract SpokePool is exclusiveRelayer.toBytes32(), quoteTimestamp, fillDeadline, - exclusivityPeriod, + exclusivityParameter, message ); } + /** + * @notice An overloaded version of `unsafeDepositV3` that accepts `address` types for backward compatibility. * + * @dev This version mirrors the original `unsafeDepositV3` function, but uses `address` types for `depositor`, `recipient`, + * `inputToken`, `outputToken`, and `exclusiveRelayer` for compatibility with contracts using the `address` type. + * + * The key functionality and logic remain identical, ensuring interoperability across both versions. + */ + function unsafeDepositV3( + address depositor, + address recipient, + address inputToken, + address outputToken, + uint256 inputAmount, + uint256 outputAmount, + uint256 destinationChainId, + address exclusiveRelayer, + uint256 depositNonce, + uint32 quoteTimestamp, + uint32 fillDeadline, + uint32 exclusivityParameter, + bytes calldata message + ) public payable { + unsafeDepositV3( + depositor.toBytes32(), + recipient.toBytes32(), + inputToken.toBytes32(), + outputToken.toBytes32(), + inputAmount, + outputAmount, + destinationChainId, + exclusiveRelayer.toBytes32(), + depositNonce, + quoteTimestamp, + fillDeadline, + exclusivityParameter, + message + ); + } + + /** + * @notice See depositV3 for details. This function is identical to depositV3 except that it does not use the + * global deposit ID counter as a deposit nonce, instead allowing the caller to pass in a deposit nonce. This + * function is designed to be used by anyone who wants to pre-compute their resultant relay data hash, which + * could be useful for filling a deposit faster and avoiding any risk of a relay hash unexpectedly changing + * due to another deposit front-running this one and incrementing the global deposit ID counter. + * @dev This is labeled "unsafe" because there is no guarantee that the depositId emitted in the resultant + * V3FundsDeposited event is unique which means that the + * corresponding fill might collide with an existing relay hash on the destination chain SpokePool, + * which would make this deposit unfillable. In this case, the depositor would subsequently receive a refund + * of `inputAmount` of `inputToken` on the origin chain after the fill deadline. + * @dev On the destination chain, the hash of the deposit data will be used to uniquely identify this deposit, so + * modifying any params in it will result in a different hash and a different deposit. The hash will comprise + * all parameters to this function along with this chain's chainId(). Relayers are only refunded for filling + * deposits with deposit hashes that map exactly to the one emitted by this contract. + * @param depositNonce The nonce that uniquely identifies this deposit. This function will combine this parameter + * with the msg.sender address to create a unique uint256 depositNonce and ensure that the msg.sender cannot + * use this function to front-run another depositor's unsafe deposit. This function guarantees that the resultant + * deposit nonce will not collide with a safe uint256 deposit nonce whose 24 most significant bytes are always 0. + * @param depositor See identically named parameter in depositV3() comments. + * @param recipient See identically named parameter in depositV3() comments. + * @param inputToken See identically named parameter in depositV3() comments. + * @param outputToken See identically named parameter in depositV3() comments. + * @param inputAmount See identically named parameter in depositV3() comments. + * @param outputAmount See identically named parameter in depositV3() comments. + * @param destinationChainId See identically named parameter in depositV3() comments. + * @param exclusiveRelayer See identically named parameter in depositV3() comments. + * @param quoteTimestamp See identically named parameter in depositV3() comments. + * @param fillDeadline See identically named parameter in depositV3() comments. + * @param exclusivityParameter See identically named parameter in depositV3() comments. + * @param message See identically named parameter in depositV3() comments. + */ + function unsafeDepositV3( + bytes32 depositor, + bytes32 recipient, + bytes32 inputToken, + bytes32 outputToken, + uint256 inputAmount, + uint256 outputAmount, + uint256 destinationChainId, + bytes32 exclusiveRelayer, + uint256 depositNonce, + uint32 quoteTimestamp, + uint32 fillDeadline, + uint32 exclusivityParameter, + bytes calldata message + ) public payable nonReentrant unpausedDeposits { + // @dev Create the uint256 deposit ID by concatenating the msg.sender and depositor address with the inputted + // depositNonce parameter. The resultant 32 byte string will be hashed and then casted to an "unsafe" + // uint256 deposit ID. The probability that the resultant ID collides with a "safe" deposit ID is + // equal to the chance that the first 28 bytes of the hash are 0, which is too small for us to consider. + + uint256 depositId = getUnsafeDepositId(msg.sender, depositor, depositNonce); + DepositV3Params memory params = DepositV3Params({ + depositor: depositor, + recipient: recipient, + inputToken: inputToken, + outputToken: outputToken, + inputAmount: inputAmount, + outputAmount: outputAmount, + destinationChainId: destinationChainId, + exclusiveRelayer: exclusiveRelayer, + depositId: depositId, + quoteTimestamp: quoteTimestamp, + fillDeadline: fillDeadline, + exclusivityParameter: exclusivityParameter, + message: message + }); + _depositV3(params); + } + /** * @notice Submits deposit and sets quoteTimestamp to current Time. Sets fill and exclusivity * deadlines as offsets added to the current time. This function is designed to be called by users @@ -740,7 +871,7 @@ abstract contract SpokePool is */ function speedUpV3Deposit( bytes32 depositor, - uint32 depositId, + uint256 depositId, uint256 updatedOutputAmount, bytes32 updatedRecipient, bytes calldata updatedMessage, @@ -794,7 +925,7 @@ abstract contract SpokePool is */ function speedUpV3Deposit( address depositor, - uint32 depositId, + uint256 depositId, uint256 updatedOutputAmount, address updatedRecipient, bytes calldata updatedMessage, @@ -1167,6 +1298,24 @@ abstract contract SpokePool is return block.timestamp; // solhint-disable-line not-rely-on-time } + /** + * @notice Returns the deposit ID for an unsafe deposit. This function is used to compute the deposit ID + * in unsafeDepositV3 and is provided as a convenience. + * @dev msgSenderand depositor are both used as inputs to allow passthrough depositors to create unique + * deposit hash spaces for unique depositors. + * @param msgSender The caller of the transaction used as input to produce the deposit ID. + * @param depositor The depositor address used as input to produce the deposit ID. + * @param depositNonce The nonce used as input to produce the deposit ID. + * @return The deposit ID for the unsafe deposit. + */ + function getUnsafeDepositId( + address msgSender, + bytes32 depositor, + uint256 depositNonce + ) public pure returns (uint256) { + return uint256(keccak256(abi.encodePacked(msgSender, depositor, depositNonce))); + } + function getRelayerRefund(address l2TokenAddress, address refundAddress) public view returns (uint256) { return relayerRefund[l2TokenAddress][refundAddress]; } @@ -1429,7 +1578,7 @@ abstract contract SpokePool is function _verifyUpdateV3DepositMessage( address depositor, - uint32 depositId, + uint256 depositId, uint256 originChainId, uint256 updatedOutputAmount, bytes32 updatedRecipient, diff --git a/contracts/erc7683/ERC7683OrderDepositor.sol b/contracts/erc7683/ERC7683OrderDepositor.sol index 05e274ffe..a6bcd4dcd 100644 --- a/contracts/erc7683/ERC7683OrderDepositor.sol +++ b/contracts/erc7683/ERC7683OrderDepositor.sol @@ -73,6 +73,7 @@ abstract contract ERC7683OrderDepositor is IOriginSettler { acrossOrderData.outputAmount, acrossOrderData.destinationChainId, acrossOriginFillerData.exclusiveRelayer, + acrossOrderData.depositNonce, // Note: simplifying assumption to avoid quote timestamps that cause orders to expire before the deadline. SafeCast.toUint32(order.openDeadline - QUOTE_BEFORE_DEADLINE), order.fillDeadline, @@ -103,6 +104,7 @@ abstract contract ERC7683OrderDepositor is IOriginSettler { acrossOrderData.outputAmount, acrossOrderData.destinationChainId, acrossOrderData.exclusiveRelayer, + acrossOrderData.depositNonce, // Note: simplifying assumption to avoid the order type having to bake in the quote timestamp. SafeCast.toUint32(block.timestamp), order.fillDeadline, @@ -161,6 +163,17 @@ abstract contract ERC7683OrderDepositor is IOriginSettler { return SafeCast.toUint32(block.timestamp); // solhint-disable-line not-rely-on-time } + /** + * @notice Convenience method to compute the Across depositId for orders sent through 7683. + * @dev if a 0 depositNonce is used, the depositId will not be deterministic (meaning it can change depending on + * when the open txn is mined), but you will be safe from collisions. See the unsafeDepositV3 method on SpokePool + * for more details on how to choose between deterministic and non-deterministic. + * @param depositNonce the depositNonce field in the order. + * @param depositor the sender or signer of the order. + * @return the resulting Across depositId. + */ + function computeDepositId(uint256 depositNonce, address depositor) public view virtual returns (uint256); + function _resolveFor(GaslessCrossChainOrder calldata order, bytes calldata fillerData) internal view @@ -223,7 +236,7 @@ abstract contract ERC7683OrderDepositor is IOriginSettler { relayData.inputAmount = acrossOrderData.inputAmount; relayData.outputAmount = acrossOrderData.outputAmount; relayData.originChainId = block.chainid; - relayData.depositId = _currentDepositId(); + relayData.depositId = computeDepositId(acrossOrderData.depositNonce, order.user); relayData.fillDeadline = order.fillDeadline; relayData.exclusivityDeadline = acrossOrderData.exclusivityPeriod; relayData.message = acrossOrderData.message; @@ -287,7 +300,7 @@ abstract contract ERC7683OrderDepositor is IOriginSettler { relayData.inputAmount = acrossOrderData.inputAmount; relayData.outputAmount = acrossOrderData.outputAmount; relayData.originChainId = block.chainid; - relayData.depositId = _currentDepositId(); + relayData.depositId = computeDepositId(acrossOrderData.depositNonce, msg.sender); relayData.fillDeadline = order.fillDeadline; relayData.exclusivityDeadline = acrossOrderData.exclusivityPeriod; relayData.message = acrossOrderData.message; @@ -357,13 +370,12 @@ abstract contract ERC7683OrderDepositor is IOriginSettler { uint256 outputAmount, uint256 destinationChainId, address exclusiveRelayer, + uint256 depositNonce, uint32 quoteTimestamp, uint32 fillDeadline, uint32 exclusivityPeriod, bytes memory message ) internal virtual; - function _currentDepositId() internal view virtual returns (uint32); - function _destinationSettler(uint256 chainId) internal view virtual returns (address); } diff --git a/contracts/erc7683/ERC7683OrderDepositorExternal.sol b/contracts/erc7683/ERC7683OrderDepositorExternal.sol index 9cefb19f1..82371f9e1 100644 --- a/contracts/erc7683/ERC7683OrderDepositorExternal.sol +++ b/contracts/erc7683/ERC7683OrderDepositorExternal.sol @@ -15,6 +15,7 @@ import "@uma/core/contracts/common/implementation/MultiCaller.sol"; */ contract ERC7683OrderDepositorExternal is ERC7683OrderDepositor, Ownable, MultiCaller { using SafeERC20 for IERC20; + using AddressToBytes32 for address; event SetDestinationSettler( uint256 indexed chainId, @@ -50,6 +51,7 @@ contract ERC7683OrderDepositorExternal is ERC7683OrderDepositor, Ownable, MultiC uint256 outputAmount, uint256 destinationChainId, address exclusiveRelayer, + uint256 depositNonce, uint32 quoteTimestamp, uint32 fillDeadline, uint32 exclusivityDeadline, @@ -57,24 +59,45 @@ contract ERC7683OrderDepositorExternal is ERC7683OrderDepositor, Ownable, MultiC ) internal override { IERC20(inputToken).forceApprove(address(SPOKE_POOL), inputAmount); - SPOKE_POOL.depositV3( - depositor, - recipient, - inputToken, - outputToken, - inputAmount, - outputAmount, - destinationChainId, - exclusiveRelayer, - quoteTimestamp, - fillDeadline, - exclusivityDeadline, - message - ); + if (depositNonce == 0) { + SPOKE_POOL.depositV3( + depositor, + recipient, + inputToken, + outputToken, + inputAmount, + outputAmount, + destinationChainId, + exclusiveRelayer, + quoteTimestamp, + fillDeadline, + exclusivityDeadline, + message + ); + } else { + SPOKE_POOL.unsafeDepositV3( + depositor, + recipient, + inputToken, + outputToken, + inputAmount, + outputAmount, + destinationChainId, + exclusiveRelayer, + depositNonce, + quoteTimestamp, + fillDeadline, + exclusivityDeadline, + message + ); + } } - function _currentDepositId() internal view override returns (uint32) { - return SPOKE_POOL.numberOfDeposits(); + function computeDepositId(uint256 depositNonce, address depositor) public view override returns (uint256) { + return + depositNonce == 0 + ? SPOKE_POOL.numberOfDeposits() + : SPOKE_POOL.getUnsafeDepositId(address(this), depositor.toBytes32(), depositNonce); } function _destinationSettler(uint256 chainId) internal view override returns (address) { diff --git a/contracts/erc7683/ERC7683Permit2Lib.sol b/contracts/erc7683/ERC7683Permit2Lib.sol index 6e1a47236..37c68946f 100644 --- a/contracts/erc7683/ERC7683Permit2Lib.sol +++ b/contracts/erc7683/ERC7683Permit2Lib.sol @@ -13,6 +13,7 @@ struct AcrossOrderData { uint256 destinationChainId; bytes32 recipient; address exclusiveRelayer; + uint256 depositNonce; uint32 exclusivityPeriod; bytes message; } @@ -34,6 +35,7 @@ bytes constant ACROSS_ORDER_DATA_TYPE = abi.encodePacked( "uint256 destinationChainId,", "bytes32 recipient,", "address exclusiveRelayer," + "uint256 depositNonce,", "uint32 exclusivityPeriod,", "bytes message)" ); diff --git a/contracts/interfaces/V3SpokePoolInterface.sol b/contracts/interfaces/V3SpokePoolInterface.sol index 15fa4fa8e..6f5a4141d 100644 --- a/contracts/interfaces/V3SpokePoolInterface.sol +++ b/contracts/interfaces/V3SpokePoolInterface.sol @@ -53,7 +53,7 @@ interface V3SpokePoolInterface { // Origin chain id. uint256 originChainId; // The id uniquely identifying this deposit on the origin chain. - uint32 depositId; + uint256 depositId; // The timestamp on the destination chain after which this deposit can no longer be filled. uint32 fillDeadline; // The timestamp on the destination chain after which any relayer can fill the deposit. @@ -102,7 +102,7 @@ interface V3SpokePoolInterface { uint256 outputAmount; uint256 destinationChainId; bytes32 exclusiveRelayer; - uint32 depositId; + uint256 depositId; uint32 quoteTimestamp; uint32 fillDeadline; uint32 exclusivityParameter; @@ -119,7 +119,7 @@ interface V3SpokePoolInterface { uint256 inputAmount, uint256 outputAmount, uint256 indexed destinationChainId, - uint32 indexed depositId, + uint256 indexed depositId, uint32 quoteTimestamp, uint32 fillDeadline, uint32 exclusivityDeadline, @@ -131,7 +131,7 @@ interface V3SpokePoolInterface { event RequestedSpeedUpV3Deposit( uint256 updatedOutputAmount, - uint32 indexed depositId, + uint256 indexed depositId, bytes32 indexed depositor, bytes32 updatedRecipient, bytes updatedMessage, @@ -145,7 +145,7 @@ interface V3SpokePoolInterface { uint256 outputAmount, uint256 repaymentChainId, uint256 indexed originChainId, - uint32 indexed depositId, + uint256 indexed depositId, uint32 fillDeadline, uint32 exclusivityDeadline, bytes32 exclusiveRelayer, @@ -162,7 +162,7 @@ interface V3SpokePoolInterface { uint256 inputAmount, uint256 outputAmount, uint256 indexed originChainId, - uint32 indexed depositId, + uint256 indexed depositId, uint32 fillDeadline, uint32 exclusivityDeadline, bytes32 exclusiveRelayer, @@ -228,7 +228,7 @@ interface V3SpokePoolInterface { function speedUpV3Deposit( bytes32 depositor, - uint32 depositId, + uint256 depositId, uint256 updatedOutputAmount, bytes32 updatedRecipient, bytes calldata updatedMessage, diff --git a/contracts/test/MockSpokePool.sol b/contracts/test/MockSpokePool.sol index b9efea1b2..42bae1e24 100644 --- a/contracts/test/MockSpokePool.sol +++ b/contracts/test/MockSpokePool.sol @@ -24,7 +24,7 @@ contract MockSpokePool is SpokePool, MockV2SpokePoolInterface, OwnableUpgradeabl bytes32 public constant UPDATE_DEPOSIT_DETAILS_HASH = keccak256( - "UpdateDepositDetails(uint32 depositId,uint256 originChainId,int64 updatedRelayerFeePct,address updatedRecipient,bytes updatedMessage)" + "UpdateDepositDetails(uint256 depositId,uint256 originChainId,int64 updatedRelayerFeePct,address updatedRecipient,bytes updatedMessage)" ); event BridgedToHubPool(uint256 amount, address token); @@ -60,7 +60,7 @@ contract MockSpokePool is SpokePool, MockV2SpokePoolInterface, OwnableUpgradeabl function _verifyUpdateDepositMessage( address depositor, - uint32 depositId, + uint256 depositId, uint256 originChainId, int64 updatedRelayerFeePct, bytes32 updatedRecipient, @@ -87,7 +87,7 @@ contract MockSpokePool is SpokePool, MockV2SpokePoolInterface, OwnableUpgradeabl function verifyUpdateV3DepositMessage( bytes32 depositor, - uint32 depositId, + uint256 depositId, uint256 originChainId, uint256 updatedOutputAmount, bytes32 updatedRecipient, @@ -109,7 +109,7 @@ contract MockSpokePool is SpokePool, MockV2SpokePoolInterface, OwnableUpgradeabl function verifyUpdateV3DepositMessage( address depositor, - uint32 depositId, + uint256 depositId, uint256 originChainId, uint256 updatedOutputAmount, address updatedRecipient, diff --git a/test/evm/hardhat/SpokePool.Deposit.ts b/test/evm/hardhat/SpokePool.Deposit.ts index 96fdc1c9a..cf7db5718 100644 --- a/test/evm/hardhat/SpokePool.Deposit.ts +++ b/test/evm/hardhat/SpokePool.Deposit.ts @@ -372,6 +372,28 @@ describe("SpokePool Depositor Logic", async function () { _relayData.message, ]; } + function getUnsafeDepositArgsFromRelayData( + _relayData: V3RelayData, + _depositId: string, + _destinationChainId = destinationChainId, + _quoteTimestamp = quoteTimestamp + ) { + return [ + addressToBytes(_relayData.depositor), + addressToBytes(_relayData.recipient), + addressToBytes(_relayData.inputToken), + addressToBytes(_relayData.outputToken), + _relayData.inputAmount, + _relayData.outputAmount, + _destinationChainId, + addressToBytes(_relayData.exclusiveRelayer), + _depositId, + _quoteTimestamp, + _relayData.fillDeadline, + _relayData.exclusivityDeadline, + _relayData.message, + ]; + } beforeEach(async function () { relayData = { depositor: addressToBytes(depositor.address), @@ -804,6 +826,44 @@ describe("SpokePool Depositor Logic", async function () { "ReentrancyGuard: reentrant call" ); }); + it("unsafe deposit ID", async function () { + // new deposit ID should be the uint256 equivalent of the keccak256 hash of packed {msg.sender, depositor, forcedDepositId}. + const forcedDepositId = "99"; + const expectedDepositId = BigNumber.from( + ethers.utils.solidityKeccak256( + ["address", "bytes32", "uint256"], + [depositor.address, addressToBytes(recipient.address), forcedDepositId] + ) + ); + expect( + await spokePool.getUnsafeDepositId(depositor.address, addressToBytes(recipient.address), forcedDepositId) + ).to.equal(expectedDepositId); + // Note: we deliberately set the depositor != msg.sender to test that the hashing algorithm correctly includes + // both addresses in the hash. + await expect( + spokePool + .connect(depositor) + [SpokePoolFuncs.unsafeDepositV3Bytes]( + ...getUnsafeDepositArgsFromRelayData({ ...relayData, depositor: recipient.address }, forcedDepositId) + ) + ) + .to.emit(spokePool, "V3FundsDeposited") + .withArgs( + relayData.inputToken, + relayData.outputToken, + relayData.inputAmount, + relayData.outputAmount, + destinationChainId, + expectedDepositId, + quoteTimestamp, + relayData.fillDeadline, + 0, + addressToBytes(recipient.address), + relayData.recipient, + relayData.exclusiveRelayer, + relayData.message + ); + }); }); describe("speed up V3 deposit", function () { const updatedOutputAmount = amountToDeposit.add(1); diff --git a/test/evm/hardhat/constants.ts b/test/evm/hardhat/constants.ts index c32b60979..df01742e0 100644 --- a/test/evm/hardhat/constants.ts +++ b/test/evm/hardhat/constants.ts @@ -111,6 +111,8 @@ export const sampleRateModel = { }; export const SpokePoolFuncs = { + unsafeDepositV3Bytes: + "unsafeDepositV3(bytes32,bytes32,bytes32,bytes32,uint256,uint256,uint256,bytes32,uint256,uint32,uint32,uint32,bytes)", depositV3Bytes: "depositV3(bytes32,bytes32,bytes32,bytes32,uint256,uint256,uint256,bytes32,uint32,uint32,uint32,bytes)", depositV3Address: @@ -119,9 +121,10 @@ export const SpokePoolFuncs = { "depositV3Now(bytes32,bytes32,bytes32,bytes32,uint256,uint256,uint256,bytes32,uint32,uint32,bytes)", depositV3NowAddress: "depositV3Now(address,address,address,address,uint256,uint256,uint256,address,uint32,uint32,bytes)", - speedUpV3DepositBytes: "speedUpV3Deposit(bytes32,uint32,uint256,bytes32,bytes,bytes)", - speedUpV3DepositAddress: "speedUpV3Deposit(address,uint32,uint256,address,bytes,bytes)", - verifyUpdateV3DepositMessageBytes: "verifyUpdateV3DepositMessage(bytes32,uint32,uint256,uint256,bytes32,bytes,bytes)", + speedUpV3DepositBytes: "speedUpV3Deposit(bytes32,uint256,uint256,bytes32,bytes,bytes)", + speedUpV3DepositAddress: "speedUpV3Deposit(address,uint256,uint256,address,bytes,bytes)", + verifyUpdateV3DepositMessageBytes: + "verifyUpdateV3DepositMessage(bytes32,uint256,uint256,uint256,bytes32,bytes,bytes)", verifyUpdateV3DepositMessageAddress: - "verifyUpdateV3DepositMessage(address,uint32,uint256,uint256,address,bytes,bytes)", + "verifyUpdateV3DepositMessage(address,uint256,uint256,uint256,address,bytes,bytes)", }; diff --git a/test/evm/hardhat/fixtures/SpokePool.Fixture.ts b/test/evm/hardhat/fixtures/SpokePool.Fixture.ts index 56f39a474..ec9d8e6db 100644 --- a/test/evm/hardhat/fixtures/SpokePool.Fixture.ts +++ b/test/evm/hardhat/fixtures/SpokePool.Fixture.ts @@ -346,7 +346,7 @@ export async function getUpdatedV3DepositSignature( const typedData = { types: { UpdateDepositDetails: [ - { name: "depositId", type: "uint32" }, + { name: "depositId", type: "uint256" }, { name: "originChainId", type: "uint256" }, { name: "updatedOutputAmount", type: "uint256" }, { name: "updatedRecipient", type: isAddressOverload ? "address" : "bytes32" }, From 48d65d419904bb362cbe75c16119ed105518ab80 Mon Sep 17 00:00:00 2001 From: Pablo Maldonado Date: Mon, 25 Nov 2024 18:23:20 +0000 Subject: [PATCH 23/29] docs: fix comment duplication (#775) Signed-off-by: Pablo Maldonado --- contracts/SpokePool.sol | 19 ++++++------------- 1 file changed, 6 insertions(+), 13 deletions(-) diff --git a/contracts/SpokePool.sol b/contracts/SpokePool.sol index 362ef30af..0ad970322 100644 --- a/contracts/SpokePool.sol +++ b/contracts/SpokePool.sol @@ -60,12 +60,11 @@ abstract contract SpokePool is WETH9Interface private DEPRECATED_wrappedNativeToken; uint32 private DEPRECATED_depositQuoteTimeBuffer; - // Count of deposits is used to construct a unique deposit identifier for this spoke pool. This value - // gets emitted and incremented on each depositV3 call. Because its a uint32, it will get implicitly cast to - // uint256 in the emitted V3FundsDeposited event by setting its most significant bits to 0. - // This variable name `numberOfDeposits` should ideally be re-named to - // depositNonceCounter or something similar because its not a true representation of the number of deposits - // because `unsafeDepositV3` can be called directly and bypass this increment. + // `numberOfDeposits` acts as a counter to generate unique deposit identifiers for this spoke pool. + // It is a uint32 that increments with each `depositV3` call. In the `V3FundsDeposited` event, it is + // implicitly cast to uint256 by setting its most significant bits to 0, reducing the risk of ID collisions + // with unsafe deposits. However, this variable's name could be improved (e.g., `depositNonceCounter`) + // since it does not accurately reflect the total number of deposits, as `unsafeDepositV3` can bypass this increment. uint32 public numberOfDeposits; // Whether deposits and fills are disabled. @@ -519,13 +518,7 @@ abstract contract SpokePool is uint32 exclusivityParameter, bytes calldata message ) public payable override nonReentrant unpausedDeposits { - // Increment deposit nonce variable `numberOfDeposits` so that deposit ID for this deposit on this - // spoke pool is unique. This variable `numberOfDeposits` should ideally be re-named to - // depositNonceCounter or something similar because its not a true representation of the number of deposits - // because `unsafeDepositV3` can be called directly and bypass this increment. - // The `numberOfDeposits` is a uint32 that will get implicitly cast to uint256 by setting the - // most significant bits to 0, which creates very little chance this an unsafe deposit ID collides - // with a safe deposit ID. + // Increment the `numberOfDeposits` counter to ensure a unique deposit ID for this spoke pool. DepositV3Params memory params = DepositV3Params({ depositor: depositor, recipient: recipient, From 0691f0a5b23e0f2445adce2348579a9f2e6af6b6 Mon Sep 17 00:00:00 2001 From: Reinis Martinsons <77973553+Reinis-FRP@users.noreply.github.com> Date: Mon, 25 Nov 2024 20:42:24 +0200 Subject: [PATCH 24/29] fix: emit hashed message in evm fill events (#772) * fix: emit hashed message in evm fill events Signed-off-by: Reinis Martinsons * WIP Signed-off-by: chrismaree * fix: linting Signed-off-by: Reinis Martinsons --------- Signed-off-by: Reinis Martinsons Signed-off-by: chrismaree Co-authored-by: chrismaree --- contracts/SpokePool.sol | 12 ++++++++--- contracts/interfaces/V3SpokePoolInterface.sol | 6 +++--- .../svm-spoke/src/state/instruction_params.rs | 2 +- test/evm/hardhat/SpokePool.Relay.ts | 21 ++++++++++--------- test/evm/hardhat/SpokePool.SlowRelay.ts | 5 +++-- utils/utils.ts | 10 +++++++++ 6 files changed, 37 insertions(+), 19 deletions(-) diff --git a/contracts/SpokePool.sol b/contracts/SpokePool.sol index 0ad970322..ee8dd7930 100644 --- a/contracts/SpokePool.sol +++ b/contracts/SpokePool.sol @@ -1120,7 +1120,7 @@ abstract contract SpokePool is relayData.exclusiveRelayer, relayData.depositor, relayData.recipient, - relayData.message + _hashNonEmptyMessage(relayData.message) ); } @@ -1704,10 +1704,10 @@ abstract contract SpokePool is relayer, relayData.depositor, relayData.recipient, - relayData.message, + _hashNonEmptyMessage(relayData.message), V3RelayExecutionEventInfo({ updatedRecipient: relayExecution.updatedRecipient, - updatedMessage: relayExecution.updatedMessage, + updatedMessageHash: _hashNonEmptyMessage(relayExecution.updatedMessage), updatedOutputAmount: relayExecution.updatedOutputAmount, fillType: fillType }) @@ -1757,6 +1757,12 @@ abstract contract SpokePool is return exclusivityDeadline >= currentTime; } + // Helper for emitting message hash. For easier easier human readability we return bytes32(0) for empty message. + function _hashNonEmptyMessage(bytes memory message) internal pure returns (bytes32) { + if (message.length == 0) return bytes32(0); + else return keccak256(message); + } + // Implementing contract needs to override this to ensure that only the appropriate cross chain admin can execute // certain admin functions. For L2 contracts, the cross chain admin refers to some L1 address or contract, and for // L1, this would just be the same admin of the HubPool. diff --git a/contracts/interfaces/V3SpokePoolInterface.sol b/contracts/interfaces/V3SpokePoolInterface.sol index 6f5a4141d..34262013c 100644 --- a/contracts/interfaces/V3SpokePoolInterface.sol +++ b/contracts/interfaces/V3SpokePoolInterface.sol @@ -87,7 +87,7 @@ interface V3SpokePoolInterface { // filled so they don't have to be unpacked by all clients. struct V3RelayExecutionEventInfo { bytes32 updatedRecipient; - bytes updatedMessage; + bytes32 updatedMessageHash; uint256 updatedOutputAmount; FillType fillType; } @@ -152,7 +152,7 @@ interface V3SpokePoolInterface { bytes32 indexed relayer, bytes32 depositor, bytes32 recipient, - bytes message, + bytes32 messageHash, V3RelayExecutionEventInfo relayExecutionInfo ); @@ -168,7 +168,7 @@ interface V3SpokePoolInterface { bytes32 exclusiveRelayer, bytes32 depositor, bytes32 recipient, - bytes message + bytes32 messageHash ); event ClaimedRelayerRefund( diff --git a/programs/svm-spoke/src/state/instruction_params.rs b/programs/svm-spoke/src/state/instruction_params.rs index f7cdae79a..704413575 100644 --- a/programs/svm-spoke/src/state/instruction_params.rs +++ b/programs/svm-spoke/src/state/instruction_params.rs @@ -8,5 +8,5 @@ pub struct ExecuteRelayerRefundLeafParams { pub root_bundle_id: u32, // ID of the root bundle to be used. pub relayer_refund_leaf: RelayerRefundLeaf, // Leaf to be verified against the proof and instruct bundle execution. #[max_len(0)] - pub proof: Vec<[u8; 32]>, // Proof to verify the leaf's inclusion in relayer refund merkle tree. + pub proof: Vec<[u8; 32]>, // Proof to verify the leaf's inclusion in relayer refund merkle tree. } diff --git a/test/evm/hardhat/SpokePool.Relay.ts b/test/evm/hardhat/SpokePool.Relay.ts index 0ab6cb6bf..506894f1f 100644 --- a/test/evm/hardhat/SpokePool.Relay.ts +++ b/test/evm/hardhat/SpokePool.Relay.ts @@ -10,6 +10,7 @@ import { BigNumber, addressToBytes, bytes32ToAddress, + hashNonEmptyMessage, } from "../../../utils/utils"; import { spokePoolFixture, @@ -130,10 +131,10 @@ describe("SpokePool Relayer Logic", async function () { addressToBytes(relayer.address), addressToBytes(relayData.depositor), addressToBytes(relayData.recipient), - relayData.message, + hashNonEmptyMessage(relayData.message), [ addressToBytes(relayData.recipient), - relayExecution.updatedMessage, + hashNonEmptyMessage(relayExecution.updatedMessage), relayExecution.updatedOutputAmount, // Testing that this FillType is not "FastFill" FillType.ReplacedSlowFill, @@ -166,10 +167,10 @@ describe("SpokePool Relayer Logic", async function () { addressToBytes(relayer.address), addressToBytes(relayData.depositor), addressToBytes(relayData.recipient), - relayData.message, + hashNonEmptyMessage(relayData.message), [ addressToBytes(relayData.recipient), - relayExecution.updatedMessage, + hashNonEmptyMessage(relayExecution.updatedMessage), relayExecution.updatedOutputAmount, // Testing that this FillType is "SlowFill" FillType.SlowFill, @@ -201,10 +202,10 @@ describe("SpokePool Relayer Logic", async function () { addressToBytes(relayer.address), addressToBytes(relayData.depositor), addressToBytes(relayData.recipient), - relayData.message, + hashNonEmptyMessage(relayData.message), [ addressToBytes(relayData.recipient), - relayExecution.updatedMessage, + hashNonEmptyMessage(relayExecution.updatedMessage), relayExecution.updatedOutputAmount, FillType.FastFill, ] @@ -372,10 +373,10 @@ describe("SpokePool Relayer Logic", async function () { addressToBytes(relayer.address), // Should be equal to msg.sender of fillRelayV3 addressToBytes(relayData.depositor), addressToBytes(relayData.recipient), - relayData.message, + hashNonEmptyMessage(relayData.message), [ addressToBytes(relayData.recipient), // updatedRecipient should be equal to recipient - relayData.message, // updatedMessage should be equal to message + hashNonEmptyMessage(relayData.message), // updatedMessageHash should be equal to message hash relayData.outputAmount, // updatedOutputAmount should be equal to outputAmount // Should be FastFill FillType.FastFill, @@ -487,11 +488,11 @@ describe("SpokePool Relayer Logic", async function () { addressToBytes(relayer.address), // Should be equal to msg.sender addressToBytes(relayData.depositor), addressToBytes(relayData.recipient), - relayData.message, + hashNonEmptyMessage(relayData.message), [ // Should use passed-in updated params: addressToBytes(updatedRecipient), - updatedMessage, + hashNonEmptyMessage(updatedMessage), updatedOutputAmount, // Should be FastFill FillType.FastFill, diff --git a/test/evm/hardhat/SpokePool.SlowRelay.ts b/test/evm/hardhat/SpokePool.SlowRelay.ts index dc242f6ee..1088b3c34 100644 --- a/test/evm/hardhat/SpokePool.SlowRelay.ts +++ b/test/evm/hardhat/SpokePool.SlowRelay.ts @@ -7,6 +7,7 @@ import { seedContract, seedWallet, addressToBytes, + hashNonEmptyMessage, } from "../../../utils/utils"; import { spokePoolFixture, V3RelayData, getV3RelayHash, V3SlowFill, FillType } from "./fixtures/SpokePool.Fixture"; import { buildV3SlowRelayTree } from "./MerkleLib.utils"; @@ -309,12 +310,12 @@ describe("SpokePool Slow Relay Logic", async function () { addressToBytes(consts.zeroAddress), // Sets relayer address to 0x0 addressToBytes(relayData.depositor), addressToBytes(relayData.recipient), - relayData.message, + hashNonEmptyMessage(relayData.message), [ // Uses relayData.recipient addressToBytes(relayData.recipient), // Uses relayData.message - relayData.message, + hashNonEmptyMessage(relayData.message), // Uses slow fill leaf's updatedOutputAmount slowRelayLeaf.updatedOutputAmount, // Should be SlowFill diff --git a/utils/utils.ts b/utils/utils.ts index 9aceac79e..05bcc8d6a 100644 --- a/utils/utils.ts +++ b/utils/utils.ts @@ -192,6 +192,16 @@ export function trimSolanaAddress(bytes32Address: string): string { return ethers.utils.hexZeroPad(ethers.utils.hexlify(uint160Address), 20); } +export function hashNonEmptyMessage(message: string) { + if (!ethers.utils.isHexString(message) || message.length % 2 !== 0) throw new Error("Invalid hex message bytes"); + + // account for 0x prefix when checking length + if (message.length > 2) { + return ethers.utils.keccak256(message); + } + return ethers.utils.hexlify(new Uint8Array(32)); +} + const { defaultAbiCoder, keccak256 } = ethers.utils; export { avmL1ToL2Alias, expect, Contract, ethers, BigNumber, defaultAbiCoder, keccak256, FakeContract, Signer }; From 0f2600f83e8a5738d0d62a78b05ff37adda4c1c9 Mon Sep 17 00:00:00 2001 From: Reinis Martinsons Date: Mon, 25 Nov 2024 18:54:09 +0000 Subject: [PATCH 25/29] fix: linting Signed-off-by: Reinis Martinsons --- programs/svm-spoke/src/state/instruction_params.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/programs/svm-spoke/src/state/instruction_params.rs b/programs/svm-spoke/src/state/instruction_params.rs index b46bef60c..5b8feeef3 100644 --- a/programs/svm-spoke/src/state/instruction_params.rs +++ b/programs/svm-spoke/src/state/instruction_params.rs @@ -8,7 +8,7 @@ pub struct ExecuteRelayerRefundLeafParams { pub root_bundle_id: u32, // ID of the root bundle to be used. pub relayer_refund_leaf: RelayerRefundLeaf, // Leaf to be verified against the proof and instruct bundle execution. #[max_len(0)] - pub proof: Vec<[u8; 32]>, // Proof to verify the leaf's inclusion in relayer refund merkle tree. + pub proof: Vec<[u8; 32]>, // Proof to verify the leaf's inclusion in relayer refund merkle tree. } #[account] From fa137a20ddd84ae54ae215f50dc5ee36ed460cf2 Mon Sep 17 00:00:00 2001 From: Chris Maree Date: Tue, 3 Dec 2024 10:31:51 +0100 Subject: [PATCH 26/29] feat: improve _getV3RelayHash method (#779) --- contracts/SpokePool.sol | 19 +++++++++- scripts/buildSampleTree.ts | 2 +- test/evm/hardhat/MerkleLib.Proofs.ts | 2 +- test/evm/hardhat/SpokePool.Deposit.ts | 8 ++--- test/evm/hardhat/SpokePool.Relay.ts | 7 ++-- .../Polygon_SpokePool.ts | 3 +- test/evm/hardhat/constants.ts | 2 +- .../evm/hardhat/fixtures/SpokePool.Fixture.ts | 36 ++++++++++++++++--- 8 files changed, 62 insertions(+), 17 deletions(-) diff --git a/contracts/SpokePool.sol b/contracts/SpokePool.sol index ee8dd7930..c8cc1c05a 100644 --- a/contracts/SpokePool.sol +++ b/contracts/SpokePool.sol @@ -1639,7 +1639,24 @@ abstract contract SpokePool is } function _getV3RelayHash(V3RelayData memory relayData) private view returns (bytes32) { - return keccak256(abi.encode(relayData, chainId())); + return + keccak256( + abi.encode( + relayData.depositor, + relayData.recipient, + relayData.exclusiveRelayer, + relayData.inputToken, + relayData.outputToken, + relayData.inputAmount, + relayData.outputAmount, + relayData.originChainId, + relayData.depositId, + relayData.fillDeadline, + relayData.exclusivityDeadline, + _hashNonEmptyMessage(relayData.message), + chainId() + ) + ); } // Unwraps ETH and does a transfer to a recipient address. If the recipient is a smart contract then sends wrappedNativeToken. diff --git a/scripts/buildSampleTree.ts b/scripts/buildSampleTree.ts index 738c73941..d70f2be81 100644 --- a/scripts/buildSampleTree.ts +++ b/scripts/buildSampleTree.ts @@ -142,7 +142,7 @@ async function main() { originChainId: SPOKE_POOL_CHAIN_ID, fillDeadline: Math.floor(Date.now() / 1000) + 14400, // 4 hours from now exclusivityDeadline: 0, - depositId: i, + depositId: toBN(i), message: "0x", }, updatedOutputAmount: toBNWeiWithDecimals(SLOW_RELAY_AMOUNT, DECIMALS), diff --git a/test/evm/hardhat/MerkleLib.Proofs.ts b/test/evm/hardhat/MerkleLib.Proofs.ts index 77e88b035..a0a1be168 100644 --- a/test/evm/hardhat/MerkleLib.Proofs.ts +++ b/test/evm/hardhat/MerkleLib.Proofs.ts @@ -122,7 +122,7 @@ describe("MerkleLib Proofs", async function () { inputAmount: randomBigNumber(), outputAmount: randomBigNumber(), originChainId: randomBigNumber(2).toNumber(), - depositId: BigNumber.from(i).toNumber(), + depositId: BigNumber.from(i), fillDeadline: randomBigNumber(2).toNumber(), exclusivityDeadline: randomBigNumber(2).toNumber(), message: ethers.utils.hexlify(ethers.utils.randomBytes(1024)), diff --git a/test/evm/hardhat/SpokePool.Deposit.ts b/test/evm/hardhat/SpokePool.Deposit.ts index cf7db5718..1160f5d72 100644 --- a/test/evm/hardhat/SpokePool.Deposit.ts +++ b/test/evm/hardhat/SpokePool.Deposit.ts @@ -404,7 +404,7 @@ describe("SpokePool Depositor Logic", async function () { inputAmount: amountToDeposit, outputAmount: amountToDeposit.sub(19), originChainId: originChainId, - depositId: 0, + depositId: toBN(0), fillDeadline: quoteTimestamp + 1000, exclusivityDeadline: 0, message: "0x", @@ -869,7 +869,7 @@ describe("SpokePool Depositor Logic", async function () { const updatedOutputAmount = amountToDeposit.add(1); const updatedRecipient = randomAddress(); const updatedMessage = "0x1234"; - const depositId = 100; + const depositId = toBN(100); it("_verifyUpdateV3DepositMessage", async function () { const signature = await getUpdatedV3DepositSignature( depositor, @@ -905,7 +905,7 @@ describe("SpokePool Depositor Logic", async function () { // @dev Creates an invalid signature using different params const invalidSignature = await getUpdatedV3DepositSignature( depositor, - depositId + 1, + depositId.add(toBN(1)), originChainId, updatedOutputAmount, addressToBytes(updatedRecipient), @@ -994,7 +994,7 @@ describe("SpokePool Depositor Logic", async function () { const updatedOutputAmount = amountToDeposit.add(1); const updatedRecipient = randomAddress(); const updatedMessage = "0x1234"; - const depositId = 100; + const depositId = toBN(100); const spokePoolChainId = await spokePool.chainId(); const signature = await getUpdatedV3DepositSignature( diff --git a/test/evm/hardhat/SpokePool.Relay.ts b/test/evm/hardhat/SpokePool.Relay.ts index 506894f1f..51c946807 100644 --- a/test/evm/hardhat/SpokePool.Relay.ts +++ b/test/evm/hardhat/SpokePool.Relay.ts @@ -11,6 +11,7 @@ import { addressToBytes, bytes32ToAddress, hashNonEmptyMessage, + toBN, } from "../../../utils/utils"; import { spokePoolFixture, @@ -306,7 +307,7 @@ describe("SpokePool Relayer Logic", async function () { addressToBytes(relayer.address), false // isSlowFill ); - const test = bytes32ToAddress(_relayData.outputToken); + expect(acrossMessageHandler.handleV3AcrossMessage).to.have.been.calledOnceWith( bytes32ToAddress(_relayData.outputToken), relayExecution.updatedOutputAmount, @@ -522,7 +523,7 @@ describe("SpokePool Relayer Logic", async function () { // Incorrect signature for new deposit ID const otherSignature = await getUpdatedV3DepositSignature( depositor, - relayData.depositId + 1, + relayData.depositId.add(toBN(1)), relayData.originChainId, updatedOutputAmount, addressToBytes(updatedRecipient), @@ -562,7 +563,7 @@ describe("SpokePool Relayer Logic", async function () { spokePool .connect(relayer) .fillV3RelayWithUpdatedDeposit( - { ...relayData, depositId: relayData.depositId + 1 }, + { ...relayData, depositId: relayData.depositId.add(toBN(1)) }, consts.repaymentChainId, addressToBytes(relayer.address), updatedOutputAmount, diff --git a/test/evm/hardhat/chain-specific-spokepools/Polygon_SpokePool.ts b/test/evm/hardhat/chain-specific-spokepools/Polygon_SpokePool.ts index 650024636..186b3df07 100644 --- a/test/evm/hardhat/chain-specific-spokepools/Polygon_SpokePool.ts +++ b/test/evm/hardhat/chain-specific-spokepools/Polygon_SpokePool.ts @@ -21,6 +21,7 @@ import { FakeContract, createFakeFromABI, addressToBytes, + toBN, } from "../../../../utils/utils"; import { hre } from "../../../../utils/utils.hre"; import { hubPoolFixture } from "../fixtures/HubPool.Fixture"; @@ -329,7 +330,7 @@ describe("Polygon Spoke Pool", function () { inputAmount: toWei("1"), outputAmount: toWei("1"), originChainId: originChainId, - depositId: 0, + depositId: toBN(0), fillDeadline: currentTime + 7200, exclusivityDeadline: 0, message: "0x1234", diff --git a/test/evm/hardhat/constants.ts b/test/evm/hardhat/constants.ts index df01742e0..a8ed598d6 100644 --- a/test/evm/hardhat/constants.ts +++ b/test/evm/hardhat/constants.ts @@ -42,7 +42,7 @@ export const originChainId = 666; export const repaymentChainId = 777; -export const firstDepositId = 0; +export const firstDepositId = toBN(0); export const bondAmount = toWei("5"); diff --git a/test/evm/hardhat/fixtures/SpokePool.Fixture.ts b/test/evm/hardhat/fixtures/SpokePool.Fixture.ts index ec9d8e6db..a54cb58dc 100644 --- a/test/evm/hardhat/fixtures/SpokePool.Fixture.ts +++ b/test/evm/hardhat/fixtures/SpokePool.Fixture.ts @@ -102,7 +102,7 @@ export interface V3RelayData { inputAmount: BigNumber; outputAmount: BigNumber; originChainId: number; - depositId: number; + depositId: BigNumber; fillDeadline: number; exclusivityDeadline: number; message: string; @@ -177,13 +177,39 @@ export function getRelayHash( } export function getV3RelayHash(relayData: V3RelayData, destinationChainId: number): string { + const messageHash = relayData.message == "0x" ? ethers.constants.HashZero : ethers.utils.keccak256(relayData.message); return ethers.utils.keccak256( defaultAbiCoder.encode( [ - "tuple(bytes32 depositor, bytes32 recipient, bytes32 exclusiveRelayer, bytes32 inputToken, bytes32 outputToken, uint256 inputAmount, uint256 outputAmount, uint256 originChainId, uint32 depositId, uint32 fillDeadline, uint32 exclusivityDeadline, bytes message)", - "uint256 destinationChainId", + "bytes32", // depositor + "bytes32", // recipient + "bytes32", // exclusiveRelayer + "bytes32", // inputToken + "bytes32", // outputToken + "uint256", // inputAmount + "uint256", // outputAmount + "uint256", // originChainId + "uint256", // depositId + "uint32", // fillDeadline + "uint32", // exclusivityDeadline + "bytes32", // messageHash + "uint256", // destinationChainId ], - [relayData, destinationChainId] + [ + relayData.depositor, + relayData.recipient, + relayData.exclusiveRelayer, + relayData.inputToken, + relayData.outputToken, + relayData.inputAmount, + relayData.outputAmount, + relayData.originChainId, + relayData.depositId, + relayData.fillDeadline, + relayData.exclusivityDeadline, + messageHash, + destinationChainId, + ] ) ); } @@ -336,7 +362,7 @@ export async function modifyRelayHelper( export async function getUpdatedV3DepositSignature( depositor: SignerWithAddress, - depositId: number, + depositId: BigNumber, originChainId: number, updatedOutputAmount: BigNumber, updatedRecipient: string, From 8e8370f89d8ec1a4e987e3c5b71a134e5b4d2b80 Mon Sep 17 00:00:00 2001 From: chrismaree Date: Tue, 3 Dec 2024 10:42:14 +0100 Subject: [PATCH 27/29] WIP Signed-off-by: chrismaree --- test/svm/SvmSpoke.Bundle.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/svm/SvmSpoke.Bundle.ts b/test/svm/SvmSpoke.Bundle.ts index 40e799e17..f5db60873 100644 --- a/test/svm/SvmSpoke.Bundle.ts +++ b/test/svm/SvmSpoke.Bundle.ts @@ -279,8 +279,8 @@ describe("svm_spoke.bundle", () => { assert.isFalse(event.deferredRefunds, "deferredRefunds should be false"); assertSE(event.caller, owner, "caller should match"); - event = events.find((event) => event.name === "tokensBridged").data; - + // Verify the tokensBridged event + event = events.find((event) => event.name === "tokensBridged")?.data; assertSE(event.amountToReturn, relayerRefundLeaves[0].amountToReturn, "amountToReturn should match"); assertSE(event.chainId, chainId, "chainId should match"); assertSE(event.leafId, leaf.leafId, "leafId should match"); From af4c107ce375eec7f867dd3f9b7f1e887bcb3331 Mon Sep 17 00:00:00 2001 From: chrismaree Date: Tue, 3 Dec 2024 10:46:03 +0100 Subject: [PATCH 28/29] WIP Signed-off-by: chrismaree --- programs/svm-spoke/src/event.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/programs/svm-spoke/src/event.rs b/programs/svm-spoke/src/event.rs index d4b797b34..b7bab22c7 100644 --- a/programs/svm-spoke/src/event.rs +++ b/programs/svm-spoke/src/event.rs @@ -84,7 +84,6 @@ pub struct FilledV3Relay { pub relayer: Pubkey, pub depositor: Pubkey, pub recipient: Pubkey, - // TODO: update EVM implementation to use message_hash in all fill related events. pub message_hash: [u8; 32], pub relay_execution_info: V3RelayExecutionEventInfo, } From c448c7e39fb5a7514e610014e461ff0ede3e7a19 Mon Sep 17 00:00:00 2001 From: Chris Maree Date: Mon, 6 Jan 2025 16:27:23 +0100 Subject: [PATCH 29/29] fix: Address Storage layout issue in CI (#836) * add new storage layout Signed-off-by: Chris Maree * Discard changes to storage-layouts/PolygonZkEVM_SpokePool.json * Discard changes to storage-layouts/Redstone_SpokePool.json * Discard changes to storage-layouts/Scroll_SpokePool.json * Discard changes to storage-layouts/Zora_SpokePool.json * Discard changes to storage-layouts/WorldChain_SpokePool.json * add new storage layout Signed-off-by: Chris Maree --------- Signed-off-by: Chris Maree --- storage-layouts/AlephZero_SpokePool.json | 8 +++++++- storage-layouts/PolygonZkEVM_SpokePool.json | 8 +++++++- storage-layouts/Redstone_SpokePool.json | 8 +++++++- storage-layouts/Scroll_SpokePool.json | 8 +++++++- storage-layouts/WorldChain_SpokePool.json | 8 +++++++- storage-layouts/Zora_SpokePool.json | 8 +++++++- 6 files changed, 42 insertions(+), 6 deletions(-) diff --git a/storage-layouts/AlephZero_SpokePool.json b/storage-layouts/AlephZero_SpokePool.json index 24a66a64d..382132dd3 100644 --- a/storage-layouts/AlephZero_SpokePool.json +++ b/storage-layouts/AlephZero_SpokePool.json @@ -146,10 +146,16 @@ }, { "contract": "contracts/AlephZero_SpokePool.sol:AlephZero_SpokePool", - "label": "__gap", + "label": "relayerRefund", "offset": 0, "slot": "2163" }, + { + "contract": "contracts/AlephZero_SpokePool.sol:AlephZero_SpokePool", + "label": "__gap", + "offset": 0, + "slot": "2164" + }, { "contract": "contracts/AlephZero_SpokePool.sol:AlephZero_SpokePool", "label": "l2GatewayRouter", diff --git a/storage-layouts/PolygonZkEVM_SpokePool.json b/storage-layouts/PolygonZkEVM_SpokePool.json index 776ed36d0..7e1fc8127 100644 --- a/storage-layouts/PolygonZkEVM_SpokePool.json +++ b/storage-layouts/PolygonZkEVM_SpokePool.json @@ -146,10 +146,16 @@ }, { "contract": "contracts/PolygonZkEVM_SpokePool.sol:PolygonZkEVM_SpokePool", - "label": "__gap", + "label": "relayerRefund", "offset": 0, "slot": "2163" }, + { + "contract": "contracts/PolygonZkEVM_SpokePool.sol:PolygonZkEVM_SpokePool", + "label": "__gap", + "offset": 0, + "slot": "2164" + }, { "contract": "contracts/PolygonZkEVM_SpokePool.sol:PolygonZkEVM_SpokePool", "label": "l2PolygonZkEVMBridge", diff --git a/storage-layouts/Redstone_SpokePool.json b/storage-layouts/Redstone_SpokePool.json index 61e0704c0..92b0654e8 100644 --- a/storage-layouts/Redstone_SpokePool.json +++ b/storage-layouts/Redstone_SpokePool.json @@ -146,10 +146,16 @@ }, { "contract": "contracts/Redstone_SpokePool.sol:Redstone_SpokePool", - "label": "__gap", + "label": "relayerRefund", "offset": 0, "slot": "2163" }, + { + "contract": "contracts/Redstone_SpokePool.sol:Redstone_SpokePool", + "label": "__gap", + "offset": 0, + "slot": "2164" + }, { "contract": "contracts/Redstone_SpokePool.sol:Redstone_SpokePool", "label": "l1Gas", diff --git a/storage-layouts/Scroll_SpokePool.json b/storage-layouts/Scroll_SpokePool.json index adec6db47..81782717b 100644 --- a/storage-layouts/Scroll_SpokePool.json +++ b/storage-layouts/Scroll_SpokePool.json @@ -146,10 +146,16 @@ }, { "contract": "contracts/Scroll_SpokePool.sol:Scroll_SpokePool", - "label": "__gap", + "label": "relayerRefund", "offset": 0, "slot": "2163" }, + { + "contract": "contracts/Scroll_SpokePool.sol:Scroll_SpokePool", + "label": "__gap", + "offset": 0, + "slot": "2164" + }, { "contract": "contracts/Scroll_SpokePool.sol:Scroll_SpokePool", "label": "l2GatewayRouter", diff --git a/storage-layouts/WorldChain_SpokePool.json b/storage-layouts/WorldChain_SpokePool.json index 2bc68b54c..e46bb47d2 100644 --- a/storage-layouts/WorldChain_SpokePool.json +++ b/storage-layouts/WorldChain_SpokePool.json @@ -146,10 +146,16 @@ }, { "contract": "contracts/WorldChain_SpokePool.sol:WorldChain_SpokePool", - "label": "__gap", + "label": "relayerRefund", "offset": 0, "slot": "2163" }, + { + "contract": "contracts/WorldChain_SpokePool.sol:WorldChain_SpokePool", + "label": "__gap", + "offset": 0, + "slot": "2164" + }, { "contract": "contracts/WorldChain_SpokePool.sol:WorldChain_SpokePool", "label": "l1Gas", diff --git a/storage-layouts/Zora_SpokePool.json b/storage-layouts/Zora_SpokePool.json index 6a6b9ae6d..ca513b7cc 100644 --- a/storage-layouts/Zora_SpokePool.json +++ b/storage-layouts/Zora_SpokePool.json @@ -146,10 +146,16 @@ }, { "contract": "contracts/Zora_SpokePool.sol:Zora_SpokePool", - "label": "__gap", + "label": "relayerRefund", "offset": 0, "slot": "2163" }, + { + "contract": "contracts/Zora_SpokePool.sol:Zora_SpokePool", + "label": "__gap", + "offset": 0, + "slot": "2164" + }, { "contract": "contracts/Zora_SpokePool.sol:Zora_SpokePool", "label": "l1Gas",