🚧 Build a smart contract on the Cudos network: Part 3 — main business logic 🕸️

CUDOS
CUDOS
Published in
13 min readNov 25, 2021
Build on Cudos

Introduction

In the first part of our series, we discussed how to set up the CosmWasm environment and the architecture for the CosmWasm contract. In the second, we discussed the process for setting up a project. Here, we are going to discuss the main business logic, as well as the functions that enable you to perform some computation and store values inside states.

Also in this series:
Build a smart contract on the Cudos network: Part 1: preparation, Part 2: set up a project

Contract.rs

Contract.rs is the soul and heart of the CosmWasm contract because it contains the business logic.

Just a recap of what you’ve learned so far:

In errors, you define different types of errors that you use inside the main logic.
In state.rs, you define the global variables our contract requires.

In msg.rs, you define what our messages look like and what are the different names of messages.

In lib.rs, you define all files which our library needs.

What’s left now is to use all the above information and create functions which are called by given msg where you can perform some computation and store this value inside a state, and if something is not right, then throw an error.

Instantiate function:

#[cfg_attr(not(feature = “library”), entry_point)]
pub fn instantiate(
mut deps: DepsMut,
env: Env,
info: MessageInfo,
msg: InstantiateMsg,
) -> Result<Response, ContractError> {
set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?;
// check valid token info
msg.validate()?;
// create initial accounts
let total_supply = create_accounts(&mut deps, &msg.initial_balances)?;

// store token info
let data = TokenInfo {
name: msg.name,
symbol: msg.symbol,
decimals: msg.decimals,
total_supply,
minter: info.sender,
};
TOKEN_INFO.save(deps.storage, &data)?;
LAST_DISTRIBUTE_TIME.save(deps.storage, &env.block.time)?;

Ok(Response::default())
}

  • #[cfg_attr(not(feature = “library”), entry_point)]
  • Cfg_attr: Conditionally compiled source code is source code that may or may not be considered a part of the source code depending on certain conditions.
  • Entry_point: This attribute macro generates the boilerplate required to call into the contract-specific logic from the entry points to the Wasm module. It should be added to the contract’s init, handle, migrate, and query implementations like this.
  • pub fn instantiate(mut deps: DepsMut, env: Env, info: MessageInfo, msg: InstantiateMsg,) -> Result<Response, ContractError>:
  • Instantiate function is public

You will receive four parameters:

  • Depsmut: a name given to the dependency which contains instances of storage, api, and querier.
  • Storage: Storage provides read and write access to persistent storage. You can think of it as an HDD, while state.rs is the schema of the DB.
  • Api: Api is a callback to system functions implemented outside of the wasm modules. Currently, supports address conversions like human to canonical and vice versa
  • Querier: Makes the query and parses the response. It also handles custom queries, so you need to specify the custom query type in the function parameters.
  • Env: contains block and contract-related data.
  • Block: contains height, time, and chain_id.
  • Contract: only contains the address of a contract
  • Info: contains sender address and the amount which sender sends to the contract.
  • Msg: contains parameters sent by the sender to instantiate the contract.

Let’s look into the instantiate function code

  1. Set_contract_version:

a. Here you store the contract name and contract version, that you’ll define outside of the contract.

b. Also, this function is coming from cw2, another package present inside a package/ directory.

c. Also, you will store this inside a storage contract.

2. msg.validate()?: msg is of type InstantiateMsg present in msg.rs. This msg has a trait that implements a validate function that checks and validates all the attributes present inside an instantiateMsg.

3. Then you have a function create_account which is responsible for assigning a balance to the given address mentioned on initial_balance of InstantiateMsg.

a. create_account:

pub fn create_accounts(deps: &mut DepsMut, accounts: &[Cw20Coin]) -> StdResult<Uint128> {
let mut total_supply = Uint128::zero();
for row in accounts {
let address = deps.api.addr_validate(&row.address)?;
BALANCES.save(deps.storage, &address, &row.amount)?;
total_supply += row.amount;
}
Ok(total_supply)
}

