Skip to content

ambrosus/ambrosus-bridge

Repository files navigation

README

Tokens

Inside the bridge there is a division into types of tokens: primary and synthetic.

Primary tokens are those that already exist in a network and we plan to make it possible to transport them by bridge.

Synthetics tokens are the ones we create as a pair to primary ones.
They know about the existence of the bridge and its address and allow it to mint and burn tokens.
They also monitor how much and from which network funds came and do not allow to withdraw to this network more than came from it.

Bridge

The bridge consists of 2 contracts, one for each network and a relay - software that monitors these contracts and sends them user transfers.

in a nutshell, the bridge works as follows:

  1. the user calls the withdraw function in the contract A; he's tokens locks on bridge contract
  2. this contract emits an Transfer event
  3. relay find new event, prepare a MPC signature with other relays and call submit function on contract B (another network)
  4. contract B saves transfers in locked state for lockTime seconds
  5. if no one dispute locked transfer during this time, contract B mint synthetic analog of primary token to user

MPC

MPC - Multi-Party Computation - is a cryptographic protocol that allows several parties to jointly compute a function over their inputs while keeping them private.
In our case, we use MPC to sign submit transaction with the public key trusted by the contract.
However, the private key never exists anywhere in memory, and is instead fragmented for each of the several relays.

So, in order to submit a transfers, all relays must be in consensus about data is going to be submitted.
If one of the relays is malicious, it will not be able to submit a transaction and any try will be reported.

We are using binance tss-lib library for MPC.

Contracts

We currently have 2 networks we work with: AMB-ETH and AMB-BSC.

So, we have 4 contracts:

  • Eth_AmbBridge - deployed on AMB, receive transfers from ETH
  • Eth_EthBridge - deployed on ETH, receive transfers from AMB
  • Bsc_AmbBridge - deployed on AMB, receive transfers from BSC
  • Bsc_BscBridge - deployed on ETH, receive transfers from AMB

Each contract is inherited from CommonBridge and CheckXxXxX

CommonBridge is a contract that has all the code common to all contracts, such as:

  • withdraw and wrapWithdraw functions - called by users to transfer tokens from this network to another.
  • locking, unlocking functionality - all received transfers firstly locked for some time to avoid fraud.
  • administration - functions for changing variables, roles, etc.
  • verification and distribution of the fees
  • and so on

CheckXxXxX, which can mean CheckAura, CheckPoW, CheckPoSA, - is contracts, that verify that Transfer event happened in other (side) network.

Currently, we migrate all bridges to CheckUntrustless2 strategy, that don't check for any blockchain consensus, but instead relies on MPC signature from relays.

Flow

uml

Frontend

  1. The user goes to the frontend
  2. The front-end takes a list of all tokens, their icons, etc.
  3. The front-end makes queries to relay fee api to get transfer fee and bridge fee
  4. the user calls withdraw(tokenAddress, toAddress, amount, {value: fee}) the bridge contract in the network from which he wants to withdraw tokens

Bridge withdraw

withdraw(address tokenThisAddress, address toAddress, uint amount, bool unwrapSide, bytes calldata feeSignature, uint transferFee, uint bridgeFee)

  1. inputs are checked

  2. the output information is added to the queue Transfer[] withdraw_queue

    struct Transfer {
        address tokenAddress;
        address toAddress;
        uint amount;
    }
    
  3. as long as the withdraw calls occur in one timeframe - transfers are simply added to the queue.
    As soon as the next withdraw call occurs in a new timeframe: Transfer(event_id, withdraw_queue):

    • emit Transfer(event_id, withdraw_queue) event
    • withdraw_queue cleared
    • event_id incremented by 1

    timeframe = block.timestamp / timeframe

Relay

  1. Relay get event Transfer(event_id, withdraw_queue) from AmbBridge
  2. Checks that event_id == EthBridge.inputEventId + 1, otherwise look for Transfer with a matching event_id
  3. Waits for N next blocks (safety blocks)
  4. Build the submit transaction with data from Transfer event
  5. Sign transaction with MPC in consensus with other relays
  6. Master relay send signed transaction to blockchain

Fees

Bridge collects 2 types of commissions from the user:

  • Transfer fee - covering the cost of the relay performing transactions in side network
  • Bridge fee - percentage of the amount of tokens sent

Fees are converted into the native currency of the network

Transfer fees - are processed in the following way:

The user pays all transfer fees. When the user wants to make a transfer from the first network to the second, he will pay the transfer fee. Transfer Fee is the actual commission of the second network converted to the actual time of the first network at the time of the transaction / transfer. Example #1:

  • I as a user want to transfer AMB from AMB-NET (first network) to Ethereum (second network).
  • Assume that the current transaction value is the equivalent of $1 for AMB-NET and $20 for ETH. This means that the user has to pay $21 for the transfer fee
  • 21$ we will convert into the native coin of the first network (AMB-NET in this case) and calculate that it will be 2800 AMB
  • We charge the user transfer fee of 2800 AMB and put it on a separate wallet. Thus, we have some AMB accumulated in this wallet, but at the same time we need ETH for final transactions into ETH network.
  • For transactions in Ethereum network, we need ETH. In the beginning (when we launch our bridge in PROD), we need to allocate ETH from the project budget, but further we need a constant process of converting the received AMB as a transfer fee (2800 AMB) into ETH.
  • We will need to keep track of the balances of several wallets, to have enough AMB and ETH on them to process transactions, otherwise, the bridge will stop working. I see this process as - in 1 day/week a certain amount of AMB is accumulated, we take it away and somehow (through CEX or DEX) convert it to ETH.

