Understanding and deploying a fungible token contract in Scilla | Zilliqa

Bibek Poudel
Builders of Zilliqa
11 min readMar 16, 2021

With all the hype going around about cryptos, you probably have wondered how to create your own token. And some of you may have learned to create tokens in Ethereum but slow transactions and high gas fee might have put you in dilemma. Zilliqa could be a better option for you given that it has a very low gas fee and faster transaction speed.

In this tutorial, I will guide you on how to create your own Tokens in Zilliqa using Scilla programming language. For this, I will use a sample fungible token contract provided by Zilliqa in Neo Savant IDE. So it will be more like a breakdown of their code rather than creating our own from scratch.

What is a Fungible Token?

Fungibility refers to the property of any goods or commodities that are identical and interchangeable with each other. Currencies we use in our daily life are fungible. So, you can understand Fungible token as a standard for creating your own currency in Blockchain. In Zilliqa, the standard is called ZRC-2.

ZRC-2

According to Zilliqa, “ZRC-2 defines a minimum interface that a smart contract must implement to allow fungible tokens to be managed, tracked, owned, and traded peer-to-peer via wallets or exchanges.

You can learn more about ZRC-2 standard at https://github.com/Zilliqa/ZRC/blob/master/zrcs/zrc-2.md

I will also talk about them as we go through the code.

TOOLS:

1. IDE

I will be using Neo Savant IDE to write and test our smart contract. It can be accessed at: https://ide.zilliqa.com/

2. ZilPay Wallet (Optional)

ZilPay is a wallet that can be added as an extension in your web browser. The reason why I wrote it as optional is because you can simply use EmulatedEnv to test your smart contract before deploying it to the mainnet or the testnet.
You can download it from https://zilpay.xyz/ .

LET’S GET STARTED

Open up your web browser and navigate to Neo Savant IDE.

You should see list of sample contacts on the left side inside your IDE. We are going to use FungibleToken.scilla.

The contract starts with the scilla version. Then certain builtin libraries are imported. You can learn more about them here.

scilla_version 0import BoolUtils ListUtils IntUtils

2.

Next, a library called Fungible Token is defined. If you wish to change the name of the library in your token contract, make sure that it matches up with your contract name.

Here, all the library values and functions are defined which will be used in our contract. These values and functions are not subject to state manipulations.

library FungibleTokenlet one_msg =
fun (msg : Message) =>
let nil_msg = Nil {Message} in
Cons {Message} msg nil_msg
let two_msgs =
fun (msg1 : Message) =>
fun (msg2 : Message) =>
let msgs_tmp = one_msg msg2 in
Cons {Message} msg1 msgs_tmp

These weird looking two msg functions are nothing but empty list of Message type. Message is used for communication between the smart contracts and is sent using send instruction. one_msg takes a single message and two_msgs takes two as their name suggest. I will explain about the syntax and the process later in this tutorial when we encounter them.

(* Error events *)
type Error =
| CodeIsSender
| CodeInsufficientFunds
| CodeInsufficientAllowance
let make_error =
fun (result : Error) =>
let result_code =
match result with
| CodeIsSender => Int32 -1
| CodeInsufficientFunds => Int32 -2
| CodeInsufficientAllowance => Int32 -3
end
in
{ _exception : "Error"; code : result_code }

Errors are raised by throwing exceptions and these error codes are basically some Int32 value.

Error type defines three different types of errors. make_error function takes Error as parameter and maps it to the associated Int32 values. Then an exception is created with the name of an exception and the code. You can also add other information after that. Exception is thrown using throw instruction. Throwing an exception inside a transition aborts the execution of the contract and aborts all other contracts that have already been executed in that chain.

let zero = Uint128 0(* Dummy user-defined ADT *)
type Unit =
| Unit
let get_val =
fun (some_val: Option Uint128) =>
match some_val with
| Some val => val
| None => zero
end

The function get_val here takes a variable of type Uint128 and returns a value if it exists else returns 0.

3.

Now it’s time to define the contract.

contract FungibleToken
(
contract_owner: ByStr20,
name : String,
symbol: String,
decimals: Uint32,
init_supply : Uint128
)
(* Mutable fields *)field total_supply : Uint128 = init_supplyfield balances: Map ByStr20 Uint128
= let emp_map = Emp ByStr20 Uint128 in
builtin put emp_map contract_owner init_supply
field allowances: Map ByStr20 (Map ByStr20 Uint128)
= Emp ByStr20 (Map ByStr20 Uint128)

We use the contract keyword followed by the contract name. Then some immutable contract parameters are defined.

a. contract_owner = contains the address of the owner of the contract
b. name = name of your token
c. symbol = ticker or short form of your token’s name
d. decimals = number of decimal places supported by the token
For example: if the 2 is supplied in decimals , 100 represents 1.00 or 1 token.
e. init_supply = Number of tokens that you want to be minted initially during the deployment of the contract.

These immutable parameters are defined during the deployment of the contract and cannot be updated afterwards.

