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(gravity): Add GravityFacet 1.0.0 #499

Draft
wants to merge 4 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
11 changes: 11 additions & 0 deletions config/gravity.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"mainnet": {
"gravityRouter": "0xa4108aA1Ec4967F8b52220a4f7e94A8201F2D906"
},
"evmos": {
"gravityRouter": "0xa4108aA1Ec4967F8b52220a4f7e94A8201F2D906"
},
"goerli": {
"gravityRouter": "0xa4108aA1Ec4967F8b52220a4f7e94A8201F2D906"
}
}
90 changes: 90 additions & 0 deletions docs/GravityFacet.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
# Gravity Facet

## How it works

The Gravity Facet works by forwarding Gravity specific calls to the Gravity router contract which is called [Gravity.sol](https://github.com/Gravity-Bridge/Gravity-Bridge/blob/main/solidity/contracts/Gravity.sol). This contract will pull and lock tokens in the given amount and emit an event. This event will be noticed by a backend service that triggers the release on the destination chain. For more information about how Gravity bridge works please [click here](https://github.com/Gravity-Bridge/Gravity-Docs).

## Public Methods

- `function startBridgeTokensViaGravity(BridgeData memory _bridgeData, GravityData calldata _gravityData)`
- Simply bridges tokens using Gravity
- `function swapAndStartBridgeTokensViaGravity(BridgeData memory _bridgeData, SwapData[] calldata _swapData, GravityData calldata _gravityData)`
- Performs one or multiple swap(s) and bridges tokens using Gravity

## Gravity Specific Parameters

Some of the methods listed above take a variable labeled `_gravityData`.

To populate `_gravityData` you will provide the address you are bridging to in string format. String format is used since Gravity can bridge to non-EVM networks and their address format may differ from the EVM address format.

This data is specific to Gravity and is represented as the following struct type:

```solidity
/// @param destinationAddress The address of the receiver on the destination chain (in string format for non-EVM compatibility)
struct GravityData {
string destinationAddress;
}
```

## 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 variaous 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: 'gravity', // 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 20 DAI on Polygon to DAI on Fantom you can execute the following request:

```shell
curl 'https://li.quest/v1/quote?fromChain=POL&fromAmount=20000000000000000000&fromToken=POL&toChain=FTM&toToken=DAI&slippage=0.03&allowBridges=multichain&fromAddress={YOUR_WALLET_ADDRESS}'
```

### Swap & Cross

To get a transaction for a transfer from 10 MATIC on Polygon to DAI on Fantom you can execute the following request:

```shell
curl 'https://li.quest/v1/quote?fromChain=POL&fromAmount=10000000000000000000&fromToken=MATIC&toChain=FTM&toToken=DAI&slippage=0.03&allowBridges=multichain&fromAddress={YOUR_WALLET_ADDRESS}'
```
1 change: 1 addition & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
- [DiamondLoupe Facet](./DiamondLoupeFacet.md)
- [Generic Swap Facet](./GenericSwapFacet.md)
- [Gnosis Bridge Facet](./GnosisBridgeFacet.md)
- [Gravity Facet](./GravityFacet.md)
- [Hop Facet](./HopFacet.md)
- [Hop Facet Packed](./HopFacetPacked.md)
- [Hyphen Facet](./HyphenFacet.md)
Expand Down
32 changes: 32 additions & 0 deletions script/deploy/facets/DeployGravityFacet.s.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.17;

import { DeployScriptBase } from "./utils/DeployScriptBase.sol";
import { stdJson } from "forge-std/Script.sol";
import { GravityFacet } from "lifi/Facets/GravityFacet.sol";

contract DeployScript is DeployScriptBase {
using stdJson for string;

constructor() DeployScriptBase("GravityFacet") {}

function run()
public
returns (GravityFacet deployed, bytes memory constructorArgs)
{
constructorArgs = getConstructorArgs();

deployed = GravityFacet(deploy(type(GravityFacet).creationCode));
}

function getConstructorArgs() internal override returns (bytes memory) {
string memory path = string.concat(root, "/config/gravity.json");
string memory json = vm.readFile(path);

address gravity = json.readAddress(
string.concat(".", network, ".gravityRouter")
);

return abi.encode(gravity);
}
}
13 changes: 13 additions & 0 deletions script/deploy/facets/UpdateGravityFacet.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("GravityFacet");
}
}
9 changes: 9 additions & 0 deletions script/deploy/resources/deployRequirements.json
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,15 @@
}
}
},
"GravityFacet": {
"configData": {
"_router": {
"configFileName": "gravity.json",
"keyInConfigFile": ".<NETWORK>.gravityRouter",
"allowToDeployWithZeroAddress": "false"
}
}
},
"HopFacet": {
"configData": {
"_router": {
Expand Down
108 changes: 108 additions & 0 deletions src/Facets/GravityFacet.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.17;

import { ILiFi } from "../Interfaces/ILiFi.sol";
import { IGravityRouter } from "../Interfaces/IGravityRouter.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 Gravity Facet
/// @author LI.FI (https://li.fi)
/// @notice Provides functionality for bridging through Gravity
/// @custom:version 1.0.0
contract GravityFacet is ILiFi, ReentrancyGuard, SwapperV2, Validatable {
/// Storage ///

/// @notice The contract address of the router on the source chain.
IGravityRouter private immutable router;

/// Types ///

struct GravityData {
string destinationAddress;
}

/// Constructor ///

/// @notice Initialize the contract.
/// @param _router The contract address of the router on the source chain.
constructor(IGravityRouter _router) {
router = _router;
}

/// External Methods ///

/// @notice Bridges tokens via Gravity
/// @param _bridgeData the core information needed for bridging
function startBridgeTokensViaGravity(
ILiFi.BridgeData memory _bridgeData,
GravityData calldata _gravityData
)
external
payable
nonReentrant
refundExcessNative(payable(msg.sender))
doesNotContainSourceSwaps(_bridgeData)
doesNotContainDestinationCalls(_bridgeData)
validateBridgeData(_bridgeData)
noNativeAsset(_bridgeData)
{
LibAsset.depositAsset(
_bridgeData.sendingAssetId,
_bridgeData.minAmount
);
_startBridge(_bridgeData, _gravityData);
}

/// @notice Performs a swap before bridging via Gravity
/// @param _bridgeData the core information needed for bridging
/// @param _swapData an array of swap related data for performing swaps before bridging
function swapAndStartBridgeTokensViaGravity(
ILiFi.BridgeData memory _bridgeData,
LibSwap.SwapData[] calldata _swapData,
GravityData calldata _gravityData
)
external
payable
nonReentrant
refundExcessNative(payable(msg.sender))
containsSourceSwaps(_bridgeData)
doesNotContainDestinationCalls(_bridgeData)
validateBridgeData(_bridgeData)
noNativeAsset(_bridgeData)
{
_bridgeData.minAmount = _depositAndSwap(
_bridgeData.transactionId,
_bridgeData.minAmount,
_swapData,
payable(msg.sender)
);
_startBridge(_bridgeData, _gravityData);
}

/// Private Methods ///

/// @dev Contains the business logic for the bridge via Hyphen
/// @param _bridgeData the core information needed for bridging
function _startBridge(
ILiFi.BridgeData memory _bridgeData,
GravityData calldata _gravityData
) private {
// Give the Gravity router approval to bridge tokens
LibAsset.maxApproveERC20(
IERC20(_bridgeData.sendingAssetId),
address(router),
_bridgeData.minAmount
);

router.sendToCosmos(
_bridgeData.sendingAssetId,
_gravityData.destinationAddress,
_bridgeData.minAmount
);

emit LiFiTransferStarted(_bridgeData);
}
}
10 changes: 10 additions & 0 deletions src/Interfaces/IGravityRouter.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.17;

interface IGravityRouter {
function sendToCosmos(
address _tokenContract,
string calldata _destination,
uint256 _amount
) external payable;
}
105 changes: 105 additions & 0 deletions test/solidity/Facets/GravityFacet.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
// SPDX-License-Identifier: Unlicense
pragma solidity 0.8.17;

import { LibAllowList, TestBaseFacet, console } from "../utils/TestBaseFacet.sol";
import { NativeAssetNotSupported } from "src/Errors/GenericErrors.sol";
import { GravityFacet } from "lifi/Facets/GravityFacet.sol";
import { IGravityRouter } from "lifi/Interfaces/IGravityRouter.sol";

// Stub GravityFacet Contract
contract TestGravityFacet is GravityFacet {
constructor(IGravityRouter _router) GravityFacet(_router) {}

function addDex(address _dex) external {
LibAllowList.addAllowedContract(_dex);
}

function setFunctionApprovalBySignature(bytes4 _signature) external {
LibAllowList.addAllowedSelector(_signature);
}
}

contract GravityFacetTest is TestBaseFacet {
// These values are for mainnet
address internal constant GRAVITY_ROUTER =
0xa4108aA1Ec4967F8b52220a4f7e94A8201F2D906;
// -----

TestGravityFacet internal gravityFacet;
GravityFacet.GravityData internal gravityData;

function setUp() public {
initTestBase();

diamond = createDiamond();
gravityFacet = new TestGravityFacet(IGravityRouter(GRAVITY_ROUTER));

bytes4[] memory functionSelectors = new bytes4[](4);
functionSelectors[0] = gravityFacet
.startBridgeTokensViaGravity
.selector;
functionSelectors[1] = gravityFacet
.swapAndStartBridgeTokensViaGravity
.selector;
functionSelectors[2] = gravityFacet.addDex.selector;
functionSelectors[3] = gravityFacet
.setFunctionApprovalBySignature
.selector;

addFacet(diamond, address(gravityFacet), functionSelectors);

gravityFacet = TestGravityFacet(address(diamond));

gravityFacet.addDex(address(uniswap));
gravityFacet.setFunctionApprovalBySignature(
uniswap.swapExactTokensForTokens.selector
);
gravityFacet.setFunctionApprovalBySignature(
uniswap.swapTokensForExactETH.selector
);

setFacetAddressInTestBase(address(gravityFacet), "GravityFacet");

bridgeData.bridge = "gravity";

gravityData = GravityFacet.GravityData({
destinationAddress: "canto1f0cukfd8xj368prlpj6x69nyer3fcnus8wy8uf"
});
}

function initiateBridgeTxWithFacet(bool isNative) internal override {
if (isNative) {
revert NativeAssetNotSupported();
} else {
gravityFacet.startBridgeTokensViaGravity(bridgeData, gravityData);
}
}

function initiateSwapAndBridgeTxWithFacet(
bool isNative
) internal override {
if (isNative) {
revert NativeAssetNotSupported();
} else {
gravityFacet.swapAndStartBridgeTokensViaGravity(
bridgeData,
swapData,
gravityData
);
}
}

function testBase_CanBridgeNativeTokens() public override {
// facet does not support bridging of native assets
}

function testBase_CanSwapAndBridgeNativeTokens() public override {
// facet does not support bridging of native assets
}

function test_revert_BridgeNativeAsset() public {
bridgeData.sendingAssetId = address(0);
vm.expectRevert(NativeAssetNotSupported.selector);
gravityFacet.startBridgeTokensViaGravity(bridgeData, gravityData);
}
}