diff --git a/config/glacis.json b/config/glacis.json new file mode 100644 index 000000000..1736d6d15 --- /dev/null +++ b/config/glacis.json @@ -0,0 +1,12 @@ +{ + "important": "these values are test deployments only. We need to update this file when Glacis has deployed their final versions", + "arbitrum": { + "airlift": "0xD9E7f6f7Dc7517678127D84dBf0F0b4477De14E0" + }, + "optimism": { + "airlift": "0xdEedFc11fCd2bC3E63915e8060ec48875E890BCB" + }, + "base": { + "airlift": "0x30095227Eb6d72FA6c09DfdeFFC766c33f7FA2DD" + } +} diff --git a/deployments/_deployments_log_file.json b/deployments/_deployments_log_file.json index f4364d6d2..67ad271d2 100644 --- a/deployments/_deployments_log_file.json +++ b/deployments/_deployments_log_file.json @@ -28186,5 +28186,21 @@ ] } } + }, + "GlacisFacet": { + "arbitrum": { + "staging": { + "1.0.0": [ + { + "ADDRESS": "0x3aF0c2dB91f75f05493E51cFcF92eC5276bc85F8", + "OPTIMIZER_RUNS": "1000000", + "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 ee64127be..d8b678fb0 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" + }, + "0x3aF0c2dB91f75f05493E51cFcF92eC5276bc85F8": { + "Name": "GlacisFacet", + "Version": "1.0.0" } }, "Periphery": { @@ -153,9 +157,9 @@ "GasZipPeriphery": "", "LiFiDEXAggregator": "", "LiFuelFeeCollector": "0x94EA56D8049e93E0308B9c7d1418Baf6A7C68280", - "Permit2Proxy": "0x6FC01BC9Ff6Cdab694Ec8Ca41B21a2F04C8c37E5", - "ReceiverAcrossV3": "0xe4F3DEF14D61e47c696374453CD64d438FD277F8", + "Permit2Proxy": "0xb33Fe241BEd9bf5F694101D7498F63a0d060F999", "Receiver": "0x36E9B2E8A627474683eF3b1E9Df26D2bF04396f3", + "ReceiverAcrossV3": "0xe4F3DEF14D61e47c696374453CD64d438FD277F8", "ReceiverStargateV2": "", "RelayerCelerIM": "0xa1Ed8783AC96385482092b82eb952153998e9b70", "TokenWrapper": "0xF63b27AE2Dc887b88f82E2Cc597d07fBB2E78E70" diff --git a/deployments/arbitrum.staging.json b/deployments/arbitrum.staging.json index 96b7f9fe6..d6dca2fec 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": "0x3aF0c2dB91f75f05493E51cFcF92eC5276bc85F8" } \ No newline at end of file diff --git a/docs/GlacisFacet.md b/docs/GlacisFacet.md new file mode 100644 index 000000000..f126e29da --- /dev/null +++ b/docs/GlacisFacet.md @@ -0,0 +1,94 @@ +# Glacis Facet + +## 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 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; + 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 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; + uint256 nativeFee; +} +``` + +## 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/demoScripts/demoGlacis.ts b/script/demoScripts/demoGlacis.ts new file mode 100644 index 000000000..d71c60941 --- /dev/null +++ b/script/demoScripts/demoGlacis.ts @@ -0,0 +1,159 @@ +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 airliftArtifact from '../../out/IGlacisAirlift.sol/IGlacisAirlift.json' + +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 +> +const AIRLIFT_ABI = airliftArtifact.abi as Narrow + +// #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.gmpFee.nativeFee as bigint, + tokenFee: estimatedFees.gmpFee.tokenFee as bigint, + }, + airliftFee: { + nativeFee: estimatedFees.airliftFeeInfo.airliftFee.nativeFee as bigint, + tokenFee: estimatedFees.airliftFeeInfo.airliftFee.tokenFee as bigint, + }, + } + const nativeFee = + structuredFees.gmpFee.nativeFee + structuredFees.airliftFee.nativeFee + + console.info(`Estimated native fee: ${nativeFee}`) + + // === 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/deploy/facets/DeployGlacisFacet.s.sol b/script/deploy/facets/DeployGlacisFacet.s.sol new file mode 100644 index 000000000..76be94f98 --- /dev/null +++ b/script/deploy/facets/DeployGlacisFacet.s.sol @@ -0,0 +1,32 @@ +// 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) { + string memory path = string.concat(root, "/config/glacis.json"); + + address airlift = _getConfigContractAddress( + path, + string.concat(".", network, ".airlift") + ); + + return abi.encode(airlift); + } +} diff --git a/script/deploy/facets/UpdateGlacisFacet.s.sol b/script/deploy/facets/UpdateGlacisFacet.s.sol new file mode 100644 index 000000000..63f37bb9c --- /dev/null +++ b/script/deploy/facets/UpdateGlacisFacet.s.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.17; + +import { UpdateScriptBase } from "./utils/UpdateScriptBase.sol"; + +contract DeployScript is UpdateScriptBase { + function run() + public + returns (address[] memory facets, bytes memory cutData) + { + return update("GlacisFacet"); + } +} diff --git a/src/Facets/GlacisFacet.sol b/src/Facets/GlacisFacet.sol new file mode 100644 index 000000000..dbf62d6d6 --- /dev/null +++ b/src/Facets/GlacisFacet.sol @@ -0,0 +1,125 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.17; + +import { ILiFi } from "../Interfaces/ILiFi.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 } from "../Interfaces/IGlacisAirlift.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 /// + + /// @notice The contract address of the glacis airlift on the source chain. + IGlacisAirlift public immutable airlift; + + /// Types /// + + /// @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; + uint256 nativeFee; + } + + /// Constructor /// + /// @notice Initializes the GlacisFacet contract + /// @param _airlift The address of Glacis Airlift contract. + constructor(IGlacisAirlift _airlift) { + airlift = _airlift; + } + + /// Errors /// + + error InvalidRefundAddress(); + + /// 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) + noNativeAsset(_bridgeData) + { + LibAsset.depositAsset( + _bridgeData.sendingAssetId, + _bridgeData.minAmount + ); + _startBridge(_bridgeData, _glacisData); + } + + /// @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) + noNativeAsset(_bridgeData) + { + _bridgeData.minAmount = _depositAndSwap( + _bridgeData.transactionId, + _bridgeData.minAmount, + _swapData, + payable(msg.sender), + _glacisData.nativeFee + ); + _startBridge(_bridgeData, _glacisData); + } + + /// 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 + ) 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. + 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.refundAddress + ); + + emit LiFiTransferStarted(_bridgeData); + } +} diff --git a/src/Interfaces/IGlacisAirlift.sol b/src/Interfaces/IGlacisAirlift.sol new file mode 100644 index 000000000..d648630f2 --- /dev/null +++ b/src/Interfaces/IGlacisAirlift.sol @@ -0,0 +1,55 @@ +// 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; +} + +interface IGlacisAirlift { + /// 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. + /// @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 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. + /// @return The amount of token and value fees required to send the token across chains. + function quoteSend( + address token, + uint256 amount, + bytes32 receiver, + uint256 destinationChainId, + address refundAddress, + uint256 msgValue + ) external returns (QuoteSendInfo memory); +} diff --git a/templates/facetDemoScript.template.hbs b/templates/facetDemoScript.template.hbs index fe0849024..ef605b169 100644 --- a/templates/facetDemoScript.template.hbs +++ b/templates/facetDemoScript.template.hbs @@ -19,16 +19,25 @@ 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() async function main() { // === Set up environment === - const srcChain: SupportedChain = "mainnet"; // Set source chain + 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 { client, publicClient, walletAccount, lifiDiamondAddress, lifiDiamondContract } = await setupEnvironment(srcChain, {{constantCase name}}_FACET_ABI) const signerAddress = walletAccount.address // === Contract addresses === @@ -59,20 +68,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 amount = parseUnits('10', srcTokenDecimals) // 10 * 1e{source token decimals} + const amount = parseUnits('10', Number(srcTokenDecimals)); // 10 * 1e{source token decimals} - console.info(`\Bridge ${amount} ${srcTokenName} (${srcTokenSymbol}) from ${srcChain} --> {DESTINATION CHAIN NAME}`) + 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. === @@ -108,7 +117,7 @@ async function main() { 'Starting bridge tokens via {{titleCase name}}', publicClient, true - ); + ) } main() diff --git a/test/solidity/Facets/GlacisFacet.t.sol b/test/solidity/Facets/GlacisFacet.t.sol new file mode 100644 index 000000000..137088282 --- /dev/null +++ b/test/solidity/Facets/GlacisFacet.t.sol @@ -0,0 +1,457 @@ +// SPDX-License-Identifier: Unlicense +pragma solidity 0.8.17; + +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"; + +// 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); + } +} + +abstract contract GlacisFacetTestBase is TestBaseFacet { + GlacisFacet.GlacisData internal glacisData; + IGlacisAirlift internal airliftContract; + TestGlacisFacet internal glacisFacet; + ERC20 internal srcToken; + uint256 internal defaultSrcTokenAmount; + uint256 internal destinationChainId; + address internal ADDRESS_SRC_TOKEN; + uint256 internal fuzzingAmountMinValue; + uint256 internal fuzzingAmountMaxValue; + + uint256 internal payableAmount = 1 ether; + + function setUp() public virtual { + initTestBase(); + + srcToken = ERC20(ADDRESS_SRC_TOKEN); + + defaultSrcTokenAmount = 1_000 * 10 ** srcToken.decimals(); + + deal( + ADDRESS_SRC_TOKEN, + USER_SENDER, + 500_000 * 10 ** srcToken.decimals() + ); + + glacisFacet = new TestGlacisFacet(airliftContract); + 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); + glacisFacet.setFunctionApprovalBySignature( + uniswap.swapExactTokensForTokens.selector + ); + glacisFacet.setFunctionApprovalBySignature( + uniswap.swapTokensForExactETH.selector + ); + glacisFacet.setFunctionApprovalBySignature( + uniswap.swapETHForExactTokens.selector + ); + _facetTestContractAddress = address(glacisFacet); + vm.label(address(glacisFacet), "GlacisFacet"); + + // adjust bridgeData + bridgeData.bridge = "glacis"; + 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. + + // 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 + // 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( + bridgeData.sendingAssetId, + bridgeData.minAmount, + bytes32(uint256(uint160(bridgeData.receiver))), + bridgeData.destinationChainId, + REFUND_WALLET, + payableAmount + ); + + addToMessageValue = + quoteSendInfo.gmpFee.nativeFee + + quoteSendInfo.airliftFeeInfo.airliftFee.nativeFee; + + // produce valid GlacisData + glacisData = GlacisFacet.GlacisData({ + refundAddress: REFUND_WALLET, + nativeFee: addToMessageValue + }); + } + + function initiateBridgeTxWithFacet(bool) internal virtual override { + glacisFacet.startBridgeTokensViaGlacis{ value: addToMessageValue }( + bridgeData, + glacisData + ); + } + + function initiateSwapAndBridgeTxWithFacet(bool) internal virtual override { + glacisFacet.swapAndStartBridgeTokensViaGlacis{ + value: addToMessageValue + }(bridgeData, swapData, glacisData); + } + + function testBase_CanBridgeNativeTokens() public virtual override { + // facet does not support bridging of native assets + } + + function testBase_CanBridgeTokens() + public + virtual + override + assertBalanceChange( + ADDRESS_SRC_TOKEN, + USER_SENDER, + -int256(defaultSrcTokenAmount) + ) + assertBalanceChange(ADDRESS_SRC_TOKEN, USER_RECEIVER, 0) + assertBalanceChange(ADDRESS_DAI, USER_SENDER, 0) + assertBalanceChange(ADDRESS_DAI, USER_RECEIVER, 0) + { + vm.startPrank(USER_SENDER); + + // approval + srcToken.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_CanBridgeTokens_fuzzed( + uint256 amount + ) public virtual override { + vm.assume( + amount > fuzzingAmountMinValue * 10 ** srcToken.decimals() && + amount < fuzzingAmountMaxValue * 10 ** srcToken.decimals() + ); + bridgeData.minAmount = amount; + + vm.startPrank(USER_SENDER); + + // approval + srcToken.approve(address(glacisFacet), bridgeData.minAmount); + + QuoteSendInfo memory quoteSendInfo = IGlacisAirlift( + address(airliftContract) + ).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 virtual override { + // facet does not support bridging of native assets + } + + function setDefaultSwapDataSingleDAItoSourceToken() internal virtual { + delete swapData; + // Swap DAI -> {SOURCE TOKEN} + address[] memory path = new address[](2); + path[0] = ADDRESS_DAI; + path[1] = ADDRESS_SRC_TOKEN; + + 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_SRC_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_RECEIVER, 0) + assertBalanceChange(ADDRESS_SRC_TOKEN, USER_SENDER, 0) + assertBalanceChange(ADDRESS_SRC_TOKEN, USER_RECEIVER, 0) + { + uint256 initialDAIBalance = dai.balanceOf(USER_SENDER); + + vm.startPrank(USER_SENDER); + + // prepare bridgeData + bridgeData.hasSourceSwaps = true; + + // reset swap data + setDefaultSwapDataSingleDAItoSourceToken(); + + // 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( + bridgeData.transactionId, + address(uniswap), + ADDRESS_DAI, + ADDRESS_SRC_TOKEN, + swapData[0].fromAmount, + bridgeData.minAmount, + block.timestamp + ); + + vm.expectEmit(true, true, true, true, _facetTestContractAddress); + emit LiFiTransferStarted(bridgeData); + + initiateSwapAndBridgeTxWithFacet(false); + + // check balances after call + assertEq( + dai.balanceOf(USER_SENDER), + initialDAIBalance - swapData[0].fromAmount + ); + assertEq(USER_SENDER.balance, initialETHBalance - addToMessageValue); + } + + function testBase_Revert_BridgeAndSwapWithInvalidReceiverAddress() + public + virtual + override + { + vm.startPrank(USER_SENDER); + // prepare bridgeData + bridgeData.receiver = address(0); + bridgeData.hasSourceSwaps = true; + + setDefaultSwapDataSingleDAItoSourceToken(); + + 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), + defaultSrcTokenAmount + ); + + // 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( + InsufficientBalance.selector, + bridgeData.minAmount, + 0 + ) + ); + + 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(); + } + + 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 { + function setUp() public virtual override { + customRpcUrlForForking = "ETH_NODE_URI_ARBITRUM"; + customBlockNumberForForking = 303669576; + + airliftContract = IGlacisAirlift( + 0xD9E7f6f7Dc7517678127D84dBf0F0b4477De14E0 + ); + 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(); + } +} + +contract GlacisFacetLINKTest is GlacisFacetTestBase { + function setUp() public virtual override { + customRpcUrlForForking = "ETH_NODE_URI_BASE"; + customBlockNumberForForking = 26082794; + + airliftContract = IGlacisAirlift( + 0x30095227Eb6d72FA6c09DfdeFFC766c33f7FA2DD + ); + 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(); + } +} 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..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; + } } } @@ -443,6 +464,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 {