Asset FT Extension

The tutorial provides an example of how to develop, deploy and use WASM contracts to be used as fungible smart token extension.

Prerequisites

To complete this tutorial, you need to:

  • Install rust and cargo.
  • Be familiar with the Rust programming language.
  • Have a general understanding of how the Coreum blockchain works.
  • Follow the instruction to install cored binary.
  • Install the required util: jq.
  • Set the network variables for the development (testnet is preferable).

Source Code

The source code is located here. You can see different examples by checking out different branches of this repository.

Getting Started

  • Clone the smart contract template
git clone https://github.com/CoreumFoundation/tutorials.git
  • Go to the contract directory.
cd tutorials/wasm/extension
  • Generate a new account
cored keys add ft-admin $COREUM_CHAIN_ID_ARGS
cored keys add ft-receiver-1 $COREUM_CHAIN_ID_ARGS
cored keys add ft-receiver-2 $COREUM_CHAIN_ID_ARGS
  • Fund account

Use the faucet to fund your account

  • Export commonly used, throughout the tutorial, environment variables.
export FT_ADMIN=$(cored keys show ft-admin --address $COREUM_CHAIN_ID_ARGS)
export FT_RECEIVER_1=$(cored keys show ft-receiver-1 --address $COREUM_CHAIN_ID_ARGS)
export FT_RECEIVER_2=$(cored keys show ft-receiver-2 --address $COREUM_CHAIN_ID_ARGS)
  • Check the balance
cored q bank balances $FT_ADMIN --denom $COREUM_DENOM $COREUM_NODE_ARGS $COREUM_CHAIN_ID_ARGS

Build contract

  • Build the contract
docker run --rm -v "$(pwd)":/code \
  --mount type=volume,source="$(basename "$(pwd)")_cache",target=/target \
  --mount type=volume,source=registry_cache,target=/usr/local/cargo/registry \
  cosmwasm/optimizer:0.15.0

This operation might take a significant amount of time.

Deploy contract

  • Deploy the built artifact.
RES=$(cored tx wasm store artifacts/extension.wasm \
    --from $FT_ADMIN --gas auto --gas-adjustment 1.4 -y -b block --output json $COREUM_NODE_ARGS $COREUM_CHAIN_ID_ARGS)
echo $RES
CODE_ID=$(echo $RES | jq -r '.logs[0].events[-1].attributes[-1].value')
echo "Code ID: $CODE_ID"
  • Check the deployed code.
cored q wasm code-info $CODE_ID $COREUM_NODE_ARGS $COREUM_CHAIN_ID_ARGS

In order to use the deployed contract as an asset extension, you don't need to instantiate it. It will be instantiated automatically when issuing the token.

Issue token

  • Issuing the smart token with extension

Now we have a wasm smart contract that can be used as an extension to a smart token. We can issue a new token like explain in Create and Manage FT with CLI but pass additional params for the extension.

Use the following command to issue your FT with extension:

cored tx assetft issue MYFT cmyft 2 100000000 "My FT token with extension" --from $FT_ADMIN --features=burning,freezing,minting,whitelisting,extension --burn-rate=0.02 --send-commission-rate=0.03 --extension_code_id=$CODE_ID --extension_label=my-extension $COREUM_NODE_ARGS $COREUM_CHAIN_ID_ARGS -y -b block --gas auto --gas-adjustment 1.4
  • On token instantiation we can set custom parameters we allow to set in the contract related to extension.

  • extension_code_id - CodeID of the deployed wasm contract

  • extension_label - optional label to assign to the extension

  • extension_funds - optional funds to transfer to the contract when instantiating the token

  • extension_issuance_msg - optional json encoded data to pass to WASM on instantiation by the ft issuer

  • Build denom and query supply.

export FT_DENOM=cmyft-$FT_ADMIN
cored q bank total --denom $FT_DENOM $COREUM_NODE_ARGS $COREUM_CHAIN_ID_ARGS
# amount: "100000000"
cored q bank denom-metadata --denom $FT_DENOM $COREUM_NODE_ARGS $COREUM_CHAIN_ID_ARGS
cored q assetft token $FT_DENOM $COREUM_NODE_ARGS $COREUM_CHAIN_ID_ARGS
  • Capture the contract address.
RES=$(cored q assetft token $FT_DENOM $COREUM_NODE_ARGS $COREUM_CHAIN_ID_ARGS --output json)
EXTENSION_ADDR=$(echo $RES | jq -r '.token.extension_cw_address')
echo "Extension Address: $EXTENSION_ADDR"

