diff --git a/ethereum/brownie-config.yaml b/ethereum/brownie-config.yaml index 5488b97e..c14f304f 100644 --- a/ethereum/brownie-config.yaml +++ b/ethereum/brownie-config.yaml @@ -409,6 +409,13 @@ networks: poolid: usdc: 1 bridges: + ccip: + chain_selector: 14767482510784806043 + router: "0x554472a2720E5E7D5D3C817529aBA05EEd5F82D8" + token: + link: + address: "0x0b9d5D9136855f6FEc3c0993feE6E9CE8a297846" + decimal: 18 cctp: domain_id: 1 token_messenger: "0xeb08f243e5d3fcff26a9e38ae5520a669f4019d0" @@ -440,6 +447,12 @@ networks: pair: ETHUSD address: "0x86d67c3D38D2bCeE722E601025C25a575021c6EA" token: + CCIP-BnM: + address: "0xD21341536c5cF5EB1bcb58f6723cE26e8D8E90e4" + decimal: 18 + link: + address: "0x0b9d5D9136855f6FEc3c0993feE6E9CE8a297846" + decimal: 18 usdc: address: "0x5425890298aed601595a70AB815c96711a31Bc65" decimal: 6 @@ -528,6 +541,13 @@ networks: poolid: usdc: 1 bridges: + ccip: + chain_selector: 12532609583862916517 + router: "0x70499c328e1E2a3c41108bd3730F6670a44595D1" + token: + link: + address: "0x326C977E6efc84E512bB9C30f76E30c160eD06FB" + decimal: 18 bool: chainid: 80001 router: "0x480CdE28DBc04EcAc2d6C503FEEFC2CCCe240288" @@ -553,6 +573,12 @@ networks: decimal: 18 anytoken: "0xb576C9403f39829565BD6051695E2AC7Ecf850E2" token: + CCIP-BnM: + address: "0xf1E3A5842EeEF51F2967b3F05D45DD4f4205FF40" + decimal: 18 + link: + address: "0x326C977E6efc84E512bB9C30f76E30c160eD06FB" + decimal: 18 bool-usdc: address: "0x3b65b11a7ed7cd6ce1d2ee279389a1668435396a" decimal: 6 diff --git a/ethereum/contracts/Facets/CCIPFacet.sol b/ethereum/contracts/Facets/CCIPFacet.sol new file mode 100644 index 00000000..d733483c --- /dev/null +++ b/ethereum/contracts/Facets/CCIPFacet.sol @@ -0,0 +1,516 @@ +// SPDX-License-Identifier: GPLv3 +pragma solidity 0.8.13; + +import "@openzeppelin/contracts/utils/math/SafeMath.sol"; + +import "../Errors/GenericErrors.sol"; +import "../Libraries/LibCCIPClient.sol"; +import "../Libraries/LibDiamond.sol"; +import "../Libraries/LibBytes.sol"; +import "../Libraries/LibCross.sol"; +import "../Libraries/LibAsset.sol"; +import "../Helpers/Swapper.sol"; +import "../Helpers/ReentrancyGuard.sol"; +import "../Interfaces/ISo.sol"; +import "../Interfaces/ILibSoFee.sol"; +import "../Interfaces/ILibPrice.sol"; +import "../Interfaces/ICCIPRouterClient.sol"; +import "../Interfaces/IAny2EVMMessageReceiver.sol"; +import "../Interfaces/IERC165.sol"; + +/// @title CCIP Facet +/// @author OmniBTC +/// @notice Provides functionality for bridging through CCIP +contract CCIPFacet is Swapper, ReentrancyGuard, IAny2EVMMessageReceiver { + using SafeMath for uint256; + using LibBytes for bytes; + + /// Storage /// + + bytes32 internal constant NAMESPACE = + hex"115c77a130824400d839f1a193041dfaef0cb83dbbe297c6b2d0a2f7a794bc1e"; // keccak256("com.so.facets.ccip") + + struct Storage { + uint64 chainSelector; + address router; + mapping(uint64 => mapping(address => bool)) allowedSources; + } + + struct CCIPData { + uint64 dstChainSelector; + address dstDiamond; + address bridgeToken; + address payFeesIn; + bytes extraArgs; + } + + struct CachePayload { + ISo.NormalizedSoData soData; + LibSwap.NormalizedSwapData[] swapDataDst; + } + + /// Events /// + + // Event emitted when setup ccip storage + event CCIPFacetInitialized(uint64 chainSelector, address router); + event setCCIPFacetAllowedSource( + uint64 chainSelector, + address sender, + bool allow + ); + + // Event emitted when a message is sent to another chain. + event CCIPMessageSent( + bytes32 indexed messageId, // The unique ID of the message. + uint64 indexed dstChainSelector, // The chain selector of the destination chain. + address dstDiamond, // The address of the receiver contract on the destination chain. + address sender, // The message being sent - will be the EOA of the person sending tokens. + Client.EVMTokenAmount tokenAmount, // The token amount that was sent. + uint256 fees // The fees paid for sending the message. + ); + + // Event emitted when a message is received from another chain. + event CCIPMessageReceived( + bytes32 indexed messageId, // The unique ID of the message. + uint64 indexed srcChainSelector, // The chain selector of the source chain. + address srcDiamond, // The address of the sender from the source chain. + address receiver, // The token receiver + Client.EVMTokenAmount tokenAmount // The token amount that was sent. + ); + + /// External Methods /// + + /// @notice Initializes local variables for the ccip facet + /// @param _chainSelector ccip chain id + /// @param _router ccip router + function initCCIP(uint64 _chainSelector, address _router) external { + LibDiamond.enforceIsContractOwner(); + if (_router == address(0)) revert InvalidConfig(); + Storage storage s = getStorage(); + s.chainSelector = _chainSelector; + s.router = _router; + + // add supported interface + LibDiamond.DiamondStorage storage ds = LibDiamond.diamondStorage(); + ds.supportedInterfaces[type(IERC165).interfaceId] = true; + ds.supportedInterfaces[ + type(IAny2EVMMessageReceiver).interfaceId + ] = true; + + emit CCIPFacetInitialized(_chainSelector, _router); + } + + function setCCIPAllowedSource( + uint64 _chainSelector, + address _sender, + bool _allow + ) external { + LibDiamond.enforceIsContractOwner(); + Storage storage s = getStorage(); + s.allowedSources[_chainSelector][_sender] = _allow; + + emit setCCIPFacetAllowedSource(_chainSelector, _sender, _allow); + } + + /// @notice Bridges tokens via CCIP + /// @param soDataNo Data for tracking cross-chain transactions and a + /// portion of the accompanying cross-chain messages + /// @param swapDataSrcNo Contains a set of data required for Swap + /// transactions on the source chain side + /// @param ccipData Data used to call CCIP's router for swap + /// @param swapDataDstNo Contains a set of Swap transaction data executed + /// on the target chain. + function soSwapViaCCIP( + ISo.NormalizedSoData calldata soDataNo, + LibSwap.NormalizedSwapData[] calldata swapDataSrcNo, + CCIPData calldata ccipData, + LibSwap.NormalizedSwapData[] calldata swapDataDstNo + ) external payable nonReentrant { + uint256 bridgeAmount; + + ISo.SoData memory soData = LibCross.denormalizeSoData(soDataNo); + LibSwap.SwapData[] memory swapDataSrc = LibCross.denormalizeSwapData( + swapDataSrcNo + ); + + if (!LibAsset.isNativeAsset(soData.sendingAssetId)) { + LibAsset.depositAsset(soData.sendingAssetId, soData.amount); + } + if (swapDataSrc.length == 0) { + require(soData.sendingAssetId == ccipData.bridgeToken, "TokenErr"); + bridgeAmount = soData.amount; + } else { + require(soData.amount == swapDataSrc[0].fromAmount, "AmountErr"); + bridgeAmount = this.executeAndCheckSwaps(soData, swapDataSrc); + require( + swapDataSrc[swapDataSrc.length - 1].receivingAssetId == + ccipData.bridgeToken, + "TokenErr" + ); + } + bytes memory payload = encodeCCIPPayload(soDataNo, swapDataDstNo); + Client.EVMTokenAmount memory bridgeTokenAmount = Client.EVMTokenAmount({ + token: ccipData.bridgeToken, + amount: bridgeAmount + }); + + Client.EVMTokenAmount[] + memory tokenAmounts = new Client.EVMTokenAmount[](1); + tokenAmounts[0] = bridgeTokenAmount; + + require(bridgeAmount > 0, "bridgeAmount>0"); + _startBridge(ccipData, tokenAmounts, payload); + + emit SoTransferStarted(soData.transactionId); + } + + function ccipReceive(Client.Any2EVMMessage calldata message) + external + override + { + Storage storage s = getStorage(); + require(msg.sender == s.router, "InvalidSender"); + require( + s.allowedSources[message.sourceChainSelector][ + abi.decode(message.sender, (address)) + ], + "InvalidSource" + ); + + ( + ISo.NormalizedSoData memory soDataNo, + LibSwap.NormalizedSwapData[] memory swapDataDstNo + ) = decodeCCIPPayload(message.data); + + ISo.SoData memory soData = LibCross.denormalizeSoData(soDataNo); + LibSwap.SwapData[] memory swapDataDst = LibCross.denormalizeSwapData( + swapDataDstNo + ); + + Client.EVMTokenAmount[] memory tokenAmounts = message.destTokenAmounts; + address token = tokenAmounts[0].token; + uint256 amount = tokenAmounts[0].amount; + + uint256 soFee = getCCIPSoFee(amount); + if (soFee < amount) { + amount = amount.sub(soFee); + } + + if (swapDataDst.length == 0) { + if (soFee > 0) { + LibAsset.transferAsset( + soData.receivingAssetId, + payable(LibDiamond.contractOwner()), + soFee + ); + } + LibAsset.transferAsset( + soData.receivingAssetId, + soData.receiver, + amount + ); + emit SoTransferCompleted(soData.transactionId, amount); + } else { + if (soFee > 0) { + LibAsset.transferAsset( + swapDataDst[0].sendingAssetId, + payable(LibDiamond.contractOwner()), + soFee + ); + } + swapDataDst[0].fromAmount = amount; + + address correctSwap = appStorage.correctSwapRouterSelectors; + + if (correctSwap != address(0)) { + swapDataDst[0].callData = ICorrectSwap(correctSwap).correctSwap( + swapDataDst[0].callData, + swapDataDst[0].fromAmount + ); + } + + try this.executeAndCheckSwaps(soData, swapDataDst) returns ( + uint256 amountFinal + ) { + transferUnwrappedAsset( + swapDataDst[swapDataDst.length - 1].receivingAssetId, + soData.receivingAssetId, + amountFinal, + soData.receiver + ); + emit SoTransferCompleted(soData.transactionId, amountFinal); + } catch Error(string memory revertReason) { + LibAsset.transferAsset( + swapDataDst[0].sendingAssetId, + soData.receiver, + amount + ); + emit SoTransferFailed( + soData.transactionId, + revertReason, + bytes("") + ); + } catch (bytes memory returnData) { + LibAsset.transferAsset( + swapDataDst[0].sendingAssetId, + soData.receiver, + amount + ); + emit SoTransferFailed(soData.transactionId, "", returnData); + } + } + + emit CCIPMessageReceived( + message.messageId, + message.sourceChainSelector, + abi.decode(message.sender, (address)), + soData.receiver, + message.destTokenAmounts[0] + ); + } + + function getCCIPExtraArgs(uint256 gasLimit, bool strict) + public + view + returns (bytes memory) + { + return + Client._argsToBytes( + Client.EVMExtraArgsV1({gasLimit: gasLimit, strict: strict}) + ); + } + + function getCCIPFees( + ISo.NormalizedSoData calldata soDataNo, + LibSwap.NormalizedSwapData[] calldata swapDataDstNo, + CCIPData calldata ccipData + ) public view returns (uint256 fees) { + Storage storage s = getStorage(); + + bytes memory payload = encodeCCIPPayload(soDataNo, swapDataDstNo); + Client.EVMTokenAmount memory bridgeTokenAmount = Client.EVMTokenAmount({ + token: ccipData.bridgeToken, + amount: 0 + }); + + Client.EVMTokenAmount[] + memory tokenAmounts = new Client.EVMTokenAmount[](1); + tokenAmounts[0] = bridgeTokenAmount; + + Client.EVM2AnyMessage memory message = Client.EVM2AnyMessage({ + receiver: abi.encode(ccipData.dstDiamond), // ABI-encoded receiver contract address + data: payload, + tokenAmounts: tokenAmounts, + extraArgs: ccipData.extraArgs, + feeToken: ccipData.payFeesIn // Setting feeToken to zero address, indicating native asset will be used for fees + }); + + // Get the fee required to send the message + fees = IRouterClient(s.router).getFee( + ccipData.dstChainSelector, + message + ); + } + + /// @dev Get so fee + function getCCIPSoFee(uint256 amount) public view returns (uint256) { + Storage storage s = getStorage(); + address soFee = appStorage.gatewaySoFeeSelectors[s.router]; + if (soFee == address(0x0)) { + return 0; + } else { + return ILibSoFee(soFee).getFees(amount); + } + } + + /// CrossData + // 1. length + transactionId(SoData) + // 2. length + receiver(SoData) + // 3. length + receivingAssetId(SoData) + // 4. length + swapDataLength(u8) + // 5. length + callTo(SwapData) + // 6. length + sendingAssetId(SwapData) + // 7. length + receivingAssetId(SwapData) + // 8. length + callData(SwapData) + function encodeCCIPPayload( + ISo.NormalizedSoData memory soData, + LibSwap.NormalizedSwapData[] memory swapDataDst + ) public pure returns (bytes memory) { + bytes memory encodeData = abi.encodePacked( + uint8(soData.transactionId.length), + soData.transactionId, + uint8(soData.receiver.length), + soData.receiver, + uint8(soData.receivingAssetId.length), + soData.receivingAssetId + ); + + if (swapDataDst.length > 0) { + bytes memory swapLenBytes = LibCross.serializeU256WithHexStr( + swapDataDst.length + ); + encodeData = encodeData.concat( + abi.encodePacked(uint8(swapLenBytes.length), swapLenBytes) + ); + } + + for (uint256 i = 0; i < swapDataDst.length; i++) { + encodeData = encodeData.concat( + abi.encodePacked( + uint8(swapDataDst[i].callTo.length), + swapDataDst[i].callTo, + uint8(swapDataDst[i].sendingAssetId.length), + swapDataDst[i].sendingAssetId, + uint8(swapDataDst[i].receivingAssetId.length), + swapDataDst[i].receivingAssetId, + uint16(swapDataDst[i].callData.length), + swapDataDst[i].callData + ) + ); + } + return encodeData; + } + + /// CrossData + // 1. length + transactionId(SoData) + // 2. length + receiver(SoData) + // 3. length + receivingAssetId(SoData) + // 4. length + swapDataLength(u8) + // 5. length + callTo(SwapData) + // 6. length + sendingAssetId(SwapData) + // 7. length + receivingAssetId(SwapData) + // 8. length + callData(SwapData) + function decodeCCIPPayload(bytes memory ccipPayload) + public + pure + returns ( + ISo.NormalizedSoData memory soData, + LibSwap.NormalizedSwapData[] memory swapDataDst + ) + { + CachePayload memory data; + uint256 index; + uint256 nextLen; + + nextLen = uint256(ccipPayload.toUint8(index)); + index += 1; + data.soData.transactionId = ccipPayload.slice(index, nextLen); + index += nextLen; + + nextLen = uint256(ccipPayload.toUint8(index)); + index += 1; + data.soData.receiver = ccipPayload.slice(index, nextLen); + index += nextLen; + + nextLen = uint256(ccipPayload.toUint8(index)); + index += 1; + data.soData.receivingAssetId = ccipPayload.slice(index, nextLen); + index += nextLen; + + if (index < ccipPayload.length) { + nextLen = uint256(ccipPayload.toUint8(index)); + index += 1; + uint256 swap_len = LibCross.deserializeU256WithHexStr( + ccipPayload.slice(index, nextLen) + ); + index += nextLen; + + data.swapDataDst = new LibSwap.NormalizedSwapData[](swap_len); + for (uint256 i = 0; i < swap_len; i++) { + nextLen = uint256(ccipPayload.toUint8(index)); + index += 1; + data.swapDataDst[i].callTo = ccipPayload.slice(index, nextLen); + data.swapDataDst[i].approveTo = data.swapDataDst[i].callTo; + index += nextLen; + + nextLen = uint256(ccipPayload.toUint8(index)); + index += 1; + data.swapDataDst[i].sendingAssetId = ccipPayload.slice( + index, + nextLen + ); + index += nextLen; + + nextLen = uint256(ccipPayload.toUint8(index)); + index += 1; + data.swapDataDst[i].receivingAssetId = ccipPayload.slice( + index, + nextLen + ); + index += nextLen; + + nextLen = uint256(ccipPayload.toUint16(index)); + index += 2; + data.swapDataDst[i].callData = ccipPayload.slice( + index, + nextLen + ); + index += nextLen; + } + } + require(index == ccipPayload.length, "LenErr"); + return (data.soData, data.swapDataDst); + } + + /// Private Methods /// + + function _startBridge( + CCIPData memory ccipData, + Client.EVMTokenAmount[] memory tokenAmounts, + bytes memory payload + ) private { + Storage storage s = getStorage(); + + // note: not consider the case of multiple tokenAmounts + address bridgeToken = tokenAmounts[0].token; + uint256 bridgeAmount = tokenAmounts[0].amount; + Client.EVM2AnyMessage memory evm2AnyMessage = Client.EVM2AnyMessage({ + receiver: abi.encode(ccipData.dstDiamond), + data: payload, + tokenAmounts: tokenAmounts, + extraArgs: ccipData.extraArgs, + feeToken: ccipData.payFeesIn + }); + + // Initialize a router client instance to interact with cross-chain router + IRouterClient router = IRouterClient(s.router); + + uint256 fees = router.getFee(ccipData.dstChainSelector, evm2AnyMessage); + + if (ccipData.bridgeToken != address(0)) { + LibAsset.maxApproveERC20( + IERC20(ccipData.bridgeToken), + s.router, + bridgeAmount + ); + } + + if (ccipData.payFeesIn == address(0)) { + // Send the message through the router and store the returned message ID + bytes32 messageId = router.ccipSend{value: fees}( + ccipData.dstChainSelector, + evm2AnyMessage + ); + + emit CCIPMessageSent( + messageId, + ccipData.dstChainSelector, + ccipData.dstDiamond, + msg.sender, + tokenAmounts[0], + fees + ); + } else { + revert("InvalidFeeToken"); + } + } + + /// @dev fetch local storage + function getStorage() private pure returns (Storage storage s) { + bytes32 namespace = NAMESPACE; + // solhint-disable-next-line no-inline-assembly + assembly { + s.slot := namespace + } + } +} diff --git a/ethereum/contracts/Interfaces/IAny2EVMMessageReceiver.sol b/ethereum/contracts/Interfaces/IAny2EVMMessageReceiver.sol new file mode 100644 index 00000000..a1ffcc91 --- /dev/null +++ b/ethereum/contracts/Interfaces/IAny2EVMMessageReceiver.sol @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {Client} from "../Libraries/LibCCIPClient.sol"; + +/// @notice Application contracts that intend to receive messages from +/// the router should implement this interface. +interface IAny2EVMMessageReceiver { + /// @notice Called by the Router to deliver a message. + /// If this reverts, any token transfers also revert. The message + /// will move to a FAILED state and become available for manual execution. + /// @param message CCIP Message + /// @dev Note ensure you check the msg.sender is the OffRampRouter + function ccipReceive(Client.Any2EVMMessage calldata message) external; +} diff --git a/ethereum/contracts/Interfaces/ICCIPPool.sol b/ethereum/contracts/Interfaces/ICCIPPool.sol new file mode 100644 index 00000000..94e95f2c --- /dev/null +++ b/ethereum/contracts/Interfaces/ICCIPPool.sol @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +// Shared public interface for multiple pool types. +// Each pool type handles a different child token model (lock/unlock, mint/burn.) +interface IPool { + /// @notice Lock tokens into the pool or burn the tokens. + /// @param originalSender Original sender of the tokens. + /// @param receiver Receiver of the tokens on destination chain. + /// @param amount Amount to lock or burn. + /// @param destChainSelector Destination chain Id. + /// @param extraArgs Additional data passed in by sender for lockOrBurn processing + /// in custom pools on source chain. + /// @return retData Optional field that contains bytes. Unused for now but already + /// implemented to allow future upgrades while preserving the interface. + function lockOrBurn( + address originalSender, + bytes calldata receiver, + uint256 amount, + uint64 destChainSelector, + bytes calldata extraArgs + ) external returns (bytes memory); + + /// @notice Releases or mints tokens to the receiver address. + /// @param originalSender Original sender of the tokens. + /// @param receiver Receiver of the tokens. + /// @param amount Amount to release or mint. + /// @param sourceChainSelector Source chain Id. + /// @param extraData Additional data supplied offchain for releaseOrMint processing in + /// custom pools on dest chain. This could be an attestation that was retrieved through a + /// third party API. + /// @dev offchainData can come from any untrusted source. + function releaseOrMint( + bytes memory originalSender, + address receiver, + uint256 amount, + uint64 sourceChainSelector, + bytes memory extraData + ) external; + + /// @notice Gets the IERC20 token that this pool can lock or burn. + /// @return token The IERC20 token representation. + function getToken() external view returns (IERC20 token); +} diff --git a/ethereum/contracts/Interfaces/ICCIPRouter.sol b/ethereum/contracts/Interfaces/ICCIPRouter.sol new file mode 100644 index 00000000..643be370 --- /dev/null +++ b/ethereum/contracts/Interfaces/ICCIPRouter.sol @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {Client} from "../Libraries/LibCCIPClient.sol"; + +interface IRouter { + error OnlyOffRamp(); + + /// @notice Route the message to its intended receiver contract. + /// @param message Client.Any2EVMMessage struct. + /// @param gasForCallExactCheck of params for exec + /// @param gasLimit set of params for exec + /// @param receiver set of params for exec + /// @dev if the receiver is a contracts that signals support for CCIP execution through EIP-165. + /// the contract is called. If not, only tokens are transferred. + /// @return success A boolean value indicating whether the ccip message was received without errors. + /// @return retBytes A bytes array containing return data form CCIP receiver. + function routeMessage( + Client.Any2EVMMessage calldata message, + uint16 gasForCallExactCheck, + uint256 gasLimit, + address receiver + ) external returns (bool success, bytes memory retBytes); +} diff --git a/ethereum/contracts/Interfaces/ICCIPRouterClient.sol b/ethereum/contracts/Interfaces/ICCIPRouterClient.sol new file mode 100644 index 00000000..2d2e9e8c --- /dev/null +++ b/ethereum/contracts/Interfaces/ICCIPRouterClient.sol @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {Client} from "../Libraries/LibCCIPClient.sol"; + +interface IRouterClient { + error UnsupportedDestinationChain(uint64 destChainSelector); + error InsufficientFeeTokenAmount(); + error InvalidMsgValue(); + + /// @notice Checks if the given chain ID is supported for sending/receiving. + /// @param chainSelector The chain to check. + /// @return supported is true if it is supported, false if not. + function isChainSupported(uint64 chainSelector) + external + view + returns (bool supported); + + /// @notice Gets a list of all supported tokens which can be sent or received + /// to/from a given chain id. + /// @param chainSelector The chainSelector. + /// @return tokens The addresses of all tokens that are supported. + function getSupportedTokens(uint64 chainSelector) + external + view + returns (address[] memory tokens); + + /// @param destinationChainSelector The destination chainSelector + /// @param message The cross-chain CCIP message including data and/or tokens + /// @return fee returns guaranteed execution fee for the specified message + /// delivery to destination chain + /// @dev returns 0 fee on invalid message. + function getFee( + uint64 destinationChainSelector, + Client.EVM2AnyMessage memory message + ) external view returns (uint256 fee); + + /// @notice Request a message to be sent to the destination chain + /// @param destinationChainSelector The destination chain ID + /// @param message The cross-chain CCIP message including data and/or tokens + /// @return messageId The message ID + /// @dev Note if msg.value is larger than the required fee (from getFee) we accept + /// the overpayment with no refund. + function ccipSend( + uint64 destinationChainSelector, + Client.EVM2AnyMessage calldata message + ) external payable returns (bytes32); +} diff --git a/ethereum/contracts/Interfaces/IEVM2AnyOnRamp.sol b/ethereum/contracts/Interfaces/IEVM2AnyOnRamp.sol new file mode 100644 index 00000000..35d5be75 --- /dev/null +++ b/ethereum/contracts/Interfaces/IEVM2AnyOnRamp.sol @@ -0,0 +1,66 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {IPool} from "./ICCIPPool.sol"; + +import {Client} from "../Libraries/LibCCIPClient.sol"; +import {Internal} from "../Libraries/LibInternal.sol"; + +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +interface IEVM2AnyOnRamp { + /// @notice Get the fee for a given ccip message + /// @param message The message to calculate the cost for + /// @return fee The calculated fee + function getFee(Client.EVM2AnyMessage calldata message) + external + view + returns (uint256 fee); + + /// @notice Get the pool for a specific token + /// @param sourceToken The source chain token to get the pool for + /// @return pool Token pool + function getPoolBySourceToken(IERC20 sourceToken) + external + view + returns (IPool); + + /// @notice Gets a list of all supported source chain tokens. + /// @return tokens The addresses of all tokens that this onRamp supports for sending. + function getSupportedTokens() + external + view + returns (address[] memory tokens); + + /// @notice Gets the next sequence number to be used in the onRamp + /// @return the next sequence number to be used + function getExpectedNextSequenceNumber() external view returns (uint64); + + /// @notice Get the next nonce for a given sender + /// @param sender The sender to get the nonce for + /// @return nonce The next nonce for the sender + function getSenderNonce(address sender) + external + view + returns (uint64 nonce); + + /// @notice Adds and removed token pools. + /// @param removes The tokens and pools to be removed + /// @param adds The tokens and pools to be added. + function applyPoolUpdates( + Internal.PoolUpdate[] memory removes, + Internal.PoolUpdate[] memory adds + ) external; + + /// @notice Send a message to the remote chain + /// @dev only callable by the Router + /// @dev approve() must have already been called on the token using the this ramp address as the spender. + /// @dev if the contract is paused, this function will revert. + /// @param message Message struct to send + /// @param originalSender The original initiator of the CCIP request + function forwardFromRouter( + Client.EVM2AnyMessage memory message, + uint256 feeTokenAmount, + address originalSender + ) external returns (bytes32); +} diff --git a/ethereum/contracts/Interfaces/ILinkToken.sol b/ethereum/contracts/Interfaces/ILinkToken.sol new file mode 100644 index 00000000..04f5d2c0 --- /dev/null +++ b/ethereum/contracts/Interfaces/ILinkToken.sol @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +interface ILinkToken { + function allowance(address owner, address spender) + external + view + returns (uint256 remaining); + + function approve(address spender, uint256 value) + external + returns (bool success); + + function balanceOf(address owner) external view returns (uint256 balance); + + function decimals() external view returns (uint8 decimalPlaces); + + function decreaseApproval(address spender, uint256 addedValue) + external + returns (bool success); + + function increaseApproval(address spender, uint256 subtractedValue) + external; + + function name() external view returns (string memory tokenName); + + function symbol() external view returns (string memory tokenSymbol); + + function totalSupply() external view returns (uint256 totalTokensIssued); + + function transfer(address to, uint256 value) + external + returns (bool success); + + function transferAndCall( + address to, + uint256 value, + bytes calldata data + ) external returns (bool success); + + function transferFrom( + address from, + address to, + uint256 value + ) external returns (bool success); +} diff --git a/ethereum/contracts/Libraries/LibCCIPClient.sol b/ethereum/contracts/Libraries/LibCCIPClient.sol new file mode 100644 index 00000000..bcfe862d --- /dev/null +++ b/ethereum/contracts/Libraries/LibCCIPClient.sol @@ -0,0 +1,44 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +// End consumer library. +library Client { + struct EVMTokenAmount { + address token; // token address on the local chain. + uint256 amount; // Amount of tokens. + } + + struct Any2EVMMessage { + bytes32 messageId; // MessageId corresponding to ccipSend on source. + uint64 sourceChainSelector; // Source chain selector. + bytes sender; // abi.decode(sender) if coming from an EVM chain. + bytes data; // payload sent in original message. + EVMTokenAmount[] destTokenAmounts; // Tokens and their amounts in their destination chain representation. + } + + // If extraArgs is empty bytes, the default is 200k gas limit and strict = false. + struct EVM2AnyMessage { + bytes receiver; // abi.encode(receiver address) for dest EVM chains + bytes data; // Data payload + EVMTokenAmount[] tokenAmounts; // Token transfers + address feeToken; // Address of feeToken. address(0) means you will send msg.value. + bytes extraArgs; // Populate this with _argsToBytes(EVMExtraArgsV1) + } + + // extraArgs will evolve to support new features + // bytes4(keccak256("CCIP EVMExtraArgsV1")); + bytes4 public constant EVM_EXTRA_ARGS_V1_TAG = 0x97a657c9; + + struct EVMExtraArgsV1 { + uint256 gasLimit; // ATTENTION!!! MAX GAS LIMIT 4M FOR BETA TESTING + bool strict; // See strict sequencing details below. + } + + function _argsToBytes(EVMExtraArgsV1 memory extraArgs) + internal + pure + returns (bytes memory bts) + { + return abi.encodeWithSelector(EVM_EXTRA_ARGS_V1_TAG, extraArgs); + } +} diff --git a/ethereum/contracts/Libraries/LibInternal.sol b/ethereum/contracts/Libraries/LibInternal.sol new file mode 100644 index 00000000..ace2e48b --- /dev/null +++ b/ethereum/contracts/Libraries/LibInternal.sol @@ -0,0 +1,105 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {Client} from "./LibCCIPClient.sol"; + +//import {MerkleMultiProof} from "./LibMerkleMultiProof.sol"; + +// Library for CCIP internal definitions common to multiple contracts. +library Internal { + struct PriceUpdates { + TokenPriceUpdate[] tokenPriceUpdates; + uint64 destChainSelector; // --┐ Destination chain selector + uint192 usdPerUnitGas; // -----┘ 1e18 USD per smallest unit (e.g. wei) of destination chain gas + } + + struct TokenPriceUpdate { + address sourceToken; // Source token + uint192 usdPerToken; // 1e18 USD per smallest unit of token + } + + struct TimestampedUint192Value { + uint192 value; // -------┐ The price, in 1e18 USD. + uint64 timestamp; // ----┘ Timestamp of the most recent price update. + } + + struct PoolUpdate { + address token; // The IERC20 token address + address pool; // The token pool address + } + + struct ExecutionReport { + EVM2EVMMessage[] messages; + // Contains a bytes array for each message + // each inner bytes array contains bytes per transferred token + bytes[][] offchainTokenData; + bytes32[] proofs; + uint256 proofFlagBits; + } + + // @notice The cross chain message that gets committed to EVM chains + struct EVM2EVMMessage { + uint64 sourceChainSelector; + uint64 sequenceNumber; + uint256 feeTokenAmount; + address sender; + uint64 nonce; + uint256 gasLimit; + bool strict; + // User fields + address receiver; + bytes data; + Client.EVMTokenAmount[] tokenAmounts; + address feeToken; + bytes32 messageId; + } + + function _toAny2EVMMessage( + EVM2EVMMessage memory original, + Client.EVMTokenAmount[] memory destTokenAmounts + ) internal pure returns (Client.Any2EVMMessage memory message) { + message = Client.Any2EVMMessage({ + messageId: original.messageId, + sourceChainSelector: original.sourceChainSelector, + sender: abi.encode(original.sender), + data: original.data, + destTokenAmounts: destTokenAmounts + }); + } + + bytes32 internal constant EVM_2_EVM_MESSAGE_HASH = + keccak256("EVM2EVMMessageEvent"); + + // function _hash(EVM2EVMMessage memory original, bytes32 metadataHash) internal pure returns (bytes32) { + // return + // keccak256( + // abi.encode( + // MerkleMultiProof.LEAF_DOMAIN_SEPARATOR, + // metadataHash, + // original.sequenceNumber, + // original.nonce, + // original.sender, + // original.receiver, + // keccak256(original.data), + // keccak256(abi.encode(original.tokenAmounts)), + // original.gasLimit, + // original.strict, + // original.feeToken, + // original.feeTokenAmount + // ) + // ); + // } + + /// @notice Enum listing the possible message execution states within + /// the offRamp contract. + /// UNTOUCHED never executed + /// IN_PROGRESS currently being executed, used a replay protection + /// SUCCESS successfully executed. End state + /// FAILURE unsuccessfully executed, manual execution is now enabled. + enum MessageExecutionState { + UNTOUCHED, + IN_PROGRESS, + SUCCESS, + FAILURE + } +} diff --git a/ethereum/contracts/Libraries/LibSoFeeCCIPV1.sol b/ethereum/contracts/Libraries/LibSoFeeCCIPV1.sol new file mode 100644 index 00000000..e1cd7014 --- /dev/null +++ b/ethereum/contracts/Libraries/LibSoFeeCCIPV1.sol @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: BUSL-1.1 + +pragma solidity 0.8.13; + +import "@openzeppelin/contracts/access/Ownable.sol"; +import "@openzeppelin/contracts/utils/math/SafeMath.sol"; +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {ILibSoFee} from "../Interfaces/ILibSoFee.sol"; + +// Celer +contract LibSoFeeCCIPV1 is ILibSoFee, Ownable { + using SafeMath for uint256; + + //--------------------------------------------------------------------------- + // VARIABLES + + uint256 public constant RAY = 1e27; + + uint256 public soFee; + + constructor(uint256 _soFee) { + soFee = _soFee; + } + + function setFee(uint256 _soFee) external onlyOwner { + soFee = _soFee; + } + + function getRestoredAmount(uint256 _amountIn) + external + view + override + returns (uint256 r) + { + // calculate the amount to be restored + r = _amountIn.mul(RAY).div((RAY - soFee)); + return r; + } + + function getFees(uint256 _amountIn) + external + view + override + returns (uint256 s) + { + // calculate the so fee + s = _amountIn.mul(soFee).div(RAY); + return s; + } + + function getTransferForGas() external view override returns (uint256) { + return 0; + } + + function getVersion() external pure override returns (string memory) { + return "CCIPV1"; + } +} diff --git a/ethereum/scripts/ccip.py b/ethereum/scripts/ccip.py new file mode 100644 index 00000000..03e61a2d --- /dev/null +++ b/ethereum/scripts/ccip.py @@ -0,0 +1,767 @@ +import json +import os +import time +from random import choice + +import brownie +import ccxt +from brownie import Contract, web3 +from brownie.project.main import Project +from retrying import retry + +from helpful_scripts import ( + get_account, + zero_address, + combine_bytes, + padding_to_bytes, + Session, + get_token_address, + get_token_decimal, + get_chain_id, + get_swap_info, + to_hex_str, + get_account_address, get_ccip_chain_selector +) + +uniswap_v3_fee_decimal = 1e6 + +root_path = os.path.dirname(os.path.dirname(os.path.realpath(__file__))) + +src_session: Session = None +dst_session: Session = None + +kucoin = ccxt.kucoin() +kucoin.load_markets() + + +def get_contract(contract_name: str, p: Project = None): + return p[contract_name] + + +def get_contract_address(contract_name: str, p: Project = None): + return get_contract(contract_name, p)[-1].address + + +def token_approve( + token_name: str, aprrove_address: str, amount: int, p: Project = None +): + token = Contract.from_abi( + token_name.upper(), get_token_address(token_name), p.interface.IERC20.abi + ) + token.approve(aprrove_address, amount, {"from": get_account()}) + + +@retry +def get_token_price(token): + if token == "eth": + return float(kucoin.fetch_ticker("ETH/USDT")['close']) + elif token == "bnb": + return float(kucoin.fetch_ticker("BNB/USDT")['close']) + elif token == "matic": + return float(kucoin.fetch_ticker("MATIC/USDT")['close']) + elif token == "avax": + return float(kucoin.fetch_ticker("AVAX/USDT")['close']) + elif token == "apt": + return float(kucoin.fetch_ticker("APT/USDT")['close']) + elif token == "sui": + return float(kucoin.fetch_ticker("SUI/USDT")['close']) + + +def get_token_amount_decimal(token): + if token in ['eth', 'matic', 'bnb', 'avax']: + return 18 + elif token == 'apt': + return 8 + elif token == 'sui': + return 9 + + +def get_network_token(network): + if 'avax' in network: + return 'avax' + elif 'polygon' in network: + return 'matic' + else: + return 'eth' + + +def get_fee_value(amount, token='sui'): + price = get_token_price(token) + decimal = get_token_amount_decimal(token) + return price * amount / pow(10, decimal) + + +def get_fee_amount(value, token='sui'): + price = get_token_price(token) + decimal = get_token_amount_decimal(token) + return int(value / price * pow(10, decimal)) + + +class View: + def __repr__(self): + data = vars(self) + for k in list(data.keys()): + if not k.startswith("_"): + continue + del data[k] + return json.dumps(data, sort_keys=True, indent=4, separators=(",", ":")) + + @staticmethod + def from_dict(obj, data: dict): + return obj(**data) + + +class SoData(View): + def __init__( + self, + transactionId, + receiver, + sourceChainId, + sendingAssetId, + destinationChainId, + receivingAssetId, + amount, + ): + # unique identification id + self.transactionId = transactionId + # token receiving account + self.receiver = receiver + # source chain id + self.sourceChainId = sourceChainId + # The starting token address of the source chain + self.sendingAssetId = sendingAssetId + # destination chain id + self.destinationChainId = destinationChainId + # The final token address of the destination chain + self.receivingAssetId = receivingAssetId + # User enters amount + self.amount = amount + + def format_to_contract(self): + """Get the SoData needed for the contract interface + + Returns: + SoData: Information for recording and tracking cross-chain transactions + """ + return [ + to_hex_str(self.transactionId), + to_hex_str(self.receiver), + self.sourceChainId if self.sourceChainId < 65535 else 0, + to_hex_str(self.sendingAssetId), + self.destinationChainId if self.destinationChainId < 65535 else 0, + to_hex_str(self.receivingAssetId), + self.amount, + ] + + @staticmethod + def generate_random_bytes32(): + """Produce random transactions iD for tracking transactions on both chains + + Returns: + result: 32 bytes hex + """ + chars = [str(i) for i in range(10)] + ["a", "b", "c", "d", "e"] + result = "0x" + for _ in range(64): + result += choice(chars) + return result + + @classmethod + def create( + cls, + src_session, + dst_session, + receiver: str, + amount: int, + sendingTokenName: str, + receiveTokenName: str, + ): + """Create SoData class + + Args: + receiver (str): The final recipient of the target token + amount (int): Amount of tokens sent + sendingTokenName (str): The name of the token sent on the source chain side, like usdt etc. + receiveTokenName (str): The name of the token to the target link, like usdt etc. + + Returns: + SoData: SoData class + """ + transactionId = cls.generate_random_bytes32() + return SoData( + transactionId=transactionId, + receiver=receiver, + sourceChainId=src_session.put_task(func=get_chain_id), + sendingAssetId=src_session.put_task( + func=get_token_address, args=(sendingTokenName,) + ), + destinationChainId=dst_session.put_task(func=get_chain_id), + receivingAssetId=dst_session.put_task( + func=get_token_address, args=(receiveTokenName,) + ), + amount=amount, + ) + + +class CCIPData(View): + """ + struct CCIPData { + uint64 dstChainSelector; + address dstDiamond; + address bridgeToken; + address payFeesIn; + bytes extraArgs; + } + """ + + def __init__( + self, + dst_chain_selector, + dst_diamond, + bridge_token, + pay_fees_in=zero_address() + ): + self.dst_chain_selector = dst_chain_selector + self.dst_diamond = dst_diamond + self.bridge_token = bridge_token + self.pay_fees_in = pay_fees_in + self.extra_args = "" + + def set_extra_args(self, extra_args): + self.extra_args = extra_args + + def format_to_contract(self): + """Get the CCIP data passed into the contract interface""" + return [ + self.dst_chain_selector, + self.dst_diamond, + self.bridge_token, + self.pay_fees_in, + self.extra_args + ] + + +class SwapType: + """Interfaces that may be called""" + + IUniswapV2Router02 = "IUniswapV2Router02" + IUniswapV2Router02AVAX = "IUniswapV2Router02AVAX" + ISwapRouter = "ISwapRouter" + + +class SwapFunc: + """Swap functions that may be called""" + + swapExactETHForTokens = "swapExactETHForTokens" + swapExactAVAXForTokens = "swapExactAVAXForTokens" + swapExactTokensForETH = "swapExactTokensForETH" + swapExactTokensForAVAX = "swapExactTokensForAVAX" + swapExactTokensForTokens = "swapExactTokensForTokens" + exactInput = "exactInput" + + +class SwapData(View): + """Constructing data for calling UniswapLike""" + + def __init__( + self, + callTo, + approveTo, + sendingAssetId, + receivingAssetId, + fromAmount, + callData, + swapType: str = None, + swapFuncName: str = None, + swapPath: list = None, + swapEncodePath: list = None, + ): + # The swap address + self.callTo = callTo + # The swap address + self.approveTo = approveTo + # The swap start token address + self.sendingAssetId = sendingAssetId + # The swap final token address + self.receivingAssetId = receivingAssetId + # The swap start token amount + self.fromAmount = fromAmount + # The swap callData + self.callData = callData + self.swapType = swapType + self.swapFuncName = swapFuncName + self.swapPath = swapPath + self.swapEncodePath = swapEncodePath + + def format_to_contract(self): + """Returns the data used to pass into the contract interface""" + return [ + to_hex_str(self.callTo), + to_hex_str(self.approveTo), + to_hex_str(self.sendingAssetId, False), + to_hex_str(self.receivingAssetId, False), + self.fromAmount, + to_hex_str(self.callData), + ] + + @classmethod + def create( + cls, + swapType: str, + swapFuncName: str, + fromAmount: int, + swapPath: list, + p: Project = None, + ): + """Create SwapData class + + Args: + swapType (str): Calling the uniswap interface type + swapFuncName (str): Calling a specific function name + fromAmount (int): Input amount for Swap + swapPath (list): Token path for Swap + p (Project, optional): Load brownie project config. Defaults to None. + + Raises: + ValueError: Not support swapFuncName + + Returns: + swap_data: SwapData class + """ + if swapFuncName not in vars(SwapFunc): + raise ValueError("Not support") + swap_info = get_swap_info()[swapType] + swap_contract = Contract.from_abi( + swapType, swap_info["router"], getattr(p.interface, swapType).abi + ) + callTo = swap_contract.address + approveTo = swap_contract.address + minAmount = 0 + + if swapType == SwapType.ISwapRouter: + path = cls.encode_path_for_uniswap_v3(swapPath) + if swapFuncName == "exactInput": + if swapPath[0] == "weth": + sendingAssetId = zero_address() + else: + sendingAssetId = get_token_address(swapPath[0]) + receivingAssetId = get_token_address(swapPath[-1]) + else: + raise ValueError("Not support") + else: + path = cls.encode_path_for_uniswap_v2(swapPath) + if swapPath[0] == "weth": + sendingAssetId = zero_address() + else: + sendingAssetId = path[0] + if swapPath[-1] == "weth": + receivingAssetId = zero_address() + else: + receivingAssetId = path[-1] + + if swapFuncName in [ + SwapFunc.swapExactTokensForETH, + SwapFunc.swapExactTokensForAVAX, + SwapFunc.swapExactTokensForTokens, + ]: + callData = getattr(swap_contract, swapFuncName).encode_input( + fromAmount, + minAmount, + path, + p["SoDiamond"][-1].address, + int(time.time() + 3000), + ) + elif swapFuncName == SwapFunc.exactInput: + callData = getattr(swap_contract, swapFuncName).encode_input( + [ + path, + p["SoDiamond"][-1].address, + int(time.time() + 3000), + fromAmount, + minAmount, + ] + ) + elif swapFuncName in [ + SwapFunc.swapExactETHForTokens, + SwapFunc.swapExactAVAXForTokens, + ]: + callData = getattr(swap_contract, swapFuncName).encode_input( + minAmount, path, p["SoDiamond"][-1].address, int(time.time() + 3000) + ) + else: + raise ValueError("Not support") + swap_data = SwapData( + callTo, + approveTo, + sendingAssetId, + receivingAssetId, + fromAmount, + callData, + swapType, + swapFuncName, + swapPath, + path, + ) + return swap_data + + @staticmethod + def reset_min_amount( + callData: str, + swapType: str, + swapFuncName: str, + minAmount: int, + p: Project = None, + ): + """Resetting the min amount of dst swap based on the results of the overall slippage calculation + + Args: + callData (str): Calldata for target chain execution swap + swapType (str): Calling the uniswap interface type + swapFuncName (str): Calling a specific function name + minAmount (int): Min amount + p (Project, optional): Load brownie project config. Defaults to None. + + Raises: + ValueError: not support swapType + + Returns: + callData: Calldata after setting min amount + """ + swap_info = get_swap_info()[swapType] + swap_contract = Contract.from_abi( + swapType, swap_info["router"], getattr(p.interface, swapType).abi + ) + if swapType == SwapType.ISwapRouter and swapFuncName == "exactInput": + [params] = getattr(swap_contract, swapFuncName).decode_input(callData) + params[4] = minAmount + return getattr(swap_contract, swapFuncName).encode_input(params) + elif swapType.startswith("IUniswapV2") and swapFuncName.startswith( + "swapExactTokens" + ): + (fromAmount, _, path, to, deadline) = getattr( + swap_contract, swapFuncName + ).decode_input(callData) + return getattr(swap_contract, swapFuncName).encode_input( + fromAmount, minAmount, path, to, deadline + ) + elif swapType.startswith("IUniswapV2") and ( + swapFuncName.startswith("swapExactETH") + or swapFuncName.startswith("swapExactAVAX") + ): + (_, path, to, deadline) = getattr(swap_contract, swapFuncName).decode_input( + callData + ) + return getattr(swap_contract, swapFuncName).encode_input( + minAmount, path, to, deadline + ) + else: + raise ValueError("Not support") + + @classmethod + def encode_path_for_uniswap_v3_revert(cls, swapPath): + return cls.encode_path_for_uniswap_v3(swapPath[::-1]) + + @staticmethod + def encode_path_for_uniswap_v2(p: list): + return [get_token_address(v) for v in p] + + @staticmethod + def encode_path_for_uniswap_v3(p: list): + """ + :param p: [token, fee, token, fee, token...] + :return: + """ + assert len(p) > 0 + assert (len(p) - 3) % 2 == 0, "p length not right" + p = [ + padding_to_bytes( + web3.toHex(int(p[i] * uniswap_v3_fee_decimal)), padding="left", length=3 + ) + if (i + 1) % 2 == 0 + else get_token_address(p[i]) + for i in range(len(p)) + ] + return combine_bytes(p) + + @classmethod + def estimate_out(cls, amountIn: int, swapType: str, swapPath, p: Project = None): + """Estimate uniswap final output amount + + Args: + amountIn (int): swap input amount + swapType (str): uniswap interface type + swapPath (_type_): swap token path + p (Project, optional): Load brownie project config. Defaults to None. + + Raises: + ValueError: not support swapType + + Returns: + amountOut: final output amount + """ + account = get_account() + swap_info = get_swap_info()[swapType] + if swapType == "ISwapRouter": + swap_contract = Contract.from_abi( + "IQuoter", swap_info["quoter"], getattr(p.interface, "IQuoter").abi + ) + amountOut = swap_contract.quoteExactInput.call( + cls.encode_path_for_uniswap_v3(swapPath), amountIn, {"from": account} + ) + elif swapType.startswith("IUniswapV2"): + swap_contract = Contract.from_abi( + swapType, swap_info["router"], getattr(p.interface, swapType).abi + ) + amountOuts = swap_contract.getAmountsOut( + amountIn, cls.encode_path_for_uniswap_v2(swapPath) + ) + amountOut = amountOuts[-1] + else: + raise ValueError("Not support") + print( + f" Swap estimate out: token {swapPath[0]}, amount {amountIn / get_token_decimal(swapPath[0])} " + f"-> token {swapPath[-1]}, amount {amountOut / get_token_decimal(swapPath[-1])}" + ) + return amountOut + + @classmethod + def estimate_in(cls, amountOut: int, swapType: str, swapPath, p: Project = None): + """Estimate uniswap input amount based on output amount + + Args: + amountOut (int): uniswap output amount + swapType (str): uniswap interface type + swapPath (_type_): swap token path + p (Project, optional): load brownie project config. Defaults to None. + + Raises: + ValueError: not support swapType + + Returns: + amountIn: input amount + """ + account = get_account() + swap_info = get_swap_info()[swapType] + if swapType == "ISwapRouter": + swap_contract = Contract.from_abi( + "IQuoter", swap_info["quoter"], getattr(p.interface, "IQuoter").abi + ) + amountIn = swap_contract.quoteExactOutput.call( + cls.encode_path_for_uniswap_v3_revert(swapPath), + amountOut, + {"from": account}, + ) + elif swapType.startswith("IUniswapV2"): + swap_contract = Contract.from_abi( + swapType, swap_info["router"], getattr(p.interface, swapType).abi + ) + amountIns = swap_contract.getAmountsIn( + amountOut, cls.encode_path_for_uniswap_v2(swapPath) + ) + amountIn = amountIns[0] + else: + raise ValueError("Not support") + print( + f" Swap estimate in: token {swapPath[0]}, amount {amountIn / get_token_decimal(swapPath[0])} " + f"<- token {swapPath[-1]}, amount {amountOut / get_token_decimal(swapPath[-1])}" + ) + return amountIn + + +def get_extra_args(gas_limit=200000, strict=False, p: Project = None): + account = get_account() + proxy_diamond = Contract.from_abi( + "CCIPFacet", p["SoDiamond"][-1].address, p["CCIPFacet"].abi + ) + return str(proxy_diamond.getCCIPExtraArgs(gas_limit, strict, {"from": account})) + + +def get_ccip_fees(so_data, dst_swap_data, ccip_data, p: Project = None): + account = get_account() + proxy_diamond = Contract.from_abi( + "CCIPFacet", p["SoDiamond"][-1].address, p["CCIPFacet"].abi + ) + + return proxy_diamond.getCCIPFees( + so_data.format_to_contract(), + [] if dst_swap_data is None else [dst_swap_data.format_to_contract()], + ccip_data.format_to_contract(), + {"from": account} + ) + + +def so_swap_via_cctp(so_data, src_swap_data, ccip_data, dst_swap_data, input_value, p: Project = None): + account = get_account() + proxy_diamond = Contract.from_abi( + "CCIPFacet", p["SoDiamond"][-1].address, p["CCIPFacet"].abi + ) + + proxy_diamond.soSwapViaCCIP( + so_data.format_to_contract(), + [] if src_swap_data is None else [src_swap_data.format_to_contract()], + ccip_data.format_to_contract(), + [] if dst_swap_data is None else [dst_swap_data.format_to_contract()], + {"from": account, "value": int(input_value)}, + ) + + +def get_gas_price(): + return brownie.web3.eth.gas_price + + +def cross_swap_via_ccip( + src_session, + dst_session, + inputAmount, + sourceTokenName, + sourceSwapType, + sourceSwapFunc, + sourceSwapPath, + destinationTokenName, + destinationSwapType, + destinationSwapFunc, + destinationSwapPath, +): + print( + f"{'-' * 100}\nSwap from: network {src_session.net}, token: {sourceTokenName}\n" + f"{dst_session.net}, token: {destinationTokenName}" + ) + src_diamond_address = src_session.put_task( + get_contract_address, args=("SoDiamond",), with_project=True + ) + dst_diamond_address = dst_session.put_task( + get_contract_address, args=("SoDiamond",), with_project=True + ) + print( + f"Source diamond address: {src_diamond_address}. Destination diamond address: {dst_diamond_address}" + ) + + so_data = SoData.create( + src_session, + dst_session, + src_session.put_task(get_account_address), + amount=inputAmount, + sendingTokenName=sourceTokenName, + receiveTokenName=destinationTokenName, + ) + print("SoData\n", so_data) + + if sourceSwapType is not None: + src_swap_data = src_session.put_task( + SwapData.create, + args=(sourceSwapType, sourceSwapFunc, inputAmount, sourceSwapPath), + with_project=True, + ) + print("SourceSwapData:\n", src_swap_data) + cross_token = src_swap_data.format_to_contract()[3] + else: + src_swap_data = None + cross_token = src_session.put_task(get_token_address, args=(sourceTokenName,), with_project=False) + + if destinationSwapType is not None: + dst_swap_data = dst_session.put_task( + SwapData.create, + args=( + destinationSwapType, + destinationSwapFunc, + inputAmount, + destinationSwapPath, + ), + with_project=True, + ) + print("DstSwapData:\n", src_swap_data) + else: + dst_swap_data = None + + if sourceTokenName != "eth": + src_session.put_task( + token_approve, + args=( + sourceTokenName, + src_session.put_task( + get_contract_address, args=("SoDiamond",), with_project=True + ), + inputAmount, + ), + with_project=True, + ) + input_eth_amount = 0 + else: + input_eth_amount = inputAmount + + dst_chain_selector = dst_session.put_task(get_ccip_chain_selector, with_project=False) + ccip_data = CCIPData(dst_chain_selector, dst_diamond_address, cross_token) + extra_args = src_session.put_task(get_extra_args, (500000,), with_project=True) + ccip_data.set_extra_args(extra_args) + ccip_fees = src_session.put_task(get_ccip_fees, args=( + so_data, dst_swap_data, ccip_data,), with_project=True) + + input_value = input_eth_amount + ccip_fees + + print(f"Input value: {input_value}") + src_session.put_task(so_swap_via_cctp, args=( + so_data, src_swap_data, ccip_data, dst_swap_data, input_value + ), with_project=True) + + +def main(src_net="avax-test", dst_net="polygon-test"): + global src_session + global dst_session + src_session = Session( + net=src_net, project_path=root_path, name=src_net, daemon=False + ) + dst_session = Session( + net=dst_net, project_path=root_path, name=dst_net, daemon=False + ) + + # without swap + # cross_swap_via_ccip( + # src_session=src_session, + # dst_session=dst_session, + # inputAmount=int(0.1 * 1e18), + # sourceTokenName="CCIP-BnM", + # sourceSwapType=None, + # sourceSwapFunc=None, + # sourceSwapPath=None, + # destinationTokenName="CCIP-BnM", + # destinationSwapType=None, + # destinationSwapFunc=None, + # destinationSwapPath=None, + # ) + + # with src swap + # cross_swap_via_ccip( + # src_session=src_session, + # dst_session=dst_session, + # inputAmount=int(0.1 * 1e6), + # sourceTokenName="usdc", + # sourceSwapType=SwapType.IUniswapV2Router02AVAX, + # sourceSwapFunc=SwapFunc.swapExactTokensForTokens, + # sourceSwapPath=("usdc", "CCIP-BnM"), + # destinationTokenName="CCIP-BnM", + # destinationSwapType=None, + # destinationSwapFunc=None, + # destinationSwapPath=None, + # ) + + # with dst swap + cross_swap_via_ccip( + src_session=src_session, + dst_session=dst_session, + inputAmount=int(0.1 * 1e18), + sourceTokenName="CCIP-BnM", + sourceSwapType=None, + sourceSwapFunc=None, + sourceSwapPath=None, + destinationTokenName="usdc", + destinationSwapType=SwapType.IUniswapV2Router02, + destinationSwapFunc=SwapFunc.swapExactTokensForTokens, + destinationSwapPath=("CCIP-BnM", "usdc"), + ) + + src_session.terminate() + dst_session.terminate() + + +if __name__ == "__main__": + main() diff --git a/ethereum/scripts/deploy.py b/ethereum/scripts/deploy.py index ee763732..9be3e105 100755 --- a/ethereum/scripts/deploy.py +++ b/ethereum/scripts/deploy.py @@ -8,13 +8,9 @@ GenericSwapFacet, LibCorrectSwapV1, SerdeFacet, - BoolFacet, - LibSoFeeBoolV1, network, - CCTPFacet, - LibSoFeeCCTPV1, - StargateFacet, - LibSoFeeStargateV1 + LibSoFeeCCIPV1, + CCIPFacet, ) from brownie.network import priority_fee, max_fee @@ -36,7 +32,8 @@ def deploy_contracts(account): DiamondCutFacet, DiamondLoupeFacet, DexManagerFacet, - StargateFacet, + CCIPFacet, + # StargateFacet, # CCTPFacet, # CelerFacet, # MultiChainFacet, @@ -55,13 +52,12 @@ def deploy_contracts(account): SoDiamond.deploy(account, DiamondCutFacet[-1], {"from": account}) so_fee = 1e-3 - - print("deploy LibSoFeeStargateV1.sol...") - transfer_for_gas = 30000 - LibSoFeeStargateV1.deploy(int(so_fee * 1e18), transfer_for_gas, {"from": account}) - ray = 1e27 + print("deploy LibSoFeeCCIPV1.sol...") + # transfer_for_gas = 30000 + LibSoFeeCCIPV1.deploy(int(so_fee * ray), {"from": account}) + # print("deploy LibSoFeeCelerV1.sol...") # LibSoFeeCelerV1.deploy(int(so_fee * ray), {"from": account}) # diff --git a/ethereum/scripts/helpful_scripts.py b/ethereum/scripts/helpful_scripts.py index 0c1ef7be..7e1f865d 100644 --- a/ethereum/scripts/helpful_scripts.py +++ b/ethereum/scripts/helpful_scripts.py @@ -16,6 +16,7 @@ "matic-fork", ] + def write_json(file: Path, data): f = file.parent f.mkdir(parents=True, exist_ok=True) @@ -216,6 +217,22 @@ def get_current_net_info(): return config["networks"][network.show_active()] +def get_ccip_info(): + return get_current_net_info()["bridges"]["ccip"] + + +def get_ccip_chain_selector(): + return get_ccip_info()["chain_selector"] + + +def get_ccip_router(): + return get_ccip_info()["router"] + + +def get_ccip_token_address(token_name): + return get_ccip_info()["token"][token_name]['address'] + + def get_cctp_info(): return get_current_net_info()['bridges']['cctp'] diff --git a/ethereum/scripts/initialize.py b/ethereum/scripts/initialize.py index 61582055..37035a95 100644 --- a/ethereum/scripts/initialize.py +++ b/ethereum/scripts/initialize.py @@ -22,8 +22,9 @@ LibSoFeeCelerV1, MultiChainFacet, LibSoFeeMultiChainV1, - LibSoFeeCCTPV1, CCTPFacet, + CCIPFacet, + LibSoFeeCCIPV1, LibSoFeeBoolV2, config ) from brownie.network import priority_fee, max_fee @@ -48,6 +49,8 @@ zero_address, get_stargate_router, get_stargate_chain_id, + get_ccip_router, + get_ccip_chain_selector, get_token_address, get_swap_info, get_token_decimal, @@ -80,9 +83,13 @@ def main(): except Exception as e: print(f"initialize_cut fail:{e}") try: - initialize_stargate(account, so_diamond) + initialize_ccip(account, so_diamond) except Exception as e: - print(f"initialize_stargate fail:{e}") + print(f"initialize_ccip fail:{e}") + # try: + # initialize_stargate(account, so_diamond) + # except Exception as e: + # print(f"initialize_stargate fail:{e}") # try: # initialize_bool(account, so_diamond) # except Exception as e: @@ -185,9 +192,10 @@ def initialize_cut(account, so_diamond): DiamondLoupeFacet, DexManagerFacet, OwnershipFacet, + CCIPFacet, # CelerFacet, # MultiChainFacet, - StargateFacet, + # StargateFacet, # BoolFacet, # CCTPFacet, # WormholeFacet, @@ -224,6 +232,17 @@ def initialize_stargate(account, so_diamond): ) +def initialize_ccip(account, so_diamond): + proxy_ccip = Contract.from_abi( + "CCIPFacet", so_diamond.address, CCIPFacet.abi + ) + net = network.show_active() + print(f"network:{net}, init ccip...") + proxy_ccip.initCCIP( + get_ccip_chain_selector(), get_ccip_router(), {"from": account} + ) + + def initialize_bool(account, so_diamond): proxy_bool = Contract.from_abi( "BoolFacet", so_diamond.address, BoolFacet.abi @@ -398,9 +417,12 @@ def initialize_dex_manager(account, so_diamond): proxy_dex.batchSetFunctionApprovalBySignature(sigs, True, {"from": account}) # register fee lib proxy_dex.addFee( - get_stargate_router(), LibSoFeeStargateV1[-1].address, {"from": account} + get_ccip_router(), LibSoFeeCCIPV1[-1].address, {"from": account} ) # proxy_dex.addFee( + # get_stargate_router(), LibSoFeeStargateV1[-1].address, {"from": account} + # ) + # proxy_dex.addFee( # get_bool_router(), LibSoFeeBoolV2[-1].address, {"from": account} # ) # proxy_dex.addFee( @@ -441,6 +463,15 @@ def redeploy_serde(): add_cut([SerdeFacet]) +def redeploy_ccip(): + account = get_account() + remove_facet(CCIPFacet) + + CCIPFacet.deploy({"from": account}) + add_cut([CCIPFacet]) + initialize_ccip(account, SoDiamond[-1]) + + def redeploy_cctp(): if "arbitrum-test" in network.show_active(): priority_fee("1 gwei") diff --git a/ethereum/scripts/publish.py b/ethereum/scripts/publish.py index 54632b53..c9c5a560 100644 --- a/ethereum/scripts/publish.py +++ b/ethereum/scripts/publish.py @@ -21,7 +21,7 @@ def main(net: str = None): p.load_config() change_network(net) deployed_contract = [ - "BoolFacet", + "CCIPFacet", # "GenericSwapFacet", # "SerdeFacet", # "SoDiamond", @@ -40,4 +40,4 @@ def main(net: str = None): if __name__ == "__main__": - main("optimism-main") + main("polygon-test")