Archway Dapp Guide — Liquid Staking Contract

Lydia Labs
17 min readMay 14, 2022

This is a guide for building a liquid staking contract in CosmWasm for Archway protocol

Introduction

_liquid staking_ is a contract for you to delegate and bond your native tokens to earn staking rewards without losing access to your funds.

We need a cw20token contract to represent our staked native token. Staking contract will be the minter of this token.

In this guide, we will learn more about how to write, deploy and interact with liquid staking contract.

Initiating your project

We create a new project without using stater template to initiate our contract.

$ archway new staking

Creating new Archway project…
✔ Do you want to use a starter template? … no
✔ Select the project environment › Testnet
? Select a testnet to use › — Use arrow-keys. Return to submit.
❯ Constantine
Stable — recommended for dApp development
Titus
Augusta
Torii

Go inside the project directory, we edit the Cargo.tomlfile as below to add dependencies for our contract:

rust
[dependencies]
cosmwasm-std = { version = “1.0.0-beta8”, features = [“staking”] }
cosmwasm-storage = “1.0.0-beta5”
cw-storage-plus = “0.12”
cw20 = “0.13.2”

The staking feature is added to cosmwasm-stdto include staking module to our contract. We also use cw20 here to get a lot of helpers for interacting with cw20contract.

Storage

Linked-list storage
We need a linked-list in our contract to crease an unstaking queue. Each node of the linked-list will contain an unstaking request.
The reason we use linked-list in our code is for unstaker to not wait full 21 days to undelegate his token. The native tokens of those who want to stake will be sent to unstakers in unstaking queue, thus making unstaking process much more faster.

To create linked-list storage, we use 2 available storages:

To save basic information of the link-list, we will use Singleton. For this storage to work, we create LinkedListstruct that contains all the vars we want to put inside the storage. To load and save data inside the storage in a transaction, we create the linked_list function. We also create a linked_list_read function to read data in a query:

rust
// file src/linked_list.rs
use cosmwasm_std::{Storage,};
use cosmwasm_storage::{
singleton, singleton_read, ReadonlySingleton,
Singleton,
};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
static LINKED_LIST_KEY: &[u8] = b”linked_list”;#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct LinkedList {
pub head_id: u64,
pub tail_id: u64,
pub length: u64,
}
pub fn linked_list(storage: &mut dyn Storage) -> Singleton<LinkedList> {
singleton(storage, LINKED_LIST_KEY)
}
pub fn linked_list_read(storage: &dyn Storage) -> ReadonlySingleton<LinkedList> {
singleton_read(storage, LINKED_LIST_KEY)
}

Now we have a storage contains 3 vars:

  • head_id: the id of the first node in linked-list.
  • tail_id: the id of the last node in linked-list.
  • length: the number of node in linked-list.

To save the data of each node in linked-list, we will use Bucket. Unlike Singleton, Bucket acts as a map where you can save different content with different keys. So we can use this type of storage to pair each node data with node id key.
For this storage to work, we create a Nodestruct that contains the data we want to assign for each node of the linked-list. To load and save data inside the storage in a transaction, we create nodefunction. We also create a node_readfunction to read data in a query:

rust
// file src/linked_list.rs
use cosmwasm_std::{Addr, Storage, Uint128,};
use cosmwasm_storage::{
bucket, bucket_read, Bucket, ReadonlyBucket,
};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
static NODE_KEY: &[u8] = b”node”;#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct Node {
pub receiver: Addr,
pub value: Uint128,
pub height: u64,
pub prev: u64,
pub next: u64,
}
pub fn node(storage: &mut dyn Storage) -> Bucket<Node> {
bucket(storage, NODE_KEY)
}
pub fn node_read(storage: &dyn Storage) -> ReadonlyBucket<Node> {
bucket_read(storage, NODE_KEY)
}

Now we have a storage contains nodes data, each node has:

  • receiver: the unstaker address.
  • value: the amount of native token receiver should earn.
  • height: the block height of the transaction the node created.
  • prev: the previous node id linked with this node.
  • next: the next node id linked with this node.

After having the storage to save data, we will create the following struct and functions for the linked-list to work:

  • NodeWithId: the struct to include node id along with Node data.
  • node_update_value: the function to update node value data.
  • linked_list_append: the function to add new node to the linked-list.
  • linked_list_clear: the function to remove all storage data of the linked-list.
  • linked_list_remove_head: the function to remove the first node.
  • linked_list_remove_tail: the function to remove the last node.
  • linked_list_remove: the function to remove the specific node.
  • linked_list_get_list: the function to turn the link-list nodes data into a list.