Here you can pass two parameters.

  • Deps: it is a reference to mutable DepsMut. You pass it here because you need to store the balance on storage which is present in DepsMut.
    Trivia: DepsMut is always used with the prefix mut because the full name DepsMut is dependency Mutable.
  • Accounts: the second value is an account that is a reference of Cw20Coin containing address and the value is used to give a CW20 token to some address at the time of initialisation before starting minting to the public.

Let’s look into the code and how you can do it:

  • You have storage and a list of addresses and amounts you need to assign.
  • You create a new variable total_supply which will give you the sum of amounts you assign to each account.
  • To calculate the sum of the amount and assign each address with respect to amount.
  • You will run a for loop up to the length of accounts
  • You validate the address using deps.api.addr_validate. Apart from validation, it changes the account from String to Addr type.
  • If it is not validated, then the function throws an error and the code breaks.
  • If it is valid, then you created BALANCE which is a mapping responsible for maintaining the balance of each address, and you call BALANCE.save(deps.storage, account, amount)
  • deps.storage is storage where the contract stores everything
  • Balance mapping requires an address object as key and Uint128 as a value. So you have an account as address and amount as Uint128.
  • Also, after storing the amount with respect to account, you will add this amount to totalSupply
  • After completing the loop, you return total_supply, but here you don’t return the result directly but wrap the total_supply with Ok(T), which wrapped the object of T with the class of StdResult.

a. After returning a total_supply from create_account you will create a TokenInfo object which contains name, symbol, decimal, total_supply, and minter_info.

b. So you have all this data coming from msg and info. Msg contains name, symbol, decimal, and info containing the sender_address (here you want the one who deployed the contract to automatically become the minter of the contract) and total_supply is coming from create_account.

5. After creating a token_info object, you will store this object on storage. If you remember in state.rs, you have a state with TOKEN_INFO. You will take this state and call the save method for storing the data inside the TOKEN_INFO. For that you require deps.storage (because it is the HDD of the smart contract) and reference to the actual token_info object you created in the previous step.

6. You also need to track the time of LAST_DISTRIBUTED_REWARDS, but at the time of deployment, you just store the current time because from now onwards, you calculate rewards based on this time.

a. Analogy: Whenever you buy a new watch, you always need to set a time and then only you can use it as a watch.

7. Ok(Response:default()) because the return type of function is Result<Response, ContractError> which means if everything is alright, you will return a Response, and if something is wrong then ContractError.

So let’s move into…

ExecuteMsg functions

execute function signature should remain the same no change at all.

Here, you define all the write-only functions that will change the state and require some gas to do computation.

#[cfg_attr(not(feature = “library”), entry_point)]
pub fn execute(
deps: DepsMut,
env: Env,
info: MessageInfo,
msg: ExecuteMsg,
) -> Result<Response, ContractError> {
match msg {
ExecuteMsg::Transfer { recipient, amount } => {
execute_transfer(deps, env, info, recipient, amount)
}
ExecuteMsg::Burn { amount } => execute_burn(deps, env, info, amount),
ExecuteMsg::Mint { recipient, amount } => execute_mint(deps, env, info, recipient, amount),
ExecuteMsg::IncreaseAllowance {
spender,
amount,
expires,
} => execute_increase_allowance(deps, env, info, spender, amount, expires),
ExecuteMsg::DecreaseAllowance {
spender,
amount,
expires,
} => execute_decrease_allowance(deps, env, info, spender, amount, expires),
ExecuteMsg::TransferFrom {
owner,
recipient,
amount,
} => execute_transfer_from(deps, env, info, owner, recipient, amount),
ExecuteMsg::BurnFrom { owner, amount } => execute_burn_from(deps, env, info, owner, amount),
ExecuteMsg::CreateStake { amount } => execute_create_stake(deps, env, info, amount),
ExecuteMsg::RemoveStake { amount } => execute_remove_stake(deps, env, info, amount),
ExecuteMsg::DistributRewards {} => execute_distribut_reward(deps, env, info),
ExecuteMsg::WithdrawRewards {} => execute_withdraw_rewards(deps, env, info),
}
}

  1. Already explained above about usage of #[cfg_attr(not(feature = “library”), entry_point)]
  2. Also explained usage depsMut, env, info and msg in above sections.
  3. A match is just like a switch statement if the given value is equal to a certain case then it will run that case.
  4. I will deep dive into two execute functions

