Liquidity Provider

Now that we have a signing wallet, some gas coins to send transactions, and some ATOM to use as collateral, it's time to start interacting with the liquidity provider component of the API. Let's start off by querying the initial state of our wallet with the lp_info query:


import { SigningCosmWasmClient } from "@cosmjs/cosmwasm-stargate"
import { DirectSecp256k1HdWallet } from "@cosmjs/proto-signing"
import { GasPrice } from "@cosmjs/stargate"

// Osmosis testnet RPC endpoint
// retrieved from https://github.com/cosmos/chain-registry/blob/aecb225b17d1cb5a3826887735cafeb7e62934d0/testnets/osmosistestnet/chain.json#L123
const rpcEndpoint = "https://rpc.osmotest5.osmosis.zone"

// Our personal wallet seed phrase
const seedPhrase =
  "hen punch extra relax craft bicycle iron core purity tissue talk impact kitchen inhale slush hip amateur during ranch inspire much correct century where"

// ATOM_USD contract address on Osmosis testnet
const marketAddress =
  "osmo1kus6tmx9ggmvgg5tf88ukgxcz0ynakx38hyjy0sjgahvp7d3ut2qqtfhf4"

// Testnet CW20 contract for ATOM collateral
const collateralAddress =
  "osmo1g68w56vq9q0vr2mdlzp2l80j0lelnffkn7y3zhek5pavtxzznuks8wkv6k"


const runAll = async () => {
  const signer = await DirectSecp256k1HdWallet.fromMnemonic(seedPhrase, {
    // This is the prefix used by the Osmosis testnet
    prefix: "osmo"
  })
  const accounts = await signer.getAccounts()
  const walletAddress = accounts[0].address

  const client = await SigningCosmWasmClient.connectWithSigner(
    rpcEndpoint,
    signer,
    // By specifying the cost of gas, we can let cosmjs determine the amount of
    // gas funds to include with transactions
    { gasPrice: GasPrice.fromString("0.025uosmo") }
  )

  const lpInfo = await client.queryContractSmart(marketAddress, {
    lp_info: { liquidity_provider: walletAddress }
  })
  console.log(lpInfo)
}

runAll()

This results in a whole bunch of zeros:

{
  lp_amount: "0",
  lp_collateral: "0",
  xlp_amount: "0",
  xlp_collateral: "0",
  available_yield: "0",
  available_yield_lp: "0",
  available_yield_xlp: "0",
  available_crank_rewards: "0",
  unstaking: null,
  history: { deposit: "0", deposit_usd: "0", yield: "0", yield_usd: "0" }
}

That's because we haven't actually deposited any liquidity. Let's fix that.

Depositing liquidity

Since we're using a CW20 contract for collateral, we have to use its interface for initiating a token transfer. You can read more on the CW20 spec, but the short version of what we want to do is:

  • Identify the underlying message to send to the market contract
  • Base64-encode that message into a simple string
  • Use the CW20's send method to send a token balance and execute a method on the market contract
import { SigningCosmWasmClient } from "@cosmjs/cosmwasm-stargate"
import { DirectSecp256k1HdWallet } from "@cosmjs/proto-signing"
import { GasPrice } from "@cosmjs/stargate"

// Osmosis testnet RPC endpoint
// retrieved from https://github.com/cosmos/chain-registry/blob/aecb225b17d1cb5a3826887735cafeb7e62934d0/testnets/osmosistestnet/chain.json#L123
const rpcEndpoint = "https://rpc.osmotest5.osmosis.zone"

// Our personal wallet seed phrase
const seedPhrase =
  "hen punch extra relax craft bicycle iron core purity tissue talk impact kitchen inhale slush hip amateur during ranch inspire much correct century where"

// ATOM_USD contract address on Osmosis testnet
const marketAddress =
  "osmo1kus6tmx9ggmvgg5tf88ukgxcz0ynakx38hyjy0sjgahvp7d3ut2qqtfhf4"

// Testnet CW20 contract for ATOM collateral
const collateralAddress =
  "osmo1g68w56vq9q0vr2mdlzp2l80j0lelnffkn7y3zhek5pavtxzznuks8wkv6k"
  
