The grc20
package is an implementation of the erc20
standard in Gnolang.
In this section, we'll be learning how to import the grc20
package in the gno.land/p/demo/grc/grc20
path to deploy the foo
realm that we'll use to mint foo
tokens.
Let's first break down the code by segments.
package foo
import (
"errors"
"std"
"strings"
"gno.land/p/demo/avl"
"gno.land/p/demo/grc/grc20"
"gno.land/p/demo/ufmt"
"gno.land/r/demo/users"
)
var (
foo *grc20.AdminToken
admin std.Address = "g1tntwtvzrkt2gex69f0pttan0fp05zmeg5yykv8"
)
We first import packages and realms that we'll be using in the foo
realm. Then, we reset the foo
variable and the admin address implemented in the AdminToken
in grc20
.
Note: The
admin
address will be the only address that can can mint or burn tokens.
func assertIsAdmin() error {
caller := std.GetOrigCaller()
if caller != admin {
return errors.New("you're not admin")
}
return nil
}
The assertIsAdmin()
function implements a logic to check if the caller of the admin-only function is the admin
address. This concept is similar to the require
or modifer
in Solidity.
func init() {
foo = grc20.NewAdminToken("Foo Token", "FOO", 4)
foo.Mint(admin, 100)
}
The init()
function resets the package and creates the foo
token with the following specifications.
- Name:
Foo Token
- Symbol:
FOO
- Decimals:
4
Then, the function mints 100 foo
tokens to the admin
address.
func Mint(address users.AddressOrName, amount uint64) error {
if err := assertIsAdmin(); err != nil {
return err
}
foo.Mint(address.Resolve(), amount)
}
func Burn(address users.AddressOrName, amount uint64) error {
if err := assertIsAdmin(); err != nil {
return err
}
foo.Burn(address.Resolve(), amount)
return nil
}
The Mint
function and the Burn
function respectively handles minting and burning of tokens. Both functions verify that the caller is the admin using the assertIsAdmin()
function declared above.
func TotalSupply() uint64 {
return foo.TotalSupply()
}
func BalanceOf(account users.AddressOrName) uint64 {
balance, err := foo.BalanceOf(account.Resolve())
if err != nil {
panic(err)
}
return balance
}
func Transfer(to users.AddressOrName, amount uint64) {
caller := std.GetOrigCaller()
foo.Transfer(caller, to.Resolve(), amount)
}
func FaucetWithAdmin() error {
if err := assertIsAdmin(); err != nil {
return err
}
caller := std.GetOrigCaller()
foo.Mint(caller, 200)
return nil
}
func FaucetWithoutAdmin() {
caller := std.GetOrigCaller()
foo.Mint(caller, 200)
}
func Allowance(owner, spender users.AddressOrName) uint64 {
allowance, err := foo.Allowance(owner.Resolve(), spender.Resolve())
if err != nil {
panic(err)
}
return allowance
}
func Approve(spender users.AddressOrName, amount uint64) error {
owner := std.GetOrigCaller()
if err := foo.Approve(owner, spender.Resolve(), amount); err != nil {
return err
}
return nil
}
func TransferFrom(from, to users.AddressOrName, amount uint64) error {
spender := std.GetOrigCaller()
if err := foo.TransferFrom(spender, from.Resolve(), to.Resolve(), amount); err != nil {
return err
}
return nil
}
Other functions implement the specifications of ERC20 with 2 additional functions for the Faucet
. Each function is explained below.
Total Supply
: Returns the total supply of tokens.
BalanceOf
: Returns the foo
token balance of an address.
Transfer
: Transfers foo
tokens.
FaucetWithAdmin
: Mints 200 foo
tokens to an address (admin-only).
FaucetWithoutAdmin
: Mints 200 foo
tokens to an address (public).
Allowance
: Returns the amount owner
's tokens that the spender
can transfer on behalf of the owner
.
Approve
: Grants the spender
with the authority to send a defined amount of caller
's foo
tokens on behalf of the caller
.
TransferFrom: The spender
sends owner
's tokens on behalf of the owner
.
// foo_test.gno
package foo
import (
"std"
"strings"
"testing"
"errors"
"gno.land/p/demo/avl"
"gno.land/r/demo/users"
)
func Test(t *testing.T) {
admin := users.AddressOrName("g1tntwtvzrkt2gex69f0pttan0fp05zmeg5yykv8")
test2 := users.AddressOrName(testutils.TestAddress("test2"))
recv := users.AddressOrName(testutils.TestAddress("recv"))
normal := users.AddressOrName(testutils.TestAddress("normal"))
owner := users.AddressOrName(testutils.TestAddress("owner"))
spender := users.AddressOrName(testutils.TestAddress("spender"))
recv2 := users.AddressOrName(testutils.TestAddress("recv2"))
mibu := users.AddressOrName(testutils.TestAddress("mint_burn"))
std.TestSetOrigCaller(admin.Resolve())
// init()
shouldEqual(t, foo.GetName(), "Foo Token")
shouldEqual(t, strings.TrimSpace(foo.GetName()), "Foo Token")
assertGRC20Balance(t, admin, 100)
// TotalSupply()
shouldEqual(t, foo.TotalSupply(), 100)
// BalanceOf()
assertGRC20Balance(t, admin, 100)
// Transfer()
std.TestSetOrigCaller(admin.Resolve())
Transfer(recv, 20)
assertGRC20Balance(t, admin, 80)
assertGRC20Balance(t, recv, 20)
// Faucet With/Without Admin
std.TestSetOrigCaller(admin.Resolve())
FaucetWithAdmin()
assertGRC20Balance(t, admin, 280)
shouldEqual(t, foo.TotalSupply(), 300)
std.TestSetOrigCaller(normal.Resolve())
assertErr(t, FaucetWithAdmin()) // must fail, since `normal` isn't admin
FaucetWithoutAdmin()
assertGRC20Balance(t, normal, 200)
shouldEqual(t, foo.TotalSupply(), 500)
// Approve && Allowance && TransferFrom
std.TestSetOrigCaller(owner.Resolve())
FaucetWithoutAdmin()
FaucetWithoutAdmin()
FaucetWithoutAdmin()
assertGRC20Balance(t, owner, 600)
Approve(spender, 300)
shouldEqual(t, Allowance(owner, spender), 300)
std.TestSetOrigCaller(spender.Resolve())
assertNoErr(t, TransferFrom(owner, recv2, 150))
assertGRC20Balance(t, owner, 450)
assertGRC20Balance(t, recv2, 150)
shouldEqual(t, Allowance(owner, spender), 150)
assertErr(t, TransferFrom(owner, recv2, 151))
// Mint
std.TestSetOrigCaller(admin.Resolve())
Mint(mibu, 500)
assertGRC20Balance(t, mibu, 500)
// Burn
std.TestSetOrigCaller(admin.Resolve())
Burn(mibu, 490)
assertGRC20Balance(t, mibu, 10)
}
func shouldEqual(t *testing.T, got interface{}, expected interface{}) {
t.Helper()
if got != expected {
t.Errorf("expected %v(%T), got %v(%T)", expected, expected, got, got)
}
}
func assertErr(t *testing.T, err error) {
t.Helper()
if err == nil {
t.Errorf("expected an error, but got nil.")
}
}
func assertNoErr(t *testing.T, err error) {
t.Helper()
if err != nil {
t.Errorf("expected no error, but got err: %s.", err.Error())
}
}
func assertNativeBalance(t *testing.T, addr std.Address, denom string, expectedBal uint64) {
t.Helper()
banker := std.GetBanker(std.BankerTypeReadonly)
coins := banker.GetCoins(addr)
got := coins.AmountOf(denom)
if got != expectedBal {
t.Errorf("invalid balance: expected %v, got %v.", expectedBal, got)
}
}
func assertGRC20Balance(t *testing.T, addr users.AddressOrName, expectedBal uint64) {
got := BalanceOf(addr)
if got != expectedBal {
t.Errorf("invalid balance: expected %v, got %v.", expectedBal, got)
}
}
Tip: the
users
realm enables users to register addresses with usernames(example) on/r/demo/users
for simplicity and convenience.
Let's assume that 3 addresses have been registered as users as the following:
Address | Username |
---|---|
g1cq2ecdq3eyn5qa0fzznpurg87zq3k77g63q6u7 | gnome1 |
g1avcw0qwyays4dl4j9l9hp0cd4gr9vhm79hty2w | gnome2 |
g1xvy0ra4wkpnwc3fey05et2y6g8s882fmgwmn4p | gnome3 |
In the images above, after deploying the foo
realm, user gnome1
has successfully received 200 foo
tokens from the faucet. Then, the balance of user gnome2
was successfully returned using the username as the argument instead of the actual address.