From 5446cc2520560245af2660e31bb68d5487c7f3e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Bl=C3=A4cker?= Date: Thu, 16 Jan 2025 08:45:51 +0700 Subject: [PATCH 01/38] deploy log fixed --- deployments/_deployments_log_file.json | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/deployments/_deployments_log_file.json b/deployments/_deployments_log_file.json index a3d183b50..3899e4fd4 100644 --- a/deployments/_deployments_log_file.json +++ b/deployments/_deployments_log_file.json @@ -16299,6 +16299,10 @@ "CONSTRUCTOR_ARGS": "0x0000000000000000000000009b36f165bab9ebe611d491180418d8de4b8f3a1f00000000000000000000000011f1022ca6adef6400e5677528a80d49a069c00c", "SALT": "", "VERIFIED": "true" + } + ] + } + }, "zksync": { "production": { "1.0.0": [ @@ -26983,4 +26987,4 @@ } } } -} \ No newline at end of file +} From fda004f3aa02ebf40b088911bcb2a1c899c151e0 Mon Sep 17 00:00:00 2001 From: Michal Date: Thu, 16 Jan 2025 18:16:48 +0100 Subject: [PATCH 02/38] Added GlacisFacet files --- config/glacis.json | 16 ++ docs/GlacisFacet.md | 92 +++++++++ script/deploy/facets/DeployGlacisFacet.s.sol | 33 ++++ script/deploy/facets/UpdateGlacisFacet.s.sol | 51 +++++ src/Facets/GlacisFacet.sol | 175 +++++++++++++++++ src/Interfaces/IGlacisAirlift.sol | 87 ++++++++ test/solidity/Facets/GlacisFacet.t.sol | 196 +++++++++++++++++++ test/solidity/utils/TestBase.sol | 21 ++ 8 files changed, 671 insertions(+) create mode 100644 config/glacis.json create mode 100644 docs/GlacisFacet.md create mode 100644 script/deploy/facets/DeployGlacisFacet.s.sol create mode 100644 script/deploy/facets/UpdateGlacisFacet.s.sol create mode 100644 src/Facets/GlacisFacet.sol create mode 100644 src/Interfaces/IGlacisAirlift.sol create mode 100644 test/solidity/Facets/GlacisFacet.t.sol diff --git a/config/glacis.json b/config/glacis.json new file mode 100644 index 000000000..bcf06aa51 --- /dev/null +++ b/config/glacis.json @@ -0,0 +1,16 @@ +{ + "mainnet": { + "example": "0x0000000000000000000000000000000000000000", + "exampleAllowedTokens": [ + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000" + ] + }, + "arbitrum": { + "example": "0x0000000000000000000000000000000000000000", + "exampleAllowedTokens": [ + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000" + ] + } +} diff --git a/docs/GlacisFacet.md b/docs/GlacisFacet.md new file mode 100644 index 000000000..3a4ad7337 --- /dev/null +++ b/docs/GlacisFacet.md @@ -0,0 +1,92 @@ +# Glacis Facet + +## How it works + +The Glacis Facet works by ... + +```mermaid +graph LR; + D{LiFiDiamond}-- DELEGATECALL -->GlacisFacet; + GlacisFacet -- CALL --> C(Glacis) +``` + +## Public Methods + +- `function startBridgeTokensViaGlacis(BridgeData calldata _bridgeData, GlacisData calldata _glacisData)` + - Simply bridges tokens using glacis +- `swapAndStartBridgeTokensViaGlacis(BridgeData memory _bridgeData, LibSwap.SwapData[] calldata _swapData, glacisData memory _glacisData)` + - Performs swap(s) before bridging tokens using glacis + +## glacis Specific Parameters + +The methods listed above take a variable labeled `_glacisData`. This data is specific to glacis and is represented as the following struct type: + +```solidity +/// @param example Example parameter. +struct glacisData { + 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: 'glacis', // 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=glacis&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=glacis&fromAddress={YOUR_WALLET_ADDRESS}' +``` diff --git a/script/deploy/facets/DeployGlacisFacet.s.sol b/script/deploy/facets/DeployGlacisFacet.s.sol new file mode 100644 index 000000000..9351e7800 --- /dev/null +++ b/script/deploy/facets/DeployGlacisFacet.s.sol @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.17; + +import { DeployScriptBase } from "./utils/DeployScriptBase.sol"; +import { stdJson } from "forge-std/Script.sol"; +import { GlacisFacet } from "lifi/Facets/GlacisFacet.sol"; + +contract DeployScript is DeployScriptBase { + using stdJson for string; + + constructor() DeployScriptBase("GlacisFacet") {} + + function run() + public + returns (GlacisFacet deployed, bytes memory constructorArgs) + { + constructorArgs = getConstructorArgs(); + + deployed = GlacisFacet(deploy(type(GlacisFacet).creationCode)); + } + + function getConstructorArgs() internal override returns (bytes memory) { + // If you don't have a constructor or it doesn't take any arguments, you can remove this function + string memory path = string.concat(root, "/config/glacis.json"); + string memory json = vm.readFile(path); + + address example = json.readAddress( + string.concat(".", network, ".example") + ); + + return abi.encode(example); + } +} diff --git a/script/deploy/facets/UpdateGlacisFacet.s.sol b/script/deploy/facets/UpdateGlacisFacet.s.sol new file mode 100644 index 000000000..fd1011103 --- /dev/null +++ b/script/deploy/facets/UpdateGlacisFacet.s.sol @@ -0,0 +1,51 @@ +// 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 { GlacisFacet } from "lifi/Facets/GlacisFacet.sol"; + +contract DeployScript is UpdateScriptBase { + using stdJson for string; + + struct Config { + uint256 a; + bool b; + address c; + } + + function run() + public + returns (address[] memory facets, bytes memory cutData) + { + return update("GlacisFacet"); + } + + function getExcludes() internal pure override returns (bytes4[] memory) { + // Use this to exclude any selectors that might clash with other facets in the diamond + // or selectors you don't want accessible e.g. init() functions. + // You can remove this function if it's not needed. + bytes4[] memory excludes = new bytes4[](1); + + return excludes; + } + + function getCallData() internal override returns (bytes memory) { + // Use this to get initialization calldata that will be executed + // when adding the facet to a diamond. + // You can remove this function it it's not needed. + path = string.concat(root, "/config/glacis.json"); + json = vm.readFile(path); + bytes memory rawConfigs = json.parseRaw(".configs"); + Config[] memory cfg = abi.decode(rawConfigs, (Config[])); + + // bytes memory callData = abi.encodeWithSelector( + // GlacisFacet.initGlacis.selector, + // cfg + // ); + bytes memory callData = abi.encodePacked(address(0x22)); + + return callData; + } +} diff --git a/src/Facets/GlacisFacet.sol b/src/Facets/GlacisFacet.sol new file mode 100644 index 000000000..27c5373d4 --- /dev/null +++ b/src/Facets/GlacisFacet.sol @@ -0,0 +1,175 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.17; + +import { ILiFi } from "../Interfaces/ILiFi.sol"; +import { LibDiamond } from "../Libraries/LibDiamond.sol"; +import { LibAsset, IERC20 } 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 { IGlacisAirlift, QuoteSendInfo } from "../Interfaces/IGlacisAirlift.sol"; + +import { console } from "forge-std/console.sol"; + +/// @title Glacis Facet +/// @author LI.FI (https://li.fi/) +/// @notice Integration of the Glacis airlift (wrapper for native token bridging standards) +/// @custom:version 1.0.0 +contract GlacisFacet is ILiFi, ReentrancyGuard, SwapperV2, Validatable { + /// Storage /// + + bytes32 internal constant NAMESPACE = keccak256("com.lifi.facets.glacis"); // Optional. Only use if you need to store data in the diamond storage. + + IGlacisAirlift public immutable airlift; + + /// Types /// + + /// @param refund Refund address + // TODO + struct GlacisData { + address refund; + } + + /// Constructor /// + /// @notice Initializes the GlacisFacet contract + /// @param _airlift The address of Glacis Airlift contract. + constructor(IGlacisAirlift _airlift) { + airlift = _airlift; + } + + /// External Methods /// + + /// @notice Bridges tokens via Glacis + /// @param _bridgeData The core information needed for bridging + /// @param _glacisData Data specific to Glacis + function startBridgeTokensViaGlacis( + ILiFi.BridgeData memory _bridgeData, + GlacisData calldata _glacisData + ) + external + payable + nonReentrant + refundExcessNative(payable(msg.sender)) + validateBridgeData(_bridgeData) + doesNotContainSourceSwaps(_bridgeData) + doesNotContainDestinationCalls(_bridgeData) + { + LibAsset.depositAsset( + _bridgeData.sendingAssetId, + _bridgeData.minAmount + ); + uint256 fees = _calculateFees(_bridgeData, _glacisData); + _startBridge(_bridgeData, _glacisData, fees); + } + + /// @notice Performs a swap before bridging via Glacis + /// @param _bridgeData The core information needed for bridging + /// @param _swapData An array of swap related data for performing swaps before bridging + /// @param _glacisData Data specific to Glacis + function swapAndStartBridgeTokensViaGlacis( + ILiFi.BridgeData memory _bridgeData, + LibSwap.SwapData[] calldata _swapData, + GlacisData calldata _glacisData + ) + external + payable + nonReentrant + refundExcessNative(payable(msg.sender)) + containsSourceSwaps(_bridgeData) + doesNotContainDestinationCalls(_bridgeData) + validateBridgeData(_bridgeData) + { + uint256 fees = _calculateFees(_bridgeData, _glacisData); + _bridgeData.minAmount = _depositAndSwap( + _bridgeData.transactionId, + _bridgeData.minAmount, + _swapData, + payable(msg.sender), + fees + ); + _startBridge(_bridgeData, _glacisData, fees); + } + + /// Internal Methods /// + + /// @dev Contains the business logic for the bridge via Glacis + /// @param _bridgeData The core information needed for bridging + /// @param _glacisData Data specific to Glacis + function _startBridge( + ILiFi.BridgeData memory _bridgeData, + GlacisData calldata _glacisData, + uint256 _fee + ) internal { + bytes32 receiver = bytes32(uint256(uint160(_bridgeData.receiver))); + // uint256 tokenFee = sendInfo.gmpFee.tokenFee + sendInfo.AirliftFeeInfo.airliftFee.tokenFee; // TODO + + if (!LibAsset.isNativeAsset(_bridgeData.sendingAssetId)) { + // Give the Airlift approval to bridge tokens + LibAsset.maxApproveERC20( + IERC20(_bridgeData.sendingAssetId), + address(airlift), + _bridgeData.minAmount + ); + airlift.send{ value: _fee }( + _bridgeData.sendingAssetId, + _bridgeData.minAmount, + receiver, + _bridgeData.destinationChainId, + _glacisData.refund + ); + } else { + // cant have tokenFee is it's native asset bridging + airlift.send{ value: _bridgeData.minAmount + _fee }( + _bridgeData.sendingAssetId, + _bridgeData.minAmount, + receiver, + _bridgeData.destinationChainId, + _glacisData.refund + ); + } + + emit LiFiTransferStarted(_bridgeData); + } + + /// Private Methods /// + + function _calculateFees( + ILiFi.BridgeData memory _bridgeData, + GlacisData calldata _glacisData + ) internal returns (uint256 nativeFees) { + console.log( + "============================ _calculateFees 0.1 ========================" + ); + (bool ok, bytes memory result) = address(airlift).staticcall( + abi.encodeWithSignature( + "quoteSend(address,uint256,bytes32,uint256,address,uint256)", + _bridgeData.sendingAssetId, + _bridgeData.minAmount, + bytes32(uint256(uint160(_bridgeData.receiver))), + _bridgeData.destinationChainId, + _glacisData.refund, + 1 ether // TODO!!! + // !LibAsset.isNativeAsset(_bridgeData.sendingAssetId) ? 0 : _bridgeData.minAmount + ) + ); + console.log( + "============================ _calculateFees 0.2 ========================" + ); + // TODO require ok + QuoteSendInfo memory sendInfo = abi.decode(result, (QuoteSendInfo)); + console.log( + "============================ _calculateFees 0.3 ========================" + ); + + uint256 nativeAssetAmount = sendInfo.gmpFee.nativeFee + + sendInfo.AirliftFeeInfo.airliftFee.nativeFee; + console.log( + "============================ _calculateFees -> nativeAssetAmount ========================" + ); + console.log(nativeAssetAmount); + nativeFees = + sendInfo.gmpFee.nativeFee + + sendInfo.AirliftFeeInfo.airliftFee.nativeFee; + } +} diff --git a/src/Interfaces/IGlacisAirlift.sol b/src/Interfaces/IGlacisAirlift.sol new file mode 100644 index 000000000..afce26e69 --- /dev/null +++ b/src/Interfaces/IGlacisAirlift.sol @@ -0,0 +1,87 @@ +// SPDX-License-Identifier: MIT +/// @custom:version 1.0.0 +pragma solidity ^0.8.17; + +struct QuoteSendInfo { + Fee gmpFee; + uint256 amountSent; + uint256 valueSent; + AirliftFeeInfo AirliftFeeInfo; +} + +struct AirliftFeeInfo { + Fee airliftFee; + uint256 correctedAmount; + uint256 correctedValue; +} + +struct Fee { + uint256 nativeFee; + uint256 tokenFee; +} + +error GlacisAirlift__NotEnoughValueFee(); +error GlacisAirlift__NotEnoughTokenFee(); +error GlacisAirlift__FeeTransferUnsuccessful(); +error GlacisAirliftFacet__TokenNotSupportedForBridging(); +error GlacisAirliftFacet__TokenFacetReverted(address token, bytes4 selector); +error GlacisAirliftFacet__SelectorAndTokenArrayMustBeSameLength(); +error GlacisAirliftFacet__NotOnWhitelist(); + +interface IGlacisAirlift { + /// Registers function selectors to multiple token. A selector's function must be added to the Diamond as a facet. + /// @param diamondSelectors The bytes4 selector of the token's handler function. + /// @param facetSelectors The bytes4 selector of the token's handler function. + /// @param token The token to register. + function addSelectorsToToken( + bytes4[] memory diamondSelectors, + bytes4[] memory facetSelectors, + address token + ) external; + + /// Use to send a token from chain A to chain B after sending this contract the token already. + /// This function should only be used when a smart contract calls it, so that the token's transfer + /// and the cross-chain send are atomic within a single transaction. + /// @param token The address of the token sending across chains. + /// @param amount The amount of the token you want to send across chains. + /// @param receiver The target address that should receive the funds on the destination chain. + /// @param destinationChainId The Ethereum chain ID of the destination chain. + /// @param refundAddress The address that should receive any funds in the case the cross-chain gas value is too high. + function send( + address token, + uint256 amount, + bytes32 receiver, + uint256 destinationChainId, + address refundAddress + ) external payable; + + /// Use to send a token from chain A to chain B after only approving this contract to transfer the tokens. + /// This function should be used by EOAs who want to do the approval and transaction in two separate blocks. + /// @param token The address of the token sending across chains. + /// @param amount The amount of the token you want to send across chains. + /// @param receiver The target address that should receive the funds on the destination chain. + /// @param destinationChainId The Ethereum chain ID of the destination chain. + /// @param refundAddress The address that should receive any funds in the case the cross-chain gas value is too high. + function sendAfterApproval( + address token, + uint256 amount, + bytes32 receiver, + uint256 destinationChainId, + address refundAddress + ) external payable; + + /// Use to quote the send a token from chain A to chain B. + /// @param token The address of the token sending across chains. + /// @param amount The amount of the token you want to send across chains. + /// @param receiver The target address that should receive the funds on the destination chain. + /// @param destinationChainId The Ethereum chain ID of the destination chain. + /// @param refundAddress The address that should receive any funds in the case the cross-chain gas value is too high. + function quoteSend( + address token, + uint256 amount, + bytes32 receiver, + uint256 destinationChainId, + address refundAddress, + uint256 msgValue + ) external returns (QuoteSendInfo memory); +} diff --git a/test/solidity/Facets/GlacisFacet.t.sol b/test/solidity/Facets/GlacisFacet.t.sol new file mode 100644 index 000000000..14ad961af --- /dev/null +++ b/test/solidity/Facets/GlacisFacet.t.sol @@ -0,0 +1,196 @@ +// SPDX-License-Identifier: Unlicense +pragma solidity 0.8.17; + +import { LibAllowList, TestBaseFacet, console, ERC20 } from "../utils/TestBaseFacet.sol"; +import { GlacisFacet } from "lifi/Facets/GlacisFacet.sol"; +import { IGlacisAirlift, QuoteSendInfo } from "lifi/Interfaces/IGlacisAirlift.sol"; + +// Stub GlacisFacet Contract +contract TestGlacisFacet is GlacisFacet { + constructor(IGlacisAirlift _airlift) GlacisFacet(_airlift) {} + + function addDex(address _dex) external { + LibAllowList.addAllowedContract(_dex); + } + + function setFunctionApprovalBySignature(bytes4 _signature) external { + LibAllowList.addAllowedSelector(_signature); + } +} + +contract GlacisFacetTest is TestBaseFacet { + GlacisFacet.GlacisData internal validGlacisData; + TestGlacisFacet internal glacisFacet; + + IGlacisAirlift internal constant airlift = + IGlacisAirlift(0xE0A049955E18CFfd09C826C2c2e965439B6Ab272); + + ERC20 internal WORMHOLE_TOKEN_ARB = + ERC20(0xB0fFa8000886e57F86dd5264b9582b2Ad87b2b91); + + uint256 internal tokenFee; + + function setUp() public { + customRpcUrlForForking = "ETH_NODE_URI_ARBITRUM"; + customBlockNumberForForking = 295706031; + initTestBase(); + + deal( + address(WORMHOLE_TOKEN_ARB), + USER_SENDER, + 100_000 * 10 ** WORMHOLE_TOKEN_ARB.decimals() + ); + deal( + address(WORMHOLE_TOKEN_ARB), + address(airlift), + 100_000 * 10 ** WORMHOLE_TOKEN_ARB.decimals() + ); + + glacisFacet = new TestGlacisFacet(airlift); + bytes4[] memory functionSelectors = new bytes4[](4); + functionSelectors[0] = glacisFacet.startBridgeTokensViaGlacis.selector; + functionSelectors[1] = glacisFacet + .swapAndStartBridgeTokensViaGlacis + .selector; + functionSelectors[2] = glacisFacet.addDex.selector; + functionSelectors[3] = glacisFacet + .setFunctionApprovalBySignature + .selector; + + addFacet(diamond, address(glacisFacet), functionSelectors); + glacisFacet = TestGlacisFacet(address(diamond)); + glacisFacet.addDex(ADDRESS_UNISWAP_ARB); + glacisFacet.setFunctionApprovalBySignature( + uniswap.swapExactTokensForTokens.selector + ); + glacisFacet.setFunctionApprovalBySignature( + uniswap.swapTokensForExactETH.selector + ); + glacisFacet.setFunctionApprovalBySignature( + uniswap.swapETHForExactTokens.selector + ); + + setFacetAddressInTestBase(address(glacisFacet), "GlacisFacet"); + + // adjust bridgeData + bridgeData.bridge = "glacis"; + bridgeData.sendingAssetId = address(WORMHOLE_TOKEN_ARB); + bridgeData.minAmount = 1 * 10 ** 18; + bridgeData.destinationChainId = 10; + + // produce valid GlacisData + validGlacisData = GlacisFacet.GlacisData({ refund: REFUND_WALLET }); + + console.log( + "============================ here0.1 ========================" + ); + (bool ok, bytes memory result) = address(airlift).staticcall( + abi.encodeWithSignature( + "quoteSend(address,uint256,bytes32,uint256,address,uint256)", + bridgeData.sendingAssetId, + bridgeData.minAmount, + bytes32(uint256(uint160(bridgeData.receiver))), + bridgeData.destinationChainId, + REFUND_WALLET, + 1 ether // TODO + ) + ); + require(ok); + QuoteSendInfo memory sendInfo = abi.decode(result, (QuoteSendInfo)); + + tokenFee = + sendInfo.gmpFee.tokenFee + + sendInfo.AirliftFeeInfo.airliftFee.tokenFee; + addToMessageValue = + sendInfo.gmpFee.nativeFee + + sendInfo.AirliftFeeInfo.airliftFee.nativeFee; + } + + function initiateBridgeTxWithFacet(bool) internal override { + bridgeData.minAmount -= tokenFee; + glacisFacet.startBridgeTokensViaGlacis{ value: addToMessageValue }( + bridgeData, + validGlacisData + ); + } + + function testBase_CanBridgeNativeTokens() public override { + // facet does not support bridging of native assets + } + + function testBase_CanBridgeTokens() + public + virtual + override + assertBalanceChange( + address(WORMHOLE_TOKEN_ARB), + USER_SENDER, + -int256(defaultUSDCAmount) + ) + assertBalanceChange(address(WORMHOLE_TOKEN_ARB), USER_RECEIVER, 0) + assertBalanceChange(ADDRESS_DAI, USER_SENDER, 0) + assertBalanceChange(ADDRESS_DAI, USER_RECEIVER, 0) + { + vm.startPrank(USER_SENDER); + + // approval + WORMHOLE_TOKEN_ARB.approve(address(glacisFacet), bridgeData.minAmount); + + //prepare check for events + vm.expectEmit(true, true, true, true, address(glacisFacet)); + emit LiFiTransferStarted(bridgeData); + + initiateBridgeTxWithFacet(false); + vm.stopPrank(); + } + + function testBase_CanSwapAndBridgeNativeTokens() public override { + // facet does not support bridging of native assets + } + + function testBase_CanBridgeTokens_fuzzed(uint256 amount) public override { + // TODO + } + + function initiateSwapAndBridgeTxWithFacet(bool) internal override { + glacisFacet.swapAndStartBridgeTokensViaGlacis{ + value: addToMessageValue + }(bridgeData, swapData, validGlacisData); + } + + function test_CanBridgeAndPayFeeWithBridgedToken() public {} + + function test_CanSwapAndBridgeAndPayFeeWithBridgedToken() public {} + + // 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 + // } +} diff --git a/test/solidity/utils/TestBase.sol b/test/solidity/utils/TestBase.sol index 14813f95d..8b52273bf 100644 --- a/test/solidity/utils/TestBase.sol +++ b/test/solidity/utils/TestBase.sol @@ -168,6 +168,17 @@ abstract contract TestBase is Test, DiamondTest, ILiFi { 0x7ceB23fD6bC0adD59E62ac25578270cFf1b9f619; address internal ADDRESS_WRAPPED_NATIVE_POL = 0x0d500B1d8E8eF31E21C99d1Db9A6444d3ADf1270; // WMATIC + // Contract addresses (OPTIMISM) + address internal ADDRESS_UNISWAP_OPTIMISM = + 0x4A7b5Da61326A6379179b40d00F57E5bbDC962c2; + address internal ADDRESS_USDC_OPTIMISM = + 0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85; + address internal ADDRESS_USDT_OPTIMISM = + 0x94b008aA00579c1307B0EF2c499aD98a8ce58e58; + address internal ADDRESS_DAI_OPTIMISM = + 0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1; + address internal ADDRESS_WRAPPED_NATIVE_OPTIMISM = + 0x4200000000000000000000000000000000000006; // User accounts (Whales: ETH only) address internal constant USER_SENDER = address(0xabc123456); // initially funded with 100,000 DAI, USDC, USDT, WETH & ETHER address internal constant USER_RECEIVER = address(0xabc654321); @@ -236,6 +247,16 @@ abstract contract TestBase is Test, DiamondTest, ILiFi { ADDRESS_WRAPPED_NATIVE = ADDRESS_WRAPPED_NATIVE_POL; ADDRESS_UNISWAP = ADDRESS_SUSHISWAP_POL; } + if ( + keccak256(abi.encode(customRpcUrlForForking)) == + keccak256(abi.encode("ETH_NODE_URI_OPTIMISM")) + ) { + ADDRESS_USDC = ADDRESS_USDC_OPTIMISM; + ADDRESS_USDT = ADDRESS_USDT_OPTIMISM; + ADDRESS_DAI = ADDRESS_DAI_OPTIMISM; + ADDRESS_WRAPPED_NATIVE = ADDRESS_WRAPPED_NATIVE_OPTIMISM; + ADDRESS_UNISWAP = ADDRESS_UNISWAP_OPTIMISM; + } } } From e4ffcb834db1119848785ff76281bd7c088187e5 Mon Sep 17 00:00:00 2001 From: Michal Date: Thu, 16 Jan 2025 18:45:22 +0100 Subject: [PATCH 03/38] Removed OPTIMISM addresses --- test/solidity/utils/TestBase.sol | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/test/solidity/utils/TestBase.sol b/test/solidity/utils/TestBase.sol index 8b52273bf..d14e92aaf 100644 --- a/test/solidity/utils/TestBase.sol +++ b/test/solidity/utils/TestBase.sol @@ -168,17 +168,6 @@ abstract contract TestBase is Test, DiamondTest, ILiFi { 0x7ceB23fD6bC0adD59E62ac25578270cFf1b9f619; address internal ADDRESS_WRAPPED_NATIVE_POL = 0x0d500B1d8E8eF31E21C99d1Db9A6444d3ADf1270; // WMATIC - // Contract addresses (OPTIMISM) - address internal ADDRESS_UNISWAP_OPTIMISM = - 0x4A7b5Da61326A6379179b40d00F57E5bbDC962c2; - address internal ADDRESS_USDC_OPTIMISM = - 0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85; - address internal ADDRESS_USDT_OPTIMISM = - 0x94b008aA00579c1307B0EF2c499aD98a8ce58e58; - address internal ADDRESS_DAI_OPTIMISM = - 0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1; - address internal ADDRESS_WRAPPED_NATIVE_OPTIMISM = - 0x4200000000000000000000000000000000000006; // User accounts (Whales: ETH only) address internal constant USER_SENDER = address(0xabc123456); // initially funded with 100,000 DAI, USDC, USDT, WETH & ETHER address internal constant USER_RECEIVER = address(0xabc654321); From 22d242a046c1afe36ae14100e662c368f7da1f9a Mon Sep 17 00:00:00 2001 From: Michal Date: Thu, 16 Jan 2025 18:45:56 +0100 Subject: [PATCH 04/38] Removed OPTIMISM addresses --- test/solidity/utils/TestBase.sol | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/test/solidity/utils/TestBase.sol b/test/solidity/utils/TestBase.sol index d14e92aaf..14813f95d 100644 --- a/test/solidity/utils/TestBase.sol +++ b/test/solidity/utils/TestBase.sol @@ -236,16 +236,6 @@ abstract contract TestBase is Test, DiamondTest, ILiFi { ADDRESS_WRAPPED_NATIVE = ADDRESS_WRAPPED_NATIVE_POL; ADDRESS_UNISWAP = ADDRESS_SUSHISWAP_POL; } - if ( - keccak256(abi.encode(customRpcUrlForForking)) == - keccak256(abi.encode("ETH_NODE_URI_OPTIMISM")) - ) { - ADDRESS_USDC = ADDRESS_USDC_OPTIMISM; - ADDRESS_USDT = ADDRESS_USDT_OPTIMISM; - ADDRESS_DAI = ADDRESS_DAI_OPTIMISM; - ADDRESS_WRAPPED_NATIVE = ADDRESS_WRAPPED_NATIVE_OPTIMISM; - ADDRESS_UNISWAP = ADDRESS_UNISWAP_OPTIMISM; - } } } From 76e1167aef38817353c445b37508dd83f55d7f6c Mon Sep 17 00:00:00 2001 From: Michal Date: Fri, 17 Jan 2025 12:56:13 +0100 Subject: [PATCH 05/38] Added GlacisFacet tests --- test/solidity/Facets/GlacisFacet.t.sol | 179 ++++++++++++++++++++----- 1 file changed, 147 insertions(+), 32 deletions(-) diff --git a/test/solidity/Facets/GlacisFacet.t.sol b/test/solidity/Facets/GlacisFacet.t.sol index 14ad961af..749f71ac5 100644 --- a/test/solidity/Facets/GlacisFacet.t.sol +++ b/test/solidity/Facets/GlacisFacet.t.sol @@ -2,6 +2,7 @@ pragma solidity 0.8.17; import { LibAllowList, TestBaseFacet, console, ERC20 } from "../utils/TestBaseFacet.sol"; +import { LibSwap } from "lifi/Libraries/LibSwap.sol"; import { GlacisFacet } from "lifi/Facets/GlacisFacet.sol"; import { IGlacisAirlift, QuoteSendInfo } from "lifi/Interfaces/IGlacisAirlift.sol"; @@ -21,29 +22,33 @@ contract TestGlacisFacet is GlacisFacet { contract GlacisFacetTest is TestBaseFacet { GlacisFacet.GlacisData internal validGlacisData; TestGlacisFacet internal glacisFacet; + uint256 internal defaultWORMHOLEAmount; + uint256 internal tokenFee; IGlacisAirlift internal constant airlift = IGlacisAirlift(0xE0A049955E18CFfd09C826C2c2e965439B6Ab272); - - ERC20 internal WORMHOLE_TOKEN_ARB = - ERC20(0xB0fFa8000886e57F86dd5264b9582b2Ad87b2b91); - - uint256 internal tokenFee; + address internal ADDRESS_WORMHOLE_TOKEN = + 0xB0fFa8000886e57F86dd5264b9582b2Ad87b2b91; + uint256 internal payableAmount = 1 ether; function setUp() public { customRpcUrlForForking = "ETH_NODE_URI_ARBITRUM"; customBlockNumberForForking = 295706031; initTestBase(); + defaultWORMHOLEAmount = + 1_000 * + 10 ** ERC20(ADDRESS_WORMHOLE_TOKEN).decimals(); + deal( - address(WORMHOLE_TOKEN_ARB), + ADDRESS_WORMHOLE_TOKEN, USER_SENDER, - 100_000 * 10 ** WORMHOLE_TOKEN_ARB.decimals() + 100_000 * 10 ** ERC20(ADDRESS_WORMHOLE_TOKEN).decimals() ); deal( - address(WORMHOLE_TOKEN_ARB), + ADDRESS_WORMHOLE_TOKEN, address(airlift), - 100_000 * 10 ** WORMHOLE_TOKEN_ARB.decimals() + 100_000 * 10 ** ERC20(ADDRESS_WORMHOLE_TOKEN).decimals() ); glacisFacet = new TestGlacisFacet(airlift); @@ -74,8 +79,8 @@ contract GlacisFacetTest is TestBaseFacet { // adjust bridgeData bridgeData.bridge = "glacis"; - bridgeData.sendingAssetId = address(WORMHOLE_TOKEN_ARB); - bridgeData.minAmount = 1 * 10 ** 18; + bridgeData.sendingAssetId = ADDRESS_WORMHOLE_TOKEN; + bridgeData.minAmount = defaultWORMHOLEAmount; bridgeData.destinationChainId = 10; // produce valid GlacisData @@ -84,30 +89,39 @@ contract GlacisFacetTest is TestBaseFacet { console.log( "============================ here0.1 ========================" ); - (bool ok, bytes memory result) = address(airlift).staticcall( - abi.encodeWithSignature( - "quoteSend(address,uint256,bytes32,uint256,address,uint256)", + // (bool ok, bytes memory result) = address(airlift).staticcall( + // abi.encodeWithSignature( + // "quoteSend(address,uint256,bytes32,uint256,address,uint256)", + // bridgeData.sendingAssetId, + // bridgeData.minAmount, + // bytes32(uint256(uint160(bridgeData.receiver))), + // bridgeData.destinationChainId, + // REFUND_WALLET, + // payableAmount // TODO + // ) + // ); + // require(ok); + QuoteSendInfo memory quoteSendInfo = IGlacisAirlift(address(airlift)) + .quoteSend( bridgeData.sendingAssetId, bridgeData.minAmount, bytes32(uint256(uint160(bridgeData.receiver))), bridgeData.destinationChainId, REFUND_WALLET, - 1 ether // TODO - ) - ); - require(ok); - QuoteSendInfo memory sendInfo = abi.decode(result, (QuoteSendInfo)); + payableAmount + ); + + // tokenFee = + // quoteSendInfo.gmpFee.tokenFee + + // quoteSendInfo.AirliftFeeInfo.airliftFee.tokenFee; // TODO Can we ignore tokenFee from smart contracts side? As far as I understand smart contract doesnt need to do any calculation with token fees. It will be only shown on the frontend side? - tokenFee = - sendInfo.gmpFee.tokenFee + - sendInfo.AirliftFeeInfo.airliftFee.tokenFee; addToMessageValue = - sendInfo.gmpFee.nativeFee + - sendInfo.AirliftFeeInfo.airliftFee.nativeFee; + quoteSendInfo.gmpFee.nativeFee + + quoteSendInfo.AirliftFeeInfo.airliftFee.nativeFee; } function initiateBridgeTxWithFacet(bool) internal override { - bridgeData.minAmount -= tokenFee; + // bridgeData.minAmount -= tokenFee; glacisFacet.startBridgeTokensViaGlacis{ value: addToMessageValue }( bridgeData, validGlacisData @@ -120,21 +134,23 @@ contract GlacisFacetTest is TestBaseFacet { function testBase_CanBridgeTokens() public - virtual override assertBalanceChange( - address(WORMHOLE_TOKEN_ARB), + ADDRESS_WORMHOLE_TOKEN, USER_SENDER, - -int256(defaultUSDCAmount) + -int256(defaultWORMHOLEAmount) ) - assertBalanceChange(address(WORMHOLE_TOKEN_ARB), USER_RECEIVER, 0) + assertBalanceChange(ADDRESS_WORMHOLE_TOKEN, USER_RECEIVER, 0) assertBalanceChange(ADDRESS_DAI, USER_SENDER, 0) assertBalanceChange(ADDRESS_DAI, USER_RECEIVER, 0) { vm.startPrank(USER_SENDER); // approval - WORMHOLE_TOKEN_ARB.approve(address(glacisFacet), bridgeData.minAmount); + ERC20(ADDRESS_WORMHOLE_TOKEN).approve( + address(glacisFacet), + bridgeData.minAmount + ); //prepare check for events vm.expectEmit(true, true, true, true, address(glacisFacet)); @@ -144,12 +160,111 @@ contract GlacisFacetTest is TestBaseFacet { vm.stopPrank(); } + // TODO + function testBase_CanBridgeTokens_fuzzed(uint256 amount) public override { + // // TODO can be related to this issue: https://github.com/glacislabs/airlift-evm/blob/main/test/tokens/MIM.t.sol#L23-L31 + // vm.assume(amount > 1_000 * 10 ** ERC20(ADDRESS_WORMHOLE_TOKEN).decimals() && amount < 100_000 * 10 ** ERC20(ADDRESS_WORMHOLE_TOKEN).decimals()); + // vm.startPrank(USER_SENDER); + // bridgeData.minAmount = amount; + // // approval + // ERC20(ADDRESS_WORMHOLE_TOKEN).approve(address(glacisFacet), bridgeData.minAmount); + // QuoteSendInfo memory quoteSendInfo = IGlacisAirlift(address(airlift)).quoteSend( + // bridgeData.sendingAssetId, + // bridgeData.minAmount, + // bytes32(uint256(uint160(bridgeData.receiver))), + // bridgeData.destinationChainId, + // REFUND_WALLET, + // payableAmount + // ); + // addToMessageValue = + // quoteSendInfo.gmpFee.nativeFee + + // quoteSendInfo.AirliftFeeInfo.airliftFee.nativeFee; + // //prepare check for events + // vm.expectEmit(true, true, true, true, address(glacisFacet)); + // emit LiFiTransferStarted(bridgeData); + // initiateBridgeTxWithFacet(false); + // vm.stopPrank(); + } + function testBase_CanSwapAndBridgeNativeTokens() public override { // facet does not support bridging of native assets } - function testBase_CanBridgeTokens_fuzzed(uint256 amount) public override { - // TODO + function setDefaultSwapDataSingleDAItoWORMHOLE() internal virtual { + delete swapData; + // Swap DAI -> USDC + address[] memory path = new address[](2); + path[0] = ADDRESS_DAI; + path[1] = ADDRESS_WORMHOLE_TOKEN; + + uint256 amountOut = defaultUSDCAmount; + + // Calculate DAI amount + uint256[] memory amounts = uniswap.getAmountsIn(amountOut, path); + uint256 amountIn = amounts[0]; + + swapData.push( + LibSwap.SwapData({ + callTo: address(uniswap), + approveTo: address(uniswap), + sendingAssetId: ADDRESS_DAI, + receivingAssetId: ADDRESS_WORMHOLE_TOKEN, + fromAmount: amountIn, + callData: abi.encodeWithSelector( + uniswap.swapExactTokensForTokens.selector, + amountIn, + amountOut, + path, + _facetTestContractAddress, + block.timestamp + 20 minutes + ), + requiresDeposit: true + }) + ); + } + + function testBase_CanSwapAndBridgeTokens() + public + virtual + override + assertBalanceChange( + ADDRESS_DAI, + USER_SENDER, + -int256(swapData[0].fromAmount) + ) + assertBalanceChange(ADDRESS_DAI, USER_RECEIVER, 0) + assertBalanceChange(ADDRESS_WORMHOLE_TOKEN, USER_SENDER, 0) + assertBalanceChange(ADDRESS_WORMHOLE_TOKEN, USER_RECEIVER, 0) + { + vm.startPrank(USER_SENDER); + + // prepare bridgeData + bridgeData.hasSourceSwaps = true; + + // reset swap data + setDefaultSwapDataSingleDAItoWORMHOLE(); + + // approval + dai.approve(_facetTestContractAddress, swapData[0].fromAmount); + + //prepare check for events + vm.expectEmit(true, true, true, true, _facetTestContractAddress); + emit AssetSwapped( + bridgeData.transactionId, + ADDRESS_UNISWAP_ARB, + ADDRESS_DAI, + ADDRESS_WORMHOLE_TOKEN, + swapData[0].fromAmount, + bridgeData.minAmount, + block.timestamp + ); + + vm.expectEmit(true, true, true, true, _facetTestContractAddress); + emit LiFiTransferStarted(bridgeData); + + // execute call in child contract + // TODO because there isnt any WORMHOLE pair on sushiswap + initiateSwapAndBridgeTxWithFacet(false); } function initiateSwapAndBridgeTxWithFacet(bool) internal override { From 4648dd44dd303832f52e2b658d3287b6662a4a2f Mon Sep 17 00:00:00 2001 From: Michal Date: Fri, 17 Jan 2025 17:36:00 +0100 Subject: [PATCH 06/38] Added GlacisFacet tests --- test/solidity/Facets/GlacisFacet.t.sol | 128 +++++++++++-------------- test/solidity/utils/Interfaces.sol | 11 +++ test/solidity/utils/TestBase.sol | 26 +++++ 3 files changed, 92 insertions(+), 73 deletions(-) diff --git a/test/solidity/Facets/GlacisFacet.t.sol b/test/solidity/Facets/GlacisFacet.t.sol index 749f71ac5..5de71b2b6 100644 --- a/test/solidity/Facets/GlacisFacet.t.sol +++ b/test/solidity/Facets/GlacisFacet.t.sol @@ -1,10 +1,11 @@ // SPDX-License-Identifier: Unlicense pragma solidity 0.8.17; -import { LibAllowList, TestBaseFacet, console, ERC20 } from "../utils/TestBaseFacet.sol"; +import { LibAllowList, TestBaseFacet, ERC20 } from "../utils/TestBaseFacet.sol"; import { LibSwap } from "lifi/Libraries/LibSwap.sol"; import { GlacisFacet } from "lifi/Facets/GlacisFacet.sol"; import { IGlacisAirlift, QuoteSendInfo } from "lifi/Interfaces/IGlacisAirlift.sol"; +import { InsufficientBalance } from "lifi/Errors/GenericErrors.sol"; // Stub GlacisFacet Contract contract TestGlacisFacet is GlacisFacet { @@ -22,6 +23,7 @@ contract TestGlacisFacet is GlacisFacet { contract GlacisFacetTest is TestBaseFacet { GlacisFacet.GlacisData internal validGlacisData; TestGlacisFacet internal glacisFacet; + ERC20 internal wormhole; uint256 internal defaultWORMHOLEAmount; uint256 internal tokenFee; @@ -29,6 +31,7 @@ contract GlacisFacetTest is TestBaseFacet { IGlacisAirlift(0xE0A049955E18CFfd09C826C2c2e965439B6Ab272); address internal ADDRESS_WORMHOLE_TOKEN = 0xB0fFa8000886e57F86dd5264b9582b2Ad87b2b91; + uint256 internal payableAmount = 1 ether; function setUp() public { @@ -36,19 +39,19 @@ contract GlacisFacetTest is TestBaseFacet { customBlockNumberForForking = 295706031; initTestBase(); - defaultWORMHOLEAmount = - 1_000 * - 10 ** ERC20(ADDRESS_WORMHOLE_TOKEN).decimals(); + wormhole = ERC20(ADDRESS_WORMHOLE_TOKEN); + + defaultWORMHOLEAmount = 1_000 * 10 ** wormhole.decimals(); deal( ADDRESS_WORMHOLE_TOKEN, USER_SENDER, - 100_000 * 10 ** ERC20(ADDRESS_WORMHOLE_TOKEN).decimals() + 100_000 * 10 ** wormhole.decimals() ); deal( ADDRESS_WORMHOLE_TOKEN, address(airlift), - 100_000 * 10 ** ERC20(ADDRESS_WORMHOLE_TOKEN).decimals() + 100_000 * 10 ** wormhole.decimals() ); glacisFacet = new TestGlacisFacet(airlift); @@ -64,7 +67,7 @@ contract GlacisFacetTest is TestBaseFacet { addFacet(diamond, address(glacisFacet), functionSelectors); glacisFacet = TestGlacisFacet(address(diamond)); - glacisFacet.addDex(ADDRESS_UNISWAP_ARB); + glacisFacet.addDex(ADDRESS_UNISWAP); glacisFacet.setFunctionApprovalBySignature( uniswap.swapExactTokensForTokens.selector ); @@ -86,21 +89,6 @@ contract GlacisFacetTest is TestBaseFacet { // produce valid GlacisData validGlacisData = GlacisFacet.GlacisData({ refund: REFUND_WALLET }); - console.log( - "============================ here0.1 ========================" - ); - // (bool ok, bytes memory result) = address(airlift).staticcall( - // abi.encodeWithSignature( - // "quoteSend(address,uint256,bytes32,uint256,address,uint256)", - // bridgeData.sendingAssetId, - // bridgeData.minAmount, - // bytes32(uint256(uint160(bridgeData.receiver))), - // bridgeData.destinationChainId, - // REFUND_WALLET, - // payableAmount // TODO - // ) - // ); - // require(ok); QuoteSendInfo memory quoteSendInfo = IGlacisAirlift(address(airlift)) .quoteSend( bridgeData.sendingAssetId, @@ -147,10 +135,7 @@ contract GlacisFacetTest is TestBaseFacet { vm.startPrank(USER_SENDER); // approval - ERC20(ADDRESS_WORMHOLE_TOKEN).approve( - address(glacisFacet), - bridgeData.minAmount - ); + wormhole.approve(address(glacisFacet), bridgeData.minAmount); //prepare check for events vm.expectEmit(true, true, true, true, address(glacisFacet)); @@ -163,11 +148,11 @@ contract GlacisFacetTest is TestBaseFacet { // TODO function testBase_CanBridgeTokens_fuzzed(uint256 amount) public override { // // TODO can be related to this issue: https://github.com/glacislabs/airlift-evm/blob/main/test/tokens/MIM.t.sol#L23-L31 - // vm.assume(amount > 1_000 * 10 ** ERC20(ADDRESS_WORMHOLE_TOKEN).decimals() && amount < 100_000 * 10 ** ERC20(ADDRESS_WORMHOLE_TOKEN).decimals()); + // vm.assume(amount > 1_000 * 10 ** wormhole.decimals() && amount < 100_000 * 10 ** wormhole.decimals()); // vm.startPrank(USER_SENDER); // bridgeData.minAmount = amount; // // approval - // ERC20(ADDRESS_WORMHOLE_TOKEN).approve(address(glacisFacet), bridgeData.minAmount); + // wormhole.approve(address(glacisFacet), bridgeData.minAmount); // QuoteSendInfo memory quoteSendInfo = IGlacisAirlift(address(airlift)).quoteSend( // bridgeData.sendingAssetId, // bridgeData.minAmount, @@ -192,12 +177,12 @@ contract GlacisFacetTest is TestBaseFacet { function setDefaultSwapDataSingleDAItoWORMHOLE() internal virtual { delete swapData; - // Swap DAI -> USDC + // Swap DAI -> WORMHOLE address[] memory path = new address[](2); path[0] = ADDRESS_DAI; path[1] = ADDRESS_WORMHOLE_TOKEN; - uint256 amountOut = defaultUSDCAmount; + uint256 amountOut = defaultWORMHOLEAmount; // Calculate DAI amount uint256[] memory amounts = uniswap.getAmountsIn(amountOut, path); @@ -225,22 +210,23 @@ contract GlacisFacetTest is TestBaseFacet { function testBase_CanSwapAndBridgeTokens() public - virtual override - assertBalanceChange( - ADDRESS_DAI, - USER_SENDER, - -int256(swapData[0].fromAmount) - ) assertBalanceChange(ADDRESS_DAI, USER_RECEIVER, 0) assertBalanceChange(ADDRESS_WORMHOLE_TOKEN, USER_SENDER, 0) assertBalanceChange(ADDRESS_WORMHOLE_TOKEN, USER_RECEIVER, 0) { - vm.startPrank(USER_SENDER); + // add liquidity for dex pair + addLiquidity( + ADDRESS_DAI, + ADDRESS_WORMHOLE_TOKEN, + 100_000 * 10 ** ERC20(ADDRESS_DAI).decimals(), + 100_000 * 10 ** wormhole.decimals() + ); + uint256 initialDAIBalance = dai.balanceOf(USER_SENDER); + vm.startPrank(USER_SENDER); // prepare bridgeData bridgeData.hasSourceSwaps = true; - // reset swap data setDefaultSwapDataSingleDAItoWORMHOLE(); @@ -251,7 +237,7 @@ contract GlacisFacetTest is TestBaseFacet { vm.expectEmit(true, true, true, true, _facetTestContractAddress); emit AssetSwapped( bridgeData.transactionId, - ADDRESS_UNISWAP_ARB, + address(uniswap), ADDRESS_DAI, ADDRESS_WORMHOLE_TOKEN, swapData[0].fromAmount, @@ -261,10 +247,15 @@ contract GlacisFacetTest is TestBaseFacet { vm.expectEmit(true, true, true, true, _facetTestContractAddress); emit LiFiTransferStarted(bridgeData); - - // execute call in child contract - // TODO because there isnt any WORMHOLE pair on sushiswap + uint256 initialETHBalance = USER_SENDER.balance; initiateSwapAndBridgeTxWithFacet(false); + + // check balances after call + assertEq( + dai.balanceOf(USER_SENDER), + initialDAIBalance - swapData[0].fromAmount + ); + assertEq(USER_SENDER.balance, initialETHBalance - addToMessageValue); } function initiateSwapAndBridgeTxWithFacet(bool) internal override { @@ -277,35 +268,26 @@ contract GlacisFacetTest is TestBaseFacet { function test_CanSwapAndBridgeAndPayFeeWithBridgedToken() public {} - // 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 testBase_Revert_CallerHasInsufficientFunds() public override { + vm.startPrank(USER_SENDER); + + wormhole.approve( + address(_facetTestContractAddress), + defaultWORMHOLEAmount + ); + + // send all available W balance to different account to ensure sending wallet has no W funds + wormhole.transfer(USER_RECEIVER, wormhole.balanceOf(USER_SENDER)); + + vm.expectRevert( + abi.encodeWithSelector( + InsufficientBalance.selector, + bridgeData.minAmount, + 0 + ) + ); + + initiateBridgeTxWithFacet(false); + vm.stopPrank(); + } } diff --git a/test/solidity/utils/Interfaces.sol b/test/solidity/utils/Interfaces.sol index 4265c35e2..7928b12c1 100644 --- a/test/solidity/utils/Interfaces.sol +++ b/test/solidity/utils/Interfaces.sol @@ -57,4 +57,15 @@ interface UniswapV2Router02 { uint256 amountIn, address[] calldata path ) external view returns (uint256[] memory amounts); + + function addLiquidity( + address tokenA, + address tokenB, + uint amountADesired, + uint amountBDesired, + uint amountAMin, + uint amountBMin, + address to, + uint deadline + ) external returns (uint amountA, uint amountB, uint liquidity); } diff --git a/test/solidity/utils/TestBase.sol b/test/solidity/utils/TestBase.sol index 14813f95d..9dc4f50bc 100644 --- a/test/solidity/utils/TestBase.sol +++ b/test/solidity/utils/TestBase.sol @@ -443,6 +443,32 @@ abstract contract TestBase is Test, DiamondTest, ILiFi { ); } + function addLiquidity( + address tokenA, + address tokenB, + uint amountADesired, + uint amountBDesired + ) internal returns (uint amountA, uint amountB, uint liquidity) { + deal(tokenA, address(this), amountADesired); + deal(tokenB, address(this), amountBDesired); + + ERC20(tokenA).approve(address(uniswap), amountADesired); + ERC20(tokenB).approve(address(uniswap), amountBDesired); + + (amountA, amountB, liquidity) = uniswap.addLiquidity( + tokenA, + tokenB, + amountADesired, + amountBDesired, + 0, + 0, + address(this), + block.timestamp + ); + + return (amountA, amountB, liquidity); + } + //#region Utility Functions (may be used in tests) function printBridgeData(ILiFi.BridgeData memory _bridgeData) internal { From c7155e32953f1a03bd67ae540d8df52c11d8506a Mon Sep 17 00:00:00 2001 From: Michal Date: Fri, 17 Jan 2025 18:30:48 +0100 Subject: [PATCH 07/38] Take nativeFee offchain --- src/Facets/GlacisFacet.sol | 65 ++++---------------------- test/solidity/Facets/GlacisFacet.t.sol | 14 +++--- 2 files changed, 16 insertions(+), 63 deletions(-) diff --git a/src/Facets/GlacisFacet.sol b/src/Facets/GlacisFacet.sol index 27c5373d4..6e8cdb8ca 100644 --- a/src/Facets/GlacisFacet.sol +++ b/src/Facets/GlacisFacet.sol @@ -10,15 +10,12 @@ import { SwapperV2 } from "../Helpers/SwapperV2.sol"; import { Validatable } from "../Helpers/Validatable.sol"; import { IGlacisAirlift, QuoteSendInfo } from "../Interfaces/IGlacisAirlift.sol"; -import { console } from "forge-std/console.sol"; - /// @title Glacis Facet /// @author LI.FI (https://li.fi/) /// @notice Integration of the Glacis airlift (wrapper for native token bridging standards) /// @custom:version 1.0.0 contract GlacisFacet is ILiFi, ReentrancyGuard, SwapperV2, Validatable { /// Storage /// - bytes32 internal constant NAMESPACE = keccak256("com.lifi.facets.glacis"); // Optional. Only use if you need to store data in the diamond storage. IGlacisAirlift public immutable airlift; @@ -26,9 +23,11 @@ contract GlacisFacet is ILiFi, ReentrancyGuard, SwapperV2, Validatable { /// Types /// /// @param refund Refund address + /// @param nativeFee TODO // TODO struct GlacisData { address refund; + uint256 nativeFee; } /// Constructor /// @@ -59,8 +58,7 @@ contract GlacisFacet is ILiFi, ReentrancyGuard, SwapperV2, Validatable { _bridgeData.sendingAssetId, _bridgeData.minAmount ); - uint256 fees = _calculateFees(_bridgeData, _glacisData); - _startBridge(_bridgeData, _glacisData, fees); + _startBridge(_bridgeData, _glacisData); } /// @notice Performs a swap before bridging via Glacis @@ -80,15 +78,13 @@ contract GlacisFacet is ILiFi, ReentrancyGuard, SwapperV2, Validatable { doesNotContainDestinationCalls(_bridgeData) validateBridgeData(_bridgeData) { - uint256 fees = _calculateFees(_bridgeData, _glacisData); _bridgeData.minAmount = _depositAndSwap( _bridgeData.transactionId, _bridgeData.minAmount, _swapData, - payable(msg.sender), - fees + payable(msg.sender) ); - _startBridge(_bridgeData, _glacisData, fees); + _startBridge(_bridgeData, _glacisData); } /// Internal Methods /// @@ -98,11 +94,9 @@ contract GlacisFacet is ILiFi, ReentrancyGuard, SwapperV2, Validatable { /// @param _glacisData Data specific to Glacis function _startBridge( ILiFi.BridgeData memory _bridgeData, - GlacisData calldata _glacisData, - uint256 _fee + GlacisData calldata _glacisData ) internal { bytes32 receiver = bytes32(uint256(uint160(_bridgeData.receiver))); - // uint256 tokenFee = sendInfo.gmpFee.tokenFee + sendInfo.AirliftFeeInfo.airliftFee.tokenFee; // TODO if (!LibAsset.isNativeAsset(_bridgeData.sendingAssetId)) { // Give the Airlift approval to bridge tokens @@ -111,7 +105,7 @@ contract GlacisFacet is ILiFi, ReentrancyGuard, SwapperV2, Validatable { address(airlift), _bridgeData.minAmount ); - airlift.send{ value: _fee }( + airlift.send{ value: _glacisData.nativeFee }( _bridgeData.sendingAssetId, _bridgeData.minAmount, receiver, @@ -120,7 +114,9 @@ contract GlacisFacet is ILiFi, ReentrancyGuard, SwapperV2, Validatable { ); } else { // cant have tokenFee is it's native asset bridging - airlift.send{ value: _bridgeData.minAmount + _fee }( + airlift.send{ + value: _bridgeData.minAmount + _glacisData.nativeFee + }( _bridgeData.sendingAssetId, _bridgeData.minAmount, receiver, @@ -131,45 +127,4 @@ contract GlacisFacet is ILiFi, ReentrancyGuard, SwapperV2, Validatable { emit LiFiTransferStarted(_bridgeData); } - - /// Private Methods /// - - function _calculateFees( - ILiFi.BridgeData memory _bridgeData, - GlacisData calldata _glacisData - ) internal returns (uint256 nativeFees) { - console.log( - "============================ _calculateFees 0.1 ========================" - ); - (bool ok, bytes memory result) = address(airlift).staticcall( - abi.encodeWithSignature( - "quoteSend(address,uint256,bytes32,uint256,address,uint256)", - _bridgeData.sendingAssetId, - _bridgeData.minAmount, - bytes32(uint256(uint160(_bridgeData.receiver))), - _bridgeData.destinationChainId, - _glacisData.refund, - 1 ether // TODO!!! - // !LibAsset.isNativeAsset(_bridgeData.sendingAssetId) ? 0 : _bridgeData.minAmount - ) - ); - console.log( - "============================ _calculateFees 0.2 ========================" - ); - // TODO require ok - QuoteSendInfo memory sendInfo = abi.decode(result, (QuoteSendInfo)); - console.log( - "============================ _calculateFees 0.3 ========================" - ); - - uint256 nativeAssetAmount = sendInfo.gmpFee.nativeFee + - sendInfo.AirliftFeeInfo.airliftFee.nativeFee; - console.log( - "============================ _calculateFees -> nativeAssetAmount ========================" - ); - console.log(nativeAssetAmount); - nativeFees = - sendInfo.gmpFee.nativeFee + - sendInfo.AirliftFeeInfo.airliftFee.nativeFee; - } } diff --git a/test/solidity/Facets/GlacisFacet.t.sol b/test/solidity/Facets/GlacisFacet.t.sol index 5de71b2b6..e8f0fc3a5 100644 --- a/test/solidity/Facets/GlacisFacet.t.sol +++ b/test/solidity/Facets/GlacisFacet.t.sol @@ -86,9 +86,6 @@ contract GlacisFacetTest is TestBaseFacet { bridgeData.minAmount = defaultWORMHOLEAmount; bridgeData.destinationChainId = 10; - // produce valid GlacisData - validGlacisData = GlacisFacet.GlacisData({ refund: REFUND_WALLET }); - QuoteSendInfo memory quoteSendInfo = IGlacisAirlift(address(airlift)) .quoteSend( bridgeData.sendingAssetId, @@ -99,17 +96,18 @@ contract GlacisFacetTest is TestBaseFacet { payableAmount ); - // tokenFee = - // quoteSendInfo.gmpFee.tokenFee + - // quoteSendInfo.AirliftFeeInfo.airliftFee.tokenFee; // TODO Can we ignore tokenFee from smart contracts side? As far as I understand smart contract doesnt need to do any calculation with token fees. It will be only shown on the frontend side? - addToMessageValue = quoteSendInfo.gmpFee.nativeFee + quoteSendInfo.AirliftFeeInfo.airliftFee.nativeFee; + + // produce valid GlacisData + validGlacisData = GlacisFacet.GlacisData({ + refund: REFUND_WALLET, + nativeFee: addToMessageValue + }); } function initiateBridgeTxWithFacet(bool) internal override { - // bridgeData.minAmount -= tokenFee; glacisFacet.startBridgeTokensViaGlacis{ value: addToMessageValue }( bridgeData, validGlacisData From aeb0b784d2ced8f1bf080e0881c1d62869845f9c Mon Sep 17 00:00:00 2001 From: Michal Date: Sat, 18 Jan 2025 09:56:13 +0100 Subject: [PATCH 08/38] GlacisFacet updated. Desc adjustments. Removed handling of native assets --- src/Facets/GlacisFacet.sol | 46 +++++++++++++------------------------- 1 file changed, 15 insertions(+), 31 deletions(-) diff --git a/src/Facets/GlacisFacet.sol b/src/Facets/GlacisFacet.sol index 6e8cdb8ca..ff9950130 100644 --- a/src/Facets/GlacisFacet.sol +++ b/src/Facets/GlacisFacet.sol @@ -16,15 +16,14 @@ import { IGlacisAirlift, QuoteSendInfo } from "../Interfaces/IGlacisAirlift.sol" /// @custom:version 1.0.0 contract GlacisFacet is ILiFi, ReentrancyGuard, SwapperV2, Validatable { /// Storage /// - bytes32 internal constant NAMESPACE = keccak256("com.lifi.facets.glacis"); // Optional. Only use if you need to store data in the diamond storage. + /// @notice The contract address of the glacis airlift on the source chain. IGlacisAirlift public immutable airlift; /// Types /// /// @param refund Refund address - /// @param nativeFee TODO - // TODO + /// @param nativeFee The fee amount in native token required by the Glacis Airlift. struct GlacisData { address refund; uint256 nativeFee; @@ -96,34 +95,19 @@ contract GlacisFacet is ILiFi, ReentrancyGuard, SwapperV2, Validatable { ILiFi.BridgeData memory _bridgeData, GlacisData calldata _glacisData ) internal { - bytes32 receiver = bytes32(uint256(uint160(_bridgeData.receiver))); - - if (!LibAsset.isNativeAsset(_bridgeData.sendingAssetId)) { - // Give the Airlift approval to bridge tokens - LibAsset.maxApproveERC20( - IERC20(_bridgeData.sendingAssetId), - address(airlift), - _bridgeData.minAmount - ); - airlift.send{ value: _glacisData.nativeFee }( - _bridgeData.sendingAssetId, - _bridgeData.minAmount, - receiver, - _bridgeData.destinationChainId, - _glacisData.refund - ); - } else { - // cant have tokenFee is it's native asset bridging - airlift.send{ - value: _bridgeData.minAmount + _glacisData.nativeFee - }( - _bridgeData.sendingAssetId, - _bridgeData.minAmount, - receiver, - _bridgeData.destinationChainId, - _glacisData.refund - ); - } + // Give the Airlift approval to bridge tokens + LibAsset.maxApproveERC20( + IERC20(_bridgeData.sendingAssetId), + address(airlift), + _bridgeData.minAmount + ); + airlift.send{ value: _glacisData.nativeFee }( + _bridgeData.sendingAssetId, + _bridgeData.minAmount, + bytes32(uint256(uint160(_bridgeData.receiver))), + _bridgeData.destinationChainId, + _glacisData.refund + ); emit LiFiTransferStarted(_bridgeData); } From 2550324eb088ec0e5d1d9d7dc39f06e80067ca8b Mon Sep 17 00:00:00 2001 From: Michal Date: Sat, 18 Jan 2025 09:57:30 +0100 Subject: [PATCH 09/38] Updated deploy, update scripts, config, docs --- config/glacis.json | 19 ++++------ docs/GlacisFacet.md | 8 +++-- script/deploy/facets/DeployGlacisFacet.s.sol | 5 ++- script/deploy/facets/UpdateGlacisFacet.s.sol | 38 -------------------- 4 files changed, 14 insertions(+), 56 deletions(-) diff --git a/config/glacis.json b/config/glacis.json index bcf06aa51..911db59b9 100644 --- a/config/glacis.json +++ b/config/glacis.json @@ -1,16 +1,11 @@ { - "mainnet": { - "example": "0x0000000000000000000000000000000000000000", - "exampleAllowedTokens": [ - "0x0000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000" - ] - }, "arbitrum": { - "example": "0x0000000000000000000000000000000000000000", - "exampleAllowedTokens": [ - "0x0000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000" - ] + "airlift": "0xE0A049955E18CFfd09C826C2c2e965439B6Ab272" + }, + "optimism": { + "airlift": "0x1B388F7ee9e44BD5aA0b13ca9dF35F2489F1c717" + }, + "base": { + "airlift": "0x56e20a6260644cc9f0b7d79a8c8e1e3fabc15cea" } } diff --git a/docs/GlacisFacet.md b/docs/GlacisFacet.md index 3a4ad7337..e741a441d 100644 --- a/docs/GlacisFacet.md +++ b/docs/GlacisFacet.md @@ -22,9 +22,11 @@ graph LR; The methods listed above take a variable labeled `_glacisData`. This data is specific to glacis and is represented as the following struct type: ```solidity -/// @param example Example parameter. -struct glacisData { - string example; +/// @param refund Refund address +/// @param nativeFee The fee amount in native token required by the Glacis Airlift. +struct GlacisData { + address refund; + uint256 nativeFee; } ``` diff --git a/script/deploy/facets/DeployGlacisFacet.s.sol b/script/deploy/facets/DeployGlacisFacet.s.sol index 9351e7800..f88fde884 100644 --- a/script/deploy/facets/DeployGlacisFacet.s.sol +++ b/script/deploy/facets/DeployGlacisFacet.s.sol @@ -20,14 +20,13 @@ contract DeployScript is DeployScriptBase { } function getConstructorArgs() internal override returns (bytes memory) { - // If you don't have a constructor or it doesn't take any arguments, you can remove this function string memory path = string.concat(root, "/config/glacis.json"); string memory json = vm.readFile(path); - address example = json.readAddress( + address airlift = json.readAddress( string.concat(".", network, ".example") ); - return abi.encode(example); + return abi.encode(airlift); } } diff --git a/script/deploy/facets/UpdateGlacisFacet.s.sol b/script/deploy/facets/UpdateGlacisFacet.s.sol index fd1011103..63f37bb9c 100644 --- a/script/deploy/facets/UpdateGlacisFacet.s.sol +++ b/script/deploy/facets/UpdateGlacisFacet.s.sol @@ -2,50 +2,12 @@ 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 { GlacisFacet } from "lifi/Facets/GlacisFacet.sol"; contract DeployScript is UpdateScriptBase { - using stdJson for string; - - struct Config { - uint256 a; - bool b; - address c; - } - function run() public returns (address[] memory facets, bytes memory cutData) { return update("GlacisFacet"); } - - function getExcludes() internal pure override returns (bytes4[] memory) { - // Use this to exclude any selectors that might clash with other facets in the diamond - // or selectors you don't want accessible e.g. init() functions. - // You can remove this function if it's not needed. - bytes4[] memory excludes = new bytes4[](1); - - return excludes; - } - - function getCallData() internal override returns (bytes memory) { - // Use this to get initialization calldata that will be executed - // when adding the facet to a diamond. - // You can remove this function it it's not needed. - path = string.concat(root, "/config/glacis.json"); - json = vm.readFile(path); - bytes memory rawConfigs = json.parseRaw(".configs"); - Config[] memory cfg = abi.decode(rawConfigs, (Config[])); - - // bytes memory callData = abi.encodeWithSelector( - // GlacisFacet.initGlacis.selector, - // cfg - // ); - bytes memory callData = abi.encodePacked(address(0x22)); - - return callData; - } } From b4767779fad075724540d7adacc0846a95dd5562 Mon Sep 17 00:00:00 2001 From: Michal Date: Mon, 20 Jan 2025 12:48:42 +0100 Subject: [PATCH 10/38] Added demoScript --- script/demoScripts/demoGlacisAirlift.ts | 94 ++++++++++++++++++++ script/deploy/facets/DeployGlacisFacet.s.sol | 2 +- test/solidity/Facets/GlacisFacet.t.sol | 56 ++++++------ 3 files changed, 123 insertions(+), 29 deletions(-) create mode 100644 script/demoScripts/demoGlacisAirlift.ts diff --git a/script/demoScripts/demoGlacisAirlift.ts b/script/demoScripts/demoGlacisAirlift.ts new file mode 100644 index 000000000..479f2f37a --- /dev/null +++ b/script/demoScripts/demoGlacisAirlift.ts @@ -0,0 +1,94 @@ +import { providers, Wallet, utils, constants } from 'ethers' +import chalk from 'chalk' +import { GlacisFacet__factory, ERC20__factory } from '../../typechain' +import { node_url } from '../utils/network' +import config from '../../config/glacis.json' + +const msg = (msg: string) => { + console.log(chalk.green(msg)) +} + +const LIFI_ADDRESS = '0x9DD11f4fc672006EA9E666b6a222C5A8141f2Ac0' // TODO +const WORMHOLE_ADDRESS = '0xB0fFa8000886e57F86dd5264b9582b2Ad87b2b91' +const amountToBridge = '1' +const destinationChainId = 10 // Optimism + +async function main() { + msg(`Transfer ${amountToBridge} Wormhole on Arbitrum to Wormhole on Optimism`) + + let wallet = Wallet.fromMnemonic(process.env.MNEMONIC) + const provider1 = new providers.JsonRpcProvider(node_url('arbitrum')) + const provider = new providers.FallbackProvider([provider1]) + wallet = wallet.connect(provider) + const walletAddress = await wallet.getAddress() + + const lifi = GlacisFacet__factory.connect(LIFI_ADDRESS, wallet) + + const token = ERC20__factory.connect(WORMHOLE_ADDRESS, wallet) + const amount = utils.parseEther(amountToBridge) + + const allowance = await token.allowance(walletAddress, LIFI_ADDRESS) + if (amount.gt(allowance)) { + await token.approve(lifi.address, amount) + + msg('Token approved for swapping') + } + + const lifiData = { + transactionId: utils.randomBytes(32), + integrator: 'ACME Devs', // TODO + referrer: constants.AddressZero, + sendingAssetId: WORMHOLE_ADDRESS, + receivingAssetId: constants.AddressZero, + receiver: walletAddress, + destinationChainId: destinationChainId, + amount: amount, + } + + // calculate native fee + const estimatedFees = await airlift.quoteSend.staticCall( + routes[routeIndex].src_erc20, + amount, + receiver, + routes[routeIndex].dst_chain_id, + refund, + value + ) + const structuredFees = { + gmpFee: { + nativeFee: estimatedFees[0][0], + tokenFee: estimatedFees[0][1], + }, + airliftFee: { + nativeFee: estimatedFees[3][0][0], + tokenFee: estimatedFees[3][0][1], + }, + } + console.log(structuredFees) + const estimatedValue = + structuredFees.gmpFee.nativeFee + structuredFees.airliftFee.nativeFee + + const glacisBridgeData = { + receiver: walletAddress, + amount: amount, + } + + const trx = await lifi.startBridgeTokensViaGlacis( + lifiData, + gnosisBridgeData, + { + gasLimit: 500000, + } + ) + + msg('Bridge process started on sending chain') + + await trx.wait() +} + +main() + .then(() => process.exit(0)) + .catch((error) => { + console.error(error) + process.exit(1) + }) diff --git a/script/deploy/facets/DeployGlacisFacet.s.sol b/script/deploy/facets/DeployGlacisFacet.s.sol index f88fde884..ceab5eef4 100644 --- a/script/deploy/facets/DeployGlacisFacet.s.sol +++ b/script/deploy/facets/DeployGlacisFacet.s.sol @@ -24,7 +24,7 @@ contract DeployScript is DeployScriptBase { string memory json = vm.readFile(path); address airlift = json.readAddress( - string.concat(".", network, ".example") + string.concat(".", network, ".airlift") ); return abi.encode(airlift); diff --git a/test/solidity/Facets/GlacisFacet.t.sol b/test/solidity/Facets/GlacisFacet.t.sol index e8f0fc3a5..00a201149 100644 --- a/test/solidity/Facets/GlacisFacet.t.sol +++ b/test/solidity/Facets/GlacisFacet.t.sol @@ -114,6 +114,12 @@ contract GlacisFacetTest is TestBaseFacet { ); } + function initiateSwapAndBridgeTxWithFacet(bool) internal override { + glacisFacet.swapAndStartBridgeTokensViaGlacis{ + value: addToMessageValue + }(bridgeData, swapData, validGlacisData); + } + function testBase_CanBridgeNativeTokens() public override { // facet does not support bridging of native assets } @@ -145,28 +151,28 @@ contract GlacisFacetTest is TestBaseFacet { // TODO function testBase_CanBridgeTokens_fuzzed(uint256 amount) public override { - // // TODO can be related to this issue: https://github.com/glacislabs/airlift-evm/blob/main/test/tokens/MIM.t.sol#L23-L31 - // vm.assume(amount > 1_000 * 10 ** wormhole.decimals() && amount < 100_000 * 10 ** wormhole.decimals()); - // vm.startPrank(USER_SENDER); - // bridgeData.minAmount = amount; - // // approval - // wormhole.approve(address(glacisFacet), bridgeData.minAmount); - // QuoteSendInfo memory quoteSendInfo = IGlacisAirlift(address(airlift)).quoteSend( - // bridgeData.sendingAssetId, - // bridgeData.minAmount, - // bytes32(uint256(uint160(bridgeData.receiver))), - // bridgeData.destinationChainId, - // REFUND_WALLET, - // payableAmount - // ); - // addToMessageValue = - // quoteSendInfo.gmpFee.nativeFee + - // quoteSendInfo.AirliftFeeInfo.airliftFee.nativeFee; - // //prepare check for events - // vm.expectEmit(true, true, true, true, address(glacisFacet)); - // emit LiFiTransferStarted(bridgeData); - // initiateBridgeTxWithFacet(false); - // vm.stopPrank(); + // TODO can be related to this issue: https://github.com/glacislabs/airlift-evm/blob/main/test/tokens/MIM.t.sol#L23-L31 + // vm.assume(amount > 1_000 * 10 ** wormhole.decimals() && amount < 100_000 * 10 ** wormhole.decimals()); + // vm.startPrank(USER_SENDER); + // bridgeData.minAmount = amount; + // // approval + // wormhole.approve(address(glacisFacet), bridgeData.minAmount); + // QuoteSendInfo memory quoteSendInfo = IGlacisAirlift(address(airlift)).quoteSend( + // bridgeData.sendingAssetId, + // bridgeData.minAmount, + // bytes32(uint256(uint160(bridgeData.receiver))), + // bridgeData.destinationChainId, + // REFUND_WALLET, + // payableAmount + // ); + // addToMessageValue = + // quoteSendInfo.gmpFee.nativeFee + + // quoteSendInfo.AirliftFeeInfo.airliftFee.nativeFee; + // //prepare check for events + // vm.expectEmit(true, true, true, true, address(glacisFacet)); + // emit LiFiTransferStarted(bridgeData); + // initiateBridgeTxWithFacet(false); + // vm.stopPrank(); } function testBase_CanSwapAndBridgeNativeTokens() public override { @@ -256,12 +262,6 @@ contract GlacisFacetTest is TestBaseFacet { assertEq(USER_SENDER.balance, initialETHBalance - addToMessageValue); } - function initiateSwapAndBridgeTxWithFacet(bool) internal override { - glacisFacet.swapAndStartBridgeTokensViaGlacis{ - value: addToMessageValue - }(bridgeData, swapData, validGlacisData); - } - function test_CanBridgeAndPayFeeWithBridgedToken() public {} function test_CanSwapAndBridgeAndPayFeeWithBridgedToken() public {} From d1b49e013614697623b2689c266e94d66fedd1bb Mon Sep 17 00:00:00 2001 From: Michal Date: Tue, 21 Jan 2025 08:54:26 +0100 Subject: [PATCH 11/38] Updates --- script/demoScripts/demoGlacisAirlift.ts | 27 ++++++------ src/Facets/GlacisFacet.sol | 11 +---- test/solidity/Facets/GlacisFacet.t.sol | 56 ++++++++++++------------- 3 files changed, 45 insertions(+), 49 deletions(-) diff --git a/script/demoScripts/demoGlacisAirlift.ts b/script/demoScripts/demoGlacisAirlift.ts index 479f2f37a..2f18d9c1b 100644 --- a/script/demoScripts/demoGlacisAirlift.ts +++ b/script/demoScripts/demoGlacisAirlift.ts @@ -1,4 +1,4 @@ -import { providers, Wallet, utils, constants } from 'ethers' +import { providers, Wallet, utils, constants, Contract } from 'ethers' import chalk from 'chalk' import { GlacisFacet__factory, ERC20__factory } from '../../typechain' import { node_url } from '../utils/network' @@ -8,21 +8,20 @@ const msg = (msg: string) => { console.log(chalk.green(msg)) } -const LIFI_ADDRESS = '0x9DD11f4fc672006EA9E666b6a222C5A8141f2Ac0' // TODO -const WORMHOLE_ADDRESS = '0xB0fFa8000886e57F86dd5264b9582b2Ad87b2b91' +const LIFI_ADDRESS = '0x1231DEB6f5749EF6cE6943a275A1D3E7486F4EaE' // LIFI Diamond on Arbitrum +const WORMHOLE_ADDRESS = '0xB0fFa8000886e57F86dd5264b9582b2Ad87b2b91' // Wormhole token on Arbitrum const amountToBridge = '1' const destinationChainId = 10 // Optimism async function main() { msg(`Transfer ${amountToBridge} Wormhole on Arbitrum to Wormhole on Optimism`) - let wallet = Wallet.fromMnemonic(process.env.MNEMONIC) const provider1 = new providers.JsonRpcProvider(node_url('arbitrum')) const provider = new providers.FallbackProvider([provider1]) wallet = wallet.connect(provider) const walletAddress = await wallet.getAddress() - const lifi = GlacisFacet__factory.connect(LIFI_ADDRESS, wallet) + const lifi = GlacisFacet__factory.connect(LIFI_ADDRESS, wallet) as any const token = ERC20__factory.connect(WORMHOLE_ADDRESS, wallet) const amount = utils.parseEther(amountToBridge) @@ -36,7 +35,7 @@ async function main() { const lifiData = { transactionId: utils.randomBytes(32), - integrator: 'ACME Devs', // TODO + integrator: 'ACME Devs', referrer: constants.AddressZero, sendingAssetId: WORMHOLE_ADDRESS, receivingAssetId: constants.AddressZero, @@ -45,14 +44,17 @@ async function main() { amount: amount, } + const airlift = new Contract(config.arbitrum.airlift, [ + 'function send(address token, uint256 amount, bytes32 receiver, uint256 destinationChainId, address refundAddress) external payable', + ]) // calculate native fee const estimatedFees = await airlift.quoteSend.staticCall( - routes[routeIndex].src_erc20, + WORMHOLE_ADDRESS, amount, - receiver, - routes[routeIndex].dst_chain_id, - refund, - value + walletAddress, + destinationChainId, + walletAddress, //refund + utils.parseEther('1') ) const structuredFees = { gmpFee: { @@ -75,8 +77,9 @@ async function main() { const trx = await lifi.startBridgeTokensViaGlacis( lifiData, - gnosisBridgeData, + glacisBridgeData, { + value: estimatedValue, gasLimit: 500000, } ) diff --git a/src/Facets/GlacisFacet.sol b/src/Facets/GlacisFacet.sol index ff9950130..a46691126 100644 --- a/src/Facets/GlacisFacet.sol +++ b/src/Facets/GlacisFacet.sol @@ -2,13 +2,12 @@ pragma solidity ^0.8.17; import { ILiFi } from "../Interfaces/ILiFi.sol"; -import { LibDiamond } from "../Libraries/LibDiamond.sol"; -import { LibAsset, IERC20 } from "../Libraries/LibAsset.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 { IGlacisAirlift, QuoteSendInfo } from "../Interfaces/IGlacisAirlift.sol"; +import { IGlacisAirlift } from "../Interfaces/IGlacisAirlift.sol"; /// @title Glacis Facet /// @author LI.FI (https://li.fi/) @@ -95,12 +94,6 @@ contract GlacisFacet is ILiFi, ReentrancyGuard, SwapperV2, Validatable { ILiFi.BridgeData memory _bridgeData, GlacisData calldata _glacisData ) internal { - // Give the Airlift approval to bridge tokens - LibAsset.maxApproveERC20( - IERC20(_bridgeData.sendingAssetId), - address(airlift), - _bridgeData.minAmount - ); airlift.send{ value: _glacisData.nativeFee }( _bridgeData.sendingAssetId, _bridgeData.minAmount, diff --git a/test/solidity/Facets/GlacisFacet.t.sol b/test/solidity/Facets/GlacisFacet.t.sol index 00a201149..a91063219 100644 --- a/test/solidity/Facets/GlacisFacet.t.sol +++ b/test/solidity/Facets/GlacisFacet.t.sol @@ -36,7 +36,7 @@ contract GlacisFacetTest is TestBaseFacet { function setUp() public { customRpcUrlForForking = "ETH_NODE_URI_ARBITRUM"; - customBlockNumberForForking = 295706031; + customBlockNumberForForking = 297418708; initTestBase(); wormhole = ERC20(ADDRESS_WORMHOLE_TOKEN); @@ -46,12 +46,12 @@ contract GlacisFacetTest is TestBaseFacet { deal( ADDRESS_WORMHOLE_TOKEN, USER_SENDER, - 100_000 * 10 ** wormhole.decimals() + 500_000 * 10 ** wormhole.decimals() ); deal( ADDRESS_WORMHOLE_TOKEN, address(airlift), - 100_000 * 10 ** wormhole.decimals() + 500_000 * 10 ** wormhole.decimals() ); glacisFacet = new TestGlacisFacet(airlift); @@ -152,27 +152,31 @@ contract GlacisFacetTest is TestBaseFacet { // TODO function testBase_CanBridgeTokens_fuzzed(uint256 amount) public override { // TODO can be related to this issue: https://github.com/glacislabs/airlift-evm/blob/main/test/tokens/MIM.t.sol#L23-L31 - // vm.assume(amount > 1_000 * 10 ** wormhole.decimals() && amount < 100_000 * 10 ** wormhole.decimals()); - // vm.startPrank(USER_SENDER); - // bridgeData.minAmount = amount; - // // approval - // wormhole.approve(address(glacisFacet), bridgeData.minAmount); - // QuoteSendInfo memory quoteSendInfo = IGlacisAirlift(address(airlift)).quoteSend( - // bridgeData.sendingAssetId, - // bridgeData.minAmount, - // bytes32(uint256(uint160(bridgeData.receiver))), - // bridgeData.destinationChainId, - // REFUND_WALLET, - // payableAmount - // ); - // addToMessageValue = - // quoteSendInfo.gmpFee.nativeFee + - // quoteSendInfo.AirliftFeeInfo.airliftFee.nativeFee; - // //prepare check for events - // vm.expectEmit(true, true, true, true, address(glacisFacet)); - // emit LiFiTransferStarted(bridgeData); - // initiateBridgeTxWithFacet(false); - // vm.stopPrank(); + vm.assume( + amount > 0 * 10 ** wormhole.decimals() && + amount < 100_000 * 10 ** wormhole.decimals() + ); + vm.startPrank(USER_SENDER); + bridgeData.minAmount = amount; + // approval + wormhole.approve(address(glacisFacet), bridgeData.minAmount); + QuoteSendInfo memory quoteSendInfo = IGlacisAirlift(address(airlift)) + .quoteSend( + bridgeData.sendingAssetId, + bridgeData.minAmount, + bytes32(uint256(uint160(bridgeData.receiver))), + bridgeData.destinationChainId, + REFUND_WALLET, + payableAmount + ); + addToMessageValue = + quoteSendInfo.gmpFee.nativeFee + + quoteSendInfo.AirliftFeeInfo.airliftFee.nativeFee; + //prepare check for events + vm.expectEmit(true, true, true, true, address(glacisFacet)); + emit LiFiTransferStarted(bridgeData); + initiateBridgeTxWithFacet(false); + vm.stopPrank(); } function testBase_CanSwapAndBridgeNativeTokens() public override { @@ -262,10 +266,6 @@ contract GlacisFacetTest is TestBaseFacet { assertEq(USER_SENDER.balance, initialETHBalance - addToMessageValue); } - function test_CanBridgeAndPayFeeWithBridgedToken() public {} - - function test_CanSwapAndBridgeAndPayFeeWithBridgedToken() public {} - function testBase_Revert_CallerHasInsufficientFunds() public override { vm.startPrank(USER_SENDER); From ec862d844984c8b95dbb046b46f2a45e5f8d80a6 Mon Sep 17 00:00:00 2001 From: Michal Date: Tue, 21 Jan 2025 19:26:08 +0100 Subject: [PATCH 12/38] Updates --- deployments/_deployments_log_file.json | 16 +++ deployments/arbitrum.diamond.staging.json | 10 +- deployments/arbitrum.staging.json | 4 +- script/demoScripts/demoGlacisAirlift.ts | 135 +++++++++++++--------- src/Facets/GlacisFacet.sol | 7 +- test/solidity/Facets/GlacisFacet.t.sol | 5 - 6 files changed, 111 insertions(+), 66 deletions(-) diff --git a/deployments/_deployments_log_file.json b/deployments/_deployments_log_file.json index 3899e4fd4..de5ff661f 100644 --- a/deployments/_deployments_log_file.json +++ b/deployments/_deployments_log_file.json @@ -26986,5 +26986,21 @@ ] } } + }, + "GlacisFacet": { + "arbitrum": { + "staging": { + "1.0.0": [ + { + "ADDRESS": "0xb65E1Cf7308f9B8B981A921603d821Fb374e5201", + "OPTIMIZER_RUNS": "1000000", + "TIMESTAMP": "2025-01-21 18:47:43", + "CONSTRUCTOR_ARGS": "0x000000000000000000000000e0a049955e18cffd09c826c2c2e965439b6ab272", + "SALT": "0x1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d", + "VERIFIED": "true" + } + ] + } + } } } diff --git a/deployments/arbitrum.diamond.staging.json b/deployments/arbitrum.diamond.staging.json index ee64127be..f613198d9 100644 --- a/deployments/arbitrum.diamond.staging.json +++ b/deployments/arbitrum.diamond.staging.json @@ -138,12 +138,16 @@ "Version": "1.0.0" }, "0xE15C7585636e62b88bA47A40621287086E0c2E33": { - "Name": "", - "Version": "" + "Name": "DeBridgeDlnFacet", + "Version": "1.0.0" }, "0x08BfAc22A3B41637edB8A7920754fDb30B18f740": { "Name": "AcrossFacetV3", "Version": "1.1.0" + }, + "0xb65E1Cf7308f9B8B981A921603d821Fb374e5201": { + "Name": "GlacisFacet", + "Version": "1.0.0" } }, "Periphery": { @@ -154,8 +158,8 @@ "LiFiDEXAggregator": "", "LiFuelFeeCollector": "0x94EA56D8049e93E0308B9c7d1418Baf6A7C68280", "Permit2Proxy": "0x6FC01BC9Ff6Cdab694Ec8Ca41B21a2F04C8c37E5", - "ReceiverAcrossV3": "0xe4F3DEF14D61e47c696374453CD64d438FD277F8", "Receiver": "0x36E9B2E8A627474683eF3b1E9Df26D2bF04396f3", + "ReceiverAcrossV3": "0xe4F3DEF14D61e47c696374453CD64d438FD277F8", "ReceiverStargateV2": "", "RelayerCelerIM": "0xa1Ed8783AC96385482092b82eb952153998e9b70", "TokenWrapper": "0xF63b27AE2Dc887b88f82E2Cc597d07fBB2E78E70" diff --git a/deployments/arbitrum.staging.json b/deployments/arbitrum.staging.json index 96b7f9fe6..b588f6341 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", + "GlacisFacet": "0xb65E1Cf7308f9B8B981A921603d821Fb374e5201" } \ No newline at end of file diff --git a/script/demoScripts/demoGlacisAirlift.ts b/script/demoScripts/demoGlacisAirlift.ts index 2f18d9c1b..c7aa40aae 100644 --- a/script/demoScripts/demoGlacisAirlift.ts +++ b/script/demoScripts/demoGlacisAirlift.ts @@ -1,92 +1,117 @@ -import { providers, Wallet, utils, constants, Contract } from 'ethers' -import chalk from 'chalk' -import { GlacisFacet__factory, ERC20__factory } from '../../typechain' -import { node_url } from '../utils/network' +import { utils, constants, Contract, ethers, BigNumber } from 'ethers' +import { + GlacisFacet__factory, + ERC20__factory, + ILiFi, + type GlacisFacet, +} from '../../typechain' +import deployments from '../../deployments/arbitrum.staging.json' import config from '../../config/glacis.json' +import { zeroPadValue } from 'ethers6' +import dotenv from 'dotenv' +dotenv.config() -const msg = (msg: string) => { - console.log(chalk.green(msg)) -} +async function main() { + const RPC_URL = process.env.ETH_NODE_URI_ARBITRUM + const PRIVATE_KEY = process.env.PRIVATE_KEY + const LIFI_ADDRESS = deployments.LiFiDiamond + const WORMHOLE_ADDRESS = '0xB0fFa8000886e57F86dd5264b9582b2Ad87b2b91' // Wormhole token on Arbitrum + const destinationChainId = 10 // Optimism -const LIFI_ADDRESS = '0x1231DEB6f5749EF6cE6943a275A1D3E7486F4EaE' // LIFI Diamond on Arbitrum -const WORMHOLE_ADDRESS = '0xB0fFa8000886e57F86dd5264b9582b2Ad87b2b91' // Wormhole token on Arbitrum -const amountToBridge = '1' -const destinationChainId = 10 // Optimism + const provider = new ethers.providers.JsonRpcProvider(RPC_URL) + const signer = new ethers.Wallet(PRIVATE_KEY as string, provider) + const glacis = GlacisFacet__factory.connect(LIFI_ADDRESS, provider) as any -async function main() { - msg(`Transfer ${amountToBridge} Wormhole on Arbitrum to Wormhole on Optimism`) - let wallet = Wallet.fromMnemonic(process.env.MNEMONIC) - const provider1 = new providers.JsonRpcProvider(node_url('arbitrum')) - const provider = new providers.FallbackProvider([provider1]) - wallet = wallet.connect(provider) - const walletAddress = await wallet.getAddress() + const address = await signer.getAddress() - const lifi = GlacisFacet__factory.connect(LIFI_ADDRESS, wallet) as any + const token = ERC20__factory.connect(WORMHOLE_ADDRESS, provider) + const amount = utils.parseUnits('0.5', 18) + console.info( + `Transfer ${amount} Wormhole on Arbitrum to Wormhole on Optimism` + ) + console.info(`Currently connected to ${address}`) - const token = ERC20__factory.connect(WORMHOLE_ADDRESS, wallet) - const amount = utils.parseEther(amountToBridge) + const balance = await token.balanceOf(address) + console.info(`Token balance for connected wallet: ${balance.toString()}`) + if (balance.eq(0)) { + console.error(`Connected account has no funds.`) + console.error(`Exiting...`) + process.exit(1) + } - const allowance = await token.allowance(walletAddress, LIFI_ADDRESS) - if (amount.gt(allowance)) { - await token.approve(lifi.address, amount) + console.info('Sending WORMHOLE...') + const currentAllowance = await token.allowance( + await signer.getAddress(), + LIFI_ADDRESS + ) - msg('Token approved for swapping') + if (currentAllowance.lt(amount)) { + console.info('Allowance is insufficient. Approving the required amount...') + const gasPrice = await provider.getGasPrice() + const tx = await token + .connect(signer) + .approve(LIFI_ADDRESS, amount, { gasPrice }) + await tx.wait() + + console.info('Approval transaction complete. New allowance set.') + } else { + console.info('Sufficient allowance already exists. No need to approve.') } + console.info('Sent WORMHOLE') - const lifiData = { + const bridgeData: ILiFi.BridgeDataStruct = { transactionId: utils.randomBytes(32), + bridge: 'glacis', integrator: 'ACME Devs', referrer: constants.AddressZero, sendingAssetId: WORMHOLE_ADDRESS, - receivingAssetId: constants.AddressZero, - receiver: walletAddress, + receiver: address, destinationChainId: destinationChainId, - amount: amount, + minAmount: amount, + hasSourceSwaps: false, + hasDestinationCall: false, } const airlift = new Contract(config.arbitrum.airlift, [ - 'function send(address token, uint256 amount, bytes32 receiver, uint256 destinationChainId, address refundAddress) external payable', + 'function quoteSend(address token, uint256 amount, bytes32 receiver, uint256 destinationChainId, address refundAddress, uint256 msgValue) external returns ((uint256, uint256), uint256, uint256, ((uint256, uint256), uint256, uint256))', ]) + // calculate native fee - const estimatedFees = await airlift.quoteSend.staticCall( + const estimatedFees = await airlift.connect(signer).callStatic.quoteSend( WORMHOLE_ADDRESS, amount, - walletAddress, + zeroPadValue(address, 32), // address to bytes32 destinationChainId, - walletAddress, //refund + address, //refund utils.parseEther('1') ) const structuredFees = { gmpFee: { - nativeFee: estimatedFees[0][0], - tokenFee: estimatedFees[0][1], + nativeFee: BigNumber.from(estimatedFees[0][0]), + tokenFee: BigNumber.from(estimatedFees[0][1]), }, airliftFee: { - nativeFee: estimatedFees[3][0][0], - tokenFee: estimatedFees[3][0][1], + nativeFee: BigNumber.from(estimatedFees[3][0][0]), + tokenFee: BigNumber.from(estimatedFees[3][0][1]), }, } - console.log(structuredFees) - const estimatedValue = - structuredFees.gmpFee.nativeFee + structuredFees.airliftFee.nativeFee - - const glacisBridgeData = { - receiver: walletAddress, - amount: amount, - } - - const trx = await lifi.startBridgeTokensViaGlacis( - lifiData, - glacisBridgeData, - { - value: estimatedValue, - gasLimit: 500000, - } + const nativeFee = structuredFees.gmpFee.nativeFee.add( + structuredFees.airliftFee.nativeFee ) - msg('Bridge process started on sending chain') + const glacisBridgeData: GlacisFacet.GlacisDataStruct = { + refund: address, + nativeFee, + } - await trx.wait() + console.info('Bridging WORMHOLE...') + const tx = await glacis + .connect(signer) + .startBridgeTokensViaGlacis(bridgeData, glacisBridgeData, { + value: nativeFee, + }) + await tx.wait() + console.info('Bridged WORMHOLE...') } main() diff --git a/src/Facets/GlacisFacet.sol b/src/Facets/GlacisFacet.sol index a46691126..8e62277bf 100644 --- a/src/Facets/GlacisFacet.sol +++ b/src/Facets/GlacisFacet.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.17; import { ILiFi } from "../Interfaces/ILiFi.sol"; -import { LibAsset } from "../Libraries/LibAsset.sol"; +import { LibAsset, IERC20, SafeERC20 } from "../Libraries/LibAsset.sol"; import { LibSwap } from "../Libraries/LibSwap.sol"; import { ReentrancyGuard } from "../Helpers/ReentrancyGuard.sol"; import { SwapperV2 } from "../Helpers/SwapperV2.sol"; @@ -94,6 +94,11 @@ contract GlacisFacet is ILiFi, ReentrancyGuard, SwapperV2, Validatable { ILiFi.BridgeData memory _bridgeData, GlacisData calldata _glacisData ) internal { + SafeERC20.safeTransfer( + IERC20(_bridgeData.sendingAssetId), + address(airlift), + _bridgeData.minAmount + ); airlift.send{ value: _glacisData.nativeFee }( _bridgeData.sendingAssetId, _bridgeData.minAmount, diff --git a/test/solidity/Facets/GlacisFacet.t.sol b/test/solidity/Facets/GlacisFacet.t.sol index a91063219..6a54ae95d 100644 --- a/test/solidity/Facets/GlacisFacet.t.sol +++ b/test/solidity/Facets/GlacisFacet.t.sol @@ -48,11 +48,6 @@ contract GlacisFacetTest is TestBaseFacet { USER_SENDER, 500_000 * 10 ** wormhole.decimals() ); - deal( - ADDRESS_WORMHOLE_TOKEN, - address(airlift), - 500_000 * 10 ** wormhole.decimals() - ); glacisFacet = new TestGlacisFacet(airlift); bytes4[] memory functionSelectors = new bytes4[](4); From 804d0974ddd699f1da49f68bed47a9b2f23775f2 Mon Sep 17 00:00:00 2001 From: Michal Date: Wed, 22 Jan 2025 12:33:11 +0100 Subject: [PATCH 13/38] Updated GlacisFacet onchain --- deployments/_deployments_log_file.json | 6 +++--- deployments/arbitrum.diamond.staging.json | 4 ++-- deployments/arbitrum.staging.json | 2 +- script/demoScripts/demoGlacisAirlift.ts | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/deployments/_deployments_log_file.json b/deployments/_deployments_log_file.json index de5ff661f..c9a129183 100644 --- a/deployments/_deployments_log_file.json +++ b/deployments/_deployments_log_file.json @@ -26992,11 +26992,11 @@ "staging": { "1.0.0": [ { - "ADDRESS": "0xb65E1Cf7308f9B8B981A921603d821Fb374e5201", + "ADDRESS": "0x3a8ce701D1c8fBa838a56cF9d6bFE8B223927Ed0", "OPTIMIZER_RUNS": "1000000", - "TIMESTAMP": "2025-01-21 18:47:43", + "TIMESTAMP": "2025-01-22 09:50:46", "CONSTRUCTOR_ARGS": "0x000000000000000000000000e0a049955e18cffd09c826c2c2e965439b6ab272", - "SALT": "0x1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d", + "SALT": "", "VERIFIED": "true" } ] diff --git a/deployments/arbitrum.diamond.staging.json b/deployments/arbitrum.diamond.staging.json index f613198d9..57eef0e1c 100644 --- a/deployments/arbitrum.diamond.staging.json +++ b/deployments/arbitrum.diamond.staging.json @@ -145,7 +145,7 @@ "Name": "AcrossFacetV3", "Version": "1.1.0" }, - "0xb65E1Cf7308f9B8B981A921603d821Fb374e5201": { + "0x3a8ce701D1c8fBa838a56cF9d6bFE8B223927Ed0": { "Name": "GlacisFacet", "Version": "1.0.0" } @@ -157,7 +157,7 @@ "GasZipPeriphery": "", "LiFiDEXAggregator": "", "LiFuelFeeCollector": "0x94EA56D8049e93E0308B9c7d1418Baf6A7C68280", - "Permit2Proxy": "0x6FC01BC9Ff6Cdab694Ec8Ca41B21a2F04C8c37E5", + "Permit2Proxy": "0xb33Fe241BEd9bf5F694101D7498F63a0d060F999", "Receiver": "0x36E9B2E8A627474683eF3b1E9Df26D2bF04396f3", "ReceiverAcrossV3": "0xe4F3DEF14D61e47c696374453CD64d438FD277F8", "ReceiverStargateV2": "", diff --git a/deployments/arbitrum.staging.json b/deployments/arbitrum.staging.json index b588f6341..4b4a962b6 100644 --- a/deployments/arbitrum.staging.json +++ b/deployments/arbitrum.staging.json @@ -51,5 +51,5 @@ "ReceiverAcrossV3": "0xe4F3DEF14D61e47c696374453CD64d438FD277F8", "AcrossFacetPackedV3": "0x21767081Ff52CE5563A29f27149D01C7127775A2", "RelayFacet": "0x3cf7dE0e31e13C93c8Aada774ADF1C7eD58157f5", - "GlacisFacet": "0xb65E1Cf7308f9B8B981A921603d821Fb374e5201" + "GlacisFacet": "0x3a8ce701D1c8fBa838a56cF9d6bFE8B223927Ed0" } \ No newline at end of file diff --git a/script/demoScripts/demoGlacisAirlift.ts b/script/demoScripts/demoGlacisAirlift.ts index c7aa40aae..b7760c081 100644 --- a/script/demoScripts/demoGlacisAirlift.ts +++ b/script/demoScripts/demoGlacisAirlift.ts @@ -111,7 +111,7 @@ async function main() { value: nativeFee, }) await tx.wait() - console.info('Bridged WORMHOLE...') + console.info('Bridged WORMHOLE') } main() From 16673062ba87467624b928bf21c04a9878518942 Mon Sep 17 00:00:00 2001 From: Michal Date: Wed, 22 Jan 2025 13:48:19 +0100 Subject: [PATCH 14/38] Updates proposed by coderabbit --- script/demoScripts/demoGlacisAirlift.ts | 56 ++++++++++++++++--------- src/Interfaces/IGlacisAirlift.sol | 2 +- test/solidity/Facets/GlacisFacet.t.sol | 4 +- 3 files changed, 40 insertions(+), 22 deletions(-) diff --git a/script/demoScripts/demoGlacisAirlift.ts b/script/demoScripts/demoGlacisAirlift.ts index b7760c081..36eafcbea 100644 --- a/script/demoScripts/demoGlacisAirlift.ts +++ b/script/demoScripts/demoGlacisAirlift.ts @@ -48,11 +48,15 @@ async function main() { if (currentAllowance.lt(amount)) { console.info('Allowance is insufficient. Approving the required amount...') const gasPrice = await provider.getGasPrice() - const tx = await token - .connect(signer) - .approve(LIFI_ADDRESS, amount, { gasPrice }) - await tx.wait() - + try { + const tx = await token + .connect(signer) + .approve(LIFI_ADDRESS, amount, { gasPrice }) + await tx.wait() + } catch (error) { + console.error('Approval failed:', error) + process.exit(1) + } console.info('Approval transaction complete. New allowance set.') } else { console.info('Sufficient allowance already exists. No need to approve.') @@ -77,14 +81,23 @@ async function main() { ]) // calculate native fee - const estimatedFees = await airlift.connect(signer).callStatic.quoteSend( - WORMHOLE_ADDRESS, - amount, - zeroPadValue(address, 32), // address to bytes32 - destinationChainId, - address, //refund - utils.parseEther('1') - ) + let estimatedFees + try { + estimatedFees = await airlift + .connect(signer) + .callStatic.quoteSend( + WORMHOLE_ADDRESS, + amount, + zeroPadValue(address, 32), + destinationChainId, + address, + utils.parseEther('1') + ) + if (!estimatedFees) throw new Error('Invalid fee estimation') + } catch (error) { + console.error('Fee estimation failed:', error) + process.exit(1) + } const structuredFees = { gmpFee: { nativeFee: BigNumber.from(estimatedFees[0][0]), @@ -105,12 +118,17 @@ async function main() { } console.info('Bridging WORMHOLE...') - const tx = await glacis - .connect(signer) - .startBridgeTokensViaGlacis(bridgeData, glacisBridgeData, { - value: nativeFee, - }) - await tx.wait() + try { + const tx = await glacis + .connect(signer) + .startBridgeTokensViaGlacis(bridgeData, glacisBridgeData, { + value: nativeFee, + }) + await tx.wait() + } catch (error) { + console.error('Approval failed:', error) + process.exit(1) + } console.info('Bridged WORMHOLE') } diff --git a/src/Interfaces/IGlacisAirlift.sol b/src/Interfaces/IGlacisAirlift.sol index afce26e69..4f7236623 100644 --- a/src/Interfaces/IGlacisAirlift.sol +++ b/src/Interfaces/IGlacisAirlift.sol @@ -6,7 +6,7 @@ struct QuoteSendInfo { Fee gmpFee; uint256 amountSent; uint256 valueSent; - AirliftFeeInfo AirliftFeeInfo; + AirliftFeeInfo airliftFeeInfo; } struct AirliftFeeInfo { diff --git a/test/solidity/Facets/GlacisFacet.t.sol b/test/solidity/Facets/GlacisFacet.t.sol index 6a54ae95d..8877327db 100644 --- a/test/solidity/Facets/GlacisFacet.t.sol +++ b/test/solidity/Facets/GlacisFacet.t.sol @@ -93,7 +93,7 @@ contract GlacisFacetTest is TestBaseFacet { addToMessageValue = quoteSendInfo.gmpFee.nativeFee + - quoteSendInfo.AirliftFeeInfo.airliftFee.nativeFee; + quoteSendInfo.airliftFeeInfo.airliftFee.nativeFee; // produce valid GlacisData validGlacisData = GlacisFacet.GlacisData({ @@ -166,7 +166,7 @@ contract GlacisFacetTest is TestBaseFacet { ); addToMessageValue = quoteSendInfo.gmpFee.nativeFee + - quoteSendInfo.AirliftFeeInfo.airliftFee.nativeFee; + quoteSendInfo.airliftFeeInfo.airliftFee.nativeFee; //prepare check for events vm.expectEmit(true, true, true, true, address(glacisFacet)); emit LiFiTransferStarted(bridgeData); From 064233d2be3a984e77608c339c7eb09a264a6ef1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Miro=C5=84czuk?= Date: Wed, 22 Jan 2025 13:54:11 +0100 Subject: [PATCH 15/38] Update script/demoScripts/demoGlacisAirlift.ts Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- script/demoScripts/demoGlacisAirlift.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/script/demoScripts/demoGlacisAirlift.ts b/script/demoScripts/demoGlacisAirlift.ts index 36eafcbea..10514d46a 100644 --- a/script/demoScripts/demoGlacisAirlift.ts +++ b/script/demoScripts/demoGlacisAirlift.ts @@ -126,7 +126,7 @@ async function main() { }) await tx.wait() } catch (error) { - console.error('Approval failed:', error) + console.error('Bridge transaction failed:', error) process.exit(1) } console.info('Bridged WORMHOLE') From b52931c2674ec64b87ef538598ef75354792d9fe Mon Sep 17 00:00:00 2001 From: Michal Date: Wed, 22 Jan 2025 14:09:13 +0100 Subject: [PATCH 16/38] Removed TODOs --- test/solidity/Facets/GlacisFacet.t.sol | 2 -- 1 file changed, 2 deletions(-) diff --git a/test/solidity/Facets/GlacisFacet.t.sol b/test/solidity/Facets/GlacisFacet.t.sol index 8877327db..958db0e53 100644 --- a/test/solidity/Facets/GlacisFacet.t.sol +++ b/test/solidity/Facets/GlacisFacet.t.sol @@ -144,9 +144,7 @@ contract GlacisFacetTest is TestBaseFacet { vm.stopPrank(); } - // TODO function testBase_CanBridgeTokens_fuzzed(uint256 amount) public override { - // TODO can be related to this issue: https://github.com/glacislabs/airlift-evm/blob/main/test/tokens/MIM.t.sol#L23-L31 vm.assume( amount > 0 * 10 ** wormhole.decimals() && amount < 100_000 * 10 ** wormhole.decimals() From e9d721bcd6265f389218733e48db9c430b53c534 Mon Sep 17 00:00:00 2001 From: Michal Date: Thu, 23 Jan 2025 14:33:45 +0100 Subject: [PATCH 17/38] added docs description, variables naming, blank lines, added comments --- docs/GlacisFacet.md | 11 ++++++++--- script/demoScripts/demoGlacisAirlift.ts | 20 +++++++++----------- src/Facets/GlacisFacet.sol | 10 +++++++--- src/Interfaces/IGlacisAirlift.sol | 8 -------- test/solidity/Facets/GlacisFacet.t.sol | 18 ++++++++++++++---- 5 files changed, 38 insertions(+), 29 deletions(-) diff --git a/docs/GlacisFacet.md b/docs/GlacisFacet.md index e741a441d..6a4e1f1ff 100644 --- a/docs/GlacisFacet.md +++ b/docs/GlacisFacet.md @@ -2,7 +2,12 @@ ## How it works -The Glacis Facet works by ... +The Glacis Facet works by forwarding calls to the [GlacisAirlift](https://github.com/glacislabs/airlift-evm/blob/main/src/facets/GlacisAirliftFacet.sol) core contract on the source chain. Glacis Airlift streamlines diverse mechanisms of General Message Passing protocols (GMPs) like Axelar, LayerZero, Wormhole by integrating interchain transfer adapters for each standard and maintaining a registry of curated addresses associated with the respective interchain tokens. This design allows users to simply specify a token address for bridging. Glacis Airlift automatically forwards the request, along with the necessary bridging data, to the appropriate adapter, ensuring seamless execution of the bridging process. + +Bridge doesn’t support stablecoins like **USDT** or **USDC**. They are custom tokens focused. We can find possible routes in [`routes.ts`](https://github.com/glacislabs/airlift-evm/blob/main/node-scripts/src/tests/routes.ts). + +The [`send`](https://github.com/glacislabs/airlift-evm/blob/main/src/facets/GlacisAirliftFacet.sol#L94) function is used to execute the cross-chain transfer. +Before calling [`send`](https://github.com/glacislabs/airlift-evm/blob/main/src/facets/GlacisAirliftFacet.sol#L94), the tokens must first be sent to the contract, as the function assumes the tokens are already in place. This function is preferred over [`sendAfterApproval`](https://github.com/glacislabs/airlift-evm/blob/af935df67c3fff873edea9758ef73cd46e1908c7/src/facets/GlacisAirliftFacet.sol#L112) because it eliminates the need for redundant token transfer steps, as tokens are already transferred to the contract beforehand. ```mermaid graph LR; @@ -22,10 +27,10 @@ graph LR; The methods listed above take a variable labeled `_glacisData`. This data is specific to glacis and is represented as the following struct type: ```solidity -/// @param refund Refund address +/// @param refundAddress Refund address /// @param nativeFee The fee amount in native token required by the Glacis Airlift. struct GlacisData { - address refund; + address refundAddress; uint256 nativeFee; } ``` diff --git a/script/demoScripts/demoGlacisAirlift.ts b/script/demoScripts/demoGlacisAirlift.ts index 10514d46a..db2f98eff 100644 --- a/script/demoScripts/demoGlacisAirlift.ts +++ b/script/demoScripts/demoGlacisAirlift.ts @@ -22,16 +22,16 @@ async function main() { const signer = new ethers.Wallet(PRIVATE_KEY as string, provider) const glacis = GlacisFacet__factory.connect(LIFI_ADDRESS, provider) as any - const address = await signer.getAddress() + const signerAddress = await signer.getAddress() const token = ERC20__factory.connect(WORMHOLE_ADDRESS, provider) const amount = utils.parseUnits('0.5', 18) console.info( `Transfer ${amount} Wormhole on Arbitrum to Wormhole on Optimism` ) - console.info(`Currently connected to ${address}`) + console.info(`Currently connected to ${signerAddress}`) - const balance = await token.balanceOf(address) + const balance = await token.balanceOf(signerAddress) console.info(`Token balance for connected wallet: ${balance.toString()}`) if (balance.eq(0)) { console.error(`Connected account has no funds.`) @@ -39,7 +39,6 @@ async function main() { process.exit(1) } - console.info('Sending WORMHOLE...') const currentAllowance = await token.allowance( await signer.getAddress(), LIFI_ADDRESS @@ -61,7 +60,6 @@ async function main() { } else { console.info('Sufficient allowance already exists. No need to approve.') } - console.info('Sent WORMHOLE') const bridgeData: ILiFi.BridgeDataStruct = { transactionId: utils.randomBytes(32), @@ -69,28 +67,28 @@ async function main() { integrator: 'ACME Devs', referrer: constants.AddressZero, sendingAssetId: WORMHOLE_ADDRESS, - receiver: address, + receiver: signerAddress, destinationChainId: destinationChainId, minAmount: amount, hasSourceSwaps: false, hasDestinationCall: false, } - const airlift = new Contract(config.arbitrum.airlift, [ + const airliftContract = new Contract(config.arbitrum.airlift, [ 'function quoteSend(address token, uint256 amount, bytes32 receiver, uint256 destinationChainId, address refundAddress, uint256 msgValue) external returns ((uint256, uint256), uint256, uint256, ((uint256, uint256), uint256, uint256))', ]) // calculate native fee let estimatedFees try { - estimatedFees = await airlift + estimatedFees = await airliftContract .connect(signer) .callStatic.quoteSend( WORMHOLE_ADDRESS, amount, - zeroPadValue(address, 32), + zeroPadValue(signerAddress, 32), destinationChainId, - address, + signerAddress, utils.parseEther('1') ) if (!estimatedFees) throw new Error('Invalid fee estimation') @@ -113,7 +111,7 @@ async function main() { ) const glacisBridgeData: GlacisFacet.GlacisDataStruct = { - refund: address, + refundAddress: signerAddress, nativeFee, } diff --git a/src/Facets/GlacisFacet.sol b/src/Facets/GlacisFacet.sol index 8e62277bf..ff616fef8 100644 --- a/src/Facets/GlacisFacet.sol +++ b/src/Facets/GlacisFacet.sol @@ -21,10 +21,10 @@ contract GlacisFacet is ILiFi, ReentrancyGuard, SwapperV2, Validatable { /// Types /// - /// @param refund Refund address + /// @param refundAddress Refund address /// @param nativeFee The fee amount in native token required by the Glacis Airlift. struct GlacisData { - address refund; + address refundAddress; uint256 nativeFee; } @@ -94,17 +94,21 @@ contract GlacisFacet is ILiFi, ReentrancyGuard, SwapperV2, Validatable { ILiFi.BridgeData memory _bridgeData, GlacisData calldata _glacisData ) internal { + // Transfer the tokens to the Airlift contract. + // This step ensures that the tokens are already in place before calling the `send` function. + // The `send` function assumes the tokens are pre-transferred to the contract. SafeERC20.safeTransfer( IERC20(_bridgeData.sendingAssetId), address(airlift), _bridgeData.minAmount ); + airlift.send{ value: _glacisData.nativeFee }( _bridgeData.sendingAssetId, _bridgeData.minAmount, bytes32(uint256(uint160(_bridgeData.receiver))), _bridgeData.destinationChainId, - _glacisData.refund + _glacisData.refundAddress ); emit LiFiTransferStarted(_bridgeData); diff --git a/src/Interfaces/IGlacisAirlift.sol b/src/Interfaces/IGlacisAirlift.sol index 4f7236623..22630622e 100644 --- a/src/Interfaces/IGlacisAirlift.sol +++ b/src/Interfaces/IGlacisAirlift.sol @@ -20,14 +20,6 @@ struct Fee { uint256 tokenFee; } -error GlacisAirlift__NotEnoughValueFee(); -error GlacisAirlift__NotEnoughTokenFee(); -error GlacisAirlift__FeeTransferUnsuccessful(); -error GlacisAirliftFacet__TokenNotSupportedForBridging(); -error GlacisAirliftFacet__TokenFacetReverted(address token, bytes4 selector); -error GlacisAirliftFacet__SelectorAndTokenArrayMustBeSameLength(); -error GlacisAirliftFacet__NotOnWhitelist(); - interface IGlacisAirlift { /// Registers function selectors to multiple token. A selector's function must be added to the Diamond as a facet. /// @param diamondSelectors The bytes4 selector of the token's handler function. diff --git a/test/solidity/Facets/GlacisFacet.t.sol b/test/solidity/Facets/GlacisFacet.t.sol index 958db0e53..6bb06e755 100644 --- a/test/solidity/Facets/GlacisFacet.t.sol +++ b/test/solidity/Facets/GlacisFacet.t.sol @@ -36,7 +36,7 @@ contract GlacisFacetTest is TestBaseFacet { function setUp() public { customRpcUrlForForking = "ETH_NODE_URI_ARBITRUM"; - customBlockNumberForForking = 297418708; + customBlockNumberForForking = 298446895; initTestBase(); wormhole = ERC20(ADDRESS_WORMHOLE_TOKEN); @@ -81,6 +81,9 @@ contract GlacisFacetTest is TestBaseFacet { bridgeData.minAmount = defaultWORMHOLEAmount; bridgeData.destinationChainId = 10; + // Call `quoteSend` to estimate the required native fee for the transfer. + // This is necessary to ensure the transaction has sufficient gas for execution. + // The `payableAmount` parameter simulates the amount of native tokens required for the estimation. QuoteSendInfo memory quoteSendInfo = IGlacisAirlift(address(airlift)) .quoteSend( bridgeData.sendingAssetId, @@ -97,7 +100,7 @@ contract GlacisFacetTest is TestBaseFacet { // produce valid GlacisData validGlacisData = GlacisFacet.GlacisData({ - refund: REFUND_WALLET, + refundAddress: REFUND_WALLET, nativeFee: addToMessageValue }); } @@ -168,6 +171,7 @@ contract GlacisFacetTest is TestBaseFacet { //prepare check for events vm.expectEmit(true, true, true, true, address(glacisFacet)); emit LiFiTransferStarted(bridgeData); + initiateBridgeTxWithFacet(false); vm.stopPrank(); } @@ -216,7 +220,8 @@ contract GlacisFacetTest is TestBaseFacet { assertBalanceChange(ADDRESS_WORMHOLE_TOKEN, USER_SENDER, 0) assertBalanceChange(ADDRESS_WORMHOLE_TOKEN, USER_RECEIVER, 0) { - // add liquidity for dex pair + // add liquidity for dex pair DAI-WORMHOLE + // this is necessary because Glacis does not provide routes for stablecoins like USDT or USDC, forcing us to work with custom tokens that often lack liquidity on V2 dexes addLiquidity( ADDRESS_DAI, ADDRESS_WORMHOLE_TOKEN, @@ -225,15 +230,20 @@ contract GlacisFacetTest is TestBaseFacet { ); uint256 initialDAIBalance = dai.balanceOf(USER_SENDER); + vm.startPrank(USER_SENDER); + // prepare bridgeData bridgeData.hasSourceSwaps = true; + // reset swap data setDefaultSwapDataSingleDAItoWORMHOLE(); // approval dai.approve(_facetTestContractAddress, swapData[0].fromAmount); + uint256 initialETHBalance = USER_SENDER.balance; + //prepare check for events vm.expectEmit(true, true, true, true, _facetTestContractAddress); emit AssetSwapped( @@ -248,7 +258,7 @@ contract GlacisFacetTest is TestBaseFacet { vm.expectEmit(true, true, true, true, _facetTestContractAddress); emit LiFiTransferStarted(bridgeData); - uint256 initialETHBalance = USER_SENDER.balance; + initiateSwapAndBridgeTxWithFacet(false); // check balances after call From 925fb9722626134d2d2362814030c25ed9d89b36 Mon Sep 17 00:00:00 2001 From: Michal Date: Thu, 23 Jan 2025 15:02:26 +0100 Subject: [PATCH 18/38] Added logs after staging deployment --- deployments/_deployments_log_file.json | 4 ++-- deployments/arbitrum.diamond.staging.json | 2 +- deployments/arbitrum.staging.json | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/deployments/_deployments_log_file.json b/deployments/_deployments_log_file.json index c9de48c81..eea1608d4 100644 --- a/deployments/_deployments_log_file.json +++ b/deployments/_deployments_log_file.json @@ -27450,9 +27450,9 @@ "staging": { "1.0.0": [ { - "ADDRESS": "0x3a8ce701D1c8fBa838a56cF9d6bFE8B223927Ed0", + "ADDRESS": "0x3aF0c2dB91f75f05493E51cFcF92eC5276bc85F8", "OPTIMIZER_RUNS": "1000000", - "TIMESTAMP": "2025-01-22 09:50:46", + "TIMESTAMP": "2025-01-23 14:43:01", "CONSTRUCTOR_ARGS": "0x000000000000000000000000e0a049955e18cffd09c826c2c2e965439b6ab272", "SALT": "", "VERIFIED": "true" diff --git a/deployments/arbitrum.diamond.staging.json b/deployments/arbitrum.diamond.staging.json index 57eef0e1c..d8b678fb0 100644 --- a/deployments/arbitrum.diamond.staging.json +++ b/deployments/arbitrum.diamond.staging.json @@ -145,7 +145,7 @@ "Name": "AcrossFacetV3", "Version": "1.1.0" }, - "0x3a8ce701D1c8fBa838a56cF9d6bFE8B223927Ed0": { + "0x3aF0c2dB91f75f05493E51cFcF92eC5276bc85F8": { "Name": "GlacisFacet", "Version": "1.0.0" } diff --git a/deployments/arbitrum.staging.json b/deployments/arbitrum.staging.json index 4b4a962b6..d6dca2fec 100644 --- a/deployments/arbitrum.staging.json +++ b/deployments/arbitrum.staging.json @@ -51,5 +51,5 @@ "ReceiverAcrossV3": "0xe4F3DEF14D61e47c696374453CD64d438FD277F8", "AcrossFacetPackedV3": "0x21767081Ff52CE5563A29f27149D01C7127775A2", "RelayFacet": "0x3cf7dE0e31e13C93c8Aada774ADF1C7eD58157f5", - "GlacisFacet": "0x3a8ce701D1c8fBa838a56cF9d6bFE8B223927Ed0" + "GlacisFacet": "0x3aF0c2dB91f75f05493E51cFcF92eC5276bc85F8" } \ No newline at end of file From ae8720ccac3c4d64bb91fce5168903026e55c3db Mon Sep 17 00:00:00 2001 From: Michal Date: Thu, 23 Jan 2025 16:59:48 +0100 Subject: [PATCH 19/38] Added BASE contracts --- test/solidity/utils/TestBase.sol | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/test/solidity/utils/TestBase.sol b/test/solidity/utils/TestBase.sol index 9dc4f50bc..39b0dfe76 100644 --- a/test/solidity/utils/TestBase.sol +++ b/test/solidity/utils/TestBase.sol @@ -168,6 +168,17 @@ abstract contract TestBase is Test, DiamondTest, ILiFi { 0x7ceB23fD6bC0adD59E62ac25578270cFf1b9f619; address internal ADDRESS_WRAPPED_NATIVE_POL = 0x0d500B1d8E8eF31E21C99d1Db9A6444d3ADf1270; // WMATIC + // Contract addresses (BASE) + address internal ADDRESS_UNISWAP_BASE = + 0x6BDED42c6DA8FBf0d2bA55B2fa120C5e0c8D7891; + address internal ADDRESS_USDC_BASE = + 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913; + address internal ADDRESS_USDT_BASE = + 0xfde4C96c8593536E31F229EA8f37b2ADa2699bb2; + address internal ADDRESS_DAI_BASE = + 0x50c5725949A6F0c72E6C4a641F24049A917DB0Cb; + address internal ADDRESS_WRAPPED_NATIVE_BASE = + 0x4200000000000000000000000000000000000006; // User accounts (Whales: ETH only) address internal constant USER_SENDER = address(0xabc123456); // initially funded with 100,000 DAI, USDC, USDT, WETH & ETHER address internal constant USER_RECEIVER = address(0xabc654321); @@ -236,6 +247,16 @@ abstract contract TestBase is Test, DiamondTest, ILiFi { ADDRESS_WRAPPED_NATIVE = ADDRESS_WRAPPED_NATIVE_POL; ADDRESS_UNISWAP = ADDRESS_SUSHISWAP_POL; } + if ( + keccak256(abi.encode(customRpcUrlForForking)) == + keccak256(abi.encode("ETH_NODE_URI_BASE")) + ) { + ADDRESS_USDC = ADDRESS_USDC_BASE; + ADDRESS_USDT = ADDRESS_USDT_BASE; + ADDRESS_DAI = ADDRESS_DAI_BASE; + ADDRESS_WRAPPED_NATIVE = ADDRESS_WRAPPED_NATIVE_BASE; + ADDRESS_UNISWAP = ADDRESS_UNISWAP_BASE; + } } } From 5cc0d08a446f457d6275f54ee596835f0f2c5b47 Mon Sep 17 00:00:00 2001 From: Michal Date: Thu, 23 Jan 2025 17:22:58 +0100 Subject: [PATCH 20/38] Added LINK tests, refactoring --- test/solidity/Facets/GlacisFacet.t.sol | 218 +++++++++++++++++-------- 1 file changed, 154 insertions(+), 64 deletions(-) diff --git a/test/solidity/Facets/GlacisFacet.t.sol b/test/solidity/Facets/GlacisFacet.t.sol index 6bb06e755..fc773661c 100644 --- a/test/solidity/Facets/GlacisFacet.t.sol +++ b/test/solidity/Facets/GlacisFacet.t.sol @@ -5,7 +5,7 @@ import { LibAllowList, TestBaseFacet, ERC20 } from "../utils/TestBaseFacet.sol"; import { LibSwap } from "lifi/Libraries/LibSwap.sol"; import { GlacisFacet } from "lifi/Facets/GlacisFacet.sol"; import { IGlacisAirlift, QuoteSendInfo } from "lifi/Interfaces/IGlacisAirlift.sol"; -import { InsufficientBalance } from "lifi/Errors/GenericErrors.sol"; +import { InsufficientBalance, InvalidReceiver, InvalidAmount, CannotBridgeToSameNetwork } from "lifi/Errors/GenericErrors.sol"; // Stub GlacisFacet Contract contract TestGlacisFacet is GlacisFacet { @@ -20,36 +20,31 @@ contract TestGlacisFacet is GlacisFacet { } } -contract GlacisFacetTest is TestBaseFacet { +abstract contract GlacisFacetTestBase is TestBaseFacet { GlacisFacet.GlacisData internal validGlacisData; + IGlacisAirlift internal airliftContract; TestGlacisFacet internal glacisFacet; - ERC20 internal wormhole; - uint256 internal defaultWORMHOLEAmount; - uint256 internal tokenFee; - - IGlacisAirlift internal constant airlift = - IGlacisAirlift(0xE0A049955E18CFfd09C826C2c2e965439B6Ab272); - address internal ADDRESS_WORMHOLE_TOKEN = - 0xB0fFa8000886e57F86dd5264b9582b2Ad87b2b91; + ERC20 internal srcToken; + uint256 internal defaultSrcTokenAmount; + uint256 internal destinationChainId; + address internal ADDRESS_SRC_TOKEN; uint256 internal payableAmount = 1 ether; - function setUp() public { - customRpcUrlForForking = "ETH_NODE_URI_ARBITRUM"; - customBlockNumberForForking = 298446895; + function setUp() public virtual { initTestBase(); - wormhole = ERC20(ADDRESS_WORMHOLE_TOKEN); + srcToken = ERC20(ADDRESS_SRC_TOKEN); - defaultWORMHOLEAmount = 1_000 * 10 ** wormhole.decimals(); + defaultSrcTokenAmount = 1_000 * 10 ** srcToken.decimals(); deal( - ADDRESS_WORMHOLE_TOKEN, + ADDRESS_SRC_TOKEN, USER_SENDER, - 500_000 * 10 ** wormhole.decimals() + 500_000 * 10 ** srcToken.decimals() ); - glacisFacet = new TestGlacisFacet(airlift); + glacisFacet = new TestGlacisFacet(airliftContract); bytes4[] memory functionSelectors = new bytes4[](4); functionSelectors[0] = glacisFacet.startBridgeTokensViaGlacis.selector; functionSelectors[1] = glacisFacet @@ -72,20 +67,30 @@ contract GlacisFacetTest is TestBaseFacet { glacisFacet.setFunctionApprovalBySignature( uniswap.swapETHForExactTokens.selector ); - - setFacetAddressInTestBase(address(glacisFacet), "GlacisFacet"); + _facetTestContractAddress = address(glacisFacet); + vm.label(address(glacisFacet), "GlacisFacet"); // adjust bridgeData bridgeData.bridge = "glacis"; - bridgeData.sendingAssetId = ADDRESS_WORMHOLE_TOKEN; - bridgeData.minAmount = defaultWORMHOLEAmount; - bridgeData.destinationChainId = 10; + bridgeData.sendingAssetId = ADDRESS_SRC_TOKEN; + bridgeData.minAmount = defaultSrcTokenAmount; + bridgeData.destinationChainId = destinationChainId; + + // add liquidity for dex pair DAI-{SOURCE TOKEN} + // this is necessary because Glacis does not provide routes for stablecoins like USDT or USDC, forcing us to work with custom tokens that often lack liquidity on V2 dexes + addLiquidity( + ADDRESS_DAI, + ADDRESS_SRC_TOKEN, + 100_000 * 10 ** ERC20(ADDRESS_DAI).decimals(), + 100_000 * 10 ** srcToken.decimals() + ); // Call `quoteSend` to estimate the required native fee for the transfer. // This is necessary to ensure the transaction has sufficient gas for execution. // The `payableAmount` parameter simulates the amount of native tokens required for the estimation. - QuoteSendInfo memory quoteSendInfo = IGlacisAirlift(address(airlift)) - .quoteSend( + QuoteSendInfo memory quoteSendInfo = IGlacisAirlift( + address(airliftContract) + ).quoteSend( bridgeData.sendingAssetId, bridgeData.minAmount, bytes32(uint256(uint160(bridgeData.receiver))), @@ -105,39 +110,40 @@ contract GlacisFacetTest is TestBaseFacet { }); } - function initiateBridgeTxWithFacet(bool) internal override { + function initiateBridgeTxWithFacet(bool) internal virtual override { glacisFacet.startBridgeTokensViaGlacis{ value: addToMessageValue }( bridgeData, validGlacisData ); } - function initiateSwapAndBridgeTxWithFacet(bool) internal override { + function initiateSwapAndBridgeTxWithFacet(bool) internal virtual override { glacisFacet.swapAndStartBridgeTokensViaGlacis{ value: addToMessageValue }(bridgeData, swapData, validGlacisData); } - function testBase_CanBridgeNativeTokens() public override { + function testBase_CanBridgeNativeTokens() public virtual override { // facet does not support bridging of native assets } function testBase_CanBridgeTokens() public + virtual override assertBalanceChange( - ADDRESS_WORMHOLE_TOKEN, + ADDRESS_SRC_TOKEN, USER_SENDER, - -int256(defaultWORMHOLEAmount) + -int256(defaultSrcTokenAmount) ) - assertBalanceChange(ADDRESS_WORMHOLE_TOKEN, USER_RECEIVER, 0) + assertBalanceChange(ADDRESS_SRC_TOKEN, USER_RECEIVER, 0) assertBalanceChange(ADDRESS_DAI, USER_SENDER, 0) assertBalanceChange(ADDRESS_DAI, USER_RECEIVER, 0) { vm.startPrank(USER_SENDER); // approval - wormhole.approve(address(glacisFacet), bridgeData.minAmount); + srcToken.approve(address(glacisFacet), bridgeData.minAmount); //prepare check for events vm.expectEmit(true, true, true, true, address(glacisFacet)); @@ -147,17 +153,22 @@ contract GlacisFacetTest is TestBaseFacet { vm.stopPrank(); } - function testBase_CanBridgeTokens_fuzzed(uint256 amount) public override { + function testBase_CanBridgeTokens_fuzzed( + uint256 amount + ) public virtual override { vm.assume( - amount > 0 * 10 ** wormhole.decimals() && - amount < 100_000 * 10 ** wormhole.decimals() + amount > 1 * 10 ** srcToken.decimals() && + amount < 100_000 * 10 ** srcToken.decimals() ); - vm.startPrank(USER_SENDER); bridgeData.minAmount = amount; + + vm.startPrank(USER_SENDER); + // approval - wormhole.approve(address(glacisFacet), bridgeData.minAmount); - QuoteSendInfo memory quoteSendInfo = IGlacisAirlift(address(airlift)) - .quoteSend( + srcToken.approve(address(glacisFacet), bridgeData.minAmount); + QuoteSendInfo memory quoteSendInfo = IGlacisAirlift( + address(airliftContract) + ).quoteSend( bridgeData.sendingAssetId, bridgeData.minAmount, bytes32(uint256(uint160(bridgeData.receiver))), @@ -168,6 +179,7 @@ contract GlacisFacetTest is TestBaseFacet { addToMessageValue = quoteSendInfo.gmpFee.nativeFee + quoteSendInfo.airliftFeeInfo.airliftFee.nativeFee; + //prepare check for events vm.expectEmit(true, true, true, true, address(glacisFacet)); emit LiFiTransferStarted(bridgeData); @@ -176,29 +188,28 @@ contract GlacisFacetTest is TestBaseFacet { vm.stopPrank(); } - function testBase_CanSwapAndBridgeNativeTokens() public override { + function testBase_CanSwapAndBridgeNativeTokens() public virtual override { // facet does not support bridging of native assets } - function setDefaultSwapDataSingleDAItoWORMHOLE() internal virtual { + function setDefaultSwapDataSingleDAItoSourceToken() internal virtual { delete swapData; - // Swap DAI -> WORMHOLE + // Swap DAI -> {SOURCE TOKEN} address[] memory path = new address[](2); path[0] = ADDRESS_DAI; - path[1] = ADDRESS_WORMHOLE_TOKEN; + path[1] = ADDRESS_SRC_TOKEN; - uint256 amountOut = defaultWORMHOLEAmount; + uint256 amountOut = defaultSrcTokenAmount; // Calculate DAI amount uint256[] memory amounts = uniswap.getAmountsIn(amountOut, path); uint256 amountIn = amounts[0]; - swapData.push( LibSwap.SwapData({ callTo: address(uniswap), approveTo: address(uniswap), sendingAssetId: ADDRESS_DAI, - receivingAssetId: ADDRESS_WORMHOLE_TOKEN, + receivingAssetId: ADDRESS_SRC_TOKEN, fromAmount: amountIn, callData: abi.encodeWithSelector( uniswap.swapExactTokensForTokens.selector, @@ -215,20 +226,12 @@ contract GlacisFacetTest is TestBaseFacet { function testBase_CanSwapAndBridgeTokens() public + virtual override assertBalanceChange(ADDRESS_DAI, USER_RECEIVER, 0) - assertBalanceChange(ADDRESS_WORMHOLE_TOKEN, USER_SENDER, 0) - assertBalanceChange(ADDRESS_WORMHOLE_TOKEN, USER_RECEIVER, 0) + assertBalanceChange(ADDRESS_SRC_TOKEN, USER_SENDER, 0) + assertBalanceChange(ADDRESS_SRC_TOKEN, USER_RECEIVER, 0) { - // add liquidity for dex pair DAI-WORMHOLE - // this is necessary because Glacis does not provide routes for stablecoins like USDT or USDC, forcing us to work with custom tokens that often lack liquidity on V2 dexes - addLiquidity( - ADDRESS_DAI, - ADDRESS_WORMHOLE_TOKEN, - 100_000 * 10 ** ERC20(ADDRESS_DAI).decimals(), - 100_000 * 10 ** wormhole.decimals() - ); - uint256 initialDAIBalance = dai.balanceOf(USER_SENDER); vm.startPrank(USER_SENDER); @@ -237,7 +240,7 @@ contract GlacisFacetTest is TestBaseFacet { bridgeData.hasSourceSwaps = true; // reset swap data - setDefaultSwapDataSingleDAItoWORMHOLE(); + setDefaultSwapDataSingleDAItoSourceToken(); // approval dai.approve(_facetTestContractAddress, swapData[0].fromAmount); @@ -250,7 +253,7 @@ contract GlacisFacetTest is TestBaseFacet { bridgeData.transactionId, address(uniswap), ADDRESS_DAI, - ADDRESS_WORMHOLE_TOKEN, + ADDRESS_SRC_TOKEN, swapData[0].fromAmount, bridgeData.minAmount, block.timestamp @@ -269,16 +272,75 @@ contract GlacisFacetTest is TestBaseFacet { assertEq(USER_SENDER.balance, initialETHBalance - addToMessageValue); } - function testBase_Revert_CallerHasInsufficientFunds() public override { + function testBase_Revert_BridgeAndSwapWithInvalidReceiverAddress() + public + virtual + override + { vm.startPrank(USER_SENDER); + // prepare bridgeData + bridgeData.receiver = address(0); + bridgeData.hasSourceSwaps = true; + + setDefaultSwapDataSingleDAItoSourceToken(); - wormhole.approve( + vm.expectRevert(InvalidReceiver.selector); + + initiateSwapAndBridgeTxWithFacet(false); + vm.stopPrank(); + } + + function testBase_Revert_SwapAndBridgeWithInvalidAmount() + public + virtual + override + { + vm.startPrank(USER_SENDER); + // prepare bridgeData + bridgeData.hasSourceSwaps = true; + bridgeData.minAmount = 0; + + setDefaultSwapDataSingleDAItoSourceToken(); + + vm.expectRevert(InvalidAmount.selector); + + initiateSwapAndBridgeTxWithFacet(false); + vm.stopPrank(); + } + + function testBase_Revert_SwapAndBridgeToSameChainId() + public + virtual + override + { + vm.startPrank(USER_SENDER); + // prepare bridgeData + bridgeData.destinationChainId = block.chainid; + bridgeData.hasSourceSwaps = true; + + setDefaultSwapDataSingleDAItoSourceToken(); + dai.approve(_facetTestContractAddress, swapData[0].fromAmount); + + vm.expectRevert(CannotBridgeToSameNetwork.selector); + + initiateSwapAndBridgeTxWithFacet(false); + vm.stopPrank(); + } + + function testBase_Revert_CallerHasInsufficientFunds() + public + virtual + override + { + vm.startPrank(USER_SENDER); + + srcToken.approve( address(_facetTestContractAddress), - defaultWORMHOLEAmount + defaultSrcTokenAmount ); - // send all available W balance to different account to ensure sending wallet has no W funds - wormhole.transfer(USER_RECEIVER, wormhole.balanceOf(USER_SENDER)); + // send all available source token balance to different account to ensure sending wallet has no source token funds + srcToken.transfer(USER_RECEIVER, srcToken.balanceOf(USER_SENDER)); vm.expectRevert( abi.encodeWithSelector( @@ -292,3 +354,31 @@ contract GlacisFacetTest is TestBaseFacet { vm.stopPrank(); } } + +contract GlacisFacetWormholeTest is GlacisFacetTestBase { + function setUp() public virtual override { + customRpcUrlForForking = "ETH_NODE_URI_ARBITRUM"; + customBlockNumberForForking = 298468086; + + airliftContract = IGlacisAirlift( + 0xE0A049955E18CFfd09C826C2c2e965439B6Ab272 + ); + ADDRESS_SRC_TOKEN = 0xB0fFa8000886e57F86dd5264b9582b2Ad87b2b91; // address of W token on Arbitrum network + destinationChainId = 10; + super.setUp(); + } +} + +contract GlacisFacetLINKTest is GlacisFacetTestBase { + function setUp() public virtual override { + customRpcUrlForForking = "ETH_NODE_URI_BASE"; + customBlockNumberForForking = 25427676; + + airliftContract = IGlacisAirlift( + 0x56E20A6260644CC9F0B7d79a8C8E1e3Fabc15CEA + ); + ADDRESS_SRC_TOKEN = 0x88Fb150BDc53A65fe94Dea0c9BA0a6dAf8C6e196; // address of LINK token on Base network + destinationChainId = 34443; + super.setUp(); + } +} From 5ada8c2acdebeecbd848d3e720511ae1fbd4e0a6 Mon Sep 17 00:00:00 2001 From: Michal Date: Mon, 27 Jan 2025 12:44:17 +0100 Subject: [PATCH 21/38] Small improvements --- config/glacis.json | 1 + docs/GlacisFacet.md | 9 ++------- src/Facets/GlacisFacet.sol | 4 ++-- src/Interfaces/IGlacisAirlift.sol | 1 + test/solidity/Facets/GlacisFacet.t.sol | 4 +++- 5 files changed, 9 insertions(+), 10 deletions(-) diff --git a/config/glacis.json b/config/glacis.json index 911db59b9..8bbbd9dc5 100644 --- a/config/glacis.json +++ b/config/glacis.json @@ -1,4 +1,5 @@ { + "important": "these values are test deployments only. We need to update this file when Glacis has deployed their final versions", "arbitrum": { "airlift": "0xE0A049955E18CFfd09C826C2c2e965439B6Ab272" }, diff --git a/docs/GlacisFacet.md b/docs/GlacisFacet.md index 6a4e1f1ff..107d9e04a 100644 --- a/docs/GlacisFacet.md +++ b/docs/GlacisFacet.md @@ -2,12 +2,7 @@ ## How it works -The Glacis Facet works by forwarding calls to the [GlacisAirlift](https://github.com/glacislabs/airlift-evm/blob/main/src/facets/GlacisAirliftFacet.sol) core contract on the source chain. Glacis Airlift streamlines diverse mechanisms of General Message Passing protocols (GMPs) like Axelar, LayerZero, Wormhole by integrating interchain transfer adapters for each standard and maintaining a registry of curated addresses associated with the respective interchain tokens. This design allows users to simply specify a token address for bridging. Glacis Airlift automatically forwards the request, along with the necessary bridging data, to the appropriate adapter, ensuring seamless execution of the bridging process. - -Bridge doesn’t support stablecoins like **USDT** or **USDC**. They are custom tokens focused. We can find possible routes in [`routes.ts`](https://github.com/glacislabs/airlift-evm/blob/main/node-scripts/src/tests/routes.ts). - -The [`send`](https://github.com/glacislabs/airlift-evm/blob/main/src/facets/GlacisAirliftFacet.sol#L94) function is used to execute the cross-chain transfer. -Before calling [`send`](https://github.com/glacislabs/airlift-evm/blob/main/src/facets/GlacisAirliftFacet.sol#L94), the tokens must first be sent to the contract, as the function assumes the tokens are already in place. This function is preferred over [`sendAfterApproval`](https://github.com/glacislabs/airlift-evm/blob/af935df67c3fff873edea9758ef73cd46e1908c7/src/facets/GlacisAirliftFacet.sol#L112) because it eliminates the need for redundant token transfer steps, as tokens are already transferred to the contract beforehand. +The Glacis Facet works by forwarding calls to the [GlacisAirlift](https://github.com/glacislabs/airlift-evm/blob/main/src/facets/GlacisAirliftFacet.sol) core contract on the source chain. Glacis Airlift serves as a unified interface for facilitating token bridging across various native token bridging standards, such as those employed by Axelar, LayerZero, and Wormhole. While these standards may leverage General Message Passing protocols (GMPs), the primary focus of Glacis Airlift lies in enabling seamless interaction with the token bridging mechanisms themselves. ```mermaid graph LR; @@ -27,7 +22,7 @@ graph LR; The methods listed above take a variable labeled `_glacisData`. This data is specific to glacis and is represented as the following struct type: ```solidity -/// @param refundAddress Refund address +/// @param refundAddress The address that would receive potential refunds on destination chain /// @param nativeFee The fee amount in native token required by the Glacis Airlift. struct GlacisData { address refundAddress; diff --git a/src/Facets/GlacisFacet.sol b/src/Facets/GlacisFacet.sol index ff616fef8..3c049571c 100644 --- a/src/Facets/GlacisFacet.sol +++ b/src/Facets/GlacisFacet.sol @@ -21,8 +21,8 @@ contract GlacisFacet is ILiFi, ReentrancyGuard, SwapperV2, Validatable { /// Types /// - /// @param refundAddress Refund address - /// @param nativeFee The fee amount in native token required by the Glacis Airlift. + /// @param refundAddress The address that would receive potential refunds on destination chain + /// @param nativeFee The fee amount in native token required by the Glacis Airlift struct GlacisData { address refundAddress; uint256 nativeFee; diff --git a/src/Interfaces/IGlacisAirlift.sol b/src/Interfaces/IGlacisAirlift.sol index 22630622e..8efe9aa42 100644 --- a/src/Interfaces/IGlacisAirlift.sol +++ b/src/Interfaces/IGlacisAirlift.sol @@ -68,6 +68,7 @@ interface IGlacisAirlift { /// @param receiver The target address that should receive the funds on the destination chain. /// @param destinationChainId The Ethereum chain ID of the destination chain. /// @param refundAddress The address that should receive any funds in the case the cross-chain gas value is too high. + /// @return The amount of token and value fees required to send the token across chains. function quoteSend( address token, uint256 amount, diff --git a/test/solidity/Facets/GlacisFacet.t.sol b/test/solidity/Facets/GlacisFacet.t.sol index fc773661c..36dbc7777 100644 --- a/test/solidity/Facets/GlacisFacet.t.sol +++ b/test/solidity/Facets/GlacisFacet.t.sol @@ -77,7 +77,9 @@ abstract contract GlacisFacetTestBase is TestBaseFacet { bridgeData.destinationChainId = destinationChainId; // add liquidity for dex pair DAI-{SOURCE TOKEN} - // this is necessary because Glacis does not provide routes for stablecoins like USDT or USDC, forcing us to work with custom tokens that often lack liquidity on V2 dexes + // this is necessary because Glacis does not provide routes for stablecoins + // like USDT or USDC, forcing us to work with custom tokens that often lack + // liquidity on V2 dexes addLiquidity( ADDRESS_DAI, ADDRESS_SRC_TOKEN, From 9a82d838f7c56f08e16f17d51c77174499a00887 Mon Sep 17 00:00:00 2001 From: Michal Date: Mon, 27 Jan 2025 13:21:46 +0100 Subject: [PATCH 22/38] Removed dot --- docs/GlacisFacet.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/GlacisFacet.md b/docs/GlacisFacet.md index 107d9e04a..37766781f 100644 --- a/docs/GlacisFacet.md +++ b/docs/GlacisFacet.md @@ -23,7 +23,7 @@ The methods listed above take a variable labeled `_glacisData`. This data is spe ```solidity /// @param refundAddress The address that would receive potential refunds on destination chain -/// @param nativeFee The fee amount in native token required by the Glacis Airlift. +/// @param nativeFee The fee amount in native token required by the Glacis Airlift struct GlacisData { address refundAddress; uint256 nativeFee; From b18d916539a605269f521550c7e7867e15edd2fb Mon Sep 17 00:00:00 2001 From: Michal Date: Mon, 27 Jan 2025 15:14:23 +0100 Subject: [PATCH 23/38] Added native reserve --- src/Facets/GlacisFacet.sol | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Facets/GlacisFacet.sol b/src/Facets/GlacisFacet.sol index 3c049571c..3da6d9c61 100644 --- a/src/Facets/GlacisFacet.sol +++ b/src/Facets/GlacisFacet.sol @@ -80,7 +80,8 @@ contract GlacisFacet is ILiFi, ReentrancyGuard, SwapperV2, Validatable { _bridgeData.transactionId, _bridgeData.minAmount, _swapData, - payable(msg.sender) + payable(msg.sender), + _glacisData.nativeFee ); _startBridge(_bridgeData, _glacisData); } From 1bbfb9341372e89ec4eaffc3cc4fdfe4edea15a4 Mon Sep 17 00:00:00 2001 From: Michal Date: Tue, 28 Jan 2025 12:31:48 +0100 Subject: [PATCH 24/38] Added explanation for quoteSend and payableAmount --- test/solidity/Facets/GlacisFacet.t.sol | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/test/solidity/Facets/GlacisFacet.t.sol b/test/solidity/Facets/GlacisFacet.t.sol index 36dbc7777..2d9e1e924 100644 --- a/test/solidity/Facets/GlacisFacet.t.sol +++ b/test/solidity/Facets/GlacisFacet.t.sol @@ -90,6 +90,14 @@ abstract contract GlacisFacetTestBase is TestBaseFacet { // Call `quoteSend` to estimate the required native fee for the transfer. // This is necessary to ensure the transaction has sufficient gas for execution. // The `payableAmount` parameter simulates the amount of native tokens required for the estimation. + + // While we are estimating nativeFee, we initially don't know what + // `msg.value` is "enough." That's why we need to provide an overestimation, + // for example, 1 ETH. It goes through the full + // bridging logic and determines "I only need 0.005ETH from that 1ETH." + // The nativeFee is then returned in QuoteSendInfo. By using 1 ETH, + // we’re just on the safe side of overestimation to prevent the function + // from reverting. QuoteSendInfo memory quoteSendInfo = IGlacisAirlift( address(airliftContract) ).quoteSend( From c63510b7decccbf19ce3956f172b3974daabaae2 Mon Sep 17 00:00:00 2001 From: Michal Date: Tue, 28 Jan 2025 13:00:21 +0100 Subject: [PATCH 25/38] Added explanation for quoteSend and payableAmount --- test/solidity/Facets/GlacisFacet.t.sol | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/test/solidity/Facets/GlacisFacet.t.sol b/test/solidity/Facets/GlacisFacet.t.sol index 2d9e1e924..e8e300829 100644 --- a/test/solidity/Facets/GlacisFacet.t.sol +++ b/test/solidity/Facets/GlacisFacet.t.sol @@ -91,6 +91,11 @@ abstract contract GlacisFacetTestBase is TestBaseFacet { // This is necessary to ensure the transaction has sufficient gas for execution. // The `payableAmount` parameter simulates the amount of native tokens required for the estimation. + // Since `quoteSend` is a view function and therefore not payable, + // we receive `msg.value` as a parameter. When quoting, you can simulate + // the impact on your `msg.value` by passing a sample amount (payableAmount), such as 1 ETH, + // to see how it would be adjusted during an actual send. + // While we are estimating nativeFee, we initially don't know what // `msg.value` is "enough." That's why we need to provide an overestimation, // for example, 1 ETH. It goes through the full From ba636f1618afa24c44d6000cfead30b8ce75b9a1 Mon Sep 17 00:00:00 2001 From: Michal Date: Thu, 30 Jan 2025 11:27:30 +0100 Subject: [PATCH 26/38] Updated demoScript for viem. Updated facetDemoScript template --- script/demoScripts/demoGlacis.ts | 197 ++++++++++++++++++++++++ script/demoScripts/demoGlacisAirlift.ts | 138 ----------------- templates/facetDemoScript.template.hbs | 26 ++-- 3 files changed, 210 insertions(+), 151 deletions(-) create mode 100644 script/demoScripts/demoGlacis.ts delete mode 100644 script/demoScripts/demoGlacisAirlift.ts diff --git a/script/demoScripts/demoGlacis.ts b/script/demoScripts/demoGlacis.ts new file mode 100644 index 000000000..0b6b568c6 --- /dev/null +++ b/script/demoScripts/demoGlacis.ts @@ -0,0 +1,197 @@ +import { getContract, parseUnits, Narrow, zeroAddress, parseEther } from 'viem' +import { randomBytes } from 'crypto' +import dotenv from 'dotenv' +import config from '../../config/glacis.json' +import erc20Artifact from '../../out/ERC20/ERC20.sol/ERC20.json' +import glacisFacetArtifact from '../../out/GlacisFacet.sol/GlacisFacet.json' +import { GlacisFacet, ILiFi } from '../../typechain' +import { SupportedChain } from './utils/demoScriptChainConfig' +import { + ensureBalance, + ensureAllowance, + executeTransaction, + setupEnvironment, + getConfigElement, + zeroPadAddressToBytes32, +} from './utils/demoScriptHelpers' + +dotenv.config() + +// #region ABIs + +const ERC20_ABI = erc20Artifact.abi as Narrow +const GLACIS_FACET_ABI = glacisFacetArtifact.abi as Narrow< + typeof glacisFacetArtifact.abi +> +export const AIRLIFT_ABI = [ + { + name: 'quoteSend', + type: 'function', + stateMutability: 'nonpayable', + inputs: [ + { name: 'token', type: 'address' }, + { name: 'amount', type: 'uint256' }, + { name: 'receiver', type: 'bytes32' }, + { name: 'destinationChainId', type: 'uint256' }, + { name: 'refundAddress', type: 'address' }, + { name: 'msgValue', type: 'uint256' }, + ], + outputs: [ + { + type: 'tuple', + components: [ + { name: 'nativeFee', type: 'uint256' }, + { name: 'tokenFee', type: 'uint256' }, + ], + }, + { name: 'amountSent', type: 'uint256' }, + { name: 'valueSent', type: 'uint256' }, + { + type: 'tuple', + components: [ + { + name: 'airliftFee', + type: 'tuple', + components: [ + { name: 'nativeFee', type: 'uint256' }, + { name: 'tokenFee', type: 'uint256' }, + ], + }, + { name: 'correctedAmount', type: 'uint256' }, + { name: 'correctedValue', type: 'uint256' }, + ], + }, + ], + }, +] as const + +// #endregion + +dotenv.config() + +async function main() { + // === Set up environment === + const srcChain: SupportedChain = 'arbitrum' + const destinationChainId = 10 + + const { + client, + publicClient, + walletAccount, + lifiDiamondAddress, + lifiDiamondContract, + } = await setupEnvironment(srcChain, GLACIS_FACET_ABI) + const signerAddress = walletAccount.address + + // === Contract addresses === + const SRC_TOKEN_ADDRESS = + '0xB0fFa8000886e57F86dd5264b9582b2Ad87b2b91' as `0x${string}` + const AIRLIFT_ADDRESS = getConfigElement(config, srcChain, 'airlift') + + // === Instantiate contracts === + const srcTokenContract = getContract({ + address: SRC_TOKEN_ADDRESS, + abi: ERC20_ABI, + client, + }) + + const airliftContract = getContract({ + address: AIRLIFT_ADDRESS, + abi: AIRLIFT_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('1', Number(srcTokenDecimals)) + + console.info( + `Bridge ${amount} ${srcTokenName} (${srcTokenSymbol}) from ${srcChain} --> Optimism` + ) + console.info(`Connected wallet address: ${signerAddress}`) + + await ensureBalance(srcTokenContract, signerAddress, amount) + + await ensureAllowance( + srcTokenContract, + signerAddress, + lifiDiamondAddress, + amount, + publicClient + ) + + let estimatedFees + try { + estimatedFees = ( + await airliftContract.simulate.quoteSend([ + SRC_TOKEN_ADDRESS, + amount, + zeroPadAddressToBytes32(signerAddress), + BigInt(destinationChainId), + signerAddress, + parseEther('1'), + ]) + ).result as any + + if (!estimatedFees) { + throw new Error('Invalid fee estimation from quoteSend.') + } + } catch (error) { + console.error('Fee estimation failed:', error) + process.exit(1) + } + + const structuredFees = { + gmpFee: { + nativeFee: estimatedFees[0].nativeFee as bigint, + tokenFee: estimatedFees[0].tokenFee as bigint, + }, + airliftFee: { + nativeFee: estimatedFees[3].airliftFee.nativeFee as bigint, + tokenFee: estimatedFees[3].airliftFee.tokenFee as bigint, + }, + } + const nativeFee = + structuredFees.gmpFee.nativeFee + structuredFees.airliftFee.nativeFee + + console.info(`Estimated native fee: ${nativeFee}\n`) + + // === Prepare bridge data === + const bridgeData: ILiFi.BridgeDataStruct = { + transactionId: `0x${randomBytes(32).toString('hex')}`, + bridge: 'glacis', + integrator: 'ACME Devs', + referrer: zeroAddress, + sendingAssetId: SRC_TOKEN_ADDRESS, + receiver: signerAddress, + destinationChainId, + minAmount: amount, + hasSourceSwaps: false, + hasDestinationCall: false, + } + + const glacisData: GlacisFacet.GlacisDataStruct = { + refundAddress: signerAddress, + nativeFee, + } + + // === Start bridging === + await executeTransaction( + () => + lifiDiamondContract.write.startBridgeTokensViaGlacis( + [bridgeData, glacisData], + { value: nativeFee } + ), + 'Starting bridge tokens via Glacis', + publicClient, + true + ) +} + +main() + .then(() => process.exit(0)) + .catch((error) => { + console.error(error) + process.exit(1) + }) diff --git a/script/demoScripts/demoGlacisAirlift.ts b/script/demoScripts/demoGlacisAirlift.ts deleted file mode 100644 index db2f98eff..000000000 --- a/script/demoScripts/demoGlacisAirlift.ts +++ /dev/null @@ -1,138 +0,0 @@ -import { utils, constants, Contract, ethers, BigNumber } from 'ethers' -import { - GlacisFacet__factory, - ERC20__factory, - ILiFi, - type GlacisFacet, -} from '../../typechain' -import deployments from '../../deployments/arbitrum.staging.json' -import config from '../../config/glacis.json' -import { zeroPadValue } from 'ethers6' -import dotenv from 'dotenv' -dotenv.config() - -async function main() { - const RPC_URL = process.env.ETH_NODE_URI_ARBITRUM - const PRIVATE_KEY = process.env.PRIVATE_KEY - const LIFI_ADDRESS = deployments.LiFiDiamond - const WORMHOLE_ADDRESS = '0xB0fFa8000886e57F86dd5264b9582b2Ad87b2b91' // Wormhole token on Arbitrum - const destinationChainId = 10 // Optimism - - const provider = new ethers.providers.JsonRpcProvider(RPC_URL) - const signer = new ethers.Wallet(PRIVATE_KEY as string, provider) - const glacis = GlacisFacet__factory.connect(LIFI_ADDRESS, provider) as any - - const signerAddress = await signer.getAddress() - - const token = ERC20__factory.connect(WORMHOLE_ADDRESS, provider) - const amount = utils.parseUnits('0.5', 18) - console.info( - `Transfer ${amount} Wormhole on Arbitrum to Wormhole on Optimism` - ) - console.info(`Currently connected to ${signerAddress}`) - - const balance = await token.balanceOf(signerAddress) - console.info(`Token balance for connected wallet: ${balance.toString()}`) - if (balance.eq(0)) { - console.error(`Connected account has no funds.`) - console.error(`Exiting...`) - process.exit(1) - } - - const currentAllowance = await token.allowance( - await signer.getAddress(), - LIFI_ADDRESS - ) - - if (currentAllowance.lt(amount)) { - console.info('Allowance is insufficient. Approving the required amount...') - const gasPrice = await provider.getGasPrice() - try { - const tx = await token - .connect(signer) - .approve(LIFI_ADDRESS, amount, { gasPrice }) - await tx.wait() - } catch (error) { - console.error('Approval failed:', error) - process.exit(1) - } - console.info('Approval transaction complete. New allowance set.') - } else { - console.info('Sufficient allowance already exists. No need to approve.') - } - - const bridgeData: ILiFi.BridgeDataStruct = { - transactionId: utils.randomBytes(32), - bridge: 'glacis', - integrator: 'ACME Devs', - referrer: constants.AddressZero, - sendingAssetId: WORMHOLE_ADDRESS, - receiver: signerAddress, - destinationChainId: destinationChainId, - minAmount: amount, - hasSourceSwaps: false, - hasDestinationCall: false, - } - - const airliftContract = new Contract(config.arbitrum.airlift, [ - 'function quoteSend(address token, uint256 amount, bytes32 receiver, uint256 destinationChainId, address refundAddress, uint256 msgValue) external returns ((uint256, uint256), uint256, uint256, ((uint256, uint256), uint256, uint256))', - ]) - - // calculate native fee - let estimatedFees - try { - estimatedFees = await airliftContract - .connect(signer) - .callStatic.quoteSend( - WORMHOLE_ADDRESS, - amount, - zeroPadValue(signerAddress, 32), - destinationChainId, - signerAddress, - utils.parseEther('1') - ) - if (!estimatedFees) throw new Error('Invalid fee estimation') - } catch (error) { - console.error('Fee estimation failed:', error) - process.exit(1) - } - const structuredFees = { - gmpFee: { - nativeFee: BigNumber.from(estimatedFees[0][0]), - tokenFee: BigNumber.from(estimatedFees[0][1]), - }, - airliftFee: { - nativeFee: BigNumber.from(estimatedFees[3][0][0]), - tokenFee: BigNumber.from(estimatedFees[3][0][1]), - }, - } - const nativeFee = structuredFees.gmpFee.nativeFee.add( - structuredFees.airliftFee.nativeFee - ) - - const glacisBridgeData: GlacisFacet.GlacisDataStruct = { - refundAddress: signerAddress, - nativeFee, - } - - console.info('Bridging WORMHOLE...') - try { - const tx = await glacis - .connect(signer) - .startBridgeTokensViaGlacis(bridgeData, glacisBridgeData, { - value: nativeFee, - }) - await tx.wait() - } catch (error) { - console.error('Bridge transaction failed:', error) - process.exit(1) - } - console.info('Bridged WORMHOLE') -} - -main() - .then(() => process.exit(0)) - .catch((error) => { - console.error(error) - process.exit(1) - }) diff --git a/templates/facetDemoScript.template.hbs b/templates/facetDemoScript.template.hbs index fe0849024..10dfc96ba 100644 --- a/templates/facetDemoScript.template.hbs +++ b/templates/facetDemoScript.template.hbs @@ -26,13 +26,13 @@ dotenv.config() async function main() { // === Set up environment === const srcChain: SupportedChain = "mainnet"; // Set source chain - const destinationChainId = 1 // Set destination chain id + const destinationChainId = 1; // Set destination chain id const { client, publicClient, walletAccount, lifiDiamondAddress, lifiDiamondContract } = await setupEnvironment(srcChain, {{constantCase name}}_FACET_ABI); - const signerAddress = walletAccount.address + const signerAddress = walletAccount.address; // === Contract addresses === - const SRC_TOKEN_ADDRESS = '' as `0x${string}` // Set the source token address here. + 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. @@ -49,7 +49,7 @@ async function main() { 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 @@ -62,17 +62,17 @@ 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', srcTokenDecimals) // 10 * 1e{source token decimals} + 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 * 1e{source token decimals} - console.info(`\Bridge ${amount} ${srcTokenName} (${srcTokenSymbol}) from ${srcChain} --> {DESTINATION CHAIN NAME}`) - console.info(`Connected wallet address: ${signerAddress}`) + console.info(`Bridge ${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); + await ensureAllowance(srcTokenContract, signerAddress, lifiDiamondAddress, amount, publicClient); // === In this part put necessary logic usually it's fetching quotes, estimating fees, signing messages etc. === @@ -92,11 +92,11 @@ async function main() { minAmount: amount, hasSourceSwaps: false, hasDestinationCall: false, - } + }; const {{camelCase name}}Data: {{titleCase name}}Facet.{{titleCase name}}DataStruct = { // Add your specific fields for {{titleCase name}} here. - } + }; // === Start bridging === await executeTransaction( From bccacb337ada2c7bb01271ed298c15663e2828cb Mon Sep 17 00:00:00 2001 From: Michal Date: Thu, 30 Jan 2025 11:53:10 +0100 Subject: [PATCH 27/38] Added semicolons --- script/demoScripts/demoGlacis.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/script/demoScripts/demoGlacis.ts b/script/demoScripts/demoGlacis.ts index 0b6b568c6..2525c5644 100644 --- a/script/demoScripts/demoGlacis.ts +++ b/script/demoScripts/demoGlacis.ts @@ -155,7 +155,7 @@ async function main() { const nativeFee = structuredFees.gmpFee.nativeFee + structuredFees.airliftFee.nativeFee - console.info(`Estimated native fee: ${nativeFee}\n`) + console.info(`Estimated native fee: ${nativeFee}`) // === Prepare bridge data === const bridgeData: ILiFi.BridgeDataStruct = { From b58f7bf78ee8b4e4b51d488710a91e1612ca5baf Mon Sep 17 00:00:00 2001 From: Michal Date: Thu, 30 Jan 2025 11:59:55 +0100 Subject: [PATCH 28/38] Template adjustments --- templates/facetDemoScript.template.hbs | 34 +++++++++++++------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/templates/facetDemoScript.template.hbs b/templates/facetDemoScript.template.hbs index 10dfc96ba..d140e6517 100644 --- a/templates/facetDemoScript.template.hbs +++ b/templates/facetDemoScript.template.hbs @@ -25,14 +25,14 @@ 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" // Set source chain + const destinationChainId = 1 // Set destination chain id - const { client, publicClient, walletAccount, lifiDiamondAddress, lifiDiamondContract } = await setupEnvironment(srcChain, {{constantCase name}}_FACET_ABI); - const signerAddress = walletAccount.address; + const { client, publicClient, walletAccount, lifiDiamondAddress, lifiDiamondContract } = await setupEnvironment(srcChain, {{constantCase name}}_FACET_ABI) + const signerAddress = walletAccount.address // === Contract addresses === - const SRC_TOKEN_ADDRESS = '' as `0x${string}`; // Set the source token address here. + 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. @@ -49,7 +49,7 @@ async function main() { 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 @@ -59,20 +59,20 @@ async function main() { // 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 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 * 1e{source token decimals} - console.info(`Bridge ${amount} ${srcTokenName} (${srcTokenSymbol}) from ${srcChain} --> {DESTINATION CHAIN NAME}`); - console.info(`Connected wallet address: ${signerAddress}`); + console.info(`Bridge ${amount} ${srcTokenName} (${srcTokenSymbol}) from ${srcChain} --> {DESTINATION CHAIN NAME}`) + console.info(`Connected wallet address: ${signerAddress}`) - await ensureBalance(srcTokenContract, signerAddress, amount); + await ensureBalance(srcTokenContract, signerAddress, amount) - await ensureAllowance(srcTokenContract, signerAddress, lifiDiamondAddress, amount, publicClient); + await ensureAllowance(srcTokenContract, signerAddress, lifiDiamondAddress, amount, publicClient) // === In this part put necessary logic usually it's fetching quotes, estimating fees, signing messages etc. === @@ -92,11 +92,11 @@ async function main() { minAmount: amount, hasSourceSwaps: false, hasDestinationCall: false, - }; + } const {{camelCase name}}Data: {{titleCase name}}Facet.{{titleCase name}}DataStruct = { // Add your specific fields for {{titleCase name}} here. - }; + } // === Start bridging === await executeTransaction( @@ -108,7 +108,7 @@ async function main() { 'Starting bridge tokens via {{titleCase name}}', publicClient, true - ); + ) } main() From dbb385235bdf67213f7e61344903d19094c7963b Mon Sep 17 00:00:00 2001 From: Michal Date: Thu, 30 Jan 2025 12:31:34 +0100 Subject: [PATCH 29/38] Added customed fuzzing amounts --- test/solidity/Facets/GlacisFacet.t.sol | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/test/solidity/Facets/GlacisFacet.t.sol b/test/solidity/Facets/GlacisFacet.t.sol index e8e300829..13802c751 100644 --- a/test/solidity/Facets/GlacisFacet.t.sol +++ b/test/solidity/Facets/GlacisFacet.t.sol @@ -28,6 +28,8 @@ abstract contract GlacisFacetTestBase is TestBaseFacet { uint256 internal defaultSrcTokenAmount; uint256 internal destinationChainId; address internal ADDRESS_SRC_TOKEN; + uint256 internal fuzzingAmountMinValue; + uint256 internal fuzzingAmountMaxValue; uint256 internal payableAmount = 1 ether; @@ -172,8 +174,8 @@ abstract contract GlacisFacetTestBase is TestBaseFacet { uint256 amount ) public virtual override { vm.assume( - amount > 1 * 10 ** srcToken.decimals() && - amount < 100_000 * 10 ** srcToken.decimals() + amount > fuzzingAmountMinValue * 10 ** srcToken.decimals() && + amount < fuzzingAmountMaxValue * 10 ** srcToken.decimals() ); bridgeData.minAmount = amount; @@ -380,6 +382,8 @@ contract GlacisFacetWormholeTest is GlacisFacetTestBase { ); ADDRESS_SRC_TOKEN = 0xB0fFa8000886e57F86dd5264b9582b2Ad87b2b91; // address of W token on Arbitrum network destinationChainId = 10; + fuzzingAmountMinValue = 1; // Minimum fuzzing amount (actual value includes token decimals) + fuzzingAmountMaxValue = 100_000; // Maximum fuzzing amount (actual value includes token decimals) super.setUp(); } } @@ -394,6 +398,8 @@ contract GlacisFacetLINKTest is GlacisFacetTestBase { ); ADDRESS_SRC_TOKEN = 0x88Fb150BDc53A65fe94Dea0c9BA0a6dAf8C6e196; // address of LINK token on Base network destinationChainId = 34443; + fuzzingAmountMinValue = 1; // Minimum fuzzing amount (actual value includes token decimals) + fuzzingAmountMaxValue = 10_000; // Maximum fuzzing amount (actual value includes token decimals) super.setUp(); } } From f6f3f9e6fd1a32d0b4d39cc2d2d835d786d0e038 Mon Sep 17 00:00:00 2001 From: Michal Mironczuk Date: Fri, 7 Feb 2025 09:04:33 +0100 Subject: [PATCH 30/38] Adjusments, changed to _getConfigContractAddress, removed addSelectorsToToken from IGlacisAirlift --- script/deploy/facets/DeployGlacisFacet.s.sol | 4 ++-- src/Interfaces/IGlacisAirlift.sol | 10 ---------- test/solidity/Facets/GlacisFacet.t.sol | 1 + 3 files changed, 3 insertions(+), 12 deletions(-) diff --git a/script/deploy/facets/DeployGlacisFacet.s.sol b/script/deploy/facets/DeployGlacisFacet.s.sol index ceab5eef4..76be94f98 100644 --- a/script/deploy/facets/DeployGlacisFacet.s.sol +++ b/script/deploy/facets/DeployGlacisFacet.s.sol @@ -21,9 +21,9 @@ contract DeployScript is DeployScriptBase { function getConstructorArgs() internal override returns (bytes memory) { string memory path = string.concat(root, "/config/glacis.json"); - string memory json = vm.readFile(path); - address airlift = json.readAddress( + address airlift = _getConfigContractAddress( + path, string.concat(".", network, ".airlift") ); diff --git a/src/Interfaces/IGlacisAirlift.sol b/src/Interfaces/IGlacisAirlift.sol index 8efe9aa42..b809f5e3a 100644 --- a/src/Interfaces/IGlacisAirlift.sol +++ b/src/Interfaces/IGlacisAirlift.sol @@ -21,16 +21,6 @@ struct Fee { } interface IGlacisAirlift { - /// Registers function selectors to multiple token. A selector's function must be added to the Diamond as a facet. - /// @param diamondSelectors The bytes4 selector of the token's handler function. - /// @param facetSelectors The bytes4 selector of the token's handler function. - /// @param token The token to register. - function addSelectorsToToken( - bytes4[] memory diamondSelectors, - bytes4[] memory facetSelectors, - address token - ) external; - /// Use to send a token from chain A to chain B after sending this contract the token already. /// This function should only be used when a smart contract calls it, so that the token's transfer /// and the cross-chain send are atomic within a single transaction. diff --git a/test/solidity/Facets/GlacisFacet.t.sol b/test/solidity/Facets/GlacisFacet.t.sol index 13802c751..5a4c19a0b 100644 --- a/test/solidity/Facets/GlacisFacet.t.sol +++ b/test/solidity/Facets/GlacisFacet.t.sol @@ -183,6 +183,7 @@ abstract contract GlacisFacetTestBase is TestBaseFacet { // approval srcToken.approve(address(glacisFacet), bridgeData.minAmount); + QuoteSendInfo memory quoteSendInfo = IGlacisAirlift( address(airliftContract) ).quoteSend( From 1a77677fb83b6ba6d643bb4f8bc201eec08077e5 Mon Sep 17 00:00:00 2001 From: Michal Mironczuk Date: Fri, 7 Feb 2025 09:30:10 +0100 Subject: [PATCH 31/38] Updated demo script --- script/demoScripts/demoGlacis.ts | 52 +++++--------------------------- 1 file changed, 7 insertions(+), 45 deletions(-) diff --git a/script/demoScripts/demoGlacis.ts b/script/demoScripts/demoGlacis.ts index 2525c5644..d71c60941 100644 --- a/script/demoScripts/demoGlacis.ts +++ b/script/demoScripts/demoGlacis.ts @@ -5,6 +5,8 @@ import config from '../../config/glacis.json' import erc20Artifact from '../../out/ERC20/ERC20.sol/ERC20.json' import glacisFacetArtifact from '../../out/GlacisFacet.sol/GlacisFacet.json' import { GlacisFacet, ILiFi } from '../../typechain' +import airliftArtifact from '../../out/IGlacisAirlift.sol/IGlacisAirlift.json' + import { SupportedChain } from './utils/demoScriptChainConfig' import { ensureBalance, @@ -23,47 +25,7 @@ const ERC20_ABI = erc20Artifact.abi as Narrow const GLACIS_FACET_ABI = glacisFacetArtifact.abi as Narrow< typeof glacisFacetArtifact.abi > -export const AIRLIFT_ABI = [ - { - name: 'quoteSend', - type: 'function', - stateMutability: 'nonpayable', - inputs: [ - { name: 'token', type: 'address' }, - { name: 'amount', type: 'uint256' }, - { name: 'receiver', type: 'bytes32' }, - { name: 'destinationChainId', type: 'uint256' }, - { name: 'refundAddress', type: 'address' }, - { name: 'msgValue', type: 'uint256' }, - ], - outputs: [ - { - type: 'tuple', - components: [ - { name: 'nativeFee', type: 'uint256' }, - { name: 'tokenFee', type: 'uint256' }, - ], - }, - { name: 'amountSent', type: 'uint256' }, - { name: 'valueSent', type: 'uint256' }, - { - type: 'tuple', - components: [ - { - name: 'airliftFee', - type: 'tuple', - components: [ - { name: 'nativeFee', type: 'uint256' }, - { name: 'tokenFee', type: 'uint256' }, - ], - }, - { name: 'correctedAmount', type: 'uint256' }, - { name: 'correctedValue', type: 'uint256' }, - ], - }, - ], - }, -] as const +const AIRLIFT_ABI = airliftArtifact.abi as Narrow // #endregion @@ -144,12 +106,12 @@ async function main() { const structuredFees = { gmpFee: { - nativeFee: estimatedFees[0].nativeFee as bigint, - tokenFee: estimatedFees[0].tokenFee as bigint, + nativeFee: estimatedFees.gmpFee.nativeFee as bigint, + tokenFee: estimatedFees.gmpFee.tokenFee as bigint, }, airliftFee: { - nativeFee: estimatedFees[3].airliftFee.nativeFee as bigint, - tokenFee: estimatedFees[3].airliftFee.tokenFee as bigint, + nativeFee: estimatedFees.airliftFeeInfo.airliftFee.nativeFee as bigint, + tokenFee: estimatedFees.airliftFeeInfo.airliftFee.tokenFee as bigint, }, } const nativeFee = From af8ac609d8cb6c83b75c5ac9e91fd9f4276eaa1b Mon Sep 17 00:00:00 2001 From: Michal Mironczuk Date: Fri, 7 Feb 2025 09:47:51 +0100 Subject: [PATCH 32/38] modified demo script template --- templates/facetDemoScript.template.hbs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/templates/facetDemoScript.template.hbs b/templates/facetDemoScript.template.hbs index d140e6517..ef605b169 100644 --- a/templates/facetDemoScript.template.hbs +++ b/templates/facetDemoScript.template.hbs @@ -19,6 +19,15 @@ dotenv.config() const ERC20_ABI = erc20Artifact.abi as Narrow const {{constantCase name}}_FACET_ABI = {{camelCase name}}FacetArtifact.abi as Narrow +// If you need to import a custom ABI, follow these steps: +// +// First, ensure you import the relevant artifact file: +// import exampleArtifact from '../../out/{example artifact json file}' +// +// Then, define the ABI using `Narrow` for proper type inference: +// const EXAMPLE_ABI = exampleArtifact.abi as Narrow +// + // #endregion dotenv.config() From 394087fa166ec97a4b19e9fbcc309b08f1110abe Mon Sep 17 00:00:00 2001 From: Michal Mironczuk Date: Fri, 7 Feb 2025 20:14:35 +0100 Subject: [PATCH 33/38] Updated airlift contracts, changed safeTransfer to maxApprove. Updated tests --- config/glacis.json | 6 +++--- src/Facets/GlacisFacet.sol | 8 ++++---- src/Interfaces/IGlacisAirlift.sol | 17 +---------------- test/solidity/Facets/GlacisFacet.t.sol | 4 ++-- 4 files changed, 10 insertions(+), 25 deletions(-) diff --git a/config/glacis.json b/config/glacis.json index 8bbbd9dc5..1736d6d15 100644 --- a/config/glacis.json +++ b/config/glacis.json @@ -1,12 +1,12 @@ { "important": "these values are test deployments only. We need to update this file when Glacis has deployed their final versions", "arbitrum": { - "airlift": "0xE0A049955E18CFfd09C826C2c2e965439B6Ab272" + "airlift": "0xD9E7f6f7Dc7517678127D84dBf0F0b4477De14E0" }, "optimism": { - "airlift": "0x1B388F7ee9e44BD5aA0b13ca9dF35F2489F1c717" + "airlift": "0xdEedFc11fCd2bC3E63915e8060ec48875E890BCB" }, "base": { - "airlift": "0x56e20a6260644cc9f0b7d79a8c8e1e3fabc15cea" + "airlift": "0x30095227Eb6d72FA6c09DfdeFFC766c33f7FA2DD" } } diff --git a/src/Facets/GlacisFacet.sol b/src/Facets/GlacisFacet.sol index 3da6d9c61..7ab7cc82e 100644 --- a/src/Facets/GlacisFacet.sol +++ b/src/Facets/GlacisFacet.sol @@ -95,10 +95,10 @@ contract GlacisFacet is ILiFi, ReentrancyGuard, SwapperV2, Validatable { ILiFi.BridgeData memory _bridgeData, GlacisData calldata _glacisData ) internal { - // Transfer the tokens to the Airlift contract. - // This step ensures that the tokens are already in place before calling the `send` function. - // The `send` function assumes the tokens are pre-transferred to the contract. - SafeERC20.safeTransfer( + // Approve the Airlift contract to spend the required amount of tokens. + // The `send` function assumes that the caller has already approved the token transfer, + // ensuring that the cross-chain transaction and token transfer happen atomically. + LibAsset.maxApproveERC20( IERC20(_bridgeData.sendingAssetId), address(airlift), _bridgeData.minAmount diff --git a/src/Interfaces/IGlacisAirlift.sol b/src/Interfaces/IGlacisAirlift.sol index b809f5e3a..d648630f2 100644 --- a/src/Interfaces/IGlacisAirlift.sol +++ b/src/Interfaces/IGlacisAirlift.sol @@ -21,7 +21,7 @@ struct Fee { } interface IGlacisAirlift { - /// Use to send a token from chain A to chain B after sending this contract the token already. + /// Use to send a token from chain A to chain B after approving this contract with the token. /// This function should only be used when a smart contract calls it, so that the token's transfer /// and the cross-chain send are atomic within a single transaction. /// @param token The address of the token sending across chains. @@ -37,21 +37,6 @@ interface IGlacisAirlift { address refundAddress ) external payable; - /// Use to send a token from chain A to chain B after only approving this contract to transfer the tokens. - /// This function should be used by EOAs who want to do the approval and transaction in two separate blocks. - /// @param token The address of the token sending across chains. - /// @param amount The amount of the token you want to send across chains. - /// @param receiver The target address that should receive the funds on the destination chain. - /// @param destinationChainId The Ethereum chain ID of the destination chain. - /// @param refundAddress The address that should receive any funds in the case the cross-chain gas value is too high. - function sendAfterApproval( - address token, - uint256 amount, - bytes32 receiver, - uint256 destinationChainId, - address refundAddress - ) external payable; - /// Use to quote the send a token from chain A to chain B. /// @param token The address of the token sending across chains. /// @param amount The amount of the token you want to send across chains. diff --git a/test/solidity/Facets/GlacisFacet.t.sol b/test/solidity/Facets/GlacisFacet.t.sol index 5a4c19a0b..a2acffca1 100644 --- a/test/solidity/Facets/GlacisFacet.t.sol +++ b/test/solidity/Facets/GlacisFacet.t.sol @@ -376,7 +376,7 @@ abstract contract GlacisFacetTestBase is TestBaseFacet { contract GlacisFacetWormholeTest is GlacisFacetTestBase { function setUp() public virtual override { customRpcUrlForForking = "ETH_NODE_URI_ARBITRUM"; - customBlockNumberForForking = 298468086; + customBlockNumberForForking = 303669576; airliftContract = IGlacisAirlift( 0xE0A049955E18CFfd09C826C2c2e965439B6Ab272 @@ -392,7 +392,7 @@ contract GlacisFacetWormholeTest is GlacisFacetTestBase { contract GlacisFacetLINKTest is GlacisFacetTestBase { function setUp() public virtual override { customRpcUrlForForking = "ETH_NODE_URI_BASE"; - customBlockNumberForForking = 25427676; + customBlockNumberForForking = 26082794; airliftContract = IGlacisAirlift( 0x56E20A6260644CC9F0B7d79a8C8E1e3Fabc15CEA From 9e623bc2a218399172e734af0ba6dfa3f76963a5 Mon Sep 17 00:00:00 2001 From: Michal Mironczuk Date: Fri, 7 Feb 2025 21:31:44 +0100 Subject: [PATCH 34/38] Updated tests addresses --- test/solidity/Facets/GlacisFacet.t.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/solidity/Facets/GlacisFacet.t.sol b/test/solidity/Facets/GlacisFacet.t.sol index a2acffca1..aa420c2dd 100644 --- a/test/solidity/Facets/GlacisFacet.t.sol +++ b/test/solidity/Facets/GlacisFacet.t.sol @@ -379,7 +379,7 @@ contract GlacisFacetWormholeTest is GlacisFacetTestBase { customBlockNumberForForking = 303669576; airliftContract = IGlacisAirlift( - 0xE0A049955E18CFfd09C826C2c2e965439B6Ab272 + 0xD9E7f6f7Dc7517678127D84dBf0F0b4477De14E0 ); ADDRESS_SRC_TOKEN = 0xB0fFa8000886e57F86dd5264b9582b2Ad87b2b91; // address of W token on Arbitrum network destinationChainId = 10; @@ -395,7 +395,7 @@ contract GlacisFacetLINKTest is GlacisFacetTestBase { customBlockNumberForForking = 26082794; airliftContract = IGlacisAirlift( - 0x56E20A6260644CC9F0B7d79a8C8E1e3Fabc15CEA + 0x30095227Eb6d72FA6c09DfdeFFC766c33f7FA2DD ); ADDRESS_SRC_TOKEN = 0x88Fb150BDc53A65fe94Dea0c9BA0a6dAf8C6e196; // address of LINK token on Base network destinationChainId = 34443; From dad7806ed0c2aeba5f68c56ea63108852987dd9f Mon Sep 17 00:00:00 2001 From: Michal Mironczuk Date: Wed, 12 Feb 2025 12:19:05 +0100 Subject: [PATCH 35/38] removed unused SafeERC20 (audit issue #1) --- src/Facets/GlacisFacet.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Facets/GlacisFacet.sol b/src/Facets/GlacisFacet.sol index 7ab7cc82e..a76b6ac4e 100644 --- a/src/Facets/GlacisFacet.sol +++ b/src/Facets/GlacisFacet.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.17; import { ILiFi } from "../Interfaces/ILiFi.sol"; -import { LibAsset, IERC20, SafeERC20 } from "../Libraries/LibAsset.sol"; +import { LibAsset, IERC20 } from "../Libraries/LibAsset.sol"; import { LibSwap } from "../Libraries/LibSwap.sol"; import { ReentrancyGuard } from "../Helpers/ReentrancyGuard.sol"; import { SwapperV2 } from "../Helpers/SwapperV2.sol"; From f5cdbc279f0f15ed469650d5b9b4185c0c668547 Mon Sep 17 00:00:00 2001 From: Michal Mironczuk Date: Wed, 12 Feb 2025 13:52:59 +0100 Subject: [PATCH 36/38] Validate GlacisData.refundAddress is non zero (audit issue #4) --- src/Facets/GlacisFacet.sol | 6 ++++++ test/solidity/Facets/GlacisFacet.t.sol | 30 ++++++++++++++++++++++---- 2 files changed, 32 insertions(+), 4 deletions(-) diff --git a/src/Facets/GlacisFacet.sol b/src/Facets/GlacisFacet.sol index a76b6ac4e..975fa78f5 100644 --- a/src/Facets/GlacisFacet.sol +++ b/src/Facets/GlacisFacet.sol @@ -35,6 +35,10 @@ contract GlacisFacet is ILiFi, ReentrancyGuard, SwapperV2, Validatable { airlift = _airlift; } + /// Errors /// + + error InvalidRefundAddress(); + /// External Methods /// /// @notice Bridges tokens via Glacis @@ -95,6 +99,8 @@ contract GlacisFacet is ILiFi, ReentrancyGuard, SwapperV2, Validatable { ILiFi.BridgeData memory _bridgeData, GlacisData calldata _glacisData ) internal { + if (_glacisData.refundAddress == address(0)) + revert InvalidRefundAddress(); // Approve the Airlift contract to spend the required amount of tokens. // The `send` function assumes that the caller has already approved the token transfer, // ensuring that the cross-chain transaction and token transfer happen atomically. diff --git a/test/solidity/Facets/GlacisFacet.t.sol b/test/solidity/Facets/GlacisFacet.t.sol index aa420c2dd..ff287815f 100644 --- a/test/solidity/Facets/GlacisFacet.t.sol +++ b/test/solidity/Facets/GlacisFacet.t.sol @@ -21,7 +21,7 @@ contract TestGlacisFacet is GlacisFacet { } abstract contract GlacisFacetTestBase is TestBaseFacet { - GlacisFacet.GlacisData internal validGlacisData; + GlacisFacet.GlacisData internal glacisData; IGlacisAirlift internal airliftContract; TestGlacisFacet internal glacisFacet; ERC20 internal srcToken; @@ -121,7 +121,7 @@ abstract contract GlacisFacetTestBase is TestBaseFacet { quoteSendInfo.airliftFeeInfo.airliftFee.nativeFee; // produce valid GlacisData - validGlacisData = GlacisFacet.GlacisData({ + glacisData = GlacisFacet.GlacisData({ refundAddress: REFUND_WALLET, nativeFee: addToMessageValue }); @@ -130,14 +130,14 @@ abstract contract GlacisFacetTestBase is TestBaseFacet { function initiateBridgeTxWithFacet(bool) internal virtual override { glacisFacet.startBridgeTokensViaGlacis{ value: addToMessageValue }( bridgeData, - validGlacisData + glacisData ); } function initiateSwapAndBridgeTxWithFacet(bool) internal virtual override { glacisFacet.swapAndStartBridgeTokensViaGlacis{ value: addToMessageValue - }(bridgeData, swapData, validGlacisData); + }(bridgeData, swapData, glacisData); } function testBase_CanBridgeNativeTokens() public virtual override { @@ -371,6 +371,28 @@ abstract contract GlacisFacetTestBase is TestBaseFacet { initiateBridgeTxWithFacet(false); vm.stopPrank(); } + + function testRevert_InvalidRefundAddress() public virtual { + vm.startPrank(USER_SENDER); + + glacisData = GlacisFacet.GlacisData({ + refundAddress: address(0), + nativeFee: addToMessageValue + }); + + srcToken.approve( + address(_facetTestContractAddress), + defaultSrcTokenAmount + ); + + vm.expectRevert( + abi.encodeWithSelector(GlacisFacet.InvalidRefundAddress.selector) + ); + + initiateBridgeTxWithFacet(false); + + vm.stopPrank(); + } } contract GlacisFacetWormholeTest is GlacisFacetTestBase { From f9276e33393986022f90b48fd0c5a025fa9702b6 Mon Sep 17 00:00:00 2001 From: Michal Mironczuk Date: Wed, 12 Feb 2025 14:25:06 +0100 Subject: [PATCH 37/38] Add noNativeAsset modifier (audit issue #3) --- src/Facets/GlacisFacet.sol | 2 ++ test/solidity/Facets/GlacisFacet.t.sol | 31 +++++++++++++++++++++++++- 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/src/Facets/GlacisFacet.sol b/src/Facets/GlacisFacet.sol index 975fa78f5..7ca8219c2 100644 --- a/src/Facets/GlacisFacet.sol +++ b/src/Facets/GlacisFacet.sol @@ -55,6 +55,7 @@ contract GlacisFacet is ILiFi, ReentrancyGuard, SwapperV2, Validatable { validateBridgeData(_bridgeData) doesNotContainSourceSwaps(_bridgeData) doesNotContainDestinationCalls(_bridgeData) + noNativeAsset(_bridgeData) { LibAsset.depositAsset( _bridgeData.sendingAssetId, @@ -79,6 +80,7 @@ contract GlacisFacet is ILiFi, ReentrancyGuard, SwapperV2, Validatable { containsSourceSwaps(_bridgeData) doesNotContainDestinationCalls(_bridgeData) validateBridgeData(_bridgeData) + noNativeAsset(_bridgeData) { _bridgeData.minAmount = _depositAndSwap( _bridgeData.transactionId, diff --git a/test/solidity/Facets/GlacisFacet.t.sol b/test/solidity/Facets/GlacisFacet.t.sol index ff287815f..137088282 100644 --- a/test/solidity/Facets/GlacisFacet.t.sol +++ b/test/solidity/Facets/GlacisFacet.t.sol @@ -5,7 +5,7 @@ import { LibAllowList, TestBaseFacet, ERC20 } from "../utils/TestBaseFacet.sol"; import { LibSwap } from "lifi/Libraries/LibSwap.sol"; import { GlacisFacet } from "lifi/Facets/GlacisFacet.sol"; import { IGlacisAirlift, QuoteSendInfo } from "lifi/Interfaces/IGlacisAirlift.sol"; -import { InsufficientBalance, InvalidReceiver, InvalidAmount, CannotBridgeToSameNetwork } from "lifi/Errors/GenericErrors.sol"; +import { InsufficientBalance, InvalidReceiver, InvalidAmount, CannotBridgeToSameNetwork, NativeAssetNotSupported } from "lifi/Errors/GenericErrors.sol"; // Stub GlacisFacet Contract contract TestGlacisFacet is GlacisFacet { @@ -393,6 +393,35 @@ abstract contract GlacisFacetTestBase is TestBaseFacet { vm.stopPrank(); } + + function testRevert_WhenTryToBridgeNativeAsset() public virtual { + vm.startPrank(USER_SENDER); + + bridgeData.sendingAssetId = address(0); // address zero is considered as native asset + + vm.expectRevert( + abi.encodeWithSelector(NativeAssetNotSupported.selector) + ); + + initiateBridgeTxWithFacet(false); + + vm.stopPrank(); + } + + function testRevert_WhenTryToSwapAndBridgeNativeAsset() public virtual { + vm.startPrank(USER_SENDER); + + bridgeData.hasSourceSwaps = true; + bridgeData.sendingAssetId = address(0); // address zero is considered as native asset + + vm.expectRevert( + abi.encodeWithSelector(NativeAssetNotSupported.selector) + ); + + initiateSwapAndBridgeTxWithFacet(false); + + vm.stopPrank(); + } } contract GlacisFacetWormholeTest is GlacisFacetTestBase { From 6914042ceea83792fb376f152a575ef89b140ecc Mon Sep 17 00:00:00 2001 From: Michal Mironczuk Date: Mon, 17 Feb 2025 14:38:01 +0100 Subject: [PATCH 38/38] Fixed description - refunds on source chain --- docs/GlacisFacet.md | 2 +- src/Facets/GlacisFacet.sol | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/GlacisFacet.md b/docs/GlacisFacet.md index 37766781f..f126e29da 100644 --- a/docs/GlacisFacet.md +++ b/docs/GlacisFacet.md @@ -22,7 +22,7 @@ graph LR; The methods listed above take a variable labeled `_glacisData`. This data is specific to glacis and is represented as the following struct type: ```solidity -/// @param refundAddress The address that would receive potential refunds on destination chain +/// @param refundAddress The address that would receive potential refunds on source chain /// @param nativeFee The fee amount in native token required by the Glacis Airlift struct GlacisData { address refundAddress; diff --git a/src/Facets/GlacisFacet.sol b/src/Facets/GlacisFacet.sol index 7ca8219c2..dbf62d6d6 100644 --- a/src/Facets/GlacisFacet.sol +++ b/src/Facets/GlacisFacet.sol @@ -21,7 +21,7 @@ contract GlacisFacet is ILiFi, ReentrancyGuard, SwapperV2, Validatable { /// Types /// - /// @param refundAddress The address that would receive potential refunds on destination chain + /// @param refundAddress The address that would receive potential refunds on source chain /// @param nativeFee The fee amount in native token required by the Glacis Airlift struct GlacisData { address refundAddress;