Archway Dapp Guide — Liquid Staking Contract
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 cw20
token 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.toml
file 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-std
to include staking module to our contract. We also use cw20
here to get a lot of helpers for interacting with cw20
contract.
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 LinkedList
struct 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 Node
struct 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 node
function. We also create a node_read
function 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 tokenreceiver
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 withNode
data.node_update_value
: the function to update nodevalue
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.
ExecuteMsgInstantiateMsg
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
: mintcw20
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.rs
file:
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.
Lydia Labs
Website: https://lydialabs.xyz/
Twitter: https://twitter.com/lydia_labs
Medium: https://medium.com/@lydialabs