const runAll = async () => {
    const signer = await DirectSecp256k1HdWallet.fromMnemonic(seedPhrase, {
        // This is the prefix used by Osmosis testnet
        prefix: "osmo"
    })
    const accounts = await signer.getAccounts()
    const walletAddress = accounts[0].address

    const client = await SigningCosmWasmClient.connectWithSigner(
        rpcEndpoint,
        signer,
        // By specifying the cost of gas, we can let cosmjs determine the amount of
        // gas funds to include with transactions
        // this default is taken from https://github.com/cosmos/chain-registry/blob/aecb225b17d1cb5a3826887735cafeb7e62934d0/testnets/osmosistestnet/chain.json#L21
        { gasPrice: GasPrice.fromString("0.025uosmo") }
    )

    // base64-encode a market ExecuteMessage, as the inner payload for the CW20 message
    const msg = Buffer.from(JSON.stringify({ deposit_liquidity: {} })).toString(
        "base64"
    )
  
    const execResult = await client.execute(
        // Send: our wallet address
        walletAddress,
        // We execute this message on the collateral CW20 token
        collateralAddress,
        // After the CW20 receives our message, it will transfer 1 ATOM to the
        // market with the deposit_liquidity message above
        { send: { contract: marketAddress, amount: "1000000", msg } },
        "auto"
    )
    // Print out the results of executing the message: events, transaction hash, etc
    console.log(execResult)
  
    // Now get our LP info, which should be much more interesting
    const lpInfo = await client.queryContractSmart(marketAddress, {
        lp_info: { liquidity_provider: walletAddress }
    })
    console.log(lpInfo)
}

runAll()

After running this command, we have updated LP information:

{
  lp_amount: '1.043547006279525745',
  lp_collateral: '0.999999999999999999',
  xlp_amount: '0',
  xlp_collateral: '0',
  available_yield: '0',
  available_yield_lp: '0',
  available_yield_xlp: '0',
  available_crank_rewards: '0',
  unstaking: null,
  history: { deposit: '1', deposit_usd: '7.1965', yield: '0', yield_usd: '0' },
  liquidity_cooldown: { at: '1695390852883629426', seconds: 3600 }
}

