Liquidity Baking contract and Taquito
The goal of this document is to acquaint yourself with the different values in the storage of the contract as well as its entrypoints and the JavaScript code necessary to interact with them.
Storage
The storage of the LB contract is made of 5 values:
- tokenPool: a value of type
nat
(i.e. a positive number) that represents the amount of tokens (in this case, tzBTC) held by the contract - xtzPool: a value of type
mumav
(i.e. an amount of tez) that represents the amount of XTZ held by the contract - lqtTotal: a value of type
nat
that represents the amount of liquidity tokens (or LB tokens) the contract holds. The LB token is an FA1.2 token that represents a pair consisting of XTZ and tzBTC provided to the contract as liquidity - tokenAddress: a value of type
address
(i.e. the address of an implicit account or a contract) that holds the address to the tzBTC contract - lqtAddress : a value of type
address
that holds the address of the LB token
Entrypoints
Entrypoint parameters and purpose
- %default: update of the
xtzPool
value when the subsidy is sent to the contract after each block is baked Parameters: no parameter, only the subsidy in tez is sent with the transaction - %tokenToXtz: exchange tzBTC for XTZ
Parameters:
- to: the account address that will receive the XTZ amount
- tokensSold: the amount of tzBTC to sell
- minXtzBought: the minimum amount of XTZ expected to be received
- deadline: the expiry time of the transaction
- %xtzToToken: exchange XTZ for tzBTC
Parameters:
- to: the account address that will receive the tzBTC tokens
- minTokensBought: the minimum amount of tzBTC expected to be received
- deadline: the expiry time of the transaction
- %tokenToToken: used as an intermediary to faciliate the exchange of tzBTC and XTZ between 2 contracts
Parameters:
- outputDexterContract: the contract address to send the tokens to
- minTokensBought: the minimum amount of tzBTC tokens expected to be received
- to: the recipient of the tokens for the transaction sent to the
outputDexterContract
address - tokensSold: the amount of tokens to be sold
- deadline: the expiry time of the transaction
- %addLiquidity: provision of XTZ and tzBTC to the contract and minting of LP tokens
Parameters:
- owner: the account address that will be credited with the LP tokens
- minLqtMinted: the minimum amount of LP tokens expected to be minted
- maxTokensDeposited: the maximum amount of tzBTC tokens expected to be withdrawn from the sender's balance
- deadline: the expiry time of the transaction
- %removeLiquidity: burning of LP tokens and credit of XTZ and tzBTC
Parameters:
- to: the account address that will be credited with XTZ and tzBTC tokens
- lqtBurned: the amount of LP tokens to burn
- minXtzWithdrawn: the minimum amount of XTZ expected to be credited
- minTokensWithdrawn: the minimum amount of tzBTC tokens expected to be credited
- deadline: the expiry time of the transaction
Interacting with the entrypoints with Taquito
The main friction point of interacting with the LB contract in JavaScript is about simulating the calculations of the expected token outputs Michelson does.
- %tokenToXtz:
import { TezosToolkit } from "@mavrykdynamics/taquito"// to take into account the subsidy added to the LB contract// when the transaction will be bakedconst creditSubsidy = (xtzPool: BigNumber | number): BigNumber => {const LIQUIDITY_BAKING_SUBSIDY = 2500000;if (BigNumber.isBigNumber(xtzPool)) {return xtzPool.plus(new BigNumber(LIQUIDITY_BAKING_SUBSIDY));} else {return new BigNumber(xtzPool).plus(new BigNumber(LIQUIDITY_BAKING_SUBSIDY));}};const tokenToXtzXtzOutput = (p: {tokenIn: BigNumber | number;xtzPool: BigNumber | number;tokenPool: BigNumber | number;}): BigNumber | null => {const { tokenIn, xtzPool: _xtzPool, tokenPool } = p;let xtzPool = creditSubsidy(_xtzPool);let tokenIn_ = new BigNumber(0);let xtzPool_ = new BigNumber(0);let tokenPool_ = new BigNumber(0);try {tokenIn_ = new BigNumber(tokenIn);xtzPool_ = new BigNumber(xtzPool);tokenPool_ = new BigNumber(tokenPool);} catch (err) {return null;}if (tokenIn_.isGreaterThan(0) &&xtzPool_.isGreaterThan(0) &&tokenPool_.isGreaterThan(0)) {// Includes 0.1% fee and 0.1% burn calculated separatedly:// 999/1000 * 999/1000 = 998001/1000000let numerator = new BigNumber(tokenIn).times(new BigNumber(xtzPool)).times(new BigNumber(998001));let denominator = new BigNumber(tokenPool).times(new BigNumber(1000000)).plus(new BigNumber(tokenIn).times(new BigNumber(999000)));return numerator.dividedBy(denominator);} else {return null;}};const Tezos = new TezosToolkit(RPC_URL);const lbContract = await Tezos.wallet.at(LB_CONTRACT_ADDRESS);// the deadline value is arbitrary and can be changedconst deadline = new Date(Date.now() + 60000).toISOString();const tzBtcContract = await Tezos.wallet.at(TZBTC_ADDRESS);const tokensSold = AMOUNT_IN_TZBTC;const minXtzBought = tokenToXtzXtzOutput({tokenIn: tokensSold,xtzPool,tokenPool}).toNumber();let batch =Tezos.wallet.batch().withContractCall(tzBtcContract.methodsObject.approve({ spender: lbContractAddress, value: 0})).withContractCall(tzBtcContract.methodsObject.approve({ spender: lbContractAddress, value: tokensSold })).withContractCall(lbContract.methodsObject.tokenToXtz({to: USER_ADDRESS,tokensSold,minXtzBought,deadline}));const batchOp = await batch.send();await batchOp.confirmation();
This code sends a transaction to the %tokenToXtz
entrypoint of the contract to exchange tzBTC tokens for XTZ. After the amount of tzBTC to exchange is provided in the AMOUNT_IN_TZBTC
variable, the expected output in XTZ is calculated using the tokenToXtzXtzOutput
function. This value is then stored in the minXtzBought
variable before being used with the provided amount in tzBTC to forge a transaction and exchange tzBTC tokens for XTZ.
Note: the different numeric values used for the XTZ and tzBTC amounts, as well as for the XTZ and tzBTC pools are raw values that are not formatted with their decimal point. Be careful not to use formatted values meant for display only that could throw off the results of the calculations.
- %xtzToToken:
import { TezosToolkit, OpKind } from "@mavrykdynamics/taquito"// outputs the amount of tzBTC tokens for a given amount of XTZconst xtzToTokenTokenOutput = (p: {xtzIn: BigNumber | number;xtzPool: BigNumber | number;tokenPool: BigNumber | number;}): BigNumber | null => {let { xtzIn, xtzPool: _xtzPool, tokenPool } = p;let xtzPool = creditSubsidy(_xtzPool);let xtzIn_ = new BigNumber(0);let xtzPool_ = new BigNumber(0);let tokenPool_ = new BigNumber(0);try {xtzIn_ = new BigNumber(xtzIn);xtzPool_ = new BigNumber(xtzPool);tokenPool_ = new BigNumber(tokenPool);} catch (err) {return null;}if (xtzIn_.isGreaterThan(0) &&xtzPool_.isGreaterThan(0) &&tokenPool_.isGreaterThan(0)) {// Includes 0.1% fee and 0.1% burn calculated separatedly: 999/1000 * 999/1000 = 998100/1000000// (xtzIn_ * tokenPool_ * 999 * 999) / (tokenPool * 1000 - tokenOut * 999 * 999)const numerator = xtzIn_.times(tokenPool_).times(new BigNumber(998001));const denominator = xtzPool_.times(new BigNumber(1000000)).plus(xtzIn_.times(new BigNumber(998001)));return numerator.dividedBy(denominator);} else {return null;}};const Tezos = new TezosToolkit(RPC_URL);const lbContract = await Tezos.wallet.at(LB_CONTRACT_ADDRESS);// the deadline value is arbitrary and can be changedconst deadline = new Date(Date.now() + 60000).toISOString();const minTokensBought = xtzToTokenTokenOutput({xtzIn: xtzAmountInMumav,xtzPool,tokenPool}).toNumber();const op = await lbContract.methodsObject.xtzToToken({to: USER_ADDRESS,minTokensBought,deadline}).send();await op.confirmation();
This code sends a transaction to the %xtzToToken
entrypoint of the contract to exchange XTZ for tzBTC tokens. The xtzToTokenTokenOutput
function is used to calculate the minimum amount of tzBTC tokens that can be expected when exchanging the given amount of XTZ.
Note: the
xtzToTokenTokenOutput
uses thecreditSubsidy
function introduced in the previous piece of code.
- %addLiquidity:
import { TezosToolkit, OpKind } from "@mavrykdynamics/taquito"const Tezos = new TezosToolkit(RPC_URL);const lbContract = await Tezos.wallet.at(LB_CONTRACT_ADDRESS);const tzBtcContract = await Tezos.wallet.at(TZBTC_ADDRESS);const maxTokensSold = Math.floor(AMOUNT_IN_TZBTC + (AMOUNT_IN_TZBTC * slippage) / 100);const minLqtMinted = Math.floor((AMOUNT_IN_XTZ * lqtTotal) / xtzPool);// the deadline value is arbitrary and can be changedconst deadline = new Date(Date.now() + 60000).toISOString();const batchOp = await Tezos.wallet.batch([{kind: OpKind.TRANSACTION,...tzBtcContract.methodsObject.approve({ spender: LB_CONTRACT_ADDRESS, value: 0 }).toTransferParams()},{kind: OpKind.TRANSACTION,...tzBtcContract.methodsObject.approve({ spender: LB_CONTRACT_ADDRESS, value: maxTokensSold }).toTransferParams()},{kind: OpKind.TRANSACTION,...lbContract.methodsObject.addLiquidity({owner: USER_ADDRESS,minLqtMinted: minLqtMinted - 3,maxTokensDeposited: maxTokensSold,deadline}).toTransferParams(),amount: AMOUNT_IN_XTZ,mumav: true},{kind: OpKind.TRANSACTION,...tzBtcContract.methodsObject.approve({ spender: LB_CONTRACT_ADDRESS, value: 0 }).toTransferParams()}]).send();await batchOp.confirmation();
The %addLiquidity
entrypoint is probably the most complex one to interact with.
The maximum amount of tzBTC tokens to be sold is calculated using this formula: AMOUNT_IN_TZBTC + (AMOUNT_IN_TZBTC * slippage) / 100
- %removeLiquidity:
import { TezosToolkit } from "@mavrykdynamics/taquito"const calculateLqtOutput = ({lqTokens,xtzPool,tzbtcPool,lqtTotal}: {lqTokens: number;xtzPool: number;tzbtcPool: number;lqtTotal: number;}): { xtz: number; tzbtc: number } => {const xtzOut = (+lqTokens * (xtzPool)) / lqtTotalconst tzbtcOut = (+lqTokens * (tzbtcPool)) / lqtTotalreturn {xtz: xtzOut,tzbtc: tzbtcOut};};const Tezos = new TezosToolkit(RPC_URL);const lbContract = await Tezos.wallet.at(LB_CONTRACT_ADDRESS);// the deadline value is arbitrary and can be changedconst deadline = new Date(Date.now() + 60000).toISOString();const { xtzOut, tzbtcOut } = calculateLqtOutput({lqTokens,xtzPool,tzbtcPool,lqtTotal});const op = await lbContract.methodsObject.removeLiquidity({to: USER_ADDRESS,lqtBurned: amountInLqt,minXtzWithdrawn: xtzOut,minTokensWithdrawn: tzBtcOut,deadline}).send();await op.confirmation();
- %tokenToToken:
The
%tokenToToken
entrypoint is meant to be used on-chain as an intermediary between 2 contracts. Although it's possible to send a transaction to the entrypoint, its use off-chain is redundant and the same effect can be obtained by calling%tokenToXtz
followed by%xtzToToken
.