There are 3 mutable fields in the contract:

a. total_supply = denotes the total supply of the coins. You can also declare it as immutable parameters. However, you might want to keep it mutable incase you have a mint function(transition). Here, total supply is initialized with initial supply.
b. balances = map that keeps track of what amount of tokens is being hold by respective wallets. In the code above, all the initial supply is added to the owner wallet or the wallet of the contract deployer.
c. allowances= map that keeps track of the contracts or addresses a user have allowed along with the allowed amount in order to send the tokens on their behalf.

4. PROCEDURES

(**************************************)
(* Procedures *)
(**************************************)
procedure ThrowError(err : Error)
e = make_error err;
throw e
end
procedure IsNotSender(address: ByStr20)
is_sender = builtin eq _sender address;
match is_sender with
| True =>
err = CodeIsSender;
ThrowError err
| False =>
end
end
procedure AuthorizedMoveIfSufficientBalance(from: ByStr20, to: ByStr20, amount: Uint128)
get_from_bal <- balances[from];
match get_from_bal with
| Some bal =>
can_do = uint128_le amount bal;
match can_do with
| True =>
(* Subtract amount from from and add it to to address *)
new_from_bal = builtin sub bal amount;
balances[from] := new_from_bal;
(* Adds amount to to address *)
get_to_bal <- balances[to];
new_to_bal = match get_to_bal with
| Some bal => builtin add bal amount
| None => amount
end;
balances[to] := new_to_bal
| False =>
(* Balance not sufficient *)
err = CodeInsufficientFunds;
ThrowError err
end
| None =>
err = CodeInsufficientFunds;
ThrowError err
end
end

Procedures are the functions that cannot be invoked via messages to the contract but can be called by another transition or procedure.

Breaking Down AuthorizedMoveIfSufficientBalance procedure

AuthorizedMoveIfSufficientBalance has 3 parameters:

a. from : Address of the token owner
b. to : Address of the receiver
c. amount : Amount allowed be the token owner to be transferred

It is called by an address that the token holder has authorized to make spend on his behalf. So the authorized wallet would send the specified amount to another address specified in to.

At first, get_from_bal copies the balance of the original spender. Then it checks if the get_from_bal is not null and actually contains the balance. This is used to check if the from address specified is actually a token holder and has some balance.

After, it is verified that the from address has enough amount of tokens, it is deducted from the balance[from] and added to balance[to].

If there was not enough fund in from wallet address, an CodeInsufficientFundserror is thrown using ThrowError procedure. This ThrowError procedure takes an error and emits an error event while also halting the transaction.

procedure AuthorizedMoveIfSufficientBalance(from: ByStr20, to: ByStr20, amount: Uint128)
get_from_bal <- balances[from];
match get_from_bal with
| Some bal =>
can_do = uint128_le amount bal;
match can_do with
| True =>
(* Subtract amount from from and add it to to address *)
new_from_bal = builtin sub bal amount;
balances[from] := new_from_bal;
(* Adds amount to to address *)
get_to_bal <- balances[to];
new_to_bal = match get_to_bal with
| Some bal => builtin add bal amount
| None => amount
end;
balances[to] := new_to_bal
| False =>
(* Balance not sufficient *)
err = CodeInsufficientFunds;
ThrowError err
end
| None =>
err = CodeInsufficientFunds;
ThrowError err
end
end

5. TRANSITIONS

Transitions are basically functions. Transitions can be invoked my sending message to the contract and are used to change the state of the contract in the blockchain like procedures.

Now, let’s break down all the transitions in the contract.

i) IncreaseAllowance

This transition can only be called by the token owner. It is invoked to increase the amount of token that a token owner wants the approved spender to spend from his wallet.

This transition takes two parameters:

a. spender: Address of the sender whom the owner of the token approves for spending token on his behalf.
b. amount : This is the number of tokens that the owner of the token wants to increase the allowance by for the spender.


transition IncreaseAllowance(spender: ByStr20, amount: Uint128)
IsNotSender spender;
some_current_allowance <- allowances[_sender][spender];
current_allowance = get_val some_current_allowance;
new_allowance = builtin add current_allowance amount;
allowances[_sender][spender] := new_allowance;
e = {_eventname : "IncreasedAllowance"; token_owner : _sender; spender: spender; new_allowance : new_allowance};
event e
end

At first, it is not sure that the spender address passed is not the caller of this transition. If it is, an error is thrown. Because it won’t make sense allow myself to use my own fund, right?

Then it takes the current allowed amount for the spender and increases it by the amount passed in the transition. An event is emitted after the transition is run successfully and is stored in the blockchain for everyone to see.

ii) DecreaseAllowance

It is invoked to decrease the amount of token that a token owner had allowed the spender to spend on his behalf. The parameters are same as of IncreaseAllowance . Here, it checks if there is some amount allowed and decreases it by amount passed in parameters. If there was no allowance, whatsoever, the allowance is set to zero. This transition also can only be called by the token holder.