You should read the linked_list.rs file in the contract if you want to understand how each function work.

To make the project to recognize the linked_list.rs file in src folder, we add the following line to the lib.rs file

rust
// file src/lib.rs
pub mod linked_list;

State storage
Beside the linked-list, we also need other storage for the contract to work.

  • CONFIG: stores the contract configuration, the items stored here should not be changed.
rust 
// file src/state.rs
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct ConfigInfo {
/// Admin to change config
pub owner: Addr,
/// This is the denomination we can stake (and only one we accept for payments)
pub bond_denom: String,
/// Liquid token address
pub liquid_token_addr: Addr,
/// All tokens are bonded to this validator
/// FIXME: address validation doesn’t work for validator addresses
pub validator: String,
}
pub const CONFIG: Item<ConfigInfo> = Item::new(“config”);
  • TOTAL_SUPPLY: count the amount of tokens the contract has.
rust 
// file src/state.rs
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema, Default)]
pub struct Supply {
/// native is how many total supply of native tokens liquid token holders can withdraw
pub native: Uint128,
// unstakings is how many total native tokens in unstaking queue
pub unstakings: Uint128,
/// claims is how many tokens need to be reserved paying back those who unbonded
pub claims: Uint128,
}
pub const TOTAL_SUPPLY: Item<Supply> = Item::new(“total_supply”);
  • CLAIMABLE: Map of claimable native token each address key can claim.
rust 
// file src/state.rs
pub const CLAIMABLE: Map<&Addr, Uint128> = Map::new(“claimable”);
  • UNDER_UNSTAKING: Map of total native token each address key has in the unstaking queue.
rust 
// file src/state.rs
pub const UNDER_UNSTAKING: Map<&Addr, Uint128> = Map::new(“under_unstaking”);

Message

InstantiateMsg

InstantiateMsg is used when you deploy new contract. The params of this msg is the things you want to setup the contract with.

To declare the params we want to have when deploying staking contract, we put items inside InstantiateMsg struct in the msg.rs file:


rust
// file src/msg.rs
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct InstantiateMsg {
/// This is the validator that all tokens will be bonded to
pub validator: String,
}

The above code means we will only need to pass an address string when we deploy the contract:

$ archway deploy — args '{“validator”:”archwayvaloper1t3zrk2vva33ajcut0rvjrtxchlynx7j5mmgj8m”}'

We put what we want to do in the contract deployment transaction in instantiate function in contract.rs file. We can set the default value for our storage data here.

rust
// file src/contract.rs
#[cfg_attr(not(feature = “library”), entry_point)]
pub fn instantiate(
deps: DepsMut,
_env: Env,
info: MessageInfo,
msg: InstantiateMsg,
) -> Result<Response, ContractError> {
set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?;
let linked_list_init = LinkedList {
head_id: 0,
tail_id: 0,
length: 0
};
linked_list(deps.storage).save(&linked_list_init)?;
let denom = deps.querier.query_bonded_denom()?;
let config_init = ConfigInfo {
owner: info.sender,
bond_denom: denom,
liquid_token_addr: Addr::unchecked(“none”),
validator: msg.validator,
};
CONFIG.save(deps.storage, &config_init)?;
// set supply to 0
let supply_init = Supply::default();
TOTAL_SUPPLY.save(deps.storage, &supply_init)?;
Ok(Response::default())
}

In staking contract InstantiateMsg, we set our contract basic info (name and version), initiate default value for the linked-list, CONFIG and TOTAL_SUPPLY storage.

ExecuteMsg
InstantiateMsg is used when you want to interact with the contract in a transaction.

To declare the methods we can call to interact with the contract in a transaction , we put items inside ExecuteMsg enum in msg.rs file:

rust
// file src/msg.rs
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
#[serde(rename_all = “snake_case”)]
pub enum ExecuteMsg {
/// Stake will stake and delegate all native tokens sent with the message and give back stkTokens
Stake {},
/// Claim is used to claim the amount of available native tokens that you previously “unstaked”
Claim {},
/// Admin call this method to set up liquid token address
SetLiquidToken { address: Addr },
/// This accepts a properly-encoded ReceiveMsg from a cw20 contract (to process unstake request)
Receive(Cw20ReceiveMsg),
_ProcessToken { balance_before: Uint128 },
_PerformCheck {},
_MintLiquidToken { receiver: Addr, native_amount: Uint128 },
}