Interactions with the extension

When we issue a token with extension, every bank transfer of this token will be intercepted and instead of the recipient, it will be sent to the smart contract used as asset extension and then the contract decides what to do with it.

The smart contract may decide to send the tokens to the recipient or not, return it to the sender, send it to multiple accounts, burn a part of it, mint some more coins to send as a reward, apply features like freezing, whitelisting, commission rate, burn rate or any custom logic. We will implement some of these ideas, but first, let's understand the flow.

Let's say alice sends 100$FT_DENOM to bob. This transaction will be intercepted and transferred to $EXTENSION_ADDR. Then sudo entrypoint of the contract will be called with ExtensionTransfer message.

Code:

#[entry_point]
pub fn sudo(deps: DepsMut<CoreumQueries>, env: Env, msg: SudoMsg) -> CoreumResult<ContractError> {
    match msg {
        SudoMsg::ExtensionTransfer {
            sender, recipient, transfer_amount, commission_amount, burn_amount, context,
        } => sudo_extension_transfer(
            deps, env, transfer_amount, sender, recipient, commission_amount, burn_amount, context,
        ),
    }
}

In the code snippet above in the SudoMsg enum, we declare the function to be called when a bank transfer is initiated. The sudo function marked with entry_point macro routes them to the proper handlers. The sudo_extension_transfer function is the handler for the ExtensionTransfer message, that can decide what to do when a bank send is intercepted.

Here we receive ExtensionTransfer message and call sudo_extension_transfer function with parameters described below:

  • deps: The CosmWasm's interface to access storage, api and querier
  • env: The CosmWasm's interface to access block info, the transaction which this message is a part of, and the contract info
  • sender: The original transfer's sender, which is alice in our example
  • recipient: The original transfer's recipient, which is bob in our example
  • transfer_amount: The 100$FT_DENOM tokens that was sent from alice to bob
  • commission_amount: The commission amount calculated based on the commission rate defined when issuing the token, which is 2$FT_DENOM in our example
  • burn_amount: The burn amount calculated based on the burn rate defined when issuing the token, which is 3$FT_DENOM in our example
  • context: Contextual information including ibc_purpose (whether the transaction is and ibc in, out, ack, timeout or none) and the indicator whether the sender, the receiver or both are smart contracts

Now, let's implement some of those ideas, starting with the most basic one to just keep the amount in the extension address. As described before, the coins are first transferred to the extension, then it should decide. It can just decide to do nothing. The template code that we deployed is going to do just that.

Code:

pub fn sudo_extension_transfer(
    _deps: DepsMut<CoreumQueries>,
    _env: Env,
    _amount: Uint128,
    _sender: String,
    _recipient: String,
    _commission_amount: Uint128,
    _burn_amount: Uint128,
    _context: TransferContext,
) -> CoreumResult<ContractError> {
    Ok(Response::new())
}

We can test it by sending some about to an account. The account won't receive it, but the extension will have it instead.

cored tx bank send $FT_ADMIN $FT_RECEIVER_1 10$FT_DENOM -y -b block $COREUM_NODE_ARGS $COREUM_CHAIN_ID_ARGS
  • Check balance
cored q bank balances $FT_ADMIN --denom $FT_DENOM $COREUM_NODE_ARGS $COREUM_CHAIN_ID_ARGS
# amount: "99999988"
cored q bank balances $FT_RECEIVER_1 --denom $FT_DENOM $COREUM_NODE_ARGS $COREUM_CHAIN_ID_ARGS
# amount: "0"
cored q bank balances $EXTENSION_ADDR --denom $FT_DENOM $COREUM_NODE_ARGS $COREUM_CHAIN_ID_ARGS
# amount: "12"

A simple bank transfer.

Checkout extension/simple branch to see code for this example.

Now let's change the code to send the amount as it should be without the extension like a normal bank transfer. With our current setup, we only need to modify sudo_extension_transfer function to achieve what we want.

Code:

pub fn sudo_extension_transfer(
    deps: DepsMut<CoreumQueries>,
    _env: Env,
    amount: Uint128,
    _sender: String,
    recipient: String,
    _commission_amount: Uint128,
    _burn_amount: Uint128,
    _context: TransferContext,
) -> CoreumResult<ContractError> {
    let denom = DENOM.load(deps.storage)?;

    let transfer_msg = cosmwasm_std::BankMsg::Send {
        to_address: recipient.to_string(),
        amount: vec![Coin { amount, denom }],
    };

    let response = Response::new()
        .add_attribute("method", "execute_transfer")
        .add_message(transfer_msg);

    Ok(response)
}
  • Build the contract again and deploy the built artifact.
