Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(ronin): Add RoninBridgeFacet 1.0.0 #498

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions config/ronin.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"mainnet": {
"gateway": "0x64192819Ac13Ef72bF6b5AE239AC672B43a9AF08"
},
"ronin": {
"gateway": "0x0CF8fF40a508bdBc39fBe1Bb679dCBa64E65C7Df"
}
}
1 change: 1 addition & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
- [Optimism Bridge Facet](./OptimismBridgeFacet.md)
- [Periphery Registry Facet](./PeripheryRegistryFacet.md)
- [Polygon Bridge Facet](./PolygonBridgeFacet.md)
- [Ronin Bridge Facet](./RoninBridgeFacet.md)
- [Standardized Call Facet](./StandardizedCallFacet.md)
- [Stargate Facet](./StargateFacet.md)
- [Synapse Bridge Facet](./SynapseBridgeFacet.md)
Expand Down
81 changes: 81 additions & 0 deletions docs/RoninBridgeFacet.md
Original file line number Diff line number Diff line change
@@ -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}'
```
127 changes: 127 additions & 0 deletions script/demoScripts/demoRoninBridge.ts
Original file line number Diff line number Diff line change
@@ -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(<string>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: <string>dexSwapData.to,
approveTo: <string>dexSwapData.to,
sendingAssetId: DAI_ADDRESS,
receivingAssetId: USDC_ADDRESS,
fromAmount: amountIn,
callData: <string>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)
})
34 changes: 34 additions & 0 deletions script/deploy/facets/DeployRoninBridgeFacet.s.sol
Original file line number Diff line number Diff line change
@@ -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);
}
}
13 changes: 13 additions & 0 deletions script/deploy/facets/UpdateRoninBridgeFacet.s.sol
Original file line number Diff line number Diff line change
@@ -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");
}
}
9 changes: 9 additions & 0 deletions script/deploy/resources/deployRequirements.json
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,15 @@
}
}
},
"RoninBridgeFacet": {
"configData": {
"_gateway": {
"configFileName": "ronin.json",
"keyInConfigFile": ".<NETWORK>.gateway",
"allowToDeployWithZeroAddress": "false"
}
}
},
"StargateFacet": {
"configData": {
"_router": {
Expand Down
107 changes: 107 additions & 0 deletions src/Facets/RoninBridgeFacet.sol
Original file line number Diff line number Diff line change
@@ -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);
}
}
Loading
Loading