Stake, Claim and SetLiquidToken are the methods that other contracts or wallets can call. Receive(Cw20ReceiveMsg) should only be trigged when someone send cw20 token to the staking contract. _ProcessToken, _PerformCheck, _MintLiquidToken internal methods that can only be called by the staking contract itself.

We map each execution above to a method in theexecute function in contract.rs file:

rust
// file src/contract.rs
#[cfg_attr(not(feature = “library”), entry_point)]
pub fn execute(
deps: DepsMut,
env: Env,
info: MessageInfo,
msg: ExecuteMsg,
) -> Result<Response, ContractError> {
match msg {
ExecuteMsg::Stake {} => execute_stake(deps, env, info),
ExecuteMsg::Claim {} => execute_claim(deps, info),
ExecuteMsg::SetLiquidToken { address } => execute_set_liquid_token(deps, info, address),
ExecuteMsg::Receive(msg) => execute_receive(deps, env, info, msg),
ExecuteMsg::_ProcessToken { balance_before } => _process_token(deps, env, info, balance_before),
ExecuteMsg::_PerformCheck {} => _perform_check(deps, env, info),
ExecuteMsg::_MintLiquidToken { receiver, native_amount } => _mint_liquid_token(deps, env, info, receiver, native_amount),
}
}

ExecuteMsg::Stake
The purpose of this execution is to give the sender the liquid tokens corresponding to his native token amount included in the transaction.

An example to stake CONST token in the testnet — we run this command in staking contract project dir:

$ archway tx — args '{“stake”: {}}' — flags — amount 10000uconst

ExecuteMsg::Stake return the response returned by execute_stake method:

rust
// file src/contract.rs
pub fn execute_stake(deps: DepsMut, env: Env, info: MessageInfo) -> Result<Response, ContractError> {
// ensure we have the proper denom
let config = CONFIG.load(deps.storage)?;
// payment finds the proper coin (or throws an error)
let payment = info
.funds
.iter()
.find(|x| x.denom == config.bond_denom)
.ok_or_else(|| ContractError::EmptyBalance {
denom: config.bond_denom.clone(),
})?;
let contract_addr = env.contract.address;
let msg1 = to_binary(&ExecuteMsg::_PerformCheck {})?;
let msg2 = to_binary(&ExecuteMsg::_MintLiquidToken { receiver: info.sender, native_amount: payment.amount })?;

let res = Response::new()
.add_message(WasmMsg::Execute {
contract_addr: contract_addr.to_string(),
msg: msg1,
funds: vec![],
})
.add_message(WasmMsg::Execute {
contract_addr: contract_addr.to_string(),
msg: msg2,
funds: vec![],
});
Ok(res)
}

Here we load the contract config, check the amount of native token sent to the contract, then call 2 internal methods of the contract:

  • _PerformCheck: claim staking reward, process withdraw queue, then stake available native token.
  • _MintLiquidToken: mint cw20 liquid token to the sender.

ExecuteMsg::Receive(msg)
This execution is trigged by cw20 token contract when someone send cw20 token to the staking contract. The purpose of this execution is to burn liquid token and process unstaking for the token sender.

An example to unstake CONST token in the testnet — we run this command in cw20-base contract project dir:

$ archway tx — args '{“send”: {"contract": "archway19nqdvd7ru6hne0kg82stz6fwhv78plstwmn60hpxcvvkkuvq6csszftw0k", "amount": "100", "msg": "eyJ1bnN0YWtlIjp7fX0="}}'

The contract param should be staking contract address.

ExecuteMsg::Receive(msg) return the response returned by execute_receive method:

rust
// file src/contract.rs
pub fn execute_receive(
deps: DepsMut,
env: Env,
info: MessageInfo,
wrapper: Cw20ReceiveMsg,
) -> Result<Response, ContractError> {
// info.sender is the address of the cw20 contract (that re-sent this message).
// wrapper.sender is the address of the user that requested the cw20 contract to send this.
let config = CONFIG.load(deps.storage)?;
// only allow liquid token contract to call
if info.sender != config.liquid_token_addr {
return Err(ContractError::Unauthorized {});
}
let api = deps.api;
execute_unstake(deps, env, api.addr_validate(&wrapper.sender)?, wrapper.amount)
}

