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/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/CrossChainMasterStrategyMock.sol b/contracts/contracts/mocks/crosschain/CrossChainMasterStrategyMock.sol new file mode 100644 index 0000000000..8ed3c46c7b --- /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() public view returns (address) { + return _remoteAddress; + } + + function setRemoteAddress(address __remoteAddress) public { + _remoteAddress = __remoteAddress; + } +} diff --git a/contracts/contracts/mocks/crosschain/CrossChainRemoteStrategyMock.sol b/contracts/contracts/mocks/crosschain/CrossChainRemoteStrategyMock.sol new file mode 100644 index 0000000000..43deb9f34c --- /dev/null +++ b/contracts/contracts/mocks/crosschain/CrossChainRemoteStrategyMock.sol @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +/** + * @title OUSD Yearn V3 Remote Strategy Mock - the Mainnet part + * @author Origin Protocol Inc + */ + +contract CrossChainRemoteStrategyMock { + address public _masterAddress; + + constructor() {} + + function masterAddress() public view returns (address) { + return _masterAddress; + } + + function setMasterAddress(address __masterAddress) public { + _masterAddress = __masterAddress; + } +} diff --git a/contracts/contracts/proxies/InitializeGovernedUpgradeabilityProxy2.sol b/contracts/contracts/proxies/InitializeGovernedUpgradeabilityProxy2.sol new file mode 100644 index 0000000000..250acbe782 --- /dev/null +++ b/contracts/contracts/proxies/InitializeGovernedUpgradeabilityProxy2.sol @@ -0,0 +1,22 @@ +// 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 a6055cb95a..03227c25a8 100644 --- a/contracts/contracts/proxies/Proxies.sol +++ b/contracts/contracts/proxies/Proxies.sol @@ -2,6 +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 diff --git a/contracts/contracts/proxies/create2/CrossChainStrategyProxy.sol b/contracts/contracts/proxies/create2/CrossChainStrategyProxy.sol new file mode 100644 index 0000000000..a5feec929b --- /dev/null +++ b/contracts/contracts/proxies/create2/CrossChainStrategyProxy.sol @@ -0,0 +1,23 @@ +// 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. + */ +contract CrossChainStrategyProxy is InitializeGovernedUpgradeabilityProxy2 { + constructor(address governor) + InitializeGovernedUpgradeabilityProxy2(governor) + {} +} diff --git a/contracts/contracts/strategies/Generalized4626Strategy.sol b/contracts/contracts/strategies/Generalized4626Strategy.sol index 1e5d850740..e847b96009 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"); @@ -147,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/AbstractCCTP4626Strategy.sol b/contracts/contracts/strategies/crosschain/AbstractCCTP4626Strategy.sol new file mode 100644 index 0000000000..89b23c1dac --- /dev/null +++ b/contracts/contracts/strategies/crosschain/AbstractCCTP4626Strategy.sol @@ -0,0 +1,112 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +/** + * @title AbstractCCTP4626Strategy - Abstract contract for CCTP morpho strategy + * @author Origin Protocol Inc + */ + +import { AbstractCCTPIntegrator } from "./AbstractCCTPIntegrator.sol"; + +abstract contract AbstractCCTP4626Strategy is AbstractCCTPIntegrator { + uint32 public constant DEPOSIT_MESSAGE = 1; + uint32 public constant WITHDRAW_MESSAGE = 2; + uint32 public constant BALANCE_CHECK_MESSAGE = 3; + + constructor(CCTPIntegrationConfig memory _config) + AbstractCCTPIntegrator(_config) + {} + + function _encodeDepositMessage(uint64 nonce, uint256 depositAmount) + internal + virtual + returns (bytes memory) + { + return + abi.encodePacked( + ORIGIN_MESSAGE_VERSION, + DEPOSIT_MESSAGE, + abi.encode(nonce, depositAmount) + ); + } + + function _decodeDepositMessage(bytes memory message) + internal + virtual + returns (uint64, uint256) + { + _verifyMessageVersionAndType( + message, + ORIGIN_MESSAGE_VERSION, + DEPOSIT_MESSAGE + ); + + (uint64 nonce, uint256 depositAmount) = abi.decode( + _getMessagePayload(message), + (uint64, uint256) + ); + return (nonce, depositAmount); + } + + function _encodeWithdrawMessage(uint64 nonce, uint256 withdrawAmount) + internal + virtual + returns (bytes memory) + { + return + abi.encodePacked( + ORIGIN_MESSAGE_VERSION, + WITHDRAW_MESSAGE, + abi.encode(nonce, withdrawAmount) + ); + } + + function _decodeWithdrawMessage(bytes memory message) + internal + virtual + returns (uint64, uint256) + { + _verifyMessageVersionAndType( + message, + ORIGIN_MESSAGE_VERSION, + WITHDRAW_MESSAGE + ); + + (uint64 nonce, uint256 withdrawAmount) = abi.decode( + _getMessagePayload(message), + (uint64, uint256) + ); + return (nonce, withdrawAmount); + } + + function _encodeBalanceCheckMessage(uint64 nonce, uint256 balance) + internal + virtual + returns (bytes memory) + { + return + abi.encodePacked( + ORIGIN_MESSAGE_VERSION, + BALANCE_CHECK_MESSAGE, + abi.encode(nonce, balance) + ); + } + + function _decodeBalanceCheckMessage(bytes memory message) + internal + virtual + returns (uint64, uint256) + { + _verifyMessageVersionAndType( + message, + ORIGIN_MESSAGE_VERSION, + BALANCE_CHECK_MESSAGE + ); + + (uint64 nonce, uint256 balance) = abi.decode( + _getMessagePayload(message), + (uint64, uint256) + ); + return (nonce, balance); + } +} diff --git a/contracts/contracts/strategies/crosschain/AbstractCCTPIntegrator.sol b/contracts/contracts/strategies/crosschain/AbstractCCTPIntegrator.sol new file mode 100644 index 0000000000..fb18feb0de --- /dev/null +++ b/contracts/contracts/strategies/crosschain/AbstractCCTPIntegrator.sol @@ -0,0 +1,296 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +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 { AbstractCCTPMessageRelayer } from "./AbstractCCTPMessageRelayer.sol"; +import "../../utils/Helpers.sol"; + +abstract contract AbstractCCTPIntegrator is + Governable, + IMessageHandlerV2, + AbstractCCTPMessageRelayer +{ + using SafeERC20 for IERC20; + + using BytesHelper for bytes; + + event CCTPMinFinalityThresholdSet(uint32 minFinalityThreshold); + event CCTPFeePremiumBpsSet(uint32 feePremiumBps); + + // 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; + // Threshold imposed by the CCTP + uint256 public constant MAX_TRANSFER_AMOUNT = 10_000_000 * 10**6; // 10M USDC + + // Nonce of the last known deposit or withdrawal + uint64 public lastTransferNonce; + + mapping(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" + ); + _; + } + + struct CCTPIntegrationConfig { + address cctpTokenMessenger; + address cctpMessageTransmitter; + uint32 destinationDomain; + address destinationStrategy; + address baseToken; + } + + constructor(CCTPIntegrationConfig memory _config) + AbstractCCTPMessageRelayer( + _config.cctpMessageTransmitter, + _config.cctpTokenMessenger + ) + { + 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) + 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, + // solhint-disable-next-line no-unused-vars + uint32 finalityThresholdExecuted, + bytes memory messageBody + ) internal returns (bool) { + // // Make sure that the finality threshold is same on both chains + // // TODO: Do we really need this? Also, fix this + // require( + // finalityThresholdExecuted >= minFinalityThreshold, + // "Finality threshold too low" + // ); + require(sourceDomain == 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 _sendTokens(uint256 tokenAmount, bytes memory hookData) + internal + virtual + { + require(tokenAmount <= MAX_TRANSFER_AMOUNT, "Token amount too high"); + + IERC20(baseToken).safeApprove(address(cctpTokenMessenger), tokenAmount); + + // TODO: figure out why getMinFeeAmount is not on CCTP v2 contract + // Ref: https://developers.circle.com/cctp/evm-smart-contracts#getminfeeamount + // The issue is that the getMinFeeAmount is not present on v2.0 contracts, but is on + // v2.1. We will only be using standard transfers and fee on those is 0. + + uint256 maxFee = feePremiumBps > 0 + ? (tokenAmount * feePremiumBps) / 10000 + : 0; + + cctpTokenMessenger.depositForBurnWithHook( + tokenAmount, + destinationDomain, + bytes32(uint256(uint160(destinationStrategy))), + address(baseToken), + bytes32(uint256(uint160(destinationStrategy))), + maxFee, + minFinalityThreshold, + hookData + ); + } + + function _getMessageVersion(bytes memory message) + internal + virtual + returns (uint32) + { + // uint32 bytes 0 to 4 is Origin message version + // uint32 bytes 4 to 8 is Message type + return message.extractSlice(0, 4).decodeUint32(); + } + + function _getMessageType(bytes memory message) + internal + virtual + returns (uint32) + { + // uint32 bytes 0 to 4 is Origin message version + // uint32 bytes 4 to 8 is Message type + return message.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 + returns (bytes memory) + { + // uint32 bytes 0 to 4 is Origin message version + // uint32 bytes 4 to 8 is Message type + // Payload starts at byte 8 + return message.extractSlice(8, message.length); + } + + function _sendMessage(bytes memory message) internal virtual { + cctpMessageTransmitter.sendMessage( + destinationDomain, + bytes32(uint256(uint160(destinationStrategy))), + bytes32(uint256(uint160(destinationStrategy))), + 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/AbstractCCTPMessageRelayer.sol b/contracts/contracts/strategies/crosschain/AbstractCCTPMessageRelayer.sol new file mode 100644 index 0000000000..955fc4b581 --- /dev/null +++ b/contracts/contracts/strategies/crosschain/AbstractCCTPMessageRelayer.sol @@ -0,0 +1,183 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +import { ICCTPTokenMessenger, ICCTPMessageTransmitter } from "../../interfaces/cctp/ICCTP.sol"; +import { BytesHelper } from "../../utils/BytesHelper.sol"; + +abstract contract AbstractCCTPMessageRelayer { + using BytesHelper for bytes; + + // CCTP Message Header fields + // Ref: https://developers.circle.com/cctp/technical-guide#message-header + uint8 private constant VERSION_INDEX = 0; + uint8 private constant SOURCE_DOMAIN_INDEX = 4; + uint8 private constant SENDER_INDEX = 44; + uint8 private constant RECIPIENT_INDEX = 76; + uint8 private constant MESSAGE_BODY_INDEX = 148; + + // Message body V2 fields + // Ref: https://developers.circle.com/cctp/technical-guide#message-body + // Ref: https://github.com/circlefin/evm-cctp-contracts/blob/master/src/messages/v2/BurnMessageV2.sol + uint8 private constant BURN_MESSAGE_V2_VERSION_INDEX = 0; + uint8 private constant BURN_MESSAGE_V2_RECIPIENT_INDEX = 36; + uint8 private constant BURN_MESSAGE_V2_AMOUNT_INDEX = 68; + uint8 private constant BURN_MESSAGE_V2_MESSAGE_SENDER_INDEX = 100; + uint8 private constant BURN_MESSAGE_V2_FEE_EXECUTED_INDEX = 164; + uint8 private constant BURN_MESSAGE_V2_HOOK_DATA_INDEX = 228; + + bytes32 private constant EMPTY_NONCE = bytes32(0); + uint32 private constant EMPTY_FINALITY_THRESHOLD_EXECUTED = 0; + + uint32 public constant CCTP_MESSAGE_VERSION = 1; + uint32 public constant ORIGIN_MESSAGE_VERSION = 1010; + + // 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; + + constructor(address _cctpMessageTransmitter, address _cctpTokenMessenger) { + cctpMessageTransmitter = ICCTPMessageTransmitter( + _cctpMessageTransmitter + ); + cctpTokenMessenger = ICCTPTokenMessenger(_cctpTokenMessenger); + } + + function _decodeMessageHeader(bytes memory message) + internal + pure + returns ( + uint32 version, + uint32 sourceDomainID, + address sender, + address recipient, + bytes memory messageBody + ) + { + version = message + .extractSlice(VERSION_INDEX, VERSION_INDEX + 4) + .decodeUint32(); + sourceDomainID = message + .extractSlice(SOURCE_DOMAIN_INDEX, SOURCE_DOMAIN_INDEX + 4) + .decodeUint32(); + // Address of MessageTransmitterV2 caller on source domain + sender = abi.decode( + message.extractSlice(SENDER_INDEX, SENDER_INDEX + 32), + (address) + ); + // Address to handle message body on destination domain + recipient = abi.decode( + message.extractSlice(RECIPIENT_INDEX, RECIPIENT_INDEX + 32), + (address) + ); + messageBody = message.extractSlice(MESSAGE_BODY_INDEX, message.length); + } + + function relay(bytes memory message, bytes memory attestation) external { + ( + uint32 version, + // solhint-disable-next-line no-unused-vars + uint32 sourceDomainID, + address sender, + address recipient, + bytes memory messageBody + ) = _decodeMessageHeader(message); + + // Ensure that it's a CCTP message + require( + version == CCTP_MESSAGE_VERSION, + "Invalid CCTP message version" + ); + + // Ensure message body version + 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 + require( + version == 1 && + messageBody.length >= BURN_MESSAGE_V2_HOOK_DATA_INDEX, + "Invalid burn message" + ); + + // Address of caller of depositForBurn (or depositForBurnWithCaller) on source domain + bytes memory messageSender = messageBody.extractSlice( + BURN_MESSAGE_V2_MESSAGE_SENDER_INDEX, + BURN_MESSAGE_V2_MESSAGE_SENDER_INDEX + 32 + ); + sender = abi.decode(messageSender, (address)); + + bytes memory recipientSlice = messageBody.extractSlice( + BURN_MESSAGE_V2_RECIPIENT_INDEX, + BURN_MESSAGE_V2_RECIPIENT_INDEX + 32 + ); + // 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"); + require(sender == address(this), "Incorrect sender/recipient address"); + + // Relay the message + // This step also mints USDC and transfers it to the recipient wallet + bool relaySuccess = cctpMessageTransmitter.receiveMessage( + message, + attestation + ); + require(relaySuccess, "Receive message failed"); + + if (isBurnMessageV1) { + bytes memory hookData = messageBody.extractSlice( + BURN_MESSAGE_V2_HOOK_DATA_INDEX, + messageBody.length + ); + + bytes memory amountSlice = messageBody.extractSlice( + BURN_MESSAGE_V2_AMOUNT_INDEX, + BURN_MESSAGE_V2_AMOUNT_INDEX + 32 + ); + uint256 tokenAmount = abi.decode(amountSlice, (uint256)); + + bytes memory feeSlice = messageBody.extractSlice( + BURN_MESSAGE_V2_FEE_EXECUTED_INDEX, + BURN_MESSAGE_V2_FEE_EXECUTED_INDEX + 32 + ); + uint256 feeExecuted = abi.decode(feeSlice, (uint256)); + + _onTokenReceived(tokenAmount - feeExecuted, feeExecuted, hookData); + } + } + + /** + * @dev Called when the USDC is received from the CCTP + * @param tokenAmount The actual amount of USDC received (amount sent - fee executed) + * @param feeExecuted The fee executed + * @param payload The payload of the message (hook data) + */ + function _onTokenReceived( + uint256 tokenAmount, + uint256 feeExecuted, + bytes memory payload + ) internal virtual; + + /** + * @dev Called when the message is received + * @param payload The payload of the message + */ + function _onMessageReceived(bytes memory payload) internal virtual; +} diff --git a/contracts/contracts/strategies/crosschain/CrossChainMasterStrategy.sol b/contracts/contracts/strategies/crosschain/CrossChainMasterStrategy.sol new file mode 100644 index 0000000000..10ee86d1d4 --- /dev/null +++ b/contracts/contracts/strategies/crosschain/CrossChainMasterStrategy.sol @@ -0,0 +1,265 @@ +// SPDX-License-Identifier: BUSL-1.1 +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"; +import { IERC20, InitializableAbstractStrategy } from "../../utils/InitializableAbstractStrategy.sol"; +import { AbstractCCTP4626Strategy } from "./AbstractCCTP4626Strategy.sol"; +import { BytesHelper } from "../../utils/BytesHelper.sol"; + +contract CrossChainMasterStrategy is + InitializableAbstractStrategy, + AbstractCCTP4626Strategy +{ + 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; + + event RemoteStrategyBalanceUpdated(uint256 balance); + + /** + * @param _stratConfig The platform and OToken vault addresses + */ + constructor( + BaseStrategyConfig memory _stratConfig, + CCTPIntegrationConfig memory _cctpConfig + ) + InitializableAbstractStrategy(_stratConfig) + AbstractCCTP4626Strategy(_cctpConfig) + {} + + // /** + // * @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(_recipient == vaultAddress, "Only Vault can withdraw"); + + _withdraw(_asset, _recipient, _amount); + } + + /** + * @dev Remove all assets from platform and send them to Vault contract. + */ + function withdrawAll() external override onlyVaultOrGovernor nonReentrant { + uint256 balance = IERC20(baseToken).balanceOf(address(this)); + _withdraw(baseToken, vaultAddress, balance); + } + + /** + * @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) + { + 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; + } + + /** + * @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 _asset == baseToken; + } + + /** + * @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 == BALANCE_CHECK_MESSAGE) { + // Received when Remote strategy checks the balance + _processBalanceCheckMessage(payload); + } else { + revert("Unknown message type"); + } + } + + 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 { + // Expecting a BALANCE_CHECK_MESSAGE + _onMessageReceived(payload); + } + + function _deposit(address _asset, uint256 depositAmount) internal virtual { + require(_asset == baseToken, "Unsupported asset"); + require(!isTransferPending(), "Transfer already pending"); + require(pendingAmount == 0, "Unexpected pending amount"); + require(depositAmount > 0, "Deposit amount must be greater than 0"); + require( + depositAmount <= MAX_TRANSFER_AMOUNT, + "Deposit amount exceeds max transfer amount" + ); + + uint64 nonce = _getNextNonce(); + + // Set pending amount + pendingAmount = depositAmount; + + // Send deposit message with payload + bytes memory message = _encodeDepositMessage(nonce, depositAmount); + _sendTokens(depositAmount, message); + emit Deposit(_asset, _asset, depositAmount); + } + + 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(!isTransferPending(), "Transfer already pending"); + require( + _amount <= MAX_TRANSFER_AMOUNT, + "Withdraw amount exceeds max transfer amount" + ); + + uint64 nonce = _getNextNonce(); + + emit Withdrawal(baseToken, baseToken, _amount); + + // Send withdrawal message with payload + bytes memory message = _encodeWithdrawMessage(nonce, _amount); + _sendMessage(message); + } + + /** + * @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 _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 (!isNonceProcessed(nonce)) { + _markNonceAsProcessed(nonce); + + // Effect of confirming a deposit, reset pending amount + pendingAmount = 0; + + uint256 usdcBalance = IERC20(baseToken).balanceOf(address(this)); + // Effect of confirming a withdrawal + if (usdcBalance > 1e6) { + IERC20(baseToken).safeTransfer(vaultAddress, usdcBalance); + } + } + } +} diff --git a/contracts/contracts/strategies/crosschain/CrossChainRemoteStrategy.sol b/contracts/contracts/strategies/crosschain/CrossChainRemoteStrategy.sol new file mode 100644 index 0000000000..653671cf1a --- /dev/null +++ b/contracts/contracts/strategies/crosschain/CrossChainRemoteStrategy.sol @@ -0,0 +1,256 @@ +// 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 + * + * @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"; +import { IERC20 } from "../../utils/InitializableAbstractStrategy.sol"; +import { IERC4626 } from "../../../lib/openzeppelin/interfaces/IERC4626.sol"; +import { Generalized4626Strategy } from "../Generalized4626Strategy.sol"; +import { AbstractCCTP4626Strategy } from "./AbstractCCTP4626Strategy.sol"; + +contract CrossChainRemoteStrategy is + AbstractCCTP4626Strategy, + Generalized4626Strategy +{ + event DepositFailed(string reason); + event WithdrawFailed(string reason); + + using SafeERC20 for IERC20; + + constructor( + BaseStrategyConfig memory _baseConfig, + CCTPIntegrationConfig memory _cctpConfig + ) + AbstractCCTP4626Strategy(_cctpConfig) + Generalized4626Strategy(_baseConfig, _cctpConfig.baseToken) + {} + + // solhint-disable-next-line no-unused-vars + 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, + address, + uint256 + ) 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) { + // Received when Master strategy requests a withdrawal + _processWithdrawMessage(payload); + } else { + revert("Unknown message type"); + } + } + + 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 { + // 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 + require(!isNonceProcessed(nonce), "Nonce already processed"); + _markNonceAsProcessed(nonce); + + // 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 = _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 + ); + + // Replay protection + require(!isNonceProcessed(nonce), "Nonce already processed"); + _markNonceAsProcessed(nonce); + + // Withdraw funds from the remote strategy + _withdraw(address(this), baseToken, withdrawAmount); + + // Check balance after withdrawal + uint256 balanceAfter = checkBalance(baseToken); + bytes memory message = _encodeBalanceCheckMessage( + lastTransferNonce, + balanceAfter + ); + + // Send the complete balance on the contract. If we were to send only the + // withdrawn amount, the call could revert if the balance is not sufficient. + // Or dust could be left on the contract that is hard to extract. + uint256 usdcBalance = IERC20(baseToken).balanceOf(address(this)); + if (usdcBalance > 1e6) { + _sendTokens(usdcBalance, message); + } else { + _sendMessage(message); + } + } + + /** + * @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( + 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 { + // TODO: Add permissioning + uint256 balance = checkBalance(baseToken); + bytes memory message = _encodeBalanceCheckMessage( + lastTransferNonce, + balance + ); + _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/contracts/utils/BytesHelper.sol b/contracts/contracts/utils/BytesHelper.sol new file mode 100644 index 0000000000..29906c2547 --- /dev/null +++ b/contracts/contracts/utils/BytesHelper.sol @@ -0,0 +1,35 @@ +// 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 + ) internal 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; + } + + 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 new file mode 100644 index 0000000000..d13f925ae1 --- /dev/null +++ b/contracts/deploy/base/040_crosschain_strategy_proxies.js @@ -0,0 +1,21 @@ +const { deployOnBase } = require("../../utils/deploy-l2"); +const { deployProxyWithCreateX } = require("../deployActions"); + +module.exports = deployOnBase( + { + deployName: "040_crosschain_strategy_proxies", + }, + async () => { + // 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( + salt, + "CrossChainStrategyProxy" + ); + 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..d79500e4c6 --- /dev/null +++ b/contracts/deploy/base/041_crosschain_strategy.js @@ -0,0 +1,51 @@ +const { deployOnBase } = require("../../utils/deploy-l2"); +const addresses = require("../../utils/addresses"); +const { deployCrossChainRemoteStrategyImpl } = require("../deployActions"); +const { 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( + `CrossChainStrategyProxy address: ${addresses.CrossChainStrategyProxy}` + ); + + const implAddress = await deployCrossChainRemoteStrategyImpl( + "0xbeeF010f9cb27031ad51e3333f9aF9C6B1228183", // 4626 Vault + addresses.CrossChainStrategyProxy, + cctpDomainIds.Ethereum, + addresses.CrossChainStrategyProxy, + addresses.base.USDC, + "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( + cCrossChainRemoteStrategy.connect(sDeployer).safeApproveAllTokens() + ); + + return { + actions: [], + }; + } +); diff --git a/contracts/deploy/deployActions.js b/contracts/deploy/deployActions.js index 94692f1a98..0b5d93430a 100644 --- a/contracts/deploy/deployActions.js +++ b/contracts/deploy/deployActions.js @@ -17,13 +17,20 @@ const { isHoodi, isHoodiOrFork, } = require("../test/helpers.js"); -const { deployWithConfirmation, withConfirmation } = require("../utils/deploy"); +const { + deployWithConfirmation, + verifyContractOnEtherscan, + 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 +1689,188 @@ const deploySonicSwapXAMOStrategyImplementation = async () => { return cSonicSwapXAMOStrategy; }; +// deploys an instance of InitializeGovernedUpgradeabilityProxy where address is defined by salt +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}`); + + const cCreateX = await ethers.getContractAt(createxAbi, addresses.createX); + const factoryEncodedSalt = encodeSaltForCreateX(deployerAddr, false, salt); + + const getFactoryBytecode = async () => { + // No deployment needed—get factory directly from artifacts + const ProxyContract = await ethers.getContractFactory(proxyName); + const encodedArgs = ProxyContract.interface.encodeDeploy([deployerAddr]); + return ethers.utils.hexConcat([ProxyContract.bytecode, encodedArgs]); + }; + + const txResponse = await withConfirmation( + cCreateX + .connect(sDeployer) + .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 + .find((event) => event.topics[0] === contractCreationTopic) + .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; +}; + +// deploys and initializes the CrossChain master strategy +const deployCrossChainMasterStrategyImpl = async ( + proxyAddress, + targetDomainId, + remoteStrategyAddress, + baseToken, + implementationName = "CrossChainMasterStrategy", + skipInitialize = false +) => { + const { deployerAddr } = await getNamedAccounts(); + const sDeployer = await ethers.provider.getSigner(deployerAddr); + log(`Deploying CrossChainMasterStrategyImpl as deployer ${deployerAddr}`); + + const cCrossChainStrategyProxy = await ethers.getContractAt( + "CrossChainStrategyProxy", + proxyAddress + ); + + const dCrossChainMasterStrategy = await deployWithConfirmation( + implementationName, + [ + [ + addresses.zero, // platform address + // TODO: change to the actual vault address + deployerAddr, // vault address + // addresses.mainnet.VaultProxy, + ], + [ + addresses.CCTPTokenMessengerV2, + addresses.CCTPMessageTransmitterV2, + targetDomainId, + remoteStrategyAddress, + baseToken, + ], + ] + ); + + 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() + ) + ); + } + + return dCrossChainMasterStrategy.address; +}; + +// deploys and initializes the CrossChain remote strategy +const deployCrossChainRemoteStrategyImpl = async ( + platformAddress, + proxyAddress, + targetDomainId, + remoteStrategyAddress, + baseToken, + implementationName = "CrossChainRemoteStrategy" +) => { + const { deployerAddr } = await getNamedAccounts(); + const sDeployer = await ethers.provider.getSigner(deployerAddr); + log(`Deploying CrossChainRemoteStrategyImpl as deployer ${deployerAddr}`); + + 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, + ], + ] + ); + + // 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]( + dCrossChainRemoteStrategy.address, + // 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() + ) + ); + + return dCrossChainRemoteStrategy.address; +}; + module.exports = { deployOracles, deployCore, @@ -1719,4 +1908,7 @@ module.exports = { deployPlumeMockRoosterAMOStrategyImplementation, getPlumeContracts, deploySonicSwapXAMOStrategyImplementation, + deployProxyWithCreateX, + deployCrossChainMasterStrategyImpl, + deployCrossChainRemoteStrategyImpl, }; 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/160_crosschain_strategy_proxies.js b/contracts/deploy/mainnet/160_crosschain_strategy_proxies.js new file mode 100644 index 0000000000..6ab1f07c19 --- /dev/null +++ b/contracts/deploy/mainnet/160_crosschain_strategy_proxies.js @@ -0,0 +1,25 @@ +const { deploymentWithGovernanceProposal } = require("../../utils/deploy"); +const { deployProxyWithCreateX } = require("../deployActions"); + +module.exports = deploymentWithGovernanceProposal( + { + deployName: "160_crosschain_strategy_proxies", + forceDeploy: false, + reduceQueueTime: true, + deployerIsProposer: false, + proposalId: "", + }, + async () => { + // 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( + salt, + "CrossChainStrategyProxy" + ); + console.log(`CrossChainStrategyProxy address: ${proxyAddress}`); + + return { + actions: [], + }; + } +); diff --git a/contracts/deploy/mainnet/161_crosschain_strategy.js b/contracts/deploy/mainnet/161_crosschain_strategy.js new file mode 100644 index 0000000000..b28e8503a8 --- /dev/null +++ b/contracts/deploy/mainnet/161_crosschain_strategy.js @@ -0,0 +1,53 @@ +const { + deploymentWithGovernanceProposal, + withConfirmation, +} = require("../../utils/deploy"); +const addresses = require("../../utils/addresses"); +const { cctpDomainIds } = require("../../utils/cctp"); +const { deployCrossChainMasterStrategyImpl } = require("../deployActions"); + +module.exports = deploymentWithGovernanceProposal( + { + deployName: "161_crosschain_strategy", + forceDeploy: false, + reduceQueueTime: true, + deployerIsProposer: false, + proposalId: "", + }, + async () => { + const { deployerAddr } = await getNamedAccounts(); + const sDeployer = await ethers.provider.getSigner(deployerAddr); + + console.log( + `CrossChainStrategyProxy address: ${addresses.CrossChainStrategyProxy}` + ); + + const implAddress = await deployCrossChainMasterStrategyImpl( + addresses.CrossChainStrategyProxy, + cctpDomainIds.Base, + // Same address for both master and remote strategy + addresses.CrossChainStrategyProxy, + addresses.mainnet.USDC, + "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 + ) + ); + + return { + actions: [], + }; + } +); diff --git a/contracts/test/_fixture-base.js b/contracts/test/_fixture-base.js index 028e10d850..3f4e016e95 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,18 @@ const bridgeHelperModuleFixture = deployments.createFixture(async () => { }; }); +const crossChainFixture = deployments.createFixture(async () => { + const fixture = await defaultBaseFixture(); + const crossChainRemoteStrategy = await ethers.getContractAt( + "CrossChainRemoteStrategy", + addresses.CrossChainStrategyProxy + ); + return { + ...fixture, + crossChainRemoteStrategy, + }; +}); + mocha.after(async () => { if (snapshotId) { await nodeRevert(snapshotId); @@ -347,4 +361,5 @@ module.exports = { MINTER_ROLE, BURNER_ROLE, bridgeHelperModuleFixture, + crossChainFixture, }; diff --git a/contracts/test/_fixture.js b/contracts/test/_fixture.js index 86cd1ecc8e..004a287d47 100644 --- a/contracts/test/_fixture.js +++ b/contracts/test/_fixture.js @@ -16,6 +16,10 @@ const { fundAccountsForOETHUnitTests, } = require("../utils/funding"); const { deployWithConfirmation } = require("../utils/deploy"); +const { + deployCrossChainMasterStrategyImpl, + deployCrossChainRemoteStrategyImpl, +} = require("../deploy/deployActions.js"); const { replaceContractAt } = require("../utils/hardhat"); const { @@ -2525,6 +2529,58 @@ 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("CrossChainStrategyProxy", [ + deployerAddr, + ]); + const masterProxyAddress = masterProxy.address; + log(`CrossChainStrategyProxy address: ${masterProxyAddress}`); + let implAddress = await deployCrossChainMasterStrategyImpl( + masterProxyAddress, + "CrossChainMasterStrategyMock" + ); + log(`CrossChainMasterStrategyMockImpl address: ${implAddress}`); + + // deploy remote strategy + const remoteProxy = await deployWithConfirmation("CrossChainStrategyProxy", [ + deployerAddr, + ]); + + const remoteProxyAddress = remoteProxy.address; + log(`CrossChainStrategyProxy address: ${remoteProxyAddress}`); + + implAddress = await deployCrossChainRemoteStrategyImpl( + remoteProxyAddress, + "CrossChainRemoteStrategyMock" + ); + log(`CrossChainRemoteStrategyMockImpl address: ${implAddress}`); + + const yearnMasterStrategy = await ethers.getContractAt( + "CrossChainMasterStrategyMock", + masterProxyAddress + ); + const yearnRemoteStrategy = await ethers.getContractAt( + "CrossChainRemoteStrategyMock", + remoteProxyAddress + ); + + await yearnMasterStrategy + .connect(sDeployer) + .setRemoteAddress(remoteProxyAddress); + await yearnRemoteStrategy + .connect(sDeployer) + .setMasterAddress(masterProxyAddress); + + fixture.yearnMasterStrategy = yearnMasterStrategy; + fixture.yearnRemoteStrategy = yearnRemoteStrategy; + return fixture; +} + /** * Configure a reborn hack attack */ @@ -2858,6 +2914,20 @@ async function enableExecutionLayerGeneralPurposeRequests() { }; } +async function crossChainFixture() { + const fixture = await defaultFixture(); + + const cCrossChainMasterStrategy = await ethers.getContractAt( + "CrossChainMasterStrategy", + addresses.CrossChainStrategyProxy + ); + + return { + ...fixture, + 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. @@ -2950,4 +3020,6 @@ module.exports = { bridgeHelperModuleFixture, beaconChainFixture, claimRewardsModuleFixture, + yearnCrossChainFixture, + crossChainFixture, }; 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..329f76f28f --- /dev/null +++ b/contracts/test/strategies/crosschain/crosschain-master-strategy.mainnet.fork-test.js @@ -0,0 +1,159 @@ +const { expect } = require("chai"); + +const { usdcUnits, isCI } = require("../../helpers"); +const { createFixtureLoader, crossChainFixture } = require("../../_fixture"); +const { impersonateAndFund } = require("../../../utils/signers"); +// const { formatUnits } = require("ethers/lib/utils"); +const addresses = require("../../../utils/addresses"); + +const loadFixture = createFixtureLoader(crossChainFixture); + +const 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); + + // Retry up to 3 times on CI + this.retries(isCI ? 3 : 0); + + let fixture; + beforeEach(async () => { + fixture = await loadFixture(); + }); + + const decodeDepositForBurnEvent = (event) => { + const [ + amount, + mintRecipient, + destinationDomain, + destinationTokenMessenger, + destinationCaller, + maxFee, + hookData, + ] = ethers.utils.defaultAbiCoder.decode( + [ + "uint256", + "address", + "uint32", + "address", + "address", + "uint256", + "bytes", + ], + event.data + ); + + 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); + 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 usdcBalanceBefore = await usdc.balanceOf( + crossChainMasterStrategy.address + ); + const strategyBalanceBefore = await crossChainMasterStrategy.checkBalance( + usdc.address + ); + + // Simulate deposit call + const tx = await crossChainMasterStrategy + .connect(impersonatedVault) + .deposit(usdc.address, usdcUnits("1000")); + + 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() + ); + + // TODO: Check Hook Data + // expect(burnEventData.hookData).to.eq(""); + }); + + it.skip("Should handle attestation relay", async function () { + const { crossChainMasterStrategy } = fixture; + const attestation = + "0xc0ee7623da7bad1b2607f12c21ce71c4314b4ade3d36a0e6e13753fbb0603daa2b10fcbbc4942ce75a2b8d5f5c11f4b6c5ee5f8dce4663d3ec834674d0a9991a1cdeb52adf17d5fb3222b1f94f0767175f06e69f9473e7f948a4b5c478814f11915ed64081cbe6e139fd277630b8807b56be7c355ccdda6c20acbf0324231fc8301b"; + const message = + "0x0000000100000006000000000384bc6f6bfe10f6df4967b6ad287d897ff729f0c7e43f73a1e18ab156e96bfb0000000000000000000000008ebcca1066d15ad901927ab01c7c6d0b057bbd340000000000000000000000008ebcca1066d15ad901927ab01c7c6d0b057bbd3400000000000000000000000030f8a2fc7d7098061c94f042b2e7e732f95af40f00000000000003e8000003f20000000300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"; + + await crossChainMasterStrategy.relay(message, attestation); + }); +}); 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..2293d484e7 --- /dev/null +++ b/contracts/test/strategies/crosschain/crosschain-remote-strategy.base.fork-test.js @@ -0,0 +1,44 @@ +// const { expect } = require("chai"); + +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 loadFixture = createFixtureLoader(crossChainFixture); + +describe("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 { crossChainRemoteStrategy } = 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 d7f4d34b84..e1babc54df 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 = {}; @@ -444,6 +449,8 @@ addresses.base.CCIPRouter = "0x881e3A65B4d4a04dD529061dd0071cf975F58bCD"; addresses.base.MerklDistributor = "0x8BB4C975Ff3c250e0ceEA271728547f3802B36Fd"; +addresses.base.USDC = "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913"; + // Sonic addresses.sonic.wS = "0x039e2fB66102314Ce7b64Ce5Ce3E5183bc94aD38"; addresses.sonic.WETH = "0x309C92261178fA0CF748A855e90Ae73FDb79EBc7"; @@ -677,4 +684,13 @@ addresses.hoodi.beaconChainDepositContract = addresses.hoodi.defenderRelayer = "0x419B6BdAE482f41b8B194515749F3A2Da26d583b"; addresses.hoodi.mockBeaconRoots = "0xdCfcAE4A084AA843eE446f400B23aA7B6340484b"; +// Crosschain Strategy + +addresses.CrossChainStrategyProxy = + "0x98F1bA152a6Bf039F5630CD74a8Ec2cC5EA9AEd3"; +addresses.mainnet.CrossChainStrategyProxy = + "0x98F1bA152a6Bf039F5630CD74a8Ec2cC5EA9AEd3"; +addresses.base.CrossChainStrategyProxy = + "0x98F1bA152a6Bf039F5630CD74a8Ec2cC5EA9AEd3"; + module.exports = addresses; 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, +}; diff --git a/contracts/utils/deploy.js b/contracts/utils/deploy.js index bd75e0bba0..8461aa5165 100644 --- a/contracts/utils/deploy.js +++ b/contracts/utils/deploy.js @@ -328,6 +328,11 @@ const _verifyProxyInitializedWithCorrectGovernor = (transactionData) => { return; } + if (isMainnet || isBase || isFork || isBaseFork) { + // TODO: Skip verification for Fork for now + return; + } + const initProxyGovernor = ( "0x" + transactionData.slice(10 + 64 + 24, 10 + 64 + 64) ).toLowerCase();