From 9870fed896306003afcf10b1591c4f3ee7f7b18b Mon Sep 17 00:00:00 2001 From: Ed Zynda Date: Mon, 10 Feb 2025 16:29:47 +0300 Subject: [PATCH 01/55] basic implementation --- config/chainflip.json | 8 + docs/ChainflipFacet.md | 92 ++++++++++ script/demoScripts/demoChainflip.ts | 132 +++++++++++++++ .../deploy/facets/DeployChainflipFacet.s.sol | 34 ++++ .../deploy/facets/UpdateChainflipFacet.s.sol | 28 +++ src/Facets/ChainflipFacet.sol | 160 ++++++++++++++++++ src/Interfaces/IChainflip.sol | 22 +++ test/solidity/Facets/ChainflipFacet.t.sol | 141 +++++++++++++++ 8 files changed, 617 insertions(+) create mode 100644 config/chainflip.json create mode 100644 docs/ChainflipFacet.md create mode 100644 script/demoScripts/demoChainflip.ts create mode 100644 script/deploy/facets/DeployChainflipFacet.s.sol create mode 100644 script/deploy/facets/UpdateChainflipFacet.s.sol create mode 100644 src/Facets/ChainflipFacet.sol create mode 100644 src/Interfaces/IChainflip.sol create mode 100644 test/solidity/Facets/ChainflipFacet.t.sol diff --git a/config/chainflip.json b/config/chainflip.json new file mode 100644 index 000000000..c589d50ea --- /dev/null +++ b/config/chainflip.json @@ -0,0 +1,8 @@ +{ + "mainnet": { + "chainflipVault": "0xF5e10380213880111522dd0efD3dbb45b9f62Bcc" + }, + "arbitrum": { + "chainflipVault": "0x79001a5e762f3bEFC8e5871b42F6734e00498920" + } +} diff --git a/docs/ChainflipFacet.md b/docs/ChainflipFacet.md new file mode 100644 index 000000000..fe010ea39 --- /dev/null +++ b/docs/ChainflipFacet.md @@ -0,0 +1,92 @@ +# Chainflip Facet + +## How it works + +The Chainflip Facet works by ... + +```mermaid +graph LR; + D{LiFiDiamond}-- DELEGATECALL -->ChainflipFacet; + ChainflipFacet -- CALL --> C(Chainflip) +``` + +## Public Methods + +- `function startBridgeTokensViaChainflip(BridgeData calldata _bridgeData, ChainflipData calldata _chainflipData)` + - Simply bridges tokens using chainflip +- `swapAndStartBridgeTokensViaChainflip(BridgeData memory _bridgeData, LibSwap.SwapData[] calldata _swapData, chainflipData memory _chainflipData)` + - Performs swap(s) before bridging tokens using chainflip + +## chainflip Specific Parameters + +The methods listed above take a variable labeled `_chainflipData`. This data is specific to chainflip and is represented as the following struct type: + +```solidity +/// @param example Example parameter. +struct chainflipData { + string example; +} +``` + +## Swap Data + +Some methods accept a `SwapData _swapData` parameter. + +Swapping is performed by a swap specific library that expects an array of calldata to can be run on various DEXs (i.e. Uniswap) to make one or multiple swaps before performing another action. + +The swap library can be found [here](../src/Libraries/LibSwap.sol). + +## LiFi Data + +Some methods accept a `BridgeData _bridgeData` parameter. + +This parameter is strictly for analytics purposes. It's used to emit events that we can later track and index in our subgraphs and provide data on how our contracts are being used. `BridgeData` and the events we can emit can be found [here](../src/Interfaces/ILiFi.sol). + +## Getting Sample Calls to interact with the Facet + +In the following some sample calls are shown that allow you to retrieve a populated transaction that can be sent to our contract via your wallet. + +All examples use our [/quote endpoint](https://apidocs.li.fi/reference/get_quote) to retrieve a quote which contains a `transactionRequest`. This request can directly be sent to your wallet to trigger the transaction. + +The quote result looks like the following: + +```javascript +const quoteResult = { + id: '0x...', // quote id + type: 'lifi', // the type of the quote (all lifi contract calls have the type "lifi") + tool: 'chainflip', // the bridge tool used for the transaction + action: {}, // information about what is going to happen + estimate: {}, // information about the estimated outcome of the call + includedSteps: [], // steps that are executed by the contract as part of this transaction, e.g. a swap step and a cross step + transactionRequest: { + // the transaction that can be sent using a wallet + data: '0x...', + to: '0x...', + value: '0x00', + from: '{YOUR_WALLET_ADDRESS}', + chainId: 100, + gasLimit: '0x...', + gasPrice: '0x...', + }, +} +``` + +A detailed explanation on how to use the /quote endpoint and how to trigger the transaction can be found [here](https://docs.li.fi/products/more-integration-options/li.fi-api/transferring-tokens-example). + +**Hint**: Don't forget to replace `{YOUR_WALLET_ADDRESS}` with your real wallet address in the examples. + +### Cross Only + +To get a transaction for a transfer from 30 USDC.e on Avalanche to USDC on Binance you can execute the following request: + +```shell +curl 'https://li.quest/v1/quote?fromChain=AVA&fromAmount=30000000&fromToken=USDC&toChain=BSC&toToken=USDC&slippage=0.03&allowBridges=chainflip&fromAddress={YOUR_WALLET_ADDRESS}' +``` + +### Swap & Cross + +To get a transaction for a transfer from 30 USDT on Avalanche to USDC on Binance you can execute the following request: + +```shell +curl 'https://li.quest/v1/quote?fromChain=AVA&fromAmount=30000000&fromToken=USDT&toChain=BSC&toToken=USDC&slippage=0.03&allowBridges=chainflip&fromAddress={YOUR_WALLET_ADDRESS}' +``` diff --git a/script/demoScripts/demoChainflip.ts b/script/demoScripts/demoChainflip.ts new file mode 100644 index 000000000..1338c1128 --- /dev/null +++ b/script/demoScripts/demoChainflip.ts @@ -0,0 +1,132 @@ +import { getContract, parseUnits, Narrow, zeroAddress } from 'viem' +import { randomBytes } from 'crypto' +import dotenv from 'dotenv' +import erc20Artifact from '../../out/ERC20/ERC20.sol/ERC20.json' +import chainflipFacetArtifact from '../../out/ChainflipFacet.sol/ChainflipFacet.json' +import { ChainflipFacet, ILiFi } from '../../typechain' +import { SupportedChain } from './utils/demoScriptChainConfig' +import { + ensureBalance, + ensureAllowance, + executeTransaction, + setupEnvironment, +} from './utils/demoScriptHelpers' + +dotenv.config() + +// #region ABIs + +const ERC20_ABI = erc20Artifact.abi as Narrow +const CHAINFLIP_FACET_ABI = chainflipFacetArtifact.abi as Narrow< + typeof chainflipFacetArtifact.abi +> + +// #endregion + +dotenv.config() + +async function main() { + // === Set up environment === + const srcChain: SupportedChain = 'mainnet' // Set source chain + const destinationChainId = 1 // Set destination chain id + + const { + client, + publicClient, + walletAccount, + lifiDiamondAddress, + lifiDiamondContract, + } = await setupEnvironment(srcChain, CHAINFLIP_FACET_ABI) + const signerAddress = walletAccount.address + + // === Contract addresses === + const SRC_TOKEN_ADDRESS = '' as `0x${string}` // Set the source token address here. + + // If you need to retrieve a specific address from your config file + // based on the chain and element name, use this helper function. + // + // First, ensure you import the relevant config file: + // import config from '../../config/chainflip.json' + // + // Then, retrieve the address: + // const EXAMPLE_ADDRESS = getConfigElement(config, srcChain, 'example'); + // + + // === Instantiate contracts === + const srcTokenContract = getContract({ + address: SRC_TOKEN_ADDRESS, + abi: ERC20_ABI, + client, + }) + + // If you need to interact with a contract, use the following helper. + // Provide the contract address, ABI, and a client instance to initialize + // the contract for both read and write operations. + // + // const exampleContract = getContract({ + // address: EXAMPLE_ADDRESS, + // abi: EXAMPLE_ABI, + // client + // }); + // + + const srcTokenName = (await srcTokenContract.read.name()) as string + const srcTokenSymbol = (await srcTokenContract.read.symbol()) as string + const srcTokenDecimals = (await srcTokenContract.read.decimals()) as bigint + const amount = parseUnits('10', srcTokenDecimals) // 10 * 1e{source token decimals} + + console.info( + `\nBridge ${amount} ${srcTokenName} (${srcTokenSymbol}) from ${srcChain} --> {DESTINATION CHAIN NAME}` + ) + console.info(`Connected wallet address: ${signerAddress}`) + + await ensureBalance(srcTokenContract, signerAddress, amount) + + await ensureAllowance( + srcTokenContract, + signerAddress, + lifiDiamondAddress, + amount, + publicClient + ) + + // === In this part put necessary logic usually it's fetching quotes, estimating fees, signing messages etc. === + + // === Prepare bridge data === + const bridgeData: ILiFi.BridgeDataStruct = { + // Edit fields as needed + transactionId: `0x${randomBytes(32).toString('hex')}`, + bridge: 'chainflip', + integrator: 'ACME Devs', + referrer: zeroAddress, + sendingAssetId: SRC_TOKEN_ADDRESS, + receiver: signerAddress, + destinationChainId, + minAmount: amount, + hasSourceSwaps: false, + hasDestinationCall: false, + } + + const chainflipData: ChainflipFacet.ChainflipDataStruct = { + // Add your specific fields for Chainflip here. + } + + // === Start bridging === + await executeTransaction( + () => + lifiDiamondContract.write.startBridgeTokensViaChainflip( + [bridgeData, chainflipData] + // { value: fee } optional value + ), + 'Starting bridge tokens via Chainflip', + publicClient, + true + ) +} + +main() + .then(() => process.exit(0)) + .catch((error) => { + console.error(error) + process.exit(1) + }) diff --git a/script/deploy/facets/DeployChainflipFacet.s.sol b/script/deploy/facets/DeployChainflipFacet.s.sol new file mode 100644 index 000000000..ee45e1a3c --- /dev/null +++ b/script/deploy/facets/DeployChainflipFacet.s.sol @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.17; + +import { DeployScriptBase } from "./utils/DeployScriptBase.sol"; +import { stdJson } from "forge-std/Script.sol"; +import { ChainflipFacet } from "lifi/Facets/ChainflipFacet.sol"; + +contract DeployScript is DeployScriptBase { + using stdJson for string; + + constructor() DeployScriptBase("ChainflipFacet") {} + + function run() + public + returns (ChainflipFacet deployed, bytes memory constructorArgs) + { + constructorArgs = getConstructorArgs(); + + deployed = ChainflipFacet(deploy(type(ChainflipFacet).creationCode)); + } + + function getConstructorArgs() internal override returns (bytes memory) { + // get path of chainflip config file + string memory path = string.concat(root, "/config/chainflip.json"); + + // Read the Chainflip vault address from config + address chainflipVault = _getConfigContractAddress( + path, + string.concat(".", network, ".chainflipVault") + ); + + return abi.encode(chainflipVault); + } +} diff --git a/script/deploy/facets/UpdateChainflipFacet.s.sol b/script/deploy/facets/UpdateChainflipFacet.s.sol new file mode 100644 index 000000000..6494b149f --- /dev/null +++ b/script/deploy/facets/UpdateChainflipFacet.s.sol @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.17; + +import { UpdateScriptBase } from "./utils/UpdateScriptBase.sol"; +import { stdJson } from "forge-std/StdJson.sol"; +import { DiamondCutFacet, IDiamondCut } from "lifi/Facets/DiamondCutFacet.sol"; +import { ChainflipFacet } from "lifi/Facets/ChainflipFacet.sol"; + +contract DeployScript is UpdateScriptBase { + using stdJson for string; + + function run() + public + returns (address[] memory facets, bytes memory cutData) + { + return update("ChainflipFacet"); + } + + function getExcludes() internal pure override returns (bytes4[] memory) { + // No functions to exclude + return new bytes4[](0); + } + + function getCallData() internal override returns (bytes memory) { + // No initialization needed + return new bytes(0); + } +} diff --git a/src/Facets/ChainflipFacet.sol b/src/Facets/ChainflipFacet.sol new file mode 100644 index 000000000..94cc39d50 --- /dev/null +++ b/src/Facets/ChainflipFacet.sol @@ -0,0 +1,160 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.17; + +import { ILiFi } from "../Interfaces/ILiFi.sol"; +import { LibDiamond } from "../Libraries/LibDiamond.sol"; +import { LibAsset } from "../Libraries/LibAsset.sol"; +import { LibSwap } from "../Libraries/LibSwap.sol"; +import { ReentrancyGuard } from "../Helpers/ReentrancyGuard.sol"; +import { SwapperV2 } from "../Helpers/SwapperV2.sol"; +import { Validatable } from "../Helpers/Validatable.sol"; +import { IChainflipVault } from "../Interfaces/IChainflip.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { console } from "forge-std/console.sol"; + +/// @title Chainflip Facet +/// @author LI.FI (https://li.fi) +/// @notice Allows bridging assets via Chainflip +/// @custom:version 1.0.0 +contract ChainflipFacet is ILiFi, ReentrancyGuard, SwapperV2, Validatable { + address public immutable chainflipVault; + + uint256 private constant CHAIN_ID_ETHEREUM = 1; + uint256 private constant CHAIN_ID_ARBITRUM = 42161; + uint256 private constant CHAIN_ID_SOLANA = 1151111081099710; + uint256 private constant CHAIN_ID_BITCOIN = 20000000000001; + + uint32 private constant CHAINFLIP_ID_ETHEREUM = 1; + uint32 private constant CHAINFLIP_ID_ARBITRUM = 4; + uint32 private constant CHAINFLIP_ID_SOLANA = 5; + uint32 private constant CHAINFLIP_ID_BITCOIN = 3; + + /// Types /// + + /// @dev Parameters specific to Chainflip bridge + /// @param dstToken Token to be received on the destination chain (uint32) + /// @param cfParameters Additional metadata for future features (currently unused) + struct ChainflipData { + uint32 dstToken; + bytes cfParameters; + } + + /// Constructor /// + + /// @notice Constructor for the contract. + /// @param _chainflipVault Address of the Chainflip vault contract + constructor(address _chainflipVault) { + chainflipVault = _chainflipVault; + } + + /// External Methods /// + + /// @notice Bridges tokens via Chainflip + /// @param _bridgeData The core information needed for bridging + /// @param _chainflipData Data specific to Chainflip + function startBridgeTokensViaChainflip( + ILiFi.BridgeData memory _bridgeData, + ChainflipData calldata _chainflipData + ) + external + payable + nonReentrant + refundExcessNative(payable(msg.sender)) + validateBridgeData(_bridgeData) + doesNotContainSourceSwaps(_bridgeData) + doesNotContainDestinationCalls(_bridgeData) + { + LibAsset.depositAsset( + _bridgeData.sendingAssetId, + _bridgeData.minAmount + ); + _startBridge(_bridgeData, _chainflipData); + } + + /// @notice Performs a swap before bridging via Chainflip + /// @param _bridgeData The core information needed for bridging + /// @param _swapData An array of swap related data for performing swaps before bridging + /// @param _chainflipData Data specific to Chainflip + function swapAndStartBridgeTokensViaChainflip( + ILiFi.BridgeData memory _bridgeData, + LibSwap.SwapData[] calldata _swapData, + ChainflipData calldata _chainflipData + ) + external + payable + nonReentrant + refundExcessNative(payable(msg.sender)) + containsSourceSwaps(_bridgeData) + doesNotContainDestinationCalls(_bridgeData) + validateBridgeData(_bridgeData) + { + _bridgeData.minAmount = _depositAndSwap( + _bridgeData.transactionId, + _bridgeData.minAmount, + _swapData, + payable(msg.sender) + ); + _startBridge(_bridgeData, _chainflipData); + } + + /// Internal Methods /// + + /// @dev Contains the business logic for the bridge via Chainflip + /// @param _bridgeData The core information needed for bridging + /// @param _chainflipData Data specific to Chainflip + function _startBridge( + ILiFi.BridgeData memory _bridgeData, + ChainflipData calldata _chainflipData + ) internal { + // Map the destination chain ID to Chainflip format + uint32 dstChain; + if (_bridgeData.destinationChainId == CHAIN_ID_ETHEREUM) { + dstChain = CHAINFLIP_ID_ETHEREUM; + } else if (_bridgeData.destinationChainId == CHAIN_ID_ARBITRUM) { + dstChain = CHAINFLIP_ID_ARBITRUM; + } else if (_bridgeData.destinationChainId == CHAIN_ID_SOLANA) { + dstChain = CHAINFLIP_ID_SOLANA; + } else if (_bridgeData.destinationChainId == CHAIN_ID_BITCOIN) { + dstChain = CHAINFLIP_ID_BITCOIN; + } else { + revert("ChainflipFacet: Unsupported destination chain"); + } + + // Encode destination address to bytes format as required by Chainflip + bytes memory encodedDstAddress = abi.encodePacked( + _bridgeData.receiver + ); + + // Handle native token case + if (_bridgeData.sendingAssetId == address(0)) { + IChainflipVault(chainflipVault).xSwapNative{ + value: _bridgeData.minAmount + }( + dstChain, + encodedDstAddress, + _chainflipData.dstToken, + _chainflipData.cfParameters + ); + } + // Handle ERC20 token case + else { + // Approve vault to spend tokens + LibAsset.maxApproveERC20( + IERC20(_bridgeData.sendingAssetId), + chainflipVault, + _bridgeData.minAmount + ); + + IChainflipVault(chainflipVault).xSwapToken( + dstChain, + encodedDstAddress, + _chainflipData.dstToken, + IERC20(_bridgeData.sendingAssetId), + _bridgeData.minAmount, + _chainflipData.cfParameters + ); + } + + emit LiFiTransferStarted(_bridgeData); + } +} diff --git a/src/Interfaces/IChainflip.sol b/src/Interfaces/IChainflip.sol new file mode 100644 index 000000000..69b760a46 --- /dev/null +++ b/src/Interfaces/IChainflip.sol @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.17; + +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +interface IChainflipVault { + function xSwapNative( + uint32 dstChain, + bytes calldata dstAddress, + uint32 dstToken, + bytes calldata cfParameters + ) external payable; + + function xSwapToken( + uint32 dstChain, + bytes calldata dstAddress, + uint32 dstToken, + IERC20 srcToken, + uint256 amount, + bytes calldata cfParameters + ) external; +} diff --git a/test/solidity/Facets/ChainflipFacet.t.sol b/test/solidity/Facets/ChainflipFacet.t.sol new file mode 100644 index 000000000..81e695ba4 --- /dev/null +++ b/test/solidity/Facets/ChainflipFacet.t.sol @@ -0,0 +1,141 @@ +// SPDX-License-Identifier: Unlicense +pragma solidity 0.8.17; + +import { LibAllowList, TestBaseFacet, console, ERC20 } from "../utils/TestBaseFacet.sol"; +import { ChainflipFacet } from "lifi/Facets/ChainflipFacet.sol"; +import { stdJson } from "forge-std/StdJson.sol"; + +using stdJson for string; + +// Stub ChainflipFacet Contract +contract TestChainflipFacet is ChainflipFacet { + constructor(address _chainflipVault) ChainflipFacet(_chainflipVault) {} + + function addDex(address _dex) external { + LibAllowList.addAllowedContract(_dex); + } + + function setFunctionApprovalBySignature(bytes4 _signature) external { + LibAllowList.addAllowedSelector(_signature); + } +} + +contract ChainflipFacetTest is TestBaseFacet { + ChainflipFacet.ChainflipData internal validChainflipData; + TestChainflipFacet internal chainflipFacet; + address internal CHAINFLIP_VAULT; + + function setUp() public { + customBlockNumberForForking = 18277082; + initTestBase(); + + // Read chainflip vault address from config + string memory path = string.concat( + vm.projectRoot(), + "/config/chainflip.json" + ); + string memory json = vm.readFile(path); + CHAINFLIP_VAULT = json.readAddress(".mainnet.chainflipVault"); + vm.label(CHAINFLIP_VAULT, "Chainflip Vault"); + console.log("Chainflip Vault Address:", CHAINFLIP_VAULT); + + chainflipFacet = new TestChainflipFacet(CHAINFLIP_VAULT); + bytes4[] memory functionSelectors = new bytes4[](4); + functionSelectors[0] = chainflipFacet + .startBridgeTokensViaChainflip + .selector; + functionSelectors[1] = chainflipFacet + .swapAndStartBridgeTokensViaChainflip + .selector; + functionSelectors[2] = chainflipFacet.addDex.selector; + functionSelectors[3] = chainflipFacet + .setFunctionApprovalBySignature + .selector; + + addFacet(diamond, address(chainflipFacet), functionSelectors); + chainflipFacet = TestChainflipFacet(address(diamond)); + chainflipFacet.addDex(ADDRESS_UNISWAP); + chainflipFacet.setFunctionApprovalBySignature( + uniswap.swapExactTokensForTokens.selector + ); + chainflipFacet.setFunctionApprovalBySignature( + uniswap.swapTokensForExactETH.selector + ); + chainflipFacet.setFunctionApprovalBySignature( + uniswap.swapETHForExactTokens.selector + ); + + setFacetAddressInTestBase(address(chainflipFacet), "ChainflipFacet"); + + // adjust bridgeData + bridgeData.bridge = "chainflip"; + bridgeData.destinationChainId = 42161; // Arbitrum chain ID + + // produce valid ChainflipData + validChainflipData = ChainflipFacet.ChainflipData({ + dstToken: 6, + cfParameters: "" + }); + } + + // All facet test files inherit from `utils/TestBaseFacet.sol` and require the following method overrides: + // - function initiateBridgeTxWithFacet(bool isNative) + // - function initiateSwapAndBridgeTxWithFacet(bool isNative) + // + // These methods are used to run the following tests which must pass: + // - testBase_CanBridgeNativeTokens() + // - testBase_CanBridgeTokens() + // - testBase_CanBridgeTokens_fuzzed(uint256) + // - testBase_CanSwapAndBridgeNativeTokens() + // - testBase_CanSwapAndBridgeTokens() + // - testBase_Revert_BridgeAndSwapWithInvalidReceiverAddress() + // - testBase_Revert_BridgeToSameChainId() + // - testBase_Revert_BridgeWithInvalidAmount() + // - testBase_Revert_BridgeWithInvalidDestinationCallFlag() + // - testBase_Revert_BridgeWithInvalidReceiverAddress() + // - testBase_Revert_CallBridgeOnlyFunctionWithSourceSwapFlag() + // - testBase_Revert_CallerHasInsufficientFunds() + // - testBase_Revert_SwapAndBridgeToSameChainId() + // - testBase_Revert_SwapAndBridgeWithInvalidAmount() + // - testBase_Revert_SwapAndBridgeWithInvalidSwapData() + // + // In some cases it doesn't make sense to have all tests. For example the bridge may not support native tokens. + // In that case you can override the test method and leave it empty. For example: + // + // function testBase_CanBridgeNativeTokens() public override { + // // facet does not support bridging of native assets + // } + // + // function testBase_CanSwapAndBridgeNativeTokens() public override { + // // facet does not support bridging of native assets + // } + + function initiateBridgeTxWithFacet(bool isNative) internal override { + if (isNative) { + chainflipFacet.startBridgeTokensViaChainflip{ + value: bridgeData.minAmount + }(bridgeData, validChainflipData); + } else { + chainflipFacet.startBridgeTokensViaChainflip( + bridgeData, + validChainflipData + ); + } + } + + function initiateSwapAndBridgeTxWithFacet( + bool isNative + ) internal override { + if (isNative) { + chainflipFacet.swapAndStartBridgeTokensViaChainflip{ + value: swapData[0].fromAmount + }(bridgeData, swapData, validChainflipData); + } else { + chainflipFacet.swapAndStartBridgeTokensViaChainflip( + bridgeData, + swapData, + validChainflipData + ); + } + } +} From dce88244ac778dfb7e306be3fecee5cd434d3c74 Mon Sep 17 00:00:00 2001 From: Ed Zynda Date: Mon, 10 Feb 2025 16:51:47 +0300 Subject: [PATCH 02/55] update demo script --- script/demoScripts/demoChainflip.ts | 51 ++++++++--------------------- 1 file changed, 13 insertions(+), 38 deletions(-) diff --git a/script/demoScripts/demoChainflip.ts b/script/demoScripts/demoChainflip.ts index 1338c1128..5f6366cc1 100644 --- a/script/demoScripts/demoChainflip.ts +++ b/script/demoScripts/demoChainflip.ts @@ -15,20 +15,16 @@ import { dotenv.config() // #region ABIs - const ERC20_ABI = erc20Artifact.abi as Narrow const CHAINFLIP_FACET_ABI = chainflipFacetArtifact.abi as Narrow< typeof chainflipFacetArtifact.abi > - // #endregion -dotenv.config() - async function main() { // === Set up environment === - const srcChain: SupportedChain = 'mainnet' // Set source chain - const destinationChainId = 1 // Set destination chain id + const srcChain: SupportedChain = 'mainnet' + const destinationChainId = 42161 // Arbitrum const { client, @@ -40,17 +36,9 @@ async function main() { const signerAddress = walletAccount.address // === Contract addresses === - const SRC_TOKEN_ADDRESS = '' as `0x${string}` // Set the source token address here. - - // If you need to retrieve a specific address from your config file - // based on the chain and element name, use this helper function. - // - // First, ensure you import the relevant config file: - // import config from '../../config/chainflip.json' - // - // Then, retrieve the address: - // const EXAMPLE_ADDRESS = getConfigElement(config, srcChain, 'example'); - // + // Using USDC on mainnet as an example + const SRC_TOKEN_ADDRESS = + '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48' as `0x${string}` // === Instantiate contracts === const srcTokenContract = getContract({ @@ -59,24 +47,13 @@ async function main() { client, }) - // If you need to interact with a contract, use the following helper. - // Provide the contract address, ABI, and a client instance to initialize - // the contract for both read and write operations. - // - // const exampleContract = getContract({ - // address: EXAMPLE_ADDRESS, - // abi: EXAMPLE_ABI, - // client - // }); - // - const srcTokenName = (await srcTokenContract.read.name()) as string const srcTokenSymbol = (await srcTokenContract.read.symbol()) as string const srcTokenDecimals = (await srcTokenContract.read.decimals()) as bigint - const amount = parseUnits('10', srcTokenDecimals) // 10 * 1e{source token decimals} + const amount = parseUnits('10', Number(srcTokenDecimals)) // 10 USDC console.info( - `\nBridge ${amount} ${srcTokenName} (${srcTokenSymbol}) from ${srcChain} --> {DESTINATION CHAIN NAME}` + `\nBridge ${amount} ${srcTokenName} (${srcTokenSymbol}) from ${srcChain} --> Arbitrum` ) console.info(`Connected wallet address: ${signerAddress}`) @@ -90,11 +67,8 @@ async function main() { publicClient ) - // === In this part put necessary logic usually it's fetching quotes, estimating fees, signing messages etc. === - // === Prepare bridge data === const bridgeData: ILiFi.BridgeDataStruct = { - // Edit fields as needed transactionId: `0x${randomBytes(32).toString('hex')}`, bridge: 'chainflip', integrator: 'ACME Devs', @@ -108,16 +82,17 @@ async function main() { } const chainflipData: ChainflipFacet.ChainflipDataStruct = { - // Add your specific fields for Chainflip here. + dstToken: 6, // Using same value as in the test + cfParameters: '0x', // Empty parameters as per implementation } // === Start bridging === await executeTransaction( () => - lifiDiamondContract.write.startBridgeTokensViaChainflip( - [bridgeData, chainflipData] - // { value: fee } optional value - ), + lifiDiamondContract.write.startBridgeTokensViaChainflip([ + bridgeData, + chainflipData, + ]), 'Starting bridge tokens via Chainflip', publicClient, true From ccf78b342b91b8e6c973f7672fda25f44b08b752 Mon Sep 17 00:00:00 2001 From: Ed Zynda Date: Mon, 10 Feb 2025 17:06:08 +0300 Subject: [PATCH 03/55] handle non-EVM addresses correctly --- script/demoScripts/demoChainflip.ts | 2 + .../deploy/facets/UpdateChainflipFacet.s.sol | 2 +- src/Facets/ChainflipFacet.sol | 33 ++++++- test/solidity/Facets/ChainflipFacet.t.sol | 96 +++++++++++++++++++ 4 files changed, 128 insertions(+), 5 deletions(-) diff --git a/script/demoScripts/demoChainflip.ts b/script/demoScripts/demoChainflip.ts index 5f6366cc1..52be82871 100644 --- a/script/demoScripts/demoChainflip.ts +++ b/script/demoScripts/demoChainflip.ts @@ -83,6 +83,8 @@ async function main() { const chainflipData: ChainflipFacet.ChainflipDataStruct = { dstToken: 6, // Using same value as in the test + nonEvmAddress: + '0x0000000000000000000000000000000000000000000000000000000000000000', cfParameters: '0x', // Empty parameters as per implementation } diff --git a/script/deploy/facets/UpdateChainflipFacet.s.sol b/script/deploy/facets/UpdateChainflipFacet.s.sol index 6494b149f..2fb27b000 100644 --- a/script/deploy/facets/UpdateChainflipFacet.s.sol +++ b/script/deploy/facets/UpdateChainflipFacet.s.sol @@ -21,7 +21,7 @@ contract DeployScript is UpdateScriptBase { return new bytes4[](0); } - function getCallData() internal override returns (bytes memory) { + function getCallData() internal pure override returns (bytes memory) { // No initialization needed return new bytes(0); } diff --git a/src/Facets/ChainflipFacet.sol b/src/Facets/ChainflipFacet.sol index 94cc39d50..6ab244a6b 100644 --- a/src/Facets/ChainflipFacet.sol +++ b/src/Facets/ChainflipFacet.sol @@ -17,6 +17,15 @@ import { console } from "forge-std/console.sol"; /// @notice Allows bridging assets via Chainflip /// @custom:version 1.0.0 contract ChainflipFacet is ILiFi, ReentrancyGuard, SwapperV2, Validatable { + /// Events /// + event BridgeToNonEVMChain( + bytes32 indexed transactionId, + uint256 indexed destinationChainId, + bytes32 receiver + ); + + /// Errors /// + error EmptyNonEvmAddress(); address public immutable chainflipVault; uint256 private constant CHAIN_ID_ETHEREUM = 1; @@ -36,6 +45,7 @@ contract ChainflipFacet is ILiFi, ReentrancyGuard, SwapperV2, Validatable { /// @param cfParameters Additional metadata for future features (currently unused) struct ChainflipData { uint32 dstToken; + bytes32 nonEvmAddress; bytes cfParameters; } @@ -120,10 +130,25 @@ contract ChainflipFacet is ILiFi, ReentrancyGuard, SwapperV2, Validatable { revert("ChainflipFacet: Unsupported destination chain"); } - // Encode destination address to bytes format as required by Chainflip - bytes memory encodedDstAddress = abi.encodePacked( - _bridgeData.receiver - ); + // Handle address encoding based on destination chain type + bytes memory encodedDstAddress; + if (_bridgeData.receiver == LibAsset.NON_EVM_ADDRESS) { + // For non-EVM chains (Solana, Bitcoin), use the raw bytes32 from chainflipData + if (_chainflipData.nonEvmAddress == bytes32(0)) { + revert EmptyNonEvmAddress(); + } + encodedDstAddress = abi.encodePacked(_chainflipData.nonEvmAddress); + + // Emit special event for non-EVM transfers + emit BridgeToNonEVMChain( + _bridgeData.transactionId, + _bridgeData.destinationChainId, + _chainflipData.nonEvmAddress + ); + } else { + // For EVM chains, encode the address + encodedDstAddress = abi.encodePacked(_bridgeData.receiver); + } // Handle native token case if (_bridgeData.sendingAssetId == address(0)) { diff --git a/test/solidity/Facets/ChainflipFacet.t.sol b/test/solidity/Facets/ChainflipFacet.t.sol index 81e695ba4..8e64d523d 100644 --- a/test/solidity/Facets/ChainflipFacet.t.sol +++ b/test/solidity/Facets/ChainflipFacet.t.sol @@ -3,6 +3,7 @@ pragma solidity 0.8.17; import { LibAllowList, TestBaseFacet, console, ERC20 } from "../utils/TestBaseFacet.sol"; import { ChainflipFacet } from "lifi/Facets/ChainflipFacet.sol"; +import { LibAsset } from "lifi/Libraries/LibAsset.sol"; import { stdJson } from "forge-std/StdJson.sol"; using stdJson for string; @@ -25,6 +26,11 @@ contract ChainflipFacetTest is TestBaseFacet { TestChainflipFacet internal chainflipFacet; address internal CHAINFLIP_VAULT; + uint256 internal constant CHAIN_ID_ETHEREUM = 1; + uint256 internal constant CHAIN_ID_ARBITRUM = 42161; + uint256 internal constant CHAIN_ID_SOLANA = 1151111081099710; + uint256 internal constant CHAIN_ID_BITCOIN = 20000000000001; + function setUp() public { customBlockNumberForForking = 18277082; initTestBase(); @@ -74,6 +80,7 @@ contract ChainflipFacetTest is TestBaseFacet { // produce valid ChainflipData validChainflipData = ChainflipFacet.ChainflipData({ dstToken: 6, + nonEvmAddress: bytes32(0), // Default to empty for EVM addresses cfParameters: "" }); } @@ -138,4 +145,93 @@ contract ChainflipFacetTest is TestBaseFacet { ); } } + + function test_CanBridgeTokensToSolana() + public + assertBalanceChange( + ADDRESS_USDC, + USER_SENDER, + -int256(defaultUSDCAmount) + ) + assertBalanceChange(ADDRESS_USDC, USER_RECEIVER, 0) + assertBalanceChange(ADDRESS_DAI, USER_SENDER, 0) + assertBalanceChange(ADDRESS_DAI, USER_RECEIVER, 0) + { + bridgeData.receiver = LibAsset.NON_EVM_ADDRESS; + bridgeData.destinationChainId = CHAIN_ID_SOLANA; + validChainflipData = ChainflipFacet.ChainflipData({ + dstToken: 6, + nonEvmAddress: bytes32( + abi.encodePacked( + "EoW7FWTdPdZKpd3WAhH98c2HMGHsdh5yhzzEtk1u68Bb" + ) + ), // Example Solana address + cfParameters: "" + }); + + vm.startPrank(USER_SENDER); + + // approval + usdc.approve(_facetTestContractAddress, bridgeData.minAmount); + + //prepare check for events + vm.expectEmit(true, true, true, true, _facetTestContractAddress); + emit LiFiTransferStarted(bridgeData); + + initiateBridgeTxWithFacet(false); + vm.stopPrank(); + } + + function test_CanBridgeTokensToBitcoin() + public + assertBalanceChange( + ADDRESS_USDC, + USER_SENDER, + -int256(defaultUSDCAmount) + ) + assertBalanceChange(ADDRESS_USDC, USER_RECEIVER, 0) + assertBalanceChange(ADDRESS_DAI, USER_SENDER, 0) + assertBalanceChange(ADDRESS_DAI, USER_RECEIVER, 0) + { + bridgeData.receiver = LibAsset.NON_EVM_ADDRESS; + bridgeData.destinationChainId = CHAIN_ID_BITCOIN; + validChainflipData = ChainflipFacet.ChainflipData({ + dstToken: 6, + nonEvmAddress: bytes32( + abi.encodePacked("bc1q6l08rtj6j907r2een0jqs6l7qnruwyxfshmf8a") + ), // Example Bitcoin address + cfParameters: "" + }); + + vm.startPrank(USER_SENDER); + + // approval + usdc.approve(_facetTestContractAddress, bridgeData.minAmount); + + //prepare check for events + vm.expectEmit(true, true, true, true, _facetTestContractAddress); + emit LiFiTransferStarted(bridgeData); + + initiateBridgeTxWithFacet(false); + vm.stopPrank(); + } + + function testRevert_WhenUsingEmptyNonEVMAddress() public { + bridgeData.receiver = LibAsset.NON_EVM_ADDRESS; + bridgeData.destinationChainId = CHAIN_ID_SOLANA; + validChainflipData = ChainflipFacet.ChainflipData({ + dstToken: 6, + nonEvmAddress: bytes32(0), // Empty address should fail + cfParameters: "" + }); + + vm.startPrank(USER_SENDER); + + // approval + usdc.approve(_facetTestContractAddress, bridgeData.minAmount); + + vm.expectRevert(ChainflipFacet.EmptyNonEvmAddress.selector); + initiateBridgeTxWithFacet(false); + vm.stopPrank(); + } } From e20815f9cf6e0a564f7e183b96426d7405fe365c Mon Sep 17 00:00:00 2001 From: Ed Zynda Date: Mon, 10 Feb 2025 17:38:39 +0300 Subject: [PATCH 04/55] implement destination calls --- src/Periphery/ReceiverChainflip.sol | 115 ++++++++ .../Periphery/ReceiverChainflip.t.sol | 255 ++++++++++++++++++ 2 files changed, 370 insertions(+) create mode 100644 src/Periphery/ReceiverChainflip.sol create mode 100644 test/solidity/Periphery/ReceiverChainflip.t.sol diff --git a/src/Periphery/ReceiverChainflip.sol b/src/Periphery/ReceiverChainflip.sol new file mode 100644 index 000000000..a6e5d11ba --- /dev/null +++ b/src/Periphery/ReceiverChainflip.sol @@ -0,0 +1,115 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.17; + +import { LibSwap } from "../Libraries/LibSwap.sol"; +import { LibAsset } from "../Libraries/LibAsset.sol"; +import { ILiFi } from "../Interfaces/ILiFi.sol"; +import { IExecutor } from "../Interfaces/IExecutor.sol"; +import { WithdrawablePeriphery } from "../Helpers/WithdrawablePeriphery.sol"; +import { SafeTransferLib } from "solady/utils/SafeTransferLib.sol"; + +/// @title ReceiverChainflip +/// @author LI.FI (https://li.fi) +/// @notice Receiver contract for Chainflip cross-chain swaps and message passing +/// @custom:version 1.0.0 +contract ReceiverChainflip is ILiFi, WithdrawablePeriphery { + using SafeTransferLib for address; + + /// Storage /// + IExecutor public immutable executor; + address public immutable chainflipVault; + + /// Modifiers /// + modifier onlyChainflipVault() { + if (msg.sender != chainflipVault) { + revert UnAuthorized(); + } + _; + } + + /// Constructor + constructor( + address _owner, + address _executor, + address _chainflipVault + ) WithdrawablePeriphery(_owner) { + executor = IExecutor(_executor); + chainflipVault = _chainflipVault; + } + + /// External Methods /// + + /// @notice Receiver function for Chainflip cross-chain messages + /// @dev This function can only be called by the Chainflip Vault on this network + /// @dev First param (unused): The source chain according to Chainflip's nomenclature + /// @dev Second param (unused): The source address on the source chain + /// @param message The message sent from the source chain + /// @param token The address of the token received + /// @param amount The amount of tokens received + function cfReceive( + uint32, // srcChain + bytes calldata, // srcAddress + bytes calldata message, + address token, + uint256 amount + ) external payable onlyChainflipVault { + // decode payload + ( + bytes32 transactionId, + LibSwap.SwapData[] memory swapData, + address receiver + ) = abi.decode(message, (bytes32, LibSwap.SwapData[], address)); + + // execute swap(s) + _swapAndCompleteBridgeTokens( + transactionId, + swapData, + token, + payable(receiver), + amount + ); + } + + /// Private Methods /// + + /// @notice Performs a swap before completing a cross-chain transaction + /// @param _transactionId the transaction id associated with the operation + /// @param _swapData array of data needed for swaps + /// @param assetId address of the token received from the source chain + /// @param receiver address that will receive tokens in the end + /// @param amount amount of token + function _swapAndCompleteBridgeTokens( + bytes32 _transactionId, + LibSwap.SwapData[] memory _swapData, + address assetId, + address payable receiver, + uint256 amount + ) private { + assetId.safeApproveWithRetry(address(executor), amount); + try + executor.swapAndCompleteBridgeTokens( + _transactionId, + _swapData, + assetId, + receiver + ) + {} catch { + // send the bridged (and unswapped) funds to receiver address + assetId.safeTransfer(receiver, amount); + + emit LiFiTransferRecovered( + _transactionId, + assetId, + receiver, + amount, + block.timestamp + ); + } + + // reset approval to 0 + assetId.safeApprove(address(executor), 0); + } + + /// @notice Receive native asset directly. + receive() external payable {} +} diff --git a/test/solidity/Periphery/ReceiverChainflip.t.sol b/test/solidity/Periphery/ReceiverChainflip.t.sol new file mode 100644 index 000000000..f1aed817e --- /dev/null +++ b/test/solidity/Periphery/ReceiverChainflip.t.sol @@ -0,0 +1,255 @@ +// SPDX-License-Identifier: Unlicense +pragma solidity ^0.8.17; + +import { Test, TestBase, Vm, LiFiDiamond, DSTest, ILiFi, LibSwap, LibAllowList, console, InvalidAmount, ERC20, UniswapV2Router02 } from "../utils/TestBase.sol"; +import { ExternalCallFailed, UnAuthorized } from "src/Errors/GenericErrors.sol"; +import { ReceiverChainflip } from "lifi/Periphery/ReceiverChainflip.sol"; +import { stdJson } from "forge-std/Script.sol"; +import { ERC20Proxy } from "lifi/Periphery/ERC20Proxy.sol"; +import { Executor } from "lifi/Periphery/Executor.sol"; +import { MockUniswapDEX, NonETHReceiver } from "../utils/TestHelpers.sol"; + +contract ReceiverChainflipTest is TestBase { + using stdJson for string; + + ReceiverChainflip internal receiver; + bytes32 guid = bytes32("12345"); + address receiverAddress = USER_RECEIVER; + + Executor executor; + ERC20Proxy erc20Proxy; + address chainflipVault; + + event ExecutorSet(address indexed executor); + + function setUp() public { + customBlockNumberForForking = 18277082; + initTestBase(); + + // Read chainflip vault address from config + string memory path = string.concat( + vm.projectRoot(), + "/config/chainflip.json" + ); + string memory json = vm.readFile(path); + chainflipVault = json.readAddress(".mainnet.chainflipVault"); + vm.label(chainflipVault, "Chainflip Vault"); + + erc20Proxy = new ERC20Proxy(address(this)); + executor = new Executor(address(erc20Proxy), address(this)); + receiver = new ReceiverChainflip( + address(this), + address(executor), + chainflipVault + ); + vm.label(address(receiver), "ReceiverChainflip"); + vm.label(address(executor), "Executor"); + vm.label(address(erc20Proxy), "ERC20Proxy"); + } + + function test_contractIsSetUpCorrectly() public { + receiver = new ReceiverChainflip( + address(this), + address(executor), + chainflipVault + ); + + assertEq(address(receiver.executor()) == address(executor), true); + assertEq(receiver.chainflipVault() == chainflipVault, true); + } + + function test_OwnerCanPullERC20Token() public { + // fund receiver with ERC20 tokens + deal(ADDRESS_DAI, address(receiver), 1000); + + uint256 initialBalance = dai.balanceOf(USER_RECEIVER); + + // pull token + vm.startPrank(USER_DIAMOND_OWNER); + + receiver.withdrawToken(ADDRESS_DAI, payable(USER_RECEIVER), 1000); + + assertEq(dai.balanceOf(USER_RECEIVER), initialBalance + 1000); + } + + function test_OwnerCanPullNativeToken() public { + // fund receiver with native tokens + vm.deal(address(receiver), 1 ether); + + uint256 initialBalance = USER_RECEIVER.balance; + + // pull token + vm.startPrank(USER_DIAMOND_OWNER); + + receiver.withdrawToken(address(0), payable(USER_RECEIVER), 1 ether); + + assertEq(USER_RECEIVER.balance, initialBalance + 1 ether); + } + + function test_WithdrawTokenWillRevertIfExternalCallFails() public { + vm.deal(address(receiver), 1 ether); + + // deploy contract that cannot receive ETH + NonETHReceiver nonETHReceiver = new NonETHReceiver(); + + vm.startPrank(USER_DIAMOND_OWNER); + + vm.expectRevert(ExternalCallFailed.selector); + + receiver.withdrawToken( + address(0), + payable(address(nonETHReceiver)), + 1 ether + ); + } + + function test_revert_WithdrawTokenNonOwner() public { + vm.startPrank(USER_SENDER); + vm.expectRevert(UnAuthorized.selector); + receiver.withdrawToken(ADDRESS_DAI, payable(USER_RECEIVER), 1000); + } + + function test_revert_OnlyChainflipVaultCanCallCfReceive() public { + // mock-send bridged funds to receiver contract + deal(ADDRESS_USDC, address(receiver), defaultUSDCAmount); + + // call from deployer of ReceiverChainflip + vm.startPrank(address(this)); + vm.expectRevert(UnAuthorized.selector); + + receiver.cfReceive( + 1, // srcChain (Ethereum) + abi.encodePacked(address(0)), + abi.encode("payload"), + ADDRESS_USDC, + defaultUSDCAmount + ); + + // call from random user + vm.startPrank(USER_SENDER); + vm.expectRevert(UnAuthorized.selector); + + receiver.cfReceive( + 1, // srcChain (Ethereum) + abi.encodePacked(address(0)), + abi.encode("payload"), + ADDRESS_USDC, + defaultUSDCAmount + ); + } + + function test_canDecodeChainflipPayloadAndExecuteSwapERC20() public { + // mock-send bridged funds to receiver contract + deal(ADDRESS_USDC, address(receiver), defaultUSDCAmount); + + // encode payload with mock data + ( + bytes memory payload, + uint256 amountOutMin + ) = _getValidChainflipPayload(ADDRESS_USDC, ADDRESS_DAI); + + // fake a call from Chainflip vault + vm.startPrank(chainflipVault); + + vm.expectEmit(); + emit LiFiTransferCompleted( + guid, + ADDRESS_USDC, + receiverAddress, + amountOutMin, + block.timestamp + ); + receiver.cfReceive( + 1, // srcChain (Ethereum) + abi.encodePacked(address(0)), + payload, + ADDRESS_USDC, + defaultUSDCAmount + ); + + assertTrue(dai.balanceOf(receiverAddress) == amountOutMin); + } + + function test_willReturnFundsToUserIfDstCallFails() public { + // mock-send bridged funds to receiver contract + deal(ADDRESS_USDC, address(receiver), defaultUSDCAmount); + + // encode payload with mock data + string memory revertReason = "Just because"; + MockUniswapDEX mockDEX = new MockUniswapDEX(); + + LibSwap.SwapData[] memory swapData = new LibSwap.SwapData[](1); + swapData[0] = LibSwap.SwapData({ + callTo: address(mockDEX), + approveTo: address(mockDEX), + sendingAssetId: ADDRESS_USDC, + receivingAssetId: ADDRESS_USDC, + fromAmount: defaultUSDCAmount, + callData: abi.encodeWithSelector( + mockDEX.mockSwapWillRevertWithReason.selector, + revertReason + ), + requiresDeposit: false + }); + + bytes memory payload = abi.encode(guid, swapData, receiverAddress); + + vm.startPrank(chainflipVault); + + vm.expectEmit(true, true, true, true, address(receiver)); + emit LiFiTransferRecovered( + guid, + ADDRESS_USDC, + receiverAddress, + defaultUSDCAmount, + block.timestamp + ); + receiver.cfReceive( + 1, // srcChain (Ethereum) + abi.encodePacked(address(0)), + payload, + ADDRESS_USDC, + defaultUSDCAmount + ); + + assertTrue(usdc.balanceOf(receiverAddress) == defaultUSDCAmount); + } + + // HELPER FUNCTIONS + function _getValidChainflipPayload( + address _sendingAssetId, + address _receivingAssetId + ) public view returns (bytes memory callData, uint256 amountOutMin) { + // create swapdata + address[] memory path = new address[](2); + path[0] = _sendingAssetId; + path[1] = _receivingAssetId; + + uint256 amountIn = defaultUSDCAmount; + + // Calculate USDC input amount + uint256[] memory amounts = uniswap.getAmountsOut(amountIn, path); + amountOutMin = amounts[1]; + + LibSwap.SwapData[] memory swapData = new LibSwap.SwapData[](1); + swapData[0] = LibSwap.SwapData({ + callTo: address(uniswap), + approveTo: address(uniswap), + sendingAssetId: _sendingAssetId, + receivingAssetId: _receivingAssetId, + fromAmount: amountIn, + callData: abi.encodeWithSelector( + uniswap.swapExactTokensForTokens.selector, + amountIn, + amountOutMin, + path, + address(executor), + block.timestamp + 20 minutes + ), + requiresDeposit: true + }); + + // this is the "message" that we would receive from the other chain + callData = abi.encode(guid, swapData, receiverAddress); + } +} From e30c1de41c196fae649d0896d0582f625d54ab9a Mon Sep 17 00:00:00 2001 From: Ed Zynda Date: Mon, 10 Feb 2025 17:46:07 +0300 Subject: [PATCH 05/55] Update docs, add deploy scripts and update deploy requirements --- docs/ChainflipFacet.md | 42 ++++++++----- .../facets/DeployReceiverChainflip.s.sol | 60 +++++++++++++++++++ .../deploy/resources/deployRequirements.json | 28 +++++++++ 3 files changed, 115 insertions(+), 15 deletions(-) create mode 100644 script/deploy/facets/DeployReceiverChainflip.s.sol diff --git a/docs/ChainflipFacet.md b/docs/ChainflipFacet.md index fe010ea39..2d109a806 100644 --- a/docs/ChainflipFacet.md +++ b/docs/ChainflipFacet.md @@ -2,37 +2,49 @@ ## How it works -The Chainflip Facet works by ... +The Chainflip Facet enables cross-chain token transfers using Chainflip's protocol. It supports both EVM chains (Ethereum, Arbitrum) and non-EVM chains (Solana, Bitcoin) as destinations. ```mermaid graph LR; D{LiFiDiamond}-- DELEGATECALL -->ChainflipFacet; - ChainflipFacet -- CALL --> C(Chainflip) + ChainflipFacet -- CALL --> ChainflipVault[Chainflip Vault] ``` ## Public Methods - `function startBridgeTokensViaChainflip(BridgeData calldata _bridgeData, ChainflipData calldata _chainflipData)` - - Simply bridges tokens using chainflip -- `swapAndStartBridgeTokensViaChainflip(BridgeData memory _bridgeData, LibSwap.SwapData[] calldata _swapData, chainflipData memory _chainflipData)` - - Performs swap(s) before bridging tokens using chainflip + - Bridges tokens using Chainflip without performing any swaps +- `swapAndStartBridgeTokensViaChainflip(BridgeData memory _bridgeData, LibSwap.SwapData[] calldata _swapData, ChainflipData memory _chainflipData)` + - Performs swap(s) before bridging tokens using Chainflip -## chainflip Specific Parameters +## Chainflip Specific Parameters -The methods listed above take a variable labeled `_chainflipData`. This data is specific to chainflip and is represented as the following struct type: +The methods listed above take a variable labeled `_chainflipData`. This data is specific to Chainflip and is represented as the following struct type: ```solidity -/// @param example Example parameter. -struct chainflipData { - string example; +struct ChainflipData { + uint32 dstToken; // Token identifier on the destination chain + bytes32 nonEvmAddress; // Destination address for non-EVM chains (Solana, Bitcoin) + bytes cfParameters; // Additional parameters for future features } ``` +For non-EVM destinations (Solana, Bitcoin), set the `receiver` in `BridgeData` to `LibAsset.NON_EVM_ADDRESS` and provide the destination address in `nonEvmAddress`. + +## Supported Chains + +The facet supports the following chains with their respective IDs: + +- Ethereum (1) +- Arbitrum (42161) +- Solana (1151111081099710) +- Bitcoin (20000000000001) + ## Swap Data Some methods accept a `SwapData _swapData` parameter. -Swapping is performed by a swap specific library that expects an array of calldata to can be run on various DEXs (i.e. Uniswap) to make one or multiple swaps before performing another action. +Swapping is performed by a swap specific library that expects an array of calldata that can be run on various DEXs (i.e. Uniswap) to make one or multiple swaps before performing another action. The swap library can be found [here](../src/Libraries/LibSwap.sol). @@ -77,16 +89,16 @@ A detailed explanation on how to use the /quote endpoint and how to trigger the ### Cross Only -To get a transaction for a transfer from 30 USDC.e on Avalanche to USDC on Binance you can execute the following request: +To get a transaction for a transfer from USDC on Ethereum to USDC on Arbitrum you can execute the following request: ```shell -curl 'https://li.quest/v1/quote?fromChain=AVA&fromAmount=30000000&fromToken=USDC&toChain=BSC&toToken=USDC&slippage=0.03&allowBridges=chainflip&fromAddress={YOUR_WALLET_ADDRESS}' +curl 'https://li.quest/v1/quote?fromChain=ETH&fromAmount=1000000&fromToken=USDC&toChain=ARB&toToken=USDC&slippage=0.03&allowBridges=chainflip&fromAddress={YOUR_WALLET_ADDRESS}' ``` ### Swap & Cross -To get a transaction for a transfer from 30 USDT on Avalanche to USDC on Binance you can execute the following request: +To get a transaction for a transfer from USDT on Ethereum to USDC on Arbitrum you can execute the following request: ```shell -curl 'https://li.quest/v1/quote?fromChain=AVA&fromAmount=30000000&fromToken=USDT&toChain=BSC&toToken=USDC&slippage=0.03&allowBridges=chainflip&fromAddress={YOUR_WALLET_ADDRESS}' +curl 'https://li.quest/v1/quote?fromChain=ETH&fromAmount=1000000&fromToken=USDT&toChain=ARB&toToken=USDC&slippage=0.03&allowBridges=chainflip&fromAddress={YOUR_WALLET_ADDRESS}' ``` diff --git a/script/deploy/facets/DeployReceiverChainflip.s.sol b/script/deploy/facets/DeployReceiverChainflip.s.sol new file mode 100644 index 000000000..09be37722 --- /dev/null +++ b/script/deploy/facets/DeployReceiverChainflip.s.sol @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.17; + +import { DeployScriptBase } from "./utils/DeployScriptBase.sol"; +import { stdJson } from "forge-std/Script.sol"; +import { ReceiverChainflip } from "lifi/Periphery/ReceiverChainflip.sol"; + +contract DeployScript is DeployScriptBase { + using stdJson for string; + + constructor() DeployScriptBase("ReceiverChainflip") {} + + function run() + public + returns (ReceiverChainflip deployed, bytes memory constructorArgs) + { + constructorArgs = getConstructorArgs(); + + deployed = ReceiverChainflip( + deploy(type(ReceiverChainflip).creationCode) + ); + } + + function getConstructorArgs() internal override returns (bytes memory) { + // get path of global config file + string memory globalConfigPath = string.concat( + root, + "/config/global.json" + ); + + // read file into json variable + string memory globalConfigJson = vm.readFile(globalConfigPath); + + // extract refundWallet address + address refundWalletAddress = globalConfigJson.readAddress( + ".refundWallet" + ); + + // obtain address of Chainflip vault in current network from config file + string memory path = string.concat(root, "/config/chainflip.json"); + + address chainflipVault = _getConfigContractAddress( + path, + string.concat(".", network, ".chainflipVault") + ); + + // get Executor address from deploy log + path = string.concat( + root, + "/deployments/", + network, + ".", + fileSuffix, + "json" + ); + address executor = _getConfigContractAddress(path, ".Executor"); + + return abi.encode(refundWalletAddress, executor, chainflipVault); + } +} diff --git a/script/deploy/resources/deployRequirements.json b/script/deploy/resources/deployRequirements.json index 3bfef1d48..112c3dc6a 100644 --- a/script/deploy/resources/deployRequirements.json +++ b/script/deploy/resources/deployRequirements.json @@ -569,5 +569,33 @@ "allowToDeployWithZeroAddress": "false" } } + }, + "ChainflipFacet": { + "configData": { + "_chainflipVault": { + "configFileName": "chainflip.json", + "keyInConfigFile": "..chainflipVault", + "allowToDeployWithZeroAddress": "false" + } + } + }, + "ReceiverChainflip": { + "configData": { + "_owner": { + "configFileName": "global.json", + "keyInConfigFile": ".refundWallet", + "allowToDeployWithZeroAddress": "false" + }, + "_chainflipVault": { + "configFileName": "chainflip.json", + "keyInConfigFile": "..chainflipVault", + "allowToDeployWithZeroAddress": "false" + } + }, + "contractAddresses": { + "Executor": { + "allowToDeployWithZeroAddress": "false" + } + } } } From c8c2988ce0b1369b084bb99b7ace5fa4d7b7d1d5 Mon Sep 17 00:00:00 2001 From: Ed Zynda Date: Wed, 12 Feb 2025 16:56:06 +0300 Subject: [PATCH 06/55] deploy to staging --- deployments/_deployments_log_file.json | 16 ++++++++++++++++ deployments/arbitrum.diamond.staging.json | 15 ++++++++++++--- deployments/arbitrum.staging.json | 4 ++-- 3 files changed, 30 insertions(+), 5 deletions(-) diff --git a/deployments/_deployments_log_file.json b/deployments/_deployments_log_file.json index f4364d6d2..ddf8870e6 100644 --- a/deployments/_deployments_log_file.json +++ b/deployments/_deployments_log_file.json @@ -28186,5 +28186,21 @@ ] } } + }, + "ChainflipFacet": { + "arbitrum": { + "staging": { + "1.0.0": [ + { + "ADDRESS": "0x01eA7231A7CFb81388171eD6Fbe02e88285BD0B9", + "OPTIMIZER_RUNS": "1000000", + "TIMESTAMP": "2025-02-12 11:08:58", + "CONSTRUCTOR_ARGS": "0x00000000000000000000000079001a5e762f3befc8e5871b42f6734e00498920", + "SALT": "28012025", + "VERIFIED": "true" + } + ] + } + } } } diff --git a/deployments/arbitrum.diamond.staging.json b/deployments/arbitrum.diamond.staging.json index ee64127be..92b8579f1 100644 --- a/deployments/arbitrum.diamond.staging.json +++ b/deployments/arbitrum.diamond.staging.json @@ -138,12 +138,20 @@ "Version": "1.0.0" }, "0xE15C7585636e62b88bA47A40621287086E0c2E33": { - "Name": "", - "Version": "" + "Name": "DeBridgeDlnFacet", + "Version": "1.0.0" }, "0x08BfAc22A3B41637edB8A7920754fDb30B18f740": { "Name": "AcrossFacetV3", "Version": "1.1.0" + }, + "0x3aF0c2dB91f75f05493E51cFcF92eC5276bc85F8": { + "Name": "", + "Version": "" + }, + "0x01eA7231A7CFb81388171eD6Fbe02e88285BD0B9": { + "Name": "ChainflipFacet", + "Version": "1.0.0" } }, "Periphery": { @@ -153,8 +161,9 @@ "GasZipPeriphery": "", "LiFiDEXAggregator": "", "LiFuelFeeCollector": "0x94EA56D8049e93E0308B9c7d1418Baf6A7C68280", - "Permit2Proxy": "0x6FC01BC9Ff6Cdab694Ec8Ca41B21a2F04C8c37E5", + "Permit2Proxy": "0xb33Fe241BEd9bf5F694101D7498F63a0d060F999", "ReceiverAcrossV3": "0xe4F3DEF14D61e47c696374453CD64d438FD277F8", + "ReceiverChainflip": "", "Receiver": "0x36E9B2E8A627474683eF3b1E9Df26D2bF04396f3", "ReceiverStargateV2": "", "RelayerCelerIM": "0xa1Ed8783AC96385482092b82eb952153998e9b70", diff --git a/deployments/arbitrum.staging.json b/deployments/arbitrum.staging.json index 96b7f9fe6..567a75ea7 100644 --- a/deployments/arbitrum.staging.json +++ b/deployments/arbitrum.staging.json @@ -35,7 +35,6 @@ "DeBridgeDlnFacet": "0xE15C7585636e62b88bA47A40621287086E0c2E33", "MayanFacet": "0xd596C903d78870786c5DB0E448ce7F87A65A0daD", "StandardizedCallFacet": "0xA7ffe57ee70Ac4998e9E9fC6f17341173E081A8f", - "MayanFacet": "0xd596C903d78870786c5DB0E448ce7F87A65A0daD", "GenericSwapFacetV3": "0xFf6Fa203573Baaaa4AE375EB7ac2819d539e16FF", "CalldataVerificationFacet": "0x90B5b319cA20D9E466cB5b843952363C34d1b54E", "AcrossFacetPacked": "0x7A3770a9504924d99D38BBba4F0116B756393Eb3", @@ -51,5 +50,6 @@ "AcrossFacetV3": "0x08BfAc22A3B41637edB8A7920754fDb30B18f740", "ReceiverAcrossV3": "0xe4F3DEF14D61e47c696374453CD64d438FD277F8", "AcrossFacetPackedV3": "0x21767081Ff52CE5563A29f27149D01C7127775A2", - "RelayFacet": "0x3cf7dE0e31e13C93c8Aada774ADF1C7eD58157f5" + "RelayFacet": "0x3cf7dE0e31e13C93c8Aada774ADF1C7eD58157f5", + "ChainflipFacet": "0x01eA7231A7CFb81388171eD6Fbe02e88285BD0B9" } \ No newline at end of file From d81478ee5fffc0e00186c96177b28c5b94fe4c7c Mon Sep 17 00:00:00 2001 From: Ed Zynda Date: Wed, 12 Feb 2025 23:36:57 +0300 Subject: [PATCH 07/55] add swap call --- script/demoScripts/demoChainflip.ts | 88 ++++++++++++++----- script/demoScripts/utils/demoScriptHelpers.ts | 28 +++--- test/solidity/Facets/ChainflipFacet.t.sol | 2 +- 3 files changed, 82 insertions(+), 36 deletions(-) diff --git a/script/demoScripts/demoChainflip.ts b/script/demoScripts/demoChainflip.ts index 52be82871..83bad9c2a 100644 --- a/script/demoScripts/demoChainflip.ts +++ b/script/demoScripts/demoChainflip.ts @@ -10,6 +10,11 @@ import { ensureAllowance, executeTransaction, setupEnvironment, + ADDRESS_USDC_ARB, + ADDRESS_USDT_ARB, + getUniswapSwapDataERC20ToERC20, + getAmountsOutUniswap, + ADDRESS_UNISWAP_ARB, } from './utils/demoScriptHelpers' dotenv.config() @@ -22,9 +27,11 @@ const CHAINFLIP_FACET_ABI = chainflipFacetArtifact.abi as Narrow< // #endregion async function main() { + const withSwap = process.argv.includes('--with-swap') + const srcTokenAddress = withSwap ? ADDRESS_USDT_ARB : ADDRESS_USDC_ARB // === Set up environment === - const srcChain: SupportedChain = 'mainnet' - const destinationChainId = 42161 // Arbitrum + const srcChain: SupportedChain = 'arbitrum' + const destinationChainId = 1 // Mainnet const { client, @@ -35,14 +42,9 @@ async function main() { } = await setupEnvironment(srcChain, CHAINFLIP_FACET_ABI) const signerAddress = walletAccount.address - // === Contract addresses === - // Using USDC on mainnet as an example - const SRC_TOKEN_ADDRESS = - '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48' as `0x${string}` - // === Instantiate contracts === const srcTokenContract = getContract({ - address: SRC_TOKEN_ADDRESS, + address: srcTokenAddress, abi: ERC20_ABI, client, }) @@ -73,32 +75,76 @@ async function main() { bridge: 'chainflip', integrator: 'ACME Devs', referrer: zeroAddress, - sendingAssetId: SRC_TOKEN_ADDRESS, + sendingAssetId: srcTokenAddress, receiver: signerAddress, destinationChainId, minAmount: amount, - hasSourceSwaps: false, + hasSourceSwaps: withSwap, hasDestinationCall: false, } const chainflipData: ChainflipFacet.ChainflipDataStruct = { - dstToken: 6, // Using same value as in the test + dstToken: 3, // USDC nonEvmAddress: '0x0000000000000000000000000000000000000000000000000000000000000000', cfParameters: '0x', // Empty parameters as per implementation } // === Start bridging === - await executeTransaction( - () => - lifiDiamondContract.write.startBridgeTokensViaChainflip([ - bridgeData, - chainflipData, - ]), - 'Starting bridge tokens via Chainflip', - publicClient, - true - ) + if (withSwap) { + // Get expected output from swapping the specified amount of USDT to USDC. + const amountsOut = await getAmountsOutUniswap( + ADDRESS_UNISWAP_ARB, + 42161, + [ADDRESS_USDT_ARB, ADDRESS_USDC_ARB], + amount + ) + console.log('Swap amounts out:', amountsOut) + + // Adjust bridgeData.minAmount to a lower value to allow for slippage. + // Here we set it to 98% of the estimated output. + const expectedOutput = (BigInt(amountsOut[1]) * 98n) / 100n + bridgeData.minAmount = expectedOutput + console.log( + 'Updated bridgeData.minAmount:', + bridgeData.minAmount.toString() + ) + + // Generate real swap data to swap USDT -> USDC. + const swapData = await getUniswapSwapDataERC20ToERC20( + ADDRESS_UNISWAP_ARB, // Uniswap router address on Arbitrum + 42161, // Arbitrum chain id + ADDRESS_USDT_ARB, // Swap from USDT + ADDRESS_USDC_ARB, // Swap to USDC + expectedOutput, // Pass the expected output (in USDC) as the exact output amount + lifiDiamondAddress, // Receiver for the swapped tokens (the diamond) + true, // requiresDeposit flag + 0 // minAmountOut (0 lets the helper calculate slippage tolerance automatically) + ) + + await executeTransaction( + () => + lifiDiamondContract.write.swapAndStartBridgeTokensViaChainflip([ + bridgeData, + [swapData], + chainflipData, + ]), + 'Swapping and starting bridge tokens via Chainflip', + publicClient, + true + ) + } else { + await executeTransaction( + () => + lifiDiamondContract.write.startBridgeTokensViaChainflip([ + bridgeData, + chainflipData, + ]), + 'Starting bridge tokens via Chainflip', + publicClient, + true + ) + } } main() diff --git a/script/demoScripts/utils/demoScriptHelpers.ts b/script/demoScripts/utils/demoScriptHelpers.ts index 3351387a0..e2a30c30b 100644 --- a/script/demoScripts/utils/demoScriptHelpers.ts +++ b/script/demoScripts/utils/demoScriptHelpers.ts @@ -180,19 +180,19 @@ export const getUniswapSwapDataERC20ToERC20 = async ( // get minAmountOut from Uniswap router console.log(`finalFromAmount : ${fromAmount}`) - const finalMinAmountOut = - minAmountOut.toString() !== '0' - ? minAmountOut - : BigNumber.from( - await getAmountsOutUniswap( - uniswapAddress, - chainId, - [sendingAssetId, receivingAssetId], - fromAmount - ) - ) - .mul(99) - .div(100) // Apply 1% slippage tolerance by default + let finalMinAmountOut: BigNumber + if (minAmountOut.toString() !== '0') { + finalMinAmountOut = minAmountOut + } else { + const amountsOut = await getAmountsOutUniswap( + uniswapAddress, + chainId, + [sendingAssetId, receivingAssetId], + fromAmount + ) + // Use the second element (index 1) as the estimated output + finalMinAmountOut = BigNumber.from(amountsOut[1]).mul(99).div(100) + } console.log(`finalMinAmountOut: ${finalMinAmountOut}`) const uniswapCalldata = ( @@ -309,7 +309,7 @@ export const getUniswapDataERC20toExactERC20 = async ( // Get the required input amount for the exact output const amounts = await uniswap.getAmountsIn(exactAmountOut, path) const requiredInputAmount = amounts[0] - const maxAmountIn = BigNumber.from(requiredInputAmount).mul(105).div(100) // 5% max slippage + const maxAmountIn = BigNumber.from(requiredInputAmount).mul(110).div(100) // 10% max slippage console.log('Required input amount:', requiredInputAmount.toString()) console.log('Max input with slippage:', maxAmountIn.toString()) diff --git a/test/solidity/Facets/ChainflipFacet.t.sol b/test/solidity/Facets/ChainflipFacet.t.sol index 8e64d523d..c5f9a7fca 100644 --- a/test/solidity/Facets/ChainflipFacet.t.sol +++ b/test/solidity/Facets/ChainflipFacet.t.sol @@ -79,7 +79,7 @@ contract ChainflipFacetTest is TestBaseFacet { // produce valid ChainflipData validChainflipData = ChainflipFacet.ChainflipData({ - dstToken: 6, + dstToken: 7, nonEvmAddress: bytes32(0), // Default to empty for EVM addresses cfParameters: "" }); From 88d291b295f87a60180c7b7b5a0ace92f555a920 Mon Sep 17 00:00:00 2001 From: Ed Zynda Date: Thu, 13 Feb 2025 11:48:18 +0300 Subject: [PATCH 08/55] fix swap --- script/demoScripts/demoChainflip.ts | 63 ++++++++----------- script/demoScripts/utils/demoScriptHelpers.ts | 2 +- 2 files changed, 27 insertions(+), 38 deletions(-) diff --git a/script/demoScripts/demoChainflip.ts b/script/demoScripts/demoChainflip.ts index 83bad9c2a..a31746f78 100644 --- a/script/demoScripts/demoChainflip.ts +++ b/script/demoScripts/demoChainflip.ts @@ -12,8 +12,7 @@ import { setupEnvironment, ADDRESS_USDC_ARB, ADDRESS_USDT_ARB, - getUniswapSwapDataERC20ToERC20, - getAmountsOutUniswap, + getUniswapDataERC20toExactERC20, ADDRESS_UNISWAP_ARB, } from './utils/demoScriptHelpers' @@ -43,8 +42,9 @@ async function main() { const signerAddress = walletAccount.address // === Instantiate contracts === + const tokenToApprove = withSwap ? ADDRESS_USDT_ARB : ADDRESS_USDC_ARB const srcTokenContract = getContract({ - address: srcTokenAddress, + address: tokenToApprove, abi: ERC20_ABI, client, }) @@ -52,7 +52,7 @@ async function main() { const srcTokenName = (await srcTokenContract.read.name()) as string const srcTokenSymbol = (await srcTokenContract.read.symbol()) as string const srcTokenDecimals = (await srcTokenContract.read.decimals()) as bigint - const amount = parseUnits('10', Number(srcTokenDecimals)) // 10 USDC + const amount = parseUnits('10', Number(srcTokenDecimals)) console.info( `\nBridge ${amount} ${srcTokenName} (${srcTokenSymbol}) from ${srcChain} --> Arbitrum` @@ -61,21 +61,13 @@ async function main() { await ensureBalance(srcTokenContract, signerAddress, amount) - await ensureAllowance( - srcTokenContract, - signerAddress, - lifiDiamondAddress, - amount, - publicClient - ) - // === Prepare bridge data === const bridgeData: ILiFi.BridgeDataStruct = { transactionId: `0x${randomBytes(32).toString('hex')}`, bridge: 'chainflip', integrator: 'ACME Devs', referrer: zeroAddress, - sendingAssetId: srcTokenAddress, + sendingAssetId: withSwap ? ADDRESS_USDC_ARB : tokenToApprove, receiver: signerAddress, destinationChainId, minAmount: amount, @@ -84,7 +76,7 @@ async function main() { } const chainflipData: ChainflipFacet.ChainflipDataStruct = { - dstToken: 3, // USDC + dstToken: 3, // Chainflip designator for USDC on ETH nonEvmAddress: '0x0000000000000000000000000000000000000000000000000000000000000000', cfParameters: '0x', // Empty parameters as per implementation @@ -92,34 +84,23 @@ async function main() { // === Start bridging === if (withSwap) { - // Get expected output from swapping the specified amount of USDT to USDC. - const amountsOut = await getAmountsOutUniswap( - ADDRESS_UNISWAP_ARB, - 42161, - [ADDRESS_USDT_ARB, ADDRESS_USDC_ARB], - amount - ) - console.log('Swap amounts out:', amountsOut) - - // Adjust bridgeData.minAmount to a lower value to allow for slippage. - // Here we set it to 98% of the estimated output. - const expectedOutput = (BigInt(amountsOut[1]) * 98n) / 100n - bridgeData.minAmount = expectedOutput - console.log( - 'Updated bridgeData.minAmount:', - bridgeData.minAmount.toString() - ) - - // Generate real swap data to swap USDT -> USDC. - const swapData = await getUniswapSwapDataERC20ToERC20( + // Generate swap data to swap USDT -> exact USDC amount + const swapData = await getUniswapDataERC20toExactERC20( ADDRESS_UNISWAP_ARB, // Uniswap router address on Arbitrum 42161, // Arbitrum chain id ADDRESS_USDT_ARB, // Swap from USDT ADDRESS_USDC_ARB, // Swap to USDC - expectedOutput, // Pass the expected output (in USDC) as the exact output amount + amount, // The exact output amount we want in USDC lifiDiamondAddress, // Receiver for the swapped tokens (the diamond) - true, // requiresDeposit flag - 0 // minAmountOut (0 lets the helper calculate slippage tolerance automatically) + true // requiresDeposit flag + ) + + await ensureAllowance( + srcTokenContract, + signerAddress, + lifiDiamondAddress, + swapData.fromAmount, + publicClient ) await executeTransaction( @@ -134,6 +115,14 @@ async function main() { true ) } else { + await ensureAllowance( + srcTokenContract, + signerAddress, + lifiDiamondAddress, + amount, + publicClient + ) + await executeTransaction( () => lifiDiamondContract.write.startBridgeTokensViaChainflip([ diff --git a/script/demoScripts/utils/demoScriptHelpers.ts b/script/demoScripts/utils/demoScriptHelpers.ts index e2a30c30b..e42bf722f 100644 --- a/script/demoScripts/utils/demoScriptHelpers.ts +++ b/script/demoScripts/utils/demoScriptHelpers.ts @@ -309,7 +309,7 @@ export const getUniswapDataERC20toExactERC20 = async ( // Get the required input amount for the exact output const amounts = await uniswap.getAmountsIn(exactAmountOut, path) const requiredInputAmount = amounts[0] - const maxAmountIn = BigNumber.from(requiredInputAmount).mul(110).div(100) // 10% max slippage + const maxAmountIn = BigNumber.from(requiredInputAmount).mul(105).div(100) // 5% max slippage console.log('Required input amount:', requiredInputAmount.toString()) console.log('Max input with slippage:', maxAmountIn.toString()) From aef020aace128b3baf7b24d4568b4dce27e8185d Mon Sep 17 00:00:00 2001 From: Ed Zynda Date: Thu, 13 Feb 2025 11:54:04 +0300 Subject: [PATCH 09/55] Update src/Interfaces/IChainflip.sol Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- src/Interfaces/IChainflip.sol | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Interfaces/IChainflip.sol b/src/Interfaces/IChainflip.sol index 69b760a46..bd0add4a3 100644 --- a/src/Interfaces/IChainflip.sol +++ b/src/Interfaces/IChainflip.sol @@ -3,7 +3,12 @@ pragma solidity ^0.8.17; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.17; + +/// @custom:version 1.0.0 interface IChainflipVault { + function xSwapNative( function xSwapNative( uint32 dstChain, bytes calldata dstAddress, From b79b7949b3c3a10658486b69a40323caa6a2c23b Mon Sep 17 00:00:00 2001 From: Ed Zynda Date: Thu, 13 Feb 2025 16:14:22 +0300 Subject: [PATCH 10/55] updates --- src/Facets/ChainflipFacet.sol | 74 +++++++++++++++++++++++++---------- src/Interfaces/IChainflip.sol | 20 ++++++++++ 2 files changed, 73 insertions(+), 21 deletions(-) diff --git a/src/Facets/ChainflipFacet.sol b/src/Facets/ChainflipFacet.sol index 6ab244a6b..1bf6ce5f3 100644 --- a/src/Facets/ChainflipFacet.sol +++ b/src/Facets/ChainflipFacet.sol @@ -10,7 +10,7 @@ import { SwapperV2 } from "../Helpers/SwapperV2.sol"; import { Validatable } from "../Helpers/Validatable.sol"; import { IChainflipVault } from "../Interfaces/IChainflip.sol"; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import { console } from "forge-std/console.sol"; +import { InformationMismatch } from "../Errors/GenericErrors.sol"; /// @title Chainflip Facet /// @author LI.FI (https://li.fi) @@ -72,7 +72,6 @@ contract ChainflipFacet is ILiFi, ReentrancyGuard, SwapperV2, Validatable { refundExcessNative(payable(msg.sender)) validateBridgeData(_bridgeData) doesNotContainSourceSwaps(_bridgeData) - doesNotContainDestinationCalls(_bridgeData) { LibAsset.depositAsset( _bridgeData.sendingAssetId, @@ -95,7 +94,6 @@ contract ChainflipFacet is ILiFi, ReentrancyGuard, SwapperV2, Validatable { nonReentrant refundExcessNative(payable(msg.sender)) containsSourceSwaps(_bridgeData) - doesNotContainDestinationCalls(_bridgeData) validateBridgeData(_bridgeData) { _bridgeData.minAmount = _depositAndSwap( @@ -150,18 +148,39 @@ contract ChainflipFacet is ILiFi, ReentrancyGuard, SwapperV2, Validatable { encodedDstAddress = abi.encodePacked(_bridgeData.receiver); } - // Handle native token case + // Validate destination call flag matches message presence + if ( + _bridgeData.hasDestinationCall != + (_chainflipData.cfParameters.length > 0) + ) { + revert InformationMismatch(); + } + + // Handle native token case with or without CCM if (_bridgeData.sendingAssetId == address(0)) { - IChainflipVault(chainflipVault).xSwapNative{ - value: _bridgeData.minAmount - }( - dstChain, - encodedDstAddress, - _chainflipData.dstToken, - _chainflipData.cfParameters - ); + if (_bridgeData.hasDestinationCall) { + IChainflipVault(chainflipVault).xCallNative{ + value: _bridgeData.minAmount + }( + dstChain, + encodedDstAddress, + _chainflipData.dstToken, + _chainflipData.cfParameters, // Used as message for CCM + 0, // Gas budget - currently unused by Chainflip + _chainflipData.cfParameters // Additional parameters + ); + } else { + IChainflipVault(chainflipVault).xSwapNative{ + value: _bridgeData.minAmount + }( + dstChain, + encodedDstAddress, + _chainflipData.dstToken, + _chainflipData.cfParameters + ); + } } - // Handle ERC20 token case + // Handle ERC20 token case with or without CCM else { // Approve vault to spend tokens LibAsset.maxApproveERC20( @@ -170,14 +189,27 @@ contract ChainflipFacet is ILiFi, ReentrancyGuard, SwapperV2, Validatable { _bridgeData.minAmount ); - IChainflipVault(chainflipVault).xSwapToken( - dstChain, - encodedDstAddress, - _chainflipData.dstToken, - IERC20(_bridgeData.sendingAssetId), - _bridgeData.minAmount, - _chainflipData.cfParameters - ); + if (_bridgeData.hasDestinationCall) { + IChainflipVault(chainflipVault).xCallToken( + dstChain, + encodedDstAddress, + _chainflipData.dstToken, + _chainflipData.cfParameters, // Used as message for CCM + 0, // Gas budget - currently unused by Chainflip + IERC20(_bridgeData.sendingAssetId), + _bridgeData.minAmount, + _chainflipData.cfParameters // Additional parameters + ); + } else { + IChainflipVault(chainflipVault).xSwapToken( + dstChain, + encodedDstAddress, + _chainflipData.dstToken, + IERC20(_bridgeData.sendingAssetId), + _bridgeData.minAmount, + _chainflipData.cfParameters + ); + } } emit LiFiTransferStarted(_bridgeData); diff --git a/src/Interfaces/IChainflip.sol b/src/Interfaces/IChainflip.sol index 69b760a46..74f4f4eb1 100644 --- a/src/Interfaces/IChainflip.sol +++ b/src/Interfaces/IChainflip.sol @@ -19,4 +19,24 @@ interface IChainflipVault { uint256 amount, bytes calldata cfParameters ) external; + + function xCallNative( + uint32 dstChain, + bytes calldata dstAddress, + uint32 dstToken, + bytes calldata message, + uint256 gasAmount, + bytes calldata cfParameters + ) external payable; + + function xCallToken( + uint32 dstChain, + bytes calldata dstAddress, + uint32 dstToken, + bytes calldata message, + uint256 gasAmount, + IERC20 srcToken, + uint256 amount, + bytes calldata cfParameters + ) external; } From 537cf7638db52df1b0caa1bddbf29322c92265e2 Mon Sep 17 00:00:00 2001 From: Ed Zynda Date: Thu, 13 Feb 2025 16:37:35 +0300 Subject: [PATCH 11/55] add more tests --- test/solidity/Facets/ChainflipFacet.t.sol | 127 ++++++++++++++++------ 1 file changed, 95 insertions(+), 32 deletions(-) diff --git a/test/solidity/Facets/ChainflipFacet.t.sol b/test/solidity/Facets/ChainflipFacet.t.sol index c5f9a7fca..c58cc09ca 100644 --- a/test/solidity/Facets/ChainflipFacet.t.sol +++ b/test/solidity/Facets/ChainflipFacet.t.sol @@ -4,6 +4,7 @@ pragma solidity 0.8.17; import { LibAllowList, TestBaseFacet, console, ERC20 } from "../utils/TestBaseFacet.sol"; import { ChainflipFacet } from "lifi/Facets/ChainflipFacet.sol"; import { LibAsset } from "lifi/Libraries/LibAsset.sol"; +import { LibSwap } from "lifi/Libraries/LibSwap.sol"; import { stdJson } from "forge-std/StdJson.sol"; using stdJson for string; @@ -85,38 +86,6 @@ contract ChainflipFacetTest is TestBaseFacet { }); } - // All facet test files inherit from `utils/TestBaseFacet.sol` and require the following method overrides: - // - function initiateBridgeTxWithFacet(bool isNative) - // - function initiateSwapAndBridgeTxWithFacet(bool isNative) - // - // These methods are used to run the following tests which must pass: - // - testBase_CanBridgeNativeTokens() - // - testBase_CanBridgeTokens() - // - testBase_CanBridgeTokens_fuzzed(uint256) - // - testBase_CanSwapAndBridgeNativeTokens() - // - testBase_CanSwapAndBridgeTokens() - // - testBase_Revert_BridgeAndSwapWithInvalidReceiverAddress() - // - testBase_Revert_BridgeToSameChainId() - // - testBase_Revert_BridgeWithInvalidAmount() - // - testBase_Revert_BridgeWithInvalidDestinationCallFlag() - // - testBase_Revert_BridgeWithInvalidReceiverAddress() - // - testBase_Revert_CallBridgeOnlyFunctionWithSourceSwapFlag() - // - testBase_Revert_CallerHasInsufficientFunds() - // - testBase_Revert_SwapAndBridgeToSameChainId() - // - testBase_Revert_SwapAndBridgeWithInvalidAmount() - // - testBase_Revert_SwapAndBridgeWithInvalidSwapData() - // - // In some cases it doesn't make sense to have all tests. For example the bridge may not support native tokens. - // In that case you can override the test method and leave it empty. For example: - // - // function testBase_CanBridgeNativeTokens() public override { - // // facet does not support bridging of native assets - // } - // - // function testBase_CanSwapAndBridgeNativeTokens() public override { - // // facet does not support bridging of native assets - // } - function initiateBridgeTxWithFacet(bool isNative) internal override { if (isNative) { chainflipFacet.startBridgeTokensViaChainflip{ @@ -216,6 +185,56 @@ contract ChainflipFacetTest is TestBaseFacet { vm.stopPrank(); } + function test_CanBridgeTokensToEthereum() + public + assertBalanceChange( + ADDRESS_USDC, + USER_SENDER, + -int256(defaultUSDCAmount) + ) + assertBalanceChange(ADDRESS_USDC, USER_RECEIVER, 0) + assertBalanceChange(ADDRESS_DAI, USER_SENDER, 0) + assertBalanceChange(ADDRESS_DAI, USER_RECEIVER, 0) + { + // Set source chain to Arbitrum for this test + vm.chainId(CHAIN_ID_ARBITRUM); + vm.roll(208460950); // Set specific block number for Arbitrum chain + + // Set destination to Ethereum + bridgeData.destinationChainId = CHAIN_ID_ETHEREUM; + validChainflipData = ChainflipFacet.ChainflipData({ + dstToken: 3, // USDC on Ethereum + nonEvmAddress: bytes32(0), // Not needed for EVM chains + cfParameters: "" + }); + + vm.startPrank(USER_SENDER); + + // approval + usdc.approve(_facetTestContractAddress, bridgeData.minAmount); + + //prepare check for events + vm.expectEmit(true, true, true, true, _facetTestContractAddress); + emit LiFiTransferStarted(bridgeData); + + initiateBridgeTxWithFacet(false); + vm.stopPrank(); + } + + function testRevert_WhenUsingUnsupportedDestinationChain() public { + // Set destination chain to Polygon (unsupported) + bridgeData.destinationChainId = 137; + + vm.startPrank(USER_SENDER); + + // approval + usdc.approve(_facetTestContractAddress, bridgeData.minAmount); + + vm.expectRevert("ChainflipFacet: Unsupported destination chain"); + initiateBridgeTxWithFacet(false); + vm.stopPrank(); + } + function testRevert_WhenUsingEmptyNonEVMAddress() public { bridgeData.receiver = LibAsset.NON_EVM_ADDRESS; bridgeData.destinationChainId = CHAIN_ID_SOLANA; @@ -234,4 +253,48 @@ contract ChainflipFacetTest is TestBaseFacet { initiateBridgeTxWithFacet(false); vm.stopPrank(); } + + function test_CanBridgeTokensWithDestinationCall() + public + assertBalanceChange( + ADDRESS_USDC, + USER_SENDER, + -int256(defaultUSDCAmount) + ) + assertBalanceChange(ADDRESS_USDC, USER_RECEIVER, 0) + assertBalanceChange(ADDRESS_DAI, USER_SENDER, 0) + assertBalanceChange(ADDRESS_DAI, USER_RECEIVER, 0) + { + // Set destination to Arbitrum where our receiver contract is + bridgeData.destinationChainId = CHAIN_ID_ARBITRUM; + bridgeData.hasDestinationCall = true; + + // Create swap data for the destination chain + LibSwap.SwapData[] memory destSwapData = new LibSwap.SwapData[](0); + + // Encode the message for the receiver contract + bytes memory message = abi.encode( + bridgeData.transactionId, + destSwapData, + USER_RECEIVER // Final receiver of the tokens + ); + + validChainflipData = ChainflipFacet.ChainflipData({ + dstToken: 7, // USDC on Arbitrum + nonEvmAddress: bytes32(0), // Not needed for EVM chains + cfParameters: message // Pass the encoded message for CCM + }); + + vm.startPrank(USER_SENDER); + + // approval + usdc.approve(_facetTestContractAddress, bridgeData.minAmount); + + //prepare check for events + vm.expectEmit(true, true, true, true, _facetTestContractAddress); + emit LiFiTransferStarted(bridgeData); + + initiateBridgeTxWithFacet(false); + vm.stopPrank(); + } } From c353f90eb4165e2759e0297941f19759383fb898 Mon Sep 17 00:00:00 2001 From: Ed Zynda Date: Thu, 13 Feb 2025 16:42:32 +0300 Subject: [PATCH 12/55] Fix interface --- docs/ChainflipFacet.md | 8 +++++--- src/Interfaces/IChainflip.sol | 4 ---- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/docs/ChainflipFacet.md b/docs/ChainflipFacet.md index 2d109a806..2cfed7dcd 100644 --- a/docs/ChainflipFacet.md +++ b/docs/ChainflipFacet.md @@ -23,9 +23,9 @@ The methods listed above take a variable labeled `_chainflipData`. This data is ```solidity struct ChainflipData { - uint32 dstToken; // Token identifier on the destination chain - bytes32 nonEvmAddress; // Destination address for non-EVM chains (Solana, Bitcoin) - bytes cfParameters; // Additional parameters for future features + uint32 dstToken; // Token identifier on the destination chain + bytes32 nonEvmAddress; // Destination address for non-EVM chains (Solana, Bitcoin) + bytes cfParameters; // Additional parameters for future features } ``` @@ -40,6 +40,8 @@ The facet supports the following chains with their respective IDs: - Solana (1151111081099710) - Bitcoin (20000000000001) +[Reference](https://docs.chainflip.io/swapping/integrations/advanced/vault-swaps#supported-chains) + ## Swap Data Some methods accept a `SwapData _swapData` parameter. diff --git a/src/Interfaces/IChainflip.sol b/src/Interfaces/IChainflip.sol index 34fcd9d6b..568a42261 100644 --- a/src/Interfaces/IChainflip.sol +++ b/src/Interfaces/IChainflip.sol @@ -3,12 +3,8 @@ pragma solidity ^0.8.17; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.17; - /// @custom:version 1.0.0 interface IChainflipVault { - function xSwapNative( function xSwapNative( uint32 dstChain, bytes calldata dstAddress, From 7c619a347049e39da9a6e7dee1ec6edd72be4edf Mon Sep 17 00:00:00 2001 From: Ed Zynda Date: Thu, 13 Feb 2025 16:57:39 +0300 Subject: [PATCH 13/55] Add helper --- test/solidity/Facets/ChainflipFacet.t.sol | 13 ++++--------- test/solidity/utils/TestBase.sol | 16 ++++++++++++++++ 2 files changed, 20 insertions(+), 9 deletions(-) diff --git a/test/solidity/Facets/ChainflipFacet.t.sol b/test/solidity/Facets/ChainflipFacet.t.sol index c58cc09ca..50b21ffbc 100644 --- a/test/solidity/Facets/ChainflipFacet.t.sol +++ b/test/solidity/Facets/ChainflipFacet.t.sol @@ -5,9 +5,6 @@ import { LibAllowList, TestBaseFacet, console, ERC20 } from "../utils/TestBaseFa import { ChainflipFacet } from "lifi/Facets/ChainflipFacet.sol"; import { LibAsset } from "lifi/Libraries/LibAsset.sol"; import { LibSwap } from "lifi/Libraries/LibSwap.sol"; -import { stdJson } from "forge-std/StdJson.sol"; - -using stdJson for string; // Stub ChainflipFacet Contract contract TestChainflipFacet is ChainflipFacet { @@ -36,13 +33,11 @@ contract ChainflipFacetTest is TestBaseFacet { customBlockNumberForForking = 18277082; initTestBase(); - // Read chainflip vault address from config - string memory path = string.concat( - vm.projectRoot(), - "/config/chainflip.json" + // Read chainflip vault address from config using the new helper + CHAINFLIP_VAULT = getConfigAddressFromPath( + "chainflip.json", + ".mainnet.chainflipVault" ); - string memory json = vm.readFile(path); - CHAINFLIP_VAULT = json.readAddress(".mainnet.chainflipVault"); vm.label(CHAINFLIP_VAULT, "Chainflip Vault"); console.log("Chainflip Vault Address:", CHAINFLIP_VAULT); diff --git a/test/solidity/utils/TestBase.sol b/test/solidity/utils/TestBase.sol index 14813f95d..6dde098b0 100644 --- a/test/solidity/utils/TestBase.sol +++ b/test/solidity/utils/TestBase.sol @@ -17,6 +17,9 @@ import { LibAccess } from "lifi/Libraries/LibAccess.sol"; import { console } from "test/solidity/utils/Console.sol"; import { FeeCollector } from "lifi/Periphery/FeeCollector.sol"; import { NoSwapDataProvided, InformationMismatch, NativeAssetTransferFailed, ReentrancyError, InsufficientBalance, CannotBridgeToSameNetwork, InvalidReceiver, InvalidAmount, InvalidConfig, InvalidSendingToken, AlreadyInitialized, NotInitialized, UnAuthorized } from "src/Errors/GenericErrors.sol"; +import { stdJson } from "forge-std/StdJson.sol"; + +using stdJson for string; contract TestFacet { constructor() {} @@ -530,5 +533,18 @@ abstract contract TestBase is Test, DiamondTest, ILiFi { uint256 targetBlock = block.number + numBlocks; vm.roll(targetBlock); } + + function getConfigAddressFromPath( + string memory configFileName, + string memory jsonPath + ) internal returns (address) { + string memory path = string.concat( + vm.projectRoot(), + "/config/", + configFileName + ); + string memory json = vm.readFile(path); + return json.readAddress(jsonPath); + } //#endregion } From 9e98ccdd01f4c86b21ffc15be7ce23ac65ecaeae Mon Sep 17 00:00:00 2001 From: Ed Zynda Date: Thu, 13 Feb 2025 17:06:00 +0300 Subject: [PATCH 14/55] flip the config structure --- config/chainflip.json | 8 +++----- script/deploy/facets/DeployChainflipFacet.s.sol | 2 +- script/deploy/facets/DeployReceiverChainflip.s.sol | 2 +- script/deploy/resources/deployRequirements.json | 4 ++-- test/solidity/Facets/ChainflipFacet.t.sol | 2 +- test/solidity/Periphery/ReceiverChainflip.t.sol | 9 +++------ 6 files changed, 11 insertions(+), 16 deletions(-) diff --git a/config/chainflip.json b/config/chainflip.json index c589d50ea..1235df3ae 100644 --- a/config/chainflip.json +++ b/config/chainflip.json @@ -1,8 +1,6 @@ { - "mainnet": { - "chainflipVault": "0xF5e10380213880111522dd0efD3dbb45b9f62Bcc" - }, - "arbitrum": { - "chainflipVault": "0x79001a5e762f3bEFC8e5871b42F6734e00498920" + "chainflipVault": { + "mainnet": "0xF5e10380213880111522dd0efD3dbb45b9f62Bcc", + "arbitrum": "0x79001a5e762f3bEFC8e5871b42F6734e00498920" } } diff --git a/script/deploy/facets/DeployChainflipFacet.s.sol b/script/deploy/facets/DeployChainflipFacet.s.sol index ee45e1a3c..9353420f6 100644 --- a/script/deploy/facets/DeployChainflipFacet.s.sol +++ b/script/deploy/facets/DeployChainflipFacet.s.sol @@ -26,7 +26,7 @@ contract DeployScript is DeployScriptBase { // Read the Chainflip vault address from config address chainflipVault = _getConfigContractAddress( path, - string.concat(".", network, ".chainflipVault") + string.concat(".chainflipVault.", network) ); return abi.encode(chainflipVault); diff --git a/script/deploy/facets/DeployReceiverChainflip.s.sol b/script/deploy/facets/DeployReceiverChainflip.s.sol index 09be37722..4557e2a93 100644 --- a/script/deploy/facets/DeployReceiverChainflip.s.sol +++ b/script/deploy/facets/DeployReceiverChainflip.s.sol @@ -41,7 +41,7 @@ contract DeployScript is DeployScriptBase { address chainflipVault = _getConfigContractAddress( path, - string.concat(".", network, ".chainflipVault") + string.concat(".chainflipVault.", network) ); // get Executor address from deploy log diff --git a/script/deploy/resources/deployRequirements.json b/script/deploy/resources/deployRequirements.json index 112c3dc6a..4c707f8fb 100644 --- a/script/deploy/resources/deployRequirements.json +++ b/script/deploy/resources/deployRequirements.json @@ -574,7 +574,7 @@ "configData": { "_chainflipVault": { "configFileName": "chainflip.json", - "keyInConfigFile": "..chainflipVault", + "keyInConfigFile": ".chainflipVault.", "allowToDeployWithZeroAddress": "false" } } @@ -588,7 +588,7 @@ }, "_chainflipVault": { "configFileName": "chainflip.json", - "keyInConfigFile": "..chainflipVault", + "keyInConfigFile": ".chainflipVault.", "allowToDeployWithZeroAddress": "false" } }, diff --git a/test/solidity/Facets/ChainflipFacet.t.sol b/test/solidity/Facets/ChainflipFacet.t.sol index 50b21ffbc..0c241d6ed 100644 --- a/test/solidity/Facets/ChainflipFacet.t.sol +++ b/test/solidity/Facets/ChainflipFacet.t.sol @@ -36,7 +36,7 @@ contract ChainflipFacetTest is TestBaseFacet { // Read chainflip vault address from config using the new helper CHAINFLIP_VAULT = getConfigAddressFromPath( "chainflip.json", - ".mainnet.chainflipVault" + ".chainflipVault.mainnet" ); vm.label(CHAINFLIP_VAULT, "Chainflip Vault"); console.log("Chainflip Vault Address:", CHAINFLIP_VAULT); diff --git a/test/solidity/Periphery/ReceiverChainflip.t.sol b/test/solidity/Periphery/ReceiverChainflip.t.sol index f1aed817e..69966f8af 100644 --- a/test/solidity/Periphery/ReceiverChainflip.t.sol +++ b/test/solidity/Periphery/ReceiverChainflip.t.sol @@ -26,13 +26,10 @@ contract ReceiverChainflipTest is TestBase { customBlockNumberForForking = 18277082; initTestBase(); - // Read chainflip vault address from config - string memory path = string.concat( - vm.projectRoot(), - "/config/chainflip.json" + chainflipVault = getConfigAddressFromPath( + "chainflip.json", + ".chainflipVault.mainnet" ); - string memory json = vm.readFile(path); - chainflipVault = json.readAddress(".mainnet.chainflipVault"); vm.label(chainflipVault, "Chainflip Vault"); erc20Proxy = new ERC20Proxy(address(this)); From 61d71b9ad80489a3dc52be0987ba66a111d86118 Mon Sep 17 00:00:00 2001 From: Ed Zynda Date: Fri, 14 Feb 2025 10:15:49 +0300 Subject: [PATCH 15/55] fix typo --- script/demoScripts/demoChainflip.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/script/demoScripts/demoChainflip.ts b/script/demoScripts/demoChainflip.ts index a31746f78..fee5a4e48 100644 --- a/script/demoScripts/demoChainflip.ts +++ b/script/demoScripts/demoChainflip.ts @@ -55,7 +55,7 @@ async function main() { const amount = parseUnits('10', Number(srcTokenDecimals)) console.info( - `\nBridge ${amount} ${srcTokenName} (${srcTokenSymbol}) from ${srcChain} --> Arbitrum` + `\nBridge ${amount} ${srcTokenName} (${srcTokenSymbol}) from ${srcChain} --> Mainnet` ) console.info(`Connected wallet address: ${signerAddress}`) From 78143ee7cd297dae8efe0a20a3f3de5058eca52f Mon Sep 17 00:00:00 2001 From: Ed Zynda Date: Fri, 14 Feb 2025 11:30:44 +0300 Subject: [PATCH 16/55] updates --- src/Facets/ChainflipFacet.sol | 55 +++++++----- src/Periphery/ReceiverChainflip.sol | 53 +++++++++--- test/solidity/Facets/ChainflipFacet.t.sol | 7 +- .../Periphery/ReceiverChainflip.t.sol | 84 +++++++++++++++++-- 4 files changed, 157 insertions(+), 42 deletions(-) diff --git a/src/Facets/ChainflipFacet.sol b/src/Facets/ChainflipFacet.sol index 1bf6ce5f3..73275edcb 100644 --- a/src/Facets/ChainflipFacet.sol +++ b/src/Facets/ChainflipFacet.sol @@ -2,7 +2,6 @@ pragma solidity ^0.8.17; import { ILiFi } from "../Interfaces/ILiFi.sol"; -import { LibDiamond } from "../Libraries/LibDiamond.sol"; import { LibAsset } from "../Libraries/LibAsset.sol"; import { LibSwap } from "../Libraries/LibSwap.sol"; import { ReentrancyGuard } from "../Helpers/ReentrancyGuard.sol"; @@ -26,13 +25,15 @@ contract ChainflipFacet is ILiFi, ReentrancyGuard, SwapperV2, Validatable { /// Errors /// error EmptyNonEvmAddress(); - address public immutable chainflipVault; + error UnsupportedChainflipChainId(); + /// Storage /// + + IChainflipVault public immutable chainflipVault; uint256 private constant CHAIN_ID_ETHEREUM = 1; uint256 private constant CHAIN_ID_ARBITRUM = 42161; uint256 private constant CHAIN_ID_SOLANA = 1151111081099710; uint256 private constant CHAIN_ID_BITCOIN = 20000000000001; - uint32 private constant CHAINFLIP_ID_ETHEREUM = 1; uint32 private constant CHAINFLIP_ID_ARBITRUM = 4; uint32 private constant CHAINFLIP_ID_SOLANA = 5; @@ -42,7 +43,7 @@ contract ChainflipFacet is ILiFi, ReentrancyGuard, SwapperV2, Validatable { /// @dev Parameters specific to Chainflip bridge /// @param dstToken Token to be received on the destination chain (uint32) - /// @param cfParameters Additional metadata for future features (currently unused) + /// @param cfParameters Additional metadata struct ChainflipData { uint32 dstToken; bytes32 nonEvmAddress; @@ -53,7 +54,7 @@ contract ChainflipFacet is ILiFi, ReentrancyGuard, SwapperV2, Validatable { /// @notice Constructor for the contract. /// @param _chainflipVault Address of the Chainflip vault contract - constructor(address _chainflipVault) { + constructor(IChainflipVault _chainflipVault) { chainflipVault = _chainflipVault; } @@ -107,26 +108,15 @@ contract ChainflipFacet is ILiFi, ReentrancyGuard, SwapperV2, Validatable { /// Internal Methods /// - /// @dev Contains the business logic for the bridge via Chainflip - /// @param _bridgeData The core information needed for bridging - /// @param _chainflipData Data specific to Chainflip + /// @notice Contains the business logic for bridging via Chainflip + /// @param _bridgeData The core information needed for bridging, including sending/receiving details + /// @param _chainflipData Data specific to Chainflip, including destination token and parameters + /// @dev Handles both EVM and non-EVM destinations, native and ERC20 tokens, and cross-chain messaging function _startBridge( ILiFi.BridgeData memory _bridgeData, ChainflipData calldata _chainflipData ) internal { - // Map the destination chain ID to Chainflip format - uint32 dstChain; - if (_bridgeData.destinationChainId == CHAIN_ID_ETHEREUM) { - dstChain = CHAINFLIP_ID_ETHEREUM; - } else if (_bridgeData.destinationChainId == CHAIN_ID_ARBITRUM) { - dstChain = CHAINFLIP_ID_ARBITRUM; - } else if (_bridgeData.destinationChainId == CHAIN_ID_SOLANA) { - dstChain = CHAINFLIP_ID_SOLANA; - } else if (_bridgeData.destinationChainId == CHAIN_ID_BITCOIN) { - dstChain = CHAINFLIP_ID_BITCOIN; - } else { - revert("ChainflipFacet: Unsupported destination chain"); - } + uint32 dstChain = _getChainflipChainId(_bridgeData.destinationChainId); // Handle address encoding based on destination chain type bytes memory encodedDstAddress; @@ -185,7 +175,7 @@ contract ChainflipFacet is ILiFi, ReentrancyGuard, SwapperV2, Validatable { // Approve vault to spend tokens LibAsset.maxApproveERC20( IERC20(_bridgeData.sendingAssetId), - chainflipVault, + address(chainflipVault), _bridgeData.minAmount ); @@ -214,4 +204,25 @@ contract ChainflipFacet is ILiFi, ReentrancyGuard, SwapperV2, Validatable { emit LiFiTransferStarted(_bridgeData); } + + /// @notice Converts LiFi chain IDs to Chainflip chain IDs + /// @param destinationChainId The LiFi chain ID to convert + /// @return The corresponding Chainflip chain ID (uint32) + /// @dev Supports Ethereum (1), Arbitrum (4), Solana (5), and Bitcoin (3) + /// @dev Reverts if the destination chain is not supported + function _getChainflipChainId( + uint256 destinationChainId + ) internal pure returns (uint32) { + if (destinationChainId == CHAIN_ID_ETHEREUM) { + return CHAINFLIP_ID_ETHEREUM; + } else if (destinationChainId == CHAIN_ID_ARBITRUM) { + return CHAINFLIP_ID_ARBITRUM; + } else if (destinationChainId == CHAIN_ID_SOLANA) { + return CHAINFLIP_ID_SOLANA; + } else if (destinationChainId == CHAIN_ID_BITCOIN) { + return CHAINFLIP_ID_BITCOIN; + } else { + revert UnsupportedChainflipChainId(); + } + } } diff --git a/src/Periphery/ReceiverChainflip.sol b/src/Periphery/ReceiverChainflip.sol index a6e5d11ba..54cbb2b1c 100644 --- a/src/Periphery/ReceiverChainflip.sol +++ b/src/Periphery/ReceiverChainflip.sol @@ -14,12 +14,19 @@ import { SafeTransferLib } from "solady/utils/SafeTransferLib.sol"; /// @custom:version 1.0.0 contract ReceiverChainflip is ILiFi, WithdrawablePeriphery { using SafeTransferLib for address; + using SafeTransferLib for address payable; /// Storage /// + + /// @notice The executor contract used for performing swaps IExecutor public immutable executor; + /// @notice The Chainflip vault contract that is authorized to call this contract address public immutable chainflipVault; /// Modifiers /// + + /// @notice Ensures only the Chainflip vault can call the function + /// @dev Reverts with UnAuthorized if called by any other address modifier onlyChainflipVault() { if (msg.sender != chainflipVault) { revert UnAuthorized(); @@ -27,7 +34,12 @@ contract ReceiverChainflip is ILiFi, WithdrawablePeriphery { _; } - /// Constructor + /// Constructor /// + + /// @notice Initializes the contract with required addresses + /// @param _owner Address that can withdraw funds from this contract + /// @param _executor Address of the executor contract for performing swaps + /// @param _chainflipVault Address of the Chainflip vault that can call this contract constructor( address _owner, address _executor, @@ -78,6 +90,14 @@ contract ReceiverChainflip is ILiFi, WithdrawablePeriphery { /// @param assetId address of the token received from the source chain /// @param receiver address that will receive tokens in the end /// @param amount amount of token + /// @notice Performs a swap before completing a cross-chain transaction + /// @param _transactionId The transaction id associated with the operation + /// @param _swapData Array of data needed for swaps + /// @param assetId Address of the token received from the source chain + /// @param receiver Address that will receive tokens in the end + /// @param amount Amount of token to swap + /// @dev If the swap fails, the original bridged tokens are sent directly to the receiver + /// @dev Approvals are reset to 0 after the operation completes function _swapAndCompleteBridgeTokens( bytes32 _transactionId, LibSwap.SwapData[] memory _swapData, @@ -85,17 +105,24 @@ contract ReceiverChainflip is ILiFi, WithdrawablePeriphery { address payable receiver, uint256 amount ) private { - assetId.safeApproveWithRetry(address(executor), amount); + // Don't need approval for native token + if (assetId != LibAsset.NATIVE_ASSETID) { + assetId.safeApproveWithRetry(address(executor), amount); + } + try - executor.swapAndCompleteBridgeTokens( - _transactionId, - _swapData, - assetId, - receiver - ) + executor.swapAndCompleteBridgeTokens{ + value: assetId == LibAsset.NATIVE_ASSETID ? amount : 0 + }(_transactionId, _swapData, assetId, receiver) {} catch { // send the bridged (and unswapped) funds to receiver address - assetId.safeTransfer(receiver, amount); + if (assetId == LibAsset.NATIVE_ASSETID) { + // Handle native token using safeTransferETH + receiver.safeTransferETH(amount); + } else { + // Handle ERC20 token + assetId.safeTransfer(receiver, amount); + } emit LiFiTransferRecovered( _transactionId, @@ -106,10 +133,14 @@ contract ReceiverChainflip is ILiFi, WithdrawablePeriphery { ); } - // reset approval to 0 - assetId.safeApprove(address(executor), 0); + // Only reset approval for non-native tokens + if (assetId != LibAsset.NATIVE_ASSETID) { + assetId.safeApprove(address(executor), 0); + } } /// @notice Receive native asset directly. + /// @notice Allows the contract to receive native assets directly + /// @dev Required for receiving native token transfers receive() external payable {} } diff --git a/test/solidity/Facets/ChainflipFacet.t.sol b/test/solidity/Facets/ChainflipFacet.t.sol index 0c241d6ed..17ad79c8d 100644 --- a/test/solidity/Facets/ChainflipFacet.t.sol +++ b/test/solidity/Facets/ChainflipFacet.t.sol @@ -3,12 +3,15 @@ pragma solidity 0.8.17; import { LibAllowList, TestBaseFacet, console, ERC20 } from "../utils/TestBaseFacet.sol"; import { ChainflipFacet } from "lifi/Facets/ChainflipFacet.sol"; +import { IChainflipVault } from "lifi/Interfaces/IChainflip.sol"; import { LibAsset } from "lifi/Libraries/LibAsset.sol"; import { LibSwap } from "lifi/Libraries/LibSwap.sol"; // Stub ChainflipFacet Contract contract TestChainflipFacet is ChainflipFacet { - constructor(address _chainflipVault) ChainflipFacet(_chainflipVault) {} + constructor( + address _chainflipVault + ) ChainflipFacet(IChainflipVault(_chainflipVault)) {} function addDex(address _dex) external { LibAllowList.addAllowedContract(_dex); @@ -225,7 +228,7 @@ contract ChainflipFacetTest is TestBaseFacet { // approval usdc.approve(_facetTestContractAddress, bridgeData.minAmount); - vm.expectRevert("ChainflipFacet: Unsupported destination chain"); + vm.expectRevert(ChainflipFacet.UnsupportedChainflipChainId.selector); initiateBridgeTxWithFacet(false); vm.stopPrank(); } diff --git a/test/solidity/Periphery/ReceiverChainflip.t.sol b/test/solidity/Periphery/ReceiverChainflip.t.sol index 69966f8af..86fe44c6c 100644 --- a/test/solidity/Periphery/ReceiverChainflip.t.sol +++ b/test/solidity/Periphery/ReceiverChainflip.t.sol @@ -4,6 +4,7 @@ pragma solidity ^0.8.17; import { Test, TestBase, Vm, LiFiDiamond, DSTest, ILiFi, LibSwap, LibAllowList, console, InvalidAmount, ERC20, UniswapV2Router02 } from "../utils/TestBase.sol"; import { ExternalCallFailed, UnAuthorized } from "src/Errors/GenericErrors.sol"; import { ReceiverChainflip } from "lifi/Periphery/ReceiverChainflip.sol"; +import { LibAsset } from "lifi/Libraries/LibAsset.sol"; import { stdJson } from "forge-std/Script.sol"; import { ERC20Proxy } from "lifi/Periphery/ERC20Proxy.sol"; import { Executor } from "lifi/Periphery/Executor.sol"; @@ -63,8 +64,8 @@ contract ReceiverChainflipTest is TestBase { // pull token vm.startPrank(USER_DIAMOND_OWNER); - receiver.withdrawToken(ADDRESS_DAI, payable(USER_RECEIVER), 1000); + vm.stopPrank(); assertEq(dai.balanceOf(USER_RECEIVER), initialBalance + 1000); } @@ -77,8 +78,8 @@ contract ReceiverChainflipTest is TestBase { // pull token vm.startPrank(USER_DIAMOND_OWNER); - receiver.withdrawToken(address(0), payable(USER_RECEIVER), 1 ether); + vm.stopPrank(); assertEq(USER_RECEIVER.balance, initialBalance + 1 ether); } @@ -90,20 +91,20 @@ contract ReceiverChainflipTest is TestBase { NonETHReceiver nonETHReceiver = new NonETHReceiver(); vm.startPrank(USER_DIAMOND_OWNER); - vm.expectRevert(ExternalCallFailed.selector); - receiver.withdrawToken( address(0), payable(address(nonETHReceiver)), 1 ether ); + vm.stopPrank(); } function test_revert_WithdrawTokenNonOwner() public { vm.startPrank(USER_SENDER); vm.expectRevert(UnAuthorized.selector); receiver.withdrawToken(ADDRESS_DAI, payable(USER_RECEIVER), 1000); + vm.stopPrank(); } function test_revert_OnlyChainflipVaultCanCallCfReceive() public { @@ -113,7 +114,6 @@ contract ReceiverChainflipTest is TestBase { // call from deployer of ReceiverChainflip vm.startPrank(address(this)); vm.expectRevert(UnAuthorized.selector); - receiver.cfReceive( 1, // srcChain (Ethereum) abi.encodePacked(address(0)), @@ -121,11 +121,11 @@ contract ReceiverChainflipTest is TestBase { ADDRESS_USDC, defaultUSDCAmount ); + vm.stopPrank(); // call from random user vm.startPrank(USER_SENDER); vm.expectRevert(UnAuthorized.selector); - receiver.cfReceive( 1, // srcChain (Ethereum) abi.encodePacked(address(0)), @@ -133,6 +133,7 @@ contract ReceiverChainflipTest is TestBase { ADDRESS_USDC, defaultUSDCAmount ); + vm.stopPrank(); } function test_canDecodeChainflipPayloadAndExecuteSwapERC20() public { @@ -202,7 +203,7 @@ contract ReceiverChainflipTest is TestBase { block.timestamp ); receiver.cfReceive( - 1, // srcChain (Ethereum) + 4, // srcChain (Arbitrum) abi.encodePacked(address(0)), payload, ADDRESS_USDC, @@ -212,6 +213,41 @@ contract ReceiverChainflipTest is TestBase { assertTrue(usdc.balanceOf(receiverAddress) == defaultUSDCAmount); } + function test_canDecodeChainflipPayloadAndExecuteSwapNative() public { + // fund receiver with native token + vm.deal(address(receiver), 0.01 ether); + // fund chainflipVault with native token + vm.deal(chainflipVault, 0.01 ether); + + // encode payload with mock data + ( + bytes memory payload, + uint256 amountOutMin + ) = _getValidChainflipPayloadNative(0.01 ether); + + // fake a call from Chainflip vault + vm.startPrank(chainflipVault); + + vm.expectEmit(); + emit LiFiTransferCompleted( + guid, + LibAsset.NATIVE_ASSETID, + receiverAddress, + amountOutMin, + block.timestamp + ); + + receiver.cfReceive{ value: 0.01 ether }( + 4, // srcChain (Arbitrum) + abi.encodePacked(address(0)), + payload, + LibAsset.NATIVE_ASSETID, + 0.01 ether + ); + + assertEq(dai.balanceOf(receiverAddress), amountOutMin); + } + // HELPER FUNCTIONS function _getValidChainflipPayload( address _sendingAssetId, @@ -249,4 +285,38 @@ contract ReceiverChainflipTest is TestBase { // this is the "message" that we would receive from the other chain callData = abi.encode(guid, swapData, receiverAddress); } + + // Add helper function for native token swap data + function _getValidChainflipPayloadNative( + uint256 amountIn + ) public view returns (bytes memory callData, uint256 amountOutMin) { + // create swapdata for ETH -> DAI + address[] memory path = new address[](2); + path[0] = ADDRESS_WRAPPED_NATIVE; + path[1] = ADDRESS_DAI; + + // Calculate ETH input amount + uint256[] memory amounts = uniswap.getAmountsOut(amountIn, path); + amountOutMin = amounts[1]; + + LibSwap.SwapData[] memory swapData = new LibSwap.SwapData[](1); + swapData[0] = LibSwap.SwapData({ + callTo: address(uniswap), + approveTo: address(uniswap), + sendingAssetId: LibAsset.NATIVE_ASSETID, + receivingAssetId: ADDRESS_DAI, + fromAmount: amountIn, + callData: abi.encodeWithSelector( + uniswap.swapExactETHForTokens.selector, + amountOutMin, + path, + address(executor), + block.timestamp + 20 minutes + ), + requiresDeposit: true + }); + + // this is the "message" that we would receive from the other chain + callData = abi.encode(guid, swapData, receiverAddress); + } } From 3167425df3355973b2d750bddb545c0de4e8fa2e Mon Sep 17 00:00:00 2001 From: Ed Zynda Date: Fri, 14 Feb 2025 11:37:53 +0300 Subject: [PATCH 17/55] updates --- test/solidity/Periphery/ReceiverChainflip.t.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/solidity/Periphery/ReceiverChainflip.t.sol b/test/solidity/Periphery/ReceiverChainflip.t.sol index 86fe44c6c..53636199c 100644 --- a/test/solidity/Periphery/ReceiverChainflip.t.sol +++ b/test/solidity/Periphery/ReceiverChainflip.t.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: Unlicense pragma solidity ^0.8.17; -import { Test, TestBase, Vm, LiFiDiamond, DSTest, ILiFi, LibSwap, LibAllowList, console, InvalidAmount, ERC20, UniswapV2Router02 } from "../utils/TestBase.sol"; +import { TestBase, ILiFi, LibSwap, console, ERC20, UniswapV2Router02 } from "../utils/TestBase.sol"; import { ExternalCallFailed, UnAuthorized } from "src/Errors/GenericErrors.sol"; import { ReceiverChainflip } from "lifi/Periphery/ReceiverChainflip.sol"; import { LibAsset } from "lifi/Libraries/LibAsset.sol"; From 8a152f4e971c1569416fb01ac67a18b22b448d66 Mon Sep 17 00:00:00 2001 From: Ed Zynda Date: Fri, 14 Feb 2025 11:49:51 +0300 Subject: [PATCH 18/55] updates --- test/solidity/Facets/ChainflipFacet.t.sol | 6 +- .../Periphery/ReceiverChainflip.t.sol | 95 +++++++------------ 2 files changed, 37 insertions(+), 64 deletions(-) diff --git a/test/solidity/Facets/ChainflipFacet.t.sol b/test/solidity/Facets/ChainflipFacet.t.sol index 17ad79c8d..e3d9e7356 100644 --- a/test/solidity/Facets/ChainflipFacet.t.sol +++ b/test/solidity/Facets/ChainflipFacet.t.sol @@ -120,9 +120,7 @@ contract ChainflipFacetTest is TestBaseFacet { USER_SENDER, -int256(defaultUSDCAmount) ) - assertBalanceChange(ADDRESS_USDC, USER_RECEIVER, 0) assertBalanceChange(ADDRESS_DAI, USER_SENDER, 0) - assertBalanceChange(ADDRESS_DAI, USER_RECEIVER, 0) { bridgeData.receiver = LibAsset.NON_EVM_ADDRESS; bridgeData.destinationChainId = CHAIN_ID_SOLANA; @@ -156,9 +154,7 @@ contract ChainflipFacetTest is TestBaseFacet { USER_SENDER, -int256(defaultUSDCAmount) ) - assertBalanceChange(ADDRESS_USDC, USER_RECEIVER, 0) assertBalanceChange(ADDRESS_DAI, USER_SENDER, 0) - assertBalanceChange(ADDRESS_DAI, USER_RECEIVER, 0) { bridgeData.receiver = LibAsset.NON_EVM_ADDRESS; bridgeData.destinationChainId = CHAIN_ID_BITCOIN; @@ -229,6 +225,7 @@ contract ChainflipFacetTest is TestBaseFacet { usdc.approve(_facetTestContractAddress, bridgeData.minAmount); vm.expectRevert(ChainflipFacet.UnsupportedChainflipChainId.selector); + initiateBridgeTxWithFacet(false); vm.stopPrank(); } @@ -248,6 +245,7 @@ contract ChainflipFacetTest is TestBaseFacet { usdc.approve(_facetTestContractAddress, bridgeData.minAmount); vm.expectRevert(ChainflipFacet.EmptyNonEvmAddress.selector); + initiateBridgeTxWithFacet(false); vm.stopPrank(); } diff --git a/test/solidity/Periphery/ReceiverChainflip.t.sol b/test/solidity/Periphery/ReceiverChainflip.t.sol index 53636199c..10f6cb89e 100644 --- a/test/solidity/Periphery/ReceiverChainflip.t.sol +++ b/test/solidity/Periphery/ReceiverChainflip.t.sol @@ -45,7 +45,7 @@ contract ReceiverChainflipTest is TestBase { vm.label(address(erc20Proxy), "ERC20Proxy"); } - function test_contractIsSetUpCorrectly() public { + function test_ContractIsSetUpCorrectly() public { receiver = new ReceiverChainflip( address(this), address(executor), @@ -56,58 +56,7 @@ contract ReceiverChainflipTest is TestBase { assertEq(receiver.chainflipVault() == chainflipVault, true); } - function test_OwnerCanPullERC20Token() public { - // fund receiver with ERC20 tokens - deal(ADDRESS_DAI, address(receiver), 1000); - - uint256 initialBalance = dai.balanceOf(USER_RECEIVER); - - // pull token - vm.startPrank(USER_DIAMOND_OWNER); - receiver.withdrawToken(ADDRESS_DAI, payable(USER_RECEIVER), 1000); - vm.stopPrank(); - - assertEq(dai.balanceOf(USER_RECEIVER), initialBalance + 1000); - } - - function test_OwnerCanPullNativeToken() public { - // fund receiver with native tokens - vm.deal(address(receiver), 1 ether); - - uint256 initialBalance = USER_RECEIVER.balance; - - // pull token - vm.startPrank(USER_DIAMOND_OWNER); - receiver.withdrawToken(address(0), payable(USER_RECEIVER), 1 ether); - vm.stopPrank(); - - assertEq(USER_RECEIVER.balance, initialBalance + 1 ether); - } - - function test_WithdrawTokenWillRevertIfExternalCallFails() public { - vm.deal(address(receiver), 1 ether); - - // deploy contract that cannot receive ETH - NonETHReceiver nonETHReceiver = new NonETHReceiver(); - - vm.startPrank(USER_DIAMOND_OWNER); - vm.expectRevert(ExternalCallFailed.selector); - receiver.withdrawToken( - address(0), - payable(address(nonETHReceiver)), - 1 ether - ); - vm.stopPrank(); - } - - function test_revert_WithdrawTokenNonOwner() public { - vm.startPrank(USER_SENDER); - vm.expectRevert(UnAuthorized.selector); - receiver.withdrawToken(ADDRESS_DAI, payable(USER_RECEIVER), 1000); - vm.stopPrank(); - } - - function test_revert_OnlyChainflipVaultCanCallCfReceive() public { + function testRevert_OnlyChainflipVaultCanCallCfReceive() public { // mock-send bridged funds to receiver contract deal(ADDRESS_USDC, address(receiver), defaultUSDCAmount); @@ -136,10 +85,13 @@ contract ReceiverChainflipTest is TestBase { vm.stopPrank(); } - function test_canDecodeChainflipPayloadAndExecuteSwapERC20() public { + function test_CanDecodeChainflipPayloadAndExecuteSwapERC20() public { // mock-send bridged funds to receiver contract deal(ADDRESS_USDC, address(receiver), defaultUSDCAmount); + // Store initial balance + uint256 initialReceiverDAI = dai.balanceOf(receiverAddress); + // encode payload with mock data ( bytes memory payload, @@ -164,11 +116,22 @@ contract ReceiverChainflipTest is TestBase { ADDRESS_USDC, defaultUSDCAmount ); + vm.stopPrank(); - assertTrue(dai.balanceOf(receiverAddress) == amountOutMin); + // Verify balances changed correctly + assertEq( + usdc.balanceOf(address(receiver)), + 0, + "Receiver should have 0 USDC after swap" + ); + assertEq( + dai.balanceOf(receiverAddress), + initialReceiverDAI + amountOutMin, + "Receiver should have received DAI" + ); } - function test_willReturnFundsToUserIfDstCallFails() public { + function test_WillReturnFundsToUserIfDstCallFails() public { // mock-send bridged funds to receiver contract deal(ADDRESS_USDC, address(receiver), defaultUSDCAmount); @@ -213,12 +176,13 @@ contract ReceiverChainflipTest is TestBase { assertTrue(usdc.balanceOf(receiverAddress) == defaultUSDCAmount); } - function test_canDecodeChainflipPayloadAndExecuteSwapNative() public { - // fund receiver with native token - vm.deal(address(receiver), 0.01 ether); + function test_CanDecodeChainflipPayloadAndExecuteSwapNative() public { // fund chainflipVault with native token vm.deal(chainflipVault, 0.01 ether); + // Store initial balance + uint256 initialReceiverDAI = dai.balanceOf(receiverAddress); + // encode payload with mock data ( bytes memory payload, @@ -244,8 +208,19 @@ contract ReceiverChainflipTest is TestBase { LibAsset.NATIVE_ASSETID, 0.01 ether ); + vm.stopPrank(); - assertEq(dai.balanceOf(receiverAddress), amountOutMin); + // Verify balances changed correctly + assertEq( + address(receiver).balance, + 0, + "Receiver should have 0 ETH after swap" + ); + assertEq( + dai.balanceOf(receiverAddress), + initialReceiverDAI + amountOutMin, + "Receiver should have received DAI" + ); } // HELPER FUNCTIONS From ed037ce0ceb55e43a8393466eee8c5df6e816b0d Mon Sep 17 00:00:00 2001 From: Ed Zynda Date: Fri, 14 Feb 2025 11:57:04 +0300 Subject: [PATCH 19/55] updates --- src/Facets/ChainflipFacet.sol | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/src/Facets/ChainflipFacet.sol b/src/Facets/ChainflipFacet.sol index 73275edcb..deeae79c5 100644 --- a/src/Facets/ChainflipFacet.sol +++ b/src/Facets/ChainflipFacet.sol @@ -43,7 +43,7 @@ contract ChainflipFacet is ILiFi, ReentrancyGuard, SwapperV2, Validatable { /// @dev Parameters specific to Chainflip bridge /// @param dstToken Token to be received on the destination chain (uint32) - /// @param cfParameters Additional metadata + /// @param cfParameters Used to encode cross-chain messages struct ChainflipData { uint32 dstToken; bytes32 nonEvmAddress; @@ -162,12 +162,7 @@ contract ChainflipFacet is ILiFi, ReentrancyGuard, SwapperV2, Validatable { } else { IChainflipVault(chainflipVault).xSwapNative{ value: _bridgeData.minAmount - }( - dstChain, - encodedDstAddress, - _chainflipData.dstToken, - _chainflipData.cfParameters - ); + }(dstChain, encodedDstAddress, _chainflipData.dstToken, ""); } } // Handle ERC20 token case with or without CCM @@ -197,7 +192,7 @@ contract ChainflipFacet is ILiFi, ReentrancyGuard, SwapperV2, Validatable { _chainflipData.dstToken, IERC20(_bridgeData.sendingAssetId), _bridgeData.minAmount, - _chainflipData.cfParameters + "" ); } } From 6d22b49c290a632969ae30a9ef75a5c7e6f27391 Mon Sep 17 00:00:00 2001 From: Ed Zynda Date: Fri, 14 Feb 2025 12:24:41 +0300 Subject: [PATCH 20/55] refactor --- docs/ChainflipFacet.md | 6 ++-- script/demoScripts/demoChainflip.ts | 3 +- src/Facets/ChainflipFacet.sol | 36 +++++++++++++++-------- src/Interfaces/IChainflip.sol | 29 ++++++++++++++++++ test/solidity/Facets/ChainflipFacet.t.sol | 26 +++++++++++----- 5 files changed, 78 insertions(+), 22 deletions(-) diff --git a/docs/ChainflipFacet.md b/docs/ChainflipFacet.md index 2cfed7dcd..153d45192 100644 --- a/docs/ChainflipFacet.md +++ b/docs/ChainflipFacet.md @@ -23,13 +23,15 @@ The methods listed above take a variable labeled `_chainflipData`. This data is ```solidity struct ChainflipData { + bytes32 nonEVMReceiver; // Destination address for non-EVM chains (Solana, Bitcoin) uint32 dstToken; // Token identifier on the destination chain - bytes32 nonEvmAddress; // Destination address for non-EVM chains (Solana, Bitcoin) + bytes message; // Message that is passed to the destination address for cross-chain messaging + uint256 gasAmount; // Gas budget for the call on the destination chain bytes cfParameters; // Additional parameters for future features } ``` -For non-EVM destinations (Solana, Bitcoin), set the `receiver` in `BridgeData` to `LibAsset.NON_EVM_ADDRESS` and provide the destination address in `nonEvmAddress`. +For non-EVM destinations (Solana, Bitcoin), set the `receiver` in `BridgeData` to `LibAsset.NON_EVM_ADDRESS` and provide the destination address in `nonEVMReceiver`. ## Supported Chains diff --git a/script/demoScripts/demoChainflip.ts b/script/demoScripts/demoChainflip.ts index fee5a4e48..d43ae0b3a 100644 --- a/script/demoScripts/demoChainflip.ts +++ b/script/demoScripts/demoChainflip.ts @@ -27,7 +27,6 @@ const CHAINFLIP_FACET_ABI = chainflipFacetArtifact.abi as Narrow< async function main() { const withSwap = process.argv.includes('--with-swap') - const srcTokenAddress = withSwap ? ADDRESS_USDT_ARB : ADDRESS_USDC_ARB // === Set up environment === const srcChain: SupportedChain = 'arbitrum' const destinationChainId = 1 // Mainnet @@ -79,6 +78,8 @@ async function main() { dstToken: 3, // Chainflip designator for USDC on ETH nonEvmAddress: '0x0000000000000000000000000000000000000000000000000000000000000000', + message: '0x', // Add empty message + gasAmount: 0n, // Add gas amount cfParameters: '0x', // Empty parameters as per implementation } diff --git a/src/Facets/ChainflipFacet.sol b/src/Facets/ChainflipFacet.sol index deeae79c5..88b2a6b94 100644 --- a/src/Facets/ChainflipFacet.sol +++ b/src/Facets/ChainflipFacet.sol @@ -42,11 +42,16 @@ contract ChainflipFacet is ILiFi, ReentrancyGuard, SwapperV2, Validatable { /// Types /// /// @dev Parameters specific to Chainflip bridge + /// @param nonEVMReceiver Destination address for non-EVM chains (Solana, Bitcoin) /// @param dstToken Token to be received on the destination chain (uint32) - /// @param cfParameters Used to encode cross-chain messages + /// @param message Message that is passed to the destination address for cross-chain messaging + /// @param gasAmount Gas budget for the call on the destination chain + /// @param cfParameters Additional metadata for future features struct ChainflipData { + bytes32 nonEVMReceiver; uint32 dstToken; - bytes32 nonEvmAddress; + bytes message; + uint256 gasAmount; bytes cfParameters; } @@ -122,16 +127,18 @@ contract ChainflipFacet is ILiFi, ReentrancyGuard, SwapperV2, Validatable { bytes memory encodedDstAddress; if (_bridgeData.receiver == LibAsset.NON_EVM_ADDRESS) { // For non-EVM chains (Solana, Bitcoin), use the raw bytes32 from chainflipData - if (_chainflipData.nonEvmAddress == bytes32(0)) { + if (_chainflipData.nonEVMReceiver == bytes32(0)) { revert EmptyNonEvmAddress(); } - encodedDstAddress = abi.encodePacked(_chainflipData.nonEvmAddress); + encodedDstAddress = abi.encodePacked( + _chainflipData.nonEVMReceiver + ); // Emit special event for non-EVM transfers emit BridgeToNonEVMChain( _bridgeData.transactionId, _bridgeData.destinationChainId, - _chainflipData.nonEvmAddress + _chainflipData.nonEVMReceiver ); } else { // For EVM chains, encode the address @@ -141,7 +148,7 @@ contract ChainflipFacet is ILiFi, ReentrancyGuard, SwapperV2, Validatable { // Validate destination call flag matches message presence if ( _bridgeData.hasDestinationCall != - (_chainflipData.cfParameters.length > 0) + (_chainflipData.message.length > 0) ) { revert InformationMismatch(); } @@ -155,14 +162,19 @@ contract ChainflipFacet is ILiFi, ReentrancyGuard, SwapperV2, Validatable { dstChain, encodedDstAddress, _chainflipData.dstToken, - _chainflipData.cfParameters, // Used as message for CCM - 0, // Gas budget - currently unused by Chainflip + _chainflipData.message, // Use message param + _chainflipData.gasAmount, // Use gasAmount param _chainflipData.cfParameters // Additional parameters ); } else { IChainflipVault(chainflipVault).xSwapNative{ value: _bridgeData.minAmount - }(dstChain, encodedDstAddress, _chainflipData.dstToken, ""); + }( + dstChain, + encodedDstAddress, + _chainflipData.dstToken, + _chainflipData.cfParameters + ); } } // Handle ERC20 token case with or without CCM @@ -179,8 +191,8 @@ contract ChainflipFacet is ILiFi, ReentrancyGuard, SwapperV2, Validatable { dstChain, encodedDstAddress, _chainflipData.dstToken, - _chainflipData.cfParameters, // Used as message for CCM - 0, // Gas budget - currently unused by Chainflip + _chainflipData.message, // Use message param + _chainflipData.gasAmount, // Use gasAmount param IERC20(_bridgeData.sendingAssetId), _bridgeData.minAmount, _chainflipData.cfParameters // Additional parameters @@ -192,7 +204,7 @@ contract ChainflipFacet is ILiFi, ReentrancyGuard, SwapperV2, Validatable { _chainflipData.dstToken, IERC20(_bridgeData.sendingAssetId), _bridgeData.minAmount, - "" + _chainflipData.cfParameters ); } } diff --git a/src/Interfaces/IChainflip.sol b/src/Interfaces/IChainflip.sol index 568a42261..502a91346 100644 --- a/src/Interfaces/IChainflip.sol +++ b/src/Interfaces/IChainflip.sol @@ -3,8 +3,14 @@ pragma solidity ^0.8.17; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +/// @title Interface for Chainflip Vault contract /// @custom:version 1.0.0 interface IChainflipVault { + /// @notice Swaps native token to any supported asset on any supported chain + /// @param dstChain Destination chain for the swap + /// @param dstAddress Address where the swapped tokens will be sent to on the destination chain + /// @param dstToken Token to be received on the destination chain + /// @param cfParameters Additional metadata for future features function xSwapNative( uint32 dstChain, bytes calldata dstAddress, @@ -12,6 +18,13 @@ interface IChainflipVault { bytes calldata cfParameters ) external payable; + /// @notice Swaps ERC20 token to any supported asset on any supported chain + /// @param dstChain Destination chain for the swap + /// @param dstAddress Address where the swapped tokens will be sent to on the destination chain + /// @param dstToken Token to be received on the destination chain + /// @param srcToken Address of the token to be swapped from the source chain + /// @param amount Amount of the source token to be swapped + /// @param cfParameters Additional metadata for future features function xSwapToken( uint32 dstChain, bytes calldata dstAddress, @@ -21,6 +34,13 @@ interface IChainflipVault { bytes calldata cfParameters ) external; + /// @notice Swaps native token and calls a contract on the destination chain with a message + /// @param dstChain Destination chain for the swap + /// @param dstAddress Address where the swapped tokens will be sent to on the destination chain + /// @param dstToken Token to be received on the destination chain + /// @param message Message that is passed to the destination address on the destination chain + /// @param gasAmount Gas budget for the call on the destination chain + /// @param cfParameters Additional metadata for future features function xCallNative( uint32 dstChain, bytes calldata dstAddress, @@ -30,6 +50,15 @@ interface IChainflipVault { bytes calldata cfParameters ) external payable; + /// @notice Swaps ERC20 token and calls a contract on the destination chain with a message + /// @param dstChain Destination chain for the swap + /// @param dstAddress Address where the swapped tokens will be sent to on the destination chain + /// @param dstToken Token to be received on the destination chain + /// @param message Message that is passed to the destination address on the destination chain + /// @param gasAmount Gas budget for the call on the destination chain + /// @param srcToken Address of the token to be swapped from the source chain + /// @param amount Amount of the source token to be swapped + /// @param cfParameters Additional metadata for future features function xCallToken( uint32 dstChain, bytes calldata dstAddress, diff --git a/test/solidity/Facets/ChainflipFacet.t.sol b/test/solidity/Facets/ChainflipFacet.t.sol index e3d9e7356..d5ef54823 100644 --- a/test/solidity/Facets/ChainflipFacet.t.sol +++ b/test/solidity/Facets/ChainflipFacet.t.sol @@ -78,8 +78,10 @@ contract ChainflipFacetTest is TestBaseFacet { // produce valid ChainflipData validChainflipData = ChainflipFacet.ChainflipData({ + nonEVMReceiver: bytes32(0), // Default to empty for EVM addresses dstToken: 7, - nonEvmAddress: bytes32(0), // Default to empty for EVM addresses + message: "", // Add new field + gasAmount: 0, // Add new field cfParameters: "" }); } @@ -126,11 +128,13 @@ contract ChainflipFacetTest is TestBaseFacet { bridgeData.destinationChainId = CHAIN_ID_SOLANA; validChainflipData = ChainflipFacet.ChainflipData({ dstToken: 6, - nonEvmAddress: bytes32( + nonEVMReceiver: bytes32( abi.encodePacked( "EoW7FWTdPdZKpd3WAhH98c2HMGHsdh5yhzzEtk1u68Bb" ) ), // Example Solana address + message: "", + gasAmount: 0, cfParameters: "" }); @@ -160,9 +164,11 @@ contract ChainflipFacetTest is TestBaseFacet { bridgeData.destinationChainId = CHAIN_ID_BITCOIN; validChainflipData = ChainflipFacet.ChainflipData({ dstToken: 6, - nonEvmAddress: bytes32( + nonEVMReceiver: bytes32( abi.encodePacked("bc1q6l08rtj6j907r2een0jqs6l7qnruwyxfshmf8a") ), // Example Bitcoin address + message: "", + gasAmount: 0, cfParameters: "" }); @@ -198,7 +204,9 @@ contract ChainflipFacetTest is TestBaseFacet { bridgeData.destinationChainId = CHAIN_ID_ETHEREUM; validChainflipData = ChainflipFacet.ChainflipData({ dstToken: 3, // USDC on Ethereum - nonEvmAddress: bytes32(0), // Not needed for EVM chains + nonEVMReceiver: bytes32(0), // Not needed for EVM chains + message: "", + gasAmount: 0, cfParameters: "" }); @@ -235,7 +243,9 @@ contract ChainflipFacetTest is TestBaseFacet { bridgeData.destinationChainId = CHAIN_ID_SOLANA; validChainflipData = ChainflipFacet.ChainflipData({ dstToken: 6, - nonEvmAddress: bytes32(0), // Empty address should fail + nonEVMReceiver: bytes32(0), // Empty address should fail + message: "", + gasAmount: 0, cfParameters: "" }); @@ -277,8 +287,10 @@ contract ChainflipFacetTest is TestBaseFacet { validChainflipData = ChainflipFacet.ChainflipData({ dstToken: 7, // USDC on Arbitrum - nonEvmAddress: bytes32(0), // Not needed for EVM chains - cfParameters: message // Pass the encoded message for CCM + nonEVMReceiver: bytes32(0), // Not needed for EVM chains + message: message, // Use message here + gasAmount: 0, // Add gas amount + cfParameters: "" // Empty parameters }); vm.startPrank(USER_SENDER); From 45a2f6cfecc32219dd8d1681af6034bd8f854924 Mon Sep 17 00:00:00 2001 From: Ed Zynda Date: Fri, 14 Feb 2025 12:39:17 +0300 Subject: [PATCH 21/55] udpate --- src/Facets/ChainflipFacet.sol | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Facets/ChainflipFacet.sol b/src/Facets/ChainflipFacet.sol index 88b2a6b94..af5749f5e 100644 --- a/src/Facets/ChainflipFacet.sol +++ b/src/Facets/ChainflipFacet.sol @@ -162,9 +162,9 @@ contract ChainflipFacet is ILiFi, ReentrancyGuard, SwapperV2, Validatable { dstChain, encodedDstAddress, _chainflipData.dstToken, - _chainflipData.message, // Use message param - _chainflipData.gasAmount, // Use gasAmount param - _chainflipData.cfParameters // Additional parameters + _chainflipData.message, + _chainflipData.gasAmount, + _chainflipData.cfParameters ); } else { IChainflipVault(chainflipVault).xSwapNative{ @@ -191,11 +191,11 @@ contract ChainflipFacet is ILiFi, ReentrancyGuard, SwapperV2, Validatable { dstChain, encodedDstAddress, _chainflipData.dstToken, - _chainflipData.message, // Use message param - _chainflipData.gasAmount, // Use gasAmount param + _chainflipData.message, + _chainflipData.gasAmount, IERC20(_bridgeData.sendingAssetId), _bridgeData.minAmount, - _chainflipData.cfParameters // Additional parameters + _chainflipData.cfParameters ); } else { IChainflipVault(chainflipVault).xSwapToken( From 6d39d19a746fb828ab62399d30e597e58cbe85a8 Mon Sep 17 00:00:00 2001 From: Ed Zynda Date: Fri, 14 Feb 2025 13:10:23 +0300 Subject: [PATCH 22/55] fix demo script --- deployments/_deployments_log_file.json | 4 ++-- deployments/arbitrum.diamond.staging.json | 2 +- deployments/arbitrum.staging.json | 2 +- script/demoScripts/demoChainflip.ts | 10 +++++----- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/deployments/_deployments_log_file.json b/deployments/_deployments_log_file.json index 6872e9a91..50c1cff50 100644 --- a/deployments/_deployments_log_file.json +++ b/deployments/_deployments_log_file.json @@ -28584,9 +28584,9 @@ "staging": { "1.0.0": [ { - "ADDRESS": "0x01eA7231A7CFb81388171eD6Fbe02e88285BD0B9", + "ADDRESS": "0x6ee80199Af6f871c8F0FaECc51EBbFf3200E61a6", "OPTIMIZER_RUNS": "1000000", - "TIMESTAMP": "2025-02-12 11:08:58", + "TIMESTAMP": "2025-02-14 13:00:45", "CONSTRUCTOR_ARGS": "0x00000000000000000000000079001a5e762f3befc8e5871b42f6734e00498920", "SALT": "28012025", "VERIFIED": "true" diff --git a/deployments/arbitrum.diamond.staging.json b/deployments/arbitrum.diamond.staging.json index 92b8579f1..69ca098e7 100644 --- a/deployments/arbitrum.diamond.staging.json +++ b/deployments/arbitrum.diamond.staging.json @@ -149,7 +149,7 @@ "Name": "", "Version": "" }, - "0x01eA7231A7CFb81388171eD6Fbe02e88285BD0B9": { + "0x6ee80199Af6f871c8F0FaECc51EBbFf3200E61a6": { "Name": "ChainflipFacet", "Version": "1.0.0" } diff --git a/deployments/arbitrum.staging.json b/deployments/arbitrum.staging.json index 567a75ea7..81b4e809e 100644 --- a/deployments/arbitrum.staging.json +++ b/deployments/arbitrum.staging.json @@ -51,5 +51,5 @@ "ReceiverAcrossV3": "0xe4F3DEF14D61e47c696374453CD64d438FD277F8", "AcrossFacetPackedV3": "0x21767081Ff52CE5563A29f27149D01C7127775A2", "RelayFacet": "0x3cf7dE0e31e13C93c8Aada774ADF1C7eD58157f5", - "ChainflipFacet": "0x01eA7231A7CFb81388171eD6Fbe02e88285BD0B9" + "ChainflipFacet": "0x6ee80199Af6f871c8F0FaECc51EBbFf3200E61a6" } \ No newline at end of file diff --git a/script/demoScripts/demoChainflip.ts b/script/demoScripts/demoChainflip.ts index d43ae0b3a..6d87dd236 100644 --- a/script/demoScripts/demoChainflip.ts +++ b/script/demoScripts/demoChainflip.ts @@ -75,12 +75,12 @@ async function main() { } const chainflipData: ChainflipFacet.ChainflipDataStruct = { - dstToken: 3, // Chainflip designator for USDC on ETH - nonEvmAddress: + nonEVMReceiver: '0x0000000000000000000000000000000000000000000000000000000000000000', - message: '0x', // Add empty message - gasAmount: 0n, // Add gas amount - cfParameters: '0x', // Empty parameters as per implementation + dstToken: 3, // Chainflip designator for USDC on ETH + message: '', // Empty bytes but properly formatted + gasAmount: 0n, + cfParameters: '', // Empty bytes but properly formatted } // === Start bridging === From de8e10a15b43c0c99d0851faf46a47d21ddadcb6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Bl=C3=A4cker?= Date: Mon, 17 Feb 2025 13:56:35 +0700 Subject: [PATCH 23/55] replace testFail_ with testRevert_ pattern due to deprecation --- .../deploy/facets/utils/DeployScriptBase.sol | 1 - test/solidity/Facets/AcrossFacet.t.sol | 15 +++++--- test/solidity/Facets/AcrossFacetV3.t.sol | 12 ++++-- test/solidity/Facets/CBridgeRefund.t.sol | 21 ++++++++-- test/solidity/Facets/DexManagerFacet.t.sol | 19 ++++++++-- test/solidity/Facets/OwnershipFacet.t.sol | 36 +++++++++++++++--- test/solidity/Facets/RelayFacet.t.sol | 22 ++++------- .../Helpers/TransferrableOwnership.t.sol | 33 +++++++++++++--- test/solidity/Periphery/Executor.t.sol | 10 +++-- test/solidity/Periphery/FeeCollector.t.sol | 38 ++++++++++--------- 10 files changed, 143 insertions(+), 64 deletions(-) diff --git a/script/deploy/facets/utils/DeployScriptBase.sol b/script/deploy/facets/utils/DeployScriptBase.sol index 55bf6f585..afe38ec25 100644 --- a/script/deploy/facets/utils/DeployScriptBase.sol +++ b/script/deploy/facets/utils/DeployScriptBase.sol @@ -44,7 +44,6 @@ contract DeployScriptBase is ScriptBase { } // reproduce and log calldata that is sent to CREATE3 - bytes4 funcSel = CREATE3Factory.deploy.selector; bytes memory create3Calldata = abi.encodeWithSelector( CREATE3Factory.deploy.selector, salt, diff --git a/test/solidity/Facets/AcrossFacet.t.sol b/test/solidity/Facets/AcrossFacet.t.sol index 196a9a8a8..661ede3bb 100644 --- a/test/solidity/Facets/AcrossFacet.t.sol +++ b/test/solidity/Facets/AcrossFacet.t.sol @@ -108,19 +108,22 @@ contract AcrossFacetTest is TestBaseFacet { } } - function testFailsToBridgeERC20TokensDueToQuoteTimeout() public { - console.logBytes4(IAcrossSpokePool.deposit.selector); - vm.startPrank(WETH_HOLDER); - ERC20 weth = ERC20(ADDRESS_WRAPPED_NATIVE); - weth.approve(address(acrossFacet), 10_000 * 10 ** weth.decimals()); + function testRevert_FailsIfCalledWithOutdatedQuote() public { + vm.startPrank(USER_SENDER); + + usdc.approve(address(acrossFacet), bridgeData.minAmount); AcrossFacet.AcrossData memory data = AcrossFacet.AcrossData( 0, // Relayer fee - uint32(block.timestamp + 20 minutes), + uint32(block.timestamp - 100 days), "", type(uint256).max ); + + vm.expectRevert("invalid quote time"); + acrossFacet.startBridgeTokensViaAcross(bridgeData, data); + vm.stopPrank(); } } diff --git a/test/solidity/Facets/AcrossFacetV3.t.sol b/test/solidity/Facets/AcrossFacetV3.t.sol index 6928bd18a..b5bad412c 100644 --- a/test/solidity/Facets/AcrossFacetV3.t.sol +++ b/test/solidity/Facets/AcrossFacetV3.t.sol @@ -36,6 +36,8 @@ contract AcrossFacetV3Test is TestBaseFacet { AcrossFacetV3.AcrossV3Data internal validAcrossData; TestAcrossFacetV3 internal acrossFacetV3; + error InvalidQuoteTimestamp(); + function setUp() public { customBlockNumberForForking = 19960294; initTestBase(); @@ -249,11 +251,13 @@ contract AcrossFacetV3Test is TestBaseFacet { vm.stopPrank(); } - function testFailsToBridgeERC20TokensDueToQuoteTimeout() public { - vm.startPrank(WETH_HOLDER); - weth.approve(address(acrossFacetV3), 10_000 * 10 ** weth.decimals()); + function testRevert_FailsIfCalledWithOutdatedQuote() public { + vm.startPrank(USER_SENDER); + usdc.approve(address(acrossFacetV3), bridgeData.minAmount); + + validAcrossData.quoteTimestamp = uint32(block.timestamp - 100 days); - validAcrossData.quoteTimestamp = uint32(block.timestamp + 20 minutes); + vm.expectRevert(InvalidQuoteTimestamp.selector); acrossFacetV3.startBridgeTokensViaAcrossV3( bridgeData, diff --git a/test/solidity/Facets/CBridgeRefund.t.sol b/test/solidity/Facets/CBridgeRefund.t.sol index da69d7723..36b15592b 100644 --- a/test/solidity/Facets/CBridgeRefund.t.sol +++ b/test/solidity/Facets/CBridgeRefund.t.sol @@ -7,6 +7,7 @@ import { DiamondTest, LiFiDiamond } from "../utils/DiamondTest.sol"; import { Vm } from "forge-std/Vm.sol"; import { ERC20 } from "solmate/tokens/ERC20.sol"; import { WithdrawFacet } from "lifi/Facets/WithdrawFacet.sol"; +import { UnAuthorized, NotAContract } from "lifi/Errors/GenericErrors.sol"; // Test CBridge refund by forking polygon at 25085298 // Actual refund was processed at 25085299(Feb-18-2022 03:24:09 PM +UTC) @@ -27,6 +28,8 @@ contract CBridgeRefundTestPolygon is DSTest, DiamondTest { 0x3db00D1334B5faDd2A897D8A702cDCbb6F159D87; uint256 internal constant REFUND_AMOUNT = 92734538876076486098; + error WithdrawFailed(); + bytes internal CALLDATA; LiFiDiamond internal diamond; @@ -126,7 +129,11 @@ contract CBridgeRefundTestPolygon is DSTest, DiamondTest { /// @notice Fails to execute extra call and withdraw from non-owner. /// @dev It calls executeCallAndWithdraw from address that is not OWNER_ADDRESS. - function testFailExecuteCallAndWithdrawFromNonOwner() public { + function testRevert_NonOwnerCannotWithdrawFunds() public { + vm.startPrank(LIFI_ADDRESS); + + vm.expectRevert(UnAuthorized.selector); + withdrawFacet.executeCallAndWithdraw( payable(CBRIDGE_ADDRESS), CALLDATA, @@ -138,9 +145,13 @@ contract CBridgeRefundTestPolygon is DSTest, DiamondTest { /// @notice Fails to execute extra call and withdraw when callTo is invalid. /// @dev It tries to execute extra call at REFUND_ADDRESS instead of CBRIDGE_ADDRESS. - function testFailExecuteCallAndWithdraw() public { + function testRevert_FailsWhenCalledWithInvalidCallToAddress() public { + vm.startPrank(OWNER_ADDRESS); + + vm.expectRevert(NotAContract.selector); + withdrawFacet.executeCallAndWithdraw( - payable(REFUND_ADDRESS), + payable(REFUND_ADDRESS), //invalid callTo address CALLDATA, REFUND_ASSET, REFUND_ADDRESS, @@ -151,7 +162,7 @@ contract CBridgeRefundTestPolygon is DSTest, DiamondTest { /// @notice Fails to execute extra call and withdraw when refund is already processed. /// @dev It tries to withdraw multiple times. /// First withdraw should be success but second withdraw should be failed. - function testFailExecuteCallAndWithdrawMultiple() public { + function testRevert_RevertsIfRefundWasAlreadyProcessed() public { ERC20 asset = ERC20(REFUND_ASSET); uint256 assetBalance = asset.balanceOf(REFUND_ADDRESS); @@ -168,6 +179,8 @@ contract CBridgeRefundTestPolygon is DSTest, DiamondTest { asset.balanceOf(REFUND_ADDRESS) == assetBalance + REFUND_AMOUNT ); + vm.expectRevert(WithdrawFailed.selector); + withdrawFacet.executeCallAndWithdraw( payable(CBRIDGE_ADDRESS), CALLDATA, diff --git a/test/solidity/Facets/DexManagerFacet.t.sol b/test/solidity/Facets/DexManagerFacet.t.sol index 0ae6e6f18..840801d93 100644 --- a/test/solidity/Facets/DexManagerFacet.t.sol +++ b/test/solidity/Facets/DexManagerFacet.t.sol @@ -6,6 +6,7 @@ import { console } from "../utils/Console.sol"; import { DiamondTest, LiFiDiamond } from "../utils/DiamondTest.sol"; import { Vm } from "forge-std/Vm.sol"; import { DexManagerFacet } from "lifi/Facets/DexManagerFacet.sol"; +import { InvalidContract } from "lifi/Errors/GenericErrors.sol"; contract Foo {} @@ -111,27 +112,37 @@ contract DexManagerFacetTest is DSTest, DiamondTest { } } - function testFailAddZeroAddress() public { + function testRevert_CannotAddZeroAddress() public { + vm.expectRevert(InvalidContract.selector); + dexMgr.addDex(address(0)); } - function testFailAddNonContract() public { + function testRevert_CannotAddNonContract() public { + vm.expectRevert(InvalidContract.selector); + dexMgr.addDex(address(1337)); } - function testFailBatchAddZeroAddress() public { + function testRevert_CannotBatchAddZeroAddress() public { address[] memory dexs = new address[](3); dexs[0] = address(c1); dexs[1] = address(c2); dexs[2] = address(0); + + vm.expectRevert(InvalidContract.selector); + dexMgr.batchAddDex(dexs); } - function testFailBatchAddNonContract() public { + function testRevert_CannotBatchAddNonContract() public { address[] memory dexs = new address[](3); dexs[0] = address(c1); dexs[1] = address(c2); dexs[2] = address(1337); + + vm.expectRevert(InvalidContract.selector); + dexMgr.batchAddDex(dexs); } } diff --git a/test/solidity/Facets/OwnershipFacet.t.sol b/test/solidity/Facets/OwnershipFacet.t.sol index 388d0b29d..37809be8b 100644 --- a/test/solidity/Facets/OwnershipFacet.t.sol +++ b/test/solidity/Facets/OwnershipFacet.t.sol @@ -3,10 +3,16 @@ pragma solidity ^0.8.17; import { OwnershipFacet } from "lifi/Facets/OwnershipFacet.sol"; import { LibAllowList, LibSwap, TestBase, console, LiFiDiamond } from "../utils/TestBase.sol"; +import { OnlyContractOwner } from "lifi/Errors/GenericErrors.sol"; contract OwnershipFacetTest is TestBase { OwnershipFacet internal ownershipFacet; + error NoNullOwner(); + error NewOwnerMustNotBeSelf(); + error NoPendingOwnershipTransfer(); + error NotPendingOwner(); + function setUp() public { initTestBase(); @@ -15,34 +21,54 @@ contract OwnershipFacetTest is TestBase { function testOwnerCanTransferOwnership() public { address newOwner = address(0x1234567890123456789012345678901234567890); + ownershipFacet.transferOwnership(newOwner); + assert(ownershipFacet.owner() != newOwner); + vm.startPrank(newOwner); + ownershipFacet.confirmOwnershipTransfer(); + assert(ownershipFacet.owner() == newOwner); + vm.stopPrank(); } - function testFailNonOwnerCanTransferOwnership() public { + function testRevert_NonOwnerCannotTransferOwnership() public { address newOwner = address(0x1234567890123456789012345678901234567890); assert(ownershipFacet.owner() != newOwner); vm.prank(newOwner); + + vm.expectRevert(OnlyContractOwner.selector); + ownershipFacet.transferOwnership(newOwner); } - function testFailOnwershipTransferToNullAddr() public { - address newOwner = address(0x0); + function testRevert_CannotTransferOnwershipToNullAddr() public { + address newOwner = address(0); + + vm.expectRevert(NoNullOwner.selector); + ownershipFacet.transferOwnership(newOwner); } - function testFailOwnerCanConfirmPendingOwnershipTransfer() public { + function testRevert_PendingOwnershipTransferCannotBeConfirmedByNonNewOwner() + public + { address newOwner = address(0x1234567890123456789012345678901234567890); ownershipFacet.transferOwnership(newOwner); + + vm.expectRevert(NotPendingOwner.selector); + ownershipFacet.confirmOwnershipTransfer(); } - function testFailOwnershipTransferToSelf() public { + function testRevert_CannotTransferOwnershipToSelf() public { address newOwner = address(this); + + vm.expectRevert(NewOwnerMustNotBeSelf.selector); + ownershipFacet.transferOwnership(newOwner); } } diff --git a/test/solidity/Facets/RelayFacet.t.sol b/test/solidity/Facets/RelayFacet.t.sol index 21c64af4d..6da0edd98 100644 --- a/test/solidity/Facets/RelayFacet.t.sol +++ b/test/solidity/Facets/RelayFacet.t.sol @@ -654,14 +654,9 @@ contract RelayFacetTest is TestBaseFacet { vm.stopPrank(); } - function testFail_RevertIsBubbledWhenBridgingTokensFails() + function testRevert_RevertIsBubbledUpWhenBridgingERC20TokensFails() public virtual - assertBalanceChange( - ADDRESS_USDC, - USER_SENDER, - -int256(defaultUSDCAmount) - ) assertBalanceChange(ADDRESS_USDC, USER_RECEIVER, 0) assertBalanceChange(ADDRESS_DAI, USER_SENDER, 0) assertBalanceChange(ADDRESS_DAI, USER_RECEIVER, 0) @@ -681,19 +676,16 @@ contract RelayFacetTest is TestBaseFacet { "I always revert" ); - vm.expectRevert("I always revert"); + vm.expectRevert(); + initiateBridgeTxWithFacet(false); + vm.stopPrank(); } - function testFail_RevertIsBubbledWhenBridgingNativeTokensFails() + function testRevert_RevertIsBubbledUpWhenBridgingNativeTokensFails() public virtual - assertBalanceChange( - address(0), - USER_SENDER, - -int256((defaultNativeAmount + addToMessageValue)) - ) assertBalanceChange(address(0), USER_RECEIVER, 0) assertBalanceChange(ADDRESS_USDC, USER_SENDER, 0) assertBalanceChange(ADDRESS_DAI, USER_SENDER, 0) @@ -706,8 +698,10 @@ contract RelayFacetTest is TestBaseFacet { _makeRevertable(RELAY_RECEIVER); - vm.expectRevert("I always revert"); + vm.expectRevert(); + initiateBridgeTxWithFacet(true); + vm.stopPrank(); } diff --git a/test/solidity/Helpers/TransferrableOwnership.t.sol b/test/solidity/Helpers/TransferrableOwnership.t.sol index 6d1f445f5..e5a16983a 100644 --- a/test/solidity/Helpers/TransferrableOwnership.t.sol +++ b/test/solidity/Helpers/TransferrableOwnership.t.sol @@ -4,11 +4,17 @@ pragma solidity ^0.8.17; import { DSTest } from "ds-test/test.sol"; import { Vm } from "forge-std/Vm.sol"; import { TransferrableOwnership } from "lifi/Helpers/TransferrableOwnership.sol"; +import { OnlyContractOwner, UnAuthorized } from "lifi/Errors/GenericErrors.sol"; contract TransferrableOwnershipTest is DSTest { TransferrableOwnership internal ownable; Vm internal immutable vm = Vm(HEVM_ADDRESS); + error NoNullOwner(); + error NewOwnerMustNotBeSelf(); + error NoPendingOwnershipTransfer(); + error NotPendingOwner(); + function setUp() public { ownable = new TransferrableOwnership(address(this)); } @@ -23,26 +29,43 @@ contract TransferrableOwnershipTest is DSTest { vm.stopPrank(); } - function testFailNonOwnerCanTransferOwnership() public { + function testRevert_NonOwnerCannotTransferOwnership() public { address newOwner = address(0x1234567890123456789012345678901234567890); + assert(ownable.owner() != newOwner); + vm.prank(newOwner); + + vm.expectRevert(UnAuthorized.selector); + ownable.transferOwnership(newOwner); } - function testFailOnwershipTransferToNullAddr() public { - address newOwner = address(0x0); + function testRevert_CannotTransferOnwershipToNullAddr() public { + address newOwner = address(0); + + vm.expectRevert(NoNullOwner.selector); + ownable.transferOwnership(newOwner); } - function testFailOwnerCanConfirmPendingOwnershipTransfer() public { + function testRevert_PendingOwnershipTransferCannotBeConfirmedByNonNewOwner() + public + { address newOwner = address(0x1234567890123456789012345678901234567890); + ownable.transferOwnership(newOwner); + + vm.expectRevert(NotPendingOwner.selector); + ownable.confirmOwnershipTransfer(); } - function testFailOwnershipTransferToSelf() public { + function testRevert_CannotTransferOwnershipToSelf() public { address newOwner = address(this); + + vm.expectRevert(NewOwnerMustNotBeSelf.selector); + ownable.transferOwnership(newOwner); } } diff --git a/test/solidity/Periphery/Executor.t.sol b/test/solidity/Periphery/Executor.t.sol index a1c8652c6..704664c7b 100644 --- a/test/solidity/Periphery/Executor.t.sol +++ b/test/solidity/Periphery/Executor.t.sol @@ -6,11 +6,11 @@ import { console } from "../utils/Console.sol"; import { Vm } from "forge-std/Vm.sol"; import { Executor } from "lifi/Periphery/Executor.sol"; import { ERC20Proxy } from "lifi/Periphery/ERC20Proxy.sol"; -import { ILiFi } from "lifi/Interfaces/ILiFi.sol"; import { TestAMM } from "../utils/TestAMM.sol"; import { TestToken as ERC20 } from "../utils/TestToken.sol"; import { LibSwap } from "lifi/Libraries/LibSwap.sol"; import { UniswapV2Router02 } from "../utils/Interfaces.sol"; +import { UnAuthorized } from "lifi/Errors/GenericErrors.sol"; // Stub Vault Contract contract Vault { @@ -548,7 +548,7 @@ contract ExecutorTest is DSTest { assertEq(tokenD.balanceOf(address(vault)), 100 ether); } - function testFailWhenCallingERC20ProxyDirectly() public { + function testRevert_DoesNotAllowToCallERC20ProxyDirectly() public { ERC20 tokenA = new ERC20("Token A", "TOKA", 18); ERC20 tokenB = new ERC20("Token B", "TOKB", 18); @@ -556,8 +556,8 @@ contract ExecutorTest is DSTest { // Get some Token B swapData[0] = LibSwap.SwapData( - address(amm), - address(amm), + address(erc20Proxy), + address(erc20Proxy), address(tokenA), address(tokenB), 0.2 ether, @@ -573,6 +573,8 @@ contract ExecutorTest is DSTest { tokenA.mint(address(this), 1 ether); tokenA.approve(address(erc20Proxy), 1 ether); + vm.expectRevert(UnAuthorized.selector); + executor.swapAndExecute( "", swapData, diff --git a/test/solidity/Periphery/FeeCollector.t.sol b/test/solidity/Periphery/FeeCollector.t.sol index f30085145..99bb26314 100644 --- a/test/solidity/Periphery/FeeCollector.t.sol +++ b/test/solidity/Periphery/FeeCollector.t.sol @@ -6,6 +6,7 @@ import { console } from "../utils/Console.sol"; import { Vm } from "forge-std/Vm.sol"; import { FeeCollector } from "lifi/Periphery/FeeCollector.sol"; import { TestToken as ERC20 } from "../utils/TestToken.sol"; +import { UnAuthorized } from "lifi/Errors/GenericErrors.sol"; contract FeeCollectorTest is DSTest { Vm internal immutable vm = Vm(HEVM_ADDRESS); @@ -184,11 +185,14 @@ contract FeeCollectorTest is DSTest { assert(feeToken.balanceOf(address(feeCollector)) == 1 ether); } - function testFailWhenNonOwnerAttemptsToWithdrawLifiFees() public { + function testRevert_NonOwnerCannotWithdrawLifiFees() public { // Arrange uint256 integratorFee = 1 ether; uint256 lifiFee = 0.015 ether; + feeToken.approve(address(feeCollector), integratorFee + lifiFee); + + // make sure feeCollector has collected fees feeCollector.collectTokenFees( address(feeToken), integratorFee, @@ -198,20 +202,27 @@ contract FeeCollectorTest is DSTest { // Act vm.prank(address(0xb33f)); + + vm.expectRevert(UnAuthorized.selector); + feeCollector.withdrawLifiFees(address(feeToken)); } - function testFailWhenNonOwnerAttemptsToBatchWithdrawLifiFees() public { + function testRevert_NonOwnerCannotBatchWithdrawLifiFees() public { // Arranges.newOwner uint256 integratorFee = 1 ether; uint256 lifiFee = 0.015 ether; + feeToken.approve(address(feeCollector), integratorFee + lifiFee); + + // make sure feeCollector has collected fees feeCollector.collectTokenFees( address(feeToken), integratorFee, lifiFee, address(0xb33f) ); + feeCollector.collectNativeFees{ value: integratorFee + lifiFee }( integratorFee, lifiFee, @@ -222,7 +233,11 @@ contract FeeCollectorTest is DSTest { address[] memory tokens = new address[](2); tokens[0] = address(feeToken); tokens[1] = address(0); + vm.prank(address(0xb33f)); + + vm.expectRevert(UnAuthorized.selector); + feeCollector.batchWithdrawLifiFees(tokens); } @@ -236,26 +251,15 @@ contract FeeCollectorTest is DSTest { vm.stopPrank(); } - function testFailNonOwnerCanTransferOwnership() public { + function testRevert_NonOwnerCannotTransferOwnership() public { address newOwner = address(0x1234567890123456789012345678901234567890); + assert(feeCollector.owner() != newOwner); - vm.prank(newOwner); - feeCollector.transferOwnership(newOwner); - } - function testFailOnwershipTransferToNullAddr() public { - address newOwner = address(0x0); - feeCollector.transferOwnership(newOwner); - } + vm.prank(newOwner); - function testFailOwnerCanConfirmPendingOwnershipTransfer() public { - address newOwner = address(0x1234567890123456789012345678901234567890); - feeCollector.transferOwnership(newOwner); - feeCollector.confirmOwnershipTransfer(); - } + vm.expectRevert(UnAuthorized.selector); - function testFailOwnershipTransferToSelf() public { - address newOwner = address(this); feeCollector.transferOwnership(newOwner); } } From b1c9366a82e923bb0f632acb2bad51936310bdb7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Bl=C3=A4cker?= Date: Mon, 17 Feb 2025 15:57:27 +0700 Subject: [PATCH 24/55] added tests for DexManagerFacet --- test/solidity/Facets/DexManagerFacet.t.sol | 95 +++++++++++++++++++++- 1 file changed, 93 insertions(+), 2 deletions(-) diff --git a/test/solidity/Facets/DexManagerFacet.t.sol b/test/solidity/Facets/DexManagerFacet.t.sol index 840801d93..ea4e1fd3d 100644 --- a/test/solidity/Facets/DexManagerFacet.t.sol +++ b/test/solidity/Facets/DexManagerFacet.t.sol @@ -6,7 +6,8 @@ import { console } from "../utils/Console.sol"; import { DiamondTest, LiFiDiamond } from "../utils/DiamondTest.sol"; import { Vm } from "forge-std/Vm.sol"; import { DexManagerFacet } from "lifi/Facets/DexManagerFacet.sol"; -import { InvalidContract } from "lifi/Errors/GenericErrors.sol"; +import { AccessManagerFacet } from "lifi/Facets/AccessManagerFacet.sol"; +import { InvalidContract, OnlyContractOwner, CannotAuthoriseSelf, UnAuthorized } from "lifi/Errors/GenericErrors.sol"; contract Foo {} @@ -16,6 +17,7 @@ contract DexManagerFacetTest is DSTest, DiamondTest { LiFiDiamond internal diamond; DexManagerFacet internal dexMgr; + AccessManagerFacet internal accessMgr; Foo internal c1; Foo internal c2; Foo internal c3; @@ -27,7 +29,7 @@ contract DexManagerFacetTest is DSTest, DiamondTest { c2 = new Foo(); c3 = new Foo(); - bytes4[] memory functionSelectors = new bytes4[](8); + bytes4[] memory functionSelectors = new bytes4[](9); functionSelectors[0] = DexManagerFacet.addDex.selector; functionSelectors[1] = DexManagerFacet.removeDex.selector; functionSelectors[2] = DexManagerFacet.batchAddDex.selector; @@ -43,6 +45,15 @@ contract DexManagerFacetTest is DSTest, DiamondTest { addFacet(diamond, address(dexMgr), functionSelectors); + // add AccessManagerFacet to be able to whitelist addresses for execution of protected functions + accessMgr = new AccessManagerFacet(); + + functionSelectors = new bytes4[](2); + functionSelectors[0] = accessMgr.setCanExecute.selector; + functionSelectors[1] = accessMgr.addressCanExecuteMethod.selector; + addFacet(diamond, address(accessMgr), functionSelectors); + + accessMgr = AccessManagerFacet(address(diamond)); dexMgr = DexManagerFacet(address(diamond)); vm.startPrank(USER_DIAMOND_OWNER); } @@ -118,6 +129,17 @@ contract DexManagerFacetTest is DSTest, DiamondTest { dexMgr.addDex(address(0)); } + function testRevert_NonOwnerCannotAddAddress() public { + vm.stopPrank(); + vm.startPrank(USER_PAUSER); // not the owner + + vm.expectRevert(UnAuthorized.selector); + + dexMgr.addDex(address(0)); + + vm.stopPrank(); + } + function testRevert_CannotAddNonContract() public { vm.expectRevert(InvalidContract.selector); @@ -135,6 +157,17 @@ contract DexManagerFacetTest is DSTest, DiamondTest { dexMgr.batchAddDex(dexs); } + function testRevert_CannotBatchAddSelf() public { + address[] memory dexs = new address[](3); + dexs[0] = address(c1); + dexs[1] = address(c2); + dexs[2] = address(dexMgr); + + vm.expectRevert(CannotAuthoriseSelf.selector); + + dexMgr.batchAddDex(dexs); + } + function testRevert_CannotBatchAddNonContract() public { address[] memory dexs = new address[](3); dexs[0] = address(c1); @@ -145,4 +178,62 @@ contract DexManagerFacetTest is DSTest, DiamondTest { dexMgr.batchAddDex(dexs); } + + function test_AllowsWhitelistedAddressToAddContract() public { + vm.stopPrank(); + vm.startPrank(USER_PAUSER); + vm.expectRevert(UnAuthorized.selector); + + dexMgr.addDex(address(c1)); + + // allow USER_PAUSER address to execute addDex() function + vm.startPrank(USER_DIAMOND_OWNER); + + accessMgr.setCanExecute( + DexManagerFacet.addDex.selector, + USER_PAUSER, + true + ); + + // try to call addDex() + vm.startPrank(USER_PAUSER); + + dexMgr.addDex(address(c1)); + + address[] memory approved = dexMgr.approvedDexs(); + + assertEq(approved[0], address(c1)); + } + + function test_AllowsWhitelistedAddressToBatchAddContract() public { + address[] memory dexs = new address[](2); + dexs[0] = address(c1); + dexs[1] = address(c2); + + vm.stopPrank(); + vm.startPrank(USER_PAUSER); + + vm.expectRevert(UnAuthorized.selector); + + dexMgr.batchAddDex(dexs); + + // allow USER_PAUSER address to execute batchAddDex() function + vm.startPrank(USER_DIAMOND_OWNER); + + accessMgr.setCanExecute( + DexManagerFacet.batchAddDex.selector, + USER_PAUSER, + true + ); + + // try to call addDex() + vm.startPrank(USER_PAUSER); + + dexMgr.batchAddDex(dexs); + + address[] memory approved = dexMgr.approvedDexs(); + + assertEq(approved[0], address(c1)); + assertEq(approved[1], address(c2)); + } } From 3550cd65ca60f8e6fb59ccabfd072a1dd7cabf8b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Bl=C3=A4cker?= Date: Mon, 17 Feb 2025 15:58:08 +0700 Subject: [PATCH 25/55] added test for AcrossFacetV3 --- test/solidity/Facets/AcrossFacetV3.t.sol | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/test/solidity/Facets/AcrossFacetV3.t.sol b/test/solidity/Facets/AcrossFacetV3.t.sol index b5bad412c..40176db07 100644 --- a/test/solidity/Facets/AcrossFacetV3.t.sol +++ b/test/solidity/Facets/AcrossFacetV3.t.sol @@ -1,11 +1,12 @@ // SPDX-License-Identifier: Unlicense pragma solidity ^0.8.17; -import { LibAllowList, TestBaseFacet, console, ERC20 } from "../utils/TestBaseFacet.sol"; +import { LibAllowList, TestBaseFacet, console } from "../utils/TestBaseFacet.sol"; import { AcrossFacetV3 } from "lifi/Facets/AcrossFacetV3.sol"; import { IAcrossSpokePool } from "lifi/Interfaces/IAcrossSpokePool.sol"; import { LibUtil } from "lifi/Libraries/LibUtil.sol"; import { LibSwap } from "lifi/Libraries/LibSwap.sol"; +import { InformationMismatch } from "lifi/Errors/GenericErrors.sol"; // Stub AcrossFacetV3 Contract contract TestAcrossFacetV3 is AcrossFacetV3 { @@ -275,4 +276,23 @@ contract AcrossFacetV3Test is TestBaseFacet { true ); } + + function testRevert_WillFailIfBridgeDataReceiverDoesNotMatchWithAcrossData() + public + { + vm.startPrank(USER_SENDER); + usdc.approve(address(acrossFacetV3), bridgeData.minAmount); + + validAcrossData.quoteTimestamp = uint32(block.timestamp - 100 days); + + bridgeData.receiver = USER_REFUND; // does not match with USER_RECEIVER + + vm.expectRevert(InformationMismatch.selector); + + acrossFacetV3.startBridgeTokensViaAcrossV3( + bridgeData, + validAcrossData + ); + vm.stopPrank(); + } } From d68e3f5ed5e29bad98c2fd4b79756e1a619ba4d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Bl=C3=A4cker?= Date: Mon, 17 Feb 2025 17:05:18 +0700 Subject: [PATCH 26/55] add tests for ownershipfacet --- test/solidity/Facets/OwnershipFacet.t.sol | 62 ++++++++++++++++++++++- 1 file changed, 61 insertions(+), 1 deletion(-) diff --git a/test/solidity/Facets/OwnershipFacet.t.sol b/test/solidity/Facets/OwnershipFacet.t.sol index 37809be8b..87f15dac4 100644 --- a/test/solidity/Facets/OwnershipFacet.t.sol +++ b/test/solidity/Facets/OwnershipFacet.t.sol @@ -13,13 +13,73 @@ contract OwnershipFacetTest is TestBase { error NoPendingOwnershipTransfer(); error NotPendingOwner(); + event OwnershipTransferRequested( + address indexed _from, + address indexed _to + ); + + event OwnershipTransferred( + address indexed previousOwner, + address indexed newOwner + ); + function setUp() public { initTestBase(); ownershipFacet = OwnershipFacet(address(diamond)); } - function testOwnerCanTransferOwnership() public { + function test_OwnerCanTransferOwnership() public { + vm.startPrank(USER_DIAMOND_OWNER); + + address newOwner = address(0x1234567890123456789012345678901234567890); + + vm.expectEmit(true, true, true, true, address(ownershipFacet)); + emit OwnershipTransferRequested(address(this), newOwner); + + ownershipFacet.transferOwnership(newOwner); + + assert(ownershipFacet.owner() != newOwner); + + vm.stopPrank(); + vm.startPrank(newOwner); + + vm.expectEmit(true, true, true, true, address(ownershipFacet)); + emit OwnershipTransferred(address(USER_DIAMOND_OWNER), newOwner); + + ownershipFacet.confirmOwnershipTransfer(); + + assert(ownershipFacet.owner() == newOwner); + + vm.stopPrank(); + } + + function testRevert_CannotCancelNonPendingOwnershipTransfer() public { + assert(ownershipFacet.owner() == USER_DIAMOND_OWNER); + vm.startPrank(USER_DIAMOND_OWNER); + + vm.expectRevert(NoPendingOwnershipTransfer.selector); + + ownershipFacet.cancelOwnershipTransfer(); + + assert(ownershipFacet.owner() == USER_DIAMOND_OWNER); + + vm.stopPrank(); + } + + function test_OwnerCanCancelOwnershipTransfer() public { + address newOwner = address(0x1234567890123456789012345678901234567890); + + ownershipFacet.transferOwnership(newOwner); + + assert(ownershipFacet.owner() != newOwner); + + ownershipFacet.cancelOwnershipTransfer(); + + assert(ownershipFacet.owner() != newOwner); + } + + function testRevert_NonOwnerCannotCancelOwnershipTransfer() public { address newOwner = address(0x1234567890123456789012345678901234567890); ownershipFacet.transferOwnership(newOwner); From ba8fdf6b3d3d5192823c711203f7d5750fbda633 Mon Sep 17 00:00:00 2001 From: Ed Zynda Date: Mon, 17 Feb 2025 15:20:16 +0300 Subject: [PATCH 27/55] improve test coverage --- test/solidity/Facets/ChainflipFacet.t.sol | 164 ++++++++++++++++++ .../Periphery/ReceiverChainflip.t.sol | 96 ++++++++++ 2 files changed, 260 insertions(+) diff --git a/test/solidity/Facets/ChainflipFacet.t.sol b/test/solidity/Facets/ChainflipFacet.t.sol index d5ef54823..436564bac 100644 --- a/test/solidity/Facets/ChainflipFacet.t.sol +++ b/test/solidity/Facets/ChainflipFacet.t.sol @@ -6,6 +6,7 @@ import { ChainflipFacet } from "lifi/Facets/ChainflipFacet.sol"; import { IChainflipVault } from "lifi/Interfaces/IChainflip.sol"; import { LibAsset } from "lifi/Libraries/LibAsset.sol"; import { LibSwap } from "lifi/Libraries/LibSwap.sol"; +import { InformationMismatch, CannotBridgeToSameNetwork } from "lifi/Errors/GenericErrors.sol"; // Stub ChainflipFacet Contract contract TestChainflipFacet is ChainflipFacet { @@ -238,6 +239,143 @@ contract ChainflipFacetTest is TestBaseFacet { vm.stopPrank(); } + function test_CanBridgeNativeTokensWithDestinationCall() + public + assertBalanceChange( + address(0), + USER_SENDER, + -int256(defaultNativeAmount) + ) + { + // Set destination to Arbitrum where our receiver contract is + bridgeData.destinationChainId = CHAIN_ID_ARBITRUM; + bridgeData.hasDestinationCall = true; + bridgeData.sendingAssetId = address(0); + bridgeData.minAmount = defaultNativeAmount; + + // Create swap data for the destination chain + LibSwap.SwapData[] memory destSwapData = new LibSwap.SwapData[](0); + + // Encode the message for the receiver contract + bytes memory message = abi.encode( + bridgeData.transactionId, + destSwapData, + USER_RECEIVER + ); + + validChainflipData = ChainflipFacet.ChainflipData({ + dstToken: 7, + nonEVMReceiver: bytes32(0), + message: message, + gasAmount: 100000, + cfParameters: "" + }); + + vm.startPrank(USER_SENDER); + + //prepare check for events + vm.expectEmit(true, true, true, true, _facetTestContractAddress); + emit LiFiTransferStarted(bridgeData); + + initiateBridgeTxWithFacet(true); + vm.stopPrank(); + } + + function test_CanBridgeNativeTokensWithoutDestinationCall() + public + assertBalanceChange( + address(0), + USER_SENDER, + -int256(defaultNativeAmount) + ) + { + bridgeData.destinationChainId = CHAIN_ID_ARBITRUM; + bridgeData.hasDestinationCall = false; + bridgeData.sendingAssetId = address(0); + bridgeData.minAmount = defaultNativeAmount; + + validChainflipData = ChainflipFacet.ChainflipData({ + dstToken: 7, + nonEVMReceiver: bytes32(0), + message: "", + gasAmount: 0, + cfParameters: "" + }); + + vm.startPrank(USER_SENDER); + + //prepare check for events + vm.expectEmit(true, true, true, true, _facetTestContractAddress); + emit LiFiTransferStarted(bridgeData); + + initiateBridgeTxWithFacet(true); + vm.stopPrank(); + } + + function testRevert_WhenDestinationCallFlagMismatchesMessage() public { + // Case 1: hasDestinationCall is true but message is empty + bridgeData.hasDestinationCall = true; + validChainflipData.message = ""; + + vm.startPrank(USER_SENDER); + usdc.approve(_facetTestContractAddress, bridgeData.minAmount); + + vm.expectRevert(InformationMismatch.selector); + initiateBridgeTxWithFacet(false); + + // Case 2: hasDestinationCall is false but message is not empty + bridgeData.hasDestinationCall = false; + validChainflipData.message = "0x123456"; + + vm.expectRevert(InformationMismatch.selector); + initiateBridgeTxWithFacet(false); + vm.stopPrank(); + } + + function test_ChainIdMappings() public { + // Set source chain to Arbitrum for these tests + vm.chainId(CHAIN_ID_ARBITRUM); + vm.roll(208460950); // Set specific block number for Arbitrum chain + + vm.startPrank(USER_SENDER); + + // Test Ethereum mapping + usdc.approve(_facetTestContractAddress, bridgeData.minAmount); + bridgeData.destinationChainId = CHAIN_ID_ETHEREUM; + initiateBridgeTxWithFacet(false); + + // Test Arbitrum mapping (should fail as same network) + usdc.approve(_facetTestContractAddress, bridgeData.minAmount); + bridgeData.destinationChainId = CHAIN_ID_ARBITRUM; + vm.expectRevert(CannotBridgeToSameNetwork.selector); + initiateBridgeTxWithFacet(false); + + // Test Solana mapping + usdc.approve(_facetTestContractAddress, bridgeData.minAmount); + bridgeData.destinationChainId = CHAIN_ID_SOLANA; + bridgeData.receiver = LibAsset.NON_EVM_ADDRESS; + validChainflipData.nonEVMReceiver = bytes32( + abi.encodePacked("EoW7FWTdPdZKpd3WAhH98c2HMGHsdh5yhzzEtk1u68Bb") + ); + initiateBridgeTxWithFacet(false); + + // Test Bitcoin mapping + usdc.approve(_facetTestContractAddress, bridgeData.minAmount); + bridgeData.destinationChainId = CHAIN_ID_BITCOIN; + validChainflipData.nonEVMReceiver = bytes32( + abi.encodePacked("bc1q6l08rtj6j907r2een0jqs6l7qnruwyxfshmf8a") + ); + initiateBridgeTxWithFacet(false); + + // Test invalid chain ID + usdc.approve(_facetTestContractAddress, bridgeData.minAmount); + bridgeData.destinationChainId = 137; // Polygon + vm.expectRevert(ChainflipFacet.UnsupportedChainflipChainId.selector); + initiateBridgeTxWithFacet(false); + + vm.stopPrank(); + } + function testRevert_WhenUsingEmptyNonEVMAddress() public { bridgeData.receiver = LibAsset.NON_EVM_ADDRESS; bridgeData.destinationChainId = CHAIN_ID_SOLANA; @@ -305,4 +443,30 @@ contract ChainflipFacetTest is TestBaseFacet { initiateBridgeTxWithFacet(false); vm.stopPrank(); } + + function testRevert_WhenNonEVMAddressWithEVMReceiver() public { + // Set source chain to Arbitrum for this test + vm.chainId(CHAIN_ID_ARBITRUM); + vm.roll(208460950); // Set specific block number for Arbitrum chain + + // Try to use nonEVMReceiver with an EVM address + bridgeData.receiver = USER_RECEIVER; // Use EVM address + bridgeData.destinationChainId = CHAIN_ID_ETHEREUM; + validChainflipData = ChainflipFacet.ChainflipData({ + dstToken: 6, + nonEVMReceiver: bytes32( + abi.encodePacked("bc1q6l08rtj6j907r2een0jqs6l7qnruwyxfshmf8a") + ), // Set nonEVMReceiver even though using EVM address + message: "", + gasAmount: 0, + cfParameters: "" + }); + + vm.startPrank(USER_SENDER); + usdc.approve(_facetTestContractAddress, bridgeData.minAmount); + + // Should proceed normally since nonEVMReceiver is ignored for EVM addresses + initiateBridgeTxWithFacet(false); + vm.stopPrank(); + } } diff --git a/test/solidity/Periphery/ReceiverChainflip.t.sol b/test/solidity/Periphery/ReceiverChainflip.t.sol index 10f6cb89e..27d659d73 100644 --- a/test/solidity/Periphery/ReceiverChainflip.t.sol +++ b/test/solidity/Periphery/ReceiverChainflip.t.sol @@ -176,6 +176,102 @@ contract ReceiverChainflipTest is TestBase { assertTrue(usdc.balanceOf(receiverAddress) == defaultUSDCAmount); } + function test_WillReturnNativeTokensToUserIfDstCallFails() public { + // Fund chainflipVault with native token as well + vm.deal(chainflipVault, 1 ether); + + // Create mock DEX that will revert + string memory revertReason = "Just because"; + MockUniswapDEX mockDEX = new MockUniswapDEX(); + + LibSwap.SwapData[] memory swapData = new LibSwap.SwapData[](1); + swapData[0] = LibSwap.SwapData({ + callTo: address(mockDEX), + approveTo: address(mockDEX), + sendingAssetId: LibAsset.NATIVE_ASSETID, + receivingAssetId: ADDRESS_DAI, + fromAmount: 1 ether, + callData: abi.encodeWithSelector( + mockDEX.mockSwapWillRevertWithReason.selector, + revertReason + ), + requiresDeposit: false + }); + + bytes memory payload = abi.encode(guid, swapData, receiverAddress); + + uint256 initialBalance = receiverAddress.balance; + + vm.startPrank(chainflipVault); + + vm.expectEmit(true, true, true, true, address(receiver)); + emit LiFiTransferRecovered( + guid, + LibAsset.NATIVE_ASSETID, + receiverAddress, + 1 ether, + block.timestamp + ); + + receiver.cfReceive{ value: 1 ether }( + 4, // srcChain (Arbitrum) + abi.encodePacked(address(0)), + payload, + LibAsset.NATIVE_ASSETID, + 1 ether + ); + + assertEq(receiverAddress.balance, initialBalance + 1 ether); + vm.stopPrank(); + } + + function testRevert_IfNativeTransferFails() public { + // Fund receiver with native token + vm.deal(address(receiver), 1 ether); + // Fund chainflipVault with native token + vm.deal(chainflipVault, 1 ether); + + // Create a contract that can't receive ETH + NonETHReceiver nonEthReceiver = new NonETHReceiver(); + + // Create mock DEX that will revert + MockUniswapDEX mockDEX = new MockUniswapDEX(); + + // Create swap data that will fail when trying to send ETH + LibSwap.SwapData[] memory swapData = new LibSwap.SwapData[](1); + swapData[0] = LibSwap.SwapData({ + callTo: address(mockDEX), + approveTo: address(mockDEX), + sendingAssetId: LibAsset.NATIVE_ASSETID, + receivingAssetId: LibAsset.NATIVE_ASSETID, // Try to get ETH back + fromAmount: 1 ether, + callData: abi.encodeWithSelector( + mockDEX.mockSwapWillRevertWithReason.selector, + "Mock swap failed" + ), + requiresDeposit: false + }); + + bytes memory payload = abi.encode( + guid, + swapData, + address(nonEthReceiver) + ); + + vm.startPrank(chainflipVault); + + vm.expectRevert(); + receiver.cfReceive{ value: 1 ether }( + 4, // srcChain (Arbitrum) + abi.encodePacked(address(0)), + payload, + LibAsset.NATIVE_ASSETID, + 1 ether + ); + + vm.stopPrank(); + } + function test_CanDecodeChainflipPayloadAndExecuteSwapNative() public { // fund chainflipVault with native token vm.deal(chainflipVault, 0.01 ether); From 282629b663a231cbe0d14fe2e745b1ad304e28c7 Mon Sep 17 00:00:00 2001 From: Ed Zynda Date: Mon, 17 Feb 2025 17:29:13 +0300 Subject: [PATCH 28/55] redeploy ChainflipFacet to staging --- deployments/_deployments_log_file.json | 6 +++--- deployments/arbitrum.diamond.staging.json | 2 +- deployments/arbitrum.staging.json | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/deployments/_deployments_log_file.json b/deployments/_deployments_log_file.json index e53b55c67..08848786d 100644 --- a/deployments/_deployments_log_file.json +++ b/deployments/_deployments_log_file.json @@ -28934,11 +28934,11 @@ "staging": { "1.0.0": [ { - "ADDRESS": "0x6ee80199Af6f871c8F0FaECc51EBbFf3200E61a6", + "ADDRESS": "0xDd661337B48BEA5194F6d26F2C59fF0855E15289", "OPTIMIZER_RUNS": "1000000", - "TIMESTAMP": "2025-02-14 13:00:45", + "TIMESTAMP": "2025-02-17 17:00:59", "CONSTRUCTOR_ARGS": "0x00000000000000000000000079001a5e762f3befc8e5871b42f6734e00498920", - "SALT": "28012025", + "SALT": "", "VERIFIED": "true" } ] diff --git a/deployments/arbitrum.diamond.staging.json b/deployments/arbitrum.diamond.staging.json index 69ca098e7..2ca6a471e 100644 --- a/deployments/arbitrum.diamond.staging.json +++ b/deployments/arbitrum.diamond.staging.json @@ -149,7 +149,7 @@ "Name": "", "Version": "" }, - "0x6ee80199Af6f871c8F0FaECc51EBbFf3200E61a6": { + "0xDd661337B48BEA5194F6d26F2C59fF0855E15289": { "Name": "ChainflipFacet", "Version": "1.0.0" } diff --git a/deployments/arbitrum.staging.json b/deployments/arbitrum.staging.json index 81b4e809e..3c3dc84e4 100644 --- a/deployments/arbitrum.staging.json +++ b/deployments/arbitrum.staging.json @@ -51,5 +51,5 @@ "ReceiverAcrossV3": "0xe4F3DEF14D61e47c696374453CD64d438FD277F8", "AcrossFacetPackedV3": "0x21767081Ff52CE5563A29f27149D01C7127775A2", "RelayFacet": "0x3cf7dE0e31e13C93c8Aada774ADF1C7eD58157f5", - "ChainflipFacet": "0x6ee80199Af6f871c8F0FaECc51EBbFf3200E61a6" + "ChainflipFacet": "0xDd661337B48BEA5194F6d26F2C59fF0855E15289" } \ No newline at end of file From 2704f8b265d621e1f56b90a21482ddc9e78098ea Mon Sep 17 00:00:00 2001 From: Ed Zynda Date: Tue, 18 Feb 2025 11:42:40 +0300 Subject: [PATCH 29/55] Update script to include destination calls --- deployments/_deployments_log_file.json | 16 ++ deployments/mainnet.diamond.staging.json | 5 +- deployments/mainnet.staging.json | 3 +- script/demoScripts/demoChainflip.ts | 178 +++++++++++++----- script/demoScripts/utils/demoScriptHelpers.ts | 61 +++++- 5 files changed, 211 insertions(+), 52 deletions(-) diff --git a/deployments/_deployments_log_file.json b/deployments/_deployments_log_file.json index 08848786d..b6dfed0c6 100644 --- a/deployments/_deployments_log_file.json +++ b/deployments/_deployments_log_file.json @@ -28944,5 +28944,21 @@ ] } } + }, + "ReceiverChainflip": { + "mainnet": { + "staging": { + "1.0.0": [ + { + "ADDRESS": "0x5DFcEbB675F3cA2049d4b6791Ce2284EA940204a", + "OPTIMIZER_RUNS": "1000000", + "TIMESTAMP": "2025-02-18 10:33:37", + "CONSTRUCTOR_ARGS": "0x000000000000000000000000156cebba59deb2cb23742f70dcb0a11cc775591f000000000000000000000000be27f03c8e6a61e2a4b1ee7940dbcb9204744d1c000000000000000000000000f5e10380213880111522dd0efd3dbb45b9f62bcc", + "SALT": "", + "VERIFIED": "true" + } + ] + } + } } } diff --git a/deployments/mainnet.diamond.staging.json b/deployments/mainnet.diamond.staging.json index 66e35168e..cd64617f4 100644 --- a/deployments/mainnet.diamond.staging.json +++ b/deployments/mainnet.diamond.staging.json @@ -108,7 +108,10 @@ "ReceiverAcrossV3": "", "ReceiverStargateV2": "", "RelayerCelerIM": "", - "TokenWrapper": "0x5215E9fd223BC909083fbdB2860213873046e45d" + "TokenWrapper": "0x5215E9fd223BC909083fbdB2860213873046e45d", + "GasZipPeriphery": "", + "Permit2Proxy": "", + "ReceiverChainflip": "" } } } \ No newline at end of file diff --git a/deployments/mainnet.staging.json b/deployments/mainnet.staging.json index d65e04d53..f92bcc700 100644 --- a/deployments/mainnet.staging.json +++ b/deployments/mainnet.staging.json @@ -29,5 +29,6 @@ "SquidFacet": "0x933A3AfE2087FB8F5c9EE9A033477C42CC14c18E", "SynapseBridgeFacet": "0x57F98A94AC66e197AF6776D5c094FF0da2C0B198", "ThorSwapFacet": "0xa696287F37d21D566B9A80AC29b2640FF910C176", - "StandardizedCallFacet": "0x637Ac9AddC9C38b3F52878E11620a9060DC71d8B" + "StandardizedCallFacet": "0x637Ac9AddC9C38b3F52878E11620a9060DC71d8B", + "ReceiverChainflip": "0x5DFcEbB675F3cA2049d4b6791Ce2284EA940204a" } \ No newline at end of file diff --git a/script/demoScripts/demoChainflip.ts b/script/demoScripts/demoChainflip.ts index 6d87dd236..1295fb25e 100644 --- a/script/demoScripts/demoChainflip.ts +++ b/script/demoScripts/demoChainflip.ts @@ -1,4 +1,12 @@ -import { getContract, parseUnits, Narrow, zeroAddress } from 'viem' +import { + getContract, + parseUnits, + Narrow, + zeroAddress, + encodeAbiParameters, + formatEther, + formatUnits, +} from 'viem' import { randomBytes } from 'crypto' import dotenv from 'dotenv' import erc20Artifact from '../../out/ERC20/ERC20.sol/ERC20.json' @@ -9,24 +17,77 @@ import { ensureBalance, ensureAllowance, executeTransaction, - setupEnvironment, ADDRESS_USDC_ARB, ADDRESS_USDT_ARB, + ADDRESS_USDC_ETH, + ADDRESS_UNISWAP_ETH, getUniswapDataERC20toExactERC20, + getUniswapDataExactETHToERC20, ADDRESS_UNISWAP_ARB, + setupEnvironment, } from './utils/demoScriptHelpers' +import deployments from '../../deployments/mainnet.staging.json' dotenv.config() -// #region ABIs +const RECEIVER_CHAINFLIP = deployments.ReceiverChainflip const ERC20_ABI = erc20Artifact.abi as Narrow const CHAINFLIP_FACET_ABI = chainflipFacetArtifact.abi as Narrow< typeof chainflipFacetArtifact.abi > -// #endregion + +async function createDestinationCallMessage( + transactionId: string, + finalReceiver: string, + totalETHAmount: bigint, + gasAmount: bigint +): Promise { + // Calculate exact ETH amount for swap (total - gas) + const swapETHAmount = totalETHAmount - gasAmount + + // Get swap data for ETH -> USDC on mainnet + const swapData = await getUniswapDataExactETHToERC20( + ADDRESS_UNISWAP_ETH, + 1, // Mainnet chainId + swapETHAmount, + ADDRESS_USDC_ETH, + finalReceiver, + false + ) + + // Encode the complete message for the receiver contract + return encodeAbiParameters( + [ + { type: 'bytes32' }, // transactionId + { + type: 'tuple[]', + components: [ + { type: 'address', name: 'callTo' }, + { type: 'address', name: 'approveTo' }, + { type: 'address', name: 'sendingAssetId' }, + { type: 'address', name: 'receivingAssetId' }, + { type: 'uint256', name: 'fromAmount' }, + { type: 'bytes', name: 'callData' }, + { type: 'bool', name: 'requiresDeposit' }, + ], + }, // swapData + { type: 'address' }, // receiver + ], + [transactionId, [swapData], finalReceiver] + ) +} async function main() { const withSwap = process.argv.includes('--with-swap') + const withDestinationCall = process.argv.includes('--with-destination-call') + + if (withSwap && withDestinationCall) { + console.error( + 'Error: Cannot use both --with-swap and --with-destination-call flags' + ) + process.exit(1) + } + // === Set up environment === const srcChain: SupportedChain = 'arbitrum' const destinationChainId = 1 // Mainnet @@ -40,25 +101,40 @@ async function main() { } = await setupEnvironment(srcChain, CHAINFLIP_FACET_ABI) const signerAddress = walletAccount.address - // === Instantiate contracts === - const tokenToApprove = withSwap ? ADDRESS_USDT_ARB : ADDRESS_USDC_ARB - const srcTokenContract = getContract({ - address: tokenToApprove, - abi: ERC20_ABI, - client, - }) + // Amount setup + const totalAmount = parseUnits('0.005', 18) // 0.005 ETH total + const gasAmount = parseUnits('0.001', 18) // 0.001 ETH for gas + const amount = withDestinationCall ? totalAmount : parseUnits('10', 6) // 10 USDC/USDT - const srcTokenName = (await srcTokenContract.read.name()) as string - const srcTokenSymbol = (await srcTokenContract.read.symbol()) as string - const srcTokenDecimals = (await srcTokenContract.read.decimals()) as bigint - const amount = parseUnits('10', Number(srcTokenDecimals)) + // Token setup + const tokenToApprove = withDestinationCall + ? zeroAddress + : withSwap + ? ADDRESS_USDT_ARB + : ADDRESS_USDC_ARB console.info( - `\nBridge ${amount} ${srcTokenName} (${srcTokenSymbol}) from ${srcChain} --> Mainnet` + `\nBridge ${ + withDestinationCall ? formatEther(amount) : formatUnits(amount, 6) + } ${withDestinationCall ? 'ETH' : 'USDC/USDT'} from ${srcChain} --> Mainnet` ) console.info(`Connected wallet address: ${signerAddress}`) - await ensureBalance(srcTokenContract, signerAddress, amount) + if (!withDestinationCall) { + const srcTokenContract = getContract({ + address: tokenToApprove, + abi: ERC20_ABI, + client, + }) + await ensureBalance(srcTokenContract, signerAddress, amount) + await ensureAllowance( + srcTokenContract, + signerAddress, + lifiDiamondAddress, + amount, + publicClient + ) + } // === Prepare bridge data === const bridgeData: ILiFi.BridgeDataStruct = { @@ -66,42 +142,54 @@ async function main() { bridge: 'chainflip', integrator: 'ACME Devs', referrer: zeroAddress, - sendingAssetId: withSwap ? ADDRESS_USDC_ARB : tokenToApprove, - receiver: signerAddress, + sendingAssetId: tokenToApprove, + receiver: withDestinationCall ? RECEIVER_CHAINFLIP : signerAddress, destinationChainId, minAmount: amount, hasSourceSwaps: withSwap, - hasDestinationCall: false, + hasDestinationCall: withDestinationCall, } + // Prepare destination call data if needed + const destinationCallMessage = withDestinationCall + ? await createDestinationCallMessage( + bridgeData.transactionId, + signerAddress, + totalAmount, + gasAmount + ) + : '' + const chainflipData: ChainflipFacet.ChainflipDataStruct = { nonEVMReceiver: '0x0000000000000000000000000000000000000000000000000000000000000000', - dstToken: 3, // Chainflip designator for USDC on ETH - message: '', // Empty bytes but properly formatted - gasAmount: 0n, - cfParameters: '', // Empty bytes but properly formatted + dstToken: withDestinationCall ? 1 : 3, // 1 for ETH, 3 for USDC on ETH + message: destinationCallMessage, + gasAmount: withDestinationCall ? gasAmount : 0n, + cfParameters: '', } - // === Start bridging === - if (withSwap) { - // Generate swap data to swap USDT -> exact USDC amount - const swapData = await getUniswapDataERC20toExactERC20( - ADDRESS_UNISWAP_ARB, // Uniswap router address on Arbitrum - 42161, // Arbitrum chain id - ADDRESS_USDT_ARB, // Swap from USDT - ADDRESS_USDC_ARB, // Swap to USDC - amount, // The exact output amount we want in USDC - lifiDiamondAddress, // Receiver for the swapped tokens (the diamond) - true // requiresDeposit flag + // === Execute the transaction === + if (withDestinationCall) { + await executeTransaction( + () => + lifiDiamondContract.write.startBridgeTokensViaChainflip( + [bridgeData, chainflipData], + { value: amount } + ), + 'Starting bridge tokens via Chainflip with destination call', + publicClient, + true ) - - await ensureAllowance( - srcTokenContract, - signerAddress, + } else if (withSwap) { + const swapData = await getUniswapDataERC20toExactERC20( + ADDRESS_UNISWAP_ARB, + 42161, + ADDRESS_USDT_ARB, + ADDRESS_USDC_ARB, + amount, lifiDiamondAddress, - swapData.fromAmount, - publicClient + true ) await executeTransaction( @@ -116,14 +204,6 @@ async function main() { true ) } else { - await ensureAllowance( - srcTokenContract, - signerAddress, - lifiDiamondAddress, - amount, - publicClient - ) - await executeTransaction( () => lifiDiamondContract.write.startBridgeTokensViaChainflip([ diff --git a/script/demoScripts/utils/demoScriptHelpers.ts b/script/demoScripts/utils/demoScriptHelpers.ts index e42bf722f..be36589d9 100644 --- a/script/demoScripts/utils/demoScriptHelpers.ts +++ b/script/demoScripts/utils/demoScriptHelpers.ts @@ -1,4 +1,5 @@ import { privateKeyToAccount } from 'viem/accounts' +import { formatEther, formatUnits, zeroAddress } from 'viem' import path from 'path' import { fileURLToPath } from 'url' import { providers, Wallet, BigNumber, constants, Contract } from 'ethers' @@ -18,7 +19,7 @@ import { import networks from '../../../config/networks.json' import { SupportedChain, viemChainMap } from './demoScriptChainConfig' -export const DEV_WALLET_ADDRESS = '0x29DaCdF7cCaDf4eE67c923b4C22255A4B2494eD7' +export const DEV_WALLET_ADDRESS = '0xb9c0dE368BECE5e76B52545a8E377a4C118f597B' export const DEFAULT_DEST_PAYLOAD_ABI = [ 'bytes32', // Transaction Id @@ -620,6 +621,64 @@ export const getConfigElement = ( /** * Executes a blockchain transaction, validates its receipt (optional), and handles errors. */ +export const getUniswapDataExactETHToERC20 = async ( + uniswapAddress: string, + chainId: number, + exactETHAmount: bigint, + receivingAssetId: string, + receiverAddress: string, + requiresDeposit = false, + deadline = Math.floor(Date.now() / 1000) + 60 * 60 +) => { + const provider = getProviderForChainId(chainId) + + const uniswap = new Contract( + uniswapAddress, + [ + 'function getAmountsOut(uint amountIn, address[] calldata path) external view returns (uint[] memory amounts)', + 'function swapExactETHForTokens(uint amountOutMin, address[] calldata path, address to, uint deadline) external payable returns (uint[] memory amounts)', + ], + provider + ) + + const path = [ADDRESS_WETH_ETH, receivingAssetId] + + try { + // Get the expected output amount for the exact ETH input + const amounts = await uniswap.getAmountsOut(exactETHAmount, path) + const expectedOutput = amounts[1] + const minAmountOut = BigNumber.from(expectedOutput).mul(95).div(100) // 5% slippage tolerance + + console.log('Exact ETH input:', formatEther(exactETHAmount)) + console.log('Expected USDC output:', formatUnits(expectedOutput, 6)) + console.log('Min USDC output with slippage:', formatUnits(minAmountOut, 6)) + + const uniswapCalldata = ( + await uniswap.populateTransaction.swapExactETHForTokens( + minAmountOut, + path, + receiverAddress, + deadline + ) + ).data + + if (!uniswapCalldata) throw Error('Could not create Uniswap calldata') + + return { + callTo: uniswapAddress, + approveTo: uniswapAddress, + sendingAssetId: zeroAddress, // ETH + receivingAssetId, + fromAmount: exactETHAmount, + callData: uniswapCalldata, + requiresDeposit, + } + } catch (error) { + console.error('Error in Uniswap contract interaction:', error) + throw error + } +} + export const executeTransaction = async ( transaction: () => Promise, transactionDescription: string, From cabbb8c15b617a111bdb85944b18f2ecc1d6dbe3 Mon Sep 17 00:00:00 2001 From: Ed Zynda Date: Tue, 18 Feb 2025 12:57:08 +0300 Subject: [PATCH 30/55] Refactor --- deployments/_deployments_log_file.json | 4 +- deployments/mainnet.diamond.staging.json | 2 +- deployments/mainnet.staging.json | 2 +- script/demoScripts/demoChainflip.ts | 150 +++++++++++++----- src/Periphery/ReceiverChainflip.sol | 26 +-- .../Periphery/ReceiverChainflip.t.sol | 8 +- 6 files changed, 136 insertions(+), 56 deletions(-) diff --git a/deployments/_deployments_log_file.json b/deployments/_deployments_log_file.json index b6dfed0c6..f77a37467 100644 --- a/deployments/_deployments_log_file.json +++ b/deployments/_deployments_log_file.json @@ -28950,9 +28950,9 @@ "staging": { "1.0.0": [ { - "ADDRESS": "0x5DFcEbB675F3cA2049d4b6791Ce2284EA940204a", + "ADDRESS": "0xf0CfD9A8Fb7954C9F9948b94AD6f1Cc27Faff54c", "OPTIMIZER_RUNS": "1000000", - "TIMESTAMP": "2025-02-18 10:33:37", + "TIMESTAMP": "2025-02-18 12:28:25", "CONSTRUCTOR_ARGS": "0x000000000000000000000000156cebba59deb2cb23742f70dcb0a11cc775591f000000000000000000000000be27f03c8e6a61e2a4b1ee7940dbcb9204744d1c000000000000000000000000f5e10380213880111522dd0efd3dbb45b9f62bcc", "SALT": "", "VERIFIED": "true" diff --git a/deployments/mainnet.diamond.staging.json b/deployments/mainnet.diamond.staging.json index cd64617f4..43aa4c1ec 100644 --- a/deployments/mainnet.diamond.staging.json +++ b/deployments/mainnet.diamond.staging.json @@ -111,7 +111,7 @@ "TokenWrapper": "0x5215E9fd223BC909083fbdB2860213873046e45d", "GasZipPeriphery": "", "Permit2Proxy": "", - "ReceiverChainflip": "" + "ReceiverChainflip": "0x5DFcEbB675F3cA2049d4b6791Ce2284EA940204a" } } } \ No newline at end of file diff --git a/deployments/mainnet.staging.json b/deployments/mainnet.staging.json index f92bcc700..1cb750528 100644 --- a/deployments/mainnet.staging.json +++ b/deployments/mainnet.staging.json @@ -30,5 +30,5 @@ "SynapseBridgeFacet": "0x57F98A94AC66e197AF6776D5c094FF0da2C0B198", "ThorSwapFacet": "0xa696287F37d21D566B9A80AC29b2640FF910C176", "StandardizedCallFacet": "0x637Ac9AddC9C38b3F52878E11620a9060DC71d8B", - "ReceiverChainflip": "0x5DFcEbB675F3cA2049d4b6791Ce2284EA940204a" + "ReceiverChainflip": "0xf0CfD9A8Fb7954C9F9948b94AD6f1Cc27Faff54c" } \ No newline at end of file diff --git a/script/demoScripts/demoChainflip.ts b/script/demoScripts/demoChainflip.ts index 1295fb25e..9ab232d9e 100644 --- a/script/demoScripts/demoChainflip.ts +++ b/script/demoScripts/demoChainflip.ts @@ -1,3 +1,83 @@ +// Import required libraries and artifacts +/** + * Executes a direct bridge transaction without any swaps + * Transfers tokens directly from source to destination chain + */ +async function executeDirect( + lifiDiamondContract: any, + bridgeData: ILiFi.BridgeDataStruct, + chainflipData: ChainflipFacet.ChainflipDataStruct, + publicClient: any +) { + await executeTransaction( + () => + lifiDiamondContract.write.startBridgeTokensViaChainflip([ + bridgeData, + chainflipData, + ]), + 'Starting bridge tokens via Chainflip', + publicClient, + true + ) +} + +/** + * Executes a bridge transaction with a source chain swap + * Swaps tokens on the source chain before bridging + */ +async function executeWithSourceSwap( + lifiDiamondContract: any, + bridgeData: ILiFi.BridgeDataStruct, + chainflipData: ChainflipFacet.ChainflipDataStruct, + amount: bigint, + publicClient: any +) { + const swapData = await getUniswapDataERC20toExactERC20( + ADDRESS_UNISWAP_ARB, + 42161, + ADDRESS_USDT_ARB, + ADDRESS_USDC_ARB, + amount, + lifiDiamondAddress, + true + ) + + await executeTransaction( + () => + lifiDiamondContract.write.swapAndStartBridgeTokensViaChainflip([ + bridgeData, + [swapData], + chainflipData, + ]), + 'Swapping and starting bridge tokens via Chainflip', + publicClient, + true + ) +} + +/** + * Executes a bridge transaction with a destination chain call + * Bridges ETH and includes instructions for a swap on the destination chain + */ +async function executeWithDestinationCall( + lifiDiamondContract: any, + bridgeData: ILiFi.BridgeDataStruct, + chainflipData: ChainflipFacet.ChainflipDataStruct, + amount: bigint, + publicClient: any +) { + await executeTransaction( + () => + lifiDiamondContract.write.startBridgeTokensViaChainflip( + [bridgeData, chainflipData], + { value: amount } + ), + 'Starting bridge tokens via Chainflip with destination call', + publicClient, + true + ) +} + import { getContract, parseUnits, @@ -30,12 +110,22 @@ import deployments from '../../deployments/mainnet.staging.json' dotenv.config() +// Contract addresses and ABIs const RECEIVER_CHAINFLIP = deployments.ReceiverChainflip const ERC20_ABI = erc20Artifact.abi as Narrow const CHAINFLIP_FACET_ABI = chainflipFacetArtifact.abi as Narrow< typeof chainflipFacetArtifact.abi > +/** + * Creates a message for cross-chain execution on the destination chain + * This message will be used to swap received ETH for USDC using Uniswap + * @param transactionId Unique identifier for the transaction + * @param finalReceiver Address that will receive the swapped tokens + * @param totalETHAmount Total amount of ETH being bridged + * @param gasAmount Amount of ETH reserved for gas on destination chain + * @returns Encoded message containing swap instructions + */ async function createDestinationCallMessage( transactionId: string, finalReceiver: string, @@ -43,9 +133,10 @@ async function createDestinationCallMessage( gasAmount: bigint ): Promise { // Calculate exact ETH amount for swap (total - gas) + // Reserve some ETH for gas fees on destination chain const swapETHAmount = totalETHAmount - gasAmount - // Get swap data for ETH -> USDC on mainnet + // Prepare swap parameters for ETH -> USDC on Ethereum mainnet const swapData = await getUniswapDataExactETHToERC20( ADDRESS_UNISWAP_ETH, 1, // Mainnet chainId @@ -55,7 +146,7 @@ async function createDestinationCallMessage( false ) - // Encode the complete message for the receiver contract + // Encode the message according to the ReceiverChainflip contract's expected format return encodeAbiParameters( [ { type: 'bytes32' }, // transactionId @@ -169,50 +260,29 @@ async function main() { cfParameters: '', } - // === Execute the transaction === + // === Execute the appropriate transaction type === if (withDestinationCall) { - await executeTransaction( - () => - lifiDiamondContract.write.startBridgeTokensViaChainflip( - [bridgeData, chainflipData], - { value: amount } - ), - 'Starting bridge tokens via Chainflip with destination call', - publicClient, - true + await executeWithDestinationCall( + lifiDiamondContract, + bridgeData, + chainflipData, + amount, + publicClient ) } else if (withSwap) { - const swapData = await getUniswapDataERC20toExactERC20( - ADDRESS_UNISWAP_ARB, - 42161, - ADDRESS_USDT_ARB, - ADDRESS_USDC_ARB, + await executeWithSourceSwap( + lifiDiamondContract, + bridgeData, + chainflipData, amount, - lifiDiamondAddress, - true - ) - - await executeTransaction( - () => - lifiDiamondContract.write.swapAndStartBridgeTokensViaChainflip([ - bridgeData, - [swapData], - chainflipData, - ]), - 'Swapping and starting bridge tokens via Chainflip', - publicClient, - true + publicClient ) } else { - await executeTransaction( - () => - lifiDiamondContract.write.startBridgeTokensViaChainflip([ - bridgeData, - chainflipData, - ]), - 'Starting bridge tokens via Chainflip', - publicClient, - true + await executeDirect( + lifiDiamondContract, + bridgeData, + chainflipData, + publicClient ) } } diff --git a/src/Periphery/ReceiverChainflip.sol b/src/Periphery/ReceiverChainflip.sol index 54cbb2b1c..f8aad1904 100644 --- a/src/Periphery/ReceiverChainflip.sol +++ b/src/Periphery/ReceiverChainflip.sol @@ -22,6 +22,9 @@ contract ReceiverChainflip is ILiFi, WithdrawablePeriphery { IExecutor public immutable executor; /// @notice The Chainflip vault contract that is authorized to call this contract address public immutable chainflipVault; + /// @notice Chainflip's native token address representation + address constant CHAINFLIP_NATIVE_ADDRESS = + 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; /// Modifiers /// @@ -105,28 +108,33 @@ contract ReceiverChainflip is ILiFi, WithdrawablePeriphery { address payable receiver, uint256 amount ) private { + // Convert Chainflip's native token address to LibAsset.NATIVE_ASSETID + address actualAssetId = assetId == CHAINFLIP_NATIVE_ADDRESS + ? LibAsset.NATIVE_ASSETID + : assetId; + // Don't need approval for native token - if (assetId != LibAsset.NATIVE_ASSETID) { - assetId.safeApproveWithRetry(address(executor), amount); + if (actualAssetId != LibAsset.NATIVE_ASSETID) { + actualAssetId.safeApproveWithRetry(address(executor), amount); } try executor.swapAndCompleteBridgeTokens{ - value: assetId == LibAsset.NATIVE_ASSETID ? amount : 0 - }(_transactionId, _swapData, assetId, receiver) + value: actualAssetId == LibAsset.NATIVE_ASSETID ? amount : 0 + }(_transactionId, _swapData, actualAssetId, receiver) {} catch { // send the bridged (and unswapped) funds to receiver address - if (assetId == LibAsset.NATIVE_ASSETID) { + if (actualAssetId == LibAsset.NATIVE_ASSETID) { // Handle native token using safeTransferETH receiver.safeTransferETH(amount); } else { // Handle ERC20 token - assetId.safeTransfer(receiver, amount); + actualAssetId.safeTransfer(receiver, amount); } emit LiFiTransferRecovered( _transactionId, - assetId, + actualAssetId, receiver, amount, block.timestamp @@ -134,8 +142,8 @@ contract ReceiverChainflip is ILiFi, WithdrawablePeriphery { } // Only reset approval for non-native tokens - if (assetId != LibAsset.NATIVE_ASSETID) { - assetId.safeApprove(address(executor), 0); + if (actualAssetId != LibAsset.NATIVE_ASSETID) { + actualAssetId.safeApprove(address(executor), 0); } } diff --git a/test/solidity/Periphery/ReceiverChainflip.t.sol b/test/solidity/Periphery/ReceiverChainflip.t.sol index 27d659d73..d5e78f1c1 100644 --- a/test/solidity/Periphery/ReceiverChainflip.t.sol +++ b/test/solidity/Periphery/ReceiverChainflip.t.sol @@ -16,6 +16,8 @@ contract ReceiverChainflipTest is TestBase { ReceiverChainflip internal receiver; bytes32 guid = bytes32("12345"); address receiverAddress = USER_RECEIVER; + address constant CHAINFLIP_NATIVE_ADDRESS = + 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; Executor executor; ERC20Proxy erc20Proxy; @@ -217,7 +219,7 @@ contract ReceiverChainflipTest is TestBase { 4, // srcChain (Arbitrum) abi.encodePacked(address(0)), payload, - LibAsset.NATIVE_ASSETID, + CHAINFLIP_NATIVE_ADDRESS, 1 ether ); @@ -265,7 +267,7 @@ contract ReceiverChainflipTest is TestBase { 4, // srcChain (Arbitrum) abi.encodePacked(address(0)), payload, - LibAsset.NATIVE_ASSETID, + CHAINFLIP_NATIVE_ADDRESS, 1 ether ); @@ -301,7 +303,7 @@ contract ReceiverChainflipTest is TestBase { 4, // srcChain (Arbitrum) abi.encodePacked(address(0)), payload, - LibAsset.NATIVE_ASSETID, + CHAINFLIP_NATIVE_ADDRESS, 0.01 ether ); vm.stopPrank(); From a9a49b168f9886730ee9018e9e94151bd3a70a8f Mon Sep 17 00:00:00 2001 From: Ed Zynda Date: Wed, 19 Feb 2025 11:34:53 +0300 Subject: [PATCH 31/55] Fix --- script/demoScripts/demoChainflip.ts | 78 ++++++++++++++--------------- 1 file changed, 39 insertions(+), 39 deletions(-) diff --git a/script/demoScripts/demoChainflip.ts b/script/demoScripts/demoChainflip.ts index 9ab232d9e..ffb652213 100644 --- a/script/demoScripts/demoChainflip.ts +++ b/script/demoScripts/demoChainflip.ts @@ -1,4 +1,43 @@ // Import required libraries and artifacts +import { + getContract, + parseUnits, + Narrow, + zeroAddress, + encodeAbiParameters, + formatEther, + formatUnits, +} from 'viem' +import { randomBytes } from 'crypto' +import dotenv from 'dotenv' +import erc20Artifact from '../../out/ERC20/ERC20.sol/ERC20.json' +import chainflipFacetArtifact from '../../out/ChainflipFacet.sol/ChainflipFacet.json' +import { ChainflipFacet, ILiFi } from '../../typechain' +import { SupportedChain } from './utils/demoScriptChainConfig' +import { + ensureBalance, + ensureAllowance, + executeTransaction, + ADDRESS_USDC_ARB, + ADDRESS_USDT_ARB, + ADDRESS_USDC_ETH, + ADDRESS_UNISWAP_ETH, + getUniswapDataERC20toExactERC20, + getUniswapDataExactETHToERC20, + ADDRESS_UNISWAP_ARB, + setupEnvironment, +} from './utils/demoScriptHelpers' +import deployments from '../../deployments/mainnet.staging.json' + +dotenv.config() + +// Contract addresses and ABIs +const RECEIVER_CHAINFLIP = deployments.ReceiverChainflip +const ERC20_ABI = erc20Artifact.abi as Narrow +const CHAINFLIP_FACET_ABI = chainflipFacetArtifact.abi as Narrow< + typeof chainflipFacetArtifact.abi +> + /** * Executes a direct bridge transaction without any swaps * Transfers tokens directly from source to destination chain @@ -78,45 +117,6 @@ async function executeWithDestinationCall( ) } -import { - getContract, - parseUnits, - Narrow, - zeroAddress, - encodeAbiParameters, - formatEther, - formatUnits, -} from 'viem' -import { randomBytes } from 'crypto' -import dotenv from 'dotenv' -import erc20Artifact from '../../out/ERC20/ERC20.sol/ERC20.json' -import chainflipFacetArtifact from '../../out/ChainflipFacet.sol/ChainflipFacet.json' -import { ChainflipFacet, ILiFi } from '../../typechain' -import { SupportedChain } from './utils/demoScriptChainConfig' -import { - ensureBalance, - ensureAllowance, - executeTransaction, - ADDRESS_USDC_ARB, - ADDRESS_USDT_ARB, - ADDRESS_USDC_ETH, - ADDRESS_UNISWAP_ETH, - getUniswapDataERC20toExactERC20, - getUniswapDataExactETHToERC20, - ADDRESS_UNISWAP_ARB, - setupEnvironment, -} from './utils/demoScriptHelpers' -import deployments from '../../deployments/mainnet.staging.json' - -dotenv.config() - -// Contract addresses and ABIs -const RECEIVER_CHAINFLIP = deployments.ReceiverChainflip -const ERC20_ABI = erc20Artifact.abi as Narrow -const CHAINFLIP_FACET_ABI = chainflipFacetArtifact.abi as Narrow< - typeof chainflipFacetArtifact.abi -> - /** * Creates a message for cross-chain execution on the destination chain * This message will be used to swap received ETH for USDC using Uniswap From feccafc9358e53c7bcc826a84ce905e0b837378d Mon Sep 17 00:00:00 2001 From: Ed Zynda Date: Wed, 19 Feb 2025 17:12:50 +0300 Subject: [PATCH 32/55] Fixes --- src/Facets/ChainflipFacet.sol | 4 ++-- test/solidity/Facets/ChainflipFacet.t.sol | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/Facets/ChainflipFacet.sol b/src/Facets/ChainflipFacet.sol index af5749f5e..837cac0c8 100644 --- a/src/Facets/ChainflipFacet.sol +++ b/src/Facets/ChainflipFacet.sol @@ -153,7 +153,7 @@ contract ChainflipFacet is ILiFi, ReentrancyGuard, SwapperV2, Validatable { revert InformationMismatch(); } - // Handle native token case with or without CCM + // Handle native token case with or without destination call if (_bridgeData.sendingAssetId == address(0)) { if (_bridgeData.hasDestinationCall) { IChainflipVault(chainflipVault).xCallNative{ @@ -177,7 +177,7 @@ contract ChainflipFacet is ILiFi, ReentrancyGuard, SwapperV2, Validatable { ); } } - // Handle ERC20 token case with or without CCM + // Handle ERC20 token case with or without destination call else { // Approve vault to spend tokens LibAsset.maxApproveERC20( diff --git a/test/solidity/Facets/ChainflipFacet.t.sol b/test/solidity/Facets/ChainflipFacet.t.sol index 436564bac..902267bfe 100644 --- a/test/solidity/Facets/ChainflipFacet.t.sol +++ b/test/solidity/Facets/ChainflipFacet.t.sol @@ -123,7 +123,6 @@ contract ChainflipFacetTest is TestBaseFacet { USER_SENDER, -int256(defaultUSDCAmount) ) - assertBalanceChange(ADDRESS_DAI, USER_SENDER, 0) { bridgeData.receiver = LibAsset.NON_EVM_ADDRESS; bridgeData.destinationChainId = CHAIN_ID_SOLANA; From 910f61cbee0105564d6cf552230f73280f864cfa Mon Sep 17 00:00:00 2001 From: Ed Zynda Date: Wed, 19 Feb 2025 17:14:45 +0300 Subject: [PATCH 33/55] Fixes --- docs/ChainflipFacet.md | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/docs/ChainflipFacet.md b/docs/ChainflipFacet.md index 153d45192..ccb54de64 100644 --- a/docs/ChainflipFacet.md +++ b/docs/ChainflipFacet.md @@ -2,7 +2,7 @@ ## How it works -The Chainflip Facet enables cross-chain token transfers using Chainflip's protocol. It supports both EVM chains (Ethereum, Arbitrum) and non-EVM chains (Solana, Bitcoin) as destinations. +The Chainflip Facet enables cross-chain token transfers using Chainflip's protocol. It supports both EVM chains and non-EVM chains as destinations. ```mermaid graph LR; @@ -24,24 +24,19 @@ The methods listed above take a variable labeled `_chainflipData`. This data is ```solidity struct ChainflipData { bytes32 nonEVMReceiver; // Destination address for non-EVM chains (Solana, Bitcoin) - uint32 dstToken; // Token identifier on the destination chain + uint32 dstToken; // Chainflip specific token identifier on the destination chain bytes message; // Message that is passed to the destination address for cross-chain messaging uint256 gasAmount; // Gas budget for the call on the destination chain bytes cfParameters; // Additional parameters for future features } ``` -For non-EVM destinations (Solana, Bitcoin), set the `receiver` in `BridgeData` to `LibAsset.NON_EVM_ADDRESS` and provide the destination address in `nonEVMReceiver`. +For non-EVM destinations (i.e. Solana, Bitcoin), set the `receiver` in `BridgeData` to `LibAsset.NON_EVM_ADDRESS` and provide the destination address in `nonEVMReceiver`. ## Supported Chains The facet supports the following chains with their respective IDs: -- Ethereum (1) -- Arbitrum (42161) -- Solana (1151111081099710) -- Bitcoin (20000000000001) - [Reference](https://docs.chainflip.io/swapping/integrations/advanced/vault-swaps#supported-chains) ## Swap Data From 676cdf98823e1b5b8b33d882bcbd8bb24466fc82 Mon Sep 17 00:00:00 2001 From: Ed Zynda Date: Wed, 19 Feb 2025 17:24:31 +0300 Subject: [PATCH 34/55] Fixes --- src/Facets/ChainflipFacet.sol | 9 ++++----- src/Interfaces/IChainflip.sol | 8 ++++---- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/src/Facets/ChainflipFacet.sol b/src/Facets/ChainflipFacet.sol index 837cac0c8..864a64977 100644 --- a/src/Facets/ChainflipFacet.sol +++ b/src/Facets/ChainflipFacet.sol @@ -43,7 +43,7 @@ contract ChainflipFacet is ILiFi, ReentrancyGuard, SwapperV2, Validatable { /// @dev Parameters specific to Chainflip bridge /// @param nonEVMReceiver Destination address for non-EVM chains (Solana, Bitcoin) - /// @param dstToken Token to be received on the destination chain (uint32) + /// @param dstToken Chainflip specific token identifier on the destination chain /// @param message Message that is passed to the destination address for cross-chain messaging /// @param gasAmount Gas budget for the call on the destination chain /// @param cfParameters Additional metadata for future features @@ -115,7 +115,7 @@ contract ChainflipFacet is ILiFi, ReentrancyGuard, SwapperV2, Validatable { /// @notice Contains the business logic for bridging via Chainflip /// @param _bridgeData The core information needed for bridging, including sending/receiving details - /// @param _chainflipData Data specific to Chainflip, including destination token and parameters + /// @param _chainflipData Data specific to Chainflip, including Chainflip token identifiers and parameters /// @dev Handles both EVM and non-EVM destinations, native and ERC20 tokens, and cross-chain messaging function _startBridge( ILiFi.BridgeData memory _bridgeData, @@ -212,10 +212,9 @@ contract ChainflipFacet is ILiFi, ReentrancyGuard, SwapperV2, Validatable { emit LiFiTransferStarted(_bridgeData); } - /// @notice Converts LiFi chain IDs to Chainflip chain IDs + /// @notice Converts LiFi internal chain IDs to Chainflip chain IDs /// @param destinationChainId The LiFi chain ID to convert - /// @return The corresponding Chainflip chain ID (uint32) - /// @dev Supports Ethereum (1), Arbitrum (4), Solana (5), and Bitcoin (3) + /// @return The corresponding Chainflip chain ID /// @dev Reverts if the destination chain is not supported function _getChainflipChainId( uint256 destinationChainId diff --git a/src/Interfaces/IChainflip.sol b/src/Interfaces/IChainflip.sol index 502a91346..751110b2d 100644 --- a/src/Interfaces/IChainflip.sol +++ b/src/Interfaces/IChainflip.sol @@ -9,7 +9,7 @@ interface IChainflipVault { /// @notice Swaps native token to any supported asset on any supported chain /// @param dstChain Destination chain for the swap /// @param dstAddress Address where the swapped tokens will be sent to on the destination chain - /// @param dstToken Token to be received on the destination chain + /// @param dstToken Chainflip specific token identifier on the destination chain /// @param cfParameters Additional metadata for future features function xSwapNative( uint32 dstChain, @@ -21,7 +21,7 @@ interface IChainflipVault { /// @notice Swaps ERC20 token to any supported asset on any supported chain /// @param dstChain Destination chain for the swap /// @param dstAddress Address where the swapped tokens will be sent to on the destination chain - /// @param dstToken Token to be received on the destination chain + /// @param dstToken Chainflip specific token identifier on the destination chain /// @param srcToken Address of the token to be swapped from the source chain /// @param amount Amount of the source token to be swapped /// @param cfParameters Additional metadata for future features @@ -37,7 +37,7 @@ interface IChainflipVault { /// @notice Swaps native token and calls a contract on the destination chain with a message /// @param dstChain Destination chain for the swap /// @param dstAddress Address where the swapped tokens will be sent to on the destination chain - /// @param dstToken Token to be received on the destination chain + /// @param dstToken Chainflip specific token identifier on the destination chain /// @param message Message that is passed to the destination address on the destination chain /// @param gasAmount Gas budget for the call on the destination chain /// @param cfParameters Additional metadata for future features @@ -53,7 +53,7 @@ interface IChainflipVault { /// @notice Swaps ERC20 token and calls a contract on the destination chain with a message /// @param dstChain Destination chain for the swap /// @param dstAddress Address where the swapped tokens will be sent to on the destination chain - /// @param dstToken Token to be received on the destination chain + /// @param dstToken Chainflip specific token identifier on the destination chain /// @param message Message that is passed to the destination address on the destination chain /// @param gasAmount Gas budget for the call on the destination chain /// @param srcToken Address of the token to be swapped from the source chain From 1df8a45471cc736929a4d309dfe0708982a00eae Mon Sep 17 00:00:00 2001 From: Ed Zynda Date: Wed, 19 Feb 2025 17:29:38 +0300 Subject: [PATCH 35/55] Fixes --- test/solidity/Periphery/ReceiverChainflip.t.sol | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/test/solidity/Periphery/ReceiverChainflip.t.sol b/test/solidity/Periphery/ReceiverChainflip.t.sol index d5e78f1c1..7f6e93e2f 100644 --- a/test/solidity/Periphery/ReceiverChainflip.t.sol +++ b/test/solidity/Periphery/ReceiverChainflip.t.sol @@ -13,6 +13,8 @@ import { MockUniswapDEX, NonETHReceiver } from "../utils/TestHelpers.sol"; contract ReceiverChainflipTest is TestBase { using stdJson for string; + error ETHTransferFailed(); + ReceiverChainflip internal receiver; bytes32 guid = bytes32("12345"); address receiverAddress = USER_RECEIVER; @@ -262,7 +264,7 @@ contract ReceiverChainflipTest is TestBase { vm.startPrank(chainflipVault); - vm.expectRevert(); + vm.expectRevert(ETHTransferFailed.selector); receiver.cfReceive{ value: 1 ether }( 4, // srcChain (Arbitrum) abi.encodePacked(address(0)), From a60a539e3f74d2af9d99876aa8460b0ce9e51d6c Mon Sep 17 00:00:00 2001 From: Ed Zynda Date: Wed, 19 Feb 2025 18:18:36 +0300 Subject: [PATCH 36/55] Fixes --- src/Periphery/ReceiverChainflip.sol | 1 + test/solidity/Periphery/ReceiverChainflip.t.sol | 13 ++++++------- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Periphery/ReceiverChainflip.sol b/src/Periphery/ReceiverChainflip.sol index f8aad1904..092868330 100644 --- a/src/Periphery/ReceiverChainflip.sol +++ b/src/Periphery/ReceiverChainflip.sol @@ -7,6 +7,7 @@ import { ILiFi } from "../Interfaces/ILiFi.sol"; import { IExecutor } from "../Interfaces/IExecutor.sol"; import { WithdrawablePeriphery } from "../Helpers/WithdrawablePeriphery.sol"; import { SafeTransferLib } from "solady/utils/SafeTransferLib.sol"; +import { console2 } from "forge-std/console2.sol"; /// @title ReceiverChainflip /// @author LI.FI (https://li.fi) diff --git a/test/solidity/Periphery/ReceiverChainflip.t.sol b/test/solidity/Periphery/ReceiverChainflip.t.sol index 7f6e93e2f..41b6daf47 100644 --- a/test/solidity/Periphery/ReceiverChainflip.t.sol +++ b/test/solidity/Periphery/ReceiverChainflip.t.sol @@ -105,7 +105,7 @@ contract ReceiverChainflipTest is TestBase { // fake a call from Chainflip vault vm.startPrank(chainflipVault); - vm.expectEmit(); + vm.expectEmit(true, true, true, true, address(executor)); emit LiFiTransferCompleted( guid, ADDRESS_USDC, @@ -113,6 +113,7 @@ contract ReceiverChainflipTest is TestBase { amountOutMin, block.timestamp ); + receiver.cfReceive( 1, // srcChain (Ethereum) abi.encodePacked(address(0)), @@ -120,6 +121,7 @@ contract ReceiverChainflipTest is TestBase { ADDRESS_USDC, defaultUSDCAmount ); + vm.stopPrank(); // Verify balances changed correctly @@ -334,10 +336,7 @@ contract ReceiverChainflipTest is TestBase { path[1] = _receivingAssetId; uint256 amountIn = defaultUSDCAmount; - - // Calculate USDC input amount - uint256[] memory amounts = uniswap.getAmountsOut(amountIn, path); - amountOutMin = amounts[1]; + amountOutMin = defaultUSDCAmount; // Use same amount for exact output LibSwap.SwapData[] memory swapData = new LibSwap.SwapData[](1); swapData[0] = LibSwap.SwapData({ @@ -347,9 +346,9 @@ contract ReceiverChainflipTest is TestBase { receivingAssetId: _receivingAssetId, fromAmount: amountIn, callData: abi.encodeWithSelector( - uniswap.swapExactTokensForTokens.selector, - amountIn, + uniswap.swapTokensForExactTokens.selector, amountOutMin, + amountIn, // Max input amount path, address(executor), block.timestamp + 20 minutes From 3ce39a5ecfa706ca5501f023e3ba8b5004aa6173 Mon Sep 17 00:00:00 2001 From: Ed Zynda Date: Wed, 19 Feb 2025 18:24:47 +0300 Subject: [PATCH 37/55] Fixes --- test/solidity/Periphery/ReceiverChainflip.t.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/solidity/Periphery/ReceiverChainflip.t.sol b/test/solidity/Periphery/ReceiverChainflip.t.sol index 41b6daf47..6a4b934a9 100644 --- a/test/solidity/Periphery/ReceiverChainflip.t.sol +++ b/test/solidity/Periphery/ReceiverChainflip.t.sol @@ -231,7 +231,7 @@ contract ReceiverChainflipTest is TestBase { vm.stopPrank(); } - function testRevert_IfNativeTransferFails() public { + function testRevert_IfNativeRecoveryTransferFailsToSendETHToUser() public { // Fund receiver with native token vm.deal(address(receiver), 1 ether); // Fund chainflipVault with native token From 3858428b27157e011a3b00e4c3942463a238a7e8 Mon Sep 17 00:00:00 2001 From: Ed Zynda Date: Thu, 20 Feb 2025 15:56:04 +0300 Subject: [PATCH 38/55] remove duplicates --- src/Periphery/ReceiverChainflip.sol | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/Periphery/ReceiverChainflip.sol b/src/Periphery/ReceiverChainflip.sol index 092868330..e5935e62c 100644 --- a/src/Periphery/ReceiverChainflip.sol +++ b/src/Periphery/ReceiverChainflip.sol @@ -88,12 +88,6 @@ contract ReceiverChainflip is ILiFi, WithdrawablePeriphery { /// Private Methods /// - /// @notice Performs a swap before completing a cross-chain transaction - /// @param _transactionId the transaction id associated with the operation - /// @param _swapData array of data needed for swaps - /// @param assetId address of the token received from the source chain - /// @param receiver address that will receive tokens in the end - /// @param amount amount of token /// @notice Performs a swap before completing a cross-chain transaction /// @param _transactionId The transaction id associated with the operation /// @param _swapData Array of data needed for swaps From 498d40f9b1c911e0d7015d653df1b20d60fadf29 Mon Sep 17 00:00:00 2001 From: Ed Zynda Date: Fri, 21 Feb 2025 12:45:33 +0300 Subject: [PATCH 39/55] use separate receiver var for dst calls --- docs/ChainflipFacet.md | 1 + script/demoScripts/demoChainflip.ts | 3 +- src/Facets/ChainflipFacet.sol | 11 ++-- src/Periphery/ReceiverChainflip.sol | 66 +++++++++++++---------- test/solidity/Facets/ChainflipFacet.t.sol | 13 ++++- 5 files changed, 59 insertions(+), 35 deletions(-) diff --git a/docs/ChainflipFacet.md b/docs/ChainflipFacet.md index ccb54de64..3c05157c1 100644 --- a/docs/ChainflipFacet.md +++ b/docs/ChainflipFacet.md @@ -25,6 +25,7 @@ The methods listed above take a variable labeled `_chainflipData`. This data is struct ChainflipData { bytes32 nonEVMReceiver; // Destination address for non-EVM chains (Solana, Bitcoin) uint32 dstToken; // Chainflip specific token identifier on the destination chain + address dstCallReceiver; // Receiver contract address used for destination calls. Ignored if there is no destination call bytes message; // Message that is passed to the destination address for cross-chain messaging uint256 gasAmount; // Gas budget for the call on the destination chain bytes cfParameters; // Additional parameters for future features diff --git a/script/demoScripts/demoChainflip.ts b/script/demoScripts/demoChainflip.ts index ffb652213..a81872fa9 100644 --- a/script/demoScripts/demoChainflip.ts +++ b/script/demoScripts/demoChainflip.ts @@ -234,7 +234,7 @@ async function main() { integrator: 'ACME Devs', referrer: zeroAddress, sendingAssetId: tokenToApprove, - receiver: withDestinationCall ? RECEIVER_CHAINFLIP : signerAddress, + receiver: signerAddress, // Always use signer address destinationChainId, minAmount: amount, hasSourceSwaps: withSwap, @@ -255,6 +255,7 @@ async function main() { nonEVMReceiver: '0x0000000000000000000000000000000000000000000000000000000000000000', dstToken: withDestinationCall ? 1 : 3, // 1 for ETH, 3 for USDC on ETH + dstCallReceiver: RECEIVER_CHAINFLIP, message: destinationCallMessage, gasAmount: withDestinationCall ? gasAmount : 0n, cfParameters: '', diff --git a/src/Facets/ChainflipFacet.sol b/src/Facets/ChainflipFacet.sol index 864a64977..fbb13c357 100644 --- a/src/Facets/ChainflipFacet.sol +++ b/src/Facets/ChainflipFacet.sol @@ -50,6 +50,7 @@ contract ChainflipFacet is ILiFi, ReentrancyGuard, SwapperV2, Validatable { struct ChainflipData { bytes32 nonEVMReceiver; uint32 dstToken; + address dstCallReceiver; bytes message; uint256 gasAmount; bytes cfParameters; @@ -126,7 +127,6 @@ contract ChainflipFacet is ILiFi, ReentrancyGuard, SwapperV2, Validatable { // Handle address encoding based on destination chain type bytes memory encodedDstAddress; if (_bridgeData.receiver == LibAsset.NON_EVM_ADDRESS) { - // For non-EVM chains (Solana, Bitcoin), use the raw bytes32 from chainflipData if (_chainflipData.nonEVMReceiver == bytes32(0)) { revert EmptyNonEvmAddress(); } @@ -134,15 +134,18 @@ contract ChainflipFacet is ILiFi, ReentrancyGuard, SwapperV2, Validatable { _chainflipData.nonEVMReceiver ); - // Emit special event for non-EVM transfers emit BridgeToNonEVMChain( _bridgeData.transactionId, _bridgeData.destinationChainId, _chainflipData.nonEVMReceiver ); } else { - // For EVM chains, encode the address - encodedDstAddress = abi.encodePacked(_bridgeData.receiver); + // For EVM chains, use dstCallReceiver if there's a destination call, otherwise use bridge receiver + encodedDstAddress = abi.encodePacked( + _bridgeData.hasDestinationCall + ? _chainflipData.dstCallReceiver + : _bridgeData.receiver + ); } // Validate destination call flag matches message presence diff --git a/src/Periphery/ReceiverChainflip.sol b/src/Periphery/ReceiverChainflip.sol index e5935e62c..cd2fee77b 100644 --- a/src/Periphery/ReceiverChainflip.sol +++ b/src/Periphery/ReceiverChainflip.sol @@ -103,42 +103,52 @@ contract ReceiverChainflip is ILiFi, WithdrawablePeriphery { address payable receiver, uint256 amount ) private { - // Convert Chainflip's native token address to LibAsset.NATIVE_ASSETID + // Group address conversion and store in memory to avoid multiple storage reads address actualAssetId = assetId == CHAINFLIP_NATIVE_ADDRESS ? LibAsset.NATIVE_ASSETID : assetId; + bool isNative = actualAssetId == LibAsset.NATIVE_ASSETID; - // Don't need approval for native token - if (actualAssetId != LibAsset.NATIVE_ASSETID) { + if (!isNative) { + // ERC20 token operations actualAssetId.safeApproveWithRetry(address(executor), amount); - } - - try - executor.swapAndCompleteBridgeTokens{ - value: actualAssetId == LibAsset.NATIVE_ASSETID ? amount : 0 - }(_transactionId, _swapData, actualAssetId, receiver) - {} catch { - // send the bridged (and unswapped) funds to receiver address - if (actualAssetId == LibAsset.NATIVE_ASSETID) { - // Handle native token using safeTransferETH - receiver.safeTransferETH(amount); - } else { - // Handle ERC20 token + try + executor.swapAndCompleteBridgeTokens( + _transactionId, + _swapData, + actualAssetId, + receiver + ) + {} catch { actualAssetId.safeTransfer(receiver, amount); + emit LiFiTransferRecovered( + _transactionId, + actualAssetId, + receiver, + amount, + block.timestamp + ); } - - emit LiFiTransferRecovered( - _transactionId, - actualAssetId, - receiver, - amount, - block.timestamp - ); - } - - // Only reset approval for non-native tokens - if (actualAssetId != LibAsset.NATIVE_ASSETID) { actualAssetId.safeApprove(address(executor), 0); + } else { + // Native token operations + try + executor.swapAndCompleteBridgeTokens{ value: amount }( + _transactionId, + _swapData, + actualAssetId, + receiver + ) + {} catch { + receiver.safeTransferETH(amount); + emit LiFiTransferRecovered( + _transactionId, + actualAssetId, + receiver, + amount, + block.timestamp + ); + } } } diff --git a/test/solidity/Facets/ChainflipFacet.t.sol b/test/solidity/Facets/ChainflipFacet.t.sol index 902267bfe..94c4b995b 100644 --- a/test/solidity/Facets/ChainflipFacet.t.sol +++ b/test/solidity/Facets/ChainflipFacet.t.sol @@ -81,8 +81,9 @@ contract ChainflipFacetTest is TestBaseFacet { validChainflipData = ChainflipFacet.ChainflipData({ nonEVMReceiver: bytes32(0), // Default to empty for EVM addresses dstToken: 7, - message: "", // Add new field - gasAmount: 0, // Add new field + dstCallReceiver: address(0), + message: "", + gasAmount: 0, cfParameters: "" }); } @@ -133,6 +134,7 @@ contract ChainflipFacetTest is TestBaseFacet { "EoW7FWTdPdZKpd3WAhH98c2HMGHsdh5yhzzEtk1u68Bb" ) ), // Example Solana address + dstCallReceiver: address(0), message: "", gasAmount: 0, cfParameters: "" @@ -167,6 +169,7 @@ contract ChainflipFacetTest is TestBaseFacet { nonEVMReceiver: bytes32( abi.encodePacked("bc1q6l08rtj6j907r2een0jqs6l7qnruwyxfshmf8a") ), // Example Bitcoin address + dstCallReceiver: address(0), message: "", gasAmount: 0, cfParameters: "" @@ -205,6 +208,7 @@ contract ChainflipFacetTest is TestBaseFacet { validChainflipData = ChainflipFacet.ChainflipData({ dstToken: 3, // USDC on Ethereum nonEVMReceiver: bytes32(0), // Not needed for EVM chains + dstCallReceiver: address(0), message: "", gasAmount: 0, cfParameters: "" @@ -265,6 +269,7 @@ contract ChainflipFacetTest is TestBaseFacet { validChainflipData = ChainflipFacet.ChainflipData({ dstToken: 7, nonEVMReceiver: bytes32(0), + dstCallReceiver: address(0x123), // Example mock address message: message, gasAmount: 100000, cfParameters: "" @@ -296,6 +301,7 @@ contract ChainflipFacetTest is TestBaseFacet { validChainflipData = ChainflipFacet.ChainflipData({ dstToken: 7, nonEVMReceiver: bytes32(0), + dstCallReceiver: address(0), message: "", gasAmount: 0, cfParameters: "" @@ -381,6 +387,7 @@ contract ChainflipFacetTest is TestBaseFacet { validChainflipData = ChainflipFacet.ChainflipData({ dstToken: 6, nonEVMReceiver: bytes32(0), // Empty address should fail + dstCallReceiver: address(0), message: "", gasAmount: 0, cfParameters: "" @@ -425,6 +432,7 @@ contract ChainflipFacetTest is TestBaseFacet { validChainflipData = ChainflipFacet.ChainflipData({ dstToken: 7, // USDC on Arbitrum nonEVMReceiver: bytes32(0), // Not needed for EVM chains + dstCallReceiver: address(0x123), message: message, // Use message here gasAmount: 0, // Add gas amount cfParameters: "" // Empty parameters @@ -456,6 +464,7 @@ contract ChainflipFacetTest is TestBaseFacet { nonEVMReceiver: bytes32( abi.encodePacked("bc1q6l08rtj6j907r2een0jqs6l7qnruwyxfshmf8a") ), // Set nonEVMReceiver even though using EVM address + dstCallReceiver: address(0), message: "", gasAmount: 0, cfParameters: "" From 094603830220c5f16f5c1806d5c59dfae0476075 Mon Sep 17 00:00:00 2001 From: Ed Zynda Date: Fri, 21 Feb 2025 14:00:20 +0300 Subject: [PATCH 40/55] make destination calls more type safe --- docs/ChainflipFacet.md | 2 +- script/demoScripts/demoChainflip.ts | 75 ++------- src/Facets/ChainflipFacet.sol | 30 ++-- test/solidity/Facets/ChainflipFacet.t.sol | 192 ++++++++++------------ 4 files changed, 124 insertions(+), 175 deletions(-) diff --git a/docs/ChainflipFacet.md b/docs/ChainflipFacet.md index 3c05157c1..68c5192b8 100644 --- a/docs/ChainflipFacet.md +++ b/docs/ChainflipFacet.md @@ -26,7 +26,7 @@ struct ChainflipData { bytes32 nonEVMReceiver; // Destination address for non-EVM chains (Solana, Bitcoin) uint32 dstToken; // Chainflip specific token identifier on the destination chain address dstCallReceiver; // Receiver contract address used for destination calls. Ignored if there is no destination call - bytes message; // Message that is passed to the destination address for cross-chain messaging + LibSwap.SwapData[] dstCallSwapData; // Swap data to be used in destination calls. Ignored if no destination call uint256 gasAmount; // Gas budget for the call on the destination chain bytes cfParameters; // Additional parameters for future features } diff --git a/script/demoScripts/demoChainflip.ts b/script/demoScripts/demoChainflip.ts index a81872fa9..49a4fb62c 100644 --- a/script/demoScripts/demoChainflip.ts +++ b/script/demoScripts/demoChainflip.ts @@ -117,57 +117,6 @@ async function executeWithDestinationCall( ) } -/** - * Creates a message for cross-chain execution on the destination chain - * This message will be used to swap received ETH for USDC using Uniswap - * @param transactionId Unique identifier for the transaction - * @param finalReceiver Address that will receive the swapped tokens - * @param totalETHAmount Total amount of ETH being bridged - * @param gasAmount Amount of ETH reserved for gas on destination chain - * @returns Encoded message containing swap instructions - */ -async function createDestinationCallMessage( - transactionId: string, - finalReceiver: string, - totalETHAmount: bigint, - gasAmount: bigint -): Promise { - // Calculate exact ETH amount for swap (total - gas) - // Reserve some ETH for gas fees on destination chain - const swapETHAmount = totalETHAmount - gasAmount - - // Prepare swap parameters for ETH -> USDC on Ethereum mainnet - const swapData = await getUniswapDataExactETHToERC20( - ADDRESS_UNISWAP_ETH, - 1, // Mainnet chainId - swapETHAmount, - ADDRESS_USDC_ETH, - finalReceiver, - false - ) - - // Encode the message according to the ReceiverChainflip contract's expected format - return encodeAbiParameters( - [ - { type: 'bytes32' }, // transactionId - { - type: 'tuple[]', - components: [ - { type: 'address', name: 'callTo' }, - { type: 'address', name: 'approveTo' }, - { type: 'address', name: 'sendingAssetId' }, - { type: 'address', name: 'receivingAssetId' }, - { type: 'uint256', name: 'fromAmount' }, - { type: 'bytes', name: 'callData' }, - { type: 'bool', name: 'requiresDeposit' }, - ], - }, // swapData - { type: 'address' }, // receiver - ], - [transactionId, [swapData], finalReceiver] - ) -} - async function main() { const withSwap = process.argv.includes('--with-swap') const withDestinationCall = process.argv.includes('--with-destination-call') @@ -241,22 +190,26 @@ async function main() { hasDestinationCall: withDestinationCall, } - // Prepare destination call data if needed - const destinationCallMessage = withDestinationCall - ? await createDestinationCallMessage( - bridgeData.transactionId, - signerAddress, - totalAmount, - gasAmount - ) - : '' + // Prepare destination swap data if needed + const dstSwapData = withDestinationCall + ? [ + await getUniswapDataExactETHToERC20( + ADDRESS_UNISWAP_ETH, + 1, // Mainnet chainId + totalAmount - gasAmount, + ADDRESS_USDC_ETH, + signerAddress, + false + ), + ] + : [] const chainflipData: ChainflipFacet.ChainflipDataStruct = { nonEVMReceiver: '0x0000000000000000000000000000000000000000000000000000000000000000', dstToken: withDestinationCall ? 1 : 3, // 1 for ETH, 3 for USDC on ETH dstCallReceiver: RECEIVER_CHAINFLIP, - message: destinationCallMessage, + dstCallSwapData: dstSwapData, gasAmount: withDestinationCall ? gasAmount : 0n, cfParameters: '', } diff --git a/src/Facets/ChainflipFacet.sol b/src/Facets/ChainflipFacet.sol index fbb13c357..c5a47c493 100644 --- a/src/Facets/ChainflipFacet.sol +++ b/src/Facets/ChainflipFacet.sol @@ -41,17 +41,18 @@ contract ChainflipFacet is ILiFi, ReentrancyGuard, SwapperV2, Validatable { /// Types /// - /// @dev Parameters specific to Chainflip bridge + /// @notice Parameters specific to Chainflip bridge operations /// @param nonEVMReceiver Destination address for non-EVM chains (Solana, Bitcoin) /// @param dstToken Chainflip specific token identifier on the destination chain - /// @param message Message that is passed to the destination address for cross-chain messaging + /// @param dstCallReceiver Receiver contract address used for destination calls. Ignored if no destination call + /// @param dstCallSwapData Swap data to be used in destination calls. Ignored if no destination call /// @param gasAmount Gas budget for the call on the destination chain - /// @param cfParameters Additional metadata for future features + /// @param cfParameters Additional parameters for future features struct ChainflipData { bytes32 nonEVMReceiver; uint32 dstToken; address dstCallReceiver; - bytes message; + LibSwap.SwapData[] dstCallSwapData; uint256 gasAmount; bytes cfParameters; } @@ -148,11 +149,20 @@ contract ChainflipFacet is ILiFi, ReentrancyGuard, SwapperV2, Validatable { ); } + // Initialize message variable at function scope level + bytes memory message; + // Validate destination call flag matches message presence - if ( - _bridgeData.hasDestinationCall != - (_chainflipData.message.length > 0) - ) { + if (_bridgeData.hasDestinationCall) { + if (_chainflipData.dstCallSwapData.length == 0) { + revert InformationMismatch(); + } + message = abi.encode( + _bridgeData.transactionId, + _chainflipData.dstCallSwapData, + _bridgeData.receiver + ); + } else if (_chainflipData.dstCallSwapData.length > 0) { revert InformationMismatch(); } @@ -165,7 +175,7 @@ contract ChainflipFacet is ILiFi, ReentrancyGuard, SwapperV2, Validatable { dstChain, encodedDstAddress, _chainflipData.dstToken, - _chainflipData.message, + message, _chainflipData.gasAmount, _chainflipData.cfParameters ); @@ -194,7 +204,7 @@ contract ChainflipFacet is ILiFi, ReentrancyGuard, SwapperV2, Validatable { dstChain, encodedDstAddress, _chainflipData.dstToken, - _chainflipData.message, + message, _chainflipData.gasAmount, IERC20(_bridgeData.sendingAssetId), _bridgeData.minAmount, diff --git a/test/solidity/Facets/ChainflipFacet.t.sol b/test/solidity/Facets/ChainflipFacet.t.sol index 94c4b995b..5709229e0 100644 --- a/test/solidity/Facets/ChainflipFacet.t.sol +++ b/test/solidity/Facets/ChainflipFacet.t.sol @@ -27,6 +27,7 @@ contract ChainflipFacetTest is TestBaseFacet { ChainflipFacet.ChainflipData internal validChainflipData; TestChainflipFacet internal chainflipFacet; address internal CHAINFLIP_VAULT; + LibSwap.SwapData[] internal dstSwapData; uint256 internal constant CHAIN_ID_ETHEREUM = 1; uint256 internal constant CHAIN_ID_ARBITRUM = 42161; @@ -77,15 +78,8 @@ contract ChainflipFacetTest is TestBaseFacet { bridgeData.bridge = "chainflip"; bridgeData.destinationChainId = 42161; // Arbitrum chain ID - // produce valid ChainflipData - validChainflipData = ChainflipFacet.ChainflipData({ - nonEVMReceiver: bytes32(0), // Default to empty for EVM addresses - dstToken: 7, - dstCallReceiver: address(0), - message: "", - gasAmount: 0, - cfParameters: "" - }); + // Most properties are unused for normal bridging + validChainflipData.dstToken = 7; } function initiateBridgeTxWithFacet(bool isNative) internal override { @@ -127,18 +121,10 @@ contract ChainflipFacetTest is TestBaseFacet { { bridgeData.receiver = LibAsset.NON_EVM_ADDRESS; bridgeData.destinationChainId = CHAIN_ID_SOLANA; - validChainflipData = ChainflipFacet.ChainflipData({ - dstToken: 6, - nonEVMReceiver: bytes32( - abi.encodePacked( - "EoW7FWTdPdZKpd3WAhH98c2HMGHsdh5yhzzEtk1u68Bb" - ) - ), // Example Solana address - dstCallReceiver: address(0), - message: "", - gasAmount: 0, - cfParameters: "" - }); + validChainflipData.dstToken = 6; + validChainflipData.nonEVMReceiver = bytes32( + abi.encodePacked("EoW7FWTdPdZKpd3WAhH98c2HMGHsdh5yhzzEtk1u68Bb") + ); vm.startPrank(USER_SENDER); @@ -164,16 +150,10 @@ contract ChainflipFacetTest is TestBaseFacet { { bridgeData.receiver = LibAsset.NON_EVM_ADDRESS; bridgeData.destinationChainId = CHAIN_ID_BITCOIN; - validChainflipData = ChainflipFacet.ChainflipData({ - dstToken: 6, - nonEVMReceiver: bytes32( - abi.encodePacked("bc1q6l08rtj6j907r2een0jqs6l7qnruwyxfshmf8a") - ), // Example Bitcoin address - dstCallReceiver: address(0), - message: "", - gasAmount: 0, - cfParameters: "" - }); + validChainflipData.dstToken = 6; + validChainflipData.nonEVMReceiver = bytes32( + abi.encodePacked("bc1q6l08rtj6j907r2een0jqs6l7qnruwyxfshmf8a") + ); vm.startPrank(USER_SENDER); @@ -205,14 +185,7 @@ contract ChainflipFacetTest is TestBaseFacet { // Set destination to Ethereum bridgeData.destinationChainId = CHAIN_ID_ETHEREUM; - validChainflipData = ChainflipFacet.ChainflipData({ - dstToken: 3, // USDC on Ethereum - nonEVMReceiver: bytes32(0), // Not needed for EVM chains - dstCallReceiver: address(0), - message: "", - gasAmount: 0, - cfParameters: "" - }); + validChainflipData.dstToken = 3; // USDC on Ethereum vm.startPrank(USER_SENDER); @@ -250,30 +223,28 @@ contract ChainflipFacetTest is TestBaseFacet { -int256(defaultNativeAmount) ) { - // Set destination to Arbitrum where our receiver contract is + delete dstSwapData; + dstSwapData.push( + LibSwap.SwapData({ + callTo: address(0x123), + approveTo: address(0x123), + sendingAssetId: address(0), + receivingAssetId: address(0), + fromAmount: 0, + callData: "0x123456", + requiresDeposit: false + }) + ); + bridgeData.destinationChainId = CHAIN_ID_ARBITRUM; bridgeData.hasDestinationCall = true; bridgeData.sendingAssetId = address(0); bridgeData.minAmount = defaultNativeAmount; - // Create swap data for the destination chain - LibSwap.SwapData[] memory destSwapData = new LibSwap.SwapData[](0); - - // Encode the message for the receiver contract - bytes memory message = abi.encode( - bridgeData.transactionId, - destSwapData, - USER_RECEIVER - ); - - validChainflipData = ChainflipFacet.ChainflipData({ - dstToken: 7, - nonEVMReceiver: bytes32(0), - dstCallReceiver: address(0x123), // Example mock address - message: message, - gasAmount: 100000, - cfParameters: "" - }); + validChainflipData.dstToken = 7; + validChainflipData.dstCallReceiver = address(0x123); + validChainflipData.dstCallSwapData = dstSwapData; + validChainflipData.gasAmount = 100000; vm.startPrank(USER_SENDER); @@ -298,14 +269,7 @@ contract ChainflipFacetTest is TestBaseFacet { bridgeData.sendingAssetId = address(0); bridgeData.minAmount = defaultNativeAmount; - validChainflipData = ChainflipFacet.ChainflipData({ - dstToken: 7, - nonEVMReceiver: bytes32(0), - dstCallReceiver: address(0), - message: "", - gasAmount: 0, - cfParameters: "" - }); + validChainflipData.dstToken = 7; vm.startPrank(USER_SENDER); @@ -320,7 +284,7 @@ contract ChainflipFacetTest is TestBaseFacet { function testRevert_WhenDestinationCallFlagMismatchesMessage() public { // Case 1: hasDestinationCall is true but message is empty bridgeData.hasDestinationCall = true; - validChainflipData.message = ""; + validChainflipData.dstCallSwapData = dstSwapData; vm.startPrank(USER_SENDER); usdc.approve(_facetTestContractAddress, bridgeData.minAmount); @@ -330,7 +294,19 @@ contract ChainflipFacetTest is TestBaseFacet { // Case 2: hasDestinationCall is false but message is not empty bridgeData.hasDestinationCall = false; - validChainflipData.message = "0x123456"; + delete dstSwapData; + dstSwapData.push( + LibSwap.SwapData({ + callTo: address(0x123), + approveTo: address(0x123), + sendingAssetId: address(0), + receivingAssetId: address(0), + fromAmount: 0, + callData: "0x123456", + requiresDeposit: false + }) + ); + validChainflipData.dstCallSwapData = dstSwapData; vm.expectRevert(InformationMismatch.selector); initiateBridgeTxWithFacet(false); @@ -384,14 +360,8 @@ contract ChainflipFacetTest is TestBaseFacet { function testRevert_WhenUsingEmptyNonEVMAddress() public { bridgeData.receiver = LibAsset.NON_EVM_ADDRESS; bridgeData.destinationChainId = CHAIN_ID_SOLANA; - validChainflipData = ChainflipFacet.ChainflipData({ - dstToken: 6, - nonEVMReceiver: bytes32(0), // Empty address should fail - dstCallReceiver: address(0), - message: "", - gasAmount: 0, - cfParameters: "" - }); + validChainflipData.dstToken = 6; + validChainflipData.nonEVMReceiver = bytes32(0); // Empty address should fail vm.startPrank(USER_SENDER); @@ -415,28 +385,26 @@ contract ChainflipFacetTest is TestBaseFacet { assertBalanceChange(ADDRESS_DAI, USER_SENDER, 0) assertBalanceChange(ADDRESS_DAI, USER_RECEIVER, 0) { - // Set destination to Arbitrum where our receiver contract is bridgeData.destinationChainId = CHAIN_ID_ARBITRUM; bridgeData.hasDestinationCall = true; - // Create swap data for the destination chain - LibSwap.SwapData[] memory destSwapData = new LibSwap.SwapData[](0); - - // Encode the message for the receiver contract - bytes memory message = abi.encode( - bridgeData.transactionId, - destSwapData, - USER_RECEIVER // Final receiver of the tokens + delete dstSwapData; + dstSwapData.push( + LibSwap.SwapData({ + callTo: address(0x123), + approveTo: address(0x123), + sendingAssetId: address(0), + receivingAssetId: address(0), + fromAmount: 0, + callData: "0x123456", + requiresDeposit: false + }) ); - validChainflipData = ChainflipFacet.ChainflipData({ - dstToken: 7, // USDC on Arbitrum - nonEVMReceiver: bytes32(0), // Not needed for EVM chains - dstCallReceiver: address(0x123), - message: message, // Use message here - gasAmount: 0, // Add gas amount - cfParameters: "" // Empty parameters - }); + validChainflipData.dstToken = 7; + validChainflipData.dstCallReceiver = address(0x123); + validChainflipData.dstCallSwapData = dstSwapData; + validChainflipData.gasAmount = 100000; vm.startPrank(USER_SENDER); @@ -459,16 +427,10 @@ contract ChainflipFacetTest is TestBaseFacet { // Try to use nonEVMReceiver with an EVM address bridgeData.receiver = USER_RECEIVER; // Use EVM address bridgeData.destinationChainId = CHAIN_ID_ETHEREUM; - validChainflipData = ChainflipFacet.ChainflipData({ - dstToken: 6, - nonEVMReceiver: bytes32( - abi.encodePacked("bc1q6l08rtj6j907r2een0jqs6l7qnruwyxfshmf8a") - ), // Set nonEVMReceiver even though using EVM address - dstCallReceiver: address(0), - message: "", - gasAmount: 0, - cfParameters: "" - }); + validChainflipData.dstToken = 6; + validChainflipData.nonEVMReceiver = bytes32( + abi.encodePacked("bc1q6l08rtj6j907r2een0jqs6l7qnruwyxfshmf8a") + ); vm.startPrank(USER_SENDER); usdc.approve(_facetTestContractAddress, bridgeData.minAmount); @@ -477,4 +439,28 @@ contract ChainflipFacetTest is TestBaseFacet { initiateBridgeTxWithFacet(false); vm.stopPrank(); } + + function testRevert_WhenDestinationCallTrueButSwapDataEmpty() public { + // Set up bridge data with destination call flag true + bridgeData.destinationChainId = CHAIN_ID_ARBITRUM; + bridgeData.hasDestinationCall = true; + bridgeData.sendingAssetId = ADDRESS_USDC; + + // Set up chainflip data but leave dstCallSwapData empty + validChainflipData.dstToken = 7; + validChainflipData.dstCallReceiver = address(0x123); + validChainflipData.gasAmount = 100000; + // Deliberately not setting dstCallSwapData + + vm.startPrank(USER_SENDER); + + // Approve spending + usdc.approve(_facetTestContractAddress, bridgeData.minAmount); + + // Expect revert with InformationMismatch + vm.expectRevert(InformationMismatch.selector); + initiateBridgeTxWithFacet(false); + + vm.stopPrank(); + } } From 0cc3bd974f979eb26c5ef404770e6165b9afa360 Mon Sep 17 00:00:00 2001 From: Ed Zynda Date: Fri, 21 Feb 2025 16:00:30 +0300 Subject: [PATCH 41/55] add docs for ReceiverChainflip --- docs/ReceiverChainflip.md | 45 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 docs/ReceiverChainflip.md diff --git a/docs/ReceiverChainflip.md b/docs/ReceiverChainflip.md new file mode 100644 index 000000000..0ce4be35f --- /dev/null +++ b/docs/ReceiverChainflip.md @@ -0,0 +1,45 @@ +# ReceiverChainflip + +## Description + +Periphery contract used for receiving cross-chain transactions via Chainflip and executing arbitrary destination calls. Inherits from WithdrawablePeriphery to allow the owner to recover stuck assets. + +## Setup + +The contract is initialized with: +- owner: Address that can withdraw stuck funds +- executor: Contract used to perform swaps +- chainflipVault: Authorized Chainflip vault that can call this contract + +## How To Use + +The contract has one method which will be called by the Chainflip vault: + +```solidity + /// @notice Receiver function for Chainflip cross-chain messages + /// @dev This function can only be called by the Chainflip Vault on this network + /// @param srcChain The source chain according to Chainflip's nomenclature + /// @param srcAddress The source address on the source chain + /// @param message The message sent from the source chain + /// @param token The address of the token received + /// @param amount The amount of tokens received + function cfReceive( + uint32 srcChain, + bytes calldata srcAddress, + bytes calldata message, + address token, + uint256 amount + ) external payable +``` + +The message parameter contains: +- transactionId: bytes32 identifier for the cross-chain transaction +- swapData: Array of LibSwap.SwapData for executing destination swaps +- receiver: Address that will receive the tokens after any swaps + +## Token Handling + +- For ERC20 tokens, the contract approves the executor to spend the received tokens before executing swaps +- For native tokens (ETH), the contract forwards the received value directly to the executor +- All approvals are reset to 0 after the operation completes +- If destination swaps fail, the original bridged tokens (ERC20 or native) will be sent directly to the receiver address From 669b017f32024db33b47e1a39e67fbc6088e1a14 Mon Sep 17 00:00:00 2001 From: Ed Zynda Date: Fri, 21 Feb 2025 16:03:15 +0300 Subject: [PATCH 42/55] update base readme --- docs/README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/README.md b/docs/README.md index 404deb64f..1ede9f3f7 100644 --- a/docs/README.md +++ b/docs/README.md @@ -8,6 +8,7 @@ - [CalldataVerification Facet](./CalldataVerificationFacet.md) - [CBridge Facet](./CBridgeFacet.md) - [Celer Circle Bridge Facet](./CelerCircleBridgeFacet.md) +- [Chainflip Facet](./ChainflipFacet.md) - [Circle Bridge Facet](./CircleBridgeFacet.md) - [DeBridge DLN Facet](/docs/DeBridgeDlnFacet.md) - [DEX Manager Facet](./DexManagerFacet.md) @@ -58,5 +59,6 @@ - [LiFuelFeeCollector](./LiFuelFeeCollector.md) - [Receiver](./Receiver.md) - [ReceiverStargateV2](./ReceiverStargateV2.md) +- [ReceiverChainflip](./ReceiverChainflip.md) - [RelayerCelerIM](./RelayerCelerIM.md) - [Permit2Proxy](./Permit2Proxy.md) From db71d41392fbc7778e3c40f20ba3b13b83cea690 Mon Sep 17 00:00:00 2001 From: Ed Zynda Date: Fri, 21 Feb 2025 16:10:34 +0300 Subject: [PATCH 43/55] update config structure --- config/chainflip.json | 8 +++++--- script/deploy/facets/DeployChainflipFacet.s.sol | 2 +- test/solidity/Facets/ChainflipFacet.t.sol | 2 +- test/solidity/Periphery/ReceiverChainflip.t.sol | 2 +- 4 files changed, 8 insertions(+), 6 deletions(-) diff --git a/config/chainflip.json b/config/chainflip.json index 1235df3ae..c589d50ea 100644 --- a/config/chainflip.json +++ b/config/chainflip.json @@ -1,6 +1,8 @@ { - "chainflipVault": { - "mainnet": "0xF5e10380213880111522dd0efD3dbb45b9f62Bcc", - "arbitrum": "0x79001a5e762f3bEFC8e5871b42F6734e00498920" + "mainnet": { + "chainflipVault": "0xF5e10380213880111522dd0efD3dbb45b9f62Bcc" + }, + "arbitrum": { + "chainflipVault": "0x79001a5e762f3bEFC8e5871b42F6734e00498920" } } diff --git a/script/deploy/facets/DeployChainflipFacet.s.sol b/script/deploy/facets/DeployChainflipFacet.s.sol index 9353420f6..ee45e1a3c 100644 --- a/script/deploy/facets/DeployChainflipFacet.s.sol +++ b/script/deploy/facets/DeployChainflipFacet.s.sol @@ -26,7 +26,7 @@ contract DeployScript is DeployScriptBase { // Read the Chainflip vault address from config address chainflipVault = _getConfigContractAddress( path, - string.concat(".chainflipVault.", network) + string.concat(".", network, ".chainflipVault") ); return abi.encode(chainflipVault); diff --git a/test/solidity/Facets/ChainflipFacet.t.sol b/test/solidity/Facets/ChainflipFacet.t.sol index 5709229e0..9e7369420 100644 --- a/test/solidity/Facets/ChainflipFacet.t.sol +++ b/test/solidity/Facets/ChainflipFacet.t.sol @@ -41,7 +41,7 @@ contract ChainflipFacetTest is TestBaseFacet { // Read chainflip vault address from config using the new helper CHAINFLIP_VAULT = getConfigAddressFromPath( "chainflip.json", - ".chainflipVault.mainnet" + ".mainnet.chainflipVault" ); vm.label(CHAINFLIP_VAULT, "Chainflip Vault"); console.log("Chainflip Vault Address:", CHAINFLIP_VAULT); diff --git a/test/solidity/Periphery/ReceiverChainflip.t.sol b/test/solidity/Periphery/ReceiverChainflip.t.sol index 6a4b934a9..113d86e70 100644 --- a/test/solidity/Periphery/ReceiverChainflip.t.sol +++ b/test/solidity/Periphery/ReceiverChainflip.t.sol @@ -33,7 +33,7 @@ contract ReceiverChainflipTest is TestBase { chainflipVault = getConfigAddressFromPath( "chainflip.json", - ".chainflipVault.mainnet" + ".mainnet.chainflipVault" ); vm.label(chainflipVault, "Chainflip Vault"); From f4bce4500c514634d9dd853774e8dcfd8f2e0c49 Mon Sep 17 00:00:00 2001 From: Ed Zynda Date: Fri, 21 Feb 2025 16:53:25 +0300 Subject: [PATCH 44/55] add return to success block so coverage is calculated correctly. --- src/Periphery/ReceiverChainflip.sol | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/Periphery/ReceiverChainflip.sol b/src/Periphery/ReceiverChainflip.sol index cd2fee77b..f4378be65 100644 --- a/src/Periphery/ReceiverChainflip.sol +++ b/src/Periphery/ReceiverChainflip.sol @@ -119,7 +119,9 @@ contract ReceiverChainflip is ILiFi, WithdrawablePeriphery { actualAssetId, receiver ) - {} catch { + { + return; + } catch { actualAssetId.safeTransfer(receiver, amount); emit LiFiTransferRecovered( _transactionId, @@ -139,7 +141,9 @@ contract ReceiverChainflip is ILiFi, WithdrawablePeriphery { actualAssetId, receiver ) - {} catch { + { + return; + } catch { receiver.safeTransferETH(amount); emit LiFiTransferRecovered( _transactionId, From 0778cc04a8df00e12235c918f04411b1a7dd0fb1 Mon Sep 17 00:00:00 2001 From: Ed Zynda Date: Mon, 24 Feb 2025 11:30:19 +0300 Subject: [PATCH 45/55] validate constructor args --- src/Facets/ChainflipFacet.sol | 5 ++++- src/Periphery/ReceiverChainflip.sol | 8 ++++++++ test/solidity/Facets/ChainflipFacet.t.sol | 7 ++++++- .../solidity/Periphery/ReceiverChainflip.t.sol | 18 +++++++++++++++++- 4 files changed, 35 insertions(+), 3 deletions(-) diff --git a/src/Facets/ChainflipFacet.sol b/src/Facets/ChainflipFacet.sol index c5a47c493..c4eef4a5f 100644 --- a/src/Facets/ChainflipFacet.sol +++ b/src/Facets/ChainflipFacet.sol @@ -9,7 +9,7 @@ import { SwapperV2 } from "../Helpers/SwapperV2.sol"; import { Validatable } from "../Helpers/Validatable.sol"; import { IChainflipVault } from "../Interfaces/IChainflip.sol"; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import { InformationMismatch } from "../Errors/GenericErrors.sol"; +import { InformationMismatch, InvalidConfig } from "../Errors/GenericErrors.sol"; /// @title Chainflip Facet /// @author LI.FI (https://li.fi) @@ -62,6 +62,9 @@ contract ChainflipFacet is ILiFi, ReentrancyGuard, SwapperV2, Validatable { /// @notice Constructor for the contract. /// @param _chainflipVault Address of the Chainflip vault contract constructor(IChainflipVault _chainflipVault) { + if (address(_chainflipVault) == address(0)) { + revert InvalidConfig(); + } chainflipVault = _chainflipVault; } diff --git a/src/Periphery/ReceiverChainflip.sol b/src/Periphery/ReceiverChainflip.sol index f4378be65..3e1cb0def 100644 --- a/src/Periphery/ReceiverChainflip.sol +++ b/src/Periphery/ReceiverChainflip.sol @@ -8,6 +8,7 @@ import { IExecutor } from "../Interfaces/IExecutor.sol"; import { WithdrawablePeriphery } from "../Helpers/WithdrawablePeriphery.sol"; import { SafeTransferLib } from "solady/utils/SafeTransferLib.sol"; import { console2 } from "forge-std/console2.sol"; +import { InvalidConfig } from "../Errors/GenericErrors.sol"; /// @title ReceiverChainflip /// @author LI.FI (https://li.fi) @@ -49,6 +50,13 @@ contract ReceiverChainflip is ILiFi, WithdrawablePeriphery { address _executor, address _chainflipVault ) WithdrawablePeriphery(_owner) { + if ( + _owner == address(0) || + _executor == address(0) || + _chainflipVault == address(0) + ) { + revert InvalidConfig(); + } executor = IExecutor(_executor); chainflipVault = _chainflipVault; } diff --git a/test/solidity/Facets/ChainflipFacet.t.sol b/test/solidity/Facets/ChainflipFacet.t.sol index 9e7369420..4804e181e 100644 --- a/test/solidity/Facets/ChainflipFacet.t.sol +++ b/test/solidity/Facets/ChainflipFacet.t.sol @@ -6,7 +6,7 @@ import { ChainflipFacet } from "lifi/Facets/ChainflipFacet.sol"; import { IChainflipVault } from "lifi/Interfaces/IChainflip.sol"; import { LibAsset } from "lifi/Libraries/LibAsset.sol"; import { LibSwap } from "lifi/Libraries/LibSwap.sol"; -import { InformationMismatch, CannotBridgeToSameNetwork } from "lifi/Errors/GenericErrors.sol"; +import { InformationMismatch, CannotBridgeToSameNetwork, InvalidConfig } from "lifi/Errors/GenericErrors.sol"; // Stub ChainflipFacet Contract contract TestChainflipFacet is ChainflipFacet { @@ -82,6 +82,11 @@ contract ChainflipFacetTest is TestBaseFacet { validChainflipData.dstToken = 7; } + function testRevert_WhenConstructedWithZeroAddress() public { + vm.expectRevert(InvalidConfig.selector); + new TestChainflipFacet(address(0)); + } + function initiateBridgeTxWithFacet(bool isNative) internal override { if (isNative) { chainflipFacet.startBridgeTokensViaChainflip{ diff --git a/test/solidity/Periphery/ReceiverChainflip.t.sol b/test/solidity/Periphery/ReceiverChainflip.t.sol index 113d86e70..75290b09d 100644 --- a/test/solidity/Periphery/ReceiverChainflip.t.sol +++ b/test/solidity/Periphery/ReceiverChainflip.t.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.17; import { TestBase, ILiFi, LibSwap, console, ERC20, UniswapV2Router02 } from "../utils/TestBase.sol"; -import { ExternalCallFailed, UnAuthorized } from "src/Errors/GenericErrors.sol"; +import { ExternalCallFailed, UnAuthorized, InvalidConfig } from "src/Errors/GenericErrors.sol"; import { ReceiverChainflip } from "lifi/Periphery/ReceiverChainflip.sol"; import { LibAsset } from "lifi/Libraries/LibAsset.sol"; import { stdJson } from "forge-std/Script.sol"; @@ -27,6 +27,20 @@ contract ReceiverChainflipTest is TestBase { event ExecutorSet(address indexed executor); + function testRevert_WhenConstructedWithZeroAddresses() public { + // Test zero owner address + vm.expectRevert(InvalidConfig.selector); + new ReceiverChainflip(address(0), address(executor), chainflipVault); + + // Test zero executor address + vm.expectRevert(InvalidConfig.selector); + new ReceiverChainflip(address(this), address(0), chainflipVault); + + // Test zero chainflip vault address + vm.expectRevert(InvalidConfig.selector); + new ReceiverChainflip(address(this), address(executor), address(0)); + } + function setUp() public { customBlockNumberForForking = 18277082; initTestBase(); @@ -49,6 +63,8 @@ contract ReceiverChainflipTest is TestBase { vm.label(address(erc20Proxy), "ERC20Proxy"); } + // AI! add a test that makes sure we revert when constructing with zero address. Check each arg. Make sure to import InvalidConfig from GenericErrors + function test_ContractIsSetUpCorrectly() public { receiver = new ReceiverChainflip( address(this), From 8e7722c41a27f148bef9502e9df0b8f590dc9192 Mon Sep 17 00:00:00 2001 From: Ed Zynda Date: Mon, 24 Feb 2025 11:34:28 +0300 Subject: [PATCH 46/55] mark internal --- src/Periphery/ReceiverChainflip.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Periphery/ReceiverChainflip.sol b/src/Periphery/ReceiverChainflip.sol index 3e1cb0def..7ec5ecd85 100644 --- a/src/Periphery/ReceiverChainflip.sol +++ b/src/Periphery/ReceiverChainflip.sol @@ -25,7 +25,7 @@ contract ReceiverChainflip is ILiFi, WithdrawablePeriphery { /// @notice The Chainflip vault contract that is authorized to call this contract address public immutable chainflipVault; /// @notice Chainflip's native token address representation - address constant CHAINFLIP_NATIVE_ADDRESS = + address internal constant CHAINFLIP_NATIVE_ADDRESS = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; /// Modifiers /// From 851c91e068e316892ba6143c3344aa84b839169d Mon Sep 17 00:00:00 2001 From: Ed Zynda Date: Mon, 24 Feb 2025 11:45:10 +0300 Subject: [PATCH 47/55] fix --- .../Periphery/ReceiverChainflip.t.sol | 28 +++++++++---------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/test/solidity/Periphery/ReceiverChainflip.t.sol b/test/solidity/Periphery/ReceiverChainflip.t.sol index 75290b09d..d0791bbb3 100644 --- a/test/solidity/Periphery/ReceiverChainflip.t.sol +++ b/test/solidity/Periphery/ReceiverChainflip.t.sol @@ -27,20 +27,6 @@ contract ReceiverChainflipTest is TestBase { event ExecutorSet(address indexed executor); - function testRevert_WhenConstructedWithZeroAddresses() public { - // Test zero owner address - vm.expectRevert(InvalidConfig.selector); - new ReceiverChainflip(address(0), address(executor), chainflipVault); - - // Test zero executor address - vm.expectRevert(InvalidConfig.selector); - new ReceiverChainflip(address(this), address(0), chainflipVault); - - // Test zero chainflip vault address - vm.expectRevert(InvalidConfig.selector); - new ReceiverChainflip(address(this), address(executor), address(0)); - } - function setUp() public { customBlockNumberForForking = 18277082; initTestBase(); @@ -63,7 +49,19 @@ contract ReceiverChainflipTest is TestBase { vm.label(address(erc20Proxy), "ERC20Proxy"); } - // AI! add a test that makes sure we revert when constructing with zero address. Check each arg. Make sure to import InvalidConfig from GenericErrors + function testRevert_WhenConstructedWithZeroAddresses() public { + // Test zero owner address + vm.expectRevert(InvalidConfig.selector); + new ReceiverChainflip(address(0), address(executor), chainflipVault); + + // Test zero executor address + vm.expectRevert(InvalidConfig.selector); + new ReceiverChainflip(address(this), address(0), chainflipVault); + + // Test zero chainflip vault address + vm.expectRevert(InvalidConfig.selector); + new ReceiverChainflip(address(this), address(executor), address(0)); + } function test_ContractIsSetUpCorrectly() public { receiver = new ReceiverChainflip( From 31aefd81d86f2dbd3e55cde85150a742fa95bf0b Mon Sep 17 00:00:00 2001 From: Ed Zynda Date: Mon, 24 Feb 2025 12:36:01 +0300 Subject: [PATCH 48/55] remove console --- src/Periphery/ReceiverChainflip.sol | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Periphery/ReceiverChainflip.sol b/src/Periphery/ReceiverChainflip.sol index 7ec5ecd85..d67daafcd 100644 --- a/src/Periphery/ReceiverChainflip.sol +++ b/src/Periphery/ReceiverChainflip.sol @@ -7,7 +7,6 @@ import { ILiFi } from "../Interfaces/ILiFi.sol"; import { IExecutor } from "../Interfaces/IExecutor.sol"; import { WithdrawablePeriphery } from "../Helpers/WithdrawablePeriphery.sol"; import { SafeTransferLib } from "solady/utils/SafeTransferLib.sol"; -import { console2 } from "forge-std/console2.sol"; import { InvalidConfig } from "../Errors/GenericErrors.sol"; /// @title ReceiverChainflip From 2727c695b3ef484b923d951a21da6a557e8b20bf Mon Sep 17 00:00:00 2001 From: Ed Zynda Date: Tue, 25 Feb 2025 16:12:45 +0300 Subject: [PATCH 49/55] validate address --- src/Facets/ChainflipFacet.sol | 46 ++++++++++++----------- test/solidity/Facets/ChainflipFacet.t.sol | 42 ++++++++++++++++++++- 2 files changed, 66 insertions(+), 22 deletions(-) diff --git a/src/Facets/ChainflipFacet.sol b/src/Facets/ChainflipFacet.sol index c4eef4a5f..bad58bd42 100644 --- a/src/Facets/ChainflipFacet.sol +++ b/src/Facets/ChainflipFacet.sol @@ -9,7 +9,7 @@ import { SwapperV2 } from "../Helpers/SwapperV2.sol"; import { Validatable } from "../Helpers/Validatable.sol"; import { IChainflipVault } from "../Interfaces/IChainflip.sol"; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import { InformationMismatch, InvalidConfig } from "../Errors/GenericErrors.sol"; +import { InformationMismatch, InvalidConfig, InvalidReceiver } from "../Errors/GenericErrors.sol"; /// @title Chainflip Facet /// @author LI.FI (https://li.fi) @@ -128,6 +128,23 @@ contract ChainflipFacet is ILiFi, ReentrancyGuard, SwapperV2, Validatable { ) internal { uint32 dstChain = _getChainflipChainId(_bridgeData.destinationChainId); + // Initialize message variable at function scope level + bytes memory message; + + // Validate destination call flag matches message presence first + if (_bridgeData.hasDestinationCall) { + if (_chainflipData.dstCallSwapData.length == 0) { + revert InformationMismatch(); + } + message = abi.encode( + _bridgeData.transactionId, + _chainflipData.dstCallSwapData, + _bridgeData.receiver + ); + } else if (_chainflipData.dstCallSwapData.length > 0) { + revert InformationMismatch(); + } + // Handle address encoding based on destination chain type bytes memory encodedDstAddress; if (_bridgeData.receiver == LibAsset.NON_EVM_ADDRESS) { @@ -145,28 +162,15 @@ contract ChainflipFacet is ILiFi, ReentrancyGuard, SwapperV2, Validatable { ); } else { // For EVM chains, use dstCallReceiver if there's a destination call, otherwise use bridge receiver - encodedDstAddress = abi.encodePacked( - _bridgeData.hasDestinationCall - ? _chainflipData.dstCallReceiver - : _bridgeData.receiver - ); - } - - // Initialize message variable at function scope level - bytes memory message; + address receiverAddress = _bridgeData.hasDestinationCall + ? _chainflipData.dstCallReceiver + : _bridgeData.receiver; - // Validate destination call flag matches message presence - if (_bridgeData.hasDestinationCall) { - if (_chainflipData.dstCallSwapData.length == 0) { - revert InformationMismatch(); + if (receiverAddress == address(0)) { + revert InvalidReceiver(); } - message = abi.encode( - _bridgeData.transactionId, - _chainflipData.dstCallSwapData, - _bridgeData.receiver - ); - } else if (_chainflipData.dstCallSwapData.length > 0) { - revert InformationMismatch(); + + encodedDstAddress = abi.encodePacked(receiverAddress); } // Handle native token case with or without destination call diff --git a/test/solidity/Facets/ChainflipFacet.t.sol b/test/solidity/Facets/ChainflipFacet.t.sol index 4804e181e..01bd4b61b 100644 --- a/test/solidity/Facets/ChainflipFacet.t.sol +++ b/test/solidity/Facets/ChainflipFacet.t.sol @@ -6,7 +6,7 @@ import { ChainflipFacet } from "lifi/Facets/ChainflipFacet.sol"; import { IChainflipVault } from "lifi/Interfaces/IChainflip.sol"; import { LibAsset } from "lifi/Libraries/LibAsset.sol"; import { LibSwap } from "lifi/Libraries/LibSwap.sol"; -import { InformationMismatch, CannotBridgeToSameNetwork, InvalidConfig } from "lifi/Errors/GenericErrors.sol"; +import { InformationMismatch, CannotBridgeToSameNetwork, InvalidConfig, InvalidReceiver } from "lifi/Errors/GenericErrors.sol"; // Stub ChainflipFacet Contract contract TestChainflipFacet is ChainflipFacet { @@ -290,6 +290,7 @@ contract ChainflipFacetTest is TestBaseFacet { // Case 1: hasDestinationCall is true but message is empty bridgeData.hasDestinationCall = true; validChainflipData.dstCallSwapData = dstSwapData; + validChainflipData.dstCallReceiver = address(0x123); // Set a non-zero address vm.startPrank(USER_SENDER); usdc.approve(_facetTestContractAddress, bridgeData.minAmount); @@ -312,12 +313,51 @@ contract ChainflipFacetTest is TestBaseFacet { }) ); validChainflipData.dstCallSwapData = dstSwapData; + validChainflipData.dstCallReceiver = address(0x123); // Ensure this is non-zero for the second case too vm.expectRevert(InformationMismatch.selector); initiateBridgeTxWithFacet(false); vm.stopPrank(); } + function testRevert_WhenReceiverAddressIsZero() public { + // Test with regular EVM destination and zero receiver + bridgeData.receiver = address(0); + bridgeData.destinationChainId = CHAIN_ID_ARBITRUM; + bridgeData.hasDestinationCall = false; + + vm.startPrank(USER_SENDER); + usdc.approve(_facetTestContractAddress, bridgeData.minAmount); + + vm.expectRevert(InvalidReceiver.selector); + initiateBridgeTxWithFacet(false); + + // Test with destination call and zero dstCallReceiver + bridgeData.receiver = USER_RECEIVER; // Set a valid receiver + bridgeData.hasDestinationCall = true; + + // Set up valid swap data + delete dstSwapData; + dstSwapData.push( + LibSwap.SwapData({ + callTo: address(0x123), + approveTo: address(0x123), + sendingAssetId: address(0), + receivingAssetId: address(0), + fromAmount: 0, + callData: "0x123456", + requiresDeposit: false + }) + ); + validChainflipData.dstCallSwapData = dstSwapData; + validChainflipData.dstCallReceiver = address(0); // Zero address for destination call + + vm.expectRevert(InvalidReceiver.selector); + initiateBridgeTxWithFacet(false); + + vm.stopPrank(); + } + function test_ChainIdMappings() public { // Set source chain to Arbitrum for these tests vm.chainId(CHAIN_ID_ARBITRUM); From 6d940bbece5b9d6e88f1a27fd1d648c68db9ebce Mon Sep 17 00:00:00 2001 From: Ed Zynda Date: Tue, 25 Feb 2025 16:16:39 +0300 Subject: [PATCH 50/55] cleanup --- src/Facets/ChainflipFacet.sol | 44 +++++++++++++++++------------------ 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/src/Facets/ChainflipFacet.sol b/src/Facets/ChainflipFacet.sol index bad58bd42..450bd766e 100644 --- a/src/Facets/ChainflipFacet.sol +++ b/src/Facets/ChainflipFacet.sol @@ -173,9 +173,18 @@ contract ChainflipFacet is ILiFi, ReentrancyGuard, SwapperV2, Validatable { encodedDstAddress = abi.encodePacked(receiverAddress); } - // Handle native token case with or without destination call - if (_bridgeData.sendingAssetId == address(0)) { - if (_bridgeData.hasDestinationCall) { + // Handle ERC20 token approval outside the if/else to avoid code duplication + if (_bridgeData.sendingAssetId != address(0)) { + LibAsset.maxApproveERC20( + IERC20(_bridgeData.sendingAssetId), + address(chainflipVault), + _bridgeData.minAmount + ); + } + + // Handle destination calls + if (_bridgeData.hasDestinationCall) { + if (_bridgeData.sendingAssetId == address(0)) { IChainflipVault(chainflipVault).xCallNative{ value: _bridgeData.minAmount }( @@ -187,34 +196,25 @@ contract ChainflipFacet is ILiFi, ReentrancyGuard, SwapperV2, Validatable { _chainflipData.cfParameters ); } else { - IChainflipVault(chainflipVault).xSwapNative{ - value: _bridgeData.minAmount - }( + IChainflipVault(chainflipVault).xCallToken( dstChain, encodedDstAddress, _chainflipData.dstToken, + message, + _chainflipData.gasAmount, + IERC20(_bridgeData.sendingAssetId), + _bridgeData.minAmount, _chainflipData.cfParameters ); } - } - // Handle ERC20 token case with or without destination call - else { - // Approve vault to spend tokens - LibAsset.maxApproveERC20( - IERC20(_bridgeData.sendingAssetId), - address(chainflipVault), - _bridgeData.minAmount - ); - - if (_bridgeData.hasDestinationCall) { - IChainflipVault(chainflipVault).xCallToken( + } else { + if (_bridgeData.sendingAssetId == address(0)) { + IChainflipVault(chainflipVault).xSwapNative{ + value: _bridgeData.minAmount + }( dstChain, encodedDstAddress, _chainflipData.dstToken, - message, - _chainflipData.gasAmount, - IERC20(_bridgeData.sendingAssetId), - _bridgeData.minAmount, _chainflipData.cfParameters ); } else { From d623247e1dd21cc96111b47edabd04be09a1747d Mon Sep 17 00:00:00 2001 From: Ed Zynda Date: Tue, 4 Mar 2025 11:22:38 +0300 Subject: [PATCH 51/55] update nonEVMReceiver from bytes32 to bytes --- docs/ChainflipFacet.md | 2 +- script/demoScripts/demoChainflip.ts | 3 +-- src/Facets/ChainflipFacet.sol | 10 ++++------ test/solidity/Facets/ChainflipFacet.t.sol | 22 +++++++++++----------- 4 files changed, 17 insertions(+), 20 deletions(-) diff --git a/docs/ChainflipFacet.md b/docs/ChainflipFacet.md index 68c5192b8..b76a60dbe 100644 --- a/docs/ChainflipFacet.md +++ b/docs/ChainflipFacet.md @@ -23,7 +23,7 @@ The methods listed above take a variable labeled `_chainflipData`. This data is ```solidity struct ChainflipData { - bytes32 nonEVMReceiver; // Destination address for non-EVM chains (Solana, Bitcoin) + bytes nonEVMReceiver; // Destination address for non-EVM chains (Solana, Bitcoin) uint32 dstToken; // Chainflip specific token identifier on the destination chain address dstCallReceiver; // Receiver contract address used for destination calls. Ignored if there is no destination call LibSwap.SwapData[] dstCallSwapData; // Swap data to be used in destination calls. Ignored if no destination call diff --git a/script/demoScripts/demoChainflip.ts b/script/demoScripts/demoChainflip.ts index 49a4fb62c..d6eba63d7 100644 --- a/script/demoScripts/demoChainflip.ts +++ b/script/demoScripts/demoChainflip.ts @@ -205,8 +205,7 @@ async function main() { : [] const chainflipData: ChainflipFacet.ChainflipDataStruct = { - nonEVMReceiver: - '0x0000000000000000000000000000000000000000000000000000000000000000', + nonEVMReceiver: '', dstToken: withDestinationCall ? 1 : 3, // 1 for ETH, 3 for USDC on ETH dstCallReceiver: RECEIVER_CHAINFLIP, dstCallSwapData: dstSwapData, diff --git a/src/Facets/ChainflipFacet.sol b/src/Facets/ChainflipFacet.sol index 450bd766e..df454c848 100644 --- a/src/Facets/ChainflipFacet.sol +++ b/src/Facets/ChainflipFacet.sol @@ -20,7 +20,7 @@ contract ChainflipFacet is ILiFi, ReentrancyGuard, SwapperV2, Validatable { event BridgeToNonEVMChain( bytes32 indexed transactionId, uint256 indexed destinationChainId, - bytes32 receiver + bytes receiver ); /// Errors /// @@ -49,7 +49,7 @@ contract ChainflipFacet is ILiFi, ReentrancyGuard, SwapperV2, Validatable { /// @param gasAmount Gas budget for the call on the destination chain /// @param cfParameters Additional parameters for future features struct ChainflipData { - bytes32 nonEVMReceiver; + bytes nonEVMReceiver; uint32 dstToken; address dstCallReceiver; LibSwap.SwapData[] dstCallSwapData; @@ -148,12 +148,10 @@ contract ChainflipFacet is ILiFi, ReentrancyGuard, SwapperV2, Validatable { // Handle address encoding based on destination chain type bytes memory encodedDstAddress; if (_bridgeData.receiver == LibAsset.NON_EVM_ADDRESS) { - if (_chainflipData.nonEVMReceiver == bytes32(0)) { + if (_chainflipData.nonEVMReceiver.length == 0) { revert EmptyNonEvmAddress(); } - encodedDstAddress = abi.encodePacked( - _chainflipData.nonEVMReceiver - ); + encodedDstAddress = _chainflipData.nonEVMReceiver; emit BridgeToNonEVMChain( _bridgeData.transactionId, diff --git a/test/solidity/Facets/ChainflipFacet.t.sol b/test/solidity/Facets/ChainflipFacet.t.sol index 01bd4b61b..bc052a818 100644 --- a/test/solidity/Facets/ChainflipFacet.t.sol +++ b/test/solidity/Facets/ChainflipFacet.t.sol @@ -127,8 +127,8 @@ contract ChainflipFacetTest is TestBaseFacet { bridgeData.receiver = LibAsset.NON_EVM_ADDRESS; bridgeData.destinationChainId = CHAIN_ID_SOLANA; validChainflipData.dstToken = 6; - validChainflipData.nonEVMReceiver = bytes32( - abi.encodePacked("EoW7FWTdPdZKpd3WAhH98c2HMGHsdh5yhzzEtk1u68Bb") + validChainflipData.nonEVMReceiver = bytes( + "EoW7FWTdPdZKpd3WAhH98c2HMGHsdh5yhzzEtk1u68Bb" ); vm.startPrank(USER_SENDER); @@ -156,8 +156,8 @@ contract ChainflipFacetTest is TestBaseFacet { bridgeData.receiver = LibAsset.NON_EVM_ADDRESS; bridgeData.destinationChainId = CHAIN_ID_BITCOIN; validChainflipData.dstToken = 6; - validChainflipData.nonEVMReceiver = bytes32( - abi.encodePacked("bc1q6l08rtj6j907r2een0jqs6l7qnruwyxfshmf8a") + validChainflipData.nonEVMReceiver = bytes( + "bc1q6l08rtj6j907r2een0jqs6l7qnruwyxfshmf8a" ); vm.startPrank(USER_SENDER); @@ -380,16 +380,16 @@ contract ChainflipFacetTest is TestBaseFacet { usdc.approve(_facetTestContractAddress, bridgeData.minAmount); bridgeData.destinationChainId = CHAIN_ID_SOLANA; bridgeData.receiver = LibAsset.NON_EVM_ADDRESS; - validChainflipData.nonEVMReceiver = bytes32( - abi.encodePacked("EoW7FWTdPdZKpd3WAhH98c2HMGHsdh5yhzzEtk1u68Bb") + validChainflipData.nonEVMReceiver = bytes( + "EoW7FWTdPdZKpd3WAhH98c2HMGHsdh5yhzzEtk1u68Bb" ); initiateBridgeTxWithFacet(false); // Test Bitcoin mapping usdc.approve(_facetTestContractAddress, bridgeData.minAmount); bridgeData.destinationChainId = CHAIN_ID_BITCOIN; - validChainflipData.nonEVMReceiver = bytes32( - abi.encodePacked("bc1q6l08rtj6j907r2een0jqs6l7qnruwyxfshmf8a") + validChainflipData.nonEVMReceiver = bytes( + "bc1q6l08rtj6j907r2een0jqs6l7qnruwyxfshmf8a" ); initiateBridgeTxWithFacet(false); @@ -406,7 +406,7 @@ contract ChainflipFacetTest is TestBaseFacet { bridgeData.receiver = LibAsset.NON_EVM_ADDRESS; bridgeData.destinationChainId = CHAIN_ID_SOLANA; validChainflipData.dstToken = 6; - validChainflipData.nonEVMReceiver = bytes32(0); // Empty address should fail + validChainflipData.nonEVMReceiver = bytes(""); // Empty address should fail vm.startPrank(USER_SENDER); @@ -473,8 +473,8 @@ contract ChainflipFacetTest is TestBaseFacet { bridgeData.receiver = USER_RECEIVER; // Use EVM address bridgeData.destinationChainId = CHAIN_ID_ETHEREUM; validChainflipData.dstToken = 6; - validChainflipData.nonEVMReceiver = bytes32( - abi.encodePacked("bc1q6l08rtj6j907r2een0jqs6l7qnruwyxfshmf8a") + validChainflipData.nonEVMReceiver = bytes( + "bc1q6l08rtj6j907r2een0jqs6l7qnruwyxfshmf8a" ); vm.startPrank(USER_SENDER); From 9e1c6458832f6dfae3f1c4c8e697af00232df00d Mon Sep 17 00:00:00 2001 From: Ed Zynda Date: Wed, 5 Mar 2025 15:40:49 +0300 Subject: [PATCH 52/55] use LibAsset helper --- src/Facets/ChainflipFacet.sol | 6 +++--- src/Periphery/ReceiverChainflip.sol | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Facets/ChainflipFacet.sol b/src/Facets/ChainflipFacet.sol index df454c848..620359558 100644 --- a/src/Facets/ChainflipFacet.sol +++ b/src/Facets/ChainflipFacet.sol @@ -172,7 +172,7 @@ contract ChainflipFacet is ILiFi, ReentrancyGuard, SwapperV2, Validatable { } // Handle ERC20 token approval outside the if/else to avoid code duplication - if (_bridgeData.sendingAssetId != address(0)) { + if (!LibAsset.isNativeAsset(_bridgeData.sendingAssetId)) { LibAsset.maxApproveERC20( IERC20(_bridgeData.sendingAssetId), address(chainflipVault), @@ -182,7 +182,7 @@ contract ChainflipFacet is ILiFi, ReentrancyGuard, SwapperV2, Validatable { // Handle destination calls if (_bridgeData.hasDestinationCall) { - if (_bridgeData.sendingAssetId == address(0)) { + if (LibAsset.isNativeAsset(_bridgeData.sendingAssetId)) { IChainflipVault(chainflipVault).xCallNative{ value: _bridgeData.minAmount }( @@ -206,7 +206,7 @@ contract ChainflipFacet is ILiFi, ReentrancyGuard, SwapperV2, Validatable { ); } } else { - if (_bridgeData.sendingAssetId == address(0)) { + if (LibAsset.isNativeAsset(_bridgeData.sendingAssetId)) { IChainflipVault(chainflipVault).xSwapNative{ value: _bridgeData.minAmount }( diff --git a/src/Periphery/ReceiverChainflip.sol b/src/Periphery/ReceiverChainflip.sol index d67daafcd..f9ac13194 100644 --- a/src/Periphery/ReceiverChainflip.sol +++ b/src/Periphery/ReceiverChainflip.sol @@ -114,7 +114,7 @@ contract ReceiverChainflip is ILiFi, WithdrawablePeriphery { address actualAssetId = assetId == CHAINFLIP_NATIVE_ADDRESS ? LibAsset.NATIVE_ASSETID : assetId; - bool isNative = actualAssetId == LibAsset.NATIVE_ASSETID; + bool isNative = LibAsset.isNativeAsset(actualAssetId); if (!isNative) { // ERC20 token operations From c3473cba3823a536edca8774858a36ff9e4abcf1 Mon Sep 17 00:00:00 2001 From: Ed Zynda Date: Wed, 5 Mar 2025 15:59:33 +0300 Subject: [PATCH 53/55] remove unnecessary type conversion --- src/Facets/ChainflipFacet.sol | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/src/Facets/ChainflipFacet.sol b/src/Facets/ChainflipFacet.sol index 620359558..5b457149f 100644 --- a/src/Facets/ChainflipFacet.sol +++ b/src/Facets/ChainflipFacet.sol @@ -183,9 +183,7 @@ contract ChainflipFacet is ILiFi, ReentrancyGuard, SwapperV2, Validatable { // Handle destination calls if (_bridgeData.hasDestinationCall) { if (LibAsset.isNativeAsset(_bridgeData.sendingAssetId)) { - IChainflipVault(chainflipVault).xCallNative{ - value: _bridgeData.minAmount - }( + chainflipVault.xCallNative{ value: _bridgeData.minAmount }( dstChain, encodedDstAddress, _chainflipData.dstToken, @@ -194,7 +192,7 @@ contract ChainflipFacet is ILiFi, ReentrancyGuard, SwapperV2, Validatable { _chainflipData.cfParameters ); } else { - IChainflipVault(chainflipVault).xCallToken( + chainflipVault.xCallToken( dstChain, encodedDstAddress, _chainflipData.dstToken, @@ -207,16 +205,14 @@ contract ChainflipFacet is ILiFi, ReentrancyGuard, SwapperV2, Validatable { } } else { if (LibAsset.isNativeAsset(_bridgeData.sendingAssetId)) { - IChainflipVault(chainflipVault).xSwapNative{ - value: _bridgeData.minAmount - }( + chainflipVault.xSwapNative{ value: _bridgeData.minAmount }( dstChain, encodedDstAddress, _chainflipData.dstToken, _chainflipData.cfParameters ); } else { - IChainflipVault(chainflipVault).xSwapToken( + chainflipVault.xSwapToken( dstChain, encodedDstAddress, _chainflipData.dstToken, From 915e5601000f0a73769393d7ff51c79e7b2c814e Mon Sep 17 00:00:00 2001 From: Ed Zynda Date: Wed, 5 Mar 2025 16:05:18 +0300 Subject: [PATCH 54/55] cache native asset check result --- src/Facets/ChainflipFacet.sol | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/Facets/ChainflipFacet.sol b/src/Facets/ChainflipFacet.sol index 5b457149f..07e6b1647 100644 --- a/src/Facets/ChainflipFacet.sol +++ b/src/Facets/ChainflipFacet.sol @@ -127,6 +127,9 @@ contract ChainflipFacet is ILiFi, ReentrancyGuard, SwapperV2, Validatable { ChainflipData calldata _chainflipData ) internal { uint32 dstChain = _getChainflipChainId(_bridgeData.destinationChainId); + bool isNativeAsset = LibAsset.isNativeAsset( + _bridgeData.sendingAssetId + ); // Initialize message variable at function scope level bytes memory message; @@ -172,7 +175,7 @@ contract ChainflipFacet is ILiFi, ReentrancyGuard, SwapperV2, Validatable { } // Handle ERC20 token approval outside the if/else to avoid code duplication - if (!LibAsset.isNativeAsset(_bridgeData.sendingAssetId)) { + if (!isNativeAsset) { LibAsset.maxApproveERC20( IERC20(_bridgeData.sendingAssetId), address(chainflipVault), @@ -182,7 +185,7 @@ contract ChainflipFacet is ILiFi, ReentrancyGuard, SwapperV2, Validatable { // Handle destination calls if (_bridgeData.hasDestinationCall) { - if (LibAsset.isNativeAsset(_bridgeData.sendingAssetId)) { + if (isNativeAsset) { chainflipVault.xCallNative{ value: _bridgeData.minAmount }( dstChain, encodedDstAddress, @@ -204,7 +207,7 @@ contract ChainflipFacet is ILiFi, ReentrancyGuard, SwapperV2, Validatable { ); } } else { - if (LibAsset.isNativeAsset(_bridgeData.sendingAssetId)) { + if (isNativeAsset) { chainflipVault.xSwapNative{ value: _bridgeData.minAmount }( dstChain, encodedDstAddress, From fe2691ce46ade06795cc298846d5357c4030960a Mon Sep 17 00:00:00 2001 From: Ed Zynda Date: Wed, 5 Mar 2025 16:16:02 +0300 Subject: [PATCH 55/55] gas optimization --- src/Facets/ChainflipFacet.sol | 31 ++++++++++------------- test/solidity/Facets/ChainflipFacet.t.sol | 27 ++++++++++++++++++++ 2 files changed, 41 insertions(+), 17 deletions(-) diff --git a/src/Facets/ChainflipFacet.sol b/src/Facets/ChainflipFacet.sol index 07e6b1647..8675eaac8 100644 --- a/src/Facets/ChainflipFacet.sol +++ b/src/Facets/ChainflipFacet.sol @@ -131,23 +131,6 @@ contract ChainflipFacet is ILiFi, ReentrancyGuard, SwapperV2, Validatable { _bridgeData.sendingAssetId ); - // Initialize message variable at function scope level - bytes memory message; - - // Validate destination call flag matches message presence first - if (_bridgeData.hasDestinationCall) { - if (_chainflipData.dstCallSwapData.length == 0) { - revert InformationMismatch(); - } - message = abi.encode( - _bridgeData.transactionId, - _chainflipData.dstCallSwapData, - _bridgeData.receiver - ); - } else if (_chainflipData.dstCallSwapData.length > 0) { - revert InformationMismatch(); - } - // Handle address encoding based on destination chain type bytes memory encodedDstAddress; if (_bridgeData.receiver == LibAsset.NON_EVM_ADDRESS) { @@ -185,6 +168,16 @@ contract ChainflipFacet is ILiFi, ReentrancyGuard, SwapperV2, Validatable { // Handle destination calls if (_bridgeData.hasDestinationCall) { + if (_chainflipData.dstCallSwapData.length == 0) { + revert InformationMismatch(); + } + + bytes memory message = abi.encode( + _bridgeData.transactionId, + _chainflipData.dstCallSwapData, + _bridgeData.receiver + ); + if (isNativeAsset) { chainflipVault.xCallNative{ value: _bridgeData.minAmount }( dstChain, @@ -207,6 +200,10 @@ contract ChainflipFacet is ILiFi, ReentrancyGuard, SwapperV2, Validatable { ); } } else { + if (_chainflipData.dstCallSwapData.length > 0) { + revert InformationMismatch(); + } + if (isNativeAsset) { chainflipVault.xSwapNative{ value: _bridgeData.minAmount }( dstChain, diff --git a/test/solidity/Facets/ChainflipFacet.t.sol b/test/solidity/Facets/ChainflipFacet.t.sol index bc052a818..38181dd04 100644 --- a/test/solidity/Facets/ChainflipFacet.t.sol +++ b/test/solidity/Facets/ChainflipFacet.t.sol @@ -286,6 +286,33 @@ contract ChainflipFacetTest is TestBaseFacet { vm.stopPrank(); } + function testBase_Revert_BridgeWithInvalidDestinationCallFlag() + public + override + { + vm.startPrank(USER_SENDER); + + // approval + usdc.approve(_facetTestContractAddress, bridgeData.minAmount); + + // prepare bridgeData + bridgeData.hasDestinationCall = true; + + // In the ChainflipFacet, when hasDestinationCall is true, it uses dstCallReceiver + // which is zero by default, so it will revert with InvalidReceiver before checking + // for the destination call data + // + // NOTE: We override this test from TestBaseFacet because the base test expects + // InformationMismatch error, but our implementation checks for zero receiver address + // first (which throws InvalidReceiver) before checking for empty destination call data. + // The order of validation in _startBridge() means we hit the InvalidReceiver check + // before we would hit the InformationMismatch check. + vm.expectRevert(InvalidReceiver.selector); + + initiateBridgeTxWithFacet(false); + vm.stopPrank(); + } + function testRevert_WhenDestinationCallFlagMismatchesMessage() public { // Case 1: hasDestinationCall is true but message is empty bridgeData.hasDestinationCall = true;