In the case of transfers from ETH (first network) to AMB-NET (second network), everything is the opposite, only in this case transfer fee, we start collecting in ETH as native Ethereum network.

Bridge fee - are processed in the following way:

  • The bridge fee is the profit the gateway product charges users for using it. Bridge fee is set as a % of the total transaction volume, as well as we have a minimum value of commission below which it is forbidden to go.
  • All values, both for limits and for the minimum values of the commission are given to the $ equivalent, in order to be able to set the commission for all coins which will be supported by the bridge at the stage of launch and after it.
  • To set % of total transaction volume, set a threshold value, for example, $10000, and set % for volume up (2% for example, and we need finalize value for PROD) to $10k and separately set % for volume over $10k (1% for example, and we need finalize value for PROD). We can also use tenths of a percent, like 0.1% or 0.5%
  • We also set the minimum value, for example, the equivalent of $ 5 (this value we also need to change for PROD), and then even with the volume of transactions/transfers of $ 10, we will still charge the user a commission of the equivalent of $ 5.
  • The bridge fee is charged from the user, in the currency of the first network from which the user makes the transfer and is stored in separate wallets, not related to the transfer fee.

Fees example:

  1. We transfer 1M AMB from AMB-NET to ETH. In this case, user will pay (all values only for example):
    1. Transfer fee from AMB net (let's think about 1$) + ETH network commission (let's think about 10$). It will be 135.13AMB for AMB-NET for + 1351.3AMB for ETH net.
    2. Bridge fee: 1M AMB = 7400$, it is less than $10k and we will charge 2% from the transfer amount, but not less than 5$ equivalent. 2% = 20000AMB or 148$ and we will charge this amount of fee.
    3. Total commissions which user will pay = 135.13 + 1351.3 + 20000 = 21486,43 AMB
  2. We transfer 10k AMB from AMB-NET to ETH
    1. Transfer fee from AMB net (let's think about 1$) + ETH network commission (let's think about 10$). It will be 135.13AMB for AMB-NET for + 1351.3AMB for ETH net.
    2. Bridge fee: 10k AMB = 74$, it is less than $10k and we will charge 2% from the transfer amount, but not less than 5$ equivalent. 2% = 200AMB or 1.48$, it less than 5$, we will charge from user 5$ or 675.67 AMB
    3. Total commissions which user will pay = 135.13 + 1351.3 + 675.67 = 2162.1 AMB
  3. We transfer 1 ETH from ETH-net to AMB-NET
    1. Transfer fee from ETH-net (let's think about 10$) it is about 0,0083 ETH + AMB network commission (let's think about 1$) ~ 0,00083 ETH. It will be 0,0083 ETH for ETH-NET for + 0,00083 ETH for AMB-net.
    2. Bridge fee: 1 ETH = 1200$, it is less than $10k and we will charge 2% from the transfer amount, but not less than 5$ equivalent. 2% = 0.02 Eth or 24$, it is more than 5$, we will charge from user 0.02 Eth.
    3. Total commissions which user will pay = 0,0083 + 0,00083 + 0.02 = 0,02913 ETH

Fees API

Endpoint: /fees

Method: POST

Request body params:

  • tokenAddress - string, hex address of token in from network
  • isAmb - bool, is the from network is AMB
  • amount - string, amount of tokens in hex
  • isAmountWithFees - bool, is the amount includes fees (used with "set max" button on the frontend)

Response params:

  • bridgeFee - string, bridge fee in hex
  • transferFee - string, transfer fee in hex
  • amount - string, used amount in calculation fees (useful when param isAmountWithFees has used)
  • signature - string, signature of the data

Examples:

Request body:

{
  "tokenAddress": "0xc778417E063141139Fce010982780140Aa0cD5Ab",
  "isAmb": true,
  "amount": "0xDE0B6B3A7640000",
  "isAmountWithFees": false
}
  • Success request:

    • Status code: 200
    • Result:
    {
      "bridgeFee": "0xbc4b381d188000",
      "transferFee": "0xe8d4a51000",
      "amount": "0xDE0B6B3A7640000",
      "signature": "0x6105ca999d43b1f1182d4955f5706e8bf27097b8cb80da35c04016238e2adff91e38f275d640911a46b208d0bb7239d83d5e427b7b93b0cd7390034d724bdb0500"
    }
  • Failure request (wrong request body):

    • Status code: 400
    • Result:
    {
      "message": "error when decoding request body",
      "developerMessage": "якась помилка"
    }
  • Failure request (internal error):

    • Status code: 500
    • Result:
    {
      "message": "error when getting bridge fee",
      "developerMessage": "якась помилка"
    }
  • Failure request (when isAmountWithFees is true and amount is too small):

    • Status code: 500
    • Result:
    {
      "message": "amount is too small"
    }