From 310c571f500ab512f975f36242dfea13d86abe12 Mon Sep 17 00:00:00 2001 From: Ihor Farion Date: Thu, 18 Dec 2025 16:20:51 -0800 Subject: [PATCH 1/7] impl Signed-off-by: Ihor Farion --- .../sponsored-oft/ComposeMsgCodec.sol | 31 ++++++++----- .../mintburn/sponsored-oft/DstOFTHandler.sol | 15 ++++--- .../mintburn/sponsored-oft/QuoteSignLib.sol | 1 + .../SponsoredOFTSrcPeriphery.sol | 45 ++++++++++++++++++- .../mintburn/sponsored-oft/Structs.sol | 1 + 5 files changed, 74 insertions(+), 19 deletions(-) diff --git a/contracts/periphery/mintburn/sponsored-oft/ComposeMsgCodec.sol b/contracts/periphery/mintburn/sponsored-oft/ComposeMsgCodec.sol index 1d8e48ef7..8a0ce1115 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_LD_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 amountLD, uint256 deadline, uint256 maxBpsToSponsor, uint256 maxUserSlippageBps, @@ -31,6 +33,7 @@ library ComposeMsgCodec { return abi.encode( nonce, + amountLD, deadline, maxBpsToSponsor, maxUserSlippageBps, @@ -47,6 +50,10 @@ library ComposeMsgCodec { return BytesLib.toBytes32(data, NONCE_OFFSET); } + function _getAmountLD(bytes memory data) internal pure returns (uint256 v) { + return BytesLib.toUint256(data, AMOUNT_LD_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..d104a4361 100644 --- a/contracts/periphery/mintburn/sponsored-oft/DstOFTHandler.sol +++ b/contracts/periphery/mintburn/sponsored-oft/DstOFTHandler.sol @@ -27,10 +27,6 @@ contract DstOFTHandler is BaseModuleHandler, ILayerZeroComposer, ArbitraryEVMFlo 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; @@ -162,6 +158,15 @@ contract DstOFTHandler is BaseModuleHandler, ILayerZeroComposer, ArbitraryEVMFlo $.usedNonces[quoteNonce] = true; uint256 amountLD = OFTComposeMsgCodec.amountLD(_message); + // We trust the src keyword to record the real send amount. + // This is safe because the source contract validates the signature of the quote. + uint256 amountSentLD = composeMsg._getAmountLD(); + + uint256 extraFeesIncurred = 0; + if (amountSentLD > amountLD) { + extraFeesIncurred = amountSentLD - amountLD; + } + uint256 maxBpsToSponsor = composeMsg._getMaxBpsToSponsor(); uint256 maxUserSlippageBps = composeMsg._getMaxUserSlippageBps(); address finalRecipient = composeMsg._getFinalRecipient().toAddress(); @@ -178,7 +183,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..a27790b65 100644 --- a/contracts/periphery/mintburn/sponsored-oft/SponsoredOFTSrcPeriphery.sol +++ b/contracts/periphery/mintburn/sponsored-oft/SponsoredOFTSrcPeriphery.sol @@ -30,6 +30,8 @@ contract SponsoredOFTSrcPeriphery is Ownable { /// @notice Source endpoint id uint32 public immutable SRC_EID; + mapping(uint32 dstEid => int8 decimalDiff) public dstEidToDecimalsDiff; + /// @custom:storage-location erc7201:SponsoredOFTSrcPeriphery.main struct MainStorage { /// @notice Signer public key to check the signed quote against @@ -83,6 +85,8 @@ contract SponsoredOFTSrcPeriphery is Ownable { error NonceAlreadyUsed(); /// @notice Thrown when provided msg.value is not sufficient to cover OFT bridging fee error InsufficientNativeFee(); + /// @notice Thrown when array lengths do not match + error ArrayLengthMismatch(); constructor(address _token, address _oftMessenger, uint32 _srcEid, address _signer) { TOKEN = _token; @@ -161,8 +165,14 @@ contract SponsoredOFTSrcPeriphery is Ownable { function _buildOftTransfer( Quote calldata quote ) internal view returns (SendParam memory, MessagingFee memory, address) { + uint256 amountDstLD = _applyDecimalDiff( + quote.signedParams.amountLD, + dstEidToDecimalsDiff[quote.signedParams.dstEid] + ); + bytes memory composeMsg = ComposeMsgCodec._encode( quote.signedParams.nonce, + amountDstLD, quote.signedParams.deadline, quote.signedParams.maxBpsToSponsor, quote.unsignedParams.maxUserSlippageBps, @@ -179,12 +189,14 @@ contract SponsoredOFTSrcPeriphery is Ownable { .addExecutorLzReceiveOption(uint128(quote.signedParams.lzReceiveGasLimit), uint128(0)) .addExecutorLzComposeOption(uint16(0), uint128(quote.signedParams.lzComposeGasLimit), uint128(0)); + // Use maxOftFeeBps to calculate minAmountLD based on expected destination amount + uint256 minAmountLD = (amountDstLD * (10000 - quote.signedParams.maxOftFeeBps)) / 10000; + 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 @@ -196,6 +208,21 @@ contract SponsoredOFTSrcPeriphery is Ownable { return (sendParam, fee, quote.unsignedParams.refundRecipient); } + /** + * @notice Applies decimal difference to the amount + * @param amount The amount to adjust + * @param diff The decimal difference (positive: multiply, negative: divide) + * @return The adjusted amount + */ + function _applyDecimalDiff(uint256 amount, int8 diff) internal pure returns (uint256) { + if (diff > 0) { + return amount * (10 ** uint8(diff)); + } else if (diff < 0) { + return amount / (10 ** uint8(-diff)); + } + return amount; + } + function _validateQuote(Quote calldata quote, bytes calldata signature) internal view { MainStorage storage $ = _getMainStorage(); if (!QuoteSignLib.isSignatureValid($.signer, quote.signedParams, signature)) { @@ -215,4 +242,18 @@ contract SponsoredOFTSrcPeriphery is Ownable { function setSigner(address _newSigner) external onlyOwner { _getMainStorage().signer = _newSigner; } + + /** + * @notice Sets the decimal difference for destination chains + * @param dstEids Array of destination endpoint IDs + * @param decimalDiffs Array of decimal differences (positive for Src < Dst, negative for Src > Dst) + */ + function setDstEidToDecimalsDiff(uint32[] calldata dstEids, int8[] calldata decimalDiffs) external onlyOwner { + if (dstEids.length != decimalDiffs.length) { + revert ArrayLengthMismatch(); + } + for (uint256 i = 0; i < dstEids.length; ++i) { + dstEidToDecimalsDiff[dstEids[i]] = decimalDiffs[i]; + } + } } 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. From a1e4f657b612fef142077dfe4d3e25c61dd976cf Mon Sep 17 00:00:00 2001 From: Ihor Farion Date: Thu, 18 Dec 2025 17:19:52 -0800 Subject: [PATCH 2/7] fix tests Signed-off-by: Ihor Farion --- script/mintburn/oft/CreateSponsoredDeposit.s.sol | 3 +++ test/evm/foundry/local/ComposeMsgCodec.t.sol | 14 ++++++++++---- .../foundry/local/SponsoredOFTSrcPeriphery.t.sol | 5 ++++- 3 files changed, 17 insertions(+), 5 deletions(-) diff --git a/script/mintburn/oft/CreateSponsoredDeposit.s.sol b/script/mintburn/oft/CreateSponsoredDeposit.s.sol index cd4b337bf..c0855a8c5 100644 --- a/script/mintburn/oft/CreateSponsoredDeposit.s.sol +++ b/script/mintburn/oft/CreateSponsoredDeposit.s.sol @@ -38,6 +38,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 +209,7 @@ contract CreateSponsoredDeposit is Script, Config { destinationDex: HyperCoreLib.CORE_SPOT_DEX_ID, lzReceiveGasLimit: lzReceiveGasLimit, lzComposeGasLimit: lzComposeGasLimit, + maxOftFeeBps: 0, accountCreationMode: 0, executionMode: 0, actionData: "" @@ -271,6 +273,7 @@ contract CreateSponsoredDeposit is Script, Config { bytes memory composeMsg = ComposeMsgCodec._encode( quote.signedParams.nonce, + quote.signedParams.amountLD, quote.signedParams.deadline, quote.signedParams.maxBpsToSponsor, quote.unsignedParams.maxUserSlippageBps, diff --git a/test/evm/foundry/local/ComposeMsgCodec.t.sol b/test/evm/foundry/local/ComposeMsgCodec.t.sol index ede2a869e..c78daae5e 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 amountLD = 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, + amountLD, deadline, maxBpsToSponsor, maxUserSlippageBps, @@ -31,6 +33,7 @@ contract ComposeMsgCodecTest is Test { ); assertEq(ComposeMsgCodec._getNonce(encoded), nonce, "Nonce mismatch"); + assertEq(ComposeMsgCodec._getAmountLD(encoded), amountLD, "AmountLD 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 amountLD, uint256 deadline, uint256 maxBpsToSponsor, uint256 maxUserSlippageBps, @@ -57,6 +61,7 @@ contract ComposeMsgCodecTest is Test { ) public { bytes memory encoded = ComposeMsgCodec._encode( nonce, + amountLD, deadline, maxBpsToSponsor, maxUserSlippageBps, @@ -69,6 +74,7 @@ contract ComposeMsgCodecTest is Test { ); assertEq(ComposeMsgCodec._getNonce(encoded), nonce, "Nonce mismatch"); + assertEq(ComposeMsgCodec._getAmountLD(encoded), amountLD, "AmountLD 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..785fc8c76 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: "" @@ -153,6 +154,7 @@ contract SponsoredOFTSrcPeripheryTest is Test { // Validate composeMsg encoding (layout from ComposeMsgCodec._encode) ( bytes32 gotNonce, + uint256 gotAmountLD, 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(gotAmountLD, SEND_AMOUNT, "amountLD mismatch"); assertEq(gotDeadline, deadline, "deadline mismatch"); assertEq(gotMaxBpsToSponsor, 500, "maxBpsToSponsor mismatch"); assertEq(gotMaxUserSlippageBps, 300, "maxUserSlippageBps mismatch"); From 820b0f3835e68bedeafae60b0e01a32a5150650e Mon Sep 17 00:00:00 2001 From: Ihor Farion Date: Thu, 18 Dec 2025 17:31:02 -0800 Subject: [PATCH 3/7] comments Signed-off-by: Ihor Farion --- .../periphery/mintburn/sponsored-oft/DstOFTHandler.sol | 6 ++++-- .../mintburn/sponsored-oft/SponsoredOFTSrcPeriphery.sol | 1 + 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/contracts/periphery/mintburn/sponsored-oft/DstOFTHandler.sol b/contracts/periphery/mintburn/sponsored-oft/DstOFTHandler.sol index d104a4361..5de54166b 100644 --- a/contracts/periphery/mintburn/sponsored-oft/DstOFTHandler.sol +++ b/contracts/periphery/mintburn/sponsored-oft/DstOFTHandler.sol @@ -157,9 +157,11 @@ 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 the src keyword to record the real send amount. - // This is safe because the source contract validates the signature of the quote. + + // We trust src periphery to encode the correct amount. This is safe because src periphery pulls the amount it + // encodes here from the user. Decimal conversion is done on src side as well, so no coversions are needed here uint256 amountSentLD = composeMsg._getAmountLD(); uint256 extraFeesIncurred = 0; diff --git a/contracts/periphery/mintburn/sponsored-oft/SponsoredOFTSrcPeriphery.sol b/contracts/periphery/mintburn/sponsored-oft/SponsoredOFTSrcPeriphery.sol index a27790b65..513cd5652 100644 --- a/contracts/periphery/mintburn/sponsored-oft/SponsoredOFTSrcPeriphery.sol +++ b/contracts/periphery/mintburn/sponsored-oft/SponsoredOFTSrcPeriphery.sol @@ -30,6 +30,7 @@ contract SponsoredOFTSrcPeriphery is Ownable { /// @notice Source endpoint id uint32 public immutable SRC_EID; + /// @notice Trusted mapping of dst decimal difference with src decimals for TOKEN mapping(uint32 dstEid => int8 decimalDiff) public dstEidToDecimalsDiff; /// @custom:storage-location erc7201:SponsoredOFTSrcPeriphery.main From 5c9cccb88a929bb5509975c4f44d2bc5ad48bd0f Mon Sep 17 00:00:00 2001 From: Ihor Farion Date: Thu, 18 Dec 2025 17:54:42 -0800 Subject: [PATCH 4/7] interim better Signed-off-by: Ihor Farion --- .../external/libraries/SharedDecimalsLib.sol | 70 +++++++++++++++++++ contracts/interfaces/IOFT.sol | 6 ++ .../sponsored-oft/ComposeMsgCodec.sol | 10 +-- .../mintburn/sponsored-oft/DstOFTHandler.sol | 13 +++- .../SponsoredOFTSrcPeriphery.sol | 48 +++---------- contracts/test/MockOFTMessenger.sol | 4 ++ .../mintburn/oft/CreateSponsoredDeposit.s.sol | 14 +++- test/evm/foundry/local/ComposeMsgCodec.t.sol | 12 ++-- .../local/SponsoredOFTSrcPeriphery.t.sol | 6 +- 9 files changed, 125 insertions(+), 58 deletions(-) create mode 100644 contracts/external/libraries/SharedDecimalsLib.sol diff --git a/contracts/external/libraries/SharedDecimalsLib.sol b/contracts/external/libraries/SharedDecimalsLib.sol new file mode 100644 index 000000000..08aac20a0 --- /dev/null +++ b/contracts/external/libraries/SharedDecimalsLib.sol @@ -0,0 +1,70 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +/** + * @title SharedDecimalsLib + * @notice Library for handling shared decimals conversions for OFT bridging. + * @dev Logic adapted from LayerZero's OFTCore.sol + */ +library SharedDecimalsLib { + error InvalidLocalDecimals(); + error AmountSDOverflowed(uint256 amountSD); + + /** + * @notice Convert an amount from local decimals into shared decimals. + * @dev Internal function to convert an amount from local decimals into shared decimals. + * @param _amountLD The amount in local decimals. + * @param _localDecimals The decimals of the token on the local chain. + * @param _sharedDecimals The shared decimals of the OFT. + * @return amountSD The amount in shared decimals. + * + * @dev Reverts if the _amountLD in shared decimals overflows uint64. + */ + function toSD( + uint256 _amountLD, + uint8 _localDecimals, + uint8 _sharedDecimals + ) internal pure returns (uint64 amountSD) { + if (_localDecimals < _sharedDecimals) revert InvalidLocalDecimals(); + uint256 conversionRate = 10 ** (_localDecimals - _sharedDecimals); + uint256 _amountSD = _amountLD / conversionRate; + if (_amountSD > type(uint64).max) revert AmountSDOverflowed(_amountSD); + return uint64(_amountSD); + } + + /** + * @notice Convert an amount from shared decimals into local decimals. + * @dev Internal function to convert an amount from shared decimals into local decimals. + * @param _amountSD The amount in shared decimals. + * @param _localDecimals The decimals of the token on the local chain. + * @param _sharedDecimals The shared decimals of the OFT. + * @return amountLD The amount in local decimals. + */ + function toLD( + uint64 _amountSD, + uint8 _localDecimals, + uint8 _sharedDecimals + ) internal pure returns (uint256 amountLD) { + if (_localDecimals < _sharedDecimals) revert InvalidLocalDecimals(); + uint256 conversionRate = 10 ** (_localDecimals - _sharedDecimals); + return uint256(_amountSD) * conversionRate; + } + + /** + * @notice Remove dust from the given local decimal amount. + * @dev Internal function to remove dust from the given local decimal amount. + * @param _amountLD The amount in local decimals. + * @param _localDecimals The decimals of the token on the local chain. + * @param _sharedDecimals The shared decimals of the OFT. + * @return amountLD The amount after removing dust. + */ + function removeDust( + uint256 _amountLD, + uint8 _localDecimals, + uint8 _sharedDecimals + ) internal pure returns (uint256 amountLD) { + if (_localDecimals < _sharedDecimals) revert InvalidLocalDecimals(); + uint256 conversionRate = 10 ** (_localDecimals - _sharedDecimals); + return (_amountLD / conversionRate) * conversionRate; + } +} 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 8a0ce1115..8125884df 100644 --- a/contracts/periphery/mintburn/sponsored-oft/ComposeMsgCodec.sol +++ b/contracts/periphery/mintburn/sponsored-oft/ComposeMsgCodec.sol @@ -5,7 +5,7 @@ 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 AMOUNT_LD_OFFSET = 32; + 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; @@ -19,7 +19,7 @@ library ComposeMsgCodec { function _encode( bytes32 nonce, - uint256 amountLD, + uint256 amountSD, uint256 deadline, uint256 maxBpsToSponsor, uint256 maxUserSlippageBps, @@ -33,7 +33,7 @@ library ComposeMsgCodec { return abi.encode( nonce, - amountLD, + amountSD, deadline, maxBpsToSponsor, maxUserSlippageBps, @@ -50,8 +50,8 @@ library ComposeMsgCodec { return BytesLib.toBytes32(data, NONCE_OFFSET); } - function _getAmountLD(bytes memory data) internal pure returns (uint256 v) { - return BytesLib.toUint256(data, AMOUNT_LD_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) { diff --git a/contracts/periphery/mintburn/sponsored-oft/DstOFTHandler.sol b/contracts/periphery/mintburn/sponsored-oft/DstOFTHandler.sol index 5de54166b..933144da2 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 { SharedDecimalsLib } from "../../../external/libraries/SharedDecimalsLib.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"; /** @@ -160,9 +162,14 @@ contract DstOFTHandler is BaseModuleHandler, ILayerZeroComposer, ArbitraryEVMFlo // 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 amount. This is safe because src periphery pulls the amount it - // encodes here from the user. Decimal conversion is done on src side as well, so no coversions are needed here - uint256 amountSentLD = composeMsg._getAmountLD(); + // We trust src periphery to encode the correct amount. + // We decode amountSD and convert it to local decimals to compare against amountLD + uint256 amountSentSD = composeMsg._getAmountSD(); + uint256 amountSentLD = SharedDecimalsLib.toLD( + uint64(amountSentSD), + IERC20Metadata(baseToken).decimals(), + IOFT(IOFT_ADDRESS).sharedDecimals() + ); uint256 extraFeesIncurred = 0; if (amountSentLD > amountLD) { diff --git a/contracts/periphery/mintburn/sponsored-oft/SponsoredOFTSrcPeriphery.sol b/contracts/periphery/mintburn/sponsored-oft/SponsoredOFTSrcPeriphery.sol index 513cd5652..565d64cf9 100644 --- a/contracts/periphery/mintburn/sponsored-oft/SponsoredOFTSrcPeriphery.sol +++ b/contracts/periphery/mintburn/sponsored-oft/SponsoredOFTSrcPeriphery.sol @@ -8,9 +8,11 @@ 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 { SharedDecimalsLib } from "../../../external/libraries/SharedDecimalsLib.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 @@ -30,9 +32,6 @@ contract SponsoredOFTSrcPeriphery is Ownable { /// @notice Source endpoint id uint32 public immutable SRC_EID; - /// @notice Trusted mapping of dst decimal difference with src decimals for TOKEN - mapping(uint32 dstEid => int8 decimalDiff) public dstEidToDecimalsDiff; - /// @custom:storage-location erc7201:SponsoredOFTSrcPeriphery.main struct MainStorage { /// @notice Signer public key to check the signed quote against @@ -166,14 +165,14 @@ contract SponsoredOFTSrcPeriphery is Ownable { function _buildOftTransfer( Quote calldata quote ) internal view returns (SendParam memory, MessagingFee memory, address) { - uint256 amountDstLD = _applyDecimalDiff( - quote.signedParams.amountLD, - dstEidToDecimalsDiff[quote.signedParams.dstEid] - ); + uint8 localDecimals = IERC20Metadata(TOKEN).decimals(); + uint8 sharedDecimals = IOFT(OFT_MESSENGER).sharedDecimals(); + + uint256 amountSD = SharedDecimalsLib.toSD(quote.signedParams.amountLD, localDecimals, sharedDecimals); bytes memory composeMsg = ComposeMsgCodec._encode( quote.signedParams.nonce, - amountDstLD, + amountSD, quote.signedParams.deadline, quote.signedParams.maxBpsToSponsor, quote.unsignedParams.maxUserSlippageBps, @@ -190,8 +189,8 @@ contract SponsoredOFTSrcPeriphery is Ownable { .addExecutorLzReceiveOption(uint128(quote.signedParams.lzReceiveGasLimit), uint128(0)) .addExecutorLzComposeOption(uint16(0), uint128(quote.signedParams.lzComposeGasLimit), uint128(0)); - // Use maxOftFeeBps to calculate minAmountLD based on expected destination amount - uint256 minAmountLD = (amountDstLD * (10000 - quote.signedParams.maxOftFeeBps)) / 10000; + // Use removeDust to calculate minAmountLD in local decimals (on src) + uint256 minAmountLD = SharedDecimalsLib.removeDust(quote.signedParams.amountLD, localDecimals, sharedDecimals); SendParam memory sendParam = SendParam( quote.signedParams.dstEid, @@ -209,21 +208,6 @@ contract SponsoredOFTSrcPeriphery is Ownable { return (sendParam, fee, quote.unsignedParams.refundRecipient); } - /** - * @notice Applies decimal difference to the amount - * @param amount The amount to adjust - * @param diff The decimal difference (positive: multiply, negative: divide) - * @return The adjusted amount - */ - function _applyDecimalDiff(uint256 amount, int8 diff) internal pure returns (uint256) { - if (diff > 0) { - return amount * (10 ** uint8(diff)); - } else if (diff < 0) { - return amount / (10 ** uint8(-diff)); - } - return amount; - } - function _validateQuote(Quote calldata quote, bytes calldata signature) internal view { MainStorage storage $ = _getMainStorage(); if (!QuoteSignLib.isSignatureValid($.signer, quote.signedParams, signature)) { @@ -243,18 +227,4 @@ contract SponsoredOFTSrcPeriphery is Ownable { function setSigner(address _newSigner) external onlyOwner { _getMainStorage().signer = _newSigner; } - - /** - * @notice Sets the decimal difference for destination chains - * @param dstEids Array of destination endpoint IDs - * @param decimalDiffs Array of decimal differences (positive for Src < Dst, negative for Src > Dst) - */ - function setDstEidToDecimalsDiff(uint32[] calldata dstEids, int8[] calldata decimalDiffs) external onlyOwner { - if (dstEids.length != decimalDiffs.length) { - revert ArrayLengthMismatch(); - } - for (uint256 i = 0; i < dstEids.length; ++i) { - dstEidToDecimalsDiff[dstEids[i]] = decimalDiffs[i]; - } - } } 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 c0855a8c5..ad9b7bd4d 100644 --- a/script/mintburn/oft/CreateSponsoredDeposit.s.sol +++ b/script/mintburn/oft/CreateSponsoredDeposit.s.sol @@ -9,9 +9,11 @@ import { Quote, SignedQuoteParams, UnsignedQuoteParams } from "../../../contract import { AddressToBytes32 } from "../../../contracts/libraries/AddressConverters.sol"; import { ComposeMsgCodec } from "../../../contracts/periphery/mintburn/sponsored-oft/ComposeMsgCodec.sol"; import { MinimalLZOptions } from "../../../contracts/external/libraries/MinimalLZOptions.sol"; +import { SharedDecimalsLib } from "../../../contracts/external/libraries/SharedDecimalsLib.sol"; 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"; @@ -270,10 +272,16 @@ 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(); + + uint256 amountSD = SharedDecimalsLib.toSD(quote.signedParams.amountLD, localDecimals, sharedDecimals); bytes memory composeMsg = ComposeMsgCodec._encode( quote.signedParams.nonce, - quote.signedParams.amountLD, + amountSD, quote.signedParams.deadline, quote.signedParams.maxBpsToSponsor, quote.unsignedParams.maxUserSlippageBps, @@ -290,11 +298,13 @@ contract CreateSponsoredDeposit is Script, Config { .addExecutorLzReceiveOption(uint128(quote.signedParams.lzReceiveGasLimit), uint128(0)) .addExecutorLzComposeOption(uint16(0), uint128(quote.signedParams.lzComposeGasLimit), uint128(0)); + uint256 minAmountLD = SharedDecimalsLib.removeDust(quote.signedParams.amountLD, localDecimals, sharedDecimals); + 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 c78daae5e..893e34d91 100644 --- a/test/evm/foundry/local/ComposeMsgCodec.t.sol +++ b/test/evm/foundry/local/ComposeMsgCodec.t.sol @@ -7,7 +7,7 @@ import { ComposeMsgCodec } from "contracts/periphery/mintburn/sponsored-oft/Comp contract ComposeMsgCodecTest is Test { function test_EncodeDecode() public { bytes32 nonce = keccak256("nonce"); - uint256 amountLD = 100 ether; + uint256 amountSD = 100 ether; uint256 deadline = 1234567890; uint256 maxBpsToSponsor = 500; uint256 maxUserSlippageBps = 100; @@ -20,7 +20,7 @@ contract ComposeMsgCodecTest is Test { bytes memory encoded = ComposeMsgCodec._encode( nonce, - amountLD, + amountSD, deadline, maxBpsToSponsor, maxUserSlippageBps, @@ -33,7 +33,7 @@ contract ComposeMsgCodecTest is Test { ); assertEq(ComposeMsgCodec._getNonce(encoded), nonce, "Nonce mismatch"); - assertEq(ComposeMsgCodec._getAmountLD(encoded), amountLD, "AmountLD 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"); @@ -48,7 +48,7 @@ contract ComposeMsgCodecTest is Test { function testFuzz_EncodeDecode( bytes32 nonce, - uint256 amountLD, + uint256 amountSD, uint256 deadline, uint256 maxBpsToSponsor, uint256 maxUserSlippageBps, @@ -61,7 +61,7 @@ contract ComposeMsgCodecTest is Test { ) public { bytes memory encoded = ComposeMsgCodec._encode( nonce, - amountLD, + amountSD, deadline, maxBpsToSponsor, maxUserSlippageBps, @@ -74,7 +74,7 @@ contract ComposeMsgCodecTest is Test { ); assertEq(ComposeMsgCodec._getNonce(encoded), nonce, "Nonce mismatch"); - assertEq(ComposeMsgCodec._getAmountLD(encoded), amountLD, "AmountLD 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"); diff --git a/test/evm/foundry/local/SponsoredOFTSrcPeriphery.t.sol b/test/evm/foundry/local/SponsoredOFTSrcPeriphery.t.sol index 785fc8c76..6b1f0188a 100644 --- a/test/evm/foundry/local/SponsoredOFTSrcPeriphery.t.sol +++ b/test/evm/foundry/local/SponsoredOFTSrcPeriphery.t.sol @@ -148,13 +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 gotAmountLD, + uint256 gotAmountSD, uint256 gotDeadline, uint256 gotMaxBpsToSponsor, uint256 gotMaxUserSlippageBps, @@ -170,7 +170,7 @@ contract SponsoredOFTSrcPeripheryTest is Test { ); assertEq(gotNonce, nonce, "nonce mismatch"); - assertEq(gotAmountLD, SEND_AMOUNT, "amountLD mismatch"); + assertEq(gotAmountSD, SEND_AMOUNT / 1e12, "amountSD mismatch"); assertEq(gotDeadline, deadline, "deadline mismatch"); assertEq(gotMaxBpsToSponsor, 500, "maxBpsToSponsor mismatch"); assertEq(gotMaxUserSlippageBps, 300, "maxUserSlippageBps mismatch"); From 06e12cd3cc5bacbde0f78ff1114a830516087bbb Mon Sep 17 00:00:00 2001 From: Ihor Farion Date: Thu, 18 Dec 2025 18:33:11 -0800 Subject: [PATCH 5/7] almost there Signed-off-by: Ihor Farion --- .../external/libraries/SharedDecimalsLib.sol | 106 +++++++++++------- .../mintburn/sponsored-oft/DstOFTHandler.sol | 18 +-- .../SponsoredOFTSrcPeriphery.sol | 28 +++-- .../mintburn/oft/CreateSponsoredDeposit.s.sol | 10 +- 4 files changed, 97 insertions(+), 65 deletions(-) diff --git a/contracts/external/libraries/SharedDecimalsLib.sol b/contracts/external/libraries/SharedDecimalsLib.sol index 08aac20a0..bc2af9f8a 100644 --- a/contracts/external/libraries/SharedDecimalsLib.sol +++ b/contracts/external/libraries/SharedDecimalsLib.sol @@ -3,68 +3,88 @@ pragma solidity ^0.8.0; /** * @title SharedDecimalsLib - * @notice Library for handling shared decimals conversions for OFT bridging. - * @dev Logic adapted from LayerZero's OFTCore.sol + * @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 removing unrelated OFT/OApp concerns. */ -library SharedDecimalsLib { +abstract contract SharedDecimalsLib { 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; + /** - * @notice Convert an amount from local decimals into shared decimals. - * @dev Internal function to convert an amount from local decimals into shared decimals. + * @dev Constructor. + * @param _localDecimals The decimals of the token on the local chain (this chain). + * @param _endpoint Unused (kept for parity with OFTCore signature). + * @param _delegate Unused (kept for parity with OFTCore signature). + */ + constructor(uint8 _localDecimals, address _endpoint, address _delegate) { + _endpoint; + _delegate; + if (_localDecimals < sharedDecimals()) revert InvalidLocalDecimals(); + decimalConversionRate = 10 ** (_localDecimals - sharedDecimals()); + } + + /** + * @dev Retrieves the shared decimals of the OFT. + * @return The shared decimals of the OFT. + * + * @dev Sets an implicit cap on the amount of tokens, over uint64.max() will need some sort of outbound cap / totalSupply cap + * Lowest common decimal denominator between chains. + * Defaults to 6 decimal places to provide up to 18,446,744,073,709.551615 units (max uint64). + * For tokens exceeding this totalSupply(), they will need to override the sharedDecimals function with something smaller. + * ie. 4 sharedDecimals would be 1,844,674,407,370,955.1615 + */ + function sharedDecimals() public view virtual returns (uint8) { + return 6; + } + + /** + * @dev Internal function to remove dust from the given local decimal amount. * @param _amountLD The amount in local decimals. - * @param _localDecimals The decimals of the token on the local chain. - * @param _sharedDecimals The shared decimals of the OFT. - * @return amountSD The amount in shared decimals. + * @return amountLD The amount after removing dust. * - * @dev Reverts if the _amountLD in shared decimals overflows uint64. + * @dev Prevents the loss of dust when moving amounts between chains with different decimals. + * @dev eg. uint(123) with a conversion rate of 100 becomes uint(100). */ - function toSD( - uint256 _amountLD, - uint8 _localDecimals, - uint8 _sharedDecimals - ) internal pure returns (uint64 amountSD) { - if (_localDecimals < _sharedDecimals) revert InvalidLocalDecimals(); - uint256 conversionRate = 10 ** (_localDecimals - _sharedDecimals); - uint256 _amountSD = _amountLD / conversionRate; - if (_amountSD > type(uint64).max) revert AmountSDOverflowed(_amountSD); - return uint64(_amountSD); + function _removeDust(uint256 _amountLD) internal view virtual returns (uint256 amountLD) { + return (_amountLD / decimalConversionRate) * decimalConversionRate; } /** - * @notice Convert an amount from shared decimals into local decimals. * @dev Internal function to convert an amount from shared decimals into local decimals. * @param _amountSD The amount in shared decimals. - * @param _localDecimals The decimals of the token on the local chain. - * @param _sharedDecimals The shared decimals of the OFT. * @return amountLD The amount in local decimals. */ - function toLD( - uint64 _amountSD, - uint8 _localDecimals, - uint8 _sharedDecimals - ) internal pure returns (uint256 amountLD) { - if (_localDecimals < _sharedDecimals) revert InvalidLocalDecimals(); - uint256 conversionRate = 10 ** (_localDecimals - _sharedDecimals); - return uint256(_amountSD) * conversionRate; + function _toLD(uint64 _amountSD) internal view virtual returns (uint256 amountLD) { + return _amountSD * decimalConversionRate; } /** - * @notice Remove dust from the given local decimal amount. - * @dev Internal function to remove dust from the given local decimal amount. + * @dev Internal function to convert an amount from local decimals into shared decimals. * @param _amountLD The amount in local decimals. - * @param _localDecimals The decimals of the token on the local chain. - * @param _sharedDecimals The shared decimals of the OFT. - * @return amountLD The amount after removing dust. + * @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 removeDust( - uint256 _amountLD, - uint8 _localDecimals, - uint8 _sharedDecimals - ) internal pure returns (uint256 amountLD) { - if (_localDecimals < _sharedDecimals) revert InvalidLocalDecimals(); - uint256 conversionRate = 10 ** (_localDecimals - _sharedDecimals); - return (_amountLD / conversionRate) * conversionRate; + 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/periphery/mintburn/sponsored-oft/DstOFTHandler.sol b/contracts/periphery/mintburn/sponsored-oft/DstOFTHandler.sol index 933144da2..0235b7782 100644 --- a/contracts/periphery/mintburn/sponsored-oft/DstOFTHandler.sol +++ b/contracts/periphery/mintburn/sponsored-oft/DstOFTHandler.sol @@ -23,7 +23,7 @@ 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, SharedDecimalsLib, ILayerZeroComposer, ArbitraryEVMFlowExecutor { using ComposeMsgCodec for bytes; using Bytes32ToAddress for bytes32; using AddressToBytes32 for address; @@ -84,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) + SharedDecimalsLib(IERC20Metadata(_baseToken).decimals(), address(0), address(0)) + { baseToken = _baseToken; if (_baseToken != IOFT(_ioft).token()) { @@ -162,14 +166,10 @@ contract DstOFTHandler is BaseModuleHandler, ILayerZeroComposer, ArbitraryEVMFlo // 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 amount. - // We decode amountSD and convert it to local decimals to compare against amountLD + // 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 = SharedDecimalsLib.toLD( - uint64(amountSentSD), - IERC20Metadata(baseToken).decimals(), - IOFT(IOFT_ADDRESS).sharedDecimals() - ); + uint256 amountSentLD = _toLD(uint64(amountSentSD)); uint256 extraFeesIncurred = 0; if (amountSentLD > amountLD) { diff --git a/contracts/periphery/mintburn/sponsored-oft/SponsoredOFTSrcPeriphery.sol b/contracts/periphery/mintburn/sponsored-oft/SponsoredOFTSrcPeriphery.sol index 565d64cf9..2e43f80c6 100644 --- a/contracts/periphery/mintburn/sponsored-oft/SponsoredOFTSrcPeriphery.sol +++ b/contracts/periphery/mintburn/sponsored-oft/SponsoredOFTSrcPeriphery.sol @@ -17,7 +17,7 @@ import { SafeERC20 } from "@openzeppelin/contracts-v4/token/ERC20/utils/SafeERC2 /// @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, SharedDecimalsLib { using AddressToBytes32 for address; using MinimalLZOptions for bytes; using SafeERC20 for IERC20; @@ -87,8 +87,15 @@ contract SponsoredOFTSrcPeriphery is Ownable { error InsufficientNativeFee(); /// @notice Thrown when array lengths do not match error ArrayLengthMismatch(); - - constructor(address _token, address _oftMessenger, uint32 _srcEid, address _signer) { + /// @notice Thrown when maxOftFeeBps is greater than 10000 + error InvalidMaxOftFeeBps(); + + constructor( + address _token, + address _oftMessenger, + uint32 _srcEid, + address _signer + ) SharedDecimalsLib(IERC20Metadata(_token).decimals(), address(0), address(0)) { TOKEN = _token; OFT_MESSENGER = _oftMessenger; SRC_EID = _srcEid; @@ -165,14 +172,11 @@ contract SponsoredOFTSrcPeriphery is Ownable { function _buildOftTransfer( Quote calldata quote ) internal view returns (SendParam memory, MessagingFee memory, address) { - uint8 localDecimals = IERC20Metadata(TOKEN).decimals(); - uint8 sharedDecimals = IOFT(OFT_MESSENGER).sharedDecimals(); - - uint256 amountSD = SharedDecimalsLib.toSD(quote.signedParams.amountLD, localDecimals, sharedDecimals); + uint64 amountSD = _toSD(quote.signedParams.amountLD); bytes memory composeMsg = ComposeMsgCodec._encode( quote.signedParams.nonce, - amountSD, + uint256(amountSD), quote.signedParams.deadline, quote.signedParams.maxBpsToSponsor, quote.unsignedParams.maxUserSlippageBps, @@ -189,8 +193,12 @@ contract SponsoredOFTSrcPeriphery is Ownable { .addExecutorLzReceiveOption(uint128(quote.signedParams.lzReceiveGasLimit), uint128(0)) .addExecutorLzComposeOption(uint16(0), uint128(quote.signedParams.lzComposeGasLimit), uint128(0)); - // Use removeDust to calculate minAmountLD in local decimals (on src) - uint256 minAmountLD = SharedDecimalsLib.removeDust(quote.signedParams.amountLD, localDecimals, sharedDecimals); + // 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, diff --git a/script/mintburn/oft/CreateSponsoredDeposit.s.sol b/script/mintburn/oft/CreateSponsoredDeposit.s.sol index ad9b7bd4d..44d09a3a2 100644 --- a/script/mintburn/oft/CreateSponsoredDeposit.s.sol +++ b/script/mintburn/oft/CreateSponsoredDeposit.s.sol @@ -9,7 +9,6 @@ import { Quote, SignedQuoteParams, UnsignedQuoteParams } from "../../../contract import { AddressToBytes32 } from "../../../contracts/libraries/AddressConverters.sol"; import { ComposeMsgCodec } from "../../../contracts/periphery/mintburn/sponsored-oft/ComposeMsgCodec.sol"; import { MinimalLZOptions } from "../../../contracts/external/libraries/MinimalLZOptions.sol"; -import { SharedDecimalsLib } from "../../../contracts/external/libraries/SharedDecimalsLib.sol"; 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"; @@ -277,7 +276,11 @@ contract CreateSponsoredDeposit is Script, Config { uint8 localDecimals = IERC20Metadata(token).decimals(); uint8 sharedDecimals = IOFT(oftMessenger).sharedDecimals(); - uint256 amountSD = SharedDecimalsLib.toSD(quote.signedParams.amountLD, localDecimals, 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, @@ -298,7 +301,8 @@ contract CreateSponsoredDeposit is Script, Config { .addExecutorLzReceiveOption(uint128(quote.signedParams.lzReceiveGasLimit), uint128(0)) .addExecutorLzComposeOption(uint16(0), uint128(quote.signedParams.lzComposeGasLimit), uint128(0)); - uint256 minAmountLD = SharedDecimalsLib.removeDust(quote.signedParams.amountLD, localDecimals, sharedDecimals); + 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, From 8cc0c5adf69c622a444a1c0a017f4e3033246762 Mon Sep 17 00:00:00 2001 From: Ihor Farion Date: Thu, 18 Dec 2025 18:38:52 -0800 Subject: [PATCH 6/7] better Signed-off-by: Ihor Farion --- .../external/libraries/SharedDecimalsLib.sol | 15 +++++++-------- .../mintburn/sponsored-oft/DstOFTHandler.sol | 2 +- .../sponsored-oft/SponsoredOFTSrcPeriphery.sol | 2 +- 3 files changed, 9 insertions(+), 10 deletions(-) diff --git a/contracts/external/libraries/SharedDecimalsLib.sol b/contracts/external/libraries/SharedDecimalsLib.sol index bc2af9f8a..0feea643b 100644 --- a/contracts/external/libraries/SharedDecimalsLib.sol +++ b/contracts/external/libraries/SharedDecimalsLib.sol @@ -25,18 +25,17 @@ abstract contract SharedDecimalsLib { // @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; + uint8 internal immutable _sharedDecimals; /** * @dev Constructor. * @param _localDecimals The decimals of the token on the local chain (this chain). - * @param _endpoint Unused (kept for parity with OFTCore signature). - * @param _delegate Unused (kept for parity with OFTCore signature). + * @param _sharedDecimalsArg The shared decimals used by the OFT. */ - constructor(uint8 _localDecimals, address _endpoint, address _delegate) { - _endpoint; - _delegate; - if (_localDecimals < sharedDecimals()) revert InvalidLocalDecimals(); - decimalConversionRate = 10 ** (_localDecimals - sharedDecimals()); + constructor(uint8 _localDecimals, uint8 _sharedDecimalsArg) { + _sharedDecimals = _sharedDecimalsArg; + if (_localDecimals < _sharedDecimalsArg) revert InvalidLocalDecimals(); + decimalConversionRate = 10 ** (_localDecimals - _sharedDecimalsArg); } /** @@ -50,7 +49,7 @@ abstract contract SharedDecimalsLib { * ie. 4 sharedDecimals would be 1,844,674,407,370,955.1615 */ function sharedDecimals() public view virtual returns (uint8) { - return 6; + return _sharedDecimals; } /** diff --git a/contracts/periphery/mintburn/sponsored-oft/DstOFTHandler.sol b/contracts/periphery/mintburn/sponsored-oft/DstOFTHandler.sol index 0235b7782..898529cb4 100644 --- a/contracts/periphery/mintburn/sponsored-oft/DstOFTHandler.sol +++ b/contracts/periphery/mintburn/sponsored-oft/DstOFTHandler.sol @@ -87,7 +87,7 @@ contract DstOFTHandler is BaseModuleHandler, SharedDecimalsLib, ILayerZeroCompos ) BaseModuleHandler(_donationBox, _baseToken, DEFAULT_ADMIN_ROLE) ArbitraryEVMFlowExecutor(_multicallHandler) - SharedDecimalsLib(IERC20Metadata(_baseToken).decimals(), address(0), address(0)) + SharedDecimalsLib(IERC20Metadata(_baseToken).decimals(), IOFT(_ioft).sharedDecimals()) { baseToken = _baseToken; diff --git a/contracts/periphery/mintburn/sponsored-oft/SponsoredOFTSrcPeriphery.sol b/contracts/periphery/mintburn/sponsored-oft/SponsoredOFTSrcPeriphery.sol index 2e43f80c6..5a539badc 100644 --- a/contracts/periphery/mintburn/sponsored-oft/SponsoredOFTSrcPeriphery.sol +++ b/contracts/periphery/mintburn/sponsored-oft/SponsoredOFTSrcPeriphery.sol @@ -95,7 +95,7 @@ contract SponsoredOFTSrcPeriphery is Ownable, SharedDecimalsLib { address _oftMessenger, uint32 _srcEid, address _signer - ) SharedDecimalsLib(IERC20Metadata(_token).decimals(), address(0), address(0)) { + ) SharedDecimalsLib(IERC20Metadata(_token).decimals(), IOFT(_oftMessenger).sharedDecimals()) { TOKEN = _token; OFT_MESSENGER = _oftMessenger; SRC_EID = _srcEid; From 92f9e844bf6eb57dc689c6c9bc4a7c867a47f6d8 Mon Sep 17 00:00:00 2001 From: Ihor Farion Date: Thu, 18 Dec 2025 19:34:03 -0800 Subject: [PATCH 7/7] polish to a more auditable state Signed-off-by: Ihor Farion --- ...{SharedDecimalsLib.sol => OFTCoreMath.sol} | 42 ++++--------------- .../mintburn/sponsored-oft/DstOFTHandler.sol | 6 +-- .../SponsoredOFTSrcPeriphery.sol | 6 +-- 3 files changed, 13 insertions(+), 41 deletions(-) rename contracts/external/libraries/{SharedDecimalsLib.sol => OFTCoreMath.sol} (58%) diff --git a/contracts/external/libraries/SharedDecimalsLib.sol b/contracts/external/libraries/OFTCoreMath.sol similarity index 58% rename from contracts/external/libraries/SharedDecimalsLib.sol rename to contracts/external/libraries/OFTCoreMath.sol index 0feea643b..43213854d 100644 --- a/contracts/external/libraries/SharedDecimalsLib.sol +++ b/contracts/external/libraries/OFTCoreMath.sol @@ -2,12 +2,12 @@ pragma solidity ^0.8.0; /** - * @title SharedDecimalsLib + * @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 removing unrelated OFT/OApp concerns. + * Code was not modified beyond adding `uint8 _sharedDecimals` to constructor args and substituting `sharedDecimals()` calls with it */ -abstract contract SharedDecimalsLib { +abstract contract OFTCoreMath { error InvalidLocalDecimals(); error AmountSDOverflowed(uint256 amountSD); @@ -25,43 +25,15 @@ abstract contract SharedDecimalsLib { // @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; - uint8 internal immutable _sharedDecimals; /** * @dev Constructor. * @param _localDecimals The decimals of the token on the local chain (this chain). - * @param _sharedDecimalsArg The shared decimals used by the OFT. + * @param _sharedDecimals The shared decimals used by the OFT. */ - constructor(uint8 _localDecimals, uint8 _sharedDecimalsArg) { - _sharedDecimals = _sharedDecimalsArg; - if (_localDecimals < _sharedDecimalsArg) revert InvalidLocalDecimals(); - decimalConversionRate = 10 ** (_localDecimals - _sharedDecimalsArg); - } - - /** - * @dev Retrieves the shared decimals of the OFT. - * @return The shared decimals of the OFT. - * - * @dev Sets an implicit cap on the amount of tokens, over uint64.max() will need some sort of outbound cap / totalSupply cap - * Lowest common decimal denominator between chains. - * Defaults to 6 decimal places to provide up to 18,446,744,073,709.551615 units (max uint64). - * For tokens exceeding this totalSupply(), they will need to override the sharedDecimals function with something smaller. - * ie. 4 sharedDecimals would be 1,844,674,407,370,955.1615 - */ - function sharedDecimals() public view virtual returns (uint8) { - return _sharedDecimals; - } - - /** - * @dev Internal function to remove dust from the given local decimal amount. - * @param _amountLD The amount in local decimals. - * @return amountLD The amount after removing dust. - * - * @dev Prevents the loss of dust when moving amounts between chains with different decimals. - * @dev eg. uint(123) with a conversion rate of 100 becomes uint(100). - */ - function _removeDust(uint256 _amountLD) internal view virtual returns (uint256 amountLD) { - return (_amountLD / decimalConversionRate) * decimalConversionRate; + constructor(uint8 _localDecimals, uint8 _sharedDecimals) { + if (_localDecimals < _sharedDecimals) revert InvalidLocalDecimals(); + decimalConversionRate = 10 ** (_localDecimals - _sharedDecimals); } /** diff --git a/contracts/periphery/mintburn/sponsored-oft/DstOFTHandler.sol b/contracts/periphery/mintburn/sponsored-oft/DstOFTHandler.sol index 898529cb4..cd50033d6 100644 --- a/contracts/periphery/mintburn/sponsored-oft/DstOFTHandler.sol +++ b/contracts/periphery/mintburn/sponsored-oft/DstOFTHandler.sol @@ -11,7 +11,7 @@ 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 { SharedDecimalsLib } from "../../../external/libraries/SharedDecimalsLib.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"; @@ -23,7 +23,7 @@ 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, SharedDecimalsLib, ILayerZeroComposer, ArbitraryEVMFlowExecutor { +contract DstOFTHandler is BaseModuleHandler, OFTCoreMath, ILayerZeroComposer, ArbitraryEVMFlowExecutor { using ComposeMsgCodec for bytes; using Bytes32ToAddress for bytes32; using AddressToBytes32 for address; @@ -87,7 +87,7 @@ contract DstOFTHandler is BaseModuleHandler, SharedDecimalsLib, ILayerZeroCompos ) BaseModuleHandler(_donationBox, _baseToken, DEFAULT_ADMIN_ROLE) ArbitraryEVMFlowExecutor(_multicallHandler) - SharedDecimalsLib(IERC20Metadata(_baseToken).decimals(), IOFT(_ioft).sharedDecimals()) + OFTCoreMath(IERC20Metadata(_baseToken).decimals(), IOFT(_ioft).sharedDecimals()) { baseToken = _baseToken; diff --git a/contracts/periphery/mintburn/sponsored-oft/SponsoredOFTSrcPeriphery.sol b/contracts/periphery/mintburn/sponsored-oft/SponsoredOFTSrcPeriphery.sol index 5a539badc..8de2e531a 100644 --- a/contracts/periphery/mintburn/sponsored-oft/SponsoredOFTSrcPeriphery.sol +++ b/contracts/periphery/mintburn/sponsored-oft/SponsoredOFTSrcPeriphery.sol @@ -8,7 +8,7 @@ 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 { SharedDecimalsLib } from "../../../external/libraries/SharedDecimalsLib.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"; @@ -17,7 +17,7 @@ import { SafeERC20 } from "@openzeppelin/contracts-v4/token/ERC20/utils/SafeERC2 /// @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, SharedDecimalsLib { +contract SponsoredOFTSrcPeriphery is Ownable, OFTCoreMath { using AddressToBytes32 for address; using MinimalLZOptions for bytes; using SafeERC20 for IERC20; @@ -95,7 +95,7 @@ contract SponsoredOFTSrcPeriphery is Ownable, SharedDecimalsLib { address _oftMessenger, uint32 _srcEid, address _signer - ) SharedDecimalsLib(IERC20Metadata(_token).decimals(), IOFT(_oftMessenger).sharedDecimals()) { + ) OFTCoreMath(IERC20Metadata(_token).decimals(), IOFT(_oftMessenger).sharedDecimals()) { TOKEN = _token; OFT_MESSENGER = _oftMessenger; SRC_EID = _srcEid;