Here we load the contract config, check to make sure the token sent is actually our liquid token, then call execute_unstake function:

rust
// file src/contract.rs
pub fn execute_unstake(
deps: DepsMut,
env: Env,
sender: Addr,
amount: Uint128,
) -> Result<Response, ContractError> {
let contract_addr = env.contract.address;
let config = CONFIG.load(deps.storage)?;
// burn liquid token
let cw20 = Cw20Contract(config.liquid_token_addr.clone());
// Build a cw20 transfer send msg, that send collected funds to target address
let msg1 = cw20.call(Cw20ExecuteMsg::Burn {
amount,
})?;
// put unstaker to unstaking queue, update info
let mut supply = TOTAL_SUPPLY.load(deps.storage)?;
let liquid_supply = get_token_supply(&deps.querier, config.liquid_token_addr)?;
let amount_to_unstake = amount.multiply_ratio(supply.native, liquid_supply);
supply.native = supply.native.checked_sub(amount_to_unstake).map_err(StdError::overflow)?;
supply.unstakings += amount_to_unstake;
TOTAL_SUPPLY.save(deps.storage, &supply)?;
linked_list_append(deps.storage, sender.clone(), amount_to_unstake, env.block.height)?;
UNDER_UNSTAKING.update(
deps.storage,
&sender,
|claimable: Option<Uint128>| -> StdResult<_> { Ok(claimable.unwrap_or_default() + amount_to_unstake) },
)?;
let msg2 = to_binary(&ExecuteMsg::_PerformCheck {})?;

let res = Response::new()
.add_message(msg1)
.add_message(WasmMsg::Execute {
contract_addr: contract_addr.to_string(),
msg: msg2,
funds: vec![],
})
.add_attribute(“action”, “unstake”)
.add_attribute(“from”, sender)
.add_attribute(“amount”, amount);
Ok(res)
}

In this function, we update TOTAL_SUPPLY storage, add new unstaking request to the linked-list, burn the liquid tokens sent to staking contract:

rust

let cw20 = Cw20Contract(config.liquid_token_addr.clone());
// Build a cw20 transfer send msg, that send collected funds to target address
let msg1 = cw20.call(Cw20ExecuteMsg::Burn {
amount,
})?;

let res = Response::new()
.add_message(msg1)

After burning the token, we call _PerformCheck internal method. Here we also log the event for the execution:

rust

let res = Response::new()

.add_attribute(“action”, “unstake”)
.add_attribute(“from”, sender)
.add_attribute(“amount”, amount);
Ok(res)

The transaction will give us this response to represent the event above:

json

