diff --git a/audit/auditLog.json b/audit/auditLog.json index 139bcc5d0..aebd08999 100644 --- a/audit/auditLog.json +++ b/audit/auditLog.json @@ -49,6 +49,13 @@ "auditReportPath": "./audit/reports/2024.11.22_Permit2Proxy.pdf", "auditCommitHash": "0e3debb78abcdf9a9f934115338b611e16b039a0" }, + "audit20241202": { + "auditCompletedOn": "02.12.2024", + "auditedBy": "Sujith Somraaj (individual security researcher)", + "auditorGitHandle": "sujithsomraaj", + "auditReportPath": "./audit/reports/2024-12-02_RelayFacet(v1.0.0).pdf", + "auditCommitHash": "291d0a78bc4174b3ec29bb2ce0b27c6b5d3e8ec8" + }, "audit20241203": { "auditCompletedOn": "03.12.2024", "auditedBy": "Sujith Somraaj (individual security researcher)", @@ -58,39 +65,73 @@ } }, "auditedContracts": { + "AcrossFacetPackedV3": { + "1.0.0": [ + "audit20241007" + ] + }, "AcrossFacetV3": { - "1.0.0": ["audit20241007"] + "1.0.0": [ + "audit20241007" + ] }, - "AcrossFacetPackedV3": { - "1.0.0": ["audit20241007"] + "EmergencyPauseFacet": { + "1.0.0": [ + "audit20240913" + ], + "1.0.1": [ + "audit20241105" + ] }, "GasZipFacet": { - "2.0.0": ["audit20241107"] + "2.0.0": [ + "audit20241107" + ] }, "GasZipPeriphery": { - "1.0.0": ["audit20241107"] + "1.0.0": [ + "audit20241107" + ] }, "IGasZip": { - "1.0.0": ["audit20241107"] + "1.0.0": [ + "audit20241107" + ] }, - "EmergencyPauseFacet": { - "1.0.0": ["audit20240913"], - "1.0.1": ["audit20241105"] + "LibAsset": { + "1.0.1": [ + "audit20241202" + ] }, "LiFiDEXAggregator": { - "1.5.0": ["audit20241203"] + "1.5.0": [ + "audit20241203" + ] }, "Permit2Proxy": { - "1.0.0": ["audit20241122"] + "1.0.0": [ + "audit20241122" + ] }, "ReceiverAcrossV3": { - "1.0.0": ["audit20241007"] + "1.0.0": [ + "audit20241007" + ] + }, + "RelayFacet": { + "1.0.0": [ + "audit20241202" + ] }, "StargateFacetV2": { - "1.0.1": ["audit20240814"] + "1.0.1": [ + "audit20240814" + ] }, "WithdrawablePeriphery": { - "1.0.0": ["audit20241014"] + "1.0.0": [ + "audit20241014" + ] } } -} +} \ No newline at end of file diff --git a/audit/reports/2024-12-02_RelayFacet(v1.0.0).pdf b/audit/reports/2024-12-02_RelayFacet(v1.0.0).pdf new file mode 100644 index 000000000..2061dacd3 Binary files /dev/null and b/audit/reports/2024-12-02_RelayFacet(v1.0.0).pdf differ diff --git a/config/relay.json b/config/relay.json new file mode 100644 index 000000000..6b68645c4 --- /dev/null +++ b/config/relay.json @@ -0,0 +1,46 @@ +{ + "mainnet": { + "relayReceiver": "0xa5f565650890fba1824ee0f21ebbbf660a179934", + "relaySolver": "0xf70da97812CB96acDF810712Aa562db8dfA3dbEF" + }, + "optimism": { + "relayReceiver": "0xa5f565650890fba1824ee0f21ebbbf660a179934", + "relaySolver": "0xf70da97812CB96acDF810712Aa562db8dfA3dbEF" + }, + "polygon": { + "relayReceiver": "0xa5f565650890fba1824ee0f21ebbbf660a179934", + "relaySolver": "0xf70da97812CB96acDF810712Aa562db8dfA3dbEF" + }, + "boba": { + "relayReceiver": "0xa06e1351e2fd2d45b5d35633ca7ecf328684a109", + "relaySolver": "0xf70da97812CB96acDF810712Aa562db8dfA3dbEF" + }, + "arbitrum": { + "relayReceiver": "0xa5f565650890fba1824ee0f21ebbbf660a179934", + "relaySolver": "0xf70da97812CB96acDF810712Aa562db8dfA3dbEF" + }, + "avalanche": { + "relayReceiver": "0xa5f565650890fba1824ee0f21ebbbf660a179934", + "relaySolver": "0xf70da97812CB96acDF810712Aa562db8dfA3dbEF" + }, + "blast": { + "relayReceiver": "0xa5f565650890fba1824ee0f21ebbbf660a179934", + "relaySolver": "0xf70da97812CB96acDF810712Aa562db8dfA3dbEF" + }, + "mode": { + "relayReceiver": "0xa5f565650890fba1824ee0f21ebbbf660a179934", + "relaySolver": "0xf70da97812CB96acDF810712Aa562db8dfA3dbEF" + }, + "linea": { + "relayReceiver": "0x00000000aa467eba42a3d604b3d74d63b2b6c6cb", + "relaySolver": "0xf70da97812CB96acDF810712Aa562db8dfA3dbEF" + }, + "taiko": { + "relayReceiver": "0xa5f565650890fba1824ee0f21ebbbf660a179934", + "relaySolver": "0xf70da97812CB96acDF810712Aa562db8dfA3dbEF" + }, + "scroll": { + "relayReceiver": "0xa5f565650890fba1824ee0f21ebbbf660a179934", + "relaySolver": "0xf70da97812CB96acDF810712Aa562db8dfA3dbEF" + } +} \ No newline at end of file diff --git a/deployments/_deployments_log_file.json b/deployments/_deployments_log_file.json index af7a48a7c..0e75c2db1 100644 --- a/deployments/_deployments_log_file.json +++ b/deployments/_deployments_log_file.json @@ -26126,5 +26126,49 @@ ] } } + }, + "RelayFacet": { + "arbitrum": { + "staging": { + "1.0.0": [ + { + "ADDRESS": "0x3cf7dE0e31e13C93c8Aada774ADF1C7eD58157f5", + "OPTIMIZER_RUNS": "1000000", + "TIMESTAMP": "2024-11-07 14:15:11", + "CONSTRUCTOR_ARGS": "0x000000000000000000000000a5f565650890fba1824ee0f21ebbbf660a179934000000000000000000000000f70da97812cb96acdf810712aa562db8dfa3dbef", + "SALT": "", + "VERIFIED": "true" + } + ] + } + }, + "polygon": { + "staging": { + "1.0.0": [ + { + "ADDRESS": "0x3cf7dE0e31e13C93c8Aada774ADF1C7eD58157f5", + "OPTIMIZER_RUNS": "1000000", + "TIMESTAMP": "2024-11-07 14:16:01", + "CONSTRUCTOR_ARGS": "0x000000000000000000000000a5f565650890fba1824ee0f21ebbbf660a179934000000000000000000000000f70da97812cb96acdf810712aa562db8dfa3dbef", + "SALT": "", + "VERIFIED": "true" + } + ] + } + }, + "optimism": { + "staging": { + "1.0.0": [ + { + "ADDRESS": "0x3cf7dE0e31e13C93c8Aada774ADF1C7eD58157f5", + "OPTIMIZER_RUNS": "1000000", + "TIMESTAMP": "2024-11-07 14:17:16", + "CONSTRUCTOR_ARGS": "0x000000000000000000000000a5f565650890fba1824ee0f21ebbbf660a179934000000000000000000000000f70da97812cb96acdf810712aa562db8dfa3dbef", + "SALT": "", + "VERIFIED": "true" + } + ] + } + } } } diff --git a/deployments/arbitrum.diamond.staging.json b/deployments/arbitrum.diamond.staging.json index 1ef30d615..f064c4857 100644 --- a/deployments/arbitrum.diamond.staging.json +++ b/deployments/arbitrum.diamond.staging.json @@ -125,9 +125,9 @@ "Name": "", "Version": "" }, - "0x2b64B62cbCfB38560222eBcfbbc4e65eC34c8Ce8": { - "Name": "", - "Version": "" + "0x74763722d92832d247cFa92825b06098cf72BAA2": { + "Name": "RelayFacet", + "Version": "1.0.0" }, "0x6124C65B6264bE13f059b7C3A891a5b77DA8Bd95": { "Name": "AcrossFacetV3", @@ -160,8 +160,8 @@ "FeeCollector": "0x7F8E9bEBd1Dea263A36a6916B99bd84405B9654a", "LiFiDEXAggregator": "", "LiFuelFeeCollector": "0x94EA56D8049e93E0308B9c7d1418Baf6A7C68280", - "Receiver": "0x36E9B2E8A627474683eF3b1E9Df26D2bF04396f3", "ReceiverAcrossV3": "0x3877f47B560819E96BBD7e7700a02dfACe36D696", + "Receiver": "0x36E9B2E8A627474683eF3b1E9Df26D2bF04396f3", "ReceiverStargateV2": "", "RelayerCelerIM": "0xa1Ed8783AC96385482092b82eb952153998e9b70", "TokenWrapper": "0xF63b27AE2Dc887b88f82E2Cc597d07fBB2E78E70" diff --git a/deployments/arbitrum.staging.json b/deployments/arbitrum.staging.json index b7df4ede9..8094fded2 100644 --- a/deployments/arbitrum.staging.json +++ b/deployments/arbitrum.staging.json @@ -49,5 +49,6 @@ "Permit2Proxy": "0x6FC01BC9Ff6Cdab694Ec8Ca41B21a2F04C8c37E5", "AcrossFacetV3": "0x6124C65B6264bE13f059b7C3A891a5b77DA8Bd95", "ReceiverAcrossV3": "0x3877f47B560819E96BBD7e7700a02dfACe36D696", - "AcrossFacetPackedV3": "0x4352459F6BE1C7D1278F8c34Bb598b0feeB50f8b" -} + "AcrossFacetPackedV3": "0x4352459F6BE1C7D1278F8c34Bb598b0feeB50f8b", + "RelayFacet": "0x3cf7dE0e31e13C93c8Aada774ADF1C7eD58157f5" +} \ No newline at end of file diff --git a/deployments/optimism.diamond.json b/deployments/optimism.diamond.json index d098cf278..b6c6a3dd2 100644 --- a/deployments/optimism.diamond.json +++ b/deployments/optimism.diamond.json @@ -163,6 +163,7 @@ "Permit2Proxy": "0x6307119078556Fc8aD77781DFC67df20d75FB4f9", "Receiver": "0x050e198E36A73a1e32F15C3afC58C4506d82f657", "ReceiverAcrossV3": "0xB9CEc304899037E661F49DdFa7f64943b5920072", + "Receiver": "0x050e198E36A73a1e32F15C3afC58C4506d82f657", "ReceiverStargateV2": "0x1493e7B8d4DfADe0a178dAD9335470337A3a219A", "RelayerCelerIM": "0x6a8b11bF29C0546991DEcD6E0Db8cC7Fda22bA97", "TokenWrapper": "0x5215E9fd223BC909083fbdB2860213873046e45d", diff --git a/deployments/optimism.diamond.staging.json b/deployments/optimism.diamond.staging.json index 92ef7979c..03011d7ce 100644 --- a/deployments/optimism.diamond.staging.json +++ b/deployments/optimism.diamond.staging.json @@ -124,6 +124,10 @@ "0x4352459F6BE1C7D1278F8c34Bb598b0feeB50f8b": { "Name": "AcrossFacetPackedV3", "Version": "1.0.0" + }, + "0x74763722d92832d247cFa92825b06098cf72BAA2": { + "Name": "RelayFacet", + "Version": "1.0.0" } }, "Periphery": { @@ -132,8 +136,8 @@ "FeeCollector": "0x7F8E9bEBd1Dea263A36a6916B99bd84405B9654a", "LiFiDEXAggregator": "", "LiFuelFeeCollector": "0x94EA56D8049e93E0308B9c7d1418Baf6A7C68280", - "Receiver": "0x36E9B2E8A627474683eF3b1E9Df26D2bF04396f3", "ReceiverAcrossV3": "0x3877f47B560819E96BBD7e7700a02dfACe36D696", + "Receiver": "0x36E9B2E8A627474683eF3b1E9Df26D2bF04396f3", "ReceiverStargateV2": "", "RelayerCelerIM": "0xa1Ed8783AC96385482092b82eb952153998e9b70", "TokenWrapper": "0xF63b27AE2Dc887b88f82E2Cc597d07fBB2E78E70" diff --git a/deployments/optimism.staging.json b/deployments/optimism.staging.json index bfc530438..91bbb5e5e 100644 --- a/deployments/optimism.staging.json +++ b/deployments/optimism.staging.json @@ -39,5 +39,6 @@ "EmergencyPauseFacet": "0x17Bb203F42d8e404ac7E8dB6ff972B7E8473850b", "AcrossFacetV3": "0x6124C65B6264bE13f059b7C3A891a5b77DA8Bd95", "ReceiverAcrossV3": "0x3877f47B560819E96BBD7e7700a02dfACe36D696", - "AcrossFacetPackedV3": "0x4352459F6BE1C7D1278F8c34Bb598b0feeB50f8b" -} + "AcrossFacetPackedV3": "0x4352459F6BE1C7D1278F8c34Bb598b0feeB50f8b", + "RelayFacet": "0x3cf7dE0e31e13C93c8Aada774ADF1C7eD58157f5" +} \ No newline at end of file diff --git a/deployments/polygon.diamond.staging.json b/deployments/polygon.diamond.staging.json index 3114c1eb9..03e420933 100644 --- a/deployments/polygon.diamond.staging.json +++ b/deployments/polygon.diamond.staging.json @@ -120,6 +120,10 @@ "0xE15C7585636e62b88bA47A40621287086E0c2E33": { "Name": "", "Version": "" + }, + "0x74763722d92832d247cFa92825b06098cf72BAA2": { + "Name": "RelayFacet", + "Version": "1.0.0" } }, "Periphery": { @@ -128,8 +132,8 @@ "FeeCollector": "0x7F8E9bEBd1Dea263A36a6916B99bd84405B9654a", "LiFiDEXAggregator": "", "LiFuelFeeCollector": "0x94EA56D8049e93E0308B9c7d1418Baf6A7C68280", - "Receiver": "0x36E9B2E8A627474683eF3b1E9Df26D2bF04396f3", "ReceiverAcrossV3": "", + "Receiver": "0x36E9B2E8A627474683eF3b1E9Df26D2bF04396f3", "ReceiverStargateV2": "", "ReceiverAcrossV3": "", "RelayerCelerIM": "0xa1Ed8783AC96385482092b82eb952153998e9b70", diff --git a/deployments/polygon.staging.json b/deployments/polygon.staging.json index de64abd96..b5a516e91 100644 --- a/deployments/polygon.staging.json +++ b/deployments/polygon.staging.json @@ -47,5 +47,6 @@ "HopFacetOptimized": "0xf82135385765f1324257ffF74489F16382EBBb8A", "SymbiosisFacet": "0x21571D628B0bCBeb954D5933A604eCac35bAF2c7", "AcrossFacetV3": "0xe2e5428F972d9C0a5Ba433e0c402752b472dB248", + "RelayFacet": "0x3cf7dE0e31e13C93c8Aada774ADF1C7eD58157f5", "Permit2Proxy": "0x6FC01BC9Ff6Cdab694Ec8Ca41B21a2F04C8c37E5" -} +} \ No newline at end of file diff --git a/docs/README.md b/docs/README.md index b64bbc7e9..cb4f60b94 100644 --- a/docs/README.md +++ b/docs/README.md @@ -28,6 +28,7 @@ - [Optimism Bridge Facet](./OptimismBridgeFacet.md) - [Periphery Registry Facet](./PeripheryRegistryFacet.md) - [Polygon Bridge Facet](./PolygonBridgeFacet.md) +- [Relay Facet](./RelayFacet.md) - [Ronin Bridge Facet](./RoninBridgeFacet.md) - [Squid Facet](./SquidFacet.md) - [Standardized Call Facet](./StandardizedCallFacet.md) diff --git a/docs/RelayFacet.md b/docs/RelayFacet.md new file mode 100644 index 000000000..1bb34b3e9 --- /dev/null +++ b/docs/RelayFacet.md @@ -0,0 +1,104 @@ +# Relay Facet + +Relay is a cross-chain payments system enabling instant, low-cost bridging and cross-chain execution using relayers as financial agents. + +## How it works + +The Relay Facet works by sending funds directly to the RelayReceiver contract in the case of Native tokens or sending tokens directly +to the official Relay solver EOA along with extra calldata bytes that reference a prefecthed quote id + +```mermaid +graph LR; + D{LiFiDiamond}-- DELEGATECALL -->RelayFacet; + RelayFacet -- CALL --> C(Relay) +``` + +## Public Methods + +- `function startBridgeTokensViaRelay(BridgeData calldata _bridgeData, RelayData calldata _relayData)` + - Simply bridges tokens using relay +- `swapAndStartBridgeTokensViaRelay(BridgeData memory _bridgeData, LibSwap.SwapData[] calldata _swapData, relayData memory _relayData)` + - Performs swap(s) before bridging tokens using relay + +## relay Specific Parameters + +The methods listed above take a variable labeled `_relayData`. This data is specific to relay and is represented as the following struct type: + +```solidity +/// @dev Relay specific parameters +/// @param requestId Realy API request ID +/// @param nonEVMReceiver set only if bridging to non-EVM chain +/// @params receivingAssetId address of receiving asset +/// @params callData calldata provided by Relay API +/// @params signature attestation signature provided by the Relay solver +struct RelayData { + bytes32 requestId; + bytes32 nonEVMReceiver; + bytes32 receivingAssetId; + bytes callData; + bytes signature; +} +``` + +## 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: 'relay', // 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=relay&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=relay&fromAddress={YOUR_WALLET_ADDRESS}' +``` diff --git a/package.json b/package.json index d680ca00c..51cc55fde 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "@nomiclabs/hardhat-ethers": "npm:hardhat-deploy-ethers@^0.3.0-beta.13", "@nomiclabs/hardhat-etherscan": "3.1.6", "@nomiclabs/hardhat-waffle": "2.0.5", + "@solana/web3.js": "^1.95.4", "@typechain/ethers-v5": "^10.2.0", "@typechain/hardhat": "^6.1.5", "@types/node": "^17.0.23", diff --git a/script/demoScripts/demoRelay.ts b/script/demoScripts/demoRelay.ts new file mode 100644 index 000000000..9340fc6d7 --- /dev/null +++ b/script/demoScripts/demoRelay.ts @@ -0,0 +1,326 @@ +import deployments from '../../deployments/arbitrum.staging.json' +import { + RelayFacet__factory, + ILiFi, + type RelayFacet, + ERC20__factory, +} from '../../typechain' +import { ethers, utils } from 'ethers' +import dotenv from 'dotenv' +import { + ADDRESS_UNISWAP_ARB, + ADDRESS_USDC_ARB, + ADDRESS_WETH_ARB, + getUniswapSwapDataERC20ToERC20, + getUniswapSwapDataERC20ToETH, +} from './utils/demoScriptHelpers' +import { _100 } from '@uniswap/sdk/dist/constants' +import { PublicKey } from '@solana/web3.js' +dotenv.config() + +const main = async () => { + const RPC_URL = process.env.ETH_NODE_URI_ARBITRUM + const PRIVATE_KEY = process.env.PRIVATE_KEY + const LIFI_ADDRESS = deployments.LiFiDiamond + + const provider = new ethers.providers.JsonRpcProvider(RPC_URL) + const signer = new ethers.Wallet(PRIVATE_KEY as string, provider) + const relay = RelayFacet__factory.connect(LIFI_ADDRESS, provider) + + const address = await signer.getAddress() + + let tx + + // Bridge ETH + + let params = { + user: deployments.LiFiDiamond, + originChainId: 42161, + destinationChainId: 137, + originCurrency: '0x0000000000000000000000000000000000000000', + destinationCurrency: '0x0000000000000000000000000000000000000000', + recipient: address, + tradeType: 'EXACT_INPUT', + amount: '1000000000000000', + referrer: 'relay.link/swap', + useExternalLiquidity: false, + } + + let resp = await fetch('https://api.relay.link/quote', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(params), + }) + let quote = await resp.json() + let requestId = quote.steps[0].requestId + + let sigResp = await fetch( + `https://api.relay.link/requests/${requestId}/signature/v2`, + { headers: { 'Content-Type': 'application/json' } } + ) + let sigData = await sigResp.json() + + let bridgeData: ILiFi.BridgeDataStruct = { + transactionId: utils.randomBytes(32), + bridge: 'Relay', + integrator: 'ACME Devs', + referrer: '0x0000000000000000000000000000000000000000', + sendingAssetId: '0x0000000000000000000000000000000000000000', + receiver: address, + minAmount: ethers.utils.parseEther('0.001'), + destinationChainId: 137, + hasSourceSwaps: false, + hasDestinationCall: false, + } + + let relayData: RelayFacet.RelayDataStruct = { + requestId, + nonEVMReceiver: ethers.constants.HashZero, + receivingAssetId: ethers.constants.HashZero, + signature: sigData.signature, + } + + console.info('Dev Wallet Address: ', address) + console.info('Bridging ETH...') + tx = await relay + .connect(signer) + .startBridgeTokensViaRelay(bridgeData, relayData, { + value: ethers.utils.parseEther('0.001'), + }) + await tx.wait() + console.info('Bridged ETH') + + // Bridge USDC + + params = { + user: deployments.LiFiDiamond, + originChainId: 42161, + destinationChainId: 10, + originCurrency: '0xaf88d065e77c8cC2239327C5EDb3A432268e5831', + destinationCurrency: '0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85', + recipient: address, + tradeType: 'EXACT_INPUT', + amount: '5000000', + referrer: 'relay.link/swap', + useExternalLiquidity: false, + } + + resp = await fetch('https://api.relay.link/quote', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(params), + }) + quote = await resp.json() + requestId = quote.steps[0].requestId + + sigResp = await fetch( + `https://api.relay.link/requests/${requestId}/signature/v2`, + { headers: { 'Content-Type': 'application/json' } } + ) + sigData = await sigResp.json() + + const token = ERC20__factory.connect( + '0xaf88d065e77c8cC2239327C5EDb3A432268e5831', + provider + ) + + bridgeData = { + transactionId: utils.randomBytes(32), + bridge: 'Relay', + integrator: 'ACME Devs', + referrer: '0x0000000000000000000000000000000000000000', + sendingAssetId: '0xaf88d065e77c8cC2239327C5EDb3A432268e5831', + receiver: address, + minAmount: '5000000', + destinationChainId: 10, + hasSourceSwaps: false, + hasDestinationCall: false, + } + + relayData = { + requestId, + nonEVMReceiver: ethers.constants.HashZero, + receivingAssetId: ethers.utils.hexZeroPad( + '0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85', + 32 + ), + signature: sigData.signature, + } + + console.info('Dev Wallet Address: ', address) + console.info('Approving USDC...') + tx = await token.connect(signer).approve(LIFI_ADDRESS, '5000000') + await tx.wait() + console.info('Approved USDC') + console.info('Bridging USDC...') + tx = await relay + .connect(signer) + .startBridgeTokensViaRelay(bridgeData, relayData) + await tx.wait() + console.info('Bridged USDC') + + // Swap USDC and Bridge ETH + + params = { + user: deployments.LiFiDiamond, + originChainId: 42161, + destinationChainId: 137, + originCurrency: '0x0000000000000000000000000000000000000000', + destinationCurrency: '0x0000000000000000000000000000000000000000', + recipient: address, + tradeType: 'EXACT_INPUT', + amount: '1000000000000000', + referrer: 'relay.link/swap', + useExternalLiquidity: false, + } + + resp = await fetch('https://api.relay.link/quote', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(params), + }) + quote = await resp.json() + requestId = quote.steps[0].requestId + + sigResp = await fetch( + `https://api.relay.link/requests/${requestId}/signature/v2`, + { headers: { 'Content-Type': 'application/json' } } + ) + sigData = await sigResp.json() + console.log(sigData) + + bridgeData = { + transactionId: utils.randomBytes(32), + bridge: 'Relay', + integrator: 'ACME Devs', + referrer: '0x0000000000000000000000000000000000000000', + sendingAssetId: '0x0000000000000000000000000000000000000000', + receiver: address, + minAmount: ethers.utils.parseEther('0.001'), + destinationChainId: 137, + hasSourceSwaps: true, + hasDestinationCall: false, + } + + const swapData = [] + + const uniswapAddress = ADDRESS_UNISWAP_ARB + swapData[0] = await getUniswapSwapDataERC20ToETH( + uniswapAddress, + 42161, + ADDRESS_USDC_ARB, + ADDRESS_WETH_ARB, + ethers.utils.parseUnits('4', 6), + LIFI_ADDRESS, + true + ) + + relayData = { + requestId, + nonEVMReceiver: ethers.constants.HashZero, + receivingAssetId: ethers.constants.HashZero, + signature: sigData.signature, + } + + console.info('Dev Wallet Address: ', address) + console.info('Approving USDC...') + tx = await token.connect(signer).approve(LIFI_ADDRESS, '4000000') + await tx.wait() + console.info('Approved USDC') + console.info('Bridging USDC -> ETH...') + tx = await relay + .connect(signer) + .swapAndStartBridgeTokensViaRelay(bridgeData, swapData, relayData) + await tx.wait() + console.info('Bridged ETH') + + // Bridge USDC to Solana + + const solanaReceiver = 'EoW7FWTdPdZKpd3WAhH98c2HMGHsdh5yhzzEtk1u68Bb' + const solanaUSDC = 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v' + + params = { + user: deployments.LiFiDiamond, + originChainId: 42161, + destinationChainId: 792703809, + originCurrency: '0xaf88d065e77c8cC2239327C5EDb3A432268e5831', + destinationCurrency: solanaUSDC, + recipient: solanaReceiver, + tradeType: 'EXACT_INPUT', + amount: '5000000', + referrer: 'relay.link/swap', + useExternalLiquidity: false, + } + + resp = await fetch('https://api.relay.link/quote', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(params), + }) + quote = await resp.json() + console.log(quote) + requestId = quote.steps[0].requestId + + console.log(requestId) + sigResp = await fetch( + `https://api.relay.link/requests/${requestId}/signature/v2`, + { headers: { 'Content-Type': 'application/json' } } + ) + sigData = await sigResp.json() + console.log(sigData) + + bridgeData = { + transactionId: utils.randomBytes(32), + bridge: 'Relay', + integrator: 'ACME Devs', + referrer: '0x0000000000000000000000000000000000000000', + sendingAssetId: '0xaf88d065e77c8cC2239327C5EDb3A432268e5831', + receiver: '0x11f111f111f111F111f111f111F111f111f111F1', + minAmount: '5000000', + destinationChainId: 1151111081099710, + hasSourceSwaps: false, + hasDestinationCall: false, + } + + relayData = { + requestId, + nonEVMReceiver: `0x${new PublicKey(solanaReceiver) + .toBuffer() + .toString('hex')}`, + receivingAssetId: `0x${new PublicKey(solanaUSDC) + .toBuffer() + .toString('hex')}`, + signature: sigData.signature, + } + + console.info('Dev Wallet Address: ', address) + console.info('Approving USDC...') + tx = await token.connect(signer).approve(LIFI_ADDRESS, '5000000') + await tx.wait() + console.info('Approved USDC') + console.info('Bridging USDC...') + tx = await relay + .connect(signer) + .startBridgeTokensViaRelay(bridgeData, relayData) + await tx.wait() + console.info('Bridged USDC') +} + +main() + .then(() => { + console.log('Success') + process.exit(0) + }) + .catch((error) => { + console.error('error') + console.error(error) + process.exit(1) + }) diff --git a/script/demoScripts/utils/demoScriptHelpers.ts b/script/demoScripts/utils/demoScriptHelpers.ts index ee2a0ab3a..3cacf9630 100644 --- a/script/demoScripts/utils/demoScriptHelpers.ts +++ b/script/demoScripts/utils/demoScriptHelpers.ts @@ -195,6 +195,63 @@ export const getUniswapSwapDataERC20ToERC20 = async ( return swapData } +export const getUniswapSwapDataERC20ToETH = async ( + uniswapAddress: string, + chainId: number, + sendingAssetId: string, + receivingAssetId: string, + fromAmount: BigNumber, + receiverAddress: string, + requiresDeposit = true, + minAmountOut = 0, + deadline = Math.floor(Date.now() / 1000) + 60 * 60 +) => { + // prepare destSwap callData + const uniswap = new Contract(uniswapAddress, [ + 'function swapExactTokensForETH(uint amountIn, uint amountOutMin, address[] calldata path, address to, uint deadline) external payable returns (uint[] memory amounts)', + ]) + const path = [sendingAssetId, receivingAssetId] + + // get minAmountOut from Uniswap router + console.log(`finalFromAmount : ${fromAmount}`) + + const finalMinAmountOut = + minAmountOut == 0 + ? await getAmountsOutUniswap( + uniswapAddress, + chainId, + [sendingAssetId, receivingAssetId], + fromAmount + ) + : minAmountOut + console.log(`finalMinAmountOut: ${finalMinAmountOut}`) + + const uniswapCalldata = ( + await uniswap.populateTransaction.swapExactTokensForETH( + fromAmount, // amountIn + finalMinAmountOut, + path, + receiverAddress, + deadline + ) + ).data + + if (!uniswapCalldata) throw Error('Could not create Uniswap calldata') + + // construct LibSwap.SwapData + const swapData: LibSwap.SwapDataStruct = { + callTo: uniswapAddress, + approveTo: uniswapAddress, + sendingAssetId, + receivingAssetId: '0x0000000000000000000000000000000000000000', + fromAmount, + callData: uniswapCalldata, + requiresDeposit, + } + + return swapData +} + export const getAmountsOutUniswap = async ( uniswapAddress: string, chainId: number, diff --git a/script/deploy/facets/DeployRelayFacet.s.sol b/script/deploy/facets/DeployRelayFacet.s.sol new file mode 100644 index 000000000..f1382e220 --- /dev/null +++ b/script/deploy/facets/DeployRelayFacet.s.sol @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.17; + +import { DeployScriptBase } from "./utils/DeployScriptBase.sol"; +import { stdJson } from "forge-std/Script.sol"; +import { RelayFacet } from "lifi/Facets/RelayFacet.sol"; + +contract DeployScript is DeployScriptBase { + using stdJson for string; + + constructor() DeployScriptBase("RelayFacet") {} + + function run() + public + returns (RelayFacet deployed, bytes memory constructorArgs) + { + constructorArgs = getConstructorArgs(); + + deployed = RelayFacet(deploy(type(RelayFacet).creationCode)); + } + + function getConstructorArgs() internal override returns (bytes memory) { + string memory path = string.concat(root, "/config/relay.json"); + string memory json = vm.readFile(path); + + address relayReceiver = json.readAddress( + string.concat(".", network, ".relayReceiver") + ); + + address relaySolver = json.readAddress( + string.concat(".", network, ".relaySolver") + ); + + return abi.encode(relayReceiver, relaySolver); + } +} diff --git a/script/deploy/facets/UpdateRelayFacet.s.sol b/script/deploy/facets/UpdateRelayFacet.s.sol new file mode 100644 index 000000000..54655e4f8 --- /dev/null +++ b/script/deploy/facets/UpdateRelayFacet.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("RelayFacet"); + } +} diff --git a/script/deploy/resources/deployRequirements.json b/script/deploy/resources/deployRequirements.json index 2fcb8bd11..d1deafe1a 100644 --- a/script/deploy/resources/deployRequirements.json +++ b/script/deploy/resources/deployRequirements.json @@ -524,6 +524,20 @@ } } }, + "RelayFacet": { + "configData": { + "_relayReceiver": { + "configFileName": "relay.json", + "keyInConfigFile": "..relayReceiver", + "allowToDeployWithZeroAddress": "false" + }, + "_relaySolver": { + "configFileName": "relay.json", + "keyInConfigFile": "..relaySolver", + "allowToDeployWithZeroAddress": "false" + } + } + }, "Permit2Proxy": { "configData": { "permit2Address": { diff --git a/src/Facets/RelayFacet.sol b/src/Facets/RelayFacet.sol new file mode 100644 index 000000000..27acfc735 --- /dev/null +++ b/src/Facets/RelayFacet.sol @@ -0,0 +1,233 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.17; + +import { ILiFi } from "../Interfaces/ILiFi.sol"; +import { LibAsset } from "../Libraries/LibAsset.sol"; +import { LibSwap } from "../Libraries/LibSwap.sol"; +import { LibUtil } from "../Libraries/LibUtil.sol"; +import { ReentrancyGuard } from "../Helpers/ReentrancyGuard.sol"; +import { SwapperV2 } from "../Helpers/SwapperV2.sol"; +import { Validatable } from "../Helpers/Validatable.sol"; +import { ECDSA } from "solady/utils/ECDSA.sol"; + +/// @title Relay Facet +/// @author LI.FI (https://li.fi) +/// @notice Provides functionality for bridging through Relay Protocol +/// @custom:version 1.0.0 +contract RelayFacet is ILiFi, ReentrancyGuard, SwapperV2, Validatable { + // Receiver for native transfers + address public immutable relayReceiver; + // Relayer wallet for ERC20 transfers + address public immutable relaySolver; + + /// Storage /// + + mapping(bytes32 => bool) public consumedIds; + + /// Types /// + + /// @dev Relay specific parameters + /// @param requestId Relay API request ID + /// @param nonEVMReceiver set only if bridging to non-EVM chain + /// @params receivingAssetId address of receiving asset + /// @params signature attestation signature provided by the Relay solver + struct RelayData { + bytes32 requestId; + bytes32 nonEVMReceiver; + bytes32 receivingAssetId; + bytes signature; + } + + /// Events /// + + event BridgeToNonEVMChain( + bytes32 indexed transactionId, + uint256 indexed destinationChainId, + bytes32 receiver + ); + + /// Errors /// + + error InvalidQuote(); + + /// Modifiers /// + + /// @param _bridgeData The core information needed for bridging + /// @param _relayData Data specific to Relay + modifier onlyValidQuote( + ILiFi.BridgeData memory _bridgeData, + RelayData calldata _relayData + ) { + // Ensure that the id isn't already consumed + if (consumedIds[_relayData.requestId]) { + revert InvalidQuote(); + } + + // Ensure nonEVMAddress is not empty + if ( + _bridgeData.receiver == LibAsset.NON_EVM_ADDRESS && + _relayData.nonEVMReceiver == bytes32(0) + ) { + revert InvalidQuote(); + } + + // Verify that the bridging quote has been signed by the Relay solver + // as attested using the attestation API + // API URL: https://api.relay.link/requests/{requestId}/signature/v2 + bytes32 message = ECDSA.toEthSignedMessageHash( + keccak256( + abi.encodePacked( + _relayData.requestId, + block.chainid, + bytes32(uint256(uint160(address(this)))), + bytes32(uint256(uint160(_bridgeData.sendingAssetId))), + _getMappedChainId(_bridgeData.destinationChainId), + _bridgeData.receiver == LibAsset.NON_EVM_ADDRESS + ? _relayData.nonEVMReceiver + : bytes32(uint256(uint160(_bridgeData.receiver))), + _relayData.receivingAssetId + ) + ) + ); + address signer = ECDSA.recover(message, _relayData.signature); + if (signer != relaySolver) { + revert InvalidQuote(); + } + _; + } + + /// Constructor /// + + /// @param _relayReceiver The receiver for native transfers + /// @param _relaySolver The relayer wallet for ERC20 transfers + constructor(address _relayReceiver, address _relaySolver) { + relayReceiver = _relayReceiver; + relaySolver = _relaySolver; + } + + /// External Methods /// + + /// @notice Bridges tokens via Relay + /// @param _bridgeData The core information needed for bridging + /// @param _relayData Data specific to Relay + function startBridgeTokensViaRelay( + ILiFi.BridgeData calldata _bridgeData, + RelayData calldata _relayData + ) + external + payable + nonReentrant + onlyValidQuote(_bridgeData, _relayData) + refundExcessNative(payable(msg.sender)) + validateBridgeData(_bridgeData) + doesNotContainSourceSwaps(_bridgeData) + doesNotContainDestinationCalls(_bridgeData) + { + LibAsset.depositAsset( + _bridgeData.sendingAssetId, + _bridgeData.minAmount + ); + _startBridge(_bridgeData, _relayData); + } + + /// @notice Performs a swap before bridging via Relay + /// @param _bridgeData The core information needed for bridging + /// @param _swapData An array of swap related data for performing swaps before bridging + /// @param _relayData Data specific to Relay + function swapAndStartBridgeTokensViaRelay( + ILiFi.BridgeData memory _bridgeData, + LibSwap.SwapData[] calldata _swapData, + RelayData calldata _relayData + ) + external + payable + nonReentrant + onlyValidQuote(_bridgeData, _relayData) + refundExcessNative(payable(msg.sender)) + containsSourceSwaps(_bridgeData) + doesNotContainDestinationCalls(_bridgeData) + validateBridgeData(_bridgeData) + { + _bridgeData.minAmount = _depositAndSwap( + _bridgeData.transactionId, + _bridgeData.minAmount, + _swapData, + payable(msg.sender) + ); + _startBridge(_bridgeData, _relayData); + } + + /// Internal Methods /// + + /// @dev Contains the business logic for the bridge via Relay + /// @param _bridgeData The core information needed for bridging + /// @param _relayData Data specific to Relay + function _startBridge( + ILiFi.BridgeData memory _bridgeData, + RelayData calldata _relayData + ) internal { + // check if sendingAsset is native or ERC20 + if (LibAsset.isNativeAsset(_bridgeData.sendingAssetId)) { + // Native + + // Send Native to relayReceiver along with requestId as extra data + (bool success, bytes memory reason) = relayReceiver.call{ + value: _bridgeData.minAmount + }(abi.encode(_relayData.requestId)); + if (!success) { + revert(LibUtil.getRevertMsg(reason)); + } + } else { + // ERC20 + + // We build the calldata from scratch to ensure that we can only + // send to the solver address + bytes memory transferCallData = bytes.concat( + abi.encodeWithSignature( + "transfer(address,uint256)", + relaySolver, + _bridgeData.minAmount + ), + abi.encode(_relayData.requestId) + ); + (bool success, bytes memory reason) = address( + _bridgeData.sendingAssetId + ).call(transferCallData); + if (!success) { + revert(LibUtil.getRevertMsg(reason)); + } + } + + consumedIds[_relayData.requestId] = true; + + // Emit special event if bridging to non-EVM chain + if (_bridgeData.receiver == LibAsset.NON_EVM_ADDRESS) { + emit BridgeToNonEVMChain( + _bridgeData.transactionId, + _getMappedChainId(_bridgeData.destinationChainId), + _relayData.nonEVMReceiver + ); + } + + emit LiFiTransferStarted(_bridgeData); + } + + /// @notice get Relay specific chain id for non-EVM chains + /// IDs found here https://li.quest/v1/chains?chainTypes=UTXO,SVM + /// @param chainId LIFI specific chain id + function _getMappedChainId( + uint256 chainId + ) internal pure returns (uint256) { + // Bitcoin + if (chainId == 20000000000001) { + return 8253038; + } + + // Solana + if (chainId == 1151111081099710) { + return 792703809; + } + + return chainId; + } +} diff --git a/src/Libraries/LibAsset.sol b/src/Libraries/LibAsset.sol index c2dd7dad0..f4e9d7d55 100644 --- a/src/Libraries/LibAsset.sol +++ b/src/Libraries/LibAsset.sol @@ -1,5 +1,4 @@ // SPDX-License-Identifier: UNLICENSED -/// @custom:version 1.0.0 pragma solidity ^0.8.17; import { InsufficientBalance, NullAddrIsNotAnERC20Token, NullAddrIsNotAValidSpender, NoTransferToNullAddress, InvalidAmount, NativeAssetTransferFailed } from "../Errors/GenericErrors.sol"; import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; @@ -7,6 +6,7 @@ import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { LibSwap } from "./LibSwap.sol"; /// @title LibAsset +/// @custom:version 1.0.1 /// @notice This library contains helpers for dealing with onchain transfers /// of assets, including accounting for the native asset `assetId` /// conventions and any noncompliant ERC20 transfers @@ -15,6 +15,9 @@ library LibAsset { address internal constant NULL_ADDRESS = address(0); + address internal constant NON_EVM_ADDRESS = + 0x11f111f111f111F111f111f111F111f111f111F1; + /// @dev All native assets use the empty address for their asset id /// by convention diff --git a/templates/facet.template.hbs b/templates/facet.template.hbs index 831a1ee10..dafaa9fc5 100644 --- a/templates/facet.template.hbs +++ b/templates/facet.template.hbs @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.17; +pragma solidity ^0.8.17; import { ILiFi } from "../Interfaces/ILiFi.sol"; import { LibDiamond } from "../Libraries/LibDiamond.sol"; diff --git a/templates/facetDeployScript.template.hbs b/templates/facetDeployScript.template.hbs index 6e130576a..840e533a2 100644 --- a/templates/facetDeployScript.template.hbs +++ b/templates/facetDeployScript.template.hbs @@ -24,7 +24,7 @@ contract DeployScript is DeployScriptBase { string memory path = string.concat(root, "/config/{{camelCase name}}.json"); string memory json = vm.readFile(path); - address acrossSpokePool = json.readAddress( + address example = json.readAddress( string.concat(".", network, ".example") ); diff --git a/templates/facetDoc.template.hbs b/templates/facetDoc.template.hbs index 4ae472edc..a825fcd29 100644 --- a/templates/facetDoc.template.hbs +++ b/templates/facetDoc.template.hbs @@ -14,7 +14,7 @@ graph LR; - `function startBridgeTokensVia{{titleCase name}}(BridgeData calldata _bridgeData, {{titleCase name}}Data calldata _{{camelCase name}}Data)` - Simply bridges tokens using {{camelCase name}} -- `swapAndStartBridgeTokensVia{{camelCase name}}(BridgeData memory _bridgeData, LibSwap.SwapData[] calldata _swapData, {{camelCase name}}Data memory _{{camelCase name}}Data)` +- `swapAndStartBridgeTokensVia{{titleCase name}}(BridgeData memory _bridgeData, LibSwap.SwapData[] calldata _swapData, {{camelCase name}}Data memory _{{camelCase name}}Data)` - Performs swap(s) before bridging tokens using {{camelCase name}} ## {{camelCase name}} Specific Parameters diff --git a/test/solidity/Facets/RelayFacet.t.sol b/test/solidity/Facets/RelayFacet.t.sol new file mode 100644 index 000000000..0ac283bfe --- /dev/null +++ b/test/solidity/Facets/RelayFacet.t.sol @@ -0,0 +1,778 @@ +// SPDX-License-Identifier: Unlicense +pragma solidity 0.8.17; + +import { LibAllowList, TestBaseFacet, console, ERC20, LibAsset, LibSwap } from "../utils/TestBaseFacet.sol"; +import { RelayFacet } from "lifi/Facets/RelayFacet.sol"; +import { ILiFi } from "lifi/Interfaces/ILiFi.sol"; + +contract Reverter { + fallback() external { + revert("I always revert"); + } +} + +// Stub RelayFacet Contract +contract TestRelayFacet is RelayFacet { + constructor( + address _relayReceiver, + address _relaySolver + ) RelayFacet(_relayReceiver, _relaySolver) {} + + function addDex(address _dex) external { + LibAllowList.addAllowedContract(_dex); + } + + function setFunctionApprovalBySignature(bytes4 _signature) external { + LibAllowList.addAllowedSelector(_signature); + } + + function getMappedChainId( + uint256 chainId + ) external pure returns (uint256) { + return _getMappedChainId(chainId); + } + + function setConsumedId(bytes32 id) external { + consumedIds[id] = true; + } +} + +contract RelayFacetTest is TestBaseFacet { + RelayFacet.RelayData internal validRelayData; + TestRelayFacet internal relayFacet; + address internal RELAY_RECEIVER = + 0xa5F565650890fBA1824Ee0F21EbBbF660a179934; + uint256 internal PRIVATE_KEY = 0x1234567890; + address RELAY_SOLVER = vm.addr(PRIVATE_KEY); + + error InvalidQuote(); + + function setUp() public { + customBlockNumberForForking = 19767662; + initTestBase(); + relayFacet = new TestRelayFacet(RELAY_RECEIVER, RELAY_SOLVER); + bytes4[] memory functionSelectors = new bytes4[](6); + functionSelectors[0] = relayFacet.startBridgeTokensViaRelay.selector; + functionSelectors[1] = relayFacet + .swapAndStartBridgeTokensViaRelay + .selector; + functionSelectors[2] = relayFacet.addDex.selector; + functionSelectors[3] = relayFacet + .setFunctionApprovalBySignature + .selector; + functionSelectors[4] = relayFacet.getMappedChainId.selector; + functionSelectors[5] = relayFacet.setConsumedId.selector; + + addFacet(diamond, address(relayFacet), functionSelectors); + relayFacet = TestRelayFacet(address(diamond)); + relayFacet.addDex(ADDRESS_UNISWAP); + relayFacet.setFunctionApprovalBySignature( + uniswap.swapExactTokensForTokens.selector + ); + relayFacet.setFunctionApprovalBySignature( + uniswap.swapTokensForExactETH.selector + ); + relayFacet.setFunctionApprovalBySignature( + uniswap.swapETHForExactTokens.selector + ); + + setFacetAddressInTestBase(address(relayFacet), "RelayFacet"); + + // adjust bridgeData + bridgeData.bridge = "relay"; + bridgeData.destinationChainId = 137; + + validRelayData = RelayFacet.RelayData({ + requestId: bytes32("1234"), + nonEVMReceiver: "", + receivingAssetId: bytes32( + uint256(uint160(0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174)) + ), // Polygon USDC + signature: "" + }); + } + + function initiateBridgeTxWithFacet(bool isNative) internal override { + validRelayData.signature = signData(bridgeData, validRelayData); + if (isNative) { + relayFacet.startBridgeTokensViaRelay{ + value: bridgeData.minAmount + }(bridgeData, validRelayData); + } else { + relayFacet.startBridgeTokensViaRelay(bridgeData, validRelayData); + } + } + + function initiateSwapAndBridgeTxWithFacet( + bool isNative + ) internal override { + validRelayData.signature = signData(bridgeData, validRelayData); + if (isNative) { + relayFacet.swapAndStartBridgeTokensViaRelay{ + value: swapData[0].fromAmount + }(bridgeData, swapData, validRelayData); + } else { + relayFacet.swapAndStartBridgeTokensViaRelay( + bridgeData, + swapData, + validRelayData + ); + } + } + + function test_CanDeployFacet() public virtual { + new RelayFacet(RELAY_RECEIVER, RELAY_SOLVER); + } + + function testRevert_BridgeWithInvalidSignature() public virtual { + vm.startPrank(USER_SENDER); + + // approval + usdc.approve(_facetTestContractAddress, bridgeData.minAmount); + + PRIVATE_KEY = 0x0987654321; + + vm.expectRevert(InvalidQuote.selector); + initiateBridgeTxWithFacet(false); + vm.stopPrank(); + } + + function test_CanBridgeTokensToSolana() + public + virtual + assertBalanceChange( + ADDRESS_USDC, + USER_SENDER, + -int256(defaultUSDCAmount) + ) + assertBalanceChange(ADDRESS_USDC, USER_RECEIVER, 0) + assertBalanceChange(ADDRESS_DAI, USER_SENDER, 0) + assertBalanceChange(ADDRESS_DAI, USER_RECEIVER, 0) + { + bridgeData.receiver = LibAsset.NON_EVM_ADDRESS; + bridgeData.destinationChainId = 1151111081099710; + validRelayData = RelayFacet.RelayData({ + requestId: bytes32("1234"), + nonEVMReceiver: bytes32( + abi.encodePacked( + "EoW7FWTdPdZKpd3WAhH98c2HMGHsdh5yhzzEtk1u68Bb" + ) + ), // DEV Wallet + receivingAssetId: bytes32( + abi.encodePacked( + "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v" + ) + ), // Solana USDC + signature: "" + }); + + vm.startPrank(USER_SENDER); + + // approval + usdc.approve(_facetTestContractAddress, bridgeData.minAmount); + + //prepare check for events + vm.expectEmit(true, true, true, true, _facetTestContractAddress); + emit LiFiTransferStarted(bridgeData); + + initiateBridgeTxWithFacet(false); + vm.stopPrank(); + } + + function testRevert_WhenUsingEmptyNonEVMAddress() public virtual { + bridgeData.receiver = LibAsset.NON_EVM_ADDRESS; + bridgeData.destinationChainId = 1151111081099710; + validRelayData = RelayFacet.RelayData({ + requestId: bytes32("1234"), + nonEVMReceiver: bytes32(0), // DEV Wallet + receivingAssetId: bytes32( + abi.encodePacked( + "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v" + ) + ), // Solana USDC + signature: "" + }); + + vm.startPrank(USER_SENDER); + + // approval + usdc.approve(_facetTestContractAddress, bridgeData.minAmount); + + vm.expectRevert(InvalidQuote.selector); + initiateBridgeTxWithFacet(false); + vm.stopPrank(); + } + + function testRevert_WhenReplayingTransactionIds() public virtual { + relayFacet.setConsumedId(validRelayData.requestId); + bridgeData.receiver = LibAsset.NON_EVM_ADDRESS; + bridgeData.destinationChainId = 1151111081099710; + validRelayData = RelayFacet.RelayData({ + requestId: bytes32("1234"), + nonEVMReceiver: bytes32( + abi.encodePacked( + "EoW7FWTdPdZKpd3WAhH98c2HMGHsdh5yhzzEtk1u68Bb" + ) + ), // DEV Wallet + receivingAssetId: bytes32( + abi.encodePacked( + "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v" + ) + ), // Solana USDC + signature: "" + }); + + vm.startPrank(USER_SENDER); + + // approval + usdc.approve(_facetTestContractAddress, bridgeData.minAmount); + + vm.expectRevert(InvalidQuote.selector); + initiateBridgeTxWithFacet(false); + vm.stopPrank(); + } + + function test_CanBridgeNativeTokensToSolana() + public + virtual + assertBalanceChange( + address(0), + USER_SENDER, + -int256((defaultNativeAmount + addToMessageValue)) + ) + assertBalanceChange(address(0), USER_RECEIVER, 0) + assertBalanceChange(ADDRESS_USDC, USER_SENDER, 0) + assertBalanceChange(ADDRESS_DAI, USER_SENDER, 0) + { + bridgeData.receiver = LibAsset.NON_EVM_ADDRESS; + bridgeData.destinationChainId = 1151111081099710; + validRelayData = RelayFacet.RelayData({ + requestId: bytes32("1234"), + nonEVMReceiver: bytes32( + abi.encodePacked( + "EoW7FWTdPdZKpd3WAhH98c2HMGHsdh5yhzzEtk1u68Bb" + ) + ), // DEV Wallet + receivingAssetId: bytes32( + abi.encodePacked( + "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v" + ) + ), // Solana USDC + signature: "" + }); + + vm.startPrank(USER_SENDER); + + // customize bridgeData + bridgeData.sendingAssetId = address(0); + bridgeData.minAmount = defaultNativeAmount; + + //prepare check for events + vm.expectEmit(true, true, true, true, _facetTestContractAddress); + emit LiFiTransferStarted(bridgeData); + + initiateBridgeTxWithFacet(true); + vm.stopPrank(); + } + + function test_CanSwapAndBridgeTokensToSolana() + public + virtual + assertBalanceChange( + ADDRESS_DAI, + USER_SENDER, + -int256(swapData[0].fromAmount) + ) + assertBalanceChange(ADDRESS_DAI, USER_RECEIVER, 0) + assertBalanceChange(ADDRESS_USDC, USER_SENDER, 0) + assertBalanceChange(ADDRESS_USDC, USER_RECEIVER, 0) + { + bridgeData.receiver = LibAsset.NON_EVM_ADDRESS; + bridgeData.destinationChainId = 1151111081099710; + validRelayData = RelayFacet.RelayData({ + requestId: bytes32("1234"), + nonEVMReceiver: bytes32( + abi.encodePacked( + "EoW7FWTdPdZKpd3WAhH98c2HMGHsdh5yhzzEtk1u68Bb" + ) + ), // DEV Wallet + receivingAssetId: bytes32( + abi.encodePacked( + "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v" + ) + ), // Solana USDC + signature: "" + }); + + vm.startPrank(USER_SENDER); + + // prepare bridgeData + bridgeData.hasSourceSwaps = true; + + // reset swap data + setDefaultSwapDataSingleDAItoUSDC(); + + // approval + dai.approve(_facetTestContractAddress, swapData[0].fromAmount); + + //prepare check for events + vm.expectEmit(true, true, true, true, _facetTestContractAddress); + emit AssetSwapped( + bridgeData.transactionId, + ADDRESS_UNISWAP, + ADDRESS_DAI, + ADDRESS_USDC, + swapData[0].fromAmount, + bridgeData.minAmount, + block.timestamp + ); + + vm.expectEmit(true, true, true, true, _facetTestContractAddress); + emit LiFiTransferStarted(bridgeData); + + // execute call in child contract + initiateSwapAndBridgeTxWithFacet(false); + } + + function test_CanSwapAndBridgeNativeTokensToSolana() + public + virtual + assertBalanceChange(ADDRESS_DAI, USER_RECEIVER, 0) + assertBalanceChange(ADDRESS_USDC, USER_RECEIVER, 0) + { + bridgeData.receiver = LibAsset.NON_EVM_ADDRESS; + bridgeData.destinationChainId = 1151111081099710; + validRelayData = RelayFacet.RelayData({ + requestId: bytes32("1234"), + nonEVMReceiver: bytes32( + abi.encodePacked( + "EoW7FWTdPdZKpd3WAhH98c2HMGHsdh5yhzzEtk1u68Bb" + ) + ), // DEV Wallet + receivingAssetId: bytes32( + abi.encodePacked( + "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v" + ) + ), // Solana USDC + signature: "" + }); + + vm.startPrank(USER_SENDER); + // store initial balances + uint256 initialUSDCBalance = usdc.balanceOf(USER_SENDER); + + // prepare bridgeData + bridgeData.hasSourceSwaps = true; + bridgeData.sendingAssetId = address(0); + + // prepare swap data + address[] memory path = new address[](2); + path[0] = ADDRESS_USDC; + path[1] = ADDRESS_WRAPPED_NATIVE; + + uint256 amountOut = defaultNativeAmount; + + // Calculate USDC input amount + uint256[] memory amounts = uniswap.getAmountsIn(amountOut, path); + uint256 amountIn = amounts[0]; + + bridgeData.minAmount = amountOut; + + delete swapData; + swapData.push( + LibSwap.SwapData({ + callTo: address(uniswap), + approveTo: address(uniswap), + sendingAssetId: ADDRESS_USDC, + receivingAssetId: address(0), + fromAmount: amountIn, + callData: abi.encodeWithSelector( + uniswap.swapTokensForExactETH.selector, + amountOut, + amountIn, + path, + _facetTestContractAddress, + block.timestamp + 20 minutes + ), + requiresDeposit: true + }) + ); + + // approval + usdc.approve(_facetTestContractAddress, amountIn); + + //prepare check for events + vm.expectEmit(true, true, true, true, _facetTestContractAddress); + emit AssetSwapped( + bridgeData.transactionId, + ADDRESS_UNISWAP, + ADDRESS_USDC, + address(0), + swapData[0].fromAmount, + bridgeData.minAmount, + block.timestamp + ); + + //@dev the bridged amount will be higher than bridgeData.minAmount since the code will + // deposit all remaining ETH to the bridge. We cannot access that value (minAmount + remaining gas) + // therefore the test is designed to only check if an event was emitted but not match the parameters + vm.expectEmit(false, false, false, false, _facetTestContractAddress); + emit LiFiTransferStarted(bridgeData); + + // execute call in child contract + initiateSwapAndBridgeTxWithFacet(false); + + // check balances after call + assertEq( + usdc.balanceOf(USER_SENDER), + initialUSDCBalance - swapData[0].fromAmount + ); + } + + function test_CanBridgeTokensToBitcoin() + public + virtual + assertBalanceChange( + ADDRESS_USDC, + USER_SENDER, + -int256(defaultUSDCAmount) + ) + assertBalanceChange(ADDRESS_USDC, USER_RECEIVER, 0) + assertBalanceChange(ADDRESS_DAI, USER_SENDER, 0) + assertBalanceChange(ADDRESS_DAI, USER_RECEIVER, 0) + { + bridgeData.receiver = LibAsset.NON_EVM_ADDRESS; + bridgeData.destinationChainId = 20000000000001; + validRelayData = RelayFacet.RelayData({ + requestId: bytes32("1234"), + nonEVMReceiver: bytes32( + abi.encodePacked("bc1q6l08rtj6j907r2een0jqs6l7qnruwyxfshmf8a") + ), // DEV Wallet + receivingAssetId: bytes32( + abi.encodePacked("bc1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqmql8k8") + ), // Solana USDC + signature: "" + }); + + vm.startPrank(USER_SENDER); + + // approval + usdc.approve(_facetTestContractAddress, bridgeData.minAmount); + + //prepare check for events + vm.expectEmit(true, true, true, true, _facetTestContractAddress); + emit LiFiTransferStarted(bridgeData); + + initiateBridgeTxWithFacet(false); + vm.stopPrank(); + } + + function test_CanBridgeNativeTokensToBitcoin() + public + virtual + assertBalanceChange( + address(0), + USER_SENDER, + -int256((defaultNativeAmount + addToMessageValue)) + ) + assertBalanceChange(address(0), USER_RECEIVER, 0) + assertBalanceChange(ADDRESS_USDC, USER_SENDER, 0) + assertBalanceChange(ADDRESS_DAI, USER_SENDER, 0) + { + bridgeData.receiver = LibAsset.NON_EVM_ADDRESS; + bridgeData.destinationChainId = 20000000000001; + validRelayData = RelayFacet.RelayData({ + requestId: bytes32("1234"), + nonEVMReceiver: bytes32( + abi.encodePacked("bc1q6l08rtj6j907r2een0jqs6l7qnruwyxfshmf8a") + ), // DEV Wallet + receivingAssetId: bytes32( + abi.encodePacked("bc1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqmql8k8") + ), // Solana USDC + signature: "" + }); + + vm.startPrank(USER_SENDER); + + // customize bridgeData + bridgeData.sendingAssetId = address(0); + bridgeData.minAmount = defaultNativeAmount; + + //prepare check for events + vm.expectEmit(true, true, true, true, _facetTestContractAddress); + emit LiFiTransferStarted(bridgeData); + + initiateBridgeTxWithFacet(true); + vm.stopPrank(); + } + + function test_CanSwapAndBridgeTokensToBitcoin() + public + virtual + assertBalanceChange( + ADDRESS_DAI, + USER_SENDER, + -int256(swapData[0].fromAmount) + ) + assertBalanceChange(ADDRESS_DAI, USER_RECEIVER, 0) + assertBalanceChange(ADDRESS_USDC, USER_SENDER, 0) + assertBalanceChange(ADDRESS_USDC, USER_RECEIVER, 0) + { + bridgeData.receiver = LibAsset.NON_EVM_ADDRESS; + bridgeData.destinationChainId = 20000000000001; + validRelayData = RelayFacet.RelayData({ + requestId: bytes32("1234"), + nonEVMReceiver: bytes32( + abi.encodePacked("bc1q6l08rtj6j907r2een0jqs6l7qnruwyxfshmf8a") + ), // DEV Wallet + receivingAssetId: bytes32( + abi.encodePacked("bc1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqmql8k8") + ), // Solana USDC + signature: "" + }); + + vm.startPrank(USER_SENDER); + + // prepare bridgeData + bridgeData.hasSourceSwaps = true; + + // reset swap data + setDefaultSwapDataSingleDAItoUSDC(); + + // approval + dai.approve(_facetTestContractAddress, swapData[0].fromAmount); + + //prepare check for events + vm.expectEmit(true, true, true, true, _facetTestContractAddress); + emit AssetSwapped( + bridgeData.transactionId, + ADDRESS_UNISWAP, + ADDRESS_DAI, + ADDRESS_USDC, + swapData[0].fromAmount, + bridgeData.minAmount, + block.timestamp + ); + + vm.expectEmit(true, true, true, true, _facetTestContractAddress); + emit LiFiTransferStarted(bridgeData); + + // execute call in child contract + initiateSwapAndBridgeTxWithFacet(false); + vm.stopPrank(); + } + + function test_CanSwapAndBridgeNativeTokensToBitcoin() + public + virtual + assertBalanceChange(ADDRESS_DAI, USER_RECEIVER, 0) + assertBalanceChange(ADDRESS_USDC, USER_RECEIVER, 0) + { + bridgeData.receiver = LibAsset.NON_EVM_ADDRESS; + bridgeData.destinationChainId = 20000000000001; + validRelayData = RelayFacet.RelayData({ + requestId: bytes32("1234"), + nonEVMReceiver: bytes32( + abi.encodePacked("bc1q6l08rtj6j907r2een0jqs6l7qnruwyxfshmf8a") + ), // DEV Wallet + receivingAssetId: bytes32( + abi.encodePacked("bc1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqmql8k8") + ), // Solana USDC + signature: "" + }); + + vm.startPrank(USER_SENDER); + // store initial balances + uint256 initialUSDCBalance = usdc.balanceOf(USER_SENDER); + + // prepare bridgeData + bridgeData.hasSourceSwaps = true; + bridgeData.sendingAssetId = address(0); + + // prepare swap data + address[] memory path = new address[](2); + path[0] = ADDRESS_USDC; + path[1] = ADDRESS_WRAPPED_NATIVE; + + uint256 amountOut = defaultNativeAmount; + + // Calculate USDC input amount + uint256[] memory amounts = uniswap.getAmountsIn(amountOut, path); + uint256 amountIn = amounts[0]; + + bridgeData.minAmount = amountOut; + + delete swapData; + swapData.push( + LibSwap.SwapData({ + callTo: address(uniswap), + approveTo: address(uniswap), + sendingAssetId: ADDRESS_USDC, + receivingAssetId: address(0), + fromAmount: amountIn, + callData: abi.encodeWithSelector( + uniswap.swapTokensForExactETH.selector, + amountOut, + amountIn, + path, + _facetTestContractAddress, + block.timestamp + 20 minutes + ), + requiresDeposit: true + }) + ); + + // approval + usdc.approve(_facetTestContractAddress, amountIn); + + //prepare check for events + vm.expectEmit(true, true, true, true, _facetTestContractAddress); + emit AssetSwapped( + bridgeData.transactionId, + ADDRESS_UNISWAP, + ADDRESS_USDC, + address(0), + swapData[0].fromAmount, + bridgeData.minAmount, + block.timestamp + ); + + //@dev the bridged amount will be higher than bridgeData.minAmount since the code will + // deposit all remaining ETH to the bridge. We cannot access that value (minAmount + remaining gas) + // therefore the test is designed to only check if an event was emitted but not match the parameters + vm.expectEmit(false, false, false, false, _facetTestContractAddress); + emit LiFiTransferStarted(bridgeData); + + // execute call in child contract + initiateSwapAndBridgeTxWithFacet(false); + + // check balances after call + assertEq( + usdc.balanceOf(USER_SENDER), + initialUSDCBalance - swapData[0].fromAmount + ); + vm.stopPrank(); + } + + function testFail_RevertIsBubbledWhenBridgingTokensFails() + public + virtual + assertBalanceChange( + ADDRESS_USDC, + USER_SENDER, + -int256(defaultUSDCAmount) + ) + assertBalanceChange(ADDRESS_USDC, USER_RECEIVER, 0) + assertBalanceChange(ADDRESS_DAI, USER_SENDER, 0) + assertBalanceChange(ADDRESS_DAI, USER_RECEIVER, 0) + { + vm.startPrank(USER_SENDER); + + // approval + usdc.approve(_facetTestContractAddress, bridgeData.minAmount); + + vm.mockCallRevert( + ADDRESS_USDC, + abi.encodeWithSignature( + "transfer(address,uint256)", + RELAY_SOLVER, + bridgeData.minAmount + ), + "I always revert" + ); + + vm.expectRevert("I always revert"); + initiateBridgeTxWithFacet(false); + vm.stopPrank(); + } + + function testFail_RevertIsBubbledWhenBridgingNativeTokensFails() + public + virtual + assertBalanceChange( + address(0), + USER_SENDER, + -int256((defaultNativeAmount + addToMessageValue)) + ) + assertBalanceChange(address(0), USER_RECEIVER, 0) + assertBalanceChange(ADDRESS_USDC, USER_SENDER, 0) + assertBalanceChange(ADDRESS_DAI, USER_SENDER, 0) + { + vm.startPrank(USER_SENDER); + + // customize bridgeData + bridgeData.sendingAssetId = address(0); + bridgeData.minAmount = defaultNativeAmount; + + _makeRevertable(RELAY_RECEIVER); + + vm.expectRevert("I always revert"); + initiateBridgeTxWithFacet(true); + vm.stopPrank(); + } + + function test_mapsCorrectChainId(uint256 chainId) public { + uint256 mapped = relayFacet.getMappedChainId(chainId); + // Bitcoin + if (chainId == 20000000000001) { + assertEq(mapped, 8253038); + return; + } + + // Solana + if (chainId == 1151111081099710) { + assertEq(mapped, 792703809); + return; + } + + assertEq(mapped, chainId); + } + + function signData( + ILiFi.BridgeData memory _bridgeData, + RelayFacet.RelayData memory _relayData + ) internal view returns (bytes memory) { + bytes32 message = keccak256( + abi.encodePacked( + "\x19Ethereum Signed Message:\n32", + keccak256( + abi.encodePacked( + _relayData.requestId, + block.chainid, + bytes32(uint256(uint160(address(relayFacet)))), + bytes32(uint256(uint160(_bridgeData.sendingAssetId))), + _getMappedChainId(_bridgeData.destinationChainId), + _bridgeData.receiver == LibAsset.NON_EVM_ADDRESS + ? _relayData.nonEVMReceiver + : bytes32(uint256(uint160(_bridgeData.receiver))), + _relayData.receivingAssetId + ) + ) + ) + ); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(PRIVATE_KEY, message); + bytes memory signature = abi.encodePacked(r, s, v); + return signature; + } + + function _getMappedChainId( + uint256 chainId + ) internal pure returns (uint256) { + if (chainId == 20000000000001) { + return 8253038; + } + + if (chainId == 1151111081099710) { + return 792703809; + } + + return chainId; + } + + function _makeRevertable(address target) internal { + Reverter reverter = new Reverter(); + bytes memory code = address(reverter).code; + vm.etch(target, code); + } +} diff --git a/yarn.lock b/yarn.lock index 668ed8ad6..ad11c3f94 100644 --- a/yarn.lock +++ b/yarn.lock @@ -62,6 +62,13 @@ dependencies: regenerator-runtime "^0.14.0" +"@babel/runtime@^7.25.0": + version "7.26.0" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.26.0.tgz#8600c2f595f277c60815256418b85356a65173c1" + integrity sha512-FDSOghenHTiToteC/QRlv2q3DhPZ/oOXTBoirfWNx1Cx3TMVcGWQtMMmQcSvb/JjpNeGzx8Pq/b4fKEJuWm1sw== + dependencies: + regenerator-runtime "^0.14.0" + "@balena/dockerignore@^1.0.2": version "1.0.2" resolved "https://registry.yarnpkg.com/@balena/dockerignore/-/dockerignore-1.0.2.tgz#9ffe4726915251e8eb69f44ef3547e0da2c03e0d" @@ -1644,6 +1651,27 @@ rpc-websockets "^7.11.1" superstruct "^1.0.4" +"@solana/web3.js@^1.95.4": + version "1.95.4" + resolved "https://registry.yarnpkg.com/@solana/web3.js/-/web3.js-1.95.4.tgz#771603f60d75cf7556ad867e1fd2efae32f9ad09" + integrity sha512-sdewnNEA42ZSMxqkzdwEWi6fDgzwtJHaQa5ndUGEJYtoOnM6X5cvPmjoTUp7/k7bRrVAxfBgDnvQQHD6yhlLYw== + dependencies: + "@babel/runtime" "^7.25.0" + "@noble/curves" "^1.4.2" + "@noble/hashes" "^1.4.0" + "@solana/buffer-layout" "^4.0.1" + agentkeepalive "^4.5.0" + bigint-buffer "^1.1.5" + bn.js "^5.2.1" + borsh "^0.7.0" + bs58 "^4.0.1" + buffer "6.0.3" + fast-stable-stringify "^1.0.0" + jayson "^4.1.1" + node-fetch "^2.7.0" + rpc-websockets "^9.0.2" + superstruct "^2.0.2" + "@solidity-parser/parser@^0.14.0", "@solidity-parser/parser@^0.14.1", "@solidity-parser/parser@^0.14.5": version "0.14.5" resolved "https://registry.yarnpkg.com/@solidity-parser/parser/-/parser-0.14.5.tgz#87bc3cc7b068e08195c219c91cd8ddff5ef1a804"