From e2dbeb2e4ac597bcf6393a5fcb37b511446a56d2 Mon Sep 17 00:00:00 2001 From: Faisal Usmani Date: Thu, 11 Dec 2025 21:20:14 -0500 Subject: [PATCH 01/21] feat: Perp destination dex support Signed-off-by: Faisal Usmani --- .../handlers/HyperliquidDepositHandler.sol | 21 +++++-- .../interfaces/SponsoredCCTPInterface.sol | 2 + contracts/libraries/HyperCoreLib.sol | 47 +++++++++++---- contracts/libraries/SponsoredCCTPQuoteLib.sol | 11 ++-- .../mintburn/HyperCoreFlowExecutor.sol | 59 +++++++++++++++---- contracts/periphery/mintburn/Structs.sol | 1 + contracts/periphery/mintburn/SwapHandler.sol | 25 ++++---- .../SponsoredCCTPDstPeriphery.sol | 1 + .../sponsored-oft/ComposeMsgCodec.sol | 19 ++++-- .../mintburn/sponsored-oft/DstOFTHandler.sol | 2 + .../SponsoredOFTSrcPeriphery.sol | 1 + .../mintburn/sponsored-oft/Structs.sol | 1 + script/DeployHyperliquidDepositHandler.s.sol | 7 ++- .../mintburn/cctp/createSponsoredDeposit.sol | 2 + .../mintburn/oft/CreateSponsoredDeposit.s.sol | 3 + .../foundry/local/HyperCoreFlowExecutor.t.sol | 2 + .../local/SponsoredOFTSrcPeriphery.t.sol | 3 +- .../local/SponsorredCCTPDstPeriphery.t.sol | 2 + 18 files changed, 158 insertions(+), 51 deletions(-) diff --git a/contracts/handlers/HyperliquidDepositHandler.sol b/contracts/handlers/HyperliquidDepositHandler.sol index a7ece2200..c443ebb18 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.transferERC20CoreToCore(tokenIndex, user, coreAmount, HyperCoreLib.CORE_SPOT_DEX_ID); } /** @@ -225,12 +225,25 @@ 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.transferERC20EVMToSelfOnCore( + token, + tokenIndex, + amountRequiredToActivate, + decimalDiff, + HyperCoreLib.CORE_SPOT_DEX_ID + ); + HyperCoreLib.transferERC20CoreToCore(tokenIndex, user, 1, HyperCoreLib.CORE_SPOT_DEX_ID); emit UserAccountActivated(user, token, amountRequiredToActivate); } - HyperCoreLib.transferERC20EVMToCore(token, tokenIndex, user, evmAmount, decimalDiff); + HyperCoreLib.transferERC20EVMToCore( + token, + tokenIndex, + user, + evmAmount, + decimalDiff, + HyperCoreLib.CORE_SPOT_DEX_ID + ); } 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..c2bd094d3 100644 --- a/contracts/interfaces/SponsoredCCTPInterface.sol +++ b/contracts/interfaces/SponsoredCCTPInterface.sol @@ -78,6 +78,8 @@ 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; // 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..7dbc7ff6d 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,7 @@ 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 * @return amountEVMSent The amount sent on HyperEVM * @return amountCoreToReceive The amount credited on Core in Core units (post conversion) */ @@ -89,15 +91,16 @@ library HyperCoreLib { uint64 erc20CoreIndex, address to, uint256 amountEVM, - int8 decimalDiff + int8 decimalDiff, + uint32 destinationDex ) 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); + transferToCore(erc20EVMAddress, erc20CoreIndex, _amountEVMToSend, destinationDex); // Transfer the tokens from this contract on HyperCore to the `to` address on HyperCore - transferERC20CoreToCore(erc20CoreIndex, to, _amountCoreToReceive); + transferERC20CoreToCore(erc20CoreIndex, to, _amountCoreToReceive, destinationDex); } return (_amountEVMToSend, _amountCoreToReceive); @@ -111,6 +114,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,12 +122,13 @@ 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); + transferToCore(erc20EVMAddress, erc20CoreIndex, _amountEVMToSend, destinationDex); } return (_amountEVMToSend, _amountCoreToReceive); @@ -134,10 +139,24 @@ 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 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 destinationDex + ) internal { + bytes memory action; + bytes memory payload; + + if (erc20CoreIndex == USDC_CORE_INDEX) { + action = abi.encode(to, address(0), destinationDex, 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); } @@ -147,12 +166,18 @@ library HyperCoreLib { * @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 + * @param destinationDex The destination DEX on HyperCore */ - function transferToCore(address erc20EVMAddress, uint64 erc20CoreIndex, uint256 amountEVMToSend) internal { + function transferToCore( + address erc20EVMAddress, + uint64 erc20CoreIndex, + uint256 amountEVMToSend, + uint32 destinationDex + ) internal { // USDC requires a special transfer to core 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); + ICoreDepositWallet(USDC_CORE_DEPOSIT_WALLET_ADDRESS).deposit(amountEVMToSend, destinationDex); } else { // For all other tokens, transfer to the asset bridge address on HyperCore IERC20(erc20EVMAddress).safeTransfer(toAssetBridgeAddress(erc20CoreIndex), amountEVMToSend); diff --git a/contracts/libraries/SponsoredCCTPQuoteLib.sol b/contracts/libraries/SponsoredCCTPQuoteLib.sol index 199f93761..847b2e57e 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 = 668; /** * @notice Gets the data for the deposit for burn. @@ -82,6 +82,7 @@ library SponsoredCCTPQuoteLib { quote.maxUserSlippageBps, quote.finalRecipient, quote.finalToken, + quote.destinationDex, quote.executionMode, quote.actionData ); @@ -109,9 +110,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, bytes) ); return finalRecipient.isValidAddress() && finalToken.isValidAddress(); @@ -147,9 +148,10 @@ library SponsoredCCTPQuoteLib { quote.maxUserSlippageBps, quote.finalRecipient, quote.finalToken, + quote.destinationDex, 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, bytes)); } /** @@ -186,6 +188,7 @@ library SponsoredCCTPQuoteLib { quote.maxUserSlippageBps, quote.finalRecipient, quote.finalToken, + quote.destinationDex, quote.executionMode, keccak256(quote.actionData) // Hash the actionData to keep signature size reasonable ) diff --git a/contracts/periphery/mintburn/HyperCoreFlowExecutor.sol b/contracts/periphery/mintburn/HyperCoreFlowExecutor.sol index 1b6457836..10da9d44a 100644 --- a/contracts/periphery/mintburn/HyperCoreFlowExecutor.sol +++ b/contracts/periphery/mintburn/HyperCoreFlowExecutor.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; @@ -475,6 +476,14 @@ contract HyperCoreFlowExecutor is AccessControlUpgradeable, AuthorizedFundedFlow * checked by a handler. Params authorization by a handler is enforced via `onlyAuthorizedFlow` modifier */ function executeFlow(CommonFlowParams memory params, uint256 maxUserSlippageBps) external onlyAuthorizedFlow { + MainStorage storage $ = _getMainStorage(); + CoreTokenInfo memory coreTokenInfo = $.coreTokenInfos[params.finalToken]; + + // If the final token is not USDC, we need to set the destination dex to the spot dex + if (coreTokenInfo.coreIndex != HyperCoreLib.USDC_CORE_INDEX) { + params.destinationDex = HyperCoreLib.CORE_SPOT_DEX_ID; + } + if (params.finalToken == baseToken) { _executeSimpleTransferFlow(params); } else { @@ -568,7 +577,8 @@ contract HyperCoreFlowExecutor is AccessControlUpgradeable, AuthorizedFundedFlow coreTokenInfo.coreIndex, params.finalRecipient, quotedEvmAmount, - coreTokenInfo.tokenInfo.evmExtraWeiDecimals + coreTokenInfo.tokenInfo.evmExtraWeiDecimals, + params.destinationDex ); emit SimpleTransferFlowCompleted( @@ -687,6 +697,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, @@ -711,7 +722,8 @@ contract HyperCoreFlowExecutor is AccessControlUpgradeable, AuthorizedFundedFlow initialToken, initialCoreTokenInfo.coreIndex, tokensToSendEvm, - initialCoreTokenInfo.tokenInfo.evmExtraWeiDecimals + initialCoreTokenInfo.tokenInfo.evmExtraWeiDecimals, + HyperCoreLib.CORE_SPOT_DEX_ID ); } @@ -794,7 +806,8 @@ contract HyperCoreFlowExecutor is AccessControlUpgradeable, AuthorizedFundedFlow finalToken, finalCoreTokenInfo.coreIndex, totalAdditionalToSendEVM, - finalCoreTokenInfo.tokenInfo.evmExtraWeiDecimals + finalCoreTokenInfo.tokenInfo.evmExtraWeiDecimals, + HyperCoreLib.CORE_SPOT_DEX_ID ); } } @@ -836,7 +849,12 @@ contract HyperCoreFlowExecutor is AccessControlUpgradeable, AuthorizedFundedFlow finalCoreTokenInfo.tokenInfo.evmExtraWeiDecimals ); - swapHandler.transferFundsToUserOnCore(finalCoreTokenInfo.coreIndex, swap.finalRecipient, totalToSend); + swapHandler.transferFundsToUserOnCore( + finalCoreTokenInfo.coreIndex, + swap.finalRecipient, + totalToSend, + swap.destinationDex + ); emit SwapFlowFinalized(quoteNonce, swap.finalRecipient, swap.finalToken, totalToSend, additionalToSendEVM); } @@ -905,12 +923,13 @@ contract HyperCoreFlowExecutor is AccessControlUpgradeable, AuthorizedFundedFlow fundingToken, coreTokenInfo.coreIndex, evmAmountToSend, - coreTokenInfo.tokenInfo.evmExtraWeiDecimals + coreTokenInfo.tokenInfo.evmExtraWeiDecimals, + HyperCoreLib.CORE_SPOT_DEX_ID ); // 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.transferERC20CoreToCore(coreTokenInfo.coreIndex, finalRecipient, 1, HyperCoreLib.CORE_SPOT_DEX_ID); emit SponsoredAccountActivation(quoteNonce, finalRecipient, fundingToken, evmAmountToSend); } @@ -1004,7 +1023,8 @@ contract HyperCoreFlowExecutor is AccessControlUpgradeable, AuthorizedFundedFlow token, coreTokenInfo.coreIndex, amountEVMToSend, - coreTokenInfo.tokenInfo.evmExtraWeiDecimals + coreTokenInfo.tokenInfo.evmExtraWeiDecimals, + HyperCoreLib.CORE_SPOT_DEX_ID ); } @@ -1116,8 +1136,13 @@ 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 + ); } function sweepOnCoreFromSwapHandler( @@ -1130,11 +1155,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..62ec7686a 100644 --- a/contracts/periphery/mintburn/Structs.sol +++ b/contracts/periphery/mintburn/Structs.sol @@ -39,6 +39,7 @@ struct CommonFlowParams { bytes32 quoteNonce; address finalRecipient; address finalToken; + uint32 destinationDex; uint256 maxBpsToSponsor; uint256 extraFeesIncurred; } diff --git a/contracts/periphery/mintburn/SwapHandler.sol b/contracts/periphery/mintburn/SwapHandler.sol index 92da0e134..ce9704ea8 100644 --- a/contracts/periphery/mintburn/SwapHandler.sol +++ b/contracts/periphery/mintburn/SwapHandler.sol @@ -21,30 +21,29 @@ 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 + int8 decimalDiff, + uint32 destinationDex ) external onlyParentHandler { - HyperCoreLib.transferERC20EVMToSelfOnCore(erc20EVMAddress, erc20CoreIndex, amountEVM, decimalDiff); + HyperCoreLib.transferERC20EVMToSelfOnCore( + erc20EVMAddress, + erc20CoreIndex, + amountEVM, + decimalDiff, + destinationDex + ); } function transferFundsToUserOnCore( uint64 erc20CoreIndex, address to, - uint64 amountCore + uint64 amountCore, + uint32 destinationDex ) external onlyParentHandler { - HyperCoreLib.transferERC20CoreToCore(erc20CoreIndex, to, amountCore); + HyperCoreLib.transferERC20CoreToCore(erc20CoreIndex, to, amountCore, destinationDex); } function submitSpotLimitOrder( diff --git a/contracts/periphery/mintburn/sponsored-cctp/SponsoredCCTPDstPeriphery.sol b/contracts/periphery/mintburn/sponsored-cctp/SponsoredCCTPDstPeriphery.sol index cbd982709..31396d1f0 100644 --- a/contracts/periphery/mintburn/sponsored-cctp/SponsoredCCTPDstPeriphery.sol +++ b/contracts/periphery/mintburn/sponsored-cctp/SponsoredCCTPDstPeriphery.sol @@ -202,6 +202,7 @@ 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 diff --git a/contracts/periphery/mintburn/sponsored-oft/ComposeMsgCodec.sol b/contracts/periphery/mintburn/sponsored-oft/ComposeMsgCodec.sol index bb24ef646..d7b3d13cf 100644 --- a/contracts/periphery/mintburn/sponsored-oft/ComposeMsgCodec.sol +++ b/contracts/periphery/mintburn/sponsored-oft/ComposeMsgCodec.sol @@ -10,9 +10,10 @@ 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; + uint256 internal constant DESTINATION_DEX_OFFSET = 164; + uint256 internal constant EXECUTION_MODE_OFFSET = 196; // 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 MIN_COMPOSE_MSG_BYTE_LENGTH = 292; function _encode( bytes32 nonce, @@ -21,6 +22,7 @@ library ComposeMsgCodec { uint256 maxUserSlippageBps, bytes32 finalRecipient, bytes32 finalToken, + uint32 destinationDex, uint8 executionMode, bytes memory actionData ) internal pure returns (bytes memory) { @@ -32,6 +34,7 @@ library ComposeMsgCodec { maxUserSlippageBps, finalRecipient, finalToken, + destinationDex, executionMode, actionData ); @@ -61,18 +64,22 @@ library ComposeMsgCodec { return BytesLib.toBytes32(data, FINAL_TOKEN_OFFSET); } + function _getDestinationDex(bytes memory data) internal pure returns (uint32 v) { + return BytesLib.toUint32(data, DESTINATION_DEX_OFFSET); + } + function _getExecutionMode(bytes memory data) internal pure returns (uint8 v) { - (, , , , , , uint8 executionMode, ) = abi.decode( + (, , , , , , , uint8 executionMode, ) = abi.decode( data, - (bytes32, uint256, uint256, uint256, bytes32, bytes32, uint8, bytes) + (bytes32, uint256, uint256, uint256, bytes32, bytes32, uint32, uint8, bytes) ); return executionMode; } 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, bytes) ); return actionData; } diff --git a/contracts/periphery/mintburn/sponsored-oft/DstOFTHandler.sol b/contracts/periphery/mintburn/sponsored-oft/DstOFTHandler.sol index add659848..28c4fd197 100644 --- a/contracts/periphery/mintburn/sponsored-oft/DstOFTHandler.sol +++ b/contracts/periphery/mintburn/sponsored-oft/DstOFTHandler.sol @@ -166,6 +166,7 @@ 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 executionMode = composeMsg._getExecutionMode(); bytes memory actionData = composeMsg._getActionData(); @@ -174,6 +175,7 @@ contract DstOFTHandler is BaseModuleHandler, ILayerZeroComposer, ArbitraryEVMFlo quoteNonce: quoteNonce, finalRecipient: finalRecipient, finalToken: finalToken, + destinationDex: destinationDex, maxBpsToSponsor: maxBpsToSponsor, extraFeesIncurred: EXTRA_FEES_TO_SPONSOR }); diff --git a/contracts/periphery/mintburn/sponsored-oft/SponsoredOFTSrcPeriphery.sol b/contracts/periphery/mintburn/sponsored-oft/SponsoredOFTSrcPeriphery.sol index 4177bdb6f..e1a6816fc 100644 --- a/contracts/periphery/mintburn/sponsored-oft/SponsoredOFTSrcPeriphery.sol +++ b/contracts/periphery/mintburn/sponsored-oft/SponsoredOFTSrcPeriphery.sol @@ -168,6 +168,7 @@ contract SponsoredOFTSrcPeriphery is Ownable { quote.unsignedParams.maxUserSlippageBps, quote.signedParams.finalRecipient, quote.signedParams.finalToken, + quote.signedParams.destinationDex, 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..8dc6922ff 100644 --- a/contracts/periphery/mintburn/sponsored-oft/Structs.sol +++ b/contracts/periphery/mintburn/sponsored-oft/Structs.sol @@ -30,6 +30,7 @@ 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 diff --git a/script/DeployHyperliquidDepositHandler.s.sol b/script/DeployHyperliquidDepositHandler.s.sol index 2c0afa490..fcc3ca424 100644 --- a/script/DeployHyperliquidDepositHandler.s.sol +++ b/script/DeployHyperliquidDepositHandler.s.sol @@ -34,7 +34,12 @@ 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 + ); 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 73c9052aa..3b7dc65c8 100644 --- a/script/mintburn/cctp/createSponsoredDeposit.sol +++ b/script/mintburn/cctp/createSponsoredDeposit.sol @@ -8,6 +8,7 @@ import { SponsoredCCTPSrcPeriphery } from "../../../contracts/periphery/mintburn import { ArbitraryEVMFlowExecutor } from "../../../contracts/periphery/mintburn/ArbitraryEVMFlowExecutor.sol"; import { SponsoredCCTPInterface } from "../../../contracts/interfaces/SponsoredCCTPInterface.sol"; import { AddressToBytes32 } from "../../../contracts/libraries/AddressConverters.sol"; +import { HyperCoreLib } from "../../../contracts/libraries/HyperCoreLib.sol"; interface IHyperSwapRouter { struct ExactInputSingleParams { @@ -97,6 +98,7 @@ contract CreateSponsoredDeposit is DeploymentUtils { maxUserSlippageBps: 0, // 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 executionMode: uint8(SponsoredCCTPInterface.ExecutionMode.DirectToCore), // DirectToCore mode actionData: emptyActionData // Empty for DirectToCore mode }); diff --git a/script/mintburn/oft/CreateSponsoredDeposit.s.sol b/script/mintburn/oft/CreateSponsoredDeposit.s.sol index 73c848e96..6b7f1c4fd 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"; @@ -196,6 +197,7 @@ contract CreateSponsoredDeposit is Script, Config { maxBpsToSponsor: maxBpsToSponsor, finalRecipient: finalRecipient.toBytes32(), finalToken: finalToken.toBytes32(), + destinationDex: HyperCoreLib.CORE_SPOT_DEX_ID, lzReceiveGasLimit: lzReceiveGasLimit, lzComposeGasLimit: lzComposeGasLimit, executionMode: 0, @@ -265,6 +267,7 @@ contract CreateSponsoredDeposit is Script, Config { quote.unsignedParams.maxUserSlippageBps, quote.signedParams.finalRecipient, quote.signedParams.finalToken, + quote.signedParams.destinationDex, quote.signedParams.executionMode, quote.signedParams.actionData ); diff --git a/test/evm/foundry/local/HyperCoreFlowExecutor.t.sol b/test/evm/foundry/local/HyperCoreFlowExecutor.t.sol index 2cf61d9c2..b770f348c 100644 --- a/test/evm/foundry/local/HyperCoreFlowExecutor.t.sol +++ b/test/evm/foundry/local/HyperCoreFlowExecutor.t.sol @@ -92,6 +92,7 @@ contract HyperCoreFlowExecutorTest is BaseSimulatorTest { quoteNonce: QUOTE_NONCE, finalRecipient: finalRecipient, finalToken: address(token), + destinationDex: HyperCoreLib.CORE_SPOT_DEX_ID, maxBpsToSponsor: maxBpsToSponsor, extraFeesIncurred: extraFeesIncurred }); @@ -143,6 +144,7 @@ contract HyperCoreFlowExecutorTest is BaseSimulatorTest { quoteNonce: keccak256("quote-2"), finalRecipient: unactivated, finalToken: address(token), + destinationDex: HyperCoreLib.CORE_SPOT_DEX_ID, maxBpsToSponsor: maxBps, extraFeesIncurred: extraFees }); diff --git a/test/evm/foundry/local/SponsoredOFTSrcPeriphery.t.sol b/test/evm/foundry/local/SponsoredOFTSrcPeriphery.t.sol index 39c2c0465..062767194 100644 --- a/test/evm/foundry/local/SponsoredOFTSrcPeriphery.t.sol +++ b/test/evm/foundry/local/SponsoredOFTSrcPeriphery.t.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,6 +77,7 @@ 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, executionMode: uint8(0), // DirectToCore diff --git a/test/evm/foundry/local/SponsorredCCTPDstPeriphery.t.sol b/test/evm/foundry/local/SponsorredCCTPDstPeriphery.t.sol index 34042f60a..e62d855ee 100644 --- a/test/evm/foundry/local/SponsorredCCTPDstPeriphery.t.sol +++ b/test/evm/foundry/local/SponsorredCCTPDstPeriphery.t.sol @@ -4,6 +4,7 @@ pragma solidity ^0.8.0; import { Test, console } from "forge-std/Test.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"; @@ -230,6 +231,7 @@ contract SponsoredCCTPDstPeripheryTest is BaseSimulatorTest { maxUserSlippageBps: 50, // 0.5% finalRecipient: finalRecipient.toBytes32(), finalToken: address(usdc).toBytes32(), + destinationDex: HyperCoreLib.CORE_SPOT_DEX_ID, executionMode: uint8(SponsoredCCTPInterface.ExecutionMode.DirectToCore), actionData: bytes("") }); From fab6286e3c6a65dff3a6516b6c9d23dfc19aee11 Mon Sep 17 00:00:00 2001 From: Faisal Usmani Date: Fri, 12 Dec 2025 12:17:13 -0500 Subject: [PATCH 02/21] depositFor for USDC Signed-off-by: Faisal Usmani --- .../interfaces/ICoreDepositWallet.sol | 11 ++++- .../handlers/HyperliquidDepositHandler.sol | 16 ++++++- contracts/libraries/HyperCoreLib.sol | 48 ++++++++++++------- .../mintburn/HyperCoreFlowExecutor.sol | 16 +------ contracts/periphery/mintburn/SwapHandler.sol | 8 +++- script/DeployHyperliquidDepositHandler.s.sol | 1 + 6 files changed, 64 insertions(+), 36 deletions(-) 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/handlers/HyperliquidDepositHandler.sol b/contracts/handlers/HyperliquidDepositHandler.sol index c443ebb18..e4cd92b6d 100644 --- a/contracts/handlers/HyperliquidDepositHandler.sol +++ b/contracts/handlers/HyperliquidDepositHandler.sol @@ -180,7 +180,13 @@ 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.CORE_SPOT_DEX_ID); + HyperCoreLib.transferERC20CoreToCore( + tokenIndex, + user, + coreAmount, + HyperCoreLib.CORE_SPOT_DEX_ID, + HyperCoreLib.CORE_SPOT_DEX_ID + ); } /** @@ -232,7 +238,13 @@ contract HyperliquidDepositHandler is AcrossMessageHandler, ReentrancyGuard, Own decimalDiff, HyperCoreLib.CORE_SPOT_DEX_ID ); - HyperCoreLib.transferERC20CoreToCore(tokenIndex, user, 1, HyperCoreLib.CORE_SPOT_DEX_ID); + HyperCoreLib.transferERC20CoreToCore( + tokenIndex, + user, + 1, + HyperCoreLib.CORE_SPOT_DEX_ID, + HyperCoreLib.CORE_SPOT_DEX_ID + ); emit UserAccountActivated(user, token, amountRequiredToActivate); } diff --git a/contracts/libraries/HyperCoreLib.sol b/contracts/libraries/HyperCoreLib.sol index 7dbc7ff6d..31e135a5e 100644 --- a/contracts/libraries/HyperCoreLib.sol +++ b/contracts/libraries/HyperCoreLib.sol @@ -98,9 +98,14 @@ library HyperCoreLib { (uint256 _amountEVMToSend, uint64 _amountCoreToReceive) = maximumEVMSendAmountToAmounts(amountEVM, decimalDiff); if (_amountEVMToSend != 0) { - transferToCore(erc20EVMAddress, erc20CoreIndex, _amountEVMToSend, destinationDex); - // Transfer the tokens from this contract on HyperCore to the `to` address on HyperCore - transferERC20CoreToCore(erc20CoreIndex, to, _amountCoreToReceive, destinationDex); + if (erc20CoreIndex == USDC_CORE_INDEX) { + IERC20(erc20EVMAddress).forceApprove(USDC_CORE_DEPOSIT_WALLET_ADDRESS, _amountEVMToSend); + 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 + transferERC20CoreToCore(erc20CoreIndex, to, _amountCoreToReceive, destinationDex, destinationDex); + } } return (_amountEVMToSend, _amountCoreToReceive); @@ -128,7 +133,12 @@ library HyperCoreLib { (uint256 _amountEVMToSend, uint64 _amountCoreToReceive) = maximumEVMSendAmountToAmounts(amountEVM, decimalDiff); if (_amountEVMToSend != 0) { - transferToCore(erc20EVMAddress, erc20CoreIndex, _amountEVMToSend, destinationDex); + if (erc20CoreIndex == USDC_CORE_INDEX) { + IERC20(erc20EVMAddress).forceApprove(USDC_CORE_DEPOSIT_WALLET_ADDRESS, _amountEVMToSend); + ICoreDepositWallet(USDC_CORE_DEPOSIT_WALLET_ADDRESS).deposit(_amountEVMToSend, destinationDex); + } else { + IERC20(erc20EVMAddress).safeTransfer(toAssetBridgeAddress(erc20CoreIndex), _amountEVMToSend); + } } return (_amountEVMToSend, _amountCoreToReceive); @@ -139,19 +149,21 @@ 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, + uint32 sourceDex, uint32 destinationDex ) internal { bytes memory action; bytes memory payload; if (erc20CoreIndex == USDC_CORE_INDEX) { - action = abi.encode(to, address(0), destinationDex, destinationDex, erc20CoreIndex, amountCore); + 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); @@ -162,25 +174,25 @@ library HyperCoreLib { } /** - * @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 - * @param destinationDex The destination DEX on HyperCore + * @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( + function activateCoreAccountFromEVM( address erc20EVMAddress, uint64 erc20CoreIndex, - uint256 amountEVMToSend, - uint32 destinationDex + address user, + uint256 amountEVM ) internal { - // USDC requires a special transfer to core if (erc20CoreIndex == USDC_CORE_INDEX) { - IERC20(erc20EVMAddress).forceApprove(USDC_CORE_DEPOSIT_WALLET_ADDRESS, amountEVMToSend); - ICoreDepositWallet(USDC_CORE_DEPOSIT_WALLET_ADDRESS).deposit(amountEVMToSend, destinationDex); + 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/periphery/mintburn/HyperCoreFlowExecutor.sol b/contracts/periphery/mintburn/HyperCoreFlowExecutor.sol index 10da9d44a..e00e9c87b 100644 --- a/contracts/periphery/mintburn/HyperCoreFlowExecutor.sol +++ b/contracts/periphery/mintburn/HyperCoreFlowExecutor.sol @@ -916,20 +916,7 @@ contract HyperCoreFlowExecutor is AccessControlUpgradeable, AuthorizedFundedFlow require(safeToBridge, "Not safe to bridge"); _getMainStorage().cumulativeSponsoredActivationFee[fundingToken] += evmAmountToSend; - // donationBox @ evm -> Handler @ evm - donationBox.withdraw(IERC20(fundingToken), evmAmountToSend); - // Handler @ evm -> Handler @ core - HyperCoreLib.transferERC20EVMToSelfOnCore( - fundingToken, - coreTokenInfo.coreIndex, - evmAmountToSend, - coreTokenInfo.tokenInfo.evmExtraWeiDecimals, - HyperCoreLib.CORE_SPOT_DEX_ID - ); - // 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.CORE_SPOT_DEX_ID); + HyperCoreLib.activateCoreAccountFromEVM(fundingToken, coreTokenInfo.coreIndex, finalRecipient, evmAmountToSend); emit SponsoredAccountActivation(quoteNonce, finalRecipient, fundingToken, evmAmountToSend); } @@ -1141,6 +1128,7 @@ contract HyperCoreFlowExecutor is AccessControlUpgradeable, AuthorizedFundedFlow _getMainStorage().coreTokenInfos[token].coreIndex, msg.sender, amount, + destinationDex, destinationDex ); } diff --git a/contracts/periphery/mintburn/SwapHandler.sol b/contracts/periphery/mintburn/SwapHandler.sol index ce9704ea8..2ce567d95 100644 --- a/contracts/periphery/mintburn/SwapHandler.sol +++ b/contracts/periphery/mintburn/SwapHandler.sol @@ -43,7 +43,13 @@ contract SwapHandler { uint64 amountCore, uint32 destinationDex ) external onlyParentHandler { - HyperCoreLib.transferERC20CoreToCore(erc20CoreIndex, to, amountCore, destinationDex); + HyperCoreLib.transferERC20CoreToCore( + erc20CoreIndex, + to, + amountCore, + HyperCoreLib.CORE_SPOT_DEX_ID, + destinationDex + ); } function submitSpotLimitOrder( diff --git a/script/DeployHyperliquidDepositHandler.s.sol b/script/DeployHyperliquidDepositHandler.s.sol index fcc3ca424..12860904f 100644 --- a/script/DeployHyperliquidDepositHandler.s.sol +++ b/script/DeployHyperliquidDepositHandler.s.sol @@ -38,6 +38,7 @@ contract DeployHyperliquidDepositHandler is Script, Test { usdhTokenIndex, address(hyperliquidDepositHandler), 1, + HyperCoreLib.CORE_SPOT_DEX_ID, HyperCoreLib.CORE_SPOT_DEX_ID ); hyperliquidDepositHandler.addSupportedToken(address(usdh), usdhTokenIndex, 1000000, usdhDecimalDiff); From 7518d6b68e55d21607372890049440434c319f76 Mon Sep 17 00:00:00 2001 From: Faisal Usmani Date: Mon, 15 Dec 2025 12:11:18 -0500 Subject: [PATCH 03/21] fixed tests Signed-off-by: Faisal Usmani --- broadcast/deployed-addresses.json | 5 +++++ broadcast/deployed-addresses.md | 1 + test/evm/foundry/local/SponsoredOFTSrcPeriphery.t.sol | 6 ++++-- test/evm/foundry/local/SponsorredCCTPDstPeriphery.t.sol | 2 ++ 4 files changed, 12 insertions(+), 2 deletions(-) diff --git a/broadcast/deployed-addresses.json b/broadcast/deployed-addresses.json index 4d72aa918..128ee8226 100644 --- a/broadcast/deployed-addresses.json +++ b/broadcast/deployed-addresses.json @@ -502,6 +502,11 @@ "address": "0x861e127036b28d32f3777b4676f6bbb9e007d195", "block_number": 20301679, "transaction_hash": "0x187b45f39be413aff2ff526946c64f76ed98763129e7e2ffb7d2d4c5bd997519" + }, + "Sample": { + "address": "0x27e3c9fd88f15b8f512e58e231a45679cd8cf238", + "block_number": 21670048, + "transaction_hash": "0xea6102d7ceec126511c4241d0e0f4a323ca0a0b5ddbe1d932ef19c86a14ff771" } } }, diff --git a/broadcast/deployed-addresses.md b/broadcast/deployed-addresses.md index 5f7a39ee1..e6b6a9863 100644 --- a/broadcast/deployed-addresses.md +++ b/broadcast/deployed-addresses.md @@ -167,6 +167,7 @@ This file contains the latest deployed smart contract addresses from the broadca | HyperliquidDepositHandler | [0x861E127036B28D32f3777B4676F6bbb9e007d195](https://hyperevmscan.io//address/0x861E127036B28D32f3777B4676F6bbb9e007d195) | | MulticallHandler | [0x5E7840E06fAcCb6d1c3b5F5E0d1d3d07F2829bba](https://hyperevmscan.io//address/0x5E7840E06fAcCb6d1c3b5F5E0d1d3d07F2829bba) | | PermissionedMulticallHandler | [0x0980D0F6799CA06C71fFAFdc0E423cF2B0f20502](https://hyperevmscan.io//address/0x0980D0F6799CA06C71fFAFdc0E423cF2B0f20502) | +| Sample | [0x27E3c9fD88f15B8F512E58e231a45679cd8cf238](https://hyperevmscan.io//address/0x27E3c9fD88f15B8F512E58e231a45679cd8cf238) | | SpokePool | [0x35E63eA3eb0fb7A3bc543C71FB66412e1F6B0E04](https://hyperevmscan.io//address/0x35E63eA3eb0fb7A3bc543C71FB66412e1F6B0E04) | | SpokePoolPeriphery | [0xF1BF00D947267Da5cC63f8c8A60568c59FA31bCb](https://hyperevmscan.io//address/0xF1BF00D947267Da5cC63f8c8A60568c59FA31bCb) | | SpokePoolVerifier | [0x3Fb9cED51E968594C87963a371Ed90c39519f65A](https://hyperevmscan.io//address/0x3Fb9cED51E968594C87963a371Ed90c39519f65A) | diff --git a/test/evm/foundry/local/SponsoredOFTSrcPeriphery.t.sol b/test/evm/foundry/local/SponsoredOFTSrcPeriphery.t.sol index 062767194..0f1c80cac 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"; @@ -157,9 +157,10 @@ contract SponsoredOFTSrcPeripheryTest is Test { uint256 gotMaxUserSlippageBps, bytes32 gotFinalRecipient, bytes32 gotFinalToken, + uint32 gotDestinationDex, 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, bytes)); assertEq(gotNonce, nonce, "nonce mismatch"); assertEq(gotDeadline, deadline, "deadline mismatch"); @@ -167,6 +168,7 @@ 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(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 e62d855ee..4ae19b315 100644 --- a/test/evm/foundry/local/SponsorredCCTPDstPeriphery.t.sol +++ b/test/evm/foundry/local/SponsorredCCTPDstPeriphery.t.sol @@ -143,6 +143,7 @@ contract SponsoredCCTPDstPeripheryTest is BaseSimulatorTest { quote.maxUserSlippageBps, quote.finalRecipient, quote.finalToken, + quote.destinationDex, quote.executionMode, quote.actionData ); @@ -202,6 +203,7 @@ contract SponsoredCCTPDstPeripheryTest is BaseSimulatorTest { quote.maxUserSlippageBps, quote.finalRecipient, quote.finalToken, + quote.destinationDex, quote.executionMode, keccak256(quote.actionData) ) From 02f5253b9bc58f46a2db5f6974405ac763787ed6 Mon Sep 17 00:00:00 2001 From: Faisal Usmani Date: Mon, 15 Dec 2025 14:01:56 -0500 Subject: [PATCH 04/21] trust api with destination dex Signed-off-by: Faisal Usmani --- contracts/libraries/HyperCoreLib.sol | 2 +- contracts/periphery/mintburn/HyperCoreFlowExecutor.sol | 8 -------- 2 files changed, 1 insertion(+), 9 deletions(-) diff --git a/contracts/libraries/HyperCoreLib.sol b/contracts/libraries/HyperCoreLib.sol index 31e135a5e..95724450b 100644 --- a/contracts/libraries/HyperCoreLib.sol +++ b/contracts/libraries/HyperCoreLib.sol @@ -162,7 +162,7 @@ library HyperCoreLib { bytes memory action; bytes memory payload; - if (erc20CoreIndex == USDC_CORE_INDEX) { + 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 { diff --git a/contracts/periphery/mintburn/HyperCoreFlowExecutor.sol b/contracts/periphery/mintburn/HyperCoreFlowExecutor.sol index e00e9c87b..da3f2bfaf 100644 --- a/contracts/periphery/mintburn/HyperCoreFlowExecutor.sol +++ b/contracts/periphery/mintburn/HyperCoreFlowExecutor.sol @@ -476,14 +476,6 @@ contract HyperCoreFlowExecutor is AccessControlUpgradeable, AuthorizedFundedFlow * checked by a handler. Params authorization by a handler is enforced via `onlyAuthorizedFlow` modifier */ function executeFlow(CommonFlowParams memory params, uint256 maxUserSlippageBps) external onlyAuthorizedFlow { - MainStorage storage $ = _getMainStorage(); - CoreTokenInfo memory coreTokenInfo = $.coreTokenInfos[params.finalToken]; - - // If the final token is not USDC, we need to set the destination dex to the spot dex - if (coreTokenInfo.coreIndex != HyperCoreLib.USDC_CORE_INDEX) { - params.destinationDex = HyperCoreLib.CORE_SPOT_DEX_ID; - } - if (params.finalToken == baseToken) { _executeSimpleTransferFlow(params); } else { From c661b36c14dfef73e3e6aa4988987e995e9f1e35 Mon Sep 17 00:00:00 2001 From: Faisal Usmani Date: Mon, 15 Dec 2025 14:19:27 -0500 Subject: [PATCH 05/21] set spot as source Signed-off-by: Faisal Usmani --- contracts/libraries/HyperCoreLib.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/libraries/HyperCoreLib.sol b/contracts/libraries/HyperCoreLib.sol index 95724450b..b8284efd2 100644 --- a/contracts/libraries/HyperCoreLib.sol +++ b/contracts/libraries/HyperCoreLib.sol @@ -104,7 +104,7 @@ library HyperCoreLib { } else { IERC20(erc20EVMAddress).safeTransfer(toAssetBridgeAddress(erc20CoreIndex), _amountEVMToSend); // Transfer the tokens from this contract on HyperCore to the `to` address on HyperCore - transferERC20CoreToCore(erc20CoreIndex, to, _amountCoreToReceive, destinationDex, destinationDex); + transferERC20CoreToCore(erc20CoreIndex, to, _amountCoreToReceive, CORE_SPOT_DEX_ID, destinationDex); } } From 8b672594f8c1064bf71b0782b31af5c9d1eb701b Mon Sep 17 00:00:00 2001 From: Faisal Usmani Date: Tue, 16 Dec 2025 16:30:34 -0500 Subject: [PATCH 06/21] add helper funcs Signed-off-by: Faisal Usmani --- .../handlers/HyperliquidDepositHandler.sol | 24 ++----------- contracts/libraries/HyperCoreLib.sol | 36 +++++++++++++++++++ .../mintburn/HyperCoreFlowExecutor.sol | 12 +++---- contracts/periphery/mintburn/SwapHandler.sol | 11 ++---- 4 files changed, 47 insertions(+), 36 deletions(-) diff --git a/contracts/handlers/HyperliquidDepositHandler.sol b/contracts/handlers/HyperliquidDepositHandler.sol index e4cd92b6d..fc80fd64f 100644 --- a/contracts/handlers/HyperliquidDepositHandler.sol +++ b/contracts/handlers/HyperliquidDepositHandler.sol @@ -180,13 +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.CORE_SPOT_DEX_ID, - HyperCoreLib.CORE_SPOT_DEX_ID - ); + HyperCoreLib.transferERC20SpotToSpot(tokenIndex, user, coreAmount); } /** @@ -231,20 +225,8 @@ 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.CORE_SPOT_DEX_ID - ); - HyperCoreLib.transferERC20CoreToCore( - tokenIndex, - user, - 1, - HyperCoreLib.CORE_SPOT_DEX_ID, - HyperCoreLib.CORE_SPOT_DEX_ID - ); + HyperCoreLib.transferERC20EVMToSelfOnSpot(token, tokenIndex, amountRequiredToActivate, decimalDiff); + HyperCoreLib.transferERC20SpotToSpot(tokenIndex, user, 1); emit UserAccountActivated(user, token, amountRequiredToActivate); } diff --git a/contracts/libraries/HyperCoreLib.sol b/contracts/libraries/HyperCoreLib.sol index b8284efd2..0f443beb5 100644 --- a/contracts/libraries/HyperCoreLib.sol +++ b/contracts/libraries/HyperCoreLib.sol @@ -111,6 +111,32 @@ library HyperCoreLib { return (_amountEVMToSend, _amountCoreToReceive); } + /** + * @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 + ); + } + /** * @notice Bridges `amountEVM` of `erc20` from this address on HyperEVM to this address on HyperCore. * @dev Returns the amount credited on Core in Core units (post conversion). @@ -144,6 +170,16 @@ library HyperCoreLib { 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); + } + /** * @notice Transfers tokens from this contract on HyperCore to the `to` address on HyperCore * @param erc20CoreIndex The HyperCore index id of the token diff --git a/contracts/periphery/mintburn/HyperCoreFlowExecutor.sol b/contracts/periphery/mintburn/HyperCoreFlowExecutor.sol index da3f2bfaf..dc47d1635 100644 --- a/contracts/periphery/mintburn/HyperCoreFlowExecutor.sol +++ b/contracts/periphery/mintburn/HyperCoreFlowExecutor.sol @@ -714,8 +714,7 @@ contract HyperCoreFlowExecutor is AccessControlUpgradeable, AuthorizedFundedFlow initialToken, initialCoreTokenInfo.coreIndex, tokensToSendEvm, - initialCoreTokenInfo.tokenInfo.evmExtraWeiDecimals, - HyperCoreLib.CORE_SPOT_DEX_ID + initialCoreTokenInfo.tokenInfo.evmExtraWeiDecimals ); } @@ -798,8 +797,7 @@ contract HyperCoreFlowExecutor is AccessControlUpgradeable, AuthorizedFundedFlow finalToken, finalCoreTokenInfo.coreIndex, totalAdditionalToSendEVM, - finalCoreTokenInfo.tokenInfo.evmExtraWeiDecimals, - HyperCoreLib.CORE_SPOT_DEX_ID + finalCoreTokenInfo.tokenInfo.evmExtraWeiDecimals ); } } @@ -908,6 +906,9 @@ contract HyperCoreFlowExecutor is AccessControlUpgradeable, AuthorizedFundedFlow require(safeToBridge, "Not safe to bridge"); _getMainStorage().cumulativeSponsoredActivationFee[fundingToken] += evmAmountToSend; + // donationBox @ evm -> Handler @ evm + donationBox.withdraw(IERC20(fundingToken), evmAmountToSend); + HyperCoreLib.activateCoreAccountFromEVM(fundingToken, coreTokenInfo.coreIndex, finalRecipient, evmAmountToSend); emit SponsoredAccountActivation(quoteNonce, finalRecipient, fundingToken, evmAmountToSend); @@ -1002,8 +1003,7 @@ contract HyperCoreFlowExecutor is AccessControlUpgradeable, AuthorizedFundedFlow token, coreTokenInfo.coreIndex, amountEVMToSend, - coreTokenInfo.tokenInfo.evmExtraWeiDecimals, - HyperCoreLib.CORE_SPOT_DEX_ID + coreTokenInfo.tokenInfo.evmExtraWeiDecimals ); } diff --git a/contracts/periphery/mintburn/SwapHandler.sol b/contracts/periphery/mintburn/SwapHandler.sol index 2ce567d95..1e8e27c4b 100644 --- a/contracts/periphery/mintburn/SwapHandler.sol +++ b/contracts/periphery/mintburn/SwapHandler.sol @@ -25,16 +25,9 @@ contract SwapHandler { address erc20EVMAddress, uint64 erc20CoreIndex, uint256 amountEVM, - int8 decimalDiff, - uint32 destinationDex + int8 decimalDiff ) external onlyParentHandler { - HyperCoreLib.transferERC20EVMToSelfOnCore( - erc20EVMAddress, - erc20CoreIndex, - amountEVM, - decimalDiff, - destinationDex - ); + HyperCoreLib.transferERC20EVMToSelfOnSpot(erc20EVMAddress, erc20CoreIndex, amountEVM, decimalDiff); } function transferFundsToUserOnCore( From 57ecb454f7e9d916ba85cd0e93a9f3b4cbdb1418 Mon Sep 17 00:00:00 2001 From: Faisal Usmani Date: Tue, 16 Dec 2025 16:38:16 -0500 Subject: [PATCH 07/21] fixed data indexes Signed-off-by: Faisal Usmani --- .../periphery/mintburn/sponsored-oft/ComposeMsgCodec.sol | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/contracts/periphery/mintburn/sponsored-oft/ComposeMsgCodec.sol b/contracts/periphery/mintburn/sponsored-oft/ComposeMsgCodec.sol index d7b3d13cf..bf573f29e 100644 --- a/contracts/periphery/mintburn/sponsored-oft/ComposeMsgCodec.sol +++ b/contracts/periphery/mintburn/sponsored-oft/ComposeMsgCodec.sol @@ -10,10 +10,10 @@ 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 DESTINATION_DEX_OFFSET = 164; + uint256 internal constant DESTINATION_DEX_OFFSET = 192; uint256 internal constant EXECUTION_MODE_OFFSET = 196; - // 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 = 292; + // Minimum length with empty actionData: 8 regular params (32 bytes each) and 1 dynamic byte array (minumum 64 bytes) + uint256 internal constant MIN_COMPOSE_MSG_BYTE_LENGTH = 320; function _encode( bytes32 nonce, From 21c2ad673d53b60368c4f60e0603ca2151ce0a95 Mon Sep 17 00:00:00 2001 From: Faisal Usmani Date: Tue, 16 Dec 2025 17:30:34 -0500 Subject: [PATCH 08/21] updated cctp min msg length Signed-off-by: Faisal Usmani --- contracts/libraries/SponsoredCCTPQuoteLib.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/libraries/SponsoredCCTPQuoteLib.sol b/contracts/libraries/SponsoredCCTPQuoteLib.sol index 847b2e57e..a8e21072b 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 = 668; + uint256 private constant MIN_MSG_BYTES_LENGTH = 696; /** * @notice Gets the data for the deposit for burn. From e179c5edc2b66f1e50c289d93e88b6a889ee94d2 Mon Sep 17 00:00:00 2001 From: Faisal Usmani Date: Tue, 16 Dec 2025 17:32:00 -0500 Subject: [PATCH 09/21] updated oft min msg length Signed-off-by: Faisal Usmani --- contracts/periphery/mintburn/sponsored-oft/ComposeMsgCodec.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/periphery/mintburn/sponsored-oft/ComposeMsgCodec.sol b/contracts/periphery/mintburn/sponsored-oft/ComposeMsgCodec.sol index bf573f29e..dd603bbd4 100644 --- a/contracts/periphery/mintburn/sponsored-oft/ComposeMsgCodec.sol +++ b/contracts/periphery/mintburn/sponsored-oft/ComposeMsgCodec.sol @@ -11,7 +11,7 @@ library ComposeMsgCodec { uint256 internal constant FINAL_RECIPIENT_OFFSET = 128; uint256 internal constant FINAL_TOKEN_OFFSET = 160; uint256 internal constant DESTINATION_DEX_OFFSET = 192; - uint256 internal constant EXECUTION_MODE_OFFSET = 196; + uint256 internal constant EXECUTION_MODE_OFFSET = 224; // Minimum length with empty actionData: 8 regular params (32 bytes each) and 1 dynamic byte array (minumum 64 bytes) uint256 internal constant MIN_COMPOSE_MSG_BYTE_LENGTH = 320; From 175ed7fd2cc33de8b4d491bf9b4c5775a2d350b4 Mon Sep 17 00:00:00 2001 From: Ihor Farion <65650773+grasphoper@users.noreply.github.com> Date: Tue, 16 Dec 2025 18:52:16 -0800 Subject: [PATCH 10/21] feat: `HyperCore` account creation from user funds (#1224) * plan out the code Signed-off-by: Ihor Farion * dst side impl Signed-off-by: Ihor Farion * remove redundant var Signed-off-by: Ihor Farion * add account creation mode to encoded / decoded data Signed-off-by: Ihor Farion * fix tests and build Signed-off-by: Ihor Farion --------- Signed-off-by: Ihor Farion --- .../handlers/HyperliquidDepositHandler.sol | 4 +- .../interfaces/SponsoredCCTPInterface.sol | 7 ++ contracts/libraries/HyperCoreLib.sol | 53 +++++++++----- contracts/libraries/SponsoredCCTPQuoteLib.sol | 9 ++- .../mintburn/HyperCoreFlowExecutor.sol | 70 +++++++++++++------ contracts/periphery/mintburn/Structs.sol | 6 ++ .../SponsoredCCTPDstPeriphery.sol | 5 +- .../sponsored-oft/ComposeMsgCodec.sol | 25 +++++-- .../mintburn/sponsored-oft/DstOFTHandler.sol | 6 +- .../mintburn/sponsored-oft/QuoteSignLib.sol | 1 + .../SponsoredOFTSrcPeriphery.sol | 1 + .../mintburn/sponsored-oft/Structs.sol | 6 ++ .../mintburn/cctp/createSponsoredDeposit.sol | 3 + .../mintburn/oft/CreateSponsoredDeposit.s.sol | 3 + .../foundry/local/HyperCoreFlowExecutor.t.sol | 8 ++- .../local/SponsoredOFTSrcPeriphery.t.sol | 8 ++- .../local/SponsorredCCTPDstPeriphery.t.sol | 3 + 17 files changed, 161 insertions(+), 57 deletions(-) diff --git a/contracts/handlers/HyperliquidDepositHandler.sol b/contracts/handlers/HyperliquidDepositHandler.sol index fc80fd64f..1d6201795 100644 --- a/contracts/handlers/HyperliquidDepositHandler.sol +++ b/contracts/handlers/HyperliquidDepositHandler.sol @@ -236,7 +236,9 @@ contract HyperliquidDepositHandler is AcrossMessageHandler, ReentrancyGuard, Own user, evmAmount, decimalDiff, - HyperCoreLib.CORE_SPOT_DEX_ID + HyperCoreLib.CORE_SPOT_DEX_ID, + // Account activation is handled in separate CoreWriter actions above + 0 ); } diff --git a/contracts/interfaces/SponsoredCCTPInterface.sol b/contracts/interfaces/SponsoredCCTPInterface.sol index c2bd094d3..dd408da4f 100644 --- a/contracts/interfaces/SponsoredCCTPInterface.sol +++ b/contracts/interfaces/SponsoredCCTPInterface.sol @@ -46,6 +46,11 @@ interface SponsoredCCTPInterface { ArbitraryActionsToEVM } + enum AccountCreationMode { + Standard, + FromUserFunds + } + // Params that will be used to create a sponsored CCTP quote and deposit for burn. struct SponsoredCCTPQuote { // The domain ID of the source chain. @@ -80,6 +85,8 @@ interface SponsoredCCTPInterface { 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 0f443beb5..8829ef97d 100644 --- a/contracts/libraries/HyperCoreLib.sol +++ b/contracts/libraries/HyperCoreLib.sol @@ -83,6 +83,7 @@ library HyperCoreLib { * @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) */ @@ -92,23 +93,44 @@ library HyperCoreLib { address to, uint256 amountEVM, int8 decimalDiff, - uint32 destinationDex + 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) { 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); - ICoreDepositWallet(USDC_CORE_DEPOSIT_WALLET_ADDRESS).depositFor(to, _amountEVMToSend, destinationDex); + 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 - transferERC20CoreToCore(erc20CoreIndex, to, _amountCoreToReceive, CORE_SPOT_DEX_ID, destinationDex); + 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); } /** @@ -156,18 +178,17 @@ library HyperCoreLib { int8 decimalDiff, uint32 destinationDex ) internal returns (uint256 amountEVMSent, uint64 amountCoreToReceive) { - (uint256 _amountEVMToSend, uint64 _amountCoreToReceive) = maximumEVMSendAmountToAmounts(amountEVM, decimalDiff); - - if (_amountEVMToSend != 0) { - if (erc20CoreIndex == USDC_CORE_INDEX) { - IERC20(erc20EVMAddress).forceApprove(USDC_CORE_DEPOSIT_WALLET_ADDRESS, _amountEVMToSend); - ICoreDepositWallet(USDC_CORE_DEPOSIT_WALLET_ADDRESS).deposit(_amountEVMToSend, destinationDex); - } else { - IERC20(erc20EVMAddress).safeTransfer(toAssetBridgeAddress(erc20CoreIndex), _amountEVMToSend); - } - } - - return (_amountEVMToSend, _amountCoreToReceive); + return + transferERC20EVMToCore( + erc20EVMAddress, + erc20CoreIndex, + address(this), + amountEVM, + decimalDiff, + destinationDex, + // Contracts that are using this function HAVE to have their accounts created already + 0 + ); } /** diff --git a/contracts/libraries/SponsoredCCTPQuoteLib.sol b/contracts/libraries/SponsoredCCTPQuoteLib.sol index a8e21072b..76715be25 100644 --- a/contracts/libraries/SponsoredCCTPQuoteLib.sol +++ b/contracts/libraries/SponsoredCCTPQuoteLib.sol @@ -83,6 +83,7 @@ library SponsoredCCTPQuoteLib { quote.finalRecipient, quote.finalToken, quote.destinationDex, + quote.accountCreationMode, quote.executionMode, quote.actionData ); @@ -110,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, uint32, uint8, bytes) + (bytes32, uint256, uint256, uint256, bytes32, bytes32, uint32, uint8, uint8, bytes) ); return finalRecipient.isValidAddress() && finalToken.isValidAddress(); @@ -149,9 +150,10 @@ library SponsoredCCTPQuoteLib { quote.finalRecipient, quote.finalToken, quote.destinationDex, + quote.accountCreationMode, quote.executionMode, quote.actionData - ) = abi.decode(hookData, (bytes32, uint256, uint256, uint256, bytes32, bytes32, uint32, uint8, bytes)); + ) = abi.decode(hookData, (bytes32, uint256, uint256, uint256, bytes32, bytes32, uint32, uint8, uint8, bytes)); } /** @@ -189,6 +191,7 @@ library SponsoredCCTPQuoteLib { 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/HyperCoreFlowExecutor.sol b/contracts/periphery/mintburn/HyperCoreFlowExecutor.sol index dc47d1635..46beb5b8d 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"; @@ -500,18 +500,30 @@ contract HyperCoreFlowExecutor is AccessControlUpgradeable, AuthorizedFundedFlow MainStorage storage $ = _getMainStorage(); CoreTokenInfo memory coreTokenInfo = $.coreTokenInfos[finalToken]; - // Check account activation - if (!HyperCoreLib.coreUserExists(params.finalRecipient)) { - if (params.maxBpsToSponsor > 0) { - revert AccountNotActivatedError(params.finalRecipient); + bool userAccountExists = HyperCoreLib.coreUserExists(params.finalRecipient); + uint64 accountActivationFeeCore; + if (!userAccountExists) { + if (params.accountCreationMode == AccountCreationMode.Standard) { + if (!HyperCoreLib.coreUserExists(params.finalRecipient)) { + if (params.maxBpsToSponsor > 0) { + revert AccountNotActivatedError(params.finalRecipient); + } else { + emit AccountNotActivated(params.quoteNonce, params.finalRecipient); + _fallbackHyperEVMFlow(params); + return; + } + } } else { - emit AccountNotActivated(params.quoteNonce, params.finalRecipient); - _fallbackHyperEVMFlow(params); - return; + // Notice. If the amount is too small to be able to activate an account, the funds will land there, but + // the account won't show up as active until an eligible transfer activates it + if (!coreTokenInfo.canBeUsedForAccountActivation) { + _fallbackHyperEVMFlow(params); + return; + } + accountActivationFeeCore = coreTokenInfo.accountActivationFeeCore; } } - // Calculate sponsorship amount in scope uint256 amountToSponsor; { uint256 maxEvmAmountToSponsor = ((params.amountInEVM + params.extraFeesIncurred) * params.maxBpsToSponsor) / @@ -570,7 +582,8 @@ contract HyperCoreFlowExecutor is AccessControlUpgradeable, AuthorizedFundedFlow params.finalRecipient, quotedEvmAmount, coreTokenInfo.tokenInfo.evmExtraWeiDecimals, - params.destinationDex + params.destinationDex, + accountActivationFeeCore ); emit SimpleTransferFlowCompleted( @@ -593,23 +606,36 @@ contract HyperCoreFlowExecutor is AccessControlUpgradeable, AuthorizedFundedFlow function _initiateSwapFlow(CommonFlowParams memory params, uint256 maxUserSlippageBps) internal { address initialToken = baseToken; - // Check account activation - if (!HyperCoreLib.coreUserExists(params.finalRecipient)) { - if (params.maxBpsToSponsor > 0) { - revert AccountNotActivatedError(params.finalRecipient); - } else { - emit AccountNotActivated(params.quoteNonce, params.finalRecipient); - params.finalToken = initialToken; - _fallbackHyperEVMFlow(params); - return; - } - } - MainStorage storage $ = _getMainStorage(); CoreTokenInfo memory initialCoreTokenInfo = $.coreTokenInfos[initialToken]; CoreTokenInfo memory finalCoreTokenInfo = $.coreTokenInfos[params.finalToken]; FinalTokenInfo memory finalTokenInfo = _getExistingFinalTokenInfo(params.finalToken); + bool userAccountExists = HyperCoreLib.coreUserExists(params.finalRecipient); + if (!userAccountExists) { + if (params.accountCreationMode == AccountCreationMode.Standard) { + if (!HyperCoreLib.coreUserExists(params.finalRecipient)) { + if (params.maxBpsToSponsor > 0) { + revert AccountNotActivatedError(params.finalRecipient); + } else { + emit AccountNotActivated(params.quoteNonce, params.finalRecipient); + params.finalToken = initialToken; + _fallbackHyperEVMFlow(params); + return; + } + } + } else { + // In the FromUserFunds logic for the swap flow, we only allow account creation if the final token is + // usable for it. Finilazing the swap flow should handle account creation automatically when doing the + // final transfer of tokens to the user + if (!finalCoreTokenInfo.canBeUsedForAccountActivation) { + emit AccountNotActivated(params.quoteNonce, params.finalRecipient); + params.finalToken = initialToken; + _fallbackHyperEVMFlow(params); + } + } + } + // Calculate limit order amounts and check if feasible uint64 minAllowableAmountToForwardCore; uint64 maxAllowableAmountToForwardCore; diff --git a/contracts/periphery/mintburn/Structs.sol b/contracts/periphery/mintburn/Structs.sol index 62ec7686a..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; @@ -40,6 +45,7 @@ struct CommonFlowParams { address finalRecipient; address finalToken; uint32 destinationDex; + AccountCreationMode accountCreationMode; uint256 maxBpsToSponsor; uint256 extraFeesIncurred; } diff --git a/contracts/periphery/mintburn/sponsored-cctp/SponsoredCCTPDstPeriphery.sol b/contracts/periphery/mintburn/sponsored-cctp/SponsoredCCTPDstPeriphery.sol index 31396d1f0..8d02231c3 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"; @@ -205,7 +205,8 @@ contract SponsoredCCTPDstPeriphery is BaseModuleHandler, SponsoredCCTPInterface, 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 diff --git a/contracts/periphery/mintburn/sponsored-oft/ComposeMsgCodec.sol b/contracts/periphery/mintburn/sponsored-oft/ComposeMsgCodec.sol index dd603bbd4..8f92329b9 100644 --- a/contracts/periphery/mintburn/sponsored-oft/ComposeMsgCodec.sol +++ b/contracts/periphery/mintburn/sponsored-oft/ComposeMsgCodec.sol @@ -11,9 +11,10 @@ library ComposeMsgCodec { uint256 internal constant FINAL_RECIPIENT_OFFSET = 128; uint256 internal constant FINAL_TOKEN_OFFSET = 160; uint256 internal constant DESTINATION_DEX_OFFSET = 192; - uint256 internal constant EXECUTION_MODE_OFFSET = 224; - // Minimum length with empty actionData: 8 regular params (32 bytes each) and 1 dynamic byte array (minumum 64 bytes) - uint256 internal constant MIN_COMPOSE_MSG_BYTE_LENGTH = 320; + 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, @@ -23,6 +24,7 @@ library ComposeMsgCodec { bytes32 finalRecipient, bytes32 finalToken, uint32 destinationDex, + uint8 accountCreationMode, uint8 executionMode, bytes memory actionData ) internal pure returns (bytes memory) { @@ -35,6 +37,7 @@ library ComposeMsgCodec { finalRecipient, finalToken, destinationDex, + accountCreationMode, executionMode, actionData ); @@ -68,18 +71,26 @@ library ComposeMsgCodec { return BytesLib.toUint32(data, DESTINATION_DEX_OFFSET); } + function _getAccountCreationMode(bytes memory data) internal pure returns (uint8 v) { + (, , , , , , , uint8 accountCreationMode, , ) = abi.decode( + data, + (bytes32, uint256, uint256, uint256, bytes32, bytes32, uint32, uint8, uint8, bytes) + ); + return accountCreationMode; + } + function _getExecutionMode(bytes memory data) internal pure returns (uint8 v) { - (, , , , , , , uint8 executionMode, ) = abi.decode( + (, , , , , , , , uint8 executionMode, ) = abi.decode( data, - (bytes32, uint256, uint256, uint256, bytes32, bytes32, uint32, uint8, bytes) + (bytes32, uint256, uint256, uint256, bytes32, bytes32, uint32, uint8, uint8, bytes) ); return executionMode; } 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, 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 28c4fd197..83adb2b3b 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"; @@ -167,6 +167,7 @@ contract DstOFTHandler is BaseModuleHandler, ILayerZeroComposer, ArbitraryEVMFlo 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(); @@ -177,7 +178,8 @@ contract DstOFTHandler is BaseModuleHandler, ILayerZeroComposer, ArbitraryEVMFlo 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 diff --git a/contracts/periphery/mintburn/sponsored-oft/QuoteSignLib.sol b/contracts/periphery/mintburn/sponsored-oft/QuoteSignLib.sol index 135e8e4a6..6fd989bee 100644 --- a/contracts/periphery/mintburn/sponsored-oft/QuoteSignLib.sol +++ b/contracts/periphery/mintburn/sponsored-oft/QuoteSignLib.sol @@ -25,6 +25,7 @@ library QuoteSignLib { p.finalToken, p.lzReceiveGasLimit, p.lzComposeGasLimit, + 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 e1a6816fc..111994096 100644 --- a/contracts/periphery/mintburn/sponsored-oft/SponsoredOFTSrcPeriphery.sol +++ b/contracts/periphery/mintburn/sponsored-oft/SponsoredOFTSrcPeriphery.sol @@ -169,6 +169,7 @@ contract SponsoredOFTSrcPeriphery is Ownable { 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 8dc6922ff..3c0039606 100644 --- a/contracts/periphery/mintburn/sponsored-oft/Structs.sol +++ b/contracts/periphery/mintburn/sponsored-oft/Structs.sol @@ -11,6 +11,11 @@ enum ExecutionMode { ArbitraryActionsToEVM } +enum AccountCreationMode { + Standard, + FromUserFunds +} + /// @notice A structure with all the relevant information about a particular sponsored bridging flow order struct Quote { SignedQuoteParams signedParams; @@ -35,6 +40,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 + 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/mintburn/cctp/createSponsoredDeposit.sol b/script/mintburn/cctp/createSponsoredDeposit.sol index 9596dcb73..6d1169fec 100644 --- a/script/mintburn/cctp/createSponsoredDeposit.sol +++ b/script/mintburn/cctp/createSponsoredDeposit.sol @@ -99,6 +99,7 @@ contract CreateSponsoredDeposit is DeploymentUtils { 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(SponsoredCCTPInterface.AccountCreationMode.Standard), // Standard mode executionMode: uint8(SponsoredCCTPInterface.ExecutionMode.DirectToCore), // DirectToCore mode actionData: emptyActionData // Empty for DirectToCore mode }); @@ -134,6 +135,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 6b7f1c4fd..c6f37f979 100644 --- a/script/mintburn/oft/CreateSponsoredDeposit.s.sol +++ b/script/mintburn/oft/CreateSponsoredDeposit.s.sol @@ -33,6 +33,7 @@ library DebugQuoteSignLib { p.finalToken, p.lzReceiveGasLimit, p.lzComposeGasLimit, + p.accountCreationMode, p.executionMode, keccak256(p.actionData) // Hash the actionData to keep signature size reasonable ) @@ -200,6 +201,7 @@ contract CreateSponsoredDeposit is Script, Config { destinationDex: HyperCoreLib.CORE_SPOT_DEX_ID, lzReceiveGasLimit: lzReceiveGasLimit, lzComposeGasLimit: lzComposeGasLimit, + accountCreationMode: 0, executionMode: 0, actionData: "" }); @@ -268,6 +270,7 @@ contract CreateSponsoredDeposit is Script, Config { 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/HyperCoreFlowExecutor.t.sol b/test/evm/foundry/local/HyperCoreFlowExecutor.t.sol index b770f348c..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"; @@ -94,7 +94,8 @@ contract HyperCoreFlowExecutorTest is BaseSimulatorTest { finalToken: address(token), destinationDex: HyperCoreLib.CORE_SPOT_DEX_ID, maxBpsToSponsor: maxBpsToSponsor, - extraFeesIncurred: extraFeesIncurred + extraFeesIncurred: extraFeesIncurred, + accountCreationMode: AccountCreationMode.Standard }); } @@ -146,7 +147,8 @@ contract HyperCoreFlowExecutorTest is BaseSimulatorTest { 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 0f1c80cac..f47600628 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, + accountCreationMode: uint8(0), // Standard executionMode: uint8(0), // DirectToCore actionData: "" }); @@ -158,9 +159,13 @@ contract SponsoredOFTSrcPeripheryTest is Test { bytes32 gotFinalRecipient, bytes32 gotFinalToken, uint32 gotDestinationDex, + uint8 gotAccountCreationMode, uint8 gotExecutionMode, bytes memory gotActionData - ) = abi.decode(spComposeMsg, (bytes32, uint256, uint256, uint256, bytes32, bytes32, uint32, 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"); @@ -169,6 +174,7 @@ contract SponsoredOFTSrcPeripheryTest is Test { 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 4ae19b315..eaeacab07 100644 --- a/test/evm/foundry/local/SponsorredCCTPDstPeriphery.t.sol +++ b/test/evm/foundry/local/SponsorredCCTPDstPeriphery.t.sol @@ -144,6 +144,7 @@ contract SponsoredCCTPDstPeripheryTest is BaseSimulatorTest { quote.finalRecipient, quote.finalToken, quote.destinationDex, + quote.accountCreationMode, quote.executionMode, quote.actionData ); @@ -204,6 +205,7 @@ contract SponsoredCCTPDstPeripheryTest is BaseSimulatorTest { quote.finalRecipient, quote.finalToken, quote.destinationDex, + quote.accountCreationMode, quote.executionMode, keccak256(quote.actionData) ) @@ -234,6 +236,7 @@ contract SponsoredCCTPDstPeripheryTest is BaseSimulatorTest { finalRecipient: finalRecipient.toBytes32(), finalToken: address(usdc).toBytes32(), destinationDex: HyperCoreLib.CORE_SPOT_DEX_ID, + accountCreationMode: uint8(SponsoredCCTPInterface.AccountCreationMode.Standard), executionMode: uint8(SponsoredCCTPInterface.ExecutionMode.DirectToCore), actionData: bytes("") }); From bf69b2bce32d0b284f1b12cbb6f6008ef34f1094 Mon Sep 17 00:00:00 2001 From: Ihor Farion Date: Tue, 16 Dec 2025 23:28:51 -0800 Subject: [PATCH 11/21] correct read indicies + add test Signed-off-by: Ihor Farion --- contracts/external/libraries/BytesLib.sol | 19 ++++ .../sponsored-oft/ComposeMsgCodec.sol | 17 ++-- .../mintburn/sponsored-oft/Structs.sol | 5 - test/evm/foundry/local/ComposeMsgCodec.t.sol | 94 +++++++++++++++++++ 4 files changed, 119 insertions(+), 16 deletions(-) create mode 100644 test/evm/foundry/local/ComposeMsgCodec.t.sol 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/periphery/mintburn/sponsored-oft/ComposeMsgCodec.sol b/contracts/periphery/mintburn/sponsored-oft/ComposeMsgCodec.sol index 8f92329b9..1d8e48ef7 100644 --- a/contracts/periphery/mintburn/sponsored-oft/ComposeMsgCodec.sol +++ b/contracts/periphery/mintburn/sponsored-oft/ComposeMsgCodec.sol @@ -68,23 +68,18 @@ library ComposeMsgCodec { } function _getDestinationDex(bytes memory data) internal pure returns (uint32 v) { - return BytesLib.toUint32(data, DESTINATION_DEX_OFFSET); + // 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) { - (, , , , , , , uint8 accountCreationMode, , ) = abi.decode( - data, - (bytes32, uint256, uint256, uint256, bytes32, bytes32, uint32, uint8, uint8, bytes) - ); - return accountCreationMode; + // 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, uint32, uint8, 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) { diff --git a/contracts/periphery/mintburn/sponsored-oft/Structs.sol b/contracts/periphery/mintburn/sponsored-oft/Structs.sol index 3c0039606..ee88d7ffc 100644 --- a/contracts/periphery/mintburn/sponsored-oft/Structs.sol +++ b/contracts/periphery/mintburn/sponsored-oft/Structs.sol @@ -11,11 +11,6 @@ enum ExecutionMode { ArbitraryActionsToEVM } -enum AccountCreationMode { - Standard, - FromUserFunds -} - /// @notice A structure with all the relevant information about a particular sponsored bridging flow order struct Quote { SignedQuoteParams signedParams; 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)); + } +} From a393df4b57400d8e017023570a61b1db1d179724 Mon Sep 17 00:00:00 2001 From: Faisal Usmani Date: Wed, 17 Dec 2025 09:44:44 -0500 Subject: [PATCH 12/21] update cctp min msg length Signed-off-by: Faisal Usmani --- contracts/libraries/SponsoredCCTPQuoteLib.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/libraries/SponsoredCCTPQuoteLib.sol b/contracts/libraries/SponsoredCCTPQuoteLib.sol index 76715be25..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 = 696; + uint256 private constant MIN_MSG_BYTES_LENGTH = 728; /** * @notice Gets the data for the deposit for burn. From 7624b3c1b769187588478b8394a9ce5b37a89582 Mon Sep 17 00:00:00 2001 From: Faisal Usmani Date: Wed, 17 Dec 2025 09:54:01 -0500 Subject: [PATCH 13/21] updated SponsoredDepositForBurn with new fields Signed-off-by: Faisal Usmani --- contracts/interfaces/SponsoredCCTPInterface.sol | 2 ++ .../mintburn/sponsored-cctp/SponsoredCCTPSrcPeriphery.sol | 2 ++ 2 files changed, 4 insertions(+) diff --git a/contracts/interfaces/SponsoredCCTPInterface.sol b/contracts/interfaces/SponsoredCCTPInterface.sol index dd408da4f..e2559facb 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 ); 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 ); } From 2e5bb7e17f5c7d7f0767ab871702cda038dc4c96 Mon Sep 17 00:00:00 2001 From: Ihor Farion Date: Wed, 17 Dec 2025 10:16:58 -0800 Subject: [PATCH 14/21] remove duplicate check from 2 places Signed-off-by: Ihor Farion --- .../mintburn/HyperCoreFlowExecutor.sol | 30 ++++++++----------- 1 file changed, 13 insertions(+), 17 deletions(-) diff --git a/contracts/periphery/mintburn/HyperCoreFlowExecutor.sol b/contracts/periphery/mintburn/HyperCoreFlowExecutor.sol index 46beb5b8d..fb8c5d583 100644 --- a/contracts/periphery/mintburn/HyperCoreFlowExecutor.sol +++ b/contracts/periphery/mintburn/HyperCoreFlowExecutor.sol @@ -504,14 +504,12 @@ contract HyperCoreFlowExecutor is AccessControlUpgradeable, AuthorizedFundedFlow uint64 accountActivationFeeCore; if (!userAccountExists) { if (params.accountCreationMode == AccountCreationMode.Standard) { - if (!HyperCoreLib.coreUserExists(params.finalRecipient)) { - if (params.maxBpsToSponsor > 0) { - revert AccountNotActivatedError(params.finalRecipient); - } else { - emit AccountNotActivated(params.quoteNonce, params.finalRecipient); - _fallbackHyperEVMFlow(params); - return; - } + if (params.maxBpsToSponsor > 0) { + revert AccountNotActivatedError(params.finalRecipient); + } else { + emit AccountNotActivated(params.quoteNonce, params.finalRecipient); + _fallbackHyperEVMFlow(params); + return; } } else { // Notice. If the amount is too small to be able to activate an account, the funds will land there, but @@ -614,15 +612,13 @@ contract HyperCoreFlowExecutor is AccessControlUpgradeable, AuthorizedFundedFlow bool userAccountExists = HyperCoreLib.coreUserExists(params.finalRecipient); if (!userAccountExists) { if (params.accountCreationMode == AccountCreationMode.Standard) { - if (!HyperCoreLib.coreUserExists(params.finalRecipient)) { - if (params.maxBpsToSponsor > 0) { - revert AccountNotActivatedError(params.finalRecipient); - } else { - emit AccountNotActivated(params.quoteNonce, params.finalRecipient); - params.finalToken = initialToken; - _fallbackHyperEVMFlow(params); - return; - } + if (params.maxBpsToSponsor > 0) { + revert AccountNotActivatedError(params.finalRecipient); + } else { + emit AccountNotActivated(params.quoteNonce, params.finalRecipient); + params.finalToken = initialToken; + _fallbackHyperEVMFlow(params); + return; } } else { // In the FromUserFunds logic for the swap flow, we only allow account creation if the final token is From 2218bf737bc349e3090780a9786a6fea4e527f80 Mon Sep 17 00:00:00 2001 From: Ihor Farion Date: Wed, 17 Dec 2025 11:10:01 -0800 Subject: [PATCH 15/21] fix params.commonParams.amountInEVM == 0 edge case Signed-off-by: Ihor Farion --- contracts/periphery/mintburn/ArbitraryEVMFlowExecutor.sol | 2 +- .../mintburn/sponsored-cctp/SponsoredCCTPDstPeriphery.sol | 5 +++++ contracts/periphery/mintburn/sponsored-oft/DstOFTHandler.sol | 5 +++++ 3 files changed, 11 insertions(+), 1 deletion(-) 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/sponsored-cctp/SponsoredCCTPDstPeriphery.sol b/contracts/periphery/mintburn/sponsored-cctp/SponsoredCCTPDstPeriphery.sol index 8d02231c3..3796c436f 100644 --- a/contracts/periphery/mintburn/sponsored-cctp/SponsoredCCTPDstPeriphery.sol +++ b/contracts/periphery/mintburn/sponsored-cctp/SponsoredCCTPDstPeriphery.sol @@ -255,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-oft/DstOFTHandler.sol b/contracts/periphery/mintburn/sponsored-oft/DstOFTHandler.sol index 83adb2b3b..c80ec6eeb 100644 --- a/contracts/periphery/mintburn/sponsored-oft/DstOFTHandler.sol +++ b/contracts/periphery/mintburn/sponsored-oft/DstOFTHandler.sol @@ -205,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 From ddf951ac3b6766b6603a6874ddbef855468f8a4b Mon Sep 17 00:00:00 2001 From: Ihor Farion Date: Thu, 18 Dec 2025 13:37:17 -0800 Subject: [PATCH 16/21] add return after falling back to EVM flow Signed-off-by: Ihor Farion --- contracts/periphery/mintburn/HyperCoreFlowExecutor.sol | 1 + 1 file changed, 1 insertion(+) diff --git a/contracts/periphery/mintburn/HyperCoreFlowExecutor.sol b/contracts/periphery/mintburn/HyperCoreFlowExecutor.sol index fb8c5d583..ce59322fe 100644 --- a/contracts/periphery/mintburn/HyperCoreFlowExecutor.sol +++ b/contracts/periphery/mintburn/HyperCoreFlowExecutor.sol @@ -628,6 +628,7 @@ contract HyperCoreFlowExecutor is AccessControlUpgradeable, AuthorizedFundedFlow emit AccountNotActivated(params.quoteNonce, params.finalRecipient); params.finalToken = initialToken; _fallbackHyperEVMFlow(params); + return; } } } From 2c97a9528cbc107a96ae4619c991afaca6a1e712 Mon Sep 17 00:00:00 2001 From: Ihor Farion Date: Thu, 18 Dec 2025 14:28:53 -0800 Subject: [PATCH 17/21] fix account activation amounts logic for when finalizing the swap flow Signed-off-by: Ihor Farion --- .../mintburn/HyperCoreFlowExecutor.sol | 84 ++++++++++++++++--- 1 file changed, 71 insertions(+), 13 deletions(-) diff --git a/contracts/periphery/mintburn/HyperCoreFlowExecutor.sol b/contracts/periphery/mintburn/HyperCoreFlowExecutor.sol index ce59322fe..c5089e257 100644 --- a/contracts/periphery/mintburn/HyperCoreFlowExecutor.sol +++ b/contracts/periphery/mintburn/HyperCoreFlowExecutor.sol @@ -231,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 AccountActivatedFromUserBalance( + 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 @@ -284,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 * **************************************/ @@ -584,6 +604,15 @@ contract HyperCoreFlowExecutor is AccessControlUpgradeable, AuthorizedFundedFlow accountActivationFeeCore ); + if (accountActivationFeeCore > 0) { + emit AccountActivatedFromUserBalance( + params.quoteNonce, + params.finalRecipient, + finalToken, + accountActivationFeeCore + ); + } + emit SimpleTransferFlowCompleted( params.quoteNonce, params.finalRecipient, @@ -839,23 +868,33 @@ 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 `FromUserBalance`. 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) + if (!finalCoreTokenInfo.canBeUsedForAccountActivation) revert 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, @@ -865,10 +904,20 @@ contract HyperCoreFlowExecutor is AccessControlUpgradeable, AuthorizedFundedFlow swapHandler.transferFundsToUserOnCore( finalCoreTokenInfo.coreIndex, swap.finalRecipient, - totalToSend, + amountToTransfer, swap.destinationDex ); - emit SwapFlowFinalized(quoteNonce, swap.finalRecipient, swap.finalToken, totalToSend, additionalToSendEVM); + + if (accountActivationFee > 0) { + emit AccountActivatedFromUserBalance( + 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 @@ -1072,24 +1121,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 + 1, InsufficientFundsForActivation()); + + amountToTransfer = totalAttributableToUser - accountActivationFee; } /// @notice Reads the current spot price from HyperLiquid and applies a configured suggested discount for faster execution From c1a8b96b7b99aeee8b7900311cda8301d5966c11 Mon Sep 17 00:00:00 2001 From: Ihor Farion Date: Thu, 18 Dec 2025 15:43:10 -0800 Subject: [PATCH 18/21] simplify if conditions Signed-off-by: Ihor Farion --- .../interfaces/SponsoredCCTPInterface.sol | 5 -- .../mintburn/HyperCoreFlowExecutor.sol | 84 ++++++++----------- 2 files changed, 36 insertions(+), 53 deletions(-) diff --git a/contracts/interfaces/SponsoredCCTPInterface.sol b/contracts/interfaces/SponsoredCCTPInterface.sol index e2559facb..fd3c10ab4 100644 --- a/contracts/interfaces/SponsoredCCTPInterface.sol +++ b/contracts/interfaces/SponsoredCCTPInterface.sol @@ -48,11 +48,6 @@ interface SponsoredCCTPInterface { ArbitraryActionsToEVM } - enum AccountCreationMode { - Standard, - FromUserFunds - } - // Params that will be used to create a sponsored CCTP quote and deposit for burn. struct SponsoredCCTPQuote { // The domain ID of the source chain. diff --git a/contracts/periphery/mintburn/HyperCoreFlowExecutor.sol b/contracts/periphery/mintburn/HyperCoreFlowExecutor.sol index c5089e257..7b397a1e5 100644 --- a/contracts/periphery/mintburn/HyperCoreFlowExecutor.sol +++ b/contracts/periphery/mintburn/HyperCoreFlowExecutor.sol @@ -238,7 +238,7 @@ contract HyperCoreFlowExecutor is AccessControlUpgradeable, AuthorizedFundedFlow * @param token The token used to fund the account activation * @param amountCore The amount paid for activation (in Core token units) */ - event AccountActivatedFromUserBalance( + event AccountActivatedFromUserFunds( bytes32 indexed quoteNonce, address indexed user, address indexed token, @@ -520,26 +520,25 @@ contract HyperCoreFlowExecutor is AccessControlUpgradeable, AuthorizedFundedFlow MainStorage storage $ = _getMainStorage(); CoreTokenInfo memory coreTokenInfo = $.coreTokenInfos[finalToken]; - bool userAccountExists = HyperCoreLib.coreUserExists(params.finalRecipient); uint64 accountActivationFeeCore; - if (!userAccountExists) { - if (params.accountCreationMode == AccountCreationMode.Standard) { - if (params.maxBpsToSponsor > 0) { - revert AccountNotActivatedError(params.finalRecipient); - } else { - emit AccountNotActivated(params.quoteNonce, params.finalRecipient); - _fallbackHyperEVMFlow(params); - return; - } - } else { - // Notice. If the amount is too small to be able to activate an account, the funds will land there, but - // the account won't show up as active until an eligible transfer activates it - if (!coreTokenInfo.canBeUsedForAccountActivation) { - _fallbackHyperEVMFlow(params); - return; - } - accountActivationFeeCore = coreTokenInfo.accountActivationFeeCore; + if (!HyperCoreLib.coreUserExists(params.finalRecipient)) { + bool isStandard = params.accountCreationMode == AccountCreationMode.Standard; + + // Standard, sponsored + if (isStandard && params.maxBpsToSponsor > 0) { + revert AccountNotActivatedError(params.finalRecipient); } + + // 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; } uint256 amountToSponsor; @@ -605,7 +604,7 @@ contract HyperCoreFlowExecutor is AccessControlUpgradeable, AuthorizedFundedFlow ); if (accountActivationFeeCore > 0) { - emit AccountActivatedFromUserBalance( + emit AccountActivatedFromUserFunds( params.quoteNonce, params.finalRecipient, finalToken, @@ -638,27 +637,21 @@ contract HyperCoreFlowExecutor is AccessControlUpgradeable, AuthorizedFundedFlow CoreTokenInfo memory finalCoreTokenInfo = $.coreTokenInfos[params.finalToken]; FinalTokenInfo memory finalTokenInfo = _getExistingFinalTokenInfo(params.finalToken); - bool userAccountExists = HyperCoreLib.coreUserExists(params.finalRecipient); - if (!userAccountExists) { - if (params.accountCreationMode == AccountCreationMode.Standard) { - if (params.maxBpsToSponsor > 0) { - revert AccountNotActivatedError(params.finalRecipient); - } else { - emit AccountNotActivated(params.quoteNonce, params.finalRecipient); - params.finalToken = initialToken; - _fallbackHyperEVMFlow(params); - return; - } - } else { - // In the FromUserFunds logic for the swap flow, we only allow account creation if the final token is - // usable for it. Finilazing the swap flow should handle account creation automatically when doing the - // final transfer of tokens to the user - if (!finalCoreTokenInfo.canBeUsedForAccountActivation) { - emit AccountNotActivated(params.quoteNonce, params.finalRecipient); - params.finalToken = initialToken; - _fallbackHyperEVMFlow(params); - return; - } + if (!HyperCoreLib.coreUserExists(params.finalRecipient)) { + bool isStandard = params.accountCreationMode == AccountCreationMode.Standard; + + // Standard, sponsored + if (isStandard && params.maxBpsToSponsor > 0) { + revert AccountNotActivatedError(params.finalRecipient); + } + + // 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); + return; } } @@ -869,7 +862,7 @@ contract HyperCoreFlowExecutor is AccessControlUpgradeable, AuthorizedFundedFlow if (swap.finalToken != finalToken) revert WrongSwapFinalizationToken(quoteNonce); uint64 accountActivationFee; - // User account can be absent if our AccountCreationMode is `FromUserBalance`. We need to adjust our transfer amount to user based on this + // 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) if (!finalCoreTokenInfo.canBeUsedForAccountActivation) revert TokenNotEligibleForActivation(); @@ -909,12 +902,7 @@ contract HyperCoreFlowExecutor is AccessControlUpgradeable, AuthorizedFundedFlow ); if (accountActivationFee > 0) { - emit AccountActivatedFromUserBalance( - quoteNonce, - swap.finalRecipient, - swap.finalToken, - accountActivationFee - ); + emit AccountActivatedFromUserFunds(quoteNonce, swap.finalRecipient, swap.finalToken, accountActivationFee); } emit SwapFlowFinalized(quoteNonce, swap.finalRecipient, swap.finalToken, amountToTransfer, additionalToSendEVM); From c10388e162ebffcfdd9f5ccbd0922c929e2aaf59 Mon Sep 17 00:00:00 2001 From: Ihor Farion Date: Thu, 18 Dec 2025 16:30:56 -0800 Subject: [PATCH 19/21] fix some problems and introduce a stack-too-deep :) Signed-off-by: Ihor Farion --- contracts/periphery/mintburn/sponsored-oft/QuoteSignLib.sol | 1 + script/mintburn/cctp/createSponsoredDeposit.sol | 3 ++- test/evm/foundry/local/SponsorredCCTPDstPeriphery.t.sol | 3 ++- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/contracts/periphery/mintburn/sponsored-oft/QuoteSignLib.sol b/contracts/periphery/mintburn/sponsored-oft/QuoteSignLib.sol index 6fd989bee..eaada9a05 100644 --- a/contracts/periphery/mintburn/sponsored-oft/QuoteSignLib.sol +++ b/contracts/periphery/mintburn/sponsored-oft/QuoteSignLib.sol @@ -23,6 +23,7 @@ library QuoteSignLib { p.maxBpsToSponsor, p.finalRecipient, p.finalToken, + p.destinationDex, p.lzReceiveGasLimit, p.lzComposeGasLimit, p.accountCreationMode, diff --git a/script/mintburn/cctp/createSponsoredDeposit.sol b/script/mintburn/cctp/createSponsoredDeposit.sol index 6d1169fec..ecc6131fe 100644 --- a/script/mintburn/cctp/createSponsoredDeposit.sol +++ b/script/mintburn/cctp/createSponsoredDeposit.sol @@ -7,6 +7,7 @@ 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"; @@ -99,7 +100,7 @@ contract CreateSponsoredDeposit is DeploymentUtils { 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(SponsoredCCTPInterface.AccountCreationMode.Standard), // Standard mode + accountCreationMode: uint8(AccountCreationMode.Standard), // Standard mode executionMode: uint8(SponsoredCCTPInterface.ExecutionMode.DirectToCore), // DirectToCore mode actionData: emptyActionData // Empty for DirectToCore mode }); diff --git a/test/evm/foundry/local/SponsorredCCTPDstPeriphery.t.sol b/test/evm/foundry/local/SponsorredCCTPDstPeriphery.t.sol index eaeacab07..136fc8793 100644 --- a/test/evm/foundry/local/SponsorredCCTPDstPeriphery.t.sol +++ b/test/evm/foundry/local/SponsorredCCTPDstPeriphery.t.sol @@ -2,6 +2,7 @@ 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"; @@ -236,7 +237,7 @@ contract SponsoredCCTPDstPeripheryTest is BaseSimulatorTest { finalRecipient: finalRecipient.toBytes32(), finalToken: address(usdc).toBytes32(), destinationDex: HyperCoreLib.CORE_SPOT_DEX_ID, - accountCreationMode: uint8(SponsoredCCTPInterface.AccountCreationMode.Standard), + accountCreationMode: uint8(AccountCreationMode.Standard), executionMode: uint8(SponsoredCCTPInterface.ExecutionMode.DirectToCore), actionData: bytes("") }); From 5784205a906c1d33fea1509406976ad2983c21dc Mon Sep 17 00:00:00 2001 From: Ihor Farion Date: Thu, 18 Dec 2025 16:50:04 -0800 Subject: [PATCH 20/21] fix stack too deep Signed-off-by: Ihor Farion --- .../mintburn/sponsored-oft/QuoteSignLib.sol | 47 +++++++++++-------- .../mintburn/oft/CreateSponsoredDeposit.s.sol | 45 ++++++++++-------- 2 files changed, 53 insertions(+), 39 deletions(-) diff --git a/contracts/periphery/mintburn/sponsored-oft/QuoteSignLib.sol b/contracts/periphery/mintburn/sponsored-oft/QuoteSignLib.sol index eaada9a05..e1c2dbe02 100644 --- a/contracts/periphery/mintburn/sponsored-oft/QuoteSignLib.sol +++ b/contracts/periphery/mintburn/sponsored-oft/QuoteSignLib.sol @@ -11,26 +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.destinationDex, - p.lzReceiveGasLimit, - p.lzComposeGasLimit, - p.accountCreationMode, - 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/script/mintburn/oft/CreateSponsoredDeposit.s.sol b/script/mintburn/oft/CreateSponsoredDeposit.s.sol index c6f37f979..cd4b337bf 100644 --- a/script/mintburn/oft/CreateSponsoredDeposit.s.sol +++ b/script/mintburn/oft/CreateSponsoredDeposit.s.sol @@ -19,25 +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.accountCreationMode, - 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). From c4dd51e67d505e79eb048ae546c32ad13d05b498 Mon Sep 17 00:00:00 2001 From: Ihor Farion Date: Thu, 18 Dec 2025 20:40:18 -0800 Subject: [PATCH 21/21] smh fix Signed-off-by: Ihor Farion --- contracts/periphery/mintburn/HyperCoreFlowExecutor.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/periphery/mintburn/HyperCoreFlowExecutor.sol b/contracts/periphery/mintburn/HyperCoreFlowExecutor.sol index 7b397a1e5..476a43d7b 100644 --- a/contracts/periphery/mintburn/HyperCoreFlowExecutor.sol +++ b/contracts/periphery/mintburn/HyperCoreFlowExecutor.sol @@ -865,7 +865,7 @@ contract HyperCoreFlowExecutor is AccessControlUpgradeable, AuthorizedFundedFlow // 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) - if (!finalCoreTokenInfo.canBeUsedForAccountActivation) revert TokenNotEligibleForActivation(); + require(finalCoreTokenInfo.canBeUsedForAccountActivation, TokenNotEligibleForActivation()); accountActivationFee = finalCoreTokenInfo.accountActivationFeeCore; } @@ -1133,7 +1133,7 @@ contract HyperCoreFlowExecutor is AccessControlUpgradeable, AuthorizedFundedFlow } // 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 + 1, InsufficientFundsForActivation()); + require(totalAttributableToUser > accountActivationFee, InsufficientFundsForActivation()); amountToTransfer = totalAttributableToUser - accountActivationFee; }