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/55] 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/55] 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/55] 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/55] 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/55] 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/55] 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/55] 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/55] 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/55] 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/55] 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/55] 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/55] 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/55] 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/55] 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/55] 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/55] 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/55] 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/55] 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/55] 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/55] 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/55] 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/55] 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/55] 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/55] 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/55] 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/55] 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/55] 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/55] 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/55] 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/55] 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/55] 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/55] 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/55] 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/55] 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/55] 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/55] 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/55] 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/55] 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; From 4a75efd4683cf929cefefea7ac9895f13646b0d8 Mon Sep 17 00:00:00 2001 From: Michal Mironczuk Date: Wed, 19 Feb 2025 08:42:27 +0100 Subject: [PATCH 39/55] Added audit --- .../reports/2025.02.19_GlacisFacet(v1.0.0).pdf | Bin 0 -> 84692 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 audit/reports/2025.02.19_GlacisFacet(v1.0.0).pdf diff --git a/audit/reports/2025.02.19_GlacisFacet(v1.0.0).pdf b/audit/reports/2025.02.19_GlacisFacet(v1.0.0).pdf new file mode 100644 index 0000000000000000000000000000000000000000..3eae433dbb956c38b22d69ff578285032c6c24d2 GIT binary patch literal 84692 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 Pi$Ouj?%Yw Date: Wed, 19 Feb 2025 08:49:45 +0100 Subject: [PATCH 40/55] Updated auditLog --- audit/auditLog.json | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/audit/auditLog.json b/audit/auditLog.json index 2405449a3..7eb4cd0dd 100644 --- a/audit/auditLog.json +++ b/audit/auditLog.json @@ -160,6 +160,13 @@ "auditorGitHandle": "sujithsomraaj", "auditReportPath": "./audit/reports/2025.01.17_LiFiDexAggregator(v1.4.0).pdf", "auditCommitHash": "n/a (one deployed contract instance was audited)" + }, + "audit20250219_1": { + "auditCompletedOn": "19.02.2025", + "auditedBy": "Sujith Somraaj (individual security researcher)", + "auditorGitHandle": "sujithsomraaj", + "auditReportPath": "./audit/reports/2025.02.19_GlacisFacet(v1.0.0).pdf", + "auditCommitHash": "9e623bc2a218399172e734af0ba6dfa3f76963a5" } }, "auditedContracts": { @@ -198,6 +205,9 @@ "1.0.0": ["audit20241107"], "1.0.1": ["audit20250110_1"] }, + "GlacisFacet": { + "1.0.0": ["audit20250219_1"] + }, "IAcrossSpokePool": { "1.0.0": ["audit20250106"] }, From f058ba2275d2a5fd6f1dc22f1795394783b720ee Mon Sep 17 00:00:00 2001 From: Michal Mironczuk Date: Wed, 19 Feb 2025 08:53:03 +0100 Subject: [PATCH 41/55] Updated auditlog. Added IGlacisAirlift --- audit/auditLog.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/audit/auditLog.json b/audit/auditLog.json index 7eb4cd0dd..3c372bd99 100644 --- a/audit/auditLog.json +++ b/audit/auditLog.json @@ -214,6 +214,9 @@ "IGasZip": { "1.0.0": ["audit20241107"] }, + "IGlacisAirlift": { + "1.0.0": ["audit20250219_1"] + }, "LibAsset": { "1.0.1": ["audit20241202"], "1.0.2": ["audit20250110_1"] From 406124ee5a21fbb4e08c27232e1d0632e36e3fc9 Mon Sep 17 00:00:00 2001 From: Michal Mironczuk Date: Wed, 19 Feb 2025 11:25:24 +0100 Subject: [PATCH 42/55] d --- .github/workflows/securityAlertsReview.yml | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/.github/workflows/securityAlertsReview.yml b/.github/workflows/securityAlertsReview.yml index bdb471fbd..0860525c5 100644 --- a/.github/workflows/securityAlertsReview.yml +++ b/.github/workflows/securityAlertsReview.yml @@ -2,7 +2,7 @@ name: Security Alerts Review # - ensures that all security alerts from olympix static analysis are properly handled before merging # - enforces a strict review policy where every alert must be either resolved or dismissed with a justification -# - prevents merging a PR if any alerts are unresolved or dismissed without a comment +# - prevents merging a PR if any alerts are unresolved or dismissed without a comment or wrong dismissed reason # - helps maintain a transparent and collaborative security review process by generating a pr comment summarizing the status of all security alerts # - automatically reverts the PR to draft status if blocking alerts exist # - leaves a summary of all alerts in a comment starting with "🤖 GitHub Action: Security Alerts Review 🔍" @@ -59,7 +59,7 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | if [ -z "${{ steps.findPr.outputs.number }}" ]; then - echo "Error: No pull request found for this push." >&2 + echo "Error: No pull request found." >&2 exit 1 fi echo "Found PR number: ${{ steps.findPr.outputs.number }}" @@ -73,7 +73,7 @@ jobs: run: | echo "Fetching security alerts for PR #${PR_NUMBER}..." - # Fetch security alerts via GitHub API + # Fetch security alerts ALERTS=$(curl -s -H "Authorization: token ${GITHUB_TOKEN}" \ "https://api.github.com/repos/${{ github.repository }}/code-scanning/alerts?pr=${PR_NUMBER}") @@ -85,7 +85,7 @@ jobs: UNRESOLVED_ALERTS=$(echo "$ALERTS" | jq -c '[.[] | select(.state == "open") ]' || echo "[]") # Extract dismissed alerts without comments (empty dismissed_comment) DISMISSED_ALERTS=$(echo "$ALERTS" | jq -c '[.[] | select(.state == "dismissed" and (.dismissed_comment == null or .dismissed_comment == ""))]' || echo "[]") - # Extract dismissed alerts with comments (successful dismissals) + # Extract resolved alerts (dismissed alerts with comments) - successful dismissals RESOLVED_ALERTS=$(echo "$ALERTS" | jq -c '[.[] | select(.state == "dismissed" and (.dismissed_comment != null and .dismissed_comment != ""))]' || echo "[]") UNRESOLVED_COUNT=$(echo "$UNRESOLVED_ALERTS" | jq -r 'length') @@ -188,7 +188,7 @@ jobs: COMMENT_BODY+="⚠️ **Please resolve the above issues before merging.**\n\n" fi - # Add Dismissed alerts With valid comments (successful dismissals) + # Add Dismissed Alerts With Valid Comments (Successful dismissals) if [[ "$COMMENTED_COUNT" -gt 0 ]]; then COMMENT_BODY+="🟢 **Dismissed Security Alerts with Comments**\n" COMMENT_BODY+="The following alerts were dismissed with proper comments:\n\n" @@ -238,7 +238,7 @@ jobs: run: | echo "🔍 Checking if the workflow should fail and revert PR to draft based on security alerts..." - # Check if there are any unresolved alerts, dismissed alerts missing comments, or invalid dismissal reasons. + # Check if there are any unresolved alerts, dismissed alerts missing comments or invalid dismissal reasons. if [[ "$UNRESOLVED_COUNT" -gt 0 || "$DISMISSED_COUNT" -gt 0 || "$INVALID_REASON_COUNT" -gt 0 ]]; then echo "❌ ERROR: Found issues in the PR:" if [[ "$UNRESOLVED_COUNT" -gt 0 ]]; then @@ -252,11 +252,12 @@ jobs: fi echo "⚠️ These alerts must be resolved before merging." - # Retrieve PR Node ID directly from github event + # Retrieve PR Node ID PULL_REQUEST_NODE_ID="${{ github.event.pull_request.node_id }}" echo "PR Node ID: $PULL_REQUEST_NODE_ID" # Revert the PR to draft. + # To convert a PR to a draft GitHub only provides a GraphQL api, there is no REST API for this echo "Reverting PR #${PR_NUMBER} to draft state due to blocking security issues..." curl -H "Authorization: Bearer $GITHUB_TOKEN" \ -H "Content-Type: application/json" \ From f41b27bc4ee35f01ae82baeb51fc3c091cf8c7ab Mon Sep 17 00:00:00 2001 From: Michal Mironczuk Date: Wed, 19 Feb 2025 12:20:04 +0100 Subject: [PATCH 43/55] Added paths --- .github/workflows/securityAlertsReview.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/securityAlertsReview.yml b/.github/workflows/securityAlertsReview.yml index 0860525c5..b132f5203 100644 --- a/.github/workflows/securityAlertsReview.yml +++ b/.github/workflows/securityAlertsReview.yml @@ -11,6 +11,8 @@ on: pull_request: types: - ready_for_review + paths: + - 'src/**/*.sol' workflow_dispatch: jobs: From c41a0da1f2e29f3a1ddf1a62c57683fa40ad73bd Mon Sep 17 00:00:00 2001 From: Michal Mironczuk Date: Wed, 19 Feb 2025 13:01:05 +0100 Subject: [PATCH 44/55] Fixed audit identifier --- audit/auditLog.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/audit/auditLog.json b/audit/auditLog.json index 3c372bd99..f3668dde4 100644 --- a/audit/auditLog.json +++ b/audit/auditLog.json @@ -161,7 +161,7 @@ "auditReportPath": "./audit/reports/2025.01.17_LiFiDexAggregator(v1.4.0).pdf", "auditCommitHash": "n/a (one deployed contract instance was audited)" }, - "audit20250219_1": { + "audit20250219": { "auditCompletedOn": "19.02.2025", "auditedBy": "Sujith Somraaj (individual security researcher)", "auditorGitHandle": "sujithsomraaj", @@ -206,7 +206,7 @@ "1.0.1": ["audit20250110_1"] }, "GlacisFacet": { - "1.0.0": ["audit20250219_1"] + "1.0.0": ["audit20250219"] }, "IAcrossSpokePool": { "1.0.0": ["audit20250106"] @@ -215,7 +215,7 @@ "1.0.0": ["audit20241107"] }, "IGlacisAirlift": { - "1.0.0": ["audit20250219_1"] + "1.0.0": ["audit20250219"] }, "LibAsset": { "1.0.1": ["audit20241202"], From f1b3f539cfd8b7a1e15acd5f209cd9f21d464117 Mon Sep 17 00:00:00 2001 From: Michal Mironczuk Date: Wed, 19 Feb 2025 14:13:34 +0100 Subject: [PATCH 45/55] Merge branch 'main' of github.com:lifinance/contracts From 8ca1d4f081efc89c248d8264d987deb4609f6f43 Mon Sep 17 00:00:00 2001 From: Michal Mironczuk Date: Wed, 26 Feb 2025 15:42:34 +0100 Subject: [PATCH 46/55] Deployed new facet version of Glacis --- deployments/_deployments_log_file.json | 6 +++--- deployments/arbitrum.diamond.staging.json | 7 +++++-- deployments/arbitrum.staging.json | 2 +- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/deployments/_deployments_log_file.json b/deployments/_deployments_log_file.json index 6cd7f2365..f0da969a9 100644 --- a/deployments/_deployments_log_file.json +++ b/deployments/_deployments_log_file.json @@ -29728,10 +29728,10 @@ "staging": { "1.0.0": [ { - "ADDRESS": "0x3aF0c2dB91f75f05493E51cFcF92eC5276bc85F8", + "ADDRESS": "0xF82830B952Bc60b93206FA22f1cD4770cedb2840", "OPTIMIZER_RUNS": "1000000", - "TIMESTAMP": "2025-01-23 14:43:01", - "CONSTRUCTOR_ARGS": "0x000000000000000000000000e0a049955e18cffd09c826c2c2e965439b6ab272", + "TIMESTAMP": "2025-02-26 15:22:09", + "CONSTRUCTOR_ARGS": "0x000000000000000000000000d9e7f6f7dc7517678127d84dbf0f0b4477de14e0", "SALT": "", "VERIFIED": "true" } diff --git a/deployments/arbitrum.diamond.staging.json b/deployments/arbitrum.diamond.staging.json index d8b678fb0..98ee3bf74 100644 --- a/deployments/arbitrum.diamond.staging.json +++ b/deployments/arbitrum.diamond.staging.json @@ -145,9 +145,13 @@ "Name": "AcrossFacetV3", "Version": "1.1.0" }, - "0x3aF0c2dB91f75f05493E51cFcF92eC5276bc85F8": { + "0xF82830B952Bc60b93206FA22f1cD4770cedb2840": { "Name": "GlacisFacet", "Version": "1.0.0" + }, + "0xDd661337B48BEA5194F6d26F2C59fF0855E15289": { + "Name": "", + "Version": "" } }, "Periphery": { @@ -158,7 +162,6 @@ "LiFiDEXAggregator": "", "LiFuelFeeCollector": "0x94EA56D8049e93E0308B9c7d1418Baf6A7C68280", "Permit2Proxy": "0xb33Fe241BEd9bf5F694101D7498F63a0d060F999", - "Receiver": "0x36E9B2E8A627474683eF3b1E9Df26D2bF04396f3", "ReceiverAcrossV3": "0xe4F3DEF14D61e47c696374453CD64d438FD277F8", "ReceiverStargateV2": "", "RelayerCelerIM": "0xa1Ed8783AC96385482092b82eb952153998e9b70", diff --git a/deployments/arbitrum.staging.json b/deployments/arbitrum.staging.json index d6dca2fec..85c94bf9a 100644 --- a/deployments/arbitrum.staging.json +++ b/deployments/arbitrum.staging.json @@ -51,5 +51,5 @@ "ReceiverAcrossV3": "0xe4F3DEF14D61e47c696374453CD64d438FD277F8", "AcrossFacetPackedV3": "0x21767081Ff52CE5563A29f27149D01C7127775A2", "RelayFacet": "0x3cf7dE0e31e13C93c8Aada774ADF1C7eD58157f5", - "GlacisFacet": "0x3aF0c2dB91f75f05493E51cFcF92eC5276bc85F8" + "GlacisFacet": "0xF82830B952Bc60b93206FA22f1cD4770cedb2840" } \ No newline at end of file From 5dd96e50b10c9c0d94c4d79c4f3aaa563f734c9f Mon Sep 17 00:00:00 2001 From: Michal Mironczuk Date: Fri, 28 Feb 2025 09:57:05 +0100 Subject: [PATCH 47/55] Updated securityAlertsReview --- .github/workflows/securityAlertsReview.yml | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/.github/workflows/securityAlertsReview.yml b/.github/workflows/securityAlertsReview.yml index 90d83147a..3b14fc04f 100644 --- a/.github/workflows/securityAlertsReview.yml +++ b/.github/workflows/securityAlertsReview.yml @@ -2,7 +2,7 @@ name: Security Alerts Review # - ensures that all security alerts from olympix static analysis are properly handled before merging # - enforces a strict review policy where every alert must be either resolved or dismissed with a justification -# - prevents merging a PR if any alerts are unresolved or dismissed without a comment or wrong dismissed reason +# - prevents merging a PR if any alerts are unresolved or dismissed without a comment # - helps maintain a transparent and collaborative security review process by generating a pr comment summarizing the status of all security alerts # - automatically reverts the PR to draft status if blocking alerts exist # - leaves a summary of all alerts in a comment starting with "🤖 GitHub Action: Security Alerts Review 🔍" @@ -61,7 +61,7 @@ jobs: GITHUB_TOKEN: ${{ secrets.GIT_ACTIONS_BOT_PAT_CLASSIC }} run: | if [ -z "${{ steps.findPr.outputs.number }}" ]; then - echo "Error: No pull request found." >&2 + echo "Error: No pull request found for this push." >&2 exit 1 fi echo "Found PR number: ${{ steps.findPr.outputs.number }}" @@ -75,7 +75,7 @@ jobs: run: | echo "Fetching security alerts for PR #${PR_NUMBER}..." - # Fetch security alerts + # Fetch security alerts via GitHub API ALERTS=$(curl -s -H "Authorization: token ${GITHUB_TOKEN}" \ "https://api.github.com/repos/${{ github.repository }}/code-scanning/alerts?pr=${PR_NUMBER}") @@ -87,7 +87,7 @@ jobs: UNRESOLVED_ALERTS=$(echo "$ALERTS" | jq -c '[.[] | select(.state == "open") ]' || echo "[]") # Extract dismissed alerts without comments (empty dismissed_comment) DISMISSED_ALERTS=$(echo "$ALERTS" | jq -c '[.[] | select(.state == "dismissed" and (.dismissed_comment == null or .dismissed_comment == ""))]' || echo "[]") - # Extract resolved alerts (dismissed alerts with comments) - successful dismissals + # Extract dismissed alerts with comments (successful dismissals) RESOLVED_ALERTS=$(echo "$ALERTS" | jq -c '[.[] | select(.state == "dismissed" and (.dismissed_comment != null and .dismissed_comment != ""))]' || echo "[]") UNRESOLVED_COUNT=$(echo "$UNRESOLVED_ALERTS" | jq -r 'length') @@ -190,7 +190,7 @@ jobs: COMMENT_BODY+="⚠️ **Please resolve the above issues before merging.**\n\n" fi - # Add Dismissed Alerts With Valid Comments (Successful dismissals) + # Add Dismissed alerts With valid comments (successful dismissals) if [[ "$COMMENTED_COUNT" -gt 0 ]]; then COMMENT_BODY+="🟢 **Dismissed Security Alerts with Comments**\n" COMMENT_BODY+="The following alerts were dismissed with proper comments:\n\n" @@ -240,7 +240,7 @@ jobs: run: | echo "🔍 Checking if the workflow should fail and revert PR to draft based on security alerts..." - # Check if there are any unresolved alerts, dismissed alerts missing comments or invalid dismissal reasons. + # Check if there are any unresolved alerts, dismissed alerts missing comments, or invalid dismissal reasons. if [[ "$UNRESOLVED_COUNT" -gt 0 || "$DISMISSED_COUNT" -gt 0 || "$INVALID_REASON_COUNT" -gt 0 ]]; then echo "❌ ERROR: Found issues in the PR:" if [[ "$UNRESOLVED_COUNT" -gt 0 ]]; then @@ -254,12 +254,11 @@ jobs: fi echo "⚠️ These alerts must be resolved before merging." - # Retrieve PR Node ID + # Retrieve PR Node ID directly from github event PULL_REQUEST_NODE_ID="${{ github.event.pull_request.node_id }}" echo "PR Node ID: $PULL_REQUEST_NODE_ID" # Revert the PR to draft. - # To convert a PR to a draft GitHub only provides a GraphQL api, there is no REST API for this echo "Reverting PR #${PR_NUMBER} to draft state due to blocking security issues..." curl -H "Authorization: Bearer $GITHUB_TOKEN" \ -H "Content-Type: application/json" \ From a9cab23b6e0bb30d1f7a11747631648cda809fc4 Mon Sep 17 00:00:00 2001 From: Michal Mironczuk Date: Fri, 28 Feb 2025 10:17:15 +0100 Subject: [PATCH 48/55] Added _airlift validation in constructor --- src/Facets/GlacisFacet.sol | 4 ++++ test/solidity/Facets/GlacisFacet.t.sol | 7 ++++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/Facets/GlacisFacet.sol b/src/Facets/GlacisFacet.sol index dbf62d6d6..65a8522a2 100644 --- a/src/Facets/GlacisFacet.sol +++ b/src/Facets/GlacisFacet.sol @@ -8,6 +8,7 @@ import { ReentrancyGuard } from "../Helpers/ReentrancyGuard.sol"; import { SwapperV2 } from "../Helpers/SwapperV2.sol"; import { Validatable } from "../Helpers/Validatable.sol"; import { IGlacisAirlift } from "../Interfaces/IGlacisAirlift.sol"; +import { InvalidConfig } from "../Errors/GenericErrors.sol"; /// @title Glacis Facet /// @author LI.FI (https://li.fi/) @@ -32,6 +33,9 @@ contract GlacisFacet is ILiFi, ReentrancyGuard, SwapperV2, Validatable { /// @notice Initializes the GlacisFacet contract /// @param _airlift The address of Glacis Airlift contract. constructor(IGlacisAirlift _airlift) { + if (address(_airlift) == address(0)) { + revert InvalidConfig(); + } airlift = _airlift; } diff --git a/test/solidity/Facets/GlacisFacet.t.sol b/test/solidity/Facets/GlacisFacet.t.sol index 137088282..bb5b4bc43 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, NativeAssetNotSupported } from "lifi/Errors/GenericErrors.sol"; +import { InsufficientBalance, InvalidReceiver, InvalidAmount, CannotBridgeToSameNetwork, NativeAssetNotSupported, InvalidConfig } from "lifi/Errors/GenericErrors.sol"; // Stub GlacisFacet Contract contract TestGlacisFacet is GlacisFacet { @@ -127,6 +127,11 @@ abstract contract GlacisFacetTestBase is TestBaseFacet { }); } + function testRevert_WhenConstructedWithZeroAddress() public { + vm.expectRevert(InvalidConfig.selector); + new TestGlacisFacet(IGlacisAirlift(address(0))); + } + function initiateBridgeTxWithFacet(bool) internal virtual override { glacisFacet.startBridgeTokensViaGlacis{ value: addToMessageValue }( bridgeData, From 56f8e893786546610bf904001a395550cea3d33d Mon Sep 17 00:00:00 2001 From: Michal Mironczuk Date: Fri, 28 Feb 2025 10:27:29 +0100 Subject: [PATCH 49/55] Changed deploy log file to not verified --- deployments/_deployments_log_file.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deployments/_deployments_log_file.json b/deployments/_deployments_log_file.json index 01aa336de..0c74257c3 100644 --- a/deployments/_deployments_log_file.json +++ b/deployments/_deployments_log_file.json @@ -29775,7 +29775,7 @@ "TIMESTAMP": "2025-02-26 15:22:09", "CONSTRUCTOR_ARGS": "0x000000000000000000000000d9e7f6f7dc7517678127d84dbf0f0b4477de14e0", "SALT": "", - "VERIFIED": "true" + "VERIFIED": "false" } ] } From 79c22c130ba533110cabb6f2604e80869db43573 Mon Sep 17 00:00:00 2001 From: Michal Mironczuk Date: Fri, 28 Feb 2025 10:32:19 +0100 Subject: [PATCH 50/55] Added test test_WillStoreConstructorParametersCorrectly --- test/solidity/Facets/GlacisFacet.t.sol | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/test/solidity/Facets/GlacisFacet.t.sol b/test/solidity/Facets/GlacisFacet.t.sol index bb5b4bc43..bac44ef03 100644 --- a/test/solidity/Facets/GlacisFacet.t.sol +++ b/test/solidity/Facets/GlacisFacet.t.sol @@ -127,6 +127,12 @@ abstract contract GlacisFacetTestBase is TestBaseFacet { }); } + function test_WillStoreConstructorParametersCorrectly() public { + glacisFacet = new TestGlacisFacet(airliftContract); + + assertEq(address(glacisFacet.airlift()), address(airliftContract)); + } + function testRevert_WhenConstructedWithZeroAddress() public { vm.expectRevert(InvalidConfig.selector); new TestGlacisFacet(IGlacisAirlift(address(0))); From 48d19ce6c0e54e81444f911842deb53545aad431 Mon Sep 17 00:00:00 2001 From: Michal Mironczuk Date: Mon, 3 Mar 2025 18:33:24 +0100 Subject: [PATCH 51/55] Added conventions.md --- conventions.md | 210 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 210 insertions(+) create mode 100644 conventions.md diff --git a/conventions.md b/conventions.md new file mode 100644 index 000000000..2e6a8c6f9 --- /dev/null +++ b/conventions.md @@ -0,0 +1,210 @@ +# Repository overview + +- **Project name:** LiFi +- **Purpose:** LiFi is a cross-chain bridge aggregation protocol that ensures secure and efficient interoperability through robust smart contracts and automated processes. +- **Core components:** + - **Smart contracts:** Primarily built using the Diamond Standard (EIP-2535) with modular facets for core functionality, along with supporting periphery contracts for extended features and integrations. + - **Automation scripts:** Deployment, migration, and operational tasks. + - **Tests:** Tests ensuring contract reliability and safety. + - **Documentation:** Detailed guides, API specifications, and deployment instructions. + +# Codebase structure + + /lifi + ├── src/ # Solidity smart contracts (Diamond facets + periphery) - `.sol` files + ├── tasks/ # Utility scripts or tasks + ├── scripts/ # Deployment, migration, and automation scripts + ├── tests/ # Unit and integration tests - `.t.sol` files + ├── docs/ # Project documentation, API specs, and guides + ├── .github/ # Github workflows + ├── config/ # Facets' configuration files + ├── deployments/ # Deployment logs and addresses + ├── audit/ # Audit reports and log + ├── README.md # High-level project overview and setup instructions + ├── conventions.md # Repository conventions and guidelines (this file) + +Follow the folder structure to locate resources and generate or modify code in accordance with these standards. + +# Smart contract conventions + +## Solidity standards and patterns + +- **Solidity version:** + All Solidity files must start with: + + pragma solidity ^0.8.17; + +- **Design patterns:** + - Use established patterns (e.g., Ownable for access control, Diamond Standard for facets). + - Favor modular design to enhance reusability and security. +- **Security best practices:** + - Validate inputs using `require` statements. + - Validate constructor inputs rigorously: if an invalid value (e.g., `address(0)` or zero value) is provided, revert with a custom error such as `InvalidConfig`. Ensure tests cover these conditions. + - Utilize reentrancy guards (e.g., OpenZeppelin’s `ReentrancyGuard` or the checks-effects-interactions pattern). + - Optimize for gas efficiency with proper data structures and minimal state changes. + +## Facet contract checklist + +- Facets must always include the following three functions: + 1. `_startBridge` – an internal function. + 2. `swapAndStartBridgeTokensVia{FacetName}`. + 3. `startBridgeTokensVia{FacetName}`. +- **Sender handling:** + Confirm whether `msg.sender` is justified. Often, pass the “sender/depositor” as a parameter so refunds return directly to the user. +- **Parameter adjustments:** + After a swap, verify if facet-specific parameters (e.g., expected `outputAmount`) require adjustment based on the actual swap outcome. +- **Parameter ordering:** + For facets with a `receiverAddress` parameter, it should be the first parameter in the `facetData` struct and must match the `bridgeData.receiver`. +- **Cross-verification:** + If `facetData` contains a `targetChainId`, verify it against `bridgeData.destinationChain`. +- **Modifiers and events:** + Ensure usage of default modifiers (e.g., `nonReentrant`, `refundExcessNative`) and that the `LiFiTransferStarted` event is emitted. +- **Fee handling and non-evm support:** + For native fees, use the `_depositAndSwap` variant that reserves the fee. For non-evm chains (e.g., Bitcoin), ensure the `receiverAddress` is declared as `bytes` (not `bytes32`). + +## Solidity tests conventions (.t.sol files) + +- **File naming and structure** + - Test files typically have a `.t.sol` extension (e.g., `AcrossFacetV3.t.sol`, `DeBridgeDlnFacet.t.sol`, `DexManagerFacet.t.sol`). + - Each file should begin with the SPDX license identifier and the Solidity version: + + // SPDX-License-Identifier: Unlicense + pragma solidity ^0.8.17; + + - Group and order imports with system libraries first and project files next. + +- **Test function naming** + - Prefix test functions expected to pass with `test_`. + - Prefix test functions expected to revert with `testRevert_` (using `vm.expectRevert` to check for specific error selectors). + - Use clear and descriptive names that capture the test’s purpose. For example: + 1. `test_CanSwapAndBridgeTokensWithOutputAmountPercent` + 2. `testRevert_FailsIfCalledWithOutdatedQuote` + 3. `test_SucceedsIfOwnerAddsDex` + 4. `testRevert_FailsIfNonOwnerTriesToAddDex` + - For base or inherited tests, prefix with `testBase_`. + +- **Test structure and setup** + - Every test contract must include a `setUp()` function to initialize the test environment. + - The `setUp()` function typically configures custom block numbers, initializes base contracts, sets up facets, and assigns labels (using `vm.label`) for clarity. + - Common initialization steps include calling `initTestBase()` and setting facet addresses in the test base. + - Use `vm.startPrank(address)` and `vm.stopPrank()` to simulate transactions from different users. + +- **Assertions and event testing** + - Use `assertEq()` for checking equality of values (e.g., balances, contract addresses, return values). + - Use custom assertion modifiers such as `assertBalanceChange()` to verify balance changes before and after transactions. + - Before executing a function call that should emit an event, use `vm.expectEmit()` with appropriate flags (to check indexed parameters) and the expected event signature. + - Verify that the expected events (e.g., `AssetSwapped` or `LiFiTransferStarted`) are emitted with the intended parameters. + +- **Reversion and error handling** + - Negative test cases should use `vm.expectRevert()` with the exact error selector (e.g., `InvalidQuoteTimestamp.selector`, `UnAuthorized.selector`). + - The test name must reflect that the function is expected to fail (using the `testRevert_` prefix). + - For example: + - `testRevert_FailsIfCalledWithOutdatedQuote` verifies that a call reverts when the quote timestamp is outdated. + - `testRevert_FailsIfNonOwnerTriesToAddDex` ensures that unauthorized addresses cannot perform owner-only actions. + +- **Overall test best practices** + - Include comments where necessary to explain the purpose of tests or to clarify non-obvious behavior. + - Maintain a consistent order in function calls and assertions. + - Structure tests to first set up the state, then execute the function under test, and finally assert the expected outcomes. + +## Solidity linter (solhint) configuration and rules + +- **Solidity set rules** + To maintain secure, consistent, and efficient Solidity code, we enforce the following Solhint configuration. + - **Gas consumption and custom errors** + `gas-custom-errors`: Enforce the use of custom errors to save gas during error handling. + - **Security rules** + - `avoid-sha3`: Use `keccak256` instead of the deprecated `sha3`. + - `avoid-suicide`: Disallow the use of `selfdestruct` (formerly suicide). + - `avoid-throw`: Enforce the use of `revert` or `require` instead of `throw`. + - `avoid-tx-origin`: Prohibit reliance on `tx.origin` for authorization. + - `check-send-result`: Require checking the return value of `send` to ensure transfers succeed. + - `compiler-version`: Ensure contracts are compiled with a version matching `^0.8.17`. + - `func-visibility`: Enforce explicit function visibility (excluding constructors). + - `multiple-sends`: Discourage making multiple `send` calls within the same function. + - `no-complex-fallback`: Prevent overly complex logic in fallback functions. + - `no-inline-assembly`: Avoid using inline assembly to maintain code safety (this rule may be turned off if inline assembly is needed). + - `not-rely-on-block-hash`: Do not use `blockhash` for security-critical operations. + - `not-rely-on-time`: Avoid relying on block timestamps for critical logic. + - `reentrancy`: Enforce reentrancy protections on functions that make external calls. + - `state-visibility`: Require explicit visibility for state variables. + - **Naming and ordering rules** + - `use-forbidden-name`: Disallow reserved or ambiguous names. + - `var-name-mixedcase`: Enforce mixedCase naming for variables. + - `imports-on-top`: All import statements must be placed at the top of the file. + - `visibility-modifier-order`: Enforce the proper order of visibility modifiers. + - `immutable-vars-naming`: Immutable variables should be named in uppercase (like constants). + - `func-name-mixedcase`: Function names must follow mixedCase notation. + - `event-name-capwords`: Event names must use CapWords style. + - `contract-name-capwords`: Contract names must be in CapWords style. + - `const-name-snakecase`: Constant names must use snake_case. + - `interface-starts-with-i`: Interfaces must start with the letter “I”. + - `quotes`: Enforce the use of double quotes for string literals. + +# Github workflows conventions + +- **Sensitive data handling:** + - Always use Github Secrets for all sensitive data (such as API keys, private keys, and RPC URLs). + - Reference secrets using the syntax: `${{ secrets.SECRET_NAME }}`. +- **File header and purpose:** + - Every workflow file must begin with a clear, concise description (using YAML comments) outlining its purpose. + - Include descriptive comments throughout the file to explain the logic, conditions, and steps involved. +- **Trigger configuration and job structure:** + - Define triggers explicitly (e.g., `workflow_dispatch`, `push`, `schedule`) with clear input descriptions when required. + - Use conditional checks (via `if:`) to control the flow. For example, verify that a required input (such as typing “UNDERSTOOD” for emergency actions) is provided before proceeding. + - Clearly name jobs and steps to reflect the action being performed (e.g., “Authenticate Git User”, “Pause Diamond”, “Send Slack Notification”). +- **Notifications and alerts:** + - Include steps to send notifications (e.g., to Slack) when critical operations occur. + +# Bash scripts + +- **General script structure:** + - Begin each Bash script with the proper shebang: + + #!/bin/bash + +- **Organize code into modular functions:** + - Use descriptive function names and clear sections (e.g., “Logging”, “Error handling and logging”, “Deployment functions”) to structure your script. +- **DRY principle:** + - Extract common logic into helper files (e.g., `script/helperFunctions.sh`). +- **Environment and configuration:** + - Always load environment variables from a `.env` or `config.sh` file and configuration parameters from a dedicated config file. + - Remember to update `.env.example` or `config.example.sh` accordingly. + - Declare global variables (like `CONTRACT_DIRECTORY`, `DEPLOY_SCRIPT_DIRECTORY`, etc.) in the `.env` or `config.sh` files so they are consistently available across all functions. +- **Error handling and logging:** + - Use dedicated helper functions for logging (e.g., `echoDebug`, `error`, `warning`, `success`) to provide consistent, color-coded feedback. + - Validate external inputs and environment variables early in the script, and exit with a clear error message if required variables are missing. + - **Child function failure checking:** When calling any underlying child function, immediately check its exit status using the `checkFailure` helper function. This ensures that if a function fails, the script halts further execution, preventing cascading errors. + - If installing a new system package, add it to `preinstall.sh` so that developers have a unified initial installation procedure. +- **User interaction and prompts:** + - When user input is needed (e.g., selecting a network or confirming an action), use clear prompts with descriptive instructions. Tools like `gum choose` can enhance usability. + - Document any TODOs and known limitations clearly at the beginning of the script for future improvements. +- **Code readability and maintenance:** + - Use consistent indentation, naming conventions, and comments throughout the script. + - Provide usage instructions at the top of each script if applicable. + - Separate core operations (e.g., deploying contracts, updating logs) into distinct, well-documented functions to facilitate easier maintenance and testing. + +# Audit logs and reports conventions + +- **Audit log file (`auditLog.json`):** + Contains two main sections: + 1. **audits:** Each audit entry has a unique ID (e.g., `auditYYYYMMDD`), an auditCompletedOn date, auditedBy, auditorGitHandle (if applicable), auditReportPath (PDF location in `audit/reports/`), and an auditCommitHash. + 2. **auditedContracts:** Maps contract names and versions to the relevant audit IDs in the audits section. +- **Storing reports:** + Place PDF reports in the `audit/reports/` directory, using a naming format that includes the date and contract info (e.g., `2025.01.17_LiFiDexAggregator(v1.3.0).pdf`). +- **Adding new audits:** + For new or updated contracts, add an entry under audits with the correct date, auditReportPath, and auditCommitHash. Then, reference that new ID in auditedContracts for each relevant version. +- **Naming and versioning:** + Use a format like `auditYYYYMMDD` for unique IDs and ensure that contract versioning is consistent between the code and the `auditLog.json`. +- **Essential fields:** + - `auditCompletedOn`: Date in `DD.MM.YYYY` or `YYYY-MM-DD` format. + - `auditedBy`: Name or firm. + - `auditorGitHandle`: (if applicable). + - `auditReportPath`: PDF location in `audit/reports/`. + - `auditCommitHash`: The commit hash that was audited (or “n/a” if not tracked). + +# Documentation and references + +- **Primary documentation:** + - `README.md`: Contains an overview and setup instructions. + - `/docs`: Contains detailed technical documentation, API specifications, and deployment guides. \ No newline at end of file From 4d10014bd6107b0c23ba8d0541fed14e23b0595f Mon Sep 17 00:00:00 2001 From: Daniel <77058885+0xDEnYO@users.noreply.github.com> Date: Tue, 4 Mar 2025 10:39:46 +0000 Subject: [PATCH 52/55] improve SAFE ownership and threshold change script (#1015) * confirm ownership and threshold changes automatically * small fixes * revert changes in confirm safe tx script * coderabbit improvements * minor fixes * bugfix * minor changes --- package.json | 2 +- script/deploy/safe/add-owners-to-safe.ts | 116 ------------ .../safe/add-safe-owners-and-threshold.ts | 176 ++++++++++++++++++ script/utils/viemScriptHelpers.ts | 9 +- 4 files changed, 184 insertions(+), 119 deletions(-) delete mode 100644 script/deploy/safe/add-owners-to-safe.ts create mode 100644 script/deploy/safe/add-safe-owners-and-threshold.ts diff --git a/package.json b/package.json index 6f7e68bdc..ef8418821 100644 --- a/package.json +++ b/package.json @@ -74,7 +74,7 @@ "healthcheck": "tsx ./script/deploy/healthCheck.ts", "propose-safe-tx": "tsx ./script/deploy/safe/propose-to-safe.ts", "confirm-safe-tx": "tsx ./script/deploy/safe/confirm-safe-tx.ts", - "add-safe-owners": "tsx ./script/deploy/safe/add-owners-to-safe.ts", + "add-safe-owners-and-threshold": "tsx ./script/deploy/safe/add-safe-owners-and-threshold.ts", "flatten": "forge flatten --output" }, "dependencies": { diff --git a/script/deploy/safe/add-owners-to-safe.ts b/script/deploy/safe/add-owners-to-safe.ts deleted file mode 100644 index ea30f2c29..000000000 --- a/script/deploy/safe/add-owners-to-safe.ts +++ /dev/null @@ -1,116 +0,0 @@ -import { defineCommand, runMain } from 'citty' -import { type SafeApiKitConfig } from '@safe-global/api-kit' -import { getAddress } from 'viem' -import { EthersAdapter } from '@safe-global/protocol-kit' -const { default: SafeApiKit } = await import('@safe-global/api-kit') -const { default: Safe } = await import('@safe-global/protocol-kit') -import { ethers } from 'ethers6' -import { getSafeUtilityContracts } from './config' -import { - NetworksObject, - getViemChainForNetworkName, -} from '../../utils/viemScriptHelpers' -import data from '../../../config/networks.json' -const networks: NetworksObject = data as NetworksObject - -const main = defineCommand({ - meta: { - name: 'propose-to-safe', - description: 'Propose a transaction to a Gnosis Safe', - }, - args: { - network: { - type: 'string', - description: 'Network name', - required: true, - }, - rpcUrl: { - type: 'string', - description: 'RPC URL', - }, - privateKey: { - type: 'string', - description: 'Private key of the signer', - required: true, - }, - owners: { - type: 'string', - description: 'List of new owners to add to the safe separated by commas', - required: true, - }, - }, - async run({ args }) { - const { network, privateKey } = args - - const chain = getViemChainForNetworkName(network) - - const config: SafeApiKitConfig = { - chainId: BigInt(chain.id), - txServiceUrl: networks[network].safeApiUrl, - } - - const safeService = new SafeApiKit(config) - - const safeAddress = getAddress(networks[network].safeAddress) - - const rpcUrl = args.rpcUrl || chain.rpcUrls.default.http[0] - const provider = new ethers.JsonRpcProvider(rpcUrl) - const signer = new ethers.Wallet(args.privateKey, provider) - - const ethAdapter = new EthersAdapter({ - ethers, - signerOrProvider: signer, - }) - - const protocolKit = await Safe.create({ - ethAdapter, - safeAddress: safeAddress, - contractNetworks: getSafeUtilityContracts(chain.id), - }) - - const owners = String(args.owners).split(',') - - let nextNonce = await safeService.getNextNonce(safeAddress) - const info = safeService.getSafeInfo(safeAddress) - for (const o of owners) { - const owner = getAddress(o) - const existingOwners = await protocolKit.getOwners() - if (existingOwners.includes(owner)) { - console.info('Owner already exists', owner) - continue - } - - const safeTransaction = await protocolKit.createAddOwnerTx( - { - ownerAddress: owner, - threshold: (await info).threshold, - }, - { - nonce: nextNonce, - } - ) - - const senderAddress = await signer.getAddress() - const safeTxHash = await protocolKit.getTransactionHash(safeTransaction) - const signature = await protocolKit.signHash(safeTxHash) - - console.info('Adding owner', owner) - console.info('Signer Address', senderAddress) - console.info('Safe Address', safeAddress) - - // Propose transaction to the service - await safeService.proposeTransaction({ - safeAddress: await protocolKit.getAddress(), - safeTransactionData: safeTransaction.data, - safeTxHash, - senderAddress, - senderSignature: signature.data, - }) - - console.info('Transaction proposed') - nextNonce++ - } - }, -}) - -runMain(main) diff --git a/script/deploy/safe/add-safe-owners-and-threshold.ts b/script/deploy/safe/add-safe-owners-and-threshold.ts new file mode 100644 index 000000000..0b074da80 --- /dev/null +++ b/script/deploy/safe/add-safe-owners-and-threshold.ts @@ -0,0 +1,176 @@ +import { defineCommand, runMain } from 'citty' +import { type SafeApiKitConfig } from '@safe-global/api-kit' +import { getAddress } from 'viem' +import { EthersAdapter } from '@safe-global/protocol-kit' +const { default: SafeApiKit } = await import('@safe-global/api-kit') +const { default: Safe } = await import('@safe-global/protocol-kit') +import { ethers } from 'ethers6' +import { getSafeUtilityContracts } from './config' +import { + NetworksObject, + getViemChainForNetworkName, +} from '../../utils/viemScriptHelpers' +import data from '../../../config/networks.json' +import globalConfig from '../../../config/global.json' +import * as dotenv from 'dotenv' +import { SafeTransaction } from '@safe-global/safe-core-sdk-types' +dotenv.config() + +const networks: NetworksObject = data as NetworksObject + +const main = defineCommand({ + meta: { + name: 'add-safe-owners-and-threshold', + description: + 'Adds all SAFE owners from global.json to the SAFE address in networks.json and sets threshold to 3', + }, + args: { + network: { + type: 'string', + description: 'Network name', + required: true, + }, + privateKey: { + type: 'string', + description: 'Private key of the signer', + }, + }, + async run({ args }) { + const { network, privateKey: privateKeyArg } = args + + const chain = getViemChainForNetworkName(network) + + const config: SafeApiKitConfig = { + chainId: BigInt(chain.id), + txServiceUrl: networks[network].safeApiUrl, + } + + const privateKey = String( + privateKeyArg || process.env.PRIVATE_KEY_PRODUCTION + ) + + if (!privateKey) + throw new Error( + 'Private key is missing, either provide it as argument or add PRIVATE_KEY_PRODUCTION to your .env' + ) + + console.info('Setting up connection to SAFE API') + + const safeService = new SafeApiKit(config) + + const safeAddress = getAddress(networks[network].safeAddress) + + const rpcUrl = chain.rpcUrls.default.http[0] || args.rpcUrl + + const provider = new ethers.JsonRpcProvider(rpcUrl) + const signer = new ethers.Wallet(privateKey, provider) + + const ethAdapter = new EthersAdapter({ + ethers, + signerOrProvider: signer, + }) + + const protocolKit = await Safe.create({ + ethAdapter, + safeAddress: safeAddress, + contractNetworks: getSafeUtilityContracts(chain.id), + }) + + const owners = globalConfig.safeOwners + + let nextNonce = await safeService.getNextNonce(safeAddress) + const currentThreshold = (await safeService.getSafeInfo(safeAddress)) + ?.threshold + if (!currentThreshold) + throw new Error('Could not get current signature threshold') + + console.info('Safe Address', safeAddress) + const senderAddress = await signer.getAddress() + console.info('Signer Address', senderAddress) + + // go through all owner addresses and add each of them individually + for (const o of owners) { + console.info('-'.repeat(80)) + const owner = getAddress(o) + const existingOwners = await protocolKit.getOwners() + if (existingOwners.includes(owner)) { + console.info('Owner already exists', owner) + continue + } + + const safeTransaction = await protocolKit.createAddOwnerTx( + { + ownerAddress: owner, + threshold: currentThreshold, + }, + { + nonce: nextNonce, + } + ) + + console.info('Adding owner', owner) + + await submitAndExecuteTransaction( + protocolKit, + safeService, + safeTransaction, + senderAddress + ) + nextNonce++ + } + + console.info('-'.repeat(80)) + + if (currentThreshold != 3) { + console.info('Now changing threshold from 1 to 3') + const changeThresholdTx = await protocolKit.createChangeThresholdTx(3) + await submitAndExecuteTransaction( + protocolKit, + safeService, + changeThresholdTx, + senderAddress + ) + } else console.log('Threshold is already set to 3 - no action required') + + console.info('-'.repeat(80)) + console.info('Script completed without errors') + }, +}) + +async function submitAndExecuteTransaction( + protocolKit: any, + safeService: any, + safeTransaction: SafeTransaction, + senderAddress: string +): Promise { + const safeTxHash = await protocolKit.getTransactionHash(safeTransaction) + const signature = await protocolKit.signHash(safeTxHash) + + // Propose the transaction + await safeService.proposeTransaction({ + safeAddress: await protocolKit.getAddress(), + safeTransactionData: safeTransaction.data, + safeTxHash, + senderAddress, + senderSignature: signature.data, + }) + + console.info('Transaction proposed:', safeTxHash) + + // Execute the transaction immediately + try { + const execResult = await protocolKit.executeTransaction(safeTransaction) + const receipt = await execResult.transactionResponse?.wait() + if (receipt?.status === 0) { + throw new Error('Transaction failed') + } + console.info('Transaction executed:', safeTxHash) + } catch (error) { + console.error('Transaction execution failed:', error) + throw error + } + + return safeTxHash +} + +runMain(main) diff --git a/script/utils/viemScriptHelpers.ts b/script/utils/viemScriptHelpers.ts index 0a8def849..a882a2959 100644 --- a/script/utils/viemScriptHelpers.ts +++ b/script/utils/viemScriptHelpers.ts @@ -1,7 +1,7 @@ import { Chain, defineChain, getAddress } from 'viem' import networksConfig from '../../config/networks.json' -import { config } from 'dotenv' -config() // Load environment variables from .env +import * as dotenv from 'dotenv' +dotenv.config() export type NetworksObject = { [key: string]: Omit @@ -47,6 +47,11 @@ export const getViemChainForNetworkName = (networkName: string): Chain => { const envKey = `ETH_NODE_URI_${networkName.toUpperCase()}` const rpcUrl = process.env[envKey] || network.rpcUrl // Use .env value if available, otherwise fallback + if (!rpcUrl) + throw new Error( + `Could not find RPC URL for network ${networkName}, please add one with the key ${envKey} to your .env file` + ) + const chain = defineChain({ id: network.chainId, name: network.name, From b45e47919b0c2e83351053c593a3d14ead0f4452 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Miro=C5=84czuk?= Date: Wed, 5 Mar 2025 12:04:12 +0100 Subject: [PATCH 53/55] LF-12061 Run healthcheck on new network (#985) * Added healthCheck for new network deployment git action * Updated healthCheckForNewNetworkDeployment github action and healthCheck script * Changed var names * test * changed networks * health check fo new network deployment action adjustments * temp comment * test * test * c * d * fix * Merge branch 'main' into lf-12061-run-healthcheck-on-new-network * removed paths * test * Revert back networks.json * Added action description, eror when no networks.json file found, added confirmations * test * up * test --- .../healthCheckForNewNetworkDeployment.yml | 112 ++++++++++++++++++ script/deploy/healthCheck.ts | 72 ++++++----- 2 files changed, 153 insertions(+), 31 deletions(-) create mode 100644 .github/workflows/healthCheckForNewNetworkDeployment.yml diff --git a/.github/workflows/healthCheckForNewNetworkDeployment.yml b/.github/workflows/healthCheckForNewNetworkDeployment.yml new file mode 100644 index 000000000..7f96b2ba7 --- /dev/null +++ b/.github/workflows/healthCheckForNewNetworkDeployment.yml @@ -0,0 +1,112 @@ +name: Health Check for New Network Deployment + +# - designed to perform health checks for newly added networks +# - triggers on pull requests and first checks if the config/networks.json file was modified +# - validates that each new network has corresponding deployment and state configuration files +# - runs network-specific health checks +# - any required file is missing or a health check fails, the action exits with an error + +on: + pull_request: + types: [opened, synchronize, reopened, ready_for_review] + +jobs: + check-new-network-health: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Check if config/networks.json was changed in this branch + id: check-file-change + run: | + if git diff --name-only origin/main...HEAD | grep -q "config/networks.json"; then + echo "config/networks.json has been modified in this branch" + echo "CONTINUE=true" >> $GITHUB_ENV + else + echo "No changes in config/networks.json detected in this branch" + echo "CONTINUE=false" >> $GITHUB_ENV + fi + + - name: Detect Newly Added Networks + if: env.CONTINUE == 'true' + id: detect-changes + run: | + echo "Comparing config/networks.json with the previous commit..." + git fetch origin main --depth=1 || echo "No previous commit found." + + if git show origin/main:config/networks.json > /dev/null 2>&1; then + OLD_NETWORKS=$(git show origin/main:config/networks.json | jq 'keys') + else + echo "❌ Error: No previous networks.json found. Expected existing network configuration." + exit 1 + fi + + NEW_NETWORKS=$(jq 'keys' config/networks.json) + + ADDED_NETWORKS=$(jq -n --argjson old "$OLD_NETWORKS" --argjson new "$NEW_NETWORKS" '$new - $old') + + echo "Added networks: $ADDED_NETWORKS" + + if [[ "$ADDED_NETWORKS" == "[]" ]]; then + echo "No new networks detected." + echo "SKIP_CHECK=true" >> $GITHUB_ENV + else + echo "New networks detected: $ADDED_NETWORKS" + echo "added_networks=$(echo $ADDED_NETWORKS | jq -c .)" >> $GITHUB_ENV + fi + + - name: Validate Network Deployment Files + if: env.CONTINUE == 'true' && env.SKIP_CHECK != 'true' + run: | + echo "Validating required files for new networks..." + for network in $(echo $added_networks | jq -r '.[]'); do + echo "🔍 Checking files for network: $network" + + # Check if network exists in _targetState.json + if ! jq -e 'has("'"$network"'")' script/deploy/_targetState.json > /dev/null; then + echo "❌ Error: Network '$network' not found in script/deploy/_targetState.json" + exit 1 + else + echo "✅ Confirmed: Network '$network' exists in script/deploy/_targetState.json" + fi + + # Check if deployments/{network}.json file exists + if [[ ! -f "deployments/$network.json" ]]; then + echo "❌ Error: Missing deployment file: deployments/$network.json" + exit 1 + else + echo "✅ Confirmed: Deployment file: deployments/$network.json exists" + fi + done + + - name: Install Bun + if: env.CONTINUE == 'true' && env.SKIP_CHECK != 'true' + uses: oven-sh/setup-bun@v1 + with: + bun-version: latest + + - name: Install Foundry (provides cast) + if: env.CONTINUE == 'true' && env.SKIP_CHECK != 'true' + uses: foundry-rs/foundry-toolchain@v1 + + - name: Install dependencies + if: env.CONTINUE == 'true' && env.SKIP_CHECK != 'true' + run: bun install + + - name: Run Health Checks on New Networks + if: env.CONTINUE == 'true' && env.SKIP_CHECK != 'true' + run: | + echo "Running health check for new networks..." + set -e + for network in $(echo $added_networks | jq -r '.[]'); do + echo "🔍 Checking network: $network" + if bun run script/deploy/healthCheck.ts --network "$network"; then + echo "✅ $network is fine." + else + echo "❌ Health check failed for $network. Exiting..." + exit 1 + fi + done diff --git a/script/deploy/healthCheck.ts b/script/deploy/healthCheck.ts index 6fa7f7540..389003988 100644 --- a/script/deploy/healthCheck.ts +++ b/script/deploy/healthCheck.ts @@ -1,6 +1,6 @@ // @ts-nocheck import { consola } from 'consola' -import { $, spinner } from 'zx' +import { $ } from 'zx' import { defineCommand, runMain } from 'citty' import * as path from 'path' import * as fs from 'fs' @@ -24,8 +24,6 @@ import { coreFacets, pauserWallet } from '../../config/global.json' const SAFE_THRESHOLD = 3 -const louperCmd = 'louper-cli' - const corePeriphery = [ 'ERC20Proxy', 'Executor', @@ -48,26 +46,8 @@ const main = defineCommand({ }, }, async run({ args }) { - if ((await $`${louperCmd}`.exitCode) !== 0) { - const answer = await consola.prompt( - 'Louper CLI is required but not installed. Would you like to install it now?', - { - type: 'confirm', - } - ) - if (answer) { - await spinner( - 'Installing...', - () => $`npm install -g @mark3labs/louper-cli` - ) - } else { - consola.error('Louper CLI is required to run this script') - process.exit(1) - } - } - const { network } = args - const deployedContracts = await import( + const { default: deployedContracts } = await import( `../../deployments/${network.toLowerCase()}.json` ) const targetStateJson = await import( @@ -161,16 +141,43 @@ const main = defineCommand({ let registeredFacets: string[] = [] try { - const facetsResult = - await $`${louperCmd} inspect diamond -a ${diamondAddress} -n ${network} --json` - registeredFacets = JSON.parse(facetsResult.stdout).facets.map( - (f: { name: string }) => f.name - ) + if (networksConfig[network.toLowerCase()].rpcUrl) { + const rpcUrl: string = networksConfig[network.toLowerCase()].rpcUrl + const facetsResult = + await $`cast call ${diamondAddress} "facets() returns ((address,bytes4[])[])" --rpc-url ${rpcUrl}` + const rawString = facetsResult.stdout + + const jsonCompatibleString = rawString + .replace(/\(/g, '[') + .replace(/\)/g, ']') + .replace(/0x[0-9a-fA-F]+/g, '"$&"') + + const onChainFacets = JSON.parse(jsonCompatibleString) + + if (Array.isArray(onChainFacets)) { + // mapping on-chain facet addresses to names in config + const configFacetsByAddress = Object.fromEntries( + Object.entries(deployedContracts).map(([name, address]) => { + return [address.toLowerCase(), name] + }) + ) + + const onChainFacetAddresses = onChainFacets.map(([address]) => + address.toLowerCase() + ) + + const configuredFacetAddresses = Object.keys(configFacetsByAddress) + + registeredFacets = onChainFacets.map(([address]) => { + return configFacetsByAddress[address.toLowerCase()] + }) + } + } else { + throw new Error('Failed to get rpc from network config file') + } } catch (error) { - consola.warn( - 'Unable to parse louper output - skipping facet registration check' - ) - consola.debug('Error:', error) + consola.warn('Unable to parse output - skipping facet registration check') + consola.warn('Error:', error) } for (const facet of [...coreFacets, ...nonCoreFacets]) { @@ -531,6 +538,7 @@ const main = defineCommand({ finish() } else { logError('No dexs configured') + finish() } }, }) @@ -592,8 +600,10 @@ const checkIsDeployed = async ( const finish = () => { if (errors.length) { consola.error(`${errors.length} Errors found in deployment`) + process.exit(1) } else { consola.success('Deployment checks passed') + process.exit(0) } } From b70d8a70d4d47f2eb2439b5b6abc016dc5ad4d31 Mon Sep 17 00:00:00 2001 From: Michal Mironczuk Date: Wed, 5 Mar 2025 14:05:33 +0100 Subject: [PATCH 54/55] Changed solhint no-empty-blocks rule --- .solhint.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.solhint.json b/.solhint.json index 72fbaf6c0..7e74ad261 100644 --- a/.solhint.json +++ b/.solhint.json @@ -11,7 +11,7 @@ "code-complexity": ["error", 20], "explicit-types": ["error", "explicit"], "max-states-count": ["error", 15], - "no-empty-blocks": "error", + "no-empty-blocks": "off", "no-global-import": "error", "no-unused-import": "error", "no-unused-vars": "error", From ab560b2fba7088d784c41915dd5ee4f2e83b9a62 Mon Sep 17 00:00:00 2001 From: Michal Mironczuk Date: Wed, 5 Mar 2025 14:06:06 +0100 Subject: [PATCH 55/55] Fixed GlacisFacet.t.sol to apply solhint rules --- test/solidity/Facets/GlacisFacet.t.sol | 30 +++++++++++++------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/test/solidity/Facets/GlacisFacet.t.sol b/test/solidity/Facets/GlacisFacet.t.sol index bac44ef03..f7a6d556a 100644 --- a/test/solidity/Facets/GlacisFacet.t.sol +++ b/test/solidity/Facets/GlacisFacet.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: Unlicense -pragma solidity 0.8.17; +pragma solidity ^0.8.17; import { LibAllowList, TestBaseFacet, ERC20 } from "../utils/TestBaseFacet.sol"; import { LibSwap } from "lifi/Libraries/LibSwap.sol"; @@ -27,7 +27,7 @@ abstract contract GlacisFacetTestBase is TestBaseFacet { ERC20 internal srcToken; uint256 internal defaultSrcTokenAmount; uint256 internal destinationChainId; - address internal ADDRESS_SRC_TOKEN; + address internal addressSrcToken; uint256 internal fuzzingAmountMinValue; uint256 internal fuzzingAmountMaxValue; @@ -36,12 +36,12 @@ abstract contract GlacisFacetTestBase is TestBaseFacet { function setUp() public virtual { initTestBase(); - srcToken = ERC20(ADDRESS_SRC_TOKEN); + srcToken = ERC20(addressSrcToken); defaultSrcTokenAmount = 1_000 * 10 ** srcToken.decimals(); deal( - ADDRESS_SRC_TOKEN, + addressSrcToken, USER_SENDER, 500_000 * 10 ** srcToken.decimals() ); @@ -74,7 +74,7 @@ abstract contract GlacisFacetTestBase is TestBaseFacet { // adjust bridgeData bridgeData.bridge = "glacis"; - bridgeData.sendingAssetId = ADDRESS_SRC_TOKEN; + bridgeData.sendingAssetId = addressSrcToken; bridgeData.minAmount = defaultSrcTokenAmount; bridgeData.destinationChainId = destinationChainId; @@ -84,7 +84,7 @@ abstract contract GlacisFacetTestBase is TestBaseFacet { // liquidity on V2 dexes addLiquidity( ADDRESS_DAI, - ADDRESS_SRC_TOKEN, + addressSrcToken, 100_000 * 10 ** ERC20(ADDRESS_DAI).decimals(), 100_000 * 10 ** srcToken.decimals() ); @@ -160,11 +160,11 @@ abstract contract GlacisFacetTestBase is TestBaseFacet { virtual override assertBalanceChange( - ADDRESS_SRC_TOKEN, + addressSrcToken, USER_SENDER, -int256(defaultSrcTokenAmount) ) - assertBalanceChange(ADDRESS_SRC_TOKEN, USER_RECEIVER, 0) + assertBalanceChange(addressSrcToken, USER_RECEIVER, 0) assertBalanceChange(ADDRESS_DAI, USER_SENDER, 0) assertBalanceChange(ADDRESS_DAI, USER_RECEIVER, 0) { @@ -226,7 +226,7 @@ abstract contract GlacisFacetTestBase is TestBaseFacet { // Swap DAI -> {SOURCE TOKEN} address[] memory path = new address[](2); path[0] = ADDRESS_DAI; - path[1] = ADDRESS_SRC_TOKEN; + path[1] = addressSrcToken; uint256 amountOut = defaultSrcTokenAmount; @@ -238,7 +238,7 @@ abstract contract GlacisFacetTestBase is TestBaseFacet { callTo: address(uniswap), approveTo: address(uniswap), sendingAssetId: ADDRESS_DAI, - receivingAssetId: ADDRESS_SRC_TOKEN, + receivingAssetId: addressSrcToken, fromAmount: amountIn, callData: abi.encodeWithSelector( uniswap.swapExactTokensForTokens.selector, @@ -258,8 +258,8 @@ abstract contract GlacisFacetTestBase is TestBaseFacet { virtual override assertBalanceChange(ADDRESS_DAI, USER_RECEIVER, 0) - assertBalanceChange(ADDRESS_SRC_TOKEN, USER_SENDER, 0) - assertBalanceChange(ADDRESS_SRC_TOKEN, USER_RECEIVER, 0) + assertBalanceChange(addressSrcToken, USER_SENDER, 0) + assertBalanceChange(addressSrcToken, USER_RECEIVER, 0) { uint256 initialDAIBalance = dai.balanceOf(USER_SENDER); @@ -282,7 +282,7 @@ abstract contract GlacisFacetTestBase is TestBaseFacet { bridgeData.transactionId, address(uniswap), ADDRESS_DAI, - ADDRESS_SRC_TOKEN, + addressSrcToken, swapData[0].fromAmount, bridgeData.minAmount, block.timestamp @@ -443,7 +443,7 @@ contract GlacisFacetWormholeTest is GlacisFacetTestBase { airliftContract = IGlacisAirlift( 0xD9E7f6f7Dc7517678127D84dBf0F0b4477De14E0 ); - ADDRESS_SRC_TOKEN = 0xB0fFa8000886e57F86dd5264b9582b2Ad87b2b91; // address of W token on Arbitrum network + addressSrcToken = 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) @@ -459,7 +459,7 @@ contract GlacisFacetLINKTest is GlacisFacetTestBase { airliftContract = IGlacisAirlift( 0x30095227Eb6d72FA6c09DfdeFFC766c33f7FA2DD ); - ADDRESS_SRC_TOKEN = 0x88Fb150BDc53A65fe94Dea0c9BA0a6dAf8C6e196; // address of LINK token on Base network + addressSrcToken = 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)