From 637fb40853dd417fb5178e552f02b7d07e7e6da8 Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Fri, 5 Dec 2025 14:54:27 +0100 Subject: [PATCH 001/101] some scaffolding --- .../crossChain/YearnV3MasterStrategy.sol | 185 ++++++++++++++++++ contracts/deploy/deployActions.js | 38 +++- .../deploy/mainnet/159_yearn_strategy.js | 24 +++ 3 files changed, 246 insertions(+), 1 deletion(-) create mode 100644 contracts/contracts/strategies/crossChain/YearnV3MasterStrategy.sol create mode 100644 contracts/deploy/mainnet/159_yearn_strategy.js diff --git a/contracts/contracts/strategies/crossChain/YearnV3MasterStrategy.sol b/contracts/contracts/strategies/crossChain/YearnV3MasterStrategy.sol new file mode 100644 index 0000000000..94050c7ecd --- /dev/null +++ b/contracts/contracts/strategies/crossChain/YearnV3MasterStrategy.sol @@ -0,0 +1,185 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +/** + * @title OUSD Yearn V3 Master Strategy - the Mainnet part + * @author Origin Protocol Inc + */ + +import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import { IERC20, InitializableAbstractStrategy } from "../../utils/InitializableAbstractStrategy.sol"; + +contract YearnV3MasterStrategy is InitializableAbstractStrategy { + using SafeERC20 for IERC20; + + /** + * @param _stratConfig The platform and OToken vault addresses + */ + constructor(BaseStrategyConfig memory _stratConfig) + InitializableAbstractStrategy(_stratConfig) + {} + + /** + * Initializer for setting up strategy internal state. + * @param _rewardTokenAddresses Addresses of reward tokens + * @param _assets Addresses of supported assets + * @param _pTokens Platform Token corresponding addresses + */ + function initialize( + address[] calldata _rewardTokenAddresses, + address[] calldata _assets, + address[] calldata _pTokens + ) external onlyGovernor initializer { + InitializableAbstractStrategy._initialize( + _rewardTokenAddresses, + _assets, + _pTokens + ); + } + + /** + * @dev Deposit asset into mainnet strategy making them ready to be + * bridged to Slave part of the strategy + * @param _asset Address of asset to deposit + * @param _amount Amount of asset to deposit + */ + function deposit(address _asset, uint256 _amount) + external + override + onlyVault + nonReentrant + { + + emit Deposit(_asset, _asset, _amount); + } + + /** + * @dev Bridge the assets prepared by a previous Deposit call to the + * Slave part of the strategy + * @param _amount Amount of asset to deposit + * @param quote Quote to bridge the assets to the Slave part of the strategy + */ + function depositWithQuote(uint256 _amount, bytes calldata quote) + external + onlyGovernorOrStrategist + nonReentrant + { + + // TODO: implement this + } + + /** + * @dev Deposit the entire balance + */ + function depositAll() external override onlyVault nonReentrant { + for (uint256 i = 0; i < assetsMapped.length; i++) { + uint256 balance = IERC20(assetsMapped[i]).balanceOf(address(this)); + if (balance > 0) { + emit Deposit(assetsMapped[i], assetsMapped[i], balance); + } + } + } + + /** + * @dev Send a withdrawal Wormhole message requesting a certain withdrawal amount + * @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 + ) external override onlyVault nonReentrant { + require(_amount > 0, "Must withdraw something"); + require(_recipient == vaultAddress, "Only Vault can withdraw"); + + // Withdraw the funds from this strategy to the Vault once + // they are allready bridged here + } + + /** + * @dev Send a withdrawal Wormhole message requesting a certain withdrawal amount + * @param _recipient Address to receive withdrawn asset + * @param _asset Address of asset to withdraw + * @param _amount Amount of asset to withdraw + * @param quote Quote to bridge the assets to the Master part of the strategy + */ + function withdrawWithQuote( + address _recipient, + address _asset, + uint256 _amount, + bytes calldata quote + ) external onlyGovernorOrStrategist nonReentrant { + require(_amount > 0, "Must withdraw something"); + require(_recipient == vaultAddress, "Only Vault can withdraw"); + } + + /** + * @dev Remove all assets from platform and send them to Vault contract. + */ + function withdrawAll() external override onlyVaultOrGovernor nonReentrant { + // + // TODO: implement this + } + + /** + * @dev Get the total asset value held in the platform + * @param _asset Address of the asset + * @return balance Total value of the asset in the platform + */ + function checkBalance(address _asset) + external + view + override + returns (uint256 balance) + { + // USDC balance on this contract + // + USDC being bridged + // + USDC cached in the corresponding Slave part of this contract + } + + /** + * @dev Returns bool indicating whether asset is supported by strategy + * @param _asset Address of the asset + */ + function supportsAsset(address _asset) public view override returns (bool) { + return assetToPToken[_asset] != address(0); + } + + /** + * @dev Approve the spending of all assets + */ + function safeApproveAllTokens() + external + override + onlyGovernor + nonReentrant + { + + } + + /** + * @dev + * @param _asset Address of the asset to approve + * @param _aToken Address of the aToken + */ + // solhint-disable-next-line no-unused-vars + function _abstractSetPToken(address _asset, address _aToken) + internal + override + { + } + + /** + * @dev + */ + function collectRewardTokens() + external + override + onlyHarvester + nonReentrant + { + + } +} diff --git a/contracts/deploy/deployActions.js b/contracts/deploy/deployActions.js index 94692f1a98..c65b4517bd 100644 --- a/contracts/deploy/deployActions.js +++ b/contracts/deploy/deployActions.js @@ -17,13 +17,15 @@ const { isHoodi, isHoodiOrFork, } = require("../test/helpers.js"); -const { deployWithConfirmation, withConfirmation } = require("../utils/deploy"); +const { deployWithConfirmation, withConfirmation, encodeSaltForCreateX } = require("../utils/deploy"); const { metapoolLPCRVPid } = require("../utils/constants"); const { replaceContractAt } = require("../utils/hardhat"); const { resolveContract } = require("../utils/resolvers"); const { impersonateAccount, getSigner } = require("../utils/signers"); const { getDefenderSigner } = require("../utils/signersNoHardhat"); const { getTxOpts } = require("../utils/tx"); +const createxAbi = require("../abi/createx.json"); + const { beaconChainGenesisTimeHoodi, beaconChainGenesisTimeMainnet, @@ -1682,6 +1684,39 @@ const deploySonicSwapXAMOStrategyImplementation = async () => { return cSonicSwapXAMOStrategy; }; +// deploys an instance of InitializeGovernedUpgradeabilityProxy where address is defined by salt +const deployProxyWithCreateX = async (salt) => { + const { deployerAddr } = await getNamedAccounts(); + const sDeployer = await ethers.provider.getSigner(deployerAddr); + log(`Deploying proxy with salt: ${salt} as deployer ${deployerAddr}`); + + const cCreateX = await ethers.getContractAt(createxAbi, addresses.createX); + const factoryEncodedSalt = encodeSaltForCreateX(deployerAddr, false, 1); + + const getFactoryBytecode = async () => { + // No deployment needed—get factory directly from artifacts + const factory = await ethers.getContractFactory("InitializeGovernedUpgradeabilityProxy"); + return factory.bytecode; + } + + const txResponse = await withConfirmation( + cCreateX + .connect(sDeployer) + .deployCreate2(factoryEncodedSalt, getFactoryBytecode()) + ); + + const contractCreationTopic = + "0xb8fda7e00c6b06a2b54e58521bc5894fee35f1090e5a3bb6390bfe2b98b497f7"; + const txReceipt = await txResponse.wait(); + const proxyAddress = ethers.utils.getAddress( + `0x${txReceipt.events + .find((event) => event.topics[0] === contractCreationTopic) + .topics[1].slice(26)}` + ); + + return proxyAddress; +}; + module.exports = { deployOracles, deployCore, @@ -1719,4 +1754,5 @@ module.exports = { deployPlumeMockRoosterAMOStrategyImplementation, getPlumeContracts, deploySonicSwapXAMOStrategyImplementation, + deployProxyWithCreateX, }; diff --git a/contracts/deploy/mainnet/159_yearn_strategy.js b/contracts/deploy/mainnet/159_yearn_strategy.js new file mode 100644 index 0000000000..d27af72e40 --- /dev/null +++ b/contracts/deploy/mainnet/159_yearn_strategy.js @@ -0,0 +1,24 @@ +const { deploymentWithGovernanceProposal } = require("../../utils/deploy"); +const addresses = require("../../utils/addresses"); +const { deployProxyWithCreateX } = require("../deployActions"); + +module.exports = deploymentWithGovernanceProposal( + { + deployName: "159_yearn_strategy", + forceDeploy: false, + reduceQueueTime: true, + deployerIsProposer: false, + proposalId: "", + }, + async ({ deployWithConfirmation }) => { + // the salt needs to match the salt on the base chain deploying the other part of the strategy + const salt = "Yean strategy 1"; + const proxyAddress = await deployProxyWithCreateX(salt); + console.log(`Proxy address: ${proxyAddress}`); + + + return { + actions: [], + }; + } +); From 7630f6dc05a16f59c44ec02e38df192635fe0f1b Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Fri, 5 Dec 2025 21:53:36 +0100 Subject: [PATCH 002/101] add basic necessities for unit tests --- .../crossChain/YearnV3MasterStrategyMock.sol | 26 ++++++ .../crossChain/YearnV3SlaveStrategyMock.sol | 26 ++++++ ...InitializeGovernedUpgradeabilityProxy2.sol | 21 +++++ contracts/contracts/proxies/Proxies.sol | 20 ++++- .../crossChain/YearnV3MasterStrategy.sol | 7 ++ .../crossChain/YearnV3SlaveStrategy.sol | 21 +++++ contracts/deploy/base/040_yearn_strategy.js | 26 ++++++ contracts/deploy/deployActions.js | 88 ++++++++++++++++++- .../deploy/mainnet/159_yearn_strategy.js | 10 ++- contracts/test/_fixture.js | 41 +++++++++ .../strategies/crossChain/yearnV3Strategy.js | 22 +++++ 11 files changed, 299 insertions(+), 9 deletions(-) create mode 100644 contracts/contracts/mocks/crossChain/YearnV3MasterStrategyMock.sol create mode 100644 contracts/contracts/mocks/crossChain/YearnV3SlaveStrategyMock.sol create mode 100644 contracts/contracts/proxies/InitializeGovernedUpgradeabilityProxy2.sol create mode 100644 contracts/contracts/strategies/crossChain/YearnV3SlaveStrategy.sol create mode 100644 contracts/deploy/base/040_yearn_strategy.js create mode 100644 contracts/test/strategies/crossChain/yearnV3Strategy.js diff --git a/contracts/contracts/mocks/crossChain/YearnV3MasterStrategyMock.sol b/contracts/contracts/mocks/crossChain/YearnV3MasterStrategyMock.sol new file mode 100644 index 0000000000..b20b764dcc --- /dev/null +++ b/contracts/contracts/mocks/crossChain/YearnV3MasterStrategyMock.sol @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +/** + * @title OUSD Yearn V3 Master Strategy Mock - the Mainnet part + * @author Origin Protocol Inc + */ + +import { YearnV3MasterStrategy } from "../../strategies/crossChain/YearnV3MasterStrategy.sol"; +import { InitializableAbstractStrategy } from "../../utils/InitializableAbstractStrategy.sol"; + +contract YearnV3MasterStrategyMock is YearnV3MasterStrategy { + address public _slaveAddress; + + constructor(InitializableAbstractStrategy.BaseStrategyConfig memory _stratConfig) YearnV3MasterStrategy(_stratConfig) {} + /** + * @dev Returns the address of the Slave part of the strategy on L2 + */ + function slaveAddress() internal override returns (address) { + return _slaveAddress; + } + + function setSlaveAddress(address __slaveAddress) public { + _slaveAddress = __slaveAddress; + } +} diff --git a/contracts/contracts/mocks/crossChain/YearnV3SlaveStrategyMock.sol b/contracts/contracts/mocks/crossChain/YearnV3SlaveStrategyMock.sol new file mode 100644 index 0000000000..484ea859a3 --- /dev/null +++ b/contracts/contracts/mocks/crossChain/YearnV3SlaveStrategyMock.sol @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +/** + * @title OUSD Yearn V3 Master Strategy Mock - the Mainnet part + * @author Origin Protocol Inc + */ + +import { YearnV3SlaveStrategy } from "../../strategies/crossChain/YearnV3SlaveStrategy.sol"; + +contract YearnV3SlaveStrategyMock is YearnV3SlaveStrategy { + address public _masterAddress; + + constructor() YearnV3SlaveStrategy() {} + + /** + * @dev Returns the address of the Slave part of the strategy on L2 + */ + function masterAddress() internal override returns (address) { + return _masterAddress; + } + + function setMasterAddress(address __masterAddress) public { + _masterAddress = __masterAddress; + } +} diff --git a/contracts/contracts/proxies/InitializeGovernedUpgradeabilityProxy2.sol b/contracts/contracts/proxies/InitializeGovernedUpgradeabilityProxy2.sol new file mode 100644 index 0000000000..0aad6b8a0b --- /dev/null +++ b/contracts/contracts/proxies/InitializeGovernedUpgradeabilityProxy2.sol @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +import { InitializeGovernedUpgradeabilityProxy } from "./InitializeGovernedUpgradeabilityProxy.sol"; + +/** + * @title BaseGovernedUpgradeabilityProxy2 + * @dev This is the same as InitializeGovernedUpgradeabilityProxy except that the + * governor is defined in the constructor. + * @author Origin Protocol Inc + */ +contract InitializeGovernedUpgradeabilityProxy2 is InitializeGovernedUpgradeabilityProxy { + + /** + * This is used when the msg.sender can not be the governor. E.g. when the proxy is + * deployed via CreateX + */ + constructor(address governor) InitializeGovernedUpgradeabilityProxy(){ + _setGovernor(governor); + } +} diff --git a/contracts/contracts/proxies/Proxies.sol b/contracts/contracts/proxies/Proxies.sol index 76eb607eb0..d84d40137c 100644 --- a/contracts/contracts/proxies/Proxies.sol +++ b/contracts/contracts/proxies/Proxies.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.0; import { InitializeGovernedUpgradeabilityProxy } from "./InitializeGovernedUpgradeabilityProxy.sol"; - +import { InitializeGovernedUpgradeabilityProxy2 } from "./InitializeGovernedUpgradeabilityProxy2.sol"; /** * @notice OUSDProxy delegates calls to an OUSD implementation */ @@ -320,3 +320,21 @@ contract CompoundingStakingSSVStrategyProxy is { } + +/** + * @notice YearnV3MasterStrategyProxy delegates calls to a YearnV3MasterStrategy implementation + */ +contract YearnV3MasterStrategyProxy is + InitializeGovernedUpgradeabilityProxy2 +{ + constructor(address governor) InitializeGovernedUpgradeabilityProxy2(governor) {} +} + +/** + * @notice YearnV3SlaveStrategyProxy delegates calls to a YearnV3SlaveStrategy implementation + */ +contract YearnV3SlaveStrategyProxy is + InitializeGovernedUpgradeabilityProxy2 +{ + constructor(address governor) InitializeGovernedUpgradeabilityProxy2(governor) {} +} diff --git a/contracts/contracts/strategies/crossChain/YearnV3MasterStrategy.sol b/contracts/contracts/strategies/crossChain/YearnV3MasterStrategy.sol index 94050c7ecd..9e96c067b5 100644 --- a/contracts/contracts/strategies/crossChain/YearnV3MasterStrategy.sol +++ b/contracts/contracts/strategies/crossChain/YearnV3MasterStrategy.sol @@ -37,6 +37,13 @@ contract YearnV3MasterStrategy is InitializableAbstractStrategy { ); } + /** + * @dev Returns the address of the Slave part of the strategy on L2 + */ + function slaveAddress() internal virtual returns (address) { + return address(this); + } + /** * @dev Deposit asset into mainnet strategy making them ready to be * bridged to Slave part of the strategy diff --git a/contracts/contracts/strategies/crossChain/YearnV3SlaveStrategy.sol b/contracts/contracts/strategies/crossChain/YearnV3SlaveStrategy.sol new file mode 100644 index 0000000000..04d3c316ed --- /dev/null +++ b/contracts/contracts/strategies/crossChain/YearnV3SlaveStrategy.sol @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +/** + * @title OUSD Yearn V3 Slave Strategy - the L2 chain part + * @author Origin Protocol Inc + */ + +import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import { IERC20 } from "../../utils/InitializableAbstractStrategy.sol"; + +contract YearnV3SlaveStrategy { + using SafeERC20 for IERC20; + + /** + * @dev Returns the address of the Slave part of the strategy on L2 + */ + function masterAddress() internal virtual returns (address) { + return address(this); + } +} diff --git a/contracts/deploy/base/040_yearn_strategy.js b/contracts/deploy/base/040_yearn_strategy.js new file mode 100644 index 0000000000..920ac16d68 --- /dev/null +++ b/contracts/deploy/base/040_yearn_strategy.js @@ -0,0 +1,26 @@ +const { deployOnBase } = require("../../utils/deploy-l2"); +const addresses = require("../../utils/addresses"); +const { deployProxyWithCreateX, deployYearn3SlaveStrategyImpl } = require("../deployActions"); +const { + deployWithConfirmation, + withConfirmation, +} = require("../../utils/deploy.js"); + +module.exports = deployOnBase( + { + deployName: "040_yearn_strategy", + }, + async ({ ethers }) => { + const salt = "Yean strategy 1"; + const proxyAddress = await deployProxyWithCreateX(salt, "YearnV3SlaveStrategyProxy"); + console.log(`YearnV3SlaveStrategyProxy address: ${proxyAddress}`); + + const implAddress = await deployYearn3SlaveStrategyImpl(proxyAddress); + console.log(`YearnV3SlaveStrategyImpl address: ${implAddress}`); + + return { + actions: [ + ], + }; + } +); diff --git a/contracts/deploy/deployActions.js b/contracts/deploy/deployActions.js index c65b4517bd..d0c77f6d38 100644 --- a/contracts/deploy/deployActions.js +++ b/contracts/deploy/deployActions.js @@ -1685,18 +1685,19 @@ const deploySonicSwapXAMOStrategyImplementation = async () => { }; // deploys an instance of InitializeGovernedUpgradeabilityProxy where address is defined by salt -const deployProxyWithCreateX = async (salt) => { +const deployProxyWithCreateX = async (salt, proxyName) => { const { deployerAddr } = await getNamedAccounts(); const sDeployer = await ethers.provider.getSigner(deployerAddr); - log(`Deploying proxy with salt: ${salt} as deployer ${deployerAddr}`); + log(`Deploying ${proxyName} with salt: ${salt} as deployer ${deployerAddr}`); const cCreateX = await ethers.getContractAt(createxAbi, addresses.createX); const factoryEncodedSalt = encodeSaltForCreateX(deployerAddr, false, 1); const getFactoryBytecode = async () => { // No deployment needed—get factory directly from artifacts - const factory = await ethers.getContractFactory("InitializeGovernedUpgradeabilityProxy"); - return factory.bytecode; + const ProxyContract = await ethers.getContractFactory(proxyName); + const encodedArgs = ProxyContract.interface.encodeDeploy([deployerAddr]); + return ethers.utils.hexConcat([ProxyContract.bytecode, encodedArgs]); } const txResponse = await withConfirmation( @@ -1717,6 +1718,83 @@ const deployProxyWithCreateX = async (salt) => { return proxyAddress; }; +// deploys and initializes the Yearn 3 master strategy +const deployYearn3MasterStrategyImpl = async (proxyAddress, implementationName = "YearnV3MasterStrategy") => { + const { deployerAddr } = await getNamedAccounts(); + const sDeployer = await ethers.provider.getSigner(deployerAddr); + log(`Deploying Yearn3MasterStrategyImpl as deployer ${deployerAddr}`); + + const cYearnV3MasterStrategyProxy = await ethers.getContractAt( + "YearnV3MasterStrategyProxy", + proxyAddress + ); + + const dYearnV3MasterStrategy = await deployWithConfirmation( + implementationName, + [ + [ + addresses.zero, // platform address + addresses.mainnet.Vault + ] + ] + ); + + // const initData = cYearnV3MasterStrategy.interface.encodeFunctionData( + // "initialize()", + // [] + // ); + + // Init the proxy to point at the implementation, set the governor, and call initialize + const initFunction = "initialize(address,address,bytes)"; + await withConfirmation( + cYearnV3MasterStrategyProxy.connect(sDeployer)[initFunction]( + dYearnV3MasterStrategy.address, + addresses.mainnet.Timelock, // governor + //initData, // data for delegate call to the initialize function on the strategy + "0x", + await getTxOpts() + ) + ); + + return dYearnV3MasterStrategy.address; +}; + +// deploys and initializes the Yearn 3 slave strategy +const deployYearn3SlaveStrategyImpl = async (proxyAddress, implementationName = "YearnV3SlaveStrategy") => { + const { deployerAddr } = await getNamedAccounts(); + const sDeployer = await ethers.provider.getSigner(deployerAddr); + log(`Deploying Yearn3SlaveStrategyImpl as deployer ${deployerAddr}`); + + const cYearnV3SlaveStrategyProxy = await ethers.getContractAt( + "YearnV3SlaveStrategyProxy", + proxyAddress + ); + + const dYearnV3SlaveStrategy = await deployWithConfirmation( + implementationName, + [] + ); + + // const initData = cYearnV3MasterStrategy.interface.encodeFunctionData( + // "initialize()", + // [] + // ); + + // Init the proxy to point at the implementation, set the governor, and call initialize + const initFunction = "initialize(address,address,bytes)"; + await withConfirmation( + cYearnV3SlaveStrategyProxy.connect(sDeployer)[initFunction]( + dYearnV3SlaveStrategy.address, + addresses.base.timelock, // governor + //initData, // data for delegate call to the initialize function on the strategy + "0x", + await getTxOpts() + ) + ); + + return dYearnV3SlaveStrategy.address; +}; + module.exports = { deployOracles, deployCore, @@ -1755,4 +1833,6 @@ module.exports = { getPlumeContracts, deploySonicSwapXAMOStrategyImplementation, deployProxyWithCreateX, + deployYearn3MasterStrategyImpl, + deployYearn3SlaveStrategyImpl, }; diff --git a/contracts/deploy/mainnet/159_yearn_strategy.js b/contracts/deploy/mainnet/159_yearn_strategy.js index d27af72e40..7c7aa481b3 100644 --- a/contracts/deploy/mainnet/159_yearn_strategy.js +++ b/contracts/deploy/mainnet/159_yearn_strategy.js @@ -1,6 +1,6 @@ const { deploymentWithGovernanceProposal } = require("../../utils/deploy"); const addresses = require("../../utils/addresses"); -const { deployProxyWithCreateX } = require("../deployActions"); +const { deployProxyWithCreateX, deployYearn3MasterStrategyImpl } = require("../deployActions"); module.exports = deploymentWithGovernanceProposal( { @@ -13,10 +13,12 @@ module.exports = deploymentWithGovernanceProposal( async ({ deployWithConfirmation }) => { // the salt needs to match the salt on the base chain deploying the other part of the strategy const salt = "Yean strategy 1"; - const proxyAddress = await deployProxyWithCreateX(salt); - console.log(`Proxy address: ${proxyAddress}`); - + const proxyAddress = await deployProxyWithCreateX(salt, "YearnV3MasterStrategyProxy"); + console.log(`YearnV3MasterStrategyProxy address: ${proxyAddress}`); + const implAddress = await deployYearn3MasterStrategyImpl(proxyAddress); + console.log(`YearnV3MasterStrategyImpl address: ${implAddress}`); + return { actions: [], }; diff --git a/contracts/test/_fixture.js b/contracts/test/_fixture.js index 86cd1ecc8e..96bf737477 100644 --- a/contracts/test/_fixture.js +++ b/contracts/test/_fixture.js @@ -16,6 +16,7 @@ const { fundAccountsForOETHUnitTests, } = require("../utils/funding"); const { deployWithConfirmation } = require("../utils/deploy"); +const { deployYearn3MasterStrategyImpl, deployYearn3SlaveStrategyImpl } = require("../deploy/deployActions.js"); const { replaceContractAt } = require("../utils/hardhat"); const { @@ -2525,6 +2526,45 @@ async function instantRebaseVaultFixture() { return fixture; } +async function yearnCrossChainFixture() { + const fixture = await defaultFixture(); + const { deployerAddr } = await getNamedAccounts(); + const sDeployer = await ethers.provider.getSigner(deployerAddr); + + // deploy master strategy + const masterProxy = await deployWithConfirmation("YearnV3MasterStrategyProxy", [ + deployerAddr + ] + ); + const masterProxyAddress = masterProxy.address; + log(`YearnV3MasterStrategyProxy address: ${masterProxyAddress}`); + let implAddress = await deployYearn3MasterStrategyImpl(masterProxyAddress, "YearnV3MasterStrategyMock"); + log(`YearnV3MasterStrategyMockImpl address: ${implAddress}`); + + + // deploy slave strategy + const slaveProxy = await deployWithConfirmation("YearnV3SlaveStrategyProxy", [ + deployerAddr + ] + ); + + const slaveProxyAddress = slaveProxy.address; + log(`YearnV3SlaveStrategyProxy address: ${slaveProxyAddress}`); + + implAddress = await deployYearn3SlaveStrategyImpl(slaveProxyAddress, "YearnV3SlaveStrategyMock"); + log(`YearnV3SlaveStrategyMockImpl address: ${implAddress}`); + + const yearnMasterStrategy = await ethers.getContractAt("YearnV3MasterStrategyMock", masterProxyAddress); + const yearnSlaveStrategy = await ethers.getContractAt("YearnV3SlaveStrategyMock", slaveProxyAddress); + + yearnMasterStrategy.connect(sDeployer).setSlaveAddress(slaveProxyAddress); + yearnSlaveStrategy.connect(sDeployer).setMasterAddress(masterProxyAddress); + + fixture.yearnMasterStrategy = yearnMasterStrategy; + fixture.yearnSlaveStrategy = yearnSlaveStrategy; + return fixture; +} + /** * Configure a reborn hack attack */ @@ -2950,4 +2990,5 @@ module.exports = { bridgeHelperModuleFixture, beaconChainFixture, claimRewardsModuleFixture, + yearnCrossChainFixture, }; diff --git a/contracts/test/strategies/crossChain/yearnV3Strategy.js b/contracts/test/strategies/crossChain/yearnV3Strategy.js new file mode 100644 index 0000000000..c85e39ca8d --- /dev/null +++ b/contracts/test/strategies/crossChain/yearnV3Strategy.js @@ -0,0 +1,22 @@ +const { expect } = require("chai"); +const { utils } = require("ethers"); + +const { createFixtureLoader, yearnCrossChainFixture } = require("../../_fixture"); + +describe.only("Yearn V3 Cross Chain Strategy", function () { + let fixture; + const loadFixture = createFixtureLoader(yearnCrossChainFixture); + + let yearnMasterStrategy, yearnSlaveStrategy; + + beforeEach(async function () { + fixture = await loadFixture(); + yearnMasterStrategy = fixture.yearnMasterStrategy; + yearnSlaveStrategy = fixture.yearnSlaveStrategy; + }); + + it("Should have correct initial state", async function () { + expect(await yearnMasterStrategy._slaveAddress()).to.equal(yearnSlaveStrategy.address); + expect(await yearnSlaveStrategy._masterAddress()).to.equal(yearnMasterStrategy.address); + }); +}); \ No newline at end of file From 6a97767e2c35610dddc33ec0ecab9c3ede60354c Mon Sep 17 00:00:00 2001 From: Shahul Hameed <10547529+shahthepro@users.noreply.github.com> Date: Thu, 11 Dec 2025 14:37:26 +0400 Subject: [PATCH 003/101] checkpoint --- contracts/contracts/interfaces/cctp/ICCTP.sol | 75 +++ .../crossChain/YearnV3MasterStrategyMock.sol | 26 - .../CrossChainMasterStrategyMock.sol | 21 + .../CrossChainRemoteStrategyMock.sol} | 11 +- ...InitializeGovernedUpgradeabilityProxy2.sol | 7 +- contracts/contracts/proxies/Proxies.sol | 17 +- .../strategies/Generalized4626Strategy.sol | 9 + .../crossChain/YearnV3MasterStrategy.sol | 192 ------- .../crossChain/YearnV3SlaveStrategy.sol | 21 - .../crosschain/AbstractCCTPIntegrator.sol | 473 ++++++++++++++++++ .../strategies/crosschain/CCTPHookWrapper.sol | 168 +++++++ .../crosschain/CrossChainMasterStrategy.sol | 296 +++++++++++ .../crosschain/CrossChainRemoteStrategy.sol | 150 ++++++ contracts/contracts/utils/BytesHelper.sol | 30 ++ contracts/deploy/base/040_yearn_strategy.js | 31 +- contracts/deploy/deployActions.js | 62 ++- .../deploy/mainnet/159_yearn_strategy.js | 20 +- contracts/test/_fixture.js | 64 ++- .../strategies/crossChain/yearnV3Strategy.js | 22 +- contracts/utils/deploy.js | 10 +- 20 files changed, 1365 insertions(+), 340 deletions(-) create mode 100644 contracts/contracts/interfaces/cctp/ICCTP.sol delete mode 100644 contracts/contracts/mocks/crossChain/YearnV3MasterStrategyMock.sol create mode 100644 contracts/contracts/mocks/crosschain/CrossChainMasterStrategyMock.sol rename contracts/contracts/mocks/{crossChain/YearnV3SlaveStrategyMock.sol => crosschain/CrossChainRemoteStrategyMock.sol} (50%) delete mode 100644 contracts/contracts/strategies/crossChain/YearnV3MasterStrategy.sol delete mode 100644 contracts/contracts/strategies/crossChain/YearnV3SlaveStrategy.sol create mode 100644 contracts/contracts/strategies/crosschain/AbstractCCTPIntegrator.sol create mode 100644 contracts/contracts/strategies/crosschain/CCTPHookWrapper.sol create mode 100644 contracts/contracts/strategies/crosschain/CrossChainMasterStrategy.sol create mode 100644 contracts/contracts/strategies/crosschain/CrossChainRemoteStrategy.sol create mode 100644 contracts/contracts/utils/BytesHelper.sol diff --git a/contracts/contracts/interfaces/cctp/ICCTP.sol b/contracts/contracts/interfaces/cctp/ICCTP.sol new file mode 100644 index 0000000000..639b0ee307 --- /dev/null +++ b/contracts/contracts/interfaces/cctp/ICCTP.sol @@ -0,0 +1,75 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +interface ICCTPTokenMessenger { + function depositForBurn( + uint256 amount, + uint32 destinationDomain, + bytes32 mintRecipient, + address burnToken, + bytes32 destinationCaller, + uint256 maxFee, + uint32 minFinalityThreshold + ) external; + + function depositForBurnWithHook( + uint256 amount, + uint32 destinationDomain, + bytes32 mintRecipient, + address burnToken, + bytes32 destinationCaller, + uint256 maxFee, + uint32 minFinalityThreshold, + bytes memory hookData + ) external; + + function getMinFeeAmount(uint256 amount) external view returns (uint256); +} + +interface ICCTPMessageTransmitter { + function sendMessage( + uint32 destinationDomain, + bytes32 recipient, + bytes32 destinationCaller, + uint32 minFinalityThreshold, + bytes memory messageBody + ) external; + + function receiveMessage(bytes calldata message, bytes calldata attestation) + external + returns (bool); +} + +interface IMessageHandlerV2 { + /** + * @notice Handles an incoming finalized message from an IReceiverV2 + * @dev Finalized messages have finality threshold values greater than or equal to 2000 + * @param sourceDomain The source domain of the message + * @param sender The sender of the message + * @param finalityThresholdExecuted the finality threshold at which the message was attested to + * @param messageBody The raw bytes of the message body + * @return success True, if successful; false, if not. + */ + function handleReceiveFinalizedMessage( + uint32 sourceDomain, + bytes32 sender, + uint32 finalityThresholdExecuted, + bytes calldata messageBody + ) external returns (bool); + + /** + * @notice Handles an incoming unfinalized message from an IReceiverV2 + * @dev Unfinalized messages have finality threshold values less than 2000 + * @param sourceDomain The source domain of the message + * @param sender The sender of the message + * @param finalityThresholdExecuted The finality threshold at which the message was attested to + * @param messageBody The raw bytes of the message body + * @return success True, if successful; false, if not. + */ + function handleReceiveUnfinalizedMessage( + uint32 sourceDomain, + bytes32 sender, + uint32 finalityThresholdExecuted, + bytes calldata messageBody + ) external returns (bool); +} diff --git a/contracts/contracts/mocks/crossChain/YearnV3MasterStrategyMock.sol b/contracts/contracts/mocks/crossChain/YearnV3MasterStrategyMock.sol deleted file mode 100644 index b20b764dcc..0000000000 --- a/contracts/contracts/mocks/crossChain/YearnV3MasterStrategyMock.sol +++ /dev/null @@ -1,26 +0,0 @@ -// SPDX-License-Identifier: BUSL-1.1 -pragma solidity ^0.8.0; - -/** - * @title OUSD Yearn V3 Master Strategy Mock - the Mainnet part - * @author Origin Protocol Inc - */ - -import { YearnV3MasterStrategy } from "../../strategies/crossChain/YearnV3MasterStrategy.sol"; -import { InitializableAbstractStrategy } from "../../utils/InitializableAbstractStrategy.sol"; - -contract YearnV3MasterStrategyMock is YearnV3MasterStrategy { - address public _slaveAddress; - - constructor(InitializableAbstractStrategy.BaseStrategyConfig memory _stratConfig) YearnV3MasterStrategy(_stratConfig) {} - /** - * @dev Returns the address of the Slave part of the strategy on L2 - */ - function slaveAddress() internal override returns (address) { - return _slaveAddress; - } - - function setSlaveAddress(address __slaveAddress) public { - _slaveAddress = __slaveAddress; - } -} diff --git a/contracts/contracts/mocks/crosschain/CrossChainMasterStrategyMock.sol b/contracts/contracts/mocks/crosschain/CrossChainMasterStrategyMock.sol new file mode 100644 index 0000000000..9019c0125e --- /dev/null +++ b/contracts/contracts/mocks/crosschain/CrossChainMasterStrategyMock.sol @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +/** + * @title OUSD Yearn V3 Master Strategy Mock - the Mainnet part + * @author Origin Protocol Inc + */ + +contract CrossChainMasterStrategyMock { + address public _remoteAddress; + + constructor() {} + + function remoteAddress() internal override returns (address) { + return _remoteAddress; + } + + function setRemoteAddress(address __remoteAddress) public { + _remoteAddress = __remoteAddress; + } +} diff --git a/contracts/contracts/mocks/crossChain/YearnV3SlaveStrategyMock.sol b/contracts/contracts/mocks/crosschain/CrossChainRemoteStrategyMock.sol similarity index 50% rename from contracts/contracts/mocks/crossChain/YearnV3SlaveStrategyMock.sol rename to contracts/contracts/mocks/crosschain/CrossChainRemoteStrategyMock.sol index 484ea859a3..fafa848097 100644 --- a/contracts/contracts/mocks/crossChain/YearnV3SlaveStrategyMock.sol +++ b/contracts/contracts/mocks/crosschain/CrossChainRemoteStrategyMock.sol @@ -2,20 +2,15 @@ pragma solidity ^0.8.0; /** - * @title OUSD Yearn V3 Master Strategy Mock - the Mainnet part + * @title OUSD Yearn V3 Remote Strategy Mock - the Mainnet part * @author Origin Protocol Inc */ -import { YearnV3SlaveStrategy } from "../../strategies/crossChain/YearnV3SlaveStrategy.sol"; - -contract YearnV3SlaveStrategyMock is YearnV3SlaveStrategy { +contract CrossChainRemoteStrategyMock { address public _masterAddress; - constructor() YearnV3SlaveStrategy() {} + constructor() {} - /** - * @dev Returns the address of the Slave part of the strategy on L2 - */ function masterAddress() internal override returns (address) { return _masterAddress; } diff --git a/contracts/contracts/proxies/InitializeGovernedUpgradeabilityProxy2.sol b/contracts/contracts/proxies/InitializeGovernedUpgradeabilityProxy2.sol index 0aad6b8a0b..250acbe782 100644 --- a/contracts/contracts/proxies/InitializeGovernedUpgradeabilityProxy2.sol +++ b/contracts/contracts/proxies/InitializeGovernedUpgradeabilityProxy2.sol @@ -9,13 +9,14 @@ import { InitializeGovernedUpgradeabilityProxy } from "./InitializeGovernedUpgra * governor is defined in the constructor. * @author Origin Protocol Inc */ -contract InitializeGovernedUpgradeabilityProxy2 is InitializeGovernedUpgradeabilityProxy { - +contract InitializeGovernedUpgradeabilityProxy2 is + InitializeGovernedUpgradeabilityProxy +{ /** * This is used when the msg.sender can not be the governor. E.g. when the proxy is * deployed via CreateX */ - constructor(address governor) InitializeGovernedUpgradeabilityProxy(){ + constructor(address governor) InitializeGovernedUpgradeabilityProxy() { _setGovernor(governor); } } diff --git a/contracts/contracts/proxies/Proxies.sol b/contracts/contracts/proxies/Proxies.sol index d84d40137c..67d747f640 100644 --- a/contracts/contracts/proxies/Proxies.sol +++ b/contracts/contracts/proxies/Proxies.sol @@ -3,6 +3,7 @@ pragma solidity ^0.8.0; import { InitializeGovernedUpgradeabilityProxy } from "./InitializeGovernedUpgradeabilityProxy.sol"; import { InitializeGovernedUpgradeabilityProxy2 } from "./InitializeGovernedUpgradeabilityProxy2.sol"; + /** * @notice OUSDProxy delegates calls to an OUSD implementation */ @@ -322,19 +323,23 @@ contract CompoundingStakingSSVStrategyProxy is } /** - * @notice YearnV3MasterStrategyProxy delegates calls to a YearnV3MasterStrategy implementation + * @notice CrossChainMasterStrategyProxy delegates calls to a CrossChainMasterStrategy implementation */ -contract YearnV3MasterStrategyProxy is +contract CrossChainMasterStrategyProxy is InitializeGovernedUpgradeabilityProxy2 { - constructor(address governor) InitializeGovernedUpgradeabilityProxy2(governor) {} + constructor(address governor) + InitializeGovernedUpgradeabilityProxy2(governor) + {} } /** - * @notice YearnV3SlaveStrategyProxy delegates calls to a YearnV3SlaveStrategy implementation + * @notice CrossChainRemoteStrategyProxy delegates calls to a CrossChainRemoteStrategy implementation */ -contract YearnV3SlaveStrategyProxy is +contract CrossChainRemoteStrategyProxy is InitializeGovernedUpgradeabilityProxy2 { - constructor(address governor) InitializeGovernedUpgradeabilityProxy2(governor) {} + constructor(address governor) + InitializeGovernedUpgradeabilityProxy2(governor) + {} } diff --git a/contracts/contracts/strategies/Generalized4626Strategy.sol b/contracts/contracts/strategies/Generalized4626Strategy.sol index 1e5d850740..deda1e32be 100644 --- a/contracts/contracts/strategies/Generalized4626Strategy.sol +++ b/contracts/contracts/strategies/Generalized4626Strategy.sol @@ -57,6 +57,7 @@ contract Generalized4626Strategy is InitializableAbstractStrategy { */ function deposit(address _asset, uint256 _amount) external + virtual override onlyVault nonReentrant @@ -99,6 +100,14 @@ contract Generalized4626Strategy is InitializableAbstractStrategy { address _asset, uint256 _amount ) external virtual override onlyVault nonReentrant { + _withdraw(_recipient, _asset, _amount); + } + + function _withdraw( + address _recipient, + address _asset, + uint256 _amount + ) internal virtual { require(_amount > 0, "Must withdraw something"); require(_recipient != address(0), "Must specify recipient"); require(_asset == address(assetToken), "Unexpected asset address"); diff --git a/contracts/contracts/strategies/crossChain/YearnV3MasterStrategy.sol b/contracts/contracts/strategies/crossChain/YearnV3MasterStrategy.sol deleted file mode 100644 index 9e96c067b5..0000000000 --- a/contracts/contracts/strategies/crossChain/YearnV3MasterStrategy.sol +++ /dev/null @@ -1,192 +0,0 @@ -// SPDX-License-Identifier: BUSL-1.1 -pragma solidity ^0.8.0; - -/** - * @title OUSD Yearn V3 Master Strategy - the Mainnet part - * @author Origin Protocol Inc - */ - -import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; -import { IERC20, InitializableAbstractStrategy } from "../../utils/InitializableAbstractStrategy.sol"; - -contract YearnV3MasterStrategy is InitializableAbstractStrategy { - using SafeERC20 for IERC20; - - /** - * @param _stratConfig The platform and OToken vault addresses - */ - constructor(BaseStrategyConfig memory _stratConfig) - InitializableAbstractStrategy(_stratConfig) - {} - - /** - * Initializer for setting up strategy internal state. - * @param _rewardTokenAddresses Addresses of reward tokens - * @param _assets Addresses of supported assets - * @param _pTokens Platform Token corresponding addresses - */ - function initialize( - address[] calldata _rewardTokenAddresses, - address[] calldata _assets, - address[] calldata _pTokens - ) external onlyGovernor initializer { - InitializableAbstractStrategy._initialize( - _rewardTokenAddresses, - _assets, - _pTokens - ); - } - - /** - * @dev Returns the address of the Slave part of the strategy on L2 - */ - function slaveAddress() internal virtual returns (address) { - return address(this); - } - - /** - * @dev Deposit asset into mainnet strategy making them ready to be - * bridged to Slave part of the strategy - * @param _asset Address of asset to deposit - * @param _amount Amount of asset to deposit - */ - function deposit(address _asset, uint256 _amount) - external - override - onlyVault - nonReentrant - { - - emit Deposit(_asset, _asset, _amount); - } - - /** - * @dev Bridge the assets prepared by a previous Deposit call to the - * Slave part of the strategy - * @param _amount Amount of asset to deposit - * @param quote Quote to bridge the assets to the Slave part of the strategy - */ - function depositWithQuote(uint256 _amount, bytes calldata quote) - external - onlyGovernorOrStrategist - nonReentrant - { - - // TODO: implement this - } - - /** - * @dev Deposit the entire balance - */ - function depositAll() external override onlyVault nonReentrant { - for (uint256 i = 0; i < assetsMapped.length; i++) { - uint256 balance = IERC20(assetsMapped[i]).balanceOf(address(this)); - if (balance > 0) { - emit Deposit(assetsMapped[i], assetsMapped[i], balance); - } - } - } - - /** - * @dev Send a withdrawal Wormhole message requesting a certain withdrawal amount - * @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 - ) external override onlyVault nonReentrant { - require(_amount > 0, "Must withdraw something"); - require(_recipient == vaultAddress, "Only Vault can withdraw"); - - // Withdraw the funds from this strategy to the Vault once - // they are allready bridged here - } - - /** - * @dev Send a withdrawal Wormhole message requesting a certain withdrawal amount - * @param _recipient Address to receive withdrawn asset - * @param _asset Address of asset to withdraw - * @param _amount Amount of asset to withdraw - * @param quote Quote to bridge the assets to the Master part of the strategy - */ - function withdrawWithQuote( - address _recipient, - address _asset, - uint256 _amount, - bytes calldata quote - ) external onlyGovernorOrStrategist nonReentrant { - require(_amount > 0, "Must withdraw something"); - require(_recipient == vaultAddress, "Only Vault can withdraw"); - } - - /** - * @dev Remove all assets from platform and send them to Vault contract. - */ - function withdrawAll() external override onlyVaultOrGovernor nonReentrant { - // - // TODO: implement this - } - - /** - * @dev Get the total asset value held in the platform - * @param _asset Address of the asset - * @return balance Total value of the asset in the platform - */ - function checkBalance(address _asset) - external - view - override - returns (uint256 balance) - { - // USDC balance on this contract - // + USDC being bridged - // + USDC cached in the corresponding Slave part of this contract - } - - /** - * @dev Returns bool indicating whether asset is supported by strategy - * @param _asset Address of the asset - */ - function supportsAsset(address _asset) public view override returns (bool) { - return assetToPToken[_asset] != address(0); - } - - /** - * @dev Approve the spending of all assets - */ - function safeApproveAllTokens() - external - override - onlyGovernor - nonReentrant - { - - } - - /** - * @dev - * @param _asset Address of the asset to approve - * @param _aToken Address of the aToken - */ - // solhint-disable-next-line no-unused-vars - function _abstractSetPToken(address _asset, address _aToken) - internal - override - { - } - - /** - * @dev - */ - function collectRewardTokens() - external - override - onlyHarvester - nonReentrant - { - - } -} diff --git a/contracts/contracts/strategies/crossChain/YearnV3SlaveStrategy.sol b/contracts/contracts/strategies/crossChain/YearnV3SlaveStrategy.sol deleted file mode 100644 index 04d3c316ed..0000000000 --- a/contracts/contracts/strategies/crossChain/YearnV3SlaveStrategy.sol +++ /dev/null @@ -1,21 +0,0 @@ -// SPDX-License-Identifier: BUSL-1.1 -pragma solidity ^0.8.0; - -/** - * @title OUSD Yearn V3 Slave Strategy - the L2 chain part - * @author Origin Protocol Inc - */ - -import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; -import { IERC20 } from "../../utils/InitializableAbstractStrategy.sol"; - -contract YearnV3SlaveStrategy { - using SafeERC20 for IERC20; - - /** - * @dev Returns the address of the Slave part of the strategy on L2 - */ - function masterAddress() internal virtual returns (address) { - return address(this); - } -} diff --git a/contracts/contracts/strategies/crosschain/AbstractCCTPIntegrator.sol b/contracts/contracts/strategies/crosschain/AbstractCCTPIntegrator.sol new file mode 100644 index 0000000000..b0aff9fba3 --- /dev/null +++ b/contracts/contracts/strategies/crosschain/AbstractCCTPIntegrator.sol @@ -0,0 +1,473 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import { IERC20, InitializableAbstractStrategy } from "../../utils/InitializableAbstractStrategy.sol"; + +import { ICCTPTokenMessenger, ICCTPMessageTransmitter, IMessageHandlerV2 } from "../../interfaces/cctp/ICCTP.sol"; + +import { Governable } from "../../governance/Governable.sol"; + +import "../../utils/Helpers.sol"; +import "../../utils/BytesHelper.sol"; + +abstract contract AbstractCCTPIntegrator is Governable, IMessageHandlerV2 { + using BytesHelper for bytes; + + event CCTPMinFinalityThresholdSet(uint32 minFinalityThreshold); + event CCTPFeePremiumBpsSet(uint32 feePremiumBps); + + uint32 public constant ORIGIN_MESSAGE_VERSION = 1010; + + uint32 public constant DEPOSIT_MESSAGE = 1; + uint32 public constant DEPOSIT_ACK_MESSAGE = 10; + uint32 public constant WITHDRAW_MESSAGE = 2; + 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; + + // USDC address on local chain + address public immutable baseToken; + + // Destination chain domain ID + uint32 public immutable destinationDomain; + + // Strategy address on destination chain + address public immutable destinationStrategy; + + // CCTP params + uint32 public minFinalityThreshold; + uint32 public feePremiumBps; + uint256 public constant MAX_TRANSFER_AMOUNT = 10_000_000 * 10**6; // 10M USDC + + // Nonce of the last known deposit or withdrawal + uint64 public lastTransferNonce; + + mapping(uint64 => bool) private nonceProcessed; + + // For future use + uint256[50] private __gap; + + modifier onlyCCTPMessageTransmitter() { + require( + msg.sender == address(cctpMessageTransmitter), + "Caller is not the CCTP message transmitter" + ); + _; + } + + constructor( + address _cctpTokenMessenger, + address _cctpMessageTransmitter, + uint32 _destinationDomain, + address _destinationStrategy, + address _baseToken, + address _cctpHookWrapper + ) { + cctpTokenMessenger = ICCTPTokenMessenger(_cctpTokenMessenger); + cctpMessageTransmitter = ICCTPMessageTransmitter( + _cctpMessageTransmitter + ); + destinationDomain = _destinationDomain; + destinationStrategy = _destinationStrategy; + baseToken = _baseToken; + cctpHookWrapper = _cctpHookWrapper; + + // Just a sanity check to ensure the base token is USDC + uint256 _baseTokenDecimals = Helpers.getDecimals(_baseToken); + require(_baseTokenDecimals == 6, "Base token decimals must be 6"); + } + + function _initialize(uint32 _minFinalityThreshold, uint32 _feePremiumBps) + internal + { + _setMinFinalityThreshold(_minFinalityThreshold); + _setFeePremiumBps(_feePremiumBps); + } + + function setMinFinalityThreshold(uint32 _minFinalityThreshold) + external + onlyGovernor + { + _setMinFinalityThreshold(_minFinalityThreshold); + } + + function _setMinFinalityThreshold(uint32 _minFinalityThreshold) internal { + // 1000 for fast transfer and 2000 for standard transfer + require( + _minFinalityThreshold == 1000 || _minFinalityThreshold == 2000, + "Invalid threshold" + ); + + minFinalityThreshold = _minFinalityThreshold; + emit CCTPMinFinalityThresholdSet(_minFinalityThreshold); + } + + function setFeePremiumBps(uint32 _feePremiumBps) external onlyGovernor { + _setFeePremiumBps(_feePremiumBps); + } + + function _setFeePremiumBps(uint32 _feePremiumBps) internal { + require(_feePremiumBps <= 3000, "Fee premium too high"); // 30% + + feePremiumBps = _feePremiumBps; + emit CCTPFeePremiumBpsSet(_feePremiumBps); + } + + function handleReceiveFinalizedMessage( + uint32 sourceDomain, + bytes32 sender, + uint32 finalityThresholdExecuted, + bytes memory messageBody + ) external override onlyCCTPMessageTransmitter returns (bool) { + return + _handleReceivedMessage( + sourceDomain, + sender, + finalityThresholdExecuted, + messageBody + ); + } + + function handleReceiveUnfinalizedMessage( + uint32 sourceDomain, + bytes32 sender, + uint32 finalityThresholdExecuted, + bytes memory messageBody + ) external override onlyCCTPMessageTransmitter returns (bool) { + return + _handleReceivedMessage( + sourceDomain, + sender, + finalityThresholdExecuted, + messageBody + ); + } + + function _handleReceivedMessage( + uint32 sourceDomain, + bytes32 sender, + uint32 finalityThresholdExecuted, + bytes memory messageBody + ) internal returns (bool) { + // Make sure that the finality threshold is same on both chains + // TODO: Do we really need this? + require( + finalityThresholdExecuted >= minFinalityThreshold, + "Finality threshold too low" + ); + require(sourceDomain == destinationDomain, "Unknown Source Domain"); + + // Extract address from bytes32 (CCTP stores addresses as right-padded bytes32) + address senderAddress = address(uint160(uint256(sender))); + require(senderAddress == destinationStrategy, "Unknown Sender"); + + _onMessageReceived(messageBody); + + 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) + internal + virtual + { + require(tokenAmount <= MAX_TRANSFER_AMOUNT, "Token amount too high"); + + // TODO: figure out why getMinFeeAmount is not on CCTP v2 contract + // Ref: https://developers.circle.com/cctp/evm-smart-contracts#getminfeeamount + + uint256 maxFee = feePremiumBps > 0 + ? (tokenAmount * feePremiumBps) / 10000 + : 0; + + cctpTokenMessenger.depositForBurnWithHook( + tokenAmount, + destinationDomain, + bytes32(uint256(uint160(destinationStrategy))), + address(baseToken), + bytes32(uint256(uint160(cctpHookWrapper))), + maxFee, + minFinalityThreshold, + hookData + ); + } + + function _getMessageType(bytes memory message) + internal + virtual + returns (uint32) + { + // uint32 bytes 0 to 4 is Origin message version + // uint32 bytes 4 to 8 is Message type + uint32 messageType = abi.decode(message.extractSlice(4, 8), (uint32)); + return messageType; + } + + function _encodeDepositMessage(uint64 nonce, uint256 depositAmount) + internal + virtual + returns (bytes memory) + { + return + abi.encodePacked( + ORIGIN_MESSAGE_VERSION, + DEPOSIT_MESSAGE_TYPE, + nonce, + depositAmount + ); + } + + function _decodeDepositMessage(bytes memory message) + internal + virtual + returns (uint64 nonce, uint256 depositAmount) + { + ( + uint32 version, + uint32 messageType, + uint64 nonce, + uint256 depositAmount + ) = abi.decode(message, (uint32, uint32, uint64, uint256)); + require( + version == ORIGIN_MESSAGE_VERSION, + "Invalid Origin Message Version" + ); + require(messageType == DEPOSIT_MESSAGE_TYPE, "Invalid Message type"); + 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_TYPE, + nonce, + amountReceived, + feeExecuted, + balanceAfter + ); + } + + function _decodeDepositAckMessage(bytes memory message) + internal + virtual + returns ( + uint64 nonce, + uint256 amountReceived, + uint256 feeExecuted, + uint256 balanceAfter + ) + { + ( + uint32 version, + uint32 messageType, + uint64 nonce, + uint256 amountReceived, + uint256 feeExecuted, + uint256 balanceAfter + ) = abi.decode( + message, + (uint32, uint32, uint64, uint256, uint256, uint256) + ); + require( + version == ORIGIN_MESSAGE_VERSION, + "Invalid Origin Message Version" + ); + require( + messageType == DEPOSIT_ACK_MESSAGE_TYPE, + "Invalid Message type" + ); + return (nonce, amountReceived, feeExecuted, balanceAfter); + } + + function _encodeWithdrawMessage(uint64 nonce, uint256 withdrawAmount) + internal + virtual + returns (bytes memory) + { + return + abi.encodePacked( + ORIGIN_MESSAGE_VERSION, + WITHDRAW_MESSAGE_TYPE, + nonce, + withdrawAmount + ); + } + + function _decodeWithdrawMessage(bytes memory message) + internal + virtual + returns (uint64 nonce, uint256 withdrawAmount) + { + ( + uint332 version, + uint332 messageType, + uint64 nonce, + uint256 withdrawAmount + ) = abi.decode(message, (uint332, uint332, uint64, uint256)); + require( + version == ORIGIN_MESSAGE_VERSION, + "Invalid Origin Message Version" + ); + require(messageType == WITHDRAW_MESSAGE_TYPE, "Invalid Message type"); + 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_TYPE, + nonce, + amountSent, + balanceAfter + ); + } + + function _decodeWithdrawAckMessage(bytes memory message) + internal + virtual + returns ( + uint64 nonce, + uint256 amountSent, + uint256 balanceAfter + ) + { + ( + uint332 version, + uint332 messageType, + uint64 nonce, + uint256 amountSent, + uint256 balanceAfter + ) = abi.decode(message, (uint332, uint332, uint64, uint256, uint256)); + require( + version == ORIGIN_MESSAGE_VERSION, + "Invalid Origin Message Version" + ); + require( + messageType == WITHDRAW_ACK_MESSAGE_TYPE, + "Invalid Message type" + ); + return (nonce, amountSent, balanceAfter); + } + + function _encodeBalanceCheckMessage(uint64 nonce, uint256 balance) + internal + virtual + returns (bytes memory) + { + return + abi.encodePacked( + ORIGIN_MESSAGE_VERSION, + BALANCE_CHECK_MESSAGE_TYPE, + nonce, + balance + ); + } + + function _decodeBalanceCheckMessage(bytes memory message) + internal + virtual + returns (uint64 nonce, uint256 balance) + { + ( + uint332 version, + uint332 messageType, + uint64 nonce, + uint256 balance + ) = abi.decode(message, (uint332, uint332, uint64, uint256)); + require( + version == ORIGIN_MESSAGE_VERSION, + "Invalid Origin Message Version" + ); + require( + messageType == BALANCE_CHECK_MESSAGE_TYPE, + "Invalid Message type" + ); + return (nonce, balance); + } + + function _sendMessage(bytes memory message) internal virtual { + cctpMessageTransmitter.sendMessage( + destinationDomain, + bytes32(uint256(uint160(destinationStrategy))), + bytes32(uint256(uint160(cctpHookWrapper))), + minFinalityThreshold, + message + ); + } + + function isTransferPending() public view returns (bool) { + uint64 nonce = lastTransferNonce; + return nonce > 0 && !nonceProcessed[nonce]; + } + + function isNonceProcessed(uint64 nonce) public view returns (bool) { + return nonceProcessed[nonce]; + } + + function _markNonceAsProcessed(uint64 nonce) internal { + uint64 lastNonce = lastTransferNonce; + + // Can only mark latest nonce as processed + require(nonce >= lastNonce, "Nonce too low"); + // Can only mark nonce as processed once + require(!nonceProcessed[nonce], "Nonce already processed"); + + nonceProcessed[nonce] = true; + + if (nonce != lastNonce) { + // Update last known nonce + lastTransferNonce = nonce; + } + } + + function _getNextNonce() internal returns (uint64) { + uint64 nonce = lastTransferNonce; + + require( + nonce == 0 || nonceProcessed[nonce], + "Pending deposit or withdrawal" + ); + + nonce = nonce + 1; + lastTransferNonce = nonce; + + return nonce; + } +} diff --git a/contracts/contracts/strategies/crosschain/CCTPHookWrapper.sol b/contracts/contracts/strategies/crosschain/CCTPHookWrapper.sol new file mode 100644 index 0000000000..e9efe50d7a --- /dev/null +++ b/contracts/contracts/strategies/crosschain/CCTPHookWrapper.sol @@ -0,0 +1,168 @@ +// 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 { + using BytesHelper for bytes; + + // CCTP Message Header fields + // Ref: https://developers.circle.com/cctp/technical-guide#message-header + uint8 private constant VERSION_INDEX = 0; + uint8 private constant SOURCE_DOMAIN_INDEX = 4; + uint8 private constant SENDER_INDEX = 44; + uint8 private constant MESSAGE_BODY_INDEX = 148; + + // Burn Message V2 fields + uint8 private constant BURN_MESSAGE_V2_VERSION_INDEX = 0; + uint8 private constant BURN_MESSAGE_V2_AMOUNT_INDEX = 68; + 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; + + ICCTPMessageTransmitter public immutable cctpMessageTransmitter; + + constructor(address _cctpMessageTransmitter) { + cctpMessageTransmitter = ICCTPMessageTransmitter( + _cctpMessageTransmitter + ); + } + + 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 relay(bytes calldata message, bytes calldata attestation) + external + { + require( + msg.sender == address(cctpMessageTransmitter), + "Caller is not the CCTP message transmitter" + ); + + // Ensure message version + uint32 version = abi.decode( + message.extractSlice(VERSION_INDEX, VERSION_INDEX + 4), + (uint32) + ); + // Ensure that it's a CCTP message + require( + version == CCTP_MESSAGE_VERSION, + "Invalid CCTP message version" + ); + + uint32 sourceDomainID = abi.decode( + message.extractSlice(SOURCE_DOMAIN_INDEX, SOURCE_DOMAIN_INDEX + 4), + (uint32) + ); + + // Make sure sender is whitelisted + address sender = abi.decode( + message.extractSlice(SENDER_INDEX, SENDER_INDEX + 32), + (address) + ); + address recipientContract = peers[sourceDomainID][sender]; + require( + recipientContract != address(0), + "Sender is not a configured peer" + ); + + // Ensure message body version + bytes memory messageBody = message.extractSlice( + MESSAGE_BODY_INDEX, + message.length + ); + bytes memory versionSlice = messageBody.extractSlice( + BURN_MESSAGE_V2_VERSION_INDEX, + BURN_MESSAGE_V2_VERSION_INDEX + 4 + ); + version = abi.decode(versionSlice, (uint32)); + + bool isBurnMessageV1 = version == 1 && + messageBody.length >= BURN_MESSAGE_V2_HOOK_DATA_INDEX; + + // It's either CCTP Burn message v1 or Origin's custom message + require( + isBurnMessageV1 || version == ORIGIN_MESSAGE_VERSION, + "Invalid CCTP message body version" + ); + + // Relay the message + bool relaySuccess = cctpMessageTransmitter.receiveMessage( + message, + attestation + ); + require(relaySuccess, "Receive message failed"); + + if (isBurnMessageV1) { + require( + messageBody.length >= BURN_MESSAGE_V2_HOOK_DATA_INDEX, + "Invalid burn message" + ); + bytes memory hookData = messageBody.extractSlice( + BURN_MESSAGE_V2_HOOK_DATA_INDEX, + messageBody.length + ); + + bytes memory amountSlice = messageBody.extractSlice( + BURN_MESSAGE_V2_AMOUNT_INDEX, + BURN_MESSAGE_V2_AMOUNT_INDEX + 32 + ); + uint256 tokenAmount = abi.decode(amountSlice, (uint256)); + + bytes memory feeSlice = messageBody.extractSlice( + BURN_MESSAGE_V2_FEE_EXECUTED_INDEX, + BURN_MESSAGE_V2_FEE_EXECUTED_INDEX + 32 + ); + uint256 feeExecuted = abi.decode(feeSlice, (uint256)); + + ICrossChainStrategy(recipientContract).onTokenReceived( + tokenAmount - feeExecuted, + feeExecuted, + hookData + ); + } + } +} diff --git a/contracts/contracts/strategies/crosschain/CrossChainMasterStrategy.sol b/contracts/contracts/strategies/crosschain/CrossChainMasterStrategy.sol new file mode 100644 index 0000000000..ffded88403 --- /dev/null +++ b/contracts/contracts/strategies/crosschain/CrossChainMasterStrategy.sol @@ -0,0 +1,296 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +/** + * @title OUSD Yearn V3 Master Strategy - the Mainnet part + * @author Origin Protocol Inc + */ + +import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import { IERC20, InitializableAbstractStrategy } from "../../utils/InitializableAbstractStrategy.sol"; +import { AbstractCCTPIntegrator } from "./AbstractCCTPIntegrator.sol"; +import { BytesHelper } from "../../utils/BytesHelper.sol"; + +contract CrossChainMasterStrategy is + InitializableAbstractStrategy, + AbstractCCTPIntegrator +{ + using SafeERC20 for IERC20; + + // Remote strategy balance + uint256 public remoteStrategyBalance; + + // Amount that's bridged but not yet received on the destination chain + uint256 public pendingAmount; + + // Transfer amounts by nonce + mapping(uint64 => uint256) public transferAmounts; + + /** + * @param _stratConfig The platform and OToken vault addresses + */ + constructor( + BaseStrategyConfig memory _stratConfig, + address _cctpTokenMessenger, + address _cctpMessageTransmitter, + uint32 _destinationDomain, + address _destinationStrategy, + address _baseToken, + address _cctpHookWrapper + ) + InitializableAbstractStrategy(_stratConfig) + AbstractCCTPIntegrator( + _cctpTokenMessenger, + _cctpMessageTransmitter, + _destinationDomain, + _destinationStrategy, + _baseToken, + _cctpHookWrapper + ) + {} + + // /** + // * @dev Returns the address of the Remote part of the strategy on L2 + // */ + // function remoteAddress() internal virtual returns (address) { + // return address(this); + // } + + /** + * @dev Deposit asset into mainnet strategy making them ready to be + * bridged to Remote part of the strategy + * @param _asset Address of asset to deposit + * @param _amount Amount of asset to deposit + */ + function deposit(address _asset, uint256 _amount) + external + override + onlyVault + nonReentrant + { + _deposit(_asset, _amount); + } + + /** + * @dev Deposit the entire balance + */ + function depositAll() external override onlyVault nonReentrant { + uint256 balance = IERC20(baseToken).balanceOf(address(this)); + if (balance > 0) { + _deposit(baseToken, balance); + } + } + + /** + * @dev Send a withdrawal Wormhole message requesting a certain withdrawal amount + * @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 + ) external override onlyVault nonReentrant { + require(_amount > 0, "Must withdraw something"); + require(_recipient == vaultAddress, "Only Vault can withdraw"); + + // Withdraw the funds from this strategy to the Vault once + // they are allready bridged here + } + + /** + * @dev Remove all assets from platform and send them to Vault contract. + */ + function withdrawAll() external override onlyVaultOrGovernor nonReentrant { + // + // TODO: implement this + } + + /** + * @dev Get the total asset value held in the platform + * @param _asset Address of the asset + * @return balance Total value of the asset in the platform + */ + function checkBalance(address _asset) + external + view + override + returns (uint256 balance) + { + // USDC balance on this contract + // + USDC being bridged + // + USDC cached in the corresponding Remote part of this contract + } + + /** + * @dev Returns bool indicating whether asset is supported by strategy + * @param _asset Address of the asset + */ + function supportsAsset(address _asset) public view override returns (bool) { + return assetToPToken[_asset] != address(0); + } + + /** + * @dev Approve the spending of all assets + */ + function safeApproveAllTokens() + external + override + onlyGovernor + nonReentrant + {} + + /** + * @dev + * @param _asset Address of the asset to approve + * @param _aToken Address of the aToken + */ + // solhint-disable-next-line no-unused-vars + function _abstractSetPToken(address _asset, address _aToken) + internal + override + {} + + /** + * @dev + */ + function collectRewardTokens() + external + override + onlyHarvester + nonReentrant + {} + + 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) { + // 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); + } + + revert("Unknown message type"); + } + + function _onTokenReceived( + uint256 tokenAmount, + 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(payload); + } + + function _deposit(address _asset, uint256 depositAmount) internal virtual { + require(_asset == baseToken, "Unsupported asset"); + + uint64 nonce = _getNextNonce(); + + 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, _amount); + + 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; + + // 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; + } + + function _withdraw(address _recipient, uint256 _amount) internal virtual { + require(_amount > 0, "Withdraw amount must be greater than 0"); + require( + _amount <= MAX_TRANSFER_AMOUNT, + "Withdraw amount exceeds max transfer amount" + ); + + uint64 nonce = _getNextNonce(); + + emit Withdrawal(baseToken, baseToken, _amount); + + transferAmounts[nonce] = _amount; + + // Send withdrawal message with payload + bytes memory message = _encodeWithdrawMessage(nonce, _amount); + _sendMessage(message); + } + + function _processWithdrawAckMessage(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; + } + + function _processBalanceCheckMessage(bytes memory message) + internal + virtual + { + (uint64 nonce, uint256 balance) = _decodeBalanceCheckMessage(message); + + uint256 _lastNonce = lastTransferNonce; + + if (_lastNonce != nonce || !isNonceProcessed(_lastNonce)) { + // Do not update pending amount if the nonce is not the latest one + return; + } + + // Update balance + remoteStrategyBalance = balance; + } +} diff --git a/contracts/contracts/strategies/crosschain/CrossChainRemoteStrategy.sol b/contracts/contracts/strategies/crosschain/CrossChainRemoteStrategy.sol new file mode 100644 index 0000000000..03dd54967a --- /dev/null +++ b/contracts/contracts/strategies/crosschain/CrossChainRemoteStrategy.sol @@ -0,0 +1,150 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +/** + * @title OUSD Yearn V3 Remote Strategy - the L2 chain part + * @author Origin Protocol Inc + */ + +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"; + +contract CrossChainRemoteStrategy is + AbstractCCTPIntegrator, + Generalized4626Strategy +{ + using SafeERC20 for IERC20; + + constructor( + BaseStrategyConfig memory _baseConfig, + address _cctpTokenMessenger, + address _cctpMessageTransmitter, + uint32 _destinationDomain, + address _destinationStrategy, + address _baseToken, + address _cctpHookWrapper + ) + AbstractCCTPIntegrator( + _cctpTokenMessenger, + _cctpMessageTransmitter, + _destinationDomain, + _destinationStrategy, + _baseToken, + _cctpHookWrapper + ) + Generalized4626Strategy(_baseConfig, _baseToken) + {} + + function deposit(address _asset, uint256 _amount) + external + virtual + override + { + // TODO: implement this + revert("Not implemented"); + } + + function depositAll() external virtual override { + // TODO: implement this + revert("Not implemented"); + } + + function withdraw( + address _recipient, + address _asset, + uint256 _amount + ) external virtual override { + // TODO: implement this + revert("Not implemented"); + } + + function withdrawAll() external virtual override { + // TODO: implement this + revert("Not implemented"); + } + + function _onMessageReceived(bytes memory payload) internal override { + uint32 messageType = _getMessageType(payload); + if (messageType == DEPOSIT_MESSAGE) { + // // Received when Master strategy sends tokens to the remote strategy + // Do nothing because we receive acknowledgement with token transfer, so _onTokenReceived will handle it + // TODO: Should _onTokenReceived always call _onMessageReceived? + // _processDepositAckMessage(payload); + } else if (messageType == WITHDRAW_MESSAGE_TYPE) { + // Received when Master strategy requests a withdrawal + _processWithdrawMessage(payload); + } + + revert("Unknown message type"); + } + + function _processDepositMessage( + uint256 tokenAmount, + uint256 feeExecuted, + bytes memory payload + ) internal virtual { + (uint64 nonce, uint256 depositAmount) = _decodeDepositMessage(payload); + + // Replay protection + require(!isNonceProcessed(nonce), "Nonce already processed"); + _markNonceAsProcessed(nonce); + + // Deposit everything we got + uint256 balance = IERC20(baseToken).balanceOf(address(this)); + _deposit(baseToken, balance); + + uint256 balanceAfter = checkBalance(baseToken); + + bytes memory message = _encodeDepositAckMessage( + nonce, + tokenAmount, + feeExecuted, + balanceAfter + ); + _sendMessage(message); + } + + function _processWithdrawMessage(bytes memory payload) internal virtual { + (uint64 nonce, uint256 withdrawAmount) = _decodeWithdrawMessage( + payload + ); + + // Replay protection + require(!isNonceProcessed(nonce), "Nonce already processed"); + _markNonceAsProcessed(nonce); + + // Withdraw funds to the remote strategy + _withdraw(address(this), baseToken, withdrawAmount); + + // Check balance after withdrawal + uint256 balanceAfter = checkBalance(baseToken); + + bytes memory message = _encodeWithdrawAckMessage( + nonce, + withdrawAmount, + balanceAfter + ); + _sendTokens(withdrawAmount, message); + } + + function _onTokenReceived( + uint256 tokenAmount, + uint256 feeExecuted, + bytes memory payload + ) internal override { + uint32 messageType = _getMessageType(payload); + + require(messageType == DEPOSIT_MESSAGE, "Invalid message type"); + + _processDepositMessage(tokenAmount, feeExecuted, payload); + } + + function sendBalanceUpdate() external virtual override { + // TODO: Add permissioning + uint256 balance = checkBalance(baseToken); + bytes memory message = _encodeBalanceUpdateMessage(balance); + _sendMessage(message); + } +} diff --git a/contracts/contracts/utils/BytesHelper.sol b/contracts/contracts/utils/BytesHelper.sol new file mode 100644 index 0000000000..aa6ef13d47 --- /dev/null +++ b/contracts/contracts/utils/BytesHelper.sol @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +library BytesHelper { + /** + * @dev Extract a slice from bytes memory + * @param data The bytes memory to slice + * @param start The start index (inclusive) + * @param end The end index (exclusive) + * @return result A new bytes memory containing the slice + */ + function extractSlice( + bytes memory data, + uint256 start, + uint256 end + ) private pure returns (bytes memory) { + require(end >= start, "Invalid slice range"); + require(end <= data.length, "Slice end exceeds data length"); + + uint256 length = end - start; + bytes memory result = new bytes(length); + + // Simple byte-by-byte copy + for (uint256 i = 0; i < length; i++) { + result[i] = data[start + i]; + } + + return result; + } +} diff --git a/contracts/deploy/base/040_yearn_strategy.js b/contracts/deploy/base/040_yearn_strategy.js index 920ac16d68..dc8c147886 100644 --- a/contracts/deploy/base/040_yearn_strategy.js +++ b/contracts/deploy/base/040_yearn_strategy.js @@ -1,26 +1,31 @@ const { deployOnBase } = require("../../utils/deploy-l2"); -const addresses = require("../../utils/addresses"); -const { deployProxyWithCreateX, deployYearn3SlaveStrategyImpl } = require("../deployActions"); +// const addresses = require("../../utils/addresses"); const { - deployWithConfirmation, - withConfirmation, -} = require("../../utils/deploy.js"); + deployProxyWithCreateX, + deployYearn3RemoteStrategyImpl, +} = require("../deployActions"); +// const { +// deployWithConfirmation, +// withConfirmation, +// } = require("../../utils/deploy.js"); module.exports = deployOnBase( { deployName: "040_yearn_strategy", }, - async ({ ethers }) => { + async () => { const salt = "Yean strategy 1"; - const proxyAddress = await deployProxyWithCreateX(salt, "YearnV3SlaveStrategyProxy"); - console.log(`YearnV3SlaveStrategyProxy address: ${proxyAddress}`); - - const implAddress = await deployYearn3SlaveStrategyImpl(proxyAddress); - console.log(`YearnV3SlaveStrategyImpl address: ${implAddress}`); + const proxyAddress = await deployProxyWithCreateX( + salt, + "CrossChainRemoteStrategyProxy" + ); + console.log(`CrossChainRemoteStrategyProxy address: ${proxyAddress}`); + + const implAddress = await deployYearn3RemoteStrategyImpl(proxyAddress); + console.log(`CrossChainRemoteStrategyImpl address: ${implAddress}`); return { - actions: [ - ], + actions: [], }; } ); diff --git a/contracts/deploy/deployActions.js b/contracts/deploy/deployActions.js index d0c77f6d38..b9ee798e20 100644 --- a/contracts/deploy/deployActions.js +++ b/contracts/deploy/deployActions.js @@ -17,7 +17,11 @@ const { isHoodi, isHoodiOrFork, } = require("../test/helpers.js"); -const { deployWithConfirmation, withConfirmation, encodeSaltForCreateX } = require("../utils/deploy"); +const { + deployWithConfirmation, + withConfirmation, + encodeSaltForCreateX, +} = require("../utils/deploy"); const { metapoolLPCRVPid } = require("../utils/constants"); const { replaceContractAt } = require("../utils/hardhat"); const { resolveContract } = require("../utils/resolvers"); @@ -1698,7 +1702,7 @@ const deployProxyWithCreateX = async (salt, proxyName) => { const ProxyContract = await ethers.getContractFactory(proxyName); const encodedArgs = ProxyContract.interface.encodeDeploy([deployerAddr]); return ethers.utils.hexConcat([ProxyContract.bytecode, encodedArgs]); - } + }; const txResponse = await withConfirmation( cCreateX @@ -1714,41 +1718,44 @@ const deployProxyWithCreateX = async (salt, proxyName) => { .find((event) => event.topics[0] === contractCreationTopic) .topics[1].slice(26)}` ); - + return proxyAddress; }; // deploys and initializes the Yearn 3 master strategy -const deployYearn3MasterStrategyImpl = async (proxyAddress, implementationName = "YearnV3MasterStrategy") => { +const deployYearn3MasterStrategyImpl = async ( + proxyAddress, + implementationName = "CrossChainMasterStrategy" +) => { const { deployerAddr } = await getNamedAccounts(); const sDeployer = await ethers.provider.getSigner(deployerAddr); log(`Deploying Yearn3MasterStrategyImpl as deployer ${deployerAddr}`); - const cYearnV3MasterStrategyProxy = await ethers.getContractAt( - "YearnV3MasterStrategyProxy", + const cCrossChainMasterStrategyProxy = await ethers.getContractAt( + "CrossChainMasterStrategyProxy", proxyAddress ); - const dYearnV3MasterStrategy = await deployWithConfirmation( + const dCrossChainMasterStrategy = await deployWithConfirmation( implementationName, [ [ addresses.zero, // platform address - addresses.mainnet.Vault - ] + addresses.mainnet.Vault, + ], ] ); - // const initData = cYearnV3MasterStrategy.interface.encodeFunctionData( + // const initData = cCrossChainMasterStrategy.interface.encodeFunctionData( // "initialize()", // [] // ); - + // Init the proxy to point at the implementation, set the governor, and call initialize const initFunction = "initialize(address,address,bytes)"; await withConfirmation( - cYearnV3MasterStrategyProxy.connect(sDeployer)[initFunction]( - dYearnV3MasterStrategy.address, + cCrossChainMasterStrategyProxy.connect(sDeployer)[initFunction]( + dCrossChainMasterStrategy.address, addresses.mainnet.Timelock, // governor //initData, // data for delegate call to the initialize function on the strategy "0x", @@ -1756,35 +1763,38 @@ const deployYearn3MasterStrategyImpl = async (proxyAddress, implementationName = ) ); - return dYearnV3MasterStrategy.address; + return dCrossChainMasterStrategy.address; }; -// deploys and initializes the Yearn 3 slave strategy -const deployYearn3SlaveStrategyImpl = async (proxyAddress, implementationName = "YearnV3SlaveStrategy") => { +// deploys and initializes the Yearn 3 remote strategy +const deployYearn3RemoteStrategyImpl = async ( + proxyAddress, + implementationName = "CrossChainRemoteStrategy" +) => { const { deployerAddr } = await getNamedAccounts(); const sDeployer = await ethers.provider.getSigner(deployerAddr); - log(`Deploying Yearn3SlaveStrategyImpl as deployer ${deployerAddr}`); + log(`Deploying Yearn3RemoteStrategyImpl as deployer ${deployerAddr}`); - const cYearnV3SlaveStrategyProxy = await ethers.getContractAt( - "YearnV3SlaveStrategyProxy", + const cCrossChainRemoteStrategyProxy = await ethers.getContractAt( + "CrossChainRemoteStrategyProxy", proxyAddress ); - const dYearnV3SlaveStrategy = await deployWithConfirmation( + const dCrossChainRemoteStrategy = await deployWithConfirmation( implementationName, [] ); - // const initData = cYearnV3MasterStrategy.interface.encodeFunctionData( + // const initData = cCrossChainMasterStrategy.interface.encodeFunctionData( // "initialize()", // [] // ); - + // Init the proxy to point at the implementation, set the governor, and call initialize const initFunction = "initialize(address,address,bytes)"; await withConfirmation( - cYearnV3SlaveStrategyProxy.connect(sDeployer)[initFunction]( - dYearnV3SlaveStrategy.address, + cCrossChainRemoteStrategyProxy.connect(sDeployer)[initFunction]( + dCrossChainRemoteStrategy.address, addresses.base.timelock, // governor //initData, // data for delegate call to the initialize function on the strategy "0x", @@ -1792,7 +1802,7 @@ const deployYearn3SlaveStrategyImpl = async (proxyAddress, implementationName = ) ); - return dYearnV3SlaveStrategy.address; + return dCrossChainRemoteStrategy.address; }; module.exports = { @@ -1834,5 +1844,5 @@ module.exports = { deploySonicSwapXAMOStrategyImplementation, deployProxyWithCreateX, deployYearn3MasterStrategyImpl, - deployYearn3SlaveStrategyImpl, + deployYearn3RemoteStrategyImpl, }; diff --git a/contracts/deploy/mainnet/159_yearn_strategy.js b/contracts/deploy/mainnet/159_yearn_strategy.js index 7c7aa481b3..93f924f3f9 100644 --- a/contracts/deploy/mainnet/159_yearn_strategy.js +++ b/contracts/deploy/mainnet/159_yearn_strategy.js @@ -1,6 +1,9 @@ const { deploymentWithGovernanceProposal } = require("../../utils/deploy"); -const addresses = require("../../utils/addresses"); -const { deployProxyWithCreateX, deployYearn3MasterStrategyImpl } = require("../deployActions"); +// const addresses = require("../../utils/addresses"); +const { + deployProxyWithCreateX, + deployYearn3MasterStrategyImpl, +} = require("../deployActions"); module.exports = deploymentWithGovernanceProposal( { @@ -10,14 +13,17 @@ module.exports = deploymentWithGovernanceProposal( deployerIsProposer: false, proposalId: "", }, - async ({ deployWithConfirmation }) => { + async () => { // the salt needs to match the salt on the base chain deploying the other part of the strategy const salt = "Yean strategy 1"; - const proxyAddress = await deployProxyWithCreateX(salt, "YearnV3MasterStrategyProxy"); - console.log(`YearnV3MasterStrategyProxy address: ${proxyAddress}`); - + const proxyAddress = await deployProxyWithCreateX( + salt, + "CrossChainMasterStrategyProxy" + ); + console.log(`CrossChainMasterStrategyProxy address: ${proxyAddress}`); + const implAddress = await deployYearn3MasterStrategyImpl(proxyAddress); - console.log(`YearnV3MasterStrategyImpl address: ${implAddress}`); + console.log(`CrossChainMasterStrategyImpl address: ${implAddress}`); return { actions: [], diff --git a/contracts/test/_fixture.js b/contracts/test/_fixture.js index 96bf737477..b710f8a327 100644 --- a/contracts/test/_fixture.js +++ b/contracts/test/_fixture.js @@ -16,7 +16,10 @@ const { fundAccountsForOETHUnitTests, } = require("../utils/funding"); const { deployWithConfirmation } = require("../utils/deploy"); -const { deployYearn3MasterStrategyImpl, deployYearn3SlaveStrategyImpl } = require("../deploy/deployActions.js"); +const { + deployYearn3MasterStrategyImpl, + deployYearn3RemoteStrategyImpl, +} = require("../deploy/deployActions.js"); const { replaceContractAt } = require("../utils/hardhat"); const { @@ -2530,38 +2533,49 @@ async function yearnCrossChainFixture() { const fixture = await defaultFixture(); const { deployerAddr } = await getNamedAccounts(); const sDeployer = await ethers.provider.getSigner(deployerAddr); - + // deploy master strategy - const masterProxy = await deployWithConfirmation("YearnV3MasterStrategyProxy", [ - deployerAddr - ] + const masterProxy = await deployWithConfirmation( + "CrossChainMasterStrategyProxy", + [deployerAddr] ); const masterProxyAddress = masterProxy.address; - log(`YearnV3MasterStrategyProxy address: ${masterProxyAddress}`); - let implAddress = await deployYearn3MasterStrategyImpl(masterProxyAddress, "YearnV3MasterStrategyMock"); - log(`YearnV3MasterStrategyMockImpl address: ${implAddress}`); - - - // deploy slave strategy - const slaveProxy = await deployWithConfirmation("YearnV3SlaveStrategyProxy", [ - deployerAddr - ] + log(`CrossChainMasterStrategyProxy address: ${masterProxyAddress}`); + let implAddress = await deployYearn3MasterStrategyImpl( + masterProxyAddress, + "CrossChainMasterStrategyMock" ); + log(`CrossChainMasterStrategyMockImpl address: ${implAddress}`); + + // deploy remote strategy + const remoteProxy = await deployWithConfirmation( + "CrossChainRemoteStrategyProxy", + [deployerAddr] + ); + + const remoteProxyAddress = remoteProxy.address; + log(`CrossChainRemoteStrategyProxy address: ${remoteProxyAddress}`); - const slaveProxyAddress = slaveProxy.address; - log(`YearnV3SlaveStrategyProxy address: ${slaveProxyAddress}`); - - implAddress = await deployYearn3SlaveStrategyImpl(slaveProxyAddress, "YearnV3SlaveStrategyMock"); - log(`YearnV3SlaveStrategyMockImpl address: ${implAddress}`); + implAddress = await deployYearn3RemoteStrategyImpl( + remoteProxyAddress, + "CrossChainRemoteStrategyMock" + ); + log(`CrossChainRemoteStrategyMockImpl address: ${implAddress}`); + + const yearnMasterStrategy = await ethers.getContractAt( + "CrossChainMasterStrategyMock", + masterProxyAddress + ); + const yearnRemoteStrategy = await ethers.getContractAt( + "CrossChainRemoteStrategyMock", + remoteProxyAddress + ); - const yearnMasterStrategy = await ethers.getContractAt("YearnV3MasterStrategyMock", masterProxyAddress); - const yearnSlaveStrategy = await ethers.getContractAt("YearnV3SlaveStrategyMock", slaveProxyAddress); - - yearnMasterStrategy.connect(sDeployer).setSlaveAddress(slaveProxyAddress); - yearnSlaveStrategy.connect(sDeployer).setMasterAddress(masterProxyAddress); + yearnMasterStrategy.connect(sDeployer).setRemoteAddress(remoteProxyAddress); + yearnRemoteStrategy.connect(sDeployer).setMasterAddress(masterProxyAddress); fixture.yearnMasterStrategy = yearnMasterStrategy; - fixture.yearnSlaveStrategy = yearnSlaveStrategy; + fixture.yearnRemoteStrategy = yearnRemoteStrategy; return fixture; } diff --git a/contracts/test/strategies/crossChain/yearnV3Strategy.js b/contracts/test/strategies/crossChain/yearnV3Strategy.js index c85e39ca8d..d4a03cda15 100644 --- a/contracts/test/strategies/crossChain/yearnV3Strategy.js +++ b/contracts/test/strategies/crossChain/yearnV3Strategy.js @@ -1,22 +1,28 @@ const { expect } = require("chai"); -const { utils } = require("ethers"); -const { createFixtureLoader, yearnCrossChainFixture } = require("../../_fixture"); +const { + createFixtureLoader, + yearnCrossChainFixture, +} = require("../../_fixture"); -describe.only("Yearn V3 Cross Chain Strategy", function () { +describe("Yearn V3 Cross Chain Strategy", function () { let fixture; const loadFixture = createFixtureLoader(yearnCrossChainFixture); - let yearnMasterStrategy, yearnSlaveStrategy; + let yearnMasterStrategy, yearnRemoteStrategy; beforeEach(async function () { fixture = await loadFixture(); yearnMasterStrategy = fixture.yearnMasterStrategy; - yearnSlaveStrategy = fixture.yearnSlaveStrategy; + yearnRemoteStrategy = fixture.yearnRemoteStrategy; }); it("Should have correct initial state", async function () { - expect(await yearnMasterStrategy._slaveAddress()).to.equal(yearnSlaveStrategy.address); - expect(await yearnSlaveStrategy._masterAddress()).to.equal(yearnMasterStrategy.address); + expect(await yearnMasterStrategy._remoteAddress()).to.equal( + yearnRemoteStrategy.address + ); + expect(await yearnRemoteStrategy._masterAddress()).to.equal( + yearnMasterStrategy.address + ); }); -}); \ No newline at end of file +}); diff --git a/contracts/utils/deploy.js b/contracts/utils/deploy.js index 0feae6e12f..dd1d85d9b6 100644 --- a/contracts/utils/deploy.js +++ b/contracts/utils/deploy.js @@ -1128,16 +1128,16 @@ function deploymentWithGuardianGovernor(opts, fn) { return main; } -function encodeSaltForCreateX(deployer, crossChainProtectionFlag, salt) { - // Generate encoded salt (deployer address || crossChainProtectionFlag || bytes11(keccak256(rewardToken, gauge))) +function encodeSaltForCreateX(deployer, crosschainProtectionFlag, salt) { + // Generate encoded salt (deployer address || crosschainProtectionFlag || bytes11(keccak256(rewardToken, gauge))) // convert deployer address to bytes20 const addressDeployerBytes20 = ethers.utils.hexlify( ethers.utils.zeroPad(deployer, 20) ); - // convert crossChainProtectionFlag to bytes1 - const crossChainProtectionFlagBytes1 = crossChainProtectionFlag + // convert crosschainProtectionFlag to bytes1 + const crosschainProtectionFlagBytes1 = crosschainProtectionFlag ? "0x01" : "0x00"; @@ -1149,7 +1149,7 @@ function encodeSaltForCreateX(deployer, crossChainProtectionFlag, salt) { const encodedSalt = ethers.utils.hexlify( ethers.utils.concat([ addressDeployerBytes20, - crossChainProtectionFlagBytes1, + crosschainProtectionFlagBytes1, saltBytes11, ]) ); From 9517fca313d2fed5fe7d753b0172c4c56ccdec45 Mon Sep 17 00:00:00 2001 From: Shahul Hameed <10547529+shahthepro@users.noreply.github.com> Date: Thu, 11 Dec 2025 18:56:03 +0400 Subject: [PATCH 004/101] Fix compiling issues --- .../CrossChainMasterStrategyMock.sol | 2 +- .../CrossChainRemoteStrategyMock.sol | 2 +- contracts/contracts/proxies/Proxies.sol | 9 ++++ .../strategies/Generalized4626Strategy.sol | 2 +- .../crosschain/AbstractCCTPIntegrator.sol | 49 ++++++++----------- .../crosschain/CrossChainMasterStrategy.sol | 36 ++++++++++---- .../crosschain/CrossChainRemoteStrategy.sol | 17 ++++--- contracts/contracts/utils/BytesHelper.sol | 2 +- ....js => 040_crosschain_strategy_proxies.js} | 10 ++-- contracts/deploy/deployActions.js | 24 ++++++--- ....js => 159_crosschain_strategy_proxies.js} | 16 ++++-- contracts/test/_fixture.js | 8 +-- contracts/utils/addresses.js | 5 ++ contracts/utils/cctp.js | 8 +++ 14 files changed, 119 insertions(+), 71 deletions(-) rename contracts/deploy/base/{040_yearn_strategy.js => 040_crosschain_strategy_proxies.js} (65%) rename contracts/deploy/mainnet/{159_yearn_strategy.js => 159_crosschain_strategy_proxies.js} (57%) create mode 100644 contracts/utils/cctp.js diff --git a/contracts/contracts/mocks/crosschain/CrossChainMasterStrategyMock.sol b/contracts/contracts/mocks/crosschain/CrossChainMasterStrategyMock.sol index 9019c0125e..8ed3c46c7b 100644 --- a/contracts/contracts/mocks/crosschain/CrossChainMasterStrategyMock.sol +++ b/contracts/contracts/mocks/crosschain/CrossChainMasterStrategyMock.sol @@ -11,7 +11,7 @@ contract CrossChainMasterStrategyMock { constructor() {} - function remoteAddress() internal override returns (address) { + function remoteAddress() public view returns (address) { return _remoteAddress; } diff --git a/contracts/contracts/mocks/crosschain/CrossChainRemoteStrategyMock.sol b/contracts/contracts/mocks/crosschain/CrossChainRemoteStrategyMock.sol index fafa848097..43deb9f34c 100644 --- a/contracts/contracts/mocks/crosschain/CrossChainRemoteStrategyMock.sol +++ b/contracts/contracts/mocks/crosschain/CrossChainRemoteStrategyMock.sol @@ -11,7 +11,7 @@ contract CrossChainRemoteStrategyMock { constructor() {} - function masterAddress() internal override returns (address) { + function masterAddress() public view returns (address) { return _masterAddress; } diff --git a/contracts/contracts/proxies/Proxies.sol b/contracts/contracts/proxies/Proxies.sol index 67d747f640..c75898e31e 100644 --- a/contracts/contracts/proxies/Proxies.sol +++ b/contracts/contracts/proxies/Proxies.sol @@ -343,3 +343,12 @@ contract CrossChainRemoteStrategyProxy is InitializeGovernedUpgradeabilityProxy2(governor) {} } + +/** + * @notice CCTPHookWrapperProxy delegates calls to a CCTPHookWrapper implementation + */ +contract CCTPHookWrapperProxy is InitializeGovernedUpgradeabilityProxy2 { + constructor(address governor) + InitializeGovernedUpgradeabilityProxy2(governor) + {} +} diff --git a/contracts/contracts/strategies/Generalized4626Strategy.sol b/contracts/contracts/strategies/Generalized4626Strategy.sol index deda1e32be..e847b96009 100644 --- a/contracts/contracts/strategies/Generalized4626Strategy.sol +++ b/contracts/contracts/strategies/Generalized4626Strategy.sol @@ -156,7 +156,7 @@ contract Generalized4626Strategy is InitializableAbstractStrategy { * @return balance Total value of the asset in the platform */ function checkBalance(address _asset) - external + public view virtual override diff --git a/contracts/contracts/strategies/crosschain/AbstractCCTPIntegrator.sol b/contracts/contracts/strategies/crosschain/AbstractCCTPIntegrator.sol index b0aff9fba3..a3e93a7bc9 100644 --- a/contracts/contracts/strategies/crosschain/AbstractCCTPIntegrator.sol +++ b/contracts/contracts/strategies/crosschain/AbstractCCTPIntegrator.sol @@ -8,8 +8,8 @@ import { ICCTPTokenMessenger, ICCTPMessageTransmitter, IMessageHandlerV2 } from import { Governable } from "../../governance/Governable.sol"; +import { BytesHelper } from "../../utils/BytesHelper.sol"; import "../../utils/Helpers.sol"; -import "../../utils/BytesHelper.sol"; abstract contract AbstractCCTPIntegrator is Governable, IMessageHandlerV2 { using BytesHelper for bytes; @@ -237,7 +237,7 @@ abstract contract AbstractCCTPIntegrator is Governable, IMessageHandlerV2 { return abi.encodePacked( ORIGIN_MESSAGE_VERSION, - DEPOSIT_MESSAGE_TYPE, + DEPOSIT_MESSAGE, nonce, depositAmount ); @@ -258,7 +258,7 @@ abstract contract AbstractCCTPIntegrator is Governable, IMessageHandlerV2 { version == ORIGIN_MESSAGE_VERSION, "Invalid Origin Message Version" ); - require(messageType == DEPOSIT_MESSAGE_TYPE, "Invalid Message type"); + require(messageType == DEPOSIT_MESSAGE, "Invalid Message type"); return (nonce, depositAmount); } @@ -271,7 +271,7 @@ abstract contract AbstractCCTPIntegrator is Governable, IMessageHandlerV2 { return abi.encodePacked( ORIGIN_MESSAGE_VERSION, - DEPOSIT_ACK_MESSAGE_TYPE, + DEPOSIT_ACK_MESSAGE, nonce, amountReceived, feeExecuted, @@ -304,10 +304,7 @@ abstract contract AbstractCCTPIntegrator is Governable, IMessageHandlerV2 { version == ORIGIN_MESSAGE_VERSION, "Invalid Origin Message Version" ); - require( - messageType == DEPOSIT_ACK_MESSAGE_TYPE, - "Invalid Message type" - ); + require(messageType == DEPOSIT_ACK_MESSAGE, "Invalid Message type"); return (nonce, amountReceived, feeExecuted, balanceAfter); } @@ -319,7 +316,7 @@ abstract contract AbstractCCTPIntegrator is Governable, IMessageHandlerV2 { return abi.encodePacked( ORIGIN_MESSAGE_VERSION, - WITHDRAW_MESSAGE_TYPE, + WITHDRAW_MESSAGE, nonce, withdrawAmount ); @@ -331,16 +328,16 @@ abstract contract AbstractCCTPIntegrator is Governable, IMessageHandlerV2 { returns (uint64 nonce, uint256 withdrawAmount) { ( - uint332 version, - uint332 messageType, + uint32 version, + uint32 messageType, uint64 nonce, uint256 withdrawAmount - ) = abi.decode(message, (uint332, uint332, uint64, uint256)); + ) = abi.decode(message, (uint32, uint32, uint64, uint256)); require( version == ORIGIN_MESSAGE_VERSION, "Invalid Origin Message Version" ); - require(messageType == WITHDRAW_MESSAGE_TYPE, "Invalid Message type"); + require(messageType == WITHDRAW_MESSAGE, "Invalid Message type"); return (nonce, withdrawAmount); } @@ -352,7 +349,7 @@ abstract contract AbstractCCTPIntegrator is Governable, IMessageHandlerV2 { return abi.encodePacked( ORIGIN_MESSAGE_VERSION, - WITHDRAW_ACK_MESSAGE_TYPE, + WITHDRAW_ACK_MESSAGE, nonce, amountSent, balanceAfter @@ -369,20 +366,17 @@ abstract contract AbstractCCTPIntegrator is Governable, IMessageHandlerV2 { ) { ( - uint332 version, - uint332 messageType, + uint32 version, + uint32 messageType, uint64 nonce, uint256 amountSent, uint256 balanceAfter - ) = abi.decode(message, (uint332, uint332, uint64, uint256, uint256)); + ) = abi.decode(message, (uint32, uint32, uint64, uint256, uint256)); require( version == ORIGIN_MESSAGE_VERSION, "Invalid Origin Message Version" ); - require( - messageType == WITHDRAW_ACK_MESSAGE_TYPE, - "Invalid Message type" - ); + require(messageType == WITHDRAW_ACK_MESSAGE, "Invalid Message type"); return (nonce, amountSent, balanceAfter); } @@ -394,7 +388,7 @@ abstract contract AbstractCCTPIntegrator is Governable, IMessageHandlerV2 { return abi.encodePacked( ORIGIN_MESSAGE_VERSION, - BALANCE_CHECK_MESSAGE_TYPE, + BALANCE_CHECK_MESSAGE, nonce, balance ); @@ -406,19 +400,16 @@ abstract contract AbstractCCTPIntegrator is Governable, IMessageHandlerV2 { returns (uint64 nonce, uint256 balance) { ( - uint332 version, - uint332 messageType, + uint32 version, + uint32 messageType, uint64 nonce, uint256 balance - ) = abi.decode(message, (uint332, uint332, uint64, uint256)); + ) = abi.decode(message, (uint32, uint32, uint64, uint256)); require( version == ORIGIN_MESSAGE_VERSION, "Invalid Origin Message Version" ); - require( - messageType == BALANCE_CHECK_MESSAGE_TYPE, - "Invalid Message type" - ); + require(messageType == BALANCE_CHECK_MESSAGE, "Invalid Message type"); return (nonce, balance); } diff --git a/contracts/contracts/strategies/crosschain/CrossChainMasterStrategy.sol b/contracts/contracts/strategies/crosschain/CrossChainMasterStrategy.sol index ffded88403..03acd9ca20 100644 --- a/contracts/contracts/strategies/crosschain/CrossChainMasterStrategy.sol +++ b/contracts/contracts/strategies/crosschain/CrossChainMasterStrategy.sol @@ -92,19 +92,17 @@ contract CrossChainMasterStrategy is address _asset, uint256 _amount ) external override onlyVault nonReentrant { - require(_amount > 0, "Must withdraw something"); require(_recipient == vaultAddress, "Only Vault can withdraw"); - // Withdraw the funds from this strategy to the Vault once - // they are allready bridged here + _withdraw(_asset, _recipient, _amount); } /** * @dev Remove all assets from platform and send them to Vault contract. */ function withdrawAll() external override onlyVaultOrGovernor nonReentrant { - // - // TODO: implement this + uint256 balance = IERC20(baseToken).balanceOf(address(this)); + _withdraw(baseToken, vaultAddress, balance); } /** @@ -190,7 +188,7 @@ contract CrossChainMasterStrategy is // Only withdraw acknowledgements are expected here require(messageType == WITHDRAW_ACK_MESSAGE, "Invalid message type"); - _processWithdrawAckMessage(payload); + _processWithdrawAckMessage(tokenAmount, feeExecuted, payload); } function _deposit(address _asset, uint256 depositAmount) internal virtual { @@ -204,7 +202,7 @@ contract CrossChainMasterStrategy is "Deposit amount exceeds max transfer amount" ); - emit Deposit(_asset, _asset, _amount); + emit Deposit(_asset, _asset, depositAmount); transferAmounts[nonce] = depositAmount; @@ -237,10 +235,20 @@ contract CrossChainMasterStrategy is // Subtract from pending amount pendingAmount = pendingAmount - amountReceived; + + // Update balance + remoteStrategyBalance = balanceAfter; } - function _withdraw(address _recipient, uint256 _amount) internal virtual { + function _withdraw( + address _asset, + address _recipient, + uint256 _amount + ) internal virtual { + require(_asset == baseToken, "Unsupported asset"); require(_amount > 0, "Withdraw amount must be greater than 0"); + require(_recipient == vaultAddress, "Only Vault can withdraw"); + require( _amount <= MAX_TRANSFER_AMOUNT, "Withdraw amount exceeds max transfer amount" @@ -257,7 +265,12 @@ contract CrossChainMasterStrategy is _sendMessage(message); } - function _processWithdrawAckMessage(bytes memory message) internal virtual { + function _processWithdrawAckMessage( + uint256 tokenAmount, + // solhint-disable-next-line no-unused-vars + uint256 feeExecuted, + bytes memory message + ) internal virtual { ( uint64 nonce, uint256 amountSent, @@ -275,6 +288,9 @@ contract CrossChainMasterStrategy is // Update balance remoteStrategyBalance = balanceAfter; + + // Transfer tokens to vault + IERC20(baseToken).safeTransfer(vaultAddress, tokenAmount); } function _processBalanceCheckMessage(bytes memory message) @@ -283,7 +299,7 @@ contract CrossChainMasterStrategy is { (uint64 nonce, uint256 balance) = _decodeBalanceCheckMessage(message); - uint256 _lastNonce = lastTransferNonce; + uint64 _lastNonce = lastTransferNonce; if (_lastNonce != nonce || !isNonceProcessed(_lastNonce)) { // Do not update pending amount if the nonce is not the latest one diff --git a/contracts/contracts/strategies/crosschain/CrossChainRemoteStrategy.sol b/contracts/contracts/strategies/crosschain/CrossChainRemoteStrategy.sol index 03dd54967a..64d29d1e23 100644 --- a/contracts/contracts/strategies/crosschain/CrossChainRemoteStrategy.sol +++ b/contracts/contracts/strategies/crosschain/CrossChainRemoteStrategy.sol @@ -37,6 +37,7 @@ contract CrossChainRemoteStrategy is Generalized4626Strategy(_baseConfig, _baseToken) {} + // solhint-disable-next-line no-unused-vars function deposit(address _asset, uint256 _amount) external virtual @@ -52,9 +53,9 @@ contract CrossChainRemoteStrategy is } function withdraw( - address _recipient, - address _asset, - uint256 _amount + address, + address, + uint256 ) external virtual override { // TODO: implement this revert("Not implemented"); @@ -72,7 +73,7 @@ contract CrossChainRemoteStrategy is // Do nothing because we receive acknowledgement with token transfer, so _onTokenReceived will handle it // TODO: Should _onTokenReceived always call _onMessageReceived? // _processDepositAckMessage(payload); - } else if (messageType == WITHDRAW_MESSAGE_TYPE) { + } else if (messageType == WITHDRAW_MESSAGE) { // Received when Master strategy requests a withdrawal _processWithdrawMessage(payload); } @@ -85,6 +86,7 @@ contract CrossChainRemoteStrategy is uint256 feeExecuted, bytes memory payload ) internal virtual { + // solhint-disable-next-line no-unused-vars (uint64 nonce, uint256 depositAmount) = _decodeDepositMessage(payload); // Replay protection @@ -141,10 +143,13 @@ contract CrossChainRemoteStrategy is _processDepositMessage(tokenAmount, feeExecuted, payload); } - function sendBalanceUpdate() external virtual override { + function sendBalanceUpdate() external virtual { // TODO: Add permissioning uint256 balance = checkBalance(baseToken); - bytes memory message = _encodeBalanceUpdateMessage(balance); + bytes memory message = _encodeBalanceCheckMessage( + lastTransferNonce, + balance + ); _sendMessage(message); } } diff --git a/contracts/contracts/utils/BytesHelper.sol b/contracts/contracts/utils/BytesHelper.sol index aa6ef13d47..e5c9319b21 100644 --- a/contracts/contracts/utils/BytesHelper.sol +++ b/contracts/contracts/utils/BytesHelper.sol @@ -13,7 +13,7 @@ library BytesHelper { bytes memory data, uint256 start, uint256 end - ) private pure returns (bytes memory) { + ) internal pure returns (bytes memory) { require(end >= start, "Invalid slice range"); require(end <= data.length, "Slice end exceeds data length"); diff --git a/contracts/deploy/base/040_yearn_strategy.js b/contracts/deploy/base/040_crosschain_strategy_proxies.js similarity index 65% rename from contracts/deploy/base/040_yearn_strategy.js rename to contracts/deploy/base/040_crosschain_strategy_proxies.js index dc8c147886..b20a4b2971 100644 --- a/contracts/deploy/base/040_yearn_strategy.js +++ b/contracts/deploy/base/040_crosschain_strategy_proxies.js @@ -2,7 +2,7 @@ const { deployOnBase } = require("../../utils/deploy-l2"); // const addresses = require("../../utils/addresses"); const { deployProxyWithCreateX, - deployYearn3RemoteStrategyImpl, + // deployCrossChainRemoteStrategyImpl, } = require("../deployActions"); // const { // deployWithConfirmation, @@ -11,18 +11,18 @@ const { module.exports = deployOnBase( { - deployName: "040_yearn_strategy", + deployName: "040_crosschain_strategy_proxies", }, async () => { - const salt = "Yean strategy 1"; + const salt = "CrossChain Strategy 1 Test"; const proxyAddress = await deployProxyWithCreateX( salt, "CrossChainRemoteStrategyProxy" ); console.log(`CrossChainRemoteStrategyProxy address: ${proxyAddress}`); - const implAddress = await deployYearn3RemoteStrategyImpl(proxyAddress); - console.log(`CrossChainRemoteStrategyImpl address: ${implAddress}`); + // const implAddress = await deployCrossChainRemoteStrategyImpl(proxyAddress); + // console.log(`CrossChainRemoteStrategyImpl address: ${implAddress}`); return { actions: [], diff --git a/contracts/deploy/deployActions.js b/contracts/deploy/deployActions.js index b9ee798e20..e2cba88533 100644 --- a/contracts/deploy/deployActions.js +++ b/contracts/deploy/deployActions.js @@ -35,6 +35,8 @@ const { beaconChainGenesisTimeMainnet, } = require("../utils/constants"); +const { cctpDomainIds } = require("../utils/cctp"); + const log = require("../utils/logger")("deploy:core"); /** @@ -1722,14 +1724,14 @@ const deployProxyWithCreateX = async (salt, proxyName) => { return proxyAddress; }; -// deploys and initializes the Yearn 3 master strategy -const deployYearn3MasterStrategyImpl = async ( +// deploys and initializes the CrossChain master strategy +const deployCrossChainMasterStrategyImpl = async ( proxyAddress, implementationName = "CrossChainMasterStrategy" ) => { const { deployerAddr } = await getNamedAccounts(); const sDeployer = await ethers.provider.getSigner(deployerAddr); - log(`Deploying Yearn3MasterStrategyImpl as deployer ${deployerAddr}`); + log(`Deploying CrossChainMasterStrategyImpl as deployer ${deployerAddr}`); const cCrossChainMasterStrategyProxy = await ethers.getContractAt( "CrossChainMasterStrategyProxy", @@ -1743,6 +1745,12 @@ const deployYearn3MasterStrategyImpl = async ( addresses.zero, // platform address addresses.mainnet.Vault, ], + addresses.CCTPTokenMessengerV2, + addresses.CCTPMessageTransmitterV2, + cctpDomainIds.Base, + addresses.base.CrossChainRemoteStrategy, + addresses.mainnet.USDC, + addresses.CCTPHookWrapper, ] ); @@ -1766,14 +1774,14 @@ const deployYearn3MasterStrategyImpl = async ( return dCrossChainMasterStrategy.address; }; -// deploys and initializes the Yearn 3 remote strategy -const deployYearn3RemoteStrategyImpl = async ( +// deploys and initializes the CrossChain remote strategy +const deployCrossChainRemoteStrategyImpl = async ( proxyAddress, implementationName = "CrossChainRemoteStrategy" ) => { const { deployerAddr } = await getNamedAccounts(); const sDeployer = await ethers.provider.getSigner(deployerAddr); - log(`Deploying Yearn3RemoteStrategyImpl as deployer ${deployerAddr}`); + log(`Deploying CrossChainRemoteStrategyImpl as deployer ${deployerAddr}`); const cCrossChainRemoteStrategyProxy = await ethers.getContractAt( "CrossChainRemoteStrategyProxy", @@ -1843,6 +1851,6 @@ module.exports = { getPlumeContracts, deploySonicSwapXAMOStrategyImplementation, deployProxyWithCreateX, - deployYearn3MasterStrategyImpl, - deployYearn3RemoteStrategyImpl, + deployCrossChainMasterStrategyImpl, + deployCrossChainRemoteStrategyImpl, }; diff --git a/contracts/deploy/mainnet/159_yearn_strategy.js b/contracts/deploy/mainnet/159_crosschain_strategy_proxies.js similarity index 57% rename from contracts/deploy/mainnet/159_yearn_strategy.js rename to contracts/deploy/mainnet/159_crosschain_strategy_proxies.js index 93f924f3f9..20c14bc19b 100644 --- a/contracts/deploy/mainnet/159_yearn_strategy.js +++ b/contracts/deploy/mainnet/159_crosschain_strategy_proxies.js @@ -2,28 +2,34 @@ const { deploymentWithGovernanceProposal } = require("../../utils/deploy"); // const addresses = require("../../utils/addresses"); const { deployProxyWithCreateX, - deployYearn3MasterStrategyImpl, + // deployCrossChainMasterStrategyImpl, } = require("../deployActions"); module.exports = deploymentWithGovernanceProposal( { - deployName: "159_yearn_strategy", + deployName: "159_crosschain_strategy_proxies", forceDeploy: false, reduceQueueTime: true, deployerIsProposer: false, proposalId: "", }, async () => { + const cctpHookWrapperProxyAddress = await deployProxyWithCreateX( + "CCTPHookWrapperTest", // Salt + "CCTPHookWrapperProxy" + ); + console.log(`CCTPHookWrapperProxy address: ${cctpHookWrapperProxyAddress}`); + // the salt needs to match the salt on the base chain deploying the other part of the strategy - const salt = "Yean strategy 1"; + const salt = "CrossChain Strategy 1 Test"; const proxyAddress = await deployProxyWithCreateX( salt, "CrossChainMasterStrategyProxy" ); console.log(`CrossChainMasterStrategyProxy address: ${proxyAddress}`); - const implAddress = await deployYearn3MasterStrategyImpl(proxyAddress); - console.log(`CrossChainMasterStrategyImpl address: ${implAddress}`); + // const implAddress = await deployCrossChainMasterStrategyImpl(proxyAddress); + // console.log(`CrossChainMasterStrategyImpl address: ${implAddress}`); return { actions: [], diff --git a/contracts/test/_fixture.js b/contracts/test/_fixture.js index b710f8a327..c475c216fc 100644 --- a/contracts/test/_fixture.js +++ b/contracts/test/_fixture.js @@ -17,8 +17,8 @@ const { } = require("../utils/funding"); const { deployWithConfirmation } = require("../utils/deploy"); const { - deployYearn3MasterStrategyImpl, - deployYearn3RemoteStrategyImpl, + deployCrossChainMasterStrategyImpl, + deployCrossChainRemoteStrategyImpl, } = require("../deploy/deployActions.js"); const { replaceContractAt } = require("../utils/hardhat"); @@ -2541,7 +2541,7 @@ async function yearnCrossChainFixture() { ); const masterProxyAddress = masterProxy.address; log(`CrossChainMasterStrategyProxy address: ${masterProxyAddress}`); - let implAddress = await deployYearn3MasterStrategyImpl( + let implAddress = await deployCrossChainMasterStrategyImpl( masterProxyAddress, "CrossChainMasterStrategyMock" ); @@ -2556,7 +2556,7 @@ async function yearnCrossChainFixture() { const remoteProxyAddress = remoteProxy.address; log(`CrossChainRemoteStrategyProxy address: ${remoteProxyAddress}`); - implAddress = await deployYearn3RemoteStrategyImpl( + implAddress = await deployCrossChainRemoteStrategyImpl( remoteProxyAddress, "CrossChainRemoteStrategyMock" ); diff --git a/contracts/utils/addresses.js b/contracts/utils/addresses.js index d7f4d34b84..b9006cabe1 100644 --- a/contracts/utils/addresses.js +++ b/contracts/utils/addresses.js @@ -10,6 +10,11 @@ addresses.multichainBuybackOperator = "0xBB077E716A5f1F1B63ed5244eBFf5214E50fec8c"; addresses.votemarket = "0x8c2c5A295450DDFf4CB360cA73FCCC12243D14D9"; +// CCTP contracts (uses same addresses on all chains) +addresses.CCTPTokenMessengerV2 = "0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d"; +addresses.CCTPMessageTransmitterV2 = + "0x81D40F21F12A8F0E3252Bccb954D722d4c464B64"; + addresses.mainnet = {}; addresses.base = {}; addresses.sonic = {}; diff --git a/contracts/utils/cctp.js b/contracts/utils/cctp.js new file mode 100644 index 0000000000..3422aba26c --- /dev/null +++ b/contracts/utils/cctp.js @@ -0,0 +1,8 @@ +const cctpDomainIds = { + Ethereum: 0, + Base: 6, +}; + +module.exports = { + cctpDomainIds, +}; From 41f1fd91ee9836459b195be5856daec146792e43 Mon Sep 17 00:00:00 2001 From: Shahul Hameed <10547529+shahthepro@users.noreply.github.com> Date: Fri, 12 Dec 2025 15:25:12 +0400 Subject: [PATCH 005/101] Add fork test scaffolding --- contracts/abi/createx.json | 24 +++ contracts/contracts/proxies/Proxies.sol | 31 ---- .../proxies/create2/CCTPHookWrapperProxy.sol | 15 ++ .../create2/CrossChainStrategyProxy.sol | 15 ++ .../crosschain/AbstractCCTPIntegrator.sol | 167 +++++++++++------- .../base/040_crosschain_strategy_proxies.js | 24 ++- .../deploy/base/041_crosschain_strategy.js | 89 ++++++++++ contracts/deploy/deployActions.js | 65 +++++-- .../159_crosschain_strategy_proxies.js | 13 +- .../deploy/mainnet/160_crosschain_strategy.js | 90 ++++++++++ contracts/test/_fixture-base.js | 24 ++- contracts/test/_fixture.js | 39 ++-- .../strategies/crossChain/yearnV3Strategy.js | 28 --- ...chain-master-strategy.mainnet.fork-test.js | 48 +++++ ...osschain-remote-strategy.base.fork-test.js | 44 +++++ contracts/utils/addresses.js | 12 ++ contracts/utils/deploy.js | 5 + 17 files changed, 555 insertions(+), 178 deletions(-) create mode 100644 contracts/contracts/proxies/create2/CCTPHookWrapperProxy.sol create mode 100644 contracts/contracts/proxies/create2/CrossChainStrategyProxy.sol create mode 100644 contracts/deploy/base/041_crosschain_strategy.js create mode 100644 contracts/deploy/mainnet/160_crosschain_strategy.js delete mode 100644 contracts/test/strategies/crossChain/yearnV3Strategy.js create mode 100644 contracts/test/strategies/crosschain/crosschain-master-strategy.mainnet.fork-test.js create mode 100644 contracts/test/strategies/crosschain/crosschain-remote-strategy.base.fork-test.js diff --git a/contracts/abi/createx.json b/contracts/abi/createx.json index 9e30b0e694..84904ff09a 100644 --- a/contracts/abi/createx.json +++ b/contracts/abi/createx.json @@ -23,6 +23,30 @@ "stateMutability": "payable", "type": "function" }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "salt", + "type": "bytes32" + }, + { + "internalType": "bytes", + "name": "initCode", + "type": "bytes" + } + ], + "name": "deployCreate3", + "outputs": [ + { + "internalType": "address", + "name": "newContract", + "type": "address" + } + ], + "stateMutability": "payable", + "type": "function" + }, { "inputs": [ { diff --git a/contracts/contracts/proxies/Proxies.sol b/contracts/contracts/proxies/Proxies.sol index c75898e31e..4bd9436418 100644 --- a/contracts/contracts/proxies/Proxies.sol +++ b/contracts/contracts/proxies/Proxies.sol @@ -321,34 +321,3 @@ contract CompoundingStakingSSVStrategyProxy is { } - -/** - * @notice CrossChainMasterStrategyProxy delegates calls to a CrossChainMasterStrategy implementation - */ -contract CrossChainMasterStrategyProxy is - InitializeGovernedUpgradeabilityProxy2 -{ - constructor(address governor) - InitializeGovernedUpgradeabilityProxy2(governor) - {} -} - -/** - * @notice CrossChainRemoteStrategyProxy delegates calls to a CrossChainRemoteStrategy implementation - */ -contract CrossChainRemoteStrategyProxy is - InitializeGovernedUpgradeabilityProxy2 -{ - constructor(address governor) - InitializeGovernedUpgradeabilityProxy2(governor) - {} -} - -/** - * @notice CCTPHookWrapperProxy delegates calls to a CCTPHookWrapper implementation - */ -contract CCTPHookWrapperProxy is InitializeGovernedUpgradeabilityProxy2 { - constructor(address governor) - InitializeGovernedUpgradeabilityProxy2(governor) - {} -} diff --git a/contracts/contracts/proxies/create2/CCTPHookWrapperProxy.sol b/contracts/contracts/proxies/create2/CCTPHookWrapperProxy.sol new file mode 100644 index 0000000000..7c23405d49 --- /dev/null +++ b/contracts/contracts/proxies/create2/CCTPHookWrapperProxy.sol @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +import { InitializeGovernedUpgradeabilityProxy2 } from "../InitializeGovernedUpgradeabilityProxy2.sol"; + +/*** IMPORTANT: DO NOT CHANGE ANYTHING IN THIS FILE. Any changes to this file (even whitespaces) will affect the create2 address of the proxy */ + +/** + * @notice CCTPHookWrapperProxy delegates calls to a CCTPHookWrapper implementation + */ +contract CCTPHookWrapperProxy is InitializeGovernedUpgradeabilityProxy2 { + constructor(address governor) + InitializeGovernedUpgradeabilityProxy2(governor) + {} +} diff --git a/contracts/contracts/proxies/create2/CrossChainStrategyProxy.sol b/contracts/contracts/proxies/create2/CrossChainStrategyProxy.sol new file mode 100644 index 0000000000..bf715ca3df --- /dev/null +++ b/contracts/contracts/proxies/create2/CrossChainStrategyProxy.sol @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +import { InitializeGovernedUpgradeabilityProxy2 } from "../InitializeGovernedUpgradeabilityProxy2.sol"; + +/*** IMPORTANT: DO NOT CHANGE ANYTHING IN THIS FILE. Any changes to this file (even whitespaces) will affect the create2 address of the proxy */ + +/** + * @notice CrossChainStrategyProxy delegates calls to a CrossChainMasterStrategy or CrossChainRemoteStrategy implementation + */ +contract CrossChainStrategyProxy is InitializeGovernedUpgradeabilityProxy2 { + constructor(address governor) + InitializeGovernedUpgradeabilityProxy2(governor) + {} +} diff --git a/contracts/contracts/strategies/crosschain/AbstractCCTPIntegrator.sol b/contracts/contracts/strategies/crosschain/AbstractCCTPIntegrator.sol index a3e93a7bc9..49ad7b4fdf 100644 --- a/contracts/contracts/strategies/crosschain/AbstractCCTPIntegrator.sol +++ b/contracts/contracts/strategies/crosschain/AbstractCCTPIntegrator.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.0; import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; -import { IERC20, InitializableAbstractStrategy } from "../../utils/InitializableAbstractStrategy.sol"; +import { IERC20 } from "../../utils/InitializableAbstractStrategy.sol"; import { ICCTPTokenMessenger, ICCTPMessageTransmitter, IMessageHandlerV2 } from "../../interfaces/cctp/ICCTP.sol"; @@ -11,7 +11,11 @@ import { Governable } from "../../governance/Governable.sol"; import { BytesHelper } from "../../utils/BytesHelper.sol"; import "../../utils/Helpers.sol"; +import "hardhat/console.sol"; + abstract contract AbstractCCTPIntegrator is Governable, IMessageHandlerV2 { + using SafeERC20 for IERC20; + using BytesHelper for bytes; event CCTPMinFinalityThresholdSet(uint32 minFinalityThreshold); @@ -198,6 +202,10 @@ abstract contract AbstractCCTPIntegrator is Governable, IMessageHandlerV2 { virtual { require(tokenAmount <= MAX_TRANSFER_AMOUNT, "Token amount too high"); + console.log("Sending tokens"); + console.logBytes(hookData); + + IERC20(baseToken).safeApprove(address(cctpTokenMessenger), tokenAmount); // TODO: figure out why getMinFeeAmount is not on CCTP v2 contract // Ref: https://developers.circle.com/cctp/evm-smart-contracts#getminfeeamount @@ -218,6 +226,20 @@ abstract contract AbstractCCTPIntegrator is Governable, IMessageHandlerV2 { ); } + function _getMessageVersion(bytes memory message) + internal + virtual + returns (uint32) + { + // uint32 bytes 0 to 4 is Origin message version + // uint32 bytes 4 to 8 is Message type + uint32 messageVersion = abi.decode( + message.extractSlice(0, 4), + (uint32) + ); + return messageVersion; + } + function _getMessageType(bytes memory message) internal virtual @@ -229,6 +251,17 @@ abstract contract AbstractCCTPIntegrator is Governable, IMessageHandlerV2 { return messageType; } + function _getMessagePayload(bytes memory message) + internal + virtual + returns (bytes memory) + { + // uint32 bytes 0 to 4 is Origin message version + // uint32 bytes 4 to 8 is Message type + // Payload starts at byte 8 + return message.extractSlice(8, message.length); + } + function _encodeDepositMessage(uint64 nonce, uint256 depositAmount) internal virtual @@ -238,27 +271,28 @@ abstract contract AbstractCCTPIntegrator is Governable, IMessageHandlerV2 { abi.encodePacked( ORIGIN_MESSAGE_VERSION, DEPOSIT_MESSAGE, - nonce, - depositAmount + abi.encode(nonce, depositAmount) ); } function _decodeDepositMessage(bytes memory message) internal virtual - returns (uint64 nonce, uint256 depositAmount) + returns (uint64, uint256) { - ( - uint32 version, - uint32 messageType, - uint64 nonce, - uint256 depositAmount - ) = abi.decode(message, (uint32, uint32, uint64, uint256)); require( - version == ORIGIN_MESSAGE_VERSION, + _getMessageVersion(message) == ORIGIN_MESSAGE_VERSION, "Invalid Origin Message Version" ); - require(messageType == DEPOSIT_MESSAGE, "Invalid Message type"); + require( + _getMessageType(message) == DEPOSIT_MESSAGE, + "Invalid Message type" + ); + + (uint64 nonce, uint256 depositAmount) = abi.decode( + _getMessagePayload(message), + (uint64, uint256) + ); return (nonce, depositAmount); } @@ -272,10 +306,7 @@ abstract contract AbstractCCTPIntegrator is Governable, IMessageHandlerV2 { abi.encodePacked( ORIGIN_MESSAGE_VERSION, DEPOSIT_ACK_MESSAGE, - nonce, - amountReceived, - feeExecuted, - balanceAfter + abi.encode(nonce, amountReceived, feeExecuted, balanceAfter) ); } @@ -283,28 +314,31 @@ abstract contract AbstractCCTPIntegrator is Governable, IMessageHandlerV2 { internal virtual returns ( - uint64 nonce, - uint256 amountReceived, - uint256 feeExecuted, - uint256 balanceAfter + uint64, + uint256, + uint256, + uint256 ) { + require( + _getMessageVersion(message) == ORIGIN_MESSAGE_VERSION, + "Invalid Origin Message Version" + ); + require( + _getMessageType(message) == DEPOSIT_ACK_MESSAGE, + "Invalid Message type" + ); + ( - uint32 version, - uint32 messageType, uint64 nonce, uint256 amountReceived, uint256 feeExecuted, uint256 balanceAfter ) = abi.decode( - message, - (uint32, uint32, uint64, uint256, uint256, uint256) + _getMessagePayload(message), + (uint64, uint256, uint256, uint256) ); - require( - version == ORIGIN_MESSAGE_VERSION, - "Invalid Origin Message Version" - ); - require(messageType == DEPOSIT_ACK_MESSAGE, "Invalid Message type"); + return (nonce, amountReceived, feeExecuted, balanceAfter); } @@ -317,27 +351,28 @@ abstract contract AbstractCCTPIntegrator is Governable, IMessageHandlerV2 { abi.encodePacked( ORIGIN_MESSAGE_VERSION, WITHDRAW_MESSAGE, - nonce, - withdrawAmount + abi.encode(nonce, withdrawAmount) ); } function _decodeWithdrawMessage(bytes memory message) internal virtual - returns (uint64 nonce, uint256 withdrawAmount) + returns (uint64, uint256) { - ( - uint32 version, - uint32 messageType, - uint64 nonce, - uint256 withdrawAmount - ) = abi.decode(message, (uint32, uint32, uint64, uint256)); require( - version == ORIGIN_MESSAGE_VERSION, + _getMessageVersion(message) == ORIGIN_MESSAGE_VERSION, "Invalid Origin Message Version" ); - require(messageType == WITHDRAW_MESSAGE, "Invalid Message type"); + require( + _getMessageType(message) == WITHDRAW_MESSAGE, + "Invalid Message type" + ); + + (uint64 nonce, uint256 withdrawAmount) = abi.decode( + _getMessagePayload(message), + (uint64, uint256) + ); return (nonce, withdrawAmount); } @@ -350,9 +385,7 @@ abstract contract AbstractCCTPIntegrator is Governable, IMessageHandlerV2 { abi.encodePacked( ORIGIN_MESSAGE_VERSION, WITHDRAW_ACK_MESSAGE, - nonce, - amountSent, - balanceAfter + abi.encode(nonce, amountSent, balanceAfter) ); } @@ -360,23 +393,24 @@ abstract contract AbstractCCTPIntegrator is Governable, IMessageHandlerV2 { internal virtual returns ( - uint64 nonce, - uint256 amountSent, - uint256 balanceAfter + uint64, + uint256, + uint256 ) { - ( - uint32 version, - uint32 messageType, - uint64 nonce, - uint256 amountSent, - uint256 balanceAfter - ) = abi.decode(message, (uint32, uint32, uint64, uint256, uint256)); require( - version == ORIGIN_MESSAGE_VERSION, + _getMessageVersion(message) == ORIGIN_MESSAGE_VERSION, "Invalid Origin Message Version" ); - require(messageType == WITHDRAW_ACK_MESSAGE, "Invalid Message type"); + require( + _getMessageType(message) == WITHDRAW_ACK_MESSAGE, + "Invalid Message type" + ); + + (uint64 nonce, uint256 amountSent, uint256 balanceAfter) = abi.decode( + _getMessagePayload(message), + (uint64, uint256, uint256) + ); return (nonce, amountSent, balanceAfter); } @@ -389,27 +423,28 @@ abstract contract AbstractCCTPIntegrator is Governable, IMessageHandlerV2 { abi.encodePacked( ORIGIN_MESSAGE_VERSION, BALANCE_CHECK_MESSAGE, - nonce, - balance + abi.encode(nonce, balance) ); } function _decodeBalanceCheckMessage(bytes memory message) internal virtual - returns (uint64 nonce, uint256 balance) + returns (uint64, uint256) { - ( - uint32 version, - uint32 messageType, - uint64 nonce, - uint256 balance - ) = abi.decode(message, (uint32, uint32, uint64, uint256)); require( - version == ORIGIN_MESSAGE_VERSION, + _getMessageVersion(message) == ORIGIN_MESSAGE_VERSION, "Invalid Origin Message Version" ); - require(messageType == BALANCE_CHECK_MESSAGE, "Invalid Message type"); + require( + _getMessageType(message) == BALANCE_CHECK_MESSAGE, + "Invalid Message type" + ); + + (uint64 nonce, uint256 balance) = abi.decode( + _getMessagePayload(message), + (uint64, uint256) + ); return (nonce, balance); } diff --git a/contracts/deploy/base/040_crosschain_strategy_proxies.js b/contracts/deploy/base/040_crosschain_strategy_proxies.js index b20a4b2971..4bd380eee0 100644 --- a/contracts/deploy/base/040_crosschain_strategy_proxies.js +++ b/contracts/deploy/base/040_crosschain_strategy_proxies.js @@ -1,28 +1,24 @@ const { deployOnBase } = require("../../utils/deploy-l2"); -// const addresses = require("../../utils/addresses"); -const { - deployProxyWithCreateX, - // deployCrossChainRemoteStrategyImpl, -} = require("../deployActions"); -// const { -// deployWithConfirmation, -// withConfirmation, -// } = require("../../utils/deploy.js"); +const { deployProxyWithCreateX } = require("../deployActions"); module.exports = deployOnBase( { deployName: "040_crosschain_strategy_proxies", }, async () => { + const cctpHookWrapperProxyAddress = await deployProxyWithCreateX( + "CCTPHookWrapperTest", // Salt + "CCTPHookWrapperProxy" + ); + console.log(`CCTPHookWrapperProxy address: ${cctpHookWrapperProxyAddress}`); + + // the salt needs to match the salt on the base chain deploying the other part of the strategy const salt = "CrossChain Strategy 1 Test"; const proxyAddress = await deployProxyWithCreateX( salt, - "CrossChainRemoteStrategyProxy" + "CrossChainStrategyProxy" ); - console.log(`CrossChainRemoteStrategyProxy address: ${proxyAddress}`); - - // const implAddress = await deployCrossChainRemoteStrategyImpl(proxyAddress); - // console.log(`CrossChainRemoteStrategyImpl address: ${implAddress}`); + console.log(`CrossChainStrategyProxy address: ${proxyAddress}`); return { actions: [], diff --git a/contracts/deploy/base/041_crosschain_strategy.js b/contracts/deploy/base/041_crosschain_strategy.js new file mode 100644 index 0000000000..08a13e774b --- /dev/null +++ b/contracts/deploy/base/041_crosschain_strategy.js @@ -0,0 +1,89 @@ +const { deployOnBase } = require("../../utils/deploy-l2"); +const addresses = require("../../utils/addresses"); +const { deployCrossChainRemoteStrategyImpl } = require("../deployActions"); +const { + deployWithConfirmation, + withConfirmation, +} = require("../../utils/deploy.js"); +const { cctpDomainIds } = require("../../utils/cctp"); + +module.exports = deployOnBase( + { + deployName: "041_crosschain_strategy", + }, + async () => { + const { deployerAddr } = await getNamedAccounts(); + const sDeployer = await ethers.provider.getSigner(deployerAddr); + + console.log(`HookWrapperProxy address: ${addresses.HookWrapperProxy}`); + const cHookWrapperProxy = await ethers.getContractAt( + "CCTPHookWrapperProxy", + addresses.HookWrapperProxy + ); + console.log( + `CrossChainStrategyProxy address: ${addresses.CrossChainStrategyProxy}` + ); + + await deployWithConfirmation("CCTPHookWrapper", [ + addresses.CCTPMessageTransmitterV2, + ]); + const cHookWrapperImpl = await ethers.getContract("CCTPHookWrapper"); + console.log(`CCTPHookWrapper address: ${cHookWrapperImpl.address}`); + + const cHookWrapper = await ethers.getContractAt( + "CCTPHookWrapper", + addresses.HookWrapperProxy + ); + + await withConfirmation( + cHookWrapperProxy.connect(sDeployer).initialize( + cHookWrapperImpl.address, + deployerAddr, // TODO: change governor later + "0x" + ) + ); + + const implAddress = await deployCrossChainRemoteStrategyImpl( + "0xbeeF010f9cb27031ad51e3333f9aF9C6B1228183", // 4626 Vault + addresses.CrossChainStrategyProxy, + cctpDomainIds.Ethereum, + addresses.CrossChainStrategyProxy, + addresses.base.USDC, + cHookWrapper.address, + "CrossChainRemoteStrategy" + ); + console.log(`CrossChainRemoteStrategyImpl address: ${implAddress}`); + + const cCrossChainRemoteStrategy = await ethers.getContractAt( + "CrossChainRemoteStrategy", + addresses.CrossChainStrategyProxy + ); + console.log( + `CrossChainRemoteStrategy address: ${cCrossChainRemoteStrategy.address}` + ); + + await withConfirmation( + cCrossChainRemoteStrategy.connect(sDeployer).setMinFinalityThreshold( + 2000 // standard transfer + ) + ); + + await withConfirmation( + cHookWrapper + .connect(sDeployer) + .setPeer( + cctpDomainIds.Ethereum, + addresses.CrossChainStrategyProxy, + addresses.CrossChainStrategyProxy + ) + ); + + await withConfirmation( + cCrossChainRemoteStrategy.connect(sDeployer).safeApproveAllTokens() + ); + + return { + actions: [], + }; + } +); diff --git a/contracts/deploy/deployActions.js b/contracts/deploy/deployActions.js index e2cba88533..c98e9273d8 100644 --- a/contracts/deploy/deployActions.js +++ b/contracts/deploy/deployActions.js @@ -35,8 +35,6 @@ const { beaconChainGenesisTimeMainnet, } = require("../utils/constants"); -const { cctpDomainIds } = require("../utils/cctp"); - const log = require("../utils/logger")("deploy:core"); /** @@ -1709,11 +1707,16 @@ const deployProxyWithCreateX = async (salt, proxyName) => { const txResponse = await withConfirmation( cCreateX .connect(sDeployer) - .deployCreate2(factoryEncodedSalt, getFactoryBytecode()) + .deployCreate2(factoryEncodedSalt, await getFactoryBytecode()) ); + // // // Create3ProxyContractCreation + // const create3ContractCreationTopic = + // "0x2feea65dd4e9f9cbd86b74b7734210c59a1b2981b5b137bd0ee3e208200c9067"; const contractCreationTopic = "0xb8fda7e00c6b06a2b54e58521bc5894fee35f1090e5a3bb6390bfe2b98b497f7"; + + // const topicToUse = isCreate3 ? create3ContractCreationTopic : contractCreationTopic; const txReceipt = await txResponse.wait(); const proxyAddress = ethers.utils.getAddress( `0x${txReceipt.events @@ -1727,14 +1730,18 @@ const deployProxyWithCreateX = async (salt, proxyName) => { // deploys and initializes the CrossChain master strategy const deployCrossChainMasterStrategyImpl = async ( proxyAddress, + targetDomainId, + remoteStrategyAddress, + baseToken, + hookWrapperAddress, implementationName = "CrossChainMasterStrategy" ) => { const { deployerAddr } = await getNamedAccounts(); const sDeployer = await ethers.provider.getSigner(deployerAddr); log(`Deploying CrossChainMasterStrategyImpl as deployer ${deployerAddr}`); - const cCrossChainMasterStrategyProxy = await ethers.getContractAt( - "CrossChainMasterStrategyProxy", + const cCrossChainStrategyProxy = await ethers.getContractAt( + "CrossChainStrategyProxy", proxyAddress ); @@ -1743,14 +1750,16 @@ const deployCrossChainMasterStrategyImpl = async ( [ [ addresses.zero, // platform address - addresses.mainnet.Vault, + // TODO: change to the actual vault address + deployerAddr, // vault address + // addresses.mainnet.VaultProxy, ], addresses.CCTPTokenMessengerV2, addresses.CCTPMessageTransmitterV2, - cctpDomainIds.Base, - addresses.base.CrossChainRemoteStrategy, - addresses.mainnet.USDC, - addresses.CCTPHookWrapper, + targetDomainId, + remoteStrategyAddress, + baseToken, + hookWrapperAddress, ] ); @@ -1762,9 +1771,11 @@ const deployCrossChainMasterStrategyImpl = async ( // Init the proxy to point at the implementation, set the governor, and call initialize const initFunction = "initialize(address,address,bytes)"; await withConfirmation( - cCrossChainMasterStrategyProxy.connect(sDeployer)[initFunction]( + cCrossChainStrategyProxy.connect(sDeployer)[initFunction]( dCrossChainMasterStrategy.address, - addresses.mainnet.Timelock, // governor + // TODO: change governor later + // addresses.mainnet.Timelock, // governor + deployerAddr, // governor //initData, // data for delegate call to the initialize function on the strategy "0x", await getTxOpts() @@ -1776,21 +1787,39 @@ const deployCrossChainMasterStrategyImpl = async ( // deploys and initializes the CrossChain remote strategy const deployCrossChainRemoteStrategyImpl = async ( + platformAddress, proxyAddress, + targetDomainId, + remoteStrategyAddress, + baseToken, + hookWrapperAddress, implementationName = "CrossChainRemoteStrategy" ) => { const { deployerAddr } = await getNamedAccounts(); const sDeployer = await ethers.provider.getSigner(deployerAddr); log(`Deploying CrossChainRemoteStrategyImpl as deployer ${deployerAddr}`); - const cCrossChainRemoteStrategyProxy = await ethers.getContractAt( - "CrossChainRemoteStrategyProxy", + const cCrossChainStrategyProxy = await ethers.getContractAt( + "CrossChainStrategyProxy", proxyAddress ); const dCrossChainRemoteStrategy = await deployWithConfirmation( implementationName, - [] + [ + [ + platformAddress, + // TODO: change to the actual vault address + deployerAddr, // vault address + // addresses.mainnet.VaultProxy, + ], + addresses.CCTPTokenMessengerV2, + addresses.CCTPMessageTransmitterV2, + targetDomainId, + remoteStrategyAddress, + baseToken, + hookWrapperAddress, + ] ); // const initData = cCrossChainMasterStrategy.interface.encodeFunctionData( @@ -1801,9 +1830,11 @@ const deployCrossChainRemoteStrategyImpl = async ( // Init the proxy to point at the implementation, set the governor, and call initialize const initFunction = "initialize(address,address,bytes)"; await withConfirmation( - cCrossChainRemoteStrategyProxy.connect(sDeployer)[initFunction]( + cCrossChainStrategyProxy.connect(sDeployer)[initFunction]( dCrossChainRemoteStrategy.address, - addresses.base.timelock, // governor + // TODO: change governor later + deployerAddr, // governor + // addresses.base.timelock, // governor //initData, // data for delegate call to the initialize function on the strategy "0x", await getTxOpts() diff --git a/contracts/deploy/mainnet/159_crosschain_strategy_proxies.js b/contracts/deploy/mainnet/159_crosschain_strategy_proxies.js index 20c14bc19b..e0efa6af41 100644 --- a/contracts/deploy/mainnet/159_crosschain_strategy_proxies.js +++ b/contracts/deploy/mainnet/159_crosschain_strategy_proxies.js @@ -1,9 +1,5 @@ const { deploymentWithGovernanceProposal } = require("../../utils/deploy"); -// const addresses = require("../../utils/addresses"); -const { - deployProxyWithCreateX, - // deployCrossChainMasterStrategyImpl, -} = require("../deployActions"); +const { deployProxyWithCreateX } = require("../deployActions"); module.exports = deploymentWithGovernanceProposal( { @@ -24,12 +20,9 @@ module.exports = deploymentWithGovernanceProposal( const salt = "CrossChain Strategy 1 Test"; const proxyAddress = await deployProxyWithCreateX( salt, - "CrossChainMasterStrategyProxy" + "CrossChainStrategyProxy" ); - console.log(`CrossChainMasterStrategyProxy address: ${proxyAddress}`); - - // const implAddress = await deployCrossChainMasterStrategyImpl(proxyAddress); - // console.log(`CrossChainMasterStrategyImpl address: ${implAddress}`); + console.log(`CrossChainStrategyProxy address: ${proxyAddress}`); return { actions: [], diff --git a/contracts/deploy/mainnet/160_crosschain_strategy.js b/contracts/deploy/mainnet/160_crosschain_strategy.js new file mode 100644 index 0000000000..38a754a414 --- /dev/null +++ b/contracts/deploy/mainnet/160_crosschain_strategy.js @@ -0,0 +1,90 @@ +const { + deploymentWithGovernanceProposal, + deployWithConfirmation, + withConfirmation, +} = require("../../utils/deploy"); +const addresses = require("../../utils/addresses"); +const { cctpDomainIds } = require("../../utils/cctp"); +const { deployCrossChainMasterStrategyImpl } = require("../deployActions"); + +module.exports = deploymentWithGovernanceProposal( + { + deployName: "160_crosschain_strategy", + forceDeploy: false, + reduceQueueTime: true, + deployerIsProposer: false, + proposalId: "", + }, + async () => { + const { deployerAddr } = await getNamedAccounts(); + const sDeployer = await ethers.provider.getSigner(deployerAddr); + + console.log(`HookWrapperProxy address: ${addresses.HookWrapperProxy}`); + const cHookWrapperProxy = await ethers.getContractAt( + "CCTPHookWrapperProxy", + addresses.HookWrapperProxy + ); + console.log( + `CrossChainStrategyProxy address: ${addresses.CrossChainStrategyProxy}` + ); + + await deployWithConfirmation("CCTPHookWrapper", [ + addresses.CCTPMessageTransmitterV2, + ]); + const cHookWrapperImpl = await ethers.getContract("CCTPHookWrapper"); + console.log(`CCTPHookWrapper address: ${cHookWrapperImpl.address}`); + + const cHookWrapper = await ethers.getContractAt( + "CCTPHookWrapper", + addresses.HookWrapperProxy + ); + + await withConfirmation( + cHookWrapperProxy.connect(sDeployer).initialize( + cHookWrapperImpl.address, + deployerAddr, // TODO: change governor later + "0x" + ) + ); + + const implAddress = await deployCrossChainMasterStrategyImpl( + addresses.CrossChainStrategyProxy, + cctpDomainIds.Base, + // Same address for both master and remote strategy + addresses.CrossChainStrategyProxy, + addresses.mainnet.USDC, + // Same address on all chains + cHookWrapper.address, + "CrossChainMasterStrategy" + ); + console.log(`CrossChainMasterStrategyImpl address: ${implAddress}`); + + const cCrossChainMasterStrategy = await ethers.getContractAt( + "CrossChainMasterStrategy", + addresses.CrossChainStrategyProxy + ); + console.log( + `CrossChainMasterStrategy address: ${cCrossChainMasterStrategy.address}` + ); + + await withConfirmation( + cCrossChainMasterStrategy.connect(sDeployer).setMinFinalityThreshold( + 2000 // standard transfer + ) + ); + + await withConfirmation( + cHookWrapper + .connect(sDeployer) + .setPeer( + cctpDomainIds.Base, + addresses.CrossChainStrategyProxy, + addresses.CrossChainStrategyProxy + ) + ); + + return { + actions: [], + }; + } +); diff --git a/contracts/test/_fixture-base.js b/contracts/test/_fixture-base.js index 028e10d850..fb1c1914b3 100644 --- a/contracts/test/_fixture-base.js +++ b/contracts/test/_fixture-base.js @@ -150,11 +150,12 @@ const defaultFixture = async () => { ); // WETH - let weth, aero; + let weth, aero, usdc; if (isFork) { weth = await ethers.getContractAt("IWETH9", addresses.base.WETH); aero = await ethers.getContractAt(erc20Abi, addresses.base.AERO); + usdc = await ethers.getContractAt(erc20Abi, addresses.base.USDC); } else { weth = await ethers.getContract("MockWETH"); aero = await ethers.getContract("MockAero"); @@ -275,8 +276,9 @@ const defaultFixture = async () => { aerodromeAmoStrategy, curveAMOStrategy, - // WETH + // Tokens weth, + usdc, // Signers governor, @@ -335,6 +337,23 @@ const bridgeHelperModuleFixture = deployments.createFixture(async () => { }; }); +const crossChainFixture = deployments.createFixture(async () => { + const fixture = await defaultBaseFixture(); + const crossChainRemoteStrategy = await ethers.getContractAt( + "CrossChainRemoteStrategy", + addresses.CrossChainStrategyProxy + ); + const hookWrapper = await ethers.getContractAt( + "CCTPHookWrapper", + addresses.HookWrapperProxy + ); + return { + ...fixture, + crossChainRemoteStrategy, + hookWrapper, + }; +}); + mocha.after(async () => { if (snapshotId) { await nodeRevert(snapshotId); @@ -347,4 +366,5 @@ module.exports = { MINTER_ROLE, BURNER_ROLE, bridgeHelperModuleFixture, + crossChainFixture, }; diff --git a/contracts/test/_fixture.js b/contracts/test/_fixture.js index c475c216fc..63add54507 100644 --- a/contracts/test/_fixture.js +++ b/contracts/test/_fixture.js @@ -2535,12 +2535,11 @@ async function yearnCrossChainFixture() { const sDeployer = await ethers.provider.getSigner(deployerAddr); // deploy master strategy - const masterProxy = await deployWithConfirmation( - "CrossChainMasterStrategyProxy", - [deployerAddr] - ); + const masterProxy = await deployWithConfirmation("CrossChainStrategyProxy", [ + deployerAddr, + ]); const masterProxyAddress = masterProxy.address; - log(`CrossChainMasterStrategyProxy address: ${masterProxyAddress}`); + log(`CrossChainStrategyProxy address: ${masterProxyAddress}`); let implAddress = await deployCrossChainMasterStrategyImpl( masterProxyAddress, "CrossChainMasterStrategyMock" @@ -2548,13 +2547,12 @@ async function yearnCrossChainFixture() { log(`CrossChainMasterStrategyMockImpl address: ${implAddress}`); // deploy remote strategy - const remoteProxy = await deployWithConfirmation( - "CrossChainRemoteStrategyProxy", - [deployerAddr] - ); + const remoteProxy = await deployWithConfirmation("CrossChainStrategyProxy", [ + deployerAddr, + ]); const remoteProxyAddress = remoteProxy.address; - log(`CrossChainRemoteStrategyProxy address: ${remoteProxyAddress}`); + log(`CrossChainStrategyProxy address: ${remoteProxyAddress}`); implAddress = await deployCrossChainRemoteStrategyImpl( remoteProxyAddress, @@ -2912,6 +2910,26 @@ async function enableExecutionLayerGeneralPurposeRequests() { }; } +async function crossChainFixture() { + const fixture = await defaultFixture(); + + const cHookWrapper = await ethers.getContractAt( + "CCTPHookWrapper", + addresses.HookWrapperProxy + ); + const cCrossChainMasterStrategy = await ethers.getContractAt( + "CrossChainMasterStrategy", + addresses.CrossChainStrategyProxy + ); + + return { + ...fixture, + + hookWrapper: cHookWrapper, + crossChainMasterStrategy: cCrossChainMasterStrategy, + }; +} + /** * A fixture is a setup function that is run only the first time it's invoked. On subsequent invocations, * Hardhat will reset the state of the network to what it was at the point after the fixture was initially executed. @@ -3005,4 +3023,5 @@ module.exports = { beaconChainFixture, claimRewardsModuleFixture, yearnCrossChainFixture, + crossChainFixture, }; diff --git a/contracts/test/strategies/crossChain/yearnV3Strategy.js b/contracts/test/strategies/crossChain/yearnV3Strategy.js deleted file mode 100644 index d4a03cda15..0000000000 --- a/contracts/test/strategies/crossChain/yearnV3Strategy.js +++ /dev/null @@ -1,28 +0,0 @@ -const { expect } = require("chai"); - -const { - createFixtureLoader, - yearnCrossChainFixture, -} = require("../../_fixture"); - -describe("Yearn V3 Cross Chain Strategy", function () { - let fixture; - const loadFixture = createFixtureLoader(yearnCrossChainFixture); - - let yearnMasterStrategy, yearnRemoteStrategy; - - beforeEach(async function () { - fixture = await loadFixture(); - yearnMasterStrategy = fixture.yearnMasterStrategy; - yearnRemoteStrategy = fixture.yearnRemoteStrategy; - }); - - it("Should have correct initial state", async function () { - expect(await yearnMasterStrategy._remoteAddress()).to.equal( - yearnRemoteStrategy.address - ); - expect(await yearnRemoteStrategy._masterAddress()).to.equal( - yearnMasterStrategy.address - ); - }); -}); diff --git a/contracts/test/strategies/crosschain/crosschain-master-strategy.mainnet.fork-test.js b/contracts/test/strategies/crosschain/crosschain-master-strategy.mainnet.fork-test.js new file mode 100644 index 0000000000..1a5abff694 --- /dev/null +++ b/contracts/test/strategies/crosschain/crosschain-master-strategy.mainnet.fork-test.js @@ -0,0 +1,48 @@ +const { expect } = require("chai"); + +const { units, ousdUnits, usdcUnits, isCI } = require("../../helpers"); +const { createFixtureLoader, crossChainFixture } = require("../../_fixture"); +const { impersonateAndFund } = require("../../../utils/signers"); +const { formatUnits } = require("ethers/lib/utils"); + +const loadFixture = createFixtureLoader(crossChainFixture); + +describe.only("ForkTest: CrossChainMasterStrategy", function () { + this.timeout(0); + + // Retry up to 3 times on CI + this.retries(isCI ? 3 : 0); + + let fixture; + beforeEach(async () => { + fixture = await loadFixture(); + }); + + it("Should initiate a bridge of deposited USDC", async function () { + const { matt, hookWrapper, crossChainMasterStrategy, usdc } = fixture; + const govAddr = await crossChainMasterStrategy.governor(); + const governor = await impersonateAndFund(govAddr); + const vaultAddr = await crossChainMasterStrategy.vaultAddress(); + + const impersonatedVault = await impersonateAndFund(vaultAddr); + + // Let the strategy hold some USDC + await usdc + .connect(matt) + .transfer(crossChainMasterStrategy.address, usdcUnits("1000")); + + const balanceBefore = await usdc.balanceOf( + crossChainMasterStrategy.address + ); + + // Simulate deposit call + await crossChainMasterStrategy + .connect(impersonatedVault) + .deposit(usdc.address, usdcUnits("1000")); + + const balanceAfter = await usdc.balanceOf(crossChainMasterStrategy.address); + + console.log(`Balance before: ${formatUnits(balanceBefore, 6)}`); + console.log(`Balance after: ${formatUnits(balanceAfter, 6)}`); + }); +}); diff --git a/contracts/test/strategies/crosschain/crosschain-remote-strategy.base.fork-test.js b/contracts/test/strategies/crosschain/crosschain-remote-strategy.base.fork-test.js new file mode 100644 index 0000000000..e9c4cf0937 --- /dev/null +++ b/contracts/test/strategies/crosschain/crosschain-remote-strategy.base.fork-test.js @@ -0,0 +1,44 @@ +const { expect } = require("chai"); + +const { ousdUnits, usdcUnits, isCI } = require("../../helpers"); +const { createFixtureLoader } = require("../../_fixture"); +const { crossChainFixture } = require("../../_fixture-base"); +const { impersonateAndFund } = require("../../../utils/signers"); +const { formatUnits } = require("ethers/lib/utils"); + +const loadFixture = createFixtureLoader(crossChainFixture); + +describe.only("ForkTest: CrossChainRemoteStrategy", function () { + this.timeout(0); + + // Retry up to 3 times on CI + this.retries(isCI ? 3 : 0); + + let fixture; + beforeEach(async () => { + fixture = await loadFixture(); + }); + + it("Should initiate a bridge of deposited USDC", async function () { + const { hookWrapper, crossChainRemoteStrategy, usdc } = fixture; + await crossChainRemoteStrategy.sendBalanceUpdate(); + // const govAddr = (await crossChainMasterStrategy.governor()) + // const governor = await impersonateAndFund(govAddr); + // const vaultAddr = await crossChainMasterStrategy.vaultAddress(); + + // const impersonatedVault = await impersonateAndFund(vaultAddr); + + // // Let the strategy hold some USDC + // await usdc.connect(matt).transfer(crossChainMasterStrategy.address, usdcUnits("1000")); + + // const balanceBefore = await usdc.balanceOf(crossChainMasterStrategy.address); + + // // Simulate deposit call + // await crossChainMasterStrategy.connect(impersonatedVault).deposit(usdc.address, usdcUnits("1000")); + + // const balanceAfter = await usdc.balanceOf(crossChainMasterStrategy.address); + + // console.log(`Balance before: ${formatUnits(balanceBefore, 6)}`); + // console.log(`Balance after: ${formatUnits(balanceAfter, 6)}`); + }); +}); diff --git a/contracts/utils/addresses.js b/contracts/utils/addresses.js index b9006cabe1..26b93f18ee 100644 --- a/contracts/utils/addresses.js +++ b/contracts/utils/addresses.js @@ -449,6 +449,8 @@ addresses.base.CCIPRouter = "0x881e3A65B4d4a04dD529061dd0071cf975F58bCD"; addresses.base.MerklDistributor = "0x8BB4C975Ff3c250e0ceEA271728547f3802B36Fd"; +addresses.base.USDC = "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913"; + // Sonic addresses.sonic.wS = "0x039e2fB66102314Ce7b64Ce5Ce3E5183bc94aD38"; addresses.sonic.WETH = "0x309C92261178fA0CF748A855e90Ae73FDb79EBc7"; @@ -682,4 +684,14 @@ addresses.hoodi.beaconChainDepositContract = addresses.hoodi.defenderRelayer = "0x419B6BdAE482f41b8B194515749F3A2Da26d583b"; addresses.hoodi.mockBeaconRoots = "0xdCfcAE4A084AA843eE446f400B23aA7B6340484b"; +// Crosschain Strategy + +addresses.HookWrapperProxy = "0xBFAc208544c41aC1A675b9147F03c6dF19D6435f"; +addresses.CrossChainStrategyProxy = + "0xb8efD2c6eAd9816841871C54d7B789eB517Cc684"; +addresses.mainnet.CrossChainStrategyProxy = + "0xb8efD2c6eAd9816841871C54d7B789eB517Cc684"; +addresses.base.CrossChainStrategyProxy = + "0xb8efD2c6eAd9816841871C54d7B789eB517Cc684"; + module.exports = addresses; diff --git a/contracts/utils/deploy.js b/contracts/utils/deploy.js index dd1d85d9b6..28a80646e8 100644 --- a/contracts/utils/deploy.js +++ b/contracts/utils/deploy.js @@ -170,6 +170,11 @@ const _verifyProxyInitializedWithCorrectGovernor = (transactionData) => { return; } + if (isFork) { + // TODO: Skip verification for Fork for now + return; + } + const initProxyGovernor = ( "0x" + transactionData.slice(10 + 64 + 24, 10 + 64 + 64) ).toLowerCase(); From 7a109ccf52e18b6d2f034a6ad9e8cf312f9ddd5f Mon Sep 17 00:00:00 2001 From: Shahul Hameed <10547529+shahthepro@users.noreply.github.com> Date: Sat, 13 Dec 2025 00:30:32 +0400 Subject: [PATCH 006/101] Fix stuffs --- .../crosschain/AbstractCCTPIntegrator.sol | 25 ++++------- .../strategies/crosschain/CCTPHookWrapper.sol | 28 +++++-------- .../crosschain/CrossChainMasterStrategy.sol | 4 +- .../crosschain/CrossChainRemoteStrategy.sol | 4 +- contracts/contracts/utils/BytesHelper.sol | 5 +++ .../base/040_crosschain_strategy_proxies.js | 4 +- contracts/deploy/deployActions.js | 41 ++++++++++--------- contracts/deploy/mainnet/156_simplify_ousd.js | 2 +- .../159_crosschain_strategy_proxies.js | 4 +- ...chain-master-strategy.mainnet.fork-test.js | 10 +++++ contracts/utils/addresses.js | 8 ++-- 11 files changed, 68 insertions(+), 67 deletions(-) diff --git a/contracts/contracts/strategies/crosschain/AbstractCCTPIntegrator.sol b/contracts/contracts/strategies/crosschain/AbstractCCTPIntegrator.sol index 49ad7b4fdf..7c6fe91667 100644 --- a/contracts/contracts/strategies/crosschain/AbstractCCTPIntegrator.sol +++ b/contracts/contracts/strategies/crosschain/AbstractCCTPIntegrator.sol @@ -11,8 +11,6 @@ import { Governable } from "../../governance/Governable.sol"; import { BytesHelper } from "../../utils/BytesHelper.sol"; import "../../utils/Helpers.sol"; -import "hardhat/console.sol"; - abstract contract AbstractCCTPIntegrator is Governable, IMessageHandlerV2 { using SafeERC20 for IERC20; @@ -160,12 +158,12 @@ abstract contract AbstractCCTPIntegrator is Governable, IMessageHandlerV2 { uint32 finalityThresholdExecuted, bytes memory messageBody ) internal returns (bool) { - // Make sure that the finality threshold is same on both chains - // TODO: Do we really need this? - require( - finalityThresholdExecuted >= minFinalityThreshold, - "Finality threshold too low" - ); + // // Make sure that the finality threshold is same on both chains + // // TODO: Do we really need this? Also, fix this + // require( + // finalityThresholdExecuted >= minFinalityThreshold, + // "Finality threshold too low" + // ); require(sourceDomain == destinationDomain, "Unknown Source Domain"); // Extract address from bytes32 (CCTP stores addresses as right-padded bytes32) @@ -202,8 +200,6 @@ abstract contract AbstractCCTPIntegrator is Governable, IMessageHandlerV2 { virtual { require(tokenAmount <= MAX_TRANSFER_AMOUNT, "Token amount too high"); - console.log("Sending tokens"); - console.logBytes(hookData); IERC20(baseToken).safeApprove(address(cctpTokenMessenger), tokenAmount); @@ -233,11 +229,7 @@ abstract contract AbstractCCTPIntegrator is Governable, IMessageHandlerV2 { { // uint32 bytes 0 to 4 is Origin message version // uint32 bytes 4 to 8 is Message type - uint32 messageVersion = abi.decode( - message.extractSlice(0, 4), - (uint32) - ); - return messageVersion; + return message.extractSlice(0, 4).decodeUint32(); } function _getMessageType(bytes memory message) @@ -247,8 +239,7 @@ abstract contract AbstractCCTPIntegrator is Governable, IMessageHandlerV2 { { // uint32 bytes 0 to 4 is Origin message version // uint32 bytes 4 to 8 is Message type - uint32 messageType = abi.decode(message.extractSlice(4, 8), (uint32)); - return messageType; + return message.extractSlice(4, 8).decodeUint32(); } function _getMessagePayload(bytes memory message) diff --git a/contracts/contracts/strategies/crosschain/CCTPHookWrapper.sol b/contracts/contracts/strategies/crosschain/CCTPHookWrapper.sol index e9efe50d7a..4ab77bd87d 100644 --- a/contracts/contracts/strategies/crosschain/CCTPHookWrapper.sol +++ b/contracts/contracts/strategies/crosschain/CCTPHookWrapper.sol @@ -74,29 +74,21 @@ contract CCTPHookWrapper is Governable { emit PeerRemoved(sourceDomainID, remoteContract, localContract); } - function relay(bytes calldata message, bytes calldata attestation) - external - { - require( - msg.sender == address(cctpMessageTransmitter), - "Caller is not the CCTP message transmitter" - ); - + function relay(bytes memory message, bytes memory attestation) external { // Ensure message version - uint32 version = abi.decode( - message.extractSlice(VERSION_INDEX, VERSION_INDEX + 4), - (uint32) - ); + uint32 version = message + .extractSlice(VERSION_INDEX, VERSION_INDEX + 4) + .decodeUint32(); + // Ensure that it's a CCTP message require( version == CCTP_MESSAGE_VERSION, "Invalid CCTP message version" ); - uint32 sourceDomainID = abi.decode( - message.extractSlice(SOURCE_DOMAIN_INDEX, SOURCE_DOMAIN_INDEX + 4), - (uint32) - ); + uint32 sourceDomainID = message + .extractSlice(SOURCE_DOMAIN_INDEX, SOURCE_DOMAIN_INDEX + 4) + .decodeUint32(); // Make sure sender is whitelisted address sender = abi.decode( @@ -114,11 +106,11 @@ contract CCTPHookWrapper is Governable { MESSAGE_BODY_INDEX, message.length ); - bytes memory versionSlice = messageBody.extractSlice( + bytes memory bodyVersionSlice = messageBody.extractSlice( BURN_MESSAGE_V2_VERSION_INDEX, BURN_MESSAGE_V2_VERSION_INDEX + 4 ); - version = abi.decode(versionSlice, (uint32)); + version = bodyVersionSlice.decodeUint32(); bool isBurnMessageV1 = version == 1 && messageBody.length >= BURN_MESSAGE_V2_HOOK_DATA_INDEX; diff --git a/contracts/contracts/strategies/crosschain/CrossChainMasterStrategy.sol b/contracts/contracts/strategies/crosschain/CrossChainMasterStrategy.sol index 03acd9ca20..e675235b6b 100644 --- a/contracts/contracts/strategies/crosschain/CrossChainMasterStrategy.sol +++ b/contracts/contracts/strategies/crosschain/CrossChainMasterStrategy.sol @@ -173,9 +173,9 @@ contract CrossChainMasterStrategy is // Do nothing because we receive acknowledgement with token transfer, so _onTokenReceived will handle it // TODO: Should _onTokenReceived always call _onMessageReceived? // _processWithdrawAckMessage(payload); + } else { + revert("Unknown message type"); } - - revert("Unknown message type"); } function _onTokenReceived( diff --git a/contracts/contracts/strategies/crosschain/CrossChainRemoteStrategy.sol b/contracts/contracts/strategies/crosschain/CrossChainRemoteStrategy.sol index 64d29d1e23..f778318316 100644 --- a/contracts/contracts/strategies/crosschain/CrossChainRemoteStrategy.sol +++ b/contracts/contracts/strategies/crosschain/CrossChainRemoteStrategy.sol @@ -76,9 +76,9 @@ contract CrossChainRemoteStrategy is } else if (messageType == WITHDRAW_MESSAGE) { // Received when Master strategy requests a withdrawal _processWithdrawMessage(payload); + } else { + revert("Unknown message type"); } - - revert("Unknown message type"); } function _processDepositMessage( diff --git a/contracts/contracts/utils/BytesHelper.sol b/contracts/contracts/utils/BytesHelper.sol index e5c9319b21..29906c2547 100644 --- a/contracts/contracts/utils/BytesHelper.sol +++ b/contracts/contracts/utils/BytesHelper.sol @@ -27,4 +27,9 @@ library BytesHelper { return result; } + + function decodeUint32(bytes memory data) internal pure returns (uint32) { + require(data.length == 4, "Invalid data length"); + return uint32(uint256(bytes32(data)) >> 224); + } } diff --git a/contracts/deploy/base/040_crosschain_strategy_proxies.js b/contracts/deploy/base/040_crosschain_strategy_proxies.js index 4bd380eee0..2b3ea130ed 100644 --- a/contracts/deploy/base/040_crosschain_strategy_proxies.js +++ b/contracts/deploy/base/040_crosschain_strategy_proxies.js @@ -7,13 +7,13 @@ module.exports = deployOnBase( }, async () => { const cctpHookWrapperProxyAddress = await deployProxyWithCreateX( - "CCTPHookWrapperTest", // Salt + "CCTPHookWrapperTest22", // Salt "CCTPHookWrapperProxy" ); console.log(`CCTPHookWrapperProxy address: ${cctpHookWrapperProxyAddress}`); // the salt needs to match the salt on the base chain deploying the other part of the strategy - const salt = "CrossChain Strategy 1 Test"; + const salt = "CrossChain Strategy 22 Test"; const proxyAddress = await deployProxyWithCreateX( salt, "CrossChainStrategyProxy" diff --git a/contracts/deploy/deployActions.js b/contracts/deploy/deployActions.js index c98e9273d8..d13a7a6742 100644 --- a/contracts/deploy/deployActions.js +++ b/contracts/deploy/deployActions.js @@ -1695,7 +1695,7 @@ const deployProxyWithCreateX = async (salt, proxyName) => { log(`Deploying ${proxyName} with salt: ${salt} as deployer ${deployerAddr}`); const cCreateX = await ethers.getContractAt(createxAbi, addresses.createX); - const factoryEncodedSalt = encodeSaltForCreateX(deployerAddr, false, 1); + const factoryEncodedSalt = encodeSaltForCreateX(deployerAddr, true, salt); const getFactoryBytecode = async () => { // No deployment needed—get factory directly from artifacts @@ -1734,7 +1734,8 @@ const deployCrossChainMasterStrategyImpl = async ( remoteStrategyAddress, baseToken, hookWrapperAddress, - implementationName = "CrossChainMasterStrategy" + implementationName = "CrossChainMasterStrategy", + skipInitialize = false ) => { const { deployerAddr } = await getNamedAccounts(); const sDeployer = await ethers.provider.getSigner(deployerAddr); @@ -1763,24 +1764,26 @@ const deployCrossChainMasterStrategyImpl = async ( ] ); - // const initData = cCrossChainMasterStrategy.interface.encodeFunctionData( - // "initialize()", - // [] - // ); + if (!skipInitialize) { + // const initData = cCrossChainMasterStrategy.interface.encodeFunctionData( + // "initialize()", + // [] + // ); - // Init the proxy to point at the implementation, set the governor, and call initialize - const initFunction = "initialize(address,address,bytes)"; - await withConfirmation( - cCrossChainStrategyProxy.connect(sDeployer)[initFunction]( - dCrossChainMasterStrategy.address, - // TODO: change governor later - // addresses.mainnet.Timelock, // governor - deployerAddr, // governor - //initData, // data for delegate call to the initialize function on the strategy - "0x", - await getTxOpts() - ) - ); + // Init the proxy to point at the implementation, set the governor, and call initialize + const initFunction = "initialize(address,address,bytes)"; + await withConfirmation( + cCrossChainStrategyProxy.connect(sDeployer)[initFunction]( + dCrossChainMasterStrategy.address, + // TODO: change governor later + // addresses.mainnet.Timelock, // governor + deployerAddr, // governor + //initData, // data for delegate call to the initialize function on the strategy + "0x", + await getTxOpts() + ) + ); + } return dCrossChainMasterStrategy.address; }; diff --git a/contracts/deploy/mainnet/156_simplify_ousd.js b/contracts/deploy/mainnet/156_simplify_ousd.js index 38f2299d1e..6d52c4034b 100644 --- a/contracts/deploy/mainnet/156_simplify_ousd.js +++ b/contracts/deploy/mainnet/156_simplify_ousd.js @@ -9,7 +9,7 @@ module.exports = deploymentWithGovernanceProposal( { deployName: "156_simplify_ousd", forceDeploy: false, - //forceSkip: true, + forceSkip: true, reduceQueueTime: true, deployerIsProposer: false, proposalId: diff --git a/contracts/deploy/mainnet/159_crosschain_strategy_proxies.js b/contracts/deploy/mainnet/159_crosschain_strategy_proxies.js index e0efa6af41..9592b2b719 100644 --- a/contracts/deploy/mainnet/159_crosschain_strategy_proxies.js +++ b/contracts/deploy/mainnet/159_crosschain_strategy_proxies.js @@ -11,13 +11,13 @@ module.exports = deploymentWithGovernanceProposal( }, async () => { const cctpHookWrapperProxyAddress = await deployProxyWithCreateX( - "CCTPHookWrapperTest", // Salt + "CCTPHookWrapperTest22", // Salt "CCTPHookWrapperProxy" ); console.log(`CCTPHookWrapperProxy address: ${cctpHookWrapperProxyAddress}`); // the salt needs to match the salt on the base chain deploying the other part of the strategy - const salt = "CrossChain Strategy 1 Test"; + const salt = "CrossChain Strategy 22 Test"; const proxyAddress = await deployProxyWithCreateX( salt, "CrossChainStrategyProxy" diff --git a/contracts/test/strategies/crosschain/crosschain-master-strategy.mainnet.fork-test.js b/contracts/test/strategies/crosschain/crosschain-master-strategy.mainnet.fork-test.js index 1a5abff694..cd3f35868f 100644 --- a/contracts/test/strategies/crosschain/crosschain-master-strategy.mainnet.fork-test.js +++ b/contracts/test/strategies/crosschain/crosschain-master-strategy.mainnet.fork-test.js @@ -45,4 +45,14 @@ describe.only("ForkTest: CrossChainMasterStrategy", function () { console.log(`Balance before: ${formatUnits(balanceBefore, 6)}`); console.log(`Balance after: ${formatUnits(balanceAfter, 6)}`); }); + + it.only("Should handle attestation relay", async function () { + const { matt, hookWrapper, crossChainMasterStrategy, usdc } = fixture; + const attestation = + "0xf0b2792bd9b046124075e93647df38c7b1d524676f48969e692b7a79826df13913ae9086db0de46a194be8c4b52fe3b985a1fa5d6b0f038230506891a59869381b61b7567dc2e82817b7c63eb5968fcdddd53fb167eeb225aaef20ffda1aa9b0337529d52344ba8dbd272821adae236d51b8af81bdbe7ad610237f66161bbb34b41b"; + const message = + "0x000000010000000600000000da5c3cfca2c93e77aeb7cd1c18df6e217d9a446930d4f95fdef03b2b59522bc5000000000000000000000000b8efd2c6ead9816841871c54d7b789eb517cc684000000000000000000000000b8efd2c6ead9816841871c54d7b789eb517cc684000000000000000000000000bfac208544c41ac1a675b9147f03c6df19d6435f00000000000003e8000003f20000000300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"; + + await hookWrapper.relay(message, attestation); + }); }); diff --git a/contracts/utils/addresses.js b/contracts/utils/addresses.js index 26b93f18ee..654342ef05 100644 --- a/contracts/utils/addresses.js +++ b/contracts/utils/addresses.js @@ -686,12 +686,12 @@ addresses.hoodi.mockBeaconRoots = "0xdCfcAE4A084AA843eE446f400B23aA7B6340484b"; // Crosschain Strategy -addresses.HookWrapperProxy = "0xBFAc208544c41aC1A675b9147F03c6dF19D6435f"; +addresses.HookWrapperProxy = "0x317D15b11c1a5165f109693d68D4845D621163cd"; addresses.CrossChainStrategyProxy = - "0xb8efD2c6eAd9816841871C54d7B789eB517Cc684"; + "0xcAE603E2ae51ADbf78dA619B9fF3A6BD0B151A16"; addresses.mainnet.CrossChainStrategyProxy = - "0xb8efD2c6eAd9816841871C54d7B789eB517Cc684"; + "0xcAE603E2ae51ADbf78dA619B9fF3A6BD0B151A16"; addresses.base.CrossChainStrategyProxy = - "0xb8efD2c6eAd9816841871C54d7B789eB517Cc684"; + "0xcAE603E2ae51ADbf78dA619B9fF3A6BD0B151A16"; module.exports = addresses; From 48d317ef94b27ab240a1dfca1e1ee4d88cdf0c44 Mon Sep 17 00:00:00 2001 From: Shahul Hameed <10547529+shahthepro@users.noreply.github.com> Date: Sat, 13 Dec 2025 09:54:44 +0400 Subject: [PATCH 007/101] Prettify and change salt --- .../proxies/create2/CCTPHookWrapperProxy.sol | 8 +++++++- .../proxies/create2/CrossChainStrategyProxy.sol | 12 ++++++++++-- .../crosschain/AbstractCCTPIntegrator.sol | 1 + .../base/040_crosschain_strategy_proxies.js | 4 ++-- contracts/deploy/deployActions.js | 2 +- .../mainnet/159_crosschain_strategy_proxies.js | 4 ++-- ...osschain-master-strategy.mainnet.fork-test.js | 16 ++++++++-------- .../crosschain-remote-strategy.base.fork-test.js | 12 ++++++------ contracts/utils/addresses.js | 8 ++++---- 9 files changed, 41 insertions(+), 26 deletions(-) diff --git a/contracts/contracts/proxies/create2/CCTPHookWrapperProxy.sol b/contracts/contracts/proxies/create2/CCTPHookWrapperProxy.sol index 7c23405d49..e94c8faac7 100644 --- a/contracts/contracts/proxies/create2/CCTPHookWrapperProxy.sol +++ b/contracts/contracts/proxies/create2/CCTPHookWrapperProxy.sol @@ -3,7 +3,13 @@ pragma solidity ^0.8.0; import { InitializeGovernedUpgradeabilityProxy2 } from "../InitializeGovernedUpgradeabilityProxy2.sol"; -/*** IMPORTANT: DO NOT CHANGE ANYTHING IN THIS FILE. Any changes to this file (even whitespaces) will affect the create2 address of the proxy */ +// ******************************************************** +// ******************************************************** +// IMPORTANT: DO NOT CHANGE ANYTHING IN THIS FILE. +// Any changes to this file (even whitespaces) will +// affect the create2 address of the proxy +// ******************************************************** +// ******************************************************** /** * @notice CCTPHookWrapperProxy delegates calls to a CCTPHookWrapper implementation diff --git a/contracts/contracts/proxies/create2/CrossChainStrategyProxy.sol b/contracts/contracts/proxies/create2/CrossChainStrategyProxy.sol index bf715ca3df..a5feec929b 100644 --- a/contracts/contracts/proxies/create2/CrossChainStrategyProxy.sol +++ b/contracts/contracts/proxies/create2/CrossChainStrategyProxy.sol @@ -3,10 +3,18 @@ pragma solidity ^0.8.0; import { InitializeGovernedUpgradeabilityProxy2 } from "../InitializeGovernedUpgradeabilityProxy2.sol"; -/*** IMPORTANT: DO NOT CHANGE ANYTHING IN THIS FILE. Any changes to this file (even whitespaces) will affect the create2 address of the proxy */ +// ******************************************************** +// ******************************************************** +// IMPORTANT: DO NOT CHANGE ANYTHING IN THIS FILE. +// Any changes to this file (even whitespaces) will +// affect the create2 address of the proxy +// ******************************************************** +// ******************************************************** /** - * @notice CrossChainStrategyProxy delegates calls to a CrossChainMasterStrategy or CrossChainRemoteStrategy implementation + * @notice CrossChainStrategyProxy delegates calls to a + * CrossChainMasterStrategy or CrossChainRemoteStrategy + * implementation contract. */ contract CrossChainStrategyProxy is InitializeGovernedUpgradeabilityProxy2 { constructor(address governor) diff --git a/contracts/contracts/strategies/crosschain/AbstractCCTPIntegrator.sol b/contracts/contracts/strategies/crosschain/AbstractCCTPIntegrator.sol index 7c6fe91667..dbd5678d46 100644 --- a/contracts/contracts/strategies/crosschain/AbstractCCTPIntegrator.sol +++ b/contracts/contracts/strategies/crosschain/AbstractCCTPIntegrator.sol @@ -155,6 +155,7 @@ abstract contract AbstractCCTPIntegrator is Governable, IMessageHandlerV2 { function _handleReceivedMessage( uint32 sourceDomain, bytes32 sender, + // solhint-disable-next-line no-unused-vars uint32 finalityThresholdExecuted, bytes memory messageBody ) internal returns (bool) { diff --git a/contracts/deploy/base/040_crosschain_strategy_proxies.js b/contracts/deploy/base/040_crosschain_strategy_proxies.js index 2b3ea130ed..adeaad4f9c 100644 --- a/contracts/deploy/base/040_crosschain_strategy_proxies.js +++ b/contracts/deploy/base/040_crosschain_strategy_proxies.js @@ -7,13 +7,13 @@ module.exports = deployOnBase( }, async () => { const cctpHookWrapperProxyAddress = await deployProxyWithCreateX( - "CCTPHookWrapperTest22", // Salt + "CCTPHookWrapperTest221", // Salt "CCTPHookWrapperProxy" ); console.log(`CCTPHookWrapperProxy address: ${cctpHookWrapperProxyAddress}`); // the salt needs to match the salt on the base chain deploying the other part of the strategy - const salt = "CrossChain Strategy 22 Test"; + const salt = "CrossChain Strategy 221 Test"; const proxyAddress = await deployProxyWithCreateX( salt, "CrossChainStrategyProxy" diff --git a/contracts/deploy/deployActions.js b/contracts/deploy/deployActions.js index d13a7a6742..1a4b5a5e49 100644 --- a/contracts/deploy/deployActions.js +++ b/contracts/deploy/deployActions.js @@ -1695,7 +1695,7 @@ const deployProxyWithCreateX = async (salt, proxyName) => { log(`Deploying ${proxyName} with salt: ${salt} as deployer ${deployerAddr}`); const cCreateX = await ethers.getContractAt(createxAbi, addresses.createX); - const factoryEncodedSalt = encodeSaltForCreateX(deployerAddr, true, salt); + const factoryEncodedSalt = encodeSaltForCreateX(deployerAddr, false, salt); const getFactoryBytecode = async () => { // No deployment needed—get factory directly from artifacts diff --git a/contracts/deploy/mainnet/159_crosschain_strategy_proxies.js b/contracts/deploy/mainnet/159_crosschain_strategy_proxies.js index 9592b2b719..cef646dd93 100644 --- a/contracts/deploy/mainnet/159_crosschain_strategy_proxies.js +++ b/contracts/deploy/mainnet/159_crosschain_strategy_proxies.js @@ -11,13 +11,13 @@ module.exports = deploymentWithGovernanceProposal( }, async () => { const cctpHookWrapperProxyAddress = await deployProxyWithCreateX( - "CCTPHookWrapperTest22", // Salt + "CCTPHookWrapperTest221", // Salt "CCTPHookWrapperProxy" ); console.log(`CCTPHookWrapperProxy address: ${cctpHookWrapperProxyAddress}`); // the salt needs to match the salt on the base chain deploying the other part of the strategy - const salt = "CrossChain Strategy 22 Test"; + const salt = "CrossChain Strategy 221 Test"; const proxyAddress = await deployProxyWithCreateX( salt, "CrossChainStrategyProxy" diff --git a/contracts/test/strategies/crosschain/crosschain-master-strategy.mainnet.fork-test.js b/contracts/test/strategies/crosschain/crosschain-master-strategy.mainnet.fork-test.js index cd3f35868f..8b8dfd832c 100644 --- a/contracts/test/strategies/crosschain/crosschain-master-strategy.mainnet.fork-test.js +++ b/contracts/test/strategies/crosschain/crosschain-master-strategy.mainnet.fork-test.js @@ -1,13 +1,13 @@ -const { expect } = require("chai"); +// const { expect } = require("chai"); -const { units, ousdUnits, usdcUnits, isCI } = require("../../helpers"); +const { usdcUnits, isCI } = require("../../helpers"); const { createFixtureLoader, crossChainFixture } = require("../../_fixture"); const { impersonateAndFund } = require("../../../utils/signers"); const { formatUnits } = require("ethers/lib/utils"); const loadFixture = createFixtureLoader(crossChainFixture); -describe.only("ForkTest: CrossChainMasterStrategy", function () { +describe("ForkTest: CrossChainMasterStrategy", function () { this.timeout(0); // Retry up to 3 times on CI @@ -19,9 +19,9 @@ describe.only("ForkTest: CrossChainMasterStrategy", function () { }); it("Should initiate a bridge of deposited USDC", async function () { - const { matt, hookWrapper, crossChainMasterStrategy, usdc } = fixture; - const govAddr = await crossChainMasterStrategy.governor(); - const governor = await impersonateAndFund(govAddr); + const { matt, crossChainMasterStrategy, usdc } = fixture; + // const govAddr = await crossChainMasterStrategy.governor(); + // const governor = await impersonateAndFund(govAddr); const vaultAddr = await crossChainMasterStrategy.vaultAddress(); const impersonatedVault = await impersonateAndFund(vaultAddr); @@ -46,8 +46,8 @@ describe.only("ForkTest: CrossChainMasterStrategy", function () { console.log(`Balance after: ${formatUnits(balanceAfter, 6)}`); }); - it.only("Should handle attestation relay", async function () { - const { matt, hookWrapper, crossChainMasterStrategy, usdc } = fixture; + it("Should handle attestation relay", async function () { + const { hookWrapper } = fixture; const attestation = "0xf0b2792bd9b046124075e93647df38c7b1d524676f48969e692b7a79826df13913ae9086db0de46a194be8c4b52fe3b985a1fa5d6b0f038230506891a59869381b61b7567dc2e82817b7c63eb5968fcdddd53fb167eeb225aaef20ffda1aa9b0337529d52344ba8dbd272821adae236d51b8af81bdbe7ad610237f66161bbb34b41b"; const message = diff --git a/contracts/test/strategies/crosschain/crosschain-remote-strategy.base.fork-test.js b/contracts/test/strategies/crosschain/crosschain-remote-strategy.base.fork-test.js index e9c4cf0937..2293d484e7 100644 --- a/contracts/test/strategies/crosschain/crosschain-remote-strategy.base.fork-test.js +++ b/contracts/test/strategies/crosschain/crosschain-remote-strategy.base.fork-test.js @@ -1,14 +1,14 @@ -const { expect } = require("chai"); +// const { expect } = require("chai"); -const { ousdUnits, usdcUnits, isCI } = require("../../helpers"); +const { isCI } = require("../../helpers"); const { createFixtureLoader } = require("../../_fixture"); const { crossChainFixture } = require("../../_fixture-base"); -const { impersonateAndFund } = require("../../../utils/signers"); -const { formatUnits } = require("ethers/lib/utils"); +// const { impersonateAndFund } = require("../../../utils/signers"); +// const { formatUnits } = require("ethers/lib/utils"); const loadFixture = createFixtureLoader(crossChainFixture); -describe.only("ForkTest: CrossChainRemoteStrategy", function () { +describe("ForkTest: CrossChainRemoteStrategy", function () { this.timeout(0); // Retry up to 3 times on CI @@ -20,7 +20,7 @@ describe.only("ForkTest: CrossChainRemoteStrategy", function () { }); it("Should initiate a bridge of deposited USDC", async function () { - const { hookWrapper, crossChainRemoteStrategy, usdc } = fixture; + const { crossChainRemoteStrategy } = fixture; await crossChainRemoteStrategy.sendBalanceUpdate(); // const govAddr = (await crossChainMasterStrategy.governor()) // const governor = await impersonateAndFund(govAddr); diff --git a/contracts/utils/addresses.js b/contracts/utils/addresses.js index 654342ef05..5297b02496 100644 --- a/contracts/utils/addresses.js +++ b/contracts/utils/addresses.js @@ -686,12 +686,12 @@ addresses.hoodi.mockBeaconRoots = "0xdCfcAE4A084AA843eE446f400B23aA7B6340484b"; // Crosschain Strategy -addresses.HookWrapperProxy = "0x317D15b11c1a5165f109693d68D4845D621163cd"; +addresses.HookWrapperProxy = "0x40eC39c3EcB0e7aD45D7BC604D8FA479D5d1F405"; addresses.CrossChainStrategyProxy = - "0xcAE603E2ae51ADbf78dA619B9fF3A6BD0B151A16"; + "0x02274FDFf21178BAD174551A9b9b68F81566A76A"; addresses.mainnet.CrossChainStrategyProxy = - "0xcAE603E2ae51ADbf78dA619B9fF3A6BD0B151A16"; + "0x02274FDFf21178BAD174551A9b9b68F81566A76A"; addresses.base.CrossChainStrategyProxy = - "0xcAE603E2ae51ADbf78dA619B9fF3A6BD0B151A16"; + "0x02274FDFf21178BAD174551A9b9b68F81566A76A"; module.exports = addresses; From c6a254ac9063155e63c7e08cc90c5bc41a7c730b Mon Sep 17 00:00:00 2001 From: Shahul Hameed <10547529+shahthepro@users.noreply.github.com> Date: Sat, 13 Dec 2025 10:20:22 +0400 Subject: [PATCH 008/101] Add auto-verification --- contracts/README.md | 9 + .../base/040_crosschain_strategy_proxies.js | 4 +- contracts/deploy/deployActions.js | 26 ++- .../159_crosschain_strategy_proxies.js | 4 +- contracts/utils/addresses.js | 8 +- contracts/utils/deploy.js | 182 +++++++++++++++++- 6 files changed, 219 insertions(+), 14 deletions(-) diff --git a/contracts/README.md b/contracts/README.md index 25d7111f7a..e03f0e0d5a 100644 --- a/contracts/README.md +++ b/contracts/README.md @@ -409,6 +409,15 @@ Validator public key: 90db8ae56a9e741775ca37dd960606541306974d4a998ef6a6227c85a9 The Hardhat plug-in [@nomiclabs/hardhat-verify](https://www.npmjs.com/package/@nomiclabs/hardhat-etherscan) is used to verify contracts on Etherscan. Etherscan has migrated to V2 api where all the chains use the same endpoint. Hardhat verify should be run with `--contract` parameter otherwise there is a significant slowdown while hardhat is gathering contract information. +### Auto-verification +When deploying contracts, set `VERIFY_CONTRACTS=true` environment variable to verify contract immediately after deployment with no manual action. +``` +VERIFY_CONTRACTS=true npx hardhat deploy:mainnet +``` +If it reverts for any reason, it'll print out the command that you can use to run manually or debug. + +### Manual verification + **IMPORTANT:** - Currently only yarn works. Do not use npx/pnpm diff --git a/contracts/deploy/base/040_crosschain_strategy_proxies.js b/contracts/deploy/base/040_crosschain_strategy_proxies.js index adeaad4f9c..4bbd34e73e 100644 --- a/contracts/deploy/base/040_crosschain_strategy_proxies.js +++ b/contracts/deploy/base/040_crosschain_strategy_proxies.js @@ -7,13 +7,13 @@ module.exports = deployOnBase( }, async () => { const cctpHookWrapperProxyAddress = await deployProxyWithCreateX( - "CCTPHookWrapperTest221", // Salt + "CCTPHookWrapperTest222", // Salt "CCTPHookWrapperProxy" ); console.log(`CCTPHookWrapperProxy address: ${cctpHookWrapperProxyAddress}`); // the salt needs to match the salt on the base chain deploying the other part of the strategy - const salt = "CrossChain Strategy 221 Test"; + const salt = "CrossChain Strategy 222 Test"; const proxyAddress = await deployProxyWithCreateX( salt, "CrossChainStrategyProxy" diff --git a/contracts/deploy/deployActions.js b/contracts/deploy/deployActions.js index 1a4b5a5e49..9805aada81 100644 --- a/contracts/deploy/deployActions.js +++ b/contracts/deploy/deployActions.js @@ -19,6 +19,7 @@ const { } = require("../test/helpers.js"); const { deployWithConfirmation, + verifyContractOnEtherscan, withConfirmation, encodeSaltForCreateX, } = require("../utils/deploy"); @@ -1689,7 +1690,12 @@ const deploySonicSwapXAMOStrategyImplementation = async () => { }; // deploys an instance of InitializeGovernedUpgradeabilityProxy where address is defined by salt -const deployProxyWithCreateX = async (salt, proxyName) => { +const deployProxyWithCreateX = async ( + salt, + proxyName, + verifyContract = false, + contractPath = null +) => { const { deployerAddr } = await getNamedAccounts(); const sDeployer = await ethers.provider.getSigner(deployerAddr); log(`Deploying ${proxyName} with salt: ${salt} as deployer ${deployerAddr}`); @@ -1724,6 +1730,24 @@ const deployProxyWithCreateX = async (salt, proxyName) => { .topics[1].slice(26)}` ); + log(`Deployed ${proxyName} at ${proxyAddress}`); + + // Verify contract on Etherscan if requested and on a live network + // Can be enabled via parameter or VERIFY_CONTRACTS environment variable + const shouldVerify = + verifyContract || process.env.VERIFY_CONTRACTS === "true"; + if (shouldVerify && !isTest && !isFork && proxyAddress) { + // Constructor args for the proxy are [deployerAddr] + const constructorArgs = [deployerAddr]; + await verifyContractOnEtherscan( + proxyName, + proxyAddress, + constructorArgs, + proxyName, + contractPath + ); + } + return proxyAddress; }; diff --git a/contracts/deploy/mainnet/159_crosschain_strategy_proxies.js b/contracts/deploy/mainnet/159_crosschain_strategy_proxies.js index cef646dd93..d55daaec38 100644 --- a/contracts/deploy/mainnet/159_crosschain_strategy_proxies.js +++ b/contracts/deploy/mainnet/159_crosschain_strategy_proxies.js @@ -11,13 +11,13 @@ module.exports = deploymentWithGovernanceProposal( }, async () => { const cctpHookWrapperProxyAddress = await deployProxyWithCreateX( - "CCTPHookWrapperTest221", // Salt + "CCTPHookWrapperTest222", // Salt "CCTPHookWrapperProxy" ); console.log(`CCTPHookWrapperProxy address: ${cctpHookWrapperProxyAddress}`); // the salt needs to match the salt on the base chain deploying the other part of the strategy - const salt = "CrossChain Strategy 221 Test"; + const salt = "CrossChain Strategy 222 Test"; const proxyAddress = await deployProxyWithCreateX( salt, "CrossChainStrategyProxy" diff --git a/contracts/utils/addresses.js b/contracts/utils/addresses.js index 5297b02496..956fcb8015 100644 --- a/contracts/utils/addresses.js +++ b/contracts/utils/addresses.js @@ -686,12 +686,12 @@ addresses.hoodi.mockBeaconRoots = "0xdCfcAE4A084AA843eE446f400B23aA7B6340484b"; // Crosschain Strategy -addresses.HookWrapperProxy = "0x40eC39c3EcB0e7aD45D7BC604D8FA479D5d1F405"; +addresses.HookWrapperProxy = "0x30f8a2fc7D7098061C94F042B2E7E732f95Af40F"; addresses.CrossChainStrategyProxy = - "0x02274FDFf21178BAD174551A9b9b68F81566A76A"; + "0x8ebcCa1066d15aD901927Ab01c7C6D0b057bBD34"; addresses.mainnet.CrossChainStrategyProxy = - "0x02274FDFf21178BAD174551A9b9b68F81566A76A"; + "0x8ebcCa1066d15aD901927Ab01c7C6D0b057bBD34"; addresses.base.CrossChainStrategyProxy = - "0x02274FDFf21178BAD174551A9b9b68F81566A76A"; + "0x8ebcCa1066d15aD901927Ab01c7C6D0b057bBD34"; module.exports = addresses; diff --git a/contracts/utils/deploy.js b/contracts/utils/deploy.js index 28a80646e8..8461aa5165 100644 --- a/contracts/utils/deploy.js +++ b/contracts/utils/deploy.js @@ -63,6 +63,147 @@ function log(msg, deployResult = null) { } } +/** + * Verifies a contract on Etherscan + * @param {string} contractName - Name of the contract (for logging) + * @param {string} contractAddress - Address of the deployed contract + * @param {Array} constructorArgs - Constructor arguments used for deployment + * @param {string} contract - Actual contract name in source code + * @param {string|null} contractPath - Optional contract path (e.g., "contracts/vault/VaultAdmin.sol:VaultAdmin") + */ +const verifyContractOnEtherscan = async ( + contractName, + contractAddress, + constructorArgs, + contract, + contractPath = null +) => { + // Declare finalContractPath outside try block so it's accessible in catch + let finalContractPath = contractPath; + + try { + log(`Verifying ${contractName} at ${contractAddress}...`); + + // Note: constructorArguments should be in the same format as used for deployment + // Structs should be passed as arrays/tuples (e.g., [[addr1, addr2]] for a struct with 2 addresses) + // Since we're using the same `args` that were used for deployment, structs will work correctly + const verifyArgs = { + address: contractAddress, + constructorArguments: constructorArgs || [], + }; + + // Try to get contract path from artifacts if not provided + if (!finalContractPath) { + try { + // Use the contract name (which is the actual contract name in source code) + const actualContractName = + typeof contract === "string" ? contract : contractName; + const artifact = await hre.artifacts.readArtifact(actualContractName); + + // artifact.sourceName contains the path like "contracts/vault/VaultAdmin.sol" + // We need to format it as "contracts/vault/VaultAdmin.sol:VaultAdmin" + if (artifact.sourceName) { + finalContractPath = `${artifact.sourceName}:${actualContractName}`; + log(`Auto-detected contract path: ${finalContractPath}`); + } + } catch (artifactError) { + // If we can't read the artifact, continue without contract path + // Verification will still work but may be slower + log(`Could not auto-detect contract path: ${artifactError.message}`); + } + } + + // If we have a contract path, use it (faster verification) + if (finalContractPath) { + verifyArgs.contract = finalContractPath; + } + + // Note: "verify:verify" is the full task name in Hardhat's task system + // The CLI command "hardhat verify" is actually calling the "verify:verify" subtask + // This is Hardhat's namespace convention: : + await hre.run("verify:verify", verifyArgs); + + log(`Verified ${contractName} at ${contractAddress}`); + } catch (error) { + // Log verification error but don't fail deployment + if (error.message.includes("Already Verified")) { + log(`${contractName} at ${contractAddress} is already verified`); + } else { + log( + `Warning: Failed to verify ${contractName} at ${contractAddress}: ${error.message}` + ); + + // Print the manual verification command for debugging + const networkName = hre.network.name; + let manualCommand = `yarn hardhat verify --network ${networkName}`; + + if (finalContractPath) { + manualCommand += ` --contract ${finalContractPath}`; + } + + // Format constructor arguments + if (constructorArgs && constructorArgs.length > 0) { + // Check if args are complex (contain arrays/objects) - if so, suggest using a file + const hasComplexArgs = constructorArgs.some( + (arg) => + Array.isArray(arg) || + (typeof arg === "object" && + arg !== null && + !BigNumber.isBigNumber(arg)) + ); + + if (hasComplexArgs) { + // For complex args, suggest creating a file + // Format args as a JavaScript module export + const formatArg = (arg) => { + if (Array.isArray(arg)) { + return `[${arg.map(formatArg).join(", ")}]`; + } else if (BigNumber.isBigNumber(arg)) { + return `"${arg.toString()}"`; + } else if (typeof arg === "string") { + return `"${arg}"`; + } else if (typeof arg === "object" && arg !== null) { + return JSON.stringify(arg); + } + return String(arg); + }; + + const argsCode = `module.exports = [${constructorArgs + .map(formatArg) + .join(", ")}];`; + log( + `\nTo verify manually, create a file (e.g., verify-args.js) with:` + ); + log(argsCode); + log(`\nThen run:`); + log( + `${manualCommand} --constructor-args verify-args.js ${contractAddress}` + ); + } else { + // Simple args can be passed directly + const argsStr = constructorArgs + .map((arg) => { + if (BigNumber.isBigNumber(arg)) { + return arg.toString(); + } else if (typeof arg === "string" && arg.startsWith("0x")) { + return arg; + } + return String(arg); + }) + .join(" "); + manualCommand += ` ${contractAddress} ${argsStr}`; + log(`\nTo verify manually, run:`); + log(manualCommand); + } + } else { + manualCommand += ` ${contractAddress}`; + log(`\nTo verify manually, run:`); + log(manualCommand); + } + } + } +}; + const deployWithConfirmation = async ( contractName, args, @@ -70,7 +211,9 @@ const deployWithConfirmation = async ( skipUpgradeSafety = false, libraries = {}, gasLimit, - useFeeData + useFeeData, + verifyContract = false, + contractPath = null ) => { // check that upgrade doesn't corrupt the storage slots if (!isTest && !skipUpgradeSafety) { @@ -109,6 +252,21 @@ const deployWithConfirmation = async ( await storeStorageLayoutForContract(hre, contractName, contract); } + log(`Deployed ${contractName}`, result); + // Verify contract on Etherscan if requested and on a live network + // Can be enabled via parameter or VERIFY_CONTRACTS environment variable + const shouldVerify = + verifyContract || process.env.VERIFY_CONTRACTS === "true"; + if (shouldVerify && !isTest && !isFork && result.address) { + await verifyContractOnEtherscan( + contractName, + result.address, + args, + contract, + contractPath + ); + } + log(`Deployed ${contractName}`, result); return result; }; @@ -170,7 +328,7 @@ const _verifyProxyInitializedWithCorrectGovernor = (transactionData) => { return; } - if (isFork) { + if (isMainnet || isBase || isFork || isBaseFork) { // TODO: Skip verification for Fork for now return; } @@ -1147,9 +1305,22 @@ function encodeSaltForCreateX(deployer, crosschainProtectionFlag, salt) { : "0x00"; // this portion hexifies salt to bytes11 - const saltBytes11 = ethers.utils.hexlify( - ethers.utils.zeroPad(ethers.utils.hexlify(salt), 11) - ); + // For strings, hash them first (as per comment: bytes11(keccak256(rewardToken, gauge))) + // Then take the first 11 bytes of the hash (most significant bytes) + let saltBytes11; + if (typeof salt === "string" && !ethers.utils.isHexString(salt)) { + // Hash the string and take first 11 bytes (leftmost bytes) + const hash = ethers.utils.keccak256(ethers.utils.toUtf8Bytes(salt)); + const hashBytes = ethers.utils.arrayify(hash); + // Take first 11 bytes and pad to 11 bytes (should already be 11, but ensure it) + saltBytes11 = ethers.utils.hexlify( + ethers.utils.zeroPad(hashBytes.slice(0, 11), 11) + ); + } else { + // For numbers or hex strings, pad to 11 bytes + const saltBytes = ethers.utils.hexlify(salt); + saltBytes11 = ethers.utils.hexlify(ethers.utils.zeroPad(saltBytes, 11)); + } // concat all bytes into a bytes32 const encodedSalt = ethers.utils.hexlify( ethers.utils.concat([ @@ -1272,6 +1443,7 @@ async function createPoolBoosterSonic({ module.exports = { log, deployWithConfirmation, + verifyContractOnEtherscan, withConfirmation, impersonateGuardian, executeProposalOnFork, From 8f4e39e2d0a3dad7d67a24582d86cca5ef353cee Mon Sep 17 00:00:00 2001 From: Shahul Hameed <10547529+shahthepro@users.noreply.github.com> Date: Sat, 13 Dec 2025 11:47:47 +0400 Subject: [PATCH 009/101] Fix checkBalance --- .../strategies/crosschain/CrossChainMasterStrategy.sol | 4 ++++ .../crosschain-master-strategy.mainnet.fork-test.js | 4 ++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/contracts/contracts/strategies/crosschain/CrossChainMasterStrategy.sol b/contracts/contracts/strategies/crosschain/CrossChainMasterStrategy.sol index e675235b6b..7c13f88a70 100644 --- a/contracts/contracts/strategies/crosschain/CrossChainMasterStrategy.sol +++ b/contracts/contracts/strategies/crosschain/CrossChainMasterStrategy.sol @@ -116,9 +116,13 @@ contract CrossChainMasterStrategy is override returns (uint256 balance) { + require(_asset == baseToken, "Unsupported asset"); + // USDC balance on this contract // + USDC being bridged // + USDC cached in the corresponding Remote part of this contract + uint256 undepositedUSDC = IERC20(baseToken).balanceOf(address(this)); + return undepositedUSDC + pendingAmount + remoteStrategyBalance; } /** diff --git a/contracts/test/strategies/crosschain/crosschain-master-strategy.mainnet.fork-test.js b/contracts/test/strategies/crosschain/crosschain-master-strategy.mainnet.fork-test.js index 8b8dfd832c..97e629e58d 100644 --- a/contracts/test/strategies/crosschain/crosschain-master-strategy.mainnet.fork-test.js +++ b/contracts/test/strategies/crosschain/crosschain-master-strategy.mainnet.fork-test.js @@ -49,9 +49,9 @@ describe("ForkTest: CrossChainMasterStrategy", function () { it("Should handle attestation relay", async function () { const { hookWrapper } = fixture; const attestation = - "0xf0b2792bd9b046124075e93647df38c7b1d524676f48969e692b7a79826df13913ae9086db0de46a194be8c4b52fe3b985a1fa5d6b0f038230506891a59869381b61b7567dc2e82817b7c63eb5968fcdddd53fb167eeb225aaef20ffda1aa9b0337529d52344ba8dbd272821adae236d51b8af81bdbe7ad610237f66161bbb34b41b"; + "0xc0ee7623da7bad1b2607f12c21ce71c4314b4ade3d36a0e6e13753fbb0603daa2b10fcbbc4942ce75a2b8d5f5c11f4b6c5ee5f8dce4663d3ec834674d0a9991a1cdeb52adf17d5fb3222b1f94f0767175f06e69f9473e7f948a4b5c478814f11915ed64081cbe6e139fd277630b8807b56be7c355ccdda6c20acbf0324231fc8301b"; const message = - "0x000000010000000600000000da5c3cfca2c93e77aeb7cd1c18df6e217d9a446930d4f95fdef03b2b59522bc5000000000000000000000000b8efd2c6ead9816841871c54d7b789eb517cc684000000000000000000000000b8efd2c6ead9816841871c54d7b789eb517cc684000000000000000000000000bfac208544c41ac1a675b9147f03c6df19d6435f00000000000003e8000003f20000000300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"; + "0x0000000100000006000000000384bc6f6bfe10f6df4967b6ad287d897ff729f0c7e43f73a1e18ab156e96bfb0000000000000000000000008ebcca1066d15ad901927ab01c7c6d0b057bbd340000000000000000000000008ebcca1066d15ad901927ab01c7c6d0b057bbd3400000000000000000000000030f8a2fc7d7098061c94f042b2e7e732f95af40f00000000000003e8000003f20000000300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"; await hookWrapper.relay(message, attestation); }); From f7a9b97d2e87c2baeb98f55f66607297d62d6267 Mon Sep 17 00:00:00 2001 From: Shahul Hameed <10547529+shahthepro@users.noreply.github.com> Date: Mon, 15 Dec 2025 10:25:31 +0400 Subject: [PATCH 010/101] Make CCTPHookWrapper more resilient --- .../strategies/crosschain/CCTPHookWrapper.sol | 58 ++++++++++++++----- .../deploy/base/041_crosschain_strategy.js | 1 + .../deploy/mainnet/160_crosschain_strategy.js | 1 + contracts/test/_fixture.js | 8 ++- 4 files changed, 50 insertions(+), 18 deletions(-) diff --git a/contracts/contracts/strategies/crosschain/CCTPHookWrapper.sol b/contracts/contracts/strategies/crosschain/CCTPHookWrapper.sol index 4ab77bd87d..7569fdafc4 100644 --- a/contracts/contracts/strategies/crosschain/CCTPHookWrapper.sol +++ b/contracts/contracts/strategies/crosschain/CCTPHookWrapper.sol @@ -25,7 +25,9 @@ contract CCTPHookWrapper is Governable { // Burn Message V2 fields 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; @@ -49,11 +51,13 @@ contract CCTPHookWrapper is Governable { uint32 private constant ORIGIN_MESSAGE_VERSION = 1010; ICCTPMessageTransmitter public immutable cctpMessageTransmitter; + ICCTPTokenMessenger public immutable cctpTokenMessenger; - constructor(address _cctpMessageTransmitter) { + constructor(address _cctpMessageTransmitter, address cctpTokenMessenger) { cctpMessageTransmitter = ICCTPMessageTransmitter( _cctpMessageTransmitter ); + cctpTokenMessenger = ICCTPTokenMessenger(cctpTokenMessenger); } function setPeer( @@ -90,16 +94,11 @@ contract CCTPHookWrapper is Governable { .extractSlice(SOURCE_DOMAIN_INDEX, SOURCE_DOMAIN_INDEX + 4) .decodeUint32(); - // Make sure sender is whitelisted + // Grab the message sender address sender = abi.decode( message.extractSlice(SENDER_INDEX, SENDER_INDEX + 32), (address) ); - address recipientContract = peers[sourceDomainID][sender]; - require( - recipientContract != address(0), - "Sender is not a configured peer" - ); // Ensure message body version bytes memory messageBody = message.extractSlice( @@ -112,13 +111,44 @@ contract CCTPHookWrapper is Governable { ); version = bodyVersionSlice.decodeUint32(); - bool isBurnMessageV1 = version == 1 && - messageBody.length >= BURN_MESSAGE_V2_HOOK_DATA_INDEX; + bool isBurnMessageV1 = sender == address(cctpTokenMessenger); + + if (isBurnMessageV1) { + // Handle burn message + require( + version == 1 && + messageBody.length >= BURN_MESSAGE_V2_MINT_RECIPIENT_INDEX, + "Invalid burn message" + ); + + // Find sender + bytes memory messageSender = messageBody.extractSlice( + BURN_MESSAGE_V2_MESSAGE_SENDER_INDEX, + BURN_MESSAGE_V2_MESSAGE_SENDER_INDEX + 32 + ); + sender = abi.decode(messageSender, (address)); + } + + 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( + recipientSlice, + (address) + ); + require( + whitelistedRecipient == recipientContract, + "Invalid recipient" + ); + } - // It's either CCTP Burn message v1 or Origin's custom message require( - isBurnMessageV1 || version == ORIGIN_MESSAGE_VERSION, - "Invalid CCTP message body version" + recipientContract != address(0), + "Sender is not a configured peer" ); // Relay the message @@ -129,10 +159,6 @@ contract CCTPHookWrapper is Governable { require(relaySuccess, "Receive message failed"); if (isBurnMessageV1) { - require( - messageBody.length >= BURN_MESSAGE_V2_HOOK_DATA_INDEX, - "Invalid burn message" - ); bytes memory hookData = messageBody.extractSlice( BURN_MESSAGE_V2_HOOK_DATA_INDEX, messageBody.length diff --git a/contracts/deploy/base/041_crosschain_strategy.js b/contracts/deploy/base/041_crosschain_strategy.js index 08a13e774b..8ceb794035 100644 --- a/contracts/deploy/base/041_crosschain_strategy.js +++ b/contracts/deploy/base/041_crosschain_strategy.js @@ -26,6 +26,7 @@ module.exports = deployOnBase( await deployWithConfirmation("CCTPHookWrapper", [ addresses.CCTPMessageTransmitterV2, + addresses.CCTPTokenMessengerV2, ]); const cHookWrapperImpl = await ethers.getContract("CCTPHookWrapper"); console.log(`CCTPHookWrapper address: ${cHookWrapperImpl.address}`); diff --git a/contracts/deploy/mainnet/160_crosschain_strategy.js b/contracts/deploy/mainnet/160_crosschain_strategy.js index 38a754a414..315e128507 100644 --- a/contracts/deploy/mainnet/160_crosschain_strategy.js +++ b/contracts/deploy/mainnet/160_crosschain_strategy.js @@ -30,6 +30,7 @@ module.exports = deploymentWithGovernanceProposal( await deployWithConfirmation("CCTPHookWrapper", [ addresses.CCTPMessageTransmitterV2, + addresses.CCTPTokenMessengerV2, ]); const cHookWrapperImpl = await ethers.getContract("CCTPHookWrapper"); console.log(`CCTPHookWrapper address: ${cHookWrapperImpl.address}`); diff --git a/contracts/test/_fixture.js b/contracts/test/_fixture.js index 63add54507..1242306d00 100644 --- a/contracts/test/_fixture.js +++ b/contracts/test/_fixture.js @@ -2569,8 +2569,12 @@ async function yearnCrossChainFixture() { remoteProxyAddress ); - yearnMasterStrategy.connect(sDeployer).setRemoteAddress(remoteProxyAddress); - yearnRemoteStrategy.connect(sDeployer).setMasterAddress(masterProxyAddress); + await yearnMasterStrategy + .connect(sDeployer) + .setRemoteAddress(remoteProxyAddress); + await yearnRemoteStrategy + .connect(sDeployer) + .setMasterAddress(masterProxyAddress); fixture.yearnMasterStrategy = yearnMasterStrategy; fixture.yearnRemoteStrategy = yearnRemoteStrategy; From b3c1eb4c54ed1587baf8e5eb5484dfa6a7a463bf Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Mon, 15 Dec 2025 17:09:05 +0100 Subject: [PATCH 011/101] refactor message version and type checks --- .../crosschain/AbstractCCTPIntegrator.sol | 56 ++++++------------- 1 file changed, 16 insertions(+), 40 deletions(-) diff --git a/contracts/contracts/strategies/crosschain/AbstractCCTPIntegrator.sol b/contracts/contracts/strategies/crosschain/AbstractCCTPIntegrator.sol index dbd5678d46..5a8c343ade 100644 --- a/contracts/contracts/strategies/crosschain/AbstractCCTPIntegrator.sol +++ b/contracts/contracts/strategies/crosschain/AbstractCCTPIntegrator.sol @@ -243,6 +243,17 @@ abstract contract AbstractCCTPIntegrator is Governable, IMessageHandlerV2 { return message.extractSlice(4, 8).decodeUint32(); } + function _verifyMessageVersionAndType(bytes memory _message, uint32 _version, uint32 _type) internal virtual { + require( + _getMessageVersion(_message) == _version, + "Invalid Origin Message Version" + ); + require( + _getMessageType(_message) == _type, + "Invalid Message type" + ); + } + function _getMessagePayload(bytes memory message) internal virtual @@ -272,14 +283,7 @@ abstract contract AbstractCCTPIntegrator is Governable, IMessageHandlerV2 { virtual returns (uint64, uint256) { - require( - _getMessageVersion(message) == ORIGIN_MESSAGE_VERSION, - "Invalid Origin Message Version" - ); - require( - _getMessageType(message) == DEPOSIT_MESSAGE, - "Invalid Message type" - ); + _verifyMessageVersionAndType(message, ORIGIN_MESSAGE_VERSION, DEPOSIT_MESSAGE); (uint64 nonce, uint256 depositAmount) = abi.decode( _getMessagePayload(message), @@ -312,14 +316,7 @@ abstract contract AbstractCCTPIntegrator is Governable, IMessageHandlerV2 { uint256 ) { - require( - _getMessageVersion(message) == ORIGIN_MESSAGE_VERSION, - "Invalid Origin Message Version" - ); - require( - _getMessageType(message) == DEPOSIT_ACK_MESSAGE, - "Invalid Message type" - ); + _verifyMessageVersionAndType(message, ORIGIN_MESSAGE_VERSION, DEPOSIT_ACK_MESSAGE); ( uint64 nonce, @@ -352,14 +349,7 @@ abstract contract AbstractCCTPIntegrator is Governable, IMessageHandlerV2 { virtual returns (uint64, uint256) { - require( - _getMessageVersion(message) == ORIGIN_MESSAGE_VERSION, - "Invalid Origin Message Version" - ); - require( - _getMessageType(message) == WITHDRAW_MESSAGE, - "Invalid Message type" - ); + _verifyMessageVersionAndType(message, ORIGIN_MESSAGE_VERSION, WITHDRAW_MESSAGE); (uint64 nonce, uint256 withdrawAmount) = abi.decode( _getMessagePayload(message), @@ -390,14 +380,7 @@ abstract contract AbstractCCTPIntegrator is Governable, IMessageHandlerV2 { uint256 ) { - require( - _getMessageVersion(message) == ORIGIN_MESSAGE_VERSION, - "Invalid Origin Message Version" - ); - require( - _getMessageType(message) == WITHDRAW_ACK_MESSAGE, - "Invalid Message type" - ); + _verifyMessageVersionAndType(message, ORIGIN_MESSAGE_VERSION, WITHDRAW_ACK_MESSAGE); (uint64 nonce, uint256 amountSent, uint256 balanceAfter) = abi.decode( _getMessagePayload(message), @@ -424,14 +407,7 @@ abstract contract AbstractCCTPIntegrator is Governable, IMessageHandlerV2 { virtual returns (uint64, uint256) { - require( - _getMessageVersion(message) == ORIGIN_MESSAGE_VERSION, - "Invalid Origin Message Version" - ); - require( - _getMessageType(message) == BALANCE_CHECK_MESSAGE, - "Invalid Message type" - ); + _verifyMessageVersionAndType(message, ORIGIN_MESSAGE_VERSION, BALANCE_CHECK_MESSAGE); (uint64 nonce, uint256 balance) = abi.decode( _getMessagePayload(message), From d3c4a394188e09c659dc4dbf007a835d65fea765 Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Tue, 16 Dec 2025 16:50:39 +0100 Subject: [PATCH 012/101] add some comments --- .../contracts/strategies/crosschain/AbstractCCTPIntegrator.sol | 2 ++ .../strategies/crosschain/CrossChainMasterStrategy.sol | 3 +++ .../strategies/crosschain/CrossChainRemoteStrategy.sol | 3 +++ 3 files changed, 8 insertions(+) diff --git a/contracts/contracts/strategies/crosschain/AbstractCCTPIntegrator.sol b/contracts/contracts/strategies/crosschain/AbstractCCTPIntegrator.sol index 5a8c343ade..2bae1382d4 100644 --- a/contracts/contracts/strategies/crosschain/AbstractCCTPIntegrator.sol +++ b/contracts/contracts/strategies/crosschain/AbstractCCTPIntegrator.sol @@ -206,6 +206,8 @@ abstract contract AbstractCCTPIntegrator is Governable, IMessageHandlerV2 { // TODO: figure out why getMinFeeAmount is not on CCTP v2 contract // Ref: https://developers.circle.com/cctp/evm-smart-contracts#getminfeeamount + // The issue is that the getMinFeeAmount is not present on v2.0 contracts, but is on + // v2.1. We will only be using standard transfers and fee on those is 0. uint256 maxFee = feePremiumBps > 0 ? (tokenAmount * feePremiumBps) / 10000 diff --git a/contracts/contracts/strategies/crosschain/CrossChainMasterStrategy.sol b/contracts/contracts/strategies/crosschain/CrossChainMasterStrategy.sol index 7c13f88a70..2736a175ce 100644 --- a/contracts/contracts/strategies/crosschain/CrossChainMasterStrategy.sol +++ b/contracts/contracts/strategies/crosschain/CrossChainMasterStrategy.sol @@ -4,6 +4,9 @@ pragma solidity ^0.8.0; /** * @title OUSD Yearn V3 Master Strategy - the Mainnet part * @author Origin Protocol Inc + * + * @dev This strategy can only perform 1 deposit or withdrawal at a time. For that + * reason it shouldn't be configured as an asset default strategy. */ import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; diff --git a/contracts/contracts/strategies/crosschain/CrossChainRemoteStrategy.sol b/contracts/contracts/strategies/crosschain/CrossChainRemoteStrategy.sol index f778318316..5e4952a1f2 100644 --- a/contracts/contracts/strategies/crosschain/CrossChainRemoteStrategy.sol +++ b/contracts/contracts/strategies/crosschain/CrossChainRemoteStrategy.sol @@ -4,6 +4,9 @@ pragma solidity ^0.8.0; /** * @title OUSD Yearn V3 Remote Strategy - the L2 chain part * @author Origin Protocol Inc + * + * @dev This strategy can only perform 1 deposit or withdrawal at a time. For that + * reason it shouldn't be configured as an asset default strategy. */ import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; From 792c89011b4296c4a56672e950a9a21cc9fb5567 Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Tue, 16 Dec 2025 17:18:37 +0100 Subject: [PATCH 013/101] add comment --- contracts/contracts/strategies/crosschain/CCTPHookWrapper.sol | 1 + 1 file changed, 1 insertion(+) diff --git a/contracts/contracts/strategies/crosschain/CCTPHookWrapper.sol b/contracts/contracts/strategies/crosschain/CCTPHookWrapper.sol index 7569fdafc4..5cbb5c7399 100644 --- a/contracts/contracts/strategies/crosschain/CCTPHookWrapper.sol +++ b/contracts/contracts/strategies/crosschain/CCTPHookWrapper.sol @@ -128,6 +128,7 @@ contract CCTPHookWrapper is Governable { ); sender = abi.decode(messageSender, (address)); } + // TODO: check the sender even if it is not a burn message address recipientContract = peers[sourceDomainID][sender]; From 70166bcbf1607faf6cee7ff1d550899a570bf022 Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Tue, 16 Dec 2025 23:21:42 +0100 Subject: [PATCH 014/101] fix compile errors --- .../strategies/crosschain/CCTPHookWrapper.sol | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/contracts/contracts/strategies/crosschain/CCTPHookWrapper.sol b/contracts/contracts/strategies/crosschain/CCTPHookWrapper.sol index 5cbb5c7399..2e5b5a5e43 100644 --- a/contracts/contracts/strategies/crosschain/CCTPHookWrapper.sol +++ b/contracts/contracts/strategies/crosschain/CCTPHookWrapper.sol @@ -53,11 +53,11 @@ contract CCTPHookWrapper is Governable { ICCTPMessageTransmitter public immutable cctpMessageTransmitter; ICCTPTokenMessenger public immutable cctpTokenMessenger; - constructor(address _cctpMessageTransmitter, address cctpTokenMessenger) { + constructor(address _cctpMessageTransmitter, address _cctpTokenMessenger) { cctpMessageTransmitter = ICCTPMessageTransmitter( _cctpMessageTransmitter ); - cctpTokenMessenger = ICCTPTokenMessenger(cctpTokenMessenger); + cctpTokenMessenger = ICCTPTokenMessenger(_cctpTokenMessenger); } function setPeer( @@ -115,11 +115,12 @@ contract CCTPHookWrapper is Governable { if (isBurnMessageV1) { // Handle burn message - require( - version == 1 && - messageBody.length >= BURN_MESSAGE_V2_MINT_RECIPIENT_INDEX, - "Invalid 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 bytes memory messageSender = messageBody.extractSlice( From 1e700b18d538b207e131f1db3a7748f6b5bd456d Mon Sep 17 00:00:00 2001 From: Shahul Hameed <10547529+shahthepro@users.noreply.github.com> Date: Wed, 17 Dec 2025 22:56:26 +0400 Subject: [PATCH 015/101] Change addresses --- contracts/deploy/base/040_crosschain_strategy_proxies.js | 4 ++-- .../deploy/mainnet/159_crosschain_strategy_proxies.js | 4 ++-- contracts/utils/addresses.js | 8 ++++---- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/contracts/deploy/base/040_crosschain_strategy_proxies.js b/contracts/deploy/base/040_crosschain_strategy_proxies.js index 4bbd34e73e..4c20c8c722 100644 --- a/contracts/deploy/base/040_crosschain_strategy_proxies.js +++ b/contracts/deploy/base/040_crosschain_strategy_proxies.js @@ -7,13 +7,13 @@ module.exports = deployOnBase( }, async () => { const cctpHookWrapperProxyAddress = await deployProxyWithCreateX( - "CCTPHookWrapperTest222", // Salt + "CCTPHookWrapperTest223", // Salt "CCTPHookWrapperProxy" ); console.log(`CCTPHookWrapperProxy address: ${cctpHookWrapperProxyAddress}`); // the salt needs to match the salt on the base chain deploying the other part of the strategy - const salt = "CrossChain Strategy 222 Test"; + const salt = "Morpho V2 Crosschain Strategy"; const proxyAddress = await deployProxyWithCreateX( salt, "CrossChainStrategyProxy" diff --git a/contracts/deploy/mainnet/159_crosschain_strategy_proxies.js b/contracts/deploy/mainnet/159_crosschain_strategy_proxies.js index d55daaec38..f57198211c 100644 --- a/contracts/deploy/mainnet/159_crosschain_strategy_proxies.js +++ b/contracts/deploy/mainnet/159_crosschain_strategy_proxies.js @@ -11,13 +11,13 @@ module.exports = deploymentWithGovernanceProposal( }, async () => { const cctpHookWrapperProxyAddress = await deployProxyWithCreateX( - "CCTPHookWrapperTest222", // Salt + "CCTPHookWrapperTest223", // Salt "CCTPHookWrapperProxy" ); console.log(`CCTPHookWrapperProxy address: ${cctpHookWrapperProxyAddress}`); // the salt needs to match the salt on the base chain deploying the other part of the strategy - const salt = "CrossChain Strategy 222 Test"; + const salt = "Morpho V2 Crosschain Strategy"; const proxyAddress = await deployProxyWithCreateX( salt, "CrossChainStrategyProxy" diff --git a/contracts/utils/addresses.js b/contracts/utils/addresses.js index 956fcb8015..f7443bcb9c 100644 --- a/contracts/utils/addresses.js +++ b/contracts/utils/addresses.js @@ -686,12 +686,12 @@ addresses.hoodi.mockBeaconRoots = "0xdCfcAE4A084AA843eE446f400B23aA7B6340484b"; // Crosschain Strategy -addresses.HookWrapperProxy = "0x30f8a2fc7D7098061C94F042B2E7E732f95Af40F"; +addresses.HookWrapperProxy = "0x1D609cAE43c7C1DcD6601311d87Ae227a0FFcD0f"; addresses.CrossChainStrategyProxy = - "0x8ebcCa1066d15aD901927Ab01c7C6D0b057bBD34"; + "0x98F1bA152a6Bf039F5630CD74a8Ec2cC5EA9AEd3"; addresses.mainnet.CrossChainStrategyProxy = - "0x8ebcCa1066d15aD901927Ab01c7C6D0b057bBD34"; + "0x98F1bA152a6Bf039F5630CD74a8Ec2cC5EA9AEd3"; addresses.base.CrossChainStrategyProxy = - "0x8ebcCa1066d15aD901927Ab01c7C6D0b057bBD34"; + "0x98F1bA152a6Bf039F5630CD74a8Ec2cC5EA9AEd3"; module.exports = addresses; From 7d606147a48c167b63e6238997d2b7b1d8056d35 Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Thu, 18 Dec 2025 08:22:25 +0100 Subject: [PATCH 016/101] Cross chain changes (#2718) * fix deploy files * minor rename * add calls to Morpho Vault into a try catch * refactor hook wrapper * don't revert if withdraw from underlying fails * use checkBalance for deposit/withdrawal acknowledgment * update message in remote strategy * remove unneeded functions --- .../crosschain/AbstractCCTP4626Strategy.sol | 103 +++++++++ .../crosschain/AbstractCCTPIntegrator.sol | 212 ++---------------- ...HookWrapper.sol => CCTPMessageRelayer.sol} | 138 +++++------- .../crosschain/CrossChainMasterStrategy.sol | 154 +++++-------- .../crosschain/CrossChainRemoteStrategy.sol | 126 ++++++++--- contracts/deploy/deployActions.js | 28 ++- 6 files changed, 350 insertions(+), 411 deletions(-) create mode 100644 contracts/contracts/strategies/crosschain/AbstractCCTP4626Strategy.sol rename contracts/contracts/strategies/crosschain/{CCTPHookWrapper.sol => CCTPMessageRelayer.sol} (60%) diff --git a/contracts/contracts/strategies/crosschain/AbstractCCTP4626Strategy.sol b/contracts/contracts/strategies/crosschain/AbstractCCTP4626Strategy.sol new file mode 100644 index 0000000000..9ec08e75de --- /dev/null +++ b/contracts/contracts/strategies/crosschain/AbstractCCTP4626Strategy.sol @@ -0,0 +1,103 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +/** + * @title AbstractCCTP4626Strategy - Abstract contract for CCTP morpho strategy + * @author Origin Protocol Inc + */ + +import { AbstractCCTPIntegrator } from "./AbstractCCTPIntegrator.sol"; + +abstract contract AbstractCCTP4626Strategy 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 _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 _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/AbstractCCTPIntegrator.sol b/contracts/contracts/strategies/crosschain/AbstractCCTPIntegrator.sol index 2bae1382d4..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 @@ -64,25 +61,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 - ) { - cctpTokenMessenger = ICCTPTokenMessenger(_cctpTokenMessenger); - cctpMessageTransmitter = ICCTPMessageTransmitter( - _cctpMessageTransmitter - ); - destinationDomain = _destinationDomain; - destinationStrategy = _destinationStrategy; - baseToken = _baseToken; - cctpHookWrapper = _cctpHookWrapper; + CCTPIntegrationConfig memory _config + ) CCTPMessageRelayer(_config.cctpMessageTransmitter, _config.cctpTokenMessenger) { + 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"); } @@ -176,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) @@ -267,157 +248,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/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 2736a175ce..885fa61fd0 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 { AbstractCCTP4626Strategy } from "./AbstractCCTP4626Strategy.sol"; import { BytesHelper } from "../../utils/BytesHelper.sol"; contract CrossChainMasterStrategy is InitializableAbstractStrategy, - AbstractCCTPIntegrator + AbstractCCTP4626Strategy { using SafeERC20 for IERC20; @@ -29,26 +29,17 @@ contract CrossChainMasterStrategy is // Transfer amounts by nonce mapping(uint64 => uint256) public transferAmounts; + event RemoteStrategyBalanceUpdated(uint256 balance); /** * @param _stratConfig The platform and OToken vault addresses */ 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 + AbstractCCTP4626Strategy( + _cctpConfig ) {} @@ -169,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"); } } @@ -190,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( @@ -255,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" @@ -272,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 5e4952a1f2..c472d0cf14 100644 --- a/contracts/contracts/strategies/crosschain/CrossChainRemoteStrategy.sol +++ b/contracts/contracts/strategies/crosschain/CrossChainRemoteStrategy.sol @@ -11,33 +11,27 @@ 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 { AbstractCCTPIntegrator } from "./AbstractCCTPIntegrator.sol"; +import { AbstractCCTP4626Strategy } from "./AbstractCCTP4626Strategy.sol"; contract CrossChainRemoteStrategy is - AbstractCCTPIntegrator, + AbstractCCTP4626Strategy, Generalized4626Strategy { + event DepositFailed(string reason); + event WithdrawFailed(string reason); + 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 + AbstractCCTP4626Strategy( + _cctpConfig ) - Generalized4626Strategy(_baseConfig, _baseToken) + Generalized4626Strategy(_baseConfig, _cctpConfig.baseToken) {} // solhint-disable-next-line no-unused-vars @@ -90,6 +84,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 @@ -98,19 +93,39 @@ 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); - - bytes memory message = _encodeDepositAckMessage( - nonce, - tokenAmount, - feeExecuted, + bytes memory message = _encodeBalanceCheckMessage( + lastTransferNonce, balanceAfter ); _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"); + + // 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)) { + 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 @@ -120,18 +135,53 @@ 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 uint256 balanceAfter = checkBalance(baseToken); - - bytes memory message = _encodeWithdrawAckMessage( - nonce, - withdrawAmount, + bytes memory message = _encodeBalanceCheckMessage( + lastTransferNonce, 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); + } else { + _sendMessage(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( @@ -155,4 +205,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; + } } 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 987dc0a00458675e89f46c4318c9691f96be35d2 Mon Sep 17 00:00:00 2001 From: Shahul Hameed <10547529+shahthepro@users.noreply.github.com> Date: Thu, 18 Dec 2025 11:30:53 +0400 Subject: [PATCH 017/101] Fix compilation issues --- .../crosschain/AbstractCCTP4626Strategy.sol | 33 ++++++------ .../crosschain/AbstractCCTPIntegrator.sol | 43 ++++++++-------- .../crosschain/CCTPMessageRelayer.sol | 44 +++++++++------- .../crosschain/CrossChainMasterStrategy.sol | 20 ++++---- .../crosschain/CrossChainRemoteStrategy.sol | 50 ++++++++++++++----- contracts/deploy/deployActions.js | 6 +-- 6 files changed, 116 insertions(+), 80 deletions(-) diff --git a/contracts/contracts/strategies/crosschain/AbstractCCTP4626Strategy.sol b/contracts/contracts/strategies/crosschain/AbstractCCTP4626Strategy.sol index 9ec08e75de..615d2f77fe 100644 --- a/contracts/contracts/strategies/crosschain/AbstractCCTP4626Strategy.sol +++ b/contracts/contracts/strategies/crosschain/AbstractCCTP4626Strategy.sol @@ -8,18 +8,11 @@ pragma solidity ^0.8.0; import { AbstractCCTPIntegrator } from "./AbstractCCTPIntegrator.sol"; -abstract contract AbstractCCTP4626Strategy is - AbstractCCTPIntegrator -{ - - constructor( - CCTPIntegrationConfig memory _config - ) - AbstractCCTPIntegrator( - _config - ) +abstract contract AbstractCCTP4626Strategy is AbstractCCTPIntegrator { + constructor(CCTPIntegrationConfig memory _config) + AbstractCCTPIntegrator(_config) {} - + function _encodeDepositMessage(uint64 nonce, uint256 depositAmount) internal virtual @@ -38,7 +31,11 @@ abstract contract AbstractCCTP4626Strategy is virtual returns (uint64, uint256) { - _verifyMessageVersionAndType(message, ORIGIN_MESSAGE_VERSION, DEPOSIT_MESSAGE); + _verifyMessageVersionAndType( + message, + ORIGIN_MESSAGE_VERSION, + DEPOSIT_MESSAGE + ); (uint64 nonce, uint256 depositAmount) = abi.decode( _getMessagePayload(message), @@ -65,7 +62,11 @@ abstract contract AbstractCCTP4626Strategy is virtual returns (uint64, uint256) { - _verifyMessageVersionAndType(message, ORIGIN_MESSAGE_VERSION, WITHDRAW_MESSAGE); + _verifyMessageVersionAndType( + message, + ORIGIN_MESSAGE_VERSION, + WITHDRAW_MESSAGE + ); (uint64 nonce, uint256 withdrawAmount) = abi.decode( _getMessagePayload(message), @@ -92,7 +93,11 @@ abstract contract AbstractCCTP4626Strategy is virtual returns (uint64, uint256) { - _verifyMessageVersionAndType(message, ORIGIN_MESSAGE_VERSION, BALANCE_CHECK_MESSAGE); + _verifyMessageVersionAndType( + message, + ORIGIN_MESSAGE_VERSION, + BALANCE_CHECK_MESSAGE + ); (uint64 nonce, uint256 balance) = abi.decode( _getMessagePayload(message), diff --git a/contracts/contracts/strategies/crosschain/AbstractCCTPIntegrator.sol b/contracts/contracts/strategies/crosschain/AbstractCCTPIntegrator.sol index 859be159c9..ea65632fda 100644 --- a/contracts/contracts/strategies/crosschain/AbstractCCTPIntegrator.sol +++ b/contracts/contracts/strategies/crosschain/AbstractCCTPIntegrator.sol @@ -11,7 +11,11 @@ import { BytesHelper } from "../../utils/BytesHelper.sol"; import { CCTPMessageRelayer } from "./CCTPMessageRelayer.sol"; import "../../utils/Helpers.sol"; -abstract contract AbstractCCTPIntegrator is Governable, IMessageHandlerV2, CCTPMessageRelayer { +abstract contract AbstractCCTPIntegrator is + Governable, + IMessageHandlerV2, + CCTPMessageRelayer +{ using SafeERC20 for IERC20; using BytesHelper for bytes; @@ -19,17 +23,12 @@ abstract contract AbstractCCTPIntegrator is Governable, IMessageHandlerV2, CCTPM event CCTPMinFinalityThresholdSet(uint32 minFinalityThreshold); event CCTPFeePremiumBpsSet(uint32 feePremiumBps); - uint32 public constant ORIGIN_MESSAGE_VERSION = 1010; - uint32 public constant DEPOSIT_MESSAGE = 1; uint32 public constant DEPOSIT_ACK_MESSAGE = 10; uint32 public constant WITHDRAW_MESSAGE = 2; uint32 public constant WITHDRAW_ACK_MESSAGE = 20; uint32 public constant BALANCE_CHECK_MESSAGE = 3; - // CCTP Hook Wrapper - address public immutable cctpHookWrapper; - // USDC address on local chain address public immutable baseToken; @@ -67,18 +66,21 @@ abstract contract AbstractCCTPIntegrator is Governable, IMessageHandlerV2, CCTPM uint32 destinationDomain; address destinationStrategy; address baseToken; - address cctpHookWrapper; } - constructor( - CCTPIntegrationConfig memory _config - ) CCTPMessageRelayer(_config.cctpMessageTransmitter, _config.cctpTokenMessenger) { + constructor(CCTPIntegrationConfig memory _config) + CCTPMessageRelayer( + _config.cctpMessageTransmitter, + _config.cctpTokenMessenger + ) + { cctpTokenMessenger = ICCTPTokenMessenger(_config.cctpTokenMessenger); - cctpMessageTransmitter = ICCTPMessageTransmitter(_config.cctpMessageTransmitter); + 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(_config.baseToken); @@ -187,7 +189,7 @@ abstract contract AbstractCCTPIntegrator is Governable, IMessageHandlerV2, CCTPM // TODO: figure out why getMinFeeAmount is not on CCTP v2 contract // Ref: https://developers.circle.com/cctp/evm-smart-contracts#getminfeeamount - // The issue is that the getMinFeeAmount is not present on v2.0 contracts, but is on + // The issue is that the getMinFeeAmount is not present on v2.0 contracts, but is on // v2.1. We will only be using standard transfers and fee on those is 0. uint256 maxFee = feePremiumBps > 0 @@ -199,7 +201,7 @@ abstract contract AbstractCCTPIntegrator is Governable, IMessageHandlerV2, CCTPM destinationDomain, bytes32(uint256(uint160(destinationStrategy))), address(baseToken), - bytes32(uint256(uint160(cctpHookWrapper))), + bytes32(uint256(uint160(destinationStrategy))), maxFee, minFinalityThreshold, hookData @@ -226,15 +228,16 @@ abstract contract AbstractCCTPIntegrator is Governable, IMessageHandlerV2, CCTPM return message.extractSlice(4, 8).decodeUint32(); } - function _verifyMessageVersionAndType(bytes memory _message, uint32 _version, uint32 _type) internal virtual { + function _verifyMessageVersionAndType( + bytes memory _message, + uint32 _version, + uint32 _type + ) internal virtual { require( _getMessageVersion(_message) == _version, "Invalid Origin Message Version" ); - require( - _getMessageType(_message) == _type, - "Invalid Message type" - ); + require(_getMessageType(_message) == _type, "Invalid Message type"); } function _getMessagePayload(bytes memory message) @@ -252,7 +255,7 @@ abstract contract AbstractCCTPIntegrator is Governable, IMessageHandlerV2, CCTPM cctpMessageTransmitter.sendMessage( destinationDomain, bytes32(uint256(uint160(destinationStrategy))), - bytes32(uint256(uint160(cctpHookWrapper))), + bytes32(uint256(uint160(destinationStrategy))), minFinalityThreshold, message ); diff --git a/contracts/contracts/strategies/crosschain/CCTPMessageRelayer.sol b/contracts/contracts/strategies/crosschain/CCTPMessageRelayer.sol index 451606778d..725855866c 100644 --- a/contracts/contracts/strategies/crosschain/CCTPMessageRelayer.sol +++ b/contracts/contracts/strategies/crosschain/CCTPMessageRelayer.sol @@ -24,12 +24,12 @@ abstract contract CCTPMessageRelayer { 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; - uint32 private constant CCTP_MESSAGE_VERSION = 1; - uint32 private constant ORIGIN_MESSAGE_VERSION = 1010; + uint32 public constant CCTP_MESSAGE_VERSION = 1; + uint32 public constant ORIGIN_MESSAGE_VERSION = 1010; // CCTP contracts // This implementation assumes that remote and local chains have these contracts @@ -45,19 +45,32 @@ abstract contract CCTPMessageRelayer { } function _decodeMessageHeader(bytes memory message) - internal pure returns ( + 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(); + ) + { + 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)); + 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)); + recipient = abi.decode( + message.extractSlice(RECIPIENT_INDEX, RECIPIENT_INDEX + 32), + (address) + ); messageBody = message.extractSlice(MESSAGE_BODY_INDEX, message.length); } @@ -83,7 +96,7 @@ abstract contract CCTPMessageRelayer { ); version = bodyVersionSlice.decodeUint32(); - // TODO should we replace this with: + // 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); @@ -109,10 +122,7 @@ abstract contract CCTPMessageRelayer { BURN_MESSAGE_V2_RECIPIENT_INDEX + 32 ); // TODO is this the same recipient as the one in the message header? - recipient = abi.decode( - recipientSlice, - (address) - ); + recipient = abi.decode(recipientSlice, (address)); } require(sender == recipient, "Sender and recipient must be the same"); @@ -144,11 +154,7 @@ abstract contract CCTPMessageRelayer { ); uint256 feeExecuted = abi.decode(feeSlice, (uint256)); - _onTokenReceived( - tokenAmount - feeExecuted, - feeExecuted, - hookData - ); + _onTokenReceived(tokenAmount - feeExecuted, feeExecuted, hookData); } } diff --git a/contracts/contracts/strategies/crosschain/CrossChainMasterStrategy.sol b/contracts/contracts/strategies/crosschain/CrossChainMasterStrategy.sol index 885fa61fd0..b9ae69a0af 100644 --- a/contracts/contracts/strategies/crosschain/CrossChainMasterStrategy.sol +++ b/contracts/contracts/strategies/crosschain/CrossChainMasterStrategy.sol @@ -4,8 +4,8 @@ pragma solidity ^0.8.0; /** * @title OUSD Yearn V3 Master Strategy - the Mainnet part * @author Origin Protocol Inc - * - * @dev This strategy can only perform 1 deposit or withdrawal at a time. For that + * + * @dev This strategy can only perform 1 deposit or withdrawal at a time. For that * reason it shouldn't be configured as an asset default strategy. */ @@ -30,6 +30,7 @@ contract CrossChainMasterStrategy is mapping(uint64 => uint256) public transferAmounts; event RemoteStrategyBalanceUpdated(uint256 balance); + /** * @param _stratConfig The platform and OToken vault addresses */ @@ -38,9 +39,7 @@ contract CrossChainMasterStrategy is CCTPIntegrationConfig memory _cctpConfig ) InitializableAbstractStrategy(_stratConfig) - AbstractCCTP4626Strategy( - _cctpConfig - ) + AbstractCCTP4626Strategy(_cctpConfig) {} // /** @@ -163,8 +162,7 @@ contract CrossChainMasterStrategy is if (messageType == BALANCE_CHECK_MESSAGE) { // Received when Remote strategy checks the balance _processBalanceCheckMessage(payload); - } - else { + } else { revert("Unknown message type"); } } @@ -226,7 +224,7 @@ contract CrossChainMasterStrategy is } /** - * @dev process balance check serves 3 purposes: + * @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 @@ -240,11 +238,11 @@ contract CrossChainMasterStrategy is 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); @@ -258,7 +256,7 @@ contract CrossChainMasterStrategy is 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 diff --git a/contracts/contracts/strategies/crosschain/CrossChainRemoteStrategy.sol b/contracts/contracts/strategies/crosschain/CrossChainRemoteStrategy.sol index c472d0cf14..ec59b3c3da 100644 --- a/contracts/contracts/strategies/crosschain/CrossChainRemoteStrategy.sol +++ b/contracts/contracts/strategies/crosschain/CrossChainRemoteStrategy.sol @@ -5,7 +5,7 @@ pragma solidity ^0.8.0; * @title OUSD Yearn V3 Remote Strategy - the L2 chain part * @author Origin Protocol Inc * - * @dev This strategy can only perform 1 deposit or withdrawal at a time. For that + * @dev This strategy can only perform 1 deposit or withdrawal at a time. For that * reason it shouldn't be configured as an asset default strategy. */ @@ -21,16 +21,14 @@ contract CrossChainRemoteStrategy is { event DepositFailed(string reason); event WithdrawFailed(string reason); - + using SafeERC20 for IERC20; constructor( BaseStrategyConfig memory _baseConfig, CCTPIntegrationConfig memory _cctpConfig ) - AbstractCCTP4626Strategy( - _cctpConfig - ) + AbstractCCTP4626Strategy(_cctpConfig) Generalized4626Strategy(_baseConfig, _cctpConfig.baseToken) {} @@ -120,9 +118,18 @@ contract CrossChainRemoteStrategy is 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))); + 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))); + emit DepositFailed( + string( + abi.encodePacked( + "Deposit failed: low-level call failed with data ", + lowLevelData + ) + ) + ); } } @@ -155,7 +162,7 @@ contract CrossChainRemoteStrategy is _sendMessage(message); } } - + /** * @dev Withdraw asset by burning shares * @param _recipient Address to receive withdrawn asset @@ -175,12 +182,27 @@ contract CrossChainRemoteStrategy is // 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)) { + 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))); + 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))); + emit WithdrawFailed( + string( + abi.encodePacked( + "Withdrawal failed: low-level call failed with data ", + lowLevelData + ) + ) + ); } } @@ -220,11 +242,13 @@ contract CrossChainRemoteStrategy is 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 + * 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; + return + platform.previewRedeem(platform.balanceOf(address(this))) + + balanceOnContract; } } diff --git a/contracts/deploy/deployActions.js b/contracts/deploy/deployActions.js index 1adbd15eb0..f4de97d24f 100644 --- a/contracts/deploy/deployActions.js +++ b/contracts/deploy/deployActions.js @@ -1779,14 +1779,14 @@ const deployCrossChainMasterStrategyImpl = async ( deployerAddr, // vault address // addresses.mainnet.VaultProxy, ], - [ + [ addresses.CCTPTokenMessengerV2, addresses.CCTPMessageTransmitterV2, targetDomainId, remoteStrategyAddress, baseToken, hookWrapperAddress, - ] + ], ] ); @@ -1849,7 +1849,7 @@ const deployCrossChainRemoteStrategyImpl = async ( remoteStrategyAddress, baseToken, hookWrapperAddress, - ] + ], ] ); From b63bd5fb15b1076baf3dd0d02c06c59960c12ffa Mon Sep 17 00:00:00 2001 From: Shahul Hameed <10547529+shahthepro@users.noreply.github.com> Date: Thu, 18 Dec 2025 11:43:12 +0400 Subject: [PATCH 018/101] Fix deployment files a bit --- .../proxies/create2/CCTPHookWrapperProxy.sol | 21 ---------- .../crosschain/AbstractCCTPIntegrator.sol | 2 - .../crosschain/CCTPMessageRelayer.sol | 6 +++ .../crosschain/CrossChainRemoteStrategy.sol | 4 +- .../base/040_crosschain_strategy_proxies.js | 6 --- .../deploy/base/041_crosschain_strategy.js | 41 +------------------ contracts/deploy/deployActions.js | 4 -- ....js => 160_crosschain_strategy_proxies.js} | 8 +--- ...strategy.js => 161_crosschain_strategy.js} | 38 ----------------- contracts/test/_fixture-base.js | 5 --- contracts/test/_fixture.js | 6 --- ...chain-master-strategy.mainnet.fork-test.js | 4 +- contracts/utils/addresses.js | 1 - 13 files changed, 13 insertions(+), 133 deletions(-) delete mode 100644 contracts/contracts/proxies/create2/CCTPHookWrapperProxy.sol rename contracts/deploy/mainnet/{159_crosschain_strategy_proxies.js => 160_crosschain_strategy_proxies.js} (71%) rename contracts/deploy/mainnet/{160_crosschain_strategy.js => 161_crosschain_strategy.js} (57%) diff --git a/contracts/contracts/proxies/create2/CCTPHookWrapperProxy.sol b/contracts/contracts/proxies/create2/CCTPHookWrapperProxy.sol deleted file mode 100644 index e94c8faac7..0000000000 --- a/contracts/contracts/proxies/create2/CCTPHookWrapperProxy.sol +++ /dev/null @@ -1,21 +0,0 @@ -// SPDX-License-Identifier: BUSL-1.1 -pragma solidity ^0.8.0; - -import { InitializeGovernedUpgradeabilityProxy2 } from "../InitializeGovernedUpgradeabilityProxy2.sol"; - -// ******************************************************** -// ******************************************************** -// IMPORTANT: DO NOT CHANGE ANYTHING IN THIS FILE. -// Any changes to this file (even whitespaces) will -// affect the create2 address of the proxy -// ******************************************************** -// ******************************************************** - -/** - * @notice CCTPHookWrapperProxy delegates calls to a CCTPHookWrapper implementation - */ -contract CCTPHookWrapperProxy is InitializeGovernedUpgradeabilityProxy2 { - constructor(address governor) - InitializeGovernedUpgradeabilityProxy2(governor) - {} -} diff --git a/contracts/contracts/strategies/crosschain/AbstractCCTPIntegrator.sol b/contracts/contracts/strategies/crosschain/AbstractCCTPIntegrator.sol index ea65632fda..0a39c1e160 100644 --- a/contracts/contracts/strategies/crosschain/AbstractCCTPIntegrator.sol +++ b/contracts/contracts/strategies/crosschain/AbstractCCTPIntegrator.sol @@ -177,8 +177,6 @@ abstract contract AbstractCCTPIntegrator is return true; } - function _onMessageReceived(bytes memory payload) internal virtual; - function _sendTokens(uint256 tokenAmount, bytes memory hookData) internal virtual diff --git a/contracts/contracts/strategies/crosschain/CCTPMessageRelayer.sol b/contracts/contracts/strategies/crosschain/CCTPMessageRelayer.sol index 725855866c..4deaa13ca7 100644 --- a/contracts/contracts/strategies/crosschain/CCTPMessageRelayer.sol +++ b/contracts/contracts/strategies/crosschain/CCTPMessageRelayer.sol @@ -169,4 +169,10 @@ abstract contract CCTPMessageRelayer { uint256 feeExecuted, bytes memory payload ) internal virtual; + + /** + * @dev Called when the message is received + * @param payload The payload of the message + */ + function _onMessageReceived(bytes memory payload) internal virtual; } diff --git a/contracts/contracts/strategies/crosschain/CrossChainRemoteStrategy.sol b/contracts/contracts/strategies/crosschain/CrossChainRemoteStrategy.sol index ec59b3c3da..653671cf1a 100644 --- a/contracts/contracts/strategies/crosschain/CrossChainRemoteStrategy.sol +++ b/contracts/contracts/strategies/crosschain/CrossChainRemoteStrategy.sol @@ -77,12 +77,14 @@ contract CrossChainRemoteStrategy is } function _processDepositMessage( + // solhint-disable-next-line no-unused-vars uint256 tokenAmount, + // solhint-disable-next-line no-unused-vars uint256 feeExecuted, bytes memory payload ) internal virtual { - // solhint-disable-next-line no-unused-vars // TODO: no need to communicate the deposit amount if we deposit everything + // solhint-disable-next-line no-unused-vars (uint64 nonce, uint256 depositAmount) = _decodeDepositMessage(payload); // Replay protection diff --git a/contracts/deploy/base/040_crosschain_strategy_proxies.js b/contracts/deploy/base/040_crosschain_strategy_proxies.js index 4c20c8c722..d13f925ae1 100644 --- a/contracts/deploy/base/040_crosschain_strategy_proxies.js +++ b/contracts/deploy/base/040_crosschain_strategy_proxies.js @@ -6,12 +6,6 @@ module.exports = deployOnBase( deployName: "040_crosschain_strategy_proxies", }, async () => { - const cctpHookWrapperProxyAddress = await deployProxyWithCreateX( - "CCTPHookWrapperTest223", // Salt - "CCTPHookWrapperProxy" - ); - console.log(`CCTPHookWrapperProxy address: ${cctpHookWrapperProxyAddress}`); - // the salt needs to match the salt on the base chain deploying the other part of the strategy const salt = "Morpho V2 Crosschain Strategy"; const proxyAddress = await deployProxyWithCreateX( diff --git a/contracts/deploy/base/041_crosschain_strategy.js b/contracts/deploy/base/041_crosschain_strategy.js index 8ceb794035..d79500e4c6 100644 --- a/contracts/deploy/base/041_crosschain_strategy.js +++ b/contracts/deploy/base/041_crosschain_strategy.js @@ -1,10 +1,7 @@ const { deployOnBase } = require("../../utils/deploy-l2"); const addresses = require("../../utils/addresses"); const { deployCrossChainRemoteStrategyImpl } = require("../deployActions"); -const { - deployWithConfirmation, - withConfirmation, -} = require("../../utils/deploy.js"); +const { withConfirmation } = require("../../utils/deploy.js"); const { cctpDomainIds } = require("../../utils/cctp"); module.exports = deployOnBase( @@ -15,42 +12,16 @@ module.exports = deployOnBase( const { deployerAddr } = await getNamedAccounts(); const sDeployer = await ethers.provider.getSigner(deployerAddr); - console.log(`HookWrapperProxy address: ${addresses.HookWrapperProxy}`); - const cHookWrapperProxy = await ethers.getContractAt( - "CCTPHookWrapperProxy", - addresses.HookWrapperProxy - ); console.log( `CrossChainStrategyProxy address: ${addresses.CrossChainStrategyProxy}` ); - await deployWithConfirmation("CCTPHookWrapper", [ - addresses.CCTPMessageTransmitterV2, - addresses.CCTPTokenMessengerV2, - ]); - const cHookWrapperImpl = await ethers.getContract("CCTPHookWrapper"); - console.log(`CCTPHookWrapper address: ${cHookWrapperImpl.address}`); - - const cHookWrapper = await ethers.getContractAt( - "CCTPHookWrapper", - addresses.HookWrapperProxy - ); - - await withConfirmation( - cHookWrapperProxy.connect(sDeployer).initialize( - cHookWrapperImpl.address, - deployerAddr, // TODO: change governor later - "0x" - ) - ); - const implAddress = await deployCrossChainRemoteStrategyImpl( "0xbeeF010f9cb27031ad51e3333f9aF9C6B1228183", // 4626 Vault addresses.CrossChainStrategyProxy, cctpDomainIds.Ethereum, addresses.CrossChainStrategyProxy, addresses.base.USDC, - cHookWrapper.address, "CrossChainRemoteStrategy" ); console.log(`CrossChainRemoteStrategyImpl address: ${implAddress}`); @@ -69,16 +40,6 @@ module.exports = deployOnBase( ) ); - await withConfirmation( - cHookWrapper - .connect(sDeployer) - .setPeer( - cctpDomainIds.Ethereum, - addresses.CrossChainStrategyProxy, - addresses.CrossChainStrategyProxy - ) - ); - await withConfirmation( cCrossChainRemoteStrategy.connect(sDeployer).safeApproveAllTokens() ); diff --git a/contracts/deploy/deployActions.js b/contracts/deploy/deployActions.js index f4de97d24f..0b5d93430a 100644 --- a/contracts/deploy/deployActions.js +++ b/contracts/deploy/deployActions.js @@ -1757,7 +1757,6 @@ const deployCrossChainMasterStrategyImpl = async ( targetDomainId, remoteStrategyAddress, baseToken, - hookWrapperAddress, implementationName = "CrossChainMasterStrategy", skipInitialize = false ) => { @@ -1785,7 +1784,6 @@ const deployCrossChainMasterStrategyImpl = async ( targetDomainId, remoteStrategyAddress, baseToken, - hookWrapperAddress, ], ] ); @@ -1821,7 +1819,6 @@ const deployCrossChainRemoteStrategyImpl = async ( targetDomainId, remoteStrategyAddress, baseToken, - hookWrapperAddress, implementationName = "CrossChainRemoteStrategy" ) => { const { deployerAddr } = await getNamedAccounts(); @@ -1848,7 +1845,6 @@ const deployCrossChainRemoteStrategyImpl = async ( targetDomainId, remoteStrategyAddress, baseToken, - hookWrapperAddress, ], ] ); diff --git a/contracts/deploy/mainnet/159_crosschain_strategy_proxies.js b/contracts/deploy/mainnet/160_crosschain_strategy_proxies.js similarity index 71% rename from contracts/deploy/mainnet/159_crosschain_strategy_proxies.js rename to contracts/deploy/mainnet/160_crosschain_strategy_proxies.js index f57198211c..6ab1f07c19 100644 --- a/contracts/deploy/mainnet/159_crosschain_strategy_proxies.js +++ b/contracts/deploy/mainnet/160_crosschain_strategy_proxies.js @@ -3,19 +3,13 @@ const { deployProxyWithCreateX } = require("../deployActions"); module.exports = deploymentWithGovernanceProposal( { - deployName: "159_crosschain_strategy_proxies", + deployName: "160_crosschain_strategy_proxies", forceDeploy: false, reduceQueueTime: true, deployerIsProposer: false, proposalId: "", }, async () => { - const cctpHookWrapperProxyAddress = await deployProxyWithCreateX( - "CCTPHookWrapperTest223", // Salt - "CCTPHookWrapperProxy" - ); - console.log(`CCTPHookWrapperProxy address: ${cctpHookWrapperProxyAddress}`); - // the salt needs to match the salt on the base chain deploying the other part of the strategy const salt = "Morpho V2 Crosschain Strategy"; const proxyAddress = await deployProxyWithCreateX( diff --git a/contracts/deploy/mainnet/160_crosschain_strategy.js b/contracts/deploy/mainnet/161_crosschain_strategy.js similarity index 57% rename from contracts/deploy/mainnet/160_crosschain_strategy.js rename to contracts/deploy/mainnet/161_crosschain_strategy.js index 315e128507..971f0aa16f 100644 --- a/contracts/deploy/mainnet/160_crosschain_strategy.js +++ b/contracts/deploy/mainnet/161_crosschain_strategy.js @@ -1,6 +1,5 @@ const { deploymentWithGovernanceProposal, - deployWithConfirmation, withConfirmation, } = require("../../utils/deploy"); const addresses = require("../../utils/addresses"); @@ -19,43 +18,16 @@ module.exports = deploymentWithGovernanceProposal( const { deployerAddr } = await getNamedAccounts(); const sDeployer = await ethers.provider.getSigner(deployerAddr); - console.log(`HookWrapperProxy address: ${addresses.HookWrapperProxy}`); - const cHookWrapperProxy = await ethers.getContractAt( - "CCTPHookWrapperProxy", - addresses.HookWrapperProxy - ); console.log( `CrossChainStrategyProxy address: ${addresses.CrossChainStrategyProxy}` ); - await deployWithConfirmation("CCTPHookWrapper", [ - addresses.CCTPMessageTransmitterV2, - addresses.CCTPTokenMessengerV2, - ]); - const cHookWrapperImpl = await ethers.getContract("CCTPHookWrapper"); - console.log(`CCTPHookWrapper address: ${cHookWrapperImpl.address}`); - - const cHookWrapper = await ethers.getContractAt( - "CCTPHookWrapper", - addresses.HookWrapperProxy - ); - - await withConfirmation( - cHookWrapperProxy.connect(sDeployer).initialize( - cHookWrapperImpl.address, - deployerAddr, // TODO: change governor later - "0x" - ) - ); - const implAddress = await deployCrossChainMasterStrategyImpl( addresses.CrossChainStrategyProxy, cctpDomainIds.Base, // Same address for both master and remote strategy addresses.CrossChainStrategyProxy, addresses.mainnet.USDC, - // Same address on all chains - cHookWrapper.address, "CrossChainMasterStrategy" ); console.log(`CrossChainMasterStrategyImpl address: ${implAddress}`); @@ -74,16 +46,6 @@ module.exports = deploymentWithGovernanceProposal( ) ); - await withConfirmation( - cHookWrapper - .connect(sDeployer) - .setPeer( - cctpDomainIds.Base, - addresses.CrossChainStrategyProxy, - addresses.CrossChainStrategyProxy - ) - ); - return { actions: [], }; diff --git a/contracts/test/_fixture-base.js b/contracts/test/_fixture-base.js index fb1c1914b3..3f4e016e95 100644 --- a/contracts/test/_fixture-base.js +++ b/contracts/test/_fixture-base.js @@ -343,14 +343,9 @@ const crossChainFixture = deployments.createFixture(async () => { "CrossChainRemoteStrategy", addresses.CrossChainStrategyProxy ); - const hookWrapper = await ethers.getContractAt( - "CCTPHookWrapper", - addresses.HookWrapperProxy - ); return { ...fixture, crossChainRemoteStrategy, - hookWrapper, }; }); diff --git a/contracts/test/_fixture.js b/contracts/test/_fixture.js index 1242306d00..004a287d47 100644 --- a/contracts/test/_fixture.js +++ b/contracts/test/_fixture.js @@ -2917,10 +2917,6 @@ async function enableExecutionLayerGeneralPurposeRequests() { async function crossChainFixture() { const fixture = await defaultFixture(); - const cHookWrapper = await ethers.getContractAt( - "CCTPHookWrapper", - addresses.HookWrapperProxy - ); const cCrossChainMasterStrategy = await ethers.getContractAt( "CrossChainMasterStrategy", addresses.CrossChainStrategyProxy @@ -2928,8 +2924,6 @@ async function crossChainFixture() { return { ...fixture, - - hookWrapper: cHookWrapper, crossChainMasterStrategy: cCrossChainMasterStrategy, }; } diff --git a/contracts/test/strategies/crosschain/crosschain-master-strategy.mainnet.fork-test.js b/contracts/test/strategies/crosschain/crosschain-master-strategy.mainnet.fork-test.js index 97e629e58d..6a39b0b311 100644 --- a/contracts/test/strategies/crosschain/crosschain-master-strategy.mainnet.fork-test.js +++ b/contracts/test/strategies/crosschain/crosschain-master-strategy.mainnet.fork-test.js @@ -47,12 +47,12 @@ describe("ForkTest: CrossChainMasterStrategy", function () { }); it("Should handle attestation relay", async function () { - const { hookWrapper } = fixture; + const { crossChainMasterStrategy } = fixture; const attestation = "0xc0ee7623da7bad1b2607f12c21ce71c4314b4ade3d36a0e6e13753fbb0603daa2b10fcbbc4942ce75a2b8d5f5c11f4b6c5ee5f8dce4663d3ec834674d0a9991a1cdeb52adf17d5fb3222b1f94f0767175f06e69f9473e7f948a4b5c478814f11915ed64081cbe6e139fd277630b8807b56be7c355ccdda6c20acbf0324231fc8301b"; const message = "0x0000000100000006000000000384bc6f6bfe10f6df4967b6ad287d897ff729f0c7e43f73a1e18ab156e96bfb0000000000000000000000008ebcca1066d15ad901927ab01c7c6d0b057bbd340000000000000000000000008ebcca1066d15ad901927ab01c7c6d0b057bbd3400000000000000000000000030f8a2fc7d7098061c94f042b2e7e732f95af40f00000000000003e8000003f20000000300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"; - await hookWrapper.relay(message, attestation); + await crossChainMasterStrategy.relay(message, attestation); }); }); diff --git a/contracts/utils/addresses.js b/contracts/utils/addresses.js index f7443bcb9c..e1babc54df 100644 --- a/contracts/utils/addresses.js +++ b/contracts/utils/addresses.js @@ -686,7 +686,6 @@ addresses.hoodi.mockBeaconRoots = "0xdCfcAE4A084AA843eE446f400B23aA7B6340484b"; // Crosschain Strategy -addresses.HookWrapperProxy = "0x1D609cAE43c7C1DcD6601311d87Ae227a0FFcD0f"; addresses.CrossChainStrategyProxy = "0x98F1bA152a6Bf039F5630CD74a8Ec2cC5EA9AEd3"; addresses.mainnet.CrossChainStrategyProxy = From bf1fbe2a15f5066792e9f559f70608c0f01ec2f4 Mon Sep 17 00:00:00 2001 From: Shahul Hameed <10547529+shahthepro@users.noreply.github.com> Date: Thu, 18 Dec 2025 11:51:44 +0400 Subject: [PATCH 019/101] Fix Message relayer --- .../strategies/crosschain/CCTPMessageRelayer.sol | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/contracts/contracts/strategies/crosschain/CCTPMessageRelayer.sol b/contracts/contracts/strategies/crosschain/CCTPMessageRelayer.sol index 4deaa13ca7..40993abf1e 100644 --- a/contracts/contracts/strategies/crosschain/CCTPMessageRelayer.sol +++ b/contracts/contracts/strategies/crosschain/CCTPMessageRelayer.sol @@ -12,7 +12,7 @@ abstract contract CCTPMessageRelayer { 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 RECIPIENT_INDEX = 76; uint8 private constant MESSAGE_BODY_INDEX = 148; // Message body V2 fields @@ -114,15 +114,19 @@ abstract contract CCTPMessageRelayer { BURN_MESSAGE_V2_MESSAGE_SENDER_INDEX + 32 ); sender = abi.decode(messageSender, (address)); - } - if (isBurnMessageV1) { bytes memory recipientSlice = messageBody.extractSlice( BURN_MESSAGE_V2_RECIPIENT_INDEX, BURN_MESSAGE_V2_RECIPIENT_INDEX + 32 ); // TODO is this the same recipient as the one in the message header? recipient = abi.decode(recipientSlice, (address)); + } else { + // We handle only Burn message or our custom messagee + require( + version == ORIGIN_MESSAGE_VERSION, + "Unsupported message version" + ); } require(sender == recipient, "Sender and recipient must be the same"); From a0dd07b02303dfd4b35d861ecb11f04c6d9c4f83 Mon Sep 17 00:00:00 2001 From: Shahul Hameed <10547529+shahthepro@users.noreply.github.com> Date: Thu, 18 Dec 2025 12:03:34 +0400 Subject: [PATCH 020/101] Clean up master strategy --- .../crosschain/CrossChainMasterStrategy.sol | 40 +++++++++---------- 1 file changed, 18 insertions(+), 22 deletions(-) diff --git a/contracts/contracts/strategies/crosschain/CrossChainMasterStrategy.sol b/contracts/contracts/strategies/crosschain/CrossChainMasterStrategy.sol index b9ae69a0af..10ee86d1d4 100644 --- a/contracts/contracts/strategies/crosschain/CrossChainMasterStrategy.sol +++ b/contracts/contracts/strategies/crosschain/CrossChainMasterStrategy.sol @@ -26,9 +26,6 @@ contract CrossChainMasterStrategy is // Amount that's bridged but not yet received on the destination chain uint256 public pendingAmount; - // Transfer amounts by nonce - mapping(uint64 => uint256) public transferAmounts; - event RemoteStrategyBalanceUpdated(uint256 balance); /** @@ -123,7 +120,7 @@ contract CrossChainMasterStrategy is * @param _asset Address of the asset */ function supportsAsset(address _asset) public view override returns (bool) { - return assetToPToken[_asset] != address(0); + return _asset == baseToken; } /** @@ -168,11 +165,13 @@ contract CrossChainMasterStrategy is } function _onTokenReceived( + // solhint-disable-next-line no-unused-vars uint256 tokenAmount, + // solhint-disable-next-line no-unused-vars uint256 feeExecuted, bytes memory payload ) internal override { - // expecring a BALANCE_CHECK_MESSAGE + // Expecting a BALANCE_CHECK_MESSAGE _onMessageReceived(payload); } @@ -187,7 +186,6 @@ contract CrossChainMasterStrategy is ); uint64 nonce = _getNextNonce(); - transferAmounts[nonce] = depositAmount; // Set pending amount pendingAmount = depositAmount; @@ -216,8 +214,6 @@ contract CrossChainMasterStrategy is emit Withdrawal(baseToken, baseToken, _amount); - transferAmounts[nonce] = _amount; - // Send withdrawal message with payload bytes memory message = _encodeWithdrawMessage(nonce, _amount); _sendMessage(message); @@ -238,32 +234,32 @@ contract CrossChainMasterStrategy is uint64 _lastCachedNonce = lastTransferNonce; + if (nonce != _lastCachedNonce) { + // If nonce is not the last cached nonce, it is an outdated message + // Ignore it + return; + } + + // Update the balance always + remoteStrategyBalance = balance; + emit RemoteStrategyBalanceUpdated(balance); + /** * 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)) { + if (!isNonceProcessed(nonce)) { _markNonceAsProcessed(nonce); - remoteStrategyBalance = balance; - emit RemoteStrategyBalanceUpdated(balance); - - // effect of confirming a deposit + // Effect of confirming a deposit, reset pending amount pendingAmount = 0; - // effect of confirming a withdrawal + uint256 usdcBalance = IERC20(baseToken).balanceOf(address(this)); + // Effect of confirming a withdrawal 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); - } - // otherwise the message nonce is smaller than the last cached nonce, meaning it is outdated - // the contract should ignore it } } From 4071943588472816a250bf1f9611b47de48b277d Mon Sep 17 00:00:00 2001 From: Shahul Hameed <10547529+shahthepro@users.noreply.github.com> Date: Thu, 18 Dec 2025 12:13:25 +0400 Subject: [PATCH 021/101] Fix deployment file name --- contracts/deploy/mainnet/161_crosschain_strategy.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/deploy/mainnet/161_crosschain_strategy.js b/contracts/deploy/mainnet/161_crosschain_strategy.js index 971f0aa16f..b28e8503a8 100644 --- a/contracts/deploy/mainnet/161_crosschain_strategy.js +++ b/contracts/deploy/mainnet/161_crosschain_strategy.js @@ -8,7 +8,7 @@ const { deployCrossChainMasterStrategyImpl } = require("../deployActions"); module.exports = deploymentWithGovernanceProposal( { - deployName: "160_crosschain_strategy", + deployName: "161_crosschain_strategy", forceDeploy: false, reduceQueueTime: true, deployerIsProposer: false, From 9e36485df4f86d9daa5b32d74666f9e1885bf35c Mon Sep 17 00:00:00 2001 From: Shahul Hameed <10547529+shahthepro@users.noreply.github.com> Date: Thu, 18 Dec 2025 14:08:30 +0400 Subject: [PATCH 022/101] move around stuff --- .../strategies/crosschain/AbstractCCTP4626Strategy.sol | 4 ++++ .../strategies/crosschain/AbstractCCTPIntegrator.sol | 10 ++-------- ...ssageRelayer.sol => AbstractCCTPMessageRelayer.sol} | 3 ++- 3 files changed, 8 insertions(+), 9 deletions(-) rename contracts/contracts/strategies/crosschain/{CCTPMessageRelayer.sol => AbstractCCTPMessageRelayer.sol} (98%) diff --git a/contracts/contracts/strategies/crosschain/AbstractCCTP4626Strategy.sol b/contracts/contracts/strategies/crosschain/AbstractCCTP4626Strategy.sol index 615d2f77fe..89b23c1dac 100644 --- a/contracts/contracts/strategies/crosschain/AbstractCCTP4626Strategy.sol +++ b/contracts/contracts/strategies/crosschain/AbstractCCTP4626Strategy.sol @@ -9,6 +9,10 @@ pragma solidity ^0.8.0; import { AbstractCCTPIntegrator } from "./AbstractCCTPIntegrator.sol"; abstract contract AbstractCCTP4626Strategy is AbstractCCTPIntegrator { + uint32 public constant DEPOSIT_MESSAGE = 1; + uint32 public constant WITHDRAW_MESSAGE = 2; + uint32 public constant BALANCE_CHECK_MESSAGE = 3; + constructor(CCTPIntegrationConfig memory _config) AbstractCCTPIntegrator(_config) {} diff --git a/contracts/contracts/strategies/crosschain/AbstractCCTPIntegrator.sol b/contracts/contracts/strategies/crosschain/AbstractCCTPIntegrator.sol index 0a39c1e160..d0eadaf6b8 100644 --- a/contracts/contracts/strategies/crosschain/AbstractCCTPIntegrator.sol +++ b/contracts/contracts/strategies/crosschain/AbstractCCTPIntegrator.sol @@ -8,13 +8,13 @@ import { ICCTPTokenMessenger, ICCTPMessageTransmitter, IMessageHandlerV2 } from import { Governable } from "../../governance/Governable.sol"; import { BytesHelper } from "../../utils/BytesHelper.sol"; -import { CCTPMessageRelayer } from "./CCTPMessageRelayer.sol"; +import { AbstractCCTPMessageRelayer } from "./AbstractCCTPMessageRelayer.sol"; import "../../utils/Helpers.sol"; abstract contract AbstractCCTPIntegrator is Governable, IMessageHandlerV2, - CCTPMessageRelayer + AbstractCCTPMessageRelayer { using SafeERC20 for IERC20; @@ -23,12 +23,6 @@ abstract contract AbstractCCTPIntegrator is event CCTPMinFinalityThresholdSet(uint32 minFinalityThreshold); event CCTPFeePremiumBpsSet(uint32 feePremiumBps); - uint32 public constant DEPOSIT_MESSAGE = 1; - uint32 public constant DEPOSIT_ACK_MESSAGE = 10; - uint32 public constant WITHDRAW_MESSAGE = 2; - uint32 public constant WITHDRAW_ACK_MESSAGE = 20; - uint32 public constant BALANCE_CHECK_MESSAGE = 3; - // USDC address on local chain address public immutable baseToken; diff --git a/contracts/contracts/strategies/crosschain/CCTPMessageRelayer.sol b/contracts/contracts/strategies/crosschain/AbstractCCTPMessageRelayer.sol similarity index 98% rename from contracts/contracts/strategies/crosschain/CCTPMessageRelayer.sol rename to contracts/contracts/strategies/crosschain/AbstractCCTPMessageRelayer.sol index 40993abf1e..955fc4b581 100644 --- a/contracts/contracts/strategies/crosschain/CCTPMessageRelayer.sol +++ b/contracts/contracts/strategies/crosschain/AbstractCCTPMessageRelayer.sol @@ -4,7 +4,7 @@ pragma solidity ^0.8.0; import { ICCTPTokenMessenger, ICCTPMessageTransmitter } from "../../interfaces/cctp/ICCTP.sol"; import { BytesHelper } from "../../utils/BytesHelper.sol"; -abstract contract CCTPMessageRelayer { +abstract contract AbstractCCTPMessageRelayer { using BytesHelper for bytes; // CCTP Message Header fields @@ -77,6 +77,7 @@ abstract contract CCTPMessageRelayer { function relay(bytes memory message, bytes memory attestation) external { ( uint32 version, + // solhint-disable-next-line no-unused-vars uint32 sourceDomainID, address sender, address recipient, From e401fa1226198463117a9c10c8a96aecb81cdb08 Mon Sep 17 00:00:00 2001 From: Shahul Hameed <10547529+shahthepro@users.noreply.github.com> Date: Thu, 18 Dec 2025 17:53:13 +0400 Subject: [PATCH 023/101] Fix CCTP Integrator --- .../strategies/crosschain/AbstractCCTPIntegrator.sol | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/contracts/contracts/strategies/crosschain/AbstractCCTPIntegrator.sol b/contracts/contracts/strategies/crosschain/AbstractCCTPIntegrator.sol index d0eadaf6b8..fb18feb0de 100644 --- a/contracts/contracts/strategies/crosschain/AbstractCCTPIntegrator.sol +++ b/contracts/contracts/strategies/crosschain/AbstractCCTPIntegrator.sol @@ -63,22 +63,24 @@ abstract contract AbstractCCTPIntegrator is } constructor(CCTPIntegrationConfig memory _config) - CCTPMessageRelayer( + AbstractCCTPMessageRelayer( _config.cctpMessageTransmitter, _config.cctpTokenMessenger ) { - cctpTokenMessenger = ICCTPTokenMessenger(_config.cctpTokenMessenger); - cctpMessageTransmitter = ICCTPMessageTransmitter( - _config.cctpMessageTransmitter - ); destinationDomain = _config.destinationDomain; destinationStrategy = _config.destinationStrategy; baseToken = _config.baseToken; // Just a sanity check to ensure the base token is USDC uint256 _baseTokenDecimals = Helpers.getDecimals(_config.baseToken); + string memory _baseTokenSymbol = Helpers.getSymbol(_config.baseToken); require(_baseTokenDecimals == 6, "Base token decimals must be 6"); + require( + keccak256(abi.encodePacked(_baseTokenSymbol)) == + keccak256(abi.encodePacked("USDC")), + "Base token symbol must be USDC" + ); } function _initialize(uint32 _minFinalityThreshold, uint32 _feePremiumBps) From 73abf6c17ff181726815a7c28e29366d2aaefcd2 Mon Sep 17 00:00:00 2001 From: Shahul Hameed <10547529+shahthepro@users.noreply.github.com> Date: Fri, 19 Dec 2025 09:31:06 +0400 Subject: [PATCH 024/101] clean up fork --- ...chain-master-strategy.mainnet.fork-test.js | 119 ++++++++++++++++-- 1 file changed, 110 insertions(+), 9 deletions(-) diff --git a/contracts/test/strategies/crosschain/crosschain-master-strategy.mainnet.fork-test.js b/contracts/test/strategies/crosschain/crosschain-master-strategy.mainnet.fork-test.js index 6a39b0b311..329f76f28f 100644 --- a/contracts/test/strategies/crosschain/crosschain-master-strategy.mainnet.fork-test.js +++ b/contracts/test/strategies/crosschain/crosschain-master-strategy.mainnet.fork-test.js @@ -1,12 +1,20 @@ -// const { expect } = require("chai"); +const { expect } = require("chai"); const { usdcUnits, isCI } = require("../../helpers"); const { createFixtureLoader, crossChainFixture } = require("../../_fixture"); const { impersonateAndFund } = require("../../../utils/signers"); -const { formatUnits } = require("ethers/lib/utils"); +// const { formatUnits } = require("ethers/lib/utils"); +const addresses = require("../../../utils/addresses"); const loadFixture = createFixtureLoader(crossChainFixture); +const DEPOSIT_FOR_BURN_EVENT_TOPIC = + "0x0c8c1cbdc5190613ebd485511d4e2812cfa45eecb79d845893331fedad5130a5"; +// const MESSAGE_SENT_EVENT_TOPIC = +// "0x8c5261668696ce22758910d05bab8f186d6eb247ceac2af2e82c7dc17669b036"; + +// const ORIGIN_MESSAGE_VERSION_HEX = "0x000003f2"; // 1010 + describe("ForkTest: CrossChainMasterStrategy", function () { this.timeout(0); @@ -18,7 +26,56 @@ describe("ForkTest: CrossChainMasterStrategy", function () { fixture = await loadFixture(); }); - it("Should initiate a bridge of deposited USDC", async function () { + const decodeDepositForBurnEvent = (event) => { + const [ + amount, + mintRecipient, + destinationDomain, + destinationTokenMessenger, + destinationCaller, + maxFee, + hookData, + ] = ethers.utils.defaultAbiCoder.decode( + [ + "uint256", + "address", + "uint32", + "address", + "address", + "uint256", + "bytes", + ], + event.data + ); + + const [burnToken] = ethers.utils.defaultAbiCoder.decode( + ["address"], + event.topics[1] + ); + const [depositer] = ethers.utils.defaultAbiCoder.decode( + ["address"], + event.topics[2] + ); + const [minFinalityThreshold] = ethers.utils.defaultAbiCoder.decode( + ["uint256"], + event.topics[3] + ); + + return { + amount, + mintRecipient, + destinationDomain, + destinationTokenMessenger, + destinationCaller, + maxFee, + hookData, + burnToken, + depositer, + minFinalityThreshold, + }; + }; + + it("Should initiate bridging of deposited USDC", async function () { const { matt, crossChainMasterStrategy, usdc } = fixture; // const govAddr = await crossChainMasterStrategy.governor(); // const governor = await impersonateAndFund(govAddr); @@ -31,22 +88,66 @@ describe("ForkTest: CrossChainMasterStrategy", function () { .connect(matt) .transfer(crossChainMasterStrategy.address, usdcUnits("1000")); - const balanceBefore = await usdc.balanceOf( + const usdcBalanceBefore = await usdc.balanceOf( crossChainMasterStrategy.address ); + const strategyBalanceBefore = await crossChainMasterStrategy.checkBalance( + usdc.address + ); // Simulate deposit call - await crossChainMasterStrategy + const tx = await crossChainMasterStrategy .connect(impersonatedVault) .deposit(usdc.address, usdcUnits("1000")); - const balanceAfter = await usdc.balanceOf(crossChainMasterStrategy.address); + const usdcBalanceAfter = await usdc.balanceOf( + crossChainMasterStrategy.address + ); + expect(usdcBalanceAfter).to.eq(usdcBalanceBefore.sub(usdcUnits("1000"))); + + const strategyBalanceAfter = await crossChainMasterStrategy.checkBalance( + usdc.address + ); + expect(strategyBalanceAfter).to.eq(strategyBalanceBefore); + + expect(await crossChainMasterStrategy.pendingAmount()).to.eq( + usdcUnits("1000") + ); + + // Check for message sent event + const receipt = await tx.wait(); + const depositForBurnEvent = receipt.events.find((e) => + e.topics.includes(DEPOSIT_FOR_BURN_EVENT_TOPIC) + ); + const burnEventData = decodeDepositForBurnEvent(depositForBurnEvent); + + expect(burnEventData.amount).to.eq(usdcUnits("1000")); + expect(burnEventData.mintRecipient.toLowerCase()).to.eq( + crossChainMasterStrategy.address.toLowerCase() + ); + expect(burnEventData.destinationDomain).to.eq(6); + expect(burnEventData.destinationTokenMessenger.toLowerCase()).to.eq( + addresses.CCTPTokenMessengerV2.toLowerCase() + ); + expect(burnEventData.destinationCaller.toLowerCase()).to.eq( + crossChainMasterStrategy.address.toLowerCase() + ); + expect(burnEventData.maxFee).to.eq(0); + expect(burnEventData.burnToken).to.eq(usdc.address); + + expect(burnEventData.depositer.toLowerCase()).to.eq( + crossChainMasterStrategy.address.toLowerCase() + ); + expect(burnEventData.minFinalityThreshold).to.eq(2000); + expect(burnEventData.burnToken.toLowerCase()).to.eq( + usdc.address.toLowerCase() + ); - console.log(`Balance before: ${formatUnits(balanceBefore, 6)}`); - console.log(`Balance after: ${formatUnits(balanceAfter, 6)}`); + // TODO: Check Hook Data + // expect(burnEventData.hookData).to.eq(""); }); - it("Should handle attestation relay", async function () { + it.skip("Should handle attestation relay", async function () { const { crossChainMasterStrategy } = fixture; const attestation = "0xc0ee7623da7bad1b2607f12c21ce71c4314b4ade3d36a0e6e13753fbb0603daa2b10fcbbc4942ce75a2b8d5f5c11f4b6c5ee5f8dce4663d3ec834674d0a9991a1cdeb52adf17d5fb3222b1f94f0767175f06e69f9473e7f948a4b5c478814f11915ed64081cbe6e139fd277630b8807b56be7c355ccdda6c20acbf0324231fc8301b"; From 266ec0139f5d06199134fd24c63215505acd6ed0 Mon Sep 17 00:00:00 2001 From: Shah <10547529+shahthepro@users.noreply.github.com> Date: Fri, 19 Dec 2025 15:01:24 +0400 Subject: [PATCH 025/101] Fix race condition (#2720) * Fix race condition * Transfer everything on wtihdrawal * Move destination domain one step above --- .../crosschain/AbstractCCTPIntegrator.sol | 15 ++--- .../crosschain/AbstractCCTPMessageRelayer.sol | 20 +++--- .../crosschain/CrossChainMasterStrategy.sol | 62 ++++++++++++++++--- 3 files changed, 71 insertions(+), 26 deletions(-) diff --git a/contracts/contracts/strategies/crosschain/AbstractCCTPIntegrator.sol b/contracts/contracts/strategies/crosschain/AbstractCCTPIntegrator.sol index fb18feb0de..7e9aad721e 100644 --- a/contracts/contracts/strategies/crosschain/AbstractCCTPIntegrator.sol +++ b/contracts/contracts/strategies/crosschain/AbstractCCTPIntegrator.sol @@ -26,9 +26,6 @@ abstract contract AbstractCCTPIntegrator is // USDC address on local chain address public immutable baseToken; - // Destination chain domain ID - uint32 public immutable destinationDomain; - // Strategy address on destination chain address public immutable destinationStrategy; @@ -57,7 +54,7 @@ abstract contract AbstractCCTPIntegrator is struct CCTPIntegrationConfig { address cctpTokenMessenger; address cctpMessageTransmitter; - uint32 destinationDomain; + uint32 peerDomainID; address destinationStrategy; address baseToken; } @@ -65,10 +62,10 @@ abstract contract AbstractCCTPIntegrator is constructor(CCTPIntegrationConfig memory _config) AbstractCCTPMessageRelayer( _config.cctpMessageTransmitter, - _config.cctpTokenMessenger + _config.cctpTokenMessenger, + _config.peerDomainID ) { - destinationDomain = _config.destinationDomain; destinationStrategy = _config.destinationStrategy; baseToken = _config.baseToken; @@ -162,7 +159,7 @@ abstract contract AbstractCCTPIntegrator is // finalityThresholdExecuted >= minFinalityThreshold, // "Finality threshold too low" // ); - require(sourceDomain == destinationDomain, "Unknown Source Domain"); + require(sourceDomain == peerDomainID, "Unknown Source Domain"); // Extract address from bytes32 (CCTP stores addresses as right-padded bytes32) address senderAddress = address(uint160(uint256(sender))); @@ -192,7 +189,7 @@ abstract contract AbstractCCTPIntegrator is cctpTokenMessenger.depositForBurnWithHook( tokenAmount, - destinationDomain, + peerDomainID, bytes32(uint256(uint160(destinationStrategy))), address(baseToken), bytes32(uint256(uint160(destinationStrategy))), @@ -247,7 +244,7 @@ abstract contract AbstractCCTPIntegrator is function _sendMessage(bytes memory message) internal virtual { cctpMessageTransmitter.sendMessage( - destinationDomain, + peerDomainID, bytes32(uint256(uint160(destinationStrategy))), bytes32(uint256(uint160(destinationStrategy))), minFinalityThreshold, diff --git a/contracts/contracts/strategies/crosschain/AbstractCCTPMessageRelayer.sol b/contracts/contracts/strategies/crosschain/AbstractCCTPMessageRelayer.sol index 955fc4b581..4fc48d233b 100644 --- a/contracts/contracts/strategies/crosschain/AbstractCCTPMessageRelayer.sol +++ b/contracts/contracts/strategies/crosschain/AbstractCCTPMessageRelayer.sol @@ -25,9 +25,6 @@ abstract contract AbstractCCTPMessageRelayer { 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; - uint32 public constant CCTP_MESSAGE_VERSION = 1; uint32 public constant ORIGIN_MESSAGE_VERSION = 1010; @@ -37,11 +34,19 @@ abstract contract AbstractCCTPMessageRelayer { ICCTPMessageTransmitter public immutable cctpMessageTransmitter; ICCTPTokenMessenger public immutable cctpTokenMessenger; - constructor(address _cctpMessageTransmitter, address _cctpTokenMessenger) { + // Domain ID of the chain from which messages are accepted + uint32 public immutable peerDomainID; + + constructor( + address _cctpMessageTransmitter, + address _cctpTokenMessenger, + uint32 _peerDomainID + ) { cctpMessageTransmitter = ICCTPMessageTransmitter( _cctpMessageTransmitter ); cctpTokenMessenger = ICCTPTokenMessenger(_cctpTokenMessenger); + peerDomainID = _peerDomainID; } function _decodeMessageHeader(bytes memory message) @@ -77,7 +82,6 @@ abstract contract AbstractCCTPMessageRelayer { function relay(bytes memory message, bytes memory attestation) external { ( uint32 version, - // solhint-disable-next-line no-unused-vars uint32 sourceDomainID, address sender, address recipient, @@ -90,6 +94,9 @@ abstract contract AbstractCCTPMessageRelayer { "Invalid CCTP message version" ); + // Ensure that the source domain is the peer domain + require(sourceDomainID == peerDomainID, "Unknown Source Domain"); + // Ensure message body version bytes memory bodyVersionSlice = messageBody.extractSlice( BURN_MESSAGE_V2_VERSION_INDEX, @@ -97,7 +104,6 @@ abstract contract AbstractCCTPMessageRelayer { ); 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); @@ -120,7 +126,7 @@ abstract contract AbstractCCTPMessageRelayer { BURN_MESSAGE_V2_RECIPIENT_INDEX, BURN_MESSAGE_V2_RECIPIENT_INDEX + 32 ); - // TODO is this the same recipient as the one in the message header? + recipient = abi.decode(recipientSlice, (address)); } else { // We handle only Burn message or our custom messagee diff --git a/contracts/contracts/strategies/crosschain/CrossChainMasterStrategy.sol b/contracts/contracts/strategies/crosschain/CrossChainMasterStrategy.sol index 10ee86d1d4..b55cd16320 100644 --- a/contracts/contracts/strategies/crosschain/CrossChainMasterStrategy.sol +++ b/contracts/contracts/strategies/crosschain/CrossChainMasterStrategy.sol @@ -26,6 +26,13 @@ contract CrossChainMasterStrategy is // Amount that's bridged but not yet received on the destination chain uint256 public pendingAmount; + enum TransferType { + None, // To avoid using 0 + Deposit, + Withdrawal + } + mapping(uint64 => TransferType) public transferTypeByNonce; + event RemoteStrategyBalanceUpdated(uint256 balance); /** @@ -171,8 +178,36 @@ contract CrossChainMasterStrategy is uint256 feeExecuted, bytes memory payload ) internal override { - // Expecting a BALANCE_CHECK_MESSAGE + uint64 _nonce = lastTransferNonce; + + // Should be expecting an acknowledgement + require(!isNonceProcessed(_nonce), "Nonce already processed"); + // Only a withdrawal can send tokens to Master strategy + require( + transferTypeByNonce[_nonce] == TransferType.Withdrawal, + "Expecting withdrawal" + ); + + // Confirm receipt of tokens from Withdraw command + _markNonceAsProcessed(_nonce); + + // Now relay to the regular flow + // NOTE: Calling _onMessageReceived would mean that we are bypassing a + // few checks that the regular flow does (like sourceDomainID check + // and sender check in `handleReceiveFinalizedMessage`). However, + // CCTPMessageRelayer relays the message first (which will go through + // all the checks) and not update balance and then finally calls this + // `_onTokenReceived` which will update the balance. + // So, if any of the checks fail during the first no-balance-update flow, + // this won't happen either, since the tx would revert. _onMessageReceived(payload); + + // Send any tokens in the contract to the Vault + uint256 usdcBalance = IERC20(baseToken).balanceOf(address(this)); + // Should always have enough tokens + require(usdcBalance >= tokenAmount, "Insufficient balance"); + // Transfer all tokens to the Vault to not leave any dust + IERC20(baseToken).safeTransfer(vaultAddress, usdcBalance); } function _deposit(address _asset, uint256 depositAmount) internal virtual { @@ -186,6 +221,7 @@ contract CrossChainMasterStrategy is ); uint64 nonce = _getNextNonce(); + transferTypeByNonce[nonce] = TransferType.Deposit; // Set pending amount pendingAmount = depositAmount; @@ -211,6 +247,7 @@ contract CrossChainMasterStrategy is ); uint64 nonce = _getNextNonce(); + transferTypeByNonce[nonce] = TransferType.Withdrawal; emit Withdrawal(baseToken, baseToken, _amount); @@ -240,26 +277,31 @@ contract CrossChainMasterStrategy is return; } + bool processedTransfer = isNonceProcessed(nonce); + if ( + !processedTransfer && + transferTypeByNonce[nonce] == TransferType.Withdrawal + ) { + // Pending withdrawal is taken care of by _onTokenReceived + // Do not update balance due to race conditions + return; + } + // Update the balance always remoteStrategyBalance = balance; emit RemoteStrategyBalanceUpdated(balance); /** - * 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. + * A deposit is being confirmed. + * A withdrawal will always be confirmed if it reaches this point of code. */ - if (!isNonceProcessed(nonce)) { + if (!processedTransfer) { _markNonceAsProcessed(nonce); // Effect of confirming a deposit, reset pending amount pendingAmount = 0; - uint256 usdcBalance = IERC20(baseToken).balanceOf(address(this)); - // Effect of confirming a withdrawal - if (usdcBalance > 1e6) { - IERC20(baseToken).safeTransfer(vaultAddress, usdcBalance); - } + // NOTE: Withdrawal is taken care of by _onTokenReceived } } } From 122fa3219f37eaa8565cb6fbc2b0e0c89e0e3f87 Mon Sep 17 00:00:00 2001 From: Shahul Hameed <10547529+shahthepro@users.noreply.github.com> Date: Fri, 19 Dec 2025 15:20:04 +0400 Subject: [PATCH 026/101] Cleanup code --- .../crosschain/AbstractCCTPIntegrator.sol | 188 ++++++++++++++--- .../crosschain/AbstractCCTPMessageRelayer.sol | 189 ------------------ contracts/contracts/utils/BytesHelper.sol | 40 ++++ 3 files changed, 204 insertions(+), 213 deletions(-) delete mode 100644 contracts/contracts/strategies/crosschain/AbstractCCTPMessageRelayer.sol diff --git a/contracts/contracts/strategies/crosschain/AbstractCCTPIntegrator.sol b/contracts/contracts/strategies/crosschain/AbstractCCTPIntegrator.sol index 7e9aad721e..e8cf7a72a9 100644 --- a/contracts/contracts/strategies/crosschain/AbstractCCTPIntegrator.sol +++ b/contracts/contracts/strategies/crosschain/AbstractCCTPIntegrator.sol @@ -8,14 +8,27 @@ import { ICCTPTokenMessenger, ICCTPMessageTransmitter, IMessageHandlerV2 } from import { Governable } from "../../governance/Governable.sol"; import { BytesHelper } from "../../utils/BytesHelper.sol"; -import { AbstractCCTPMessageRelayer } from "./AbstractCCTPMessageRelayer.sol"; import "../../utils/Helpers.sol"; -abstract contract AbstractCCTPIntegrator is - Governable, - IMessageHandlerV2, - AbstractCCTPMessageRelayer -{ +// CCTP Message Header fields +// Ref: https://developers.circle.com/cctp/technical-guide#message-header +uint8 constant VERSION_INDEX = 0; +uint8 constant SOURCE_DOMAIN_INDEX = 4; +uint8 constant SENDER_INDEX = 44; +uint8 constant RECIPIENT_INDEX = 76; +uint8 constant MESSAGE_BODY_INDEX = 148; + +// 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 constant BURN_MESSAGE_V2_VERSION_INDEX = 0; +uint8 constant BURN_MESSAGE_V2_RECIPIENT_INDEX = 36; +uint8 constant BURN_MESSAGE_V2_AMOUNT_INDEX = 68; +uint8 constant BURN_MESSAGE_V2_MESSAGE_SENDER_INDEX = 100; +uint8 constant BURN_MESSAGE_V2_FEE_EXECUTED_INDEX = 164; +uint8 constant BURN_MESSAGE_V2_HOOK_DATA_INDEX = 228; + +abstract contract AbstractCCTPIntegrator is Governable, IMessageHandlerV2 { using SafeERC20 for IERC20; using BytesHelper for bytes; @@ -23,11 +36,23 @@ abstract contract AbstractCCTPIntegrator is event CCTPMinFinalityThresholdSet(uint32 minFinalityThreshold); event CCTPFeePremiumBpsSet(uint32 feePremiumBps); + uint32 public constant CCTP_MESSAGE_VERSION = 1; + uint32 public 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; + // USDC address on local chain address public immutable baseToken; - // Strategy address on destination chain - address public immutable destinationStrategy; + // Domain ID of the chain from which messages are accepted + uint32 public immutable peerDomainID; + + // Strategy address on other chain + address public immutable peerStrategy; // CCTP params uint32 public minFinalityThreshold; @@ -55,18 +80,18 @@ abstract contract AbstractCCTPIntegrator is address cctpTokenMessenger; address cctpMessageTransmitter; uint32 peerDomainID; - address destinationStrategy; + address peerStrategy; address baseToken; } - constructor(CCTPIntegrationConfig memory _config) - AbstractCCTPMessageRelayer( - _config.cctpMessageTransmitter, - _config.cctpTokenMessenger, - _config.peerDomainID - ) - { - destinationStrategy = _config.destinationStrategy; + constructor(CCTPIntegrationConfig memory _config) { + cctpMessageTransmitter = ICCTPMessageTransmitter( + _config.cctpMessageTransmitter + ); + cctpTokenMessenger = ICCTPTokenMessenger(_config.cctpTokenMessenger); + peerDomainID = _config.peerDomainID; + + peerStrategy = _config.peerStrategy; baseToken = _config.baseToken; // Just a sanity check to ensure the base token is USDC @@ -163,7 +188,7 @@ abstract contract AbstractCCTPIntegrator is // Extract address from bytes32 (CCTP stores addresses as right-padded bytes32) address senderAddress = address(uint160(uint256(sender))); - require(senderAddress == destinationStrategy, "Unknown Sender"); + require(senderAddress == peerStrategy, "Unknown Sender"); _onMessageReceived(messageBody); @@ -190,9 +215,9 @@ abstract contract AbstractCCTPIntegrator is cctpTokenMessenger.depositForBurnWithHook( tokenAmount, peerDomainID, - bytes32(uint256(uint160(destinationStrategy))), + bytes32(uint256(uint160(peerStrategy))), address(baseToken), - bytes32(uint256(uint160(destinationStrategy))), + bytes32(uint256(uint160(peerStrategy))), maxFee, minFinalityThreshold, hookData @@ -206,7 +231,7 @@ abstract contract AbstractCCTPIntegrator is { // uint32 bytes 0 to 4 is Origin message version // uint32 bytes 4 to 8 is Message type - return message.extractSlice(0, 4).decodeUint32(); + return message.extractUint32(0); } function _getMessageType(bytes memory message) @@ -216,7 +241,7 @@ abstract contract AbstractCCTPIntegrator is { // uint32 bytes 0 to 4 is Origin message version // uint32 bytes 4 to 8 is Message type - return message.extractSlice(4, 8).decodeUint32(); + return message.extractUint32(4); } function _verifyMessageVersionAndType( @@ -245,8 +270,8 @@ abstract contract AbstractCCTPIntegrator is function _sendMessage(bytes memory message) internal virtual { cctpMessageTransmitter.sendMessage( peerDomainID, - bytes32(uint256(uint160(destinationStrategy))), - bytes32(uint256(uint160(destinationStrategy))), + bytes32(uint256(uint160(peerStrategy))), + bytes32(uint256(uint160(peerStrategy))), minFinalityThreshold, message ); @@ -290,4 +315,119 @@ abstract contract AbstractCCTPIntegrator is return nonce; } + + function _decodeMessageHeader(bytes memory message) + internal + pure + returns ( + uint32 version, + uint32 sourceDomainID, + address sender, + address recipient, + bytes memory messageBody + ) + { + version = message.extractUint32(VERSION_INDEX); + sourceDomainID = message.extractUint32(SOURCE_DOMAIN_INDEX); + // Address of MessageTransmitterV2 caller on source domain + sender = message.extractAddress(SENDER_INDEX); + // Address to handle message body on destination domain + recipient = message.extractAddress(RECIPIENT_INDEX); + messageBody = message.extractSlice(MESSAGE_BODY_INDEX, message.length); + } + + function relay(bytes memory message, bytes memory attestation) external { + ( + uint32 version, + uint32 sourceDomainID, + address sender, + address recipient, + bytes memory messageBody + ) = _decodeMessageHeader(message); + + // Ensure that it's a CCTP message + require( + version == CCTP_MESSAGE_VERSION, + "Invalid CCTP message version" + ); + + // Ensure that the source domain is the peer domain + require(sourceDomainID == peerDomainID, "Unknown Source Domain"); + + // Ensure message body version + version = messageBody.extractUint32(BURN_MESSAGE_V2_VERSION_INDEX); + + // 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 + require( + version == 1 && + messageBody.length >= BURN_MESSAGE_V2_HOOK_DATA_INDEX, + "Invalid burn message" + ); + + // Address of caller of depositForBurn (or depositForBurnWithCaller) on source domain + sender = messageBody.extractAddress( + BURN_MESSAGE_V2_MESSAGE_SENDER_INDEX + ); + + recipient = messageBody.extractAddress( + BURN_MESSAGE_V2_RECIPIENT_INDEX + ); + } else { + // We handle only Burn message or our custom messagee + require( + version == ORIGIN_MESSAGE_VERSION, + "Unsupported message version" + ); + } + + require(sender == recipient, "Sender and recipient must be the same"); + require(sender == peerStrategy, "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 + ); + require(relaySuccess, "Receive message failed"); + + if (isBurnMessageV1) { + bytes memory hookData = messageBody.extractSlice( + BURN_MESSAGE_V2_HOOK_DATA_INDEX, + messageBody.length + ); + + uint256 tokenAmount = messageBody.extractUint256( + BURN_MESSAGE_V2_AMOUNT_INDEX + ); + + uint256 feeExecuted = messageBody.extractUint256( + BURN_MESSAGE_V2_FEE_EXECUTED_INDEX + ); + + _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; + + /** + * @dev Called when the message is received + * @param payload The payload of the message + */ + function _onMessageReceived(bytes memory payload) internal virtual; } diff --git a/contracts/contracts/strategies/crosschain/AbstractCCTPMessageRelayer.sol b/contracts/contracts/strategies/crosschain/AbstractCCTPMessageRelayer.sol deleted file mode 100644 index 4fc48d233b..0000000000 --- a/contracts/contracts/strategies/crosschain/AbstractCCTPMessageRelayer.sol +++ /dev/null @@ -1,189 +0,0 @@ -// SPDX-License-Identifier: BUSL-1.1 -pragma solidity ^0.8.0; - -import { ICCTPTokenMessenger, ICCTPMessageTransmitter } from "../../interfaces/cctp/ICCTP.sol"; -import { BytesHelper } from "../../utils/BytesHelper.sol"; - -abstract contract AbstractCCTPMessageRelayer { - using BytesHelper for bytes; - - // CCTP Message Header fields - // Ref: https://developers.circle.com/cctp/technical-guide#message-header - uint8 private constant VERSION_INDEX = 0; - uint8 private constant SOURCE_DOMAIN_INDEX = 4; - uint8 private constant SENDER_INDEX = 44; - uint8 private constant RECIPIENT_INDEX = 76; - uint8 private constant MESSAGE_BODY_INDEX = 148; - - // 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; - - uint32 public constant CCTP_MESSAGE_VERSION = 1; - uint32 public 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; - - // Domain ID of the chain from which messages are accepted - uint32 public immutable peerDomainID; - - constructor( - address _cctpMessageTransmitter, - address _cctpTokenMessenger, - uint32 _peerDomainID - ) { - cctpMessageTransmitter = ICCTPMessageTransmitter( - _cctpMessageTransmitter - ); - cctpTokenMessenger = ICCTPTokenMessenger(_cctpTokenMessenger); - peerDomainID = _peerDomainID; - } - - 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 { - ( - uint32 version, - uint32 sourceDomainID, - address sender, - address recipient, - bytes memory messageBody - ) = _decodeMessageHeader(message); - - // Ensure that it's a CCTP message - require( - version == CCTP_MESSAGE_VERSION, - "Invalid CCTP message version" - ); - - // Ensure that the source domain is the peer domain - require(sourceDomainID == peerDomainID, "Unknown Source Domain"); - - // Ensure message body version - bytes memory bodyVersionSlice = messageBody.extractSlice( - BURN_MESSAGE_V2_VERSION_INDEX, - BURN_MESSAGE_V2_VERSION_INDEX + 4 - ); - version = bodyVersionSlice.decodeUint32(); - - // 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 - 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)); - - bytes memory recipientSlice = messageBody.extractSlice( - BURN_MESSAGE_V2_RECIPIENT_INDEX, - BURN_MESSAGE_V2_RECIPIENT_INDEX + 32 - ); - - recipient = abi.decode(recipientSlice, (address)); - } else { - // We handle only Burn message or our custom messagee - require( - version == ORIGIN_MESSAGE_VERSION, - "Unsupported message version" - ); - } - - 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 - ); - require(relaySuccess, "Receive message failed"); - - if (isBurnMessageV1) { - bytes memory hookData = messageBody.extractSlice( - BURN_MESSAGE_V2_HOOK_DATA_INDEX, - messageBody.length - ); - - bytes memory amountSlice = messageBody.extractSlice( - BURN_MESSAGE_V2_AMOUNT_INDEX, - BURN_MESSAGE_V2_AMOUNT_INDEX + 32 - ); - uint256 tokenAmount = abi.decode(amountSlice, (uint256)); - - bytes memory feeSlice = messageBody.extractSlice( - BURN_MESSAGE_V2_FEE_EXECUTED_INDEX, - BURN_MESSAGE_V2_FEE_EXECUTED_INDEX + 32 - ); - uint256 feeExecuted = abi.decode(feeSlice, (uint256)); - - _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; - - /** - * @dev Called when the message is received - * @param payload The payload of the message - */ - function _onMessageReceived(bytes memory payload) internal virtual; -} diff --git a/contracts/contracts/utils/BytesHelper.sol b/contracts/contracts/utils/BytesHelper.sol index 29906c2547..84dce7a6d9 100644 --- a/contracts/contracts/utils/BytesHelper.sol +++ b/contracts/contracts/utils/BytesHelper.sol @@ -1,6 +1,11 @@ // SPDX-License-Identifier: BUSL-1.1 pragma solidity ^0.8.0; +uint256 constant UINT32_LENGTH = 4; +uint256 constant UINT64_LENGTH = 8; +uint256 constant UINT256_LENGTH = 32; +uint256 constant ADDRESS_LENGTH = 32; + library BytesHelper { /** * @dev Extract a slice from bytes memory @@ -32,4 +37,39 @@ library BytesHelper { require(data.length == 4, "Invalid data length"); return uint32(uint256(bytes32(data)) >> 224); } + + function extractUint32(bytes memory data, uint256 start) + internal + pure + returns (uint32) + { + return decodeUint32(extractSlice(data, start, start + UINT32_LENGTH)); + } + + function decodeAddress(bytes memory data) internal pure returns (address) { + // We expect the data to be padded with 0s, so length is 32 not 20 + require(data.length == 32, "Invalid data length"); + return abi.decode(data, (address)); + } + + function extractAddress(bytes memory data, uint256 start) + internal + pure + returns (address) + { + return decodeAddress(extractSlice(data, start, start + ADDRESS_LENGTH)); + } + + function decodeUint256(bytes memory data) internal pure returns (uint256) { + require(data.length == 32, "Invalid data length"); + return abi.decode(data, (uint256)); + } + + function extractUint256(bytes memory data, uint256 start) + internal + pure + returns (uint256) + { + return decodeUint256(extractSlice(data, start, start + UINT256_LENGTH)); + } } From 7507994f612474270e70823215fcecef073a852a Mon Sep 17 00:00:00 2001 From: Shahul Hameed <10547529+shahthepro@users.noreply.github.com> Date: Fri, 19 Dec 2025 19:21:46 +0400 Subject: [PATCH 027/101] decode payloads in fork tests --- .../crosschain/CrossChainMasterStrategy.sol | 6 +- ...chain-master-strategy.mainnet.fork-test.js | 140 +++++++++++++++++- 2 files changed, 136 insertions(+), 10 deletions(-) diff --git a/contracts/contracts/strategies/crosschain/CrossChainMasterStrategy.sol b/contracts/contracts/strategies/crosschain/CrossChainMasterStrategy.sol index b55cd16320..9aa1230ddc 100644 --- a/contracts/contracts/strategies/crosschain/CrossChainMasterStrategy.sol +++ b/contracts/contracts/strategies/crosschain/CrossChainMasterStrategy.sol @@ -98,8 +98,8 @@ contract CrossChainMasterStrategy is * @dev Remove all assets from platform and send them to Vault contract. */ function withdrawAll() external override onlyVaultOrGovernor nonReentrant { - uint256 balance = IERC20(baseToken).balanceOf(address(this)); - _withdraw(baseToken, vaultAddress, balance); + // Withdraw everything in Remote strategy + _withdraw(baseToken, vaultAddress, remoteStrategyBalance); } /** @@ -108,7 +108,7 @@ contract CrossChainMasterStrategy is * @return balance Total value of the asset in the platform */ function checkBalance(address _asset) - external + public view override returns (uint256 balance) diff --git a/contracts/test/strategies/crosschain/crosschain-master-strategy.mainnet.fork-test.js b/contracts/test/strategies/crosschain/crosschain-master-strategy.mainnet.fork-test.js index 329f76f28f..b58788a2c7 100644 --- a/contracts/test/strategies/crosschain/crosschain-master-strategy.mainnet.fork-test.js +++ b/contracts/test/strategies/crosschain/crosschain-master-strategy.mainnet.fork-test.js @@ -5,13 +5,13 @@ const { createFixtureLoader, crossChainFixture } = require("../../_fixture"); const { impersonateAndFund } = require("../../../utils/signers"); // const { formatUnits } = require("ethers/lib/utils"); const addresses = require("../../../utils/addresses"); - const loadFixture = createFixtureLoader(crossChainFixture); +const { setStorageAt } = require("@nomicfoundation/hardhat-network-helpers"); const DEPOSIT_FOR_BURN_EVENT_TOPIC = "0x0c8c1cbdc5190613ebd485511d4e2812cfa45eecb79d845893331fedad5130a5"; -// const MESSAGE_SENT_EVENT_TOPIC = -// "0x8c5261668696ce22758910d05bab8f186d6eb247ceac2af2e82c7dc17669b036"; +const MESSAGE_SENT_EVENT_TOPIC = + "0x8c5261668696ce22758910d05bab8f186d6eb247ceac2af2e82c7dc17669b036"; // const ORIGIN_MESSAGE_VERSION_HEX = "0x000003f2"; // 1010 @@ -75,10 +75,68 @@ describe("ForkTest: CrossChainMasterStrategy", function () { }; }; + const decodeMessageSentEvent = (event) => { + const evData = event.data.slice(130); // ignore first two slots along with 0x prefix + + const version = ethers.BigNumber.from(`0x${evData.slice(0, 8)}`); + const sourceDomain = ethers.BigNumber.from(`0x${evData.slice(8, 16)}`); + const desinationDomain = ethers.BigNumber.from(`0x${evData.slice(16, 24)}`); + // Ignore empty nonce from 24 to 88 + const [sender, recipient, destinationCaller] = + ethers.utils.defaultAbiCoder.decode( + ["address", "address", "address"], + `0x${evData.slice(88, 280)}` + ); + const minFinalityThreshold = ethers.BigNumber.from( + `0x${evData.slice(280, 288)}` + ); + // Ignore empty threshold from 288 to 296 + const payload = `0x${evData.slice(296, evData.length - 8)}`; + + return { + version, + sourceDomain, + desinationDomain, + sender, + recipient, + destinationCaller, + minFinalityThreshold, + payload, + }; + }; + + const decodeDepositOrWithdrawMessage = (message) => { + message = message.slice(2); // Ignore 0x prefix + + const originMessageVersion = ethers.BigNumber.from( + `0x${message.slice(0, 8)}` + ); + const messageType = ethers.BigNumber.from(`0x${message.slice(8, 16)}`); + expect(originMessageVersion).to.eq(1010); + + const [nonce, amount] = ethers.utils.defaultAbiCoder.decode( + ["uint64", "uint256"], + `0x${message.slice(16)}` + ); + + return { + messageType, + nonce, + amount, + }; + }; + it("Should initiate bridging of deposited USDC", async function () { const { matt, crossChainMasterStrategy, usdc } = fixture; - // const govAddr = await crossChainMasterStrategy.governor(); - // const governor = await impersonateAndFund(govAddr); + + if (await crossChainMasterStrategy.isTransferPending()) { + // Skip if there's a pending transfer + console.log( + "Skipping deposit fork test because there's a pending transfer" + ); + return; + } + const vaultAddr = await crossChainMasterStrategy.vaultAddress(); const impersonatedVault = await impersonateAndFund(vaultAddr); @@ -143,8 +201,76 @@ describe("ForkTest: CrossChainMasterStrategy", function () { usdc.address.toLowerCase() ); - // TODO: Check Hook Data - // expect(burnEventData.hookData).to.eq(""); + // Decode and verify payload + const { messageType, nonce, amount } = decodeDepositOrWithdrawMessage( + burnEventData.hookData + ); + expect(messageType).to.eq(1); + expect(nonce).to.eq(1); + expect(amount).to.eq(usdcUnits("1000")); + }); + + it("Should request withdrawal", async function () { + const { crossChainMasterStrategy, usdc } = fixture; + + if (await crossChainMasterStrategy.isTransferPending()) { + // Skip if there's a pending transfer + console.log( + "Skipping deposit fork test because there's a pending transfer" + ); + return; + } + + const vaultAddr = await crossChainMasterStrategy.vaultAddress(); + const impersonatedVault = await impersonateAndFund(vaultAddr); + + // set an arbitrary remote strategy balance + const remoteStrategyBalanceSlot = 209; // Slot 209 + await setStorageAt( + crossChainMasterStrategy.address, + `0x${remoteStrategyBalanceSlot.toString(16)}`, + usdcUnits("1000").toHexString() + ); + + const tx = await crossChainMasterStrategy + .connect(impersonatedVault) + .withdraw(vaultAddr, usdc.address, usdcUnits("1000")); + const receipt = await tx.wait(); + const messageSentEvent = receipt.events.find((e) => + e.topics.includes(MESSAGE_SENT_EVENT_TOPIC) + ); + + const { + version, + sourceDomain, + desinationDomain, + sender, + recipient, + destinationCaller, + minFinalityThreshold, + payload, + } = decodeMessageSentEvent(messageSentEvent); + + expect(version).to.eq(1); + expect(sourceDomain).to.eq(0); + expect(desinationDomain).to.eq(6); + expect(sender.toLowerCase()).to.eq( + crossChainMasterStrategy.address.toLowerCase() + ); + expect(recipient.toLowerCase()).to.eq( + crossChainMasterStrategy.address.toLowerCase() + ); + expect(destinationCaller.toLowerCase()).to.eq( + crossChainMasterStrategy.address.toLowerCase() + ); + expect(minFinalityThreshold).to.eq(2000); + + // Decode and verify payload + const { messageType, nonce, amount } = + decodeDepositOrWithdrawMessage(payload); + expect(messageType).to.eq(2); + expect(nonce).to.eq(1); + expect(amount).to.eq(usdcUnits("1000")); }); it.skip("Should handle attestation relay", async function () { From 7998ffa31f245802ac2bb10f2ded0450486f4df6 Mon Sep 17 00:00:00 2001 From: Shahul Hameed <10547529+shahthepro@users.noreply.github.com> Date: Fri, 19 Dec 2025 19:43:33 +0400 Subject: [PATCH 028/101] Add library for message handling --- .../crosschain/AbstractCCTP4626Strategy.sol | 112 --------------- .../crosschain/AbstractCCTPIntegrator.sol | 51 +------ .../crosschain/CrossChainMasterStrategy.sol | 27 ++-- .../crosschain/CrossChainRemoteStrategy.sol | 50 ++++--- .../crosschain/CrossChainStrategyHelper.sol | 134 ++++++++++++++++++ 5 files changed, 178 insertions(+), 196 deletions(-) delete mode 100644 contracts/contracts/strategies/crosschain/AbstractCCTP4626Strategy.sol create mode 100644 contracts/contracts/strategies/crosschain/CrossChainStrategyHelper.sol diff --git a/contracts/contracts/strategies/crosschain/AbstractCCTP4626Strategy.sol b/contracts/contracts/strategies/crosschain/AbstractCCTP4626Strategy.sol deleted file mode 100644 index 89b23c1dac..0000000000 --- a/contracts/contracts/strategies/crosschain/AbstractCCTP4626Strategy.sol +++ /dev/null @@ -1,112 +0,0 @@ -// SPDX-License-Identifier: BUSL-1.1 -pragma solidity ^0.8.0; - -/** - * @title AbstractCCTP4626Strategy - Abstract contract for CCTP morpho strategy - * @author Origin Protocol Inc - */ - -import { AbstractCCTPIntegrator } from "./AbstractCCTPIntegrator.sol"; - -abstract contract AbstractCCTP4626Strategy is AbstractCCTPIntegrator { - uint32 public constant DEPOSIT_MESSAGE = 1; - uint32 public constant WITHDRAW_MESSAGE = 2; - uint32 public constant BALANCE_CHECK_MESSAGE = 3; - - 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 _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 _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/AbstractCCTPIntegrator.sol b/contracts/contracts/strategies/crosschain/AbstractCCTPIntegrator.sol index e8cf7a72a9..625a915662 100644 --- a/contracts/contracts/strategies/crosschain/AbstractCCTPIntegrator.sol +++ b/contracts/contracts/strategies/crosschain/AbstractCCTPIntegrator.sol @@ -6,6 +6,7 @@ import { IERC20 } from "../../utils/InitializableAbstractStrategy.sol"; import { ICCTPTokenMessenger, ICCTPMessageTransmitter, IMessageHandlerV2 } from "../../interfaces/cctp/ICCTP.sol"; +import { CrossChainStrategyHelper } from "./CrossChainStrategyHelper.sol"; import { Governable } from "../../governance/Governable.sol"; import { BytesHelper } from "../../utils/BytesHelper.sol"; import "../../utils/Helpers.sol"; @@ -36,9 +37,6 @@ abstract contract AbstractCCTPIntegrator is Governable, IMessageHandlerV2 { event CCTPMinFinalityThresholdSet(uint32 minFinalityThreshold); event CCTPFeePremiumBpsSet(uint32 feePremiumBps); - uint32 public constant CCTP_MESSAGE_VERSION = 1; - uint32 public constant ORIGIN_MESSAGE_VERSION = 1010; - // CCTP contracts // This implementation assumes that remote and local chains have these contracts // deployed on the same addresses. @@ -224,49 +222,6 @@ abstract contract AbstractCCTPIntegrator is Governable, IMessageHandlerV2 { ); } - function _getMessageVersion(bytes memory message) - internal - virtual - returns (uint32) - { - // uint32 bytes 0 to 4 is Origin message version - // uint32 bytes 4 to 8 is Message type - return message.extractUint32(0); - } - - function _getMessageType(bytes memory message) - internal - virtual - returns (uint32) - { - // uint32 bytes 0 to 4 is Origin message version - // uint32 bytes 4 to 8 is Message type - return message.extractUint32(4); - } - - function _verifyMessageVersionAndType( - bytes memory _message, - uint32 _version, - uint32 _type - ) internal virtual { - require( - _getMessageVersion(_message) == _version, - "Invalid Origin Message Version" - ); - require(_getMessageType(_message) == _type, "Invalid Message type"); - } - - function _getMessagePayload(bytes memory message) - internal - virtual - returns (bytes memory) - { - // uint32 bytes 0 to 4 is Origin message version - // uint32 bytes 4 to 8 is Message type - // Payload starts at byte 8 - return message.extractSlice(8, message.length); - } - function _sendMessage(bytes memory message) internal virtual { cctpMessageTransmitter.sendMessage( peerDomainID, @@ -347,7 +302,7 @@ abstract contract AbstractCCTPIntegrator is Governable, IMessageHandlerV2 { // Ensure that it's a CCTP message require( - version == CCTP_MESSAGE_VERSION, + version == CrossChainStrategyHelper.CCTP_MESSAGE_VERSION, "Invalid CCTP message version" ); @@ -379,7 +334,7 @@ abstract contract AbstractCCTPIntegrator is Governable, IMessageHandlerV2 { } else { // We handle only Burn message or our custom messagee require( - version == ORIGIN_MESSAGE_VERSION, + version == CrossChainStrategyHelper.ORIGIN_MESSAGE_VERSION, "Unsupported message version" ); } diff --git a/contracts/contracts/strategies/crosschain/CrossChainMasterStrategy.sol b/contracts/contracts/strategies/crosschain/CrossChainMasterStrategy.sol index 9aa1230ddc..f0e4443b62 100644 --- a/contracts/contracts/strategies/crosschain/CrossChainMasterStrategy.sol +++ b/contracts/contracts/strategies/crosschain/CrossChainMasterStrategy.sol @@ -11,14 +11,15 @@ pragma solidity ^0.8.0; import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import { IERC20, InitializableAbstractStrategy } from "../../utils/InitializableAbstractStrategy.sol"; -import { AbstractCCTP4626Strategy } from "./AbstractCCTP4626Strategy.sol"; -import { BytesHelper } from "../../utils/BytesHelper.sol"; +import { AbstractCCTPIntegrator } from "./AbstractCCTPIntegrator.sol"; +import { CrossChainStrategyHelper } from "./CrossChainStrategyHelper.sol"; contract CrossChainMasterStrategy is - InitializableAbstractStrategy, - AbstractCCTP4626Strategy + AbstractCCTPIntegrator, + InitializableAbstractStrategy { using SafeERC20 for IERC20; + using CrossChainStrategyHelper for bytes; // Remote strategy balance uint256 public remoteStrategyBalance; @@ -43,7 +44,7 @@ contract CrossChainMasterStrategy is CCTPIntegrationConfig memory _cctpConfig ) InitializableAbstractStrategy(_stratConfig) - AbstractCCTP4626Strategy(_cctpConfig) + AbstractCCTPIntegrator(_cctpConfig) {} // /** @@ -162,8 +163,8 @@ contract CrossChainMasterStrategy is {} function _onMessageReceived(bytes memory payload) internal override { - uint32 messageType = _getMessageType(payload); - if (messageType == BALANCE_CHECK_MESSAGE) { + uint32 messageType = payload.getMessageType(); + if (messageType == CrossChainStrategyHelper.BALANCE_CHECK_MESSAGE) { // Received when Remote strategy checks the balance _processBalanceCheckMessage(payload); } else { @@ -227,7 +228,10 @@ contract CrossChainMasterStrategy is pendingAmount = depositAmount; // Send deposit message with payload - bytes memory message = _encodeDepositMessage(nonce, depositAmount); + bytes memory message = CrossChainStrategyHelper.encodeDepositMessage( + nonce, + depositAmount + ); _sendTokens(depositAmount, message); emit Deposit(_asset, _asset, depositAmount); } @@ -252,7 +256,10 @@ contract CrossChainMasterStrategy is emit Withdrawal(baseToken, baseToken, _amount); // Send withdrawal message with payload - bytes memory message = _encodeWithdrawMessage(nonce, _amount); + bytes memory message = CrossChainStrategyHelper.encodeWithdrawMessage( + nonce, + _amount + ); _sendMessage(message); } @@ -267,7 +274,7 @@ contract CrossChainMasterStrategy is internal virtual { - (uint64 nonce, uint256 balance) = _decodeBalanceCheckMessage(message); + (uint64 nonce, uint256 balance) = message.decodeBalanceCheckMessage(); uint64 _lastCachedNonce = lastTransferNonce; diff --git a/contracts/contracts/strategies/crosschain/CrossChainRemoteStrategy.sol b/contracts/contracts/strategies/crosschain/CrossChainRemoteStrategy.sol index 653671cf1a..355cb1c36a 100644 --- a/contracts/contracts/strategies/crosschain/CrossChainRemoteStrategy.sol +++ b/contracts/contracts/strategies/crosschain/CrossChainRemoteStrategy.sol @@ -13,22 +13,24 @@ 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 { AbstractCCTP4626Strategy } from "./AbstractCCTP4626Strategy.sol"; +import { AbstractCCTPIntegrator } from "./AbstractCCTPIntegrator.sol"; +import { CrossChainStrategyHelper } from "./CrossChainStrategyHelper.sol"; contract CrossChainRemoteStrategy is - AbstractCCTP4626Strategy, + AbstractCCTPIntegrator, Generalized4626Strategy { + using SafeERC20 for IERC20; + using CrossChainStrategyHelper for bytes; + event DepositFailed(string reason); event WithdrawFailed(string reason); - using SafeERC20 for IERC20; - constructor( BaseStrategyConfig memory _baseConfig, CCTPIntegrationConfig memory _cctpConfig ) - AbstractCCTP4626Strategy(_cctpConfig) + AbstractCCTPIntegrator(_cctpConfig) Generalized4626Strategy(_baseConfig, _cctpConfig.baseToken) {} @@ -62,13 +64,13 @@ contract CrossChainRemoteStrategy is } function _onMessageReceived(bytes memory payload) internal override { - uint32 messageType = _getMessageType(payload); - if (messageType == DEPOSIT_MESSAGE) { + uint32 messageType = payload.getMessageType(); + if (messageType == CrossChainStrategyHelper.DEPOSIT_MESSAGE) { // // Received when Master strategy sends tokens to the remote strategy // Do nothing because we receive acknowledgement with token transfer, so _onTokenReceived will handle it // TODO: Should _onTokenReceived always call _onMessageReceived? // _processDepositAckMessage(payload); - } else if (messageType == WITHDRAW_MESSAGE) { + } else if (messageType == CrossChainStrategyHelper.WITHDRAW_MESSAGE) { // Received when Master strategy requests a withdrawal _processWithdrawMessage(payload); } else { @@ -85,7 +87,7 @@ contract CrossChainRemoteStrategy is ) internal virtual { // TODO: no need to communicate the deposit amount if we deposit everything // solhint-disable-next-line no-unused-vars - (uint64 nonce, uint256 depositAmount) = _decodeDepositMessage(payload); + (uint64 nonce, uint256 depositAmount) = payload.decodeDepositMessage(); // Replay protection require(!isNonceProcessed(nonce), "Nonce already processed"); @@ -99,10 +101,8 @@ contract CrossChainRemoteStrategy is _deposit(baseToken, balance); uint256 balanceAfter = checkBalance(baseToken); - bytes memory message = _encodeBalanceCheckMessage( - lastTransferNonce, - balanceAfter - ); + bytes memory message = CrossChainStrategyHelper + .encodeBalanceCheckMessage(lastTransferNonce, balanceAfter); _sendMessage(message); } @@ -136,9 +136,8 @@ contract CrossChainRemoteStrategy is } function _processWithdrawMessage(bytes memory payload) internal virtual { - (uint64 nonce, uint256 withdrawAmount) = _decodeWithdrawMessage( - payload - ); + (uint64 nonce, uint256 withdrawAmount) = payload + .decodeWithdrawMessage(); // Replay protection require(!isNonceProcessed(nonce), "Nonce already processed"); @@ -149,10 +148,8 @@ contract CrossChainRemoteStrategy is // Check balance after withdrawal uint256 balanceAfter = checkBalance(baseToken); - bytes memory message = _encodeBalanceCheckMessage( - lastTransferNonce, - balanceAfter - ); + bytes memory message = CrossChainStrategyHelper + .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. @@ -213,9 +210,12 @@ contract CrossChainRemoteStrategy is uint256 feeExecuted, bytes memory payload ) internal override { - uint32 messageType = _getMessageType(payload); + uint32 messageType = payload.getMessageType(); - require(messageType == DEPOSIT_MESSAGE, "Invalid message type"); + require( + messageType == CrossChainStrategyHelper.DEPOSIT_MESSAGE, + "Invalid message type" + ); _processDepositMessage(tokenAmount, feeExecuted, payload); } @@ -223,10 +223,8 @@ contract CrossChainRemoteStrategy is function sendBalanceUpdate() external virtual { // TODO: Add permissioning uint256 balance = checkBalance(baseToken); - bytes memory message = _encodeBalanceCheckMessage( - lastTransferNonce, - balance - ); + bytes memory message = CrossChainStrategyHelper + .encodeBalanceCheckMessage(lastTransferNonce, balance); _sendMessage(message); } diff --git a/contracts/contracts/strategies/crosschain/CrossChainStrategyHelper.sol b/contracts/contracts/strategies/crosschain/CrossChainStrategyHelper.sol new file mode 100644 index 0000000000..c65fbbd6b2 --- /dev/null +++ b/contracts/contracts/strategies/crosschain/CrossChainStrategyHelper.sol @@ -0,0 +1,134 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +import { BytesHelper } from "../../utils/BytesHelper.sol"; + +library CrossChainStrategyHelper { + using BytesHelper for bytes; + + uint32 public constant DEPOSIT_MESSAGE = 1; + uint32 public constant WITHDRAW_MESSAGE = 2; + uint32 public constant BALANCE_CHECK_MESSAGE = 3; + + uint32 public constant CCTP_MESSAGE_VERSION = 1; + uint32 public constant ORIGIN_MESSAGE_VERSION = 1010; + + function getMessageVersion(bytes memory message) + internal + view + returns (uint32) + { + // uint32 bytes 0 to 4 is Origin message version + // uint32 bytes 4 to 8 is Message type + return message.extractUint32(0); + } + + function getMessageType(bytes memory message) + internal + view + returns (uint32) + { + // uint32 bytes 0 to 4 is Origin message version + // uint32 bytes 4 to 8 is Message type + return message.extractUint32(4); + } + + function verifyMessageVersionAndType(bytes memory _message, uint32 _type) + internal + { + require( + getMessageVersion(_message) == ORIGIN_MESSAGE_VERSION, + "Invalid Origin Message Version" + ); + require(getMessageType(_message) == _type, "Invalid Message type"); + } + + function getMessagePayload(bytes memory message) + internal + view + returns (bytes memory) + { + // uint32 bytes 0 to 4 is Origin message version + // uint32 bytes 4 to 8 is Message type + // Payload starts at byte 8 + return message.extractSlice(8, message.length); + } + + function encodeDepositMessage(uint64 nonce, uint256 depositAmount) + internal + view + returns (bytes memory) + { + return + abi.encodePacked( + ORIGIN_MESSAGE_VERSION, + DEPOSIT_MESSAGE, + abi.encode(nonce, depositAmount) + ); + } + + function decodeDepositMessage(bytes memory message) + internal + returns (uint64, uint256) + { + verifyMessageVersionAndType(message, DEPOSIT_MESSAGE); + + (uint64 nonce, uint256 depositAmount) = abi.decode( + getMessagePayload(message), + (uint64, uint256) + ); + return (nonce, depositAmount); + } + + function encodeWithdrawMessage(uint64 nonce, uint256 withdrawAmount) + internal + view + returns (bytes memory) + { + return + abi.encodePacked( + ORIGIN_MESSAGE_VERSION, + WITHDRAW_MESSAGE, + abi.encode(nonce, withdrawAmount) + ); + } + + function decodeWithdrawMessage(bytes memory message) + internal + returns (uint64, uint256) + { + verifyMessageVersionAndType(message, WITHDRAW_MESSAGE); + + (uint64 nonce, uint256 withdrawAmount) = abi.decode( + getMessagePayload(message), + (uint64, uint256) + ); + return (nonce, withdrawAmount); + } + + function encodeBalanceCheckMessage(uint64 nonce, uint256 balance) + internal + view + returns (bytes memory) + { + return + abi.encodePacked( + ORIGIN_MESSAGE_VERSION, + BALANCE_CHECK_MESSAGE, + abi.encode(nonce, balance) + ); + } + + function decodeBalanceCheckMessage(bytes memory message) + internal + returns (uint64, uint256) + { + verifyMessageVersionAndType(message, BALANCE_CHECK_MESSAGE); + + (uint64 nonce, uint256 balance) = abi.decode( + getMessagePayload(message), + (uint64, uint256) + ); + return (nonce, balance); + } +} From e34706a4e1271bfb10010b4631fc41a20b19a0af Mon Sep 17 00:00:00 2001 From: Shahul Hameed <10547529+shahthepro@users.noreply.github.com> Date: Fri, 19 Dec 2025 22:05:08 +0400 Subject: [PATCH 029/101] More changes --- .../crosschain/AbstractCCTPIntegrator.sol | 30 ++++++-- .../crosschain/CrossChainMasterStrategy.sol | 15 ++++ .../crosschain/CrossChainRemoteStrategy.sol | 70 ++++++++++++++----- 3 files changed, 94 insertions(+), 21 deletions(-) diff --git a/contracts/contracts/strategies/crosschain/AbstractCCTPIntegrator.sol b/contracts/contracts/strategies/crosschain/AbstractCCTPIntegrator.sol index 625a915662..d84842b872 100644 --- a/contracts/contracts/strategies/crosschain/AbstractCCTPIntegrator.sol +++ b/contracts/contracts/strategies/crosschain/AbstractCCTPIntegrator.sol @@ -36,6 +36,7 @@ abstract contract AbstractCCTPIntegrator is Governable, IMessageHandlerV2 { event CCTPMinFinalityThresholdSet(uint32 minFinalityThreshold); event CCTPFeePremiumBpsSet(uint32 feePremiumBps); + event OperatorChanged(address operator); // CCTP contracts // This implementation assumes that remote and local chains have these contracts @@ -63,6 +64,8 @@ abstract contract AbstractCCTPIntegrator is Governable, IMessageHandlerV2 { mapping(uint64 => bool) private nonceProcessed; + address public operator; + // For future use uint256[50] private __gap; @@ -74,6 +77,11 @@ abstract contract AbstractCCTPIntegrator is Governable, IMessageHandlerV2 { _; } + modifier onlyOperator() { + require(msg.sender == operator, "Caller is not the Operator"); + _; + } + struct CCTPIntegrationConfig { address cctpTokenMessenger; address cctpMessageTransmitter; @@ -103,13 +111,24 @@ abstract contract AbstractCCTPIntegrator is Governable, IMessageHandlerV2 { ); } - function _initialize(uint32 _minFinalityThreshold, uint32 _feePremiumBps) - internal - { + function _initialize( + address _operator, + uint32 _minFinalityThreshold, + uint32 _feePremiumBps + ) internal { + _setOperator(_operator); _setMinFinalityThreshold(_minFinalityThreshold); _setFeePremiumBps(_feePremiumBps); } + function setOperator(address _operator) external onlyGovernor { + _setOperator(_operator); + } + function _setOperator(address _operator) internal { + operator = _operator; + emit OperatorChanged(_operator); + } + function setMinFinalityThreshold(uint32 _minFinalityThreshold) external onlyGovernor @@ -291,7 +310,10 @@ abstract contract AbstractCCTPIntegrator is Governable, IMessageHandlerV2 { messageBody = message.extractSlice(MESSAGE_BODY_INDEX, message.length); } - function relay(bytes memory message, bytes memory attestation) external { + function relay(bytes memory message, bytes memory attestation) + external + onlyOperator + { ( uint32 version, uint32 sourceDomainID, diff --git a/contracts/contracts/strategies/crosschain/CrossChainMasterStrategy.sol b/contracts/contracts/strategies/crosschain/CrossChainMasterStrategy.sol index f0e4443b62..e2a8bc5d4b 100644 --- a/contracts/contracts/strategies/crosschain/CrossChainMasterStrategy.sol +++ b/contracts/contracts/strategies/crosschain/CrossChainMasterStrategy.sol @@ -47,6 +47,21 @@ contract CrossChainMasterStrategy is AbstractCCTPIntegrator(_cctpConfig) {} + + function initialize(address _operator, uint32 _minFinalityThreshold, uint32 _feePremiumBps) external virtual onlyGovernor initializer { + _initialize(_operator, _minFinalityThreshold, _feePremiumBps); + + address[] memory rewardTokens = new address[](0); + address[] memory assets = new address[](0); + address[] memory pTokens = new address[](0); + + InitializableAbstractStrategy._initialize( + rewardTokens, + assets, + pTokens + ); + } + // /** // * @dev Returns the address of the Remote part of the strategy on L2 // */ diff --git a/contracts/contracts/strategies/crosschain/CrossChainRemoteStrategy.sol b/contracts/contracts/strategies/crosschain/CrossChainRemoteStrategy.sol index 355cb1c36a..50be594f45 100644 --- a/contracts/contracts/strategies/crosschain/CrossChainRemoteStrategy.sol +++ b/contracts/contracts/strategies/crosschain/CrossChainRemoteStrategy.sol @@ -15,6 +15,7 @@ import { IERC4626 } from "../../../lib/openzeppelin/interfaces/IERC4626.sol"; import { Generalized4626Strategy } from "../Generalized4626Strategy.sol"; import { AbstractCCTPIntegrator } from "./AbstractCCTPIntegrator.sol"; import { CrossChainStrategyHelper } from "./CrossChainStrategyHelper.sol"; +import { InitializableAbstractStrategy } from "../../utils/InitializableAbstractStrategy.sol"; contract CrossChainRemoteStrategy is AbstractCCTPIntegrator, @@ -25,6 +26,9 @@ contract CrossChainRemoteStrategy is event DepositFailed(string reason); event WithdrawFailed(string reason); + event StrategistUpdated(address _address); + + address public strategistAddr; constructor( BaseStrategyConfig memory _baseConfig, @@ -32,35 +36,67 @@ contract CrossChainRemoteStrategy is ) AbstractCCTPIntegrator(_cctpConfig) Generalized4626Strategy(_baseConfig, _cctpConfig.baseToken) - {} + { + // NOTE: Vault address must always be the proxy address + // so that IVault(vaultAddress).strategistAddr() + } + + function initialize(address _strategist, address _operator, uint32 _minFinalityThreshold, uint32 _feePremiumBps) external virtual onlyGovernor initializer { + _initialize(_operator, _minFinalityThreshold, _feePremiumBps); + _setStrategistAddr(_strategist); + + address[] memory rewardTokens = new address[](0); + address[] memory assets = new address[](1); + address[] memory pTokens = new address[](1); + + assets[0] = address(assetToken); + pTokens[0] = address(platformAddress); + + InitializableAbstractStrategy._initialize( + rewardTokens, + assets, + pTokens + ); + } + + /** + * @notice Set address of Strategist + * @param _address Address of Strategist + */ + function setStrategistAddr(address _address) external onlyGovernor { + _setStrategistAddr(_address); + } + function _setStrategistAddr(address _address) internal { + strategistAddr = _address; + emit StrategistUpdated(_address); + } // solhint-disable-next-line no-unused-vars function deposit(address _asset, uint256 _amount) external virtual override + onlyGovernorOrStrategist { - // TODO: implement this - revert("Not implemented"); + _deposit(_asset, _amount); } - function depositAll() external virtual override { - // TODO: implement this - revert("Not implemented"); + function depositAll() external virtual override onlyGovernorOrStrategist { + _deposit(baseToken, IERC20(baseToken).balanceOf(address(this))); } function withdraw( - address, - address, - uint256 - ) external virtual override { - // TODO: implement this - revert("Not implemented"); + address _recipient, + address _asset, + uint256 _amount + ) external virtual override onlyGovernorOrStrategist { + _withdraw(_recipient, _asset, _amount); } - function withdrawAll() external virtual override { - // TODO: implement this - revert("Not implemented"); + function withdrawAll() external virtual override onlyGovernorOrStrategist { + uint256 contractBalance = IERC20(baseToken).balanceOf(address(this)); + uint256 balance = checkBalance(baseToken) - contractBalance; + _withdraw(address(this), baseToken, balance); } function _onMessageReceived(bytes memory payload) internal override { @@ -174,7 +210,7 @@ contract CrossChainRemoteStrategy is uint256 _amount ) internal override { require(_amount > 0, "Must withdraw something"); - require(_recipient != address(0), "Must specify recipient"); + require(_recipient != address(this), "Invalid recipient"); require(_asset == address(assetToken), "Unexpected asset address"); // slither-disable-next-line unused-return @@ -184,7 +220,7 @@ contract CrossChainRemoteStrategy is try IERC4626(platformAddress).withdraw( _amount, - _recipient, + address(this), address(this) ) { From df59d53f1c9c3a74c6c8de78ce8a84734a3baba2 Mon Sep 17 00:00:00 2001 From: Shahul Hameed <10547529+shahthepro@users.noreply.github.com> Date: Sat, 20 Dec 2025 11:01:10 +0400 Subject: [PATCH 030/101] Add comments and prettify --- .../crosschain/AbstractCCTPIntegrator.sol | 1 + .../crosschain/CrossChainMasterStrategy.sol | 15 ++-- .../crosschain/CrossChainRemoteStrategy.sol | 8 +- .../crosschain/CrossChainStrategyHelper.sol | 73 +++++++++++++++++++ 4 files changed, 90 insertions(+), 7 deletions(-) diff --git a/contracts/contracts/strategies/crosschain/AbstractCCTPIntegrator.sol b/contracts/contracts/strategies/crosschain/AbstractCCTPIntegrator.sol index d84842b872..d8767915aa 100644 --- a/contracts/contracts/strategies/crosschain/AbstractCCTPIntegrator.sol +++ b/contracts/contracts/strategies/crosschain/AbstractCCTPIntegrator.sol @@ -124,6 +124,7 @@ abstract contract AbstractCCTPIntegrator is Governable, IMessageHandlerV2 { function setOperator(address _operator) external onlyGovernor { _setOperator(_operator); } + function _setOperator(address _operator) internal { operator = _operator; emit OperatorChanged(_operator); diff --git a/contracts/contracts/strategies/crosschain/CrossChainMasterStrategy.sol b/contracts/contracts/strategies/crosschain/CrossChainMasterStrategy.sol index e2a8bc5d4b..26c5127832 100644 --- a/contracts/contracts/strategies/crosschain/CrossChainMasterStrategy.sol +++ b/contracts/contracts/strategies/crosschain/CrossChainMasterStrategy.sol @@ -47,8 +47,11 @@ contract CrossChainMasterStrategy is AbstractCCTPIntegrator(_cctpConfig) {} - - function initialize(address _operator, uint32 _minFinalityThreshold, uint32 _feePremiumBps) external virtual onlyGovernor initializer { + function initialize( + address _operator, + uint32 _minFinalityThreshold, + uint32 _feePremiumBps + ) external virtual onlyGovernor initializer { _initialize(_operator, _minFinalityThreshold, _feePremiumBps); address[] memory rewardTokens = new address[](0); @@ -279,10 +282,10 @@ contract CrossChainMasterStrategy is } /** - * @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 + * @dev Process balance check: + * - Confirms a deposit to the remote strategy + * - Skips balance update if there's a pending withdrawal + * - Updates the remote strategy balance * @param message The message containing the nonce and balance */ function _processBalanceCheckMessage(bytes memory message) diff --git a/contracts/contracts/strategies/crosschain/CrossChainRemoteStrategy.sol b/contracts/contracts/strategies/crosschain/CrossChainRemoteStrategy.sol index 50be594f45..5a408e420f 100644 --- a/contracts/contracts/strategies/crosschain/CrossChainRemoteStrategy.sol +++ b/contracts/contracts/strategies/crosschain/CrossChainRemoteStrategy.sol @@ -41,7 +41,12 @@ contract CrossChainRemoteStrategy is // so that IVault(vaultAddress).strategistAddr() } - function initialize(address _strategist, address _operator, uint32 _minFinalityThreshold, uint32 _feePremiumBps) external virtual onlyGovernor initializer { + function initialize( + address _strategist, + address _operator, + uint32 _minFinalityThreshold, + uint32 _feePremiumBps + ) external virtual onlyGovernor initializer { _initialize(_operator, _minFinalityThreshold, _feePremiumBps); _setStrategistAddr(_strategist); @@ -66,6 +71,7 @@ contract CrossChainRemoteStrategy is function setStrategistAddr(address _address) external onlyGovernor { _setStrategistAddr(_address); } + function _setStrategistAddr(address _address) internal { strategistAddr = _address; emit StrategistUpdated(_address); diff --git a/contracts/contracts/strategies/crosschain/CrossChainStrategyHelper.sol b/contracts/contracts/strategies/crosschain/CrossChainStrategyHelper.sol index c65fbbd6b2..d1025a1d7a 100644 --- a/contracts/contracts/strategies/crosschain/CrossChainStrategyHelper.sol +++ b/contracts/contracts/strategies/crosschain/CrossChainStrategyHelper.sol @@ -1,6 +1,13 @@ // SPDX-License-Identifier: BUSL-1.1 pragma solidity ^0.8.0; +/** + * @title CrossChainStrategyHelper + * @author Origin Protocol Inc + * @dev This library is used to encode and decode the messages for the cross-chain strategy. + * It is used to ensure that the messages are valid and to get the message version and type. + */ + import { BytesHelper } from "../../utils/BytesHelper.sol"; library CrossChainStrategyHelper { @@ -13,6 +20,13 @@ library CrossChainStrategyHelper { uint32 public constant CCTP_MESSAGE_VERSION = 1; uint32 public constant ORIGIN_MESSAGE_VERSION = 1010; + /** + * @dev Get the message version from the message. + * It should always be 4 bytes long, + * starting from the 0th index. + * @param message The message to get the version from + * @return The message version + */ function getMessageVersion(bytes memory message) internal view @@ -23,6 +37,13 @@ library CrossChainStrategyHelper { return message.extractUint32(0); } + /** + * @dev Get the message type from the message. + * It should always be 4 bytes long, + * starting from the 4th index. + * @param message The message to get the type from + * @return The message type + */ function getMessageType(bytes memory message) internal view @@ -33,6 +54,13 @@ library CrossChainStrategyHelper { return message.extractUint32(4); } + /** + * @dev Verify the message version and type. + * The message version should be the same as the Origin message version, + * and the message type should be the same as the expected message type. + * @param _message The message to verify + * @param _type The expected message type + */ function verifyMessageVersionAndType(bytes memory _message, uint32 _type) internal { @@ -43,6 +71,12 @@ library CrossChainStrategyHelper { require(getMessageType(_message) == _type, "Invalid Message type"); } + /** + * @dev Get the message payload from the message. + * The payload starts at the 8th byte. + * @param message The message to get the payload from + * @return The message payload + */ function getMessagePayload(bytes memory message) internal view @@ -54,6 +88,13 @@ library CrossChainStrategyHelper { return message.extractSlice(8, message.length); } + /** + * @dev Encode the deposit message. + * The message version and type are always encoded in the message. + * @param nonce The nonce of the deposit + * @param depositAmount The amount of the deposit + * @return The encoded deposit message + */ function encodeDepositMessage(uint64 nonce, uint256 depositAmount) internal view @@ -67,6 +108,12 @@ library CrossChainStrategyHelper { ); } + /** + * @dev Decode the deposit message. + * The message version and type are verified in the message. + * @param message The message to decode + * @return The nonce and the amount of the deposit + */ function decodeDepositMessage(bytes memory message) internal returns (uint64, uint256) @@ -80,6 +127,13 @@ library CrossChainStrategyHelper { return (nonce, depositAmount); } + /** + * @dev Encode the withdrawal message. + * The message version and type are always encoded in the message. + * @param nonce The nonce of the withdrawal + * @param withdrawAmount The amount of the withdrawal + * @return The encoded withdrawal message + */ function encodeWithdrawMessage(uint64 nonce, uint256 withdrawAmount) internal view @@ -93,6 +147,12 @@ library CrossChainStrategyHelper { ); } + /** + * @dev Decode the withdrawal message. + * The message version and type are verified in the message. + * @param message The message to decode + * @return The nonce and the amount of the withdrawal + */ function decodeWithdrawMessage(bytes memory message) internal returns (uint64, uint256) @@ -106,6 +166,13 @@ library CrossChainStrategyHelper { return (nonce, withdrawAmount); } + /** + * @dev Encode the balance check message. + * The message version and type are always encoded in the message. + * @param nonce The nonce of the balance check + * @param balance The balance to check + * @return The encoded balance check message + */ function encodeBalanceCheckMessage(uint64 nonce, uint256 balance) internal view @@ -119,6 +186,12 @@ library CrossChainStrategyHelper { ); } + /** + * @dev Decode the balance check message. + * The message version and type are verified in the message. + * @param message The message to decode + * @return The nonce and the balance to check + */ function decodeBalanceCheckMessage(bytes memory message) internal returns (uint64, uint256) From 29ee0e60108810a1a3da05ca8210ec1ade88aacd Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Mon, 22 Dec 2025 14:24:00 +0100 Subject: [PATCH 031/101] WIP Unit test setup (#2722) * add cross chain unit test basic files * add basic unit test setup * add header encoding * more tests --- .../crosschain/CCTPMessageTransmitterMock.sol | 177 +++++++++++++++++ .../crosschain/CCTPTokenMessengerMock.sol | 101 ++++++++++ .../CrossChainMasterStrategyMock.sol | 21 -- .../CrossChainRemoteStrategyMock.sol | 21 -- contracts/contracts/mocks/crosschain/Untitled | 1 + .../crosschain/AbstractCCTPIntegrator.sol | 181 ++++++++++++------ contracts/deploy/deployActions.js | 62 +++++- contracts/deploy/mainnet/000_mock.js | 4 + contracts/deploy/mainnet/001_core.js | 4 + contracts/test/_fixture.js | 72 +++---- .../crosschain/cross-chain-strategy.js | 65 +++++++ 11 files changed, 555 insertions(+), 154 deletions(-) create mode 100644 contracts/contracts/mocks/crosschain/CCTPMessageTransmitterMock.sol create mode 100644 contracts/contracts/mocks/crosschain/CCTPTokenMessengerMock.sol delete mode 100644 contracts/contracts/mocks/crosschain/CrossChainMasterStrategyMock.sol delete mode 100644 contracts/contracts/mocks/crosschain/CrossChainRemoteStrategyMock.sol create mode 100644 contracts/contracts/mocks/crosschain/Untitled create mode 100644 contracts/test/strategies/crosschain/cross-chain-strategy.js diff --git a/contracts/contracts/mocks/crosschain/CCTPMessageTransmitterMock.sol b/contracts/contracts/mocks/crosschain/CCTPMessageTransmitterMock.sol new file mode 100644 index 0000000000..9a9149d74a --- /dev/null +++ b/contracts/contracts/mocks/crosschain/CCTPMessageTransmitterMock.sol @@ -0,0 +1,177 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +import { ICCTPMessageTransmitter } from "../../interfaces/cctp/ICCTP.sol"; +import { IERC20 } from "../../utils/InitializableAbstractStrategy.sol"; + +/** + * @title Mock conctract simulating the functionality of the CCTPTokenMessenger contract + * for the porposes of unit testing. + * @author Origin Protocol Inc + */ + +contract CCTPMessageTransmitterMock is ICCTPMessageTransmitter { + IERC20 public usdc; + uint256 public nonce = 0; + + + // Full message with header + struct Message { + uint32 version; + uint32 sourceDomain; + uint32 destinationDomain; + bytes32 recipient; + bytes32 sender; + bytes32 destinationCaller; + uint32 minFinalityThreshold; + bool isTokenTransfer; + uint256 tokenAmount; + bytes messageBody; + } + + Message[] public messages; + + constructor(address _usdc) { + usdc = IERC20(_usdc); + } + + // @dev for the porposes of unit tests queues the message to be mock-sent using + // the cctp bridge. + function sendMessage( + uint32 destinationDomain, + bytes32 recipient, + bytes32 destinationCaller, + uint32 minFinalityThreshold, + bytes memory messageBody + ) external override { + bytes32 nonceHash = keccak256(abi.encodePacked(nonce)); + nonce++; + + Message memory message = Message({ + version: 1, + sourceDomain: 1, + destinationDomain: destinationDomain, + recipient: recipient, + sender: bytes32(uint256(uint160(msg.sender))), + destinationCaller: destinationCaller, + minFinalityThreshold: minFinalityThreshold, + isTokenTransfer: false, + tokenAmount: 0, + messageBody: messageBody + }); + + messages.push(message); + } + + // @dev for the porposes of unit tests queues the USDC burn/mint to be executed + // using the cctp bridge. + function sendTokenTransferMessage( + uint32 destinationDomain, + bytes32 recipient, + bytes32 destinationCaller, + uint32 minFinalityThreshold, + uint256 tokenAmount, + bytes memory messageBody + ) external { + bytes32 nonceHash = keccak256(abi.encodePacked(nonce)); + nonce++; + + Message memory message = Message({ + version: 1, + sourceDomain: 1, + destinationDomain: destinationDomain, + recipient: recipient, + sender: bytes32(uint256(uint160(msg.sender))), + destinationCaller: destinationCaller, + minFinalityThreshold: minFinalityThreshold, + isTokenTransfer: true, + tokenAmount: tokenAmount, + messageBody: messageBody + }); + + messages.push(message); + } + + function receiveMessage(bytes memory message, bytes memory attestation) + public + override + returns (bool) { + // For mock, assume we can decode and push, but simplified: just push the bytes as body or something + // To properly decode, we'd need the header parsing logic + // For now, emit or log, but to store, perhaps add a function later + + // this step also needs to mint USDC and transfer it to the recipient wallet + revert("Not implemented"); + //return true; + } + + function addMessage(Message memory msg) external { + messages.push(msg); + } + + function _encodeMessageHeader( + uint32 version, + uint32 sourceDomain, + bytes32 sender, + bytes32 recipient, + bytes memory messageBody + ) internal pure returns (bytes memory) { + bytes memory header = abi.encodePacked( + version, // 0-3 + sourceDomain, // 4-7 + bytes32(0), // 8-39 destinationDomain + bytes4(0), // 40-43 nonce + sender, // 44-75 sender + recipient, // 76-107 recipient + bytes32(0), // other stuff + bytes8(0) // other stuff + ); + return abi.encodePacked(header, messageBody); + } + + function _removeFront() internal returns (Message memory) { + require(messages.length > 0, "No messages"); + Message memory removed = messages[0]; + // Shift array + for (uint256 i = 0; i < messages.length - 1; i++) { + messages[i] = messages[i + 1]; + } + messages.pop(); + return removed; + } + + function _processMessage(Message memory msg) internal { + bytes memory encoded = _encodeMessageHeader( + msg.version, + msg.sourceDomain, + msg.sender, + msg.recipient, + msg.messageBody + ); + + receiveMessage(encoded, bytes("")); + } + + function _removeBack() internal returns (Message memory) { + require(messages.length > 0, "No messages"); + Message memory last = messages[messages.length - 1]; + messages.pop(); + return last; + } + + function processFront() external { + Message memory msg = _removeFront(); + _processMessage(msg); + } + + function processBack() external { + Message memory msg = _removeBack(); + _processMessage(msg); + } + + function getMessagesLength() external view returns (uint256) { + return messages.length; + } + + +} diff --git a/contracts/contracts/mocks/crosschain/CCTPTokenMessengerMock.sol b/contracts/contracts/mocks/crosschain/CCTPTokenMessengerMock.sol new file mode 100644 index 0000000000..3e00ad5012 --- /dev/null +++ b/contracts/contracts/mocks/crosschain/CCTPTokenMessengerMock.sol @@ -0,0 +1,101 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +import { ICCTPTokenMessenger } from "../../interfaces/cctp/ICCTP.sol"; +import { CCTPMessageTransmitterMock } from "./CCTPMessageTransmitterMock.sol"; +import { IERC20 } from "../../utils/InitializableAbstractStrategy.sol"; + +/** + * @title Mock conctract simulating the functionality of the CCTPTokenMessenger contract + * for the porposes of unit testing. + * @author Origin Protocol Inc + */ + +contract CCTPTokenMessengerMock is ICCTPTokenMessenger{ + IERC20 public usdc; + CCTPMessageTransmitterMock public cctpMessageTransmitterMock; + + constructor(address _usdc, address _cctpMessageTransmitterMock) { + usdc = IERC20(_usdc); + cctpMessageTransmitterMock = CCTPMessageTransmitterMock(_cctpMessageTransmitterMock); + } + + function depositForBurn( + uint256 amount, + uint32 destinationDomain, + bytes32 mintRecipient, + address burnToken, + bytes32 destinationCaller, + uint256 maxFee, + uint32 minFinalityThreshold + ) external override { + revert("Not implemented"); + } + + /** + * @dev mocks the depositForBurnWithHook function by sending the USDC to the CCTPMessageTransmitterMock + * called by the AbstractCCTPIntegrator contract. + */ + function depositForBurnWithHook( + uint256 amount, + uint32 destinationDomain, + bytes32 mintRecipient, + address burnToken, + bytes32 destinationCaller, + uint256 maxFee, + uint32 minFinalityThreshold, + bytes memory hookData + ) external override { + require(burnToken == address(usdc), "Invalid burn token"); + + usdc.transferFrom(msg.sender, address(this), maxFee); + uint256 destinationAmount = amount - maxFee; + usdc.transferFrom(msg.sender, address(cctpMessageTransmitterMock), destinationAmount); + + + bytes memory burnMessage = _encodeBurnMessageV2( + mintRecipient, + amount, + msg.sender, + maxFee, + maxFee, + hookData + ); + + cctpMessageTransmitterMock.sendTokenTransferMessage( + destinationDomain, + mintRecipient, + destinationCaller, + minFinalityThreshold, + destinationAmount, + burnMessage + ); + } + + function _encodeBurnMessageV2( + bytes32 mintRecipient, + uint256 amount, + address messageSender, + uint256 maxFee, + uint256 feeExecuted, + bytes memory hookData + ) internal view returns (bytes memory) { + bytes32 burnTokenBytes32 = bytes32(abi.encodePacked(bytes12(0), bytes20(uint160(address(usdc))))); + bytes32 messageSenderBytes32 = bytes32(abi.encodePacked(bytes12(0), bytes20(uint160(messageSender)))); + + return abi.encodePacked( + uint32(1), // 0-3: version + burnTokenBytes32, // 4-35: burnToken (bytes32 left-padded address) + mintRecipient, // 36-67: mintRecipient (bytes32 left-padded address) + amount, // 68-99: uint256 amount + messageSenderBytes32, // 100-131: messageSender (bytes32 left-padded address) + maxFee, // 132-163: uint256 maxFee + feeExecuted, // 164-195: uint256 feeExecuted + hookData // 196+: dynamic hookData + ); + } + + function getMinFeeAmount(uint256 amount) external view override returns (uint256) { + return 0; + } +} diff --git a/contracts/contracts/mocks/crosschain/CrossChainMasterStrategyMock.sol b/contracts/contracts/mocks/crosschain/CrossChainMasterStrategyMock.sol deleted file mode 100644 index 8ed3c46c7b..0000000000 --- a/contracts/contracts/mocks/crosschain/CrossChainMasterStrategyMock.sol +++ /dev/null @@ -1,21 +0,0 @@ -// SPDX-License-Identifier: BUSL-1.1 -pragma solidity ^0.8.0; - -/** - * @title OUSD Yearn V3 Master Strategy Mock - the Mainnet part - * @author Origin Protocol Inc - */ - -contract CrossChainMasterStrategyMock { - address public _remoteAddress; - - constructor() {} - - function remoteAddress() public view returns (address) { - return _remoteAddress; - } - - function setRemoteAddress(address __remoteAddress) public { - _remoteAddress = __remoteAddress; - } -} diff --git a/contracts/contracts/mocks/crosschain/CrossChainRemoteStrategyMock.sol b/contracts/contracts/mocks/crosschain/CrossChainRemoteStrategyMock.sol deleted file mode 100644 index 43deb9f34c..0000000000 --- a/contracts/contracts/mocks/crosschain/CrossChainRemoteStrategyMock.sol +++ /dev/null @@ -1,21 +0,0 @@ -// SPDX-License-Identifier: BUSL-1.1 -pragma solidity ^0.8.0; - -/** - * @title OUSD Yearn V3 Remote Strategy Mock - the Mainnet part - * @author Origin Protocol Inc - */ - -contract CrossChainRemoteStrategyMock { - address public _masterAddress; - - constructor() {} - - function masterAddress() public view returns (address) { - return _masterAddress; - } - - function setMasterAddress(address __masterAddress) public { - _masterAddress = __masterAddress; - } -} diff --git a/contracts/contracts/mocks/crosschain/Untitled b/contracts/contracts/mocks/crosschain/Untitled new file mode 100644 index 0000000000..4942ce0833 --- /dev/null +++ b/contracts/contracts/mocks/crosschain/Untitled @@ -0,0 +1 @@ +depositForBurnWithHook \ No newline at end of file diff --git a/contracts/contracts/strategies/crosschain/AbstractCCTPIntegrator.sol b/contracts/contracts/strategies/crosschain/AbstractCCTPIntegrator.sol index d8767915aa..15ccfb6e0a 100644 --- a/contracts/contracts/strategies/crosschain/AbstractCCTPIntegrator.sol +++ b/contracts/contracts/strategies/crosschain/AbstractCCTPIntegrator.sol @@ -121,6 +121,9 @@ abstract contract AbstractCCTPIntegrator is Governable, IMessageHandlerV2 { _setFeePremiumBps(_feePremiumBps); } + /*************************************** + Settings + ****************************************/ function setOperator(address _operator) external onlyGovernor { _setOperator(_operator); } @@ -159,6 +162,10 @@ abstract contract AbstractCCTPIntegrator is Governable, IMessageHandlerV2 { emit CCTPFeePremiumBpsSet(_feePremiumBps); } + /*************************************** + CCTP message handling + ****************************************/ + function handleReceiveFinalizedMessage( uint32 sourceDomain, bytes32 sender, @@ -252,65 +259,6 @@ abstract contract AbstractCCTPIntegrator is Governable, IMessageHandlerV2 { ); } - function isTransferPending() public view returns (bool) { - uint64 nonce = lastTransferNonce; - return nonce > 0 && !nonceProcessed[nonce]; - } - - function isNonceProcessed(uint64 nonce) public view returns (bool) { - return nonceProcessed[nonce]; - } - - function _markNonceAsProcessed(uint64 nonce) internal { - uint64 lastNonce = lastTransferNonce; - - // Can only mark latest nonce as processed - require(nonce >= lastNonce, "Nonce too low"); - // Can only mark nonce as processed once - require(!nonceProcessed[nonce], "Nonce already processed"); - - nonceProcessed[nonce] = true; - - if (nonce != lastNonce) { - // Update last known nonce - lastTransferNonce = nonce; - } - } - - function _getNextNonce() internal returns (uint64) { - uint64 nonce = lastTransferNonce; - - require( - nonce == 0 || nonceProcessed[nonce], - "Pending deposit or withdrawal" - ); - - nonce = nonce + 1; - lastTransferNonce = nonce; - - return nonce; - } - - function _decodeMessageHeader(bytes memory message) - internal - pure - returns ( - uint32 version, - uint32 sourceDomainID, - address sender, - address recipient, - bytes memory messageBody - ) - { - version = message.extractUint32(VERSION_INDEX); - sourceDomainID = message.extractUint32(SOURCE_DOMAIN_INDEX); - // Address of MessageTransmitterV2 caller on source domain - sender = message.extractAddress(SENDER_INDEX); - // Address to handle message body on destination domain - recipient = message.extractAddress(RECIPIENT_INDEX); - messageBody = message.extractSlice(MESSAGE_BODY_INDEX, message.length); - } - function relay(bytes memory message, bytes memory attestation) external onlyOperator @@ -391,6 +339,121 @@ abstract contract AbstractCCTPIntegrator is Governable, IMessageHandlerV2 { } } + /*************************************** + Message utils + ****************************************/ + + + function _getMessageVersion(bytes memory message) + internal + virtual + returns (uint32) + { + // uint32 bytes 0 to 4 is Origin message version + // uint32 bytes 4 to 8 is Message type + return message.extractUint32(0); + } + + function _getMessageType(bytes memory message) + internal + virtual + returns (uint32) + { + // uint32 bytes 0 to 4 is Origin message version + // uint32 bytes 4 to 8 is Message type + return message.extractUint32(4); + } + + function _verifyMessageVersionAndType( + bytes memory _message, + uint32 _version, + uint32 _type + ) internal virtual { + require( + _getMessageVersion(_message) == _version, + "Invalid Origin Message Version" + ); + require(_getMessageType(_message) == _type, "Invalid Message type"); + } + + function _getMessagePayload(bytes memory message) + internal + virtual + returns (bytes memory) + { + // uint32 bytes 0 to 4 is Origin message version + // uint32 bytes 4 to 8 is Message type + // Payload starts at byte 8 + return message.extractSlice(8, message.length); + } + + function _decodeMessageHeader(bytes memory message) + internal + pure + returns ( + uint32 version, + uint32 sourceDomainID, + address sender, + address recipient, + bytes memory messageBody + ) + { + version = message.extractUint32(VERSION_INDEX); + sourceDomainID = message.extractUint32(SOURCE_DOMAIN_INDEX); + // Address of MessageTransmitterV2 caller on source domain + sender = message.extractAddress(SENDER_INDEX); + // Address to handle message body on destination domain + recipient = message.extractAddress(RECIPIENT_INDEX); + messageBody = message.extractSlice(MESSAGE_BODY_INDEX, message.length); + } + + /*************************************** + Nonce Handling + ****************************************/ + + function isTransferPending() public view returns (bool) { + uint64 nonce = lastTransferNonce; + return nonce > 0 && !nonceProcessed[nonce]; + } + + function isNonceProcessed(uint64 nonce) public view returns (bool) { + return nonceProcessed[nonce]; + } + + function _markNonceAsProcessed(uint64 nonce) internal { + uint64 lastNonce = lastTransferNonce; + + // Can only mark latest nonce as processed + require(nonce >= lastNonce, "Nonce too low"); + // Can only mark nonce as processed once + require(!nonceProcessed[nonce], "Nonce already processed"); + + nonceProcessed[nonce] = true; + + if (nonce != lastNonce) { + // Update last known nonce + lastTransferNonce = nonce; + } + } + + function _getNextNonce() internal returns (uint64) { + uint64 nonce = lastTransferNonce; + + require( + nonce == 0 || nonceProcessed[nonce], + "Pending deposit or withdrawal" + ); + + nonce = nonce + 1; + lastTransferNonce = nonce; + + return nonce; + } + + /*************************************** + Inheritence overrides + ****************************************/ + /** * @dev Called when the USDC is received from the CCTP * @param tokenAmount The actual amount of USDC received (amount sent - fee executed) diff --git a/contracts/deploy/deployActions.js b/contracts/deploy/deployActions.js index 0b5d93430a..6944e53ac6 100644 --- a/contracts/deploy/deployActions.js +++ b/contracts/deploy/deployActions.js @@ -1758,7 +1758,9 @@ const deployCrossChainMasterStrategyImpl = async ( remoteStrategyAddress, baseToken, implementationName = "CrossChainMasterStrategy", - skipInitialize = false + skipInitialize = false, + tokenMessengerAddress = addresses.CCTPTokenMessengerV2, + messageTransmitterAddress = addresses.CCTPMessageTransmitterV2 ) => { const { deployerAddr } = await getNamedAccounts(); const sDeployer = await ethers.provider.getSigner(deployerAddr); @@ -1779,8 +1781,8 @@ const deployCrossChainMasterStrategyImpl = async ( // addresses.mainnet.VaultProxy, ], [ - addresses.CCTPTokenMessengerV2, - addresses.CCTPMessageTransmitterV2, + tokenMessengerAddress, + messageTransmitterAddress, targetDomainId, remoteStrategyAddress, baseToken, @@ -1819,7 +1821,9 @@ const deployCrossChainRemoteStrategyImpl = async ( targetDomainId, remoteStrategyAddress, baseToken, - implementationName = "CrossChainRemoteStrategy" + implementationName = "CrossChainRemoteStrategy", + tokenMessengerAddress = addresses.CCTPTokenMessengerV2, + messageTransmitterAddress = addresses.CCTPMessageTransmitterV2, ) => { const { deployerAddr } = await getNamedAccounts(); const sDeployer = await ethers.provider.getSigner(deployerAddr); @@ -1840,8 +1844,8 @@ const deployCrossChainRemoteStrategyImpl = async ( // addresses.mainnet.VaultProxy, ], [ - addresses.CCTPTokenMessengerV2, - addresses.CCTPMessageTransmitterV2, + tokenMessengerAddress, + messageTransmitterAddress, targetDomainId, remoteStrategyAddress, baseToken, @@ -1871,6 +1875,51 @@ const deployCrossChainRemoteStrategyImpl = async ( return dCrossChainRemoteStrategy.address; }; +// deploy the corss chain Master / Remote strategy pair for unit testing +const deployCrossChainUnitTestStrategy = async ( + usdcAddress, +) => { + const { deployerAddr } = await getNamedAccounts(); + const dMasterProxy = await deployWithConfirmation( + "CrossChainMasterStrategyProxy", + [deployerAddr], + "CrossChainStrategyProxy" + ); + const dRemoteProxy = await deployWithConfirmation( + "CrossChainRemoteStrategyProxy", + [deployerAddr], + "CrossChainStrategyProxy" + ); + + const messageTransmitter = await ethers.getContract("CCTPMessageTransmitterMock"); + const tokenMessenger = await ethers.getContract("CCTPTokenMessengerMock"); + + + await deployCrossChainMasterStrategyImpl( + dMasterProxy.address, + 6, // Base domain id + // unit tests differ from mainnet where remote strategy has a different address + dRemoteProxy.address, + usdcAddress, + "CrossChainMasterStrategy", + false, + tokenMessenger.address, + messageTransmitter.address, + ); + + await deployCrossChainRemoteStrategyImpl( + deployerAddr, // TODO platform address needs to be replaces with mock 4626 Moprho Vault + dRemoteProxy.address, + 0, // Ethereum domain id + dMasterProxy.address, + usdcAddress, + "CrossChainRemoteStrategy", + tokenMessenger.address, + messageTransmitter.address, + ); + +}; + module.exports = { deployOracles, deployCore, @@ -1911,4 +1960,5 @@ module.exports = { deployProxyWithCreateX, deployCrossChainMasterStrategyImpl, deployCrossChainRemoteStrategyImpl, + deployCrossChainUnitTestStrategy, }; diff --git a/contracts/deploy/mainnet/000_mock.js b/contracts/deploy/mainnet/000_mock.js index f2a598e705..f826306855 100644 --- a/contracts/deploy/mainnet/000_mock.js +++ b/contracts/deploy/mainnet/000_mock.js @@ -447,6 +447,10 @@ const deployMocks = async ({ getNamedAccounts, deployments }) => { const mockBeaconRoots = await ethers.getContract("MockBeaconRoots"); await replaceContractAt(addresses.mainnet.beaconRoots, mockBeaconRoots); + await deploy("CCTPMessageTransmitterMock", { from: deployerAddr, args: [usdc.address] }); + const messageTransmitter = await ethers.getContract("CCTPMessageTransmitterMock"); + await deploy("CCTPTokenMessengerMock", { from: deployerAddr, args: [usdc.address, messageTransmitter.address] }); + console.log("000_mock deploy done."); return true; diff --git a/contracts/deploy/mainnet/001_core.js b/contracts/deploy/mainnet/001_core.js index 024146b8b9..37cc3fc746 100644 --- a/contracts/deploy/mainnet/001_core.js +++ b/contracts/deploy/mainnet/001_core.js @@ -21,10 +21,13 @@ const { deployWOeth, deployOETHSwapper, deployOUSDSwapper, + deployCrossChainUnitTestStrategy, } = require("../deployActions"); const main = async () => { console.log("Running 001_core deployment..."); + const usdc = await ethers.getContract("MockUSDC"); + await deployOracles(); await deployCore(); await deployCurveMetapoolMocks(); @@ -48,6 +51,7 @@ const main = async () => { await deployWOeth(); await deployOETHSwapper(); await deployOUSDSwapper(); + await deployCrossChainUnitTestStrategy(usdc.address); console.log("001_core deploy done."); return true; }; diff --git a/contracts/test/_fixture.js b/contracts/test/_fixture.js index 004a287d47..da0b8d1d95 100644 --- a/contracts/test/_fixture.js +++ b/contracts/test/_fixture.js @@ -16,10 +16,6 @@ const { fundAccountsForOETHUnitTests, } = require("../utils/funding"); const { deployWithConfirmation } = require("../utils/deploy"); -const { - deployCrossChainMasterStrategyImpl, - deployCrossChainRemoteStrategyImpl, -} = require("../deploy/deployActions.js"); const { replaceContractAt } = require("../utils/hardhat"); const { @@ -2529,56 +2525,38 @@ async function instantRebaseVaultFixture() { return fixture; } -async function yearnCrossChainFixture() { +// Unit test cross chain fixture where both contracts are deployed on the same chain for the +// purposes of unit testing +async function crossChainFixtureUnit() { const fixture = await defaultFixture(); - const { deployerAddr } = await getNamedAccounts(); - const sDeployer = await ethers.provider.getSigner(deployerAddr); - // deploy master strategy - const masterProxy = await deployWithConfirmation("CrossChainStrategyProxy", [ - deployerAddr, - ]); - const masterProxyAddress = masterProxy.address; - log(`CrossChainStrategyProxy address: ${masterProxyAddress}`); - let implAddress = await deployCrossChainMasterStrategyImpl( - masterProxyAddress, - "CrossChainMasterStrategyMock" + const crossChainMasterStrategyProxy = await ethers.getContract( + "CrossChainMasterStrategyProxy" ); - log(`CrossChainMasterStrategyMockImpl address: ${implAddress}`); - - // deploy remote strategy - const remoteProxy = await deployWithConfirmation("CrossChainStrategyProxy", [ - deployerAddr, - ]); - - const remoteProxyAddress = remoteProxy.address; - log(`CrossChainStrategyProxy address: ${remoteProxyAddress}`); - - implAddress = await deployCrossChainRemoteStrategyImpl( - remoteProxyAddress, - "CrossChainRemoteStrategyMock" + const crossChainRemoteStrategyProxy = await ethers.getContract( + "CrossChainRemoteStrategyProxy" ); - log(`CrossChainRemoteStrategyMockImpl address: ${implAddress}`); - - const yearnMasterStrategy = await ethers.getContractAt( - "CrossChainMasterStrategyMock", - masterProxyAddress + + const cCrossChainMasterStrategy = await ethers.getContractAt( + "CrossChainMasterStrategy", + crossChainMasterStrategyProxy.address ); - const yearnRemoteStrategy = await ethers.getContractAt( - "CrossChainRemoteStrategyMock", - remoteProxyAddress + + const cCrossChainRemoteStrategy = await ethers.getContractAt( + "CrossChainRemoteStrategy", + crossChainRemoteStrategyProxy.address ); - await yearnMasterStrategy - .connect(sDeployer) - .setRemoteAddress(remoteProxyAddress); - await yearnRemoteStrategy - .connect(sDeployer) - .setMasterAddress(masterProxyAddress); + const messageTransmitter = await ethers.getContract("CCTPMessageTransmitterMock"); + const tokenMessenger = await ethers.getContract("CCTPTokenMessengerMock"); - fixture.yearnMasterStrategy = yearnMasterStrategy; - fixture.yearnRemoteStrategy = yearnRemoteStrategy; - return fixture; + return { + ...fixture, + crossChainMasterStrategy: cCrossChainMasterStrategy, + crossChainRemoteStrategy: cCrossChainRemoteStrategy, + messageTransmitter: messageTransmitter, + tokenMessenger: tokenMessenger, + }; } /** @@ -3020,6 +2998,6 @@ module.exports = { bridgeHelperModuleFixture, beaconChainFixture, claimRewardsModuleFixture, - yearnCrossChainFixture, + crossChainFixtureUnit, crossChainFixture, }; diff --git a/contracts/test/strategies/crosschain/cross-chain-strategy.js b/contracts/test/strategies/crosschain/cross-chain-strategy.js new file mode 100644 index 0000000000..50b42ffcd1 --- /dev/null +++ b/contracts/test/strategies/crosschain/cross-chain-strategy.js @@ -0,0 +1,65 @@ +// const { expect } = require("chai"); + +const { isCI } = require("../../helpers"); +const { createFixtureLoader, crossChainFixtureUnit } = require("../../_fixture"); +const { + units +} = require("../../helpers"); + +const loadFixture = createFixtureLoader(crossChainFixtureUnit); + +describe("ForkTest: CrossChainRemoteStrategy", function () { + this.timeout(0); + + // Retry up to 3 times on CI + this.retries(isCI ? 3 : 0); + + let fixture, + josh, + governor, + usdc, + crossChainRemoteStrategy, + crossChainMasterStrategy, + vault; + beforeEach(async () => { + fixture = await loadFixture(); + josh = fixture.josh; + governor = fixture.governor; + usdc = fixture.usdc; + crossChainRemoteStrategy = fixture.crossChainRemoteStrategy; + crossChainMasterStrategy = fixture.crossChainMasterStrategy; + vault = fixture.vault; + }); + + const mint = async (amount) => { + await usdc + .connect(josh) + .approve(vault.address, await units(amount, usdc)); + await vault + .connect(josh) + .mint(usdc.address, await units(amount, usdc), 0); + }; + + const depositToMasterStrategy = async (amount) => { + await vault + .connect(governor) + .depositToStrategy( + crossChainMasterStrategy.address, + [usdc.address], + [await units(amount, usdc)] + ); + }; + + const depositToStrategy = async (amount) => { + await usdc.connect(josh).approve(crossChainRemoteStrategy.address, await units(amount, usdc)); + await crossChainRemoteStrategy.connect(josh).depositToStrategy(amount, usdc.address); + }; + + it("Should initiate a bridge of deposited USDC", async function () { + //const { crossChainRemoteStrategy, messageTransmitter, tokenMessenger } = fixture; + + await mint("1000"); + await depositToMasterStrategy("1000"); + await depositToStrategy("1000"); + }); +}); From a0d537933532df3792186a48179b928d3638b94d Mon Sep 17 00:00:00 2001 From: Shahul Hameed <10547529+shahthepro@users.noreply.github.com> Date: Mon, 22 Dec 2025 22:05:06 +0400 Subject: [PATCH 032/101] Add more fork tests --- .../crosschain/CCTPMessageTransmitterMock.sol | 80 +- .../crosschain/CCTPTokenMessengerMock.sol | 53 +- .../crosschain/AbstractCCTPIntegrator.sol | 3 +- .../crosschain/CrossChainMasterStrategy.sol | 4 + .../crosschain/CrossChainRemoteStrategy.sol | 2 +- contracts/deploy/deployActions.js | 67 +- contracts/deploy/mainnet/000_mock.js | 14 +- ....js => 161_crosschain_strategy_proxies.js} | 2 +- ...strategy.js => 162_crosschain_strategy.js} | 22 +- contracts/test/_fixture.js | 20 +- .../crosschain/cross-chain-strategy.js | 27 +- ...chain-master-strategy.mainnet.fork-test.js | 764 +++++++++++++----- 12 files changed, 723 insertions(+), 335 deletions(-) rename contracts/deploy/mainnet/{160_crosschain_strategy_proxies.js => 161_crosschain_strategy_proxies.js} (93%) rename contracts/deploy/mainnet/{161_crosschain_strategy.js => 162_crosschain_strategy.js} (66%) diff --git a/contracts/contracts/mocks/crosschain/CCTPMessageTransmitterMock.sol b/contracts/contracts/mocks/crosschain/CCTPMessageTransmitterMock.sol index 9a9149d74a..1112fbd038 100644 --- a/contracts/contracts/mocks/crosschain/CCTPMessageTransmitterMock.sol +++ b/contracts/contracts/mocks/crosschain/CCTPMessageTransmitterMock.sol @@ -3,18 +3,33 @@ pragma solidity ^0.8.0; import { ICCTPMessageTransmitter } from "../../interfaces/cctp/ICCTP.sol"; import { IERC20 } from "../../utils/InitializableAbstractStrategy.sol"; +import { BytesHelper } from "../../utils/BytesHelper.sol"; +import { IMessageHandlerV2 } from "../../interfaces/cctp/ICCTP.sol"; + +// CCTP Message Header fields +// Ref: https://developers.circle.com/cctp/technical-guide#message-header +uint8 constant VERSION_INDEX = 0; +uint8 constant SOURCE_DOMAIN_INDEX = 4; +uint8 constant SENDER_INDEX = 44; +uint8 constant RECIPIENT_INDEX = 76; +uint8 constant MESSAGE_BODY_INDEX = 148; /** * @title Mock conctract simulating the functionality of the CCTPTokenMessenger contract - * for the porposes of unit testing. + * for the porposes of unit testing. * @author Origin Protocol Inc */ contract CCTPMessageTransmitterMock is ICCTPMessageTransmitter { + using BytesHelper for bytes; + IERC20 public usdc; uint256 public nonce = 0; - + bool public shouldRevertNextReceiveMessage; + + event MessageReceivedInMockTransmitter(bytes message); + // Full message with header struct Message { uint32 version; @@ -34,9 +49,9 @@ contract CCTPMessageTransmitterMock is ICCTPMessageTransmitter { constructor(address _usdc) { usdc = IERC20(_usdc); } - - // @dev for the porposes of unit tests queues the message to be mock-sent using - // the cctp bridge. + + // @dev for the porposes of unit tests queues the message to be mock-sent using + // the cctp bridge. function sendMessage( uint32 destinationDomain, bytes32 recipient, @@ -59,12 +74,12 @@ contract CCTPMessageTransmitterMock is ICCTPMessageTransmitter { tokenAmount: 0, messageBody: messageBody }); - + messages.push(message); } - // @dev for the porposes of unit tests queues the USDC burn/mint to be executed - // using the cctp bridge. + // @dev for the porposes of unit tests queues the USDC burn/mint to be executed + // using the cctp bridge. function sendTokenTransferMessage( uint32 destinationDomain, bytes32 recipient, @@ -88,21 +103,40 @@ contract CCTPMessageTransmitterMock is ICCTPMessageTransmitter { tokenAmount: tokenAmount, messageBody: messageBody }); - + messages.push(message); } function receiveMessage(bytes memory message, bytes memory attestation) public override - returns (bool) { + returns (bool) + { // For mock, assume we can decode and push, but simplified: just push the bytes as body or something // To properly decode, we'd need the header parsing logic // For now, emit or log, but to store, perhaps add a function later - // this step also needs to mint USDC and transfer it to the recipient wallet - revert("Not implemented"); - //return true; + uint32 sourceDomain = message.extractUint32(SOURCE_DOMAIN_INDEX); + address recipient = message.extractAddress(RECIPIENT_INDEX); + address sender = message.extractAddress(SENDER_INDEX); + IMessageHandlerV2(recipient).handleReceiveFinalizedMessage( + sourceDomain, + bytes32(uint256(uint160(sender))), + 2000, + message.extractSlice(MESSAGE_BODY_INDEX, message.length) + ); + + // This step won't mint USDC, transfer it to the recipient address + // in your tests + emit MessageReceivedInMockTransmitter(message); + + // // For testing purposes, we can revert the next receive message + // if (shouldRevertNextReceiveMessage) { + // shouldRevertNextReceiveMessage = false; + // return false; + // } + + return true; } function addMessage(Message memory msg) external { @@ -117,14 +151,14 @@ contract CCTPMessageTransmitterMock is ICCTPMessageTransmitter { bytes memory messageBody ) internal pure returns (bytes memory) { bytes memory header = abi.encodePacked( - version, // 0-3 - sourceDomain, // 4-7 - bytes32(0), // 8-39 destinationDomain - bytes4(0), // 40-43 nonce - sender, // 44-75 sender - recipient, // 76-107 recipient - bytes32(0), // other stuff - bytes8(0) // other stuff + version, // 0-3 + sourceDomain, // 4-7 + bytes32(0), // 8-39 destinationDomain + bytes4(0), // 40-43 nonce + sender, // 44-75 sender + recipient, // 76-107 recipient + bytes32(0), // other stuff + bytes8(0) // other stuff ); return abi.encodePacked(header, messageBody); } @@ -173,5 +207,7 @@ contract CCTPMessageTransmitterMock is ICCTPMessageTransmitter { return messages.length; } - + function revertNextReceiveMessage() external { + shouldRevertNextReceiveMessage = true; + } } diff --git a/contracts/contracts/mocks/crosschain/CCTPTokenMessengerMock.sol b/contracts/contracts/mocks/crosschain/CCTPTokenMessengerMock.sol index 3e00ad5012..49b7b83c3d 100644 --- a/contracts/contracts/mocks/crosschain/CCTPTokenMessengerMock.sol +++ b/contracts/contracts/mocks/crosschain/CCTPTokenMessengerMock.sol @@ -7,17 +7,19 @@ import { IERC20 } from "../../utils/InitializableAbstractStrategy.sol"; /** * @title Mock conctract simulating the functionality of the CCTPTokenMessenger contract - * for the porposes of unit testing. + * for the porposes of unit testing. * @author Origin Protocol Inc */ -contract CCTPTokenMessengerMock is ICCTPTokenMessenger{ +contract CCTPTokenMessengerMock is ICCTPTokenMessenger { IERC20 public usdc; CCTPMessageTransmitterMock public cctpMessageTransmitterMock; constructor(address _usdc, address _cctpMessageTransmitterMock) { usdc = IERC20(_usdc); - cctpMessageTransmitterMock = CCTPMessageTransmitterMock(_cctpMessageTransmitterMock); + cctpMessageTransmitterMock = CCTPMessageTransmitterMock( + _cctpMessageTransmitterMock + ); } function depositForBurn( @@ -45,13 +47,16 @@ contract CCTPTokenMessengerMock is ICCTPTokenMessenger{ uint256 maxFee, uint32 minFinalityThreshold, bytes memory hookData - ) external override { + ) external override { require(burnToken == address(usdc), "Invalid burn token"); usdc.transferFrom(msg.sender, address(this), maxFee); uint256 destinationAmount = amount - maxFee; - usdc.transferFrom(msg.sender, address(cctpMessageTransmitterMock), destinationAmount); - + usdc.transferFrom( + msg.sender, + address(cctpMessageTransmitterMock), + destinationAmount + ); bytes memory burnMessage = _encodeBurnMessageV2( mintRecipient, @@ -80,22 +85,32 @@ contract CCTPTokenMessengerMock is ICCTPTokenMessenger{ uint256 feeExecuted, bytes memory hookData ) internal view returns (bytes memory) { - bytes32 burnTokenBytes32 = bytes32(abi.encodePacked(bytes12(0), bytes20(uint160(address(usdc))))); - bytes32 messageSenderBytes32 = bytes32(abi.encodePacked(bytes12(0), bytes20(uint160(messageSender)))); - - return abi.encodePacked( - uint32(1), // 0-3: version - burnTokenBytes32, // 4-35: burnToken (bytes32 left-padded address) - mintRecipient, // 36-67: mintRecipient (bytes32 left-padded address) - amount, // 68-99: uint256 amount - messageSenderBytes32, // 100-131: messageSender (bytes32 left-padded address) - maxFee, // 132-163: uint256 maxFee - feeExecuted, // 164-195: uint256 feeExecuted - hookData // 196+: dynamic hookData + bytes32 burnTokenBytes32 = bytes32( + abi.encodePacked(bytes12(0), bytes20(uint160(address(usdc)))) ); + bytes32 messageSenderBytes32 = bytes32( + abi.encodePacked(bytes12(0), bytes20(uint160(messageSender))) + ); + + return + abi.encodePacked( + uint32(1), // 0-3: version + burnTokenBytes32, // 4-35: burnToken (bytes32 left-padded address) + mintRecipient, // 36-67: mintRecipient (bytes32 left-padded address) + amount, // 68-99: uint256 amount + messageSenderBytes32, // 100-131: messageSender (bytes32 left-padded address) + maxFee, // 132-163: uint256 maxFee + feeExecuted, // 164-195: uint256 feeExecuted + hookData // 196+: dynamic hookData + ); } - function getMinFeeAmount(uint256 amount) external view override returns (uint256) { + function getMinFeeAmount(uint256 amount) + external + view + override + returns (uint256) + { return 0; } } diff --git a/contracts/contracts/strategies/crosschain/AbstractCCTPIntegrator.sol b/contracts/contracts/strategies/crosschain/AbstractCCTPIntegrator.sol index 15ccfb6e0a..f6d917b72d 100644 --- a/contracts/contracts/strategies/crosschain/AbstractCCTPIntegrator.sol +++ b/contracts/contracts/strategies/crosschain/AbstractCCTPIntegrator.sol @@ -343,7 +343,6 @@ abstract contract AbstractCCTPIntegrator is Governable, IMessageHandlerV2 { Message utils ****************************************/ - function _getMessageVersion(bytes memory message) internal virtual @@ -417,7 +416,7 @@ abstract contract AbstractCCTPIntegrator is Governable, IMessageHandlerV2 { } function isNonceProcessed(uint64 nonce) public view returns (bool) { - return nonceProcessed[nonce]; + return nonce == 0 || nonceProcessed[nonce]; } function _markNonceAsProcessed(uint64 nonce) internal { diff --git a/contracts/contracts/strategies/crosschain/CrossChainMasterStrategy.sol b/contracts/contracts/strategies/crosschain/CrossChainMasterStrategy.sol index 26c5127832..d254c3f7a4 100644 --- a/contracts/contracts/strategies/crosschain/CrossChainMasterStrategy.sol +++ b/contracts/contracts/strategies/crosschain/CrossChainMasterStrategy.sol @@ -263,6 +263,10 @@ contract CrossChainMasterStrategy is require(_amount > 0, "Withdraw amount must be greater than 0"); require(_recipient == vaultAddress, "Only Vault can withdraw"); require(!isTransferPending(), "Transfer already pending"); + require( + _amount <= remoteStrategyBalance, + "Withdraw amount exceeds remote strategy balance" + ); require( _amount <= MAX_TRANSFER_AMOUNT, "Withdraw amount exceeds max transfer amount" diff --git a/contracts/contracts/strategies/crosschain/CrossChainRemoteStrategy.sol b/contracts/contracts/strategies/crosschain/CrossChainRemoteStrategy.sol index 5a408e420f..7e59500f33 100644 --- a/contracts/contracts/strategies/crosschain/CrossChainRemoteStrategy.sol +++ b/contracts/contracts/strategies/crosschain/CrossChainRemoteStrategy.sol @@ -38,7 +38,7 @@ contract CrossChainRemoteStrategy is Generalized4626Strategy(_baseConfig, _cctpConfig.baseToken) { // NOTE: Vault address must always be the proxy address - // so that IVault(vaultAddress).strategistAddr() + // so that IVault(vaultAddress).strategistAddr() works } function initialize( diff --git a/contracts/deploy/deployActions.js b/contracts/deploy/deployActions.js index 6944e53ac6..7e40551205 100644 --- a/contracts/deploy/deployActions.js +++ b/contracts/deploy/deployActions.js @@ -1762,7 +1762,7 @@ const deployCrossChainMasterStrategyImpl = async ( tokenMessengerAddress = addresses.CCTPTokenMessengerV2, messageTransmitterAddress = addresses.CCTPMessageTransmitterV2 ) => { - const { deployerAddr } = await getNamedAccounts(); + const { deployerAddr, multichainStrategistAddr } = await getNamedAccounts(); const sDeployer = await ethers.provider.getSigner(deployerAddr); log(`Deploying CrossChainMasterStrategyImpl as deployer ${deployerAddr}`); @@ -1771,30 +1771,30 @@ const deployCrossChainMasterStrategyImpl = async ( proxyAddress ); - const dCrossChainMasterStrategy = await deployWithConfirmation( - implementationName, + await deployWithConfirmation(implementationName, [ [ - [ - addresses.zero, // platform address - // TODO: change to the actual vault address - deployerAddr, // vault address - // addresses.mainnet.VaultProxy, - ], - [ - tokenMessengerAddress, - messageTransmitterAddress, - targetDomainId, - remoteStrategyAddress, - baseToken, - ], - ] + addresses.zero, // platform address + // TODO: change to the actual vault address + deployerAddr, // vault address + // addresses.mainnet.VaultProxy, + ], + [ + tokenMessengerAddress, + messageTransmitterAddress, + targetDomainId, + remoteStrategyAddress, + baseToken, + ], + ]); + const dCrossChainMasterStrategy = await ethers.getContract( + implementationName ); if (!skipInitialize) { - // const initData = cCrossChainMasterStrategy.interface.encodeFunctionData( - // "initialize()", - // [] - // ); + const initData = dCrossChainMasterStrategy.interface.encodeFunctionData( + "initialize(address,uint32,uint32)", + [multichainStrategistAddr, 2000, 0] + ); // Init the proxy to point at the implementation, set the governor, and call initialize const initFunction = "initialize(address,address,bytes)"; @@ -1804,8 +1804,7 @@ const deployCrossChainMasterStrategyImpl = async ( // TODO: change governor later // addresses.mainnet.Timelock, // governor deployerAddr, // governor - //initData, // data for delegate call to the initialize function on the strategy - "0x", + initData, // data for delegate call to the initialize function on the strategy await getTxOpts() ) ); @@ -1823,7 +1822,7 @@ const deployCrossChainRemoteStrategyImpl = async ( baseToken, implementationName = "CrossChainRemoteStrategy", tokenMessengerAddress = addresses.CCTPTokenMessengerV2, - messageTransmitterAddress = addresses.CCTPMessageTransmitterV2, + messageTransmitterAddress = addresses.CCTPMessageTransmitterV2 ) => { const { deployerAddr } = await getNamedAccounts(); const sDeployer = await ethers.provider.getSigner(deployerAddr); @@ -1839,8 +1838,8 @@ const deployCrossChainRemoteStrategyImpl = async ( [ [ platformAddress, - // TODO: change to the actual vault address - deployerAddr, // vault address + // Vault address should be same as the proxy address + proxyAddress, // vault address // addresses.mainnet.VaultProxy, ], [ @@ -1876,9 +1875,7 @@ const deployCrossChainRemoteStrategyImpl = async ( }; // deploy the corss chain Master / Remote strategy pair for unit testing -const deployCrossChainUnitTestStrategy = async ( - usdcAddress, -) => { +const deployCrossChainUnitTestStrategy = async (usdcAddress) => { const { deployerAddr } = await getNamedAccounts(); const dMasterProxy = await deployWithConfirmation( "CrossChainMasterStrategyProxy", @@ -1891,10 +1888,11 @@ const deployCrossChainUnitTestStrategy = async ( "CrossChainStrategyProxy" ); - const messageTransmitter = await ethers.getContract("CCTPMessageTransmitterMock"); + const messageTransmitter = await ethers.getContract( + "CCTPMessageTransmitterMock" + ); const tokenMessenger = await ethers.getContract("CCTPTokenMessengerMock"); - await deployCrossChainMasterStrategyImpl( dMasterProxy.address, 6, // Base domain id @@ -1904,9 +1902,9 @@ const deployCrossChainUnitTestStrategy = async ( "CrossChainMasterStrategy", false, tokenMessenger.address, - messageTransmitter.address, + messageTransmitter.address ); - + await deployCrossChainRemoteStrategyImpl( deployerAddr, // TODO platform address needs to be replaces with mock 4626 Moprho Vault dRemoteProxy.address, @@ -1915,9 +1913,8 @@ const deployCrossChainUnitTestStrategy = async ( usdcAddress, "CrossChainRemoteStrategy", tokenMessenger.address, - messageTransmitter.address, + messageTransmitter.address ); - }; module.exports = { diff --git a/contracts/deploy/mainnet/000_mock.js b/contracts/deploy/mainnet/000_mock.js index f826306855..379446b49c 100644 --- a/contracts/deploy/mainnet/000_mock.js +++ b/contracts/deploy/mainnet/000_mock.js @@ -447,9 +447,17 @@ const deployMocks = async ({ getNamedAccounts, deployments }) => { const mockBeaconRoots = await ethers.getContract("MockBeaconRoots"); await replaceContractAt(addresses.mainnet.beaconRoots, mockBeaconRoots); - await deploy("CCTPMessageTransmitterMock", { from: deployerAddr, args: [usdc.address] }); - const messageTransmitter = await ethers.getContract("CCTPMessageTransmitterMock"); - await deploy("CCTPTokenMessengerMock", { from: deployerAddr, args: [usdc.address, messageTransmitter.address] }); + await deploy("CCTPMessageTransmitterMock", { + from: deployerAddr, + args: [usdc.address], + }); + const messageTransmitter = await ethers.getContract( + "CCTPMessageTransmitterMock" + ); + await deploy("CCTPTokenMessengerMock", { + from: deployerAddr, + args: [usdc.address, messageTransmitter.address], + }); console.log("000_mock deploy done."); diff --git a/contracts/deploy/mainnet/160_crosschain_strategy_proxies.js b/contracts/deploy/mainnet/161_crosschain_strategy_proxies.js similarity index 93% rename from contracts/deploy/mainnet/160_crosschain_strategy_proxies.js rename to contracts/deploy/mainnet/161_crosschain_strategy_proxies.js index 6ab1f07c19..9aee00016e 100644 --- a/contracts/deploy/mainnet/160_crosschain_strategy_proxies.js +++ b/contracts/deploy/mainnet/161_crosschain_strategy_proxies.js @@ -3,7 +3,7 @@ const { deployProxyWithCreateX } = require("../deployActions"); module.exports = deploymentWithGovernanceProposal( { - deployName: "160_crosschain_strategy_proxies", + deployName: "161_crosschain_strategy_proxies", forceDeploy: false, reduceQueueTime: true, deployerIsProposer: false, diff --git a/contracts/deploy/mainnet/161_crosschain_strategy.js b/contracts/deploy/mainnet/162_crosschain_strategy.js similarity index 66% rename from contracts/deploy/mainnet/161_crosschain_strategy.js rename to contracts/deploy/mainnet/162_crosschain_strategy.js index b28e8503a8..c9a3e3c9ec 100644 --- a/contracts/deploy/mainnet/161_crosschain_strategy.js +++ b/contracts/deploy/mainnet/162_crosschain_strategy.js @@ -1,26 +1,22 @@ -const { - deploymentWithGovernanceProposal, - withConfirmation, -} = require("../../utils/deploy"); +const { deploymentWithGovernanceProposal } = require("../../utils/deploy"); const addresses = require("../../utils/addresses"); const { cctpDomainIds } = require("../../utils/cctp"); const { deployCrossChainMasterStrategyImpl } = require("../deployActions"); module.exports = deploymentWithGovernanceProposal( { - deployName: "161_crosschain_strategy", + deployName: "162_crosschain_strategy", forceDeploy: false, reduceQueueTime: true, deployerIsProposer: false, proposalId: "", }, async () => { - const { deployerAddr } = await getNamedAccounts(); - const sDeployer = await ethers.provider.getSigner(deployerAddr); - - console.log( - `CrossChainStrategyProxy address: ${addresses.CrossChainStrategyProxy}` + const cProxy = await ethers.getContractAt( + "CrossChainStrategyProxy", + addresses.CrossChainStrategyProxy ); + console.log(`CrossChainStrategyProxy address: ${cProxy.address}`); const implAddress = await deployCrossChainMasterStrategyImpl( addresses.CrossChainStrategyProxy, @@ -40,11 +36,7 @@ module.exports = deploymentWithGovernanceProposal( `CrossChainMasterStrategy address: ${cCrossChainMasterStrategy.address}` ); - await withConfirmation( - cCrossChainMasterStrategy.connect(sDeployer).setMinFinalityThreshold( - 2000 // standard transfer - ) - ); + // TODO: Set reward tokens to Morpho return { actions: [], diff --git a/contracts/test/_fixture.js b/contracts/test/_fixture.js index da0b8d1d95..08866735af 100644 --- a/contracts/test/_fixture.js +++ b/contracts/test/_fixture.js @@ -2536,7 +2536,7 @@ async function crossChainFixtureUnit() { const crossChainRemoteStrategyProxy = await ethers.getContract( "CrossChainRemoteStrategyProxy" ); - + const cCrossChainMasterStrategy = await ethers.getContractAt( "CrossChainMasterStrategy", crossChainMasterStrategyProxy.address @@ -2547,7 +2547,9 @@ async function crossChainFixtureUnit() { crossChainRemoteStrategyProxy.address ); - const messageTransmitter = await ethers.getContract("CCTPMessageTransmitterMock"); + const messageTransmitter = await ethers.getContract( + "CCTPMessageTransmitterMock" + ); const tokenMessenger = await ethers.getContract("CCTPTokenMessengerMock"); return { @@ -2900,9 +2902,23 @@ async function crossChainFixture() { addresses.CrossChainStrategyProxy ); + await deployWithConfirmation("CCTPMessageTransmitterMock", [ + fixture.usdc.address, + ]); + const mockMessageTransmitter = await ethers.getContract( + "CCTPMessageTransmitterMock" + ); + await deployWithConfirmation("CCTPTokenMessengerMock", [ + fixture.usdc.address, + mockMessageTransmitter.address, + ]); + const mockTokenMessenger = await ethers.getContract("CCTPTokenMessengerMock"); + return { ...fixture, crossChainMasterStrategy: cCrossChainMasterStrategy, + mockMessageTransmitter: mockMessageTransmitter, + mockTokenMessenger: mockTokenMessenger, }; } diff --git a/contracts/test/strategies/crosschain/cross-chain-strategy.js b/contracts/test/strategies/crosschain/cross-chain-strategy.js index 50b42ffcd1..10199f66e8 100644 --- a/contracts/test/strategies/crosschain/cross-chain-strategy.js +++ b/contracts/test/strategies/crosschain/cross-chain-strategy.js @@ -1,10 +1,11 @@ // const { expect } = require("chai"); const { isCI } = require("../../helpers"); -const { createFixtureLoader, crossChainFixtureUnit } = require("../../_fixture"); const { - units -} = require("../../helpers"); + createFixtureLoader, + crossChainFixtureUnit, +} = require("../../_fixture"); +const { units } = require("../../helpers"); const loadFixture = createFixtureLoader(crossChainFixtureUnit); @@ -30,14 +31,10 @@ describe("ForkTest: CrossChainRemoteStrategy", function () { crossChainMasterStrategy = fixture.crossChainMasterStrategy; vault = fixture.vault; }); - + const mint = async (amount) => { - await usdc - .connect(josh) - .approve(vault.address, await units(amount, usdc)); - await vault - .connect(josh) - .mint(usdc.address, await units(amount, usdc), 0); + await usdc.connect(josh).approve(vault.address, await units(amount, usdc)); + await vault.connect(josh).mint(usdc.address, await units(amount, usdc), 0); }; const depositToMasterStrategy = async (amount) => { @@ -49,10 +46,14 @@ describe("ForkTest: CrossChainRemoteStrategy", function () { [await units(amount, usdc)] ); }; - + const depositToStrategy = async (amount) => { - await usdc.connect(josh).approve(crossChainRemoteStrategy.address, await units(amount, usdc)); - await crossChainRemoteStrategy.connect(josh).depositToStrategy(amount, usdc.address); + await usdc + .connect(josh) + .approve(crossChainRemoteStrategy.address, await units(amount, usdc)); + await crossChainRemoteStrategy + .connect(josh) + .depositToStrategy(amount, usdc.address); }; it("Should initiate a bridge of deposited USDC", async function () { diff --git a/contracts/test/strategies/crosschain/crosschain-master-strategy.mainnet.fork-test.js b/contracts/test/strategies/crosschain/crosschain-master-strategy.mainnet.fork-test.js index b58788a2c7..8ff5188bbe 100644 --- a/contracts/test/strategies/crosschain/crosschain-master-strategy.mainnet.fork-test.js +++ b/contracts/test/strategies/crosschain/crosschain-master-strategy.mainnet.fork-test.js @@ -7,6 +7,7 @@ const { impersonateAndFund } = require("../../../utils/signers"); const addresses = require("../../../utils/addresses"); const loadFixture = createFixtureLoader(crossChainFixture); const { setStorageAt } = require("@nomicfoundation/hardhat-network-helpers"); +const { replaceContractAt } = require("../../../utils/hardhat"); const DEPOSIT_FOR_BURN_EVENT_TOPIC = "0x0c8c1cbdc5190613ebd485511d4e2812cfa45eecb79d845893331fedad5130a5"; @@ -15,6 +16,133 @@ const MESSAGE_SENT_EVENT_TOPIC = // const ORIGIN_MESSAGE_VERSION_HEX = "0x000003f2"; // 1010 +const decodeDepositForBurnEvent = (event) => { + const [ + amount, + mintRecipient, + destinationDomain, + destinationTokenMessenger, + destinationCaller, + maxFee, + hookData, + ] = ethers.utils.defaultAbiCoder.decode( + ["uint256", "address", "uint32", "address", "address", "uint256", "bytes"], + event.data + ); + + const [burnToken] = ethers.utils.defaultAbiCoder.decode( + ["address"], + event.topics[1] + ); + const [depositer] = ethers.utils.defaultAbiCoder.decode( + ["address"], + event.topics[2] + ); + const [minFinalityThreshold] = ethers.utils.defaultAbiCoder.decode( + ["uint256"], + event.topics[3] + ); + + return { + amount, + mintRecipient, + destinationDomain, + destinationTokenMessenger, + destinationCaller, + maxFee, + hookData, + burnToken, + depositer, + minFinalityThreshold, + }; +}; + +const decodeMessageSentEvent = (event) => { + const evData = event.data.slice(130); // ignore first two slots along with 0x prefix + + const version = ethers.BigNumber.from(`0x${evData.slice(0, 8)}`); + const sourceDomain = ethers.BigNumber.from(`0x${evData.slice(8, 16)}`); + const desinationDomain = ethers.BigNumber.from(`0x${evData.slice(16, 24)}`); + // Ignore empty nonce from 24 to 88 + const [sender, recipient, destinationCaller] = + ethers.utils.defaultAbiCoder.decode( + ["address", "address", "address"], + `0x${evData.slice(88, 280)}` + ); + const minFinalityThreshold = ethers.BigNumber.from( + `0x${evData.slice(280, 288)}` + ); + // Ignore empty threshold from 288 to 296 + const payload = `0x${evData.slice(296, evData.length - 8)}`; + + return { + version, + sourceDomain, + desinationDomain, + sender, + recipient, + destinationCaller, + minFinalityThreshold, + payload, + }; +}; + +const decodeDepositOrWithdrawMessage = (message) => { + message = message.slice(2); // Ignore 0x prefix + + const originMessageVersion = ethers.BigNumber.from( + `0x${message.slice(0, 8)}` + ); + const messageType = ethers.BigNumber.from(`0x${message.slice(8, 16)}`); + expect(originMessageVersion).to.eq(1010); + + const [nonce, amount] = ethers.utils.defaultAbiCoder.decode( + ["uint64", "uint256"], + `0x${message.slice(16)}` + ); + + return { + messageType, + nonce, + amount, + }; +}; + +const encodeCCTPMessage = ( + sourceDomain, + sender, + recipient, + messageBody, + version = 1 +) => { + const versionStr = version.toString(16).padStart(8, "0"); + const sourceDomainStr = sourceDomain.toString(16).padStart(8, "0"); + const senderStr = sender.replace("0x", "").toLowerCase().padStart(64, "0"); + const recipientStr = recipient + .replace("0x", "") + .toLowerCase() + .padStart(64, "0"); + const messageBodyStr = messageBody.slice(2); + const emptyByte = "0000"; + const empty2Bytes = emptyByte.repeat(2); + const empty4Bytes = emptyByte.repeat(4); + const empty16Bytes = empty4Bytes.repeat(4); + const empty18Bytes = `${empty2Bytes}${empty16Bytes}`; + const empty20Bytes = empty4Bytes.repeat(5); + return `0x${versionStr}${sourceDomainStr}${empty18Bytes}${senderStr}${recipientStr}${empty20Bytes}${messageBodyStr}`; +}; + +const encodeBalanceCheckMessageBody = (nonce, balance) => { + const encodedPayload = ethers.utils.defaultAbiCoder.encode( + ["uint64", "uint256"], + [nonce, balance] + ); + + // const version = 1010; // ORIGIN_MESSAGE_VERSION + // const messageType = 3; // BALANCE_CHECK_MESSAGE + return `0x000003f200000003${encodedPayload.slice(2)}`; +}; + describe("ForkTest: CrossChainMasterStrategy", function () { this.timeout(0); @@ -26,260 +154,452 @@ describe("ForkTest: CrossChainMasterStrategy", function () { fixture = await loadFixture(); }); - const decodeDepositForBurnEvent = (event) => { - const [ - amount, - mintRecipient, - destinationDomain, - destinationTokenMessenger, - destinationCaller, - maxFee, - hookData, - ] = ethers.utils.defaultAbiCoder.decode( - [ - "uint256", - "address", - "uint32", - "address", - "address", - "uint256", - "bytes", - ], - event.data - ); + describe("Message sending", function () { + it("Should initiate bridging of deposited USDC", async function () { + const { matt, crossChainMasterStrategy, usdc } = fixture; - const [burnToken] = ethers.utils.defaultAbiCoder.decode( - ["address"], - event.topics[1] - ); - const [depositer] = ethers.utils.defaultAbiCoder.decode( - ["address"], - event.topics[2] - ); - const [minFinalityThreshold] = ethers.utils.defaultAbiCoder.decode( - ["uint256"], - event.topics[3] - ); + if (await crossChainMasterStrategy.isTransferPending()) { + // Skip if there's a pending transfer + console.log( + "Skipping deposit fork test because there's a pending transfer" + ); + return; + } - return { - amount, - mintRecipient, - destinationDomain, - destinationTokenMessenger, - destinationCaller, - maxFee, - hookData, - burnToken, - depositer, - minFinalityThreshold, - }; - }; + const vaultAddr = await crossChainMasterStrategy.vaultAddress(); + + const impersonatedVault = await impersonateAndFund(vaultAddr); - const decodeMessageSentEvent = (event) => { - const evData = event.data.slice(130); // ignore first two slots along with 0x prefix + // Let the strategy hold some USDC + await usdc + .connect(matt) + .transfer(crossChainMasterStrategy.address, usdcUnits("1000")); - const version = ethers.BigNumber.from(`0x${evData.slice(0, 8)}`); - const sourceDomain = ethers.BigNumber.from(`0x${evData.slice(8, 16)}`); - const desinationDomain = ethers.BigNumber.from(`0x${evData.slice(16, 24)}`); - // Ignore empty nonce from 24 to 88 - const [sender, recipient, destinationCaller] = - ethers.utils.defaultAbiCoder.decode( - ["address", "address", "address"], - `0x${evData.slice(88, 280)}` + const usdcBalanceBefore = await usdc.balanceOf( + crossChainMasterStrategy.address + ); + const strategyBalanceBefore = await crossChainMasterStrategy.checkBalance( + usdc.address ); - const minFinalityThreshold = ethers.BigNumber.from( - `0x${evData.slice(280, 288)}` - ); - // Ignore empty threshold from 288 to 296 - const payload = `0x${evData.slice(296, evData.length - 8)}`; - - return { - version, - sourceDomain, - desinationDomain, - sender, - recipient, - destinationCaller, - minFinalityThreshold, - payload, - }; - }; - const decodeDepositOrWithdrawMessage = (message) => { - message = message.slice(2); // Ignore 0x prefix + // Simulate deposit call + const tx = await crossChainMasterStrategy + .connect(impersonatedVault) + .deposit(usdc.address, usdcUnits("1000")); - const originMessageVersion = ethers.BigNumber.from( - `0x${message.slice(0, 8)}` - ); - const messageType = ethers.BigNumber.from(`0x${message.slice(8, 16)}`); - expect(originMessageVersion).to.eq(1010); + const usdcBalanceAfter = await usdc.balanceOf( + crossChainMasterStrategy.address + ); + expect(usdcBalanceAfter).to.eq(usdcBalanceBefore.sub(usdcUnits("1000"))); - const [nonce, amount] = ethers.utils.defaultAbiCoder.decode( - ["uint64", "uint256"], - `0x${message.slice(16)}` - ); + const strategyBalanceAfter = await crossChainMasterStrategy.checkBalance( + usdc.address + ); + expect(strategyBalanceAfter).to.eq(strategyBalanceBefore); - return { - messageType, - nonce, - amount, - }; - }; + expect(await crossChainMasterStrategy.pendingAmount()).to.eq( + usdcUnits("1000") + ); - it("Should initiate bridging of deposited USDC", async function () { - const { matt, crossChainMasterStrategy, usdc } = fixture; + // Check for message sent event + const receipt = await tx.wait(); + const depositForBurnEvent = receipt.events.find((e) => + e.topics.includes(DEPOSIT_FOR_BURN_EVENT_TOPIC) + ); + const burnEventData = decodeDepositForBurnEvent(depositForBurnEvent); - if (await crossChainMasterStrategy.isTransferPending()) { - // Skip if there's a pending transfer - console.log( - "Skipping deposit fork test because there's a pending transfer" + expect(burnEventData.amount).to.eq(usdcUnits("1000")); + expect(burnEventData.mintRecipient.toLowerCase()).to.eq( + crossChainMasterStrategy.address.toLowerCase() + ); + expect(burnEventData.destinationDomain).to.eq(6); + expect(burnEventData.destinationTokenMessenger.toLowerCase()).to.eq( + addresses.CCTPTokenMessengerV2.toLowerCase() + ); + expect(burnEventData.destinationCaller.toLowerCase()).to.eq( + crossChainMasterStrategy.address.toLowerCase() ); - return; - } + expect(burnEventData.maxFee).to.eq(0); + expect(burnEventData.burnToken).to.eq(usdc.address); - const vaultAddr = await crossChainMasterStrategy.vaultAddress(); + expect(burnEventData.depositer.toLowerCase()).to.eq( + crossChainMasterStrategy.address.toLowerCase() + ); + expect(burnEventData.minFinalityThreshold).to.eq(2000); + expect(burnEventData.burnToken.toLowerCase()).to.eq( + usdc.address.toLowerCase() + ); - const impersonatedVault = await impersonateAndFund(vaultAddr); + // Decode and verify payload + const { messageType, nonce, amount } = decodeDepositOrWithdrawMessage( + burnEventData.hookData + ); + expect(messageType).to.eq(1); + expect(nonce).to.eq(1); + expect(amount).to.eq(usdcUnits("1000")); + }); + + it("Should request withdrawal", async function () { + const { crossChainMasterStrategy, usdc } = fixture; + + if (await crossChainMasterStrategy.isTransferPending()) { + // Skip if there's a pending transfer + console.log( + "Skipping deposit fork test because there's a pending transfer" + ); + return; + } + + const vaultAddr = await crossChainMasterStrategy.vaultAddress(); + const impersonatedVault = await impersonateAndFund(vaultAddr); + + // set an arbitrary remote strategy balance + const remoteStrategyBalanceSlot = 209; // Slot 209 + await setStorageAt( + crossChainMasterStrategy.address, + `0x${remoteStrategyBalanceSlot.toString(16)}`, + usdcUnits("1000").toHexString() + ); - // Let the strategy hold some USDC - await usdc - .connect(matt) - .transfer(crossChainMasterStrategy.address, usdcUnits("1000")); + const tx = await crossChainMasterStrategy + .connect(impersonatedVault) + .withdraw(vaultAddr, usdc.address, usdcUnits("1000")); + const receipt = await tx.wait(); + const messageSentEvent = receipt.events.find((e) => + e.topics.includes(MESSAGE_SENT_EVENT_TOPIC) + ); - const usdcBalanceBefore = await usdc.balanceOf( - crossChainMasterStrategy.address - ); - const strategyBalanceBefore = await crossChainMasterStrategy.checkBalance( - usdc.address - ); + const { + version, + sourceDomain, + desinationDomain, + sender, + recipient, + destinationCaller, + minFinalityThreshold, + payload, + } = decodeMessageSentEvent(messageSentEvent); + + expect(version).to.eq(1); + expect(sourceDomain).to.eq(0); + expect(desinationDomain).to.eq(6); + expect(sender.toLowerCase()).to.eq( + crossChainMasterStrategy.address.toLowerCase() + ); + expect(recipient.toLowerCase()).to.eq( + crossChainMasterStrategy.address.toLowerCase() + ); + expect(destinationCaller.toLowerCase()).to.eq( + crossChainMasterStrategy.address.toLowerCase() + ); + expect(minFinalityThreshold).to.eq(2000); + + // Decode and verify payload + const { messageType, nonce, amount } = + decodeDepositOrWithdrawMessage(payload); + expect(messageType).to.eq(2); + expect(nonce).to.eq(1); + expect(amount).to.eq(usdcUnits("1000")); + }); + }); - // Simulate deposit call - const tx = await crossChainMasterStrategy - .connect(impersonatedVault) - .deposit(usdc.address, usdcUnits("1000")); + describe("Message receiving", function () { + it("Should handle balance check message", async function () { + const { crossChainMasterStrategy, mockMessageTransmitter, strategist } = + fixture; + + if (await crossChainMasterStrategy.isTransferPending()) { + // Skip if there's a pending transfer + console.log( + "Skipping balance check message fork test because there's a pending transfer" + ); + return; + } + + const lastNonce = ( + await crossChainMasterStrategy.lastTransferNonce() + ).toNumber(); + + // Replace transmitter to mock transmitter + await replaceContractAt( + await crossChainMasterStrategy.cctpMessageTransmitter(), + mockMessageTransmitter + ); - const usdcBalanceAfter = await usdc.balanceOf( - crossChainMasterStrategy.address - ); - expect(usdcBalanceAfter).to.eq(usdcBalanceBefore.sub(usdcUnits("1000"))); + // Build check balance payload + const payload = encodeBalanceCheckMessageBody( + lastNonce, + usdcUnits("12345") + ); + const message = encodeCCTPMessage( + 6, + crossChainMasterStrategy.address, + crossChainMasterStrategy.address, + payload + ); - const strategyBalanceAfter = await crossChainMasterStrategy.checkBalance( - usdc.address - ); - expect(strategyBalanceAfter).to.eq(strategyBalanceBefore); + // Relay the message with fake attestation + await crossChainMasterStrategy.connect(strategist).relay(message, "0x"); + + const remoteStrategyBalance = + await crossChainMasterStrategy.remoteStrategyBalance(); + expect(remoteStrategyBalance).to.eq(usdcUnits("12345")); + }); + + it("Should handle balance check message for a pending deposit", async function () { + const { + crossChainMasterStrategy, + mockMessageTransmitter, + strategist, + usdc, + matt, + } = fixture; + + if (await crossChainMasterStrategy.isTransferPending()) { + // Skip if there's a pending transfer + console.log( + "Skipping balance check message fork test because there's a pending transfer" + ); + return; + } + + // Do a pre-deposit + const vaultAddr = await crossChainMasterStrategy.vaultAddress(); + + const impersonatedVault = await impersonateAndFund(vaultAddr); + + // Let the strategy hold some USDC + await usdc + .connect(matt) + .transfer(crossChainMasterStrategy.address, usdcUnits("1000")); + + // Simulate deposit call + await crossChainMasterStrategy + .connect(impersonatedVault) + .deposit(usdc.address, usdcUnits("1000")); + + const lastNonce = ( + await crossChainMasterStrategy.lastTransferNonce() + ).toNumber(); + + // Replace transmitter to mock transmitter + await replaceContractAt( + await crossChainMasterStrategy.cctpMessageTransmitter(), + mockMessageTransmitter + ); - expect(await crossChainMasterStrategy.pendingAmount()).to.eq( - usdcUnits("1000") - ); + // Build check balance payload + const payload = encodeBalanceCheckMessageBody( + lastNonce, + usdcUnits("10000") + ); + const message = encodeCCTPMessage( + 6, + crossChainMasterStrategy.address, + crossChainMasterStrategy.address, + payload + ); - // Check for message sent event - const receipt = await tx.wait(); - const depositForBurnEvent = receipt.events.find((e) => - e.topics.includes(DEPOSIT_FOR_BURN_EVENT_TOPIC) - ); - const burnEventData = decodeDepositForBurnEvent(depositForBurnEvent); + // Relay the message with fake attestation + await crossChainMasterStrategy.connect(strategist).relay(message, "0x"); - expect(burnEventData.amount).to.eq(usdcUnits("1000")); - expect(burnEventData.mintRecipient.toLowerCase()).to.eq( - crossChainMasterStrategy.address.toLowerCase() - ); - expect(burnEventData.destinationDomain).to.eq(6); - expect(burnEventData.destinationTokenMessenger.toLowerCase()).to.eq( - addresses.CCTPTokenMessengerV2.toLowerCase() - ); - expect(burnEventData.destinationCaller.toLowerCase()).to.eq( - crossChainMasterStrategy.address.toLowerCase() - ); - expect(burnEventData.maxFee).to.eq(0); - expect(burnEventData.burnToken).to.eq(usdc.address); + const remoteStrategyBalance = + await crossChainMasterStrategy.remoteStrategyBalance(); + // We did a deposit of 1000 USDC but had the remote strategy report 10k for the test. + expect(remoteStrategyBalance).to.eq(usdcUnits("10000")); - expect(burnEventData.depositer.toLowerCase()).to.eq( - crossChainMasterStrategy.address.toLowerCase() - ); - expect(burnEventData.minFinalityThreshold).to.eq(2000); - expect(burnEventData.burnToken.toLowerCase()).to.eq( - usdc.address.toLowerCase() - ); + expect(await crossChainMasterStrategy.pendingAmount()).to.eq( + usdcUnits("0") + ); + }); + + it.skip("Should accept tokens for a pending withdrawal", async function () { + // TODO: + }); + + it("Should ignore balance check message for a pending withdrawal", async function () { + const { + crossChainMasterStrategy, + mockMessageTransmitter, + strategist, + usdc, + } = fixture; + + if (await crossChainMasterStrategy.isTransferPending()) { + // Skip if there's a pending transfer + console.log( + "Skipping balance check message fork test because there's a pending transfer" + ); + return; + } + + const vaultAddr = await crossChainMasterStrategy.vaultAddress(); + const impersonatedVault = await impersonateAndFund(vaultAddr); + + // set an arbitrary remote strategy balance + const remoteStrategyBalanceSlot = 209; // Slot 209 + await setStorageAt( + crossChainMasterStrategy.address, + `0x${remoteStrategyBalanceSlot.toString(16)}`, + usdcUnits("1000").toHexString() + ); - // Decode and verify payload - const { messageType, nonce, amount } = decodeDepositOrWithdrawMessage( - burnEventData.hookData - ); - expect(messageType).to.eq(1); - expect(nonce).to.eq(1); - expect(amount).to.eq(usdcUnits("1000")); - }); + const remoteStrategyBalanceBefore = + await crossChainMasterStrategy.remoteStrategyBalance(); + + // Simulate withdrawal call + await crossChainMasterStrategy + .connect(impersonatedVault) + .withdraw(vaultAddr, usdc.address, usdcUnits("1000")); - it("Should request withdrawal", async function () { - const { crossChainMasterStrategy, usdc } = fixture; + const lastNonce = ( + await crossChainMasterStrategy.lastTransferNonce() + ).toNumber(); - if (await crossChainMasterStrategy.isTransferPending()) { - // Skip if there's a pending transfer - console.log( - "Skipping deposit fork test because there's a pending transfer" + // Replace transmitter to mock transmitter + await replaceContractAt( + await crossChainMasterStrategy.cctpMessageTransmitter(), + mockMessageTransmitter ); - return; - } - const vaultAddr = await crossChainMasterStrategy.vaultAddress(); - const impersonatedVault = await impersonateAndFund(vaultAddr); + // Build check balance payload + const payload = encodeBalanceCheckMessageBody( + lastNonce, + usdcUnits("10000") + ); + const message = encodeCCTPMessage( + 6, + crossChainMasterStrategy.address, + crossChainMasterStrategy.address, + payload + ); - // set an arbitrary remote strategy balance - const remoteStrategyBalanceSlot = 209; // Slot 209 - await setStorageAt( - crossChainMasterStrategy.address, - `0x${remoteStrategyBalanceSlot.toString(16)}`, - usdcUnits("1000").toHexString() - ); + // Relay the message with fake attestation + await crossChainMasterStrategy.connect(strategist).relay(message, "0x"); + + // Should've ignore the message + const remoteStrategyBalance = + await crossChainMasterStrategy.remoteStrategyBalance(); + expect(remoteStrategyBalance).to.eq(remoteStrategyBalanceBefore); + }); + + it("Should ignore balance check message with older nonce", async function () { + const { + crossChainMasterStrategy, + mockMessageTransmitter, + strategist, + matt, + usdc, + } = fixture; + + if (await crossChainMasterStrategy.isTransferPending()) { + // Skip if there's a pending transfer + console.log( + "Skipping balance check message fork test because there's a pending transfer" + ); + return; + } + + const lastNonce = ( + await crossChainMasterStrategy.lastTransferNonce() + ).toNumber(); + + // Do a pre-deposit + const vaultAddr = await crossChainMasterStrategy.vaultAddress(); + + const impersonatedVault = await impersonateAndFund(vaultAddr); + + // Let the strategy hold some USDC + await usdc + .connect(matt) + .transfer(crossChainMasterStrategy.address, usdcUnits("1000")); + + // Simulate deposit call + await crossChainMasterStrategy + .connect(impersonatedVault) + .deposit(usdc.address, usdcUnits("1000")); + + const remoteStrategyBalanceBefore = + await crossChainMasterStrategy.remoteStrategyBalance(); + + // Replace transmitter to mock transmitter + await replaceContractAt( + await crossChainMasterStrategy.cctpMessageTransmitter(), + mockMessageTransmitter + ); - const tx = await crossChainMasterStrategy - .connect(impersonatedVault) - .withdraw(vaultAddr, usdc.address, usdcUnits("1000")); - const receipt = await tx.wait(); - const messageSentEvent = receipt.events.find((e) => - e.topics.includes(MESSAGE_SENT_EVENT_TOPIC) - ); + // Build check balance payload + const payload = encodeBalanceCheckMessageBody( + lastNonce, + usdcUnits("123244") + ); + const message = encodeCCTPMessage( + 6, + crossChainMasterStrategy.address, + crossChainMasterStrategy.address, + payload + ); - const { - version, - sourceDomain, - desinationDomain, - sender, - recipient, - destinationCaller, - minFinalityThreshold, - payload, - } = decodeMessageSentEvent(messageSentEvent); - - expect(version).to.eq(1); - expect(sourceDomain).to.eq(0); - expect(desinationDomain).to.eq(6); - expect(sender.toLowerCase()).to.eq( - crossChainMasterStrategy.address.toLowerCase() - ); - expect(recipient.toLowerCase()).to.eq( - crossChainMasterStrategy.address.toLowerCase() - ); - expect(destinationCaller.toLowerCase()).to.eq( - crossChainMasterStrategy.address.toLowerCase() - ); - expect(minFinalityThreshold).to.eq(2000); - - // Decode and verify payload - const { messageType, nonce, amount } = - decodeDepositOrWithdrawMessage(payload); - expect(messageType).to.eq(2); - expect(nonce).to.eq(1); - expect(amount).to.eq(usdcUnits("1000")); - }); + // Relay the message with fake attestation + await crossChainMasterStrategy.connect(strategist).relay(message, "0x"); + + const remoteStrategyBalance = + await crossChainMasterStrategy.remoteStrategyBalance(); + expect(remoteStrategyBalance).to.eq(remoteStrategyBalanceBefore); + }); + + it("Should ignore if nonce is higher", async function () { + const { crossChainMasterStrategy, mockMessageTransmitter, strategist } = + fixture; + + if (await crossChainMasterStrategy.isTransferPending()) { + // Skip if there's a pending transfer + console.log( + "Skipping balance check message fork test because there's a pending transfer" + ); + return; + } + + const lastNonce = ( + await crossChainMasterStrategy.lastTransferNonce() + ).toNumber(); + + // Replace transmitter to mock transmitter + await replaceContractAt( + await crossChainMasterStrategy.cctpMessageTransmitter(), + mockMessageTransmitter + ); + + const remoteStrategyBalanceBefore = + await crossChainMasterStrategy.remoteStrategyBalance(); - it.skip("Should handle attestation relay", async function () { - const { crossChainMasterStrategy } = fixture; - const attestation = - "0xc0ee7623da7bad1b2607f12c21ce71c4314b4ade3d36a0e6e13753fbb0603daa2b10fcbbc4942ce75a2b8d5f5c11f4b6c5ee5f8dce4663d3ec834674d0a9991a1cdeb52adf17d5fb3222b1f94f0767175f06e69f9473e7f948a4b5c478814f11915ed64081cbe6e139fd277630b8807b56be7c355ccdda6c20acbf0324231fc8301b"; - const message = - "0x0000000100000006000000000384bc6f6bfe10f6df4967b6ad287d897ff729f0c7e43f73a1e18ab156e96bfb0000000000000000000000008ebcca1066d15ad901927ab01c7c6d0b057bbd340000000000000000000000008ebcca1066d15ad901927ab01c7c6d0b057bbd3400000000000000000000000030f8a2fc7d7098061c94f042b2e7e732f95af40f00000000000003e8000003f20000000300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"; + // Build check balance payload + const payload = encodeBalanceCheckMessageBody( + lastNonce + 2, + usdcUnits("123244") + ); + const message = encodeCCTPMessage( + 6, + crossChainMasterStrategy.address, + crossChainMasterStrategy.address, + payload + ); - await crossChainMasterStrategy.relay(message, attestation); + // Relay the message with fake attestation + await crossChainMasterStrategy.connect(strategist).relay(message, "0x"); + const remoteStrategyBalanceAfter = + await crossChainMasterStrategy.remoteStrategyBalance(); + expect(remoteStrategyBalanceAfter).to.eq(remoteStrategyBalanceBefore); + }); }); + + // it.skip("Should handle attestation relay", async function () { + // const { crossChainMasterStrategy } = fixture; + // const attestation = + // "0xc0ee7623da7bad1b2607f12c21ce71c4314b4ade3d36a0e6e13753fbb0603daa2b10fcbbc4942ce75a2b8d5f5c11f4b6c5ee5f8dce4663d3ec834674d0a9991a1cdeb52adf17d5fb3222b1f94f0767175f06e69f9473e7f948a4b5c478814f11915ed64081cbe6e139fd277630b8807b56be7c355ccdda6c20acbf0324231fc8301b"; + // const message = + // "0x0000000100000006000000000384bc6f6bfe10f6df4967b6ad287d897ff729f0c7e43f73a1e18ab156e96bfb0000000000000000000000008ebcca1066d15ad901927ab01c7c6d0b057bbd340000000000000000000000008ebcca1066d15ad901927ab01c7c6d0b057bbd3400000000000000000000000030f8a2fc7d7098061c94f042b2e7e732f95af40f00000000000003e8000003f20000000300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"; + + // await crossChainMasterStrategy.relay(message, attestation); + // }); }); From 3585d9d0983bcf467756dae9e6a6f6401f5b1c93 Mon Sep 17 00:00:00 2001 From: Shahul Hameed <10547529+shahthepro@users.noreply.github.com> Date: Tue, 23 Dec 2025 15:08:21 +0400 Subject: [PATCH 033/101] Add token transfer tests --- .../crosschain/CCTPMessageTransmitterMock.sol | 34 ++++- contracts/deploy/mainnet/000_mock.js | 5 + contracts/test/_fixture.js | 10 ++ ...chain-master-strategy.mainnet.fork-test.js | 120 ++++++++++++++++-- 4 files changed, 150 insertions(+), 19 deletions(-) diff --git a/contracts/contracts/mocks/crosschain/CCTPMessageTransmitterMock.sol b/contracts/contracts/mocks/crosschain/CCTPMessageTransmitterMock.sol index 1112fbd038..f31064d681 100644 --- a/contracts/contracts/mocks/crosschain/CCTPMessageTransmitterMock.sol +++ b/contracts/contracts/mocks/crosschain/CCTPMessageTransmitterMock.sol @@ -14,6 +14,11 @@ uint8 constant SENDER_INDEX = 44; uint8 constant RECIPIENT_INDEX = 76; uint8 constant MESSAGE_BODY_INDEX = 148; +// 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 constant BURN_MESSAGE_V2_RECIPIENT_INDEX = 36; + /** * @title Mock conctract simulating the functionality of the CCTPTokenMessenger contract * for the porposes of unit testing. @@ -27,6 +32,7 @@ contract CCTPMessageTransmitterMock is ICCTPMessageTransmitter { uint256 public nonce = 0; bool public shouldRevertNextReceiveMessage; + address public cctpTokenMessenger; event MessageReceivedInMockTransmitter(bytes message); @@ -50,6 +56,10 @@ contract CCTPMessageTransmitterMock is ICCTPMessageTransmitter { usdc = IERC20(_usdc); } + function setCCTPTokenMessenger(address _cctpTokenMessenger) external { + cctpTokenMessenger = _cctpTokenMessenger; + } + // @dev for the porposes of unit tests queues the message to be mock-sent using // the cctp bridge. function sendMessage( @@ -119,13 +129,27 @@ contract CCTPMessageTransmitterMock is ICCTPMessageTransmitter { uint32 sourceDomain = message.extractUint32(SOURCE_DOMAIN_INDEX); address recipient = message.extractAddress(RECIPIENT_INDEX); address sender = message.extractAddress(SENDER_INDEX); - IMessageHandlerV2(recipient).handleReceiveFinalizedMessage( - sourceDomain, - bytes32(uint256(uint160(sender))), - 2000, - message.extractSlice(MESSAGE_BODY_INDEX, message.length) + + bytes memory messageBody = message.extractSlice( + MESSAGE_BODY_INDEX, + message.length ); + bool isBurnMessage = recipient == cctpTokenMessenger; + + if (isBurnMessage) { + // recipient = messageBody.extractAddress(BURN_MESSAGE_V2_RECIPIENT_INDEX); + // This step won't mint USDC, transfer it to the recipient address + // in your tests + } else { + IMessageHandlerV2(recipient).handleReceiveFinalizedMessage( + sourceDomain, + bytes32(uint256(uint160(sender))), + 2000, + messageBody + ); + } + // This step won't mint USDC, transfer it to the recipient address // in your tests emit MessageReceivedInMockTransmitter(message); diff --git a/contracts/deploy/mainnet/000_mock.js b/contracts/deploy/mainnet/000_mock.js index 379446b49c..514b413aca 100644 --- a/contracts/deploy/mainnet/000_mock.js +++ b/contracts/deploy/mainnet/000_mock.js @@ -28,6 +28,7 @@ const { const deployMocks = async ({ getNamedAccounts, deployments }) => { const { deploy } = deployments; const { deployerAddr, governorAddr } = await getNamedAccounts(); + const sDeployer = await ethers.provider.getSigner(deployerAddr); console.log("Running 000_mock deployment..."); console.log("Deployer address", deployerAddr); @@ -458,6 +459,10 @@ const deployMocks = async ({ getNamedAccounts, deployments }) => { from: deployerAddr, args: [usdc.address, messageTransmitter.address], }); + const tokenMessenger = await ethers.getContract("CCTPTokenMessengerMock"); + await messageTransmitter + .connect(sDeployer) + .setCCTPTokenMessenger(tokenMessenger.address); console.log("000_mock deploy done."); diff --git a/contracts/test/_fixture.js b/contracts/test/_fixture.js index 08866735af..f444c6c0ed 100644 --- a/contracts/test/_fixture.js +++ b/contracts/test/_fixture.js @@ -24,6 +24,7 @@ const { getOracleAddresses, oethUnits, ousdUnits, + usdcUnits, units, isTest, isFork, @@ -2913,6 +2914,15 @@ async function crossChainFixture() { mockMessageTransmitter.address, ]); const mockTokenMessenger = await ethers.getContract("CCTPTokenMessengerMock"); + await mockMessageTransmitter.setCCTPTokenMessenger( + addresses.CCTPTokenMessengerV2 + ); + + await setERC20TokenBalance( + fixture.matt.address, + fixture.usdc, + usdcUnits("1000000") + ); return { ...fixture, diff --git a/contracts/test/strategies/crosschain/crosschain-master-strategy.mainnet.fork-test.js b/contracts/test/strategies/crosschain/crosschain-master-strategy.mainnet.fork-test.js index 8ff5188bbe..15cec8baf5 100644 --- a/contracts/test/strategies/crosschain/crosschain-master-strategy.mainnet.fork-test.js +++ b/contracts/test/strategies/crosschain/crosschain-master-strategy.mainnet.fork-test.js @@ -16,6 +16,15 @@ const MESSAGE_SENT_EVENT_TOPIC = // const ORIGIN_MESSAGE_VERSION_HEX = "0x000003f2"; // 1010 +const emptyByte = "0000"; +const empty2Bytes = emptyByte.repeat(2); +const empty4Bytes = emptyByte.repeat(4); +const empty16Bytes = empty4Bytes.repeat(4); +const empty18Bytes = `${empty2Bytes}${empty16Bytes}`; +const empty20Bytes = empty4Bytes.repeat(5); + +const REMOTE_STRATEGY_BALANCE_SLOT = 210; + const decodeDepositForBurnEvent = (event) => { const [ amount, @@ -123,15 +132,25 @@ const encodeCCTPMessage = ( .toLowerCase() .padStart(64, "0"); const messageBodyStr = messageBody.slice(2); - const emptyByte = "0000"; - const empty2Bytes = emptyByte.repeat(2); - const empty4Bytes = emptyByte.repeat(4); - const empty16Bytes = empty4Bytes.repeat(4); - const empty18Bytes = `${empty2Bytes}${empty16Bytes}`; - const empty20Bytes = empty4Bytes.repeat(5); return `0x${versionStr}${sourceDomainStr}${empty18Bytes}${senderStr}${recipientStr}${empty20Bytes}${messageBodyStr}`; }; +const encodeBurnMessageBody = (sender, recipient, amount, hookData) => { + const senderEncoded = ethers.utils.defaultAbiCoder + .encode(["address"], [sender]) + .slice(2); + const recipientEncoded = ethers.utils.defaultAbiCoder + .encode(["address"], [recipient]) + .slice(2); + const amountEncoded = ethers.utils.defaultAbiCoder + .encode(["uint256"], [amount]) + .slice(2); + const encodedHookData = hookData.slice(2); + return `0x00000001${empty16Bytes}${recipientEncoded}${amountEncoded}${senderEncoded}${empty16Bytes.repeat( + 3 + )}${encodedHookData}`; +}; + const encodeBalanceCheckMessageBody = (nonce, balance) => { const encodedPayload = ethers.utils.defaultAbiCoder.encode( ["uint64", "uint256"], @@ -254,10 +273,9 @@ describe("ForkTest: CrossChainMasterStrategy", function () { const impersonatedVault = await impersonateAndFund(vaultAddr); // set an arbitrary remote strategy balance - const remoteStrategyBalanceSlot = 209; // Slot 209 await setStorageAt( crossChainMasterStrategy.address, - `0x${remoteStrategyBalanceSlot.toString(16)}`, + `0x${REMOTE_STRATEGY_BALANCE_SLOT.toString(16)}`, usdcUnits("1000").toHexString() ); @@ -327,7 +345,7 @@ describe("ForkTest: CrossChainMasterStrategy", function () { ); // Build check balance payload - const payload = encodeBalanceCheckMessageBody( + const balancePayload = encodeBalanceCheckMessageBody( lastNonce, usdcUnits("12345") ); @@ -335,7 +353,7 @@ describe("ForkTest: CrossChainMasterStrategy", function () { 6, crossChainMasterStrategy.address, crossChainMasterStrategy.address, - payload + balancePayload ); // Relay the message with fake attestation @@ -413,8 +431,83 @@ describe("ForkTest: CrossChainMasterStrategy", function () { ); }); - it.skip("Should accept tokens for a pending withdrawal", async function () { - // TODO: + it("Should accept tokens for a pending withdrawal", async function () { + const { + crossChainMasterStrategy, + mockMessageTransmitter, + strategist, + matt, + usdc, + } = fixture; + + if (await crossChainMasterStrategy.isTransferPending()) { + // Skip if there's a pending transfer + console.log( + "Skipping balance check message fork test because there's a pending transfer" + ); + return; + } + + const vaultAddr = await crossChainMasterStrategy.vaultAddress(); + const impersonatedVault = await impersonateAndFund(vaultAddr); + + // set an arbitrary remote strategy balance + await setStorageAt( + crossChainMasterStrategy.address, + `0x${REMOTE_STRATEGY_BALANCE_SLOT.toString(16)}`, + usdcUnits("123456").toHexString() + ); + + // Simulate withdrawal call + await crossChainMasterStrategy + .connect(impersonatedVault) + .withdraw(vaultAddr, usdc.address, usdcUnits("1000")); + + const lastNonce = ( + await crossChainMasterStrategy.lastTransferNonce() + ).toNumber(); + + // Replace transmitter to mock transmitter + const actualTransmitter = + await crossChainMasterStrategy.cctpMessageTransmitter(); + await replaceContractAt(actualTransmitter, mockMessageTransmitter); + const replacedTransmitter = await ethers.getContractAt( + "CCTPMessageTransmitterMock", + actualTransmitter + ); + await replacedTransmitter.setCCTPTokenMessenger( + addresses.CCTPTokenMessengerV2 + ); + + // Build check balance payload + const balancePayload = encodeBalanceCheckMessageBody( + lastNonce, + usdcUnits("12345") + ); + const burnPayload = encodeBurnMessageBody( + crossChainMasterStrategy.address, + crossChainMasterStrategy.address, + usdcUnits("2342"), + balancePayload + ); + const message = encodeCCTPMessage( + 6, + addresses.CCTPTokenMessengerV2, + addresses.CCTPTokenMessengerV2, + burnPayload + ); + + // transfer some USDC to master strategy + await usdc + .connect(matt) + .transfer(crossChainMasterStrategy.address, usdcUnits("2342")); + + // Relay the message with fake attestation + await crossChainMasterStrategy.connect(strategist).relay(message, "0x"); + + const remoteStrategyBalance = + await crossChainMasterStrategy.remoteStrategyBalance(); + expect(remoteStrategyBalance).to.eq(usdcUnits("12345")); }); it("Should ignore balance check message for a pending withdrawal", async function () { @@ -437,10 +530,9 @@ describe("ForkTest: CrossChainMasterStrategy", function () { const impersonatedVault = await impersonateAndFund(vaultAddr); // set an arbitrary remote strategy balance - const remoteStrategyBalanceSlot = 209; // Slot 209 await setStorageAt( crossChainMasterStrategy.address, - `0x${remoteStrategyBalanceSlot.toString(16)}`, + `0x${REMOTE_STRATEGY_BALANCE_SLOT.toString(16)}`, usdcUnits("1000").toHexString() ); From e69490639feacdd858e7ba8e4da7c51534295f73 Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Wed, 24 Dec 2025 06:49:34 +0100 Subject: [PATCH 034/101] WIP Unit tests for OUSD Simplified strategy (#2724) * more unit test integration * more tying up ends * fix bug * cleanup * add full round-trip test * cleanup * Fix approve all and prettify --------- Co-authored-by: Shahul Hameed <10547529+shahthepro@users.noreply.github.com> --- .../contracts/mocks/MockERC4626Vault.sol | 166 ++++++++++++++++++ .../crosschain/CCTPMessageTransmitterMock.sol | 142 +++++++-------- .../crosschain/CCTPTokenMessengerMock.sol | 5 +- contracts/contracts/mocks/crosschain/Untitled | 1 - .../crosschain/AbstractCCTPIntegrator.sol | 2 +- .../crosschain/CrossChainMasterStrategy.sol | 4 + .../crosschain/CrossChainRemoteStrategy.sol | 16 +- .../deploy/base/041_crosschain_strategy.js | 1 + contracts/deploy/deployActions.js | 47 +++-- contracts/deploy/mainnet/000_mock.js | 14 +- .../deploy/mainnet/162_crosschain_strategy.js | 2 + contracts/test/_fixture.js | 23 ++- .../crosschain/cross-chain-strategy.js | 122 +++++++++++-- 13 files changed, 428 insertions(+), 117 deletions(-) create mode 100644 contracts/contracts/mocks/MockERC4626Vault.sol delete mode 100644 contracts/contracts/mocks/crosschain/Untitled diff --git a/contracts/contracts/mocks/MockERC4626Vault.sol b/contracts/contracts/mocks/MockERC4626Vault.sol new file mode 100644 index 0000000000..02b4672c2d --- /dev/null +++ b/contracts/contracts/mocks/MockERC4626Vault.sol @@ -0,0 +1,166 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { IERC4626 } from "../../lib/openzeppelin/interfaces/IERC4626.sol"; +import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; + +contract MockERC4626Vault is IERC4626, ERC20 { + using SafeERC20 for IERC20; + + address public asset; + uint8 public constant DECIMALS = 18; + + constructor(address _asset) ERC20("Mock Vault Share", "MVS") { + asset = _asset; + } + + // ERC20 totalSupply is inherited + + // ERC20 balanceOf is inherited + + function deposit(uint256 assets, address receiver) + public + override + returns (uint256 shares) + { + shares = previewDeposit(assets); + IERC20(asset).safeTransferFrom(msg.sender, address(this), assets); + _mint(receiver, shares); + return shares; + } + + function mint(uint256 shares, address receiver) + public + override + returns (uint256 assets) + { + assets = previewMint(shares); + IERC20(asset).safeTransferFrom(msg.sender, address(this), assets); + _mint(receiver, shares); + return assets; + } + + function withdraw( + uint256 assets, + address receiver, + address owner + ) public override returns (uint256 shares) { + shares = previewWithdraw(assets); + if (msg.sender != owner) { + // No approval check for mock + } + _burn(owner, shares); + IERC20(asset).safeTransfer(receiver, assets); + return shares; + } + + function redeem( + uint256 shares, + address receiver, + address owner + ) public override returns (uint256 assets) { + assets = previewRedeem(shares); + if (msg.sender != owner) { + // No approval check for mock + } + _burn(owner, shares); + IERC20(asset).safeTransfer(receiver, assets); + return assets; + } + + function totalAssets() public view override returns (uint256) { + return IERC20(asset).balanceOf(address(this)); + } + + function convertToShares(uint256 assets) + public + view + override + returns (uint256 shares) + { + uint256 supply = totalSupply(); // Use ERC20 totalSupply + return + supply == 0 || assets == 0 + ? assets + : (assets * supply) / totalAssets(); + } + + function convertToAssets(uint256 shares) + public + view + override + returns (uint256 assets) + { + uint256 supply = totalSupply(); // Use ERC20 totalSupply + return supply == 0 ? shares : (shares * totalAssets()) / supply; + } + + function maxDeposit(address receiver) + public + view + override + returns (uint256) + { + return type(uint256).max; + } + + function maxMint(address receiver) public view override returns (uint256) { + return type(uint256).max; + } + + function maxWithdraw(address owner) public view override returns (uint256) { + return convertToAssets(balanceOf(owner)); + } + + function maxRedeem(address owner) public view override returns (uint256) { + return balanceOf(owner); + } + + function previewDeposit(uint256 assets) + public + view + override + returns (uint256 shares) + { + return convertToShares(assets); + } + + function previewMint(uint256 shares) + public + view + override + returns (uint256 assets) + { + return convertToAssets(shares); + } + + function previewWithdraw(uint256 assets) + public + view + override + returns (uint256 shares) + { + return convertToShares(assets); + } + + function previewRedeem(uint256 shares) + public + view + override + returns (uint256 assets) + { + return convertToAssets(shares); + } + + function _mint(address account, uint256 amount) internal override { + super._mint(account, amount); + } + + function _burn(address account, uint256 amount) internal override { + super._burn(account, amount); + } + + // Inherited from ERC20 +} diff --git a/contracts/contracts/mocks/crosschain/CCTPMessageTransmitterMock.sol b/contracts/contracts/mocks/crosschain/CCTPMessageTransmitterMock.sol index f31064d681..69f13eec27 100644 --- a/contracts/contracts/mocks/crosschain/CCTPMessageTransmitterMock.sol +++ b/contracts/contracts/mocks/crosschain/CCTPMessageTransmitterMock.sol @@ -4,20 +4,7 @@ pragma solidity ^0.8.0; import { ICCTPMessageTransmitter } from "../../interfaces/cctp/ICCTP.sol"; import { IERC20 } from "../../utils/InitializableAbstractStrategy.sol"; import { BytesHelper } from "../../utils/BytesHelper.sol"; -import { IMessageHandlerV2 } from "../../interfaces/cctp/ICCTP.sol"; - -// CCTP Message Header fields -// Ref: https://developers.circle.com/cctp/technical-guide#message-header -uint8 constant VERSION_INDEX = 0; -uint8 constant SOURCE_DOMAIN_INDEX = 4; -uint8 constant SENDER_INDEX = 44; -uint8 constant RECIPIENT_INDEX = 76; -uint8 constant MESSAGE_BODY_INDEX = 148; - -// 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 constant BURN_MESSAGE_V2_RECIPIENT_INDEX = 36; +import { AbstractCCTPIntegrator } from "../../strategies/crosschain/AbstractCCTPIntegrator.sol"; /** * @title Mock conctract simulating the functionality of the CCTPTokenMessenger contract @@ -30,11 +17,10 @@ contract CCTPMessageTransmitterMock is ICCTPMessageTransmitter { IERC20 public usdc; uint256 public nonce = 0; - - bool public shouldRevertNextReceiveMessage; - address public cctpTokenMessenger; - - event MessageReceivedInMockTransmitter(bytes message); + // Sender index in the burn message v2 + // Ref: https://github.com/circlefin/evm-cctp-contracts/blob/master/src/messages/v2/BurnMessageV2.sol + uint8 constant BURN_MESSAGE_V2_MESSAGE_SENDER_INDEX = 100; + uint8 constant BURN_MESSAGE_V2_HOOK_DATA_INDEX = 228; // Full message with header struct Message { @@ -51,15 +37,13 @@ contract CCTPMessageTransmitterMock is ICCTPMessageTransmitter { } Message[] public messages; + // map of encoded messages to the corresponding message structs + mapping(bytes32 => Message) public encodedMessages; constructor(address _usdc) { usdc = IERC20(_usdc); } - function setCCTPTokenMessenger(address _cctpTokenMessenger) external { - cctpTokenMessenger = _cctpTokenMessenger; - } - // @dev for the porposes of unit tests queues the message to be mock-sent using // the cctp bridge. function sendMessage( @@ -72,9 +56,12 @@ contract CCTPMessageTransmitterMock is ICCTPMessageTransmitter { bytes32 nonceHash = keccak256(abi.encodePacked(nonce)); nonce++; + // If destination is mainnet, source is base and vice versa + uint32 sourceDomain = destinationDomain == 0 ? 6 : 0; + Message memory message = Message({ version: 1, - sourceDomain: 1, + sourceDomain: sourceDomain, destinationDomain: destinationDomain, recipient: recipient, sender: bytes32(uint256(uint160(msg.sender))), @@ -101,9 +88,12 @@ contract CCTPMessageTransmitterMock is ICCTPMessageTransmitter { bytes32 nonceHash = keccak256(abi.encodePacked(nonce)); nonce++; + // If destination is mainnet, source is base and vice versa + uint32 sourceDomain = destinationDomain == 0 ? 6 : 0; + Message memory message = Message({ version: 1, - sourceDomain: 1, + sourceDomain: sourceDomain, destinationDomain: destinationDomain, recipient: recipient, sender: bytes32(uint256(uint160(msg.sender))), @@ -122,49 +112,47 @@ contract CCTPMessageTransmitterMock is ICCTPMessageTransmitter { override returns (bool) { - // For mock, assume we can decode and push, but simplified: just push the bytes as body or something - // To properly decode, we'd need the header parsing logic - // For now, emit or log, but to store, perhaps add a function later - - uint32 sourceDomain = message.extractUint32(SOURCE_DOMAIN_INDEX); - address recipient = message.extractAddress(RECIPIENT_INDEX); - address sender = message.extractAddress(SENDER_INDEX); - - bytes memory messageBody = message.extractSlice( - MESSAGE_BODY_INDEX, - message.length + Message memory storedMsg = encodedMessages[keccak256(message)]; + AbstractCCTPIntegrator recipient = AbstractCCTPIntegrator( + address(uint160(uint256(storedMsg.recipient))) ); - bool isBurnMessage = recipient == cctpTokenMessenger; - - if (isBurnMessage) { - // recipient = messageBody.extractAddress(BURN_MESSAGE_V2_RECIPIENT_INDEX); - // This step won't mint USDC, transfer it to the recipient address - // in your tests - } else { - IMessageHandlerV2(recipient).handleReceiveFinalizedMessage( - sourceDomain, - bytes32(uint256(uint160(sender))), - 2000, - messageBody + bytes32 sender = storedMsg.sender; + bytes memory messageBody = storedMsg.messageBody; + + // Credit USDC in this step as it is done in the live cctp contracts + if (storedMsg.isTokenTransfer) { + usdc.transfer(address(recipient), storedMsg.tokenAmount); + // override the sender with the one stored in the Burn message as the sender int he + // message header is the TokenMessenger. + sender = bytes32( + uint256( + uint160( + storedMsg.messageBody.extractAddress( + BURN_MESSAGE_V2_MESSAGE_SENDER_INDEX + ) + ) + ) + ); + messageBody = storedMsg.messageBody.extractSlice( + BURN_MESSAGE_V2_HOOK_DATA_INDEX, + storedMsg.messageBody.length ); } - // This step won't mint USDC, transfer it to the recipient address - // in your tests - emit MessageReceivedInMockTransmitter(message); - - // // For testing purposes, we can revert the next receive message - // if (shouldRevertNextReceiveMessage) { - // shouldRevertNextReceiveMessage = false; - // return false; - // } + // TODO: should we also handle unfinalized messages: handleReceiveUnfinalizedMessage? + recipient.handleReceiveFinalizedMessage( + storedMsg.sourceDomain, + sender, + 2000, // finality threshold + messageBody + ); return true; } - function addMessage(Message memory msg) external { - messages.push(msg); + function addMessage(Message memory storedMsg) external { + messages.push(storedMsg); } function _encodeMessageHeader( @@ -198,16 +186,20 @@ contract CCTPMessageTransmitterMock is ICCTPMessageTransmitter { return removed; } - function _processMessage(Message memory msg) internal { - bytes memory encoded = _encodeMessageHeader( - msg.version, - msg.sourceDomain, - msg.sender, - msg.recipient, - msg.messageBody + function _processMessage(Message memory storedMsg) internal { + bytes memory encodedMessage = _encodeMessageHeader( + storedMsg.version, + storedMsg.sourceDomain, + storedMsg.sender, + storedMsg.recipient, + storedMsg.messageBody ); - receiveMessage(encoded, bytes("")); + encodedMessages[keccak256(encodedMessage)] = storedMsg; + + address recipient = address(uint160(uint256(storedMsg.recipient))); + + AbstractCCTPIntegrator(recipient).relay(encodedMessage, bytes("")); } function _removeBack() internal returns (Message memory) { @@ -217,21 +209,21 @@ contract CCTPMessageTransmitterMock is ICCTPMessageTransmitter { return last; } + function messagesInQueue() external view returns (uint256) { + return messages.length; + } + function processFront() external { - Message memory msg = _removeFront(); - _processMessage(msg); + Message memory storedMsg = _removeFront(); + _processMessage(storedMsg); } function processBack() external { - Message memory msg = _removeBack(); - _processMessage(msg); + Message memory storedMsg = _removeBack(); + _processMessage(storedMsg); } function getMessagesLength() external view returns (uint256) { return messages.length; } - - function revertNextReceiveMessage() external { - shouldRevertNextReceiveMessage = true; - } } diff --git a/contracts/contracts/mocks/crosschain/CCTPTokenMessengerMock.sol b/contracts/contracts/mocks/crosschain/CCTPTokenMessengerMock.sol index 49b7b83c3d..e33cc9c0d1 100644 --- a/contracts/contracts/mocks/crosschain/CCTPTokenMessengerMock.sol +++ b/contracts/contracts/mocks/crosschain/CCTPTokenMessengerMock.sol @@ -91,7 +91,9 @@ contract CCTPTokenMessengerMock is ICCTPTokenMessenger { bytes32 messageSenderBytes32 = bytes32( abi.encodePacked(bytes12(0), bytes20(uint160(messageSender))) ); + bytes32 expirationBlock = bytes32(0); + // Ref: https://developers.circle.com/cctp/technical-guide#message-body return abi.encodePacked( uint32(1), // 0-3: version @@ -101,7 +103,8 @@ contract CCTPTokenMessengerMock is ICCTPTokenMessenger { messageSenderBytes32, // 100-131: messageSender (bytes32 left-padded address) maxFee, // 132-163: uint256 maxFee feeExecuted, // 164-195: uint256 feeExecuted - hookData // 196+: dynamic hookData + expirationBlock, // 196-227: bytes32 expirationBlock + hookData // 228+: dynamic hookData ); } diff --git a/contracts/contracts/mocks/crosschain/Untitled b/contracts/contracts/mocks/crosschain/Untitled deleted file mode 100644 index 4942ce0833..0000000000 --- a/contracts/contracts/mocks/crosschain/Untitled +++ /dev/null @@ -1 +0,0 @@ -depositForBurnWithHook \ No newline at end of file diff --git a/contracts/contracts/strategies/crosschain/AbstractCCTPIntegrator.sol b/contracts/contracts/strategies/crosschain/AbstractCCTPIntegrator.sol index f6d917b72d..1695268b81 100644 --- a/contracts/contracts/strategies/crosschain/AbstractCCTPIntegrator.sol +++ b/contracts/contracts/strategies/crosschain/AbstractCCTPIntegrator.sol @@ -310,7 +310,7 @@ abstract contract AbstractCCTPIntegrator is Governable, IMessageHandlerV2 { ); } - require(sender == recipient, "Sender and recipient must be the same"); + require(address(this) == recipient, "Unexpected recipient address"); require(sender == peerStrategy, "Incorrect sender/recipient address"); // Relay the message diff --git a/contracts/contracts/strategies/crosschain/CrossChainMasterStrategy.sol b/contracts/contracts/strategies/crosschain/CrossChainMasterStrategy.sol index d254c3f7a4..b2976f2524 100644 --- a/contracts/contracts/strategies/crosschain/CrossChainMasterStrategy.sol +++ b/contracts/contracts/strategies/crosschain/CrossChainMasterStrategy.sol @@ -250,6 +250,7 @@ contract CrossChainMasterStrategy is nonce, depositAmount ); + _sendTokens(depositAmount, message); emit Deposit(_asset, _asset, depositAmount); } @@ -275,6 +276,9 @@ contract CrossChainMasterStrategy is uint64 nonce = _getNextNonce(); transferTypeByNonce[nonce] = TransferType.Withdrawal; + // TODO: not sure that we should really emit a withdrawal here + // nothing is withdrawn to the vault yet. We might rather emit this in the + // _onTokenReceived function. emit Withdrawal(baseToken, baseToken, _amount); // Send withdrawal message with payload diff --git a/contracts/contracts/strategies/crosschain/CrossChainRemoteStrategy.sol b/contracts/contracts/strategies/crosschain/CrossChainRemoteStrategy.sol index 7e59500f33..9f04e09337 100644 --- a/contracts/contracts/strategies/crosschain/CrossChainRemoteStrategy.sol +++ b/contracts/contracts/strategies/crosschain/CrossChainRemoteStrategy.sol @@ -37,6 +37,10 @@ contract CrossChainRemoteStrategy is AbstractCCTPIntegrator(_cctpConfig) Generalized4626Strategy(_baseConfig, _cctpConfig.baseToken) { + // TODO: having 2 tokens representing the same asset is not ideal. + // We use both tokens interchangeably in the contract. + require(baseToken == address(assetToken), "Token mismatch"); + // NOTE: Vault address must always be the proxy address // so that IVault(vaultAddress).strategistAddr() works } @@ -159,6 +163,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)) { emit Deposit(_asset, address(shareToken), _amount); } catch Error(string memory reason) { @@ -190,6 +195,7 @@ contract CrossChainRemoteStrategy is // Check balance after withdrawal uint256 balanceAfter = checkBalance(baseToken); + bytes memory message = CrossChainStrategyHelper .encodeBalanceCheckMessage(lastTransferNonce, balanceAfter); @@ -198,6 +204,12 @@ contract CrossChainRemoteStrategy is // Or dust could be left on the contract that is hard to extract. uint256 usdcBalance = IERC20(baseToken).balanceOf(address(this)); if (usdcBalance > 1e6) { + // The new balance on the contract needs to have USDC subtracted from it as + // that will be withdrawn in the next steps + message = CrossChainStrategyHelper.encodeBalanceCheckMessage( + lastTransferNonce, + balanceAfter - usdcBalance + ); _sendTokens(usdcBalance, message); } else { _sendMessage(message); @@ -216,7 +228,8 @@ contract CrossChainRemoteStrategy is uint256 _amount ) internal override { require(_amount > 0, "Must withdraw something"); - require(_recipient != address(this), "Invalid recipient"); + // TODO: do we really need this check below? + // require(_recipient != address(this), "Invalid recipient"); require(_asset == address(assetToken), "Unexpected asset address"); // slither-disable-next-line unused-return @@ -288,6 +301,7 @@ contract CrossChainRemoteStrategy is * bridged transfer. */ uint256 balanceOnContract = IERC20(baseToken).balanceOf(address(this)); + IERC4626 platform = IERC4626(platformAddress); return platform.previewRedeem(platform.balanceOf(address(this))) + diff --git a/contracts/deploy/base/041_crosschain_strategy.js b/contracts/deploy/base/041_crosschain_strategy.js index d79500e4c6..1a01b10d2b 100644 --- a/contracts/deploy/base/041_crosschain_strategy.js +++ b/contracts/deploy/base/041_crosschain_strategy.js @@ -22,6 +22,7 @@ module.exports = deployOnBase( cctpDomainIds.Ethereum, addresses.CrossChainStrategyProxy, addresses.base.USDC, + deployerAddr, "CrossChainRemoteStrategy" ); console.log(`CrossChainRemoteStrategyImpl address: ${implAddress}`); diff --git a/contracts/deploy/deployActions.js b/contracts/deploy/deployActions.js index 7e40551205..64f8d8f88f 100644 --- a/contracts/deploy/deployActions.js +++ b/contracts/deploy/deployActions.js @@ -1757,10 +1757,12 @@ const deployCrossChainMasterStrategyImpl = async ( targetDomainId, remoteStrategyAddress, baseToken, + vaultAddress, implementationName = "CrossChainMasterStrategy", skipInitialize = false, tokenMessengerAddress = addresses.CCTPTokenMessengerV2, - messageTransmitterAddress = addresses.CCTPMessageTransmitterV2 + messageTransmitterAddress = addresses.CCTPMessageTransmitterV2, + governor = addresses.mainnet.Timelock ) => { const { deployerAddr, multichainStrategistAddr } = await getNamedAccounts(); const sDeployer = await ethers.provider.getSigner(deployerAddr); @@ -1774,9 +1776,7 @@ const deployCrossChainMasterStrategyImpl = async ( await deployWithConfirmation(implementationName, [ [ addresses.zero, // platform address - // TODO: change to the actual vault address - deployerAddr, // vault address - // addresses.mainnet.VaultProxy, + vaultAddress, // vault address ], [ tokenMessengerAddress, @@ -1801,9 +1801,7 @@ const deployCrossChainMasterStrategyImpl = async ( await withConfirmation( cCrossChainStrategyProxy.connect(sDeployer)[initFunction]( dCrossChainMasterStrategy.address, - // TODO: change governor later - // addresses.mainnet.Timelock, // governor - deployerAddr, // governor + governor, // governor initData, // data for delegate call to the initialize function on the strategy await getTxOpts() ) @@ -1815,14 +1813,15 @@ const deployCrossChainMasterStrategyImpl = async ( // deploys and initializes the CrossChain remote strategy const deployCrossChainRemoteStrategyImpl = async ( - platformAddress, + platformAddress, // underlying 4626 vault address proxyAddress, targetDomainId, remoteStrategyAddress, baseToken, implementationName = "CrossChainRemoteStrategy", tokenMessengerAddress = addresses.CCTPTokenMessengerV2, - messageTransmitterAddress = addresses.CCTPMessageTransmitterV2 + messageTransmitterAddress = addresses.CCTPMessageTransmitterV2, + governor = addresses.base.timelock ) => { const { deployerAddr } = await getNamedAccounts(); const sDeployer = await ethers.provider.getSigner(deployerAddr); @@ -1862,9 +1861,7 @@ const deployCrossChainRemoteStrategyImpl = async ( await withConfirmation( cCrossChainStrategyProxy.connect(sDeployer)[initFunction]( dCrossChainRemoteStrategy.address, - // TODO: change governor later - deployerAddr, // governor - // addresses.base.timelock, // governor + governor, // governor //initData, // data for delegate call to the initialize function on the strategy "0x", await getTxOpts() @@ -1876,7 +1873,9 @@ const deployCrossChainRemoteStrategyImpl = async ( // deploy the corss chain Master / Remote strategy pair for unit testing const deployCrossChainUnitTestStrategy = async (usdcAddress) => { - const { deployerAddr } = await getNamedAccounts(); + const { deployerAddr, governorAddr } = await getNamedAccounts(); + // const sDeployer = await ethers.provider.getSigner(deployerAddr); + const sGovernor = await ethers.provider.getSigner(governorAddr); const dMasterProxy = await deployWithConfirmation( "CrossChainMasterStrategyProxy", [deployerAddr], @@ -1888,10 +1887,12 @@ const deployCrossChainUnitTestStrategy = async (usdcAddress) => { "CrossChainStrategyProxy" ); + const cVaultProxy = await ethers.getContract("VaultProxy"); const messageTransmitter = await ethers.getContract( "CCTPMessageTransmitterMock" ); const tokenMessenger = await ethers.getContract("CCTPTokenMessengerMock"); + const c4626Vault = await ethers.getContract("MockERC4626Vault"); await deployCrossChainMasterStrategyImpl( dMasterProxy.address, @@ -1899,22 +1900,36 @@ const deployCrossChainUnitTestStrategy = async (usdcAddress) => { // unit tests differ from mainnet where remote strategy has a different address dRemoteProxy.address, usdcAddress, + cVaultProxy.address, "CrossChainMasterStrategy", false, tokenMessenger.address, - messageTransmitter.address + messageTransmitter.address, + governorAddr ); await deployCrossChainRemoteStrategyImpl( - deployerAddr, // TODO platform address needs to be replaces with mock 4626 Moprho Vault + c4626Vault.address, dRemoteProxy.address, 0, // Ethereum domain id dMasterProxy.address, usdcAddress, "CrossChainRemoteStrategy", tokenMessenger.address, - messageTransmitter.address + messageTransmitter.address, + governorAddr ); + + const cCrossChainRemoteStrategy = await ethers.getContractAt( + "CrossChainRemoteStrategy", + dRemoteProxy.address + ); + await withConfirmation( + cCrossChainRemoteStrategy.connect(sGovernor).safeApproveAllTokens() + ); + // await withConfirmation( + // messageTransmitter.connect(sDeployer).setCCTPTokenMessenger(tokenMessenger.address) + // ); }; module.exports = { diff --git a/contracts/deploy/mainnet/000_mock.js b/contracts/deploy/mainnet/000_mock.js index 514b413aca..7a5f2d9976 100644 --- a/contracts/deploy/mainnet/000_mock.js +++ b/contracts/deploy/mainnet/000_mock.js @@ -28,7 +28,7 @@ const { const deployMocks = async ({ getNamedAccounts, deployments }) => { const { deploy } = deployments; const { deployerAddr, governorAddr } = await getNamedAccounts(); - const sDeployer = await ethers.provider.getSigner(deployerAddr); + // const sDeployer = await ethers.provider.getSigner(deployerAddr); console.log("Running 000_mock deployment..."); console.log("Deployer address", deployerAddr); @@ -459,10 +459,14 @@ const deployMocks = async ({ getNamedAccounts, deployments }) => { from: deployerAddr, args: [usdc.address, messageTransmitter.address], }); - const tokenMessenger = await ethers.getContract("CCTPTokenMessengerMock"); - await messageTransmitter - .connect(sDeployer) - .setCCTPTokenMessenger(tokenMessenger.address); + await deploy("MockERC4626Vault", { + from: deployerAddr, + args: [usdc.address], + }); + // const tokenMessenger = await ethers.getContract("CCTPTokenMessengerMock"); + // await messageTransmitter + // .connect(sDeployer) + // .setCCTPTokenMessenger(tokenMessenger.address); console.log("000_mock deploy done."); diff --git a/contracts/deploy/mainnet/162_crosschain_strategy.js b/contracts/deploy/mainnet/162_crosschain_strategy.js index c9a3e3c9ec..fdb07f3e02 100644 --- a/contracts/deploy/mainnet/162_crosschain_strategy.js +++ b/contracts/deploy/mainnet/162_crosschain_strategy.js @@ -12,6 +12,7 @@ module.exports = deploymentWithGovernanceProposal( proposalId: "", }, async () => { + const { deployerAddr } = await getNamedAccounts(); const cProxy = await ethers.getContractAt( "CrossChainStrategyProxy", addresses.CrossChainStrategyProxy @@ -24,6 +25,7 @@ module.exports = deploymentWithGovernanceProposal( // Same address for both master and remote strategy addresses.CrossChainStrategyProxy, addresses.mainnet.USDC, + deployerAddr, "CrossChainMasterStrategy" ); console.log(`CrossChainMasterStrategyImpl address: ${implAddress}`); diff --git a/contracts/test/_fixture.js b/contracts/test/_fixture.js index f444c6c0ed..9581c54b1c 100644 --- a/contracts/test/_fixture.js +++ b/contracts/test/_fixture.js @@ -2530,6 +2530,7 @@ async function instantRebaseVaultFixture() { // purposes of unit testing async function crossChainFixtureUnit() { const fixture = await defaultFixture(); + const { governor, vault } = fixture; const crossChainMasterStrategyProxy = await ethers.getContract( "CrossChainMasterStrategyProxy" @@ -2548,17 +2549,33 @@ async function crossChainFixtureUnit() { crossChainRemoteStrategyProxy.address ); + await vault + .connect(governor) + .approveStrategy(cCrossChainMasterStrategy.address); + const messageTransmitter = await ethers.getContract( "CCTPMessageTransmitterMock" ); const tokenMessenger = await ethers.getContract("CCTPTokenMessengerMock"); + // In unit test environment it is not the off-chain defender action that calls the "relay" + // to relay the messages but rather the message transmitter. + await cCrossChainMasterStrategy + .connect(governor) + .setOperator(messageTransmitter.address); + await cCrossChainRemoteStrategy + .connect(governor) + .setOperator(messageTransmitter.address); + + const morphoVault = await ethers.getContract("MockERC4626Vault"); + return { ...fixture, crossChainMasterStrategy: cCrossChainMasterStrategy, crossChainRemoteStrategy: cCrossChainRemoteStrategy, messageTransmitter: messageTransmitter, tokenMessenger: tokenMessenger, + morphoVault: morphoVault, }; } @@ -2914,9 +2931,9 @@ async function crossChainFixture() { mockMessageTransmitter.address, ]); const mockTokenMessenger = await ethers.getContract("CCTPTokenMessengerMock"); - await mockMessageTransmitter.setCCTPTokenMessenger( - addresses.CCTPTokenMessengerV2 - ); + // await mockMessageTransmitter.setCCTPTokenMessenger( + // addresses.CCTPTokenMessengerV2 + // ); await setERC20TokenBalance( fixture.matt.address, diff --git a/contracts/test/strategies/crosschain/cross-chain-strategy.js b/contracts/test/strategies/crosschain/cross-chain-strategy.js index 10199f66e8..7964aa0c92 100644 --- a/contracts/test/strategies/crosschain/cross-chain-strategy.js +++ b/contracts/test/strategies/crosschain/cross-chain-strategy.js @@ -1,5 +1,4 @@ -// const { expect } = require("chai"); - +const { expect } = require("chai"); const { isCI } = require("../../helpers"); const { createFixtureLoader, @@ -47,20 +46,115 @@ describe("ForkTest: CrossChainRemoteStrategy", function () { ); }; - const depositToStrategy = async (amount) => { - await usdc - .connect(josh) - .approve(crossChainRemoteStrategy.address, await units(amount, usdc)); - await crossChainRemoteStrategy - .connect(josh) - .depositToStrategy(amount, usdc.address); + // Even though remote strategy has funds withdrawn the message initiates on master strategy + const withdrawFromRemoteStrategy = async (amount) => { + await vault + .connect(governor) + .withdrawFromStrategy( + crossChainMasterStrategy.address, + [usdc.address], + [await units(amount, usdc)] + ); + }; + + const mintToMasterDepositToRemote = async (amount) => { + const { messageTransmitter, morphoVault } = fixture; + const amountBn = await units(amount, usdc); + + await mint(amount); + const remoteBalanceBefore = await crossChainRemoteStrategy.checkBalance( + usdc.address + ); + const remoteBalanceRecByMasterBefore = + await crossChainMasterStrategy.remoteStrategyBalance(); + const messagesinQueueBefore = await messageTransmitter.messagesInQueue(); + await depositToMasterStrategy(amount); + await expect(await messageTransmitter.messagesInQueue()).to.eq( + messagesinQueueBefore + 1 + ); + + // Simulate off chain component processing deposit message + await expect(messageTransmitter.processFront()) + .to.emit(crossChainRemoteStrategy, "Deposit") + .withArgs(usdc.address, morphoVault.address, amountBn); + + // 1 message is processed, another one (checkBalance) has entered the queue + await expect(await messageTransmitter.messagesInQueue()).to.eq( + messagesinQueueBefore + 1 + ); + await expect( + await morphoVault.balanceOf(crossChainRemoteStrategy.address) + ).to.eq(remoteBalanceBefore + amountBn); + + // Simulate off chain component processing checkBalance message + await expect(messageTransmitter.processFront()) + .to.emit(crossChainMasterStrategy, "RemoteStrategyBalanceUpdated") + .withArgs(amountBn); + + await expect(await messageTransmitter.messagesInQueue()).to.eq( + messagesinQueueBefore + ); + await expect(await crossChainMasterStrategy.remoteStrategyBalance()).to.eq( + remoteBalanceRecByMasterBefore + amountBn + ); + }; + + const withdrawFromRemoteToVault = async (amount) => { + const { messageTransmitter, morphoVault } = fixture; + const amountBn = await units(amount, usdc); + const remoteBalanceBefore = await crossChainRemoteStrategy.checkBalance( + usdc.address + ); + const remoteBalanceRecByMasterBefore = + await crossChainMasterStrategy.remoteStrategyBalance(); + const messagesinQueueBefore = await messageTransmitter.messagesInQueue(); + + await withdrawFromRemoteStrategy(amount); + await expect(await messageTransmitter.messagesInQueue()).to.eq( + messagesinQueueBefore + 1 + ); + + await expect(messageTransmitter.processFront()) + // TODO: this event might be removed from the master strategy at some point + .to.emit(crossChainRemoteStrategy, "Withdrawal") + .withArgs(usdc.address, morphoVault.address, amountBn); + + await expect(await messageTransmitter.messagesInQueue()).to.eq( + messagesinQueueBefore + 1 + ); + + // master strategy still has the old value fo the remote strategy balance + await expect(await crossChainMasterStrategy.remoteStrategyBalance()).to.eq( + remoteBalanceRecByMasterBefore + ); + await expect( + await morphoVault.balanceOf(crossChainRemoteStrategy.address) + ).to.eq(remoteBalanceBefore - amountBn); + // Simulate off chain component processing checkBalance message + await expect(messageTransmitter.processFront()) + .to.emit(crossChainMasterStrategy, "RemoteStrategyBalanceUpdated") + .withArgs(amountBn); + + await expect(await crossChainMasterStrategy.remoteStrategyBalance()).to.eq( + remoteBalanceRecByMasterBefore - amountBn + ); }; - it("Should initiate a bridge of deposited USDC", async function () { - //const { crossChainRemoteStrategy, messageTransmitter, tokenMessenger } = fixture; + it("Should mint USDC to master strategy, transfer to remote and update balance", async function () { + const { morphoVault } = fixture; + await expect(await morphoVault.totalAssets()).to.eq(await units("0", usdc)); + await mintToMasterDepositToRemote("1000"); + await expect(await morphoVault.totalAssets()).to.eq( + await units("1000", usdc) + ); + }); - await mint("1000"); - await depositToMasterStrategy("1000"); - await depositToStrategy("1000"); + it("Should be able to withdraw from the remote strategy", async function () { + const { morphoVault } = fixture; + await mintToMasterDepositToRemote("1000"); + await expect(await morphoVault.totalAssets()).to.eq( + await units("1000", usdc) + ); + await withdrawFromRemoteToVault("500"); }); }); From 77bc7308aedb4faf319db6210ff16743406b5293 Mon Sep 17 00:00:00 2001 From: Shahul Hameed <10547529+shahthepro@users.noreply.github.com> Date: Wed, 24 Dec 2025 15:29:35 +0400 Subject: [PATCH 035/101] Fix master fork tests --- .../crosschain/CCTPMessageTransmitterMock.sol | 1 + .../CCTPMessageTransmitterMock2.sol | 68 ++++++++++++ contracts/test/_fixture.js | 10 +- ...chain-master-strategy.mainnet.fork-test.js | 102 ++++++------------ 4 files changed, 105 insertions(+), 76 deletions(-) create mode 100644 contracts/contracts/mocks/crosschain/CCTPMessageTransmitterMock2.sol diff --git a/contracts/contracts/mocks/crosschain/CCTPMessageTransmitterMock.sol b/contracts/contracts/mocks/crosschain/CCTPMessageTransmitterMock.sol index 69f13eec27..a208d22988 100644 --- a/contracts/contracts/mocks/crosschain/CCTPMessageTransmitterMock.sol +++ b/contracts/contracts/mocks/crosschain/CCTPMessageTransmitterMock.sol @@ -109,6 +109,7 @@ contract CCTPMessageTransmitterMock is ICCTPMessageTransmitter { function receiveMessage(bytes memory message, bytes memory attestation) public + virtual override returns (bool) { diff --git a/contracts/contracts/mocks/crosschain/CCTPMessageTransmitterMock2.sol b/contracts/contracts/mocks/crosschain/CCTPMessageTransmitterMock2.sol new file mode 100644 index 0000000000..0b4233cee9 --- /dev/null +++ b/contracts/contracts/mocks/crosschain/CCTPMessageTransmitterMock2.sol @@ -0,0 +1,68 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +import { IMessageHandlerV2 } from "../../interfaces/cctp/ICCTP.sol"; +import { BytesHelper } from "../../utils/BytesHelper.sol"; +import { CCTPMessageTransmitterMock } from "./CCTPMessageTransmitterMock.sol"; + +uint8 constant SOURCE_DOMAIN_INDEX = 4; +uint8 constant RECIPIENT_INDEX = 76; +uint8 constant SENDER_INDEX = 44; +uint8 constant MESSAGE_BODY_INDEX = 148; + +/** + * @title Mock conctract simulating the functionality of the CCTPTokenMessenger contract + * for the porposes of unit testing. + * @author Origin Protocol Inc + */ + +contract CCTPMessageTransmitterMock2 is CCTPMessageTransmitterMock { + using BytesHelper for bytes; + + address public cctpTokenMessenger; + + event MessageReceivedInMockTransmitter(bytes message); + + constructor(address _usdc) CCTPMessageTransmitterMock(_usdc) {} + + function setCCTPTokenMessenger(address _cctpTokenMessenger) external { + cctpTokenMessenger = _cctpTokenMessenger; + } + + function receiveMessage(bytes memory message, bytes memory attestation) + public + virtual + override + returns (bool) + { + uint32 sourceDomain = message.extractUint32(SOURCE_DOMAIN_INDEX); + address recipient = message.extractAddress(RECIPIENT_INDEX); + address sender = message.extractAddress(SENDER_INDEX); + + bytes memory messageBody = message.extractSlice( + MESSAGE_BODY_INDEX, + message.length + ); + + bool isBurnMessage = recipient == cctpTokenMessenger; + + if (isBurnMessage) { + // recipient = messageBody.extractAddress(BURN_MESSAGE_V2_RECIPIENT_INDEX); + // This step won't mint USDC, transfer it to the recipient address + // in your tests + } else { + IMessageHandlerV2(recipient).handleReceiveFinalizedMessage( + sourceDomain, + bytes32(uint256(uint160(sender))), + 2000, + messageBody + ); + } + + // This step won't mint USDC, transfer it to the recipient address + // in your tests + emit MessageReceivedInMockTransmitter(message); + + return true; + } +} diff --git a/contracts/test/_fixture.js b/contracts/test/_fixture.js index 9581c54b1c..1762d40899 100644 --- a/contracts/test/_fixture.js +++ b/contracts/test/_fixture.js @@ -2920,20 +2920,20 @@ async function crossChainFixture() { addresses.CrossChainStrategyProxy ); - await deployWithConfirmation("CCTPMessageTransmitterMock", [ + await deployWithConfirmation("CCTPMessageTransmitterMock2", [ fixture.usdc.address, ]); const mockMessageTransmitter = await ethers.getContract( - "CCTPMessageTransmitterMock" + "CCTPMessageTransmitterMock2" ); await deployWithConfirmation("CCTPTokenMessengerMock", [ fixture.usdc.address, mockMessageTransmitter.address, ]); const mockTokenMessenger = await ethers.getContract("CCTPTokenMessengerMock"); - // await mockMessageTransmitter.setCCTPTokenMessenger( - // addresses.CCTPTokenMessengerV2 - // ); + await mockMessageTransmitter.setCCTPTokenMessenger( + addresses.CCTPTokenMessengerV2 + ); await setERC20TokenBalance( fixture.matt.address, diff --git a/contracts/test/strategies/crosschain/crosschain-master-strategy.mainnet.fork-test.js b/contracts/test/strategies/crosschain/crosschain-master-strategy.mainnet.fork-test.js index 15cec8baf5..fa8a3f558a 100644 --- a/contracts/test/strategies/crosschain/crosschain-master-strategy.mainnet.fork-test.js +++ b/contracts/test/strategies/crosschain/crosschain-master-strategy.mainnet.fork-test.js @@ -162,6 +162,25 @@ const encodeBalanceCheckMessageBody = (nonce, balance) => { return `0x000003f200000003${encodedPayload.slice(2)}`; }; +const replaceMessageTransmitter = async () => { + const mockMessageTransmitter = await ethers.getContract( + "CCTPMessageTransmitterMock2" + ); + await replaceContractAt( + addresses.CCTPMessageTransmitterV2, + mockMessageTransmitter + ); + const replacedTransmitter = await ethers.getContractAt( + "CCTPMessageTransmitterMock2", + addresses.CCTPMessageTransmitterV2 + ); + await replacedTransmitter.setCCTPTokenMessenger( + addresses.CCTPTokenMessengerV2 + ); + + return replacedTransmitter; +}; + describe("ForkTest: CrossChainMasterStrategy", function () { this.timeout(0); @@ -323,8 +342,7 @@ describe("ForkTest: CrossChainMasterStrategy", function () { describe("Message receiving", function () { it("Should handle balance check message", async function () { - const { crossChainMasterStrategy, mockMessageTransmitter, strategist } = - fixture; + const { crossChainMasterStrategy, strategist } = fixture; if (await crossChainMasterStrategy.isTransferPending()) { // Skip if there's a pending transfer @@ -339,10 +357,7 @@ describe("ForkTest: CrossChainMasterStrategy", function () { ).toNumber(); // Replace transmitter to mock transmitter - await replaceContractAt( - await crossChainMasterStrategy.cctpMessageTransmitter(), - mockMessageTransmitter - ); + await replaceMessageTransmitter(); // Build check balance payload const balancePayload = encodeBalanceCheckMessageBody( @@ -365,13 +380,7 @@ describe("ForkTest: CrossChainMasterStrategy", function () { }); it("Should handle balance check message for a pending deposit", async function () { - const { - crossChainMasterStrategy, - mockMessageTransmitter, - strategist, - usdc, - matt, - } = fixture; + const { crossChainMasterStrategy, strategist, usdc, matt } = fixture; if (await crossChainMasterStrategy.isTransferPending()) { // Skip if there's a pending transfer @@ -401,10 +410,7 @@ describe("ForkTest: CrossChainMasterStrategy", function () { ).toNumber(); // Replace transmitter to mock transmitter - await replaceContractAt( - await crossChainMasterStrategy.cctpMessageTransmitter(), - mockMessageTransmitter - ); + await replaceMessageTransmitter(); // Build check balance payload const payload = encodeBalanceCheckMessageBody( @@ -432,13 +438,7 @@ describe("ForkTest: CrossChainMasterStrategy", function () { }); it("Should accept tokens for a pending withdrawal", async function () { - const { - crossChainMasterStrategy, - mockMessageTransmitter, - strategist, - matt, - usdc, - } = fixture; + const { crossChainMasterStrategy, strategist, matt, usdc } = fixture; if (await crossChainMasterStrategy.isTransferPending()) { // Skip if there's a pending transfer @@ -468,16 +468,7 @@ describe("ForkTest: CrossChainMasterStrategy", function () { ).toNumber(); // Replace transmitter to mock transmitter - const actualTransmitter = - await crossChainMasterStrategy.cctpMessageTransmitter(); - await replaceContractAt(actualTransmitter, mockMessageTransmitter); - const replacedTransmitter = await ethers.getContractAt( - "CCTPMessageTransmitterMock", - actualTransmitter - ); - await replacedTransmitter.setCCTPTokenMessenger( - addresses.CCTPTokenMessengerV2 - ); + await replaceMessageTransmitter(); // Build check balance payload const balancePayload = encodeBalanceCheckMessageBody( @@ -511,12 +502,7 @@ describe("ForkTest: CrossChainMasterStrategy", function () { }); it("Should ignore balance check message for a pending withdrawal", async function () { - const { - crossChainMasterStrategy, - mockMessageTransmitter, - strategist, - usdc, - } = fixture; + const { crossChainMasterStrategy, strategist, usdc } = fixture; if (await crossChainMasterStrategy.isTransferPending()) { // Skip if there's a pending transfer @@ -549,10 +535,7 @@ describe("ForkTest: CrossChainMasterStrategy", function () { ).toNumber(); // Replace transmitter to mock transmitter - await replaceContractAt( - await crossChainMasterStrategy.cctpMessageTransmitter(), - mockMessageTransmitter - ); + await replaceMessageTransmitter(); // Build check balance payload const payload = encodeBalanceCheckMessageBody( @@ -576,13 +559,7 @@ describe("ForkTest: CrossChainMasterStrategy", function () { }); it("Should ignore balance check message with older nonce", async function () { - const { - crossChainMasterStrategy, - mockMessageTransmitter, - strategist, - matt, - usdc, - } = fixture; + const { crossChainMasterStrategy, strategist, matt, usdc } = fixture; if (await crossChainMasterStrategy.isTransferPending()) { // Skip if there's a pending transfer @@ -615,10 +592,7 @@ describe("ForkTest: CrossChainMasterStrategy", function () { await crossChainMasterStrategy.remoteStrategyBalance(); // Replace transmitter to mock transmitter - await replaceContractAt( - await crossChainMasterStrategy.cctpMessageTransmitter(), - mockMessageTransmitter - ); + await replaceMessageTransmitter(); // Build check balance payload const payload = encodeBalanceCheckMessageBody( @@ -641,8 +615,7 @@ describe("ForkTest: CrossChainMasterStrategy", function () { }); it("Should ignore if nonce is higher", async function () { - const { crossChainMasterStrategy, mockMessageTransmitter, strategist } = - fixture; + const { crossChainMasterStrategy, strategist } = fixture; if (await crossChainMasterStrategy.isTransferPending()) { // Skip if there's a pending transfer @@ -657,10 +630,7 @@ describe("ForkTest: CrossChainMasterStrategy", function () { ).toNumber(); // Replace transmitter to mock transmitter - await replaceContractAt( - await crossChainMasterStrategy.cctpMessageTransmitter(), - mockMessageTransmitter - ); + await replaceMessageTransmitter(); const remoteStrategyBalanceBefore = await crossChainMasterStrategy.remoteStrategyBalance(); @@ -684,14 +654,4 @@ describe("ForkTest: CrossChainMasterStrategy", function () { expect(remoteStrategyBalanceAfter).to.eq(remoteStrategyBalanceBefore); }); }); - - // it.skip("Should handle attestation relay", async function () { - // const { crossChainMasterStrategy } = fixture; - // const attestation = - // "0xc0ee7623da7bad1b2607f12c21ce71c4314b4ade3d36a0e6e13753fbb0603daa2b10fcbbc4942ce75a2b8d5f5c11f4b6c5ee5f8dce4663d3ec834674d0a9991a1cdeb52adf17d5fb3222b1f94f0767175f06e69f9473e7f948a4b5c478814f11915ed64081cbe6e139fd277630b8807b56be7c355ccdda6c20acbf0324231fc8301b"; - // const message = - // "0x0000000100000006000000000384bc6f6bfe10f6df4967b6ad287d897ff729f0c7e43f73a1e18ab156e96bfb0000000000000000000000008ebcca1066d15ad901927ab01c7c6d0b057bbd340000000000000000000000008ebcca1066d15ad901927ab01c7c6d0b057bbd3400000000000000000000000030f8a2fc7d7098061c94f042b2e7e732f95af40f00000000000003e8000003f20000000300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"; - - // await crossChainMasterStrategy.relay(message, attestation); - // }); }); From 6dca3ccca66f46a4071b920fb16763043d6bf6c9 Mon Sep 17 00:00:00 2001 From: Shahul Hameed <10547529+shahthepro@users.noreply.github.com> Date: Wed, 24 Dec 2025 15:31:10 +0400 Subject: [PATCH 036/101] linter --- contracts/.solhintignore | 3 ++- .../strategies/crosschain/CrossChainRemoteStrategy.sol | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/contracts/.solhintignore b/contracts/.solhintignore index ffbcf74d3a..988f3bc831 100644 --- a/contracts/.solhintignore +++ b/contracts/.solhintignore @@ -1,2 +1,3 @@ node_modules -contracts/interfaces/morpho/Types.sol \ No newline at end of file +contracts/interfaces/morpho/Types.sol +contracts/mocks/**/*.sol \ No newline at end of file diff --git a/contracts/contracts/strategies/crosschain/CrossChainRemoteStrategy.sol b/contracts/contracts/strategies/crosschain/CrossChainRemoteStrategy.sol index 9f04e09337..810fbf0004 100644 --- a/contracts/contracts/strategies/crosschain/CrossChainRemoteStrategy.sol +++ b/contracts/contracts/strategies/crosschain/CrossChainRemoteStrategy.sol @@ -223,6 +223,7 @@ contract CrossChainRemoteStrategy is * @param _amount Amount of asset to withdraw */ function _withdraw( + // solhint-disable-next-line no-unused-vars address _recipient, address _asset, uint256 _amount From 7de74b40d8058b49aae48887c61c6dce961a3f69 Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Wed, 24 Dec 2025 17:15:50 +0100 Subject: [PATCH 037/101] add direct withdrawal paths and additional checks --- .../crosschain/CrossChainRemoteStrategy.sol | 2 +- .../crosschain/cross-chain-strategy.js | 128 +++++++++++++++++- 2 files changed, 123 insertions(+), 7 deletions(-) diff --git a/contracts/contracts/strategies/crosschain/CrossChainRemoteStrategy.sol b/contracts/contracts/strategies/crosschain/CrossChainRemoteStrategy.sol index 810fbf0004..29317f25f3 100644 --- a/contracts/contracts/strategies/crosschain/CrossChainRemoteStrategy.sol +++ b/contracts/contracts/strategies/crosschain/CrossChainRemoteStrategy.sol @@ -112,7 +112,7 @@ contract CrossChainRemoteStrategy is function _onMessageReceived(bytes memory payload) internal override { uint32 messageType = payload.getMessageType(); if (messageType == CrossChainStrategyHelper.DEPOSIT_MESSAGE) { - // // Received when Master strategy sends tokens to the remote strategy + // Received when Master strategy sends tokens to the remote strategy // Do nothing because we receive acknowledgement with token transfer, so _onTokenReceived will handle it // TODO: Should _onTokenReceived always call _onMessageReceived? // _processDepositAckMessage(payload); diff --git a/contracts/test/strategies/crosschain/cross-chain-strategy.js b/contracts/test/strategies/crosschain/cross-chain-strategy.js index 7964aa0c92..88770eafc9 100644 --- a/contracts/test/strategies/crosschain/cross-chain-strategy.js +++ b/contracts/test/strategies/crosschain/cross-chain-strategy.js @@ -1,10 +1,11 @@ const { expect } = require("chai"); -const { isCI } = require("../../helpers"); +const { isCI, ousdUnits } = require("../../helpers"); const { createFixtureLoader, crossChainFixtureUnit, } = require("../../_fixture"); const { units } = require("../../helpers"); +const addresses = require("../../../utils/addresses"); const loadFixture = createFixtureLoader(crossChainFixtureUnit); @@ -20,7 +21,8 @@ describe("ForkTest: CrossChainRemoteStrategy", function () { usdc, crossChainRemoteStrategy, crossChainMasterStrategy, - vault; + vault, + initialVaultValue; beforeEach(async () => { fixture = await loadFixture(); josh = fixture.josh; @@ -29,6 +31,7 @@ describe("ForkTest: CrossChainRemoteStrategy", function () { crossChainRemoteStrategy = fixture.crossChainRemoteStrategy; crossChainMasterStrategy = fixture.crossChainMasterStrategy; vault = fixture.vault; + initialVaultValue = await vault.totalValue(); }); const mint = async (amount) => { @@ -57,27 +60,62 @@ describe("ForkTest: CrossChainRemoteStrategy", function () { ); }; + // Withdraws from the remote strategy directly, without going through the master strategy + const directWithdrawFromRemoteStrategy = async (amount) => { + await crossChainRemoteStrategy.connect(governor).withdraw( + addresses.zeroAddress, // this gets ignored anyway + usdc.address, + await units(amount, usdc) + ); + }; + + // Withdraws all the remote strategy directly, without going through the master strategy + const directWithdrawAllFromRemoteStrategy = async () => { + await crossChainRemoteStrategy.connect(governor).withdrawAll(); + }; + + // Checks the diff in the total expected value in the vault + // (plus accompanying strategy value) + const assetVaultTotalValue = async (amountExpected) => { + const amountToCompare = + typeof amountExpected === "string" + ? ousdUnits(amountExpected) + : amountExpected; + + await expect((await vault.totalValue()).sub(initialVaultValue)).to.eq( + amountToCompare + ); + }; + const mintToMasterDepositToRemote = async (amount) => { const { messageTransmitter, morphoVault } = fixture; const amountBn = await units(amount, usdc); await mint(amount); + const vaultDiffAfterMint = (await vault.totalValue()).sub( + initialVaultValue + ); + const remoteBalanceBefore = await crossChainRemoteStrategy.checkBalance( usdc.address ); const remoteBalanceRecByMasterBefore = await crossChainMasterStrategy.remoteStrategyBalance(); const messagesinQueueBefore = await messageTransmitter.messagesInQueue(); + await assetVaultTotalValue(vaultDiffAfterMint); + await depositToMasterStrategy(amount); await expect(await messageTransmitter.messagesInQueue()).to.eq( messagesinQueueBefore + 1 ); + await assetVaultTotalValue(vaultDiffAfterMint); // Simulate off chain component processing deposit message await expect(messageTransmitter.processFront()) .to.emit(crossChainRemoteStrategy, "Deposit") .withArgs(usdc.address, morphoVault.address, amountBn); + await assetVaultTotalValue(vaultDiffAfterMint); // 1 message is processed, another one (checkBalance) has entered the queue await expect(await messageTransmitter.messagesInQueue()).to.eq( messagesinQueueBefore + 1 @@ -94,6 +132,7 @@ describe("ForkTest: CrossChainRemoteStrategy", function () { await expect(await messageTransmitter.messagesInQueue()).to.eq( messagesinQueueBefore ); + await assetVaultTotalValue(vaultDiffAfterMint); await expect(await crossChainMasterStrategy.remoteStrategyBalance()).to.eq( remoteBalanceRecByMasterBefore + amountBn ); @@ -107,6 +146,12 @@ describe("ForkTest: CrossChainRemoteStrategy", function () { ); const remoteBalanceRecByMasterBefore = await crossChainMasterStrategy.remoteStrategyBalance(); + + // If there is any pre-existing USDC balance on the remote strategy it will get swept up by the next + // withdrawal + const usdcBalanceOnRemoteStrategyBefore = await usdc.balanceOf( + crossChainRemoteStrategy.address + ); const messagesinQueueBefore = await messageTransmitter.messagesInQueue(); await withdrawFromRemoteStrategy(amount); @@ -115,7 +160,6 @@ describe("ForkTest: CrossChainRemoteStrategy", function () { ); await expect(messageTransmitter.processFront()) - // TODO: this event might be removed from the master strategy at some point .to.emit(crossChainRemoteStrategy, "Withdrawal") .withArgs(usdc.address, morphoVault.address, amountBn); @@ -127,23 +171,30 @@ describe("ForkTest: CrossChainRemoteStrategy", function () { await expect(await crossChainMasterStrategy.remoteStrategyBalance()).to.eq( remoteBalanceRecByMasterBefore ); + + const remoteBalanceAfter = + remoteBalanceBefore - amountBn - usdcBalanceOnRemoteStrategyBefore; await expect( await morphoVault.balanceOf(crossChainRemoteStrategy.address) - ).to.eq(remoteBalanceBefore - amountBn); + ).to.eq(remoteBalanceAfter); // Simulate off chain component processing checkBalance message await expect(messageTransmitter.processFront()) .to.emit(crossChainMasterStrategy, "RemoteStrategyBalanceUpdated") - .withArgs(amountBn); + .withArgs(remoteBalanceAfter); await expect(await crossChainMasterStrategy.remoteStrategyBalance()).to.eq( - remoteBalanceRecByMasterBefore - amountBn + remoteBalanceAfter ); }; it("Should mint USDC to master strategy, transfer to remote and update balance", async function () { const { morphoVault } = fixture; + await assetVaultTotalValue("0"); await expect(await morphoVault.totalAssets()).to.eq(await units("0", usdc)); + await mintToMasterDepositToRemote("1000"); + await assetVaultTotalValue("1000"); + await expect(await morphoVault.totalAssets()).to.eq( await units("1000", usdc) ); @@ -152,9 +203,74 @@ describe("ForkTest: CrossChainRemoteStrategy", function () { it("Should be able to withdraw from the remote strategy", async function () { const { morphoVault } = fixture; await mintToMasterDepositToRemote("1000"); + await assetVaultTotalValue("1000"); + await expect(await morphoVault.totalAssets()).to.eq( await units("1000", usdc) ); await withdrawFromRemoteToVault("500"); + await assetVaultTotalValue("1000"); + }); + + it("Should be able to direct withdraw from the remote strategy direclty", async function () { + const { morphoVault } = fixture; + await mintToMasterDepositToRemote("1000"); + await assetVaultTotalValue("1000"); + + await expect(await morphoVault.totalAssets()).to.eq( + await units("1000", usdc) + ); + await directWithdrawFromRemoteStrategy("500"); + await assetVaultTotalValue("1000"); + + // 500 has been withdrawn from the Morpho vault but still remains on the + // remote strategy + await expect( + await crossChainRemoteStrategy.checkBalance(usdc.address) + ).to.eq(await units("1000", usdc)); + + // Next withdraw should withdraw additional 10 from the remote strategy and pick up the + // previous 500 totaling a transfer of 510 + await withdrawFromRemoteToVault("10"); + + await assetVaultTotalValue("1000"); + await expect( + await crossChainRemoteStrategy.checkBalance(usdc.address) + ).to.eq(await units("490", usdc)); + }); + + it("Should be able to direct withdraw all from the remote strategy direclty and collect to master", async function () { + const { morphoVault, messageTransmitter } = fixture; + await mintToMasterDepositToRemote("1000"); + await assetVaultTotalValue("1000"); + + await expect(await morphoVault.totalAssets()).to.eq( + await units("1000", usdc) + ); + await directWithdrawAllFromRemoteStrategy(); + await assetVaultTotalValue("1000"); + + // All has been withdrawn from the Morpho vault but still remains on the + // remote strategy + await expect( + await crossChainRemoteStrategy.checkBalance(usdc.address) + ).to.eq(await units("1000", usdc)); + + // The remote strategy doesn't have anything in the Morpho vault anymore. This + // withdrawal will thus fail on the vault, but the transactoin receiving all the + // funds should still succeed. + await withdrawFromRemoteStrategy("10"); + await expect(messageTransmitter.processFront()).to.emit( + crossChainRemoteStrategy, + "WithdrawFailed" + ); + await expect(messageTransmitter.processFront()) + .to.emit(crossChainMasterStrategy, "RemoteStrategyBalanceUpdated") + .withArgs(await units("0", usdc)); + + await assetVaultTotalValue("1000"); + await expect( + await crossChainRemoteStrategy.checkBalance(usdc.address) + ).to.eq(await units("0", usdc)); }); }); From fb31b79d036e399f0520ae1a12f57cfdff0136e0 Mon Sep 17 00:00:00 2001 From: Shahul Hameed <10547529+shahthepro@users.noreply.github.com> Date: Thu, 25 Dec 2025 23:03:29 +0400 Subject: [PATCH 038/101] Fix Remote strategy tests --- .../crosschain/CCTPMessageTransmitterMock.sol | 15 +- .../CCTPMessageTransmitterMock2.sol | 23 ++ .../crosschain/AbstractCCTPIntegrator.sol | 12 +- .../crosschain/CrossChainMasterStrategy.sol | 11 +- .../crosschain/CrossChainRemoteStrategy.sol | 36 +-- .../deploy/base/041_crosschain_strategy.js | 19 +- contracts/deploy/deployActions.js | 44 +-- contracts/test/_fixture-base.js | 39 ++- .../crosschain/_crosschain-helpers.js | 253 ++++++++++++++++++ ...chain-master-strategy.mainnet.fork-test.js | 208 ++------------ ...osschain-remote-strategy.base.fork-test.js | 243 +++++++++++++++-- 11 files changed, 636 insertions(+), 267 deletions(-) create mode 100644 contracts/test/strategies/crosschain/_crosschain-helpers.js diff --git a/contracts/contracts/mocks/crosschain/CCTPMessageTransmitterMock.sol b/contracts/contracts/mocks/crosschain/CCTPMessageTransmitterMock.sol index a208d22988..8a2a3f9a7a 100644 --- a/contracts/contracts/mocks/crosschain/CCTPMessageTransmitterMock.sol +++ b/contracts/contracts/mocks/crosschain/CCTPMessageTransmitterMock.sol @@ -52,7 +52,7 @@ contract CCTPMessageTransmitterMock is ICCTPMessageTransmitter { bytes32 destinationCaller, uint32 minFinalityThreshold, bytes memory messageBody - ) external override { + ) external virtual override { bytes32 nonceHash = keccak256(abi.encodePacked(nonce)); nonce++; @@ -139,15 +139,16 @@ contract CCTPMessageTransmitterMock is ICCTPMessageTransmitter { BURN_MESSAGE_V2_HOOK_DATA_INDEX, storedMsg.messageBody.length ); + } else { + recipient.handleReceiveFinalizedMessage( + storedMsg.sourceDomain, + sender, + 2000, // finality threshold + messageBody + ); } // TODO: should we also handle unfinalized messages: handleReceiveUnfinalizedMessage? - recipient.handleReceiveFinalizedMessage( - storedMsg.sourceDomain, - sender, - 2000, // finality threshold - messageBody - ); return true; } diff --git a/contracts/contracts/mocks/crosschain/CCTPMessageTransmitterMock2.sol b/contracts/contracts/mocks/crosschain/CCTPMessageTransmitterMock2.sol index 0b4233cee9..a44d5d3fbe 100644 --- a/contracts/contracts/mocks/crosschain/CCTPMessageTransmitterMock2.sol +++ b/contracts/contracts/mocks/crosschain/CCTPMessageTransmitterMock2.sol @@ -22,6 +22,7 @@ contract CCTPMessageTransmitterMock2 is CCTPMessageTransmitterMock { address public cctpTokenMessenger; event MessageReceivedInMockTransmitter(bytes message); + event MessageSent(bytes message); constructor(address _usdc) CCTPMessageTransmitterMock(_usdc) {} @@ -29,6 +30,28 @@ contract CCTPMessageTransmitterMock2 is CCTPMessageTransmitterMock { cctpTokenMessenger = _cctpTokenMessenger; } + function sendMessage( + uint32 destinationDomain, + bytes32 recipient, + bytes32 destinationCaller, + uint32 minFinalityThreshold, + bytes memory messageBody + ) external virtual override { + bytes memory message = abi.encodePacked( + uint32(1), // version + uint32(destinationDomain == 0 ? 6 : 0), // source domain + uint32(destinationDomain), // destination domain + uint256(0), + bytes32(uint256(uint160(msg.sender))), // sender + recipient, // recipient + destinationCaller, // destination caller + minFinalityThreshold, // min finality threshold + uint32(0), + messageBody // message body + ); + emit MessageSent(message); + } + function receiveMessage(bytes memory message, bytes memory attestation) public virtual diff --git a/contracts/contracts/strategies/crosschain/AbstractCCTPIntegrator.sol b/contracts/contracts/strategies/crosschain/AbstractCCTPIntegrator.sol index 1695268b81..356da25ed1 100644 --- a/contracts/contracts/strategies/crosschain/AbstractCCTPIntegrator.sol +++ b/contracts/contracts/strategies/crosschain/AbstractCCTPIntegrator.sol @@ -228,10 +228,9 @@ abstract contract AbstractCCTPIntegrator is Governable, IMessageHandlerV2 { IERC20(baseToken).safeApprove(address(cctpTokenMessenger), tokenAmount); - // TODO: figure out why getMinFeeAmount is not on CCTP v2 contract // Ref: https://developers.circle.com/cctp/evm-smart-contracts#getminfeeamount // The issue is that the getMinFeeAmount is not present on v2.0 contracts, but is on - // v2.1. We will only be using standard transfers and fee on those is 0. + // v2.1. We will only be using standard transfers and fee on those is 0 for now uint256 maxFee = feePremiumBps > 0 ? (tokenAmount * feePremiumBps) / 10000 @@ -283,7 +282,14 @@ abstract contract AbstractCCTPIntegrator is Governable, IMessageHandlerV2 { // Ensure message body version version = messageBody.extractUint32(BURN_MESSAGE_V2_VERSION_INDEX); - // TODO: what if the sender sends another type of a message not just the burn message? + // NOTE: There's a possibility that the CCTP Token Messenger might + // send other types of messages in future, not just the burn message. + // If it ever comes to that, this shouldn't cause us any problems + // because it has to still go through the followign checks: + // - version check + // - message body length check + // - sender and recipient (which should be in the same slots and same as address(this)) + // - hook data handling (which will revert even if all the above checks pass) bool isBurnMessageV1 = sender == address(cctpTokenMessenger); if (isBurnMessageV1) { diff --git a/contracts/contracts/strategies/crosschain/CrossChainMasterStrategy.sol b/contracts/contracts/strategies/crosschain/CrossChainMasterStrategy.sol index b2976f2524..9856e0eeb8 100644 --- a/contracts/contracts/strategies/crosschain/CrossChainMasterStrategy.sol +++ b/contracts/contracts/strategies/crosschain/CrossChainMasterStrategy.sol @@ -35,6 +35,7 @@ contract CrossChainMasterStrategy is mapping(uint64 => TransferType) public transferTypeByNonce; event RemoteStrategyBalanceUpdated(uint256 balance); + event WithdrawRequested(address indexed asset, uint256 amount); /** * @param _stratConfig The platform and OToken vault addresses @@ -227,6 +228,9 @@ contract CrossChainMasterStrategy is require(usdcBalance >= tokenAmount, "Insufficient balance"); // Transfer all tokens to the Vault to not leave any dust IERC20(baseToken).safeTransfer(vaultAddress, usdcBalance); + + // Emit withdrawal amount + emit Withdrawal(baseToken, baseToken, usdcBalance); } function _deposit(address _asset, uint256 depositAmount) internal virtual { @@ -276,10 +280,9 @@ contract CrossChainMasterStrategy is uint64 nonce = _getNextNonce(); transferTypeByNonce[nonce] = TransferType.Withdrawal; - // TODO: not sure that we should really emit a withdrawal here - // nothing is withdrawn to the vault yet. We might rather emit this in the - // _onTokenReceived function. - emit Withdrawal(baseToken, baseToken, _amount); + // Emit Withdrawequested event here, + // Withdraw will emitted in _onTokenReceived + emit WithdrawRequested(baseToken, _amount); // Send withdrawal message with payload bytes memory message = CrossChainStrategyHelper.encodeWithdrawMessage( diff --git a/contracts/contracts/strategies/crosschain/CrossChainRemoteStrategy.sol b/contracts/contracts/strategies/crosschain/CrossChainRemoteStrategy.sol index 29317f25f3..cfff700c1b 100644 --- a/contracts/contracts/strategies/crosschain/CrossChainRemoteStrategy.sol +++ b/contracts/contracts/strategies/crosschain/CrossChainRemoteStrategy.sol @@ -30,6 +30,16 @@ contract CrossChainRemoteStrategy is address public strategistAddr; + modifier onlyOperatorOrStrategistOrGovernor() { + require( + msg.sender == operator || + msg.sender == strategistAddr || + isGovernor(), + "Caller is not the Operator, Strategist or the Governor" + ); + _; + } + constructor( BaseStrategyConfig memory _baseConfig, CCTPIntegrationConfig memory _cctpConfig @@ -37,8 +47,6 @@ contract CrossChainRemoteStrategy is AbstractCCTPIntegrator(_cctpConfig) Generalized4626Strategy(_baseConfig, _cctpConfig.baseToken) { - // TODO: having 2 tokens representing the same asset is not ideal. - // We use both tokens interchangeably in the contract. require(baseToken == address(assetToken), "Token mismatch"); // NOTE: Vault address must always be the proxy address @@ -58,7 +66,7 @@ contract CrossChainRemoteStrategy is address[] memory assets = new address[](1); address[] memory pTokens = new address[](1); - assets[0] = address(assetToken); + assets[0] = address(baseToken); pTokens[0] = address(platformAddress); InitializableAbstractStrategy._initialize( @@ -114,8 +122,6 @@ contract CrossChainRemoteStrategy is if (messageType == CrossChainStrategyHelper.DEPOSIT_MESSAGE) { // Received when Master strategy sends tokens to the remote strategy // Do nothing because we receive acknowledgement with token transfer, so _onTokenReceived will handle it - // TODO: Should _onTokenReceived always call _onMessageReceived? - // _processDepositAckMessage(payload); } else if (messageType == CrossChainStrategyHelper.WITHDRAW_MESSAGE) { // Received when Master strategy requests a withdrawal _processWithdrawMessage(payload); @@ -131,9 +137,7 @@ contract CrossChainRemoteStrategy is uint256 feeExecuted, bytes memory payload ) internal virtual { - // TODO: no need to communicate the deposit amount if we deposit everything - // solhint-disable-next-line no-unused-vars - (uint64 nonce, uint256 depositAmount) = payload.decodeDepositMessage(); + (uint64 nonce, ) = payload.decodeDepositMessage(); // Replay protection require(!isNonceProcessed(nonce), "Nonce already processed"); @@ -159,7 +163,7 @@ contract CrossChainRemoteStrategy is */ function _deposit(address _asset, uint256 _amount) internal override { require(_amount > 0, "Must deposit something"); - require(_asset == address(assetToken), "Unexpected asset address"); + require(_asset == address(baseToken), "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. @@ -229,9 +233,8 @@ contract CrossChainRemoteStrategy is uint256 _amount ) internal override { require(_amount > 0, "Must withdraw something"); - // TODO: do we really need this check below? - // require(_recipient != address(this), "Invalid recipient"); - require(_asset == address(assetToken), "Unexpected asset address"); + require(_recipient == address(this), "Invalid recipient"); + require(_asset == address(baseToken), "Unexpected asset address"); // slither-disable-next-line unused-return @@ -276,8 +279,11 @@ contract CrossChainRemoteStrategy is _processDepositMessage(tokenAmount, feeExecuted, payload); } - function sendBalanceUpdate() external virtual { - // TODO: Add permissioning + function sendBalanceUpdate() + external + virtual + onlyOperatorOrStrategistOrGovernor + { uint256 balance = checkBalance(baseToken); bytes memory message = CrossChainStrategyHelper .encodeBalanceCheckMessage(lastTransferNonce, balance); @@ -293,7 +299,7 @@ contract CrossChainRemoteStrategy is public view override - returns (uint256 balance) + returns (uint256) { require(_asset == baseToken, "Unexpected asset address"); /** diff --git a/contracts/deploy/base/041_crosschain_strategy.js b/contracts/deploy/base/041_crosschain_strategy.js index 1a01b10d2b..b06b89d4d8 100644 --- a/contracts/deploy/base/041_crosschain_strategy.js +++ b/contracts/deploy/base/041_crosschain_strategy.js @@ -22,8 +22,10 @@ module.exports = deployOnBase( cctpDomainIds.Ethereum, addresses.CrossChainStrategyProxy, addresses.base.USDC, - deployerAddr, - "CrossChainRemoteStrategy" + "CrossChainRemoteStrategy", + addresses.CCTPTokenMessengerV2, + addresses.CCTPMessageTransmitterV2, + deployerAddr ); console.log(`CrossChainRemoteStrategyImpl address: ${implAddress}`); @@ -35,18 +37,17 @@ module.exports = deployOnBase( `CrossChainRemoteStrategy address: ${cCrossChainRemoteStrategy.address}` ); - await withConfirmation( - cCrossChainRemoteStrategy.connect(sDeployer).setMinFinalityThreshold( - 2000 // standard transfer - ) - ); - + // TODO: Move to governance actions when going live await withConfirmation( cCrossChainRemoteStrategy.connect(sDeployer).safeApproveAllTokens() ); return { - actions: [], + // actions: [{ + // contract: cCrossChainRemoteStrategy, + // signature: "safeApproveAllTokens()", + // args: [], + // }], }; } ); diff --git a/contracts/deploy/deployActions.js b/contracts/deploy/deployActions.js index 64f8d8f88f..321dae4238 100644 --- a/contracts/deploy/deployActions.js +++ b/contracts/deploy/deployActions.js @@ -1823,7 +1823,7 @@ const deployCrossChainRemoteStrategyImpl = async ( messageTransmitterAddress = addresses.CCTPMessageTransmitterV2, governor = addresses.base.timelock ) => { - const { deployerAddr } = await getNamedAccounts(); + const { deployerAddr, multichainStrategistAddr } = await getNamedAccounts(); const sDeployer = await ethers.provider.getSigner(deployerAddr); log(`Deploying CrossChainRemoteStrategyImpl as deployer ${deployerAddr}`); @@ -1832,29 +1832,29 @@ const deployCrossChainRemoteStrategyImpl = async ( proxyAddress ); - const dCrossChainRemoteStrategy = await deployWithConfirmation( - implementationName, + await deployWithConfirmation(implementationName, [ [ - [ - platformAddress, - // Vault address should be same as the proxy address - proxyAddress, // vault address - // addresses.mainnet.VaultProxy, - ], - [ - tokenMessengerAddress, - messageTransmitterAddress, - targetDomainId, - remoteStrategyAddress, - baseToken, - ], - ] + platformAddress, + // Vault address should be same as the proxy address + proxyAddress, // vault address + // addresses.mainnet.VaultProxy, + ], + [ + tokenMessengerAddress, + messageTransmitterAddress, + targetDomainId, + remoteStrategyAddress, + baseToken, + ], + ]); + const dCrossChainRemoteStrategy = await ethers.getContract( + implementationName ); - // const initData = cCrossChainMasterStrategy.interface.encodeFunctionData( - // "initialize()", - // [] - // ); + const initData = dCrossChainRemoteStrategy.interface.encodeFunctionData( + "initialize(address,address,uint32,uint32)", + [multichainStrategistAddr, multichainStrategistAddr, 2000, 0] + ); // Init the proxy to point at the implementation, set the governor, and call initialize const initFunction = "initialize(address,address,bytes)"; @@ -1863,7 +1863,7 @@ const deployCrossChainRemoteStrategyImpl = async ( dCrossChainRemoteStrategy.address, governor, // governor //initData, // data for delegate call to the initialize function on the strategy - "0x", + initData, await getTxOpts() ) ); diff --git a/contracts/test/_fixture-base.js b/contracts/test/_fixture-base.js index 3f4e016e95..0c882ffd66 100644 --- a/contracts/test/_fixture-base.js +++ b/contracts/test/_fixture-base.js @@ -1,7 +1,7 @@ const hre = require("hardhat"); const { ethers } = hre; const mocha = require("mocha"); -const { isFork, isBaseFork, oethUnits } = require("./helpers"); +const { isFork, isBaseFork, oethUnits, usdcUnits } = require("./helpers"); const { impersonateAndFund, impersonateAccount } = require("../utils/signers"); const { nodeRevert, nodeSnapshot } = require("./_fixture"); const { deployWithConfirmation } = require("../utils/deploy"); @@ -343,9 +343,46 @@ const crossChainFixture = deployments.createFixture(async () => { "CrossChainRemoteStrategy", addresses.CrossChainStrategyProxy ); + + await deployWithConfirmation("CCTPMessageTransmitterMock2", [ + fixture.usdc.address, + ]); + const mockMessageTransmitter = await ethers.getContract( + "CCTPMessageTransmitterMock2" + ); + await deployWithConfirmation("CCTPTokenMessengerMock", [ + fixture.usdc.address, + mockMessageTransmitter.address, + ]); + const mockTokenMessenger = await ethers.getContract("CCTPTokenMessengerMock"); + await mockMessageTransmitter.setCCTPTokenMessenger( + addresses.CCTPTokenMessengerV2 + ); + + const usdcMinter = await impersonateAndFund( + "0x2230393EDAD0299b7E7B59F20AA856cD1bEd52e1" + ); + const usdcContract = await ethers.getContractAt( + [ + "function mint(address to, uint256 amount) external", + "function configureMinter(address minter, uint256 minterAmount) external", + ], + addresses.base.USDC + ); + + await usdcContract + .connect(usdcMinter) + .configureMinter(fixture.rafael.address, usdcUnits("100000000")); + + await usdcContract + .connect(fixture.rafael) + .mint(fixture.rafael.address, usdcUnits("1000000")); + return { ...fixture, crossChainRemoteStrategy, + mockMessageTransmitter, + mockTokenMessenger, }; }); diff --git a/contracts/test/strategies/crosschain/_crosschain-helpers.js b/contracts/test/strategies/crosschain/_crosschain-helpers.js new file mode 100644 index 0000000000..e5cbce5c0b --- /dev/null +++ b/contracts/test/strategies/crosschain/_crosschain-helpers.js @@ -0,0 +1,253 @@ +const { expect } = require("chai"); + +const addresses = require("../../../utils/addresses"); +const { replaceContractAt } = require("../../../utils/hardhat"); +const { setStorageAt } = require("@nomicfoundation/hardhat-network-helpers"); + +const DEPOSIT_FOR_BURN_EVENT_TOPIC = + "0x0c8c1cbdc5190613ebd485511d4e2812cfa45eecb79d845893331fedad5130a5"; +const MESSAGE_SENT_EVENT_TOPIC = + "0x8c5261668696ce22758910d05bab8f186d6eb247ceac2af2e82c7dc17669b036"; + +const emptyByte = "0000"; +const empty2Bytes = emptyByte.repeat(2); +const empty4Bytes = emptyByte.repeat(4); +const empty16Bytes = empty4Bytes.repeat(4); +const empty18Bytes = `${empty2Bytes}${empty16Bytes}`; +const empty20Bytes = empty4Bytes.repeat(5); + +const REMOTE_STRATEGY_BALANCE_SLOT = 210; + +const decodeDepositForBurnEvent = (event) => { + const [ + amount, + mintRecipient, + destinationDomain, + destinationTokenMessenger, + destinationCaller, + maxFee, + hookData, + ] = ethers.utils.defaultAbiCoder.decode( + ["uint256", "address", "uint32", "address", "address", "uint256", "bytes"], + event.data + ); + + const [burnToken] = ethers.utils.defaultAbiCoder.decode( + ["address"], + event.topics[1] + ); + const [depositer] = ethers.utils.defaultAbiCoder.decode( + ["address"], + event.topics[2] + ); + const [minFinalityThreshold] = ethers.utils.defaultAbiCoder.decode( + ["uint256"], + event.topics[3] + ); + + return { + amount, + mintRecipient, + destinationDomain, + destinationTokenMessenger, + destinationCaller, + maxFee, + hookData, + burnToken, + depositer, + minFinalityThreshold, + }; +}; + +const decodeMessageSentEvent = (event) => { + const evData = event.data.slice(130); // ignore first two slots along with 0x prefix + + const version = ethers.BigNumber.from(`0x${evData.slice(0, 8)}`); + const sourceDomain = ethers.BigNumber.from(`0x${evData.slice(8, 16)}`); + const desinationDomain = ethers.BigNumber.from(`0x${evData.slice(16, 24)}`); + // Ignore empty nonce from 24 to 88 + const [sender, recipient, destinationCaller] = + ethers.utils.defaultAbiCoder.decode( + ["address", "address", "address"], + `0x${evData.slice(88, 280)}` + ); + const minFinalityThreshold = ethers.BigNumber.from( + `0x${evData.slice(280, 288)}` + ); + // Ignore empty threshold from 288 to 296 + const endIndex = evData.endsWith("00000000") + ? evData.length - 8 + : evData.length; + const payload = `0x${evData.slice(296, endIndex)}`; + + return { + version, + sourceDomain, + desinationDomain, + sender, + recipient, + destinationCaller, + minFinalityThreshold, + payload, + }; +}; + +const encodeDepositMessageBody = (nonce, amount) => { + const encodedPayload = ethers.utils.defaultAbiCoder.encode( + ["uint64", "uint256"], + [nonce, amount] + ); + return `0x000003f200000001${encodedPayload.slice(2)}`; +}; + +const encodeWithdrawMessageBody = (nonce, amount) => { + const encodedPayload = ethers.utils.defaultAbiCoder.encode( + ["uint64", "uint256"], + [nonce, amount] + ); + return `0x000003f200000002${encodedPayload.slice(2)}`; +}; + +const decodeDepositOrWithdrawMessage = (message) => { + message = message.slice(2); // Ignore 0x prefix + + const originMessageVersion = ethers.BigNumber.from( + `0x${message.slice(0, 8)}` + ); + const messageType = ethers.BigNumber.from(`0x${message.slice(8, 16)}`); + expect(originMessageVersion).to.eq(1010); + + const [nonce, amount] = ethers.utils.defaultAbiCoder.decode( + ["uint64", "uint256"], + `0x${message.slice(16)}` + ); + + return { + messageType, + nonce, + amount, + }; +}; + +const encodeCCTPMessage = ( + sourceDomain, + sender, + recipient, + messageBody, + version = 1 +) => { + const versionStr = version.toString(16).padStart(8, "0"); + const sourceDomainStr = sourceDomain.toString(16).padStart(8, "0"); + const senderStr = sender.replace("0x", "").toLowerCase().padStart(64, "0"); + const recipientStr = recipient + .replace("0x", "") + .toLowerCase() + .padStart(64, "0"); + const messageBodyStr = messageBody.slice(2); + return `0x${versionStr}${sourceDomainStr}${empty18Bytes}${senderStr}${recipientStr}${empty20Bytes}${messageBodyStr}`; +}; + +const encodeBurnMessageBody = (sender, recipient, amount, hookData) => { + const senderEncoded = ethers.utils.defaultAbiCoder + .encode(["address"], [sender]) + .slice(2); + const recipientEncoded = ethers.utils.defaultAbiCoder + .encode(["address"], [recipient]) + .slice(2); + const amountEncoded = ethers.utils.defaultAbiCoder + .encode(["uint256"], [amount]) + .slice(2); + const encodedHookData = hookData.slice(2); + return `0x00000001${empty16Bytes}${recipientEncoded}${amountEncoded}${senderEncoded}${empty16Bytes.repeat( + 3 + )}${encodedHookData}`; +}; +const decodeBurnMessageBody = (message) => { + message = message.slice(2); // Ignore 0x prefix + + const version = ethers.BigNumber.from(`0x${message.slice(0, 8)}`); + expect(version).to.eq(1); + const [burnToken, recipient, amount, sender] = + ethers.utils.defaultAbiCoder.decode( + ["address", "address", "uint256", "address"], + `0x${message.slice(8, 264)}` + ); + + const hookData = `0x${message.slice(456)}`; // Ignore 0x prefix and following 96 bytes + return { version, burnToken, recipient, amount, sender, hookData }; +}; + +const encodeBalanceCheckMessageBody = (nonce, balance) => { + const encodedPayload = ethers.utils.defaultAbiCoder.encode( + ["uint64", "uint256"], + [nonce, balance] + ); + + // const version = 1010; // ORIGIN_MESSAGE_VERSION + // const messageType = 3; // BALANCE_CHECK_MESSAGE + return `0x000003f200000003${encodedPayload.slice(2)}`; +}; + +const decodeBalanceCheckMessageBody = (message) => { + message = message.slice(2); // Ignore 0x prefix + const version = ethers.BigNumber.from(`0x${message.slice(0, 8)}`); + const messageType = ethers.BigNumber.from(`0x${message.slice(8, 16)}`); + expect(version).to.eq(1010); + expect(messageType).to.eq(3); + const [nonce, balance] = ethers.utils.defaultAbiCoder.decode( + ["uint64", "uint256"], + `0x${message.slice(16)}` + ); + return { version, messageType, nonce, balance }; +}; + +const replaceMessageTransmitter = async () => { + const mockMessageTransmitter = await ethers.getContract( + "CCTPMessageTransmitterMock2" + ); + await replaceContractAt( + addresses.CCTPMessageTransmitterV2, + mockMessageTransmitter + ); + const replacedTransmitter = await ethers.getContractAt( + "CCTPMessageTransmitterMock2", + addresses.CCTPMessageTransmitterV2 + ); + await replacedTransmitter.setCCTPTokenMessenger( + addresses.CCTPTokenMessengerV2 + ); + + return replacedTransmitter; +}; + +const setRemoteStrategyBalance = async (strategy, balance) => { + await setStorageAt( + strategy.address, + `0x${REMOTE_STRATEGY_BALANCE_SLOT.toString(16)}`, + balance.toHexString() + ); +}; + +module.exports = { + DEPOSIT_FOR_BURN_EVENT_TOPIC, + MESSAGE_SENT_EVENT_TOPIC, + emptyByte, + empty2Bytes, + empty4Bytes, + empty16Bytes, + empty18Bytes, + empty20Bytes, + REMOTE_STRATEGY_BALANCE_SLOT, + setRemoteStrategyBalance, + decodeDepositForBurnEvent, + decodeMessageSentEvent, + decodeDepositOrWithdrawMessage, + encodeCCTPMessage, + encodeDepositMessageBody, + encodeWithdrawMessageBody, + encodeBurnMessageBody, + decodeBurnMessageBody, + encodeBalanceCheckMessageBody, + decodeBalanceCheckMessageBody, + replaceMessageTransmitter, +}; diff --git a/contracts/test/strategies/crosschain/crosschain-master-strategy.mainnet.fork-test.js b/contracts/test/strategies/crosschain/crosschain-master-strategy.mainnet.fork-test.js index fa8a3f558a..295dbb2b52 100644 --- a/contracts/test/strategies/crosschain/crosschain-master-strategy.mainnet.fork-test.js +++ b/contracts/test/strategies/crosschain/crosschain-master-strategy.mainnet.fork-test.js @@ -3,183 +3,20 @@ const { expect } = require("chai"); const { usdcUnits, isCI } = require("../../helpers"); const { createFixtureLoader, crossChainFixture } = require("../../_fixture"); const { impersonateAndFund } = require("../../../utils/signers"); -// const { formatUnits } = require("ethers/lib/utils"); const addresses = require("../../../utils/addresses"); const loadFixture = createFixtureLoader(crossChainFixture); -const { setStorageAt } = require("@nomicfoundation/hardhat-network-helpers"); -const { replaceContractAt } = require("../../../utils/hardhat"); - -const DEPOSIT_FOR_BURN_EVENT_TOPIC = - "0x0c8c1cbdc5190613ebd485511d4e2812cfa45eecb79d845893331fedad5130a5"; -const MESSAGE_SENT_EVENT_TOPIC = - "0x8c5261668696ce22758910d05bab8f186d6eb247ceac2af2e82c7dc17669b036"; - -// const ORIGIN_MESSAGE_VERSION_HEX = "0x000003f2"; // 1010 - -const emptyByte = "0000"; -const empty2Bytes = emptyByte.repeat(2); -const empty4Bytes = emptyByte.repeat(4); -const empty16Bytes = empty4Bytes.repeat(4); -const empty18Bytes = `${empty2Bytes}${empty16Bytes}`; -const empty20Bytes = empty4Bytes.repeat(5); - -const REMOTE_STRATEGY_BALANCE_SLOT = 210; - -const decodeDepositForBurnEvent = (event) => { - const [ - amount, - mintRecipient, - destinationDomain, - destinationTokenMessenger, - destinationCaller, - maxFee, - hookData, - ] = ethers.utils.defaultAbiCoder.decode( - ["uint256", "address", "uint32", "address", "address", "uint256", "bytes"], - event.data - ); - - const [burnToken] = ethers.utils.defaultAbiCoder.decode( - ["address"], - event.topics[1] - ); - const [depositer] = ethers.utils.defaultAbiCoder.decode( - ["address"], - event.topics[2] - ); - const [minFinalityThreshold] = ethers.utils.defaultAbiCoder.decode( - ["uint256"], - event.topics[3] - ); - - return { - amount, - mintRecipient, - destinationDomain, - destinationTokenMessenger, - destinationCaller, - maxFee, - hookData, - burnToken, - depositer, - minFinalityThreshold, - }; -}; - -const decodeMessageSentEvent = (event) => { - const evData = event.data.slice(130); // ignore first two slots along with 0x prefix - - const version = ethers.BigNumber.from(`0x${evData.slice(0, 8)}`); - const sourceDomain = ethers.BigNumber.from(`0x${evData.slice(8, 16)}`); - const desinationDomain = ethers.BigNumber.from(`0x${evData.slice(16, 24)}`); - // Ignore empty nonce from 24 to 88 - const [sender, recipient, destinationCaller] = - ethers.utils.defaultAbiCoder.decode( - ["address", "address", "address"], - `0x${evData.slice(88, 280)}` - ); - const minFinalityThreshold = ethers.BigNumber.from( - `0x${evData.slice(280, 288)}` - ); - // Ignore empty threshold from 288 to 296 - const payload = `0x${evData.slice(296, evData.length - 8)}`; - - return { - version, - sourceDomain, - desinationDomain, - sender, - recipient, - destinationCaller, - minFinalityThreshold, - payload, - }; -}; - -const decodeDepositOrWithdrawMessage = (message) => { - message = message.slice(2); // Ignore 0x prefix - - const originMessageVersion = ethers.BigNumber.from( - `0x${message.slice(0, 8)}` - ); - const messageType = ethers.BigNumber.from(`0x${message.slice(8, 16)}`); - expect(originMessageVersion).to.eq(1010); - - const [nonce, amount] = ethers.utils.defaultAbiCoder.decode( - ["uint64", "uint256"], - `0x${message.slice(16)}` - ); - - return { - messageType, - nonce, - amount, - }; -}; - -const encodeCCTPMessage = ( - sourceDomain, - sender, - recipient, - messageBody, - version = 1 -) => { - const versionStr = version.toString(16).padStart(8, "0"); - const sourceDomainStr = sourceDomain.toString(16).padStart(8, "0"); - const senderStr = sender.replace("0x", "").toLowerCase().padStart(64, "0"); - const recipientStr = recipient - .replace("0x", "") - .toLowerCase() - .padStart(64, "0"); - const messageBodyStr = messageBody.slice(2); - return `0x${versionStr}${sourceDomainStr}${empty18Bytes}${senderStr}${recipientStr}${empty20Bytes}${messageBodyStr}`; -}; - -const encodeBurnMessageBody = (sender, recipient, amount, hookData) => { - const senderEncoded = ethers.utils.defaultAbiCoder - .encode(["address"], [sender]) - .slice(2); - const recipientEncoded = ethers.utils.defaultAbiCoder - .encode(["address"], [recipient]) - .slice(2); - const amountEncoded = ethers.utils.defaultAbiCoder - .encode(["uint256"], [amount]) - .slice(2); - const encodedHookData = hookData.slice(2); - return `0x00000001${empty16Bytes}${recipientEncoded}${amountEncoded}${senderEncoded}${empty16Bytes.repeat( - 3 - )}${encodedHookData}`; -}; - -const encodeBalanceCheckMessageBody = (nonce, balance) => { - const encodedPayload = ethers.utils.defaultAbiCoder.encode( - ["uint64", "uint256"], - [nonce, balance] - ); - - // const version = 1010; // ORIGIN_MESSAGE_VERSION - // const messageType = 3; // BALANCE_CHECK_MESSAGE - return `0x000003f200000003${encodedPayload.slice(2)}`; -}; - -const replaceMessageTransmitter = async () => { - const mockMessageTransmitter = await ethers.getContract( - "CCTPMessageTransmitterMock2" - ); - await replaceContractAt( - addresses.CCTPMessageTransmitterV2, - mockMessageTransmitter - ); - const replacedTransmitter = await ethers.getContractAt( - "CCTPMessageTransmitterMock2", - addresses.CCTPMessageTransmitterV2 - ); - await replacedTransmitter.setCCTPTokenMessenger( - addresses.CCTPTokenMessengerV2 - ); - - return replacedTransmitter; -}; +const { + DEPOSIT_FOR_BURN_EVENT_TOPIC, + MESSAGE_SENT_EVENT_TOPIC, + setRemoteStrategyBalance, + decodeDepositForBurnEvent, + decodeMessageSentEvent, + decodeDepositOrWithdrawMessage, + encodeCCTPMessage, + encodeBurnMessageBody, + encodeBalanceCheckMessageBody, + replaceMessageTransmitter, +} = require("./_crosschain-helpers"); describe("ForkTest: CrossChainMasterStrategy", function () { this.timeout(0); @@ -292,10 +129,9 @@ describe("ForkTest: CrossChainMasterStrategy", function () { const impersonatedVault = await impersonateAndFund(vaultAddr); // set an arbitrary remote strategy balance - await setStorageAt( - crossChainMasterStrategy.address, - `0x${REMOTE_STRATEGY_BALANCE_SLOT.toString(16)}`, - usdcUnits("1000").toHexString() + await setRemoteStrategyBalance( + crossChainMasterStrategy, + usdcUnits("1000") ); const tx = await crossChainMasterStrategy @@ -452,10 +288,9 @@ describe("ForkTest: CrossChainMasterStrategy", function () { const impersonatedVault = await impersonateAndFund(vaultAddr); // set an arbitrary remote strategy balance - await setStorageAt( - crossChainMasterStrategy.address, - `0x${REMOTE_STRATEGY_BALANCE_SLOT.toString(16)}`, - usdcUnits("123456").toHexString() + await setRemoteStrategyBalance( + crossChainMasterStrategy, + usdcUnits("123456") ); // Simulate withdrawal call @@ -516,10 +351,9 @@ describe("ForkTest: CrossChainMasterStrategy", function () { const impersonatedVault = await impersonateAndFund(vaultAddr); // set an arbitrary remote strategy balance - await setStorageAt( - crossChainMasterStrategy.address, - `0x${REMOTE_STRATEGY_BALANCE_SLOT.toString(16)}`, - usdcUnits("1000").toHexString() + await setRemoteStrategyBalance( + crossChainMasterStrategy, + usdcUnits("1000") ); const remoteStrategyBalanceBefore = diff --git a/contracts/test/strategies/crosschain/crosschain-remote-strategy.base.fork-test.js b/contracts/test/strategies/crosschain/crosschain-remote-strategy.base.fork-test.js index 2293d484e7..4398e768bf 100644 --- a/contracts/test/strategies/crosschain/crosschain-remote-strategy.base.fork-test.js +++ b/contracts/test/strategies/crosschain/crosschain-remote-strategy.base.fork-test.js @@ -1,10 +1,20 @@ -// const { expect } = require("chai"); +const { expect } = require("chai"); -const { isCI } = require("../../helpers"); +const { isCI, usdcUnits } = require("../../helpers"); const { createFixtureLoader } = require("../../_fixture"); const { crossChainFixture } = require("../../_fixture-base"); -// const { impersonateAndFund } = require("../../../utils/signers"); -// const { formatUnits } = require("ethers/lib/utils"); +const { + MESSAGE_SENT_EVENT_TOPIC, + decodeMessageSentEvent, + decodeBalanceCheckMessageBody, + replaceMessageTransmitter, + encodeBurnMessageBody, + decodeBurnMessageBody, + encodeCCTPMessage, + encodeDepositMessageBody, + encodeWithdrawMessageBody, +} = require("./_crosschain-helpers"); +const addresses = require("../../../utils/addresses"); const loadFixture = createFixtureLoader(crossChainFixture); @@ -19,26 +29,221 @@ describe("ForkTest: CrossChainRemoteStrategy", function () { fixture = await loadFixture(); }); - it("Should initiate a bridge of deposited USDC", async function () { - const { crossChainRemoteStrategy } = fixture; - await crossChainRemoteStrategy.sendBalanceUpdate(); - // const govAddr = (await crossChainMasterStrategy.governor()) - // const governor = await impersonateAndFund(govAddr); - // const vaultAddr = await crossChainMasterStrategy.vaultAddress(); + const verifyBalanceCheckMessage = ( + messageSentEvent, + expectedNonce, + expectedBalance, + transferAmount = "0" + ) => { + const { crossChainRemoteStrategy, usdc } = fixture; + const { + version, + sourceDomain, + desinationDomain, + sender, + recipient, + destinationCaller, + minFinalityThreshold, + payload, + } = decodeMessageSentEvent(messageSentEvent); - // const impersonatedVault = await impersonateAndFund(vaultAddr); + expect(version).to.eq(1); + expect(sourceDomain).to.eq(6); + expect(desinationDomain).to.eq(0); + expect(destinationCaller.toLowerCase()).to.eq( + crossChainRemoteStrategy.address.toLowerCase() + ); + expect(minFinalityThreshold).to.eq(2000); - // // Let the strategy hold some USDC - // await usdc.connect(matt).transfer(crossChainMasterStrategy.address, usdcUnits("1000")); + let balanceCheckPayload = payload; - // const balanceBefore = await usdc.balanceOf(crossChainMasterStrategy.address); + const isBurnMessage = + sender.toLowerCase() == addresses.CCTPTokenMessengerV2.toLowerCase(); + if (isBurnMessage) { + // Verify burn message + const { burnToken, recipient, amount, sender, hookData } = + decodeBurnMessageBody(payload); + expect(burnToken.toLowerCase()).to.eq(usdc.address.toLowerCase()); + expect(recipient.toLowerCase()).to.eq( + crossChainRemoteStrategy.address.toLowerCase() + ); + expect(amount).to.eq(transferAmount); + expect(sender.toLowerCase()).to.eq( + crossChainRemoteStrategy.address.toLowerCase() + ); + balanceCheckPayload = hookData; + } else { + // Ensure sender and recipient are the strategy address + expect(sender.toLowerCase()).to.eq( + crossChainRemoteStrategy.address.toLowerCase() + ); + expect(recipient.toLowerCase()).to.eq( + crossChainRemoteStrategy.address.toLowerCase() + ); + } - // // Simulate deposit call - // await crossChainMasterStrategy.connect(impersonatedVault).deposit(usdc.address, usdcUnits("1000")); + const { + version: balanceCheckVersion, + messageType, + nonce, + balance, + } = decodeBalanceCheckMessageBody(balanceCheckPayload); - // const balanceAfter = await usdc.balanceOf(crossChainMasterStrategy.address); + expect(balanceCheckVersion).to.eq(1010); + expect(messageType).to.eq(3); + expect(nonce).to.eq(expectedNonce); + expect(balance).to.approxEqual(expectedBalance); + }; - // console.log(`Balance before: ${formatUnits(balanceBefore, 6)}`); - // console.log(`Balance after: ${formatUnits(balanceAfter, 6)}`); + it("Should send a balance update message", async function () { + const { crossChainRemoteStrategy, strategist, rafael, usdc } = fixture; + // Send some USDC to the remote strategy + await usdc + .connect(rafael) + .transfer(crossChainRemoteStrategy.address, usdcUnits("1234")); + + const balanceBefore = await crossChainRemoteStrategy.checkBalance( + usdc.address + ); + const nonceBefore = await crossChainRemoteStrategy.lastTransferNonce(); + + const tx = await crossChainRemoteStrategy + .connect(strategist) + .sendBalanceUpdate(); + const receipt = await tx.wait(); + const messageSentEvent = receipt.events.find((e) => + e.topics.includes(MESSAGE_SENT_EVENT_TOPIC) + ); + + verifyBalanceCheckMessage( + messageSentEvent, + nonceBefore.toNumber(), + balanceBefore + ); + }); + + it("Should handle deposits", async function () { + const { crossChainRemoteStrategy, strategist, rafael, usdc } = fixture; + + // snapshot state + const balanceBefore = await crossChainRemoteStrategy.checkBalance( + usdc.address + ); + const nonceBefore = await crossChainRemoteStrategy.lastTransferNonce(); + + const depositAmount = usdcUnits("1234.56"); + + // Replace transmitter to mock transmitter + await replaceMessageTransmitter(); + + const nextNonce = nonceBefore.toNumber() + 1; + + // Build deposit message + const depositPayload = encodeDepositMessageBody(nextNonce, depositAmount); + const burnPayload = encodeBurnMessageBody( + crossChainRemoteStrategy.address, + crossChainRemoteStrategy.address, + depositAmount, + depositPayload + ); + const message = encodeCCTPMessage( + 0, + addresses.CCTPTokenMessengerV2, + addresses.CCTPTokenMessengerV2, + burnPayload + ); + + // Simulate token transfer + await usdc + .connect(rafael) + .transfer(crossChainRemoteStrategy.address, depositAmount); + + // Relay the message + const tx = await crossChainRemoteStrategy + .connect(strategist) + .relay(message, "0x"); + + // Check if it sent the check balance message + const receipt = await tx.wait(); + const messageSentEvent = receipt.events.find((e) => + e.topics.includes(MESSAGE_SENT_EVENT_TOPIC) + ); + + // Verify the balance check message + const expectedBalance = balanceBefore.add(depositAmount); + verifyBalanceCheckMessage(messageSentEvent, nextNonce, expectedBalance); + + const nonceAfter = await crossChainRemoteStrategy.lastTransferNonce(); + expect(nonceAfter).to.eq(nextNonce); + + const balanceAfter = await crossChainRemoteStrategy.checkBalance( + usdc.address + ); + expect(balanceAfter).to.approxEqual(expectedBalance); + }); + + it("Should handle withdrawals", async function () { + const { crossChainRemoteStrategy, strategist, rafael, usdc } = fixture; + + const withdrawalAmount = usdcUnits("1234.56"); + + // Make sure the strategy has enough balance + const depositAmount = withdrawalAmount.mul(2); + await usdc + .connect(rafael) + .transfer(crossChainRemoteStrategy.address, depositAmount); + await crossChainRemoteStrategy + .connect(strategist) + .deposit(usdc.address, depositAmount); + + // snapshot state + const balanceBefore = await crossChainRemoteStrategy.checkBalance( + usdc.address + ); + const nonceBefore = await crossChainRemoteStrategy.lastTransferNonce(); + const nextNonce = nonceBefore.toNumber() + 1; + + // Build withdrawal message + const withdrawalPayload = encodeWithdrawMessageBody( + nextNonce, + withdrawalAmount + ); + const message = encodeCCTPMessage( + 0, + crossChainRemoteStrategy.address, + crossChainRemoteStrategy.address, + withdrawalPayload + ); + + // Replace transmitter to mock transmitter + await replaceMessageTransmitter(); + + // Relay the message + const tx = await crossChainRemoteStrategy + .connect(strategist) + .relay(message, "0x"); + + // Check if it sent the check balance message + const receipt = await tx.wait(); + const messageSentEvent = receipt.events.find((e) => + e.topics.includes(MESSAGE_SENT_EVENT_TOPIC) + ); + + // Verify the balance check message + const expectedBalance = balanceBefore.sub(withdrawalAmount); + verifyBalanceCheckMessage( + messageSentEvent, + nextNonce, + expectedBalance, + withdrawalAmount + ); + + const nonceAfter = await crossChainRemoteStrategy.lastTransferNonce(); + expect(nonceAfter).to.eq(nextNonce); + + const balanceAfter = await crossChainRemoteStrategy.checkBalance( + usdc.address + ); + expect(balanceAfter).to.approxEqual(expectedBalance); }); }); From 54ca3c01b7e072dbc142932eec16df873b272901 Mon Sep 17 00:00:00 2001 From: Shahul Hameed <10547529+shahthepro@users.noreply.github.com> Date: Fri, 26 Dec 2025 11:32:10 +0400 Subject: [PATCH 039/101] Update comments and clean up code --- .../crosschain/AbstractCCTPIntegrator.sol | 195 +++++++++++++----- .../crosschain/CrossChainMasterStrategy.sol | 103 ++++----- .../crosschain/CrossChainRemoteStrategy.sol | 79 +++++-- 3 files changed, 249 insertions(+), 128 deletions(-) diff --git a/contracts/contracts/strategies/crosschain/AbstractCCTPIntegrator.sol b/contracts/contracts/strategies/crosschain/AbstractCCTPIntegrator.sol index 356da25ed1..1fc6e0d116 100644 --- a/contracts/contracts/strategies/crosschain/AbstractCCTPIntegrator.sol +++ b/contracts/contracts/strategies/crosschain/AbstractCCTPIntegrator.sol @@ -1,6 +1,13 @@ // SPDX-License-Identifier: BUSL-1.1 pragma solidity ^0.8.0; +/** + * @title AbstractCCTPIntegrator + * @author Origin Protocol Inc + * + * @dev Abstract contract that contains all the logic used to integrate with CCTP. + */ + import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import { IERC20 } from "../../utils/InitializableAbstractStrategy.sol"; @@ -56,14 +63,17 @@ 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 uint64 public lastTransferNonce; + // Mapping of processed nonces mapping(uint64 => bool) private nonceProcessed; + // Operator address: Can relay CCTP messages address public operator; // For future use @@ -95,9 +105,15 @@ abstract contract AbstractCCTPIntegrator is Governable, IMessageHandlerV2 { _config.cctpMessageTransmitter ); cctpTokenMessenger = ICCTPTokenMessenger(_config.cctpTokenMessenger); + + // Domain ID of the chain from which messages are accepted peerDomainID = _config.peerDomainID; + // Strategy address on other chain, should + // always be same as the proxy of this strategy peerStrategy = _config.peerStrategy; + + // USDC address on local chain baseToken = _config.baseToken; // Just a sanity check to ensure the base token is USDC @@ -111,6 +127,12 @@ abstract contract AbstractCCTPIntegrator is Governable, IMessageHandlerV2 { ); } + /** + * @dev Initialize the implementation contract + * @param _operator Operator address + * @param _minFinalityThreshold Minimum finality threshold + * @param _feePremiumBps Fee premium in basis points + */ function _initialize( address _operator, uint32 _minFinalityThreshold, @@ -124,15 +146,30 @@ abstract contract AbstractCCTPIntegrator is Governable, IMessageHandlerV2 { /*************************************** Settings ****************************************/ + /** + * @dev Set the operator address + * @param _operator Operator address + */ function setOperator(address _operator) external onlyGovernor { _setOperator(_operator); } + /** + * @dev Set the operator address + * @param _operator Operator address + */ function _setOperator(address _operator) internal { operator = _operator; emit OperatorChanged(_operator); } + /** + * @dev Set the minimum finality threshold at which + * the message is considered to be finalized to relay. + * Only accepts a value of 1000 (Safe, after 1 epoch) or + * 2000 (Finalized, after 2 epochs). + * @param _minFinalityThreshold Minimum finality threshold + */ function setMinFinalityThreshold(uint32 _minFinalityThreshold) external onlyGovernor @@ -140,6 +177,10 @@ abstract contract AbstractCCTPIntegrator is Governable, IMessageHandlerV2 { _setMinFinalityThreshold(_minFinalityThreshold); } + /** + * @dev Set the minimum finality threshold + * @param _minFinalityThreshold Minimum finality threshold + */ function _setMinFinalityThreshold(uint32 _minFinalityThreshold) internal { // 1000 for fast transfer and 2000 for standard transfer require( @@ -151,10 +192,19 @@ abstract contract AbstractCCTPIntegrator is Governable, IMessageHandlerV2 { emit CCTPMinFinalityThresholdSet(_minFinalityThreshold); } + /** + * @dev Set the fee premium in basis points. + * Cannot be higher than 30% (3000 basis points). + * @param _feePremiumBps Fee premium in basis points + */ function setFeePremiumBps(uint32 _feePremiumBps) external onlyGovernor { _setFeePremiumBps(_feePremiumBps); } + /** + * @dev Set the fee premium in basis points + * @param _feePremiumBps Fee premium in basis points + */ function _setFeePremiumBps(uint32 _feePremiumBps) internal { require(_feePremiumBps <= 3000, "Fee premium too high"); // 30% @@ -166,6 +216,13 @@ abstract contract AbstractCCTPIntegrator is Governable, IMessageHandlerV2 { CCTP message handling ****************************************/ + /** + * @dev Handles a finalized CCTP message + * @param sourceDomain Source domain of the message + * @param sender Sender of the message + * @param finalityThresholdExecuted Fidelity threshold executed + * @param messageBody Message body + */ function handleReceiveFinalizedMessage( uint32 sourceDomain, bytes32 sender, @@ -181,12 +238,25 @@ abstract contract AbstractCCTPIntegrator is Governable, IMessageHandlerV2 { ); } + /** + * @dev Handles an unfinalized but safe CCTP message + * @param sourceDomain Source domain of the message + * @param sender Sender of the message + * @param finalityThresholdExecuted Fidelity threshold executed + * @param messageBody Message body + */ function handleReceiveUnfinalizedMessage( uint32 sourceDomain, bytes32 sender, uint32 finalityThresholdExecuted, bytes memory messageBody ) external override onlyCCTPMessageTransmitter returns (bool) { + // Make sure the contract is configured to handle unfinalized messages + require( + minFinalityThreshold == 1000, + "Unfinalized messages are not supported" + ); + return _handleReceivedMessage( sourceDomain, @@ -196,6 +266,13 @@ abstract contract AbstractCCTPIntegrator is Governable, IMessageHandlerV2 { ); } + /** + * @dev Handles a CCTP message + * @param sourceDomain Source domain of the message + * @param sender Sender of the message + * @param finalityThresholdExecuted Fidelity threshold executed + * @param messageBody Message body + */ function _handleReceivedMessage( uint32 sourceDomain, bytes32 sender, @@ -203,12 +280,6 @@ abstract contract AbstractCCTPIntegrator is Governable, IMessageHandlerV2 { uint32 finalityThresholdExecuted, bytes memory messageBody ) internal returns (bool) { - // // Make sure that the finality threshold is same on both chains - // // TODO: Do we really need this? Also, fix this - // require( - // finalityThresholdExecuted >= minFinalityThreshold, - // "Finality threshold too low" - // ); require(sourceDomain == peerDomainID, "Unknown Source Domain"); // Extract address from bytes32 (CCTP stores addresses as right-padded bytes32) @@ -220,22 +291,34 @@ abstract contract AbstractCCTPIntegrator is Governable, IMessageHandlerV2 { return true; } + /** + * @dev Sends tokens to the peer strategy using CCTP Token Messenger + * @param tokenAmount Amount of tokens to send + * @param hookData Hook data + */ function _sendTokens(uint256 tokenAmount, bytes memory hookData) internal virtual { + // CCTP has a maximum transfer amount of 10M USDC per tx require(tokenAmount <= MAX_TRANSFER_AMOUNT, "Token amount too high"); + // Approve only what needs to be transferred IERC20(baseToken).safeApprove(address(cctpTokenMessenger), tokenAmount); + // Compute the max fee to be paid. // Ref: https://developers.circle.com/cctp/evm-smart-contracts#getminfeeamount + // The right way to compute fees would be to use CCTP's getMinFeeAmount function. // The issue is that the getMinFeeAmount is not present on v2.0 contracts, but is on - // v2.1. We will only be using standard transfers and fee on those is 0 for now - + // v2.1. Some of CCTP's deployed contracts are v2.0, some are v2.1. + // We will only be using standard transfers and fee on those is 0 for now. If they + // ever start implementing fee for standard transfers or if we decide to use fast + // trasnfer, we can use feePremiumBps as a workaround. uint256 maxFee = feePremiumBps > 0 ? (tokenAmount * feePremiumBps) / 10000 : 0; + // Send tokens to the peer strategy using CCTP Token Messenger cctpTokenMessenger.depositForBurnWithHook( tokenAmount, peerDomainID, @@ -248,6 +331,10 @@ abstract contract AbstractCCTPIntegrator is Governable, IMessageHandlerV2 { ); } + /** + * @dev Sends a message to the peer strategy using CCTP Message Transmitter + * @param message Payload of the message to send + */ function _sendMessage(bytes memory message) internal virtual { cctpMessageTransmitter.sendMessage( peerDomainID, @@ -258,6 +345,14 @@ abstract contract AbstractCCTPIntegrator is Governable, IMessageHandlerV2 { ); } + /** + * @dev Receives a message from the peer strategy on the other chain, + * does some basic checks and relays it to the local MessageTransmitterV2. + * If the message is a burn message, it will also handle the hook data + * and call the _onTokenReceived function. + * @param message Payload of the message to send + * @param attestation Attestation of the message + */ function relay(bytes memory message, bytes memory attestation) external onlyOperator @@ -316,6 +411,8 @@ abstract contract AbstractCCTPIntegrator is Governable, IMessageHandlerV2 { ); } + // Ensure the recipient is this contract + // Both sender and recipient should be deployed to same address on both chains. require(address(this) == recipient, "Unexpected recipient address"); require(sender == peerStrategy, "Incorrect sender/recipient address"); @@ -328,19 +425,23 @@ abstract contract AbstractCCTPIntegrator is Governable, IMessageHandlerV2 { require(relaySuccess, "Receive message failed"); if (isBurnMessageV1) { + // Extract the hook data from the message body bytes memory hookData = messageBody.extractSlice( BURN_MESSAGE_V2_HOOK_DATA_INDEX, messageBody.length ); + // Extract the token amount from the message body uint256 tokenAmount = messageBody.extractUint256( BURN_MESSAGE_V2_AMOUNT_INDEX ); + // Extract the fee executed from the message body uint256 feeExecuted = messageBody.extractUint256( BURN_MESSAGE_V2_FEE_EXECUTED_INDEX ); + // Call the _onTokenReceived function _onTokenReceived(tokenAmount - feeExecuted, feeExecuted, hookData); } } @@ -348,50 +449,15 @@ abstract contract AbstractCCTPIntegrator is Governable, IMessageHandlerV2 { /*************************************** Message utils ****************************************/ - - function _getMessageVersion(bytes memory message) - internal - virtual - returns (uint32) - { - // uint32 bytes 0 to 4 is Origin message version - // uint32 bytes 4 to 8 is Message type - return message.extractUint32(0); - } - - function _getMessageType(bytes memory message) - internal - virtual - returns (uint32) - { - // uint32 bytes 0 to 4 is Origin message version - // uint32 bytes 4 to 8 is Message type - return message.extractUint32(4); - } - - function _verifyMessageVersionAndType( - bytes memory _message, - uint32 _version, - uint32 _type - ) internal virtual { - require( - _getMessageVersion(_message) == _version, - "Invalid Origin Message Version" - ); - require(_getMessageType(_message) == _type, "Invalid Message type"); - } - - function _getMessagePayload(bytes memory message) - internal - virtual - returns (bytes memory) - { - // uint32 bytes 0 to 4 is Origin message version - // uint32 bytes 4 to 8 is Message type - // Payload starts at byte 8 - return message.extractSlice(8, message.length); - } - + /** + * @dev Decodes the CCTP message header + * @param message Message to decode + * @return version Version of the message + * @return sourceDomainID Source domain ID + * @return sender Sender of the message + * @return recipient Recipient of the message + * @return messageBody Message body + */ function _decodeMessageHeader(bytes memory message) internal pure @@ -415,16 +481,33 @@ abstract contract AbstractCCTPIntegrator is Governable, IMessageHandlerV2 { /*************************************** Nonce Handling ****************************************/ - + /** + * @dev Checks if the last known transfer is pending. + * Nonce starts at 1, so 0 is disregarded. + * @return True if a transfer is pending, false otherwise + */ function isTransferPending() public view returns (bool) { uint64 nonce = lastTransferNonce; return nonce > 0 && !nonceProcessed[nonce]; } + /** + * @dev Checks if a given nonce is processed. + * Nonce starts at 1, so 0 is disregarded. + * @param nonce Nonce to check + * @return True if the nonce is processed, false otherwise + */ function isNonceProcessed(uint64 nonce) public view returns (bool) { return nonce == 0 || nonceProcessed[nonce]; } + /** + * @dev Marks a given nonce as processed. + * Can only mark nonce as processed once. New nonce should + * always be greater than the last known nonce. Also updates + * the last known nonce. + * @param nonce Nonce to mark as processed + */ function _markNonceAsProcessed(uint64 nonce) internal { uint64 lastNonce = lastTransferNonce; @@ -441,6 +524,12 @@ abstract contract AbstractCCTPIntegrator is Governable, IMessageHandlerV2 { } } + /** + * @dev Gets the next nonce to use. + * Nonce starts at 1, so 0 is disregarded. + * Reverts if last nonce hasn't been processed yet. + * @return Next nonce + */ function _getNextNonce() internal returns (uint64) { uint64 nonce = lastTransferNonce; diff --git a/contracts/contracts/strategies/crosschain/CrossChainMasterStrategy.sol b/contracts/contracts/strategies/crosschain/CrossChainMasterStrategy.sol index 9856e0eeb8..81705b5179 100644 --- a/contracts/contracts/strategies/crosschain/CrossChainMasterStrategy.sol +++ b/contracts/contracts/strategies/crosschain/CrossChainMasterStrategy.sol @@ -32,6 +32,7 @@ contract CrossChainMasterStrategy is Deposit, Withdrawal } + // Mapping of nonce to transfer type mapping(uint64 => TransferType) public transferTypeByNonce; event RemoteStrategyBalanceUpdated(uint256 balance); @@ -48,6 +49,12 @@ contract CrossChainMasterStrategy is AbstractCCTPIntegrator(_cctpConfig) {} + /** + * @dev Initialize the strategy implementation + * @param _operator Address of the operator + * @param _minFinalityThreshold Minimum finality threshold + * @param _feePremiumBps Fee premium in basis points + */ function initialize( address _operator, uint32 _minFinalityThreshold, @@ -66,19 +73,7 @@ contract CrossChainMasterStrategy is ); } - // /** - // * @dev Returns the address of the Remote part of the strategy on L2 - // */ - // function remoteAddress() internal virtual returns (address) { - // return address(this); - // } - - /** - * @dev Deposit asset into mainnet strategy making them ready to be - * bridged to Remote part of the strategy - * @param _asset Address of asset to deposit - * @param _amount Amount of asset to deposit - */ + /// @inheritdoc Generalized4626Strategy function deposit(address _asset, uint256 _amount) external override @@ -88,9 +83,7 @@ contract CrossChainMasterStrategy is _deposit(_asset, _amount); } - /** - * @dev Deposit the entire balance - */ + /// @inheritdoc Generalized4626Strategy function depositAll() external override onlyVault nonReentrant { uint256 balance = IERC20(baseToken).balanceOf(address(this)); if (balance > 0) { @@ -98,12 +91,7 @@ contract CrossChainMasterStrategy is } } - /** - * @dev Send a withdrawal Wormhole message requesting a certain withdrawal amount - * @param _recipient Address to receive withdrawn asset - * @param _asset Address of asset to withdraw - * @param _amount Amount of asset to withdraw - */ + /// @inheritdoc Generalized4626Strategy function withdraw( address _recipient, address _asset, @@ -114,19 +102,13 @@ contract CrossChainMasterStrategy is _withdraw(_asset, _recipient, _amount); } - /** - * @dev Remove all assets from platform and send them to Vault contract. - */ + /// @inheritdoc Generalized4626Strategy function withdrawAll() external override onlyVaultOrGovernor nonReentrant { // Withdraw everything in Remote strategy _withdraw(baseToken, vaultAddress, remoteStrategyBalance); } - /** - * @dev Get the total asset value held in the platform - * @param _asset Address of the asset - * @return balance Total value of the asset in the platform - */ + /// @inheritdoc Generalized4626Strategy function checkBalance(address _asset) public view @@ -142,17 +124,12 @@ contract CrossChainMasterStrategy is return undepositedUSDC + pendingAmount + remoteStrategyBalance; } - /** - * @dev Returns bool indicating whether asset is supported by strategy - * @param _asset Address of the asset - */ + /// @inheritdoc Generalized4626Strategy function supportsAsset(address _asset) public view override returns (bool) { return _asset == baseToken; } - /** - * @dev Approve the spending of all assets - */ + /// @inheritdoc Generalized4626Strategy function safeApproveAllTokens() external override @@ -160,20 +137,10 @@ contract CrossChainMasterStrategy is nonReentrant {} - /** - * @dev - * @param _asset Address of the asset to approve - * @param _aToken Address of the aToken - */ - // solhint-disable-next-line no-unused-vars - function _abstractSetPToken(address _asset, address _aToken) - internal - override - {} + /// @inheritdoc Generalized4626Strategy + function _abstractSetPToken(address, address) internal override {} - /** - * @dev - */ + /// @inheritdoc Generalized4626Strategy function collectRewardTokens() external override @@ -181,6 +148,7 @@ contract CrossChainMasterStrategy is nonReentrant {} + /// @inheritdoc AbstractCCTPIntegrator function _onMessageReceived(bytes memory payload) internal override { uint32 messageType = payload.getMessageType(); if (messageType == CrossChainStrategyHelper.BALANCE_CHECK_MESSAGE) { @@ -191,8 +159,8 @@ contract CrossChainMasterStrategy is } } + /// @inheritdoc AbstractCCTPIntegrator function _onTokenReceived( - // solhint-disable-next-line no-unused-vars uint256 tokenAmount, // solhint-disable-next-line no-unused-vars uint256 feeExecuted, @@ -233,6 +201,11 @@ contract CrossChainMasterStrategy is emit Withdrawal(baseToken, baseToken, usdcBalance); } + /** + * @dev Bridge and deposit asset into the remote strategy + * @param _asset Address of the asset to deposit + * @param depositAmount Amount of the asset to deposit + */ function _deposit(address _asset, uint256 depositAmount) internal virtual { require(_asset == baseToken, "Unsupported asset"); require(!isTransferPending(), "Transfer already pending"); @@ -243,22 +216,32 @@ contract CrossChainMasterStrategy is "Deposit amount exceeds max transfer amount" ); + // Get the next nonce uint64 nonce = _getNextNonce(); transferTypeByNonce[nonce] = TransferType.Deposit; // Set pending amount pendingAmount = depositAmount; - // Send deposit message with payload + // Build deposit message payload bytes memory message = CrossChainStrategyHelper.encodeDepositMessage( nonce, depositAmount ); + // Send deposit message to the remote strategy _sendTokens(depositAmount, message); + + // Emit deposit event emit Deposit(_asset, _asset, depositAmount); } + /** + * @dev Send a withdraw request to the remote strategy + * @param _asset Address of the asset to withdraw + * @param _recipient Address to receive the withdrawn asset + * @param _amount Amount of the asset to withdraw + */ function _withdraw( address _asset, address _recipient, @@ -277,19 +260,20 @@ contract CrossChainMasterStrategy is "Withdraw amount exceeds max transfer amount" ); + // Get the next nonce uint64 nonce = _getNextNonce(); transferTypeByNonce[nonce] = TransferType.Withdrawal; - // Emit Withdrawequested event here, - // Withdraw will emitted in _onTokenReceived - emit WithdrawRequested(baseToken, _amount); - - // Send withdrawal message with payload + // Build and send withdrawal message with payload bytes memory message = CrossChainStrategyHelper.encodeWithdrawMessage( nonce, _amount ); _sendMessage(message); + + // Emit WithdrawRequested event here, + // Withdraw will be emitted in _onTokenReceived + emit WithdrawRequested(baseToken, _amount); } /** @@ -303,8 +287,10 @@ contract CrossChainMasterStrategy is internal virtual { + // Decode the message (uint64 nonce, uint256 balance) = message.decodeBalanceCheckMessage(); + // Get the last cached nonce uint64 _lastCachedNonce = lastTransferNonce; if (nonce != _lastCachedNonce) { @@ -313,6 +299,7 @@ contract CrossChainMasterStrategy is return; } + // Check if the nonce has been processed bool processedTransfer = isNonceProcessed(nonce); if ( !processedTransfer && @@ -323,7 +310,7 @@ contract CrossChainMasterStrategy is return; } - // Update the balance always + // Update the remote strategy balance always remoteStrategyBalance = balance; emit RemoteStrategyBalanceUpdated(balance); diff --git a/contracts/contracts/strategies/crosschain/CrossChainRemoteStrategy.sol b/contracts/contracts/strategies/crosschain/CrossChainRemoteStrategy.sol index cfff700c1b..307e3e7374 100644 --- a/contracts/contracts/strategies/crosschain/CrossChainRemoteStrategy.sol +++ b/contracts/contracts/strategies/crosschain/CrossChainRemoteStrategy.sol @@ -2,11 +2,12 @@ pragma solidity ^0.8.0; /** - * @title OUSD Yearn V3 Remote Strategy - the L2 chain part + * @title CrossChainRemoteStrategy * @author Origin Protocol Inc * - * @dev This strategy can only perform 1 deposit or withdrawal at a time. For that - * reason it shouldn't be configured as an asset default strategy. + * @dev Part of the cross-chain strategy that lives on the remote chain. + * Handles deposits and withdrawals from the master strategy on peer chain + * and locally deposits the funds to a 4626 compatible vault. */ import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; @@ -28,6 +29,12 @@ contract CrossChainRemoteStrategy is event WithdrawFailed(string reason); event StrategistUpdated(address _address); + /** + * @notice Address of the strategist. + * This is important to have the variable name same as in IVault. + * Because the parent contract (Generalized4626Strategy) uses this + * function to get the strategist address. + */ address public strategistAddr; modifier onlyOperatorOrStrategistOrGovernor() { @@ -53,6 +60,13 @@ contract CrossChainRemoteStrategy is // so that IVault(vaultAddress).strategistAddr() works } + /** + * @dev Initialize the strategy implementation + * @param _strategist Address of the strategist + * @param _operator Address of the operator + * @param _minFinalityThreshold Minimum finality threshold + * @param _feePremiumBps Fee premium in basis points + */ function initialize( address _strategist, address _operator, @@ -77,19 +91,26 @@ contract CrossChainRemoteStrategy is } /** - * @notice Set address of Strategist + * @notice Set address of Strategist. + * This is important to have the function name same as IVault. + * Because the parent contract (Generalized4626Strategy) uses this + * function to get/set the strategist address. * @param _address Address of Strategist */ function setStrategistAddr(address _address) external onlyGovernor { _setStrategistAddr(_address); } + /** + * @dev Set the strategist address + * @param _address Address of the strategist + */ function _setStrategistAddr(address _address) internal { strategistAddr = _address; emit StrategistUpdated(_address); } - // solhint-disable-next-line no-unused-vars + /// @inheritdoc Generalized4626Strategy function deposit(address _asset, uint256 _amount) external virtual @@ -99,10 +120,12 @@ contract CrossChainRemoteStrategy is _deposit(_asset, _amount); } + /// @inheritdoc Generalized4626Strategy function depositAll() external virtual override onlyGovernorOrStrategist { _deposit(baseToken, IERC20(baseToken).balanceOf(address(this))); } + /// @inheritdoc Generalized4626Strategy function withdraw( address _recipient, address _asset, @@ -111,17 +134,20 @@ contract CrossChainRemoteStrategy is _withdraw(_recipient, _asset, _amount); } + /// @inheritdoc Generalized4626Strategy function withdrawAll() external virtual override onlyGovernorOrStrategist { uint256 contractBalance = IERC20(baseToken).balanceOf(address(this)); uint256 balance = checkBalance(baseToken) - contractBalance; _withdraw(address(this), baseToken, balance); } + /// @inheritdoc AbstractCCTPIntegrator function _onMessageReceived(bytes memory payload) internal override { uint32 messageType = payload.getMessageType(); if (messageType == CrossChainStrategyHelper.DEPOSIT_MESSAGE) { // Received when Master strategy sends tokens to the remote strategy - // Do nothing because we receive acknowledgement with token transfer, so _onTokenReceived will handle it + // Do nothing because we receive acknowledgement with token transfer, + // so _onTokenReceived will handle it } else if (messageType == CrossChainStrategyHelper.WITHDRAW_MESSAGE) { // Received when Master strategy requests a withdrawal _processWithdrawMessage(payload); @@ -130,6 +156,12 @@ contract CrossChainRemoteStrategy is } } + /** + * @dev Process deposit message from peer strategy + * @param tokenAmount Amount of tokens received + * @param feeExecuted Fee executed + * @param payload Payload of the message + */ function _processDepositMessage( // solhint-disable-next-line no-unused-vars uint256 tokenAmount, @@ -143,13 +175,14 @@ contract CrossChainRemoteStrategy is require(!isNonceProcessed(nonce), "Nonce already processed"); _markNonceAsProcessed(nonce); - // Deposit everything we got + // Deposit everything we got, not just what was bridged 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); + // Send balance check message to the peer strategy uint256 balanceAfter = checkBalance(baseToken); bytes memory message = CrossChainStrategyHelper .encodeBalanceCheckMessage(lastTransferNonce, balanceAfter); @@ -186,6 +219,10 @@ contract CrossChainRemoteStrategy is } } + /** + * @dev Process withdrawal message from peer strategy + * @param payload Payload of the message + */ function _processWithdrawMessage(bytes memory payload) internal virtual { (uint64 nonce, uint256 withdrawAmount) = payload .decodeWithdrawMessage(); @@ -200,9 +237,6 @@ contract CrossChainRemoteStrategy is // Check balance after withdrawal uint256 balanceAfter = checkBalance(baseToken); - bytes memory message = CrossChainStrategyHelper - .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. @@ -210,12 +244,16 @@ contract CrossChainRemoteStrategy is if (usdcBalance > 1e6) { // The new balance on the contract needs to have USDC subtracted from it as // that will be withdrawn in the next steps - message = CrossChainStrategyHelper.encodeBalanceCheckMessage( - lastTransferNonce, - balanceAfter - usdcBalance - ); + bytes memory message = CrossChainStrategyHelper + .encodeBalanceCheckMessage( + lastTransferNonce, + balanceAfter - usdcBalance + ); _sendTokens(usdcBalance, message); } else { + // Contract only has a small dust, so only send the balance update message + bytes memory message = CrossChainStrategyHelper + .encodeBalanceCheckMessage(lastTransferNonce, balanceAfter); _sendMessage(message); } } @@ -227,7 +265,6 @@ contract CrossChainRemoteStrategy is * @param _amount Amount of asset to withdraw */ function _withdraw( - // solhint-disable-next-line no-unused-vars address _recipient, address _asset, uint256 _amount @@ -236,11 +273,10 @@ contract CrossChainRemoteStrategy is require(_recipient == address(this), "Invalid recipient"); require(_asset == address(baseToken), "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 + // slither-disable-next-line unused-return IERC4626(platformAddress).withdraw( _amount, address(this), @@ -264,6 +300,12 @@ contract CrossChainRemoteStrategy is } } + /** + * @dev Process token received message from peer strategy + * @param tokenAmount Amount of tokens received + * @param feeExecuted Fee executed + * @param payload Payload of the message + */ function _onTokenReceived( uint256 tokenAmount, uint256 feeExecuted, @@ -279,6 +321,9 @@ contract CrossChainRemoteStrategy is _processDepositMessage(tokenAmount, feeExecuted, payload); } + /** + * @dev Send balance update message to the peer strategy + */ function sendBalanceUpdate() external virtual From 5ac664c831807d652b0ccff55398e7d695d732c4 Mon Sep 17 00:00:00 2001 From: Shahul Hameed <10547529+shahthepro@users.noreply.github.com> Date: Fri, 26 Dec 2025 12:29:01 +0400 Subject: [PATCH 040/101] Fix comment --- .../crosschain/CrossChainMasterStrategy.sol | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/contracts/contracts/strategies/crosschain/CrossChainMasterStrategy.sol b/contracts/contracts/strategies/crosschain/CrossChainMasterStrategy.sol index 81705b5179..4fbca98498 100644 --- a/contracts/contracts/strategies/crosschain/CrossChainMasterStrategy.sol +++ b/contracts/contracts/strategies/crosschain/CrossChainMasterStrategy.sol @@ -73,7 +73,7 @@ contract CrossChainMasterStrategy is ); } - /// @inheritdoc Generalized4626Strategy + /// @inheritdoc InitializableAbstractStrategy function deposit(address _asset, uint256 _amount) external override @@ -83,7 +83,7 @@ contract CrossChainMasterStrategy is _deposit(_asset, _amount); } - /// @inheritdoc Generalized4626Strategy + /// @inheritdoc InitializableAbstractStrategy function depositAll() external override onlyVault nonReentrant { uint256 balance = IERC20(baseToken).balanceOf(address(this)); if (balance > 0) { @@ -91,7 +91,7 @@ contract CrossChainMasterStrategy is } } - /// @inheritdoc Generalized4626Strategy + /// @inheritdoc InitializableAbstractStrategy function withdraw( address _recipient, address _asset, @@ -102,13 +102,13 @@ contract CrossChainMasterStrategy is _withdraw(_asset, _recipient, _amount); } - /// @inheritdoc Generalized4626Strategy + /// @inheritdoc InitializableAbstractStrategy function withdrawAll() external override onlyVaultOrGovernor nonReentrant { // Withdraw everything in Remote strategy _withdraw(baseToken, vaultAddress, remoteStrategyBalance); } - /// @inheritdoc Generalized4626Strategy + /// @inheritdoc InitializableAbstractStrategy function checkBalance(address _asset) public view @@ -124,12 +124,12 @@ contract CrossChainMasterStrategy is return undepositedUSDC + pendingAmount + remoteStrategyBalance; } - /// @inheritdoc Generalized4626Strategy + /// @inheritdoc InitializableAbstractStrategy function supportsAsset(address _asset) public view override returns (bool) { return _asset == baseToken; } - /// @inheritdoc Generalized4626Strategy + /// @inheritdoc InitializableAbstractStrategy function safeApproveAllTokens() external override @@ -137,10 +137,10 @@ contract CrossChainMasterStrategy is nonReentrant {} - /// @inheritdoc Generalized4626Strategy + /// @inheritdoc InitializableAbstractStrategy function _abstractSetPToken(address, address) internal override {} - /// @inheritdoc Generalized4626Strategy + /// @inheritdoc InitializableAbstractStrategy function collectRewardTokens() external override From b16c7fd60455f72f92ebe75bbb7ed473166e17b5 Mon Sep 17 00:00:00 2001 From: Shahul Hameed <10547529+shahthepro@users.noreply.github.com> Date: Fri, 26 Dec 2025 12:35:14 +0400 Subject: [PATCH 041/101] Fix failing unit test --- .../strategies/crosschain/cross-chain-strategy.js | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/contracts/test/strategies/crosschain/cross-chain-strategy.js b/contracts/test/strategies/crosschain/cross-chain-strategy.js index 88770eafc9..1d1f9838ff 100644 --- a/contracts/test/strategies/crosschain/cross-chain-strategy.js +++ b/contracts/test/strategies/crosschain/cross-chain-strategy.js @@ -5,7 +5,6 @@ const { crossChainFixtureUnit, } = require("../../_fixture"); const { units } = require("../../helpers"); -const addresses = require("../../../utils/addresses"); const loadFixture = createFixtureLoader(crossChainFixtureUnit); @@ -62,11 +61,13 @@ describe("ForkTest: CrossChainRemoteStrategy", function () { // Withdraws from the remote strategy directly, without going through the master strategy const directWithdrawFromRemoteStrategy = async (amount) => { - await crossChainRemoteStrategy.connect(governor).withdraw( - addresses.zeroAddress, // this gets ignored anyway - usdc.address, - await units(amount, usdc) - ); + await crossChainRemoteStrategy + .connect(governor) + .withdraw( + crossChainRemoteStrategy.address, + usdc.address, + await units(amount, usdc) + ); }; // Withdraws all the remote strategy directly, without going through the master strategy From a4c2968709cff33ab0a1567b387ae627fe1e552a Mon Sep 17 00:00:00 2001 From: Shahul Hameed <10547529+shahthepro@users.noreply.github.com> Date: Tue, 30 Dec 2025 07:47:33 +0400 Subject: [PATCH 042/101] fix: withdraw only if balance is lower than requested amount --- .../crosschain/CrossChainRemoteStrategy.sol | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/contracts/contracts/strategies/crosschain/CrossChainRemoteStrategy.sol b/contracts/contracts/strategies/crosschain/CrossChainRemoteStrategy.sol index 307e3e7374..593e145363 100644 --- a/contracts/contracts/strategies/crosschain/CrossChainRemoteStrategy.sol +++ b/contracts/contracts/strategies/crosschain/CrossChainRemoteStrategy.sol @@ -231,16 +231,21 @@ contract CrossChainRemoteStrategy is require(!isNonceProcessed(nonce), "Nonce already processed"); _markNonceAsProcessed(nonce); - // Withdraw funds from the remote strategy - _withdraw(address(this), baseToken, withdrawAmount); + uint256 usdcBalance = IERC20(baseToken).balanceOf(address(this)); + + if (usdcBalance < withdrawAmount) { + // Withdraw funds from the remote strategy + _withdraw(address(this), baseToken, withdrawAmount); + + // 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. + usdcBalance = IERC20(baseToken).balanceOf(address(this)); + } // Check balance after withdrawal uint256 balanceAfter = checkBalance(baseToken); - // 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) { // The new balance on the contract needs to have USDC subtracted from it as // that will be withdrawn in the next steps From fca3df2893d37084684a324b288743c70a613940 Mon Sep 17 00:00:00 2001 From: Shahul Hameed <10547529+shahthepro@users.noreply.github.com> Date: Tue, 30 Dec 2025 10:50:20 +0400 Subject: [PATCH 043/101] Document crosschain strategy --- .../crosschain/crosschain-strategy.md | 698 ++++++++++++++++++ 1 file changed, 698 insertions(+) create mode 100644 contracts/contracts/strategies/crosschain/crosschain-strategy.md diff --git a/contracts/contracts/strategies/crosschain/crosschain-strategy.md b/contracts/contracts/strategies/crosschain/crosschain-strategy.md new file mode 100644 index 0000000000..36c1cbc489 --- /dev/null +++ b/contracts/contracts/strategies/crosschain/crosschain-strategy.md @@ -0,0 +1,698 @@ +# Cross-Chain Strategy Documentation + +## Overview + +The Cross-Chain Strategy enables OUSD Vault to deploy funds across multiple EVM chains using Circle's Cross-Chain Transfer Protocol (CCTP). The strategy consists of two main contracts: + +- **CrossChainMasterStrategy**: Deployed on Ethereum (same chain as OUSD Vault), acts as the primary strategy interface +- **CrossChainRemoteStrategy**: Deployed on a remote EVM chain (e.g., Base), manages funds in a 4626-compatible vault + +### Key Design Decisions + +- **Single Pending Transfer**: Only one deposit or withdrawal can be in-flight at a time to simplify state management and prevent race conditions +- **Nonce-Based Ordering**: All transfers use incrementing nonces to ensure proper sequencing and prevent replay attacks +- **CCTP Integration**: Uses Circle's CCTP for secure cross-chain token transfers and message passing + +--- + +## Architecture + +### High-Level Flow + +```mermaid +graph TB + subgraph Ethereum["Ethereum Chain"] + Vault[OUSD Vault] + Master[CrossChainMasterStrategy] + CCTPTokenMessenger1[CCTP Token Messenger] + CCTPMessageTransmitter1[CCTP Message Transmitter] + end + + subgraph Remote["Remote Chain (Base)"] + RemoteStrategy[CrossChainRemoteStrategy] + Vault4626[4626 Vault] + CCTPTokenMessenger2[CCTP Token Messenger] + CCTPMessageTransmitter2[CCTP Message Transmitter] + end + + Vault -->|deposit/withdraw| Master + Master -->|bridge USDC + messages| CCTPTokenMessenger1 + Master -->|send messages| CCTPMessageTransmitter1 + + CCTPTokenMessenger1 -.->|CCTP Bridge| CCTPTokenMessenger2 + CCTPMessageTransmitter1 -.->|CCTP Bridge| CCTPMessageTransmitter2 + + CCTPTokenMessenger2 -->|mint USDC| RemoteStrategy + CCTPMessageTransmitter2 -->|deliver messages| RemoteStrategy + + RemoteStrategy -->|deposit/withdraw| Vault4626 + RemoteStrategy -->|send balance updates| CCTPMessageTransmitter2 + + style Ethereum fill:#e1f5ff + style Remote fill:#fff4e1 + style Master fill:#c8e6c9 + style RemoteStrategy fill:#c8e6c9 + style Vault fill:#ffccbc + style Vault4626 fill:#ffccbc +``` + +### Contract Inheritance + +```mermaid +classDiagram + class Governable { + <> + +governor: address + +onlyGovernor() + } + + class AbstractCCTPIntegrator { + <> + +cctpMessageTransmitter: ICCTPMessageTransmitter + +cctpTokenMessenger: ICCTPTokenMessenger + +baseToken: address + +peerDomainID: uint32 + +peerStrategy: address + +lastTransferNonce: uint64 + +operator: address + +_sendTokens(amount, hookData) + +_sendMessage(message) + +relay(message, attestation) + +_getNextNonce() uint64 + +_markNonceAsProcessed(nonce) + +_onTokenReceived()* void + +_onMessageReceived()* void + } + + class InitializableAbstractStrategy { + <> + +vaultAddress: address + +deposit(asset, amount) + +withdraw(recipient, asset, amount) + +checkBalance(asset) uint256 + } + + class Generalized4626Strategy { + <> + +platformAddress: address + +assetToken: address + +shareToken: address + +_deposit(asset, amount) + +_withdraw(recipient, asset, amount) + } + + class CrossChainMasterStrategy { + +remoteStrategyBalance: uint256 + +pendingAmount: uint256 + +transferTypeByNonce: mapping + +deposit(asset, amount) + +withdraw(recipient, asset, amount) + +checkBalance(asset) uint256 + +_processBalanceCheckMessage(message) + } + + class CrossChainRemoteStrategy { + +strategistAddr: address + +deposit(asset, amount) + +withdraw(recipient, asset, amount) + +checkBalance(asset) uint256 + +sendBalanceUpdate() + +_processDepositMessage(tokenAmount, fee, payload) + +_processWithdrawMessage(payload) + } + + Governable <|-- AbstractCCTPIntegrator + AbstractCCTPIntegrator <|-- CrossChainMasterStrategy + AbstractCCTPIntegrator <|-- CrossChainRemoteStrategy + InitializableAbstractStrategy <|-- CrossChainMasterStrategy + Generalized4626Strategy <|-- CrossChainRemoteStrategy + + note for AbstractCCTPIntegrator "_onTokenReceived() and\n_onMessageReceived() are\nabstract functions" + note for CrossChainMasterStrategy "Deployed on Ethereum\nInterfaces with OUSD Vault" + note for CrossChainRemoteStrategy "Deployed on Remote Chain\nManages 4626 Vault" +``` + +--- + +## Contracts and Libraries + +### AbstractCCTPIntegrator + +**Purpose**: Base contract providing CCTP integration functionality shared by both Master and Remote strategies. + +**Key Responsibilities**: +- CCTP message handling (`handleReceiveFinalizedMessage`, `handleReceiveUnfinalizedMessage`) +- Token bridging via CCTP Token Messenger (`_sendTokens`) +- Message sending via CCTP Message Transmitter (`_sendMessage`) +- Message relaying by operators (`relay`) +- Nonce management for transfer ordering +- Security checks (domain validation, sender validation) + +**Key State Variables**: +- `cctpMessageTransmitter`: CCTP Message Transmitter contract +- `cctpTokenMessenger`: CCTP Token Messenger contract +- `baseToken`: USDC address on local chain +- `peerDomainID`: Domain ID of the peer chain +- `peerStrategy`: Address of the strategy on peer chain +- `minFinalityThreshold`: Minimum finality threshold (1000 or 2000) +- `feePremiumBps`: Fee premium in basis points (max 3000) +- `lastTransferNonce`: Last known transfer nonce +- `nonceProcessed`: Mapping of processed nonces +- `operator`: Address authorized to relay messages + +**Key Functions**: +- `_sendTokens(uint256 tokenAmount, bytes memory hookData)`: Bridges USDC via CCTP with hook data +- `_sendMessage(bytes memory message)`: Sends a message via CCTP +- `relay(bytes memory message, bytes memory attestation)`: Relays a finalized CCTP message (operator-only) +- `_getNextNonce()`: Gets and increments the next nonce +- `_markNonceAsProcessed(uint64 nonce)`: Marks a nonce as processed +- `isTransferPending()`: Checks if there's a pending transfer +- `isNonceProcessed(uint64 nonce)`: Checks if a nonce has been processed + +**Abstract Functions** (implemented by child contracts): +- `_onTokenReceived(uint256 tokenAmount, uint256 feeExecuted, bytes memory payload)`: Called when USDC is received via CCTP +- `_onMessageReceived(bytes memory payload)`: Called when a message is received + +### CrossChainMasterStrategy + +**Purpose**: Strategy deployed on Ethereum that interfaces with OUSD Vault and coordinates with Remote strategy. + +**Key Responsibilities**: +- Receiving deposits from OUSD Vault +- Initiating withdrawals requested by OUSD Vault +- Tracking remote strategy balance +- Managing pending transfer state +- Processing balance check messages from Remote strategy + +**Key State Variables**: +- `remoteStrategyBalance`: Cached balance of funds in Remote strategy +- `pendingAmount`: Amount bridged but not yet confirmed received +- `transferTypeByNonce`: Mapping of nonce to transfer type (Deposit/Withdrawal) + +**Key Functions**: +- `deposit(address _asset, uint256 _amount)`: Called by Vault to deposit funds +- `withdraw(address _recipient, address _asset, uint256 _amount)`: Called by Vault to withdraw funds +- `checkBalance(address _asset)`: Returns total balance (local + pending + remote) +- `_deposit(address _asset, uint256 depositAmount)`: Internal deposit handler +- `_withdraw(address _asset, address _recipient, uint256 _amount)`: Internal withdrawal handler +- `_processBalanceCheckMessage(bytes memory message)`: Processes balance check from Remote + +**Deposit Flow**: +1. Validate no pending transfer exists +2. Get next nonce and mark as Deposit type +3. Set `pendingAmount` +4. Bridge USDC via CCTP with deposit message in hook data +5. Wait for balance check message to confirm + +**Withdrawal Flow**: +1. Validate no pending transfer exists +2. Validate sufficient remote balance +3. Get next nonce and mark as Withdrawal type +4. Send withdrawal message via CCTP +5. Wait for tokens to be bridged back with balance check in hook data + +**Balance Check Processing**: +- Validates nonce matches `lastTransferNonce` +- Updates `remoteStrategyBalance` +- If pending deposit: marks nonce as processed, resets `pendingAmount` +- If pending withdrawal: skips balance update (handled in `_onTokenReceived`) + +### CrossChainRemoteStrategy + +**Purpose**: Strategy deployed on remote chain that manages funds in a 4626 vault and responds to Master strategy commands. + +**Key Responsibilities**: +- Receiving bridged USDC from Master strategy +- Depositing to 4626 vault +- Withdrawing from 4626 vault +- Sending balance check messages to Master strategy +- Managing strategist permissions + +**Key State Variables**: +- `strategistAddr`: Address of strategist (for compatibility with Generalized4626Strategy) + +**Key Functions**: +- `deposit(address _asset, uint256 _amount)`: Deposits to 4626 vault (governor/strategist only) +- `withdraw(address _recipient, address _asset, uint256 _amount)`: Withdraws from 4626 vault (governor/strategist only) +- `checkBalance(address _asset)`: Returns total balance (4626 vault + contract balance) +- `sendBalanceUpdate()`: Manually sends balance check message (operator/strategist/governor) +- `_processDepositMessage(uint256 tokenAmount, uint256 feeExecuted, bytes memory payload)`: Handles deposit message +- `_processWithdrawMessage(bytes memory payload)`: Handles withdrawal message +- `_deposit(address _asset, uint256 _amount)`: Internal deposit to 4626 vault (with error handling) +- `_withdraw(address _recipient, address _asset, uint256 _amount)`: Internal withdrawal from 4626 vault (with error handling) + +**Deposit Message Handling**: +1. Decode nonce and amount from payload +2. Verify nonce not already processed +3. Mark nonce as processed +4. Deposit all USDC balance to 4626 vault (may fail silently) +5. Send balance check message with updated balance + +**Withdrawal Message Handling**: +1. Decode nonce and amount from payload +2. Verify nonce not already processed +3. Mark nonce as processed +4. Withdraw from 4626 vault (may fail silently) +5. Bridge USDC back to Master (if balance > 1e6) with balance check in hook data +6. If balance <= 1e6, send balance check message only + +### CrossChainStrategyHelper + +**Purpose**: Library for encoding and decoding cross-chain messages. + +**Message Constants**: +- `DEPOSIT_MESSAGE = 1` +- `WITHDRAW_MESSAGE = 2` +- `BALANCE_CHECK_MESSAGE = 3` +- `CCTP_MESSAGE_VERSION = 1` +- `ORIGIN_MESSAGE_VERSION = 1010` + +**Message Format**: +``` +[0-4 bytes]: Origin Message Version (1010) +[4-8 bytes]: Message Type (1, 2, or 3) +[8+ bytes]: Message Payload (ABI-encoded) +``` + +**Key Functions**: +- `encodeDepositMessage(uint64 nonce, uint256 depositAmount)`: Encodes deposit message +- `decodeDepositMessage(bytes memory message)`: Decodes deposit message +- `encodeWithdrawMessage(uint64 nonce, uint256 withdrawAmount)`: Encodes withdrawal message +- `decodeWithdrawMessage(bytes memory message)`: Decodes withdrawal message +- `encodeBalanceCheckMessage(uint64 nonce, uint256 balance)`: Encodes balance check message +- `decodeBalanceCheckMessage(bytes memory message)`: Decodes balance check message +- `getMessageVersion(bytes memory message)`: Extracts message version +- `getMessageType(bytes memory message)`: Extracts message type +- `verifyMessageVersionAndType(bytes memory _message, uint32 _type)`: Validates message format + +**Message Payloads**: +- **Deposit**: `abi.encode(nonce, depositAmount)` +- **Withdraw**: `abi.encode(nonce, withdrawAmount)` +- **Balance Check**: `abi.encode(nonce, balance)` + +### BytesHelper + +**Purpose**: Utility library for extracting typed data from byte arrays. + +**Key Functions**: +- `extractSlice(bytes memory data, uint256 start, uint256 end)`: Extracts a byte slice +- `extractUint32(bytes memory data, uint256 start)`: Extracts uint32 at offset +- `extractUint256(bytes memory data, uint256 start)`: Extracts uint256 at offset +- `extractAddress(bytes memory data, uint256 start)`: Extracts address at offset (32-byte padded) + +**Usage**: Used by `CrossChainStrategyHelper` and `AbstractCCTPIntegrator` to parse CCTP message headers and bodies. + +--- + +## Message Protocol + +### CCTP Message Structure + +CCTP messages have a header and body: + +**Header**: +Ref: https://developers.circle.com/cctp/technical-guide#message-header + +**Message Body for Burn Messages (V2)**: +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 + +### Origin's Custom Message Body + +All Origin messages follow this format: +``` +[0-4 bytes]: ORIGIN_MESSAGE_VERSION (1010) +[4-8 bytes]: MESSAGE_TYPE (1, 2, or 3) +[8+ bytes]: Payload (ABI-encoded) +``` + +### Message Types + +#### 1. Deposit Message + +**Sent By**: Master Strategy +**Sent Via**: CCTP Token Messenger (as hook data) +**Contains**: +- Nonce (uint64) +- Deposit Amount (uint256) + +**Encoding**: `abi.encodePacked(ORIGIN_MESSAGE_VERSION, DEPOSIT_MESSAGE, abi.encode(nonce, depositAmount))` + +**Flow**: +1. Master bridges USDC with deposit message as hook data +2. Remote receives USDC and hook data via `_onTokenReceived` +3. Remote deposits to 4626 vault +4. Remote sends balance check message + +#### 2. Withdraw Message + +**Sent By**: Master Strategy +**Sent Via**: CCTP Message Transmitter +**Contains**: +- Nonce (uint64) +- Withdraw Amount (uint256) + +**Encoding**: `abi.encodePacked(ORIGIN_MESSAGE_VERSION, WITHDRAW_MESSAGE, abi.encode(nonce, withdrawAmount))` + +**Flow**: +1. Master sends withdrawal message +2. Remote receives via `_onMessageReceived` +3. Remote withdraws from 4626 vault +4. Remote bridges USDC back with balance check as hook data +5. Master receives USDC and processes balance check + +#### 3. Balance Check Message + +**Sent By**: Remote Strategy +**Sent Via**: CCTP Message Transmitter (or as hook data in burn message) +**Contains**: +- Nonce (uint64) +- Balance (uint256) + +**Encoding**: `abi.encodePacked(ORIGIN_MESSAGE_VERSION, BALANCE_CHECK_MESSAGE, abi.encode(nonce, balance))` + +**Flow**: +1. Remote sends balance check after deposit/withdrawal +2. Master receives and validates nonce +3. Master updates `remoteStrategyBalance` +4. If nonce matches pending transfer, marks as processed + +--- + +## Communication Flows + +### Deposit Flow + +```mermaid +sequenceDiagram + participant Vault as OUSD Vault + participant Master as CrossChainMasterStrategy
(Ethereum) + participant CCTP1 as CCTP Token Messenger
(Ethereum) + participant Bridge as CCTP Bridge + participant CCTP2 as CCTP Token Messenger
(Base) + participant Operator as Operator + participant Remote as CrossChainRemoteStrategy
(Base) + participant Vault4626 as 4626 Vault + participant CCTPMsg1 as CCTP Message Transmitter
(Base) + participant CCTPMsg2 as CCTP Message Transmitter
(Ethereum) + + Vault->>Master: deposit(USDC, amount) + Note over Master: Validates: no pending transfer,
amount > 0, amount <= MAX + Note over Master: Increments nonce, sets pendingAmount,
marks as Deposit type + Master->>CCTP1: depositForBurnWithHook(amount, hookData) + Note over Master: hookData = DepositMessage(nonce, amount) + Master-->>Vault: Deposit event emitted + + CCTP1->>Bridge: Burn USDC + Send message + Bridge->>CCTP2: Message with attestation + + Operator->>Remote: relay(message, attestation) + Note over Remote: Validates message:
domain, sender, version + Remote->>CCTPMsg1: receiveMessage(message, attestation) + Note over CCTPMsg1: Detects burn message,
forwards to TokenMessenger + CCTPMsg1->>CCTP2: Process burn message + CCTP2->>Remote: Mint USDC (amount - fee) + CCTPMsg1->>Remote: _onTokenReceived(tokenAmount, fee, hookData) + Note over Remote: Decodes DepositMessage(nonce, amount) + Note over Remote: Marks nonce as processed + Remote->>Vault4626: deposit(USDC balance) + Note over Remote: May fail silently, emits DepositFailed if fails + Remote->>Remote: Calculate new balance + Remote->>CCTPMsg1: sendMessage(BalanceCheckMessage) + Note over Remote: BalanceCheckMessage(nonce, balance) + + CCTPMsg1->>Bridge: Message with attestation + Bridge->>CCTPMsg2: Message with attestation + + Operator->>Master: relay(message, attestation) + Note over Master: Validates message + Master->>CCTPMsg2: receiveMessage(message, attestation) + Note over CCTPMsg2: Not a burn message,
forwards to handleReceiveFinalizedMessage + CCTPMsg2->>Master: handleReceiveFinalizedMessage(...) + Master->>Master: _onMessageReceived(payload) + Note over Master: Processes BalanceCheckMessage + Note over Master: Validates nonce matches lastTransferNonce + Note over Master: Marks nonce as processed + Note over Master: Resets pendingAmount = 0 + Note over Master: Updates remoteStrategyBalance +``` + +### Withdrawal Flow + +```mermaid +sequenceDiagram + participant Vault as OUSD Vault + participant Master as CrossChainMasterStrategy
(Ethereum) + participant CCTPMsg1 as CCTP Message Transmitter
(Ethereum) + participant Bridge as CCTP Bridge + participant CCTPMsg2 as CCTP Message Transmitter
(Base) + participant Operator as Operator + participant Remote as CrossChainRemoteStrategy
(Base) + participant Vault4626 as 4626 Vault + participant CCTP1 as CCTP Token Messenger
(Base) + participant CCTP2 as CCTP Token Messenger
(Ethereum) + + Vault->>Master: withdraw(vault, USDC, amount) + Note over Master: Validates: no pending transfer,
amount > 0, amount <= remoteBalance,
amount <= MAX_TRANSFER_AMOUNT + Note over Master: Increments nonce,
marks as Withdrawal type + Master->>CCTPMsg1: sendMessage(WithdrawMessage) + Note over Master: WithdrawMessage(nonce, amount) + Master-->>Vault: WithdrawRequested event emitted + + CCTPMsg1->>Bridge: Message with attestation + Bridge->>CCTPMsg2: Message with attestation + + Operator->>Remote: relay(message, attestation) + Note over Remote: Validates message + Remote->>CCTPMsg2: receiveMessage(message, attestation) + Note over CCTPMsg2: Not a burn message,
forwards to handleReceiveFinalizedMessage + CCTPMsg2->>Remote: handleReceiveFinalizedMessage(...) + Remote->>Remote: _onMessageReceived(payload) + Note over Remote: Decodes WithdrawMessage(nonce, amount) + Note over Remote: Marks nonce as processed + Remote->>Vault4626: withdraw(amount) + Note over Remote: May fail silently, emits WithdrawFailed if fails + Remote->>Remote: Calculate new balance + + alt USDC balance > 1e6 + Remote->>CCTP1: depositForBurnWithHook(usdcBalance, hookData) + Note over Remote: hookData = BalanceCheckMessage(nonce, balance) + else USDC balance <= 1e6 + Remote->>CCTPMsg2: sendMessage(BalanceCheckMessage) + Note over Remote: BalanceCheckMessage(nonce, balance) + end + + CCTP1->>Bridge: Burn USDC + Send message + Bridge->>CCTP2: Message with attestation + + Operator->>Master: relay(message, attestation) + Note over Master: Validates message + Master->>CCTPMsg2: receiveMessage(message, attestation) + Note over CCTPMsg2: Detects burn message,
forwards to TokenMessenger + CCTPMsg2->>CCTP2: Process burn message + CCTP2->>Master: Mint USDC (amount - fee) + CCTPMsg2->>Master: _onTokenReceived(tokenAmount, fee, hookData) + Note over Master: Validates nonce matches lastTransferNonce + Note over Master: Validates transfer type is Withdrawal + Note over Master: Marks nonce as processed + Master->>Master: _onMessageReceived(payload) + Note over Master: Processes BalanceCheckMessage + Note over Master: Updates remoteStrategyBalance + Master->>Vault: Transfer all USDC + Master-->>Vault: Withdrawal event emitted +``` + +### Balance Update Flow (Manual) + +```mermaid +sequenceDiagram + participant Caller as Operator/Strategist/
Governor + participant Remote as CrossChainRemoteStrategy
(Base) + participant Vault4626 as 4626 Vault + participant CCTPMsg1 as CCTP Message Transmitter
(Base) + participant Bridge as CCTP Bridge + participant CCTPMsg2 as CCTP Message Transmitter
(Ethereum) + participant Operator as Operator + participant Master as CrossChainMasterStrategy
(Ethereum) + + Caller->>Remote: sendBalanceUpdate() + Note over Remote: Calculates current balance:
4626 vault + contract balance + Remote->>Vault4626: previewRedeem(shares) + Vault4626-->>Remote: Asset value + Remote->>Remote: balance = vaultValue + contractBalance + Remote->>CCTPMsg1: sendMessage(BalanceCheckMessage) + Note over Remote: BalanceCheckMessage(lastTransferNonce, balance) + + CCTPMsg1->>Bridge: Message with attestation + Bridge->>CCTPMsg2: Message with attestation + + Operator->>Master: relay(message, attestation) + Note over Master: Validates message + Master->>CCTPMsg2: receiveMessage(message, attestation) + Note over CCTPMsg2: Not a burn message,
forwards to handleReceiveFinalizedMessage + CCTPMsg2->>Master: handleReceiveFinalizedMessage(...) + Master->>Master: _onMessageReceived(payload) + Note over Master: Processes BalanceCheckMessage + + alt nonce matches lastTransferNonce + alt no pending transfer + Note over Master: Updates remoteStrategyBalance + else pending deposit + Note over Master: Marks nonce as processed + Note over Master: Resets pendingAmount = 0 + Note over Master: Updates remoteStrategyBalance + else pending withdrawal + Note over Master: Ignores (handled by _onTokenReceived) + end + else nonce doesn't match + Note over Master: Ignores message (out of order) + end +``` + +--- + +## Nonce Management + +### Nonce Lifecycle + +1. **Initialization**: Nonces start at 0 (but 0 is disregarded, first nonce is 1) +2. **Increment**: `_getNextNonce()` increments `lastTransferNonce` and returns new value +3. **Processing**: `_markNonceAsProcessed(nonce)` marks nonce as processed +4. **Validation**: `isNonceProcessed(nonce)` checks if nonce has been processed + +### Nonce Rules + +- Nonces must be strictly increasing +- A nonce can only be marked as processed once +- Only the latest nonce can be marked as processed (nonce >= lastTransferNonce) +- New transfers cannot start if last nonce hasn't been processed + +### Replay Protection + +- Each message includes a nonce +- Nonces are checked before processing +- Once processed, a nonce cannot be processed again +- Out-of-order messages with non-matching nonces are ignored + +--- + +## State Management + +### Master Strategy State + +**Local State**: +- `IERC20(baseToken).balanceOf(address(this))`: USDC held locally +- `pendingAmount`: USDC bridged but not confirmed +- `remoteStrategyBalance`: Cached balance in Remote strategy + +**Total Balance**: `localBalance + pendingAmount + remoteStrategyBalance` + +**Transfer State**: +- `lastTransferNonce`: Last known nonce +- `transferTypeByNonce`: Type of each transfer (Deposit/Withdrawal) +- `nonceProcessed`: Which nonces have been processed + +### Remote Strategy State + +**Local State**: +- `IERC20(baseToken).balanceOf(address(this))`: USDC held locally +- `IERC4626(platformAddress).balanceOf(address(this))`: Shares in 4626 vault + +**Total Balance**: `contractBalance + previewRedeem(shares)` + +**Transfer State**: +- `lastTransferNonce`: Last known nonce +- `nonceProcessed`: Which nonces have been processed + +--- + +## Error Handling and Edge Cases + +### Deposit Failures + +**Remote Strategy Deposit Failure**: +- If 4626 vault deposit fails, Remote emits `DepositFailed` event +- Balance check message is still sent (includes undeposited USDC) +- Master strategy updates balance correctly +- Funds remain on Remote contract until manual deposit by the Guardian + +### Withdrawal Failures + +**Remote Strategy Withdrawal Failure**: +- If 4626 vault withdrawal fails, Remote emits `WithdrawFailed` event +- Balance check message is still sent (with original balance) +- Master strategy updates balance correctly +- No tokens are bridged back (or minimal dust if balance <= 1e6) +- Guardian will have to manually call the public `withdraw` method later to process the withdrawal and then call the `relay` method the WithdrawMessage again + +### Message Ordering + +**Out-of-Order Messages**: +- Balance check messages with non-matching nonces are ignored +- Master strategy only processes balance checks for `lastTransferNonce` +- Older messages are safely discarded + +**Race Conditions**: +- Single pending transfer design prevents most race conditions +- Withdrawal balance checks are ignored if withdrawal is pending (handled by `_onTokenReceived`) + +### Nonce Edge Cases + +**Nonce Too Low**: +- `_markNonceAsProcessed` reverts if nonce < lastTransferNonce +- Prevents replay attacks with old nonces + +**Nonce Already Processed**: +- `_markNonceAsProcessed` reverts if nonce already processed +- Prevents duplicate processing + +**Pending Transfer**: +- `_getNextNonce` reverts if last nonce not processed +- Prevents starting new transfer while one is pending + +### CCTP Limitations + +**Max Transfer Amount**: +- CCTP limits transfers to 10M USDC per transaction +- Both strategies enforce `MAX_TRANSFER_AMOUNT = 10_000_000 * 10**6` + +**Finality Thresholds**: +- Supports 1000 (safe, 1 epoch) or 2000 (finalized, 2 epochs) +- Configurable via `setMinFinalityThreshold` +- Unfinalized messages only supported if threshold is 1000 + +**Fee Handling**: +- Fee premium configurable up to 30% (3000 bps) +- Fees are deducted from bridged amount +- Remote strategy receives `tokenAmount - feeExecuted` + +### Operator Requirements + +**Message Relaying**: +- Only `operator` can call `relay()` +- Operator must provide valid CCTP attestation +- Operator is responsible for monitoring and relaying finalized messages + +**Security**: +- Messages are validated for domain, sender, and recipient +- Only messages from `peerStrategy` are accepted +- Only messages to `address(this)` are processed + +--- + +## Other Notes + +### Proxies +- Both strategies use Create2 to deploy their proxy to the same address on all networks + +### Initialization + +Both strategies require initialization: +- **Master**: `initialize(operator, minFinalityThreshold, feePremiumBps)` +- **Remote**: `initialize(strategist, operator, minFinalityThreshold, feePremiumBps)` + +### Governance + +- Both strategies inherit from `Governable` +- Governor can upgrade implementation, update operator, finality threshold, fee premium +- Remote strategy governor can update strategist address From b2feed110c7554589c5de2906fe8ab7b5697c83d Mon Sep 17 00:00:00 2001 From: Shahul Hameed <10547529+shahthepro@users.noreply.github.com> Date: Fri, 2 Jan 2026 10:39:17 +0400 Subject: [PATCH 044/101] Update deployment file numbers --- ...n_strategy_proxies.js => 162_crosschain_strategy_proxies.js} | 2 +- .../{162_crosschain_strategy.js => 163_crosschain_strategy.js} | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename contracts/deploy/mainnet/{161_crosschain_strategy_proxies.js => 162_crosschain_strategy_proxies.js} (93%) rename contracts/deploy/mainnet/{162_crosschain_strategy.js => 163_crosschain_strategy.js} (97%) diff --git a/contracts/deploy/mainnet/161_crosschain_strategy_proxies.js b/contracts/deploy/mainnet/162_crosschain_strategy_proxies.js similarity index 93% rename from contracts/deploy/mainnet/161_crosschain_strategy_proxies.js rename to contracts/deploy/mainnet/162_crosschain_strategy_proxies.js index 9aee00016e..ec959f27c8 100644 --- a/contracts/deploy/mainnet/161_crosschain_strategy_proxies.js +++ b/contracts/deploy/mainnet/162_crosschain_strategy_proxies.js @@ -3,7 +3,7 @@ const { deployProxyWithCreateX } = require("../deployActions"); module.exports = deploymentWithGovernanceProposal( { - deployName: "161_crosschain_strategy_proxies", + deployName: "162_crosschain_strategy_proxies", forceDeploy: false, reduceQueueTime: true, deployerIsProposer: false, diff --git a/contracts/deploy/mainnet/162_crosschain_strategy.js b/contracts/deploy/mainnet/163_crosschain_strategy.js similarity index 97% rename from contracts/deploy/mainnet/162_crosschain_strategy.js rename to contracts/deploy/mainnet/163_crosschain_strategy.js index fdb07f3e02..77d798cbed 100644 --- a/contracts/deploy/mainnet/162_crosschain_strategy.js +++ b/contracts/deploy/mainnet/163_crosschain_strategy.js @@ -5,7 +5,7 @@ const { deployCrossChainMasterStrategyImpl } = require("../deployActions"); module.exports = deploymentWithGovernanceProposal( { - deployName: "162_crosschain_strategy", + deployName: "163_crosschain_strategy", forceDeploy: false, reduceQueueTime: true, deployerIsProposer: false, From 4150f9a9c11e4f91f3b65480d0045c3130336c55 Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Mon, 5 Jan 2026 16:52:51 +0100 Subject: [PATCH 045/101] adjust the charts --- .../strategies/crosschain/crosschain-strategy.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/contracts/contracts/strategies/crosschain/crosschain-strategy.md b/contracts/contracts/strategies/crosschain/crosschain-strategy.md index 36c1cbc489..1c38e7e6ed 100644 --- a/contracts/contracts/strategies/crosschain/crosschain-strategy.md +++ b/contracts/contracts/strategies/crosschain/crosschain-strategy.md @@ -401,7 +401,7 @@ sequenceDiagram Note over Master: Increments nonce, sets pendingAmount,
marks as Deposit type Master->>CCTP1: depositForBurnWithHook(amount, hookData) Note over Master: hookData = DepositMessage(nonce, amount) - Master-->>Vault: Deposit event emitted + Master-->>Master: Deposit event emitted CCTP1->>Bridge: Burn USDC + Send message Bridge->>CCTP2: Message with attestation @@ -412,7 +412,7 @@ sequenceDiagram Note over CCTPMsg1: Detects burn message,
forwards to TokenMessenger CCTPMsg1->>CCTP2: Process burn message CCTP2->>Remote: Mint USDC (amount - fee) - CCTPMsg1->>Remote: _onTokenReceived(tokenAmount, fee, hookData) + Remote->>Remote: _onTokenReceived(tokenAmount, fee, hookData) Note over Remote: Decodes DepositMessage(nonce, amount) Note over Remote: Marks nonce as processed Remote->>Vault4626: deposit(USDC balance) @@ -457,7 +457,7 @@ sequenceDiagram Note over Master: Increments nonce,
marks as Withdrawal type Master->>CCTPMsg1: sendMessage(WithdrawMessage) Note over Master: WithdrawMessage(nonce, amount) - Master-->>Vault: WithdrawRequested event emitted + Master-->>Master: WithdrawRequested event emitted CCTPMsg1->>Bridge: Message with attestation Bridge->>CCTPMsg2: Message with attestation @@ -499,7 +499,7 @@ sequenceDiagram Note over Master: Processes BalanceCheckMessage Note over Master: Updates remoteStrategyBalance Master->>Vault: Transfer all USDC - Master-->>Vault: Withdrawal event emitted + Master-->>Master: Withdrawal event emitted ``` ### Balance Update Flow (Manual) From 7c32c012fdfce3a90759a4258130b4e092f22d77 Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Mon, 5 Jan 2026 17:06:57 +0100 Subject: [PATCH 046/101] change the function visibility to pure --- .../crosschain/CrossChainStrategyHelper.sol | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/contracts/contracts/strategies/crosschain/CrossChainStrategyHelper.sol b/contracts/contracts/strategies/crosschain/CrossChainStrategyHelper.sol index d1025a1d7a..8aee9e8f55 100644 --- a/contracts/contracts/strategies/crosschain/CrossChainStrategyHelper.sol +++ b/contracts/contracts/strategies/crosschain/CrossChainStrategyHelper.sol @@ -29,7 +29,7 @@ library CrossChainStrategyHelper { */ function getMessageVersion(bytes memory message) internal - view + pure returns (uint32) { // uint32 bytes 0 to 4 is Origin message version @@ -46,7 +46,7 @@ library CrossChainStrategyHelper { */ function getMessageType(bytes memory message) internal - view + pure returns (uint32) { // uint32 bytes 0 to 4 is Origin message version @@ -63,6 +63,7 @@ library CrossChainStrategyHelper { */ function verifyMessageVersionAndType(bytes memory _message, uint32 _type) internal + pure { require( getMessageVersion(_message) == ORIGIN_MESSAGE_VERSION, @@ -79,7 +80,7 @@ library CrossChainStrategyHelper { */ function getMessagePayload(bytes memory message) internal - view + pure returns (bytes memory) { // uint32 bytes 0 to 4 is Origin message version @@ -97,7 +98,7 @@ library CrossChainStrategyHelper { */ function encodeDepositMessage(uint64 nonce, uint256 depositAmount) internal - view + pure returns (bytes memory) { return @@ -116,6 +117,7 @@ library CrossChainStrategyHelper { */ function decodeDepositMessage(bytes memory message) internal + pure returns (uint64, uint256) { verifyMessageVersionAndType(message, DEPOSIT_MESSAGE); @@ -136,7 +138,7 @@ library CrossChainStrategyHelper { */ function encodeWithdrawMessage(uint64 nonce, uint256 withdrawAmount) internal - view + pure returns (bytes memory) { return @@ -155,6 +157,7 @@ library CrossChainStrategyHelper { */ function decodeWithdrawMessage(bytes memory message) internal + pure returns (uint64, uint256) { verifyMessageVersionAndType(message, WITHDRAW_MESSAGE); @@ -175,7 +178,7 @@ library CrossChainStrategyHelper { */ function encodeBalanceCheckMessage(uint64 nonce, uint256 balance) internal - view + pure returns (bytes memory) { return @@ -194,6 +197,7 @@ library CrossChainStrategyHelper { */ function decodeBalanceCheckMessage(bytes memory message) internal + pure returns (uint64, uint256) { verifyMessageVersionAndType(message, BALANCE_CHECK_MESSAGE); From cbc074588e1cdfac55300e3ca2bf74d52da3c772 Mon Sep 17 00:00:00 2001 From: Shahul Hameed <10547529+shahthepro@users.noreply.github.com> Date: Mon, 5 Jan 2026 20:22:55 +0400 Subject: [PATCH 047/101] fix: create2 proxy without using deployer address --- contracts/deploy/deployActions.js | 11 +++++++++-- contracts/utils/addresses.js | 6 +++--- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/contracts/deploy/deployActions.js b/contracts/deploy/deployActions.js index 321dae4238..3bb102ebe4 100644 --- a/contracts/deploy/deployActions.js +++ b/contracts/deploy/deployActions.js @@ -1698,10 +1698,17 @@ const deployProxyWithCreateX = async ( ) => { const { deployerAddr } = await getNamedAccounts(); const sDeployer = await ethers.provider.getSigner(deployerAddr); - log(`Deploying ${proxyName} with salt: ${salt} as deployer ${deployerAddr}`); + // Basically hex of "originprotocol" padded to 20 bytes to mimic an address + const addrForSalt = "0x0000000000006f726967696e70726f746f636f6c"; + // NOTE: We always use fixed address to compute the salt for the proxy. + // It makes the address predictable, easier to verify and easier to use + // with CI and local fork testing. + log( + `Deploying ${proxyName} with salt: ${salt} and fixed address: ${addrForSalt}` + ); const cCreateX = await ethers.getContractAt(createxAbi, addresses.createX); - const factoryEncodedSalt = encodeSaltForCreateX(deployerAddr, false, salt); + const factoryEncodedSalt = encodeSaltForCreateX(addrForSalt, false, salt); const getFactoryBytecode = async () => { // No deployment needed—get factory directly from artifacts diff --git a/contracts/utils/addresses.js b/contracts/utils/addresses.js index 7c0153d74c..0ac41b9608 100644 --- a/contracts/utils/addresses.js +++ b/contracts/utils/addresses.js @@ -691,10 +691,10 @@ addresses.hoodi.mockBeaconRoots = "0xdCfcAE4A084AA843eE446f400B23aA7B6340484b"; // Crosschain Strategy addresses.CrossChainStrategyProxy = - "0x98F1bA152a6Bf039F5630CD74a8Ec2cC5EA9AEd3"; + "0xEc923B471DD0220Aa1596Ead5fbE0580E334A660"; addresses.mainnet.CrossChainStrategyProxy = - "0x98F1bA152a6Bf039F5630CD74a8Ec2cC5EA9AEd3"; + "0xEc923B471DD0220Aa1596Ead5fbE0580E334A660"; addresses.base.CrossChainStrategyProxy = - "0x98F1bA152a6Bf039F5630CD74a8Ec2cC5EA9AEd3"; + "0xEc923B471DD0220Aa1596Ead5fbE0580E334A660"; module.exports = addresses; From 8bd5edc2096bf95b5a7fdb17dc3d344132fa13ba Mon Sep 17 00:00:00 2001 From: Shahul Hameed <10547529+shahthepro@users.noreply.github.com> Date: Mon, 5 Jan 2026 20:34:21 +0400 Subject: [PATCH 048/101] fix: impersonate a single deployer on fork --- contracts/deploy/deployActions.js | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/contracts/deploy/deployActions.js b/contracts/deploy/deployActions.js index 3bb102ebe4..01825313a2 100644 --- a/contracts/deploy/deployActions.js +++ b/contracts/deploy/deployActions.js @@ -29,6 +29,7 @@ const { resolveContract } = require("../utils/resolvers"); const { impersonateAccount, getSigner } = require("../utils/signers"); const { getDefenderSigner } = require("../utils/signersNoHardhat"); const { getTxOpts } = require("../utils/tx"); +const { impersonateAndFund } = require("../utils/signers"); const createxAbi = require("../abi/createx.json"); const { @@ -1697,7 +1698,13 @@ const deployProxyWithCreateX = async ( contractPath = null ) => { const { deployerAddr } = await getNamedAccounts(); - const sDeployer = await ethers.provider.getSigner(deployerAddr); + + // Impersonate a single deployer for fork testing + const deployerToImpersonate = "0x58890A9cB27586E83Cb51d2d26bbE18a1a647245"; + await impersonateAndFund(deployerToImpersonate); + const deployerToUse = isFork ? deployerToImpersonate : deployerAddr; + const sDeployer = await ethers.provider.getSigner(deployerToUse); + // Basically hex of "originprotocol" padded to 20 bytes to mimic an address const addrForSalt = "0x0000000000006f726967696e70726f746f636f6c"; // NOTE: We always use fixed address to compute the salt for the proxy. @@ -1739,6 +1746,16 @@ const deployProxyWithCreateX = async ( log(`Deployed ${proxyName} at ${proxyAddress}`); + if (isFork && deployerToUse !== deployerAddr) { + // Transfer governance of proxy to real deployer + const cProxy = await ethers.getContractAt(proxyName, proxyAddress); + const actualDeployer = await ethers.getSigner(deployerAddr); + await withConfirmation( + cProxy.connect(sDeployer).transferGovernance(deployerAddr) + ); + await withConfirmation(cProxy.connect(actualDeployer).claimGovernance()); + } + // Verify contract on Etherscan if requested and on a live network // Can be enabled via parameter or VERIFY_CONTRACTS environment variable const shouldVerify = From 45578366f5d2175025f7cdd64f9c6d5dbe1b5c93 Mon Sep 17 00:00:00 2001 From: Shahul Hameed <10547529+shahthepro@users.noreply.github.com> Date: Mon, 5 Jan 2026 20:39:02 +0400 Subject: [PATCH 049/101] deploy script bug fix --- contracts/deploy/deployActions.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/deploy/deployActions.js b/contracts/deploy/deployActions.js index 01825313a2..ffd82f57df 100644 --- a/contracts/deploy/deployActions.js +++ b/contracts/deploy/deployActions.js @@ -1720,7 +1720,7 @@ const deployProxyWithCreateX = async ( const getFactoryBytecode = async () => { // No deployment needed—get factory directly from artifacts const ProxyContract = await ethers.getContractFactory(proxyName); - const encodedArgs = ProxyContract.interface.encodeDeploy([deployerAddr]); + const encodedArgs = ProxyContract.interface.encodeDeploy([deployerToUse]); return ethers.utils.hexConcat([ProxyContract.bytecode, encodedArgs]); }; From 31b9f502d5cee949e4de6db9ad81c6ff4b8649c6 Mon Sep 17 00:00:00 2001 From: Shahul Hameed <10547529+shahthepro@users.noreply.github.com> Date: Mon, 5 Jan 2026 21:04:20 +0400 Subject: [PATCH 050/101] Store create2 proxy addresses --- .../deploy/base/041_crosschain_strategy.js | 16 +++-- contracts/deploy/deployActions.js | 64 ++++++++++++++----- .../deploy/mainnet/163_crosschain_strategy.js | 16 +++-- contracts/test/_fixture-base.js | 6 +- contracts/test/_fixture.js | 6 +- contracts/utils/addresses.js | 13 ++-- 6 files changed, 86 insertions(+), 35 deletions(-) diff --git a/contracts/deploy/base/041_crosschain_strategy.js b/contracts/deploy/base/041_crosschain_strategy.js index b06b89d4d8..996b96bd2a 100644 --- a/contracts/deploy/base/041_crosschain_strategy.js +++ b/contracts/deploy/base/041_crosschain_strategy.js @@ -1,6 +1,9 @@ const { deployOnBase } = require("../../utils/deploy-l2"); const addresses = require("../../utils/addresses"); -const { deployCrossChainRemoteStrategyImpl } = require("../deployActions"); +const { + deployCrossChainRemoteStrategyImpl, + getCreate2ProxyAddress, +} = require("../deployActions"); const { withConfirmation } = require("../../utils/deploy.js"); const { cctpDomainIds } = require("../../utils/cctp"); @@ -12,15 +15,18 @@ module.exports = deployOnBase( const { deployerAddr } = await getNamedAccounts(); const sDeployer = await ethers.provider.getSigner(deployerAddr); + const crossChainStrategyProxyAddress = await getCreate2ProxyAddress( + "CrossChainStrategyProxy" + ); console.log( - `CrossChainStrategyProxy address: ${addresses.CrossChainStrategyProxy}` + `CrossChainStrategyProxy address: ${crossChainStrategyProxyAddress}` ); const implAddress = await deployCrossChainRemoteStrategyImpl( "0xbeeF010f9cb27031ad51e3333f9aF9C6B1228183", // 4626 Vault - addresses.CrossChainStrategyProxy, + crossChainStrategyProxyAddress, cctpDomainIds.Ethereum, - addresses.CrossChainStrategyProxy, + crossChainStrategyProxyAddress, addresses.base.USDC, "CrossChainRemoteStrategy", addresses.CCTPTokenMessengerV2, @@ -31,7 +37,7 @@ module.exports = deployOnBase( const cCrossChainRemoteStrategy = await ethers.getContractAt( "CrossChainRemoteStrategy", - addresses.CrossChainStrategyProxy + crossChainStrategyProxyAddress ); console.log( `CrossChainRemoteStrategy address: ${cCrossChainRemoteStrategy.address}` diff --git a/contracts/deploy/deployActions.js b/contracts/deploy/deployActions.js index ffd82f57df..afff4e7e7c 100644 --- a/contracts/deploy/deployActions.js +++ b/contracts/deploy/deployActions.js @@ -1,4 +1,6 @@ const hre = require("hardhat"); +const fs = require("fs"); +const path = require("path"); const { setStorageAt } = require("@nomicfoundation/hardhat-network-helpers"); const { getNetworkName } = require("../utils/hardhat-helpers"); const { parseUnits } = require("ethers/lib/utils.js"); @@ -29,7 +31,6 @@ const { resolveContract } = require("../utils/resolvers"); const { impersonateAccount, getSigner } = require("../utils/signers"); const { getDefenderSigner } = require("../utils/signersNoHardhat"); const { getTxOpts } = require("../utils/tx"); -const { impersonateAndFund } = require("../utils/signers"); const createxAbi = require("../abi/createx.json"); const { @@ -1690,6 +1691,47 @@ const deploySonicSwapXAMOStrategyImplementation = async () => { return cSonicSwapXAMOStrategy; }; +const getCreate2ProxiesFilePath = async () => { + const networkName = isFork ? "localhost" : await getNetworkName(); + return path.resolve( + __dirname, + `./../deployments/${networkName}/create2Proxies.json` + ); +}; + +const storeCreate2ProxyAddress = async (proxyName, proxyAddress) => { + const filePath = await getCreate2ProxiesFilePath(); + + let existingContents = {}; + if (fs.existsSync(filePath)) { + existingContents = JSON.parse(fs.readFileSync(filePath, "utf8")); + } + + fs.writeFileSync( + filePath, + JSON.stringify( + { + ...existingContents, + [proxyName]: proxyAddress, + }, + undefined, + 2 + ) + ); +}; + +const getCreate2ProxyAddress = async (proxyName) => { + const filePath = await getCreate2ProxiesFilePath(); + if (!fs.existsSync(filePath)) { + throw new Error(`Create2 proxies file not found at ${filePath}`); + } + const contents = JSON.parse(fs.readFileSync(filePath, "utf8")); + if (!contents[proxyName]) { + throw new Error(`Proxy ${proxyName} not found in ${filePath}`); + } + return contents[proxyName]; +}; + // deploys an instance of InitializeGovernedUpgradeabilityProxy where address is defined by salt const deployProxyWithCreateX = async ( salt, @@ -1699,11 +1741,7 @@ const deployProxyWithCreateX = async ( ) => { const { deployerAddr } = await getNamedAccounts(); - // Impersonate a single deployer for fork testing - const deployerToImpersonate = "0x58890A9cB27586E83Cb51d2d26bbE18a1a647245"; - await impersonateAndFund(deployerToImpersonate); - const deployerToUse = isFork ? deployerToImpersonate : deployerAddr; - const sDeployer = await ethers.provider.getSigner(deployerToUse); + const sDeployer = await ethers.provider.getSigner(deployerAddr); // Basically hex of "originprotocol" padded to 20 bytes to mimic an address const addrForSalt = "0x0000000000006f726967696e70726f746f636f6c"; @@ -1720,7 +1758,7 @@ const deployProxyWithCreateX = async ( const getFactoryBytecode = async () => { // No deployment needed—get factory directly from artifacts const ProxyContract = await ethers.getContractFactory(proxyName); - const encodedArgs = ProxyContract.interface.encodeDeploy([deployerToUse]); + const encodedArgs = ProxyContract.interface.encodeDeploy([deployerAddr]); return ethers.utils.hexConcat([ProxyContract.bytecode, encodedArgs]); }; @@ -1746,15 +1784,7 @@ const deployProxyWithCreateX = async ( log(`Deployed ${proxyName} at ${proxyAddress}`); - if (isFork && deployerToUse !== deployerAddr) { - // Transfer governance of proxy to real deployer - const cProxy = await ethers.getContractAt(proxyName, proxyAddress); - const actualDeployer = await ethers.getSigner(deployerAddr); - await withConfirmation( - cProxy.connect(sDeployer).transferGovernance(deployerAddr) - ); - await withConfirmation(cProxy.connect(actualDeployer).claimGovernance()); - } + storeCreate2ProxyAddress(proxyName, proxyAddress); // Verify contract on Etherscan if requested and on a live network // Can be enabled via parameter or VERIFY_CONTRACTS environment variable @@ -1997,4 +2027,6 @@ module.exports = { deployCrossChainMasterStrategyImpl, deployCrossChainRemoteStrategyImpl, deployCrossChainUnitTestStrategy, + + getCreate2ProxyAddress, }; diff --git a/contracts/deploy/mainnet/163_crosschain_strategy.js b/contracts/deploy/mainnet/163_crosschain_strategy.js index 77d798cbed..cb12ff12df 100644 --- a/contracts/deploy/mainnet/163_crosschain_strategy.js +++ b/contracts/deploy/mainnet/163_crosschain_strategy.js @@ -1,7 +1,10 @@ const { deploymentWithGovernanceProposal } = require("../../utils/deploy"); const addresses = require("../../utils/addresses"); const { cctpDomainIds } = require("../../utils/cctp"); -const { deployCrossChainMasterStrategyImpl } = require("../deployActions"); +const { + deployCrossChainMasterStrategyImpl, + getCreate2ProxyAddress, +} = require("../deployActions"); module.exports = deploymentWithGovernanceProposal( { @@ -13,17 +16,20 @@ module.exports = deploymentWithGovernanceProposal( }, async () => { const { deployerAddr } = await getNamedAccounts(); + const crossChainStrategyProxyAddress = await getCreate2ProxyAddress( + "CrossChainStrategyProxy" + ); const cProxy = await ethers.getContractAt( "CrossChainStrategyProxy", - addresses.CrossChainStrategyProxy + crossChainStrategyProxyAddress ); console.log(`CrossChainStrategyProxy address: ${cProxy.address}`); const implAddress = await deployCrossChainMasterStrategyImpl( - addresses.CrossChainStrategyProxy, + crossChainStrategyProxyAddress, cctpDomainIds.Base, // Same address for both master and remote strategy - addresses.CrossChainStrategyProxy, + crossChainStrategyProxyAddress, addresses.mainnet.USDC, deployerAddr, "CrossChainMasterStrategy" @@ -32,7 +38,7 @@ module.exports = deploymentWithGovernanceProposal( const cCrossChainMasterStrategy = await ethers.getContractAt( "CrossChainMasterStrategy", - addresses.CrossChainStrategyProxy + crossChainStrategyProxyAddress ); console.log( `CrossChainMasterStrategy address: ${cCrossChainMasterStrategy.address}` diff --git a/contracts/test/_fixture-base.js b/contracts/test/_fixture-base.js index 0c882ffd66..88a18314a5 100644 --- a/contracts/test/_fixture-base.js +++ b/contracts/test/_fixture-base.js @@ -8,6 +8,7 @@ const { deployWithConfirmation } = require("../utils/deploy"); const addresses = require("../utils/addresses"); const erc20Abi = require("./abi/erc20.json"); const hhHelpers = require("@nomicfoundation/hardhat-network-helpers"); +const { getCreate2ProxyAddress } = require("../deploy/deployActions"); const log = require("../utils/logger")("test:fixtures-base"); @@ -339,9 +340,12 @@ const bridgeHelperModuleFixture = deployments.createFixture(async () => { const crossChainFixture = deployments.createFixture(async () => { const fixture = await defaultBaseFixture(); + const crossChainStrategyProxyAddress = await getCreate2ProxyAddress( + "CrossChainStrategyProxy" + ); const crossChainRemoteStrategy = await ethers.getContractAt( "CrossChainRemoteStrategy", - addresses.CrossChainStrategyProxy + crossChainStrategyProxyAddress ); await deployWithConfirmation("CCTPMessageTransmitterMock2", [ diff --git a/contracts/test/_fixture.js b/contracts/test/_fixture.js index 488891734f..05cf394252 100644 --- a/contracts/test/_fixture.js +++ b/contracts/test/_fixture.js @@ -32,6 +32,7 @@ const { isHoleskyFork, } = require("./helpers"); const { hardhatSetBalance, setERC20TokenBalance } = require("./_fund"); +const { getCreate2ProxyAddress } = require("../deploy/deployActions"); const usdsAbi = require("./abi/usds.json").abi; const usdtAbi = require("./abi/usdt.json").abi; @@ -3000,9 +3001,12 @@ async function enableExecutionLayerGeneralPurposeRequests() { async function crossChainFixture() { const fixture = await defaultFixture(); + const crossChainStrategyProxyAddress = await getCreate2ProxyAddress( + "CrossChainStrategyProxy" + ); const cCrossChainMasterStrategy = await ethers.getContractAt( "CrossChainMasterStrategy", - addresses.CrossChainStrategyProxy + crossChainStrategyProxyAddress ); await deployWithConfirmation("CCTPMessageTransmitterMock2", [ diff --git a/contracts/utils/addresses.js b/contracts/utils/addresses.js index 0ac41b9608..b5df71600d 100644 --- a/contracts/utils/addresses.js +++ b/contracts/utils/addresses.js @@ -689,12 +689,11 @@ addresses.hoodi.defenderRelayer = "0x419B6BdAE482f41b8B194515749F3A2Da26d583b"; addresses.hoodi.mockBeaconRoots = "0xdCfcAE4A084AA843eE446f400B23aA7B6340484b"; // Crosschain Strategy - -addresses.CrossChainStrategyProxy = - "0xEc923B471DD0220Aa1596Ead5fbE0580E334A660"; -addresses.mainnet.CrossChainStrategyProxy = - "0xEc923B471DD0220Aa1596Ead5fbE0580E334A660"; -addresses.base.CrossChainStrategyProxy = - "0xEc923B471DD0220Aa1596Ead5fbE0580E334A660"; +// addresses.CrossChainStrategyProxy = +// "TBD"; +// addresses.mainnet.CrossChainStrategyProxy = +// "TBD"; +// addresses.base.CrossChainStrategyProxy = +// "TBD"; module.exports = addresses; From 057bd784196be8be2e79f5c0cd911be710bd97d0 Mon Sep 17 00:00:00 2001 From: Shahul Hameed <10547529+shahthepro@users.noreply.github.com> Date: Mon, 5 Jan 2026 21:07:22 +0400 Subject: [PATCH 051/101] fix: await --- contracts/deploy/deployActions.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/deploy/deployActions.js b/contracts/deploy/deployActions.js index afff4e7e7c..add946fbb3 100644 --- a/contracts/deploy/deployActions.js +++ b/contracts/deploy/deployActions.js @@ -1784,7 +1784,7 @@ const deployProxyWithCreateX = async ( log(`Deployed ${proxyName} at ${proxyAddress}`); - storeCreate2ProxyAddress(proxyName, proxyAddress); + await storeCreate2ProxyAddress(proxyName, proxyAddress); // Verify contract on Etherscan if requested and on a live network // Can be enabled via parameter or VERIFY_CONTRACTS environment variable From f4bff9e441129f8755a8dbe0fdffc095f0457fe4 Mon Sep 17 00:00:00 2001 From: Shahul Hameed <10547529+shahthepro@users.noreply.github.com> Date: Mon, 5 Jan 2026 21:13:22 +0400 Subject: [PATCH 052/101] more logging --- contracts/deploy/deployActions.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/contracts/deploy/deployActions.js b/contracts/deploy/deployActions.js index add946fbb3..0f5fdd5849 100644 --- a/contracts/deploy/deployActions.js +++ b/contracts/deploy/deployActions.js @@ -1702,6 +1702,8 @@ const getCreate2ProxiesFilePath = async () => { const storeCreate2ProxyAddress = async (proxyName, proxyAddress) => { const filePath = await getCreate2ProxiesFilePath(); + console.log(`Storing create2 proxy address for ${proxyName} at ${filePath}`); + let existingContents = {}; if (fs.existsSync(filePath)) { existingContents = JSON.parse(fs.readFileSync(filePath, "utf8")); @@ -1716,7 +1718,10 @@ const storeCreate2ProxyAddress = async (proxyName, proxyAddress) => { }, undefined, 2 - ) + ), + { + mode: "w", + } ); }; From 2948739b677aa9b901115b19fbed1f62828e79ad Mon Sep 17 00:00:00 2001 From: Shahul Hameed <10547529+shahthepro@users.noreply.github.com> Date: Mon, 5 Jan 2026 21:17:05 +0400 Subject: [PATCH 053/101] fix opts --- contracts/deploy/deployActions.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/deploy/deployActions.js b/contracts/deploy/deployActions.js index 0f5fdd5849..af1ceff98f 100644 --- a/contracts/deploy/deployActions.js +++ b/contracts/deploy/deployActions.js @@ -1720,7 +1720,7 @@ const storeCreate2ProxyAddress = async (proxyName, proxyAddress) => { 2 ), { - mode: "w", + flag: "w", } ); }; From 801eacf6f0de41fd90dd56b9c5571846c9facd89 Mon Sep 17 00:00:00 2001 From: Shahul Hameed <10547529+shahthepro@users.noreply.github.com> Date: Mon, 5 Jan 2026 21:20:33 +0400 Subject: [PATCH 054/101] Fix env for deploy action --- contracts/deploy/deployActions.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/contracts/deploy/deployActions.js b/contracts/deploy/deployActions.js index af1ceff98f..6ee7d0b13d 100644 --- a/contracts/deploy/deployActions.js +++ b/contracts/deploy/deployActions.js @@ -15,6 +15,8 @@ const { isSonicOrFork, isTest, isFork, + isForkTest, + isCI, isPlume, isHoodi, isHoodiOrFork, @@ -1692,7 +1694,8 @@ const deploySonicSwapXAMOStrategyImplementation = async () => { }; const getCreate2ProxiesFilePath = async () => { - const networkName = isFork ? "localhost" : await getNetworkName(); + const networkName = + isFork || isForkTest || isCI ? "localhost" : await getNetworkName(); return path.resolve( __dirname, `./../deployments/${networkName}/create2Proxies.json` From 8d9ace2beb05f5bcf5f7f2cff9a5f255f332b003 Mon Sep 17 00:00:00 2001 From: Shahul Hameed <10547529+shahthepro@users.noreply.github.com> Date: Mon, 5 Jan 2026 21:31:27 +0400 Subject: [PATCH 055/101] Change writeFileSync to writeFile --- contracts/deploy/deployActions.js | 30 ++++++++++++++++++------------ 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/contracts/deploy/deployActions.js b/contracts/deploy/deployActions.js index 6ee7d0b13d..5d45388672 100644 --- a/contracts/deploy/deployActions.js +++ b/contracts/deploy/deployActions.js @@ -1712,20 +1712,26 @@ const storeCreate2ProxyAddress = async (proxyName, proxyAddress) => { existingContents = JSON.parse(fs.readFileSync(filePath, "utf8")); } - fs.writeFileSync( - filePath, - JSON.stringify( + await new Promise((resolve, reject) => { + fs.writeFile( + filePath, + JSON.stringify( + { + ...existingContents, + [proxyName]: proxyAddress, + }, + undefined, + 2 + ), { - ...existingContents, - [proxyName]: proxyAddress, + flag: "w", }, - undefined, - 2 - ), - { - flag: "w", - } - ); + (err) => { + if (err) reject(err); + resolve(); + } + ); + }); }; const getCreate2ProxyAddress = async (proxyName) => { From cb19da21284710bbee91e490aaac47e588999ecb Mon Sep 17 00:00:00 2001 From: Shahul Hameed <10547529+shahthepro@users.noreply.github.com> Date: Mon, 5 Jan 2026 21:35:29 +0400 Subject: [PATCH 056/101] add log --- contracts/deploy/deployActions.js | 1 + 1 file changed, 1 insertion(+) diff --git a/contracts/deploy/deployActions.js b/contracts/deploy/deployActions.js index 5d45388672..acc11fb929 100644 --- a/contracts/deploy/deployActions.js +++ b/contracts/deploy/deployActions.js @@ -1728,6 +1728,7 @@ const storeCreate2ProxyAddress = async (proxyName, proxyAddress) => { }, (err) => { if (err) reject(err); + console.log(`Stored create2 proxy address for ${proxyName} at ${filePath}`); resolve(); } ); From 8dd21bd2e912d692933f322625301cf16c36aa28 Mon Sep 17 00:00:00 2001 From: Shahul Hameed <10547529+shahthepro@users.noreply.github.com> Date: Mon, 5 Jan 2026 21:41:15 +0400 Subject: [PATCH 057/101] Add more logs --- contracts/deploy/deployActions.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/contracts/deploy/deployActions.js b/contracts/deploy/deployActions.js index acc11fb929..4a6bc237de 100644 --- a/contracts/deploy/deployActions.js +++ b/contracts/deploy/deployActions.js @@ -1728,7 +1728,9 @@ const storeCreate2ProxyAddress = async (proxyName, proxyAddress) => { }, (err) => { if (err) reject(err); - console.log(`Stored create2 proxy address for ${proxyName} at ${filePath}`); + console.log( + `Stored create2 proxy address for ${proxyName} at ${filePath}` + ); resolve(); } ); @@ -1737,10 +1739,14 @@ const storeCreate2ProxyAddress = async (proxyName, proxyAddress) => { const getCreate2ProxyAddress = async (proxyName) => { const filePath = await getCreate2ProxiesFilePath(); + console.log( + `Getting create2 proxy address for ${proxyName} from ${filePath}` + ); if (!fs.existsSync(filePath)) { throw new Error(`Create2 proxies file not found at ${filePath}`); } const contents = JSON.parse(fs.readFileSync(filePath, "utf8")); + console.log(contents); if (!contents[proxyName]) { throw new Error(`Proxy ${proxyName} not found in ${filePath}`); } @@ -1800,6 +1806,7 @@ const deployProxyWithCreateX = async ( log(`Deployed ${proxyName} at ${proxyAddress}`); await storeCreate2ProxyAddress(proxyName, proxyAddress); + console.log("Stored create2 proxy address"); // Verify contract on Etherscan if requested and on a live network // Can be enabled via parameter or VERIFY_CONTRACTS environment variable From b54ef4b288ccd3e286935d71dd454edb4a1444e6 Mon Sep 17 00:00:00 2001 From: Shahul Hameed <10547529+shahthepro@users.noreply.github.com> Date: Mon, 5 Jan 2026 21:46:49 +0400 Subject: [PATCH 058/101] fix callback --- contracts/deploy/deployActions.js | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/contracts/deploy/deployActions.js b/contracts/deploy/deployActions.js index 4a6bc237de..1c7d1a52b2 100644 --- a/contracts/deploy/deployActions.js +++ b/contracts/deploy/deployActions.js @@ -1712,7 +1712,7 @@ const storeCreate2ProxyAddress = async (proxyName, proxyAddress) => { existingContents = JSON.parse(fs.readFileSync(filePath, "utf8")); } - await new Promise((resolve, reject) => { + await new Promise((resolve) => { fs.writeFile( filePath, JSON.stringify( @@ -1723,11 +1723,8 @@ const storeCreate2ProxyAddress = async (proxyName, proxyAddress) => { undefined, 2 ), - { - flag: "w", - }, (err) => { - if (err) reject(err); + console.log("Err:", err); console.log( `Stored create2 proxy address for ${proxyName} at ${filePath}` ); From c0a277e8ff394fc1cda24a038ae93cea7f8955cc Mon Sep 17 00:00:00 2001 From: Shahul Hameed <10547529+shahthepro@users.noreply.github.com> Date: Mon, 5 Jan 2026 21:51:43 +0400 Subject: [PATCH 059/101] Add empty file --- contracts/deploy/deployActions.js | 6 ++++++ contracts/deployments/base/create2Proxies.json | 1 + contracts/deployments/mainnet/create2Proxies.json | 1 + 3 files changed, 8 insertions(+) create mode 100644 contracts/deployments/base/create2Proxies.json create mode 100644 contracts/deployments/mainnet/create2Proxies.json diff --git a/contracts/deploy/deployActions.js b/contracts/deploy/deployActions.js index 1c7d1a52b2..4b102a30ba 100644 --- a/contracts/deploy/deployActions.js +++ b/contracts/deploy/deployActions.js @@ -1707,6 +1707,12 @@ const storeCreate2ProxyAddress = async (proxyName, proxyAddress) => { console.log(`Storing create2 proxy address for ${proxyName} at ${filePath}`); + // Ensure the directory exists before writing the file + const dirPath = path.dirname(filePath); + if (!fs.existsSync(dirPath)) { + fs.mkdirSync(dirPath, { recursive: true }); + } + let existingContents = {}; if (fs.existsSync(filePath)) { existingContents = JSON.parse(fs.readFileSync(filePath, "utf8")); diff --git a/contracts/deployments/base/create2Proxies.json b/contracts/deployments/base/create2Proxies.json new file mode 100644 index 0000000000..9e26dfeeb6 --- /dev/null +++ b/contracts/deployments/base/create2Proxies.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/contracts/deployments/mainnet/create2Proxies.json b/contracts/deployments/mainnet/create2Proxies.json new file mode 100644 index 0000000000..9e26dfeeb6 --- /dev/null +++ b/contracts/deployments/mainnet/create2Proxies.json @@ -0,0 +1 @@ +{} \ No newline at end of file From bfb411c169c162cea0d98b4c8d5bacba7c853034 Mon Sep 17 00:00:00 2001 From: Shahul Hameed <10547529+shahthepro@users.noreply.github.com> Date: Mon, 5 Jan 2026 22:02:34 +0400 Subject: [PATCH 060/101] Cleanup logs --- contracts/deploy/deployActions.js | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/contracts/deploy/deployActions.js b/contracts/deploy/deployActions.js index 4b102a30ba..711bb7e523 100644 --- a/contracts/deploy/deployActions.js +++ b/contracts/deploy/deployActions.js @@ -1705,8 +1705,6 @@ const getCreate2ProxiesFilePath = async () => { const storeCreate2ProxyAddress = async (proxyName, proxyAddress) => { const filePath = await getCreate2ProxiesFilePath(); - console.log(`Storing create2 proxy address for ${proxyName} at ${filePath}`); - // Ensure the directory exists before writing the file const dirPath = path.dirname(filePath); if (!fs.existsSync(dirPath)) { @@ -1718,7 +1716,7 @@ const storeCreate2ProxyAddress = async (proxyName, proxyAddress) => { existingContents = JSON.parse(fs.readFileSync(filePath, "utf8")); } - await new Promise((resolve) => { + await new Promise((resolve, reject) => { fs.writeFile( filePath, JSON.stringify( @@ -1730,7 +1728,11 @@ const storeCreate2ProxyAddress = async (proxyName, proxyAddress) => { 2 ), (err) => { - console.log("Err:", err); + if (err) { + console.log("Err:", err); + reject(err); + return; + } console.log( `Stored create2 proxy address for ${proxyName} at ${filePath}` ); @@ -1742,14 +1744,10 @@ const storeCreate2ProxyAddress = async (proxyName, proxyAddress) => { const getCreate2ProxyAddress = async (proxyName) => { const filePath = await getCreate2ProxiesFilePath(); - console.log( - `Getting create2 proxy address for ${proxyName} from ${filePath}` - ); if (!fs.existsSync(filePath)) { throw new Error(`Create2 proxies file not found at ${filePath}`); } const contents = JSON.parse(fs.readFileSync(filePath, "utf8")); - console.log(contents); if (!contents[proxyName]) { throw new Error(`Proxy ${proxyName} not found in ${filePath}`); } @@ -1809,7 +1807,6 @@ const deployProxyWithCreateX = async ( log(`Deployed ${proxyName} at ${proxyAddress}`); await storeCreate2ProxyAddress(proxyName, proxyAddress); - console.log("Stored create2 proxy address"); // Verify contract on Etherscan if requested and on a live network // Can be enabled via parameter or VERIFY_CONTRACTS environment variable From 2178676b1e6779103a96bdfb59412648610e5e29 Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Tue, 6 Jan 2026 00:40:33 +0100 Subject: [PATCH 061/101] withdraw funds according to the spec --- .../crosschain/CrossChainRemoteStrategy.sol | 42 +++--- .../crosschain/cross-chain-strategy.js | 134 ++++++++++++------ 2 files changed, 120 insertions(+), 56 deletions(-) diff --git a/contracts/contracts/strategies/crosschain/CrossChainRemoteStrategy.sol b/contracts/contracts/strategies/crosschain/CrossChainRemoteStrategy.sol index 593e145363..a1babf1829 100644 --- a/contracts/contracts/strategies/crosschain/CrossChainRemoteStrategy.sol +++ b/contracts/contracts/strategies/crosschain/CrossChainRemoteStrategy.sol @@ -25,8 +25,9 @@ contract CrossChainRemoteStrategy is using SafeERC20 for IERC20; using CrossChainStrategyHelper for bytes; - event DepositFailed(string reason); - event WithdrawFailed(string reason); + event DepositUnderlyingFailed(string reason); + event WithdrawFailed(uint256 amountRequested, uint256 amountAvailable); + event WithdrawUnderlyingFailed(string reason); event StrategistUpdated(address _address); /** @@ -204,11 +205,11 @@ contract CrossChainRemoteStrategy is try IERC4626(platformAddress).deposit(_amount, address(this)) { emit Deposit(_asset, address(shareToken), _amount); } catch Error(string memory reason) { - emit DepositFailed( + emit DepositUnderlyingFailed( string(abi.encodePacked("Deposit failed: ", reason)) ); } catch (bytes memory lowLevelData) { - emit DepositFailed( + emit DepositUnderlyingFailed( string( abi.encodePacked( "Deposit failed: low-level call failed with data ", @@ -234,32 +235,41 @@ contract CrossChainRemoteStrategy is uint256 usdcBalance = IERC20(baseToken).balanceOf(address(this)); if (usdcBalance < withdrawAmount) { - // Withdraw funds from the remote strategy - _withdraw(address(this), baseToken, withdrawAmount); + // Withdraw the missing funds from the remote strategy. This call can fail and + // the failure doesn't bubble up to the _processWithdrawMessage function + _withdraw(address(this), baseToken, withdrawAmount - usdcBalance); - // 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. + // Update the possible increase in the balance on the contract. usdcBalance = IERC20(baseToken).balanceOf(address(this)); } // Check balance after withdrawal uint256 balanceAfter = checkBalance(baseToken); - if (usdcBalance > 1e6) { + // If there are some tokens to be sent AND the balance is sufficient + // to satisfy the withdrawal request then send the funds to the peer strategy. + // In case a direct withdraw(All) has previously been called + // there is a possibility of USDC funds remaining on the contract. + // A separate withdraw to extract or deposit to the Morpho vault needs to be + // initiated from the peer Master strategy to utilise USDC funds. + if (usdcBalance > 1e6 && usdcBalance >= withdrawAmount) { // The new balance on the contract needs to have USDC subtracted from it as - // that will be withdrawn in the next steps + // that will be withdrawn in the next step bytes memory message = CrossChainStrategyHelper .encodeBalanceCheckMessage( lastTransferNonce, - balanceAfter - usdcBalance + balanceAfter - withdrawAmount ); - _sendTokens(usdcBalance, message); + _sendTokens(withdrawAmount, message); } else { - // Contract only has a small dust, so only send the balance update message + // Contract either: + // - only has a small dust + // - doesn't have sufficient funds to satisfy the withdrawal request + // In both cases send the balance update message to the peer strategy. bytes memory message = CrossChainStrategyHelper .encodeBalanceCheckMessage(lastTransferNonce, balanceAfter); _sendMessage(message); + emit WithdrawFailed(withdrawAmount, usdcBalance); } } @@ -290,11 +300,11 @@ contract CrossChainRemoteStrategy is { emit Withdrawal(_asset, address(shareToken), _amount); } catch Error(string memory reason) { - emit WithdrawFailed( + emit WithdrawUnderlyingFailed( string(abi.encodePacked("Withdrawal failed: ", reason)) ); } catch (bytes memory lowLevelData) { - emit WithdrawFailed( + emit WithdrawUnderlyingFailed( string( abi.encodePacked( "Withdrawal failed: low-level call failed with data ", diff --git a/contracts/test/strategies/crosschain/cross-chain-strategy.js b/contracts/test/strategies/crosschain/cross-chain-strategy.js index 1d1f9838ff..93af766b82 100644 --- a/contracts/test/strategies/crosschain/cross-chain-strategy.js +++ b/contracts/test/strategies/crosschain/cross-chain-strategy.js @@ -77,7 +77,7 @@ describe("ForkTest: CrossChainRemoteStrategy", function () { // Checks the diff in the total expected value in the vault // (plus accompanying strategy value) - const assetVaultTotalValue = async (amountExpected) => { + const assertVaultTotalValue = async (amountExpected) => { const amountToCompare = typeof amountExpected === "string" ? ousdUnits(amountExpected) @@ -103,20 +103,20 @@ describe("ForkTest: CrossChainRemoteStrategy", function () { const remoteBalanceRecByMasterBefore = await crossChainMasterStrategy.remoteStrategyBalance(); const messagesinQueueBefore = await messageTransmitter.messagesInQueue(); - await assetVaultTotalValue(vaultDiffAfterMint); + await assertVaultTotalValue(vaultDiffAfterMint); await depositToMasterStrategy(amount); await expect(await messageTransmitter.messagesInQueue()).to.eq( messagesinQueueBefore + 1 ); - await assetVaultTotalValue(vaultDiffAfterMint); + await assertVaultTotalValue(vaultDiffAfterMint); // Simulate off chain component processing deposit message await expect(messageTransmitter.processFront()) .to.emit(crossChainRemoteStrategy, "Deposit") .withArgs(usdc.address, morphoVault.address, amountBn); - await assetVaultTotalValue(vaultDiffAfterMint); + await assertVaultTotalValue(vaultDiffAfterMint); // 1 message is processed, another one (checkBalance) has entered the queue await expect(await messageTransmitter.messagesInQueue()).to.eq( messagesinQueueBefore + 1 @@ -133,13 +133,13 @@ describe("ForkTest: CrossChainRemoteStrategy", function () { await expect(await messageTransmitter.messagesInQueue()).to.eq( messagesinQueueBefore ); - await assetVaultTotalValue(vaultDiffAfterMint); + await assertVaultTotalValue(vaultDiffAfterMint); await expect(await crossChainMasterStrategy.remoteStrategyBalance()).to.eq( remoteBalanceRecByMasterBefore + amountBn ); }; - const withdrawFromRemoteToVault = async (amount) => { + const withdrawFromRemoteToVault = async (amount, expectWithdrawalEvent) => { const { messageTransmitter, morphoVault } = fixture; const amountBn = await units(amount, usdc); const remoteBalanceBefore = await crossChainRemoteStrategy.checkBalance( @@ -148,11 +148,6 @@ describe("ForkTest: CrossChainRemoteStrategy", function () { const remoteBalanceRecByMasterBefore = await crossChainMasterStrategy.remoteStrategyBalance(); - // If there is any pre-existing USDC balance on the remote strategy it will get swept up by the next - // withdrawal - const usdcBalanceOnRemoteStrategyBefore = await usdc.balanceOf( - crossChainRemoteStrategy.address - ); const messagesinQueueBefore = await messageTransmitter.messagesInQueue(); await withdrawFromRemoteStrategy(amount); @@ -160,9 +155,13 @@ describe("ForkTest: CrossChainRemoteStrategy", function () { messagesinQueueBefore + 1 ); - await expect(messageTransmitter.processFront()) - .to.emit(crossChainRemoteStrategy, "Withdrawal") - .withArgs(usdc.address, morphoVault.address, amountBn); + if (expectWithdrawalEvent) { + await expect(messageTransmitter.processFront()) + .to.emit(crossChainRemoteStrategy, "Withdrawal") + .withArgs(usdc.address, morphoVault.address, amountBn); + } else { + await messageTransmitter.processFront(); + } await expect(await messageTransmitter.messagesInQueue()).to.eq( messagesinQueueBefore + 1 @@ -173,10 +172,10 @@ describe("ForkTest: CrossChainRemoteStrategy", function () { remoteBalanceRecByMasterBefore ); - const remoteBalanceAfter = - remoteBalanceBefore - amountBn - usdcBalanceOnRemoteStrategyBefore; + const remoteBalanceAfter = remoteBalanceBefore - amountBn; + await expect( - await morphoVault.balanceOf(crossChainRemoteStrategy.address) + await crossChainRemoteStrategy.checkBalance(usdc.address) ).to.eq(remoteBalanceAfter); // Simulate off chain component processing checkBalance message await expect(messageTransmitter.processFront()) @@ -190,11 +189,11 @@ describe("ForkTest: CrossChainRemoteStrategy", function () { it("Should mint USDC to master strategy, transfer to remote and update balance", async function () { const { morphoVault } = fixture; - await assetVaultTotalValue("0"); + await assertVaultTotalValue("0"); await expect(await morphoVault.totalAssets()).to.eq(await units("0", usdc)); await mintToMasterDepositToRemote("1000"); - await assetVaultTotalValue("1000"); + await assertVaultTotalValue("1000"); await expect(await morphoVault.totalAssets()).to.eq( await units("1000", usdc) @@ -204,25 +203,25 @@ describe("ForkTest: CrossChainRemoteStrategy", function () { it("Should be able to withdraw from the remote strategy", async function () { const { morphoVault } = fixture; await mintToMasterDepositToRemote("1000"); - await assetVaultTotalValue("1000"); + await assertVaultTotalValue("1000"); await expect(await morphoVault.totalAssets()).to.eq( await units("1000", usdc) ); - await withdrawFromRemoteToVault("500"); - await assetVaultTotalValue("1000"); + await withdrawFromRemoteToVault("500", true); + await assertVaultTotalValue("1000"); }); - it("Should be able to direct withdraw from the remote strategy direclty", async function () { + it("Should be able to direct withdraw from the remote strategy directly and collect to master", async function () { const { morphoVault } = fixture; await mintToMasterDepositToRemote("1000"); - await assetVaultTotalValue("1000"); + await assertVaultTotalValue("1000"); await expect(await morphoVault.totalAssets()).to.eq( await units("1000", usdc) ); await directWithdrawFromRemoteStrategy("500"); - await assetVaultTotalValue("1000"); + await assertVaultTotalValue("1000"); // 500 has been withdrawn from the Morpho vault but still remains on the // remote strategy @@ -230,26 +229,78 @@ describe("ForkTest: CrossChainRemoteStrategy", function () { await crossChainRemoteStrategy.checkBalance(usdc.address) ).to.eq(await units("1000", usdc)); - // Next withdraw should withdraw additional 10 from the remote strategy and pick up the - // previous 500 totaling a transfer of 510 - await withdrawFromRemoteToVault("10"); + // Next withdraw should not withdraw any additional funds from Morpho and just send + // 450 USDC to the master. + await withdrawFromRemoteToVault("450", false); - await assetVaultTotalValue("1000"); + await assertVaultTotalValue("1000"); + // The remote strategy should have 500 USDC in Morpho vault and 50 USDC on the contract await expect( await crossChainRemoteStrategy.checkBalance(usdc.address) - ).to.eq(await units("490", usdc)); + ).to.eq(await units("550", usdc)); + await expect(await usdc.balanceOf(crossChainRemoteStrategy.address)).to.eq( + await units("50", usdc) + ); }); - it("Should be able to direct withdraw all from the remote strategy direclty and collect to master", async function () { + it("Should be able to direct withdraw from the remote strategy directly and withdrawing More from Morpho when collecting to the master", async function () { + const { morphoVault } = fixture; + await mintToMasterDepositToRemote("1000"); + await assertVaultTotalValue("1000"); + + await expect(await morphoVault.totalAssets()).to.eq( + await units("1000", usdc) + ); + await directWithdrawFromRemoteStrategy("500"); + await assertVaultTotalValue("1000"); + + // 500 has been withdrawn from the Morpho vault but still remains on the + // remote strategy + await expect( + await crossChainRemoteStrategy.checkBalance(usdc.address) + ).to.eq(await units("1000", usdc)); + + // Next withdraw should withdraw 50 additional funds and send them with existing + // 500 USDC to the master. + await withdrawFromRemoteToVault("550", false); + + await assertVaultTotalValue("1000"); + // The remote strategy should have 500 USDC in Morpho vault and 50 USDC on the contract + await expect( + await crossChainRemoteStrategy.checkBalance(usdc.address) + ).to.eq(await units("450", usdc)); + await expect(await usdc.balanceOf(crossChainRemoteStrategy.address)).to.eq( + await units("0", usdc) + ); + }); + + it("Should fail when a withdrawal too large is requested", async function () { + const { morphoVault } = fixture; + await mintToMasterDepositToRemote("1000"); + await assertVaultTotalValue("1000"); + + await expect(await morphoVault.totalAssets()).to.eq( + await units("1000", usdc) + ); + + // Master strategy should prevent withdrawing more than is available in the remote strategy + await expect(withdrawFromRemoteStrategy("1001")).to.be.revertedWith( + "Withdraw amount exceeds remote strategy balance" + ); + + await assertVaultTotalValue("1000"); + }); + + it("Should be able to direct withdraw all from the remote strategy directly and collect to master", async function () { const { morphoVault, messageTransmitter } = fixture; await mintToMasterDepositToRemote("1000"); - await assetVaultTotalValue("1000"); + await assertVaultTotalValue("1000"); await expect(await morphoVault.totalAssets()).to.eq( await units("1000", usdc) ); await directWithdrawAllFromRemoteStrategy(); - await assetVaultTotalValue("1000"); + await assertVaultTotalValue("1000"); // All has been withdrawn from the Morpho vault but still remains on the // remote strategy @@ -257,21 +308,24 @@ describe("ForkTest: CrossChainRemoteStrategy", function () { await crossChainRemoteStrategy.checkBalance(usdc.address) ).to.eq(await units("1000", usdc)); - // The remote strategy doesn't have anything in the Morpho vault anymore. This - // withdrawal will thus fail on the vault, but the transactoin receiving all the - // funds should still succeed. - await withdrawFromRemoteStrategy("10"); - await expect(messageTransmitter.processFront()).to.emit( + await withdrawFromRemoteStrategy("1000"); + await expect(messageTransmitter.processFront()).not.to.emit( crossChainRemoteStrategy, - "WithdrawFailed" + "WithdrawUnderlyingFailed" ); await expect(messageTransmitter.processFront()) .to.emit(crossChainMasterStrategy, "RemoteStrategyBalanceUpdated") .withArgs(await units("0", usdc)); - await assetVaultTotalValue("1000"); + await assertVaultTotalValue("1000"); await expect( await crossChainRemoteStrategy.checkBalance(usdc.address) ).to.eq(await units("0", usdc)); }); + + it("Should be able to process withdrawal & checkBalance on Remote strategy and in reverse order on master strategy", async function () {}); + + it("Should fail when a withdrawal too large is requested on the remote strategy", async function () { + // TODO: trick master into thinking there is more on remote strategy than is actually there + }); }); From 812d78b0f0b9dbce4ecd352049aa1cd1157db76f Mon Sep 17 00:00:00 2001 From: Shah <10547529+shahthepro@users.noreply.github.com> Date: Tue, 6 Jan 2026 18:26:10 +0400 Subject: [PATCH 062/101] Address Code Review comments (#2732) * Address Code Review comments * Fix initialize method * Fix initialize method * fix remote strat initialize method --- .../crosschain/AbstractCCTPIntegrator.sol | 71 ++++++++++++------- .../crosschain/CrossChainMasterStrategy.sol | 33 +++++---- .../crosschain/CrossChainRemoteStrategy.sol | 13 ++-- contracts/contracts/utils/BytesHelper.sol | 35 +++++++++ contracts/deploy/deployActions.js | 4 +- .../deploy/mainnet/999_fork_test_setup.js | 2 +- 6 files changed, 112 insertions(+), 46 deletions(-) diff --git a/contracts/contracts/strategies/crosschain/AbstractCCTPIntegrator.sol b/contracts/contracts/strategies/crosschain/AbstractCCTPIntegrator.sol index 1fc6e0d116..b2053e9696 100644 --- a/contracts/contracts/strategies/crosschain/AbstractCCTPIntegrator.sol +++ b/contracts/contracts/strategies/crosschain/AbstractCCTPIntegrator.sol @@ -41,48 +41,59 @@ abstract contract AbstractCCTPIntegrator is Governable, IMessageHandlerV2 { using BytesHelper for bytes; - event CCTPMinFinalityThresholdSet(uint32 minFinalityThreshold); - event CCTPFeePremiumBpsSet(uint32 feePremiumBps); + event CCTPMinFinalityThresholdSet(uint16 minFinalityThreshold); + event CCTPFeePremiumBpsSet(uint16 feePremiumBps); event OperatorChanged(address operator); + /** + * @notice Max trasnfer threshold imposed by the CCTP + * Ref: https://developers.circle.com/cctp/evm-smart-contracts#depositforburn + */ + uint256 public constant MAX_TRANSFER_AMOUNT = 10_000_000 * 10**6; // 10M USDC + // CCTP contracts // This implementation assumes that remote and local chains have these contracts // deployed on the same addresses. + /// @notice CCTP message transmitter contract ICCTPMessageTransmitter public immutable cctpMessageTransmitter; + /// @notice CCTP token messenger contract ICCTPTokenMessenger public immutable cctpTokenMessenger; - // USDC address on local chain + /// @notice USDC address on local chain address public immutable baseToken; - // Domain ID of the chain from which messages are accepted + /// @notice Domain ID of the chain from which messages are accepted uint32 public immutable peerDomainID; - // Strategy address on other chain + /// @notice Strategy address on other chain address public immutable peerStrategy; - // CCTP params - uint32 public minFinalityThreshold; - uint32 public feePremiumBps; + /** + * @notice Minimum finality threshold + * Can be 1000 (safe, after 1 epoch) or 2000 (finalized, after 2 epochs). + * Ref: https://developers.circle.com/cctp/technical-guide#finality-thresholds + */ + uint16 public minFinalityThreshold; - // Threshold imposed by the CCTP - uint256 public constant MAX_TRANSFER_AMOUNT = 10_000_000 * 10**6; // 10M USDC + /// @notice Fee premium in basis points + uint16 public feePremiumBps; - // Nonce of the last known deposit or withdrawal + /// @notice Nonce of the last known deposit or withdrawal uint64 public lastTransferNonce; - // Mapping of processed nonces - mapping(uint64 => bool) private nonceProcessed; - - // Operator address: Can relay CCTP messages + /// @notice Operator address: Can relay CCTP messages address public operator; + /// @notice Mapping of processed nonces + mapping(uint64 => bool) private nonceProcessed; + // For future use uint256[50] private __gap; modifier onlyCCTPMessageTransmitter() { require( msg.sender == address(cctpMessageTransmitter), - "Caller is not the CCTP message transmitter" + "Caller is not CCTP transmitter" ); _; } @@ -92,6 +103,16 @@ abstract contract AbstractCCTPIntegrator is Governable, IMessageHandlerV2 { _; } + /** + * @notice Configuration for CCTP integration + * @param cctpTokenMessenger Address of the CCTP token messenger contract + * @param cctpMessageTransmitter Address of the CCTP message transmitter contract + * @param peerDomainID Domain ID of the chain from which messages are accepted. + * 0 for Ethereum, 6 for Base, etc. + * Ref: https://developers.circle.com/cctp/cctp-supported-blockchains + * @param peerStrategy Address of the master or remote strategy on the other chain + * @param baseToken USDC address on local chain + */ struct CCTPIntegrationConfig { address cctpTokenMessenger; address cctpMessageTransmitter; @@ -135,8 +156,8 @@ abstract contract AbstractCCTPIntegrator is Governable, IMessageHandlerV2 { */ function _initialize( address _operator, - uint32 _minFinalityThreshold, - uint32 _feePremiumBps + uint16 _minFinalityThreshold, + uint16 _feePremiumBps ) internal { _setOperator(_operator); _setMinFinalityThreshold(_minFinalityThreshold); @@ -170,7 +191,7 @@ abstract contract AbstractCCTPIntegrator is Governable, IMessageHandlerV2 { * 2000 (Finalized, after 2 epochs). * @param _minFinalityThreshold Minimum finality threshold */ - function setMinFinalityThreshold(uint32 _minFinalityThreshold) + function setMinFinalityThreshold(uint16 _minFinalityThreshold) external onlyGovernor { @@ -181,7 +202,7 @@ abstract contract AbstractCCTPIntegrator is Governable, IMessageHandlerV2 { * @dev Set the minimum finality threshold * @param _minFinalityThreshold Minimum finality threshold */ - function _setMinFinalityThreshold(uint32 _minFinalityThreshold) internal { + function _setMinFinalityThreshold(uint16 _minFinalityThreshold) internal { // 1000 for fast transfer and 2000 for standard transfer require( _minFinalityThreshold == 1000 || _minFinalityThreshold == 2000, @@ -197,15 +218,17 @@ abstract contract AbstractCCTPIntegrator is Governable, IMessageHandlerV2 { * Cannot be higher than 30% (3000 basis points). * @param _feePremiumBps Fee premium in basis points */ - function setFeePremiumBps(uint32 _feePremiumBps) external onlyGovernor { + function setFeePremiumBps(uint16 _feePremiumBps) external onlyGovernor { _setFeePremiumBps(_feePremiumBps); } /** * @dev Set the fee premium in basis points + * Cannot be higher than 30% (3000 basis points). + * Ref: https://developers.circle.com/cctp/technical-guide#fees * @param _feePremiumBps Fee premium in basis points */ - function _setFeePremiumBps(uint32 _feePremiumBps) internal { + function _setFeePremiumBps(uint16 _feePremiumBps) internal { require(_feePremiumBps <= 3000, "Fee premium too high"); // 30% feePremiumBps = _feePremiumBps; @@ -326,7 +349,7 @@ abstract contract AbstractCCTPIntegrator is Governable, IMessageHandlerV2 { address(baseToken), bytes32(uint256(uint160(peerStrategy))), maxFee, - minFinalityThreshold, + uint32(minFinalityThreshold), hookData ); } @@ -340,7 +363,7 @@ abstract contract AbstractCCTPIntegrator is Governable, IMessageHandlerV2 { peerDomainID, bytes32(uint256(uint160(peerStrategy))), bytes32(uint256(uint160(peerStrategy))), - minFinalityThreshold, + uint32(minFinalityThreshold), message ); } diff --git a/contracts/contracts/strategies/crosschain/CrossChainMasterStrategy.sol b/contracts/contracts/strategies/crosschain/CrossChainMasterStrategy.sol index 4fbca98498..5543ab753a 100644 --- a/contracts/contracts/strategies/crosschain/CrossChainMasterStrategy.sol +++ b/contracts/contracts/strategies/crosschain/CrossChainMasterStrategy.sol @@ -21,10 +21,10 @@ contract CrossChainMasterStrategy is using SafeERC20 for IERC20; using CrossChainStrategyHelper for bytes; - // Remote strategy balance + /// @notice Remote strategy balance uint256 public remoteStrategyBalance; - // Amount that's bridged but not yet received on the destination chain + /// @notice Amount that's bridged but not yet received on the destination chain uint256 public pendingAmount; enum TransferType { @@ -32,7 +32,7 @@ contract CrossChainMasterStrategy is Deposit, Withdrawal } - // Mapping of nonce to transfer type + /// @notice Mapping of nonce to transfer type mapping(uint64 => TransferType) public transferTypeByNonce; event RemoteStrategyBalanceUpdated(uint256 balance); @@ -57,8 +57,8 @@ contract CrossChainMasterStrategy is */ function initialize( address _operator, - uint32 _minFinalityThreshold, - uint32 _feePremiumBps + uint16 _minFinalityThreshold, + uint16 _feePremiumBps ) external virtual onlyGovernor initializer { _initialize(_operator, _minFinalityThreshold, _feePremiumBps); @@ -120,8 +120,10 @@ contract CrossChainMasterStrategy is // USDC balance on this contract // + USDC being bridged // + USDC cached in the corresponding Remote part of this contract - uint256 undepositedUSDC = IERC20(baseToken).balanceOf(address(this)); - return undepositedUSDC + pendingAmount + remoteStrategyBalance; + return + IERC20(baseToken).balanceOf(address(this)) + + pendingAmount + + remoteStrategyBalance; } /// @inheritdoc InitializableAbstractStrategy @@ -150,13 +152,16 @@ contract CrossChainMasterStrategy is /// @inheritdoc AbstractCCTPIntegrator function _onMessageReceived(bytes memory payload) internal override { - uint32 messageType = payload.getMessageType(); - if (messageType == CrossChainStrategyHelper.BALANCE_CHECK_MESSAGE) { + if ( + payload.getMessageType() == + CrossChainStrategyHelper.BALANCE_CHECK_MESSAGE + ) { // Received when Remote strategy checks the balance _processBalanceCheckMessage(payload); - } else { - revert("Unknown message type"); + return; } + + revert("Unknown message type"); } /// @inheritdoc AbstractCCTPIntegrator @@ -208,7 +213,6 @@ contract CrossChainMasterStrategy is */ function _deposit(address _asset, uint256 depositAmount) internal virtual { require(_asset == baseToken, "Unsupported asset"); - require(!isTransferPending(), "Transfer already pending"); require(pendingAmount == 0, "Unexpected pending amount"); require(depositAmount > 0, "Deposit amount must be greater than 0"); require( @@ -217,6 +221,7 @@ contract CrossChainMasterStrategy is ); // Get the next nonce + // Note: reverts if a transfer is pending uint64 nonce = _getNextNonce(); transferTypeByNonce[nonce] = TransferType.Deposit; @@ -250,7 +255,6 @@ 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 <= remoteStrategyBalance, "Withdraw amount exceeds remote strategy balance" @@ -261,6 +265,7 @@ contract CrossChainMasterStrategy is ); // Get the next nonce + // Note: reverts if a transfer is pending uint64 nonce = _getNextNonce(); transferTypeByNonce[nonce] = TransferType.Withdrawal; @@ -322,7 +327,7 @@ contract CrossChainMasterStrategy is _markNonceAsProcessed(nonce); // Effect of confirming a deposit, reset pending amount - pendingAmount = 0; + delete pendingAmount; // NOTE: Withdrawal is taken care of by _onTokenReceived } diff --git a/contracts/contracts/strategies/crosschain/CrossChainRemoteStrategy.sol b/contracts/contracts/strategies/crosschain/CrossChainRemoteStrategy.sol index a1babf1829..0aab6bd349 100644 --- a/contracts/contracts/strategies/crosschain/CrossChainRemoteStrategy.sol +++ b/contracts/contracts/strategies/crosschain/CrossChainRemoteStrategy.sol @@ -71,8 +71,8 @@ contract CrossChainRemoteStrategy is function initialize( address _strategist, address _operator, - uint32 _minFinalityThreshold, - uint32 _feePremiumBps + uint16 _minFinalityThreshold, + uint16 _feePremiumBps ) external virtual onlyGovernor initializer { _initialize(_operator, _minFinalityThreshold, _feePremiumBps); _setStrategistAddr(_strategist); @@ -137,9 +137,12 @@ contract CrossChainRemoteStrategy is /// @inheritdoc Generalized4626Strategy function withdrawAll() external virtual override onlyGovernorOrStrategist { - uint256 contractBalance = IERC20(baseToken).balanceOf(address(this)); - uint256 balance = checkBalance(baseToken) - contractBalance; - _withdraw(address(this), baseToken, balance); + IERC4626 platform = IERC4626(platformAddress); + _withdraw( + address(this), + baseToken, + platform.previewRedeem(platform.balanceOf(address(this))) + ); } /// @inheritdoc AbstractCCTPIntegrator diff --git a/contracts/contracts/utils/BytesHelper.sol b/contracts/contracts/utils/BytesHelper.sol index 84dce7a6d9..75a0fa1875 100644 --- a/contracts/contracts/utils/BytesHelper.sol +++ b/contracts/contracts/utils/BytesHelper.sol @@ -4,6 +4,7 @@ pragma solidity ^0.8.0; uint256 constant UINT32_LENGTH = 4; uint256 constant UINT64_LENGTH = 8; uint256 constant UINT256_LENGTH = 32; +// Address is 20 bytes, but we expect the data to be padded with 0s to 32 bytes uint256 constant ADDRESS_LENGTH = 32; library BytesHelper { @@ -33,11 +34,22 @@ library BytesHelper { return result; } + /** + * @dev Decode a uint32 from a bytes memory + * @param data The bytes memory to decode + * @return uint32 The decoded uint32 + */ function decodeUint32(bytes memory data) internal pure returns (uint32) { require(data.length == 4, "Invalid data length"); return uint32(uint256(bytes32(data)) >> 224); } + /** + * @dev Extract a uint32 from a bytes memory + * @param data The bytes memory to extract from + * @param start The start index (inclusive) + * @return uint32 The extracted uint32 + */ function extractUint32(bytes memory data, uint256 start) internal pure @@ -46,12 +58,24 @@ library BytesHelper { return decodeUint32(extractSlice(data, start, start + UINT32_LENGTH)); } + /** + * @dev Decode an address from a bytes memory. + * Expects the data to be padded with 0s to 32 bytes. + * @param data The bytes memory to decode + * @return address The decoded address + */ function decodeAddress(bytes memory data) internal pure returns (address) { // We expect the data to be padded with 0s, so length is 32 not 20 require(data.length == 32, "Invalid data length"); return abi.decode(data, (address)); } + /** + * @dev Extract an address from a bytes memory + * @param data The bytes memory to extract from + * @param start The start index (inclusive) + * @return address The extracted address + */ function extractAddress(bytes memory data, uint256 start) internal pure @@ -60,11 +84,22 @@ library BytesHelper { return decodeAddress(extractSlice(data, start, start + ADDRESS_LENGTH)); } + /** + * @dev Decode a uint256 from a bytes memory + * @param data The bytes memory to decode + * @return uint256 The decoded uint256 + */ function decodeUint256(bytes memory data) internal pure returns (uint256) { require(data.length == 32, "Invalid data length"); return abi.decode(data, (uint256)); } + /** + * @dev Extract a uint256 from a bytes memory + * @param data The bytes memory to extract from + * @param start The start index (inclusive) + * @return uint256 The extracted uint256 + */ function extractUint256(bytes memory data, uint256 start) internal pure diff --git a/contracts/deploy/deployActions.js b/contracts/deploy/deployActions.js index 711bb7e523..8387848452 100644 --- a/contracts/deploy/deployActions.js +++ b/contracts/deploy/deployActions.js @@ -1868,7 +1868,7 @@ const deployCrossChainMasterStrategyImpl = async ( if (!skipInitialize) { const initData = dCrossChainMasterStrategy.interface.encodeFunctionData( - "initialize(address,uint32,uint32)", + "initialize(address,uint16,uint16)", [multichainStrategistAddr, 2000, 0] ); @@ -1928,7 +1928,7 @@ const deployCrossChainRemoteStrategyImpl = async ( ); const initData = dCrossChainRemoteStrategy.interface.encodeFunctionData( - "initialize(address,address,uint32,uint32)", + "initialize(address,address,uint16,uint16)", [multichainStrategistAddr, multichainStrategistAddr, 2000, 0] ); diff --git a/contracts/deploy/mainnet/999_fork_test_setup.js b/contracts/deploy/mainnet/999_fork_test_setup.js index ac8abcaee0..63ac242876 100644 --- a/contracts/deploy/mainnet/999_fork_test_setup.js +++ b/contracts/deploy/mainnet/999_fork_test_setup.js @@ -108,6 +108,6 @@ const main = async (hre) => { }; main.id = "999_no_stale_oracles"; -main.skip = () => isForkWithLocalNode || !isFork; +main.skip = () => true || isForkWithLocalNode || !isFork; module.exports = main; From bfa9b2cc88321c5ee7581837eb8e4e7d25d7105a Mon Sep 17 00:00:00 2001 From: Shahul Hameed <10547529+shahthepro@users.noreply.github.com> Date: Tue, 6 Jan 2026 18:27:09 +0400 Subject: [PATCH 063/101] Revert 999 --- contracts/deploy/mainnet/999_fork_test_setup.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/deploy/mainnet/999_fork_test_setup.js b/contracts/deploy/mainnet/999_fork_test_setup.js index 63ac242876..ac8abcaee0 100644 --- a/contracts/deploy/mainnet/999_fork_test_setup.js +++ b/contracts/deploy/mainnet/999_fork_test_setup.js @@ -108,6 +108,6 @@ const main = async (hre) => { }; main.id = "999_no_stale_oracles"; -main.skip = () => true || isForkWithLocalNode || !isFork; +main.skip = () => isForkWithLocalNode || !isFork; module.exports = main; From 72f03376ffd602b42622406fd32ac485b354e6bf Mon Sep 17 00:00:00 2001 From: Shahul Hameed <10547529+shahthepro@users.noreply.github.com> Date: Wed, 7 Jan 2026 11:48:24 +0400 Subject: [PATCH 064/101] fix comments --- .../crosschain/CrossChainMasterStrategy.sol | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/contracts/contracts/strategies/crosschain/CrossChainMasterStrategy.sol b/contracts/contracts/strategies/crosschain/CrossChainMasterStrategy.sol index 5543ab753a..fc696274c6 100644 --- a/contracts/contracts/strategies/crosschain/CrossChainMasterStrategy.sol +++ b/contracts/contracts/strategies/crosschain/CrossChainMasterStrategy.sol @@ -108,7 +108,14 @@ contract CrossChainMasterStrategy is _withdraw(baseToken, vaultAddress, remoteStrategyBalance); } - /// @inheritdoc InitializableAbstractStrategy + /** + * @notice Check the balance of the strategy that includes + * the balance of the asset on this contract, + * the amount of the asset being bridged, + * and the balance reported by the Remote strategy. + * @param _asset Address of the asset to check + * @return balance Total balance of the asset + */ function checkBalance(address _asset) public view @@ -214,10 +221,10 @@ contract CrossChainMasterStrategy is function _deposit(address _asset, uint256 depositAmount) internal virtual { require(_asset == baseToken, "Unsupported asset"); require(pendingAmount == 0, "Unexpected pending amount"); - require(depositAmount > 0, "Deposit amount must be greater than 0"); + require(depositAmount > 0, "Must deposit somethin"); require( depositAmount <= MAX_TRANSFER_AMOUNT, - "Deposit amount exceeds max transfer amount" + "Deposit amount too high" ); // Get the next nonce From a17cd0a4b09baaf8e222c4a56c6a75822075f266 Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Wed, 7 Jan 2026 11:25:03 +0100 Subject: [PATCH 065/101] add a test that uncovers a withdrawal error (#2733) --- .../crosschain/CrossChainMasterStrategy.sol | 60 ++++---- .../crosschain/CrossChainRemoteStrategy.sol | 13 +- .../crosschain/CrossChainStrategyHelper.sol | 28 ++-- .../crosschain/cross-chain-strategy.js | 129 +++++++++++++++++- 4 files changed, 175 insertions(+), 55 deletions(-) diff --git a/contracts/contracts/strategies/crosschain/CrossChainMasterStrategy.sol b/contracts/contracts/strategies/crosschain/CrossChainMasterStrategy.sol index fc696274c6..d5f6be7392 100644 --- a/contracts/contracts/strategies/crosschain/CrossChainMasterStrategy.sol +++ b/contracts/contracts/strategies/crosschain/CrossChainMasterStrategy.sol @@ -32,8 +32,6 @@ contract CrossChainMasterStrategy is Deposit, Withdrawal } - /// @notice Mapping of nonce to transfer type - mapping(uint64 => TransferType) public transferTypeByNonce; event RemoteStrategyBalanceUpdated(uint256 balance); event WithdrawRequested(address indexed asset, uint256 amount); @@ -182,14 +180,6 @@ contract CrossChainMasterStrategy is // Should be expecting an acknowledgement require(!isNonceProcessed(_nonce), "Nonce already processed"); - // Only a withdrawal can send tokens to Master strategy - require( - transferTypeByNonce[_nonce] == TransferType.Withdrawal, - "Expecting withdrawal" - ); - - // Confirm receipt of tokens from Withdraw command - _markNonceAsProcessed(_nonce); // Now relay to the regular flow // NOTE: Calling _onMessageReceived would mean that we are bypassing a @@ -230,7 +220,6 @@ contract CrossChainMasterStrategy is // Get the next nonce // Note: reverts if a transfer is pending uint64 nonce = _getNextNonce(); - transferTypeByNonce[nonce] = TransferType.Deposit; // Set pending amount pendingAmount = depositAmount; @@ -274,7 +263,6 @@ contract CrossChainMasterStrategy is // Get the next nonce // Note: reverts if a transfer is pending uint64 nonce = _getNextNonce(); - transferTypeByNonce[nonce] = TransferType.Withdrawal; // Build and send withdrawal message with payload bytes memory message = CrossChainStrategyHelper.encodeWithdrawMessage( @@ -300,8 +288,10 @@ contract CrossChainMasterStrategy is virtual { // Decode the message - (uint64 nonce, uint256 balance) = message.decodeBalanceCheckMessage(); - + // When transferConfirmation is true, it means that the message is a result of a deposit or a withdrawal + // process. + (uint64 nonce, uint256 balance, bool transferConfirmation) = message + .decodeBalanceCheckMessage(); // Get the last cached nonce uint64 _lastCachedNonce = lastTransferNonce; @@ -311,32 +301,28 @@ contract CrossChainMasterStrategy is return; } - // Check if the nonce has been processed - bool processedTransfer = isNonceProcessed(nonce); - if ( - !processedTransfer && - transferTypeByNonce[nonce] == TransferType.Withdrawal - ) { - // Pending withdrawal is taken care of by _onTokenReceived - // Do not update balance due to race conditions - return; + // A received message nonce not yet processed indicates there is a + // deposit or withdrawal in progress. + bool transferInProgress = !isNonceProcessed(nonce); + + if (transferInProgress) { + if (transferConfirmation) { + // Apply the effects of the deposit / withdrawal completion + _markNonceAsProcessed(nonce); + pendingAmount = 0; + } else { + // A balanceCheck arrived that is not part of the deposit / withdrawal process + // that has been generated on the Remote contract after the deposit / withdrawal which is + // still pending. This can happen when the CCTP bridge delivers the messages out of order. + // Ignore it, since the pending deposit / withdrawal must first be cofirmed. + return; + } } - // Update the remote strategy balance always + // At this point update the strategy balance the balanceCheck message is either: + // - a confirmation of a deposit / withdrawal + // - a message that updates balances when no deposit / withdrawal is in progress remoteStrategyBalance = balance; emit RemoteStrategyBalanceUpdated(balance); - - /** - * A deposit is being confirmed. - * A withdrawal will always be confirmed if it reaches this point of code. - */ - if (!processedTransfer) { - _markNonceAsProcessed(nonce); - - // Effect of confirming a deposit, reset pending amount - delete pendingAmount; - - // NOTE: Withdrawal is taken care of by _onTokenReceived - } } } diff --git a/contracts/contracts/strategies/crosschain/CrossChainRemoteStrategy.sol b/contracts/contracts/strategies/crosschain/CrossChainRemoteStrategy.sol index 0aab6bd349..3070797b37 100644 --- a/contracts/contracts/strategies/crosschain/CrossChainRemoteStrategy.sol +++ b/contracts/contracts/strategies/crosschain/CrossChainRemoteStrategy.sol @@ -189,7 +189,7 @@ contract CrossChainRemoteStrategy is // Send balance check message to the peer strategy uint256 balanceAfter = checkBalance(baseToken); bytes memory message = CrossChainStrategyHelper - .encodeBalanceCheckMessage(lastTransferNonce, balanceAfter); + .encodeBalanceCheckMessage(lastTransferNonce, balanceAfter, true); _sendMessage(message); } @@ -261,7 +261,8 @@ contract CrossChainRemoteStrategy is bytes memory message = CrossChainStrategyHelper .encodeBalanceCheckMessage( lastTransferNonce, - balanceAfter - withdrawAmount + balanceAfter - withdrawAmount, + true ); _sendTokens(withdrawAmount, message); } else { @@ -270,7 +271,11 @@ contract CrossChainRemoteStrategy is // - doesn't have sufficient funds to satisfy the withdrawal request // In both cases send the balance update message to the peer strategy. bytes memory message = CrossChainStrategyHelper - .encodeBalanceCheckMessage(lastTransferNonce, balanceAfter); + .encodeBalanceCheckMessage( + lastTransferNonce, + balanceAfter, + true + ); _sendMessage(message); emit WithdrawFailed(withdrawAmount, usdcBalance); } @@ -349,7 +354,7 @@ contract CrossChainRemoteStrategy is { uint256 balance = checkBalance(baseToken); bytes memory message = CrossChainStrategyHelper - .encodeBalanceCheckMessage(lastTransferNonce, balance); + .encodeBalanceCheckMessage(lastTransferNonce, balance, false); _sendMessage(message); } diff --git a/contracts/contracts/strategies/crosschain/CrossChainStrategyHelper.sol b/contracts/contracts/strategies/crosschain/CrossChainStrategyHelper.sol index 8aee9e8f55..44cf12e1d3 100644 --- a/contracts/contracts/strategies/crosschain/CrossChainStrategyHelper.sol +++ b/contracts/contracts/strategies/crosschain/CrossChainStrategyHelper.sol @@ -174,18 +174,20 @@ library CrossChainStrategyHelper { * The message version and type are always encoded in the message. * @param nonce The nonce of the balance check * @param balance The balance to check + * @param transferConfirmation Indicates if the message is a transfer confirmation. This is true + * when the message is a result of a deposit or a withdrawal. * @return The encoded balance check message */ - function encodeBalanceCheckMessage(uint64 nonce, uint256 balance) - internal - pure - returns (bytes memory) - { + function encodeBalanceCheckMessage( + uint64 nonce, + uint256 balance, + bool transferConfirmation + ) internal pure returns (bytes memory) { return abi.encodePacked( ORIGIN_MESSAGE_VERSION, BALANCE_CHECK_MESSAGE, - abi.encode(nonce, balance) + abi.encode(nonce, balance, transferConfirmation) ); } @@ -193,19 +195,23 @@ library CrossChainStrategyHelper { * @dev Decode the balance check message. * The message version and type are verified in the message. * @param message The message to decode - * @return The nonce and the balance to check + * @return The nonce, the balance and indicates if the message is a transfer confirmation */ function decodeBalanceCheckMessage(bytes memory message) internal pure - returns (uint64, uint256) + returns ( + uint64, + uint256, + bool + ) { verifyMessageVersionAndType(message, BALANCE_CHECK_MESSAGE); - (uint64 nonce, uint256 balance) = abi.decode( + (uint64 nonce, uint256 balance, bool transferConfirmation) = abi.decode( getMessagePayload(message), - (uint64, uint256) + (uint64, uint256, bool) ); - return (nonce, balance); + return (nonce, balance, transferConfirmation); } } diff --git a/contracts/test/strategies/crosschain/cross-chain-strategy.js b/contracts/test/strategies/crosschain/cross-chain-strategy.js index 93af766b82..62de14f88c 100644 --- a/contracts/test/strategies/crosschain/cross-chain-strategy.js +++ b/contracts/test/strategies/crosschain/cross-chain-strategy.js @@ -5,6 +5,7 @@ const { crossChainFixtureUnit, } = require("../../_fixture"); const { units } = require("../../helpers"); +const { impersonateAndFund } = require("../../../utils/signers"); const loadFixture = createFixtureLoader(crossChainFixtureUnit); @@ -75,6 +76,10 @@ describe("ForkTest: CrossChainRemoteStrategy", function () { await crossChainRemoteStrategy.connect(governor).withdrawAll(); }; + const sendBalanceUpdateToMaster = async () => { + await crossChainRemoteStrategy.connect(governor).sendBalanceUpdate(); + }; + // Checks the diff in the total expected value in the vault // (plus accompanying strategy value) const assertVaultTotalValue = async (amountExpected) => { @@ -177,6 +182,7 @@ describe("ForkTest: CrossChainRemoteStrategy", function () { await expect( await crossChainRemoteStrategy.checkBalance(usdc.address) ).to.eq(remoteBalanceAfter); + // Simulate off chain component processing checkBalance message await expect(messageTransmitter.processFront()) .to.emit(crossChainMasterStrategy, "RemoteStrategyBalanceUpdated") @@ -323,9 +329,126 @@ describe("ForkTest: CrossChainRemoteStrategy", function () { ).to.eq(await units("0", usdc)); }); - it("Should be able to process withdrawal & checkBalance on Remote strategy and in reverse order on master strategy", async function () {}); - it("Should fail when a withdrawal too large is requested on the remote strategy", async function () { - // TODO: trick master into thinking there is more on remote strategy than is actually there + const { messageTransmitter } = fixture; + const remoteStrategySigner = await impersonateAndFund( + crossChainRemoteStrategy.address + ); + + await mintToMasterDepositToRemote("1000"); + await assertVaultTotalValue("1000"); + + await directWithdrawFromRemoteStrategy("10"); + + // Trick the remote strategy into thinking it has 10 USDC more than it actually does + await usdc + .connect(remoteStrategySigner) + .transfer(vault.address, await units("10", usdc)); + // Vault has 10 USDC more & Master strategy still thinks it has 1000 USDC + await assertVaultTotalValue("1010"); + + // This step should fail because the remote strategy no longer holds 1000 USDC + await withdrawFromRemoteStrategy("1000"); + + // Process on remote strategy + await expect(messageTransmitter.processFront()) + .to.emit(crossChainRemoteStrategy, "WithdrawFailed") + .withArgs(await units("1000", usdc), await units("0", usdc)); + + // Process on master strategy + // This event doesn't get triggerred as the master strategy considers the balance check update + // as a race condition, and is exoecting an "on TokenReceived " to be called instead + + // which also causes the master strategy not to update the balance of the remote strategy + await expect(messageTransmitter.processFront()) + .to.emit(crossChainMasterStrategy, "RemoteStrategyBalanceUpdated") + .withArgs(await units("990", usdc)); + + await expect(await messageTransmitter.messagesInQueue()).to.eq(0); + + await expect( + await crossChainRemoteStrategy.checkBalance(usdc.address) + ).to.eq(await units("990", usdc)); + + await expect( + await crossChainMasterStrategy.checkBalance(usdc.address) + ).to.eq(await units("990", usdc)); + }); + + it("Should be able to process withdrawal & checkBalance on Remote strategy and in reverse order on master strategy", async function () { + const { messageTransmitter } = fixture; + + await mintToMasterDepositToRemote("1000"); + + await withdrawFromRemoteStrategy("300"); + + // Process on remote strategy + await expect(messageTransmitter.processFront()); + // This sends a second balanceUpdate message to the CCTP bridge + await sendBalanceUpdateToMaster(); + + await expect(await messageTransmitter.messagesInQueue()).to.eq(2); + + // first process the standalone balanceCheck message - meaning we process messages out of order + // this message should be ignored on Master + await expect(messageTransmitter.processBack()).to.not.emit( + crossChainMasterStrategy, + "RemoteStrategyBalanceUpdated" + ); + + // Second balance update message is part of the deposit / withdrawal process and should be processed + await expect(messageTransmitter.processFront()) + .to.emit(crossChainMasterStrategy, "RemoteStrategyBalanceUpdated") + .withArgs(await units("700", usdc)); + + await expect(await messageTransmitter.messagesInQueue()).to.eq(0); + + await expect( + await crossChainRemoteStrategy.checkBalance(usdc.address) + ).to.eq(await units("700", usdc)); + + await expect( + await crossChainMasterStrategy.checkBalance(usdc.address) + ).to.eq(await units("700", usdc)); + + await assertVaultTotalValue("1000"); + }); + + it("Should fail on deposit if a previous one has not completed", async function () { + await mint("100"); + await depositToMasterStrategy("50"); + + await expect(depositToMasterStrategy("50")).to.be.revertedWith( + "Unexpected pending amount" + ); + }); + + it("Should fail to withdraw if a previous deposit has not completed", async function () { + await mintToMasterDepositToRemote("40"); + await mint("50"); + await depositToMasterStrategy("50"); + + await expect(withdrawFromRemoteStrategy("40")).to.be.revertedWith( + "Pending deposit or withdrawal" + ); + }); + + it("Should fail on deposit if a previous withdrawal has not completed", async function () { + await mintToMasterDepositToRemote("230"); + await withdrawFromRemoteStrategy("50"); + + await mint("30"); + await expect(depositToMasterStrategy("30")).to.be.revertedWith( + "Pending deposit or withdrawal" + ); + }); + + it("Should fail to withdraw if a previous withdrawal has not completed", async function () { + await mintToMasterDepositToRemote("230"); + await withdrawFromRemoteStrategy("50"); + + await expect(withdrawFromRemoteStrategy("40")).to.be.revertedWith( + "Pending deposit or withdrawal" + ); }); }); From 8cf968a7fad75c87b738462013d23b1482d41ab8 Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Wed, 7 Jan 2026 11:28:29 +0100 Subject: [PATCH 066/101] remove transferType --- .../strategies/crosschain/CrossChainMasterStrategy.sol | 6 ------ 1 file changed, 6 deletions(-) diff --git a/contracts/contracts/strategies/crosschain/CrossChainMasterStrategy.sol b/contracts/contracts/strategies/crosschain/CrossChainMasterStrategy.sol index d5f6be7392..0cc09206da 100644 --- a/contracts/contracts/strategies/crosschain/CrossChainMasterStrategy.sol +++ b/contracts/contracts/strategies/crosschain/CrossChainMasterStrategy.sol @@ -27,12 +27,6 @@ contract CrossChainMasterStrategy is /// @notice Amount that's bridged but not yet received on the destination chain uint256 public pendingAmount; - enum TransferType { - None, // To avoid using 0 - Deposit, - Withdrawal - } - event RemoteStrategyBalanceUpdated(uint256 balance); event WithdrawRequested(address indexed asset, uint256 amount); From 59fdeeb7fa54104ca352a6293743be2c41de08ff Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Wed, 7 Jan 2026 11:57:29 +0100 Subject: [PATCH 067/101] correct spelling --- .../contracts/strategies/crosschain/AbstractCCTPIntegrator.sol | 2 +- .../strategies/crosschain/CrossChainMasterStrategy.sol | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/contracts/contracts/strategies/crosschain/AbstractCCTPIntegrator.sol b/contracts/contracts/strategies/crosschain/AbstractCCTPIntegrator.sol index b2053e9696..5c6cb43847 100644 --- a/contracts/contracts/strategies/crosschain/AbstractCCTPIntegrator.sol +++ b/contracts/contracts/strategies/crosschain/AbstractCCTPIntegrator.sol @@ -46,7 +46,7 @@ abstract contract AbstractCCTPIntegrator is Governable, IMessageHandlerV2 { event OperatorChanged(address operator); /** - * @notice Max trasnfer threshold imposed by the CCTP + * @notice Max transfer threshold imposed by the CCTP * Ref: https://developers.circle.com/cctp/evm-smart-contracts#depositforburn */ uint256 public constant MAX_TRANSFER_AMOUNT = 10_000_000 * 10**6; // 10M USDC diff --git a/contracts/contracts/strategies/crosschain/CrossChainMasterStrategy.sol b/contracts/contracts/strategies/crosschain/CrossChainMasterStrategy.sol index 0cc09206da..5abe77f2c6 100644 --- a/contracts/contracts/strategies/crosschain/CrossChainMasterStrategy.sol +++ b/contracts/contracts/strategies/crosschain/CrossChainMasterStrategy.sol @@ -24,7 +24,8 @@ contract CrossChainMasterStrategy is /// @notice Remote strategy balance uint256 public remoteStrategyBalance; - /// @notice Amount that's bridged but not yet received on the destination chain + /// @notice Amount that's bridged due to a pending Deposit process + /// but not yet received on the destination chain uint256 public pendingAmount; event RemoteStrategyBalanceUpdated(uint256 balance); From 8e06d553515909b8e7f36ce5184b29fba5082be7 Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Wed, 7 Jan 2026 12:03:40 +0100 Subject: [PATCH 068/101] rename baseToken to usdcToken --- .../crosschain/AbstractCCTPIntegrator.sol | 20 +++++------ .../crosschain/CrossChainMasterStrategy.sol | 24 ++++++------- .../crosschain/CrossChainRemoteStrategy.sol | 34 +++++++++---------- .../crosschain/crosschain-strategy.md | 8 ++--- 4 files changed, 43 insertions(+), 43 deletions(-) diff --git a/contracts/contracts/strategies/crosschain/AbstractCCTPIntegrator.sol b/contracts/contracts/strategies/crosschain/AbstractCCTPIntegrator.sol index 5c6cb43847..b6846f6990 100644 --- a/contracts/contracts/strategies/crosschain/AbstractCCTPIntegrator.sol +++ b/contracts/contracts/strategies/crosschain/AbstractCCTPIntegrator.sol @@ -60,7 +60,7 @@ abstract contract AbstractCCTPIntegrator is Governable, IMessageHandlerV2 { ICCTPTokenMessenger public immutable cctpTokenMessenger; /// @notice USDC address on local chain - address public immutable baseToken; + address public immutable usdcToken; /// @notice Domain ID of the chain from which messages are accepted uint32 public immutable peerDomainID; @@ -111,14 +111,14 @@ abstract contract AbstractCCTPIntegrator is Governable, IMessageHandlerV2 { * 0 for Ethereum, 6 for Base, etc. * Ref: https://developers.circle.com/cctp/cctp-supported-blockchains * @param peerStrategy Address of the master or remote strategy on the other chain - * @param baseToken USDC address on local chain + * @param usdcToken USDC address on local chain */ struct CCTPIntegrationConfig { address cctpTokenMessenger; address cctpMessageTransmitter; uint32 peerDomainID; address peerStrategy; - address baseToken; + address usdcToken; } constructor(CCTPIntegrationConfig memory _config) { @@ -135,14 +135,14 @@ abstract contract AbstractCCTPIntegrator is Governable, IMessageHandlerV2 { peerStrategy = _config.peerStrategy; // USDC address on local chain - baseToken = _config.baseToken; + usdcToken = _config.usdcToken; // Just a sanity check to ensure the base token is USDC - uint256 _baseTokenDecimals = Helpers.getDecimals(_config.baseToken); - string memory _baseTokenSymbol = Helpers.getSymbol(_config.baseToken); - require(_baseTokenDecimals == 6, "Base token decimals must be 6"); + uint256 _usdcTokenDecimals = Helpers.getDecimals(_config.usdcToken); + string memory _usdcTokenSymbol = Helpers.getSymbol(_config.usdcToken); + require(_usdcTokenDecimals == 6, "Base token decimals must be 6"); require( - keccak256(abi.encodePacked(_baseTokenSymbol)) == + keccak256(abi.encodePacked(_usdcTokenSymbol)) == keccak256(abi.encodePacked("USDC")), "Base token symbol must be USDC" ); @@ -327,7 +327,7 @@ abstract contract AbstractCCTPIntegrator is Governable, IMessageHandlerV2 { require(tokenAmount <= MAX_TRANSFER_AMOUNT, "Token amount too high"); // Approve only what needs to be transferred - IERC20(baseToken).safeApprove(address(cctpTokenMessenger), tokenAmount); + IERC20(usdcToken).safeApprove(address(cctpTokenMessenger), tokenAmount); // Compute the max fee to be paid. // Ref: https://developers.circle.com/cctp/evm-smart-contracts#getminfeeamount @@ -346,7 +346,7 @@ abstract contract AbstractCCTPIntegrator is Governable, IMessageHandlerV2 { tokenAmount, peerDomainID, bytes32(uint256(uint160(peerStrategy))), - address(baseToken), + address(usdcToken), bytes32(uint256(uint160(peerStrategy))), maxFee, uint32(minFinalityThreshold), diff --git a/contracts/contracts/strategies/crosschain/CrossChainMasterStrategy.sol b/contracts/contracts/strategies/crosschain/CrossChainMasterStrategy.sol index 5abe77f2c6..12967a0ae8 100644 --- a/contracts/contracts/strategies/crosschain/CrossChainMasterStrategy.sol +++ b/contracts/contracts/strategies/crosschain/CrossChainMasterStrategy.sol @@ -78,9 +78,9 @@ contract CrossChainMasterStrategy is /// @inheritdoc InitializableAbstractStrategy function depositAll() external override onlyVault nonReentrant { - uint256 balance = IERC20(baseToken).balanceOf(address(this)); + uint256 balance = IERC20(usdcToken).balanceOf(address(this)); if (balance > 0) { - _deposit(baseToken, balance); + _deposit(usdcToken, balance); } } @@ -98,7 +98,7 @@ contract CrossChainMasterStrategy is /// @inheritdoc InitializableAbstractStrategy function withdrawAll() external override onlyVaultOrGovernor nonReentrant { // Withdraw everything in Remote strategy - _withdraw(baseToken, vaultAddress, remoteStrategyBalance); + _withdraw(usdcToken, vaultAddress, remoteStrategyBalance); } /** @@ -115,20 +115,20 @@ contract CrossChainMasterStrategy is override returns (uint256 balance) { - require(_asset == baseToken, "Unsupported asset"); + require(_asset == usdcToken, "Unsupported asset"); // USDC balance on this contract // + USDC being bridged // + USDC cached in the corresponding Remote part of this contract return - IERC20(baseToken).balanceOf(address(this)) + + IERC20(usdcToken).balanceOf(address(this)) + pendingAmount + remoteStrategyBalance; } /// @inheritdoc InitializableAbstractStrategy function supportsAsset(address _asset) public view override returns (bool) { - return _asset == baseToken; + return _asset == usdcToken; } /// @inheritdoc InitializableAbstractStrategy @@ -188,14 +188,14 @@ contract CrossChainMasterStrategy is _onMessageReceived(payload); // Send any tokens in the contract to the Vault - uint256 usdcBalance = IERC20(baseToken).balanceOf(address(this)); + uint256 usdcBalance = IERC20(usdcToken).balanceOf(address(this)); // Should always have enough tokens require(usdcBalance >= tokenAmount, "Insufficient balance"); // Transfer all tokens to the Vault to not leave any dust - IERC20(baseToken).safeTransfer(vaultAddress, usdcBalance); + IERC20(usdcToken).safeTransfer(vaultAddress, usdcBalance); // Emit withdrawal amount - emit Withdrawal(baseToken, baseToken, usdcBalance); + emit Withdrawal(usdcToken, usdcToken, usdcBalance); } /** @@ -204,7 +204,7 @@ contract CrossChainMasterStrategy is * @param depositAmount Amount of the asset to deposit */ function _deposit(address _asset, uint256 depositAmount) internal virtual { - require(_asset == baseToken, "Unsupported asset"); + require(_asset == usdcToken, "Unsupported asset"); require(pendingAmount == 0, "Unexpected pending amount"); require(depositAmount > 0, "Must deposit somethin"); require( @@ -243,7 +243,7 @@ contract CrossChainMasterStrategy is address _recipient, uint256 _amount ) internal virtual { - require(_asset == baseToken, "Unsupported asset"); + require(_asset == usdcToken, "Unsupported asset"); require(_amount > 0, "Withdraw amount must be greater than 0"); require(_recipient == vaultAddress, "Only Vault can withdraw"); require( @@ -268,7 +268,7 @@ contract CrossChainMasterStrategy is // Emit WithdrawRequested event here, // Withdraw will be emitted in _onTokenReceived - emit WithdrawRequested(baseToken, _amount); + emit WithdrawRequested(usdcToken, _amount); } /** diff --git a/contracts/contracts/strategies/crosschain/CrossChainRemoteStrategy.sol b/contracts/contracts/strategies/crosschain/CrossChainRemoteStrategy.sol index 3070797b37..dafa026bea 100644 --- a/contracts/contracts/strategies/crosschain/CrossChainRemoteStrategy.sol +++ b/contracts/contracts/strategies/crosschain/CrossChainRemoteStrategy.sol @@ -53,9 +53,9 @@ contract CrossChainRemoteStrategy is CCTPIntegrationConfig memory _cctpConfig ) AbstractCCTPIntegrator(_cctpConfig) - Generalized4626Strategy(_baseConfig, _cctpConfig.baseToken) + Generalized4626Strategy(_baseConfig, _cctpConfig.usdcToken) { - require(baseToken == address(assetToken), "Token mismatch"); + require(usdcToken == address(assetToken), "Token mismatch"); // NOTE: Vault address must always be the proxy address // so that IVault(vaultAddress).strategistAddr() works @@ -81,7 +81,7 @@ contract CrossChainRemoteStrategy is address[] memory assets = new address[](1); address[] memory pTokens = new address[](1); - assets[0] = address(baseToken); + assets[0] = address(usdcToken); pTokens[0] = address(platformAddress); InitializableAbstractStrategy._initialize( @@ -123,7 +123,7 @@ contract CrossChainRemoteStrategy is /// @inheritdoc Generalized4626Strategy function depositAll() external virtual override onlyGovernorOrStrategist { - _deposit(baseToken, IERC20(baseToken).balanceOf(address(this))); + _deposit(usdcToken, IERC20(usdcToken).balanceOf(address(this))); } /// @inheritdoc Generalized4626Strategy @@ -140,7 +140,7 @@ contract CrossChainRemoteStrategy is IERC4626 platform = IERC4626(platformAddress); _withdraw( address(this), - baseToken, + usdcToken, platform.previewRedeem(platform.balanceOf(address(this))) ); } @@ -180,14 +180,14 @@ contract CrossChainRemoteStrategy is _markNonceAsProcessed(nonce); // Deposit everything we got, not just what was bridged - uint256 balance = IERC20(baseToken).balanceOf(address(this)); + uint256 balance = IERC20(usdcToken).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); + _deposit(usdcToken, balance); // Send balance check message to the peer strategy - uint256 balanceAfter = checkBalance(baseToken); + uint256 balanceAfter = checkBalance(usdcToken); bytes memory message = CrossChainStrategyHelper .encodeBalanceCheckMessage(lastTransferNonce, balanceAfter, true); _sendMessage(message); @@ -200,7 +200,7 @@ contract CrossChainRemoteStrategy is */ function _deposit(address _asset, uint256 _amount) internal override { require(_amount > 0, "Must deposit something"); - require(_asset == address(baseToken), "Unexpected asset address"); + require(_asset == address(usdcToken), "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. @@ -235,19 +235,19 @@ contract CrossChainRemoteStrategy is require(!isNonceProcessed(nonce), "Nonce already processed"); _markNonceAsProcessed(nonce); - uint256 usdcBalance = IERC20(baseToken).balanceOf(address(this)); + uint256 usdcBalance = IERC20(usdcToken).balanceOf(address(this)); if (usdcBalance < withdrawAmount) { // Withdraw the missing funds from the remote strategy. This call can fail and // the failure doesn't bubble up to the _processWithdrawMessage function - _withdraw(address(this), baseToken, withdrawAmount - usdcBalance); + _withdraw(address(this), usdcToken, withdrawAmount - usdcBalance); // Update the possible increase in the balance on the contract. - usdcBalance = IERC20(baseToken).balanceOf(address(this)); + usdcBalance = IERC20(usdcToken).balanceOf(address(this)); } // Check balance after withdrawal - uint256 balanceAfter = checkBalance(baseToken); + uint256 balanceAfter = checkBalance(usdcToken); // If there are some tokens to be sent AND the balance is sufficient // to satisfy the withdrawal request then send the funds to the peer strategy. @@ -294,7 +294,7 @@ contract CrossChainRemoteStrategy is ) internal override { require(_amount > 0, "Must withdraw something"); require(_recipient == address(this), "Invalid recipient"); - require(_asset == address(baseToken), "Unexpected asset address"); + require(_asset == address(usdcToken), "Unexpected asset address"); // 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. @@ -352,7 +352,7 @@ contract CrossChainRemoteStrategy is virtual onlyOperatorOrStrategistOrGovernor { - uint256 balance = checkBalance(baseToken); + uint256 balance = checkBalance(usdcToken); bytes memory message = CrossChainStrategyHelper .encodeBalanceCheckMessage(lastTransferNonce, balance, false); _sendMessage(message); @@ -369,13 +369,13 @@ contract CrossChainRemoteStrategy is override returns (uint256) { - require(_asset == baseToken, "Unexpected asset address"); + require(_asset == usdcToken, "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)); + uint256 balanceOnContract = IERC20(usdcToken).balanceOf(address(this)); IERC4626 platform = IERC4626(platformAddress); return diff --git a/contracts/contracts/strategies/crosschain/crosschain-strategy.md b/contracts/contracts/strategies/crosschain/crosschain-strategy.md index 1c38e7e6ed..aecb142b93 100644 --- a/contracts/contracts/strategies/crosschain/crosschain-strategy.md +++ b/contracts/contracts/strategies/crosschain/crosschain-strategy.md @@ -70,7 +70,7 @@ classDiagram <> +cctpMessageTransmitter: ICCTPMessageTransmitter +cctpTokenMessenger: ICCTPTokenMessenger - +baseToken: address + +usdcToken: address +peerDomainID: uint32 +peerStrategy: address +lastTransferNonce: uint64 @@ -151,7 +151,7 @@ classDiagram **Key State Variables**: - `cctpMessageTransmitter`: CCTP Message Transmitter contract - `cctpTokenMessenger`: CCTP Token Messenger contract -- `baseToken`: USDC address on local chain +- `usdcToken`: USDC address on local chain - `peerDomainID`: Domain ID of the peer chain - `peerStrategy`: Address of the strategy on peer chain - `minFinalityThreshold`: Minimum finality threshold (1000 or 2000) @@ -581,7 +581,7 @@ sequenceDiagram ### Master Strategy State **Local State**: -- `IERC20(baseToken).balanceOf(address(this))`: USDC held locally +- `IERC20(usdcToken).balanceOf(address(this))`: USDC held locally - `pendingAmount`: USDC bridged but not confirmed - `remoteStrategyBalance`: Cached balance in Remote strategy @@ -595,7 +595,7 @@ sequenceDiagram ### Remote Strategy State **Local State**: -- `IERC20(baseToken).balanceOf(address(this))`: USDC held locally +- `IERC20(usdcToken).balanceOf(address(this))`: USDC held locally - `IERC4626(platformAddress).balanceOf(address(this))`: Shares in 4626 vault **Total Balance**: `contractBalance + previewRedeem(shares)` From b9fee742494ed84436f8a06d9e4229bf3e6ffa7b Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Wed, 7 Jan 2026 14:15:36 +0100 Subject: [PATCH 069/101] improve error message --- .../strategies/crosschain/AbstractCCTPIntegrator.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/contracts/strategies/crosschain/AbstractCCTPIntegrator.sol b/contracts/contracts/strategies/crosschain/AbstractCCTPIntegrator.sol index b6846f6990..fcb9db3807 100644 --- a/contracts/contracts/strategies/crosschain/AbstractCCTPIntegrator.sol +++ b/contracts/contracts/strategies/crosschain/AbstractCCTPIntegrator.sol @@ -84,7 +84,7 @@ abstract contract AbstractCCTPIntegrator is Governable, IMessageHandlerV2 { /// @notice Operator address: Can relay CCTP messages address public operator; - /// @notice Mapping of processed nonces + /// @notice Mapping of processed nonces mapping(uint64 => bool) private nonceProcessed; // For future use @@ -144,7 +144,7 @@ abstract contract AbstractCCTPIntegrator is Governable, IMessageHandlerV2 { require( keccak256(abi.encodePacked(_usdcTokenSymbol)) == keccak256(abi.encodePacked("USDC")), - "Base token symbol must be USDC" + "Token symbol must be USDC" ); } From 5f290e8f0c763a451fe17ddfc1710026bddf019a Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Wed, 7 Jan 2026 14:50:32 +0100 Subject: [PATCH 070/101] simplify code --- .../strategies/crosschain/CrossChainMasterStrategy.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/contracts/strategies/crosschain/CrossChainMasterStrategy.sol b/contracts/contracts/strategies/crosschain/CrossChainMasterStrategy.sol index 12967a0ae8..c9679fc3a6 100644 --- a/contracts/contracts/strategies/crosschain/CrossChainMasterStrategy.sol +++ b/contracts/contracts/strategies/crosschain/CrossChainMasterStrategy.sol @@ -298,7 +298,7 @@ contract CrossChainMasterStrategy is // A received message nonce not yet processed indicates there is a // deposit or withdrawal in progress. - bool transferInProgress = !isNonceProcessed(nonce); + bool transferInProgress = isTransferPending(); if (transferInProgress) { if (transferConfirmation) { From 9117a1809833a8abb1c2fc8ecbd0dbf930b7e0f6 Mon Sep 17 00:00:00 2001 From: Shahul Hameed <10547529+shahthepro@users.noreply.github.com> Date: Wed, 7 Jan 2026 20:20:00 +0400 Subject: [PATCH 071/101] fix: min withdraw amount is 1e6 --- .../contracts/strategies/crosschain/AbstractCCTPIntegrator.sol | 2 +- .../strategies/crosschain/CrossChainRemoteStrategy.sol | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/contracts/strategies/crosschain/AbstractCCTPIntegrator.sol b/contracts/contracts/strategies/crosschain/AbstractCCTPIntegrator.sol index fcb9db3807..4935621aaa 100644 --- a/contracts/contracts/strategies/crosschain/AbstractCCTPIntegrator.sol +++ b/contracts/contracts/strategies/crosschain/AbstractCCTPIntegrator.sol @@ -84,7 +84,7 @@ abstract contract AbstractCCTPIntegrator is Governable, IMessageHandlerV2 { /// @notice Operator address: Can relay CCTP messages address public operator; - /// @notice Mapping of processed nonces + /// @notice Mapping of processed nonces mapping(uint64 => bool) private nonceProcessed; // For future use diff --git a/contracts/contracts/strategies/crosschain/CrossChainRemoteStrategy.sol b/contracts/contracts/strategies/crosschain/CrossChainRemoteStrategy.sol index dafa026bea..d1e6542e38 100644 --- a/contracts/contracts/strategies/crosschain/CrossChainRemoteStrategy.sol +++ b/contracts/contracts/strategies/crosschain/CrossChainRemoteStrategy.sol @@ -255,7 +255,7 @@ contract CrossChainRemoteStrategy is // there is a possibility of USDC funds remaining on the contract. // A separate withdraw to extract or deposit to the Morpho vault needs to be // initiated from the peer Master strategy to utilise USDC funds. - if (usdcBalance > 1e6 && usdcBalance >= withdrawAmount) { + if (withdrawAmount > 1e6 && usdcBalance >= withdrawAmount) { // The new balance on the contract needs to have USDC subtracted from it as // that will be withdrawn in the next step bytes memory message = CrossChainStrategyHelper From c7b2a92a62b7feb3fb68142516141cd70e74eb91 Mon Sep 17 00:00:00 2001 From: Shahul Hameed <10547529+shahthepro@users.noreply.github.com> Date: Wed, 7 Jan 2026 20:24:58 +0400 Subject: [PATCH 072/101] add validations for config --- .../crosschain/AbstractCCTPIntegrator.sol | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/contracts/contracts/strategies/crosschain/AbstractCCTPIntegrator.sol b/contracts/contracts/strategies/crosschain/AbstractCCTPIntegrator.sol index 4935621aaa..2010d470dd 100644 --- a/contracts/contracts/strategies/crosschain/AbstractCCTPIntegrator.sol +++ b/contracts/contracts/strategies/crosschain/AbstractCCTPIntegrator.sol @@ -88,7 +88,7 @@ abstract contract AbstractCCTPIntegrator is Governable, IMessageHandlerV2 { mapping(uint64 => bool) private nonceProcessed; // For future use - uint256[50] private __gap; + uint256[48] private __gap; modifier onlyCCTPMessageTransmitter() { require( @@ -122,6 +122,20 @@ abstract contract AbstractCCTPIntegrator is Governable, IMessageHandlerV2 { } constructor(CCTPIntegrationConfig memory _config) { + require(_config.usdcToken != address(0), "Invalid USDC address"); + require( + _config.cctpTokenMessenger != address(0), + "Invalid CCTP config" + ); + require( + _config.cctpMessageTransmitter != address(0), + "Invalid CCTP config" + ); + require( + _config.peerStrategy != address(0), + "Invalid peer strategy address" + ); + cctpMessageTransmitter = ICCTPMessageTransmitter( _config.cctpMessageTransmitter ); From b0a3f6f58533d73002a83df4ced544c6e8ce1de8 Mon Sep 17 00:00:00 2001 From: Shahul Hameed <10547529+shahthepro@users.noreply.github.com> Date: Wed, 7 Jan 2026 20:28:20 +0400 Subject: [PATCH 073/101] fix: require a min deposit amount of 1e6 --- .../strategies/crosschain/CrossChainMasterStrategy.sol | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/contracts/contracts/strategies/crosschain/CrossChainMasterStrategy.sol b/contracts/contracts/strategies/crosschain/CrossChainMasterStrategy.sol index c9679fc3a6..1a2d42ac8f 100644 --- a/contracts/contracts/strategies/crosschain/CrossChainMasterStrategy.sol +++ b/contracts/contracts/strategies/crosschain/CrossChainMasterStrategy.sol @@ -79,7 +79,8 @@ contract CrossChainMasterStrategy is /// @inheritdoc InitializableAbstractStrategy function depositAll() external override onlyVault nonReentrant { uint256 balance = IERC20(usdcToken).balanceOf(address(this)); - if (balance > 0) { + // Deposit if balance is greater than 1 USDC + if (balance > 1e6) { _deposit(usdcToken, balance); } } @@ -206,7 +207,8 @@ contract CrossChainMasterStrategy is function _deposit(address _asset, uint256 depositAmount) internal virtual { require(_asset == usdcToken, "Unsupported asset"); require(pendingAmount == 0, "Unexpected pending amount"); - require(depositAmount > 0, "Must deposit somethin"); + // Deposit at least 1 USDC + require(depositAmount > 1e6, "Deposit amount too small"); require( depositAmount <= MAX_TRANSFER_AMOUNT, "Deposit amount too high" @@ -244,7 +246,8 @@ contract CrossChainMasterStrategy is uint256 _amount ) internal virtual { require(_asset == usdcToken, "Unsupported asset"); - require(_amount > 0, "Withdraw amount must be greater than 0"); + // Withdraw at least 1 USDC + require(_amount > 1e6, "Withdraw amount too small"); require(_recipient == vaultAddress, "Only Vault can withdraw"); require( _amount <= remoteStrategyBalance, From 66015acb13c3bf974e7a9663bb5c262fc35e7f9c Mon Sep 17 00:00:00 2001 From: Shahul Hameed <10547529+shahthepro@users.noreply.github.com> Date: Wed, 7 Jan 2026 20:39:19 +0400 Subject: [PATCH 074/101] fix: withdrawAll caps withdraw amount --- .../crosschain/CrossChainMasterStrategy.sol | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/contracts/contracts/strategies/crosschain/CrossChainMasterStrategy.sol b/contracts/contracts/strategies/crosschain/CrossChainMasterStrategy.sol index 1a2d42ac8f..1941a5be11 100644 --- a/contracts/contracts/strategies/crosschain/CrossChainMasterStrategy.sol +++ b/contracts/contracts/strategies/crosschain/CrossChainMasterStrategy.sol @@ -99,7 +99,19 @@ contract CrossChainMasterStrategy is /// @inheritdoc InitializableAbstractStrategy function withdrawAll() external override onlyVaultOrGovernor nonReentrant { // Withdraw everything in Remote strategy - _withdraw(usdcToken, vaultAddress, remoteStrategyBalance); + uint256 _remoteBalance = remoteStrategyBalance; + if (_remoteBalance < 1e6) { + // Do nothing if there is less than 1 USDC in the Remote strategy + return; + } + + _withdraw( + usdcToken, + vaultAddress, + _remoteBalance > MAX_TRANSFER_AMOUNT + ? MAX_TRANSFER_AMOUNT + : _remoteBalance + ); } /** From 4cc4524ed2a1657e9f79bd602d3f4765d367d35d Mon Sep 17 00:00:00 2001 From: Shahul Hameed <10547529+shahthepro@users.noreply.github.com> Date: Wed, 7 Jan 2026 20:41:20 +0400 Subject: [PATCH 075/101] Move around constants --- .../crosschain/AbstractCCTPIntegrator.sol | 36 +++++++++---------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/contracts/contracts/strategies/crosschain/AbstractCCTPIntegrator.sol b/contracts/contracts/strategies/crosschain/AbstractCCTPIntegrator.sol index 2010d470dd..0e4e08750d 100644 --- a/contracts/contracts/strategies/crosschain/AbstractCCTPIntegrator.sol +++ b/contracts/contracts/strategies/crosschain/AbstractCCTPIntegrator.sol @@ -18,24 +18,6 @@ import { Governable } from "../../governance/Governable.sol"; import { BytesHelper } from "../../utils/BytesHelper.sol"; import "../../utils/Helpers.sol"; -// CCTP Message Header fields -// Ref: https://developers.circle.com/cctp/technical-guide#message-header -uint8 constant VERSION_INDEX = 0; -uint8 constant SOURCE_DOMAIN_INDEX = 4; -uint8 constant SENDER_INDEX = 44; -uint8 constant RECIPIENT_INDEX = 76; -uint8 constant MESSAGE_BODY_INDEX = 148; - -// 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 constant BURN_MESSAGE_V2_VERSION_INDEX = 0; -uint8 constant BURN_MESSAGE_V2_RECIPIENT_INDEX = 36; -uint8 constant BURN_MESSAGE_V2_AMOUNT_INDEX = 68; -uint8 constant BURN_MESSAGE_V2_MESSAGE_SENDER_INDEX = 100; -uint8 constant BURN_MESSAGE_V2_FEE_EXECUTED_INDEX = 164; -uint8 constant BURN_MESSAGE_V2_HOOK_DATA_INDEX = 228; - abstract contract AbstractCCTPIntegrator is Governable, IMessageHandlerV2 { using SafeERC20 for IERC20; @@ -45,6 +27,24 @@ abstract contract AbstractCCTPIntegrator is Governable, IMessageHandlerV2 { event CCTPFeePremiumBpsSet(uint16 feePremiumBps); event OperatorChanged(address operator); + // CCTP Message Header fields + // Ref: https://developers.circle.com/cctp/technical-guide#message-header + uint8 private constant VERSION_INDEX = 0; + uint8 private constant SOURCE_DOMAIN_INDEX = 4; + uint8 private constant SENDER_INDEX = 44; + uint8 private constant RECIPIENT_INDEX = 76; + uint8 private constant MESSAGE_BODY_INDEX = 148; + + // 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; + /** * @notice Max transfer threshold imposed by the CCTP * Ref: https://developers.circle.com/cctp/evm-smart-contracts#depositforburn From a0d7bef5526fca70ec4722e1253dfa582a168b0a Mon Sep 17 00:00:00 2001 From: Shahul Hameed <10547529+shahthepro@users.noreply.github.com> Date: Wed, 7 Jan 2026 20:48:12 +0400 Subject: [PATCH 076/101] Move decode message header function --- .../crosschain/AbstractCCTPIntegrator.sol | 39 +------------------ .../crosschain/CrossChainStrategyHelper.sol | 37 ++++++++++++++++++ 2 files changed, 39 insertions(+), 37 deletions(-) diff --git a/contracts/contracts/strategies/crosschain/AbstractCCTPIntegrator.sol b/contracts/contracts/strategies/crosschain/AbstractCCTPIntegrator.sol index 0e4e08750d..eb5cd9b39e 100644 --- a/contracts/contracts/strategies/crosschain/AbstractCCTPIntegrator.sol +++ b/contracts/contracts/strategies/crosschain/AbstractCCTPIntegrator.sol @@ -22,19 +22,12 @@ abstract contract AbstractCCTPIntegrator is Governable, IMessageHandlerV2 { using SafeERC20 for IERC20; using BytesHelper for bytes; + using CrossChainStrategyHelper for bytes; event CCTPMinFinalityThresholdSet(uint16 minFinalityThreshold); event CCTPFeePremiumBpsSet(uint16 feePremiumBps); event OperatorChanged(address operator); - // CCTP Message Header fields - // Ref: https://developers.circle.com/cctp/technical-guide#message-header - uint8 private constant VERSION_INDEX = 0; - uint8 private constant SOURCE_DOMAIN_INDEX = 4; - uint8 private constant SENDER_INDEX = 44; - uint8 private constant RECIPIENT_INDEX = 76; - uint8 private constant MESSAGE_BODY_INDEX = 148; - // 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 @@ -400,7 +393,7 @@ abstract contract AbstractCCTPIntegrator is Governable, IMessageHandlerV2 { address sender, address recipient, bytes memory messageBody - ) = _decodeMessageHeader(message); + ) = message.decodeMessageHeader(); // Ensure that it's a CCTP message require( @@ -486,34 +479,6 @@ abstract contract AbstractCCTPIntegrator is Governable, IMessageHandlerV2 { /*************************************** Message utils ****************************************/ - /** - * @dev Decodes the CCTP message header - * @param message Message to decode - * @return version Version of the message - * @return sourceDomainID Source domain ID - * @return sender Sender of the message - * @return recipient Recipient of the message - * @return messageBody Message body - */ - function _decodeMessageHeader(bytes memory message) - internal - pure - returns ( - uint32 version, - uint32 sourceDomainID, - address sender, - address recipient, - bytes memory messageBody - ) - { - version = message.extractUint32(VERSION_INDEX); - sourceDomainID = message.extractUint32(SOURCE_DOMAIN_INDEX); - // Address of MessageTransmitterV2 caller on source domain - sender = message.extractAddress(SENDER_INDEX); - // Address to handle message body on destination domain - recipient = message.extractAddress(RECIPIENT_INDEX); - messageBody = message.extractSlice(MESSAGE_BODY_INDEX, message.length); - } /*************************************** Nonce Handling diff --git a/contracts/contracts/strategies/crosschain/CrossChainStrategyHelper.sol b/contracts/contracts/strategies/crosschain/CrossChainStrategyHelper.sol index 44cf12e1d3..8dc5766d19 100644 --- a/contracts/contracts/strategies/crosschain/CrossChainStrategyHelper.sol +++ b/contracts/contracts/strategies/crosschain/CrossChainStrategyHelper.sol @@ -20,6 +20,14 @@ library CrossChainStrategyHelper { uint32 public constant CCTP_MESSAGE_VERSION = 1; uint32 public constant ORIGIN_MESSAGE_VERSION = 1010; + // CCTP Message Header fields + // Ref: https://developers.circle.com/cctp/technical-guide#message-header + uint8 private constant VERSION_INDEX = 0; + uint8 private constant SOURCE_DOMAIN_INDEX = 4; + uint8 private constant SENDER_INDEX = 44; + uint8 private constant RECIPIENT_INDEX = 76; + uint8 private constant MESSAGE_BODY_INDEX = 148; + /** * @dev Get the message version from the message. * It should always be 4 bytes long, @@ -214,4 +222,33 @@ library CrossChainStrategyHelper { ); return (nonce, balance, transferConfirmation); } + + /** + * @dev Decode the CCTP message header + * @param message Message to decode + * @return version Version of the message + * @return sourceDomainID Source domain ID + * @return sender Sender of the message + * @return recipient Recipient of the message + * @return messageBody Message body + */ + function decodeMessageHeader(bytes memory message) + internal + pure + returns ( + uint32 version, + uint32 sourceDomainID, + address sender, + address recipient, + bytes memory messageBody + ) + { + version = message.extractUint32(VERSION_INDEX); + sourceDomainID = message.extractUint32(SOURCE_DOMAIN_INDEX); + // Address of MessageTransmitterV2 caller on source domain + sender = message.extractAddress(SENDER_INDEX); + // Address to handle message body on destination domain + recipient = message.extractAddress(RECIPIENT_INDEX); + messageBody = message.extractSlice(MESSAGE_BODY_INDEX, message.length); + } } From 85e91f4efccff0f5ddf60af1f439ed594e5299e8 Mon Sep 17 00:00:00 2001 From: Shahul Hameed <10547529+shahthepro@users.noreply.github.com> Date: Wed, 7 Jan 2026 21:32:19 +0400 Subject: [PATCH 077/101] fix fork tests --- .../crosschain/_crosschain-helpers.js | 8 ++++---- ...schain-master-strategy.mainnet.fork-test.js | 18 ++++++++++++------ 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/contracts/test/strategies/crosschain/_crosschain-helpers.js b/contracts/test/strategies/crosschain/_crosschain-helpers.js index e5cbce5c0b..114bfae0fd 100644 --- a/contracts/test/strategies/crosschain/_crosschain-helpers.js +++ b/contracts/test/strategies/crosschain/_crosschain-helpers.js @@ -16,7 +16,7 @@ const empty16Bytes = empty4Bytes.repeat(4); const empty18Bytes = `${empty2Bytes}${empty16Bytes}`; const empty20Bytes = empty4Bytes.repeat(5); -const REMOTE_STRATEGY_BALANCE_SLOT = 210; +const REMOTE_STRATEGY_BALANCE_SLOT = 207; const decodeDepositForBurnEvent = (event) => { const [ @@ -177,10 +177,10 @@ const decodeBurnMessageBody = (message) => { return { version, burnToken, recipient, amount, sender, hookData }; }; -const encodeBalanceCheckMessageBody = (nonce, balance) => { +const encodeBalanceCheckMessageBody = (nonce, balance, transferConfirmation) => { const encodedPayload = ethers.utils.defaultAbiCoder.encode( - ["uint64", "uint256"], - [nonce, balance] + ["uint64", "uint256", "bool"], + [nonce, balance, transferConfirmation] ); // const version = 1010; // ORIGIN_MESSAGE_VERSION diff --git a/contracts/test/strategies/crosschain/crosschain-master-strategy.mainnet.fork-test.js b/contracts/test/strategies/crosschain/crosschain-master-strategy.mainnet.fork-test.js index 295dbb2b52..ec72199028 100644 --- a/contracts/test/strategies/crosschain/crosschain-master-strategy.mainnet.fork-test.js +++ b/contracts/test/strategies/crosschain/crosschain-master-strategy.mainnet.fork-test.js @@ -198,7 +198,8 @@ describe("ForkTest: CrossChainMasterStrategy", function () { // Build check balance payload const balancePayload = encodeBalanceCheckMessageBody( lastNonce, - usdcUnits("12345") + usdcUnits("12345"), + false ); const message = encodeCCTPMessage( 6, @@ -251,7 +252,8 @@ describe("ForkTest: CrossChainMasterStrategy", function () { // Build check balance payload const payload = encodeBalanceCheckMessageBody( lastNonce, - usdcUnits("10000") + usdcUnits("10000"), + true // deposit confirmation ); const message = encodeCCTPMessage( 6, @@ -308,7 +310,8 @@ describe("ForkTest: CrossChainMasterStrategy", function () { // Build check balance payload const balancePayload = encodeBalanceCheckMessageBody( lastNonce, - usdcUnits("12345") + usdcUnits("12345"), + true // withdrawal confirmation ); const burnPayload = encodeBurnMessageBody( crossChainMasterStrategy.address, @@ -374,7 +377,8 @@ describe("ForkTest: CrossChainMasterStrategy", function () { // Build check balance payload const payload = encodeBalanceCheckMessageBody( lastNonce, - usdcUnits("10000") + usdcUnits("10000"), + false ); const message = encodeCCTPMessage( 6, @@ -431,7 +435,8 @@ describe("ForkTest: CrossChainMasterStrategy", function () { // Build check balance payload const payload = encodeBalanceCheckMessageBody( lastNonce, - usdcUnits("123244") + usdcUnits("123244"), + false // deposit confirmation ); const message = encodeCCTPMessage( 6, @@ -472,7 +477,8 @@ describe("ForkTest: CrossChainMasterStrategy", function () { // Build check balance payload const payload = encodeBalanceCheckMessageBody( lastNonce + 2, - usdcUnits("123244") + usdcUnits("123244"), + false ); const message = encodeCCTPMessage( 6, From 4ebe400e18afb7e325fcfd602029304ef1a180b2 Mon Sep 17 00:00:00 2001 From: Shahul Hameed <10547529+shahthepro@users.noreply.github.com> Date: Wed, 7 Jan 2026 21:32:25 +0400 Subject: [PATCH 078/101] prettify --- contracts/test/strategies/crosschain/_crosschain-helpers.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/contracts/test/strategies/crosschain/_crosschain-helpers.js b/contracts/test/strategies/crosschain/_crosschain-helpers.js index 114bfae0fd..8fb9766d03 100644 --- a/contracts/test/strategies/crosschain/_crosschain-helpers.js +++ b/contracts/test/strategies/crosschain/_crosschain-helpers.js @@ -177,7 +177,11 @@ const decodeBurnMessageBody = (message) => { return { version, burnToken, recipient, amount, sender, hookData }; }; -const encodeBalanceCheckMessageBody = (nonce, balance, transferConfirmation) => { +const encodeBalanceCheckMessageBody = ( + nonce, + balance, + transferConfirmation +) => { const encodedPayload = ethers.utils.defaultAbiCoder.encode( ["uint64", "uint256", "bool"], [nonce, balance, transferConfirmation] From b9b0035d8963dbbb13fb471cb89a9282faaeb72b Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Wed, 7 Jan 2026 18:55:34 +0100 Subject: [PATCH 079/101] adjust some comments --- .../strategies/crosschain/AbstractCCTPIntegrator.sol | 5 +++++ .../strategies/crosschain/CrossChainRemoteStrategy.sol | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/contracts/contracts/strategies/crosschain/AbstractCCTPIntegrator.sol b/contracts/contracts/strategies/crosschain/AbstractCCTPIntegrator.sol index eb5cd9b39e..81d4b91f48 100644 --- a/contracts/contracts/strategies/crosschain/AbstractCCTPIntegrator.sol +++ b/contracts/contracts/strategies/crosschain/AbstractCCTPIntegrator.sol @@ -514,6 +514,11 @@ abstract contract AbstractCCTPIntegrator is Governable, IMessageHandlerV2 { uint64 lastNonce = lastTransferNonce; // Can only mark latest nonce as processed + // Master strategy when receiving a message from the remote strategy + // will have lastNone == nonce, as the nonce is increase at the start + // of deposit / withdrawal flow. + // Remote strategy will have lastNonce < nonce, as a new nonce initiated + // from master will be greater than the last one. require(nonce >= lastNonce, "Nonce too low"); // Can only mark nonce as processed once require(!nonceProcessed[nonce], "Nonce already processed"); diff --git a/contracts/contracts/strategies/crosschain/CrossChainRemoteStrategy.sol b/contracts/contracts/strategies/crosschain/CrossChainRemoteStrategy.sol index d1e6542e38..0f73eaf94b 100644 --- a/contracts/contracts/strategies/crosschain/CrossChainRemoteStrategy.sol +++ b/contracts/contracts/strategies/crosschain/CrossChainRemoteStrategy.sol @@ -267,7 +267,7 @@ contract CrossChainRemoteStrategy is _sendTokens(withdrawAmount, message); } else { // Contract either: - // - only has a small dust + // - only has small dust amount of USDC // - doesn't have sufficient funds to satisfy the withdrawal request // In both cases send the balance update message to the peer strategy. bytes memory message = CrossChainStrategyHelper From c4bc36d20f47c942dd95104985224c5447c48abe Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Wed, 7 Jan 2026 18:56:52 +0100 Subject: [PATCH 080/101] have consistent event names --- .../strategies/crosschain/CrossChainRemoteStrategy.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/contracts/strategies/crosschain/CrossChainRemoteStrategy.sol b/contracts/contracts/strategies/crosschain/CrossChainRemoteStrategy.sol index 0f73eaf94b..65a03f07fe 100644 --- a/contracts/contracts/strategies/crosschain/CrossChainRemoteStrategy.sol +++ b/contracts/contracts/strategies/crosschain/CrossChainRemoteStrategy.sol @@ -26,7 +26,7 @@ contract CrossChainRemoteStrategy is using CrossChainStrategyHelper for bytes; event DepositUnderlyingFailed(string reason); - event WithdrawFailed(uint256 amountRequested, uint256 amountAvailable); + event WithdrawalFailed(uint256 amountRequested, uint256 amountAvailable); event WithdrawUnderlyingFailed(string reason); event StrategistUpdated(address _address); @@ -277,7 +277,7 @@ contract CrossChainRemoteStrategy is true ); _sendMessage(message); - emit WithdrawFailed(withdrawAmount, usdcBalance); + emit WithdrawalFailed(withdrawAmount, usdcBalance); } } From 9f7d4237370ecfef8c399bab67d50ebc14ad394d Mon Sep 17 00:00:00 2001 From: Shahul Hameed <10547529+shahthepro@users.noreply.github.com> Date: Wed, 7 Jan 2026 22:00:55 +0400 Subject: [PATCH 081/101] fix: remove redundant check --- .../strategies/crosschain/CrossChainRemoteStrategy.sol | 2 -- 1 file changed, 2 deletions(-) diff --git a/contracts/contracts/strategies/crosschain/CrossChainRemoteStrategy.sol b/contracts/contracts/strategies/crosschain/CrossChainRemoteStrategy.sol index 65a03f07fe..414f079e43 100644 --- a/contracts/contracts/strategies/crosschain/CrossChainRemoteStrategy.sol +++ b/contracts/contracts/strategies/crosschain/CrossChainRemoteStrategy.sol @@ -176,7 +176,6 @@ contract CrossChainRemoteStrategy is (uint64 nonce, ) = payload.decodeDepositMessage(); // Replay protection - require(!isNonceProcessed(nonce), "Nonce already processed"); _markNonceAsProcessed(nonce); // Deposit everything we got, not just what was bridged @@ -232,7 +231,6 @@ contract CrossChainRemoteStrategy is .decodeWithdrawMessage(); // Replay protection - require(!isNonceProcessed(nonce), "Nonce already processed"); _markNonceAsProcessed(nonce); uint256 usdcBalance = IERC20(usdcToken).balanceOf(address(this)); From ad9dc472c86a6ce9e9dd7ccfcab85d81a32c2af2 Mon Sep 17 00:00:00 2001 From: Shahul Hameed <10547529+shahthepro@users.noreply.github.com> Date: Wed, 7 Jan 2026 22:03:22 +0400 Subject: [PATCH 082/101] simplify stuff --- .../strategies/crosschain/CrossChainRemoteStrategy.sol | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/contracts/contracts/strategies/crosschain/CrossChainRemoteStrategy.sol b/contracts/contracts/strategies/crosschain/CrossChainRemoteStrategy.sol index 414f079e43..1b13ebcbe1 100644 --- a/contracts/contracts/strategies/crosschain/CrossChainRemoteStrategy.sol +++ b/contracts/contracts/strategies/crosschain/CrossChainRemoteStrategy.sol @@ -186,9 +186,12 @@ contract CrossChainRemoteStrategy is _deposit(usdcToken, balance); // Send balance check message to the peer strategy - uint256 balanceAfter = checkBalance(usdcToken); bytes memory message = CrossChainStrategyHelper - .encodeBalanceCheckMessage(lastTransferNonce, balanceAfter, true); + .encodeBalanceCheckMessage( + lastTransferNonce, + checkBalance(usdcToken), + true + ); _sendMessage(message); } From bd51e5630d8e48af95f7ca6bf2e3c2304a792aeb Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Wed, 7 Jan 2026 21:06:08 +0100 Subject: [PATCH 083/101] adjust comment --- .../strategies/crosschain/CrossChainRemoteStrategy.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/contracts/strategies/crosschain/CrossChainRemoteStrategy.sol b/contracts/contracts/strategies/crosschain/CrossChainRemoteStrategy.sol index 1b13ebcbe1..78d6fc4f09 100644 --- a/contracts/contracts/strategies/crosschain/CrossChainRemoteStrategy.sol +++ b/contracts/contracts/strategies/crosschain/CrossChainRemoteStrategy.sol @@ -175,7 +175,7 @@ contract CrossChainRemoteStrategy is ) internal virtual { (uint64 nonce, ) = payload.decodeDepositMessage(); - // Replay protection + // Replay protection is part of the _markNonceAsProcessed function _markNonceAsProcessed(nonce); // Deposit everything we got, not just what was bridged @@ -233,7 +233,7 @@ contract CrossChainRemoteStrategy is (uint64 nonce, uint256 withdrawAmount) = payload .decodeWithdrawMessage(); - // Replay protection + // Replay protection is part of the _markNonceAsProcessed function _markNonceAsProcessed(nonce); uint256 usdcBalance = IERC20(usdcToken).balanceOf(address(this)); From 3729fa8cd40393435ad381e6a320eea16669478e Mon Sep 17 00:00:00 2001 From: Shahul Hameed <10547529+shahthepro@users.noreply.github.com> Date: Thu, 8 Jan 2026 14:52:39 +0400 Subject: [PATCH 084/101] fix: variable name --- .../strategies/crosschain/CrossChainRemoteStrategy.sol | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/contracts/contracts/strategies/crosschain/CrossChainRemoteStrategy.sol b/contracts/contracts/strategies/crosschain/CrossChainRemoteStrategy.sol index 78d6fc4f09..3665840f53 100644 --- a/contracts/contracts/strategies/crosschain/CrossChainRemoteStrategy.sol +++ b/contracts/contracts/strategies/crosschain/CrossChainRemoteStrategy.sol @@ -248,7 +248,7 @@ contract CrossChainRemoteStrategy is } // Check balance after withdrawal - uint256 balanceAfter = checkBalance(usdcToken); + uint256 strategyBalance = checkBalance(usdcToken); // If there are some tokens to be sent AND the balance is sufficient // to satisfy the withdrawal request then send the funds to the peer strategy. @@ -262,7 +262,7 @@ contract CrossChainRemoteStrategy is bytes memory message = CrossChainStrategyHelper .encodeBalanceCheckMessage( lastTransferNonce, - balanceAfter - withdrawAmount, + strategyBalance - withdrawAmount, true ); _sendTokens(withdrawAmount, message); @@ -274,7 +274,7 @@ contract CrossChainRemoteStrategy is bytes memory message = CrossChainStrategyHelper .encodeBalanceCheckMessage( lastTransferNonce, - balanceAfter, + strategyBalance, true ); _sendMessage(message); From 800e21ba15756dd8a99735ce201afbed8ac66be7 Mon Sep 17 00:00:00 2001 From: Shahul Hameed <10547529+shahthepro@users.noreply.github.com> Date: Thu, 8 Jan 2026 15:00:21 +0400 Subject: [PATCH 085/101] Add TokensBridged and MessageTransmitted events --- .../crosschain/AbstractCCTPIntegrator.sol | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/contracts/contracts/strategies/crosschain/AbstractCCTPIntegrator.sol b/contracts/contracts/strategies/crosschain/AbstractCCTPIntegrator.sol index 81d4b91f48..882954db5c 100644 --- a/contracts/contracts/strategies/crosschain/AbstractCCTPIntegrator.sol +++ b/contracts/contracts/strategies/crosschain/AbstractCCTPIntegrator.sol @@ -27,6 +27,21 @@ abstract contract AbstractCCTPIntegrator is Governable, IMessageHandlerV2 { event CCTPMinFinalityThresholdSet(uint16 minFinalityThreshold); event CCTPFeePremiumBpsSet(uint16 feePremiumBps); event OperatorChanged(address operator); + event TokensBridged( + uint32 destinationDomain, + address peerStrategy, + address tokenAddress, + uint256 tokenAmount, + uint256 maxFee, + uint32 minFinalityThreshold, + bytes hookData + ); + event MessageTransmitted( + uint32 destinationDomain, + address peerStrategy, + uint32 minFinalityThreshold, + bytes message + ); // Message body V2 fields // Ref: https://developers.circle.com/cctp/technical-guide#message-body @@ -359,6 +374,16 @@ abstract contract AbstractCCTPIntegrator is Governable, IMessageHandlerV2 { uint32(minFinalityThreshold), hookData ); + + emit TokensBridged( + peerDomainID, + peerStrategy, + usdcToken, + tokenAmount, + maxFee, + uint32(minFinalityThreshold), + hookData + ); } /** @@ -373,6 +398,13 @@ abstract contract AbstractCCTPIntegrator is Governable, IMessageHandlerV2 { uint32(minFinalityThreshold), message ); + + emit MessageTransmitted( + peerDomainID, + peerStrategy, + uint32(minFinalityThreshold), + message + ); } /** From 75e260f8ed731678715a5c747bc199a234a88001 Mon Sep 17 00:00:00 2001 From: Shahul Hameed <10547529+shahthepro@users.noreply.github.com> Date: Thu, 8 Jan 2026 15:13:35 +0400 Subject: [PATCH 086/101] Add finality threshold checks --- .../strategies/crosschain/AbstractCCTPIntegrator.sol | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/contracts/contracts/strategies/crosschain/AbstractCCTPIntegrator.sol b/contracts/contracts/strategies/crosschain/AbstractCCTPIntegrator.sol index 882954db5c..f8406f0b8d 100644 --- a/contracts/contracts/strategies/crosschain/AbstractCCTPIntegrator.sol +++ b/contracts/contracts/strategies/crosschain/AbstractCCTPIntegrator.sol @@ -274,6 +274,12 @@ abstract contract AbstractCCTPIntegrator is Governable, IMessageHandlerV2 { uint32 finalityThresholdExecuted, bytes memory messageBody ) external override onlyCCTPMessageTransmitter returns (bool) { + // Make sure the finality threshold at execution is at least 2000 + require( + finalityThresholdExecuted >= 2000, + "Finality threshold too low" + ); + return _handleReceivedMessage( sourceDomain, @@ -301,6 +307,11 @@ abstract contract AbstractCCTPIntegrator is Governable, IMessageHandlerV2 { minFinalityThreshold == 1000, "Unfinalized messages are not supported" ); + // Make sure the finality threshold at execution is at least 1000 + require( + finalityThresholdExecuted >= 1000, + "Finality threshold too low" + ); return _handleReceivedMessage( From a1482a00cdb161ca75ccc31eccc7cadee1274584 Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Thu, 8 Jan 2026 19:19:23 +0100 Subject: [PATCH 087/101] add comment regarding fast transfers --- .../contracts/strategies/crosschain/AbstractCCTPIntegrator.sol | 3 +++ 1 file changed, 3 insertions(+) diff --git a/contracts/contracts/strategies/crosschain/AbstractCCTPIntegrator.sol b/contracts/contracts/strategies/crosschain/AbstractCCTPIntegrator.sol index f8406f0b8d..0878d68cae 100644 --- a/contracts/contracts/strategies/crosschain/AbstractCCTPIntegrator.sol +++ b/contracts/contracts/strategies/crosschain/AbstractCCTPIntegrator.sol @@ -80,6 +80,9 @@ abstract contract AbstractCCTPIntegrator is Governable, IMessageHandlerV2 { * @notice Minimum finality threshold * Can be 1000 (safe, after 1 epoch) or 2000 (finalized, after 2 epochs). * Ref: https://developers.circle.com/cctp/technical-guide#finality-thresholds + * @dev When configuring the contract for fast transfer we should check the available + * allowance of USDC that can be bridged using fast mode: + * wget https://iris-api.circle.com/v2/fastBurn/USDC/allowance */ uint16 public minFinalityThreshold; From b674c752d95ddc37c08538523531f312085afe1d Mon Sep 17 00:00:00 2001 From: Shahul Hameed <10547529+shahthepro@users.noreply.github.com> Date: Fri, 9 Jan 2026 13:34:16 +0400 Subject: [PATCH 088/101] Add analytics info --- .../crosschain/crosschain-strategy.md | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/contracts/contracts/strategies/crosschain/crosschain-strategy.md b/contracts/contracts/strategies/crosschain/crosschain-strategy.md index aecb142b93..ab1afed9d9 100644 --- a/contracts/contracts/strategies/crosschain/crosschain-strategy.md +++ b/contracts/contracts/strategies/crosschain/crosschain-strategy.md @@ -696,3 +696,49 @@ Both strategies require initialization: - Both strategies inherit from `Governable` - Governor can upgrade implementation, update operator, finality threshold, fee premium - Remote strategy governor can update strategist address + +## Analytics & Monitoring + +### Useful Contract Methods and Variables +- `MasterStrategy.checkBalance(address)`: Returns the sum of balance held locally in the master contract, balance reported by the remote strategy and any tokens that are being bridged and are yet to be acknowledged by the rremote strategy +- `MasterStrategy.pendingAmount`: Returns the amount that is being bridged from Master to Remote strategy. Once it's received on Remote strategy and it sends back an acknowledgement, it'll set back to zero. +- `MasterStrategy.remoteStrategyBalacne`: Last reported balance of Remote strategy +- `RemoteStrategy.checkBalance(address)`: Returns the balance held by the remote strategy as well as the amount it has deposited into the underlying 4626 vault +- `RemoteStrategy.platformAddress`: Returns the underlying 4626 Vault to which remote strategy deposits funds to. + + +### Contract Events +The following events need to be monitored from the contracts and an alert be sent to any of the channels as they happen: + + ``` + event CCTPMinFinalityThresholdSet(uint16 minFinalityThreshold); + event CCTPFeePremiumBpsSet(uint16 feePremiumBps); + event OperatorChanged(address operator); + + event TokensBridged( + uint32 destinationDomain, + address peerStrategy, + address tokenAddress, + uint256 tokenAmount, + uint256 maxFee, + uint32 minFinalityThreshold, + bytes hookData + ); + + event MessageTransmitted( + uint32 destinationDomain, + address peerStrategy, + uint32 minFinalityThreshold, + bytes message + ); + + event DepositUnderlyingFailed(string reason); + event WithdrawalFailed(uint256 amountRequested, uint256 amountAvailable); + event WithdrawUnderlyingFailed(string reason); + event StrategistUpdated(address _address); + + event RemoteStrategyBalanceUpdated(uint256 balance); + event WithdrawRequested(address indexed asset, uint256 amount); + ``` + +Out of these, `DepositUnderlyingFailed`, `WithdrawalFailed` and `WithdrawUnderlyingFailed` are of higher importance as they require manual intervention by Guardian when they get emitted. \ No newline at end of file From d23548e9fcc605a815a8bd19f6f5b71bdda25f15 Mon Sep 17 00:00:00 2001 From: Shahul Hameed <10547529+shahthepro@users.noreply.github.com> Date: Fri, 9 Jan 2026 15:41:34 +0400 Subject: [PATCH 089/101] Change error message --- .../strategies/crosschain/AbstractCCTPIntegrator.sol | 7 ++----- .../test/strategies/crosschain/cross-chain-strategy.js | 6 +++--- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/contracts/contracts/strategies/crosschain/AbstractCCTPIntegrator.sol b/contracts/contracts/strategies/crosschain/AbstractCCTPIntegrator.sol index 0878d68cae..b0cd943763 100644 --- a/contracts/contracts/strategies/crosschain/AbstractCCTPIntegrator.sol +++ b/contracts/contracts/strategies/crosschain/AbstractCCTPIntegrator.sol @@ -81,7 +81,7 @@ abstract contract AbstractCCTPIntegrator is Governable, IMessageHandlerV2 { * Can be 1000 (safe, after 1 epoch) or 2000 (finalized, after 2 epochs). * Ref: https://developers.circle.com/cctp/technical-guide#finality-thresholds * @dev When configuring the contract for fast transfer we should check the available - * allowance of USDC that can be bridged using fast mode: + * allowance of USDC that can be bridged using fast mode: * wget https://iris-api.circle.com/v2/fastBurn/USDC/allowance */ uint16 public minFinalityThreshold; @@ -586,10 +586,7 @@ abstract contract AbstractCCTPIntegrator is Governable, IMessageHandlerV2 { function _getNextNonce() internal returns (uint64) { uint64 nonce = lastTransferNonce; - require( - nonce == 0 || nonceProcessed[nonce], - "Pending deposit or withdrawal" - ); + require(nonce == 0 || nonceProcessed[nonce], "Pending token transfer"); nonce = nonce + 1; lastTransferNonce = nonce; diff --git a/contracts/test/strategies/crosschain/cross-chain-strategy.js b/contracts/test/strategies/crosschain/cross-chain-strategy.js index 62de14f88c..5911e3bf2e 100644 --- a/contracts/test/strategies/crosschain/cross-chain-strategy.js +++ b/contracts/test/strategies/crosschain/cross-chain-strategy.js @@ -429,7 +429,7 @@ describe("ForkTest: CrossChainRemoteStrategy", function () { await depositToMasterStrategy("50"); await expect(withdrawFromRemoteStrategy("40")).to.be.revertedWith( - "Pending deposit or withdrawal" + "Pending token transfer" ); }); @@ -439,7 +439,7 @@ describe("ForkTest: CrossChainRemoteStrategy", function () { await mint("30"); await expect(depositToMasterStrategy("30")).to.be.revertedWith( - "Pending deposit or withdrawal" + "Pending token transfer" ); }); @@ -448,7 +448,7 @@ describe("ForkTest: CrossChainRemoteStrategy", function () { await withdrawFromRemoteStrategy("50"); await expect(withdrawFromRemoteStrategy("40")).to.be.revertedWith( - "Pending deposit or withdrawal" + "Pending token transfer" ); }); }); From d9b2a49b4fa585e9016ce86aeec1d41a34e8f204 Mon Sep 17 00:00:00 2001 From: Shahul Hameed <10547529+shahthepro@users.noreply.github.com> Date: Fri, 9 Jan 2026 15:48:11 +0400 Subject: [PATCH 090/101] Set 1 USDC as min allowed value for deposits --- .../strategies/crosschain/CrossChainMasterStrategy.sol | 6 +++--- .../strategies/crosschain/CrossChainRemoteStrategy.sol | 6 ++++-- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/contracts/contracts/strategies/crosschain/CrossChainMasterStrategy.sol b/contracts/contracts/strategies/crosschain/CrossChainMasterStrategy.sol index 1941a5be11..133ac629e0 100644 --- a/contracts/contracts/strategies/crosschain/CrossChainMasterStrategy.sol +++ b/contracts/contracts/strategies/crosschain/CrossChainMasterStrategy.sol @@ -80,7 +80,7 @@ contract CrossChainMasterStrategy is function depositAll() external override onlyVault nonReentrant { uint256 balance = IERC20(usdcToken).balanceOf(address(this)); // Deposit if balance is greater than 1 USDC - if (balance > 1e6) { + if (balance >= 1e6) { _deposit(usdcToken, balance); } } @@ -220,7 +220,7 @@ contract CrossChainMasterStrategy is require(_asset == usdcToken, "Unsupported asset"); require(pendingAmount == 0, "Unexpected pending amount"); // Deposit at least 1 USDC - require(depositAmount > 1e6, "Deposit amount too small"); + require(depositAmount >= 1e6, "Deposit amount too small"); require( depositAmount <= MAX_TRANSFER_AMOUNT, "Deposit amount too high" @@ -259,7 +259,7 @@ contract CrossChainMasterStrategy is ) internal virtual { require(_asset == usdcToken, "Unsupported asset"); // Withdraw at least 1 USDC - require(_amount > 1e6, "Withdraw amount too small"); + require(_amount >= 1e6, "Withdraw amount too small"); require(_recipient == vaultAddress, "Only Vault can withdraw"); require( _amount <= remoteStrategyBalance, diff --git a/contracts/contracts/strategies/crosschain/CrossChainRemoteStrategy.sol b/contracts/contracts/strategies/crosschain/CrossChainRemoteStrategy.sol index 3665840f53..44a6629a09 100644 --- a/contracts/contracts/strategies/crosschain/CrossChainRemoteStrategy.sol +++ b/contracts/contracts/strategies/crosschain/CrossChainRemoteStrategy.sol @@ -183,7 +183,9 @@ contract CrossChainRemoteStrategy is // Underlying call to deposit funds can fail. It mustn't affect the overall // flow as confirmation message should still be sent. - _deposit(usdcToken, balance); + if (balance >= 1e6) { + _deposit(usdcToken, balance); + } // Send balance check message to the peer strategy bytes memory message = CrossChainStrategyHelper @@ -256,7 +258,7 @@ contract CrossChainRemoteStrategy is // there is a possibility of USDC funds remaining on the contract. // A separate withdraw to extract or deposit to the Morpho vault needs to be // initiated from the peer Master strategy to utilise USDC funds. - if (withdrawAmount > 1e6 && usdcBalance >= withdrawAmount) { + if (withdrawAmount >= 1e6 && usdcBalance >= withdrawAmount) { // The new balance on the contract needs to have USDC subtracted from it as // that will be withdrawn in the next step bytes memory message = CrossChainStrategyHelper From eb66a55f76815cabd721d00464cb0a162076b104 Mon Sep 17 00:00:00 2001 From: Shahul Hameed <10547529+shahthepro@users.noreply.github.com> Date: Fri, 9 Jan 2026 15:54:03 +0400 Subject: [PATCH 091/101] Change comment --- .../strategies/crosschain/CrossChainMasterStrategy.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/contracts/strategies/crosschain/CrossChainMasterStrategy.sol b/contracts/contracts/strategies/crosschain/CrossChainMasterStrategy.sol index 133ac629e0..234d961ec4 100644 --- a/contracts/contracts/strategies/crosschain/CrossChainMasterStrategy.sol +++ b/contracts/contracts/strategies/crosschain/CrossChainMasterStrategy.sol @@ -25,7 +25,7 @@ contract CrossChainMasterStrategy is uint256 public remoteStrategyBalance; /// @notice Amount that's bridged due to a pending Deposit process - /// but not yet received on the destination chain + /// but with no acknowledgement from the remote strategy yet uint256 public pendingAmount; event RemoteStrategyBalanceUpdated(uint256 balance); From a7e0ae9e3dd32d82a58191c1035f4c2f94122cc4 Mon Sep 17 00:00:00 2001 From: Shahul Hameed <10547529+shahthepro@users.noreply.github.com> Date: Fri, 9 Jan 2026 15:56:03 +0400 Subject: [PATCH 092/101] Change comment --- .../strategies/crosschain/CrossChainMasterStrategy.sol | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/contracts/contracts/strategies/crosschain/CrossChainMasterStrategy.sol b/contracts/contracts/strategies/crosschain/CrossChainMasterStrategy.sol index 234d961ec4..53cc86073a 100644 --- a/contracts/contracts/strategies/crosschain/CrossChainMasterStrategy.sol +++ b/contracts/contracts/strategies/crosschain/CrossChainMasterStrategy.sol @@ -21,7 +21,11 @@ contract CrossChainMasterStrategy is using SafeERC20 for IERC20; using CrossChainStrategyHelper for bytes; - /// @notice Remote strategy balance + /** + * @notice Remote strategy balance + * @dev The remote balance is cached and might not reflect the actual + * real-time balance of the remote strategy. + */ uint256 public remoteStrategyBalance; /// @notice Amount that's bridged due to a pending Deposit process From 6022c93f3c04900f4976d32922ad5011710f8a37 Mon Sep 17 00:00:00 2001 From: Shahul Hameed <10547529+shahthepro@users.noreply.github.com> Date: Fri, 9 Jan 2026 15:59:21 +0400 Subject: [PATCH 093/101] Update max transfer amount comment --- .../strategies/crosschain/AbstractCCTPIntegrator.sol | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/contracts/contracts/strategies/crosschain/AbstractCCTPIntegrator.sol b/contracts/contracts/strategies/crosschain/AbstractCCTPIntegrator.sol index b0cd943763..efe2024876 100644 --- a/contracts/contracts/strategies/crosschain/AbstractCCTPIntegrator.sol +++ b/contracts/contracts/strategies/crosschain/AbstractCCTPIntegrator.sol @@ -54,8 +54,16 @@ abstract contract AbstractCCTPIntegrator is Governable, IMessageHandlerV2 { uint8 private constant BURN_MESSAGE_V2_HOOK_DATA_INDEX = 228; /** - * @notice Max transfer threshold imposed by the CCTP - * Ref: https://developers.circle.com/cctp/evm-smart-contracts#depositforburn + * @notice Max transfer threshold imposed by the CCTP + * Ref: https://developers.circle.com/cctp/evm-smart-contracts#depositforburn + * @dev 10M USDC limit applies to both standard and fast transfer modes. The fast transfer mode has + * an additional limitation that is not present on-chain and Circle may alter that amount off-chain + * at their preference. The amount available for fast transfer can be queried here: + * https://iris-api.circle.com/v2/fastBurn/USDC/allowance . + * If a fast transfer token transaction has been issued and there is not enough allowance for it + * the off-chain Iris component will re-attempt the transaction and if it fails it will fallback + * to a standard transfer. Reference section 4.3 in the whitepaper: + * https://6778953.fs1.hubspotusercontent-na1.net/hubfs/6778953/PDFs/Whitepapers/CCTPV2_White_Paper.pdf */ uint256 public constant MAX_TRANSFER_AMOUNT = 10_000_000 * 10**6; // 10M USDC From 6a3846dc67a1544d69faaab7616dc32b262616af Mon Sep 17 00:00:00 2001 From: Shahul Hameed <10547529+shahthepro@users.noreply.github.com> Date: Fri, 9 Jan 2026 16:03:31 +0400 Subject: [PATCH 094/101] Set nonce(0) as processed during initialization --- .../strategies/crosschain/AbstractCCTPIntegrator.sol | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/contracts/contracts/strategies/crosschain/AbstractCCTPIntegrator.sol b/contracts/contracts/strategies/crosschain/AbstractCCTPIntegrator.sol index efe2024876..3006fc5b7e 100644 --- a/contracts/contracts/strategies/crosschain/AbstractCCTPIntegrator.sol +++ b/contracts/contracts/strategies/crosschain/AbstractCCTPIntegrator.sol @@ -195,6 +195,11 @@ abstract contract AbstractCCTPIntegrator is Governable, IMessageHandlerV2 { _setOperator(_operator); _setMinFinalityThreshold(_minFinalityThreshold); _setFeePremiumBps(_feePremiumBps); + + // Nonce starts at 1, so assume nonce 0 as processed. + // NOTE: This will cause the deposit/withdraw to fail if the + // strategy is not initialized properly (which is expected). + nonceProcessed[0] = true; } /*************************************** @@ -543,8 +548,7 @@ abstract contract AbstractCCTPIntegrator is Governable, IMessageHandlerV2 { * @return True if a transfer is pending, false otherwise */ function isTransferPending() public view returns (bool) { - uint64 nonce = lastTransferNonce; - return nonce > 0 && !nonceProcessed[nonce]; + return !nonceProcessed[lastTransferNonce]; } /** @@ -554,7 +558,7 @@ abstract contract AbstractCCTPIntegrator is Governable, IMessageHandlerV2 { * @return True if the nonce is processed, false otherwise */ function isNonceProcessed(uint64 nonce) public view returns (bool) { - return nonce == 0 || nonceProcessed[nonce]; + return nonceProcessed[nonce]; } /** @@ -594,7 +598,7 @@ abstract contract AbstractCCTPIntegrator is Governable, IMessageHandlerV2 { function _getNextNonce() internal returns (uint64) { uint64 nonce = lastTransferNonce; - require(nonce == 0 || nonceProcessed[nonce], "Pending token transfer"); + require(nonceProcessed[nonce], "Pending token transfer"); nonce = nonce + 1; lastTransferNonce = nonce; From fafe7d4e71cd406b59ef1c9404657d5df6c07d28 Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Fri, 9 Jan 2026 13:14:25 +0100 Subject: [PATCH 095/101] Use Strategizable for strategist functionality (#2740) * use Strategizable * Add comment --------- Co-authored-by: Shahul Hameed <10547529+shahthepro@users.noreply.github.com> --- .../contracts/governance/Strategizable.sol | 2 +- .../crosschain/CrossChainRemoteStrategy.sol | 51 ++++++++----------- .../utils/InitializableAbstractStrategy.sol | 2 +- 3 files changed, 23 insertions(+), 32 deletions(-) diff --git a/contracts/contracts/governance/Strategizable.sol b/contracts/contracts/governance/Strategizable.sol index 62a3c0c028..4d823d6d1a 100644 --- a/contracts/contracts/governance/Strategizable.sol +++ b/contracts/contracts/governance/Strategizable.sol @@ -15,7 +15,7 @@ contract Strategizable is Governable { /** * @dev Verifies that the caller is either Governor or Strategist. */ - modifier onlyGovernorOrStrategist() { + modifier onlyGovernorOrStrategist() virtual { require( msg.sender == strategistAddr || isGovernor(), "Caller is not the Strategist or Governor" diff --git a/contracts/contracts/strategies/crosschain/CrossChainRemoteStrategy.sol b/contracts/contracts/strategies/crosschain/CrossChainRemoteStrategy.sol index 44a6629a09..dd9f67a7fa 100644 --- a/contracts/contracts/strategies/crosschain/CrossChainRemoteStrategy.sol +++ b/contracts/contracts/strategies/crosschain/CrossChainRemoteStrategy.sol @@ -17,10 +17,12 @@ import { Generalized4626Strategy } from "../Generalized4626Strategy.sol"; import { AbstractCCTPIntegrator } from "./AbstractCCTPIntegrator.sol"; import { CrossChainStrategyHelper } from "./CrossChainStrategyHelper.sol"; import { InitializableAbstractStrategy } from "../../utils/InitializableAbstractStrategy.sol"; +import { Strategizable } from "../../governance/Strategizable.sol"; contract CrossChainRemoteStrategy is AbstractCCTPIntegrator, - Generalized4626Strategy + Generalized4626Strategy, + Strategizable { using SafeERC20 for IERC20; using CrossChainStrategyHelper for bytes; @@ -28,15 +30,6 @@ contract CrossChainRemoteStrategy is event DepositUnderlyingFailed(string reason); event WithdrawalFailed(uint256 amountRequested, uint256 amountAvailable); event WithdrawUnderlyingFailed(string reason); - event StrategistUpdated(address _address); - - /** - * @notice Address of the strategist. - * This is important to have the variable name same as in IVault. - * Because the parent contract (Generalized4626Strategy) uses this - * function to get the strategist address. - */ - address public strategistAddr; modifier onlyOperatorOrStrategistOrGovernor() { require( @@ -48,6 +41,24 @@ contract CrossChainRemoteStrategy is _; } + /** + * @dev Overriding this modifier is important because the parent + * contract`Generalized4626Strategy` uses `IVault.strategistAddr()` + * in its implementation and there's no Vault on the remote strategy. + * So, we set the Vault address of remote to be the proxy address and + * inherit from Strategizable to get the same function signature. + * Essentially, any calls to `IVault.strategistAddr()` will be equal to + * calling `address(this).strategistAddr()`. + */ + modifier onlyGovernorOrStrategist() + override(InitializableAbstractStrategy, Strategizable) { + require( + msg.sender == strategistAddr || isGovernor(), + "Caller is not the Strategist or Governor" + ); + _; + } + constructor( BaseStrategyConfig memory _baseConfig, CCTPIntegrationConfig memory _cctpConfig @@ -91,26 +102,6 @@ contract CrossChainRemoteStrategy is ); } - /** - * @notice Set address of Strategist. - * This is important to have the function name same as IVault. - * Because the parent contract (Generalized4626Strategy) uses this - * function to get/set the strategist address. - * @param _address Address of Strategist - */ - function setStrategistAddr(address _address) external onlyGovernor { - _setStrategistAddr(_address); - } - - /** - * @dev Set the strategist address - * @param _address Address of the strategist - */ - function _setStrategistAddr(address _address) internal { - strategistAddr = _address; - emit StrategistUpdated(_address); - } - /// @inheritdoc Generalized4626Strategy function deposit(address _asset, uint256 _amount) external diff --git a/contracts/contracts/utils/InitializableAbstractStrategy.sol b/contracts/contracts/utils/InitializableAbstractStrategy.sol index 890da82e57..dbb4adb097 100644 --- a/contracts/contracts/utils/InitializableAbstractStrategy.sol +++ b/contracts/contracts/utils/InitializableAbstractStrategy.sol @@ -81,7 +81,7 @@ abstract contract InitializableAbstractStrategy is Initializable, Governable { /** * @dev Verifies that the caller is the Governor or Strategist. */ - modifier onlyGovernorOrStrategist() { + modifier onlyGovernorOrStrategist() virtual { require( isGovernor() || msg.sender == IVault(vaultAddress).strategistAddr(), "Caller is not the Strategist or Governor" From d80f7549730136e861b92784a809578a44aa618d Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Fri, 9 Jan 2026 15:05:32 +0100 Subject: [PATCH 096/101] set vault address to zero (#2742) --- contracts/deploy/deployActions.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/contracts/deploy/deployActions.js b/contracts/deploy/deployActions.js index 8387848452..ee32d7d507 100644 --- a/contracts/deploy/deployActions.js +++ b/contracts/deploy/deployActions.js @@ -1912,8 +1912,7 @@ const deployCrossChainRemoteStrategyImpl = async ( [ platformAddress, // Vault address should be same as the proxy address - proxyAddress, // vault address - // addresses.mainnet.VaultProxy, + addresses.zero, // There is no vault on the remote strategy ], [ tokenMessengerAddress, From 7bbafc3b89a1ab0f89bd1db6706fb207650c3eeb Mon Sep 17 00:00:00 2001 From: Shahul Hameed <10547529+shahthepro@users.noreply.github.com> Date: Fri, 9 Jan 2026 19:26:15 +0400 Subject: [PATCH 097/101] remove unnecessary comments --- .../crosschain/CrossChainRemoteStrategy.sol | 12 ------------ contracts/deploy/deployActions.js | 1 - 2 files changed, 13 deletions(-) diff --git a/contracts/contracts/strategies/crosschain/CrossChainRemoteStrategy.sol b/contracts/contracts/strategies/crosschain/CrossChainRemoteStrategy.sol index dd9f67a7fa..8a67332fe1 100644 --- a/contracts/contracts/strategies/crosschain/CrossChainRemoteStrategy.sol +++ b/contracts/contracts/strategies/crosschain/CrossChainRemoteStrategy.sol @@ -41,15 +41,6 @@ contract CrossChainRemoteStrategy is _; } - /** - * @dev Overriding this modifier is important because the parent - * contract`Generalized4626Strategy` uses `IVault.strategistAddr()` - * in its implementation and there's no Vault on the remote strategy. - * So, we set the Vault address of remote to be the proxy address and - * inherit from Strategizable to get the same function signature. - * Essentially, any calls to `IVault.strategistAddr()` will be equal to - * calling `address(this).strategistAddr()`. - */ modifier onlyGovernorOrStrategist() override(InitializableAbstractStrategy, Strategizable) { require( @@ -67,9 +58,6 @@ contract CrossChainRemoteStrategy is Generalized4626Strategy(_baseConfig, _cctpConfig.usdcToken) { require(usdcToken == address(assetToken), "Token mismatch"); - - // NOTE: Vault address must always be the proxy address - // so that IVault(vaultAddress).strategistAddr() works } /** diff --git a/contracts/deploy/deployActions.js b/contracts/deploy/deployActions.js index ee32d7d507..4069db4799 100644 --- a/contracts/deploy/deployActions.js +++ b/contracts/deploy/deployActions.js @@ -1911,7 +1911,6 @@ const deployCrossChainRemoteStrategyImpl = async ( await deployWithConfirmation(implementationName, [ [ platformAddress, - // Vault address should be same as the proxy address addresses.zero, // There is no vault on the remote strategy ], [ From 7d7d2f94e9e30acbfcb56a6aca38af60ccd255c8 Mon Sep 17 00:00:00 2001 From: Shahul Hameed <10547529+shahthepro@users.noreply.github.com> Date: Fri, 9 Jan 2026 19:36:11 +0400 Subject: [PATCH 098/101] Add comment --- .../strategies/crosschain/CrossChainRemoteStrategy.sol | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/contracts/contracts/strategies/crosschain/CrossChainRemoteStrategy.sol b/contracts/contracts/strategies/crosschain/CrossChainRemoteStrategy.sol index 8a67332fe1..1ba1e1b468 100644 --- a/contracts/contracts/strategies/crosschain/CrossChainRemoteStrategy.sol +++ b/contracts/contracts/strategies/crosschain/CrossChainRemoteStrategy.sol @@ -182,6 +182,12 @@ contract CrossChainRemoteStrategy is * @param _amount Amount of asset to deposit */ function _deposit(address _asset, uint256 _amount) internal override { + // By design, this function should not revert. Otherwise, it'd + // not be able to process messages and might freeze the contracts + // state. However these two require statements would never fail + // in every function invoking this. The same kind of checks should + // be enforced in all the calling functions for these two and any + // other require statements added to this function. require(_amount > 0, "Must deposit something"); require(_asset == address(usdcToken), "Unexpected asset address"); From 51d62c8b738d091dfd18b73b6c83e507ac7d0bc0 Mon Sep 17 00:00:00 2001 From: Shahul Hameed <10547529+shahthepro@users.noreply.github.com> Date: Fri, 9 Jan 2026 21:10:25 +0400 Subject: [PATCH 099/101] Add nonReentrant for deposit and withdraw methods --- .../crosschain/CrossChainRemoteStrategy.sol | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/contracts/contracts/strategies/crosschain/CrossChainRemoteStrategy.sol b/contracts/contracts/strategies/crosschain/CrossChainRemoteStrategy.sol index 1ba1e1b468..91a79d2098 100644 --- a/contracts/contracts/strategies/crosschain/CrossChainRemoteStrategy.sol +++ b/contracts/contracts/strategies/crosschain/CrossChainRemoteStrategy.sol @@ -96,12 +96,19 @@ contract CrossChainRemoteStrategy is virtual override onlyGovernorOrStrategist + nonReentrant { _deposit(_asset, _amount); } /// @inheritdoc Generalized4626Strategy - function depositAll() external virtual override onlyGovernorOrStrategist { + function depositAll() + external + virtual + override + onlyGovernorOrStrategist + nonReentrant + { _deposit(usdcToken, IERC20(usdcToken).balanceOf(address(this))); } @@ -110,12 +117,18 @@ contract CrossChainRemoteStrategy is address _recipient, address _asset, uint256 _amount - ) external virtual override onlyGovernorOrStrategist { + ) external virtual override onlyGovernorOrStrategist nonReentrant { _withdraw(_recipient, _asset, _amount); } /// @inheritdoc Generalized4626Strategy - function withdrawAll() external virtual override onlyGovernorOrStrategist { + function withdrawAll() + external + virtual + override + onlyGovernorOrStrategist + nonReentrant + { IERC4626 platform = IERC4626(platformAddress); _withdraw( address(this), From 8324c9ce740e2ccd2962e68cddbbaf01a44389bc Mon Sep 17 00:00:00 2001 From: Shahul Hameed <10547529+shahthepro@users.noreply.github.com> Date: Fri, 9 Jan 2026 21:14:23 +0400 Subject: [PATCH 100/101] Add more checks in constructor --- .../strategies/crosschain/CrossChainMasterStrategy.sol | 7 ++++++- .../strategies/crosschain/CrossChainRemoteStrategy.sol | 9 +++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/contracts/contracts/strategies/crosschain/CrossChainMasterStrategy.sol b/contracts/contracts/strategies/crosschain/CrossChainMasterStrategy.sol index 53cc86073a..4744c0750b 100644 --- a/contracts/contracts/strategies/crosschain/CrossChainMasterStrategy.sol +++ b/contracts/contracts/strategies/crosschain/CrossChainMasterStrategy.sol @@ -44,7 +44,12 @@ contract CrossChainMasterStrategy is ) InitializableAbstractStrategy(_stratConfig) AbstractCCTPIntegrator(_cctpConfig) - {} + { + require( + _stratConfig.vaultAddress != address(0), + "Invalid Vault address" + ); + } /** * @dev Initialize the strategy implementation diff --git a/contracts/contracts/strategies/crosschain/CrossChainRemoteStrategy.sol b/contracts/contracts/strategies/crosschain/CrossChainRemoteStrategy.sol index 91a79d2098..f7dbcb0263 100644 --- a/contracts/contracts/strategies/crosschain/CrossChainRemoteStrategy.sol +++ b/contracts/contracts/strategies/crosschain/CrossChainRemoteStrategy.sol @@ -58,6 +58,15 @@ contract CrossChainRemoteStrategy is Generalized4626Strategy(_baseConfig, _cctpConfig.usdcToken) { require(usdcToken == address(assetToken), "Token mismatch"); + require( + _baseConfig.platformAddress != address(0), + "Invalid platform address" + ); + // Vault address must always be address(0) for the remote strategy + require( + _baseConfig.vaultAddress == address(0), + "Invalid vault address" + ); } /** From b731385bf16b1dd0db30fa205d149b7d660fef75 Mon Sep 17 00:00:00 2001 From: Shahul Hameed <10547529+shahthepro@users.noreply.github.com> Date: Fri, 9 Jan 2026 21:23:16 +0400 Subject: [PATCH 101/101] Fix withdrawAll --- .../strategies/crosschain/CrossChainMasterStrategy.sol | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/contracts/contracts/strategies/crosschain/CrossChainMasterStrategy.sol b/contracts/contracts/strategies/crosschain/CrossChainMasterStrategy.sol index 4744c0750b..7f49b59966 100644 --- a/contracts/contracts/strategies/crosschain/CrossChainMasterStrategy.sol +++ b/contracts/contracts/strategies/crosschain/CrossChainMasterStrategy.sol @@ -107,6 +107,11 @@ contract CrossChainMasterStrategy is /// @inheritdoc InitializableAbstractStrategy function withdrawAll() external override onlyVaultOrGovernor nonReentrant { + if (isTransferPending()) { + // Do nothing if there is a pending transfer + return; + } + // Withdraw everything in Remote strategy uint256 _remoteBalance = remoteStrategyBalance; if (_remoteBalance < 1e6) { @@ -117,6 +122,7 @@ contract CrossChainMasterStrategy is _withdraw( usdcToken, vaultAddress, + // Withdraw at most the max transfer amount _remoteBalance > MAX_TRANSFER_AMOUNT ? MAX_TRANSFER_AMOUNT : _remoteBalance