From efa6070d6244beb0deabbdeed35e1d80842ab687 Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Wed, 17 Dec 2025 00:32:37 +0100 Subject: [PATCH 1/8] fix deploy files --- .../crosschain/AbstractCCTPIntegrator.sol | 183 ++---------------- .../crosschain/AbstractCCTPStrategy.sol | 172 ++++++++++++++++ .../crosschain/CrossChainMasterStrategy.sol | 20 +- .../crosschain/CrossChainRemoteStrategy.sol | 22 +-- contracts/deploy/deployActions.js | 28 +-- 5 files changed, 216 insertions(+), 209 deletions(-) create mode 100644 contracts/contracts/strategies/crosschain/AbstractCCTPStrategy.sol diff --git a/contracts/contracts/strategies/crosschain/AbstractCCTPIntegrator.sol b/contracts/contracts/strategies/crosschain/AbstractCCTPIntegrator.sol index 2bae1382d4..147db8f5f7 100644 --- a/contracts/contracts/strategies/crosschain/AbstractCCTPIntegrator.sol +++ b/contracts/contracts/strategies/crosschain/AbstractCCTPIntegrator.sol @@ -64,25 +64,27 @@ abstract contract AbstractCCTPIntegrator is Governable, IMessageHandlerV2 { _; } + struct CCTPIntegrationConfig { + address cctpTokenMessenger; + address cctpMessageTransmitter; + uint32 destinationDomain; + address destinationStrategy; + address baseToken; + address cctpHookWrapper; + } + constructor( - address _cctpTokenMessenger, - address _cctpMessageTransmitter, - uint32 _destinationDomain, - address _destinationStrategy, - address _baseToken, - address _cctpHookWrapper + CCTPIntegrationConfig memory _config ) { - cctpTokenMessenger = ICCTPTokenMessenger(_cctpTokenMessenger); - cctpMessageTransmitter = ICCTPMessageTransmitter( - _cctpMessageTransmitter - ); - destinationDomain = _destinationDomain; - destinationStrategy = _destinationStrategy; - baseToken = _baseToken; - cctpHookWrapper = _cctpHookWrapper; + cctpTokenMessenger = ICCTPTokenMessenger(_config.cctpTokenMessenger); + cctpMessageTransmitter = ICCTPMessageTransmitter(_config.cctpMessageTransmitter); + destinationDomain = _config.destinationDomain; + destinationStrategy = _config.destinationStrategy; + baseToken = _config.baseToken; + cctpHookWrapper = _config.cctpHookWrapper; // Just a sanity check to ensure the base token is USDC - uint256 _baseTokenDecimals = Helpers.getDecimals(_baseToken); + uint256 _baseTokenDecimals = Helpers.getDecimals(_config.baseToken); require(_baseTokenDecimals == 6, "Base token decimals must be 6"); } @@ -267,157 +269,6 @@ abstract contract AbstractCCTPIntegrator is Governable, IMessageHandlerV2 { return message.extractSlice(8, message.length); } - function _encodeDepositMessage(uint64 nonce, uint256 depositAmount) - internal - virtual - returns (bytes memory) - { - return - abi.encodePacked( - ORIGIN_MESSAGE_VERSION, - DEPOSIT_MESSAGE, - abi.encode(nonce, depositAmount) - ); - } - - function _decodeDepositMessage(bytes memory message) - internal - virtual - returns (uint64, uint256) - { - _verifyMessageVersionAndType(message, ORIGIN_MESSAGE_VERSION, DEPOSIT_MESSAGE); - - (uint64 nonce, uint256 depositAmount) = abi.decode( - _getMessagePayload(message), - (uint64, uint256) - ); - return (nonce, depositAmount); - } - - function _encodeDepositAckMessage( - uint64 nonce, - uint256 amountReceived, - uint256 feeExecuted, - uint256 balanceAfter - ) internal virtual returns (bytes memory) { - return - abi.encodePacked( - ORIGIN_MESSAGE_VERSION, - DEPOSIT_ACK_MESSAGE, - abi.encode(nonce, amountReceived, feeExecuted, balanceAfter) - ); - } - - function _decodeDepositAckMessage(bytes memory message) - internal - virtual - returns ( - uint64, - uint256, - uint256, - uint256 - ) - { - _verifyMessageVersionAndType(message, ORIGIN_MESSAGE_VERSION, DEPOSIT_ACK_MESSAGE); - - ( - uint64 nonce, - uint256 amountReceived, - uint256 feeExecuted, - uint256 balanceAfter - ) = abi.decode( - _getMessagePayload(message), - (uint64, uint256, uint256, uint256) - ); - - return (nonce, amountReceived, feeExecuted, balanceAfter); - } - - function _encodeWithdrawMessage(uint64 nonce, uint256 withdrawAmount) - internal - virtual - returns (bytes memory) - { - return - abi.encodePacked( - ORIGIN_MESSAGE_VERSION, - WITHDRAW_MESSAGE, - abi.encode(nonce, withdrawAmount) - ); - } - - function _decodeWithdrawMessage(bytes memory message) - internal - virtual - returns (uint64, uint256) - { - _verifyMessageVersionAndType(message, ORIGIN_MESSAGE_VERSION, WITHDRAW_MESSAGE); - - (uint64 nonce, uint256 withdrawAmount) = abi.decode( - _getMessagePayload(message), - (uint64, uint256) - ); - return (nonce, withdrawAmount); - } - - function _encodeWithdrawAckMessage( - uint64 nonce, - uint256 amountSent, - uint256 balanceAfter - ) internal virtual returns (bytes memory) { - return - abi.encodePacked( - ORIGIN_MESSAGE_VERSION, - WITHDRAW_ACK_MESSAGE, - abi.encode(nonce, amountSent, balanceAfter) - ); - } - - function _decodeWithdrawAckMessage(bytes memory message) - internal - virtual - returns ( - uint64, - uint256, - uint256 - ) - { - _verifyMessageVersionAndType(message, ORIGIN_MESSAGE_VERSION, WITHDRAW_ACK_MESSAGE); - - (uint64 nonce, uint256 amountSent, uint256 balanceAfter) = abi.decode( - _getMessagePayload(message), - (uint64, uint256, uint256) - ); - return (nonce, amountSent, balanceAfter); - } - - function _encodeBalanceCheckMessage(uint64 nonce, uint256 balance) - internal - virtual - returns (bytes memory) - { - return - abi.encodePacked( - ORIGIN_MESSAGE_VERSION, - BALANCE_CHECK_MESSAGE, - abi.encode(nonce, balance) - ); - } - - function _decodeBalanceCheckMessage(bytes memory message) - internal - virtual - returns (uint64, uint256) - { - _verifyMessageVersionAndType(message, ORIGIN_MESSAGE_VERSION, BALANCE_CHECK_MESSAGE); - - (uint64 nonce, uint256 balance) = abi.decode( - _getMessagePayload(message), - (uint64, uint256) - ); - return (nonce, balance); - } - function _sendMessage(bytes memory message) internal virtual { cctpMessageTransmitter.sendMessage( destinationDomain, diff --git a/contracts/contracts/strategies/crosschain/AbstractCCTPStrategy.sol b/contracts/contracts/strategies/crosschain/AbstractCCTPStrategy.sol new file mode 100644 index 0000000000..020cdb06b3 --- /dev/null +++ b/contracts/contracts/strategies/crosschain/AbstractCCTPStrategy.sol @@ -0,0 +1,172 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +/** + * @title AbstractCCTPStrategy - Abstract contract for CCTP strategies + * @author Origin Protocol Inc + */ + +import { AbstractCCTPIntegrator } from "./AbstractCCTPIntegrator.sol"; + +abstract contract AbstractCCTPStrategy is + AbstractCCTPIntegrator +{ + constructor( + CCTPIntegrationConfig memory _config + ) + AbstractCCTPIntegrator( + _config + ) + {} + + function _encodeDepositMessage(uint64 nonce, uint256 depositAmount) + internal + virtual + returns (bytes memory) + { + return + abi.encodePacked( + ORIGIN_MESSAGE_VERSION, + DEPOSIT_MESSAGE, + abi.encode(nonce, depositAmount) + ); + } + + function _decodeDepositMessage(bytes memory message) + internal + virtual + returns (uint64, uint256) + { + _verifyMessageVersionAndType(message, ORIGIN_MESSAGE_VERSION, DEPOSIT_MESSAGE); + + (uint64 nonce, uint256 depositAmount) = abi.decode( + _getMessagePayload(message), + (uint64, uint256) + ); + return (nonce, depositAmount); + } + + function _encodeDepositAckMessage( + uint64 nonce, + uint256 amountReceived, + uint256 feeExecuted, + uint256 balanceAfter + ) internal virtual returns (bytes memory) { + return + abi.encodePacked( + ORIGIN_MESSAGE_VERSION, + DEPOSIT_ACK_MESSAGE, + abi.encode(nonce, amountReceived, feeExecuted, balanceAfter) + ); + } + + function _decodeDepositAckMessage(bytes memory message) + internal + virtual + returns ( + uint64, + uint256, + uint256, + uint256 + ) + { + _verifyMessageVersionAndType(message, ORIGIN_MESSAGE_VERSION, DEPOSIT_ACK_MESSAGE); + + ( + uint64 nonce, + uint256 amountReceived, + uint256 feeExecuted, + uint256 balanceAfter + ) = abi.decode( + _getMessagePayload(message), + (uint64, uint256, uint256, uint256) + ); + + return (nonce, amountReceived, feeExecuted, balanceAfter); + } + + function _encodeWithdrawMessage(uint64 nonce, uint256 withdrawAmount) + internal + virtual + returns (bytes memory) + { + return + abi.encodePacked( + ORIGIN_MESSAGE_VERSION, + WITHDRAW_MESSAGE, + abi.encode(nonce, withdrawAmount) + ); + } + + function _decodeWithdrawMessage(bytes memory message) + internal + virtual + returns (uint64, uint256) + { + _verifyMessageVersionAndType(message, ORIGIN_MESSAGE_VERSION, WITHDRAW_MESSAGE); + + (uint64 nonce, uint256 withdrawAmount) = abi.decode( + _getMessagePayload(message), + (uint64, uint256) + ); + return (nonce, withdrawAmount); + } + + function _encodeWithdrawAckMessage( + uint64 nonce, + uint256 amountSent, + uint256 balanceAfter + ) internal virtual returns (bytes memory) { + return + abi.encodePacked( + ORIGIN_MESSAGE_VERSION, + WITHDRAW_ACK_MESSAGE, + abi.encode(nonce, amountSent, balanceAfter) + ); + } + + function _decodeWithdrawAckMessage(bytes memory message) + internal + virtual + returns ( + uint64, + uint256, + uint256 + ) + { + _verifyMessageVersionAndType(message, ORIGIN_MESSAGE_VERSION, WITHDRAW_ACK_MESSAGE); + + (uint64 nonce, uint256 amountSent, uint256 balanceAfter) = abi.decode( + _getMessagePayload(message), + (uint64, uint256, uint256) + ); + return (nonce, amountSent, balanceAfter); + } + + function _encodeBalanceCheckMessage(uint64 nonce, uint256 balance) + internal + virtual + returns (bytes memory) + { + return + abi.encodePacked( + ORIGIN_MESSAGE_VERSION, + BALANCE_CHECK_MESSAGE, + abi.encode(nonce, balance) + ); + } + + function _decodeBalanceCheckMessage(bytes memory message) + internal + virtual + returns (uint64, uint256) + { + _verifyMessageVersionAndType(message, ORIGIN_MESSAGE_VERSION, BALANCE_CHECK_MESSAGE); + + (uint64 nonce, uint256 balance) = abi.decode( + _getMessagePayload(message), + (uint64, uint256) + ); + return (nonce, balance); + } +} diff --git a/contracts/contracts/strategies/crosschain/CrossChainMasterStrategy.sol b/contracts/contracts/strategies/crosschain/CrossChainMasterStrategy.sol index 2736a175ce..d5a03e292b 100644 --- a/contracts/contracts/strategies/crosschain/CrossChainMasterStrategy.sol +++ b/contracts/contracts/strategies/crosschain/CrossChainMasterStrategy.sol @@ -11,12 +11,12 @@ pragma solidity ^0.8.0; import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import { IERC20, InitializableAbstractStrategy } from "../../utils/InitializableAbstractStrategy.sol"; -import { AbstractCCTPIntegrator } from "./AbstractCCTPIntegrator.sol"; +import { AbstractCCTPStrategy } from "./AbstractCCTPStrategy.sol"; import { BytesHelper } from "../../utils/BytesHelper.sol"; contract CrossChainMasterStrategy is InitializableAbstractStrategy, - AbstractCCTPIntegrator + AbstractCCTPStrategy { using SafeERC20 for IERC20; @@ -34,21 +34,11 @@ contract CrossChainMasterStrategy is */ constructor( BaseStrategyConfig memory _stratConfig, - address _cctpTokenMessenger, - address _cctpMessageTransmitter, - uint32 _destinationDomain, - address _destinationStrategy, - address _baseToken, - address _cctpHookWrapper + CCTPIntegrationConfig memory _cctpConfig ) InitializableAbstractStrategy(_stratConfig) - AbstractCCTPIntegrator( - _cctpTokenMessenger, - _cctpMessageTransmitter, - _destinationDomain, - _destinationStrategy, - _baseToken, - _cctpHookWrapper + AbstractCCTPStrategy( + _cctpConfig ) {} diff --git a/contracts/contracts/strategies/crosschain/CrossChainRemoteStrategy.sol b/contracts/contracts/strategies/crosschain/CrossChainRemoteStrategy.sol index 5e4952a1f2..27963f1cf2 100644 --- a/contracts/contracts/strategies/crosschain/CrossChainRemoteStrategy.sol +++ b/contracts/contracts/strategies/crosschain/CrossChainRemoteStrategy.sol @@ -12,32 +12,22 @@ pragma solidity ^0.8.0; import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import { IERC20 } from "../../utils/InitializableAbstractStrategy.sol"; import { Generalized4626Strategy } from "../Generalized4626Strategy.sol"; -import { AbstractCCTPIntegrator } from "./AbstractCCTPIntegrator.sol"; +import { AbstractCCTPStrategy } from "./AbstractCCTPStrategy.sol"; contract CrossChainRemoteStrategy is - AbstractCCTPIntegrator, + AbstractCCTPStrategy, Generalized4626Strategy { using SafeERC20 for IERC20; constructor( BaseStrategyConfig memory _baseConfig, - address _cctpTokenMessenger, - address _cctpMessageTransmitter, - uint32 _destinationDomain, - address _destinationStrategy, - address _baseToken, - address _cctpHookWrapper + CCTPIntegrationConfig memory _cctpConfig ) - AbstractCCTPIntegrator( - _cctpTokenMessenger, - _cctpMessageTransmitter, - _destinationDomain, - _destinationStrategy, - _baseToken, - _cctpHookWrapper + AbstractCCTPStrategy( + _cctpConfig ) - Generalized4626Strategy(_baseConfig, _baseToken) + Generalized4626Strategy(_baseConfig, _cctpConfig.baseToken) {} // solhint-disable-next-line no-unused-vars diff --git a/contracts/deploy/deployActions.js b/contracts/deploy/deployActions.js index 9805aada81..1adbd15eb0 100644 --- a/contracts/deploy/deployActions.js +++ b/contracts/deploy/deployActions.js @@ -1779,12 +1779,14 @@ const deployCrossChainMasterStrategyImpl = async ( deployerAddr, // vault address // addresses.mainnet.VaultProxy, ], - addresses.CCTPTokenMessengerV2, - addresses.CCTPMessageTransmitterV2, - targetDomainId, - remoteStrategyAddress, - baseToken, - hookWrapperAddress, + [ + addresses.CCTPTokenMessengerV2, + addresses.CCTPMessageTransmitterV2, + targetDomainId, + remoteStrategyAddress, + baseToken, + hookWrapperAddress, + ] ] ); @@ -1840,12 +1842,14 @@ const deployCrossChainRemoteStrategyImpl = async ( deployerAddr, // vault address // addresses.mainnet.VaultProxy, ], - addresses.CCTPTokenMessengerV2, - addresses.CCTPMessageTransmitterV2, - targetDomainId, - remoteStrategyAddress, - baseToken, - hookWrapperAddress, + [ + addresses.CCTPTokenMessengerV2, + addresses.CCTPMessageTransmitterV2, + targetDomainId, + remoteStrategyAddress, + baseToken, + hookWrapperAddress, + ] ] ); From 29ad94ab6451d7e80e33f4d9c2df5c0ad2143fac Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Wed, 17 Dec 2025 00:35:17 +0100 Subject: [PATCH 2/8] minor rename --- ...tractCCTPStrategy.sol => AbstractCCTPMorphoStrategy.sol} | 4 ++-- .../strategies/crosschain/CrossChainMasterStrategy.sol | 6 +++--- .../strategies/crosschain/CrossChainRemoteStrategy.sol | 6 +++--- 3 files changed, 8 insertions(+), 8 deletions(-) rename contracts/contracts/strategies/crosschain/{AbstractCCTPStrategy.sol => AbstractCCTPMorphoStrategy.sol} (97%) diff --git a/contracts/contracts/strategies/crosschain/AbstractCCTPStrategy.sol b/contracts/contracts/strategies/crosschain/AbstractCCTPMorphoStrategy.sol similarity index 97% rename from contracts/contracts/strategies/crosschain/AbstractCCTPStrategy.sol rename to contracts/contracts/strategies/crosschain/AbstractCCTPMorphoStrategy.sol index 020cdb06b3..55f3c43d8c 100644 --- a/contracts/contracts/strategies/crosschain/AbstractCCTPStrategy.sol +++ b/contracts/contracts/strategies/crosschain/AbstractCCTPMorphoStrategy.sol @@ -2,13 +2,13 @@ pragma solidity ^0.8.0; /** - * @title AbstractCCTPStrategy - Abstract contract for CCTP strategies + * @title AbstractCCTPMorphoStrategy - Abstract contract for CCTP morpho strategy * @author Origin Protocol Inc */ import { AbstractCCTPIntegrator } from "./AbstractCCTPIntegrator.sol"; -abstract contract AbstractCCTPStrategy is +abstract contract AbstractCCTPMorphoStrategy is AbstractCCTPIntegrator { constructor( diff --git a/contracts/contracts/strategies/crosschain/CrossChainMasterStrategy.sol b/contracts/contracts/strategies/crosschain/CrossChainMasterStrategy.sol index d5a03e292b..3d7321aa46 100644 --- a/contracts/contracts/strategies/crosschain/CrossChainMasterStrategy.sol +++ b/contracts/contracts/strategies/crosschain/CrossChainMasterStrategy.sol @@ -11,12 +11,12 @@ pragma solidity ^0.8.0; import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import { IERC20, InitializableAbstractStrategy } from "../../utils/InitializableAbstractStrategy.sol"; -import { AbstractCCTPStrategy } from "./AbstractCCTPStrategy.sol"; +import { AbstractCCTPMorphoStrategy } from "./AbstractCCTPMorphoStrategy.sol"; import { BytesHelper } from "../../utils/BytesHelper.sol"; contract CrossChainMasterStrategy is InitializableAbstractStrategy, - AbstractCCTPStrategy + AbstractCCTPMorphoStrategy { using SafeERC20 for IERC20; @@ -37,7 +37,7 @@ contract CrossChainMasterStrategy is CCTPIntegrationConfig memory _cctpConfig ) InitializableAbstractStrategy(_stratConfig) - AbstractCCTPStrategy( + AbstractCCTPMorphoStrategy( _cctpConfig ) {} diff --git a/contracts/contracts/strategies/crosschain/CrossChainRemoteStrategy.sol b/contracts/contracts/strategies/crosschain/CrossChainRemoteStrategy.sol index 27963f1cf2..c32462a477 100644 --- a/contracts/contracts/strategies/crosschain/CrossChainRemoteStrategy.sol +++ b/contracts/contracts/strategies/crosschain/CrossChainRemoteStrategy.sol @@ -12,10 +12,10 @@ pragma solidity ^0.8.0; import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import { IERC20 } from "../../utils/InitializableAbstractStrategy.sol"; import { Generalized4626Strategy } from "../Generalized4626Strategy.sol"; -import { AbstractCCTPStrategy } from "./AbstractCCTPStrategy.sol"; +import { AbstractCCTPMorphoStrategy } from "./AbstractCCTPMorphoStrategy.sol"; contract CrossChainRemoteStrategy is - AbstractCCTPStrategy, + AbstractCCTPMorphoStrategy, Generalized4626Strategy { using SafeERC20 for IERC20; @@ -24,7 +24,7 @@ contract CrossChainRemoteStrategy is BaseStrategyConfig memory _baseConfig, CCTPIntegrationConfig memory _cctpConfig ) - AbstractCCTPStrategy( + AbstractCCTPMorphoStrategy( _cctpConfig ) Generalized4626Strategy(_baseConfig, _cctpConfig.baseToken) From 25664b0fa337fe59a1e69f82152643c44629d636 Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Wed, 17 Dec 2025 01:12:06 +0100 Subject: [PATCH 3/8] add calls to Morpho Vault into a try catch --- .../crosschain/AbstractCCTPMorphoStrategy.sol | 1 + .../crosschain/CrossChainRemoteStrategy.sol | 47 +++++++++++++++++++ 2 files changed, 48 insertions(+) diff --git a/contracts/contracts/strategies/crosschain/AbstractCCTPMorphoStrategy.sol b/contracts/contracts/strategies/crosschain/AbstractCCTPMorphoStrategy.sol index 55f3c43d8c..380dfc94d9 100644 --- a/contracts/contracts/strategies/crosschain/AbstractCCTPMorphoStrategy.sol +++ b/contracts/contracts/strategies/crosschain/AbstractCCTPMorphoStrategy.sol @@ -11,6 +11,7 @@ import { AbstractCCTPIntegrator } from "./AbstractCCTPIntegrator.sol"; abstract contract AbstractCCTPMorphoStrategy is AbstractCCTPIntegrator { + constructor( CCTPIntegrationConfig memory _config ) diff --git a/contracts/contracts/strategies/crosschain/CrossChainRemoteStrategy.sol b/contracts/contracts/strategies/crosschain/CrossChainRemoteStrategy.sol index c32462a477..8445aed254 100644 --- a/contracts/contracts/strategies/crosschain/CrossChainRemoteStrategy.sol +++ b/contracts/contracts/strategies/crosschain/CrossChainRemoteStrategy.sol @@ -11,6 +11,7 @@ pragma solidity ^0.8.0; import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import { IERC20 } from "../../utils/InitializableAbstractStrategy.sol"; +import { IERC4626 } from "../../../lib/openzeppelin/interfaces/IERC4626.sol"; import { Generalized4626Strategy } from "../Generalized4626Strategy.sol"; import { AbstractCCTPMorphoStrategy } from "./AbstractCCTPMorphoStrategy.sol"; @@ -18,6 +19,8 @@ contract CrossChainRemoteStrategy is AbstractCCTPMorphoStrategy, Generalized4626Strategy { + event DepositFailed(string reason); + using SafeERC20 for IERC20; constructor( @@ -80,6 +83,7 @@ contract CrossChainRemoteStrategy is bytes memory payload ) internal virtual { // solhint-disable-next-line no-unused-vars + // TODO: no need to communicate the deposit amount if we deposit everything (uint64 nonce, uint256 depositAmount) = _decodeDepositMessage(payload); // Replay protection @@ -88,6 +92,9 @@ contract CrossChainRemoteStrategy is // Deposit everything we got uint256 balance = IERC20(baseToken).balanceOf(address(this)); + + // Underlying call to deposit funds can fail. It mustn't affect the overall + // flow as confirmation message should still be sent. _deposit(baseToken, balance); uint256 balanceAfter = checkBalance(baseToken); @@ -101,6 +108,24 @@ contract CrossChainRemoteStrategy is _sendMessage(message); } + /** + * @dev Deposit assets by converting them to shares + * @param _asset Address of asset to deposit + * @param _amount Amount of asset to deposit + */ + function _deposit(address _asset, uint256 _amount) internal override { + require(_amount > 0, "Must deposit something"); + require(_asset == address(assetToken), "Unexpected asset address"); + + try IERC4626(platformAddress).deposit(_amount, address(this)) returns (uint256 shares) { + emit Deposit(_asset, address(shareToken), _amount); + } catch Error(string memory reason) { + emit DepositFailed(string(abi.encodePacked("Deposit failed: ", reason))); + } catch (bytes memory lowLevelData) { + emit DepositFailed(string(abi.encodePacked("Deposit failed: low-level call failed with data ", lowLevelData))); + } + } + function _processWithdrawMessage(bytes memory payload) internal virtual { (uint64 nonce, uint256 withdrawAmount) = _decodeWithdrawMessage( payload @@ -145,4 +170,26 @@ contract CrossChainRemoteStrategy is ); _sendMessage(message); } + + /** + * @notice Get the total asset value held in the platform and contract + * @param _asset Address of the asset + * @return balance Total value of the asset in the platform and contract + */ + function checkBalance(address _asset) + public + view + override + returns (uint256 balance) + { + require(_asset == baseToken, "Unexpected asset address"); + /** + * Balance of USDC on the contract is counted towards the total balance, since a deposit + * to the Morpho V2 might fail and the USDC might remain on this contract as a result of a + * bridged transfer. + */ + uint256 balanceOnContract = IERC20(baseToken).balanceOf(address(this)); + IERC4626 platform = IERC4626(platformAddress); + return platform.previewRedeem(platform.balanceOf(address(this))) + balanceOnContract; + } } From e9b9b580593064b74fa15104f4a44d36f125556f Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Wed, 17 Dec 2025 15:39:00 +0100 Subject: [PATCH 4/8] refactor hook wrapper --- ...ategy.sol => AbstractCCTP4626Strategy.sol} | 4 +- .../crosschain/AbstractCCTPIntegrator.sol | 29 +--- ...HookWrapper.sol => CCTPMessageRelayer.sol} | 138 ++++++++---------- .../crosschain/CrossChainMasterStrategy.sol | 6 +- .../crosschain/CrossChainRemoteStrategy.sol | 10 +- .../contracts/strategies/crosschain/Untitled | 1 + 6 files changed, 74 insertions(+), 114 deletions(-) rename contracts/contracts/strategies/crosschain/{AbstractCCTPMorphoStrategy.sol => AbstractCCTP4626Strategy.sol} (97%) rename contracts/contracts/strategies/crosschain/{CCTPHookWrapper.sol => CCTPMessageRelayer.sol} (60%) create mode 100644 contracts/contracts/strategies/crosschain/Untitled diff --git a/contracts/contracts/strategies/crosschain/AbstractCCTPMorphoStrategy.sol b/contracts/contracts/strategies/crosschain/AbstractCCTP4626Strategy.sol similarity index 97% rename from contracts/contracts/strategies/crosschain/AbstractCCTPMorphoStrategy.sol rename to contracts/contracts/strategies/crosschain/AbstractCCTP4626Strategy.sol index 380dfc94d9..cb930e6715 100644 --- a/contracts/contracts/strategies/crosschain/AbstractCCTPMorphoStrategy.sol +++ b/contracts/contracts/strategies/crosschain/AbstractCCTP4626Strategy.sol @@ -2,13 +2,13 @@ pragma solidity ^0.8.0; /** - * @title AbstractCCTPMorphoStrategy - Abstract contract for CCTP morpho strategy + * @title AbstractCCTP4626Strategy - Abstract contract for CCTP morpho strategy * @author Origin Protocol Inc */ import { AbstractCCTPIntegrator } from "./AbstractCCTPIntegrator.sol"; -abstract contract AbstractCCTPMorphoStrategy is +abstract contract AbstractCCTP4626Strategy is AbstractCCTPIntegrator { diff --git a/contracts/contracts/strategies/crosschain/AbstractCCTPIntegrator.sol b/contracts/contracts/strategies/crosschain/AbstractCCTPIntegrator.sol index 147db8f5f7..859be159c9 100644 --- a/contracts/contracts/strategies/crosschain/AbstractCCTPIntegrator.sol +++ b/contracts/contracts/strategies/crosschain/AbstractCCTPIntegrator.sol @@ -7,11 +7,11 @@ import { IERC20 } from "../../utils/InitializableAbstractStrategy.sol"; import { ICCTPTokenMessenger, ICCTPMessageTransmitter, IMessageHandlerV2 } from "../../interfaces/cctp/ICCTP.sol"; import { Governable } from "../../governance/Governable.sol"; - import { BytesHelper } from "../../utils/BytesHelper.sol"; +import { CCTPMessageRelayer } from "./CCTPMessageRelayer.sol"; import "../../utils/Helpers.sol"; -abstract contract AbstractCCTPIntegrator is Governable, IMessageHandlerV2 { +abstract contract AbstractCCTPIntegrator is Governable, IMessageHandlerV2, CCTPMessageRelayer { using SafeERC20 for IERC20; using BytesHelper for bytes; @@ -27,10 +27,6 @@ abstract contract AbstractCCTPIntegrator is Governable, IMessageHandlerV2 { uint32 public constant WITHDRAW_ACK_MESSAGE = 20; uint32 public constant BALANCE_CHECK_MESSAGE = 3; - // CCTP contracts - ICCTPTokenMessenger public immutable cctpTokenMessenger; - ICCTPMessageTransmitter public immutable cctpMessageTransmitter; - // CCTP Hook Wrapper address public immutable cctpHookWrapper; @@ -46,6 +42,7 @@ abstract contract AbstractCCTPIntegrator is Governable, IMessageHandlerV2 { // CCTP params uint32 public minFinalityThreshold; uint32 public feePremiumBps; + // Threshold imposed by the CCTP uint256 public constant MAX_TRANSFER_AMOUNT = 10_000_000 * 10**6; // 10M USDC // Nonce of the last known deposit or withdrawal @@ -75,7 +72,7 @@ abstract contract AbstractCCTPIntegrator is Governable, IMessageHandlerV2 { constructor( CCTPIntegrationConfig memory _config - ) { + ) CCTPMessageRelayer(_config.cctpMessageTransmitter, _config.cctpTokenMessenger) { cctpTokenMessenger = ICCTPTokenMessenger(_config.cctpTokenMessenger); cctpMessageTransmitter = ICCTPMessageTransmitter(_config.cctpMessageTransmitter); destinationDomain = _config.destinationDomain; @@ -178,24 +175,6 @@ abstract contract AbstractCCTPIntegrator is Governable, IMessageHandlerV2 { return true; } - function onTokenReceived( - uint256 tokenAmount, - uint256 feeExecuted, - bytes memory payload - ) external virtual { - require( - msg.sender == cctpHookWrapper, - "Caller is not the CCTP hook wrapper" - ); - _onTokenReceived(tokenAmount, feeExecuted, payload); - } - - function _onTokenReceived( - uint256 tokenAmount, - uint256 feeExecuted, - bytes memory payload - ) internal virtual; - function _onMessageReceived(bytes memory payload) internal virtual; function _sendTokens(uint256 tokenAmount, bytes memory hookData) diff --git a/contracts/contracts/strategies/crosschain/CCTPHookWrapper.sol b/contracts/contracts/strategies/crosschain/CCTPMessageRelayer.sol similarity index 60% rename from contracts/contracts/strategies/crosschain/CCTPHookWrapper.sol rename to contracts/contracts/strategies/crosschain/CCTPMessageRelayer.sol index 2e5b5a5e43..451606778d 100644 --- a/contracts/contracts/strategies/crosschain/CCTPHookWrapper.sol +++ b/contracts/contracts/strategies/crosschain/CCTPMessageRelayer.sol @@ -1,19 +1,10 @@ // SPDX-License-Identifier: BUSL-1.1 pragma solidity ^0.8.0; -import { Governable } from "../../governance/Governable.sol"; import { ICCTPTokenMessenger, ICCTPMessageTransmitter } from "../../interfaces/cctp/ICCTP.sol"; import { BytesHelper } from "../../utils/BytesHelper.sol"; -interface ICrossChainStrategy { - function onTokenReceived( - uint256 tokenAmount, - uint256 feeExecuted, - bytes memory payload - ) external; -} - -contract CCTPHookWrapper is Governable { +abstract contract CCTPMessageRelayer { using BytesHelper for bytes; // CCTP Message Header fields @@ -21,35 +12,28 @@ contract CCTPHookWrapper is Governable { uint8 private constant VERSION_INDEX = 0; uint8 private constant SOURCE_DOMAIN_INDEX = 4; uint8 private constant SENDER_INDEX = 44; + uint8 private constant RECIPIENT_INDEX = 44; uint8 private constant MESSAGE_BODY_INDEX = 148; - // Burn Message V2 fields + // Message body V2 fields + // Ref: https://developers.circle.com/cctp/technical-guide#message-body + // Ref: https://github.com/circlefin/evm-cctp-contracts/blob/master/src/messages/v2/BurnMessageV2.sol uint8 private constant BURN_MESSAGE_V2_VERSION_INDEX = 0; uint8 private constant BURN_MESSAGE_V2_RECIPIENT_INDEX = 36; uint8 private constant BURN_MESSAGE_V2_AMOUNT_INDEX = 68; uint8 private constant BURN_MESSAGE_V2_MESSAGE_SENDER_INDEX = 100; uint8 private constant BURN_MESSAGE_V2_FEE_EXECUTED_INDEX = 164; uint8 private constant BURN_MESSAGE_V2_HOOK_DATA_INDEX = 228; - + bytes32 private constant EMPTY_NONCE = bytes32(0); uint32 private constant EMPTY_FINALITY_THRESHOLD_EXECUTED = 0; - // mapping[sourceDomainID][remoteStrategyAddress] => localStrategyAddress - mapping(uint32 => mapping(address => address)) public peers; - event PeerAdded( - uint32 sourceDomainID, - address remoteContract, - address localContract - ); - event PeerRemoved( - uint32 sourceDomainID, - address remoteContract, - address localContract - ); - uint32 private constant CCTP_MESSAGE_VERSION = 1; uint32 private constant ORIGIN_MESSAGE_VERSION = 1010; + // CCTP contracts + // This implementation assumes that remote and local chains have these contracts + // deployed on the same addresses. ICCTPMessageTransmitter public immutable cctpMessageTransmitter; ICCTPTokenMessenger public immutable cctpTokenMessenger; @@ -60,29 +44,31 @@ contract CCTPHookWrapper is Governable { cctpTokenMessenger = ICCTPTokenMessenger(_cctpTokenMessenger); } - function setPeer( - uint32 sourceDomainID, - address remoteContract, - address localContract - ) external onlyGovernor { - peers[sourceDomainID][remoteContract] = localContract; - emit PeerAdded(sourceDomainID, remoteContract, localContract); - } - - function removePeer(uint32 sourceDomainID, address remoteContract) - external - onlyGovernor - { - address localContract = peers[sourceDomainID][remoteContract]; - delete peers[sourceDomainID][remoteContract]; - emit PeerRemoved(sourceDomainID, remoteContract, localContract); + function _decodeMessageHeader(bytes memory message) + internal pure returns ( + uint32 version, + uint32 sourceDomainID, + address sender, + address recipient, + bytes memory messageBody + ) { + version = message.extractSlice(VERSION_INDEX, VERSION_INDEX + 4).decodeUint32(); + sourceDomainID = message.extractSlice(SOURCE_DOMAIN_INDEX, SOURCE_DOMAIN_INDEX + 4).decodeUint32(); + // Address of MessageTransmitterV2 caller on source domain + sender = abi.decode(message.extractSlice(SENDER_INDEX, SENDER_INDEX + 32), (address)); + // Address to handle message body on destination domain + recipient = abi.decode(message.extractSlice(RECIPIENT_INDEX, RECIPIENT_INDEX + 32), (address)); + messageBody = message.extractSlice(MESSAGE_BODY_INDEX, message.length); } function relay(bytes memory message, bytes memory attestation) external { - // Ensure message version - uint32 version = message - .extractSlice(VERSION_INDEX, VERSION_INDEX + 4) - .decodeUint32(); + ( + uint32 version, + uint32 sourceDomainID, + address sender, + address recipient, + bytes memory messageBody + ) = _decodeMessageHeader(message); // Ensure that it's a CCTP message require( @@ -90,70 +76,50 @@ contract CCTPHookWrapper is Governable { "Invalid CCTP message version" ); - uint32 sourceDomainID = message - .extractSlice(SOURCE_DOMAIN_INDEX, SOURCE_DOMAIN_INDEX + 4) - .decodeUint32(); - - // Grab the message sender - address sender = abi.decode( - message.extractSlice(SENDER_INDEX, SENDER_INDEX + 32), - (address) - ); - // Ensure message body version - bytes memory messageBody = message.extractSlice( - MESSAGE_BODY_INDEX, - message.length - ); bytes memory bodyVersionSlice = messageBody.extractSlice( BURN_MESSAGE_V2_VERSION_INDEX, BURN_MESSAGE_V2_VERSION_INDEX + 4 ); version = bodyVersionSlice.decodeUint32(); + // TODO should we replace this with: + // TODO: what if the sender sends another type of a message not just the burn message? bool isBurnMessageV1 = sender == address(cctpTokenMessenger); if (isBurnMessageV1) { // Handle burn message - // TODO: commenting this out as the BURN_MESSAGE_V2_MINT_RECIPIENT_INDEX is not defined - // require( - // version == 1 && - // messageBody.length >= BURN_MESSAGE_V2_MINT_RECIPIENT_INDEX, - // "Invalid burn message" - // ); - - // Find sender + require( + version == 1 && + messageBody.length >= BURN_MESSAGE_V2_HOOK_DATA_INDEX, + "Invalid burn message" + ); + + // Address of caller of depositForBurn (or depositForBurnWithCaller) on source domain bytes memory messageSender = messageBody.extractSlice( BURN_MESSAGE_V2_MESSAGE_SENDER_INDEX, BURN_MESSAGE_V2_MESSAGE_SENDER_INDEX + 32 ); sender = abi.decode(messageSender, (address)); } - // TODO: check the sender even if it is not a burn message - - address recipientContract = peers[sourceDomainID][sender]; if (isBurnMessageV1) { bytes memory recipientSlice = messageBody.extractSlice( BURN_MESSAGE_V2_RECIPIENT_INDEX, BURN_MESSAGE_V2_RECIPIENT_INDEX + 32 ); - address whitelistedRecipient = abi.decode( + // TODO is this the same recipient as the one in the message header? + recipient = abi.decode( recipientSlice, (address) ); - require( - whitelistedRecipient == recipientContract, - "Invalid recipient" - ); } - require( - recipientContract != address(0), - "Sender is not a configured peer" - ); + require(sender == recipient, "Sender and recipient must be the same"); + require(sender == address(this), "Incorrect sender/recipient address"); // Relay the message + // This step also mints USDC and transfers it to the recipient wallet bool relaySuccess = cctpMessageTransmitter.receiveMessage( message, attestation @@ -178,11 +144,23 @@ contract CCTPHookWrapper is Governable { ); uint256 feeExecuted = abi.decode(feeSlice, (uint256)); - ICrossChainStrategy(recipientContract).onTokenReceived( + _onTokenReceived( tokenAmount - feeExecuted, feeExecuted, hookData ); } } + + /** + * @dev Called when the USDC is received from the CCTP + * @param tokenAmount The actual amount of USDC received (amount sent - fee executed) + * @param feeExecuted The fee executed + * @param payload The payload of the message (hook data) + */ + function _onTokenReceived( + uint256 tokenAmount, + uint256 feeExecuted, + bytes memory payload + ) internal virtual; } diff --git a/contracts/contracts/strategies/crosschain/CrossChainMasterStrategy.sol b/contracts/contracts/strategies/crosschain/CrossChainMasterStrategy.sol index 3d7321aa46..685ff22d74 100644 --- a/contracts/contracts/strategies/crosschain/CrossChainMasterStrategy.sol +++ b/contracts/contracts/strategies/crosschain/CrossChainMasterStrategy.sol @@ -11,12 +11,12 @@ pragma solidity ^0.8.0; import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import { IERC20, InitializableAbstractStrategy } from "../../utils/InitializableAbstractStrategy.sol"; -import { AbstractCCTPMorphoStrategy } from "./AbstractCCTPMorphoStrategy.sol"; +import { AbstractCCTP4626Strategy } from "./AbstractCCTP4626Strategy.sol"; import { BytesHelper } from "../../utils/BytesHelper.sol"; contract CrossChainMasterStrategy is InitializableAbstractStrategy, - AbstractCCTPMorphoStrategy + AbstractCCTP4626Strategy { using SafeERC20 for IERC20; @@ -37,7 +37,7 @@ contract CrossChainMasterStrategy is CCTPIntegrationConfig memory _cctpConfig ) InitializableAbstractStrategy(_stratConfig) - AbstractCCTPMorphoStrategy( + AbstractCCTP4626Strategy( _cctpConfig ) {} diff --git a/contracts/contracts/strategies/crosschain/CrossChainRemoteStrategy.sol b/contracts/contracts/strategies/crosschain/CrossChainRemoteStrategy.sol index 8445aed254..d59aad9e2c 100644 --- a/contracts/contracts/strategies/crosschain/CrossChainRemoteStrategy.sol +++ b/contracts/contracts/strategies/crosschain/CrossChainRemoteStrategy.sol @@ -13,10 +13,10 @@ import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.s import { IERC20 } from "../../utils/InitializableAbstractStrategy.sol"; import { IERC4626 } from "../../../lib/openzeppelin/interfaces/IERC4626.sol"; import { Generalized4626Strategy } from "../Generalized4626Strategy.sol"; -import { AbstractCCTPMorphoStrategy } from "./AbstractCCTPMorphoStrategy.sol"; +import { AbstractCCTP4626Strategy } from "./AbstractCCTP4626Strategy.sol"; contract CrossChainRemoteStrategy is - AbstractCCTPMorphoStrategy, + AbstractCCTP4626Strategy, Generalized4626Strategy { event DepositFailed(string reason); @@ -27,7 +27,7 @@ contract CrossChainRemoteStrategy is BaseStrategyConfig memory _baseConfig, CCTPIntegrationConfig memory _cctpConfig ) - AbstractCCTPMorphoStrategy( + AbstractCCTP4626Strategy( _cctpConfig ) Generalized4626Strategy(_baseConfig, _cctpConfig.baseToken) @@ -114,9 +114,11 @@ contract CrossChainRemoteStrategy is * @param _amount Amount of asset to deposit */ function _deposit(address _asset, uint256 _amount) internal override { - require(_amount > 0, "Must deposit something"); + require(_amount > 0, "Must deposit something"); require(_asset == address(assetToken), "Unexpected asset address"); + // This call can fail, and the failure doesn't need to bubble up to the _processDepositMessage function + // as the flow is not affected by the failure. try IERC4626(platformAddress).deposit(_amount, address(this)) returns (uint256 shares) { emit Deposit(_asset, address(shareToken), _amount); } catch Error(string memory reason) { diff --git a/contracts/contracts/strategies/crosschain/Untitled b/contracts/contracts/strategies/crosschain/Untitled new file mode 100644 index 0000000000..d3419ca315 --- /dev/null +++ b/contracts/contracts/strategies/crosschain/Untitled @@ -0,0 +1 @@ +_onTokenReceived \ No newline at end of file From 5f75e4db4e63e99447d8f0775702dac4d67bcda9 Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Wed, 17 Dec 2025 16:10:19 +0100 Subject: [PATCH 5/8] don't revert if withdraw from underlying fails --- .../crosschain/CrossChainRemoteStrategy.sol | 43 +++++++++++++++++-- 1 file changed, 39 insertions(+), 4 deletions(-) diff --git a/contracts/contracts/strategies/crosschain/CrossChainRemoteStrategy.sol b/contracts/contracts/strategies/crosschain/CrossChainRemoteStrategy.sol index d59aad9e2c..ba18fbbce8 100644 --- a/contracts/contracts/strategies/crosschain/CrossChainRemoteStrategy.sol +++ b/contracts/contracts/strategies/crosschain/CrossChainRemoteStrategy.sol @@ -20,7 +20,8 @@ contract CrossChainRemoteStrategy is Generalized4626Strategy { event DepositFailed(string reason); - + event WithdrawFailed(string reason); + using SafeERC20 for IERC20; constructor( @@ -119,7 +120,7 @@ contract CrossChainRemoteStrategy is // This call can fail, and the failure doesn't need to bubble up to the _processDepositMessage function // as the flow is not affected by the failure. - try IERC4626(platformAddress).deposit(_amount, address(this)) returns (uint256 shares) { + try IERC4626(platformAddress).deposit(_amount, address(this)) { emit Deposit(_asset, address(shareToken), _amount); } catch Error(string memory reason) { emit DepositFailed(string(abi.encodePacked("Deposit failed: ", reason))); @@ -137,7 +138,7 @@ contract CrossChainRemoteStrategy is require(!isNonceProcessed(nonce), "Nonce already processed"); _markNonceAsProcessed(nonce); - // Withdraw funds to the remote strategy + // Withdraw funds from the remote strategy _withdraw(address(this), baseToken, withdrawAmount); // Check balance after withdrawal @@ -148,7 +149,41 @@ contract CrossChainRemoteStrategy is withdrawAmount, balanceAfter ); - _sendTokens(withdrawAmount, message); + // Send the complete balance on the contract. If we were to send only the + // withdrawn amount, the call could revert if the balance is not sufficient. + // Or dust could be left on the contract that is hard to extract. + uint256 usdcBalance = IERC20(baseToken).balanceOf(address(this)); + if (usdcBalance > 1e6) { + _sendTokens(usdcBalance, message); + } + } + + /** + * @dev Withdraw asset by burning shares + * @param _recipient Address to receive withdrawn asset + * @param _asset Address of asset to withdraw + * @param _amount Amount of asset to withdraw + */ + function _withdraw( + address _recipient, + address _asset, + uint256 _amount + ) internal override { + require(_amount > 0, "Must withdraw something"); + require(_recipient != address(0), "Must specify recipient"); + require(_asset == address(assetToken), "Unexpected asset address"); + + // slither-disable-next-line unused-return + + // This call can fail, and the failure doesn't need to bubble up to the _processWithdrawMessage function + // as the flow is not affected by the failure. + try IERC4626(platformAddress).withdraw(_amount, _recipient, address(this)) { + emit Withdrawal(_asset, address(shareToken), _amount); + } catch Error(string memory reason) { + emit WithdrawFailed(string(abi.encodePacked("Withdrawal failed: ", reason))); + } catch (bytes memory lowLevelData) { + emit WithdrawFailed(string(abi.encodePacked("Withdrawal failed: low-level call failed with data ", lowLevelData))); + } } function _onTokenReceived( From 90ebf9d8360b1771f32710ad90d2825e7385f6ff Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Wed, 17 Dec 2025 17:26:27 +0100 Subject: [PATCH 6/8] use checkBalance for deposit/withdrawal acknowledgment --- .../crosschain/CrossChainMasterStrategy.sol | 134 +++++++----------- .../crosschain/CrossChainRemoteStrategy.sol | 9 +- 2 files changed, 53 insertions(+), 90 deletions(-) diff --git a/contracts/contracts/strategies/crosschain/CrossChainMasterStrategy.sol b/contracts/contracts/strategies/crosschain/CrossChainMasterStrategy.sol index 685ff22d74..885fa61fd0 100644 --- a/contracts/contracts/strategies/crosschain/CrossChainMasterStrategy.sol +++ b/contracts/contracts/strategies/crosschain/CrossChainMasterStrategy.sol @@ -29,6 +29,7 @@ contract CrossChainMasterStrategy is // Transfer amounts by nonce mapping(uint64 => uint256) public transferAmounts; + event RemoteStrategyBalanceUpdated(uint256 balance); /** * @param _stratConfig The platform and OToken vault addresses */ @@ -159,18 +160,11 @@ contract CrossChainMasterStrategy is function _onMessageReceived(bytes memory payload) internal override { uint32 messageType = _getMessageType(payload); - if (messageType == DEPOSIT_ACK_MESSAGE) { - // Received when Remote strategy acknowledges the deposit - _processDepositAckMessage(payload); - } else if (messageType == BALANCE_CHECK_MESSAGE) { + if (messageType == BALANCE_CHECK_MESSAGE) { // Received when Remote strategy checks the balance _processBalanceCheckMessage(payload); - } else if (messageType == WITHDRAW_ACK_MESSAGE) { - // Received when Remote strategy acknowledges the withdrawal - // Do nothing because we receive acknowledgement with token transfer, so _onTokenReceived will handle it - // TODO: Should _onTokenReceived always call _onMessageReceived? - // _processWithdrawAckMessage(payload); - } else { + } + else { revert("Unknown message type"); } } @@ -180,61 +174,30 @@ contract CrossChainMasterStrategy is uint256 feeExecuted, bytes memory payload ) internal override { - // Received when Remote strategy sends tokens to the master strategy - uint32 messageType = _getMessageType(payload); - // Only withdraw acknowledgements are expected here - require(messageType == WITHDRAW_ACK_MESSAGE, "Invalid message type"); - - _processWithdrawAckMessage(tokenAmount, feeExecuted, payload); + // expecring a BALANCE_CHECK_MESSAGE + _onMessageReceived(payload); } function _deposit(address _asset, uint256 depositAmount) internal virtual { require(_asset == baseToken, "Unsupported asset"); - - uint64 nonce = _getNextNonce(); - + require(!isTransferPending(), "Transfer already pending"); + require(pendingAmount == 0, "Unexpected pending amount"); require(depositAmount > 0, "Deposit amount must be greater than 0"); require( depositAmount <= MAX_TRANSFER_AMOUNT, "Deposit amount exceeds max transfer amount" ); - emit Deposit(_asset, _asset, depositAmount); - + uint64 nonce = _getNextNonce(); transferAmounts[nonce] = depositAmount; - // Add to pending amount - // TODO: make sure overflow doesn't happen here (it shouldn't because of 0.8.0 but still make sure) - pendingAmount = pendingAmount + depositAmount; + // Set pending amount + pendingAmount = depositAmount; // Send deposit message with payload bytes memory message = _encodeDepositMessage(nonce, depositAmount); _sendTokens(depositAmount, message); - } - - function _processDepositAckMessage(bytes memory message) internal virtual { - ( - uint64 nonce, - uint256 amountReceived, - uint256 feeExecuted, - uint256 balanceAfter - ) = _decodeDepositAckMessage(message); - - // Replay protection - require(!isNonceProcessed(nonce), "Nonce already processed"); - _markNonceAsProcessed(nonce); - - // TODO: Do we need any tolerance here? - require( - transferAmounts[nonce] == amountReceived + feeExecuted, - "Transfer amount mismatch" - ); - - // Subtract from pending amount - pendingAmount = pendingAmount - amountReceived; - - // Update balance - remoteStrategyBalance = balanceAfter; + emit Deposit(_asset, _asset, depositAmount); } function _withdraw( @@ -245,7 +208,7 @@ contract CrossChainMasterStrategy is require(_asset == baseToken, "Unsupported asset"); require(_amount > 0, "Withdraw amount must be greater than 0"); require(_recipient == vaultAddress, "Only Vault can withdraw"); - + require(!isTransferPending(), "Transfer already pending"); require( _amount <= MAX_TRANSFER_AMOUNT, "Withdraw amount exceeds max transfer amount" @@ -262,48 +225,47 @@ contract CrossChainMasterStrategy is _sendMessage(message); } - function _processWithdrawAckMessage( - uint256 tokenAmount, - // solhint-disable-next-line no-unused-vars - uint256 feeExecuted, - bytes memory message - ) internal virtual { - ( - uint64 nonce, - uint256 amountSent, - uint256 balanceAfter - ) = _decodeWithdrawAckMessage(message); - - // Replay protection - require(!isNonceProcessed(nonce), "Nonce already processed"); - _markNonceAsProcessed(nonce); - - require( - transferAmounts[nonce] == amountSent, - "Transfer amount mismatch" - ); - - // Update balance - remoteStrategyBalance = balanceAfter; - - // Transfer tokens to vault - IERC20(baseToken).safeTransfer(vaultAddress, tokenAmount); - } - + /** + * @dev process balance check serves 3 purposes: + * - confirms a deposit to the remote strategy + * - confirms a withdrawal from the remote strategy + * - updates the remote strategy balance + * @param message The message containing the nonce and balance + */ function _processBalanceCheckMessage(bytes memory message) internal virtual { (uint64 nonce, uint256 balance) = _decodeBalanceCheckMessage(message); - uint64 _lastNonce = lastTransferNonce; - - if (_lastNonce != nonce || !isNonceProcessed(_lastNonce)) { - // Do not update pending amount if the nonce is not the latest one - return; + uint64 _lastCachedNonce = lastTransferNonce; + + /** + * Either a deposit or withdrawal are being confirmed. + * Since only one transfer is allowed to be pending at a time we can apply the effects + * of deposit or withdrawal acknowledgement. + */ + if (nonce == _lastCachedNonce && !isNonceProcessed(nonce)) { + _markNonceAsProcessed(nonce); + + remoteStrategyBalance = balance; + emit RemoteStrategyBalanceUpdated(balance); + + // effect of confirming a deposit + pendingAmount = 0; + // effect of confirming a withdrawal + uint256 usdcBalance = IERC20(baseToken).balanceOf(address(this)); + if (usdcBalance > 1e6) { + IERC20(baseToken).safeTransfer(vaultAddress, usdcBalance); + } + } + // Nonces match and are confirmed meaning it is just a balance update + else if (nonce == _lastCachedNonce) { + // Update balance + remoteStrategyBalance = balance; + emit RemoteStrategyBalanceUpdated(balance); } - - // Update balance - remoteStrategyBalance = balance; + // otherwise the message nonce is smaller than the last cached nonce, meaning it is outdated + // the contract should ignore it } } diff --git a/contracts/contracts/strategies/crosschain/CrossChainRemoteStrategy.sol b/contracts/contracts/strategies/crosschain/CrossChainRemoteStrategy.sol index ba18fbbce8..94ac4a6885 100644 --- a/contracts/contracts/strategies/crosschain/CrossChainRemoteStrategy.sol +++ b/contracts/contracts/strategies/crosschain/CrossChainRemoteStrategy.sol @@ -143,18 +143,19 @@ contract CrossChainRemoteStrategy is // Check balance after withdrawal uint256 balanceAfter = checkBalance(baseToken); - - bytes memory message = _encodeWithdrawAckMessage( - nonce, - withdrawAmount, + bytes memory message = _encodeBalanceCheckMessage( + lastTransferNonce, balanceAfter ); + // Send the complete balance on the contract. If we were to send only the // withdrawn amount, the call could revert if the balance is not sufficient. // Or dust could be left on the contract that is hard to extract. uint256 usdcBalance = IERC20(baseToken).balanceOf(address(this)); if (usdcBalance > 1e6) { _sendTokens(usdcBalance, message); + } else { + _sendMessage(message); } } From 0a9e8fc36abeeeee04db7f9bb5f7e27d8e3af7e2 Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Wed, 17 Dec 2025 17:28:58 +0100 Subject: [PATCH 7/8] update message in remote strategy --- .../strategies/crosschain/CrossChainRemoteStrategy.sol | 7 ++----- contracts/contracts/strategies/crosschain/Untitled | 1 - 2 files changed, 2 insertions(+), 6 deletions(-) delete mode 100644 contracts/contracts/strategies/crosschain/Untitled diff --git a/contracts/contracts/strategies/crosschain/CrossChainRemoteStrategy.sol b/contracts/contracts/strategies/crosschain/CrossChainRemoteStrategy.sol index 94ac4a6885..c472d0cf14 100644 --- a/contracts/contracts/strategies/crosschain/CrossChainRemoteStrategy.sol +++ b/contracts/contracts/strategies/crosschain/CrossChainRemoteStrategy.sol @@ -99,11 +99,8 @@ contract CrossChainRemoteStrategy is _deposit(baseToken, balance); uint256 balanceAfter = checkBalance(baseToken); - - bytes memory message = _encodeDepositAckMessage( - nonce, - tokenAmount, - feeExecuted, + bytes memory message = _encodeBalanceCheckMessage( + lastTransferNonce, balanceAfter ); _sendMessage(message); diff --git a/contracts/contracts/strategies/crosschain/Untitled b/contracts/contracts/strategies/crosschain/Untitled deleted file mode 100644 index d3419ca315..0000000000 --- a/contracts/contracts/strategies/crosschain/Untitled +++ /dev/null @@ -1 +0,0 @@ -_onTokenReceived \ No newline at end of file From 27984d04c072b7310d65bbbf60f0bfd87adb410b Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Wed, 17 Dec 2025 17:30:17 +0100 Subject: [PATCH 8/8] remove unneeded functions --- .../crosschain/AbstractCCTP4626Strategy.sol | 70 ------------------- 1 file changed, 70 deletions(-) diff --git a/contracts/contracts/strategies/crosschain/AbstractCCTP4626Strategy.sol b/contracts/contracts/strategies/crosschain/AbstractCCTP4626Strategy.sol index cb930e6715..9ec08e75de 100644 --- a/contracts/contracts/strategies/crosschain/AbstractCCTP4626Strategy.sol +++ b/contracts/contracts/strategies/crosschain/AbstractCCTP4626Strategy.sol @@ -47,45 +47,6 @@ abstract contract AbstractCCTP4626Strategy is return (nonce, depositAmount); } - function _encodeDepositAckMessage( - uint64 nonce, - uint256 amountReceived, - uint256 feeExecuted, - uint256 balanceAfter - ) internal virtual returns (bytes memory) { - return - abi.encodePacked( - ORIGIN_MESSAGE_VERSION, - DEPOSIT_ACK_MESSAGE, - abi.encode(nonce, amountReceived, feeExecuted, balanceAfter) - ); - } - - function _decodeDepositAckMessage(bytes memory message) - internal - virtual - returns ( - uint64, - uint256, - uint256, - uint256 - ) - { - _verifyMessageVersionAndType(message, ORIGIN_MESSAGE_VERSION, DEPOSIT_ACK_MESSAGE); - - ( - uint64 nonce, - uint256 amountReceived, - uint256 feeExecuted, - uint256 balanceAfter - ) = abi.decode( - _getMessagePayload(message), - (uint64, uint256, uint256, uint256) - ); - - return (nonce, amountReceived, feeExecuted, balanceAfter); - } - function _encodeWithdrawMessage(uint64 nonce, uint256 withdrawAmount) internal virtual @@ -113,37 +74,6 @@ abstract contract AbstractCCTP4626Strategy is return (nonce, withdrawAmount); } - function _encodeWithdrawAckMessage( - uint64 nonce, - uint256 amountSent, - uint256 balanceAfter - ) internal virtual returns (bytes memory) { - return - abi.encodePacked( - ORIGIN_MESSAGE_VERSION, - WITHDRAW_ACK_MESSAGE, - abi.encode(nonce, amountSent, balanceAfter) - ); - } - - function _decodeWithdrawAckMessage(bytes memory message) - internal - virtual - returns ( - uint64, - uint256, - uint256 - ) - { - _verifyMessageVersionAndType(message, ORIGIN_MESSAGE_VERSION, WITHDRAW_ACK_MESSAGE); - - (uint64 nonce, uint256 amountSent, uint256 balanceAfter) = abi.decode( - _getMessagePayload(message), - (uint64, uint256, uint256) - ); - return (nonce, amountSent, balanceAfter); - } - function _encodeBalanceCheckMessage(uint64 nonce, uint256 balance) internal virtual