diff --git a/contracts/external/interfaces/ICoreDepositWallet.sol b/contracts/external/interfaces/ICoreDepositWallet.sol index b92a4c9e4..25ba71a43 100644 --- a/contracts/external/interfaces/ICoreDepositWallet.sol +++ b/contracts/external/interfaces/ICoreDepositWallet.sol @@ -20,7 +20,7 @@ pragma solidity ^0.8.0; /** * @title ICoreDepositWallet * @notice Interface for the core deposit wallet - * @dev Source: https://developers.circle.com/cctp/coredepositwallet-contract-interface#deposit-function + * @dev Source: https://github.com/circlefin/hyperevm-circle-contracts/blob/master/src/interfaces/ICoreDepositWallet.sol */ interface ICoreDepositWallet { /** @@ -29,4 +29,13 @@ interface ICoreDepositWallet { * @param destinationDex The destination dex on HyperCore. */ function deposit(uint256 amount, uint32 destinationDex) external; + + /** + * @notice Deposit tokens for a recipient + * @param recipient Recipient of the deposit + * @param amount Amount of tokens to deposit + * @param destinationId Forwarding-address-specific id used in conjunction with + * recipient to route the deposit to a specific location. + */ + function depositFor(address recipient, uint256 amount, uint32 destinationId) external; } diff --git a/contracts/external/libraries/BytesLib.sol b/contracts/external/libraries/BytesLib.sol index 03d38d7ca..768dd68c8 100644 --- a/contracts/external/libraries/BytesLib.sol +++ b/contracts/external/libraries/BytesLib.sol @@ -17,6 +17,25 @@ library BytesLib { // https://github.com/GNSPS/solidity-bytes-utils/blob/fc502455bb2a7e26a743378df042612dd50d1eb9/contracts/BytesLib.sol#L323C5-L398C6 // Code was copied, and slightly modified to use revert instead of require + /** + * @notice Reads a uint8 from a bytes array at a given start index + * @param _bytes The bytes array to convert + * @param _start The start index of the uint8 + * @return result The uint8 result + */ + function toUint8(bytes memory _bytes, uint256 _start) internal pure returns (uint8) { + if (_bytes.length < _start + 1) { + revert OutOfBounds(); + } + uint8 tempUint; + + assembly { + tempUint := mload(add(add(_bytes, 0x1), _start)) + } + + return tempUint; + } + /** * @notice Reads a uint16 from a bytes array at a given start index * @param _bytes The bytes array to convert diff --git a/contracts/handlers/HyperliquidDepositHandler.sol b/contracts/handlers/HyperliquidDepositHandler.sol index a7ece2200..1d6201795 100644 --- a/contracts/handlers/HyperliquidDepositHandler.sol +++ b/contracts/handlers/HyperliquidDepositHandler.sol @@ -180,7 +180,7 @@ contract HyperliquidDepositHandler is AcrossMessageHandler, ReentrancyGuard, Own */ function sweepCoreFundsToUser(address token, uint64 coreAmount, address user) external onlyOwner nonReentrant { uint64 tokenIndex = _getTokenInfo(token).tokenId; - HyperCoreLib.transferERC20CoreToCore(tokenIndex, user, coreAmount); + HyperCoreLib.transferERC20SpotToSpot(tokenIndex, user, coreAmount); } /** @@ -225,12 +225,21 @@ contract HyperliquidDepositHandler is AcrossMessageHandler, ReentrancyGuard, Own donationBox.withdraw(IERC20(token), amountRequiredToActivate); // Deposit the activation fee + 1 wei into this contract's core account to pay for the user's // account activation. - HyperCoreLib.transferERC20EVMToSelfOnCore(token, tokenIndex, amountRequiredToActivate, decimalDiff); - HyperCoreLib.transferERC20CoreToCore(tokenIndex, user, 1); + HyperCoreLib.transferERC20EVMToSelfOnSpot(token, tokenIndex, amountRequiredToActivate, decimalDiff); + HyperCoreLib.transferERC20SpotToSpot(tokenIndex, user, 1); emit UserAccountActivated(user, token, amountRequiredToActivate); } - HyperCoreLib.transferERC20EVMToCore(token, tokenIndex, user, evmAmount, decimalDiff); + HyperCoreLib.transferERC20EVMToCore( + token, + tokenIndex, + user, + evmAmount, + decimalDiff, + HyperCoreLib.CORE_SPOT_DEX_ID, + // Account activation is handled in separate CoreWriter actions above + 0 + ); } function _verifySignature(address expectedUser, bytes memory signature) internal view returns (bool) { diff --git a/contracts/interfaces/SponsoredCCTPInterface.sol b/contracts/interfaces/SponsoredCCTPInterface.sol index 0eec4b37c..fd3c10ab4 100644 --- a/contracts/interfaces/SponsoredCCTPInterface.sol +++ b/contracts/interfaces/SponsoredCCTPInterface.sol @@ -30,6 +30,8 @@ interface SponsoredCCTPInterface { uint256 maxBpsToSponsor, uint256 maxUserSlippageBps, bytes32 finalToken, + uint32 destinationDex, + uint8 accountCreationMode, bytes signature ); @@ -78,6 +80,10 @@ interface SponsoredCCTPInterface { // The final token that final recipient will receive. This is needed as it can be different from the burnToken // in which case we perform a swap on the destination chain. bytes32 finalToken; + // The destination DEX on HyperCore. + uint32 destinationDex; + // AccountCreationMode: Standard or FromUserFunds + uint8 accountCreationMode; // Execution mode: DirectToCore, ArbitraryActionsToCore, or ArbitraryActionsToEVM uint8 executionMode; // Encoded action data for arbitrary execution. Empty for DirectToCore mode. diff --git a/contracts/libraries/HyperCoreLib.sol b/contracts/libraries/HyperCoreLib.sol index 0c5828041..8829ef97d 100644 --- a/contracts/libraries/HyperCoreLib.sol +++ b/contracts/libraries/HyperCoreLib.sol @@ -60,9 +60,10 @@ library HyperCoreLib { bytes4 public constant LIMIT_ORDER_HEADER = 0x01000001; // version=1, action=1 bytes4 public constant SPOT_SEND_HEADER = 0x01000006; // version=1, action=6 bytes4 public constant CANCEL_BY_CLOID_HEADER = 0x0100000B; // version=1, action=11 + bytes4 public constant SEND_ASSET_TO_DEX_HEADER = 0x0100000D; // version=1, action=13 // HyperCore protocol constants - uint32 private constant CORE_SPOT_DEX_ID = type(uint32).max; + uint32 public constant CORE_SPOT_DEX_ID = type(uint32).max; // Errors error LimitPxIsZero(); @@ -81,6 +82,8 @@ library HyperCoreLib { * @param to The address to receive tokens on HyperCore * @param amountEVM The amount to transfer on HyperEVM * @param decimalDiff The decimal difference of evmDecimals - coreDecimals + * @param destinationDex The destination DEX on HyperCore + * @param accountActivationFeeCore Present if we have to pay for account activation of `to` (meaning we're reducing our send amount) * @return amountEVMSent The amount sent on HyperEVM * @return amountCoreToReceive The amount credited on Core in Core units (post conversion) */ @@ -89,18 +92,71 @@ library HyperCoreLib { uint64 erc20CoreIndex, address to, uint256 amountEVM, - int8 decimalDiff + int8 decimalDiff, + uint32 destinationDex, + uint64 accountActivationFeeCore ) internal returns (uint256 amountEVMSent, uint64 amountCoreToReceive) { // if the transfer amount exceeds the bridge balance, this wil revert (uint256 _amountEVMToSend, uint64 _amountCoreToReceive) = maximumEVMSendAmountToAmounts(amountEVM, decimalDiff); if (_amountEVMToSend != 0) { - transferToCore(erc20EVMAddress, erc20CoreIndex, _amountEVMToSend); - // Transfer the tokens from this contract on HyperCore to the `to` address on HyperCore - transferERC20CoreToCore(erc20CoreIndex, to, _amountCoreToReceive); + if (erc20CoreIndex == USDC_CORE_INDEX) { + // USDC flow takes care of account creation fee for us, we don't need to reduce `_amountEVMToSend` + IERC20(erc20EVMAddress).forceApprove(USDC_CORE_DEPOSIT_WALLET_ADDRESS, _amountEVMToSend); + if (to == address(this)) { + ICoreDepositWallet(USDC_CORE_DEPOSIT_WALLET_ADDRESS).deposit(_amountEVMToSend, destinationDex); + } else { + ICoreDepositWallet(USDC_CORE_DEPOSIT_WALLET_ADDRESS).depositFor( + to, + _amountEVMToSend, + destinationDex + ); + } + } else { + IERC20(erc20EVMAddress).safeTransfer(toAssetBridgeAddress(erc20CoreIndex), _amountEVMToSend); + // Transfer the tokens from this contract on HyperCore to the `to` address on HyperCore + if ( + (to != address(this) || destinationDex != CORE_SPOT_DEX_ID) && + _amountCoreToReceive > accountActivationFeeCore + ) { + transferERC20CoreToCore( + erc20CoreIndex, + to, + _amountCoreToReceive - accountActivationFeeCore, + CORE_SPOT_DEX_ID, + destinationDex + ); + } + } } - return (_amountEVMToSend, _amountCoreToReceive); + return (_amountEVMToSend, _amountCoreToReceive - accountActivationFeeCore); + } + + /** + * @notice Bridges `amountEVM` of `erc20` from this address on HyperEVM to this address on HyperCore on the Spot DEX. + * @dev Returns the amount sent on HyperEVM and the amount credited on Core in Core units (post conversion). + * @dev The decimal difference is evmDecimals - coreDecimals + * @param erc20EVMAddress The address of the ERC20 token on HyperEVM + * @param erc20CoreIndex The HyperCore index id of the token to transfer + * @param amountEVM The amount to transfer on HyperEVM + * @param decimalDiff The decimal difference of evmDecimals - coreDecimals + * @return amountEVMSent The amount sent on HyperEVM + * @return amountCoreToReceive The amount credited on Core in Core units (post conversion) + */ + function transferERC20EVMToSelfOnSpot( + address erc20EVMAddress, + uint64 erc20CoreIndex, + uint256 amountEVM, + int8 decimalDiff + ) internal returns (uint256 amountEVMSent, uint64 amountCoreToReceive) { + (amountEVMSent, amountCoreToReceive) = transferERC20EVMToSelfOnCore( + erc20EVMAddress, + erc20CoreIndex, + amountEVM, + decimalDiff, + CORE_SPOT_DEX_ID + ); } /** @@ -111,6 +167,7 @@ library HyperCoreLib { * @param erc20CoreIndex The HyperCore index id of the token to transfer * @param amountEVM The amount to transfer on HyperEVM * @param decimalDiff The decimal difference of evmDecimals - coreDecimals + * @param destinationDex The destination DEX on HyperCore * @return amountEVMSent The amount sent on HyperEVM * @return amountCoreToReceive The amount credited on Core in Core units (post conversion) */ @@ -118,15 +175,30 @@ library HyperCoreLib { address erc20EVMAddress, uint64 erc20CoreIndex, uint256 amountEVM, - int8 decimalDiff + int8 decimalDiff, + uint32 destinationDex ) internal returns (uint256 amountEVMSent, uint64 amountCoreToReceive) { - (uint256 _amountEVMToSend, uint64 _amountCoreToReceive) = maximumEVMSendAmountToAmounts(amountEVM, decimalDiff); - - if (_amountEVMToSend != 0) { - transferToCore(erc20EVMAddress, erc20CoreIndex, _amountEVMToSend); - } + return + transferERC20EVMToCore( + erc20EVMAddress, + erc20CoreIndex, + address(this), + amountEVM, + decimalDiff, + destinationDex, + // Contracts that are using this function HAVE to have their accounts created already + 0 + ); + } - return (_amountEVMToSend, _amountCoreToReceive); + /** + * @notice Transfers tokens from this contract on HyperCore to the `to` address on HyperCore on the Spot DEX. + * @param erc20CoreIndex The HyperCore index id of the token + * @param to The address to receive tokens on HyperCore + * @param amountCore The amount to transfer on HyperCore + */ + function transferERC20SpotToSpot(uint64 erc20CoreIndex, address to, uint64 amountCore) internal { + transferERC20CoreToCore(erc20CoreIndex, to, amountCore, CORE_SPOT_DEX_ID, CORE_SPOT_DEX_ID); } /** @@ -134,28 +206,50 @@ library HyperCoreLib { * @param erc20CoreIndex The HyperCore index id of the token * @param to The address to receive tokens on HyperCore * @param amountCore The amount to transfer on HyperCore + * @param sourceDex The source DEX on HyperCore + * @param destinationDex The destination DEX on HyperCore */ - function transferERC20CoreToCore(uint64 erc20CoreIndex, address to, uint64 amountCore) internal { - bytes memory action = abi.encode(to, erc20CoreIndex, amountCore); - bytes memory payload = abi.encodePacked(SPOT_SEND_HEADER, action); + function transferERC20CoreToCore( + uint64 erc20CoreIndex, + address to, + uint64 amountCore, + uint32 sourceDex, + uint32 destinationDex + ) internal { + bytes memory action; + bytes memory payload; + + if (destinationDex != CORE_SPOT_DEX_ID) { + action = abi.encode(to, address(0), sourceDex, destinationDex, erc20CoreIndex, amountCore); + payload = abi.encodePacked(SEND_ASSET_TO_DEX_HEADER, action); + } else { + action = abi.encode(to, erc20CoreIndex, amountCore); + payload = abi.encodePacked(SPOT_SEND_HEADER, action); + } ICoreWriter(CORE_WRITER_PRECOMPILE_ADDRESS).sendRawAction(payload); } /** - * @notice Transfers tokens from this contract on HyperEVM to this contract's address on HyperCore - * @param erc20EVMAddress The address of the ERC20 token on HyperEVM - * @param erc20CoreIndex The HyperCore index id of the token to transfer - * @param amountEVMToSend The amount to transfer on HyperEVM + * @notice Activate a user account on HyperCore from HyperEVM. + * @param erc20EVMAddress The address of the ERC20 token on HyperEVM. + * @param erc20CoreIndex The HyperCore index id of the token to transfer. + * @param user The address to activate on HyperCore. + * @param amountEVM The amount to transfer on HyperEVM. */ - function transferToCore(address erc20EVMAddress, uint64 erc20CoreIndex, uint256 amountEVMToSend) internal { - // USDC requires a special transfer to core + function activateCoreAccountFromEVM( + address erc20EVMAddress, + uint64 erc20CoreIndex, + address user, + uint256 amountEVM + ) internal { if (erc20CoreIndex == USDC_CORE_INDEX) { - IERC20(erc20EVMAddress).forceApprove(USDC_CORE_DEPOSIT_WALLET_ADDRESS, amountEVMToSend); - ICoreDepositWallet(USDC_CORE_DEPOSIT_WALLET_ADDRESS).deposit(amountEVMToSend, CORE_SPOT_DEX_ID); + IERC20(erc20EVMAddress).forceApprove(USDC_CORE_DEPOSIT_WALLET_ADDRESS, amountEVM); + ICoreDepositWallet(USDC_CORE_DEPOSIT_WALLET_ADDRESS).depositFor(user, amountEVM, CORE_SPOT_DEX_ID); } else { - // For all other tokens, transfer to the asset bridge address on HyperCore - IERC20(erc20EVMAddress).safeTransfer(toAssetBridgeAddress(erc20CoreIndex), amountEVMToSend); + IERC20(erc20EVMAddress).safeTransfer(toAssetBridgeAddress(erc20CoreIndex), amountEVM); + // Transfer 1 wei to user on HyperCore to activate account + transferERC20CoreToCore(erc20CoreIndex, user, 1, CORE_SPOT_DEX_ID, CORE_SPOT_DEX_ID); } } diff --git a/contracts/libraries/SponsoredCCTPQuoteLib.sol b/contracts/libraries/SponsoredCCTPQuoteLib.sol index 199f93761..8529e914a 100644 --- a/contracts/libraries/SponsoredCCTPQuoteLib.sol +++ b/contracts/libraries/SponsoredCCTPQuoteLib.sol @@ -38,7 +38,7 @@ library SponsoredCCTPQuoteLib { uint256 private constant HOOK_DATA_INDEX = 228; // Minimum length of the message body (can be longer due to variable actionData) - uint256 private constant MIN_MSG_BYTES_LENGTH = 664; + uint256 private constant MIN_MSG_BYTES_LENGTH = 728; /** * @notice Gets the data for the deposit for burn. @@ -82,6 +82,8 @@ library SponsoredCCTPQuoteLib { quote.maxUserSlippageBps, quote.finalRecipient, quote.finalToken, + quote.destinationDex, + quote.accountCreationMode, quote.executionMode, quote.actionData ); @@ -109,9 +111,9 @@ library SponsoredCCTPQuoteLib { bytes memory hookData = messageBody.slice(HOOK_DATA_INDEX, messageBody.length); // Decode to check address validity - (, , , , bytes32 finalRecipient, bytes32 finalToken, , ) = abi.decode( + (, , , , bytes32 finalRecipient, bytes32 finalToken, , , , ) = abi.decode( hookData, - (bytes32, uint256, uint256, uint256, bytes32, bytes32, uint8, bytes) + (bytes32, uint256, uint256, uint256, bytes32, bytes32, uint32, uint8, uint8, bytes) ); return finalRecipient.isValidAddress() && finalToken.isValidAddress(); @@ -147,9 +149,11 @@ library SponsoredCCTPQuoteLib { quote.maxUserSlippageBps, quote.finalRecipient, quote.finalToken, + quote.destinationDex, + quote.accountCreationMode, quote.executionMode, quote.actionData - ) = abi.decode(hookData, (bytes32, uint256, uint256, uint256, bytes32, bytes32, uint8, bytes)); + ) = abi.decode(hookData, (bytes32, uint256, uint256, uint256, bytes32, bytes32, uint32, uint8, uint8, bytes)); } /** @@ -186,6 +190,8 @@ library SponsoredCCTPQuoteLib { quote.maxUserSlippageBps, quote.finalRecipient, quote.finalToken, + quote.destinationDex, + quote.accountCreationMode, quote.executionMode, keccak256(quote.actionData) // Hash the actionData to keep signature size reasonable ) diff --git a/contracts/periphery/mintburn/ArbitraryEVMFlowExecutor.sol b/contracts/periphery/mintburn/ArbitraryEVMFlowExecutor.sol index 1ab40f3cd..8662e1c48 100644 --- a/contracts/periphery/mintburn/ArbitraryEVMFlowExecutor.sol +++ b/contracts/periphery/mintburn/ArbitraryEVMFlowExecutor.sol @@ -93,7 +93,7 @@ abstract contract ArbitraryEVMFlowExecutor { // This means the swap did happen, so we check the balance of the output token and send it. finalAmount = finalBalance - finalAmountSnapshot; } else { - // If we somehow lost final tokens, just set the finalAmount to 0. + // If we somehow lost final tokens(e.g. by depositing into some contract), just set the finalAmount to 0. finalAmount = 0; } } diff --git a/contracts/periphery/mintburn/HyperCoreFlowExecutor.sol b/contracts/periphery/mintburn/HyperCoreFlowExecutor.sol index 1b6457836..476a43d7b 100644 --- a/contracts/periphery/mintburn/HyperCoreFlowExecutor.sol +++ b/contracts/periphery/mintburn/HyperCoreFlowExecutor.sol @@ -9,7 +9,7 @@ import { CoreTokenInfo } from "./Structs.sol"; import { FinalTokenInfo } from "./Structs.sol"; import { SwapHandler } from "./SwapHandler.sol"; import { BPS_SCALAR, BPS_DECIMALS } from "./Constants.sol"; -import { CommonFlowParams } from "./Structs.sol"; +import { CommonFlowParams, AccountCreationMode } from "./Structs.sol"; // Note: v5 is necessary since v4 does not use ERC-7201. import { AccessControlUpgradeable } from "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol"; @@ -46,6 +46,7 @@ contract HyperCoreFlowExecutor is AccessControlUpgradeable, AuthorizedFundedFlow struct SwapFlowState { address finalRecipient; address finalToken; + uint32 destinationDex; uint64 minAmountToSend; // for sponsored: one to one, non-sponsored: one to one minus slippage uint64 maxAmountToSend; // for sponsored: one to one (from total bridged amt), for non-sponsored: one to one, less bridging fees incurred bool isSponsored; @@ -230,6 +231,20 @@ contract HyperCoreFlowExecutor is AccessControlUpgradeable, AuthorizedFundedFlow uint256 evmAmountSponsored ); + /** + * @notice Emitted whenever a user account is activated using funds from the user's transfer/swap + * @param quoteNonce Unique identifier for this quote/transaction + * @param user The address of the user whose account is being activated + * @param token The token used to fund the account activation + * @param amountCore The amount paid for activation (in Core token units) + */ + event AccountActivatedFromUserFunds( + bytes32 indexed quoteNonce, + address indexed user, + address indexed token, + uint64 amountCore + ); + /** * @notice Emitted whenever a new CoreTokenInfo is configured * @param token The token address being configured @@ -283,6 +298,12 @@ contract HyperCoreFlowExecutor is AccessControlUpgradeable, AuthorizedFundedFlow /// @notice Thrown when we can't bridge some token from HyperEVM to HyperCore error UnsafeToBridgeError(address token, uint64 amount); + /// @notice Thrown when finalizing a swap flow, if a finalToken used to be eligible for account activation, but is no longer + error TokenNotEligibleForActivation(); + + /// @notice Thrown when a user order total is not enough to cover account activation when finalizing a swap flow + error InsufficientFundsForActivation(); + /************************************** * MODIFIERS * **************************************/ @@ -499,18 +520,27 @@ contract HyperCoreFlowExecutor is AccessControlUpgradeable, AuthorizedFundedFlow MainStorage storage $ = _getMainStorage(); CoreTokenInfo memory coreTokenInfo = $.coreTokenInfos[finalToken]; - // Check account activation + uint64 accountActivationFeeCore; if (!HyperCoreLib.coreUserExists(params.finalRecipient)) { - if (params.maxBpsToSponsor > 0) { + bool isStandard = params.accountCreationMode == AccountCreationMode.Standard; + + // Standard, sponsored + if (isStandard && params.maxBpsToSponsor > 0) { revert AccountNotActivatedError(params.finalRecipient); - } else { + } + + // Standard, non-sponsored OR + // FromUserAccount AND token can't be used for account activation + if (isStandard || !coreTokenInfo.canBeUsedForAccountActivation) { emit AccountNotActivated(params.quoteNonce, params.finalRecipient); _fallbackHyperEVMFlow(params); return; } + + // FromUserAccount AND token can be used for account activation + accountActivationFeeCore = coreTokenInfo.accountActivationFeeCore; } - // Calculate sponsorship amount in scope uint256 amountToSponsor; { uint256 maxEvmAmountToSponsor = ((params.amountInEVM + params.extraFeesIncurred) * params.maxBpsToSponsor) / @@ -568,9 +598,20 @@ contract HyperCoreFlowExecutor is AccessControlUpgradeable, AuthorizedFundedFlow coreTokenInfo.coreIndex, params.finalRecipient, quotedEvmAmount, - coreTokenInfo.tokenInfo.evmExtraWeiDecimals + coreTokenInfo.tokenInfo.evmExtraWeiDecimals, + params.destinationDex, + accountActivationFeeCore ); + if (accountActivationFeeCore > 0) { + emit AccountActivatedFromUserFunds( + params.quoteNonce, + params.finalRecipient, + finalToken, + accountActivationFeeCore + ); + } + emit SimpleTransferFlowCompleted( params.quoteNonce, params.finalRecipient, @@ -591,11 +632,22 @@ contract HyperCoreFlowExecutor is AccessControlUpgradeable, AuthorizedFundedFlow function _initiateSwapFlow(CommonFlowParams memory params, uint256 maxUserSlippageBps) internal { address initialToken = baseToken; - // Check account activation + MainStorage storage $ = _getMainStorage(); + CoreTokenInfo memory initialCoreTokenInfo = $.coreTokenInfos[initialToken]; + CoreTokenInfo memory finalCoreTokenInfo = $.coreTokenInfos[params.finalToken]; + FinalTokenInfo memory finalTokenInfo = _getExistingFinalTokenInfo(params.finalToken); + if (!HyperCoreLib.coreUserExists(params.finalRecipient)) { - if (params.maxBpsToSponsor > 0) { + bool isStandard = params.accountCreationMode == AccountCreationMode.Standard; + + // Standard, sponsored + if (isStandard && params.maxBpsToSponsor > 0) { revert AccountNotActivatedError(params.finalRecipient); - } else { + } + + // Standard, non-sponsored OR + // FromUserFunds AND final token can't be used for account creation + if (isStandard || !finalCoreTokenInfo.canBeUsedForAccountActivation) { emit AccountNotActivated(params.quoteNonce, params.finalRecipient); params.finalToken = initialToken; _fallbackHyperEVMFlow(params); @@ -603,11 +655,6 @@ contract HyperCoreFlowExecutor is AccessControlUpgradeable, AuthorizedFundedFlow } } - MainStorage storage $ = _getMainStorage(); - CoreTokenInfo memory initialCoreTokenInfo = $.coreTokenInfos[initialToken]; - CoreTokenInfo memory finalCoreTokenInfo = $.coreTokenInfos[params.finalToken]; - FinalTokenInfo memory finalTokenInfo = _getExistingFinalTokenInfo(params.finalToken); - // Calculate limit order amounts and check if feasible uint64 minAllowableAmountToForwardCore; uint64 maxAllowableAmountToForwardCore; @@ -687,6 +734,7 @@ contract HyperCoreFlowExecutor is AccessControlUpgradeable, AuthorizedFundedFlow $.swaps[params.quoteNonce] = SwapFlowState({ finalRecipient: params.finalRecipient, finalToken: params.finalToken, + destinationDex: params.destinationDex, minAmountToSend: minAllowableAmountToForwardCore, maxAmountToSend: maxAllowableAmountToForwardCore, isSponsored: params.maxBpsToSponsor > 0, @@ -813,31 +861,51 @@ contract HyperCoreFlowExecutor is AccessControlUpgradeable, AuthorizedFundedFlow if (swap.finalized) revert SwapAlreadyFinalized(); if (swap.finalToken != finalToken) revert WrongSwapFinalizationToken(quoteNonce); - uint64 totalToSend; - (totalToSend, additionalToSend) = _calcSwapFlowSendAmounts( + uint64 accountActivationFee; + // User account can be absent if our AccountCreationMode is `FromUserFunds`. We need to adjust our transfer amount to user based on this + if (!HyperCoreLib.coreUserExists(swap.finalRecipient)) { + // This is enforced by `_initiateSwapFlow`. If the setting changed, this revert can trigger. Requires manual resolution (creating an account) + require(finalCoreTokenInfo.canBeUsedForAccountActivation, TokenNotEligibleForActivation()); + accountActivationFee = finalCoreTokenInfo.accountActivationFeeCore; + } + + uint64 amountToTransfer; + uint64 totalConsumed; + (amountToTransfer, additionalToSend, totalConsumed) = _calcSwapFlowSendAmounts( limitOrderOut, swap.minAmountToSend, swap.maxAmountToSend, - swap.isSponsored + swap.isSponsored, + accountActivationFee ); // `additionalToSend` will land on HCore before this core > core send will need to be executed balanceRemaining = availableBalance + additionalToSend; - if (totalToSend > balanceRemaining) { + if (totalConsumed > balanceRemaining) { return (false, 0, availableBalance); } swap.finalized = true; success = true; - balanceRemaining -= totalToSend; + balanceRemaining -= totalConsumed; (uint256 additionalToSendEVM, ) = HyperCoreLib.minimumCoreReceiveAmountToAmounts( additionalToSend, finalCoreTokenInfo.tokenInfo.evmExtraWeiDecimals ); - swapHandler.transferFundsToUserOnCore(finalCoreTokenInfo.coreIndex, swap.finalRecipient, totalToSend); - emit SwapFlowFinalized(quoteNonce, swap.finalRecipient, swap.finalToken, totalToSend, additionalToSendEVM); + swapHandler.transferFundsToUserOnCore( + finalCoreTokenInfo.coreIndex, + swap.finalRecipient, + amountToTransfer, + swap.destinationDex + ); + + if (accountActivationFee > 0) { + emit AccountActivatedFromUserFunds(quoteNonce, swap.finalRecipient, swap.finalToken, accountActivationFee); + } + + emit SwapFlowFinalized(quoteNonce, swap.finalRecipient, swap.finalToken, amountToTransfer, additionalToSendEVM); } /// @notice Forwards `amount` plus potential sponsorship funds (for bridging fee) to user on HyperEVM @@ -900,17 +968,8 @@ contract HyperCoreFlowExecutor is AccessControlUpgradeable, AuthorizedFundedFlow // donationBox @ evm -> Handler @ evm donationBox.withdraw(IERC20(fundingToken), evmAmountToSend); - // Handler @ evm -> Handler @ core - HyperCoreLib.transferERC20EVMToSelfOnCore( - fundingToken, - coreTokenInfo.coreIndex, - evmAmountToSend, - coreTokenInfo.tokenInfo.evmExtraWeiDecimals - ); - // The total balance withdrawn from Handler @ Core for this operation is activationFee + amountSent, so we set - // amountSent to 1 wei to only activate the account - // Handler @ core -> finalRecipient @ core - HyperCoreLib.transferERC20CoreToCore(coreTokenInfo.coreIndex, finalRecipient, 1); + + HyperCoreLib.activateCoreAccountFromEVM(fundingToken, coreTokenInfo.coreIndex, finalRecipient, evmAmountToSend); emit SponsoredAccountActivation(quoteNonce, finalRecipient, fundingToken, evmAmountToSend); } @@ -1050,24 +1109,33 @@ contract HyperCoreFlowExecutor is AccessControlUpgradeable, AuthorizedFundedFlow } /** - * @return totalToSend What we will forward to user on HCore + * @return amountToTransfer What we will forward to user on HCore * @return additionalToSend What we will send from donationBox right now + * @return totalAttributableToUser Total amount that we reserve for this user. The amount consumed from SwapHandler + during send execution. Differs from `amountToTransfer` if account creation fee is + present */ function _calcSwapFlowSendAmounts( uint64 limitOrderOut, uint64 minAmountToSend, uint64 maxAmountToSend, - bool isSponsored - ) internal pure returns (uint64 totalToSend, uint64 additionalToSend) { + bool isSponsored, + uint64 accountActivationFee + ) internal pure returns (uint64 amountToTransfer, uint64 additionalToSend, uint64 totalAttributableToUser) { if (isSponsored) { // `minAmountToSend` is equal to `maxAmountToSend` for the sponsored flow - totalToSend = minAmountToSend; - additionalToSend = totalToSend > limitOrderOut ? totalToSend - limitOrderOut : 0; + totalAttributableToUser = minAmountToSend; + additionalToSend = totalAttributableToUser > limitOrderOut ? totalAttributableToUser - limitOrderOut : 0; } else { additionalToSend = limitOrderOut < minAmountToSend ? minAmountToSend - limitOrderOut : 0; uint64 proposedToSend = limitOrderOut + additionalToSend; - totalToSend = proposedToSend > maxAmountToSend ? maxAmountToSend : proposedToSend; + totalAttributableToUser = proposedToSend > maxAmountToSend ? maxAmountToSend : proposedToSend; } + + // If the order total is not enough to cover account creation, the account has to be created separately (by the User or Bot) to be able to finalize this swap flow + require(totalAttributableToUser > accountActivationFee, InsufficientFundsForActivation()); + + amountToTransfer = totalAttributableToUser - accountActivationFee; } /// @notice Reads the current spot price from HyperLiquid and applies a configured suggested discount for faster execution @@ -1116,8 +1184,14 @@ contract HyperCoreFlowExecutor is AccessControlUpgradeable, AuthorizedFundedFlow IERC20(token).safeTransfer(msg.sender, amount); } - function sweepOnCore(address token, uint64 amount) external onlyRole(FUNDS_SWEEPER_ROLE) { - HyperCoreLib.transferERC20CoreToCore(_getMainStorage().coreTokenInfos[token].coreIndex, msg.sender, amount); + function sweepOnCore(address token, uint64 amount, uint32 destinationDex) external onlyRole(FUNDS_SWEEPER_ROLE) { + HyperCoreLib.transferERC20CoreToCore( + _getMainStorage().coreTokenInfos[token].coreIndex, + msg.sender, + amount, + destinationDex, + destinationDex + ); } function sweepOnCoreFromSwapHandler( @@ -1130,11 +1204,23 @@ contract HyperCoreFlowExecutor is AccessControlUpgradeable, AuthorizedFundedFlow $.lastPullFundsBlock[finalToken] = block.number; SwapHandler swapHandler = $.finalTokenInfos[finalToken].swapHandler; + + // funds in swap handler will always be in spot dex if (finalTokenAmount > 0) { - swapHandler.transferFundsToUserOnCore($.coreTokenInfos[finalToken].coreIndex, msg.sender, finalTokenAmount); + swapHandler.transferFundsToUserOnCore( + $.coreTokenInfos[finalToken].coreIndex, + msg.sender, + finalTokenAmount, + HyperCoreLib.CORE_SPOT_DEX_ID + ); } if (baseTokenAmount > 0) { - swapHandler.transferFundsToUserOnCore($.coreTokenInfos[baseToken].coreIndex, msg.sender, baseTokenAmount); + swapHandler.transferFundsToUserOnCore( + $.coreTokenInfos[baseToken].coreIndex, + msg.sender, + baseTokenAmount, + HyperCoreLib.CORE_SPOT_DEX_ID + ); } } } diff --git a/contracts/periphery/mintburn/Structs.sol b/contracts/periphery/mintburn/Structs.sol index 6b7e6ef47..9ed274fd0 100644 --- a/contracts/periphery/mintburn/Structs.sol +++ b/contracts/periphery/mintburn/Structs.sol @@ -20,6 +20,11 @@ struct CoreTokenInfo { uint64 bridgeSafetyBufferCore; } +enum AccountCreationMode { + Standard, + FromUserFunds +} + struct FinalTokenInfo { // The index of the market where we're going to swap baseToken -> finalToken uint32 spotIndex; @@ -39,6 +44,8 @@ struct CommonFlowParams { bytes32 quoteNonce; address finalRecipient; address finalToken; + uint32 destinationDex; + AccountCreationMode accountCreationMode; uint256 maxBpsToSponsor; uint256 extraFeesIncurred; } diff --git a/contracts/periphery/mintburn/SwapHandler.sol b/contracts/periphery/mintburn/SwapHandler.sol index 92da0e134..1e8e27c4b 100644 --- a/contracts/periphery/mintburn/SwapHandler.sol +++ b/contracts/periphery/mintburn/SwapHandler.sol @@ -21,30 +21,28 @@ contract SwapHandler { _; } - function activateCoreAccount( - address erc20EVMAddress, - uint64 erc20CoreIndex, - uint256 amountEVM, - int8 decimalDiff - ) external onlyParentHandler { - HyperCoreLib.transferERC20EVMToSelfOnCore(erc20EVMAddress, erc20CoreIndex, amountEVM, decimalDiff); - } - function transferFundsToSelfOnCore( address erc20EVMAddress, uint64 erc20CoreIndex, uint256 amountEVM, int8 decimalDiff ) external onlyParentHandler { - HyperCoreLib.transferERC20EVMToSelfOnCore(erc20EVMAddress, erc20CoreIndex, amountEVM, decimalDiff); + HyperCoreLib.transferERC20EVMToSelfOnSpot(erc20EVMAddress, erc20CoreIndex, amountEVM, decimalDiff); } function transferFundsToUserOnCore( uint64 erc20CoreIndex, address to, - uint64 amountCore + uint64 amountCore, + uint32 destinationDex ) external onlyParentHandler { - HyperCoreLib.transferERC20CoreToCore(erc20CoreIndex, to, amountCore); + HyperCoreLib.transferERC20CoreToCore( + erc20CoreIndex, + to, + amountCore, + HyperCoreLib.CORE_SPOT_DEX_ID, + destinationDex + ); } function submitSpotLimitOrder( diff --git a/contracts/periphery/mintburn/sponsored-cctp/SponsoredCCTPDstPeriphery.sol b/contracts/periphery/mintburn/sponsored-cctp/SponsoredCCTPDstPeriphery.sol index cbd982709..3796c436f 100644 --- a/contracts/periphery/mintburn/sponsored-cctp/SponsoredCCTPDstPeriphery.sol +++ b/contracts/periphery/mintburn/sponsored-cctp/SponsoredCCTPDstPeriphery.sol @@ -8,7 +8,7 @@ import { SponsoredCCTPInterface } from "../../../interfaces/SponsoredCCTPInterfa import { Bytes32ToAddress } from "../../../libraries/AddressConverters.sol"; import { HyperCoreFlowExecutor } from "../HyperCoreFlowExecutor.sol"; import { ArbitraryEVMFlowExecutor } from "../ArbitraryEVMFlowExecutor.sol"; -import { CommonFlowParams, EVMFlowParams } from "../Structs.sol"; +import { CommonFlowParams, EVMFlowParams, AccountCreationMode as StructsAccountCreationMode } from "../Structs.sol"; import { IERC20Metadata } from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; @@ -202,9 +202,11 @@ contract SponsoredCCTPDstPeriphery is BaseModuleHandler, SponsoredCCTPInterface, finalRecipient: quote.finalRecipient.toAddress(), // If the quote is invalid we don't want to swap, so we use the base token as the final token finalToken: isQuoteValid ? quote.finalToken.toAddress() : baseToken, + destinationDex: quote.destinationDex, // If the quote is invalid we don't sponsor the flow or the extra fees maxBpsToSponsor: isQuoteValid ? quote.maxBpsToSponsor : 0, - extraFeesIncurred: feeExecuted + extraFeesIncurred: feeExecuted, + accountCreationMode: StructsAccountCreationMode(quote.accountCreationMode) }); // Route to appropriate execution based on executionMode @@ -253,6 +255,11 @@ contract SponsoredCCTPDstPeriphery is BaseModuleHandler, SponsoredCCTPInterface, function _executeWithEVMFlow(EVMFlowParams memory params) internal { params.commonParams = ArbitraryEVMFlowExecutor._executeFlow(params); + // If the full balance was deposited as a part of EVM action, return early + if (params.commonParams.amountInEVM == 0) { + return; + } + // Route to appropriate destination based on transferToCore flag _delegateToHyperCore( params.transferToCore diff --git a/contracts/periphery/mintburn/sponsored-cctp/SponsoredCCTPSrcPeriphery.sol b/contracts/periphery/mintburn/sponsored-cctp/SponsoredCCTPSrcPeriphery.sol index 0931e0ef1..91ff5d50b 100644 --- a/contracts/periphery/mintburn/sponsored-cctp/SponsoredCCTPSrcPeriphery.sol +++ b/contracts/periphery/mintburn/sponsored-cctp/SponsoredCCTPSrcPeriphery.sol @@ -116,6 +116,8 @@ contract SponsoredCCTPSrcPeriphery is SponsoredCCTPInterface, Ownable { quote.maxBpsToSponsor, quote.maxUserSlippageBps, quote.finalToken, + quote.destinationDex, + quote.accountCreationMode, signature ); } diff --git a/contracts/periphery/mintburn/sponsored-oft/ComposeMsgCodec.sol b/contracts/periphery/mintburn/sponsored-oft/ComposeMsgCodec.sol index bb24ef646..1d8e48ef7 100644 --- a/contracts/periphery/mintburn/sponsored-oft/ComposeMsgCodec.sol +++ b/contracts/periphery/mintburn/sponsored-oft/ComposeMsgCodec.sol @@ -10,9 +10,11 @@ library ComposeMsgCodec { 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 EXECUTION_MODE_OFFSET = 192; - // Minimum length with empty actionData: 7 regular params (32 bytes each) and 1 dynamic byte array (minumum 64 bytes) - uint256 internal constant MIN_COMPOSE_MSG_BYTE_LENGTH = 288; + 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; function _encode( bytes32 nonce, @@ -21,6 +23,8 @@ library ComposeMsgCodec { uint256 maxUserSlippageBps, bytes32 finalRecipient, bytes32 finalToken, + uint32 destinationDex, + uint8 accountCreationMode, uint8 executionMode, bytes memory actionData ) internal pure returns (bytes memory) { @@ -32,6 +36,8 @@ library ComposeMsgCodec { maxUserSlippageBps, finalRecipient, finalToken, + destinationDex, + accountCreationMode, executionMode, actionData ); @@ -61,18 +67,25 @@ library ComposeMsgCodec { return BytesLib.toBytes32(data, FINAL_TOKEN_OFFSET); } + function _getDestinationDex(bytes memory data) internal pure returns (uint32 v) { + // 28 is the offset to read uint32 at the end of the 32-byte long slot + return BytesLib.toUint32(data, DESTINATION_DEX_OFFSET + 28); + } + + function _getAccountCreationMode(bytes memory data) internal pure returns (uint8 v) { + // 31 is the offset to read uint8 at the end of the 32-byte long slot + return BytesLib.toUint8(data, ACCOUNT_CREATION_MODE_OFFSET + 31); + } + function _getExecutionMode(bytes memory data) internal pure returns (uint8 v) { - (, , , , , , uint8 executionMode, ) = abi.decode( - data, - (bytes32, uint256, uint256, uint256, bytes32, bytes32, uint8, bytes) - ); - return executionMode; + // 31 is the offset to read uint8 at the end of the 32-byte long slot + return BytesLib.toUint8(data, EXECUTION_MODE_OFFSET + 31); } 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, uint8, bytes) + (bytes32, 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 add659848..c80ec6eeb 100644 --- a/contracts/periphery/mintburn/sponsored-oft/DstOFTHandler.sol +++ b/contracts/periphery/mintburn/sponsored-oft/DstOFTHandler.sol @@ -10,7 +10,7 @@ import { AddressToBytes32, Bytes32ToAddress } from "../../../libraries/AddressCo import { IOFT, IOAppCore } from "../../../interfaces/IOFT.sol"; import { HyperCoreFlowExecutor } from "../HyperCoreFlowExecutor.sol"; import { ArbitraryEVMFlowExecutor } from "../ArbitraryEVMFlowExecutor.sol"; -import { CommonFlowParams, EVMFlowParams } from "../Structs.sol"; +import { CommonFlowParams, EVMFlowParams, AccountCreationMode } from "../Structs.sol"; import { IERC20 } from "@openzeppelin/contracts-v4/token/ERC20/IERC20.sol"; import { SafeERC20 } from "@openzeppelin/contracts-v4/token/ERC20/utils/SafeERC20.sol"; @@ -166,6 +166,8 @@ contract DstOFTHandler is BaseModuleHandler, ILayerZeroComposer, ArbitraryEVMFlo uint256 maxUserSlippageBps = composeMsg._getMaxUserSlippageBps(); address finalRecipient = composeMsg._getFinalRecipient().toAddress(); address finalToken = composeMsg._getFinalToken().toAddress(); + uint32 destinationDex = composeMsg._getDestinationDex(); + uint8 accountCreationMode = composeMsg._getAccountCreationMode(); uint8 executionMode = composeMsg._getExecutionMode(); bytes memory actionData = composeMsg._getActionData(); @@ -174,8 +176,10 @@ contract DstOFTHandler is BaseModuleHandler, ILayerZeroComposer, ArbitraryEVMFlo quoteNonce: quoteNonce, finalRecipient: finalRecipient, finalToken: finalToken, + destinationDex: destinationDex, maxBpsToSponsor: maxBpsToSponsor, - extraFeesIncurred: EXTRA_FEES_TO_SPONSOR + extraFeesIncurred: EXTRA_FEES_TO_SPONSOR, + accountCreationMode: AccountCreationMode(accountCreationMode) }); // Route to appropriate execution based on executionMode @@ -201,6 +205,11 @@ contract DstOFTHandler is BaseModuleHandler, ILayerZeroComposer, ArbitraryEVMFlo function _executeWithEVMFlow(EVMFlowParams memory params) internal { params.commonParams = ArbitraryEVMFlowExecutor._executeFlow(params); + // If the full balance was deposited as a part of EVM action, return early + if (params.commonParams.amountInEVM == 0) { + return; + } + // Route to appropriate destination based on transferToCore flag _delegateToHyperCore( params.transferToCore diff --git a/contracts/periphery/mintburn/sponsored-oft/QuoteSignLib.sol b/contracts/periphery/mintburn/sponsored-oft/QuoteSignLib.sol index 135e8e4a6..e1c2dbe02 100644 --- a/contracts/periphery/mintburn/sponsored-oft/QuoteSignLib.sol +++ b/contracts/periphery/mintburn/sponsored-oft/QuoteSignLib.sol @@ -11,24 +11,33 @@ library QuoteSignLib { /// @notice Compute the keccak of all `SignedQuoteParams` fields function hash(SignedQuoteParams calldata p) internal pure returns (bytes32) { - return - keccak256( - abi.encode( - p.srcEid, - p.dstEid, - p.destinationHandler, - p.amountLD, - p.nonce, - p.deadline, - p.maxBpsToSponsor, - p.finalRecipient, - p.finalToken, - p.lzReceiveGasLimit, - p.lzComposeGasLimit, - p.executionMode, - keccak256(p.actionData) // Hash the actionData to keep signature size reasonable - ) - ); + // We split the hashing into two parts to avoid "stack too deep" error + bytes32 hash1 = keccak256( + abi.encode( + p.srcEid, + p.dstEid, + p.destinationHandler, + p.amountLD, + p.nonce, + p.deadline, + p.maxBpsToSponsor, + p.finalRecipient + ) + ); + + bytes32 hash2 = keccak256( + abi.encode( + p.finalToken, + p.destinationDex, + p.lzReceiveGasLimit, + p.lzComposeGasLimit, + p.accountCreationMode, + p.executionMode, + keccak256(p.actionData) // Hash the actionData to keep signature size reasonable + ) + ); + + return keccak256(abi.encode(hash1, hash2)); } /// @notice Recover the signer for the given params and signature. diff --git a/contracts/periphery/mintburn/sponsored-oft/SponsoredOFTSrcPeriphery.sol b/contracts/periphery/mintburn/sponsored-oft/SponsoredOFTSrcPeriphery.sol index 4177bdb6f..111994096 100644 --- a/contracts/periphery/mintburn/sponsored-oft/SponsoredOFTSrcPeriphery.sol +++ b/contracts/periphery/mintburn/sponsored-oft/SponsoredOFTSrcPeriphery.sol @@ -168,6 +168,8 @@ contract SponsoredOFTSrcPeriphery is Ownable { quote.unsignedParams.maxUserSlippageBps, quote.signedParams.finalRecipient, quote.signedParams.finalToken, + quote.signedParams.destinationDex, + quote.signedParams.accountCreationMode, quote.signedParams.executionMode, quote.signedParams.actionData ); diff --git a/contracts/periphery/mintburn/sponsored-oft/Structs.sol b/contracts/periphery/mintburn/sponsored-oft/Structs.sol index 6085ec4c8..ee88d7ffc 100644 --- a/contracts/periphery/mintburn/sponsored-oft/Structs.sol +++ b/contracts/periphery/mintburn/sponsored-oft/Structs.sol @@ -30,10 +30,12 @@ struct SignedQuoteParams { uint256 maxBpsToSponsor; // max bps (of sent amount) to sponsor for 1:1 bytes32 finalRecipient; // user address on destination bytes32 finalToken; // final token user will receive (might be different from OFT token we're sending) + uint32 destinationDex; // destination DEX on HyperCore // Signed gas limits for destination-side LZ execution uint256 lzReceiveGasLimit; // gas limit for `lzReceive` call on destination side uint256 lzComposeGasLimit; // gas limit for `lzCompose` call on destination side // Execution mode and action data + 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/script/DeployHyperliquidDepositHandler.s.sol b/script/DeployHyperliquidDepositHandler.s.sol index 2c0afa490..12860904f 100644 --- a/script/DeployHyperliquidDepositHandler.s.sol +++ b/script/DeployHyperliquidDepositHandler.s.sol @@ -34,7 +34,13 @@ contract DeployHyperliquidDepositHandler is Script, Test { HyperliquidDepositHandler hyperliquidDepositHandler = new HyperliquidDepositHandler(signer, spokePool); // Activate Handler account so it can write to CoreWriter by sending 1 core wei. - HyperCoreLib.transferERC20CoreToCore(usdhTokenIndex, address(hyperliquidDepositHandler), 1); + HyperCoreLib.transferERC20CoreToCore( + usdhTokenIndex, + address(hyperliquidDepositHandler), + 1, + HyperCoreLib.CORE_SPOT_DEX_ID, + HyperCoreLib.CORE_SPOT_DEX_ID + ); hyperliquidDepositHandler.addSupportedToken(address(usdh), usdhTokenIndex, 1000000, usdhDecimalDiff); // Log the deployed addresses diff --git a/script/mintburn/cctp/createSponsoredDeposit.sol b/script/mintburn/cctp/createSponsoredDeposit.sol index 7ce4ac4cc..ecc6131fe 100644 --- a/script/mintburn/cctp/createSponsoredDeposit.sol +++ b/script/mintburn/cctp/createSponsoredDeposit.sol @@ -7,7 +7,9 @@ import { DeploymentUtils } from "../../utils/DeploymentUtils.sol"; import { SponsoredCCTPSrcPeriphery } from "../../../contracts/periphery/mintburn/sponsored-cctp/SponsoredCCTPSrcPeriphery.sol"; import { ArbitraryEVMFlowExecutor } from "../../../contracts/periphery/mintburn/ArbitraryEVMFlowExecutor.sol"; import { SponsoredCCTPInterface } from "../../../contracts/interfaces/SponsoredCCTPInterface.sol"; +import { AccountCreationMode } from "../../../contracts/periphery/mintburn/Structs.sol"; import { AddressToBytes32 } from "../../../contracts/libraries/AddressConverters.sol"; +import { HyperCoreLib } from "../../../contracts/libraries/HyperCoreLib.sol"; interface IHyperSwapRouter { struct ExactInputSingleParams { @@ -97,6 +99,8 @@ contract CreateSponsoredDeposit is DeploymentUtils { maxUserSlippageBps: 400, // 4% max user slippage (400 basis points) finalRecipient: address(0x9A8f92a830A5cB89a3816e3D267CB7791c16b04D).toBytes32(), // Final recipient finalToken: address(0xb88339CB7199b77E23DB6E890353E22632Ba630f).toBytes32(), // USDC on HyperEVM + destinationDex: HyperCoreLib.CORE_SPOT_DEX_ID, // Spot DEX on HyperCore + accountCreationMode: uint8(AccountCreationMode.Standard), // Standard mode executionMode: uint8(SponsoredCCTPInterface.ExecutionMode.DirectToCore), // DirectToCore mode actionData: emptyActionData // Empty for DirectToCore mode }); @@ -132,6 +136,8 @@ contract CreateSponsoredDeposit is DeploymentUtils { quote.maxUserSlippageBps, quote.finalRecipient, quote.finalToken, + quote.destinationDex, + quote.accountCreationMode, quote.executionMode, keccak256(quote.actionData) ) diff --git a/script/mintburn/oft/CreateSponsoredDeposit.s.sol b/script/mintburn/oft/CreateSponsoredDeposit.s.sol index 73c848e96..cd4b337bf 100644 --- a/script/mintburn/oft/CreateSponsoredDeposit.s.sol +++ b/script/mintburn/oft/CreateSponsoredDeposit.s.sol @@ -10,6 +10,7 @@ import { AddressToBytes32 } from "../../../contracts/libraries/AddressConverters import { ComposeMsgCodec } from "../../../contracts/periphery/mintburn/sponsored-oft/ComposeMsgCodec.sol"; import { MinimalLZOptions } from "../../../contracts/external/libraries/MinimalLZOptions.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 { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; @@ -18,24 +19,32 @@ import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.s library DebugQuoteSignLib { /// @notice Compute the keccak of all `SignedQuoteParams` fields. Accept memory arg function hashMemory(SignedQuoteParams memory p) internal pure returns (bytes32) { - return - keccak256( - abi.encode( - p.srcEid, - p.dstEid, - p.destinationHandler, - p.amountLD, - p.nonce, - p.deadline, - p.maxBpsToSponsor, - p.finalRecipient, - p.finalToken, - p.lzReceiveGasLimit, - p.lzComposeGasLimit, - p.executionMode, - keccak256(p.actionData) // Hash the actionData to keep signature size reasonable - ) - ); + bytes32 hash1 = keccak256( + abi.encode( + p.srcEid, + p.dstEid, + p.destinationHandler, + p.amountLD, + p.nonce, + p.deadline, + p.maxBpsToSponsor, + p.finalRecipient + ) + ); + + bytes32 hash2 = keccak256( + abi.encode( + p.finalToken, + p.destinationDex, + p.lzReceiveGasLimit, + p.lzComposeGasLimit, + p.accountCreationMode, + p.executionMode, + keccak256(p.actionData) // Hash the actionData to keep signature size reasonable + ) + ); + + return keccak256(abi.encode(hash1, hash2)); } /// @notice Sign the quote using Foundry's Vm cheatcode and return concatenated bytes signature (r,s,v). @@ -196,8 +205,10 @@ contract CreateSponsoredDeposit is Script, Config { maxBpsToSponsor: maxBpsToSponsor, finalRecipient: finalRecipient.toBytes32(), finalToken: finalToken.toBytes32(), + destinationDex: HyperCoreLib.CORE_SPOT_DEX_ID, lzReceiveGasLimit: lzReceiveGasLimit, lzComposeGasLimit: lzComposeGasLimit, + accountCreationMode: 0, executionMode: 0, actionData: "" }); @@ -265,6 +276,8 @@ contract CreateSponsoredDeposit is Script, Config { quote.unsignedParams.maxUserSlippageBps, quote.signedParams.finalRecipient, quote.signedParams.finalToken, + quote.signedParams.destinationDex, + quote.signedParams.accountCreationMode, quote.signedParams.executionMode, quote.signedParams.actionData ); diff --git a/test/evm/foundry/local/ComposeMsgCodec.t.sol b/test/evm/foundry/local/ComposeMsgCodec.t.sol new file mode 100644 index 000000000..ede2a869e --- /dev/null +++ b/test/evm/foundry/local/ComposeMsgCodec.t.sol @@ -0,0 +1,94 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.23; + +import { Test } from "forge-std/Test.sol"; +import { ComposeMsgCodec } from "contracts/periphery/mintburn/sponsored-oft/ComposeMsgCodec.sol"; + +contract ComposeMsgCodecTest is Test { + function test_EncodeDecode() public { + bytes32 nonce = keccak256("nonce"); + uint256 deadline = 1234567890; + uint256 maxBpsToSponsor = 500; + uint256 maxUserSlippageBps = 100; + bytes32 finalRecipient = keccak256("recipient"); + bytes32 finalToken = keccak256("token"); + uint32 destinationDex = 17; + uint8 accountCreationMode = 5; + uint8 executionMode = 7; + bytes memory actionData = hex"deadbeef"; + + bytes memory encoded = ComposeMsgCodec._encode( + nonce, + deadline, + maxBpsToSponsor, + maxUserSlippageBps, + finalRecipient, + finalToken, + destinationDex, + accountCreationMode, + executionMode, + actionData + ); + + assertEq(ComposeMsgCodec._getNonce(encoded), nonce, "Nonce mismatch"); + assertEq(ComposeMsgCodec._getDeadline(encoded), deadline, "Deadline mismatch"); + assertEq(ComposeMsgCodec._getMaxBpsToSponsor(encoded), maxBpsToSponsor, "MaxBpsToSponsor mismatch"); + assertEq(ComposeMsgCodec._getMaxUserSlippageBps(encoded), maxUserSlippageBps, "MaxUserSlippageBps mismatch"); + assertEq(ComposeMsgCodec._getFinalRecipient(encoded), finalRecipient, "FinalRecipient mismatch"); + assertEq(ComposeMsgCodec._getFinalToken(encoded), finalToken, "FinalToken mismatch"); + assertEq(ComposeMsgCodec._getDestinationDex(encoded), destinationDex, "DestinationDex mismatch"); + assertEq(ComposeMsgCodec._getAccountCreationMode(encoded), accountCreationMode, "AccountCreationMode mismatch"); + assertEq(ComposeMsgCodec._getExecutionMode(encoded), executionMode, "ExecutionMode mismatch"); + assertEq(ComposeMsgCodec._getActionData(encoded), actionData, "ActionData mismatch"); + assertTrue(ComposeMsgCodec._isValidComposeMsgBytelength(encoded), "Invalid length"); + } + + function testFuzz_EncodeDecode( + bytes32 nonce, + uint256 deadline, + uint256 maxBpsToSponsor, + uint256 maxUserSlippageBps, + bytes32 finalRecipient, + bytes32 finalToken, + uint32 destinationDex, + uint8 accountCreationMode, + uint8 executionMode, + bytes memory actionData + ) public { + bytes memory encoded = ComposeMsgCodec._encode( + nonce, + deadline, + maxBpsToSponsor, + maxUserSlippageBps, + finalRecipient, + finalToken, + destinationDex, + accountCreationMode, + executionMode, + actionData + ); + + assertEq(ComposeMsgCodec._getNonce(encoded), nonce, "Nonce mismatch"); + assertEq(ComposeMsgCodec._getDeadline(encoded), deadline, "Deadline mismatch"); + assertEq(ComposeMsgCodec._getMaxBpsToSponsor(encoded), maxBpsToSponsor, "MaxBpsToSponsor mismatch"); + assertEq(ComposeMsgCodec._getMaxUserSlippageBps(encoded), maxUserSlippageBps, "MaxUserSlippageBps mismatch"); + assertEq(ComposeMsgCodec._getFinalRecipient(encoded), finalRecipient, "FinalRecipient mismatch"); + assertEq(ComposeMsgCodec._getFinalToken(encoded), finalToken, "FinalToken mismatch"); + assertEq(ComposeMsgCodec._getDestinationDex(encoded), destinationDex, "DestinationDex mismatch"); + assertEq(ComposeMsgCodec._getAccountCreationMode(encoded), accountCreationMode, "AccountCreationMode mismatch"); + assertEq(ComposeMsgCodec._getExecutionMode(encoded), executionMode, "ExecutionMode mismatch"); + assertEq(ComposeMsgCodec._getActionData(encoded), actionData, "ActionData mismatch"); + assertTrue(ComposeMsgCodec._isValidComposeMsgBytelength(encoded), "Invalid length"); + } + + 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 + + bytes memory data = new bytes(352); + assertTrue(ComposeMsgCodec._isValidComposeMsgBytelength(data)); + + bytes memory tooShort = new bytes(351); + assertFalse(ComposeMsgCodec._isValidComposeMsgBytelength(tooShort)); + } +} diff --git a/test/evm/foundry/local/HyperCoreFlowExecutor.t.sol b/test/evm/foundry/local/HyperCoreFlowExecutor.t.sol index 2cf61d9c2..6f37f2305 100644 --- a/test/evm/foundry/local/HyperCoreFlowExecutor.t.sol +++ b/test/evm/foundry/local/HyperCoreFlowExecutor.t.sol @@ -11,7 +11,7 @@ import { HyperCoreLib } from "../../../../contracts/libraries/HyperCoreLib.sol"; import { DonationBox } from "../../../../contracts/chain-adapters/DonationBox.sol"; import { MockERC20 } from "../../../../contracts/test/MockERC20.sol"; import { IHyperCoreFlowExecutor } from "../../../../contracts/test/interfaces/IHyperCoreFlowExecutor.sol"; -import { CommonFlowParams } from "../../../../contracts/periphery/mintburn/Structs.sol"; +import { CommonFlowParams, AccountCreationMode } from "../../../../contracts/periphery/mintburn/Structs.sol"; import { HyperCoreFlowExecutor } from "../../../../contracts/periphery/mintburn/HyperCoreFlowExecutor.sol"; import { BaseModuleHandler } from "../../../../contracts/periphery/mintburn/BaseModuleHandler.sol"; @@ -92,8 +92,10 @@ contract HyperCoreFlowExecutorTest is BaseSimulatorTest { quoteNonce: QUOTE_NONCE, finalRecipient: finalRecipient, finalToken: address(token), + destinationDex: HyperCoreLib.CORE_SPOT_DEX_ID, maxBpsToSponsor: maxBpsToSponsor, - extraFeesIncurred: extraFeesIncurred + extraFeesIncurred: extraFeesIncurred, + accountCreationMode: AccountCreationMode.Standard }); } @@ -143,8 +145,10 @@ contract HyperCoreFlowExecutorTest is BaseSimulatorTest { quoteNonce: keccak256("quote-2"), finalRecipient: unactivated, finalToken: address(token), + destinationDex: HyperCoreLib.CORE_SPOT_DEX_ID, maxBpsToSponsor: maxBps, - extraFeesIncurred: extraFees + extraFeesIncurred: extraFees, + accountCreationMode: AccountCreationMode.Standard }); uint256 balBefore = IERC20(address(token)).balanceOf(unactivated); diff --git a/test/evm/foundry/local/SponsoredOFTSrcPeriphery.t.sol b/test/evm/foundry/local/SponsoredOFTSrcPeriphery.t.sol index 39c2c0465..f47600628 100644 --- a/test/evm/foundry/local/SponsoredOFTSrcPeriphery.t.sol +++ b/test/evm/foundry/local/SponsoredOFTSrcPeriphery.t.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.23; import { Test } from "forge-std/Test.sol"; - +import { console } from "forge-std/console.sol"; import { SponsoredOFTSrcPeriphery } from "../../../../contracts/periphery/mintburn/sponsored-oft/SponsoredOFTSrcPeriphery.sol"; import { Quote, SignedQuoteParams, UnsignedQuoteParams } from "../../../../contracts/periphery/mintburn/sponsored-oft/Structs.sol"; import { AddressToBytes32 } from "../../../../contracts/libraries/AddressConverters.sol"; @@ -10,7 +10,7 @@ import { AddressToBytes32 } from "../../../../contracts/libraries/AddressConvert import { MockERC20 } from "../../../../contracts/test/MockERC20.sol"; import { MockOFTMessenger } from "../../../../contracts/test/MockOFTMessenger.sol"; import { MockEndpoint } from "../../../../contracts/test/MockEndpoint.sol"; - +import { HyperCoreLib } from "../../../../contracts/libraries/HyperCoreLib.sol"; import { IERC20 } from "@openzeppelin/contracts-v4/token/ERC20/IERC20.sol"; import { DebugQuoteSignLib } from "../../../../script/mintburn/oft/CreateSponsoredDeposit.s.sol"; @@ -77,8 +77,10 @@ contract SponsoredOFTSrcPeripheryTest is Test { maxBpsToSponsor: 500, // 5% finalRecipient: finalRecipientAddr.toBytes32(), finalToken: finalTokenAddr.toBytes32(), + destinationDex: HyperCoreLib.CORE_SPOT_DEX_ID, lzReceiveGasLimit: 500_000, lzComposeGasLimit: 500_000, + accountCreationMode: uint8(0), // Standard executionMode: uint8(0), // DirectToCore actionData: "" }); @@ -156,9 +158,14 @@ contract SponsoredOFTSrcPeripheryTest is Test { uint256 gotMaxUserSlippageBps, bytes32 gotFinalRecipient, bytes32 gotFinalToken, + uint32 gotDestinationDex, + uint8 gotAccountCreationMode, uint8 gotExecutionMode, bytes memory gotActionData - ) = abi.decode(spComposeMsg, (bytes32, uint256, uint256, uint256, bytes32, bytes32, uint8, bytes)); + ) = abi.decode( + spComposeMsg, + (bytes32, uint256, uint256, uint256, bytes32, bytes32, uint32, uint8, uint8, bytes) + ); assertEq(gotNonce, nonce, "nonce mismatch"); assertEq(gotDeadline, deadline, "deadline mismatch"); @@ -166,6 +173,8 @@ contract SponsoredOFTSrcPeripheryTest is Test { assertEq(gotMaxUserSlippageBps, 300, "maxUserSlippageBps mismatch"); assertEq(gotFinalRecipient, finalRecipientAddr.toBytes32(), "finalRecipient mismatch"); assertEq(gotFinalToken, finalTokenAddr.toBytes32(), "finalToken mismatch"); + assertEq(gotDestinationDex, HyperCoreLib.CORE_SPOT_DEX_ID, "destinationDex mismatch"); + assertEq(gotAccountCreationMode, 0, "accountCreationMode mismatch"); assertEq(gotExecutionMode, 0, "executionMode mismatch"); assertEq(keccak256(gotActionData), keccak256(""), "actionData mismatch"); diff --git a/test/evm/foundry/local/SponsorredCCTPDstPeriphery.t.sol b/test/evm/foundry/local/SponsorredCCTPDstPeriphery.t.sol index 34042f60a..136fc8793 100644 --- a/test/evm/foundry/local/SponsorredCCTPDstPeriphery.t.sol +++ b/test/evm/foundry/local/SponsorredCCTPDstPeriphery.t.sol @@ -2,8 +2,10 @@ pragma solidity ^0.8.0; import { Test, console } from "forge-std/Test.sol"; +import { AccountCreationMode } from "../../../../contracts/periphery/mintburn/Structs.sol"; import { SponsoredCCTPDstPeriphery } from "../../../../contracts/periphery/mintburn/sponsored-cctp/SponsoredCCTPDstPeriphery.sol"; import { IHyperCoreFlowExecutor } from "../../../../contracts/test/interfaces/IHyperCoreFlowExecutor.sol"; +import { HyperCoreLib } from "../../../../contracts/libraries/HyperCoreLib.sol"; import { SponsoredCCTPInterface } from "../../../../contracts/interfaces/SponsoredCCTPInterface.sol"; import { IMessageTransmitterV2 } from "../../../../contracts/external/interfaces/CCTPInterfaces.sol"; import { AddressToBytes32, Bytes32ToAddress } from "../../../../contracts/libraries/AddressConverters.sol"; @@ -142,6 +144,8 @@ contract SponsoredCCTPDstPeripheryTest is BaseSimulatorTest { quote.maxUserSlippageBps, quote.finalRecipient, quote.finalToken, + quote.destinationDex, + quote.accountCreationMode, quote.executionMode, quote.actionData ); @@ -201,6 +205,8 @@ contract SponsoredCCTPDstPeripheryTest is BaseSimulatorTest { quote.maxUserSlippageBps, quote.finalRecipient, quote.finalToken, + quote.destinationDex, + quote.accountCreationMode, quote.executionMode, keccak256(quote.actionData) ) @@ -230,6 +236,8 @@ contract SponsoredCCTPDstPeripheryTest is BaseSimulatorTest { maxUserSlippageBps: 50, // 0.5% finalRecipient: finalRecipient.toBytes32(), finalToken: address(usdc).toBytes32(), + destinationDex: HyperCoreLib.CORE_SPOT_DEX_ID, + accountCreationMode: uint8(AccountCreationMode.Standard), executionMode: uint8(SponsoredCCTPInterface.ExecutionMode.DirectToCore), actionData: bytes("") });