The lp_collateral represents the share of the collateral we have within the pool. This starts off at (approximately) 1, but will potentially change over time due to impairment. Impairment is the fact that, as traders win and lose money versus the liquidity pool, the size of the pool will change. By contrast, the lp_amount represents the amount of LP tokens our wallet has representing our collateral. This number will stay the same until we take some action to change it (which we'll see shortly).

The xlp_ field variants talk about the amount of xLP tokens, which are time-locked. We'll experiment with those shortly. The available_ fields indicate what reward amounts are available to be claimed by our wallet. Unsurprisingly, since we just deposited liquidity, we haven't earned any rewards yet. Note that the available_yield field will always be the sum of the other available_ fields.

The unstaking field is currently null, meaning we are not in the process of unstaking xLP. We'll have to get some xLP tokens before we can experiment with that.

And finally, the history field provides historical information on the total deposits and total collected yields.

Deposit to xLP

The deposit_liquidity message we used above didn't take any parameters, which led it to use its default behavior: minting new LP tokens for the deposited collateral. We can instead, however, deposit directly into xLP tokens. All we have to change is:

const msg = Buffer.from(
  JSON.stringify({ deposit_liquidity: { stake_to_xlp: true } })
).toString("base64")

With this bit of code in place instead, rerunning our program now shows us both our LP and xLP balances:

{
  lp_amount: '1.043547006279525745',
  lp_collateral: '0.999999999999999999',
  xlp_amount: '1.043547006279525745',
  xlp_collateral: '0.999999999999999999',
  available_yield: '0',
  available_yield_lp: '0',
  available_yield_xlp: '0',
  available_crank_rewards: '0',
  unstaking: null,
  history: {
    deposit: '2',
    deposit_usd: '14.3980746',
    yield: '0',
    yield_usd: '0'
  },
  liquidity_cooldown: { at: '1695390852883629426', seconds: 3467 }
}

Note that we have slightly different amounts of LP and xLP tokens, and the collateral backing them is also slightly different. This is because of the impairment that occurred within the protocol between deposit for LP and depositing for xLP. Remember: the ratio between LP/xLP tokens and the underlying collateral will change over time.

Stake LP to xLP

In addition to depositing directly into xLP tokens, you can instead stake existing LP tokens into xLP tokens. This will be our first example of executing a transaction directly against the market contract, without going through the collateral CW20 contract first.

Instead of just replacing the msg payload, let's look at the entire program code again - notice that there is no base64 encoded msg payload at all, and we no longer care about the collateralAddress either:

import { SigningCosmWasmClient } from "@cosmjs/cosmwasm-stargate"
import { DirectSecp256k1HdWallet } from "@cosmjs/proto-signing"
import { GasPrice } from "@cosmjs/stargate"

// Osmosis testnet RPC endpoint
// retrieved from https://github.com/cosmos/chain-registry/blob/aecb225b17d1cb5a3826887735cafeb7e62934d0/testnets/osmosistestnet/chain.json#L123
const rpcEndpoint = "https://rpc.osmotest5.osmosis.zone"

// Our personal wallet seed phrase
const seedPhrase =
  "hen punch extra relax craft bicycle iron core purity tissue talk impact kitchen inhale slush hip amateur during ranch inspire much correct century where"

// ATOM_USD contract address on Osmosis testnet
const marketAddress =
  "osmo1kus6tmx9ggmvgg5tf88ukgxcz0ynakx38hyjy0sjgahvp7d3ut2qqtfhf4"
  
const runAll = async () => {
    const signer = await DirectSecp256k1HdWallet.fromMnemonic(seedPhrase, {
        // This is the prefix used by Osmosis testnet
        prefix: "osmo"
    })
    const accounts = await signer.getAccounts()
    const walletAddress = accounts[0].address

    const client = await SigningCosmWasmClient.connectWithSigner(
        rpcEndpoint,
        signer,
        // By specifying the cost of gas, we can let cosmjs determine the amount of
        // gas funds to include with transactions
        // this default is taken from https://github.com/cosmos/chain-registry/blob/aecb225b17d1cb5a3826887735cafeb7e62934d0/testnets/osmosistestnet/chain.json#L21
        { gasPrice: GasPrice.fromString("0.025uosmo") }
    )

    const execResult = await client.execute(walletAddress, marketAddress, { stake_lp: {} }, "auto")

    // Print out the results of executing the message: events, transaction hash, etc
    console.log(execResult)
  
    // Now get our LP info, which should be much more interesting
    const lpInfo = await client.queryContractSmart(marketAddress, {
        lp_info: { liquidity_provider: walletAddress }
    })
    console.log(lpInfo)
}

runAll()

Without any parameters, stake_lp will stake all LP tokens into xLP tokens:

{
  lp_amount: '0',
  lp_collateral: '0',
  xlp_amount: '2.08709401255905149',
  xlp_collateral: '1.999999999999999998',
  available_yield: '0',
  available_yield_lp: '0',
  available_yield_xlp: '0',
  available_crank_rewards: '0',
  unstaking: null,
  history: {
    deposit: '2',
    deposit_usd: '14.3980746',
    yield: '0',
    yield_usd: '0'
  },
  liquidity_cooldown: { at: '1695390852883629426', seconds: 3338 }
}

Alternatively, we can choose to only stake some of our LP tokens. This example will first deposit some new LP tokens and then stake some of them into xLP. Therefore, we need to base64 the payload message and use collateralAddress for the deposit part, and then a direct market contract execution for the staking part:

import { SigningCosmWasmClient } from "@cosmjs/cosmwasm-stargate"
import { DirectSecp256k1HdWallet } from "@cosmjs/proto-signing"
import { GasPrice } from "@cosmjs/stargate"

// Osmosis testnet RPC endpoint
// retrieved from https://github.com/cosmos/chain-registry/blob/aecb225b17d1cb5a3826887735cafeb7e62934d0/testnets/osmosistestnet/chain.json#L123
const rpcEndpoint = "https://rpc.osmotest5.osmosis.zone"

// Our personal wallet seed phrase
const seedPhrase =
  "hen punch extra relax craft bicycle iron core purity tissue talk impact kitchen inhale slush hip amateur during ranch inspire much correct century where"

// ATOM_USD contract address on Osmosis testnet
const marketAddress =
  "osmo1kus6tmx9ggmvgg5tf88ukgxcz0ynakx38hyjy0sjgahvp7d3ut2qqtfhf4"

// Testnet CW20 contract for ATOM collateral
const collateralAddress =
  "osmo1g68w56vq9q0vr2mdlzp2l80j0lelnffkn7y3zhek5pavtxzznuks8wkv6k"
  
const runAll = async () => {
    const signer = await DirectSecp256k1HdWallet.fromMnemonic(seedPhrase, {
        // This is the prefix used by Osmosis testnet
        prefix: "osmo"
    })
    const accounts = await signer.getAccounts()
    const walletAddress = accounts[0].address

    const client = await SigningCosmWasmClient.connectWithSigner(
        rpcEndpoint,
        signer,
        // By specifying the cost of gas, we can let cosmjs determine the amount of
        // gas funds to include with transactions
        // this default is taken from https://github.com/cosmos/chain-registry/blob/aecb225b17d1cb5a3826887735cafeb7e62934d0/testnets/osmosistestnet/chain.json#L21
        { gasPrice: GasPrice.fromString("0.025uosmo") }
    )
    const msg = Buffer.from(
        JSON.stringify({ deposit_liquidity: { stake_to_xlp: false } })
    ).toString("base64")
      
    await client.execute(
        walletAddress,
        collateralAddress,
        { send: { contract: marketAddress, amount: "5000000", msg } },
        "auto"
    )
    
    console.log("After depositing LP, before staking to xLP")
    const lpInfo = await client.queryContractSmart(marketAddress, {
        lp_info: { liquidity_provider: walletAddress }
    })
    console.log(lpInfo)
    
    await client.execute(
        walletAddress,
        marketAddress,
        { stake_lp: { amount: "0.75" } },
        "auto"
    )
    
    console.log("After staking to xLP")
    const lpInfo2 = await client.queryContractSmart(marketAddress, {
        lp_info: { liquidity_provider: walletAddress }
    })
    console.log(lpInfo2)
}

runAll()

With this we can see the two-step process changing our LP and xLP token balances:

After depositing LP, before staking to xLP
{
  lp_amount: '4.396704079666426507',
  lp_collateral: '4.999999999999999998',
  xlp_amount: '1.756804235075048688',
  xlp_collateral: '1.997864995281119361',
  ...
}
After staking to xLP
{
  lp_amount: '3.646704079666426507',
  lp_collateral: '4.14708837982916769',
  xlp_amount: '2.506804235075048688',
  xlp_collateral: '2.850776615451951669',
  ...
}

Which brings us to a final point: number representations. The CW20 standard always represents numbers as string-encoded integers with a certain number of decimal points. Therefore, representing a value like "2.5 ATOM" in CW20 would be "2500000". However, within the market contract we use string-encoded decimals. To represent "0.75 LP tokens", we use the string "0.75".

Unstake

xLP tokens can be unstaked into LP tokens the process happens as a linear unlock over a configurable period of time (currently: 90 days). In other words, if you unstake 90 xLP tokens today, in 5 days you'll be able to collect 5 LP tokens. You can only have one unstaking process occurring at a time, and the lp_info response's unstaking field gives information on that process. Let's start off by unstaking some xLP tokens and immediately query for the unstaking status:

await client.execute(
  walletAddress,
  marketAddress,
  { unstake_xlp: { amount: "2.2" } },
  "auto"
)

const lpInfo = await client.queryContractSmart(marketAddress, {
  lp_info: { liquidity_provider: walletAddress }
})
console.log(lpInfo)

Running this code shows us an unstaking process in progress:

{
  lp_amount: '3.646704079666426507',
  lp_collateral: '4.147007773402060011',
  xlp_amount: '2.506804235075048688',
  xlp_collateral: '2.850721205271022955',
  available_yield: '0.00034326701085305',
  available_yield_lp: '0.000039480958148038',
  available_yield_xlp: '0.000303786052705012',
  available_crank_rewards: '0',
  unstaking: {
    start: '1679304954886038378',
    end: '1687080954886038378',
    xlp_unstaking: '2.2',
    xlp_unstaking_collateral: '2.501825457227413605',
    collected: '0',
    available: '0',
    pending: '2.2'
  },
  history: { deposit: '7', deposit_usd: '85.086', yield: '0', yield_usd: '0' }
}

Notice that it is telling us: in total we are unstaking 2.2 xLP tokens. Since no time has ellapsed since we started the unstaking, none of our tokens are available as LP yet, and therefore pending still shows 2.2. However, if we query lp_info again after a small bit of time, we'll see some LP is now available:

{
  lp_amount: '3.646735364074815934',
  lp_collateral: '4.147132285714419282',
  xlp_amount: '2.506772950666659261',
  xlp_collateral: '2.850746763551560125',
  available_yield: '0.000349311652552045',
  available_yield_lp: '0.000042441072835686',
  available_yield_xlp: '0.000306870579716359',
  available_crank_rewards: '0',
  unstaking: {
    start: '1679304954886038378',
    end: '1687080954886038378',
    xlp_unstaking: '2.2',
    xlp_unstaking_collateral: '2.50187911040987234',
    collected: '0',
    available: '0.000031284408389427',
    pending: '2.199968715591610573'
  },
  history: { deposit: '7', deposit_usd: '85.086', yield: '0', yield_usd: '0' }
}

Notice how some of the LP has "moved" from pending into available. The way the linear unstaking process works is that the available amount is calculated lazily on each query, based on the current timestamp. However, some operations--like depositing more liquidity--will "collect" that available liquidity into the calculated field. The rule is that xlp_unstaking = collected + available + pending. We can see this in practice by explicitly collecting the unstaked LP:

await client.execute(
  walletAddress,
  marketAddress,
  { collect_unstaked_lp: {} },
  "auto"
)

const lpInfo = await client.queryContractSmart(marketAddress, {
  lp_info: { liquidity_provider: walletAddress }
})
console.log(lpInfo)

The immediate query results in some collected LP and no available LP:

{
  lp_amount: '3.646819314114995898',
  lp_collateral: '4.147126593965646177',
  xlp_amount: '2.506689000626479297',
  xlp_collateral: '2.850581759579722187',
  available_yield: '0.000388004059523683',
  available_yield_lp: '0.000061392359620793',
  available_yield_xlp: '0.00032661169990289',
  available_crank_rewards: '0',
  unstaking: {
    start: '1679304954886038378',
    end: '1687080954886038378',
    xlp_unstaking: '2.2',
    xlp_unstaking_collateral: '2.501818083339437631',
    collected: '0.000115234448569391',
    available: '0',
    pending: '2.199884765551430609'
  },
  history: { deposit: '7', deposit_usd: '85.086', yield: '0', yield_usd: '0' }
}

And if we wait a bit longer and then query again, we'll see some available again as the linear unstaking process continues:

{
  lp_amount: '3.646844912323768016',
  lp_collateral: '4.147219276631564072',
  xlp_amount: '2.506663402417707179',
  xlp_collateral: '2.850596346283739866',
  available_yield: '0.000397952113847755',
  available_yield_lp: '0.000066266349895727',
  available_yield_xlp: '0.000331685763952028',
  available_crank_rewards: '0',
  unstaking: {
    start: '1679304954886038378',
    end: '1687080954886038378',
    xlp_unstaking: '2.2',
    xlp_unstaking_collateral: '2.501856434244610363',
    collected: '0.000115234448569391',
    available: '0.000025598208772118',
    pending: '2.199859167342658491'
  },
  history: { deposit: '7', deposit_usd: '85.086', yield: '0', yield_usd: '0' }
}

Claim rewards

As traders pay trading and borrow fees, yield is allocated to LP and xLP token holders. This yield is kept in the market contract until claimed by the liquidity providers. Claiming it is accomplished by calling the claim_yield execute message. Here is that claim in practice, together with some queries to demonstrate the change in available yield and ATOM CW20 balance:

const printValues = async () => {
  const lpInfo = await client.queryContractSmart(marketAddress, {
    lp_info: { liquidity_provider: walletAddress }
  })
  console.log(`Available yield: ${lpInfo.available_yield}`)
  const { balance } = await client.queryContractSmart(collateralAddress, {
    balance: { address: walletAddress }
  })
  console.log(`ATOM CW20 balance: ${balance}`)
}

console.log("Before claiming")
await printValues()

await client.execute(
  walletAddress,
  marketAddress,
  { claim_yield: {} },
  "auto"
)

console.log("After claiming")
await printValues()

And we can see the CW20 balance increase with the available yield disappearing:

Before claiming
Available yield: 0.000450582621158018
ATOM CW20 balance: 993000000
After claiming
Available yield: 0
ATOM CW20 balance: 993000452

Withdraw liquidity

LP tokens can be withdrawn into their underlying collateral. The limitation on this is that you can only withdraw unlocked liquidity (as displayed in the status query). When you specifying withdrawing, you use the amount of LP tokens you would like to withdraw:

const printValues = async () => {
  const lpInfo = await client.queryContractSmart(marketAddress, {
    lp_info: { liquidity_provider: walletAddress }
  })
  console.log(`LP tokens: ${lpInfo.lp_amount}`)
  console.log(`LP token collateral: ${lpInfo.lp_collateral}`)
  const { balance } = await client.queryContractSmart(collateralAddress, {
    balance: { address: walletAddress }
  })
  console.log(`ATOM CW20 balance: ${balance}`)
}

console.log("Before withdrawing")
await printValues()

await client.execute(
  walletAddress,
  marketAddress,
  { withdraw_liquidity: { lp_amount: "1.2" } },
  "auto"
)

console.log("After withdrawing")
await printValues()

Results in:

Before withdrawing
LP tokens: 3.647034094435032443
LP token collateral: 4.147209037218631394
ATOM CW20 balance: 993000452
After withdrawing
LP tokens: 2.447035517075706802
LP token collateral: 2.782635848207894757
ATOM CW20 balance: 994365026

Summary

Congratulations! You're now an expert at the liquidity provider portion of the API. Time to check out trading!