RES=$(cored tx wasm store artifacts/extension.wasm \
    --from $FT_ADMIN --gas auto --gas-adjustment 1.4 -y -b block --output json $COREUM_NODE_ARGS $COREUM_CHAIN_ID_ARGS)
echo $RES
CODE_ID=$(echo $RES | jq -r '.logs[0].events[-1].attributes[-1].value')
echo "Code ID: $CODE_ID"

We can either issue a new token with our new extension, or we can migrate the extension code of our previous token.

  • Migrate the extension code.
cored tx wasm migrate $EXTENSION_ADDR $CODE_ID '{}' --from $FT_ADMIN -y -b block $COREUM_NODE_ARGS $COREUM_CHAIN_ID_ARGS

Now if we send some amount, it will be transferred normally.

cored tx bank send $FT_ADMIN $FT_RECEIVER_1 10$FT_DENOM -y -b block $COREUM_NODE_ARGS $COREUM_CHAIN_ID_ARGS
  • Check balance
cored q bank balances $FT_ADMIN --denom $FT_DENOM $COREUM_NODE_ARGS $COREUM_CHAIN_ID_ARGS
# amount: "99999976"
cored q bank balances $FT_RECEIVER_1 --denom $FT_DENOM $COREUM_NODE_ARGS $COREUM_CHAIN_ID_ARGS
# amount: "10"

We see that 10 coins are transferred from $FT_ADMIN to $FT_RECEIVER_1.

Reject some transactions.

Checkout extension/reject branch to see code for this example.

Let's add an arbitrary rule to reject any bank transfer, sending 7 coins.

Code:

pub fn sudo_extension_transfer(
    deps: DepsMut<CoreumQueries>,
    _env: Env,
    amount: Uint128,
    _sender: String,
    recipient: String,
    _commission_amount: Uint128,
    _burn_amount: Uint128,
    _context: TransferContext,
) -> CoreumResult<ContractError> {
    let denom = DENOM.load(deps.storage)?;

    if amount == Uint128::new(7) {
        return Err(ContractError::Std(StdError::generic_err(
            "7 is not allowed",
        )));
    }

    let transfer_msg = cosmwasm_std::BankMsg::Send {
        to_address: recipient.to_string(),
        amount: vec![Coin { amount, denom }],
    };

    let response = Response::new()
        .add_attribute("method", "execute_transfer")
        .add_message(transfer_msg);

    Ok(response)
}
  • Build the contract, deploy the built artifact and migrate the extension code again.

Now if we send exactly 7 coins, it will be rejected with 7 is not allowed error.

cored tx bank send $FT_ADMIN $FT_RECEIVER_1 7$FT_DENOM -y -b block $COREUM_NODE_ARGS $COREUM_CHAIN_ID_ARGS
# 7 is not allowed
  • Check balance
cored q bank balances $FT_ADMIN --denom $FT_DENOM $COREUM_NODE_ARGS $COREUM_CHAIN_ID_ARGS
# amount: "99999976"
cored q bank balances $FT_RECEIVER_1 --denom $FT_DENOM $COREUM_NODE_ARGS $COREUM_CHAIN_ID_ARGS
# amount: "10"

Since the transaction is rejected, we can see that the balance of none of the accounts are changed.

Custom whitelisting.

Checkout extension/whitelist branch to see code for this example.

Let's say we already have one coin with whitelisting feature with many whitelisted accounts, now we want to issue a new coin, but we don't want to whitelist those accounts again for this new coin. We can write a smart contracts that checks whitelisting for both coins when some account transfer some token of this new coin.

We need to change our smart contract to add whitelisting feature to it. Just for demonstration purposes, let's enable the whitelisting assertions only of the token is issued with --features=whitelisting.

In this special case, we want to receive denom of the parent token if there is any and also store it, so we need to expand IssuanceMsg struct and also add a new state item.

pub struct IssuanceMsg {
    pub parent_denom: Option<String>,
}
pub const PARENT_DENOM: Item<String> = Item::new("parent_denom");

We need to store the denom in the instantiate function.

