From 637fb40853dd417fb5178e552f02b7d07e7e6da8 Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Fri, 5 Dec 2025 14:54:27 +0100 Subject: [PATCH 01/24] 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/24] 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/24] 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/24] 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/24] 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/24] 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/24] 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/24] 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/24] 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/24] 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/24] 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/24] 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/24] 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/24] 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/24] 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/24] 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/24] 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/24] 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/24] 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/24] 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/24] 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/24] 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/24] 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/24] 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";