Mint

a. ExecuteMsg::Mint { recipient, amount } => execute_mint(deps, env, info, recipient, amount): in ExecuteMsg enum, Mint will require two param recipients to whom you mint the CW20 token and of what amount.

  • Here execute_mint returns Result<Response, ContractError> because if you look into the execute function mentioned above its return signature is the same as execute_mint means you need to return the same object.

b. Once msg contain ExecuteMsg::Mint { recipient, amount } then call a new function execute_mint(deps, env, info, recipient, amount)

pub fn execute_mint(
deps: DepsMut,
_env: Env,
info: MessageInfo,
recipient: String,
amount: Uint128,
) -> Result<Response, ContractError> {
let config = TOKEN_INFO.load(deps.storage)?;
if info.sender != config.minter {
return Err(ContractError::Unauthorized {});
}

mint_tokens(deps, recipient.clone(), amount)?;
let res = Response::new()
.add_attribute(“action”, “mint”)
.add_attribute(“to”, recipient)
.add_attribute(“amount”, amount);
Ok(res)
}

c. Here if you look first you are getting TOKEN_INFO data because here you stored minter address value. After all, you are allowing the minter address to mint new tokens. In the next line, if stmt you’ll find that you are comparing the current caller address with config.minter if it is not equal then throwing an error Unauthorized which you define inside the error.rs

d. If the caller address is a minter address then call the mint_tokens(deps, recipient.clone(), amount)? (‘?’ at end of mint_tokens because if some error occurred inside mint_tokens then return that error to the user).

e. Let’s ignore the mint_tokens implementation for now and for now it will mint a given amount of tokens to the user.

f. After minting tokens, you need to create a response object for successful minting.

  1. Why do you need a Response?
  2. They facilitate communication between smart contracts and their user interfaces. In traditional web development, a server response is provided in a callback to the frontend.

g. In the end, you return the res by wrapping inside the Ok()

h. Now, have a look into minter_tokens()

  1. I pass a depsMut, recipient, and amount
  2. After that, load the TOKEN_INFO from storage into mutable variable config.
  3. Validate amount if it is greater than zero
  4. Updated the amount in config.total_supply because it contains the current total_supply.
  5. Store the update total_supply back into TOKEN_INFO.
  6. Validate the recipient address also this addr_validate the String to Addr type
  7. This is one of the main codes which you need to understand. You need to update the balance of the recipient to update it call Balances.update with the following data:

i. Deps.storage: because this is the place where all our contract data resides.

ii. recipient(key): here recipient acts as a key to the mapping Balances.

iii. Update logic: in update logic, you receive the previous/old object. So in the case of Balance, it is Uint128 (but you are wondering why it is wrapped inside an Option enum?)

  1. Because it is sure a Uint128 if it is an existing recipient address ie.. recipient already have CW20 tokens
  2. What if a new user wants to have CW20 tokens? In that case the value of that user is null or undefined so to handle that case you need to use Option enums.
  3. Option enum gives Some and None
  4. Some is used when you found an old data
  5. None is used when you don’t have any existing data.
  6. balance.unwrap_or_default() is used when if data found it will return that value but if it is not found then return a zero/default.

fn mint_tokens(deps: DepsMut, recipient: String, amount: Uint128) -> Result<bool, ContractError> {
let mut config = TOKEN_INFO.load(deps.storage)?;
if amount == Uint128::zero() {
return Err(ContractError::InvalidZeroAmount {});
}

// update supply and enforce cap
config.total_supply += amount;
TOKEN_INFO.save(deps.storage, &config)?;

// add amount to recipient balance
let rcpt_addr = deps.api.addr_validate(&recipient)?;
BALANCES.update(
deps.storage,
&rcpt_addr,
|balance: Option<Uint128>| -> StdResult<_> { Ok(balance.unwrap_or_default() + amount) },
)?;
Ok(true)
}

RemoveStake

a. ExecuteMsg::RemoveStake { amount } => execute_remove_stake(deps, env, info, amount): in ExecuteMsg enum, RemoveStake will require one parameter amount. It is used to remove amount(s) from staking.