PARENT_DENOM.save(
    deps.storage,
    &msg.issuance_msg
        .parent_denom
        .unwrap_or_default(),
)?;

Now we have everything needed to implement whitelisting in sudo_extension_transfer function.

Code:

pub fn sudo_extension_transfer(
    deps: DepsMut<CoreumQueries>,
    _env: Env,
    amount: Uint128,
    _sender: String,
    recipient: String,
    _commission_amount: Uint128,
    _burn_amount: Uint128,
    _context: TransferContext,
) -> CoreumResult<ContractError> {
    let denom = DENOM.load(deps.storage)?;
    let token = query_token(deps.as_ref(), &denom)?;

    if let Some(features) = &token.features {
        if features.contains(&assetft::WHITELISTING) {
            assert_whitelisting(deps.as_ref(), &recipient, &token.denom, amount)?;
        }
    }

    let transfer_msg = cosmwasm_std::BankMsg::Send {
        to_address: recipient.to_string(),
        amount: vec![Coin { amount, denom }],
    };

    let response = Response::new()
        .add_attribute("method", "execute_transfer")
        .add_message(transfer_msg);

    Ok(response)
}

fn assert_whitelisting(
    deps: Deps<CoreumQueries>,
    account: &str,
    denom: &str,
    amount: Uint128,
) -> Result<(), ContractError> {
    let parent_denom = PARENT_DENOM.load(deps.storage).unwrap_or_default();
    if !parent_denom.is_empty() {
        let whitelisted_balance = query_whitelisted_balance(deps, account, &parent_denom)?;
        if whitelisted_balance.amount.gt(&Uint128::zero()) {
            return Ok(());
        }
    }

    let bank_balance = query_bank_balance(deps, account, denom)?;
    let whitelisted_balance = query_whitelisted_balance(deps, account, denom)?;

    if amount + bank_balance.amount > whitelisted_balance.amount {
        return Err(ContractError::WhitelistingError {});
    }

    Ok(())
}

fn query_token(deps: Deps<CoreumQueries>, denom: &str) -> StdResult<Token> {
    let token: TokenResponse = deps.querier.query(
        &CoreumQueries::AssetFT(Query::Token {
            denom: denom.to_string(),
        })
        .into(),
    )?;

    Ok(token.token)
}

fn query_bank_balance(deps: Deps<CoreumQueries>, account: &str, denom: &str) -> StdResult<Coin> {
    let bank_balance: BalanceResponse = deps.querier.query(
        &BankQuery::Balance {
            address: account.to_string(),
            denom: denom.to_string(),
        }
        .into(),
    )?;

    Ok(bank_balance.amount)
}

fn query_whitelisted_balance(
    deps: Deps<CoreumQueries>,
    account: &str,
    denom: &str,
) -> StdResult<Coin> {
    let whitelisted_balance: WhitelistedBalanceResponse = deps.querier.query(
        &CoreumQueries::AssetFT(Query::WhitelistedBalance {
            account: account.to_string(),
            denom: denom.to_string(),
        })
        .into(),
    )?;
    Ok(whitelisted_balance.balance)
}
  • Build the contract, deploy the built artifact and migrate the extension code again.

For the second token, we instantiate the extension with extra flag to store the first token's denom to be able to query the first token's whitelist first and if the account is whitelisted there, allow the transfer to move forward even if the account is not whitelisted in this token.

cored tx assetft issue MYFT2 cmyft2 2 1000000 "My second FT token with extension" --from $FT_ADMIN --features=whitelisting,extension --extension_code_id=$CODE_ID --extension_label=my-second-extension --extension_issuance_msg='{"parent_denom": "'$FT_DENOM'"}' $COREUM_NODE_ARGS $COREUM_CHAIN_ID_ARGS -y -b block --gas auto --gas-adjustment 1.4
  • Build denom
export FT_DENOM2=cmyft2-$FT_ADMIN

Now if we send some of these coins, it will be rejected by the extension because the account is not whitelisted anywhere.

cored tx bank send $FT_ADMIN $FT_RECEIVER_1 10$FT_DENOM2 -y -b block $COREUM_NODE_ARGS $COREUM_CHAIN_ID_ARGS
# Whitelisted limit exceeded.

Let's whitelist $FT_RECEIVER_1 in the first FT and whitelist $FT_RECEIVER_2 in the second FT.

