Home

Awesome

swapr: trustless token exchange

An exploration on how to implement an automated token exchange modeled after Uniswap in Clarity for Stacks 2.0

Of special interest, reading the x-y-k paper and V2 whitepaper would provide some background on understanding how things work (some things have been simplified, notably the initial burning of 1000 times the minimum pool share to prevent attacks right after the first addition to the liquidity pool).

Basically, when doing an exchange, the contract will maintain the invariant (amount of tokenx) * (amount of tokeny) = k, or for short x * y = k, so when exchanging dx for dy, you need to also have (x + dx) * (y - dy) = k. Of course if dx is close to x, the total liquidity available, the exchange rate will not be favorable. Liquidity providers earn a small fee (25 to 30 basis points whether the operator takes a cut of 5 basis points or not).

The API has been reimagined, and hopefully simplified to its minima, withough impeding on proper functioning of the trustless exchange.

So that you can also exchange STX with other tokens, a separate contract, wrapr, is also included, and can be used on its own. This contract will allow you to wrap STX into a fungible token, in a fashion similar to what WETH provides in the ETH world.

Important note: to run the integration tests, you need my custom version of Sidecar, see instructions below. This versions exposes the returned value from the transaction, which the current of version of Sidecar does not.

Wrapr contract API

You can find the contract here.

(wrap (amount uint))

Wrap amount of STX from sender into a fungible-token (called "wrapr token") and transfer that token amount back to sender

(unwrap (amount uint))

Unwrap the STX tokens held for the sender in the wrapr token, and sends back amount of STX if below the amount held by sender

(transfer (recipient principal) (amount uint))

Transfer amount of wrapr token to recipient

(get-total-supply) read-only

Get the total amount of STX currently wrapped by all

(balance-of (owner principal)) read-only

Get the balance of wrapr owned by owner

Wrapr contract notes

Unfortunately, as of writing this, there is no way to add STX to an address in the testing framework used for unit testing, so only minimal testing is provided as part of the swapr unit tests

However, there is a scenario that shows how to use wrapr on a real node (testnet/mocknet for now) under test/integration

2 Typescript clients are provided to make it easier to interact with the contracts, either via @blockstack/clarity or via @blockstack/stacks-transactions

Swapr contract API

You can find the contract here.

(add-to-position (x uint) (y uint))

Add x amount of the X token, and y amount of Y token by transfering from the sender. Currently does not check that the exchange rate makes sense, so could lead to losses. Eventually, you'll be able to specify u0 for either x or y and the contract will calculate the proper amount to send to match the current exchange rate.

(reduce-position (percent uint))

Transfer back proportional amount of token X and token Y to the sender based on the sender's share of the pool, up to 100% of what the sender owns.

(swap-x-for-y (dx uint))

Send x of the X token, and gets back an amount of token Y based on current exchange rate, give or take slippage

Returns dy.

(swap-y-for-x (dy uint))

Send y of the Y token, and gets back an amount of token X based on current exchange rate, give or take slippage

Returns dx.

(get-position-of (owner principal)) read-only

Get the X and Y token positions for owner

(get-positions) read-only

Get the X and Y token for the contract, the sums of positions owned by all liquidity providers

(get-balances-of (owner principal)) read-only

Get the share of the pool owned by owner

(get-balances) read-only

Get the total of shares of the pool collectively owned by all liquidity providers

(set-fee-to-address (address principal))

When set, the contract will collect a 5 basis point (bp) fee to benefit the contract operator. none by default.

(reset-fee-to-address)

Clear the contract fee addres

(get-fee-to-address) read-only

Get the current address used to collect a fee, if set

(get-fees) read-only

Get the amount of fees charged on x-token and y-token exchanges that have not been collected yet

(collect-fees)

Send the collected fees the fee-to-address

Setup with mocknet

setup mocknet with lastest from master

git clone https://github.com/blockstack/stacks-blockchain.git
cd stacks-blockchain
cargo testnet start --config=./testnet/stacks-node/Stacks.toml

generate keys

cargo run --bin blockstack-cli generate-sk --testnet > keys-alice.json
cargo run --bin blockstack-cli generate-sk --testnet > keys-bob.json
cargo run --bin blockstack-cli generate-sk --testnet > keys-zoe.json
cargo run --bin blockstack-cli generate-sk --testnet > keys-contracts.json

Then move the keys to the swapr folder

setup STX balances

Under [burnchain], add

# alice
[[mstx_balance]]
address = "SP2J6ZY48GV1EZ5V2V5RB9MP66SW86PYKKNRV9EJ7"
amount = 1000000

# bob
[[mstx_balance]]
address = "S02J6ZY48GV1EZ5V2V5RB9MP66SW86PYKKPVKG2CE"
amount = 1000000

