diff --git a/contracts/external/libraries/OFTCoreMath.sol b/contracts/external/libraries/OFTCoreMath.sol new file mode 100644 index 000000000..43213854d --- /dev/null +++ b/contracts/external/libraries/OFTCoreMath.sol @@ -0,0 +1,61 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +/** + * @title OFTCoreMath + * @notice Copied from LZ implementation here: + * `https://github.com/LayerZero-Labs/devtools/blob/16daaee36fe802d11aa99b89c29bb74447354483/packages/oft-evm/contracts/OFTCore.sol#L364` + * Code was not modified beyond adding `uint8 _sharedDecimals` to constructor args and substituting `sharedDecimals()` calls with it + */ +abstract contract OFTCoreMath { + error InvalidLocalDecimals(); + error AmountSDOverflowed(uint256 amountSD); + + // @notice Provides a conversion rate when swapping between denominations of SD and LD + // - shareDecimals == SD == shared Decimals + // - localDecimals == LD == local decimals + // @dev Considers that tokens have different decimal amounts on various chains. + // @dev eg. + // For a token + // - locally with 4 decimals --> 1.2345 => uint(12345) + // - remotely with 2 decimals --> 1.23 => uint(123) + // - The conversion rate would be 10 ** (4 - 2) = 100 + // @dev If you want to send 1.2345 -> (uint 12345), you CANNOT represent that value on the remote, + // you can only display 1.23 -> uint(123). + // @dev To preserve the dust that would otherwise be lost on that conversion, + // we need to unify a denomination that can be represented on ALL chains inside of the OFT mesh + uint256 public immutable decimalConversionRate; + + /** + * @dev Constructor. + * @param _localDecimals The decimals of the token on the local chain (this chain). + * @param _sharedDecimals The shared decimals used by the OFT. + */ + constructor(uint8 _localDecimals, uint8 _sharedDecimals) { + if (_localDecimals < _sharedDecimals) revert InvalidLocalDecimals(); + decimalConversionRate = 10 ** (_localDecimals - _sharedDecimals); + } + + /** + * @dev Internal function to convert an amount from shared decimals into local decimals. + * @param _amountSD The amount in shared decimals. + * @return amountLD The amount in local decimals. + */ + function _toLD(uint64 _amountSD) internal view virtual returns (uint256 amountLD) { + return _amountSD * decimalConversionRate; + } + + /** + * @dev Internal function to convert an amount from local decimals into shared decimals. + * @param _amountLD The amount in local decimals. + * @return amountSD The amount in shared decimals. + * + * @dev Reverts if the _amountLD in shared decimals overflows uint64. + * @dev eg. uint(2**64 + 123) with a conversion rate of 1 wraps around 2**64 to uint(123). + */ + function _toSD(uint256 _amountLD) internal view virtual returns (uint64 amountSD) { + uint256 _amountSD = _amountLD / decimalConversionRate; + if (_amountSD > type(uint64).max) revert AmountSDOverflowed(_amountSD); + return uint64(_amountSD); + } +} diff --git a/contracts/interfaces/IOFT.sol b/contracts/interfaces/IOFT.sol index 28ac0ba1d..9aaebd921 100644 --- a/contracts/interfaces/IOFT.sol +++ b/contracts/interfaces/IOFT.sol @@ -54,6 +54,12 @@ interface IOFT { */ function token() external view returns (address); + /** + * @notice Retrieves the shared decimals of the OFT. + * @return sharedDecimals The shared decimals of the OFT. + */ + function sharedDecimals() external view returns (uint8); + /** * @notice Provides a quote for the send() operation. * @param _sendParam The parameters for the send() operation. diff --git a/contracts/periphery/mintburn/sponsored-oft/ComposeMsgCodec.sol b/contracts/periphery/mintburn/sponsored-oft/ComposeMsgCodec.sol index 1d8e48ef7..8125884df 100644 --- a/contracts/periphery/mintburn/sponsored-oft/ComposeMsgCodec.sol +++ b/contracts/periphery/mintburn/sponsored-oft/ComposeMsgCodec.sol @@ -5,19 +5,21 @@ import { BytesLib } from "../../../external/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 DESTINATION_DEX_OFFSET = 192; - uint256 internal constant ACCOUNT_CREATION_MODE_OFFSET = 224; - uint256 internal constant EXECUTION_MODE_OFFSET = 256; - // Minimum length with empty actionData: 9 regular params (32 bytes each) and 1 dynamic byte array (minumum 64 bytes) - uint256 internal constant MIN_COMPOSE_MSG_BYTE_LENGTH = 352; + uint256 internal constant AMOUNT_SD_OFFSET = 32; + uint256 internal constant DEADLINE_OFFSET = 64; + uint256 internal constant MAX_BPS_TO_SPONSOR_OFFSET = 96; + uint256 internal constant MAX_USER_SLIPPAGE_BPS_OFFSET = 128; + uint256 internal constant FINAL_RECIPIENT_OFFSET = 160; + uint256 internal constant FINAL_TOKEN_OFFSET = 192; + uint256 internal constant DESTINATION_DEX_OFFSET = 224; + uint256 internal constant ACCOUNT_CREATION_MODE_OFFSET = 256; + uint256 internal constant EXECUTION_MODE_OFFSET = 288; + // Minimum length with empty actionData: 10 regular params (32 bytes each) and 1 dynamic byte array (minumum 64 bytes) + uint256 internal constant MIN_COMPOSE_MSG_BYTE_LENGTH = 384; function _encode( bytes32 nonce, + uint256 amountSD, uint256 deadline, uint256 maxBpsToSponsor, uint256 maxUserSlippageBps, @@ -31,6 +33,7 @@ library ComposeMsgCodec { return abi.encode( nonce, + amountSD, deadline, maxBpsToSponsor, maxUserSlippageBps, @@ -47,6 +50,10 @@ library ComposeMsgCodec { return BytesLib.toBytes32(data, NONCE_OFFSET); } + function _getAmountSD(bytes memory data) internal pure returns (uint256 v) { + return BytesLib.toUint256(data, AMOUNT_SD_OFFSET); + } + function _getDeadline(bytes memory data) internal pure returns (uint256 v) { return BytesLib.toUint256(data, DEADLINE_OFFSET); } @@ -83,9 +90,9 @@ library ComposeMsgCodec { } function _getActionData(bytes memory data) internal pure returns (bytes memory v) { - (, , , , , , , , , bytes memory actionData) = abi.decode( + (, , , , , , , , , , bytes memory actionData) = abi.decode( data, - (bytes32, uint256, uint256, uint256, bytes32, bytes32, uint32, uint8, uint8, bytes) + (bytes32, uint256, uint256, uint256, uint256, bytes32, bytes32, uint32, uint8, uint8, bytes) ); return actionData; } diff --git a/contracts/periphery/mintburn/sponsored-oft/DstOFTHandler.sol b/contracts/periphery/mintburn/sponsored-oft/DstOFTHandler.sol index c80ec6eeb..cd50033d6 100644 --- a/contracts/periphery/mintburn/sponsored-oft/DstOFTHandler.sol +++ b/contracts/periphery/mintburn/sponsored-oft/DstOFTHandler.sol @@ -11,8 +11,10 @@ import { IOFT, IOAppCore } from "../../../interfaces/IOFT.sol"; import { HyperCoreFlowExecutor } from "../HyperCoreFlowExecutor.sol"; import { ArbitraryEVMFlowExecutor } from "../ArbitraryEVMFlowExecutor.sol"; import { CommonFlowParams, EVMFlowParams, AccountCreationMode } from "../Structs.sol"; +import { OFTCoreMath } from "../../../external/libraries/OFTCoreMath.sol"; import { IERC20 } from "@openzeppelin/contracts-v4/token/ERC20/IERC20.sol"; +import { IERC20Metadata } from "@openzeppelin/contracts-v4/token/ERC20/extensions/IERC20Metadata.sol"; import { SafeERC20 } from "@openzeppelin/contracts-v4/token/ERC20/utils/SafeERC20.sol"; /** @@ -21,16 +23,12 @@ import { SafeERC20 } from "@openzeppelin/contracts-v4/token/ERC20/utils/SafeERC2 * @dev IMPORTANT. `BaseModuleHandler` should always be the first contract in inheritance chain. Read `BaseModuleHandler` contract code to learn more. */ -contract DstOFTHandler is BaseModuleHandler, ILayerZeroComposer, ArbitraryEVMFlowExecutor { +contract DstOFTHandler is BaseModuleHandler, OFTCoreMath, ILayerZeroComposer, ArbitraryEVMFlowExecutor { using ComposeMsgCodec for bytes; using Bytes32ToAddress for bytes32; using AddressToBytes32 for address; using SafeERC20 for IERC20; - /// @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; @@ -86,7 +84,11 @@ contract DstOFTHandler is BaseModuleHandler, ILayerZeroComposer, ArbitraryEVMFlo address _donationBox, address _baseToken, address _multicallHandler - ) BaseModuleHandler(_donationBox, _baseToken, DEFAULT_ADMIN_ROLE) ArbitraryEVMFlowExecutor(_multicallHandler) { + ) + BaseModuleHandler(_donationBox, _baseToken, DEFAULT_ADMIN_ROLE) + ArbitraryEVMFlowExecutor(_multicallHandler) + OFTCoreMath(IERC20Metadata(_baseToken).decimals(), IOFT(_ioft).sharedDecimals()) + { baseToken = _baseToken; if (_baseToken != IOFT(_ioft).token()) { @@ -161,7 +163,19 @@ contract DstOFTHandler is BaseModuleHandler, ILayerZeroComposer, ArbitraryEVMFlo } $.usedNonces[quoteNonce] = true; + // Amount received from `lzReceive` on destination. May be different from the amount the user originally sent (if the OFT takes a fee in the bridged token) uint256 amountLD = OFTComposeMsgCodec.amountLD(_message); + + // We trust src periphery to encode the correct amountSD. It pulls amountLD from the user, using IOFT's sharedDecimals() + // for further conversion to SD. The DEFAULT_ADMIN is responsible for keeping a correct mapping of authorized src peripheries. + uint256 amountSentSD = composeMsg._getAmountSD(); + uint256 amountSentLD = _toLD(uint64(amountSentSD)); + + uint256 extraFeesIncurred = 0; + if (amountSentLD > amountLD) { + extraFeesIncurred = amountSentLD - amountLD; + } + uint256 maxBpsToSponsor = composeMsg._getMaxBpsToSponsor(); uint256 maxUserSlippageBps = composeMsg._getMaxUserSlippageBps(); address finalRecipient = composeMsg._getFinalRecipient().toAddress(); @@ -178,7 +192,7 @@ contract DstOFTHandler is BaseModuleHandler, ILayerZeroComposer, ArbitraryEVMFlo finalToken: finalToken, destinationDex: destinationDex, maxBpsToSponsor: maxBpsToSponsor, - extraFeesIncurred: EXTRA_FEES_TO_SPONSOR, + extraFeesIncurred: extraFeesIncurred, accountCreationMode: AccountCreationMode(accountCreationMode) }); diff --git a/contracts/periphery/mintburn/sponsored-oft/QuoteSignLib.sol b/contracts/periphery/mintburn/sponsored-oft/QuoteSignLib.sol index e1c2dbe02..4592f461d 100644 --- a/contracts/periphery/mintburn/sponsored-oft/QuoteSignLib.sol +++ b/contracts/periphery/mintburn/sponsored-oft/QuoteSignLib.sol @@ -31,6 +31,7 @@ library QuoteSignLib { p.destinationDex, p.lzReceiveGasLimit, p.lzComposeGasLimit, + p.maxOftFeeBps, p.accountCreationMode, p.executionMode, keccak256(p.actionData) // Hash the actionData to keep signature size reasonable diff --git a/contracts/periphery/mintburn/sponsored-oft/SponsoredOFTSrcPeriphery.sol b/contracts/periphery/mintburn/sponsored-oft/SponsoredOFTSrcPeriphery.sol index 111994096..8de2e531a 100644 --- a/contracts/periphery/mintburn/sponsored-oft/SponsoredOFTSrcPeriphery.sol +++ b/contracts/periphery/mintburn/sponsored-oft/SponsoredOFTSrcPeriphery.sol @@ -8,14 +8,16 @@ import { ComposeMsgCodec } from "./ComposeMsgCodec.sol"; import { IOFT, IOAppCore, SendParam, MessagingFee } from "../../../interfaces/IOFT.sol"; import { AddressToBytes32 } from "../../../libraries/AddressConverters.sol"; import { MinimalLZOptions } from "../../../external/libraries/MinimalLZOptions.sol"; +import { OFTCoreMath } from "../../../external/libraries/OFTCoreMath.sol"; import { Ownable } from "@openzeppelin/contracts-v4/access/Ownable.sol"; import { IERC20 } from "@openzeppelin/contracts-v4/token/ERC20/IERC20.sol"; +import { IERC20Metadata } from "@openzeppelin/contracts-v4/token/ERC20/extensions/IERC20Metadata.sol"; import { SafeERC20 } from "@openzeppelin/contracts-v4/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 { +contract SponsoredOFTSrcPeriphery is Ownable, OFTCoreMath { using AddressToBytes32 for address; using MinimalLZOptions for bytes; using SafeERC20 for IERC20; @@ -83,8 +85,17 @@ contract SponsoredOFTSrcPeriphery is Ownable { error NonceAlreadyUsed(); /// @notice Thrown when provided msg.value is not sufficient to cover OFT bridging fee error InsufficientNativeFee(); - - constructor(address _token, address _oftMessenger, uint32 _srcEid, address _signer) { + /// @notice Thrown when array lengths do not match + error ArrayLengthMismatch(); + /// @notice Thrown when maxOftFeeBps is greater than 10000 + error InvalidMaxOftFeeBps(); + + constructor( + address _token, + address _oftMessenger, + uint32 _srcEid, + address _signer + ) OFTCoreMath(IERC20Metadata(_token).decimals(), IOFT(_oftMessenger).sharedDecimals()) { TOKEN = _token; OFT_MESSENGER = _oftMessenger; SRC_EID = _srcEid; @@ -161,8 +172,11 @@ contract SponsoredOFTSrcPeriphery is Ownable { function _buildOftTransfer( Quote calldata quote ) internal view returns (SendParam memory, MessagingFee memory, address) { + uint64 amountSD = _toSD(quote.signedParams.amountLD); + bytes memory composeMsg = ComposeMsgCodec._encode( quote.signedParams.nonce, + uint256(amountSD), quote.signedParams.deadline, quote.signedParams.maxBpsToSponsor, quote.unsignedParams.maxUserSlippageBps, @@ -179,12 +193,18 @@ contract SponsoredOFTSrcPeriphery is Ownable { .addExecutorLzReceiveOption(uint128(quote.signedParams.lzReceiveGasLimit), uint128(0)) .addExecutorLzComposeOption(uint16(0), uint128(quote.signedParams.lzComposeGasLimit), uint128(0)); + // Apply maxOftFeeBps in src-local decimals. + // If maxOftFeeBps == 0, this preserves strict behavior: OFT will revert if dust removal reduces amount below min. + if (quote.signedParams.maxOftFeeBps > 10_000) { + revert InvalidMaxOftFeeBps(); + } + uint256 minAmountLD = (quote.signedParams.amountLD * (10_000 - quote.signedParams.maxOftFeeBps)) / 10_000; + 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, + minAmountLD, extraOptions, composeMsg, // Only support empty OFT commands diff --git a/contracts/periphery/mintburn/sponsored-oft/Structs.sol b/contracts/periphery/mintburn/sponsored-oft/Structs.sol index ee88d7ffc..40bcf6848 100644 --- a/contracts/periphery/mintburn/sponsored-oft/Structs.sol +++ b/contracts/periphery/mintburn/sponsored-oft/Structs.sol @@ -35,6 +35,7 @@ struct SignedQuoteParams { uint256 lzReceiveGasLimit; // gas limit for `lzReceive` call on destination side uint256 lzComposeGasLimit; // gas limit for `lzCompose` call on destination side // Execution mode and action data + uint256 maxOftFeeBps; // max fee deducted by the OFT bridge uint8 accountCreationMode; // AccountCreationMode: Standard or FromUserFunds uint8 executionMode; // ExecutionMode: DirectToCore, ArbitraryActionsToCore, or ArbitraryActionsToEVM bytes actionData; // Encoded action data for arbitrary execution. Empty for DirectToCore mode. diff --git a/contracts/test/MockOFTMessenger.sol b/contracts/test/MockOFTMessenger.sol index 8b732eb82..cc7027c3d 100644 --- a/contracts/test/MockOFTMessenger.sol +++ b/contracts/test/MockOFTMessenger.sol @@ -40,6 +40,10 @@ contract MockOFTMessenger is IOFT, IOAppCore { return endpoint_; } + function sharedDecimals() external pure returns (uint8) { + return 6; + } + // IOFT function quoteSend( SendParam calldata /*_sendParam*/, diff --git a/script/mintburn/oft/CreateSponsoredDeposit.s.sol b/script/mintburn/oft/CreateSponsoredDeposit.s.sol index cd4b337bf..44d09a3a2 100644 --- a/script/mintburn/oft/CreateSponsoredDeposit.s.sol +++ b/script/mintburn/oft/CreateSponsoredDeposit.s.sol @@ -12,6 +12,7 @@ import { MinimalLZOptions } from "../../../contracts/external/libraries/MinimalL import { IOFT, SendParam, MessagingFee, IOAppCore } from "../../../contracts/interfaces/IOFT.sol"; import { HyperCoreLib } from "../../../contracts/libraries/HyperCoreLib.sol"; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { IERC20Metadata } from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.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"; @@ -38,6 +39,7 @@ library DebugQuoteSignLib { p.destinationDex, p.lzReceiveGasLimit, p.lzComposeGasLimit, + p.maxOftFeeBps, p.accountCreationMode, p.executionMode, keccak256(p.actionData) // Hash the actionData to keep signature size reasonable @@ -208,6 +210,7 @@ contract CreateSponsoredDeposit is Script, Config { destinationDex: HyperCoreLib.CORE_SPOT_DEX_ID, lzReceiveGasLimit: lzReceiveGasLimit, lzComposeGasLimit: lzComposeGasLimit, + maxOftFeeBps: 0, accountCreationMode: 0, executionMode: 0, actionData: "" @@ -268,9 +271,20 @@ contract CreateSponsoredDeposit is Script, Config { Quote memory quote ) internal view returns (MessagingFee memory) { address oftMessenger = srcPeripheryContract.OFT_MESSENGER(); + address token = srcPeripheryContract.TOKEN(); + + uint8 localDecimals = IERC20Metadata(token).decimals(); + uint8 sharedDecimals = IOFT(oftMessenger).sharedDecimals(); + + require(localDecimals >= sharedDecimals, "InvalidLocalDecimals"); + uint256 decimalConversionRate = 10 ** (localDecimals - sharedDecimals); + uint256 _amountSD = quote.signedParams.amountLD / decimalConversionRate; + require(_amountSD <= type(uint64).max, "AmountSDOverflowed"); + uint256 amountSD = uint256(uint64(_amountSD)); bytes memory composeMsg = ComposeMsgCodec._encode( quote.signedParams.nonce, + amountSD, quote.signedParams.deadline, quote.signedParams.maxBpsToSponsor, quote.unsignedParams.maxUserSlippageBps, @@ -287,11 +301,14 @@ contract CreateSponsoredDeposit is Script, Config { .addExecutorLzReceiveOption(uint128(quote.signedParams.lzReceiveGasLimit), uint128(0)) .addExecutorLzComposeOption(uint16(0), uint128(quote.signedParams.lzComposeGasLimit), uint128(0)); + require(quote.signedParams.maxOftFeeBps <= 10_000, "maxOftFeeBps > 10000"); + uint256 minAmountLD = (quote.signedParams.amountLD * (10_000 - quote.signedParams.maxOftFeeBps)) / 10_000; + SendParam memory sendParam = SendParam({ dstEid: quote.signedParams.dstEid, to: quote.signedParams.destinationHandler, amountLD: quote.signedParams.amountLD, - minAmountLD: quote.signedParams.amountLD, + minAmountLD: minAmountLD, extraOptions: extraOptions, composeMsg: composeMsg, oftCmd: srcPeripheryContract.EMPTY_OFT_COMMAND() diff --git a/test/evm/foundry/local/ComposeMsgCodec.t.sol b/test/evm/foundry/local/ComposeMsgCodec.t.sol index ede2a869e..893e34d91 100644 --- a/test/evm/foundry/local/ComposeMsgCodec.t.sol +++ b/test/evm/foundry/local/ComposeMsgCodec.t.sol @@ -7,6 +7,7 @@ import { ComposeMsgCodec } from "contracts/periphery/mintburn/sponsored-oft/Comp contract ComposeMsgCodecTest is Test { function test_EncodeDecode() public { bytes32 nonce = keccak256("nonce"); + uint256 amountSD = 100 ether; uint256 deadline = 1234567890; uint256 maxBpsToSponsor = 500; uint256 maxUserSlippageBps = 100; @@ -19,6 +20,7 @@ contract ComposeMsgCodecTest is Test { bytes memory encoded = ComposeMsgCodec._encode( nonce, + amountSD, deadline, maxBpsToSponsor, maxUserSlippageBps, @@ -31,6 +33,7 @@ contract ComposeMsgCodecTest is Test { ); assertEq(ComposeMsgCodec._getNonce(encoded), nonce, "Nonce mismatch"); + assertEq(ComposeMsgCodec._getAmountSD(encoded), amountSD, "AmountSD mismatch"); assertEq(ComposeMsgCodec._getDeadline(encoded), deadline, "Deadline mismatch"); assertEq(ComposeMsgCodec._getMaxBpsToSponsor(encoded), maxBpsToSponsor, "MaxBpsToSponsor mismatch"); assertEq(ComposeMsgCodec._getMaxUserSlippageBps(encoded), maxUserSlippageBps, "MaxUserSlippageBps mismatch"); @@ -45,6 +48,7 @@ contract ComposeMsgCodecTest is Test { function testFuzz_EncodeDecode( bytes32 nonce, + uint256 amountSD, uint256 deadline, uint256 maxBpsToSponsor, uint256 maxUserSlippageBps, @@ -57,6 +61,7 @@ contract ComposeMsgCodecTest is Test { ) public { bytes memory encoded = ComposeMsgCodec._encode( nonce, + amountSD, deadline, maxBpsToSponsor, maxUserSlippageBps, @@ -69,6 +74,7 @@ contract ComposeMsgCodecTest is Test { ); assertEq(ComposeMsgCodec._getNonce(encoded), nonce, "Nonce mismatch"); + assertEq(ComposeMsgCodec._getAmountSD(encoded), amountSD, "AmountSD mismatch"); assertEq(ComposeMsgCodec._getDeadline(encoded), deadline, "Deadline mismatch"); assertEq(ComposeMsgCodec._getMaxBpsToSponsor(encoded), maxBpsToSponsor, "MaxBpsToSponsor mismatch"); assertEq(ComposeMsgCodec._getMaxUserSlippageBps(encoded), maxUserSlippageBps, "MaxUserSlippageBps mismatch"); @@ -82,13 +88,13 @@ contract ComposeMsgCodecTest is Test { } function test_IsValidComposeMsgBytelength_Boundary() public pure { - // Minimum length is 352 bytes (9 static params + actionData offset + actionData length + 0 bytes actionData) - // 9 * 32 + 32 + 32 = 352 bytes + // Minimum length is 384 bytes (10 static params + actionData offset + actionData length + 0 bytes actionData) + // 10 * 32 + 32 + 32 = 384 bytes - bytes memory data = new bytes(352); + bytes memory data = new bytes(384); assertTrue(ComposeMsgCodec._isValidComposeMsgBytelength(data)); - bytes memory tooShort = new bytes(351); + bytes memory tooShort = new bytes(383); assertFalse(ComposeMsgCodec._isValidComposeMsgBytelength(tooShort)); } } diff --git a/test/evm/foundry/local/SponsoredOFTSrcPeriphery.t.sol b/test/evm/foundry/local/SponsoredOFTSrcPeriphery.t.sol index f47600628..6b1f0188a 100644 --- a/test/evm/foundry/local/SponsoredOFTSrcPeriphery.t.sol +++ b/test/evm/foundry/local/SponsoredOFTSrcPeriphery.t.sol @@ -80,6 +80,7 @@ contract SponsoredOFTSrcPeripheryTest is Test { destinationDex: HyperCoreLib.CORE_SPOT_DEX_ID, lzReceiveGasLimit: 500_000, lzComposeGasLimit: 500_000, + maxOftFeeBps: 0, accountCreationMode: uint8(0), // Standard executionMode: uint8(0), // DirectToCore actionData: "" @@ -147,12 +148,13 @@ contract SponsoredOFTSrcPeripheryTest is Test { assertEq(spDstEid, quote.signedParams.dstEid, "dstEid mismatch"); assertEq(spTo, quote.signedParams.destinationHandler, "destination handler mismatch"); assertEq(spAmountLD, SEND_AMOUNT, "amountLD mismatch"); - assertEq(spMinAmountLD, SEND_AMOUNT, "minAmountLD should equal amountLD (no fee-in-token)"); + assertEq(spMinAmountLD, SEND_AMOUNT, "minAmountLD should be SEND_AMOUNT (no dust loss)"); assertEq(spOftCmd.length, 0, "oftCmd must be empty"); // Validate composeMsg encoding (layout from ComposeMsgCodec._encode) ( bytes32 gotNonce, + uint256 gotAmountSD, uint256 gotDeadline, uint256 gotMaxBpsToSponsor, uint256 gotMaxUserSlippageBps, @@ -164,10 +166,11 @@ contract SponsoredOFTSrcPeripheryTest is Test { bytes memory gotActionData ) = abi.decode( spComposeMsg, - (bytes32, uint256, uint256, uint256, bytes32, bytes32, uint32, uint8, uint8, bytes) + (bytes32, uint256, uint256, uint256, uint256, bytes32, bytes32, uint32, uint8, uint8, bytes) ); assertEq(gotNonce, nonce, "nonce mismatch"); + assertEq(gotAmountSD, SEND_AMOUNT / 1e12, "amountSD mismatch"); assertEq(gotDeadline, deadline, "deadline mismatch"); assertEq(gotMaxBpsToSponsor, 500, "maxBpsToSponsor mismatch"); assertEq(gotMaxUserSlippageBps, 300, "maxUserSlippageBps mismatch");