diff --git a/contracts/external/interfaces/ILayerZeroComposer.sol b/contracts/external/interfaces/ILayerZeroComposer.sol new file mode 100644 index 000000000..1fdd962d6 --- /dev/null +++ b/contracts/external/interfaces/ILayerZeroComposer.sol @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: MIT + +pragma solidity >=0.8.0; + +/** + * @title ILayerZeroComposer + * @dev Copied over from https://github.com/LayerZero-Labs/LayerZero-v2/blob/2ff4988f85b5c94032eb71bbc4073e69c078179d/packages/layerzero-v2/evm/protocol/contracts/interfaces/ILayerZeroComposer.sol#L8 + */ +interface ILayerZeroComposer { + /** + * @notice Composes a LayerZero message from an OApp. + * @param _from The address initiating the composition, typically the OApp where the lzReceive was called. + * @param _guid The unique identifier for the corresponding LayerZero src/dst tx. + * @param _message The composed message payload in bytes. NOT necessarily the same payload passed via lzReceive. + * @param _executor The address of the executor for the composed message. + * @param _extraData Additional arbitrary data in bytes passed by the entity who executes the lzCompose. + */ + function lzCompose( + address _from, + bytes32 _guid, + bytes calldata _message, + address _executor, + bytes calldata _extraData + ) external payable; +} diff --git a/contracts/interfaces/IOFT.sol b/contracts/interfaces/IOFT.sol index 297229143..28ac0ba1d 100644 --- a/contracts/interfaces/IOFT.sol +++ b/contracts/interfaces/IOFT.sol @@ -87,3 +87,15 @@ interface IOFT { address _refundAddress ) external payable returns (MessagingReceipt memory, OFTReceipt memory); } + +interface IEndpoint { + function eid() external view returns (uint32); +} + +interface IOAppCore { + /** + * @notice Retrieves the LayerZero endpoint associated with the OApp. + * @return iEndpoint The LayerZero endpoint as an interface. + */ + function endpoint() external view returns (IEndpoint iEndpoint); +} diff --git a/contracts/libraries/BytesLib.sol b/contracts/libraries/BytesLib.sol index 451118641..fe83d77c7 100644 --- a/contracts/libraries/BytesLib.sol +++ b/contracts/libraries/BytesLib.sol @@ -13,6 +13,21 @@ library BytesLib { * FUNCTIONS * **************************************/ + /** + * @notice Reads a uint16 from a bytes array at a given start index + * @param _bytes The bytes array to convert + * @param _start The start index of the uint16 + * @return result The uint16 result + */ + function toUint16(bytes memory _bytes, uint256 _start) internal pure returns (uint16 result) { + require(_bytes.length >= _start + 2, "toUint16_outOfBounds"); + + // solhint-disable-next-line no-inline-assembly + assembly { + result := mload(add(add(_bytes, 0x2), _start)) + } + } + /** * @notice Reads a uint32 from a bytes array at a given start index * @param _bytes The bytes array to convert diff --git a/contracts/libraries/MinimalLZOptions.sol b/contracts/libraries/MinimalLZOptions.sol new file mode 100644 index 000000000..6314a6b0d --- /dev/null +++ b/contracts/libraries/MinimalLZOptions.sol @@ -0,0 +1,124 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import { BytesLib } from "./BytesLib.sol"; +import "@openzeppelin/contracts/utils/math/SafeCast.sol"; + +/** + * @title MinimalExecutorOptions + * @notice This library is used to provide minimal required functionality of + * https://github.com/LayerZero-Labs/LayerZero-v2/blob/2ff4988f85b5c94032eb71bbc4073e69c078179d/packages/layerzero-v2/evm/messagelib/contracts/libs/ExecutorOptions.sol#L7 + */ +library MinimalExecutorOptions { + uint8 internal constant WORKER_ID = 1; + + uint8 internal constant OPTION_TYPE_LZRECEIVE = 1; + uint8 internal constant OPTION_TYPE_LZCOMPOSE = 3; + + function encodeLzReceiveOption(uint128 _gas, uint128 _value) internal pure returns (bytes memory) { + return _value == 0 ? abi.encodePacked(_gas) : abi.encodePacked(_gas, _value); + } + + function encodeLzComposeOption(uint16 _index, uint128 _gas, uint128 _value) internal pure returns (bytes memory) { + return _value == 0 ? abi.encodePacked(_index, _gas) : abi.encodePacked(_index, _gas, _value); + } +} + +/** + * @title MinimalLZOptions + * @notice This library is used to provide minimal functionality of + * https://github.com/LayerZero-Labs/devtools/blob/52ad590ab249f660f803ae3aafcbf7115733359c/packages/oapp-evm/contracts/oapp/libs/OptionsBuilder.sol + */ +library MinimalLZOptions { + // @dev Only used in `onlyType3` modifier + using BytesLib for bytes; + // @dev Only used in `addExecutorOption` + using SafeCast for uint256; + + // Constants for options types + uint16 internal constant TYPE_1 = 1; // legacy options type 1 + uint16 internal constant TYPE_2 = 2; // legacy options type 2 + uint16 internal constant TYPE_3 = 3; + + // Custom error message + error InvalidSize(uint256 max, uint256 actual); + error InvalidOptionType(uint16 optionType); + + // Modifier to ensure only options of type 3 are used + modifier onlyType3(bytes memory _options) { + if (_options.toUint16(0) != TYPE_3) revert InvalidOptionType(_options.toUint16(0)); + _; + } + + /** + * @dev Creates a new options container with type 3. + * @return options The newly created options container. + */ + function newOptions() internal pure returns (bytes memory) { + return abi.encodePacked(TYPE_3); + } + + /** + * @dev Adds an executor LZ receive option to the existing options. + * @param _options The existing options container. + * @param _gas The gasLimit used on the lzReceive() function in the OApp. + * @param _value The msg.value passed to the lzReceive() function in the OApp. + * @return options The updated options container. + * + * @dev When multiples of this option are added, they are summed by the executor + * eg. if (_gas: 200k, and _value: 1 ether) AND (_gas: 100k, _value: 0.5 ether) are sent in an option to the LayerZeroEndpoint, + * that becomes (300k, 1.5 ether) when the message is executed on the remote lzReceive() function. + */ + function addExecutorLzReceiveOption( + bytes memory _options, + uint128 _gas, + uint128 _value + ) internal pure onlyType3(_options) returns (bytes memory) { + bytes memory option = MinimalExecutorOptions.encodeLzReceiveOption(_gas, _value); + return addExecutorOption(_options, MinimalExecutorOptions.OPTION_TYPE_LZRECEIVE, option); + } + + /** + * @dev Adds an executor LZ compose option to the existing options. + * @param _options The existing options container. + * @param _index The index for the lzCompose() function call. + * @param _gas The gasLimit for the lzCompose() function call. + * @param _value The msg.value for the lzCompose() function call. + * @return options The updated options container. + * + * @dev When multiples of this option are added, they are summed PER index by the executor on the remote chain. + * @dev If the OApp sends N lzCompose calls on the remote, you must provide N incremented indexes starting with 0. + * ie. When your remote OApp composes (N = 3) messages, you must set this option for index 0,1,2 + */ + function addExecutorLzComposeOption( + bytes memory _options, + uint16 _index, + uint128 _gas, + uint128 _value + ) internal pure onlyType3(_options) returns (bytes memory) { + bytes memory option = MinimalExecutorOptions.encodeLzComposeOption(_index, _gas, _value); + return addExecutorOption(_options, MinimalExecutorOptions.OPTION_TYPE_LZCOMPOSE, option); + } + + /** + * @dev Adds an executor option to the existing options. + * @param _options The existing options container. + * @param _optionType The type of the executor option. + * @param _option The encoded data for the executor option. + * @return options The updated options container. + */ + function addExecutorOption( + bytes memory _options, + uint8 _optionType, + bytes memory _option + ) internal pure onlyType3(_options) returns (bytes memory) { + return + abi.encodePacked( + _options, + MinimalExecutorOptions.WORKER_ID, + _option.length.toUint16() + 1, // +1 for optionType + _optionType, + _option + ); + } +} diff --git a/contracts/libraries/OFTComposeMsgCodec.sol b/contracts/libraries/OFTComposeMsgCodec.sol new file mode 100644 index 000000000..a2b4bae6c --- /dev/null +++ b/contracts/libraries/OFTComposeMsgCodec.sol @@ -0,0 +1,97 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +/** + * @title OFTComposeMsgCodec + * @notice Copied from LZ implementation here: + * https://github.com/LayerZero-Labs/devtools/blob/608915a7e260d995ce28e41c4e4877db9b18613b/packages/oft-evm/contracts/libs/OFTComposeMsgCodec.sol#L5 + * + */ +library OFTComposeMsgCodec { + // Offset constants for decoding composed messages + uint8 private constant NONCE_OFFSET = 8; + uint8 private constant SRC_EID_OFFSET = 12; + uint8 private constant AMOUNT_LD_OFFSET = 44; + uint8 private constant COMPOSE_FROM_OFFSET = 76; + + /** + * @dev Encodes a OFT composed message. + * @param _nonce The nonce value. + * @param _srcEid The source endpoint ID. + * @param _amountLD The amount in local decimals. + * @param _composeMsg The composed message. + * @return _msg The encoded Composed message. + */ + function encode( + uint64 _nonce, + uint32 _srcEid, + uint256 _amountLD, + bytes memory _composeMsg // 0x[composeFrom][composeMsg] + ) internal pure returns (bytes memory _msg) { + _msg = abi.encodePacked(_nonce, _srcEid, _amountLD, _composeMsg); + } + + /** + * @dev Retrieves the nonce for the composed message. + * @param _msg The message. + * @return The nonce value. + */ + function nonce(bytes calldata _msg) internal pure returns (uint64) { + return uint64(bytes8(_msg[:NONCE_OFFSET])); + } + + /** + * @dev Retrieves the source endpoint ID for the composed message. + * @param _msg The message. + * @return The source endpoint ID. + */ + function srcEid(bytes calldata _msg) internal pure returns (uint32) { + return uint32(bytes4(_msg[NONCE_OFFSET:SRC_EID_OFFSET])); + } + + /** + * @dev Retrieves the amount in local decimals from the composed message. + * @param _msg The message. + * @return The amount in local decimals. + */ + function amountLD(bytes calldata _msg) internal pure returns (uint256) { + return uint256(bytes32(_msg[SRC_EID_OFFSET:AMOUNT_LD_OFFSET])); + } + + /** + * @dev Retrieves the composeFrom value from the composed message. + * @param _msg The message. + * @return The composeFrom value. + */ + function composeFrom(bytes calldata _msg) internal pure returns (bytes32) { + return bytes32(_msg[AMOUNT_LD_OFFSET:COMPOSE_FROM_OFFSET]); + } + + /** + * @dev Retrieves the composed message. + * @param _msg The message. + * @return The composed message. + */ + function composeMsg(bytes calldata _msg) internal pure returns (bytes memory) { + return _msg[COMPOSE_FROM_OFFSET:]; + } + + /** + * @dev Converts an address to bytes32. + * @param _addr The address to convert. + * @return The bytes32 representation of the address. + */ + function addressToBytes32(address _addr) internal pure returns (bytes32) { + return bytes32(uint256(uint160(_addr))); + } + + /** + * @dev Converts bytes32 to an address. + * @param _b The bytes32 value to convert. + * @return The address representation of bytes32. + */ + function bytes32ToAddress(bytes32 _b) internal pure returns (address) { + return address(uint160(uint256(_b))); + } +} diff --git a/contracts/periphery/mintburn/sponsored-oft/ComposeMsgCodec.sol b/contracts/periphery/mintburn/sponsored-oft/ComposeMsgCodec.sol new file mode 100644 index 000000000..d1d92b1ae --- /dev/null +++ b/contracts/periphery/mintburn/sponsored-oft/ComposeMsgCodec.sol @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.23; +import { BytesLib } from "../../../libraries/BytesLib.sol"; + +/// @notice Codec for params passed in OFT `composeMsg`. +library ComposeMsgCodec { + uint256 internal constant NONCE_OFFSET = 0; + uint256 internal constant DEADLINE_OFFSET = 32; + uint256 internal constant MAX_BPS_TO_SPONSOR_OFFSET = 64; + uint256 internal constant MAX_USER_SLIPPAGE_BPS_OFFSET = 96; + uint256 internal constant FINAL_RECIPIENT_OFFSET = 128; + uint256 internal constant FINAL_TOKEN_OFFSET = 160; + uint256 internal constant COMPOSE_MSG_BYTE_LENGTH = 192; + + function _encode( + bytes32 nonce, + uint256 deadline, + uint256 maxBpsToSponsor, + uint256 maxUserSlippageBps, + bytes32 finalRecipient, + bytes32 finalToken + ) internal pure returns (bytes memory) { + return abi.encode(nonce, deadline, maxBpsToSponsor, maxUserSlippageBps, finalRecipient, finalToken); + } + + function _getNonce(bytes memory data) internal pure returns (bytes32 v) { + return BytesLib.toBytes32(data, NONCE_OFFSET); + } + + function _getDeadline(bytes memory data) internal pure returns (uint256 v) { + return BytesLib.toUint256(data, DEADLINE_OFFSET); + } + + function _getMaxBpsToSponsor(bytes memory data) internal pure returns (uint256 v) { + return BytesLib.toUint256(data, MAX_BPS_TO_SPONSOR_OFFSET); + } + + function _getMaxUserSlippageBps(bytes memory data) internal pure returns (uint256 v) { + return BytesLib.toUint256(data, MAX_USER_SLIPPAGE_BPS_OFFSET); + } + + function _getFinalRecipient(bytes memory data) internal pure returns (bytes32 v) { + return BytesLib.toBytes32(data, FINAL_RECIPIENT_OFFSET); + } + + function _getFinalToken(bytes memory data) internal pure returns (bytes32 v) { + return BytesLib.toBytes32(data, FINAL_TOKEN_OFFSET); + } + + function _isValidComposeMsgBytelength(bytes memory data) internal pure returns (bool valid) { + valid = data.length == COMPOSE_MSG_BYTE_LENGTH; + } +} diff --git a/contracts/periphery/mintburn/sponsored-oft/DstOFTHandler.sol b/contracts/periphery/mintburn/sponsored-oft/DstOFTHandler.sol new file mode 100644 index 000000000..8aa342133 --- /dev/null +++ b/contracts/periphery/mintburn/sponsored-oft/DstOFTHandler.sol @@ -0,0 +1,170 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.23; + +import { ILayerZeroComposer } from "../../../external/interfaces/ILayerZeroComposer.sol"; +import { OFTComposeMsgCodec } from "../../../libraries/OFTComposeMsgCodec.sol"; +import { DonationBox } from "../../../chain-adapters/DonationBox.sol"; +import { HyperCoreLib } from "../../../libraries/HyperCoreLib.sol"; +import { ComposeMsgCodec } from "./ComposeMsgCodec.sol"; +import { AddressToBytes32, Bytes32ToAddress } from "../../../libraries/AddressConverters.sol"; +import { IOFT, IOAppCore } from "../../../interfaces/IOFT.sol"; +import { HyperCoreFlowExecutor } from "../HyperCoreFlowExecutor.sol"; + +import { AccessControl } from "@openzeppelin/contracts/access/AccessControl.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +/// @notice Handler that receives funds from LZ system, checks authorizations(both against LZ system and src chain +/// sender), and forwards authorized params to the `_executeFlow` function +contract DstOFTHandler is ILayerZeroComposer, HyperCoreFlowExecutor { + using ComposeMsgCodec for bytes; + using Bytes32ToAddress for bytes32; + using AddressToBytes32 for address; + + /// @notice We expect bridge amount that comes through to this Handler to be 1:1 with the src send amount, and we + /// require our src handler to ensure that it is. We don't sponsor extra bridge fees in this handler + uint256 public constant EXTRA_FEES_TO_SPONSOR = 0; + + address public immutable OFT_ENDPOINT_ADDRESS; + address public immutable IOFT_ADDRESS; + + /// @notice A mapping used to validate an incoming message against a list of authorized src periphery contracts. In + /// bytes32 to support non-EVM src chains + mapping(uint64 eid => bytes32 authorizedSrcPeriphery) public authorizedSrcPeripheryContracts; + + /// @notice A mapping used for nonce uniqueness checks. Our src periphery and LZ should have prevented this already, + /// but I guess better safe than sorry + mapping(bytes32 quoteNonce => bool used) usedNonces; + + /// @notice Emitted when a new authorized src periphery is configured + event SetAuthorizedPeriphery(uint32 srcEid, bytes32 srcPeriphery); + + /// @notice Thrown when trying to call lzCompose from a source periphery that's not been configured in `authorizedSrcPeripheryContracts` + error AuthorizedPeripheryNotSet(uint32 _srcEid); + /// @notice Thrown when source chain recipient is not authorized periphery contract + error UnauthorizedSrcPeriphery(uint32 _srcEid); + /// @notice Thrown when the supplied token does not match the supplied ioft messenger + error TokenIOFTMismatch(); + /// @notice Thrown when the supplied ioft address does not match the supplied endpoint address + error IOFTEndpointMismatch(); + /// @notice Thrown if Quote nonce was already used + error NonceAlreadyUsed(); + /// @notice Thrown if supplied OApp is not configured ioft + error InvalidOApp(); + /// @notice Thrown if called by an unauthorized endpoint + error UnauthorizedEndpoint(); + /// @notice Thrown when supplied _composeMsg format is unexpected + error InvalidComposeMsgFormat(); + + constructor( + address _oftEndpoint, + address _ioft, + address _donationBox, + address _baseToken, + uint32 _coreIndex, + bool _canBeUsedForAccountActivation, + uint64 _accountActivationFeeCore, + uint64 _bridgeSafetyBufferCore + ) + HyperCoreFlowExecutor( + _donationBox, + _baseToken, + _coreIndex, + _canBeUsedForAccountActivation, + _accountActivationFeeCore, + _bridgeSafetyBufferCore + ) + { + // baseToken is assigned on `HyperCoreFlowExecutor` creation + if (baseToken != IOFT(_ioft).token()) { + revert TokenIOFTMismatch(); + } + + OFT_ENDPOINT_ADDRESS = _oftEndpoint; + IOFT_ADDRESS = _ioft; + if (address(IOAppCore(IOFT_ADDRESS).endpoint()) != address(OFT_ENDPOINT_ADDRESS)) { + revert IOFTEndpointMismatch(); + } + } + + function setAuthorizedPeriphery(uint32 srcEid, bytes32 srcPeriphery) external onlyDefaultAdmin { + authorizedSrcPeripheryContracts[srcEid] = srcPeriphery; + emit SetAuthorizedPeriphery(srcEid, srcPeriphery); + } + + /** + * @notice Handles incoming composed messages from LayerZero. + * @dev Ensures the message comes from the correct OApp and is sent through the authorized endpoint. + * + * @param _oApp The address of the OApp that is sending the composed message. + */ + function lzCompose( + address _oApp, + bytes32 /* _guid */, + bytes calldata _message, + address /* _executor */, + bytes calldata /* _extraData */ + ) external payable override { + _requireAuthorizedMessage(_oApp, _message); + + // Decode the actual `composeMsg` payload to extract the recipient address + bytes memory composeMsg = OFTComposeMsgCodec.composeMsg(_message); + + // This check is a safety mechanism against blackholing funds. The funds were sent by the authorized periphery + // contract, but if the length is unexpected, we require funds be rescued, this is not a situation we aim to + // revover from in `lzCompose` call + if (composeMsg._isValidComposeMsgBytelength() == false) { + revert InvalidComposeMsgFormat(); + } + + bytes32 quoteNonce = composeMsg._getNonce(); + if (usedNonces[quoteNonce]) { + revert NonceAlreadyUsed(); + } + usedNonces[quoteNonce] = true; + + uint256 amountLD = OFTComposeMsgCodec.amountLD(_message); + uint256 maxBpsToSponsor = composeMsg._getMaxBpsToSponsor(); + uint256 maxUserSlippageBps = composeMsg._getMaxUserSlippageBps(); + address finalRecipient = composeMsg._getFinalRecipient().toAddress(); + address finalToken = composeMsg._getFinalToken().toAddress(); + + _executeFlow( + amountLD, + quoteNonce, + maxBpsToSponsor, + maxUserSlippageBps, + finalRecipient, + finalToken, + EXTRA_FEES_TO_SPONSOR + ); + } + + /// @notice Checks that message was authorized by LayerZero's identity system and that it came from authorized src periphery + function _requireAuthorizedMessage(address _oApp, bytes calldata _message) internal view { + if (_oApp != IOFT_ADDRESS) { + revert InvalidOApp(); + } + if (msg.sender != OFT_ENDPOINT_ADDRESS) { + revert UnauthorizedEndpoint(); + } + _requireAuthorizedPeriphery(_message); + } + + /// @dev Checks that _message came from the authorized src periphery contract stored in `authorizedSrcPeripheryContracts` + function _requireAuthorizedPeriphery(bytes calldata _message) internal view { + uint32 _srcEid = OFTComposeMsgCodec.srcEid(_message); + bytes32 authorizedPeriphery = authorizedSrcPeripheryContracts[_srcEid]; + if (authorizedPeriphery == bytes32(0)) { + revert AuthorizedPeripheryNotSet(_srcEid); + } + + // Decode original sender + bytes32 _composeFromBytes32 = OFTComposeMsgCodec.composeFrom(_message); + + // We don't allow arbitrary src chain callers. If such a caller does send a message to this handler, the funds + // will remain in this contract and will have to be rescued by an admin rescue function + if (authorizedPeriphery != _composeFromBytes32) { + revert UnauthorizedSrcPeriphery(_srcEid); + } + } +} diff --git a/contracts/periphery/mintburn/sponsored-oft/QuoteSignLib.sol b/contracts/periphery/mintburn/sponsored-oft/QuoteSignLib.sol new file mode 100644 index 000000000..26351d6ed --- /dev/null +++ b/contracts/periphery/mintburn/sponsored-oft/QuoteSignLib.sol @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.23; + +import { ECDSA } from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; +import { SignedQuoteParams } from "./Structs.sol"; + +/// @notice Lib to check the signature for `SignedQuoteParams`. +/// The signature is checked against a keccak hash of abi-encoded fields of `SignedQuoteParams` +library QuoteSignLib { + using ECDSA for bytes32; + + /// @notice Compute the keccak of all `SignedQuoteParams` fields + function hash(SignedQuoteParams calldata p) internal pure returns (bytes32) { + return + keccak256( + abi.encode( + p.srcEid, + p.dstEid, + p.destinationHandler, + p.amountLD, + p.nonce, + p.deadline, + p.maxBpsToSponsor, + p.finalRecipient, + p.finalToken, + p.lzReceiveGasLimit, + p.lzComposeGasLimit + ) + ); + } + + /// @notice Recover the signer for the given params and signature. + function recoverSigner(SignedQuoteParams calldata p, bytes calldata signature) internal pure returns (address) { + bytes32 digest = hash(p); + return digest.recover(signature); + } + + /// @notice Verify that `expectedSigner` signed `p` with `signature`. + function isSignatureValid( + address expectedSigner, + SignedQuoteParams calldata p, + bytes calldata signature + ) internal pure returns (bool) { + return recoverSigner(p, signature) == expectedSigner; + } +} diff --git a/contracts/periphery/mintburn/sponsored-oft/SponsoredOFTSrcPeriphery.sol b/contracts/periphery/mintburn/sponsored-oft/SponsoredOFTSrcPeriphery.sol new file mode 100644 index 000000000..226f88d89 --- /dev/null +++ b/contracts/periphery/mintburn/sponsored-oft/SponsoredOFTSrcPeriphery.sol @@ -0,0 +1,155 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.23; + +import { Quote } from "./Structs.sol"; +import { QuoteSignLib } from "./QuoteSignLib.sol"; +import { ComposeMsgCodec } from "./ComposeMsgCodec.sol"; + +import { IOFT, IOAppCore, IEndpoint, SendParam, MessagingFee } from "../../../interfaces/IOFT.sol"; +import { AddressToBytes32 } from "../../../libraries/AddressConverters.sol"; +import { MinimalLZOptions } from "../../../libraries/MinimalLZOptions.sol"; + +import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; + +/// @notice Source chain periphery contract for users to interact with to start a sponsored or a non-sponsored flow +/// that allows custom Accross-supported flows on destination chain. Uses LayzerZero's OFT as an underlying bridge +contract SponsoredOFTSrcPeriphery is Ownable { + using AddressToBytes32 for address; + using MinimalLZOptions for bytes; + using SafeERC20 for IERC20; + + bytes public constant EMPTY_OFT_COMMAND = new bytes(0); + + /// @notice Token that's being sent by an OFT bridge + address public immutable TOKEN; + /// @notice OFT contract to interact with to initiate the bridge + address public immutable OFT_MESSENGER; + + /// @notice Source endpoint id + uint32 public immutable SRC_EID; + + /// @notice Signer public key to check the signed quote against + address public signer; + + /// @notice A mapping to enforce only a single usage per quote + mapping(bytes32 => bool) public quoteNonces; + + /// @notice Event with auxiliary information. To be used in concert with OftSent event to get relevant quote details + event SponsoredOFTSend( + bytes32 indexed quoteNonce, + address indexed originSender, + bytes32 indexed finalRecipient, + uint256 quoteDeadline, + uint256 maxBpsToSponsor, + uint256 maxUserSlippageBps, + bytes32 finalToken, + bytes sig + ); + + /// @notice Thrown when the source eid of the ioft messenger does not match the src eid supplied + error IncorrectSrcEid(); + /// @notice Thrown when the supplied token does not match the supplied ioft messenger + error TokenIOFTMismatch(); + /// @notice Thrown when the signer for quote does not match `signer` + error IncorrectSignature(); + /// @notice Thrown if Quote has expired + error QuoteExpired(); + /// @notice Thrown if Quote nonce was already used + error NonceAlreadyUsed(); + + constructor(address _token, address _oftMessenger, uint32 _srcEid, address _signer) { + TOKEN = _token; + OFT_MESSENGER = _oftMessenger; + SRC_EID = _srcEid; + if (IOAppCore(_oftMessenger).endpoint().eid() != _srcEid) { + revert IncorrectSrcEid(); + } + if (IOFT(_oftMessenger).token() != _token) { + revert TokenIOFTMismatch(); + } + signer = _signer; + } + + /// @notice Main entrypoint function to start the user flow + function deposit(Quote calldata quote, bytes calldata signature) external payable { + // Step 1: validate quote and mark quote nonce used + _validateQuote(quote, signature); + quoteNonces[quote.signedParams.nonce] = true; + + // Step 2: build oft send params from quote + (SendParam memory sendParam, MessagingFee memory fee, address refundAddress) = _buildOftTransfer(quote); + + // Step 3: pull tokens from user and apporove OFT messenger + IERC20(TOKEN).safeTransferFrom(msg.sender, address(this), quote.signedParams.amountLD); + IERC20(TOKEN).forceApprove(address(OFT_MESSENGER), quote.signedParams.amountLD); + + // Step 4: send oft transfer and emit event with auxiliary data + IOFT(OFT_MESSENGER).send{ value: msg.value }(sendParam, fee, refundAddress); + emit SponsoredOFTSend( + quote.signedParams.nonce, + msg.sender, + quote.signedParams.finalRecipient, + quote.signedParams.deadline, + quote.signedParams.maxBpsToSponsor, + quote.unsignedParams.maxUserSlippageBps, + quote.signedParams.finalToken, + signature + ); + } + + function _buildOftTransfer( + Quote calldata quote + ) internal view returns (SendParam memory, MessagingFee memory, address) { + bytes memory composeMsg = ComposeMsgCodec._encode( + quote.signedParams.nonce, + quote.signedParams.deadline, + quote.signedParams.maxBpsToSponsor, + quote.unsignedParams.maxUserSlippageBps, + quote.signedParams.finalRecipient, + quote.signedParams.finalToken + ); + + bytes memory extraOptions = MinimalLZOptions + .newOptions() + .addExecutorLzReceiveOption(uint128(quote.signedParams.lzReceiveGasLimit), uint128(0)) + .addExecutorLzComposeOption(uint16(0), uint128(quote.signedParams.lzComposeGasLimit), uint128(0)); + + SendParam memory sendParam = SendParam( + quote.signedParams.dstEid, + quote.signedParams.destinationHandler, + // Only support OFT sends that don't take fees in sent token. Set `minAmountLD = amountLD` to enforce this + quote.signedParams.amountLD, + quote.signedParams.amountLD, + extraOptions, + composeMsg, + // TODO? Is this an issue for ~classic tokens like USDT0? + // Only support empty OFT commands + EMPTY_OFT_COMMAND + ); + + MessagingFee memory fee = IOFT(OFT_MESSENGER).quoteSend(sendParam, false); + + return (sendParam, fee, quote.unsignedParams.refundRecipient); + } + + function _validateQuote(Quote calldata quote, bytes calldata signature) internal view { + if (!QuoteSignLib.isSignatureValid(signer, quote.signedParams, signature)) { + revert IncorrectSignature(); + } + if (quote.signedParams.deadline < block.timestamp) { + revert QuoteExpired(); + } + if (quote.signedParams.srcEid != SRC_EID) { + revert IncorrectSrcEid(); + } + if (quoteNonces[quote.signedParams.nonce]) { + revert NonceAlreadyUsed(); + } + } + + function setSigner(address _newSigner) external onlyOwner { + signer = _newSigner; + } +} diff --git a/contracts/periphery/mintburn/sponsored-oft/Structs.sol b/contracts/periphery/mintburn/sponsored-oft/Structs.sol new file mode 100644 index 000000000..cfc3eaf33 --- /dev/null +++ b/contracts/periphery/mintburn/sponsored-oft/Structs.sol @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.23; + +import { SendParam, MessagingFee } from "../../../interfaces/IOFT.sol"; + +/// @notice A structure with all the relevant information about a particular sponsored bridging flow order +struct Quote { + SignedQuoteParams signedParams; + UnsignedQuoteParams unsignedParams; +} + +/// @notice Signed params of the sponsored bridging flow quote +struct SignedQuoteParams { + uint32 srcEid; // Source endpoint ID in OFT system. + // Params passed into OFT.send() + uint32 dstEid; // Destination endpoint ID in OFT system. + bytes32 destinationHandler; // `to`. Recipient address. Address of our Composer contract + uint256 amountLD; // Amount to send in local decimals. + // Signed params that go into `composeMsg` + bytes32 nonce; // quote nonce + uint256 deadline; // quote deadline + uint256 maxBpsToSponsor; // max bps (of sent amount) to sponsor for 1:1 + bytes32 finalRecipient; // user address on destination + bytes32 finalToken; // final token user will receive (might be different from OFT token we're sending) + // Signed gas limits for destination-side LZ execution + uint256 lzReceiveGasLimit; // gas limit for `lzReceive` call on destination side + uint256 lzComposeGasLimit; // gas limit for `lzCompose` call on destination side +} + +/// @notice Unsigned params of the sponsored bridging flow quote: user is free to choose these +struct UnsignedQuoteParams { + address refundRecipient; // recipient of extra msg.value passed into the OFT send on src chain + uint256 maxUserSlippageBps; // slippage tolerance for the swap on the destination +} diff --git a/deploy/consts.ts b/deploy/consts.ts index 15d00e33f..2ec5a9bb5 100644 --- a/deploy/consts.ts +++ b/deploy/consts.ts @@ -194,6 +194,7 @@ export const L2_ADDRESS_MAP: { [key: number]: { [contractName: string]: string } }, [CHAIN_IDs.HYPEREVM]: { cctpV2TokenMessenger: "0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d", + oftEndpoint: "0x3A73033C0b1407574C76BdBAc67f126f6b4a9AA9", cctpV2MessageTransmitter: "0x81D40F21F12A8F0E3252Bccb954D722d4c464B64", permit2: "0x000000000022D473030F116dDEE9F6B43aC78BA3", }, diff --git a/generated/constants.json b/generated/constants.json index 50ae54d74..62f7b8d55 100644 --- a/generated/constants.json +++ b/generated/constants.json @@ -611,6 +611,7 @@ }, "999": { "cctpV2TokenMessenger": "0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d", + "oftEndpoint": "0x3A73033C0b1407574C76BdBAc67f126f6b4a9AA9", "cctpV2MessageTransmitter": "0x81D40F21F12A8F0E3252Bccb954D722d4c464B64", "permit2": "0x000000000022D473030F116dDEE9F6B43aC78BA3" }, diff --git a/script/115DeploySrcOFTPeriphery.sol b/script/115DeploySrcOFTPeriphery.sol new file mode 100644 index 000000000..7c9c4af01 --- /dev/null +++ b/script/115DeploySrcOFTPeriphery.sol @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +import { Script } from "forge-std/Script.sol"; +import { Test } from "forge-std/Test.sol"; +import { console } from "forge-std/console.sol"; + +import { DonationBox } from "../contracts/chain-adapters/DonationBox.sol"; +import { DeploymentUtils } from "./utils/DeploymentUtils.sol"; +import { SponsoredOFTSrcPeriphery } from "../contracts/periphery/mintburn/sponsored-oft/SponsoredOFTSrcPeriphery.sol"; + +// Deploy: forge script script/115DeploySrcOFTPeriphery.sol:DepoySrcOFTPeriphery --rpc-url -vvvv +contract DepoySrcOFTPeriphery is Script, Test, DeploymentUtils { + function run() external { + console.log("Deploying SponsoredOFTSrcPeriphery..."); + console.log("Chain ID:", block.chainid); + + string memory deployerMnemonic = vm.envString("MNEMONIC"); + uint256 deployerPrivateKey = vm.deriveKey(deployerMnemonic, 0); + address deployer = vm.addr(deployerPrivateKey); + + vm.startBroadcast(deployerPrivateKey); + + SponsoredOFTSrcPeriphery srcOftPeriphery = new SponsoredOFTSrcPeriphery( + 0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9, + 0x14E4A1B13bf7F943c8ff7C51fb60FA964A298D92, + 30110, + deployer + ); + + console.log("SponsoredOFTSrcPeriphery deployed to:", address(srcOftPeriphery)); + + vm.stopBroadcast(); + } +} diff --git a/script/116DeployDstOFTHandler.sol b/script/116DeployDstOFTHandler.sol new file mode 100644 index 000000000..a9f7b04aa --- /dev/null +++ b/script/116DeployDstOFTHandler.sol @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +import { Script } from "forge-std/Script.sol"; +import { Test } from "forge-std/Test.sol"; +import { console } from "forge-std/console.sol"; + +import { DonationBox } from "../contracts/chain-adapters/DonationBox.sol"; +import { DeploymentUtils } from "./utils/DeploymentUtils.sol"; +import { DstOFTHandler } from "../contracts/periphery/mintburn/sponsored-oft/DstOFTHandler.sol"; + +// Deploy: forge script script/116DeployDstOFTHandler.sol:DeployDstOFTHandler --rpc-url -vvvv +contract DeployDstOFTHandler is Script, Test, DeploymentUtils { + function run() external { + console.log("Deploying DstOFTHandler..."); + console.log("Chain ID:", block.chainid); + + string memory deployerMnemonic = vm.envString("MNEMONIC"); + uint256 deployerPrivateKey = vm.deriveKey(deployerMnemonic, 0); + + vm.startBroadcast(deployerPrivateKey); + + address oftEndpoint = getL2Address(block.chainid, "oftEndpoint"); + address ioft = 0x904861a24F30EC96ea7CFC3bE9EA4B476d237e98; + + address baseToken = 0xB8CE59FC3717ada4C02eaDF9682A9e934F625ebb; + uint32 coreIndex = 268; + bool canBeUsedForAccountActivation = true; + uint64 accountActivationFeeCore = 100000000; + uint64 bridgeSafetyBufferCore = 1_000_000_000_00000000; // 1billion USDC (8 decimals) + + DonationBox donationBox = new DonationBox(); + + DstOFTHandler dstOFTHandler = new DstOFTHandler( + oftEndpoint, + ioft, + address(donationBox), + baseToken, + coreIndex, + canBeUsedForAccountActivation, + accountActivationFeeCore, + bridgeSafetyBufferCore + ); + console.log("DstOFTHandler deployed to:", address(dstOFTHandler)); + + donationBox.transferOwnership(address(dstOFTHandler)); + + console.log("DonationBox ownership transferred to:", address(dstOFTHandler)); + + vm.stopBroadcast(); + } +} diff --git a/script/mintburn/CreateSponsoredDeposit.sol b/script/mintburn/CreateSponsoredDeposit.sol new file mode 100644 index 000000000..2fccf74b9 --- /dev/null +++ b/script/mintburn/CreateSponsoredDeposit.sol @@ -0,0 +1,138 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +import { Script } from "forge-std/Script.sol"; +import { SponsoredOFTSrcPeriphery } from "../../contracts/periphery/mintburn/sponsored-oft/SponsoredOFTSrcPeriphery.sol"; +import { Quote, SignedQuoteParams, UnsignedQuoteParams } from "../../contracts/periphery/mintburn/sponsored-oft/Structs.sol"; +import { AddressToBytes32 } from "../../contracts/libraries/AddressConverters.sol"; +import { ComposeMsgCodec } from "../../contracts/periphery/mintburn/sponsored-oft/ComposeMsgCodec.sol"; +import { MinimalLZOptions } from "../../contracts/libraries/MinimalLZOptions.sol"; +import { IOFT, SendParam, MessagingFee } from "../../contracts/interfaces/IOFT.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; + +/// @notice Used in place of // import { QuoteSignLib } from "../contracts/periphery/mintburn/sponsored-oft/QuoteSignLib.sol"; +/// just for the hashing function that works with a memory funciton argument +library DebugQuoteSignLib { + /// @notice Compute the keccak of all `SignedQuoteParams` fields. Accept memory arg + function hashMemory(SignedQuoteParams memory p) internal pure returns (bytes32) { + return + keccak256( + abi.encode( + p.srcEid, + p.dstEid, + p.destinationHandler, + p.amountLD, + p.nonce, + p.deadline, + p.maxBpsToSponsor, + p.finalRecipient, + p.finalToken, + p.lzReceiveGasLimit, + p.lzComposeGasLimit + ) + ); + } +} + +// forge script script/mintburn/CreateSponsoredDeposit.sol:CreateSponsoredDeposit --rpc-url arbitrum -vvvv +contract CreateSponsoredDeposit is Script { + using AddressToBytes32 for address; + using SafeERC20 for IERC20; + using MinimalLZOptions for bytes; + + function run() external { + string memory deployerMnemonic = vm.envString("MNEMONIC"); + uint256 deployerPrivateKey = vm.deriveKey(deployerMnemonic, 0); + address deployer = vm.addr(deployerPrivateKey); // dev wallet + + // --- START CONFIG --- + uint32 srcEid = 30110; // Arbitrum + address srcPeriphery = 0x2C4413C70Fd1BDB109d7DFEE7310f4B692Dec381; + address token = 0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9; // Arbitrum USDT + uint256 amountLD = 2 * 10 ** 6 + 5456; // 1 USDT (6 decimals) + bytes32 nonce = bytes32(uint256(12351)); // Replace with unique nonce per deposit + uint256 deadline = block.timestamp + 1 hours; + uint256 maxBpsToSponsor = 100; // 1% + uint256 maxUserSlippageBps = 50; // 0.5% + uint32 dstEid = 30367; // HyperEVM + address destinationHandler = 0x40ad479382Ad2a5c3061487A5094a677B00f6Cb0; + address finalRecipient = 0xD1A68de1d242B3b98A7230ba003c19f7cF90e360; // alternative dev wallet + address finalToken = 0xB8CE59FC3717ada4C02eaDF9682A9e934F625ebb; // USDT0 @ HyperEVM + uint256 lzReceiveGasLimit = 200_000; + uint256 lzComposeGasLimit = 300_000; + address refundRecipient = deployer; // dev wallet + // --- END CONFIG --- + + SponsoredOFTSrcPeriphery srcPeripheryContract = SponsoredOFTSrcPeriphery(srcPeriphery); + require(srcPeripheryContract.signer() == deployer, "quote signer mismatch"); + + SignedQuoteParams memory signedParams = SignedQuoteParams({ + srcEid: srcEid, + dstEid: dstEid, + destinationHandler: destinationHandler.toBytes32(), + amountLD: amountLD, + nonce: nonce, + deadline: deadline, + maxBpsToSponsor: maxBpsToSponsor, + finalRecipient: finalRecipient.toBytes32(), + finalToken: finalToken.toBytes32(), + lzReceiveGasLimit: lzReceiveGasLimit, + lzComposeGasLimit: lzComposeGasLimit + }); + + UnsignedQuoteParams memory unsignedParams = UnsignedQuoteParams({ + refundRecipient: refundRecipient, + maxUserSlippageBps: maxUserSlippageBps + }); + + Quote memory quote = Quote({ signedParams: signedParams, unsignedParams: unsignedParams }); + + bytes32 quoteDigest = DebugQuoteSignLib.hashMemory(signedParams); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(deployerPrivateKey, quoteDigest); + bytes memory signature = abi.encodePacked(r, s, v); + + MessagingFee memory fee = _quoteMessagingFee(srcPeripheryContract, quote); + + vm.startBroadcast(deployerPrivateKey); + + IERC20(token).forceApprove(srcPeriphery, amountLD); + + srcPeripheryContract.deposit{ value: fee.nativeFee }(quote, signature); + + vm.stopBroadcast(); + } + + function _quoteMessagingFee( + SponsoredOFTSrcPeriphery srcPeripheryContract, + Quote memory quote + ) internal view returns (MessagingFee memory) { + address oftMessenger = srcPeripheryContract.OFT_MESSENGER(); + + bytes memory composeMsg = ComposeMsgCodec._encode( + quote.signedParams.nonce, + quote.signedParams.deadline, + quote.signedParams.maxBpsToSponsor, + quote.unsignedParams.maxUserSlippageBps, + quote.signedParams.finalRecipient, + quote.signedParams.finalToken + ); + + bytes memory extraOptions = MinimalLZOptions + .newOptions() + .addExecutorLzReceiveOption(uint128(quote.signedParams.lzReceiveGasLimit), uint128(0)) + .addExecutorLzComposeOption(uint16(0), uint128(quote.signedParams.lzComposeGasLimit), uint128(0)); + + SendParam memory sendParam = SendParam({ + dstEid: quote.signedParams.dstEid, + to: quote.signedParams.destinationHandler, + amountLD: quote.signedParams.amountLD, + minAmountLD: quote.signedParams.amountLD, + extraOptions: extraOptions, + composeMsg: composeMsg, + oftCmd: srcPeripheryContract.EMPTY_OFT_COMMAND() + }); + + return IOFT(oftMessenger).quoteSend(sendParam, false); + } +} diff --git a/script/mintburn/SetAuthorizedPeriphery.s.sol b/script/mintburn/SetAuthorizedPeriphery.s.sol new file mode 100644 index 000000000..d94524388 --- /dev/null +++ b/script/mintburn/SetAuthorizedPeriphery.s.sol @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +import { Script } from "forge-std/Script.sol"; +import { DstOFTHandler } from "../../contracts/periphery/mintburn/sponsored-oft/DstOFTHandler.sol"; +import { AddressToBytes32 } from "../../contracts/libraries/AddressConverters.sol"; + +// forge script script/mintburn/SetAuthorizedPeriphery.s.sol:SetAuthorizedPeriphery --rpc-url hyper-evm -vvvv +contract SetAuthorizedPeriphery is Script { + using AddressToBytes32 for address; + + function run() external { + string memory deployerMnemonic = vm.envString("MNEMONIC"); + uint256 deployerPrivateKey = vm.deriveKey(deployerMnemonic, 0); + + // --- START CONFIG --- + uint32 srcEid = 30110; // Arbitrum + address srcPeriphery = 0x2C4413C70Fd1BDB109d7DFEE7310f4B692Dec381; + address dstHandlerAddress = 0x40ad479382Ad2a5c3061487A5094a677B00f6Cb0; + // --- END CONFIG --- + + DstOFTHandler dstHandler = DstOFTHandler(dstHandlerAddress); + + vm.startBroadcast(deployerPrivateKey); + + dstHandler.setAuthorizedPeriphery(srcEid, srcPeriphery.toBytes32()); + + vm.stopBroadcast(); + } +} diff --git a/test/evm/hardhat/chain-adapters/Arbitrum_Adapter.ts b/test/evm/hardhat/chain-adapters/Arbitrum_Adapter.ts index 2e58c5d2f..57c31b866 100644 --- a/test/evm/hardhat/chain-adapters/Arbitrum_Adapter.ts +++ b/test/evm/hardhat/chain-adapters/Arbitrum_Adapter.ts @@ -27,8 +27,8 @@ import { MessagingReceiptStructOutput, OFTReceiptStructOutput, SendParamStruct, -} from "../../../../typechain/contracts/interfaces/IOFT"; -import { IOFT__factory } from "../../../../typechain/factories/contracts/interfaces/IOFT__factory"; +} from "../../../../typechain/contracts/interfaces/IOFT.sol/IOFT"; +import { IOFT__factory } from "../../../../typechain/factories/contracts/interfaces/IOFT.sol/IOFT__factory"; import { hubPoolFixture, enableTokensForLP } from "../fixtures/HubPool.Fixture"; import { constructSingleChainTree } from "../MerkleLib.utils"; import { CIRCLE_DOMAIN_IDs } from "../../../../deploy/consts"; diff --git a/test/evm/hardhat/chain-adapters/Polygon_Adapter.ts b/test/evm/hardhat/chain-adapters/Polygon_Adapter.ts index b36181d4d..f8f3850a6 100644 --- a/test/evm/hardhat/chain-adapters/Polygon_Adapter.ts +++ b/test/evm/hardhat/chain-adapters/Polygon_Adapter.ts @@ -32,8 +32,8 @@ import { MessagingReceiptStructOutput, OFTReceiptStructOutput, SendParamStruct, -} from "../../../../typechain/contracts/interfaces/IOFT"; -import { IOFT__factory } from "../../../../typechain/factories/contracts/interfaces/IOFT__factory"; +} from "../../../../typechain/contracts/interfaces/IOFT.sol/IOFT"; +import { IOFT__factory } from "../../../../typechain/factories/contracts/interfaces/IOFT.sol/IOFT__factory"; import { CIRCLE_DOMAIN_IDs } from "../../../../deploy/consts"; import { AdapterStore, AdapterStore__factory } from "../../../../typechain"; import { CHAIN_IDs } from "@across-protocol/constants"; diff --git a/test/evm/hardhat/chain-specific-spokepools/Arbitrum_SpokePool.ts b/test/evm/hardhat/chain-specific-spokepools/Arbitrum_SpokePool.ts index 36a74031f..690d3a206 100644 --- a/test/evm/hardhat/chain-specific-spokepools/Arbitrum_SpokePool.ts +++ b/test/evm/hardhat/chain-specific-spokepools/Arbitrum_SpokePool.ts @@ -26,8 +26,8 @@ import { MessagingReceiptStructOutput, OFTReceiptStructOutput, SendParamStruct, -} from "../../../../typechain/contracts/interfaces/IOFT"; -import { IOFT__factory } from "../../../../typechain/factories/contracts/interfaces/IOFT__factory"; +} from "../../../../typechain/contracts/interfaces/IOFT.sol/IOFT"; +import { IOFT__factory } from "../../../../typechain/factories/contracts/interfaces/IOFT.sol/IOFT__factory"; import { CHAIN_IDs } from "@across-protocol/constants"; let hubPool: Contract, arbitrumSpokePool: Contract, dai: Contract, weth: Contract, l2UsdtContract: Contract; diff --git a/test/evm/hardhat/chain-specific-spokepools/Polygon_SpokePool.ts b/test/evm/hardhat/chain-specific-spokepools/Polygon_SpokePool.ts index e74092837..b3989e5c3 100644 --- a/test/evm/hardhat/chain-specific-spokepools/Polygon_SpokePool.ts +++ b/test/evm/hardhat/chain-specific-spokepools/Polygon_SpokePool.ts @@ -25,13 +25,13 @@ import { } from "../../../../utils/utils"; import { getOftEid } from "../../../../utils/utils"; import { CHAIN_IDs } from "@across-protocol/constants"; -import { IOFT__factory } from "../../../../typechain/factories/contracts/interfaces/IOFT__factory"; +import { IOFT__factory } from "../../../../typechain/factories/contracts/interfaces/IOFT.sol/IOFT__factory"; import { MessagingFeeStructOutput, MessagingReceiptStructOutput, OFTReceiptStructOutput, SendParamStruct, -} from "../../../../typechain/contracts/interfaces/IOFT"; +} from "../../../../typechain/contracts/interfaces/IOFT.sol/IOFT"; import { hre } from "../../../../utils/utils.hre"; import { hubPoolFixture } from "../fixtures/HubPool.Fixture"; import { buildRelayerRefundLeaves, buildRelayerRefundTree, constructSingleRelayerRefundTree } from "../MerkleLib.utils";