From 3affd9551db9128a7a8a7972613d296535a49f07 Mon Sep 17 00:00:00 2001 From: Manfred Touron <94029+moul@users.noreply.github.com> Date: Sun, 7 Jul 2024 16:05:56 -0700 Subject: [PATCH] feat(examples): refactor grc20 (#2529) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Main changes: - rename `AdminToken` -> `Banker` - rename `GRC20` -> `Token` - remove unused helpers - remove vault (temporarily, will be reimplemented) - remove the returner ˋerror` when unnecessary - use `std.Emit` - use uassert for testing - better file naming and organization for improved readability Fixes #2294 Replaces #2314 (h/t @leohhhn) ~Depends on #2534~ BREAKING CHANGE --------- Signed-off-by: moul <94029+moul@users.noreply.github.com> Co-authored-by: Leon Hudak <33522493+leohhhn@users.noreply.github.com> --- .../creating-grc20/mytoken-1.gno | 13 +- .../creating-grc20/mytoken-2.gno | 82 ++---- docs/concepts/effective-gno.md | 5 +- docs/concepts/packages.md | 10 +- docs/how-to-guides/creating-grc20.md | 105 +++---- .../gno.land/p/demo/grc/exts/vault/errors.gno | 8 - .../gno.land/p/demo/grc/exts/vault/gno.mod | 6 - .../gno.land/p/demo/grc/exts/vault/vault.gno | 125 -------- .../p/demo/grc/exts/vault/vault_filetest.gno | 78 ----- .../p/demo/grc/exts/vault/vault_test.gno | 3 - .../gno.land/p/demo/grc/grc20/admin_token.gno | 266 ------------------ .../p/demo/grc/grc20/admin_token_test.gno | 64 ----- examples/gno.land/p/demo/grc/grc20/banker.gno | 217 ++++++++++++++ .../gno.land/p/demo/grc/grc20/banker_test.gno | 51 ++++ .../gno.land/p/demo/grc/grc20/dummy_test.gno | 32 --- examples/gno.land/p/demo/grc/grc20/errors.gno | 10 - examples/gno.land/p/demo/grc/grc20/gno.mod | 2 + .../gno.land/p/demo/grc/grc20/imustgrc20.gno | 21 -- .../gno.land/p/demo/grc/grc20/mustgrc20.gno | 53 ---- .../p/demo/grc/grc20/mustgrc20_test.gno | 3 - examples/gno.land/p/demo/grc/grc20/token.gno | 88 ++++++ .../{user_token_test.gno => token_test.gno} | 46 +-- .../demo/grc/grc20/{igrc20.gno => types.gno} | 34 +-- .../gno.land/p/demo/grc/grc20/user_token.gno | 49 ---- examples/gno.land/p/demo/grc/grc20/util.gno | 16 -- examples/gno.land/r/demo/bar20/bar20.gno | 16 +- examples/gno.land/r/demo/bar20/bar20_test.gno | 20 +- examples/gno.land/r/demo/bar20/gno.mod | 1 + examples/gno.land/r/demo/foo20/foo20.gno | 116 +++----- examples/gno.land/r/demo/foo20/foo20_test.gno | 56 ++-- examples/gno.land/r/demo/foo20/gno.mod | 2 + examples/gno.land/r/demo/grc20factory/gno.mod | 2 + .../r/demo/grc20factory/grc20factory.gno | 146 ++++------ .../r/demo/grc20factory/grc20factory_test.gno | 20 +- examples/gno.land/r/demo/wugnot/wugnot.gno | 104 +++---- 35 files changed, 681 insertions(+), 1189 deletions(-) delete mode 100644 examples/gno.land/p/demo/grc/exts/vault/errors.gno delete mode 100644 examples/gno.land/p/demo/grc/exts/vault/gno.mod delete mode 100644 examples/gno.land/p/demo/grc/exts/vault/vault.gno delete mode 100644 examples/gno.land/p/demo/grc/exts/vault/vault_filetest.gno delete mode 100644 examples/gno.land/p/demo/grc/exts/vault/vault_test.gno delete mode 100644 examples/gno.land/p/demo/grc/grc20/admin_token.gno delete mode 100644 examples/gno.land/p/demo/grc/grc20/admin_token_test.gno create mode 100644 examples/gno.land/p/demo/grc/grc20/banker.gno create mode 100644 examples/gno.land/p/demo/grc/grc20/banker_test.gno delete mode 100644 examples/gno.land/p/demo/grc/grc20/dummy_test.gno delete mode 100644 examples/gno.land/p/demo/grc/grc20/errors.gno delete mode 100644 examples/gno.land/p/demo/grc/grc20/imustgrc20.gno delete mode 100644 examples/gno.land/p/demo/grc/grc20/mustgrc20.gno delete mode 100644 examples/gno.land/p/demo/grc/grc20/mustgrc20_test.gno create mode 100644 examples/gno.land/p/demo/grc/grc20/token.gno rename examples/gno.land/p/demo/grc/grc20/{user_token_test.gno => token_test.gno} (52%) rename examples/gno.land/p/demo/grc/grc20/{igrc20.gno => types.gno} (72%) delete mode 100644 examples/gno.land/p/demo/grc/grc20/user_token.gno delete mode 100644 examples/gno.land/p/demo/grc/grc20/util.gno diff --git a/docs/assets/how-to-guides/creating-grc20/mytoken-1.gno b/docs/assets/how-to-guides/creating-grc20/mytoken-1.gno index 2a9856bb6d6..bbdf84f8a9f 100644 --- a/docs/assets/how-to-guides/creating-grc20/mytoken-1.gno +++ b/docs/assets/how-to-guides/creating-grc20/mytoken-1.gno @@ -1,3 +1,5 @@ +package mytoken + import ( "std" "strings" @@ -7,7 +9,8 @@ import ( ) var ( - mytoken *grc20.AdminToken + banker *grc20.Banker + mytoken grc20.Token admin std.Address ) @@ -17,9 +20,11 @@ func init() { admin = std.PrevRealm().Addr() // Set token name, symbol and number of decimals - mytoken = grc20.NewAdminToken("My Token", "TKN", 4) + banker = grc20.NewBanker("My Token", "TKN", 4) // Mint 1 million tokens to admin - mytoken.Mint(admin, 1000000*10000) -} + banker.Mint(admin, 1_000_000*10_000) // 1M + // Get the GRC20 compatible safe object + mytoken = banker.Token() +} diff --git a/docs/assets/how-to-guides/creating-grc20/mytoken-2.gno b/docs/assets/how-to-guides/creating-grc20/mytoken-2.gno index 3ce2346b903..71616feba15 100644 --- a/docs/assets/how-to-guides/creating-grc20/mytoken-2.gno +++ b/docs/assets/how-to-guides/creating-grc20/mytoken-2.gno @@ -1,3 +1,12 @@ +package mytoken + +import ( + "std" + "strings" + + "gno.land/p/demo/ufmt" +) + // TotalSupply returns the total supply of mytoken func TotalSupply() uint64 { return mytoken.TotalSupply() @@ -10,84 +19,39 @@ func Decimals() uint { // BalanceOf returns the balance mytoken for `account` func BalanceOf(account std.Address) uint64 { - balance, err := mytoken.BalanceOf(account) - if err != nil { - panic(err) - } - - return balance + return mytoken.BalanceOf(account) } // Allowance returns the allowance of spender on owner's balance func Allowance(owner, spender std.Address) uint64 { - allowance, err := mytoken.Allowance(owner, spender) - if err != nil { - panic(err) - } - - return allowance + return mytoken.Allowance(owner, spender) } // Transfer transfers amount from caller to recipient func Transfer(recipient std.Address, amount uint64) { - caller := std.PrevRealm().Addr() - if err := mytoken.Transfer(caller, recipient, amount); err != nil { - panic(err) - } + checkErr(mytoken.Transfer(recipient, amount)) } // Approve approves amount of caller's tokens to be spent by spender func Approve(spender std.Address, amount uint64) { - caller := std.PrevRealm().Addr() - if err := mytoken.Approve(caller, spender, amount); err != nil { - panic(err) - } + checkErr(mytoken.Approve(spender, amount)) } // TransferFrom transfers `amount` of tokens from `from` to `to` func TransferFrom(from, to std.Address, amount uint64) { - caller := std.PrevRealm().Addr() - - if amount <= 0 { - panic("transfer amount must be greater than zero") - } - - if err := mytoken.TransferFrom(caller, from, to, amount); err != nil { - panic(err) - } + checkErr(mytoken.TransferFrom(from, to, amount)) } // Mint mints amount of tokens to address. Callable only by admin of token func Mint(address std.Address, amount uint64) { assertIsAdmin(std.PrevRealm().Addr()) - - if amount <= 0 { - panic("mint amount must be greater than zero") - } - - if err := mytoken.Mint(address, amount); err != nil { - panic(err) - } + checkErr(banker.Mint(address, amount)) } // Burn burns amount of tokens from address. Callable only by admin of token func Burn(address std.Address, amount uint64) { assertIsAdmin(std.PrevRealm().Addr()) - - if amount <= 0 { - panic("burn amount must be greater than zero") - } - - if err := mytoken.Burn(address, amount); err != nil { - panic(err) - } -} - -// assertIsAdmin asserts the address is the admin of token -func assertIsAdmin(address std.Address) { - if address != admin { - panic("restricted access") - } + checkErr(banker.Burn(address, amount)) } // Render renders the state of the realm @@ -108,3 +72,17 @@ func Render(path string) string { return "404\n" } } + +// assertIsAdmin asserts the address is the admin of token +func assertIsAdmin(address std.Address) { + if address != admin { + panic("restricted access") + } +} + +// checkErr asserts the function didn't returned an error +func checkErr(err error) { + if err != nil { + panic(err) + } +} diff --git a/docs/concepts/effective-gno.md b/docs/concepts/effective-gno.md index 8f563b192dc..45872dd1d63 100644 --- a/docs/concepts/effective-gno.md +++ b/docs/concepts/effective-gno.md @@ -701,12 +701,11 @@ best of both worlds, you can wrap a Coins into a GRC20 compatible token. ```go import "gno.land/p/demo/grc/grc20" -var fooToken grc20.AdminToken = grc20.NewAdminToken("Foo Token", "FOO", 4) +var fooToken = grc20.NewBanker("Foo Token", "FOO", 4) func MyBalance() uint64 { caller := std.PrevRealm().Addr() - balance, _ := fooToken.BalanceOf(caller) - return balance + return fooToken.BalanceOf(caller) } ``` diff --git a/docs/concepts/packages.md b/docs/concepts/packages.md index cd3e2ace96a..dfc49a87caf 100644 --- a/docs/concepts/packages.md +++ b/docs/concepts/packages.md @@ -41,12 +41,12 @@ The role of each function is as follows: Two types of contracts exist in`grc20`: -1. `AdminToken` +1. `Banker` - Implements the token factory with `Helper` functions. - - The underlying struct should not be exposed to users. However, it can be typecasted as UserToken using the `GRC20()` method. -2. `UserToken` - - Implements the `IGRC20` interface. - - The underlying struct can be exposed to users. Created with the `GRC20()` method of `adminToken`. + - The underlying struct should not be exposed to users. However, it can return a typecasted `Token` object using the `Token()` method. +2. `Token` + - Implements the `GRC20` interface. + - The underlying struct can be exposed to users. Created with the `Token()` method of `Banker`. ## `grc721` diff --git a/docs/how-to-guides/creating-grc20.md b/docs/how-to-guides/creating-grc20.md index b7507946e5b..13f22fcc6a2 100644 --- a/docs/how-to-guides/creating-grc20.md +++ b/docs/how-to-guides/creating-grc20.md @@ -22,6 +22,8 @@ the main functionality of our token realm. The package can be found at the [embedmd]:# (../assets/how-to-guides/creating-grc20/mytoken-1.gno go) ```go +package mytoken + import ( "std" "strings" @@ -31,7 +33,8 @@ import ( ) var ( - mytoken *grc20.AdminToken + banker *grc20.Banker + mytoken grc20.Token admin std.Address ) @@ -41,21 +44,23 @@ func init() { admin = std.PrevRealm().Addr() // Set token name, symbol and number of decimals - mytoken = grc20.NewAdminToken("My Token", "TKN", 4) + banker = grc20.NewBanker("My Token", "TKN", 4) // Mint 1 million tokens to admin - mytoken.Mint(admin, 1000000*10000) -} + banker.Mint(admin, 1_000_000*10_000) // 1M + // Get the GRC20 compatible safe object + mytoken = banker.Token() +} ``` The code snippet above does the following: -- Defines a new token variable, `mytoken`, and assigns it to a -pointer to the GRC20 token type, `grc20.AdminToken`, +- Defines a new token variable, `banker`, and assigns it to a +pointer to the GRC20 banker type, `*grc20.Banker`, - Defines and sets the value of `admin` with a type of `std.Address` to contain the address of the deployer -- Initializes `mytoken` as a new GRC20 token, and sets its name, symbol, and -decimal values, +- Initializes `mytoken` as a GRC20-compatible token, and sets its name, symbol, + and decimal values, - Mint 1 million units of `My Token` and assign them to the admin's address. ## 2. Adding token functionality @@ -64,15 +69,16 @@ In order to call exported functions from the `grc20` package, we also need to expose them in the realm. Let's go through all functions in the GRC20 package, one by one: +[embedmd]:# (../assets/how-to-guides/creating-grc20/mytoken-2.gno go /.*TotalSupply/ /^}/) ```go // TotalSupply returns the total supply of mytoken func TotalSupply() uint64 { return mytoken.TotalSupply() } - ``` Calling the `TotalSupply` method would return the total number of tokens minted. +[embedmd]:# (../assets/how-to-guides/creating-grc20/mytoken-2.gno go /.*Decimals/ /^}/) ```go // Decimals returns the number of decimals of mytoken func Decimals() uint { @@ -81,120 +87,89 @@ func Decimals() uint { ``` Calling the `Decimals` method would return number of decimals of the token. +[embedmd]:# (../assets/how-to-guides/creating-grc20/mytoken-2.gno go /.*BalanceOf/ /^}/) ```go // BalanceOf returns the balance mytoken for `account` func BalanceOf(account std.Address) uint64 { - balance, err := mytoken.BalanceOf(account) - if err != nil { - panic(err) - } - - return balance + return mytoken.BalanceOf(account) } ``` Calling the `BalanceOf` method would return the total balance of an account. +[embedmd]:# (../assets/how-to-guides/creating-grc20/mytoken-2.gno go /.*Allowance/ /^}/) ```go // Allowance returns the allowance of spender on owner's balance func Allowance(owner, spender std.Address) uint64 { - allowance, err := mytoken.Allowance(owner, spender) - if err != nil { - panic(err) - } - - return allowance + return mytoken.Allowance(owner, spender) } ``` -Calling the `Allowance` method will return the amount `spender` is allowed to spend -from `owner`'s balance. +Calling the `Allowance` method will return the amount `spender` is allowed to +spend from `owner`'s balance. +[embedmd]:# (../assets/how-to-guides/creating-grc20/mytoken-2.gno go /.*Transfer/ /^}/) ```go // Transfer transfers amount from caller to recipient func Transfer(recipient std.Address, amount uint64) { - caller := std.PrevRealm().Addr() - if err := mytoken.Transfer(caller, recipient, amount); err != nil { - panic(err) - } + checkErr(mytoken.Transfer(recipient, amount)) } ``` -Calling the `Transfer` method transfers amount of token from the calling account -to the recipient account. +Calling the `Transfer` method transfers amount of token from the calling account +to the recipient account. +[embedmd]:# (../assets/how-to-guides/creating-grc20/mytoken-2.gno go /.*Approve/ /^}/) ```go +// Approve approves amount of caller's tokens to be spent by spender func Approve(spender std.Address, amount uint64) { - caller := std.PrevRealm().Addr() - if err := mytoken.Approve(caller, spender, amount); err != nil { - panic(err) - } + checkErr(mytoken.Approve(spender, amount)) } ``` Calling the `Approve` method approves `spender` to spend `amount` from the caller's balance of tokens. +[embedmd]:# (../assets/how-to-guides/creating-grc20/mytoken-2.gno go /.*TransferFrom/ /^}/) ```go -// TransferFrom transfers `amount` of tokens from `from` to `to` -func TransferFrom(sender, recipient std.Address, amount uint64) { - caller := std.PrevRealm().Addr() - - if amount <= 0 { - panic("transfer amount must be greater than zero") - } - - if err := mytoken.TransferFrom(caller, sender, recipient, amount); err != nil { - panic(err) - } +// TransferFrom transfers `amount` of tokens from `from` to `to` +func TransferFrom(from, to std.Address, amount uint64) { + checkErr(mytoken.TransferFrom(from, to, amount)) } ``` Calling the `TransferFrom` method moves `amount` of tokens from `sender` to `recipient` using the allowance mechanism. `amount` is then deducted from the caller’s allowance. +[embedmd]:# (../assets/how-to-guides/creating-grc20/mytoken-2.gno go /.*Mint/ /^}/) ```go // Mint mints amount of tokens to address. Callable only by admin of token func Mint(address std.Address, amount uint64) { assertIsAdmin(std.PrevRealm().Addr()) - - if amount <= 0 { - panic("mint amount must be greater than zero") - } - - if err := mytoken.Mint(address, amount); err != nil { - panic(err) - } + checkErr(banker.Mint(address, amount)) } ``` Calling the `Mint` method creates `amount` of tokens and assigns them to `address`, increasing the total supply. +[embedmd]:# (../assets/how-to-guides/creating-grc20/mytoken-2.gno go /.*Burn/ /^}/) ```go // Burn burns amount of tokens from address. Callable only by admin of token func Burn(address std.Address, amount uint64) { assertIsAdmin(std.PrevRealm().Addr()) - - if amount <= 0 { - panic("burn amount must be greater than zero") - } - - if err := mytoken.Burn(address, amount); err != nil { - panic(err) - } + checkErr(banker.Burn(address, amount)) } ``` Calling the `Mint` method burns `amount` of tokens from the balance of `address`, decreasing the total supply. +[embedmd]:# (../assets/how-to-guides/creating-grc20/mytoken-2.gno go /.*assertIsAdmin/ /^}/) ```go -// assertIsAdmin asserts the address is the admin of token -func assertIsAdmin(address std.Address) { - if address != admin { - panic("restricted access") - } + assertIsAdmin(std.PrevRealm().Addr()) + checkErr(banker.Mint(address, amount)) } ``` Calling the `assertIsAdmin` method checks if `address` is equal to the package-level `admin` variable. +[embedmd]:# (../assets/how-to-guides/creating-grc20/mytoken-2.gno go /.*Render/ /^}/) ```go // Render renders the state of the realm func Render(path string) string { diff --git a/examples/gno.land/p/demo/grc/exts/vault/errors.gno b/examples/gno.land/p/demo/grc/exts/vault/errors.gno deleted file mode 100644 index 895fe687b01..00000000000 --- a/examples/gno.land/p/demo/grc/exts/vault/errors.gno +++ /dev/null @@ -1,8 +0,0 @@ -package vault - -import "errors" - -var ( - ErrNoSuchVault = errors.New("no such vault") - ErrTooEarlyToRedeem = errors.New("too early to redeem") -) diff --git a/examples/gno.land/p/demo/grc/exts/vault/gno.mod b/examples/gno.land/p/demo/grc/exts/vault/gno.mod deleted file mode 100644 index 2720bf09d95..00000000000 --- a/examples/gno.land/p/demo/grc/exts/vault/gno.mod +++ /dev/null @@ -1,6 +0,0 @@ -module gno.land/p/demo/grc/exts/vault - -require ( - gno.land/p/demo/avl v0.0.0-latest - gno.land/p/demo/grc/grc20 v0.0.0-latest -) diff --git a/examples/gno.land/p/demo/grc/exts/vault/vault.gno b/examples/gno.land/p/demo/grc/exts/vault/vault.gno deleted file mode 100644 index 47aa70f483e..00000000000 --- a/examples/gno.land/p/demo/grc/exts/vault/vault.gno +++ /dev/null @@ -1,125 +0,0 @@ -package vault - -import ( - "std" - - "gno.land/p/demo/avl" - "gno.land/p/demo/grc/grc20" -) - -// Vault is a GRC20 compatible token with vault features. -type Vault interface { - Deposit(amount uint, recovery std.Address, lockDuration uint) error - Unvault(amount uint) error - Recover(dest std.Address) error - Redeem() error -} - -func New(adminToken *grc20.AdminToken) Vault { - return &impl{ - adminToken: adminToken, - users: avl.Tree{}, - } -} - -type impl struct { - adminToken *grc20.AdminToken - users avl.Tree // std.Address -> userVault -} - -type userVault struct { - // constructor parameters. - recover std.Address - lockDuration uint - - // internal parameters. - owner std.Address - redeemMinHeight int64 - unvaultedAmount uint -} - -func (v *impl) Deposit(amount uint, recover std.Address, lockDuration uint) error { - caller := std.GetOrigCaller() - pkgAddr := std.GetOrigPkgAddr() - - uv := userVault{ - lockDuration: lockDuration, - redeemMinHeight: 0, // will be set in Unvault. - unvaultedAmount: 0, // will be increased in Unvault, zeroed in Redeem. - owner: caller, - } - - // deposit. - err := v.adminToken.Transfer(caller, pkgAddr, uint64(amount)) - if err != nil { - return err - } - v.users.Set(caller.String(), &uv) - - return nil -} - -func (v *impl) Unvault(amount uint) error { - caller := std.GetOrigCaller() - uv, err := v.getUserVault(caller) - if err != nil { - return err - } - - balance, err := v.adminToken.BalanceOf(caller) - if err != nil { - return err - } - if balance < uint64(amount) { - return grc20.ErrInsufficientBalance - } - - println("AAA1", std.GetHeight(), uv.redeemMinHeight, uv.lockDuration) - uv.redeemMinHeight = std.GetHeight() + int64(uv.lockDuration) - uv.unvaultedAmount += amount - v.users.Set(caller.String(), uv) - println("AAA2", std.GetHeight(), uv.redeemMinHeight, uv.lockDuration) - return nil -} - -func (v *impl) Redeem() error { - pkgAddr := std.GetOrigPkgAddr() - caller := std.GetOrigCaller() - uv, err := v.getUserVault(caller) - if err != nil { - return err - } - - println("AAA3", std.GetHeight(), uv.redeemMinHeight, uv.lockDuration) - if std.GetHeight() < uv.redeemMinHeight { - return ErrTooEarlyToRedeem - } - // TODO: check balance. (should be optional, but let's be sure). - // TODO: check height. - - // transfer token. - err = v.adminToken.Transfer(pkgAddr, caller, uint64(uv.unvaultedAmount)) - if err != nil { - return err - } - - uv.unvaultedAmount = 0 - // TODO: if balance == 0 -> destroy? - return nil -} - -func (v *impl) Recover(dest std.Address) error { - // TODO: assert caller (recovery). - // TODO: trasfertToken. - // TODO: destroy? - return nil -} - -func (v *impl) getUserVault(address std.Address) (*userVault, error) { - uvI, exists := v.users.Get(address.String()) - if !exists { - return nil, ErrNoSuchVault - } - uv := uvI.(*userVault) - return uv, nil -} diff --git a/examples/gno.land/p/demo/grc/exts/vault/vault_filetest.gno b/examples/gno.land/p/demo/grc/exts/vault/vault_filetest.gno deleted file mode 100644 index 34d38afef1f..00000000000 --- a/examples/gno.land/p/demo/grc/exts/vault/vault_filetest.gno +++ /dev/null @@ -1,78 +0,0 @@ -package main - -import ( - "std" - - "gno.land/p/demo/grc/exts/vault" - "gno.land/p/demo/grc/grc20" - "gno.land/p/demo/testutils" - "gno.land/p/demo/ufmt" -) - -func main() { - alice := testutils.TestAddress("alice") - bob := testutils.TestAddress("bob") // recovery request address (cold wallet). - charly := testutils.TestAddress("charly") // recovery dest. - pkgaddr := std.GetOrigPkgAddr() - - // create a fooAdminToken + fooToken (GRC20) pair. - fooAdminToken := grc20.NewAdminToken("Foo", "FOO", 4) - fooAdminToken.Mint(alice, 1000) - fooToken := fooAdminToken.GRC20() - - printBalances := func() { - aliceBalance, _ := fooToken.BalanceOf(alice) - bobBalance, _ := fooToken.BalanceOf(bob) - charlyBalance, _ := fooToken.BalanceOf(charly) - pkgBalance, _ := fooToken.BalanceOf(pkgaddr) - println(ufmt.Sprintf( - "balances: alice=%d, bob=%d, charly=%d, pkg=%d, height=%d", - aliceBalance, bobBalance, charlyBalance, pkgBalance, std.GetHeight(), - )) - } - - // create a vault for fooAdminToken. - v := vault.New(fooAdminToken) - printBalances() - - // alice deposits 300 with an unlock duration of 5 blocks. - std.TestSetOrigCaller(alice) - lockAmount := uint(300) - lockDuration := uint(5) - checkErr(v.Deposit(lockAmount, bob, lockDuration)) - printBalances() - - // alice calls unvault for 200 tokens. - checkErr(v.Unvault(200)) - printBalances() - - // alice waits for few blocks. - std.TestSkipHeights(int64(lockDuration) + 1) - printBalances() - - // alice redeems 200 tokens. - checkErr(v.Redeem()) - printBalances() - - // bob instantly recover everything in the wallet. - std.TestSetOrigCaller(bob) - checkErr(v.Recover(charly)) - printBalances() -} - -func checkErr(err error) { - if err != nil { - panic(err) - } -} - -// Output: -// balances: alice=1000, bob=0, charly=0, pkg=0, height=123 -// balances: alice=700, bob=0, charly=0, pkg=300, height=123 -// AAA1 123 0 5 -// AAA2 123 128 5 -// balances: alice=700, bob=0, charly=0, pkg=300, height=123 -// balances: alice=700, bob=0, charly=0, pkg=300, height=129 -// AAA3 129 128 5 -// balances: alice=900, bob=0, charly=0, pkg=100, height=129 -// balances: alice=900, bob=0, charly=0, pkg=100, height=129 diff --git a/examples/gno.land/p/demo/grc/exts/vault/vault_test.gno b/examples/gno.land/p/demo/grc/exts/vault/vault_test.gno deleted file mode 100644 index c0dc6499300..00000000000 --- a/examples/gno.land/p/demo/grc/exts/vault/vault_test.gno +++ /dev/null @@ -1,3 +0,0 @@ -package vault - -// TODO: unit tests, edge cases. diff --git a/examples/gno.land/p/demo/grc/grc20/admin_token.gno b/examples/gno.land/p/demo/grc/grc20/admin_token.gno deleted file mode 100644 index 83197d940c7..00000000000 --- a/examples/gno.land/p/demo/grc/grc20/admin_token.gno +++ /dev/null @@ -1,266 +0,0 @@ -package grc20 - -import ( - "std" - - "gno.land/p/demo/avl" - "gno.land/p/demo/ufmt" -) - -// AdminToken implements a token factory with admin helpers. -// -// Warning: you should not expose this struct to enduser directly. -// -// It allows token administrators to call privileged helpers -// like Mint, Burn, or any Transfer helpers by passing custom owners. -// -// You should initialize your token, then call AdminToken.SafeGRC20() to -// expose a safe instance to the endusers. -type AdminToken struct { - name string - symbol string - decimals uint - totalSupply uint64 - balances avl.Tree // std.Address(owner) -> uint64 - allowances avl.Tree // string(owner+":"+spender) -> uint64 -} - -// safeToken implements the IGRC20 interface. -// -// It is generated by AdminToken.SafeGRC20(). -// It can safely be explosed publicly. -type safeToken struct { - IGRC20 // implements the GRC20 interface. - - factory *AdminToken -} - -func NewAdminToken(name, symbol string, decimals uint) *AdminToken { - // FIXME: check for limits - - return &AdminToken{ - name: name, - symbol: symbol, - decimals: decimals, - - balances: avl.Tree{}, - allowances: avl.Tree{}, - } -} - -func (t *AdminToken) GetName() string { return t.name } -func (t *AdminToken) GetSymbol() string { return t.symbol } -func (t *AdminToken) GetDecimals() uint { return t.decimals } -func (t *AdminToken) TotalSupply() uint64 { return t.totalSupply } - -func (t *AdminToken) BalanceOf(owner std.Address) (uint64, error) { - return t.balanceOf(owner) -} - -func (t *AdminToken) Transfer(owner, to std.Address, amount uint64) error { - return t.transfer(owner, to, amount) -} - -func (t *AdminToken) Allowance(owner, spender std.Address) (uint64, error) { - return t.allowance(owner, spender) -} - -func (t *AdminToken) Approve(owner, spender std.Address, amount uint64) error { - return t.approve(owner, spender, amount) -} - -func (t *AdminToken) TransferFrom(spender, from, to std.Address, amount uint64) error { - if err := t.spendAllowance(from, spender, amount); err != nil { - return err - } - return t.transfer(from, to, amount) -} - -// Administration helpers implementation. -// - -func (t *AdminToken) Mint(to std.Address, amount uint64) error { - return t.mint(to, amount) -} - -func (t *AdminToken) Burn(from std.Address, amount uint64) error { - return t.burn(from, amount) -} - -// private helpers -// - -func (t *AdminToken) mint(address std.Address, amount uint64) error { - if err := checkIsValidAddress(address); err != nil { - return err - } - - // TODO: check for overflow - - t.totalSupply += amount - currentBalance, err := t.balanceOf(address) - if err != nil { - return err - } - newBalance := currentBalance + amount - - t.balances.Set(string(address), newBalance) - - event := TransferEvent{zeroAddress, address, amount} - emit(&event) - - return nil -} - -func (t *AdminToken) burn(address std.Address, amount uint64) error { - if err := checkIsValidAddress(address); err != nil { - return err - } - // TODO: check for overflow - - currentBalance, err := t.balanceOf(address) - if err != nil { - return err - } - if currentBalance < amount { - return ErrInsufficientBalance - } - - t.totalSupply -= amount - newBalance := currentBalance - amount - - t.balances.Set(string(address), newBalance) - - event := TransferEvent{address, zeroAddress, amount} - emit(&event) - - return nil -} - -func (t *AdminToken) balanceOf(address std.Address) (uint64, error) { - if err := checkIsValidAddress(address); err != nil { - return 0, err - } - - balance, found := t.balances.Get(address.String()) - if !found { - return 0, nil - } - return balance.(uint64), nil -} - -func (t *AdminToken) spendAllowance(owner, spender std.Address, amount uint64) error { - if err := checkIsValidAddress(owner); err != nil { - return err - } - if err := checkIsValidAddress(spender); err != nil { - return err - } - - currentAllowance, err := t.allowance(owner, spender) - if err != nil { - return err - } - if currentAllowance < amount { - return ErrInsufficientAllowance - } - - key := allowanceKey(owner, spender) - if currentAllowance > amount { - t.allowances.Set(key, currentAllowance-amount) - } else { - t.allowances.Remove(key) - } - - return nil -} - -func (t *AdminToken) transfer(from, to std.Address, amount uint64) error { - if err := checkIsValidAddress(from); err != nil { - return err - } - if err := checkIsValidAddress(to); err != nil { - return err - } - - if from == to { - return ErrCannotTransferToSelf - } - - toBalance, err := t.balanceOf(to) - if err != nil { - return err - } - fromBalance, err := t.balanceOf(from) - if err != nil { - return err - } - - // debug. - // println("from", from, "to", to, "amount", amount, "fromBalance", fromBalance, "toBalance", toBalance) - - if fromBalance < amount { - return ErrInsufficientBalance - } - - newToBalance := toBalance + amount - newFromBalance := fromBalance - amount - - t.balances.Set(string(to), newToBalance) - t.balances.Set(string(from), newFromBalance) - - event := TransferEvent{from, to, amount} - emit(&event) - - return nil -} - -func (t *AdminToken) allowance(owner, spender std.Address) (uint64, error) { - if err := checkIsValidAddress(owner); err != nil { - return 0, err - } - if err := checkIsValidAddress(spender); err != nil { - return 0, err - } - - allowance, found := t.allowances.Get(allowanceKey(owner, spender)) - if !found { - return 0, nil - } - - return allowance.(uint64), nil -} - -func (t *AdminToken) approve(owner, spender std.Address, amount uint64) error { - if err := checkIsValidAddress(owner); err != nil { - return err - } - if err := checkIsValidAddress(spender); err != nil { - return err - } - - t.allowances.Set(allowanceKey(owner, spender), amount) - - event := ApprovalEvent{owner, spender, amount} - emit(&event) - - return nil -} - -func allowanceKey(owner, spender std.Address) string { - return owner.String() + ":" + spender.String() -} - -func (t *AdminToken) RenderHome() string { - str := "" - str += ufmt.Sprintf("# %s ($%s)\n\n", t.name, t.symbol) - str += ufmt.Sprintf("* **Decimals**: %d\n", t.decimals) - str += ufmt.Sprintf("* **Total supply**: %d\n", t.totalSupply) - str += ufmt.Sprintf("* **Known accounts**: %d\n", t.balances.Size()) - return str -} - -// GRC20 returns an instance that can be exposed to the end user. -func (t *AdminToken) GRC20() IGRC20 { - return &userToken{admin: t} -} diff --git a/examples/gno.land/p/demo/grc/grc20/admin_token_test.gno b/examples/gno.land/p/demo/grc/grc20/admin_token_test.gno deleted file mode 100644 index ea872f4da79..00000000000 --- a/examples/gno.land/p/demo/grc/grc20/admin_token_test.gno +++ /dev/null @@ -1,64 +0,0 @@ -package grc20 - -import ( - "std" - "testing" -) - -func TestAdminTokenImpl(t *testing.T) { - dummy := NewAdminToken("Dummy", "DUMMY", 4) - if dummy == nil { - t.Errorf("should not be nil") - } -} - -func TestAllowance(t *testing.T) { - owner := std.Address("g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj") - spender := std.Address("g1us8428u2a5satrlxzagsqa5m6vmuze027sxc8x") - dest := std.Address("g1us8428m6a5satrlxzagsqa5m6vmuze02tyelwj") - - token := NewAdminToken("Dummy", "DUMMY", 6) - assertE(t, token.Mint(owner, 100000000)) - assertE(t, token.Approve(owner, spender, 5000000)) - - err := token.TransferFrom(spender, owner, dest, 10000000) - assert(t, err != nil, "should not be able to transfer more than approved") - - tests := []struct { - spend uint64 - exp uint64 - }{ - {3, 4999997}, - {999997, 4000000}, - {4000000, 0}, - } - - for _, tt := range tests { - b0, _ := token.BalanceOf(dest) - assertE(t, token.TransferFrom(spender, owner, dest, tt.spend)) - a, _ := token.Allowance(owner, spender) - assert(t, a == tt.exp, "allowance exp: %d, got %d", tt.exp, a) - - b, _ := token.BalanceOf(dest) - expB := b0 + tt.spend - assert(t, b == expB, "balance exp: %d, got %d", expB, b) - } - - err = token.TransferFrom(spender, owner, dest, 1) - assert(t, err != nil, "no allowance") - - key := allowanceKey(owner, spender) - assert(t, !token.allowances.Has(key), "allowance should be removed") -} - -func assertE(t *testing.T, err error) { - if err != nil { - t.Fatalf("unexpected error: %s", err.Error()) - } -} - -func assert(t *testing.T, cond bool, format string, args ...interface{}) { - if !cond { - t.Fatalf(format, args...) - } -} diff --git a/examples/gno.land/p/demo/grc/grc20/banker.gno b/examples/gno.land/p/demo/grc/grc20/banker.gno new file mode 100644 index 00000000000..f643d3e2635 --- /dev/null +++ b/examples/gno.land/p/demo/grc/grc20/banker.gno @@ -0,0 +1,217 @@ +package grc20 + +import ( + "std" + "strconv" + + "gno.land/p/demo/avl" + "gno.land/p/demo/ufmt" +) + +// Banker implements a token banker with admin privileges. +// +// The Banker is intended to be used in two main ways: +// 1. as a temporary object used to make the initial minting, then deleted. +// 2. preserved in an unexported variable to support conditional administrative +// tasks protected by the contract. +type Banker struct { + name string + symbol string + decimals uint + totalSupply uint64 + balances avl.Tree // std.Address(owner) -> uint64 + allowances avl.Tree // string(owner+":"+spender) -> uint64 + token *token // to share the same pointer +} + +func NewBanker(name, symbol string, decimals uint) *Banker { + if name == "" { + panic("name should not be empty") + } + if symbol == "" { + panic("symbol should not be empty") + } + // XXX additional checks (length, characters, limits, etc) + + b := Banker{ + name: name, + symbol: symbol, + decimals: decimals, + } + t := &token{banker: &b} + b.token = t + return &b +} + +func (b Banker) Token() Token { return b.token } // Token returns a grc20 safe-object implementation. +func (b Banker) GetName() string { return b.name } +func (b Banker) GetSymbol() string { return b.symbol } +func (b Banker) GetDecimals() uint { return b.decimals } +func (b Banker) TotalSupply() uint64 { return b.totalSupply } +func (b Banker) KnownAccounts() int { return b.balances.Size() } + +func (b *Banker) Mint(address std.Address, amount uint64) error { + if !address.IsValid() { + return ErrInvalidAddress + } + + // TODO: check for overflow + + b.totalSupply += amount + currentBalance := b.BalanceOf(address) + newBalance := currentBalance + amount + + b.balances.Set(string(address), newBalance) + + std.Emit( + TransferEvent, + "from", "", + "to", string(address), + "value", strconv.Itoa(int(amount)), + ) + + return nil +} + +func (b *Banker) Burn(address std.Address, amount uint64) error { + if !address.IsValid() { + return ErrInvalidAddress + } + // TODO: check for overflow + + currentBalance := b.BalanceOf(address) + if currentBalance < amount { + return ErrInsufficientBalance + } + + b.totalSupply -= amount + newBalance := currentBalance - amount + + b.balances.Set(string(address), newBalance) + + std.Emit( + TransferEvent, + "from", string(address), + "to", "", + "value", strconv.Itoa(int(amount)), + ) + + return nil +} + +func (b Banker) BalanceOf(address std.Address) uint64 { + balance, found := b.balances.Get(address.String()) + if !found { + return 0 + } + return balance.(uint64) +} + +func (b *Banker) SpendAllowance(owner, spender std.Address, amount uint64) error { + if !owner.IsValid() { + return ErrInvalidAddress + } + if !spender.IsValid() { + return ErrInvalidAddress + } + + currentAllowance := b.Allowance(owner, spender) + if currentAllowance < amount { + return ErrInsufficientAllowance + } + + key := allowanceKey(owner, spender) + newAllowance := currentAllowance - amount + + if newAllowance == 0 { + b.allowances.Remove(key) + } else { + b.allowances.Set(key, newAllowance) + } + + return nil +} + +func (b *Banker) Transfer(from, to std.Address, amount uint64) error { + if !from.IsValid() { + return ErrInvalidAddress + } + if !to.IsValid() { + return ErrInvalidAddress + } + if from == to { + return ErrCannotTransferToSelf + } + + toBalance := b.BalanceOf(to) + fromBalance := b.BalanceOf(from) + + // debug. + // println("from", from, "to", to, "amount", amount, "fromBalance", fromBalance, "toBalance", toBalance) + + if fromBalance < amount { + return ErrInsufficientBalance + } + + newToBalance := toBalance + amount + newFromBalance := fromBalance - amount + + b.balances.Set(string(to), newToBalance) + b.balances.Set(string(from), newFromBalance) + + std.Emit( + TransferEvent, + "from", from.String(), + "to", to.String(), + "value", strconv.Itoa(int(amount)), + ) + return nil +} + +func (b *Banker) TransferFrom(spender, from, to std.Address, amount uint64) error { + if err := b.SpendAllowance(from, spender, amount); err != nil { + return err + } + return b.Transfer(from, to, amount) +} + +func (b *Banker) Allowance(owner, spender std.Address) uint64 { + allowance, found := b.allowances.Get(allowanceKey(owner, spender)) + if !found { + return 0 + } + return allowance.(uint64) +} + +func (b *Banker) Approve(owner, spender std.Address, amount uint64) error { + if !owner.IsValid() { + return ErrInvalidAddress + } + if !spender.IsValid() { + return ErrInvalidAddress + } + + b.allowances.Set(allowanceKey(owner, spender), amount) + + std.Emit( + ApprovalEvent, + "owner", string(owner), + "spender", string(spender), + "value", strconv.Itoa(int(amount)), + ) + + return nil +} + +func (b *Banker) RenderHome() string { + str := "" + str += ufmt.Sprintf("# %s ($%s)\n\n", b.name, b.symbol) + str += ufmt.Sprintf("* **Decimals**: %d\n", b.decimals) + str += ufmt.Sprintf("* **Total supply**: %d\n", b.totalSupply) + str += ufmt.Sprintf("* **Known accounts**: %d\n", b.KnownAccounts()) + return str +} + +func allowanceKey(owner, spender std.Address) string { + return owner.String() + ":" + spender.String() +} diff --git a/examples/gno.land/p/demo/grc/grc20/banker_test.gno b/examples/gno.land/p/demo/grc/grc20/banker_test.gno new file mode 100644 index 00000000000..00a1e75df1f --- /dev/null +++ b/examples/gno.land/p/demo/grc/grc20/banker_test.gno @@ -0,0 +1,51 @@ +package grc20 + +import ( + "testing" + + "gno.land/p/demo/testutils" + "gno.land/p/demo/ufmt" + "gno.land/p/demo/urequire" +) + +func TestBankerImpl(t *testing.T) { + dummy := NewBanker("Dummy", "DUMMY", 4) + urequire.False(t, dummy == nil, "dummy should not be nil") +} + +func TestAllowance(t *testing.T) { + var ( + owner = testutils.TestAddress("owner") + spender = testutils.TestAddress("spender") + dest = testutils.TestAddress("dest") + ) + + b := NewBanker("Dummy", "DUMMY", 6) + urequire.NoError(t, b.Mint(owner, 100000000)) + urequire.NoError(t, b.Approve(owner, spender, 5000000)) + urequire.Error(t, b.TransferFrom(spender, owner, dest, 10000000), ErrInsufficientAllowance.Error(), "should not be able to transfer more than approved") + + tests := []struct { + spend uint64 + exp uint64 + }{ + {3, 4999997}, + {999997, 4000000}, + {4000000, 0}, + } + + for _, tt := range tests { + b0 := b.BalanceOf(dest) + urequire.NoError(t, b.TransferFrom(spender, owner, dest, tt.spend)) + a := b.Allowance(owner, spender) + urequire.Equal(t, a, tt.exp, ufmt.Sprintf("allowance exp: %d, got %d", tt.exp, a)) + b := b.BalanceOf(dest) + expB := b0 + tt.spend + urequire.Equal(t, b, expB, ufmt.Sprintf("balance exp: %d, got %d", expB, b)) + } + + urequire.Error(t, b.TransferFrom(spender, owner, dest, 1), "no allowance") + key := allowanceKey(owner, spender) + urequire.False(t, b.allowances.Has(key), "allowance should be removed") + urequire.Equal(t, b.Allowance(owner, spender), uint64(0), "allowance should be 0") +} diff --git a/examples/gno.land/p/demo/grc/grc20/dummy_test.gno b/examples/gno.land/p/demo/grc/grc20/dummy_test.gno deleted file mode 100644 index 52ed7ecde31..00000000000 --- a/examples/gno.land/p/demo/grc/grc20/dummy_test.gno +++ /dev/null @@ -1,32 +0,0 @@ -package grc20 - -import ( - "std" - "testing" -) - -// TODO: test implementing an IMustGRC20 interface. -// TODO: test implementing a custom method that hides its usage of the IGRC20 interface. - -type dummyImpl struct{} - -// FIXME: this should fail. -var _ IGRC20 = (*dummyImpl)(nil) - -func TestInterface(t *testing.T) { - var dummy IGRC20 = &dummyImpl{} -} - -func (impl *dummyImpl) GetName() string { panic("not implemented") } -func (impl *dummyImpl) GetSymbol() string { panic("not implemented") } -func (impl *dummyImpl) GetDecimals() uint { panic("not implemented") } -func (impl *dummyImpl) TotalSupply() uint64 { panic("not implemented") } -func (impl *dummyImpl) BalanceOf(account std.Address) (uint64, error) { panic("not implemented") } -func (impl *dummyImpl) Transfer(to std.Address, amount uint64) error { panic("not implemented") } -func (impl *dummyImpl) Allowance(owner, spender std.Address) (uint64, error) { - panic("not implemented") -} -func (impl *dummyImpl) Approve(spender std.Address, amount uint64) error { panic("not implemented") } -func (impl *dummyImpl) TransferFrom(from, to std.Address, amount uint64) error { - panic("not implemented") -} diff --git a/examples/gno.land/p/demo/grc/grc20/errors.gno b/examples/gno.land/p/demo/grc/grc20/errors.gno deleted file mode 100644 index 68783ef0df9..00000000000 --- a/examples/gno.land/p/demo/grc/grc20/errors.gno +++ /dev/null @@ -1,10 +0,0 @@ -package grc20 - -import "errors" - -var ( - ErrInsufficientBalance = errors.New("insufficient balance") - ErrInsufficientAllowance = errors.New("insufficient allowance") - ErrInvalidAddress = errors.New("invalid address") - ErrCannotTransferToSelf = errors.New("cannot send transfer to self") -) diff --git a/examples/gno.land/p/demo/grc/grc20/gno.mod b/examples/gno.land/p/demo/grc/grc20/gno.mod index fd80766a956..e872d80ec12 100644 --- a/examples/gno.land/p/demo/grc/grc20/gno.mod +++ b/examples/gno.land/p/demo/grc/grc20/gno.mod @@ -3,5 +3,7 @@ module gno.land/p/demo/grc/grc20 require ( gno.land/p/demo/avl v0.0.0-latest gno.land/p/demo/grc/exts v0.0.0-latest + gno.land/p/demo/testutils v0.0.0-latest gno.land/p/demo/ufmt v0.0.0-latest + gno.land/p/demo/urequire v0.0.0-latest ) diff --git a/examples/gno.land/p/demo/grc/grc20/imustgrc20.gno b/examples/gno.land/p/demo/grc/grc20/imustgrc20.gno deleted file mode 100644 index 01623060ae7..00000000000 --- a/examples/gno.land/p/demo/grc/grc20/imustgrc20.gno +++ /dev/null @@ -1,21 +0,0 @@ -package grc20 - -import ( - "std" - - "gno.land/p/demo/grc/exts" -) - -// IMustGRC20 is like IGRC20 but without returned errors. -// -// It will either panic or silently ignore invalid usages, -// depending on the method. -type IMustGRC20 interface { - exts.TokenMetadata - TotalSupply() uint64 - BalanceOf(account std.Address) uint64 - Transfer(to std.Address, amount uint64) - Allowance(owner, spender std.Address) uint64 - Approve(spender std.Address, amount uint64) - TransferFrom(from, to std.Address, amount uint64) -} diff --git a/examples/gno.land/p/demo/grc/grc20/mustgrc20.gno b/examples/gno.land/p/demo/grc/grc20/mustgrc20.gno deleted file mode 100644 index 5e029e8e9b8..00000000000 --- a/examples/gno.land/p/demo/grc/grc20/mustgrc20.gno +++ /dev/null @@ -1,53 +0,0 @@ -package grc20 - -import "std" - -func Mustify(original IGRC20) IMustGRC20 { - return &mustGRC20{original: original} -} - -type mustGRC20 struct { - original IGRC20 -} - -func (t *mustGRC20) GetName() string { return t.original.GetName() } -func (t *mustGRC20) GetSymbol() string { return t.original.GetSymbol() } -func (t *mustGRC20) GetDecimals() uint { return t.original.GetDecimals() } -func (t *mustGRC20) TotalSupply() uint64 { return t.original.TotalSupply() } - -func (t *mustGRC20) BalanceOf(owner std.Address) uint64 { - balance, err := t.original.BalanceOf(owner) - if err != nil { - return 0 - } - return balance -} - -func (t *mustGRC20) Transfer(to std.Address, amount uint64) { - err := t.original.Transfer(to, amount) - if err != nil { - panic(err) - } -} - -func (t *mustGRC20) Allowance(owner, spender std.Address) uint64 { - allowance, err := t.original.Allowance(owner, spender) - if err != nil { - return 0 - } - return allowance -} - -func (t *mustGRC20) Approve(spender std.Address, amount uint64) { - err := t.original.Approve(spender, amount) - if err != nil { - panic(err) - } -} - -func (t *mustGRC20) TransferFrom(from, to std.Address, amount uint64) { - err := t.original.TransferFrom(from, to, amount) - if err != nil { - panic(err) - } -} diff --git a/examples/gno.land/p/demo/grc/grc20/mustgrc20_test.gno b/examples/gno.land/p/demo/grc/grc20/mustgrc20_test.gno deleted file mode 100644 index ea3d6fdaf65..00000000000 --- a/examples/gno.land/p/demo/grc/grc20/mustgrc20_test.gno +++ /dev/null @@ -1,3 +0,0 @@ -package grc20 - -// TODO: unit tests against MustGRC20 interfaces and helpers. diff --git a/examples/gno.land/p/demo/grc/grc20/token.gno b/examples/gno.land/p/demo/grc/grc20/token.gno new file mode 100644 index 00000000000..e13599e90bb --- /dev/null +++ b/examples/gno.land/p/demo/grc/grc20/token.gno @@ -0,0 +1,88 @@ +package grc20 + +import ( + "std" + + "gno.land/p/demo/grc/exts" +) + +// token implements the Token interface. +// +// It is generated with Banker.Token(). +// It can safely be explosed publicly. +type token struct { + banker *Banker +} + +// var _ Token = (*token)(nil) +func (t *token) GetName() string { return t.banker.name } +func (t *token) GetSymbol() string { return t.banker.symbol } +func (t *token) GetDecimals() uint { return t.banker.decimals } +func (t *token) TotalSupply() uint64 { return t.banker.totalSupply } + +func (t *token) BalanceOf(owner std.Address) uint64 { + return t.banker.BalanceOf(owner) +} + +func (t *token) Transfer(to std.Address, amount uint64) error { + caller := std.PrevRealm().Addr() + return t.banker.Transfer(caller, to, amount) +} + +func (t *token) Allowance(owner, spender std.Address) uint64 { + return t.banker.Allowance(owner, spender) +} + +func (t *token) Approve(spender std.Address, amount uint64) error { + caller := std.PrevRealm().Addr() + return t.banker.Approve(caller, spender, amount) +} + +func (t *token) TransferFrom(from, to std.Address, amount uint64) error { + spender := std.PrevRealm().Addr() + if err := t.banker.SpendAllowance(from, spender, amount); err != nil { + return err + } + return t.banker.Transfer(from, to, amount) +} + +type Token2 interface { + exts.TokenMetadata + + // Returns the amount of tokens in existence. + TotalSupply() uint64 + + // Returns the amount of tokens owned by `account`. + BalanceOf(account std.Address) uint64 + + // Moves `amount` tokens from the caller's account to `to`. + // + // Returns an error if the operation failed. + Transfer(to std.Address, amount uint64) error + + // Returns the remaining number of tokens that `spender` will be + // allowed to spend on behalf of `owner` through {transferFrom}. This is + // zero by default. + // + // This value changes when {approve} or {transferFrom} are called. + Allowance(owner, spender std.Address) uint64 + + // Sets `amount` as the allowance of `spender` over the caller's tokens. + // + // Returns an error if the operation failed. + // + // IMPORTANT: Beware that changing an allowance with this method brings the risk + // that someone may use both the old and the new allowance by unfortunate + // transaction ordering. One possible solution to mitigate this race + // condition is to first reduce the spender's allowance to 0 and set the + // desired value afterwards: + // https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729 + Approve(spender std.Address, amount uint64) error + + // Moves `amount` tokens from `from` to `to` using the + // allowance mechanism. `amount` is then deducted from the caller's + // allowance. + // + // Returns an error if the operation failed. + TransferFrom(from, to std.Address, amount uint64) error +} diff --git a/examples/gno.land/p/demo/grc/grc20/user_token_test.gno b/examples/gno.land/p/demo/grc/grc20/token_test.gno similarity index 52% rename from examples/gno.land/p/demo/grc/grc20/user_token_test.gno rename to examples/gno.land/p/demo/grc/grc20/token_test.gno index b2a923cec47..713ad734ed8 100644 --- a/examples/gno.land/p/demo/grc/grc20/user_token_test.gno +++ b/examples/gno.land/p/demo/grc/grc20/token_test.gno @@ -3,35 +3,42 @@ package grc20 import ( "std" "testing" + + "gno.land/p/demo/testutils" + "gno.land/p/demo/ufmt" + "gno.land/p/demo/urequire" ) func TestUserTokenImpl(t *testing.T) { - dummyAdmin := NewAdminToken("Dummy", "DUMMY", 4) - dummyUser := dummyAdmin.GRC20() - _ = dummyUser + bank := NewBanker("Dummy", "DUMMY", 4) + tok := bank.Token() + _ = tok } func TestUserApprove(t *testing.T) { - owner := std.Address("g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj") - spender := std.Address("g1us8428u2a5satrlxzagsqa5m6vmuze027sxc8x") - dest := std.Address("g1us8428m6a5satrlxzagsqa5m6vmuze02tyelwj") + owner := testutils.TestAddress("owner") + spender := testutils.TestAddress("spender") + dest := testutils.TestAddress("dest") - dummyAdmin := NewAdminToken("Dummy", "DUMMY", 6) + bank := NewBanker("Dummy", "DUMMY", 6) + tok := bank.Token() // Set owner as the original caller std.TestSetOrigCaller(owner) // Mint 100000000 tokens for owner - assertE(t, dummyAdmin.Mint(owner, 100000000)) + urequire.NoError(t, bank.Mint(owner, 100000000)) - dummyUser := dummyAdmin.GRC20() // Approve spender to spend 5000000 tokens - assertE(t, dummyUser.Approve(spender, 5000000)) + urequire.NoError(t, tok.Approve(spender, 5000000)) // Set spender as the original caller std.TestSetOrigCaller(spender) // Try to transfer 10000000 tokens from owner to dest, should fail because it exceeds allowance - err := dummyUser.TransferFrom(owner, dest, 10000000) - assert(t, err != nil, "should not be able to transfer more than approved") + urequire.Error(t, + tok.TransferFrom(owner, dest, 10000000), + ErrInsufficientAllowance.Error(), + "should not be able to transfer more than approved", + ) // Define a set of test data with spend amount and expected remaining allowance tests := []struct { @@ -45,22 +52,21 @@ func TestUserApprove(t *testing.T) { // perform transfer operation,and check if allowance and balance are correct for _, tt := range tests { - b0, _ := dummyUser.BalanceOf(dest) + b0 := tok.BalanceOf(dest) // Perform transfer from owner to dest - assertE(t, dummyUser.TransferFrom(owner, dest, tt.spend)) - a, _ := dummyUser.Allowance(owner, spender) + urequire.NoError(t, tok.TransferFrom(owner, dest, tt.spend)) + a := tok.Allowance(owner, spender) // Check if allowance equals expected value - assert(t, a == tt.exp, "allowance exp: %d,got %d", tt.exp, a) + urequire.True(t, a == tt.exp, ufmt.Sprintf("allowance exp: %d,got %d", tt.exp, a)) // Get dest current balance - b, _ := dummyUser.BalanceOf(dest) + b := tok.BalanceOf(dest) // Calculate expected balance ,should be initial balance plus transfer amount expB := b0 + tt.spend // Check if balance equals expected value - assert(t, b == expB, "balance exp: %d,got %d", expB, b) + urequire.True(t, b == expB, ufmt.Sprintf("balance exp: %d,got %d", expB, b)) } // Try to transfer one token from owner to dest ,should fail because no allowance left - err = dummyUser.TransferFrom(owner, dest, 1) - assert(t, err != nil, "no allowance") + urequire.Error(t, tok.TransferFrom(owner, dest, 1), ErrInsufficientAllowance.Error(), "no allowance") } diff --git a/examples/gno.land/p/demo/grc/grc20/igrc20.gno b/examples/gno.land/p/demo/grc/grc20/types.gno similarity index 72% rename from examples/gno.land/p/demo/grc/grc20/igrc20.gno rename to examples/gno.land/p/demo/grc/grc20/types.gno index 1256ec22086..fe3aef349d9 100644 --- a/examples/gno.land/p/demo/grc/grc20/igrc20.gno +++ b/examples/gno.land/p/demo/grc/grc20/types.gno @@ -1,19 +1,27 @@ package grc20 import ( + "errors" "std" "gno.land/p/demo/grc/exts" ) -type IGRC20 interface { +var ( + ErrInsufficientBalance = errors.New("insufficient balance") + ErrInsufficientAllowance = errors.New("insufficient allowance") + ErrInvalidAddress = errors.New("invalid address") + ErrCannotTransferToSelf = errors.New("cannot send transfer to self") +) + +type Token interface { exts.TokenMetadata // Returns the amount of tokens in existence. TotalSupply() uint64 // Returns the amount of tokens owned by `account`. - BalanceOf(account std.Address) (uint64, error) + BalanceOf(account std.Address) uint64 // Moves `amount` tokens from the caller's account to `to`. // @@ -25,7 +33,7 @@ type IGRC20 interface { // zero by default. // // This value changes when {approve} or {transferFrom} are called. - Allowance(owner, spender std.Address) (uint64, error) + Allowance(owner, spender std.Address) uint64 // Sets `amount` as the allowance of `spender` over the caller's tokens. // @@ -47,19 +55,7 @@ type IGRC20 interface { TransferFrom(from, to std.Address, amount uint64) error } -// Emitted when `value` tokens are moved from one account (`from`) to another (`to`). -// -// Note that `value` may be zero. -type TransferEvent struct { - From std.Address - To std.Address - Value uint64 -} - -// Emitted when the allowance of a `spender` for an `owner` is set by -// a call to {approve}. `value` is the new allowance. -type ApprovalEvent struct { - Owner std.Address - Spender std.Address - Value uint64 -} +const ( + TransferEvent = "Transfer" + ApprovalEvent = "Approval" +) diff --git a/examples/gno.land/p/demo/grc/grc20/user_token.gno b/examples/gno.land/p/demo/grc/grc20/user_token.gno deleted file mode 100644 index 7bb10c412b2..00000000000 --- a/examples/gno.land/p/demo/grc/grc20/user_token.gno +++ /dev/null @@ -1,49 +0,0 @@ -package grc20 - -import ( - "std" -) - -// userToken implements the IGRC20 interface. -// -// It is generated by userToken.GRC20(). -// It can safely be explosed publicly. -type userToken struct { - IGRC20 // implements the GRC20 interface. - - admin *AdminToken -} - -// IGRC20 implementation. -// - -func (t *userToken) GetName() string { return t.admin.name } -func (t *userToken) GetSymbol() string { return t.admin.symbol } -func (t *userToken) GetDecimals() uint { return t.admin.decimals } -func (t *userToken) TotalSupply() uint64 { return t.admin.totalSupply } - -func (t *userToken) BalanceOf(owner std.Address) (uint64, error) { - return t.admin.balanceOf(owner) -} - -func (t *userToken) Transfer(to std.Address, amount uint64) error { - caller := std.PrevRealm().Addr() - return t.admin.transfer(caller, to, amount) -} - -func (t *userToken) Allowance(owner, spender std.Address) (uint64, error) { - return t.admin.allowance(owner, spender) -} - -func (t *userToken) Approve(spender std.Address, amount uint64) error { - caller := std.PrevRealm().Addr() - return t.admin.approve(caller, spender, amount) -} - -func (t *userToken) TransferFrom(from, to std.Address, amount uint64) error { - spender := std.PrevRealm().Addr() - if err := t.admin.spendAllowance(from, spender, amount); err != nil { - return err - } - return t.admin.transfer(from, to, amount) -} diff --git a/examples/gno.land/p/demo/grc/grc20/util.gno b/examples/gno.land/p/demo/grc/grc20/util.gno deleted file mode 100644 index 2892b036bbd..00000000000 --- a/examples/gno.land/p/demo/grc/grc20/util.gno +++ /dev/null @@ -1,16 +0,0 @@ -package grc20 - -import "std" - -const zeroAddress = std.Address("") - -func checkIsValidAddress(addr std.Address) error { - if !addr.IsValid() { - return ErrInvalidAddress - } - return nil -} - -func emit(event interface{}) { - // TODO: setup a pubsub system here? -} diff --git a/examples/gno.land/r/demo/bar20/bar20.gno b/examples/gno.land/r/demo/bar20/bar20.gno index 7388d87d24d..1d6ecd3d378 100644 --- a/examples/gno.land/r/demo/bar20/bar20.gno +++ b/examples/gno.land/r/demo/bar20/bar20.gno @@ -1,6 +1,6 @@ -// Package bar20 is similar to foo20 but exposes a safe-object that can be used -// by `maketx run`, another contract importing foo20, and in the future when -// we'll support `maketx call Token.XXX`. +// Package bar20 is similar to gno.land/r/demo/foo20 but exposes a safe-object +// that can be used by `maketx run`, another contract importing foo20, and in +// the future when we'll support `maketx call Token.XXX`. package bar20 import ( @@ -12,13 +12,13 @@ import ( ) var ( - banker *grc20.AdminToken // private banker. - Token grc20.IGRC20 // public safe-object. + banker *grc20.Banker // private banker. + Token grc20.Token // public safe-object. ) func init() { - banker = grc20.NewAdminToken("Bar", "BAR", 4) - Token = banker.GRC20() + banker = grc20.NewBanker("Bar", "BAR", 4) + Token = banker.Token() } func Faucet() string { @@ -38,7 +38,7 @@ func Render(path string) string { return banker.RenderHome() // XXX: should be Token.RenderHome() case c == 2 && parts[0] == "balance": owner := std.Address(parts[1]) - balance, _ := Token.BalanceOf(owner) + balance := Token.BalanceOf(owner) return ufmt.Sprintf("%d\n", balance) default: return "404\n" diff --git a/examples/gno.land/r/demo/bar20/bar20_test.gno b/examples/gno.land/r/demo/bar20/bar20_test.gno index b2a49ebd864..20349258c1b 100644 --- a/examples/gno.land/r/demo/bar20/bar20_test.gno +++ b/examples/gno.land/r/demo/bar20/bar20_test.gno @@ -5,6 +5,7 @@ import ( "testing" "gno.land/p/demo/testutils" + "gno.land/p/demo/urequire" ) func TestPackage(t *testing.T) { @@ -12,20 +13,7 @@ func TestPackage(t *testing.T) { std.TestSetRealm(std.NewUserRealm(alice)) std.TestSetOrigCaller(alice) // XXX: should not need this - balance, _ := Token.BalanceOf(alice) - expected := uint64(0) - if balance != expected { - t.Errorf("balance should be %d, got %d", expected, balance) - } - - ret := Faucet() - if ret != "OK" { - t.Errorf("faucet should be OK, got %s", ret) - } - - balance, _ = Token.BalanceOf(alice) - expected = uint64(1_000_000) - if balance != expected { - t.Errorf("balance should be %d, got %d", expected, balance) - } + urequire.Equal(t, Token.BalanceOf(alice), uint64(0)) + urequire.Equal(t, Faucet(), "OK") + urequire.Equal(t, Token.BalanceOf(alice), uint64(1_000_000)) } diff --git a/examples/gno.land/r/demo/bar20/gno.mod b/examples/gno.land/r/demo/bar20/gno.mod index fd804beb4c4..2ec82d7be0b 100644 --- a/examples/gno.land/r/demo/bar20/gno.mod +++ b/examples/gno.land/r/demo/bar20/gno.mod @@ -4,4 +4,5 @@ require ( gno.land/p/demo/grc/grc20 v0.0.0-latest gno.land/p/demo/testutils v0.0.0-latest gno.land/p/demo/ufmt v0.0.0-latest + gno.land/p/demo/urequire v0.0.0-latest ) diff --git a/examples/gno.land/r/demo/foo20/foo20.gno b/examples/gno.land/r/demo/foo20/foo20.gno index 162454800e8..9d4e5d40193 100644 --- a/examples/gno.land/r/demo/foo20/foo20.gno +++ b/examples/gno.land/r/demo/foo20/foo20.gno @@ -1,3 +1,5 @@ +// foo20 is a GRC20 token contract where all the GRC20 methods are proxified +// with top-level functions. see also gno.land/r/demo/bar20. package foo20 import ( @@ -5,127 +7,93 @@ import ( "strings" "gno.land/p/demo/grc/grc20" + "gno.land/p/demo/ownable" "gno.land/p/demo/ufmt" - "gno.land/r/demo/users" - pusers "gno.land/p/demo/users" + "gno.land/r/demo/users" ) var ( - foo *grc20.AdminToken - admin std.Address = "g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj" // TODO: helper to change admin + banker *grc20.Banker + admin *ownable.Ownable + token grc20.Token ) func init() { - foo = grc20.NewAdminToken("Foo", "FOO", 4) - foo.Mint(admin, 1000000*10000) // @administrator (1M) - foo.Mint("g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq", 10000*10000) // @manfred (10k) + admin = ownable.NewWithAddress("g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq") // @manfred + banker = grc20.NewBanker("Foo", "FOO", 4) + banker.Mint(admin.Owner(), 1000000*10000) // @administrator (1M) + token = banker.Token() } -// method proxies as public functions. -// - -// getters. - -func TotalSupply() uint64 { - return foo.TotalSupply() -} +func TotalSupply() uint64 { return token.TotalSupply() } func BalanceOf(owner pusers.AddressOrName) uint64 { - balance, err := foo.BalanceOf(users.Resolve(owner)) - if err != nil { - panic(err) - } - return balance + ownerAddr := users.Resolve(owner) + return token.BalanceOf(ownerAddr) } func Allowance(owner, spender pusers.AddressOrName) uint64 { - allowance, err := foo.Allowance(users.Resolve(owner), users.Resolve(spender)) - if err != nil { - panic(err) - } - return allowance + ownerAddr := users.Resolve(owner) + spenderAddr := users.Resolve(spender) + return token.Allowance(ownerAddr, spenderAddr) } -// setters. - func Transfer(to pusers.AddressOrName, amount uint64) { - caller := std.PrevRealm().Addr() - err := foo.Transfer(caller, users.Resolve(to), amount) - if err != nil { - panic(err) - } + toAddr := users.Resolve(to) + checkErr(token.Transfer(toAddr, amount)) } func Approve(spender pusers.AddressOrName, amount uint64) { - caller := std.PrevRealm().Addr() - err := foo.Approve(caller, users.Resolve(spender), amount) - if err != nil { - panic(err) - } + spenderAddr := users.Resolve(spender) + checkErr(token.Approve(spenderAddr, amount)) } func TransferFrom(from, to pusers.AddressOrName, amount uint64) { - caller := std.PrevRealm().Addr() - err := foo.TransferFrom(caller, users.Resolve(from), users.Resolve(to), amount) - if err != nil { - panic(err) - } + fromAddr := users.Resolve(from) + toAddr := users.Resolve(to) + checkErr(token.TransferFrom(fromAddr, toAddr, amount)) } -// faucet. - +// Faucet is distributing foo20 tokens without restriction (unsafe). +// For a real token faucet, you should take care of setting limits are asking payment. func Faucet() { - // FIXME: add limits? - // FIXME: add payment in gnot? caller := std.PrevRealm().Addr() - err := foo.Mint(caller, 1000*10000) // 1k - if err != nil { - panic(err) - } + amount := uint64(1_000 * 10_000) // 1k + checkErr(banker.Mint(caller, amount)) } -// administration. - -func Mint(address pusers.AddressOrName, amount uint64) { - caller := std.PrevRealm().Addr() - assertIsAdmin(caller) - err := foo.Mint(users.Resolve(address), amount) - if err != nil { - panic(err) - } +func Mint(to pusers.AddressOrName, amount uint64) { + admin.AssertCallerIsOwner() + toAddr := users.Resolve(to) + checkErr(banker.Mint(toAddr, amount)) } -func Burn(address pusers.AddressOrName, amount uint64) { - caller := std.PrevRealm().Addr() - assertIsAdmin(caller) - err := foo.Burn(users.Resolve(address), amount) - if err != nil { - panic(err) - } +func Burn(from pusers.AddressOrName, amount uint64) { + admin.AssertCallerIsOwner() + fromAddr := users.Resolve(from) + checkErr(banker.Burn(fromAddr, amount)) } -// render. -// - func Render(path string) string { parts := strings.Split(path, "/") c := len(parts) switch { case path == "": - return foo.RenderHome() + return banker.RenderHome() case c == 2 && parts[0] == "balance": owner := pusers.AddressOrName(parts[1]) - balance, _ := foo.BalanceOf(users.Resolve(owner)) + ownerAddr := users.Resolve(owner) + balance := banker.BalanceOf(ownerAddr) return ufmt.Sprintf("%d\n", balance) default: return "404\n" } } -func assertIsAdmin(address std.Address) { - if address != admin { - panic("restricted access") +func checkErr(err error) { + if err != nil { + panic(err) } } diff --git a/examples/gno.land/r/demo/foo20/foo20_test.gno b/examples/gno.land/r/demo/foo20/foo20_test.gno index 9c452ed6e3b..77c99d0525e 100644 --- a/examples/gno.land/r/demo/foo20/foo20_test.gno +++ b/examples/gno.land/r/demo/foo20/foo20_test.gno @@ -4,15 +4,18 @@ import ( "std" "testing" + "gno.land/p/demo/testutils" "gno.land/p/demo/uassert" pusers "gno.land/p/demo/users" "gno.land/r/demo/users" ) func TestReadOnlyPublicMethods(t *testing.T) { - admin := pusers.AddressOrName("g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj") - manfred := pusers.AddressOrName("g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq") - unknown := pusers.AddressOrName("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // valid but never used. + var ( + admin = pusers.AddressOrName("g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq") + alice = pusers.AddressOrName(testutils.TestAddress("alice")) + bob = pusers.AddressOrName(testutils.TestAddress("bob")) + ) type test struct { name string @@ -23,43 +26,44 @@ func TestReadOnlyPublicMethods(t *testing.T) { // check balances #1. { tests := []test{ - {"TotalSupply", 10100000000, func() uint64 { return TotalSupply() }}, - {"BalanceOf(admin)", 10000000000, func() uint64 { return BalanceOf(admin) }}, - {"BalanceOf(manfred)", 100000000, func() uint64 { return BalanceOf(manfred) }}, - {"Allowance(admin, manfred)", 0, func() uint64 { return Allowance(admin, manfred) }}, - {"BalanceOf(unknown)", 0, func() uint64 { return BalanceOf(unknown) }}, + {"TotalSupply", 10_000_000_000, func() uint64 { return TotalSupply() }}, + {"BalanceOf(admin)", 10_000_000_000, func() uint64 { return BalanceOf(admin) }}, + {"BalanceOf(alice)", 0, func() uint64 { return BalanceOf(alice) }}, + {"Allowance(admin, alice)", 0, func() uint64 { return Allowance(admin, alice) }}, + {"BalanceOf(bob)", 0, func() uint64 { return BalanceOf(bob) }}, } for _, tc := range tests { - if tc.fn() != tc.balance { - t.Errorf("%s: have: %d want: %d", tc.name, tc.fn(), tc.balance) - } + got := tc.fn() + uassert.Equal(t, got, tc.balance) } } - // unknown uses the faucet. - std.TestSetOrigCaller(users.Resolve(unknown)) + // bob uses the faucet. + std.TestSetOrigCaller(users.Resolve(bob)) Faucet() // check balances #2. { tests := []test{ - {"TotalSupply", 10110000000, func() uint64 { return TotalSupply() }}, - {"BalanceOf(admin)", 10000000000, func() uint64 { return BalanceOf(admin) }}, - {"BalanceOf(manfred)", 100000000, func() uint64 { return BalanceOf(manfred) }}, - {"Allowance(admin, manfred)", 0, func() uint64 { return Allowance(admin, manfred) }}, - {"BalanceOf(unknown)", 10000000, func() uint64 { return BalanceOf(unknown) }}, + {"TotalSupply", 10_010_000_000, func() uint64 { return TotalSupply() }}, + {"BalanceOf(admin)", 10_000_000_000, func() uint64 { return BalanceOf(admin) }}, + {"BalanceOf(alice)", 0, func() uint64 { return BalanceOf(alice) }}, + {"Allowance(admin, alice)", 0, func() uint64 { return Allowance(admin, alice) }}, + {"BalanceOf(bob)", 10_000_000, func() uint64 { return BalanceOf(bob) }}, } for _, tc := range tests { - if tc.fn() != tc.balance { - t.Errorf("%s: have: %d want: %d", tc.name, tc.fn(), tc.balance) - } + got := tc.fn() + uassert.Equal(t, got, tc.balance) } } } func TestErrConditions(t *testing.T) { - admin := std.Address("g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj") - empty := std.Address("") + var ( + admin = pusers.AddressOrName("g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq") + alice = pusers.AddressOrName(testutils.TestAddress("alice")) + empty = pusers.AddressOrName("") + ) type test struct { name string @@ -67,11 +71,11 @@ func TestErrConditions(t *testing.T) { fn func() } - std.TestSetOrigCaller(admin) + std.TestSetOrigCaller(users.Resolve(admin)) { tests := []test{ - {"Transfer(admin, 1)", "cannot send transfer to self", func() { Transfer(pusers.AddressOrName(admin), 1) }}, - {"Approve(empty, 1))", "invalid address", func() { Approve(pusers.AddressOrName(empty), 1) }}, + {"Transfer(admin, 1)", "cannot send transfer to self", func() { Transfer(admin, 1) }}, + {"Approve(empty, 1))", "invalid address", func() { Approve(empty, 1) }}, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { diff --git a/examples/gno.land/r/demo/foo20/gno.mod b/examples/gno.land/r/demo/foo20/gno.mod index bf50c3b5202..4035f9b1200 100644 --- a/examples/gno.land/r/demo/foo20/gno.mod +++ b/examples/gno.land/r/demo/foo20/gno.mod @@ -2,6 +2,8 @@ module gno.land/r/demo/foo20 require ( gno.land/p/demo/grc/grc20 v0.0.0-latest + gno.land/p/demo/ownable v0.0.0-latest + gno.land/p/demo/testutils v0.0.0-latest gno.land/p/demo/uassert v0.0.0-latest gno.land/p/demo/ufmt v0.0.0-latest gno.land/p/demo/users v0.0.0-latest diff --git a/examples/gno.land/r/demo/grc20factory/gno.mod b/examples/gno.land/r/demo/grc20factory/gno.mod index a430a1f9559..864fdf21ff8 100644 --- a/examples/gno.land/r/demo/grc20factory/gno.mod +++ b/examples/gno.land/r/demo/grc20factory/gno.mod @@ -3,4 +3,6 @@ module gno.land/r/demo/grc20factory require ( gno.land/p/demo/avl v0.0.0-latest gno.land/p/demo/grc/grc20 v0.0.0-latest + gno.land/p/demo/ownable v0.0.0-latest + gno.land/p/demo/ufmt v0.0.0-latest ) diff --git a/examples/gno.land/r/demo/grc20factory/grc20factory.gno b/examples/gno.land/r/demo/grc20factory/grc20factory.gno index 0146d945f0d..f37a9370a9e 100644 --- a/examples/gno.land/r/demo/grc20factory/grc20factory.gno +++ b/examples/gno.land/r/demo/grc20factory/grc20factory.gno @@ -2,122 +2,103 @@ package foo20 import ( "std" - "strconv" "strings" "gno.land/p/demo/avl" "gno.land/p/demo/grc/grc20" + "gno.land/p/demo/ownable" + "gno.land/p/demo/ufmt" ) -// XXX: the p/grc20 library needs a refactor to change names (i.e., adminToken) +var instances avl.Tree // symbol -> instance -type token struct { - adminToken *grc20.AdminToken - admin std.Address -} - -var tokens avl.Tree // symbol -> token - -func NewToken(name, symbol string, decimals uint, initialMint uint64) { - admin := std.PrevRealm().Addr() - NewTokenWithAdmin(name, symbol, decimals, initialMint, admin) +func New(name, symbol string, decimals uint, initialMint, faucet uint64) { + caller := std.PrevRealm().Addr() + NewWithAdmin(name, symbol, decimals, initialMint, faucet, caller) } -func NewTokenWithAdmin(name, symbol string, decimals uint, initialMint uint64, admin std.Address) { - exists := tokens.Has(symbol) +func NewWithAdmin(name, symbol string, decimals uint, initialMint, faucet uint64, admin std.Address) { + exists := instances.Has(symbol) if exists { panic("token already exists") } - newToken := grc20.NewAdminToken(name, symbol, decimals) - newToken.Mint(admin, initialMint) - t := token{ - adminToken: newToken, - admin: admin, + banker := grc20.NewBanker(name, symbol, decimals) + if initialMint > 0 { + banker.Mint(admin, initialMint) + } + + inst := instance{ + banker: banker, + admin: ownable.NewWithAddress(admin), + faucet: faucet, } - tokens.Set(symbol, &t) + + instances.Set(symbol, &inst) } -// method proxies as public functions. -// +type instance struct { + banker *grc20.Banker + admin *ownable.Ownable + faucet uint64 // per-request amount. disabled if 0. +} -// getters. +func (inst instance) Token() grc20.Token { return inst.banker.Token() } func TotalSupply(symbol string) uint64 { - token := mustTokenBySymbol(symbol) - return token.adminToken.TotalSupply() + inst := mustGetInstance(symbol) + return inst.Token().TotalSupply() } func BalanceOf(symbol string, owner std.Address) uint64 { - token := mustTokenBySymbol(symbol) - balance, err := token.adminToken.BalanceOf(owner) - checkErr(err) - return balance + inst := mustGetInstance(symbol) + return inst.Token().BalanceOf(owner) } func Allowance(symbol string, owner, spender std.Address) uint64 { - token := mustTokenBySymbol(symbol) - allowance, err := token.adminToken.Allowance(owner, spender) - checkErr(err) - return allowance + inst := mustGetInstance(symbol) + return inst.Token().Allowance(owner, spender) } -// setters. - func Transfer(symbol string, to std.Address, amount uint64) { - token := mustTokenBySymbol(symbol) - caller := std.PrevRealm().Addr() - err := token.adminToken.Transfer(caller, to, amount) - checkErr(err) + inst := mustGetInstance(symbol) + checkErr(inst.Token().Transfer(to, amount)) } func Approve(symbol string, spender std.Address, amount uint64) { - token := mustTokenBySymbol(symbol) - caller := std.PrevRealm().Addr() - err := token.adminToken.Approve(caller, spender, amount) - checkErr(err) + inst := mustGetInstance(symbol) + checkErr(inst.Token().Approve(spender, amount)) } func TransferFrom(symbol string, from, to std.Address, amount uint64) { - token := mustTokenBySymbol(symbol) - caller := std.PrevRealm().Addr() - err := token.adminToken.TransferFrom(caller, from, to, amount) - if err != nil { - panic(err) - } + inst := mustGetInstance(symbol) + checkErr(inst.Token().TransferFrom(from, to, amount)) } // faucet. func Faucet(symbol string) { - token := mustTokenBySymbol(symbol) + inst := mustGetInstance(symbol) + if inst.faucet == 0 { + panic("faucet disabled for this token") + } // FIXME: add limits? // FIXME: add payment in gnot? caller := std.PrevRealm().Addr() - err := token.adminToken.Mint(caller, 1000*10000) // 1k - checkErr(err) + checkErr(inst.banker.Mint(caller, inst.faucet)) } -// administration. - -func Mint(symbol string, address std.Address, amount uint64) { - token := mustTokenBySymbol(symbol) - caller := std.PrevRealm().Addr() - assertIsAdmin(caller, token.admin) - err := token.adminToken.Mint(address, amount) - checkErr(err) +func Mint(symbol string, to std.Address, amount uint64) { + inst := mustGetInstance(symbol) + inst.admin.AssertCallerIsOwner() + checkErr(inst.banker.Mint(to, amount)) } -func Burn(symbol string, address std.Address, amount uint64) { - token := mustTokenBySymbol(symbol) - caller := std.PrevRealm().Addr() - assertIsAdmin(caller, token.admin) - err := token.adminToken.Burn(address, amount) - checkErr(err) +func Burn(symbol string, from std.Address, amount uint64) { + inst := mustGetInstance(symbol) + inst.admin.AssertCallerIsOwner() + checkErr(inst.banker.Burn(from, amount)) } -// render. -// - func Render(path string) string { parts := strings.Split(path, "/") c := len(parts) @@ -127,34 +108,25 @@ func Render(path string) string { return "TODO: list existing tokens and admins" case c == 1: symbol := parts[0] - token := mustTokenBySymbol(symbol) - return token.adminToken.RenderHome() + inst := mustGetInstance(symbol) + return inst.banker.RenderHome() case c == 3 && parts[1] == "balance": symbol := parts[0] - token := mustTokenBySymbol(symbol) + inst := mustGetInstance(symbol) owner := std.Address(parts[2]) - balance, _ := token.adminToken.BalanceOf(owner) - return strconv.FormatUint(balance, 10) + balance := inst.Token().BalanceOf(owner) + return ufmt.Sprintf("%d", balance) default: return "404\n" } } -// helpers. -// - -func assertIsAdmin(caller, admin std.Address) { - if caller != admin { - panic("restricted access") - } -} - -func mustTokenBySymbol(symbol string) *token { - t, exists := tokens.Get(symbol) +func mustGetInstance(symbol string) *instance { + t, exists := instances.Get(symbol) if !exists { - panic("token does not exist") + panic("token instance does not exist") } - return t.(*token) + return t.(*instance) } func checkErr(err error) { diff --git a/examples/gno.land/r/demo/grc20factory/grc20factory_test.gno b/examples/gno.land/r/demo/grc20factory/grc20factory_test.gno index fb1830ecef8..8b4cf3c6c1a 100644 --- a/examples/gno.land/r/demo/grc20factory/grc20factory_test.gno +++ b/examples/gno.land/r/demo/grc20factory/grc20factory_test.gno @@ -9,9 +9,9 @@ func TestReadOnlyPublicMethods(t *testing.T) { admin := std.Address("g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj") manfred := std.Address("g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq") unknown := std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // valid but never used. - NewTokenWithAdmin("Foo", "FOO", 4, 10000000000, admin) - NewTokenWithAdmin("Bar", "BAR", 4, 10000000, admin) - mustTokenBySymbol("FOO").adminToken.Mint(manfred, 100000000) + NewWithAdmin("Foo", "FOO", 4, 10_000*1_000_000, 0, admin) + NewWithAdmin("Bar", "BAR", 4, 10_000*1_000, 0, admin) + mustGetInstance("FOO").banker.Mint(manfred, 100_000_000) type test struct { name string @@ -22,9 +22,9 @@ func TestReadOnlyPublicMethods(t *testing.T) { // check balances #1. { tests := []test{ - {"TotalSupply", 10100000000, func() uint64 { return TotalSupply("FOO") }}, - {"BalanceOf(admin)", 10000000000, func() uint64 { return BalanceOf("FOO", admin) }}, - {"BalanceOf(manfred)", 100000000, func() uint64 { return BalanceOf("FOO", manfred) }}, + {"TotalSupply", 10_100_000_000, func() uint64 { return TotalSupply("FOO") }}, + {"BalanceOf(admin)", 10_000_000_000, func() uint64 { return BalanceOf("FOO", admin) }}, + {"BalanceOf(manfred)", 100_000_000, func() uint64 { return BalanceOf("FOO", manfred) }}, {"Allowance(admin, manfred)", 0, func() uint64 { return Allowance("FOO", admin, manfred) }}, {"BalanceOf(unknown)", 0, func() uint64 { return BalanceOf("FOO", unknown) }}, } @@ -43,11 +43,11 @@ func TestReadOnlyPublicMethods(t *testing.T) { // check balances #2. { tests := []test{ - {"TotalSupply", 10110000000, func() uint64 { return TotalSupply("FOO") }}, - {"BalanceOf(admin)", 10000000000, func() uint64 { return BalanceOf("FOO", admin) }}, - {"BalanceOf(manfred)", 100000000, func() uint64 { return BalanceOf("FOO", manfred) }}, + {"TotalSupply", 10_110_000_000, func() uint64 { return TotalSupply("FOO") }}, + {"BalanceOf(admin)", 10_000_000_000, func() uint64 { return BalanceOf("FOO", admin) }}, + {"BalanceOf(manfred)", 100_000_000, func() uint64 { return BalanceOf("FOO", manfred) }}, {"Allowance(admin, manfred)", 0, func() uint64 { return Allowance("FOO", admin, manfred) }}, - {"BalanceOf(unknown)", 10000000, func() uint64 { return BalanceOf("FOO", unknown) }}, + {"BalanceOf(unknown)", 10_000_000, func() uint64 { return BalanceOf("FOO", unknown) }}, } for _, tc := range tests { if tc.fn() != tc.balance { diff --git a/examples/gno.land/r/demo/wugnot/wugnot.gno b/examples/gno.land/r/demo/wugnot/wugnot.gno index 4896d23499e..2f73a6a4f35 100644 --- a/examples/gno.land/r/demo/wugnot/wugnot.gno +++ b/examples/gno.land/r/demo/wugnot/wugnot.gno @@ -6,17 +6,13 @@ import ( "gno.land/p/demo/grc/grc20" "gno.land/p/demo/ufmt" - - "gno.land/r/demo/users" - pusers "gno.land/p/demo/users" + "gno.land/r/demo/users" ) var ( - // wugnot is the admin token, able to mint and burn. - wugnot *grc20.AdminToken = grc20.NewAdminToken("wrapped GNOT", "wugnot", 0) - // WUGNOT is the banker usable by users directly. - WUGNOT = wugnot.GRC20() + banker *grc20.Banker = grc20.NewBanker("wrapped GNOT", "wugnot", 0) + Token = banker.Token() ) const ( @@ -24,106 +20,82 @@ const ( wugnotMinDeposit uint64 = 1 ) -// wrapper. -// - func Deposit() { caller := std.PrevRealm().Addr() sent := std.GetOrigSend() amount := sent.AmountOf("ugnot") - if uint64(amount) < ugnotMinDeposit { - panic(ufmt.Sprintf("Deposit below minimum: %d/%d ugnot.", amount, ugnotMinDeposit)) - } - wugnot.Mint(caller, uint64(amount)) + require(uint64(amount) >= ugnotMinDeposit, ufmt.Sprintf("Deposit below minimum: %d/%d ugnot.", amount, ugnotMinDeposit)) + checkErr(banker.Mint(caller, uint64(amount))) } func Withdraw(amount uint64) { - if amount < wugnotMinDeposit { - panic(ufmt.Sprintf("Deposit below minimum: %d/%d wugnot.", amount, wugnotMinDeposit)) - } + require(amount >= wugnotMinDeposit, ufmt.Sprintf("Deposit below minimum: %d/%d wugnot.", amount, wugnotMinDeposit)) caller := std.PrevRealm().Addr() pkgaddr := std.CurrentRealm().Addr() + callerBal := Token.BalanceOf(caller) + require(amount < callerBal, ufmt.Sprintf("Insufficient balance: %d available, %d needed.", callerBal, amount)) - callerBal, _ := wugnot.BalanceOf(caller) - if callerBal < amount { - panic(ufmt.Sprintf("Insufficient balance: %d available, %d needed.", callerBal, amount)) - } - - // send swapped ugnots to caller - banker := std.GetBanker(std.BankerTypeRealmSend) + // send swapped ugnots to qcaller + stdBanker := std.GetBanker(std.BankerTypeRealmSend) send := std.Coins{{"ugnot", int64(amount)}} - banker.SendCoins(pkgaddr, caller, send) - wugnot.Burn(caller, amount) + stdBanker.SendCoins(pkgaddr, caller, send) + checkErr(banker.Burn(caller, amount)) } -// render. -// - func Render(path string) string { parts := strings.Split(path, "/") c := len(parts) switch { case path == "": - return wugnot.RenderHome() + return banker.RenderHome() case c == 2 && parts[0] == "balance": owner := std.Address(parts[1]) - balance, _ := wugnot.BalanceOf(owner) - return ufmt.Sprintf("%d\n", balance) + balance := Token.BalanceOf(owner) + return ufmt.Sprintf("%d", balance) default: - return "404\n" + return "404" } } -// XXX: if we could call WUGNOT.XXX instead of XXX from gnokey, then, all the following lines would not be needed. - -// direct getters. -// XXX: remove them in favor of q_call wugnot.XXX - -func TotalSupply() uint64 { - return wugnot.TotalSupply() -} +func TotalSupply() uint64 { return Token.TotalSupply() } func BalanceOf(owner pusers.AddressOrName) uint64 { - balance, err := wugnot.BalanceOf(users.Resolve(owner)) - if err != nil { - panic(err) - } - return balance + ownerAddr := users.Resolve(owner) + return Token.BalanceOf(ownerAddr) } func Allowance(owner, spender pusers.AddressOrName) uint64 { - allowance, err := wugnot.Allowance(users.Resolve(owner), users.Resolve(spender)) - if err != nil { - panic(err) - } - return allowance + ownerAddr := users.Resolve(owner) + spenderAddr := users.Resolve(spender) + return Token.Allowance(ownerAddr, spenderAddr) } -// setters. -// - func Transfer(to pusers.AddressOrName, amount uint64) { - caller := std.PrevRealm().Addr() - err := wugnot.Transfer(caller, users.Resolve(to), amount) - if err != nil { - panic(err) - } + toAddr := users.Resolve(to) + checkErr(Token.Transfer(toAddr, amount)) } func Approve(spender pusers.AddressOrName, amount uint64) { - caller := std.PrevRealm().Addr() - err := wugnot.Approve(caller, users.Resolve(spender), amount) - if err != nil { - panic(err) - } + spenderAddr := users.Resolve(spender) + checkErr(Token.Approve(spenderAddr, amount)) } func TransferFrom(from, to pusers.AddressOrName, amount uint64) { - caller := std.PrevRealm().Addr() - err := wugnot.TransferFrom(caller, users.Resolve(from), users.Resolve(to), amount) + fromAddr := users.Resolve(from) + toAddr := users.Resolve(to) + checkErr(Token.TransferFrom(fromAddr, toAddr, amount)) +} + +func require(condition bool, msg string) { + if !condition { + panic(msg) + } +} + +func checkErr(err error) { if err != nil { panic(err) }