From 637fb40853dd417fb5178e552f02b7d07e7e6da8 Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Fri, 5 Dec 2025 14:54:27 +0100 Subject: [PATCH 01/70] 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 02/70] 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 03/70] 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 04/70] 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 05/70] 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 06/70] 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 07/70] 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 08/70] 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 09/70] 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 10/70] 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 11/70] 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 12/70] 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 13/70] 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 14/70] 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 15/70] 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 16/70] 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 17/70] 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 18/70] 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 19/70] 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 20/70] 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 21/70] 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 22/70] 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 23/70] 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 24/70] 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 25/70] 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 26/70] 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 27/70] 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 28/70] 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 29/70] 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 30/70] 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 31/70] 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 32/70] 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 33/70] 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 34/70] 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 35/70] 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 36/70] 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 37/70] 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 38/70] 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 39/70] 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 40/70] 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 41/70] 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 42/70] 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 43/70] 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 44/70] 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 45/70] 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 46/70] 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 47/70] 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 48/70] 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 49/70] 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 50/70] 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 51/70] 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 52/70] 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 53/70] 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 54/70] 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 55/70] 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 56/70] 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 57/70] 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 58/70] 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 59/70] 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 60/70] 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 61/70] 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 62/70] 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 63/70] 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 64/70] 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 65/70] 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 66/70] 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 67/70] 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 68/70] 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 69/70] 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 70/70] 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) {