# zoe
[[mstx_balance]]
address = "SZ2J6ZY48GV1EZ5V2V5RB9MP66SW86PYKKQ9H6DPR"
amount = 1000000

by using the addresses generated in keys-*.json, not the above which are the ones from the unit tests, you need the private keys :)

Verify the balances by using

Verify the balances with

again, using the proper addresses

Running the unit tests

npm test

Running the wrapr integration tests using @blockstack/stacks-transactions

npm run wrapr

Sidecar is required for running the integration tests. Sidecar is needed to check the state of the transaction. See setup instructions.

wrapr test scenario

Check that balances match what is expected as contract calls are made.

Running the swapr integration tests using @blockstack/stacks-transactions

npm run swapr

Sidecar is required for running the integration tests. Sidecar is needed to check the state of the transaction. See setup instructions.

swapr test scenario

The test deploys 2 instances of the my-token contract to implement token1 and token2, 1 instance of the wrapr contract, and 2 instances of the swapr contract to implement 2 exchanges, token1-token2 and swapr-token1.

Check that balances match what is expected as contract calls are made.

Using the clients from an app

The wrapr and swapr clients under src/tx-clients can be use to interact with the contracts. You would just skip the deploy step and only need to provide the stackAddress in the keys object

  import { StacksTestnet } from '@blockstack/stacks-transactions'
  import { WraprTXClient } from 'swapr'

  const network = new StacksTestnet()
  network.coreApiUrl = STACKS_API_URL  // a Sidecar server URL that is connected to a stacks node connected to a network where the contracts are deployed

  const wraprTXClient = new WraprTXClient({ stackAddress: 'ST32N7A3G9P7J0VZ2JCJCG5DMB1TDWY8Q08KQ3B99' }, network)

Then you can call the wrap function like so

  const tx_wrap_result = await wraprTXClient.wrap(
  	2000000,
  	{ keys_sender: { stackAddress: 'STF4HK0PN8S7B8CBN564KY6VAH5D0P2J8WDGEZAH' } }
  )

  // as per the contract, the return value is a `(list amount total-supply)` returned as `ClarityValue`
  const result = tx_wrap_alice.list[0].value

  // displays the `BigNum` value for `amount`
  console.log(result.toString())

Setup with sidecar

npm run dev:integrated

or when using my feature branch, you can run devenv:build once:

npm run devenv:build

then run Sidecar with:

npm run dev:integrated:run

Funding addresses with STX

Edit the Stacks-dev.toml file, and setup STX balances as described above

Restart Sidecar

Note: you need to restart Sidecar each time you want to run the integration tests, so that the contracts can be re-deployed.

Further thoughts

Solidity does not make it easy to implement sqrt, although the "egyptian" method seems fine, however not having loops in Clarity makes it impractical to implement, so the contract uses the old method, but if the x pair is a lot less valuable than the y pair, rounding issues may be more prominent. Rather, I would hope sqrt can be added as a prinitive to Clarity (see section 3.4 of the V2 whitepaper), at least the isqrt variant:

the integer square root (isqrt) of a positive integer n is the positive integer m which is the greatest integer less than or equal to the square root of n

From the Wikipedia definition.

The current design of the Clarity traits makes it quite impractical to implement exchanging multiple pairs with a single contract, so a contract will need to be custom written (easy to automate) and deployed for each pair. There is ongoing work to make traits more usable. However, to be able to use a single contract for all pairs, there would be a need to keep some reference to a trait so data can be stored and retrieved. The current implementation forbids to store any references to traits so that you can't call them later on (a valid concern), but maybe being able to keep a hash of a trait would make sense, so that hash can be used as a key into a map to store a pair's data.

Sidecar does not include the return value from the contract, so you need to make extra calls to find out what happened, you can only tell whether the transaction succeeded and committed, and failed and nothing was committed. I created a PR for this, but still under discussion: https://github.com/blockstack/stacks-blockchain-sidecar/pull/129.

I disagree with the formula showed in section 3.3.2 of the x-y-k paper (the + 1 should not be there), so I'm using my own formula, which I re-calculated several times. The modeling I did in a spreadsheet was clearly showing that with small numbers, the formula would be way off using the one from the x-y-k paper... Story to be continued.

The client code feels very repetitive (whether for @blockstack/clarity, for @blockstack/stacks-transactions), and there is probably an opportunity to generate the client code from the contract itself, as the patterns used are pretty similar and only depend on the paramter/returnt type.

Some web app would be nice, and should be next step.

And finally, whether this contract ends up being valuable or not will depend on whether a healthy token ecosystem develops on Stacks 2.0 and there is a need to exchange tokens in a trustless manner.