Architecture

The contract code and architecture is discussed briefly to give an overview of the smart contracts. However, the contracts should only be used through Rango's API and contract details that are not discussed here.

Overview

Rango v2 contracts are designed to handle swaps and bridge transactions and also message passing transactions.

There are two main type of contracts:

  • Diamond : Handles swap and bridge transactions (RangoDiamond.sol)

  • Middlewares : Receives token and message on destination chain and handles the transaction

RangoDiamond is based on EIP-2535 and v3 implementation. The functionality and support for each bridge is handled by a separate facet. All swapping protocols are handled by a single facet. For example:

Example scenario: Swap 100 USDT on chain A to DAI on chain B

Example scenario: Swap 100 USDT on chain A to BUSD on chain B through a Middleware contract

Example scenario: Swap 100 USDT on chain A to BUSD on chain B through a Middleware contract and call a third-party dApp contract.

Bridges Facet Code Structure

Bridge Facets follow a pattern of having two external functions which are the main entry points:

  • One function for direct bridging:

    • Stargate: function StargateBridge(...) external

    • Wormhole: function wormholeBridge(...) external

  • Another function for swap and bridge:

    • Stargate: function StargateSwapAndBridge(...) external

    • Wormhole: function wormholeSwapAndBridge(...) external

To handle the interaction with the bridge, each bridge has one or more internal functions where the name is like doBridge:

  • Stargate: function doStargateSwap(...) internal

  • Symbiosis: function doSymbiosisBridge(...) internal

In summary, the contract code architecture looks like this:

contract RangoSatelliteFacet {

    // entrypoint external function to swap and then bridge 
    function satelliteSwapAndBridge(
        LibSwapper.SwapRequest memory request,
        LibSwapper.Call[] calldata calls,
        IRangoSatellite.SatelliteBridgeRequest memory bridgeRequest
    ) external {
        ...
        LibSwapper.onChainSwapsPreBridge(...) // swapping 
        ...
        doSatelliteBridge(...); // interact with the bridge 
    }

    // entrypoint external function to bridge
    function satelliteBridge(
        SatelliteBridgeRequest memory request,
        RangoBridgeRequest memory bridgeRequest
    ) external {
        ...
        doSatelliteBridge(request, token, amount);// interact with the bridge 
    }

    // internal function where interaction logic with the bridge happens
    function doSatelliteBridge(
        SatelliteBridgeRequest memory request,
        address token,
        uint256 amount
    ) internal {
        ...
        // interact with bridge
        IAxelarGateway(s.gatewayAddress).callContractWithToken(
        request.toChain,
        request.receiver,
        payload,
    request.symbol,
    amount);
    }
}

Swap Code Structure

The swap functionality for each bridge is very similar and the core implementation is handled by LibSwapper.sol and RangoSwapperFacet.sol

To handle swap logic, we use two structs SwapRequest and Call. A swap transaction has one SwapRequest and one or more swap Call:

The SwapRequest defines which tokens is to be swapped and which token should be received and some helper data:

struct SwapRequest {
    address requestId;
    address fromToken;
    address toToken;
    uint amountIn;
    uint platformFee;
    uint destinationExecutorFee;
    uint affiliateFee;
    address payable affiliatorAddress;
    uint minimumAmountExpected;
    uint16 dAppTag;
}

The Call defines the contract that should be given approval (spender) and the contract to be called (target) and the calldata to be sent to target contract (callData). Note that Call also defines swapFromToken and swapToToken which might be different from SwapRequest. This is because a swap can have multiple Calls and the SwapRequest needs to know initial and final token, but each Call might have its own tokens.

struct Call {
    address spender;
    address payable target;
    address swapFromToken;
    address swapToToken;
    bool needsTransferFromUser;
    uint amount;
    bytes callData;
}

An example of helper function that uses SwapRequest and Calls in bridges is provided here:

function onChainSwapsPreBridge(
    SwapRequest memory request,
    Call[] calldata calls,
    uint extraFee
) internal;

Interchain Messages Code Structure

In the case of sending a message across chains, we use a standard struct called RangoInterChainMessage. This object is used when a swap or contract call is needed on the destination chain. The message is received in the destination chain and is handled by the Middleware Contract specific to that bridge.

struct RangoInterChainMessage {
    address requestId;
    uint64 dstChainId;
    address bridgeRealOutput;
    address toToken;
    address originalSender;
    address recipient;
    ActionType actionType;
    bytes action;
    CallSubActionType postAction;
    uint16 dAppTag;

    // Extra message
    bytes dAppMessage;
    address dAppSourceContract;
    address dAppDestContract;
}

Middleware Contracts Code Structure

Each bridge has its own messaging model if it supports message passing. Therefore, there is no standard or pattern for interchain message calls for different bridges. But to keep the code clean, we have defined a parent contract(RangoBaseInterchainMiddleware.sol) which all middlewares can inherit from in order to have functionalities such as whitelists and refunding.

Last updated