pub fn execute_remove_stake(
deps: DepsMut,
env: Env,
info: MessageInfo,
amount: Uint128,
) -> Result<Response, ContractError> {
if amount == Uint128::zero() {
return Err(ContractError::InvalidZeroAmount {});
}
STAKE_HOLDERS.update(
deps.storage,
&info.sender,
|d: Option<StakeHolderInfo>| -> Result<StakeHolderInfo, ContractError> {
match d {
Some(one) => {
if one.amount.lt(&amount) {
return Err(ContractError::Unauthorized {});
} else {
Ok(StakeHolderInfo {
amount: one.amount.sub(amount),
rewards: one.rewards
})
}
}
None => Err(ContractError::Unauthorized {}),
}
},
)?;
TOTAL_STAKE.update(deps.storage, |mut info| -> StdResult<Uint128> {
info = info.checked_sub(amount).unwrap();
Ok(info)
})?;
update_balance_sender_to_recipient(deps, &env.contract.address, &info.sender, amount)?;
let res = Response::new()
.add_attribute(“action”, “remove stake”)
.add_attribute(“from”, env.contract.address)
.add_attribute(“to”, info.sender)
.add_attribute(“amount”, amount);
Ok(res)
}

b. First, validate whether the amount is zero or not.

c. After that, use the STAKE_HOLDER.update method to update the state. As you know, STAKE_HOLDER is a map so it requires a key to find a value so in this case, caller address is a key

  1. Update Logic: in update logic, get Option<StakeHolderInfo> and return a Result<StakeHolderInfo, ContractError>
  2. First, match data with Option enum ie… Some and None.
  3. If data is found then go into the Some. In Some, you check that the staked amount is less than the amount the user wants to remove. If yes then throw an error. If not, then subtract that amount from the stake Amount.
  4. If no data is found, then throw an error saying “unauthorized”.

d. After Removing the stake from the stakeholder’s balance you need to remove the stake from the total_staked amount which is stored inside the TOTAL_STAKE state.

e. Up until this point, you have removed staked amount from stake_holder mapping and total_stake item, but CW20 tokens still belong to the contract address now.

f. In this step, you will transfer CW20 tokens back to the caller from the contract address.

  1. I will call the update_balance_sender_to_recipient with deps, env.contract.address, info.sender.address, amount
  2. update_balance_sender_to_recipient to recipient transfer balance from contract address(sender) to caller(recipient)

g. Let’s ignore the update_balance_sender_to_recipient implementation for now and for now the amount is transferred from contract to caller.

h. At the end, return the res by wrapping inside the Ok()

i. Now, have a look into update_balance_sender_to_recipient ()

fn update_balance_sender_to_recipient(
deps: DepsMut,
from: &Addr,
to: &Addr,
amount: Uint128,
) -> Result<Uint128, StdError> {
BALANCES.update(
deps.storage,
&from,
|balance: Option<Uint128>| -> StdResult<_> {
Ok(balance.unwrap_or_default().checked_sub(amount)?)
},
)?;
BALANCES.update(
deps.storage,
&to,
|balance: Option<Uint128>| -> StdResult<_> { Ok(balance.unwrap_or_default() + amount) },
)?;
Ok(amount)
}

Here, you need to deduct a balance from the sender address and updating it to the recipient address.

QueryMsg functions

query function signature should remain the same no change at all.

Here, you define all the read functions in which no gas is used.

#[cfg_attr(not(feature = “library”), entry_point)]
pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult<Binary> {
match msg {
QueryMsg::Balance { address } => to_binary(&query_balance(deps, address)?),
QueryMsg::TokenInfo {} => to_binary(&query_token_info(deps)?),
QueryMsg::Minter {} => to_binary(&query_minter(deps)?),
QueryMsg::Allowance { owner, spender } => {
to_binary(&query_allowance(deps, owner, spender)?)
}
QueryMsg::AllAllowances {
owner,
start_after,
limit,
} => to_binary(&query_all_allowances(deps, owner, start_after, limit)?),
QueryMsg::AllAccounts { start_after, limit } => {
to_binary(&query_all_accounts(deps, start_after, limit)?)
}
QueryMsg::AllStakeHolders { start_after, limit } => {
to_binary(&query_all_stake_holders(deps, start_after, limit)?)
}
QueryMsg::TotalStake {} => to_binary(&query_total_stake(deps)?),
QueryMsg::StakeAndRewardOf { account } => {
to_binary(&query_stake_and_reward_of(deps, account)?)
}
}
}

  1. Check the return type of query it is StdResult(Binary) so all functions inside queryMsg should return StdResult(Binary).
  2. If you look into params of query, deps is non-mutable that you can only read from storage.
  3. I will deep dive into two query functions:

