- rfq-aleo-dex
- Build, deploy and test instructions
- Program specification
Arcane.finance is a decentralized exchange on Aleo that offers two major benefits:
- Private swaps. Users swap encrypted
Records
for encryptedRecords
. - Zero slippage, no front-running. Quotes are cryptographically signed so users trade with 100% price certainty.
Program id: rfq_v000003.aleo
Deployment tx id: at1vr7mt5706yntaf8xgyscer4f7v5fps34nstptum4pmks9g29ssgspxf8hx
Live demo is available at https://app.arcane.finance/
Use Private faucet to get some test tokens (1000 tokens max per transaction) and Swap interface to trade them.
Please note that sometimes fetching private records takes time. If it takes longer then 3-5 minutes, please try refreshing a page and/or re-syncing Leo wallet (see "Advanced settings" tab).
Constant function Automated Market Makers (AMMs), such as Uniswap or Curve, are perhaps the most integral part of the traditional DeFi ecosystem.
While having many advantages, AMMs on Aleo cannot fully utilize the core privacy-enabling primitives of Aleo: off chain transition
functions and encrypted Records
. Because an AMM needs to know up-to-date pool reserves, most of its code has to be placed in a finalize
function, which is executed on chain and cannot operate on Records
. This can fixed by relaxing constraints on price function invariant, but this introduce new challenges e.g. with correct handling of slippage.
For the Aleo ecosystem to grow and be competitive, its decentralized exchanges need to leverage unique privacy-preserving features of Aleo and off-chain computation. This is why we are building a DEX on an entirely different model called "Request-For-Quote" (RFQ).
Request-for-quote (RFQ) is a form of P2P swaps. Our protocol offers users OTC (over-the-counter) desk experience, but automated, private and cryptographically signed.
Here’s traditional how OTC trades work:
- You ask the seller: “Hey, I want to trade my 10 BTC for USDT.”
- She responds with an offer: “260,079 USDT. Take it or leave it.”
- If you like the offer, you execute the trade.
Now imagine that you receive cryptographically signed quotes from several sellers, automatically pick the best deal and can execute the trade immediately if you like it. And all this while remaining private. Sounds good? This is exactly how our RFQ DEX works.
In a nutshell, an AMM pricing function x*y=k
is now replaced with a cryptographically signed, private quote, which is verified off chain, in a transition
. The pricing is done off chain but the trade is executed on chain.
Some examples of DeFi projects that use RFQ model are: Hashflow, Orbiter Finance, Hop, Airswap
While this project is an early stage Proof-of-Concept, it already shows that Aleo enables building privacy-preserving DeFi protocols that have real competitive advantages over existing solutions.
There are other considerations behind our choice of RFQ model on Aleo:
- Institutional Adoption
RFQ model is rooted in traditional financial markets. The adoption of RFQ mechanics can attract to Aleo more sophisticated, institutional participants, with deep liquidity and diverse assets.
- Expansion to Other Asset Classes
RFQ-based execution engines are extremely flexible and can easily be expanded to accommodate derivatives and other financial instruments.
- Transformation to Dark Pools
Our long term vision includes an extension to RFQ model that would allow for completely trustless, but fully regulated dark pools. This will provide additional liquidity and anonymity for trading large blocks of securities without incurring market impact costs.
Create a .env
file with your private key and fee record:
YOUR_ADDRESS=aleo1...
PRIVATE_KEY=APrivateKey1...
RECORD="{
owner: aleo1....private,
microcredits: ...u64.private,
_nonce: ...group.public
}"
Run source .env
before each command.
To deploy the program run:
snarkos developer deploy "rfq_v000003.aleo" --private-key "${PRIVATE_KEY}" --query "https://vm.aleo.org/api" --path "./build/" --broadcast "https://vm.aleo.org/api/testnet3/transaction/broadcast" --fee 1000 --record "${RECORD}"
To initialize demo tokens run with ids 1,2,3 and 4:
snarkos developer execute "rfq_v000003.aleo" "init_demo_tokens" "1field" --private-key "${PRIVATE_KEY}" --query "https://vm.aleo.org/api" --broadcast "https://vm.aleo.org/api/testnet3/transaction/broadcast" --fee 1000 --record "${RECORD}"
Test tokens correspond to following real assets:
Token ID | Token |
---|---|
1 | Tether USD (USDT) |
2 | Cicle USD (USDC) |
3 | Bitcoin (BTC) |
4 | Ethereum (ETH) |
To simplify testing, all test tokens have 6 decimals.
To initalize demoe a market maker run:
snarkos developer execute "rfq_v000003.aleo" "init_demo_market_maker" "1field" --private-key "${PRIVATE_KEY}" --query "https://vm.aleo.org/api" --broadcast "https://vm.aleo.org/api/testnet3/transaction/broadcast" --fee 1000 --record "${RECORD}"
To mint test tokens with id 1 (USDT) run:
snarkos developer execute "rfq_v000003.aleo" "mint_private" "${YOUR_ADDRESS}" "1u64" "1000000000u128" --private-key "${PRIVATE_KEY}" --query "https://vm.aleo.org/api" --broadcast "https://vm.aleo.org/api/testnet3/transaction/broadcast" --fee 1000 --record "${RECORD}"
To execute a swap run:
snarkos developer execute "rfq_v000003.aleo" "quote_swap" "{TOKEN}" "{QUOTE}" "{SIGNATURE}" --private-key "${PRIVATE_KEY}" --query "https://vm.aleo.org/api" --broadcast "https://vm.aleo.org/api/testnet3/transaction/broadcast" --fee 1000 --record "${RECORD}"
where:
{TOKEN}
is a record returned bymint_private
{QUOTE}
and{SIGNATURE}
can be obtained by sending a GET query request to our market maker with the following format:
https://ftoy1oiyo6.execute-api.us-east-1.amazonaws.com/default/leoswap-maker?amount_in=<AMOUNT>&token_in=<TOKEN_YOU_SELL>&token_out=<TOKEN_YOU_BUY>
where
<AMOUNT>
should be the amount you want to sell (all test tokens are fixed point integer with 6 decimals)<TOKEN_YOU_SELL>
the id of the token you sell, i.e. the id of the{TOKEN}
<TOKEN_YOU_BUY>
the id of the token you want to buy.
For example, to get a quote for a swap of 1000 USDT for ETH, request:
https://ftoy1oiyo6.execute-api.us-east-1.amazonaws.com/default/leoswap-maker?amount_in=1000000000&token_in=1&token_out=4
It will return a json with a quote and a signature:
{
"signature": "{challenge:134761902584291722445245311261060722474143214429316094887383208318845166464scalar,response:1174910261812754497995625895072005505083850262557116981170653056543943978848scalar,pk_sig:3281664209406841603541028760915163024036467969430202533099724532705308889105group,pr_sig:5539506505093814818821647255067173289296108627180178059588713937273990243414group,sk_prf:51394841380773997286630059211554203011051867539942123398656527573489016493scalar}",
"quote": "{amount_in:1000000000u128,amount_out:863132u128,token_in:1u64,token_out:4u64,maker_address:aleo1r3qlsxnuux6rkrhk24rktdtzu7kjr3c2fw5fvtp6a9dwghe0xgzs9c2nhu,nonce:264473671193890215536616807758839040807335648385338432938227630636884179618field,valid_until:999999u32}"
}
This means you will get 0.863132 ETH for 1000 USDT.
This section provides a detailed description of all elements of the main Aleo program.
Token
record stores tradeable, fungible tokens.
record Token {
owner: address,
amount: u128,
token_id: u64,
}
Record fields:
Name | Type | Description |
---|---|---|
owner |
address |
token owner's address |
amount |
u128 |
amount stored in a records |
token_id |
u64 |
a unique id to distinguish between different kinds of token |
Our program operates on test tokens that are defined in the same program because Aleo currently doesn't have a common token standard and primitives required to build one are missing (e.g. self.parent
proposed here https://github.com/AleoHQ/ARCs/tree/master/arc-0030). Our team is working on relevant ARCs.
TokenInfo
defines the basic properties of a token.
struct TokenInfo {
token_id: u64,
max_supply: u128,
decimals: u8,
}
Struct fields:
Name | Type | Description |
---|---|---|
token_id |
address |
a unique token identifier |
max_supply |
u128 |
supply cap |
decimals |
u8 |
fixed point decimals in this token's amounts |
Quote
contains the maker's quote information. A Quote
with a valid Signature
is a commitment of a maker (seller) to enter a swap.
struct Quote {
amount_in: u128,
amount_out: u128,
token_in: u64,
token_out: u64,
maker_address: address,
nonce: field,
valid_until: u32,
}
Struct fields:
Name | Type | Description |
---|---|---|
amount_in |
u128 |
amount of tokens a user wants to swap |
amount_out |
u128 |
amount of tokens a maker offers for amount_in |
token_in |
u64 |
id of the token users wants to sell |
token_out |
u64 |
id of the token users wants to buy |
maker_address |
address |
address of maker (seller) |
nonce |
field |
random/semi-random number that a maker can use just once |
valid_until |
u32 |
block number the quote is valid until |
- Quote is private for a user (buyer): it does not include any user information.
- Quote is pseudo private for a maker (seller): a maker can use several unrelated values for
maker_address
nonce
is used to prevent replay attacks, i.e. using the same quote in more than one trade.valid_until
limits the commitment of a maker to enter a swap up until a certain block. More accurate time constraints will be introduced as Leofinalize
functions will be able to access current time information.
Signature
is a Schnorr signature that is used to verify a quote.
struct Signature {
challenge: scalar,
response: scalar,
pk_sig: group,
pr_sig: group,
sk_prf: scalar,
}
Struct fields:
Signature
has the same fields as https://github.com/AleoHQ/snarkVM/blob/f2eda3eb2f2b3d469d67df8b45019b59904be10e/console/account/src/signature/mod.rs#L33. pk_sig
, pr_sig
and sk_prf
represent Compute Key, see https://github.com/AleoHQ/snarkVM/blob/f2eda3eb2f2b3d469d67df8b45019b59904be10e/circuit/account/src/compute_key/mod.rs#L25.
Leo will soon have a native signature verification op code, see PR ProvableHQ/leo#2519. It will replace our custom signature implementation when released.
Deposit
is a struct that packs maker's address and token id, so that they can be hashed together to form a unique deposit id.
struct Deposit {
maker_address: address,
token_id: u64,
}
Struct fields:
Name | Type | Description |
---|---|---|
maker_address |
address |
address of maker (seller) |
token_id |
u64 |
id of the token that maker deposits to trade |
This section describes the public, on chain state of the main program.
registered_tokens
tracks which internal tokens have been created.
mapping registered_tokens: u64 => TokenInfo;
Key | Value |
---|---|
Id of a token | TokenInfo that describes the token |
maker_balance
keeps track of reserves of makers. These reserves guarantee execution of valid quotes.
mapping maker_balances: field => u128;
Key | Value |
---|---|
hash(maker's address) hash(token_id) | amount of token reserves |
A key in maker_balance
is a sum of hashes of the maker's address and an id of a token reserved. This allows a maker to create several pseudo-anonymous reserve buckets using different addresses.
executed_quotes
is used to prevent using the same Quote
more than once.
mapping executed_quotes: field => u32;
Key | Value |
---|---|
hash(Quote ) |
block in which Quote was executed |
A maker should use a different nonce
in every new quote, regardless of whether it has been executed or not. For example, it can be achieved by picking random or monotonically increasing nonces.
Below is the description of functions and transitions related to RFQ swaps.
A maker executes add_liquidity
to provide token reserves that will be used to execute swaps she quoted.
transition add_liquidity(t: Token, maker_address: address, amount: u128) -> Token
Parameters:
Parameter | Description |
---|---|
t: Token |
token record |
maker_address: address |
signing address of a maker |
amount: u128 |
amount of liquidity to reserve |
Returns: a "change" Token
if the amount of liquidity to reserve is less than the amount stored in the Token
record t
.
A maker can use different signing addresses to provide liquidity for the same token. This allows her to have "buckets" of liquidity that do not reveal information on which token she is going to trade, only the amount, i.e. finalize
for this transition reveals neither maker's address nor token id:
finalize add_liquidity(deposit_id: field, amount: u128)
Finalize only makes public the amount
and deposit_id
, which is calculated off chain as:
let deposit_id: field = Poseidon8::hash_to_field(
Deposit {
maker_address,
token_id: t.token_id
}
);
and thus conceal token_id
because maker_address
can by any address controlled by the maker.
This means that before the maker signs and sends the first quote using this maker_address
, it is impossible to know that a certain amount of a particular token has been deposited to the DEX.
remove_liquidity
withdraws maker's reservers from the exchange. Must be called using the same address that was passed as a parameter while depositing tokens using add_liquidity
transition remove_liquidity(token_id: u64, amount: u128) -> Token
Parameters:
Parameter | Description |
---|---|
token_id: u64 |
token to be withdrawn |
amount: u128 |
amount to be withdrawn |
Returns: a private Token
record if there are enough tokens to withdraw.
Similar to add_liquidity
, remove_liquidity
by itself does not make public which token is withdrawn and which address is the owner.
Note that due to the private nature of swaps, a maker can always withdraw her deposit via executing a swap against her own quote. The remove_liquidity
transition is just a more convenient way to do it.
quote_swap
is the transition that performs swap of a given Token
record t
, by verifying a signature s
of a maker's quote q
.
transition quote_swap(
t: Token,
q: Quote,
s: Signature
) -> (Token, Token, Token)
Parameters:
Parameter | Description |
---|---|
t: Token |
token record to be swapped |
q: Quote |
maker's quote |
s: Signature |
signature to verify quote q |
The transition returns three token records:
- record with tokens bought by a user (owner: user)
- record with the "change" remaining from
t
(owner: user) - record with tokens sold by a user (owner: maker)
The finalize
function makes public only the following fields:
finalize quote_swap(
withdraw_from: field, // deposit id
quote_hash: field, // hash of a quote
amount_out: u128, // amount of tokens bought
valid_until: u32 // expiry block of a quote
)
The following information remains entirely private:
- What token has a user sold
- How many tokens has a user sold
- At what price was the trade executed
This function uses a modified Schnorr signature algorithm from snarkVM to validate a Signature
of a quote.
function verify_signature(q: Quote, s: Signature) -> bool
Parameter | Description |
---|---|
q: Quote |
a signed quote |
s: Signature |
a signature to verify |
Returns: true
if a signature is valid and maker_address
in a quote is the signer, else returns false
.
The signing and verification algorithms are modified versions of the algorithms found in snarVM (see, https://github.com/AleoHQ/snarkVM/blob/f2eda3eb2f2b3d469d67df8b45019b59904be10e/console/account/src/signature/verify.rs). There are two major differences:
- Rather than using
g_scalar_multiply
with 251 generators, we had to use only the first generator ofGENERATOR_G
. Initially, we used all generators, but the size of a proving key forquote_swap
was 4.6GB (see program with id leoswapxyz_v000004.aleo). But because WASM32 only supports 4GB, no wallet could execute this transition. - Because there are no arrays, we had to sum hashes of individual values instead of hashing
(r * G, pk_sig, pr_sig, address, message)
at once. This can decrease cryptographic security of the signature. A better choice would be to pack these values in astruct
and hash it in a single operation, but we decided to leave it as is and wait until proper hash verification is introduced to Leo.
get_deposit_id
is a helper function that hashes the maker's address and token id to get a deposit bucket id.
function get_deposit_id(maker_address: address, token_id: u64) -> field
Parameters:
Parameter | Description |
---|---|
maker_address: address |
maker's address |
token_id: u64 |
id of a token to be deposited |
Returns a field element that uniquely identifies a deposit.
init_demo_market_maker
is an auxiliary configuration function that simplifies testing by providing 1M test token liquidity for each demo tokens with ids 1,2,3 and 4 for a test market maker with an address aleo1r3qlsxnuux6rkrhk24rktdtzu7kjr3c2fw5fvtp6a9dwghe0xgzs9c2nhu
.
transition init_demo_market_maker(dummy: field) -> field
Parameters:
Parameter | Description |
---|---|
dummy: field |
any value, see comment below |
Returns a dummy
value plus one.
This transition does not requireany input parameters or return values. Dummy parameter and output value are used only as a workaround for https://github.com/AleoHQ/snarkOS/issues/2510
Our program uses internal tokens.For the sake of brevity, it implements only the required minimum of token-related functions.
create_token
transition registers a new internal token.
transition create_token(token_id: u64, decimals: u8, max_supply: u128)
Parameters:
Parameter | Description |
---|---|
token_id: u64 |
id of a new token |
decimals: u8 |
the number of decimal digits |
max_supply: u128 |
maximum supply |
Result: a new token is registered and the registered_tokens
map is updated.
mint_private
transition performs a private mint
transition mint_private(
receiver: address,
token_id: u64,
amount: u128,
) -> Token
Parameters:
Parameter | Description |
---|---|
receiver: address |
owner of the minted tokens |
token_id: u64 |
token to mint |
amount: u128 |
amount to mint |
Result: a new Token
record is created with the specified amount and an owner set to be the receiver
Each call of mint_private
can mint at most 1000 tokens (assuming 6 decimals) to prevent depletion of demo maker resources.
init_demo_tokens
is an auxiliary configuration function that simplifies testing. Running init_demo_tokens
configures 4 test tokens with 6 decimals in one transaction.
transition init_demo_tokens(dummy: field) -> field
Parameters:
Parameter | Description |
---|---|
dummy: field |
any value, see comment below |
Result: a dummy
value plus one
Similar to init_demo_market_maker
, dummy parameters are used only as a workaround.