# Swaps and Limit Orders
# Atomic swaps
Atomic swaps let you safely and cheaply swap funds with an existing zkSync account.
There are 3 steps required to successfully make a swap:
- Sign an order that confirms that you want perform a certain swap
- Acquire a signed order of the same format from the account that you want to swap with
- Submit both orders with a fee to the zkSync server
# Signing an order
To sign an order, you need the following info:
- token you want to swap
- token you want to swap for
- amount of the token that you want to swap
- ratio of the swapped tokens, relevant to one another
Ratios are 15-byte integers that represent the proportion in which tokens are swapped.
To sign an order, use the getOrder
method of Wallet
:
const order = await wallet.getOrder({
tokenSell: 'ETH',
tokenBuy: 'USDT',
amount: tokenSet.parseToken('ETH', '2.5'),
ratio: utils.tokenRatio({
ETH: 1,
USDT: '4234.5'
})
});
An order can also include:
recipient
- an address of an existing account, to which the result of a swap should be transferred, in case you want to perform a swap-and-transfer. Defaults to selfvalidFrom
andvalidUntil
Unix timestamps which limit a timespan where a block with a swap can be processednonce
that is going to be used for the swap
# Submitting a swap
Anyone can submit 2 orders for a swap if they meet the following limitations:
- orders have matching tokens: if
orderA
specifiestokenA -> tokenB
, thenorderB
should specifytokenB -> tokenA
- ratios in orders are compatible:
1/orderB.ratio <= orderA.amount/orderB.amount <= orderA.ratio
- if orders have recipients, their accounts already exist in zkSync
Fee is paid by the submitter, and the token it is paid in should be specified. After a swap is executed, nonce is incremented on both swapping accounts and the submitter. If swap was submitted from one of the swapping accounts, nonce is incremented only once.
If the user wishes to cancel the swap that has not yet been submitted, they simply have to increment their nonce (e.g. send a zero-transfer).
To submit a swap, use the syncSwap
method of Wallet
:
const swap = await wallet.syncSwap({
orders: [orderA, orderB],
feeToken: 'wBTC'
});
# Limit orders
Limit orders provide a way to exchange a certain token for another at a certain price. They are designed to be used primarily by other platforms that want to provide trustless and scalable exchange services.
The differences between an atomic swap and a limit order are:
- limit orders infer the amount that can be exchanged directly from the balance
- limit orders can be partially filled
- limit orders do not increment account's nonce when partially filled
This means that once a limit order is signed for a certain token, the whole balance can potentially be
exchanged for another token (at a specified ratio). There is no way to limit the amount to be exchanged other than use a special trading account. :::
# Trading accounts
A trading account is an ordinary account that can be used to sign a limit order. It's function is to limit the amount of a certain token that a user wants to exchange.
To do this, user has to:
- Transfer the desired amount of a desired token to a new account.
- Set a signing key for the account.
- Sign a limit order.
This way the limit order will exchange at most the amount you transferred to the trading account. Remaining balance on the main account will be left untouched.
# Signing limit orders
To sign a limit order, use the getLimitOrder
method of Wallet
:
const order = await wallet.getLimitOrder({
tokenSell: 'ETH',
tokenBuy: 'USDT',
ratio: utils.tokenRatio({
ETH: 1,
USDT: 3900
})
});
# Filling a limit order
Limit order itself represents only a half of the swap operation. In order to be filled, the following criteria must be met:
- There exists a counterpart order (a normal order or a limit order) that fits the original order's tokens and buy/sell ratio.
- There exists someone willing to combine both orders into a swap operation and submit it.
Amounts that are being filled should be specified in the swap operation. Limit orders can be partially filled, so amounts can be different from actual balances, although must be compatible with the ratios specified in the orders. For details, see example.
const swap = await wallet.syncSwap({
orders: [orderA, orderB],
amounts: [tokenSet.parseToken('ETH', '2'), tokenSet.parseToken('UDST', '7800')],
feeToken: 'wBTC'
});
# Note on limit order matching
Performing atomic swaps is as simple as sharing a signed order message with a party that you want to swap with.
On the other hand, collecting and matching limit orders should probably be performed by platform(s) with an already existing userbase by some kind of matching engine. zkSync tries to be as generic as possible, so creating matching engines is not considered - we will only provide an L2 framework that other platforms can integrate with.
# Suggestions
Trading accounts can be created as CREATE2 accounts. This approach has the following benefits:
- Setting a signing key on a CREATE2 account is cheaper
- Salt argument in CREATE2 can be used to deterministically generate trading account addresses for a certain main account
- Same L2 private key can be used for all trading accounts and the main account if desired. Although this bears some risks (compromising a single account would mean compromising all of them), key management can be inconvenient in some situations.
Should a platform decide to use CREATE2 for trading accounts, it will have to choose a contract bytecode to be used for address calculation. The contract should be open-source and have full exit and withdrawal functionality since in the rare case of censorship users will have to deploy it to rescue their funds.
It is also suggested to reuse trading accounts on which orders were filled or cancelled since this way a signing key would not have to be set again.
# Example
This section provides an example of how ratios specified in orders affect account balances.
Let the swap consist of 2 limit orders:
// walletA wants to swap wBTC for ETH at a ratio 2:5
const orderA = await walletA.getLimitOrder({
tokenSell: 'wBTC',
tokenBuy: 'ETH',
ratio: utils.tokenRatio({
wBTC: 2,
ETH: 5
})
});
// walletB wants to swap ETH for wBTC at a ratio 4:1
const orderB = await walletB.getLimitOrder({
tokenSell: 'ETH',
tokenBuy: 'wBTC',
ratio: utils.tokenRatio({
wBTC: 1,
ETH: 4
})
});
Specified ratios mean that:
walletA
expects to get 2.5 ETH for each wBTC (or more)walletB
expects to get 0.25 wBTC for each ETH (or more)
Ratios are compatible, because at either ratio (or in between), both parties will be happy:
- at
orderB
's ratiowalletA
will get 4 ETH per wBTC, which is more than expected - at
orderA
's ratiowalletB
will get 0.4 wBTC per ETH, which is more than expected
Now let's actually submit the swap, and pick a ratio in between - 3 ETH per wBTC:
const swap = await walletC.syncSwap({
orders: [orderA, orderB],
amounts: [tokenSet.parseToken('wBTC', '100'), tokenSet.parseToken('ETH', '300')],
feeToken: 'USDT'
});
This will exchange 100 wBTC from walletA
for 300 ETH from walletB
. For detailed information, see table below.
walletA | walletB | |
---|---|---|
wBTC before swap | 100 | 0 |
ETH before swap | 0 | 300 |
Expected wBTC after swap | 0 | 75 |
Expected ETH after swap | 250 | 0 |
Actual wBTC after swap | 0 | 100 |
Actual ETH after swap | 300 | 0 |
# Utils
To construct a ratio, use either of the two utility functions:
tokenRatio
constructs a ratio relevant to the tokens themselves, so{ ETH: 4, wBTC: 1 }
would mean you want 4 ETH for each wBTC.weiRatio
constructs a ratio relevant to the lowest denomination of the token, so{ ETH: 4, wBTC: 1 }
would mean you want 4 wei (10-18 ETH) for each satoshi (10-8 wBTC).
If tokens symbols or IDs are contained in variables, use the following syntax to pass them into ratio objects:
utils.tokenRatio({
[tokenA]: valueA,
[tokenB]: valueB
});
For more information, visit the API reference.