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.
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:
- the user calls the
withdraw
function in the contractA
; he's tokens locks on bridge contract - this contract emits an
Transfer
event - relay find new event, prepare a MPC signature with other relays and call
submit
function on contractB
(another network) - contract
B
saves transfers in locked state forlockTime
seconds - if no one dispute locked transfer during this time, contract
B
mint synthetic analog of primary token to user
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.
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 ETHEth_EthBridge
- deployed on ETH, receive transfers from AMBBsc_AmbBridge
- deployed on AMB, receive transfers from BSCBsc_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
andwrapWithdraw
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.
- The user goes to the frontend
- The front-end takes a list of all tokens, their icons, etc.
- The front-end makes queries to relay fee api to get transfer fee and bridge fee
- the user calls
withdraw(tokenAddress, toAddress, amount, {value: fee})
the bridge contract in the network from which he wants to withdraw tokens
withdraw(address tokenThisAddress, address toAddress, uint amount, bool unwrapSide, bytes calldata feeSignature, uint transferFee, uint bridgeFee)
-
inputs are checked
-
the output information is added to the queue
Transfer[] withdraw_queue
struct Transfer { address tokenAddress; address toAddress; uint amount; }
-
as long as the
withdraw
calls occur in one timeframe - transfers are simply added to the queue.
As soon as the nextwithdraw
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
- emit
- Relay get event
Transfer(event_id, withdraw_queue)
from AmbBridge - Checks that
event_id == EthBridge.inputEventId + 1
, otherwise look for Transfer with a matching event_id - Waits for N next blocks (safety blocks)
- Build the
submit
transaction with data from Transfer event - Sign transaction with MPC in consensus with other relays
- Master relay send signed transaction to blockchain
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 aminimum 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.
- We transfer 1M AMB from AMB-NET to ETH. In this case, user will pay (all values only for example):
- 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.
- 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. - Total commissions which user will pay = 135.13 + 1351.3 + 20000 = 21486,43 AMB
- We transfer 10k AMB from AMB-NET to ETH
- 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.
- 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 - Total commissions which user will pay = 135.13 + 1351.3 + 675.67 = 2162.1 AMB
- We transfer 1 ETH from ETH-net to AMB-NET
- 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.
- 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. - Total commissions which user will pay = 0,0083 + 0,00083 + 0.02 = 0,02913 ETH
tokenAddress
- string, hex address of token in from networkisAmb
- bool, is the from network is AMBamount
- string, amount of tokens in hexisAmountWithFees
- bool, is the amount includes fees (used with "set max" button on the frontend)
bridgeFee
- string, bridge fee in hextransferFee
- string, transfer fee in hexamount
- string, used amount in calculation fees (useful when paramisAmountWithFees
has used)signature
- string, signature of the data
Request body:
- URL: http://localhost:8080/fees
- 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 andamount
is too small):- Status code: 500
- Result:
{ "message": "amount is too small" }