cored tx assetft set-whitelisted-limit $FT_RECEIVER_1 800$FT_DENOM --from $FT_ADMIN $COREUM_NODE_ARGS $COREUM_CHAIN_ID_ARGS -y -b block
cored tx assetft set-whitelisted-limit $FT_RECEIVER_2 800$FT_DENOM2 --from $FT_ADMIN $COREUM_NODE_ARGS $COREUM_CHAIN_ID_ARGS -y -b block

We can now send second tokens to both accounts and it will be successful.

cored tx bank send $FT_ADMIN $FT_RECEIVER_1 10$FT_DENOM2 -y -b block $COREUM_NODE_ARGS $COREUM_CHAIN_ID_ARGS
cored tx bank send $FT_ADMIN $FT_RECEIVER_2 10$FT_DENOM2 -y -b block $COREUM_NODE_ARGS $COREUM_CHAIN_ID_ARGS

Custom send commission rate.

Checkout extension/send-commission-rate branch to see code for this example.

For this example, we want to implement send commission rate feature in the extension, but the commission is split between the extension and the token admin.

Code:

pub fn sudo_extension_transfer(
    deps: DepsMut<CoreumQueries>,
    _env: Env,
    amount: Uint128,
    _sender: String,
    recipient: String,
    commission_amount: Uint128,
    _burn_amount: Uint128,
    _context: TransferContext,
) -> CoreumResult<ContractError> {
    let denom = DENOM.load(deps.storage)?;
    let token = query_token(deps.as_ref(), &denom)?;

    let transfer_msg = cosmwasm_std::BankMsg::Send {
        to_address: recipient.to_string(),
        amount: vec![Coin { amount, denom }],
    };

    let mut response = Response::new()
        .add_attribute("method", "execute_transfer")
        .add_message(transfer_msg);

    if !commission_amount.is_zero() {
        // if token has an admin, send half of the commission to the admin and let the extension keep
        // the rest of the commission
        if let Some(admin) = &token.admin {
            let admin_commission_amount = commission_amount.div(Uint128::new(2));
            let admin_commission_msg = cosmwasm_std::BankMsg::Send {
                to_address: admin.to_string(),
                amount: vec![Coin {
                    amount: admin_commission_amount,
                    denom: token.denom.to_string(),
                }],
            };
            response = response
                .add_attribute(
                    "admin_send_commission_amount",
                    admin_commission_amount.to_string(),
                )
                .add_message(admin_commission_msg);
        }
    }

    Ok(response)
}

fn query_token(deps: Deps<CoreumQueries>, denom: &str) -> StdResult<Token> {
    let token: TokenResponse = deps.querier.query(
        &CoreumQueries::AssetFT(Query::Token {
            denom: denom.to_string(),
        })
        .into(),
    )?;

    Ok(token.token)
}
  • Build the contract, deploy the built artifact and migrate the extension code again.

Now if we send 200 coins to another account, the commission would be 200 * 0.03 = 6, which will be split between admin and extension, so each one will receive 3 coins as commission.

Let's check the sender ($FT_ADMIN), the recipient ($FT_RECEIVER_1) and the extension ($EXTENSION_ADDR)'s balance before send.

  • Check balances
cored q bank balances $FT_ADMIN --denom $FT_DENOM $COREUM_NODE_ARGS $COREUM_CHAIN_ID_ARGS
# amount: "99999976"
cored q bank balances $FT_RECEIVER_1 --denom $FT_DENOM $COREUM_NODE_ARGS $COREUM_CHAIN_ID_ARGS
# amount: "10"
cored q bank balances $EXTENSION_ADDR --denom $FT_DENOM $COREUM_NODE_ARGS $COREUM_CHAIN_ID_ARGS
# amount: "14"
  • Send tokens
cored tx bank send $FT_ADMIN $FT_RECEIVER_1 200$FT_DENOM -y -b block $COREUM_NODE_ARGS $COREUM_CHAIN_ID_ARGS
  • Check balances again
cored q bank balances $FT_ADMIN --denom $FT_DENOM $COREUM_NODE_ARGS $COREUM_CHAIN_ID_ARGS
# amount: "99999769"
cored q bank balances $FT_RECEIVER_1 --denom $FT_DENOM $COREUM_NODE_ARGS $COREUM_CHAIN_ID_ARGS
# amount: "210"
cored q bank balances $EXTENSION_ADDR --denom $FT_DENOM $COREUM_NODE_ARGS $COREUM_CHAIN_ID_ARGS
# amount: "21"