{“key”:”_contract_address”,”value”:”archway1h6wc9tk64j9pv3e9exa4fxaa70e2ya5049z05lx9t9hs4wtn0ewske6mcd”},
{“key”:”action”,”value”:”unstake”},{“key”:”from”,”value”:”archway10h2w7jfxtc8m7wvjar9jtufpm3s7srqgqqyjea”},
{“key”:”amount”,”value”:”100"}

ExecuteMsg::Claim
The purpose of this execution is to give the sender the native tokens he can claim after unstaking request is done.

An example to claim CONST token in the testnet — we run this command in staking contract project dir:

$ archway tx — args '{“claim”: {}}'

ExecuteMsg::Claim return the response returned by execute_claim method:

rust
// file src/contract.rs
pub fn execute_claim(
deps: DepsMut,
info: MessageInfo,
) -> Result<Response, ContractError> {
let config = CONFIG.load(deps.storage)?;
let mut to_send:Uint128 = Uint128::zero();
CLAIMABLE.update(
deps.storage,
&info.sender,
|claimable: Option<Uint128>| -> StdResult<_> {
to_send = claimable.unwrap_or_default();
Ok(Uint128::zero())
},
)?;
if to_send.is_zero() {
return Err(ContractError::NothingToClaim {});
}
// update total supply (lower claim)
TOTAL_SUPPLY.update(deps.storage, |mut supply| -> StdResult<_> {
supply.claims = supply.claims.checked_sub(to_send)?;
Ok(supply)
})?;

// transfer tokens to the sender
let res = Response::new()
.add_message(BankMsg::Send {
to_address: info.sender.to_string(),
amount: coins(to_send.u128(), config.bond_denom),
})
.add_attribute(“action”, “claim”)
.add_attribute(“from”, info.sender)
.add_attribute(“amount”, to_send);
Ok(res)
}

Here we load the contract config, check the amount of native token the sender can claim. If sender has nothing to claim, the execution will return a custom error:

rust
if to_send.is_zero() {
return Err(ContractError::NothingToClaim {});
}

We can define the custom error like that in error.rs file:

rust
// file src/error.rs

#[derive(Error, Debug)]
pub enum ContractError {

#[error(“No claims that can be released currently”)]
NothingToClaim {},

}

We send the native tokens to sender and log event in the response:

rust

// transfer tokens to the sender
let res = Response::new()
.add_message(BankMsg::Send {
to_address: info.sender.to_string(),
amount: coins(to_send.u128(), config.bond_denom),
})
.add_attribute(“action”, “claim”)
.add_attribute(“from”, info.sender)
.add_attribute(“amount”, to_send);
Ok(res)

ExecuteMsg::SetLiquidToken
We deploy staking contract then deploy cw20 contract and set staking contract as token minter in the deployment transaction. The cw20 liquid token contract is deployed after deploying staking contract, so we can not add it as a param in staking contract deployment.

The purpose of this execution is for the dev to config the cw20 liquid token address for staking contract.

An example to set cw20 liquid token address in the testnet — we run this command in staking contract project dir:

$ archway tx — args '{"set_liquid_token": {"address": "archway10vvudha5w6rmyx7y2d6uw3jd7auk97zgnvqdnsrptg0t82e7mc5ssudd7q"}}'

ExecuteMsg::SetLiquidToken return the response returned by execute_set_liquid_token method:

rust
// file src/contract.rs
pub fn execute_set_liquid_token(
deps: DepsMut,
info: MessageInfo,
address: Addr,
) -> Result<Response, ContractError> {
let config = CONFIG.load(deps.storage)?;
// only allow owner to call
if info.sender != config.owner {
return Err(ContractError::Unauthorized {});
}
CONFIG.update(deps.storage, |mut config| -> StdResult<_> {
config.liquid_token_addr = address.clone();
Ok(config)
})?;
let res = Response::new()
.add_attribute(“action”, “setLiquidToken”)
.add_attribute(“from”, info.sender)
.add_attribute(“address”, address);
Ok(res)
}

Here we load the contract config, check to make sure the sender is authorized, then update the liquid token address in CONFIG.

ExecuteMsg::_ProcessToken
This execution can only be called by the contract itself.
The purpose of this execution is to process the unstaking queue then stake remain available native token.

ExecuteMsg::_ProcessToken return the response returned by _process_token method:

rust
// file src/contract.rs
pub fn _process_token(
deps: DepsMut,
env: Env,
info: MessageInfo,
balance_before: Uint128,
) -> Result<Response, ContractError> {
// only allow this contract to call itself
if info.sender != env.contract.address {
return Err(ContractError::Unauthorized {});
}
let zero_balance = Uint128::zero();
// check how many available native token we have
let config = CONFIG.load(deps.storage)?;
let mut balance = deps
.querier
.query_balance(&env.contract.address, &config.bond_denom)?;
let claimed_reward = balance.amount.checked_sub(balance_before).map_err(StdError::overflow)?;
// process unstaking queue
let mut supply = TOTAL_SUPPLY.load(deps.storage)?;
supply.native += claimed_reward;
balance.amount = balance.amount.checked_sub(supply.claims).map_err(StdError::overflow)?;
// process withdrawal queue
let unstaking_requests: Vec<NodeWithId> = linked_list_get_list(deps.storage, 50)?;
for request in unstaking_requests {
if balance.amount == zero_balance {
break;
}
let payout: Uint128;
if request.info.value <= balance.amount {
payout = request.info.value;
linked_list_remove_head(deps.storage)?;
} else {
payout = balance.amount;
node_update_value(deps.storage, request.id, request.info.value.checked_sub(payout).map_err(StdError::overflow)?)?;
}
supply.unstakings = supply.unstakings.checked_sub(payout).map_err(StdError::overflow)?;
balance.amount = balance.amount.checked_sub(payout).map_err(StdError::overflow)?;
supply.claims += payout;
CLAIMABLE.update(
deps.storage,
&request.info.receiver,
|claimable: Option<Uint128>| -> StdResult<_> { Ok(claimable.unwrap_or_default() + payout) },
)?;
UNDER_UNSTAKING.update(
deps.storage,
&request.info.receiver,
|unstaking: Option<Uint128>| -> StdResult<_> { Ok(unstaking.unwrap_or_default().checked_sub(payout)?) },
)?;
}
let mut res = Response::new();
// and bond remain available to the validator
if supply.unstakings == zero_balance && balance.amount > zero_balance{
res = res.add_message(StakingMsg::Delegate {
validator: config.validator,
amount: balance.clone(),
})
} else if supply.unstakings > zero_balance && balance.amount == zero_balance {
// unbond if not enough available native token to process unstaking
let bonded = get_bonded(&deps.querier, &env.contract.address)?;
if bonded > supply.native {
let unstake_amount = bonded.checked_sub(supply.native).map_err(StdError::overflow)?;
res = res.add_message(StakingMsg::Undelegate {
validator: config.validator,
amount: coin(unstake_amount.u128(), &config.bond_denom),
})
}
}
TOTAL_SUPPLY.save(deps.storage, &supply)?;
res = res
.add_attribute(“action”, “_processToken”)
.add_attribute(“bonded”, balance.amount);
Ok(res)
}

Here we check to make sure sender is the contract itself, handle the unstaking queue, then bond remain available to the validator:

rust

res = res.add_message(StakingMsg::Delegate {
validator: config.validator,
amount: balance.clone(),
})

Or unbond if not enough available native token to process unstaking:

rust

res = res.add_message(StakingMsg::Undelegate {
validator: config.validator,
amount: coin(unstake_amount.u128(), &config.bond_denom),
})

ExecuteMsg::_PerformCheck
This execution can only be called by the contract itself.
The purpose of this execution is to claim staking reward, process withdraw queue, then stake available native token.

ExecuteMsg::_PerformCheck return the response returned by _perform_check method:

rust
// file src/contract.rs
pub fn _perform_check(deps: DepsMut, env: Env, info: MessageInfo) -> Result<Response, ContractError> {
// only allow this contract to call itself
if info.sender != env.contract.address {
return Err(ContractError::Unauthorized {});
}

let config = CONFIG.load(deps.storage)?;
let balance_before = deps
.querier
.query_balance(&env.contract.address, &config.bond_denom)?.amount;
// claim reward then process available native token
let mut res = Response::new();
// claim staking rewards
let bonded = get_bonded(&deps.querier, &env.contract.address)?;
if bonded > Uint128::zero() {
res = res.add_message(DistributionMsg::WithdrawDelegatorReward {
validator: config.validator,
})
}
// process unstaking queue and available native token
let msg = to_binary(&ExecuteMsg::_ProcessToken { balance_before })?;
res = res.add_message(WasmMsg::Execute {
contract_addr: env.contract.address.to_string(),
msg,
funds: vec![],
})
.add_attribute(“action”, “_performCheck”)
.add_attribute(“balance”, balance_before);
Ok(res)
}

Here we check to make sure sender is the contract itself, claim staking rewards:

rust

res = res.add_message(DistributionMsg::WithdrawDelegatorReward {
validator: config.validator,
})

Then call _ProcessToken internal execution:

rust

let msg = to_binary(&ExecuteMsg::_ProcessToken { balance_before })?;
res = res.add_message(WasmMsg::Execute {
contract_addr: env.contract.address.to_string(),
msg,
funds: vec![],
})

ExecuteMsg::_MintLiquidToken
This execution can only be called by the contract itself.
The purpose of this execution is to mint new cw20 liquid token to native token sender (staker).

ExecuteMsg::_MintLiquidToken return the response returned by _mint_liquid_token method:

rust
// file src/contract.rs
pub fn _mint_liquid_token(
deps: DepsMut,
env: Env,
info: MessageInfo,
receiver: Addr,
native_amount: Uint128,
) -> Result<Response, ContractError> {
// only allow this contract to call itself
if info.sender != env.contract.address {
return Err(ContractError::Unauthorized {});
}
let config = CONFIG.load(deps.storage)?;
// calculate to_mint and update total supply
let mut supply = TOTAL_SUPPLY.load(deps.storage)?;
let liquid_supply = get_token_supply(&deps.querier, config.liquid_token_addr.clone())?;
let to_mint = if liquid_supply.is_zero() {
FALLBACK_RATIO * native_amount
} else {
native_amount.multiply_ratio(liquid_supply, supply.native)
};
supply.native += native_amount;
TOTAL_SUPPLY.save(deps.storage, &supply)?;
let mut res = Response::new()
.add_attribute(“action”, “_mintLiquidToken”)
.add_attribute(“from”, receiver.clone())
.add_attribute(“staked”, native_amount)
.add_attribute(“minted”, to_mint);
// transfer cw20 liquid token to staker
// Cw20Contract is a function helper that provides several queries and message builder.
let cw20 = Cw20Contract(config.liquid_token_addr);
// Build a cw20 transfer send msg, that send collected funds to target address
let msg = cw20.call(Cw20ExecuteMsg::Mint {
recipient: receiver.into_string(),
amount: to_mint,
})?;
res = res.add_message(msg);

Ok(res)
}

Here we check to make sure sender is the contract itself, update TOTAL_SUPPLY storage, then mint cw20 liquid token:

rust

let cw20 = Cw20Contract(config.liquid_token_addr);
// Build a cw20 transfer send msg, that send collected funds to target address
let msg = cw20.call(Cw20ExecuteMsg::Mint {
recipient: receiver.into_string(),
amount: to_mint,
})?;
res = res.add_message(msg);

QueryMsg

QueryMsg is used when you want to interact with the contract in a query (to check the contract storage data without changing it state .

To declare the methods we can call to interact with the contract in a query, we put items inside QueryMsg enum in msg.rsfile:

rust
// file src/msg.rs
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
#[serde(rename_all = “snake_case”)]
pub enum QueryMsg {
/// ClaimableOf shows the number of native tokens the address can claim
ClaimableOf { address: String },
/// ConfigInfo shows the config of the contract
ConfigInfo {},
/// StatusInfo shows staking info of the contract
StatusInfo {},
/// UnstakingQueue shows first 50 nodes in the unstaking queue of the contract
UnstakingQueue {},
/// UnderUnstaking shows the total number of native tokens this address is waiting to be unstaked
UnderUnstakingOf { address: String },
}

We map each query above to a method in query function in contract.rs file:

rust
// file src/contract.rs
#[cfg_attr(not(feature = “library”), entry_point)]
pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult<Binary> {
match msg {
QueryMsg::ClaimableOf { address } => {
to_binary(&query_claimable_of(deps, address)?)
},
QueryMsg::ConfigInfo {} => to_binary(&query_config(deps)?),
QueryMsg::StatusInfo {} => to_binary(&query_status(deps, _env)?),
QueryMsg::UnstakingQueue {} => to_binary(&query_unstaking_queue(deps)?),
QueryMsg::UnderUnstakingOf { address } => {
to_binary(&query_under_unstaking_of(deps, address)?)
},
}
}

QueryMsg::ClaimableOf

Example of query using archwayd:

$ archwayd query wasm contract-state smart archway19nqdvd7ru6hne0kg82stz6fwhv78plstwmn60hpxcvvkkuvq6csszftw0k '{"claimable_of": {"address": "archway10h2w7jfxtc8m7wvjar9jtufpm3s7srqgqqyjea"}}' — node "https://rpc.constantine-1.archway.tech:443"

The purpose of this query is to check the claimable native token of the address param. It return a binary contains the response returned by query_claimable_of method:

rust
// file src/contract.rs
pub fn query_claimable_of(deps: Deps, address: String) -> StdResult<BalanceResponse> {
let address = deps.api.addr_validate(&address)?;
let claimable = CLAIMABLE
.may_load(deps.storage, &address)?
.unwrap_or_default();
Ok(BalanceResponse { balance: claimable })
}

First, we check if the address param is actually a real address with deps.api.addr_validate. If not, the query will return error.

let address = deps.api.addr_validate(&address)?;

After that, we check the claimable amount of the address in CLAIMABLE storage. if the address does not exist in the storage, the Zero default value will be returned.

let claimable = CLAIMABLE
.may_load(deps.storage, &address)?
.unwrap_or_default();

QueryMsg::ConfigInfo
Example of query using archwayd:

$ archwayd query wasm contract-state smart archway19nqdvd7ru6hne0kg82stz6fwhv78plstwmn60hpxcvvkkuvq6csszftw0k '{"config_info": {}}' — node "https://rpc.constantine-1.archway.tech:443"

The purpose of this query is to check the config of the contract. It return a binary contains the response returned by query_config method:

rust
// file src/contract.rs
pub fn query_config(deps: Deps) -> StdResult<ConfigResponse> {
let config = CONFIG.load(deps.storage)?;
let res = ConfigResponse {
owner: config.owner.to_string(),
bond_denom: config.bond_denom,
liquid_token_addr: config.liquid_token_addr.to_string(),
validator: config.validator,
};
Ok(res)
}

Here we load the data inside CONFIG storage then return all of them with `ConfigResponse` defined in msg.rs file:

rust
// file src/msg.rs
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct ConfigResponse {
/// Admin to change config
pub owner: String,
/// This is the denomination we can stake (and only one we accept for payments)
pub bond_denom: String,
/// Liquid token address
pub liquid_token_addr: String,
/// All tokens are bonded to this validator
/// FIXME: address validation doesn’t work for validator addresses
pub validator: String,
}

QueryMsg::StatusInfo
Example of query using archwayd:

$ archwayd query wasm contract-state smart archway19nqdvd7ru6hne0kg82stz6fwhv78plstwmn60hpxcvvkkuvq6csszftw0k '{"status_info": {}}' — node "https://rpc.constantine-1.archway.tech:443"

The purpose of this query is to check the current status of the contract. It return a binary contains the response returned by query_status method:

rust
// file src/contract.rs
pub fn query_status(deps: Deps, _env: Env) -> StdResult<StatusResponse> {
let config = CONFIG.load(deps.storage)?;
let supply = TOTAL_SUPPLY.load(deps.storage)?;
let bonded = get_bonded(&deps.querier, &_env.contract.address).unwrap();
let balance = deps
.querier
.query_balance(&_env.contract.address, &config.bond_denom)?;
let liquid_supply = get_token_supply(&deps.querier, config.liquid_token_addr)?;
let res = StatusResponse {
issued: liquid_supply,
native: coin(supply.native.u128(), &config.bond_denom),
unstakings: supply.unstakings,
claims: supply.claims,
bonded: bonded,
balance: balance.amount,
ratio: if liquid_supply.is_zero() {
FALLBACK_RATIO
} else {
Decimal::from_ratio(supply.native, liquid_supply)
},
};
Ok(res)
}

Here we load the data inside CONFIG and TOTAL_SUPPLY storage, query bonded native token, query native token balance of the contract, query cw20 total supply, then return necessary info with StatusResponse defined in msg.rs file.

QueryMsg::UnstakingQueue
Example of query using archwayd:

$ archwayd query wasm contract-state smart archway19nqdvd7ru6hne0kg82stz6fwhv78plstwmn60hpxcvvkkuvq6csszftw0k '{"unstaking_queue": {}}' — node “https://rpc.constantine-1.archway.tech:443"

The purpose of this query is to check some of the head nodes of the unstaking queue linked-list. It return a binary contains the response returned by query_unstaking_queue method:

rust
// file src/contract.rs
pub fn query_unstaking_queue(deps: Deps) -> StdResult<UnstakingQueueResponse> {
let state = linked_list_read(deps.storage).load()?;
let unstaking_requests: Vec<NodeWithId> = linked_list_get_list(deps.storage, 50)?;
let res = UnstakingQueueResponse {
state,
queue: unstaking_requests,
};
Ok(res)
}

Here we load the data of 50 head nodes in the linked-list, then return them as a list with UnstakingQueueResponse defined in msg.rs file.

QueryMsg::UnderUnstakingOf
Example of query using archwayd:

$ archwayd query wasm contract-state smart archway19nqdvd7ru6hne0kg82stz6fwhv78plstwmn60hpxcvvkkuvq6csszftw0k '{"under_unstaking_of": {"address": "archway10h2w7jfxtc8m7wvjar9jtufpm3s7srqgqqyjea"}}' — node "https://rpc.constantine-1.archway.tech:443"

The purpose of this query is to check the total amount of native token of an address that inside the unstaking queue linked-list. It return a binary contains the response returned by query_under_unstaking_of method:

rust
// file src/contract.rs
pub fn query_under_unstaking_of(deps: Deps, address: String) -> StdResult<BalanceResponse> {
let address = deps.api.addr_validate(&address)?;
let unstaking = UNDER_UNSTAKING
.may_load(deps.storage, &address)?
.unwrap_or_default();
Ok(BalanceResponse { balance: unstaking })
}

First, we check if the address param is actually a real address.
After that, we check the under unstaking amount of the address in UNDER_UNSTAKING storage. If the address does not exist in the storage, the Zero default value will be returned.

Demo

We have put together a web app to exercise the above staking contract, please visit: https://liquid-staking.vercel.app/ and log in via Keplr wallet. The application is currently hosted on the Constantine testnet, you can request for testnet tokens on the Archway discord #faucet channel.

--

--