Balance:

a. QueryMsg::Balance { address } => to_binary(&query_balance(deps, address)?): this function returns CW20 token balance of address.

b. I am using the to_binary method to convert the response coming from the function into binary.

pub fn query_balance(deps: Deps, address: String) -> StdResult<BalanceResponse> {
let address = deps.api.addr_validate(&address)?;
let balance = BALANCES
.may_load(deps.storage, &address)?
.unwrap_or_default();
Ok(BalanceResponse { balance })
}

c. Now look into query_balance function takes two argument deps and address

d. First, validate the address for two reasons. The first is to check whether it is a valid cudo address. The second is to convert the string to addr type.

e. After that, fetch the balance from BALANCES mapping by using may_load function.

f. may_load will parse the data stored at the key if present, returns Ok(None) if no data there.

g. As mentioned above in may_load it may return None. To handle the None condition used unwrap_or_default().

AllStakeHolders

a. QueryMsg::AllStakeHolders { start_after, limit }: this function is used to return a paginated list of AllStakeHolders.

b. Params start_after(Option<String>) and limit(Option<Uint128>) means they are optional if None is passed then that value is undefined or optional.

c. Now let’s look into the query_all_stake_holders function

d. First, prepare a limit value:

i. Unwrap_or ensures if None is passed then it will set the DEFAULT_LIMIT

ii. After that make sure it DEFAULT_LIMIT is less than MAX_LIMIT;

e. After that, prepare a start_after value:

i. Bound exclusive means it is similar to the skip feature.

f. After preparing Pagination conditions, to fetch all the stakeHolder account info:

i. First, need to fetch all the keys with the filter of start_after STAKE_HOLDERS.keys(deps.storage, start, None, Order::Ascending) in ascending order

ii. After that, you have added a map because keys in STAKE_HOLDERS state is of type Addr and you need keys in String that’s why added a String::from_utf8

iii. After fetching all this data, you need to implement the limit, so for that use the take function

iv. In the end, to collect everything into a vector, use collect.

pub fn query_all_stake_holders(
deps: Deps,
start_after: Option<String>,
limit: Option<u32>,
) -> StdResult<AllStakeHolderResponse> {
let limit = limit.unwrap_or(DEFAULT_LIMIT).min(MAX_LIMIT) as usize;
let start = start_after.map(Bound::exclusive);

let accounts: Result<Vec<_>, _> = STAKE_HOLDERS
.keys(deps.storage, start, None, Order::Ascending)
.map(String::from_utf8)
.take(limit)
.collect();
Ok(AllStakeHolderResponse {
accounts: accounts?,
})
}

In the 4th and final part of this series, we are going to carry out unit and integration testing by applying what we have learned so far. We will also be trying a few functions to test our understanding of the entire process.

What can you do right now?

You can get involved right away by joining our incentivised testnet Project Artemis by following the links below:

Notably, you’ll receive rewards based on the tasks you complete as part of the testnet.

P.S. If you’ve already bought your CUDOS tokens, you can make the most of them by staking them on our platform to secure the network and, in return, receive rewards.

About Cudos

The Cudos Network is a layer-one blockchain and layer-two computation and oracle network designed to ensure decentralized, permissionless access to high-performance computing at scale and enables scaling of computing resources to hundreds of thousands of nodes. Once bridged onto Ethereum, Algorand, Polkadot and Cosmos, Cudos will enable scalable computational and layer-two oracles on all of the bridged blockchains.

For more, please visit:
Website, Twitter, Telegram, YouTube, Discord, Medium

--

--

CUDOS
CUDOS
Editor for

CUDOS is powering AI by uniting blockchain and cloud computing to realise the vision of a sustainable, equitable, and democratised Web3.