From 14fa76ad538b308ae45ab7be2e2767532530eac1 Mon Sep 17 00:00:00 2001 From: Ed Zynda Date: Thu, 3 Aug 2023 19:32:18 +0700 Subject: [PATCH] Add RoninBridgeFacet --- config/ronin.json | 8 ++ docs/README.md | 1 + docs/RoninBridgeFacet.md | 81 +++++++++++ script/demoScripts/demoRoninBridge.ts | 127 ++++++++++++++++++ .../facets/DeployRoninBridgeFacet.s.sol | 34 +++++ .../facets/UpdateRoninBridgeFacet.s.sol | 13 ++ .../deploy/resources/deployRequirements.json | 9 ++ src/Facets/RoninBridgeFacet.sol | 107 +++++++++++++++ src/Interfaces/IRoninBridgeGateway.sol | 33 +++++ test/solidity/Facets/RoninBridgeFacet.t.sol | 97 +++++++++++++ 10 files changed, 510 insertions(+) create mode 100644 config/ronin.json create mode 100644 docs/RoninBridgeFacet.md create mode 100644 script/demoScripts/demoRoninBridge.ts create mode 100644 script/deploy/facets/DeployRoninBridgeFacet.s.sol create mode 100644 script/deploy/facets/UpdateRoninBridgeFacet.s.sol create mode 100644 src/Facets/RoninBridgeFacet.sol create mode 100644 src/Interfaces/IRoninBridgeGateway.sol create mode 100644 test/solidity/Facets/RoninBridgeFacet.t.sol diff --git a/config/ronin.json b/config/ronin.json new file mode 100644 index 000000000..6d0d19fe8 --- /dev/null +++ b/config/ronin.json @@ -0,0 +1,8 @@ +{ + "mainnet": { + "gateway": "0x64192819Ac13Ef72bF6b5AE239AC672B43a9AF08" + }, + "ronin": { + "gateway": "0x0CF8fF40a508bdBc39fBe1Bb679dCBa64E65C7Df" + } +} diff --git a/docs/README.md b/docs/README.md index d26436128..924c58892 100644 --- a/docs/README.md +++ b/docs/README.md @@ -27,6 +27,7 @@ - [Optimism Bridge Facet](./OptimismBridgeFacet.md) - [Periphery Registry Facet](./PeripheryRegistryFacet.md) - [Polygon Bridge Facet](./PolygonBridgeFacet.md) +- [Ronin Bridge Facet](./RoninBridgeFacet.md) - [Stargate Facet](./StargateFacet.md) - [Synapse Bridge Facet](./SynapseBridgeFacet.md) - [Withdraw Facet](./WithdrawFacet.md) diff --git a/docs/RoninBridgeFacet.md b/docs/RoninBridgeFacet.md new file mode 100644 index 000000000..4722867c1 --- /dev/null +++ b/docs/RoninBridgeFacet.md @@ -0,0 +1,81 @@ +# Ronin Bridge Facet + +## How it works + +The Ronin Bridge Facet works by forwarding Ronin Bridge specific calls to Bridge gateway [contract](https://github.com/axieinfinity/ronin-dpos-contracts/blob/main/contracts/mainchain/MainchainGatewayV2.sol). All user funds are fully backed 1:1 by the Ronin bridge. + +```mermaid +graph LR; + D{LiFiDiamond}-- DELEGATECALL -->RoninBridgeFacet; + RoninBridgeFacet -- CALL --> M(MainchainGateway) +``` + +## Public Methods + +- `function startBridgeTokensViaRoninBridge(BridgeData memory _bridgeData)` + - Simply bridges tokens using Ronin Bridge +- `function swapAndStartBridgeTokensViaRoninBridge(BridgeData memory _bridgeData, SwapData[] calldata _swapData)` + - Performs swap(s) before bridging tokens using Ronin Bridge + +## 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.finance/reference/get_quote-1) 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: 'ronin', // 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 of how to use the /quote endpoint and how to trigger the transaction can be found [here](https://apidocs.li.finance/reference/how-to-transfer-tokens). + +**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 20 USDC on Ethereum to USDC on Ronin you can execute the following request: + +```shell +curl 'https://li.quest/v1/quote?fromChain=ETH&fromAmount=20000000&fromToken=USDC&toChain=RON&toToken=USDC&slippage=0.03&allowBridges=ronin&fromAddress={YOUR_WALLET_ADDRESS}' +``` + +### Swap & Cross + +To get a transaction for a transfer from 10 USDT on Ethereum to USDC on Ronin you can execute the following request: + +```shell +curl 'https://li.quest/v1/quote?fromChain=ETH&fromAmount=10000000000000000000&fromToken=USDT&toChain=RON&toToken=USDC&slippage=0.03&allowBridges=ronin&fromAddress={YOUR_WALLET_ADDRESS}' +``` diff --git a/script/demoScripts/demoRoninBridge.ts b/script/demoScripts/demoRoninBridge.ts new file mode 100644 index 000000000..48cde38cc --- /dev/null +++ b/script/demoScripts/demoRoninBridge.ts @@ -0,0 +1,127 @@ +import { providers, Wallet, utils, constants, Contract } from 'ethers' +import { RoninBridgeFacet__factory, ERC20__factory } from '../typechain' +import { node_url } from '../../utils/network' +import chalk from 'chalk' + +const msg = (msg: string) => { + console.log(chalk.green(msg)) +} + +const LIFI_ADDRESS = '0x1D7554F2EF87Faf41f9c678cF2501497D38c014f' +const DAI_ADDRESS = '0x6B175474E89094C44Da98b954EedeAC495271d0F' +const USDC_ADDRESS = '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48' +const UNISWAP_ADDRESS = '0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D' +const ZERO_ADDRESS = constants.AddressZero +const destinationChainId = 2020 + +const amountIn = utils.parseUnits('5', 18) +const amountOut = utils.parseUnits('4', 6) + +async function main() { + const jsonProvider = new providers.JsonRpcProvider(node_url('mainnet')) + const provider = new providers.FallbackProvider([jsonProvider]) + + let wallet = Wallet.fromMnemonic(process.env.MNEMONIC) + wallet = wallet.connect(provider) + const walletAddress = await wallet.getAddress() + + const lifi = RoninBridgeFacet__factory.connect(LIFI_ADDRESS, wallet) + + // Swap and Bridge Non-Native Asset + { + const path = [DAI_ADDRESS, USDC_ADDRESS] + const to = LIFI_ADDRESS // should be a checksummed recipient address + const deadline = Math.floor(Date.now() / 1000) + 60 * 20 // 20 minutes from the current Unix time + + const uniswap = new Contract( + UNISWAP_ADDRESS, + [ + 'function swapTokensForExactTokens(uint amountOut, uint amountInMax, address[] calldata path, address to, uint deadline) external payable returns (uint[] memory amounts)', + ], + wallet + ) + + // Generate swap calldata + + const dexSwapData = + await uniswap.populateTransaction.swapTokensForExactTokens( + amountOut, + amountIn, + path, + to, + deadline + ) + const swapData = [ + { + callTo: dexSwapData.to, + approveTo: dexSwapData.to, + sendingAssetId: DAI_ADDRESS, + receivingAssetId: USDC_ADDRESS, + fromAmount: amountIn, + callData: dexSwapData?.data, + requiresDeposit: true, + }, + ] + + const bridgeData = { + transactionId: utils.randomBytes(32), + bridge: 'ronin', + integrator: 'ACME Devs', + referrer: ZERO_ADDRESS, + sendingAssetId: USDC_ADDRESS, + receiver: walletAddress, + minAmount: amountOut, + destinationChainId: destinationChainId, + hasSourceSwaps: true, + hasDestinationCall: false, + } + + // Approve ERC20 for swapping -- DAI -> USDC + const dai = ERC20__factory.connect(DAI_ADDRESS, wallet) + const allowance = await dai.allowance(walletAddress, LIFI_ADDRESS) + if (amountIn.gt(allowance)) { + await dai.approve(LIFI_ADDRESS, amountIn) + + msg('Token approved for swapping') + } + + // Call LiFi smart contract to start the bridge process -- WITH SWAP + await lifi.swapAndStartBridgeTokensViaRoninBridge(bridgeData, swapData, { + gasLimit: 500000, + }) + } + + // Bridge Native Asset + { + const amount = utils.parseEther('0.01') + const bridgeData = { + transactionId: utils.randomBytes(32), + bridge: 'ronin', + integrator: 'ACME Devs', + referrer: ZERO_ADDRESS, + sendingAssetId: ZERO_ADDRESS, + receiver: walletAddress, + minAmount: amount, + destinationChainId: destinationChainId, + hasSourceSwaps: false, + hasDestinationCall: false, + } + + // Call LiFi smart contract to start the bridge process + await lifi.startBridgeTokensViaRoninBridge(bridgeData, { + value: amount, + gasLimit: 500000, + }) + } +} + +main() + .then(() => { + console.log('Success') + process.exit(0) + }) + .catch((error) => { + console.error('error') + console.error(error) + process.exit(1) + }) diff --git a/script/deploy/facets/DeployRoninBridgeFacet.s.sol b/script/deploy/facets/DeployRoninBridgeFacet.s.sol new file mode 100644 index 000000000..a80cf30ff --- /dev/null +++ b/script/deploy/facets/DeployRoninBridgeFacet.s.sol @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.17; + +import { DeployScriptBase } from "./utils/DeployScriptBase.sol"; +import { stdJson } from "forge-std/Script.sol"; +import { RoninBridgeFacet } from "lifi/Facets/RoninBridgeFacet.sol"; + +contract DeployScript is DeployScriptBase { + using stdJson for string; + + constructor() DeployScriptBase("RoninBridgeFacet") {} + + function run() + public + returns (RoninBridgeFacet deployed, bytes memory constructorArgs) + { + constructorArgs = getConstructorArgs(); + + deployed = RoninBridgeFacet( + deploy(type(RoninBridgeFacet).creationCode) + ); + } + + function getConstructorArgs() internal override returns (bytes memory) { + string memory path = string.concat(root, "/config/ronin.json"); + string memory json = vm.readFile(path); + + address gateway = json.readAddress( + string.concat(".", network, ".gateway") + ); + + return abi.encode(gateway); + } +} diff --git a/script/deploy/facets/UpdateRoninBridgeFacet.s.sol b/script/deploy/facets/UpdateRoninBridgeFacet.s.sol new file mode 100644 index 000000000..2941b5706 --- /dev/null +++ b/script/deploy/facets/UpdateRoninBridgeFacet.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("RoninBridgeFacet"); + } +} diff --git a/script/deploy/resources/deployRequirements.json b/script/deploy/resources/deployRequirements.json index 5b393b150..58192260c 100644 --- a/script/deploy/resources/deployRequirements.json +++ b/script/deploy/resources/deployRequirements.json @@ -290,6 +290,15 @@ } } }, + "RoninBridgeFacet": { + "configData": { + "_gateway": { + "configFileName": "ronin.json", + "keyInConfigFile": "..gateway", + "allowToDeployWithZeroAddress": "false" + } + } + }, "StargateFacet": { "configData": { "_router": { diff --git a/src/Facets/RoninBridgeFacet.sol b/src/Facets/RoninBridgeFacet.sol new file mode 100644 index 000000000..0f509e9ab --- /dev/null +++ b/src/Facets/RoninBridgeFacet.sol @@ -0,0 +1,107 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.17; + +import { ILiFi } from "../Interfaces/ILiFi.sol"; +import { IRoninBridgeGateway } from "../Interfaces/IRoninBridgeGateway.sol"; +import { LibAsset, IERC20 } from "../Libraries/LibAsset.sol"; +import { ReentrancyGuard } from "../Helpers/ReentrancyGuard.sol"; +import { SwapperV2, LibSwap } from "../Helpers/SwapperV2.sol"; +import { Validatable } from "../Helpers/Validatable.sol"; + +/// @title Ronin Bridge Facet +/// @author Li.Finance (https://li.finance) +/// @notice Provides functionality for bridging through Ronin Bridge +/// @custom:version 1.0.0 +contract RoninBridgeFacet is ILiFi, ReentrancyGuard, SwapperV2, Validatable { + /// Storage /// + + /// @notice The contract address of the gateway on the source chain. + IRoninBridgeGateway private immutable gateway; + + /// Constructor /// + + /// @notice Initialize the contract. + /// @param _gateway The contract address of the gateway on the source chain. + constructor(IRoninBridgeGateway _gateway) { + gateway = _gateway; + } + + /// External Methods /// + + /// @notice Bridges tokens via Ronin Bridge + /// @param _bridgeData Data containing core information for bridging + function startBridgeTokensViaRoninBridge( + ILiFi.BridgeData memory _bridgeData + ) + external + payable + nonReentrant + refundExcessNative(payable(msg.sender)) + doesNotContainSourceSwaps(_bridgeData) + doesNotContainDestinationCalls(_bridgeData) + validateBridgeData(_bridgeData) + { + LibAsset.depositAsset( + _bridgeData.sendingAssetId, + _bridgeData.minAmount + ); + _startBridge(_bridgeData); + } + + /// @notice Performs a swap before bridging via Ronin Bridge + /// @param _bridgeData Data containing core information for bridging + /// @param _swapData An array of swap related data for performing swaps before bridging + function swapAndStartBridgeTokensViaRoninBridge( + ILiFi.BridgeData memory _bridgeData, + LibSwap.SwapData[] calldata _swapData + ) + external + payable + nonReentrant + refundExcessNative(payable(msg.sender)) + containsSourceSwaps(_bridgeData) + doesNotContainDestinationCalls(_bridgeData) + validateBridgeData(_bridgeData) + { + _bridgeData.minAmount = _depositAndSwap( + _bridgeData.transactionId, + _bridgeData.minAmount, + _swapData, + payable(msg.sender) + ); + _startBridge(_bridgeData); + } + + /// Private Methods /// + + /// @dev Contains the business logic for the bridge via Ronin Bridge + /// @param _bridgeData Data containing core information for bridging + function _startBridge(ILiFi.BridgeData memory _bridgeData) private { + IRoninBridgeGateway.Request memory request = IRoninBridgeGateway + .Request( + _bridgeData.receiver, + _bridgeData.sendingAssetId, + IRoninBridgeGateway.Info( + IRoninBridgeGateway.Standard.ERC20, + 0, + _bridgeData.minAmount + ) + ); + + uint256 nativeAssetAmount; + + if (LibAsset.isNativeAsset(_bridgeData.sendingAssetId)) { + nativeAssetAmount = _bridgeData.minAmount; + } else { + LibAsset.maxApproveERC20( + IERC20(_bridgeData.sendingAssetId), + address(gateway), + _bridgeData.minAmount + ); + } + + gateway.requestDepositFor{ value: nativeAssetAmount }(request); + + emit LiFiTransferStarted(_bridgeData); + } +} diff --git a/src/Interfaces/IRoninBridgeGateway.sol b/src/Interfaces/IRoninBridgeGateway.sol new file mode 100644 index 000000000..5726a6260 --- /dev/null +++ b/src/Interfaces/IRoninBridgeGateway.sol @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.17; + +interface IRoninBridgeGateway { + enum Standard { + ERC20, + ERC721 + } + + /// @dev For ERC20: the id must be 0 and the quantity is larger than 0. + /// For ERC721: the quantity must be 0. + /// @param erc The standard of asset to bridge. + /// @param id The id of asset if it's ERC721. + /// @param quantity The amount of asset if it's ERC20. + struct Info { + Standard erc; + uint256 id; + uint256 quantity; + } + + /// @param recipientAddr Recipient address on Ronin network. + /// @param tokenAddr Token address to bridge. + /// @param info Details of token to bridge. + struct Request { + address recipientAddr; + address tokenAddr; + Info info; + } + + /// @notice Locks the assets and request deposit. + /// @param _request Details of request. + function requestDepositFor(Request calldata _request) external payable; +} diff --git a/test/solidity/Facets/RoninBridgeFacet.t.sol b/test/solidity/Facets/RoninBridgeFacet.t.sol new file mode 100644 index 000000000..0a20c54e4 --- /dev/null +++ b/test/solidity/Facets/RoninBridgeFacet.t.sol @@ -0,0 +1,97 @@ +// SPDX-License-Identifier: Unlicense +pragma solidity 0.8.17; + +import { LibAllowList, TestBaseFacet } from "../utils/TestBaseFacet.sol"; +import { RoninBridgeFacet } from "lifi/Facets/RoninBridgeFacet.sol"; +import { IRoninBridgeGateway } from "lifi/Interfaces/IRoninBridgeGateway.sol"; + +// Stub RoninBridgeFacet Contract +contract TestRoninBridgeFacet is RoninBridgeFacet { + constructor(IRoninBridgeGateway _gateway) RoninBridgeFacet(_gateway) {} + + function addDex(address _dex) external { + LibAllowList.addAllowedContract(_dex); + } + + function setFunctionApprovalBySignature(bytes4 _signature) external { + LibAllowList.addAllowedSelector(_signature); + } +} + +contract RoninBridgeFacetTest is TestBaseFacet { + // These values are for Mainnet + address internal constant MAINCHAIN_GATEWAY = + 0x64192819Ac13Ef72bF6b5AE239AC672B43a9AF08; + + // ----- + + TestRoninBridgeFacet internal roninBridgeFacet; + + function setUp() public { + // set custom block number for forking + customBlockNumberForForking = 16705000; + + initTestBase(); + + roninBridgeFacet = new TestRoninBridgeFacet( + IRoninBridgeGateway(MAINCHAIN_GATEWAY) + ); + + bytes4[] memory functionSelectors = new bytes4[](4); + functionSelectors[0] = roninBridgeFacet + .startBridgeTokensViaRoninBridge + .selector; + functionSelectors[1] = roninBridgeFacet + .swapAndStartBridgeTokensViaRoninBridge + .selector; + functionSelectors[2] = roninBridgeFacet.addDex.selector; + functionSelectors[3] = roninBridgeFacet + .setFunctionApprovalBySignature + .selector; + + addFacet(diamond, address(roninBridgeFacet), functionSelectors); + + roninBridgeFacet = TestRoninBridgeFacet(address(diamond)); + + roninBridgeFacet.addDex(address(uniswap)); + roninBridgeFacet.setFunctionApprovalBySignature( + uniswap.swapExactTokensForTokens.selector + ); + roninBridgeFacet.setFunctionApprovalBySignature( + uniswap.swapTokensForExactETH.selector + ); + + setFacetAddressInTestBase( + address(roninBridgeFacet), + "RoninBridgeFacet" + ); + + bridgeData.destinationChainId = 2020; + bridgeData.bridge = "ronin"; + } + + function initiateBridgeTxWithFacet(bool isNative) internal override { + if (isNative) { + roninBridgeFacet.startBridgeTokensViaRoninBridge{ + value: bridgeData.minAmount + }(bridgeData); + } else { + roninBridgeFacet.startBridgeTokensViaRoninBridge(bridgeData); + } + } + + function initiateSwapAndBridgeTxWithFacet( + bool isNative + ) internal override { + if (isNative) { + roninBridgeFacet.swapAndStartBridgeTokensViaRoninBridge{ + value: swapData[0].fromAmount + }(bridgeData, swapData); + } else { + roninBridgeFacet.swapAndStartBridgeTokensViaRoninBridge( + bridgeData, + swapData + ); + } + } +}