Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions contracts/external/libraries/OFTCoreMath.sol
Original file line number Diff line number Diff line change
@@ -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);
}
}
6 changes: 6 additions & 0 deletions contracts/interfaces/IOFT.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
31 changes: 19 additions & 12 deletions contracts/periphery/mintburn/sponsored-oft/ComposeMsgCodec.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -31,6 +33,7 @@ library ComposeMsgCodec {
return
abi.encode(
nonce,
amountSD,
deadline,
maxBpsToSponsor,
maxUserSlippageBps,
Expand All @@ -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);
}
Expand Down Expand Up @@ -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;
}
Expand Down
28 changes: 21 additions & 7 deletions contracts/periphery/mintburn/sponsored-oft/DstOFTHandler.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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";

/**
Expand All @@ -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;

Expand Down Expand Up @@ -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()) {
Expand Down Expand Up @@ -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();
Expand All @@ -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)
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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,
Expand All @@ -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
Expand Down
1 change: 1 addition & 0 deletions contracts/periphery/mintburn/sponsored-oft/Structs.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
4 changes: 4 additions & 0 deletions contracts/test/MockOFTMessenger.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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*/,
Expand Down
19 changes: 18 additions & 1 deletion script/mintburn/oft/CreateSponsoredDeposit.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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
Expand Down Expand Up @@ -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: ""
Expand Down Expand Up @@ -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,
Expand All @@ -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()
Expand Down
Loading
Loading