We see that admin has 207 fewer coins (-200 -6 [commission amount] -4 [burn amount] +3 [50% commission refund] = -207), recipient has 200 more coins and the extension has 3 more coins [50% commission refund].

Custom burn rate.

Checkout extension/burn-rate branch to see code for this example.

Let's say we have a token with burn rate, but you want to burn less and less for transfers with more and more amount. For example, the burn amount would be the same as defined when issuing the token for transfers up to 200 coins, and then from 200 to 400 coins, burn half of the defined burn amount and refund the rest, and for transfers of more than 400 coins, burn 20% of the defined burn amount and refund the rest.

Code:

pub fn sudo_extension_transfer(
    deps: DepsMut<CoreumQueries>,
    _env: Env,
    amount: Uint128,
    sender: String,
    recipient: String,
    _commission_amount: Uint128,
    burn_amount: Uint128,
    _context: TransferContext,
) -> CoreumResult<ContractError> {
    let denom = DENOM.load(deps.storage)?;
    let token = query_token(deps.as_ref(), &denom)?;

    let transfer_msg = cosmwasm_std::BankMsg::Send {
        to_address: recipient.to_string(),
        amount: vec![Coin { amount, denom }],
    };

    let mut response = Response::new()
        .add_attribute("method", "execute_transfer")
        .add_message(transfer_msg);

    if !burn_amount.is_zero() {
        let new_burn_amount = match amount.u128() {
            0..=200 => burn_amount,
            201..=400 => burn_amount.div(Uint128::new(2)),
            _ => burn_amount.div(Uint128::new(5)),
        };

        let burn_message = CoreumMsg::AssetFT(assetft::Msg::Burn {
            coin: cosmwasm_std::coin(new_burn_amount.u128(), &token.denom),
        });

        response = response
            .add_attribute("burn_amount", new_burn_amount)
            .add_message(burn_message);

        if new_burn_amount.lt(&burn_amount) {
            let refund_amount = burn_amount.sub(new_burn_amount);

            let refund_burn_rate_msg = cosmwasm_std::BankMsg::Send {
                to_address: sender.to_string(),
                amount: vec![Coin {
                    amount: refund_amount,
                    denom: token.denom.to_string(),
                }],
            };

            response = response
                .add_attribute("burn_rate_refund", refund_amount.to_string())
                .add_message(refund_burn_rate_msg);
        }
    }

    Ok(response)
}

fn query_token(deps: Deps<CoreumQueries>, denom: &str) -> StdResult<Token> {
    let token: TokenResponse = deps.querier.query(
        &CoreumQueries::AssetFT(Query::Token {
            denom: denom.to_string(),
        })
        .into(),
    )?;

    Ok(token.token)
}
  • Build the contract, deploy the built artifact and migrate the extension code again.

Now if we send 100 coins to another account, 2 coins will be burnt (1000.02=2). _not related to this example but 3 more coins are also sent for the commission which is kept by the extension (1000.03=3)._

cored tx bank send $FT_ADMIN $FT_RECEIVER_1 100$FT_DENOM -y -b block $COREUM_NODE_ARGS $COREUM_CHAIN_ID_ARGS
  • Check balances
cored q bank balances $FT_ADMIN --denom $FT_DENOM $COREUM_NODE_ARGS $COREUM_CHAIN_ID_ARGS
# amount: "99999664"

We see that admin has 105 fewer coins.

If we send 300 coins to another account, 3 coins will be burnt (3000.020.5=3) and the rest (3) will send back to admin. not related to this example but 9 more coins are also sent for the commission which is kept by the extension (300*0.03=9).

cored tx bank send $FT_ADMIN $FT_RECEIVER_1 300$FT_DENOM -y -b block $COREUM_NODE_ARGS $COREUM_CHAIN_ID_ARGS
  • Check balances
cored q bank balances $FT_ADMIN --denom $FT_DENOM $COREUM_NODE_ARGS $COREUM_CHAIN_ID_ARGS
# amount: "99999352"

We see that admin has 312 fewer coins (-300-6+3-9=-312)

And if we send 500 coins to another account, 2 coins will be burnt (5000.020.2=2) and the rest (8) will send back to admin. not related to this example but 15 more coins are also sent for the commission which is kept by the extension (500*0.03=15).

cored tx bank send $FT_ADMIN $FT_RECEIVER_1 500$FT_DENOM -y -b block $COREUM_NODE_ARGS $COREUM_CHAIN_ID_ARGS
  • Check balances