transition DecreaseAllowance(spender: ByStr20, amount: Uint128)
IsNotSender spender;
some_current_allowance <- allowances[_sender][spender];
current_allowance = get_val some_current_allowance;
new_allowance =
let amount_le_allowance = uint128_le amount current_allowance in
match amount_le_allowance with
| True => builtin sub current_allowance amount
| False => zero
end;
allowances[_sender][spender] := new_allowance;
e = {_eventname : "DecreasedAllowance"; token_owner : _sender; spender: spender; new_allowance : new_allowance};
event e
end

iii) Transfer

This transition is called by the token owner to transfer certain amount of token from his wallet to the recipient’s wallet.

It has 2 parameters:
a. to : address of the recipient
b. amount : amount of tokens to be transferred.


transition Transfer(to: ByStr20, amount: Uint128)
AuthorizedMoveIfSufficientBalance _sender to amount;
e = {_eventname : "TransferSuccess"; sender : _sender; recipient : to; amount : amount};
event e;
msg_to_recipient = {_tag : "RecipientAcceptTransfer"; _recipient : to; _amount : zero;
sender : _sender; recipient : to; amount : amount};
msg_to_sender = {_tag : "TransferSuccessCallBack"; _recipient : _sender; _amount : zero;
sender : _sender; recipient : to; amount : amount};
msgs = two_msgs msg_to_recipient msg_to_sender;
send msgs
end

The transition calls AuthorizedMoveIfSufficientBalance procedure (described above) to make a transfer. After the transfer is success, the TransferSuccess event is emitted.

What if the transfer is made to a contract address that does not accept tokens. For that a dummy callback transition RecipientAcceptTransfer is called which prevents the transfer to that contract. A recipient contract accepting the token should have RecipientAcceptTransfer transition which looks like this:

transition RecipientAcceptTransferFrom(
initiator : ByStr20,
sender : ByStr20,
recipient : ByStr20,
amount : Uint128
)
end

The TransferSuccessCallBack informs the sender about the status of the transfer. This contract should also be included in the recipient contract. It looks like this:

transition TransferSuccessCallBack(
sender : ByStr20,
recipient : ByStr20,
amount : Uint128
)
end

iv) TransferFrom

This transition moves the amount of allowed token from one address to another. Basically, as mentioned in IncreaseAllowance section above, the owner of the token authorizes an another address to spend the certain amount of token for him. Thus, this transition is called by the spender who was allowed by the token owner to spend specified number of tokens.

This transition has 3 parameters:

a. from : Address of the token owner
b. to : Address of the receiver
c. amount : Amount allowed by the token owner to be transferred.


transition TransferFrom(from: ByStr20, to: ByStr20, amount: Uint128)
get_spender_allowed <- allowances[from][_sender];
match get_spender_allowed with
| Some allowed =>
can_do = uint128_le amount allowed;
match can_do with
| True =>
AuthorizedMoveIfSufficientBalance from to amount;
e = {_eventname : "TransferFromSuccess"; initiator : _sender; sender : from; recipient : to; amount : amount};
event e;
new_allowed = builtin sub allowed amount;
allowances[from][_sender] := new_allowed;
(* Prevent sending to a contract address that does not support transfers of token *)
msg_to_recipient = {_tag : "RecipientAcceptTransferFrom"; _recipient : to; _amount : zero;
initiator : _sender; sender : from; recipient : to; amount : amount};
msg_to_sender = {_tag : "TransferFromSuccessCallBack"; _recipient : _sender; _amount : zero;
initiator : _sender; sender : from; recipient : to; amount : amount};
msgs = two_msgs msg_to_recipient msg_to_sender;
send msgs
| False =>
err = CodeInsufficientAllowance;
ThrowError err
end
| None =>
err = CodeInsufficientAllowance;
ThrowError err
end
end

At first it checks if the caller is allowed to call this transition, i.e. if he was allowed to spend some token by some token owner. It works almost the same way the Transfer transition works. The only new thing you might notice is CodeInsufficientAllowance which is emitted if the sender was not authorized spender or the allowed amount is less than the amount passed in this transition.

6. CHECK AND DEPLOY:

Once you are done typing your smart contract hit CHECK to see if there is any error in the code. Then you can SAVE and DEPLOY your contract.

Make sure that you selected Simulated ENV on the top right side of your IDE window. Then click on Deploy. You should get a prompt asking for several initialization parameters like this:

Just copy and paste your wallet address from the top of your IDE and other information mentioned above in section 5. Hit Deploy Contract and if the deployment is successful, you should get something like this:

You should see list of deployed contracts on bottom left side of your IDE. You can click on it and a prompt should appear where you can invoke transitions in your contract.

You can also find the code @ https://github.com/bibekpoudel/Scilla-Example-Contracts.

This was my second tutorial on Scilla. Obviously, it is not perfect. I would really appreciate feedbacks from you guys.

I will write my next tutorial on token transfer mechanism between contracts.

Cheers!

References:

https://github.com/Zilliqa/ZRC/blob/master/zrcs/zrc-2.md

--

--