cored q bank balances $FT_ADMIN --denom $FT_DENOM $COREUM_NODE_ARGS $COREUM_CHAIN_ID_ARGS
# amount: "99998835"

We see that admin has 517 fewer coins (-500-10+8-15=-517).

Limitations.

A token cannot have extension feature and any of ibc or block smart contract features at the same time, because the extension can decide what to do with bank transfers involving ibc or smart contracts, and it doesn't make sense to have these features enabled.

Custom block smart contract.

Checkout extension/block-smart-contract branch to see code for this example.

Let's say we want to implement block smart contract feature that rejects any payment to any recipient which is a smart contract, except the smart contract used as the asset extension.

Code:

pub fn sudo_extension_transfer(
    deps: DepsMut<CoreumQueries>,
    _env: Env,
    amount: Uint128,
    _sender: String,
    recipient: String,
    _commission_amount: Uint128,
    _burn_amount: Uint128,
    context: TransferContext,
) -> CoreumResult<ContractError> {
    let denom = DENOM.load(deps.storage)?;
    let token = query_token(deps.as_ref(), &denom)?;

    assert_block_smart_contracts(&context, &recipient, &token, amount)?;

    let transfer_msg = cosmwasm_std::BankMsg::Send {
        to_address: recipient.to_string(),
        amount: vec![Coin { amount, denom }],
    };

    let response = Response::new()
        .add_attribute("method", "execute_transfer")
        .add_message(transfer_msg);

    Ok(response)
}

fn assert_block_smart_contracts(
    context: &TransferContext,
    recipient: &str,
    token: &Token,
    amount: Uint128,
) -> Result<(), ContractError> {
    if Some(recipient.to_string()) == token.extension_cw_address
    {
        return Ok(());
    }

    if context.recipient_is_smart_contract {
        return Err(ContractError::SmartContractBlocked {});
    }

    return Ok(());
}
  • Build the contract, deploy the built artifact and migrate the extension code again.

To test it, we need another contract address. If you followed above examples, you should have access $FT_DENOM2 with a contract that is not the same as this extension's contract and the transfer to it will be blocked.

  • Capture some other contract address.
RES=$(cored q assetft token $FT_DENOM2 $COREUM_NODE_ARGS $COREUM_CHAIN_ID_ARGS --output json)
EXTENSION_ADDR2=$(echo $RES | jq -r '.token.extension_cw_address')
echo "Other Extension Address: $EXTENSION_ADDR2"

If we send some coins to the other contract, it will be blocked.

cored tx bank send $FT_ADMIN $EXTENSION_ADDR2 10$FT_DENOM -y -b block $COREUM_NODE_ARGS $COREUM_CHAIN_ID_ARGS

But if we do a bank send to our current contract, it will be transferred.

cored tx bank send $FT_ADMIN $EXTENSION_ADDR 10$FT_DENOM -y -b block $COREUM_NODE_ARGS $COREUM_CHAIN_ID_ARGS

Custom ibc.

Checkout extension/ibc branch to see code for this example.

As the last example, let's say we want to disallow ibc transfer more than an arbitrary amount, like 100 tokens, but allow other amounts.

Code:

pub fn sudo_extension_transfer(
    deps: DepsMut<CoreumQueries>,
    _env: Env,
    amount: Uint128,
    _sender: String,
    recipient: String,
    _commission_amount: Uint128,
    _burn_amount: Uint128,
    context: TransferContext,
) -> CoreumResult<ContractError> {
    let denom = DENOM.load(deps.storage)?;

    if matches!(context.ibc_purpose, IBCPurpose::Out) && amount > Uint128::new(100) {
        return Err(ContractError::IBCDisabled {});
    }

    let transfer_msg = cosmwasm_std::BankMsg::Send {
        to_address: recipient.to_string(),
        amount: vec![Coin { amount, denom }],
    };

    let response = Response::new()
        .add_attribute("method", "execute_transfer")
        .add_message(transfer_msg);

    Ok(response)
}
  • Build the contract, deploy the built artifact and migrate the extension code again.

If you transfer some amount more than 100 tokens, it will fail and show the error.

Next steps

  • Read Coreum modules specification, to be familiar with the custom Coreum functionality you can use for your extension.
  • Read WASM docs to understand all supported WASM features.
  • Check other tutorials to find something you might be interested in additionally.