From 3bb666c0bc6538418f0fae4cec784b734b20b62c Mon Sep 17 00:00:00 2001 From: Manfred Touron <94029+moul@users.noreply.github.com> Date: Tue, 12 Nov 2024 17:41:48 +0100 Subject: [PATCH] feat(examples): grc20 refactor (#2983) This PR extracts the grc20 refactor from #2551, which is a meta PR containing several contract improvements and additions that depend on new Gnovm features that haven't been merged yet. Please review this grc20 refactor with a focus on its API. Several valuable comments can be found in #2551. Additionally, you can discover new contracts using grc20 in #2551, such as `minidex`, `atomicswap`, `grc20reg`, `test20`, and `vault`. Addresses #1832 --------- Signed-off-by: moul <94029+moul@users.noreply.github.com> Co-authored-by: Morgan Co-authored-by: Morgan Bazalgette Co-authored-by: Leon Hudak <33522493+leohhhn@users.noreply.github.com> --- examples/gno.land/p/demo/fqname/fqname.gno | 4 +- .../gno.land/p/demo/fqname/fqname_test.gno | 4 +- examples/gno.land/p/demo/grc/grc20/banker.gno | 215 --------------- .../gno.land/p/demo/grc/grc20/banker_test.gno | 51 ---- .../p/demo/grc/grc20/examples_test.gno | 18 ++ examples/gno.land/p/demo/grc/grc20/gno.mod | 1 + examples/gno.land/p/demo/grc/grc20/mock.gno | 3 + .../gno.land/p/demo/grc/grc20/tellers.gno | 139 ++++++++++ .../p/demo/grc/grc20/tellers_test.gno | 130 ++++++++++ examples/gno.land/p/demo/grc/grc20/token.gno | 245 ++++++++++++++++-- .../gno.land/p/demo/grc/grc20/token_test.gno | 125 +++++---- examples/gno.land/p/demo/grc/grc20/types.gno | 61 ++++- examples/gno.land/r/demo/bar20/bar20.gno | 11 +- examples/gno.land/r/demo/bar20/bar20_test.gno | 4 +- examples/gno.land/r/demo/foo20/foo20.gno | 44 ++-- examples/gno.land/r/demo/foo20/foo20_test.gno | 12 +- examples/gno.land/r/demo/grc20factory/gno.mod | 1 + .../r/demo/grc20factory/grc20factory.gno | 61 +++-- .../r/demo/grc20factory/grc20factory_test.gno | 76 +++--- examples/gno.land/r/demo/wugnot/wugnot.gno | 25 +- 20 files changed, 780 insertions(+), 450 deletions(-) delete mode 100644 examples/gno.land/p/demo/grc/grc20/banker.gno delete mode 100644 examples/gno.land/p/demo/grc/grc20/banker_test.gno create mode 100644 examples/gno.land/p/demo/grc/grc20/examples_test.gno create mode 100644 examples/gno.land/p/demo/grc/grc20/mock.gno create mode 100644 examples/gno.land/p/demo/grc/grc20/tellers.gno create mode 100644 examples/gno.land/p/demo/grc/grc20/tellers_test.gno diff --git a/examples/gno.land/p/demo/fqname/fqname.gno b/examples/gno.land/p/demo/fqname/fqname.gno index d28453e5c1b..8cccdb9e8b7 100644 --- a/examples/gno.land/p/demo/fqname/fqname.gno +++ b/examples/gno.land/p/demo/fqname/fqname.gno @@ -43,9 +43,9 @@ func Parse(fqname string) (pkgpath, name string) { // Construct a qualified identifier. // -// fqName := fqname.Construct("gno.land/r/demo/foo20", "GRC20") +// fqName := fqname.Construct("gno.land/r/demo/foo20", "Token") // fmt.Println("Fully Qualified Name:", fqName) -// // Output: gno.land/r/demo/foo20.GRC20 +// // Output: gno.land/r/demo/foo20.Token func Construct(pkgpath, name string) string { // TODO: ensure pkgpath is valid - and as such last part does not contain a dot. if name == "" { diff --git a/examples/gno.land/p/demo/fqname/fqname_test.gno b/examples/gno.land/p/demo/fqname/fqname_test.gno index 55a220776be..5f0f83968a3 100644 --- a/examples/gno.land/p/demo/fqname/fqname_test.gno +++ b/examples/gno.land/p/demo/fqname/fqname_test.gno @@ -36,7 +36,7 @@ func TestConstruct(t *testing.T) { name string expected string }{ - {"gno.land/r/demo/foo20", "GRC20", "gno.land/r/demo/foo20.GRC20"}, + {"gno.land/r/demo/foo20", "Token", "gno.land/r/demo/foo20.Token"}, {"gno.land/r/demo/foo20", "", "gno.land/r/demo/foo20"}, {"path", "", "path"}, {"path", "Split", "path.Split"}, @@ -62,7 +62,7 @@ func TestRenderLink(t *testing.T) { {"gno.land/p/demo/avl", "", "[gno.land/p/demo/avl](/p/demo/avl)"}, {"github.com/a/b", "C", "github.com/a/b.C"}, {"example.com/pkg", "Func", "example.com/pkg.Func"}, - {"gno.land/r/demo/foo20", "GRC20", "[gno.land/r/demo/foo20](/r/demo/foo20).GRC20"}, + {"gno.land/r/demo/foo20", "Token", "[gno.land/r/demo/foo20](/r/demo/foo20).Token"}, {"gno.land/r/demo/foo20", "", "[gno.land/r/demo/foo20](/r/demo/foo20)"}, {"", "", ""}, } diff --git a/examples/gno.land/p/demo/grc/grc20/banker.gno b/examples/gno.land/p/demo/grc/grc20/banker.gno deleted file mode 100644 index 7a3ebb18ef5..00000000000 --- a/examples/gno.land/p/demo/grc/grc20/banker.gno +++ /dev/null @@ -1,215 +0,0 @@ -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( - MintEvent, - "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( - BurnEvent, - "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) - - 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 deleted file mode 100644 index 00a1e75df1f..00000000000 --- a/examples/gno.land/p/demo/grc/grc20/banker_test.gno +++ /dev/null @@ -1,51 +0,0 @@ -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/examples_test.gno b/examples/gno.land/p/demo/grc/grc20/examples_test.gno new file mode 100644 index 00000000000..6a2bfa11d8c --- /dev/null +++ b/examples/gno.land/p/demo/grc/grc20/examples_test.gno @@ -0,0 +1,18 @@ +package grc20 + +// XXX: write Examples + +func ExampleInit() {} +func ExampleExposeBankForMaketxRunOrImports() {} +func ExampleCustomTellerImpl() {} +func ExampleAllowance() {} +func ExampleRealmBanker() {} +func ExamplePrevRealmBanker() {} +func ExampleAccountBanker() {} +func ExampleTransfer() {} +func ExampleApprove() {} +func ExampleTransferFrom() {} +func ExampleMint() {} +func ExampleBurn() {} + +// ... diff --git a/examples/gno.land/p/demo/grc/grc20/gno.mod b/examples/gno.land/p/demo/grc/grc20/gno.mod index e872d80ec12..91b430d3d2f 100644 --- a/examples/gno.land/p/demo/grc/grc20/gno.mod +++ b/examples/gno.land/p/demo/grc/grc20/gno.mod @@ -4,6 +4,7 @@ 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/uassert 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/mock.gno b/examples/gno.land/p/demo/grc/grc20/mock.gno new file mode 100644 index 00000000000..4952470d665 --- /dev/null +++ b/examples/gno.land/p/demo/grc/grc20/mock.gno @@ -0,0 +1,3 @@ +package grc20 + +// XXX: func Mock(t *Token) diff --git a/examples/gno.land/p/demo/grc/grc20/tellers.gno b/examples/gno.land/p/demo/grc/grc20/tellers.gno new file mode 100644 index 00000000000..ee5d2d7fcca --- /dev/null +++ b/examples/gno.land/p/demo/grc/grc20/tellers.gno @@ -0,0 +1,139 @@ +package grc20 + +import ( + "std" +) + +// CallerTeller returns a GRC20 compatible teller that checks the PrevRealm +// caller for each call. It's usually safe to expose it publicly to let users +// manipulate their tokens directly, or for realms to use their allowance. +func (tok *Token) CallerTeller() Teller { + if tok == nil { + panic("Token cannot be nil") + } + + return &fnTeller{ + accountFn: func() std.Address { + caller := std.PrevRealm().Addr() + return caller + }, + Token: tok, + } +} + +// ReadonlyTeller is a GRC20 compatible teller that panics for any write operation. +func (tok *Token) ReadonlyTeller() Teller { + if tok == nil { + panic("Token cannot be nil") + } + + return &fnTeller{ + accountFn: nil, + Token: tok, + } +} + +// RealmTeller returns a GRC20 compatible teller that will store the +// caller realm permanently. Calling anything through this teller will +// result in allowance or balance changes for the realm that initialized the teller. +// The initializer of this teller should usually never share the resulting Teller from +// this method except maybe for advanced delegation flows such as a DAO treasury +// management. +func (tok *Token) RealmTeller() Teller { + if tok == nil { + panic("Token cannot be nil") + } + + caller := std.PrevRealm().Addr() + + return &fnTeller{ + accountFn: func() std.Address { + return caller + }, + Token: tok, + } +} + +// RealmSubTeller is like RealmTeller but uses the provided slug to derive a +// subaccount. +func (tok *Token) RealmSubTeller(slug string) Teller { + if tok == nil { + panic("Token cannot be nil") + } + + caller := std.PrevRealm().Addr() + account := accountSlugAddr(caller, slug) + + return &fnTeller{ + accountFn: func() std.Address { + return account + }, + Token: tok, + } +} + +// ImpersonateTeller returns a GRC20 compatible teller that impersonates as a +// specified address. This allows operations to be performed as if they were +// executed by the given address, enabling the caller to manipulate tokens on +// behalf of that address. +// +// It is particularly useful in scenarios where a contract needs to perform +// actions on behalf of a user or another account, without exposing the +// underlying logic or requiring direct access to the user's account. The +// returned teller will use the provided address for all operations, effectively +// masking the original caller. +// +// This method should be used with caution, as it allows for potentially +// sensitive operations to be performed under the guise of another address. +func (ledger *PrivateLedger) ImpersonateTeller(addr std.Address) Teller { + if ledger == nil { + panic("Ledger cannot be nil") + } + + return &fnTeller{ + accountFn: func() std.Address { + return addr + }, + Token: ledger.token, + } +} + +// generic tellers methods. +// + +func (ft *fnTeller) Transfer(to std.Address, amount uint64) error { + if ft.accountFn == nil { + return ErrReadonly + } + caller := ft.accountFn() + return ft.Token.ledger.Transfer(caller, to, amount) +} + +func (ft *fnTeller) Approve(spender std.Address, amount uint64) error { + if ft.accountFn == nil { + return ErrReadonly + } + caller := ft.accountFn() + return ft.Token.ledger.Approve(caller, spender, amount) +} + +func (ft *fnTeller) TransferFrom(owner, to std.Address, amount uint64) error { + if ft.accountFn == nil { + return ErrReadonly + } + spender := ft.accountFn() + return ft.Token.ledger.TransferFrom(owner, spender, to, amount) +} + +// helpers +// + +// accountSlugAddr returns the address derived from the specified address and slug. +func accountSlugAddr(addr std.Address, slug string) std.Address { + // XXX: use a new `std.XXX` call for this. + if slug == "" { + return addr + } + key := addr.String() + "/" + slug + return std.DerivePkgAddr(key) // temporarily using this helper +} diff --git a/examples/gno.land/p/demo/grc/grc20/tellers_test.gno b/examples/gno.land/p/demo/grc/grc20/tellers_test.gno new file mode 100644 index 00000000000..2a724964edc --- /dev/null +++ b/examples/gno.land/p/demo/grc/grc20/tellers_test.gno @@ -0,0 +1,130 @@ +package grc20 + +import ( + "std" + "testing" + + "gno.land/p/demo/testutils" + "gno.land/p/demo/uassert" + "gno.land/p/demo/ufmt" + "gno.land/p/demo/urequire" +) + +func TestCallerTellerImpl(t *testing.T) { + tok, _ := NewToken("Dummy", "DUMMY", 4) + teller := tok.CallerTeller() + urequire.False(t, tok == nil) + var _ Teller = teller +} + +func TestTeller(t *testing.T) { + var ( + alice = testutils.TestAddress("alice") + bob = testutils.TestAddress("bob") + carl = testutils.TestAddress("carl") + ) + + token, ledger := NewToken("Dummy", "DUMMY", 6) + + checkBalances := func(aliceEB, bobEB, carlEB uint64) { + t.Helper() + exp := ufmt.Sprintf("alice=%d bob=%d carl=%d", aliceEB, bobEB, carlEB) + aliceGB := token.BalanceOf(alice) + bobGB := token.BalanceOf(bob) + carlGB := token.BalanceOf(carl) + got := ufmt.Sprintf("alice=%d bob=%d carl=%d", aliceGB, bobGB, carlGB) + uassert.Equal(t, got, exp, "invalid balances") + } + checkAllowances := func(abEB, acEB, baEB, bcEB, caEB, cbEB uint64) { + t.Helper() + exp := ufmt.Sprintf("ab=%d ac=%d ba=%d bc=%d ca=%d cb=%s", abEB, acEB, baEB, bcEB, caEB, cbEB) + abGB := token.Allowance(alice, bob) + acGB := token.Allowance(alice, carl) + baGB := token.Allowance(bob, alice) + bcGB := token.Allowance(bob, carl) + caGB := token.Allowance(carl, alice) + cbGB := token.Allowance(carl, bob) + got := ufmt.Sprintf("ab=%d ac=%d ba=%d bc=%d ca=%d cb=%s", abGB, acGB, baGB, bcGB, caGB, cbGB) + uassert.Equal(t, got, exp, "invalid allowances") + } + + checkBalances(0, 0, 0) + checkAllowances(0, 0, 0, 0, 0, 0) + + urequire.NoError(t, ledger.Mint(alice, 1000)) + urequire.NoError(t, ledger.Mint(alice, 100)) + checkBalances(1100, 0, 0) + checkAllowances(0, 0, 0, 0, 0, 0) + + urequire.NoError(t, ledger.Approve(alice, bob, 99999999)) + checkBalances(1100, 0, 0) + checkAllowances(99999999, 0, 0, 0, 0, 0) + + urequire.NoError(t, ledger.Approve(alice, bob, 400)) + checkBalances(1100, 0, 0) + checkAllowances(400, 0, 0, 0, 0, 0) + + urequire.Error(t, ledger.TransferFrom(alice, bob, carl, 100000000)) + checkBalances(1100, 0, 0) + checkAllowances(400, 0, 0, 0, 0, 0) + + urequire.NoError(t, ledger.TransferFrom(alice, bob, carl, 100)) + checkBalances(1000, 0, 100) + checkAllowances(300, 0, 0, 0, 0, 0) + + urequire.Error(t, ledger.SpendAllowance(alice, bob, 2000000)) + checkBalances(1000, 0, 100) + checkAllowances(300, 0, 0, 0, 0, 0) + + urequire.NoError(t, ledger.SpendAllowance(alice, bob, 100)) + checkBalances(1000, 0, 100) + checkAllowances(200, 0, 0, 0, 0, 0) +} + +func TestCallerTeller(t *testing.T) { + alice := testutils.TestAddress("alice") + bob := testutils.TestAddress("bob") + carl := testutils.TestAddress("carl") + + token, ledger := NewToken("Dummy", "DUMMY", 6) + teller := token.CallerTeller() + + checkBalances := func(aliceEB, bobEB, carlEB uint64) { + t.Helper() + exp := ufmt.Sprintf("alice=%d bob=%d carl=%d", aliceEB, bobEB, carlEB) + aliceGB := token.BalanceOf(alice) + bobGB := token.BalanceOf(bob) + carlGB := token.BalanceOf(carl) + got := ufmt.Sprintf("alice=%d bob=%d carl=%d", aliceGB, bobGB, carlGB) + uassert.Equal(t, got, exp, "invalid balances") + } + checkAllowances := func(abEB, acEB, baEB, bcEB, caEB, cbEB uint64) { + t.Helper() + exp := ufmt.Sprintf("ab=%d ac=%d ba=%d bc=%d ca=%d cb=%s", abEB, acEB, baEB, bcEB, caEB, cbEB) + abGB := token.Allowance(alice, bob) + acGB := token.Allowance(alice, carl) + baGB := token.Allowance(bob, alice) + bcGB := token.Allowance(bob, carl) + caGB := token.Allowance(carl, alice) + cbGB := token.Allowance(carl, bob) + got := ufmt.Sprintf("ab=%d ac=%d ba=%d bc=%d ca=%d cb=%s", abGB, acGB, baGB, bcGB, caGB, cbGB) + uassert.Equal(t, got, exp, "invalid allowances") + } + + urequire.NoError(t, ledger.Mint(alice, 1000)) + checkBalances(1000, 0, 0) + checkAllowances(0, 0, 0, 0, 0, 0) + + std.TestSetOrigCaller(alice) + urequire.NoError(t, teller.Approve(bob, 600)) + checkBalances(1000, 0, 0) + checkAllowances(600, 0, 0, 0, 0, 0) + + std.TestSetOrigCaller(bob) + urequire.Error(t, teller.TransferFrom(alice, carl, 700)) + checkBalances(1000, 0, 0) + checkAllowances(600, 0, 0, 0, 0, 0) + urequire.NoError(t, teller.TransferFrom(alice, carl, 400)) + checkBalances(600, 0, 400) + checkAllowances(200, 0, 0, 0, 0, 0) +} diff --git a/examples/gno.land/p/demo/grc/grc20/token.gno b/examples/gno.land/p/demo/grc/grc20/token.gno index c9e125261b5..4634bae933b 100644 --- a/examples/gno.land/p/demo/grc/grc20/token.gno +++ b/examples/gno.land/p/demo/grc/grc20/token.gno @@ -1,45 +1,240 @@ package grc20 import ( + "math/overflow" "std" + "strconv" + + "gno.land/p/demo/ufmt" ) -// token implements the Token interface. -// -// It is generated with Banker.Token(). -// It can safely be exposed publicly. -type token struct { - banker *Banker +// NewToken creates a new Token. +// It returns a pointer to the Token and a pointer to the Ledger. +// Expected usage: Token, admin := NewToken("Dummy", "DUMMY", 4) +func NewToken(name, symbol string, decimals uint) (*Token, *PrivateLedger) { + if name == "" { + panic("name should not be empty") + } + if symbol == "" { + panic("symbol should not be empty") + } + // XXX additional checks (length, characters, limits, etc) + + ledger := &PrivateLedger{} + token := &Token{ + name: name, + symbol: symbol, + decimals: decimals, + ledger: ledger, + } + ledger.token = token + return token, ledger } -// 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 } +// GetName returns the name of the token. +func (tok Token) GetName() string { return tok.name } + +// GetSymbol returns the symbol of the token. +func (tok Token) GetSymbol() string { return tok.symbol } + +// GetDecimals returns the number of decimals used to get the token's precision. +func (tok Token) GetDecimals() uint { return tok.decimals } + +// TotalSupply returns the total supply of the token. +func (tok Token) TotalSupply() uint64 { return tok.ledger.totalSupply } -func (t *token) BalanceOf(owner std.Address) uint64 { - return t.banker.BalanceOf(owner) +// KnownAccounts returns the number of known accounts in the bank. +func (tok Token) KnownAccounts() int { return tok.ledger.balances.Size() } + +// BalanceOf returns the balance of the specified address. +func (tok Token) BalanceOf(address std.Address) uint64 { + return tok.ledger.balanceOf(address) } -func (t *token) Transfer(to std.Address, amount uint64) error { - caller := std.PrevRealm().Addr() - return t.banker.Transfer(caller, to, amount) +// Allowance returns the allowance of the specified owner and spender. +func (tok Token) Allowance(owner, spender std.Address) uint64 { + return tok.ledger.allowance(owner, spender) } -func (t *token) Allowance(owner, spender std.Address) uint64 { - return t.banker.Allowance(owner, spender) +func (tok *Token) RenderHome() string { + str := "" + str += ufmt.Sprintf("# %s ($%s)\n\n", tok.name, tok.symbol) + str += ufmt.Sprintf("* **Decimals**: %d\n", tok.decimals) + str += ufmt.Sprintf("* **Total supply**: %d\n", tok.ledger.totalSupply) + str += ufmt.Sprintf("* **Known accounts**: %d\n", tok.KnownAccounts()) + return str } -func (t *token) Approve(spender std.Address, amount uint64) error { - caller := std.PrevRealm().Addr() - return t.banker.Approve(caller, spender, amount) +// SpendAllowance decreases the allowance of the specified owner and spender. +func (led *PrivateLedger) SpendAllowance(owner, spender std.Address, amount uint64) error { + if !owner.IsValid() { + return ErrInvalidAddress + } + if !spender.IsValid() { + return ErrInvalidAddress + } + + currentAllowance := led.allowance(owner, spender) + if currentAllowance < amount { + return ErrInsufficientAllowance + } + + key := allowanceKey(owner, spender) + newAllowance := currentAllowance - amount + + if newAllowance == 0 { + led.allowances.Remove(key) + } else { + led.allowances.Set(key, newAllowance) + } + + return nil } -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 { +// Transfer transfers tokens from the specified from address to the specified to address. +func (led *PrivateLedger) Transfer(from, to std.Address, amount uint64) error { + if !from.IsValid() { + return ErrInvalidAddress + } + if !to.IsValid() { + return ErrInvalidAddress + } + if from == to { + return ErrCannotTransferToSelf + } + + var ( + toBalance = led.balanceOf(to) + fromBalance = led.balanceOf(from) + ) + + if fromBalance < amount { + return ErrInsufficientBalance + } + + var ( + newToBalance = toBalance + amount + newFromBalance = fromBalance - amount + ) + + led.balances.Set(string(to), newToBalance) + led.balances.Set(string(from), newFromBalance) + + std.Emit( + TransferEvent, + "from", from.String(), + "to", to.String(), + "value", strconv.Itoa(int(amount)), + ) + + return nil +} + +// TransferFrom transfers tokens from the specified owner to the specified to address. +// It first checks if the owner has sufficient balance and then decreases the allowance. +func (led *PrivateLedger) TransferFrom(owner, spender, to std.Address, amount uint64) error { + if led.balanceOf(owner) < amount { + return ErrInsufficientBalance + } + if err := led.SpendAllowance(owner, spender, amount); err != nil { return err } - return t.banker.Transfer(from, to, amount) + // XXX: since we don't "panic", we should take care of rollbacking spendAllowance if transfer fails. + return led.Transfer(owner, to, amount) +} + +// Approve sets the allowance of the specified owner and spender. +func (led *PrivateLedger) Approve(owner, spender std.Address, amount uint64) error { + if !owner.IsValid() || !spender.IsValid() { + return ErrInvalidAddress + } + + led.allowances.Set(allowanceKey(owner, spender), amount) + + std.Emit( + ApprovalEvent, + "owner", string(owner), + "spender", string(spender), + "value", strconv.Itoa(int(amount)), + ) + + return nil +} + +// Mint increases the total supply of the token and adds the specified amount to the specified address. +func (led *PrivateLedger) Mint(address std.Address, amount uint64) error { + if !address.IsValid() { + return ErrInvalidAddress + } + + // XXX: math/overflow is not supporting uint64. + // This checks prevents overflow but makes the totalSupply limited to a uint63. + sum, ok := overflow.Add64(int64(led.totalSupply), int64(amount)) + if !ok { + return ErrOverflow + } + + led.totalSupply = uint64(sum) + currentBalance := led.balanceOf(address) + newBalance := currentBalance + amount + + led.balances.Set(string(address), newBalance) + + std.Emit( + TransferEvent, + "from", "", + "to", string(address), + "value", strconv.Itoa(int(amount)), + ) + + return nil +} + +// Burn decreases the total supply of the token and subtracts the specified amount from the specified address. +func (led *PrivateLedger) Burn(address std.Address, amount uint64) error { + if !address.IsValid() { + return ErrInvalidAddress + } + + currentBalance := led.balanceOf(address) + if currentBalance < amount { + return ErrInsufficientBalance + } + + led.totalSupply -= amount + newBalance := currentBalance - amount + + led.balances.Set(string(address), newBalance) + + std.Emit( + TransferEvent, + "from", string(address), + "to", "", + "value", strconv.Itoa(int(amount)), + ) + + return nil +} + +// balanceOf returns the balance of the specified address. +func (led PrivateLedger) balanceOf(address std.Address) uint64 { + balance, found := led.balances.Get(address.String()) + if !found { + return 0 + } + return balance.(uint64) +} + +// allowance returns the allowance of the specified owner and spender. +func (led PrivateLedger) allowance(owner, spender std.Address) uint64 { + allowance, found := led.allowances.Get(allowanceKey(owner, spender)) + if !found { + return 0 + } + return allowance.(uint64) +} + +// allowanceKey returns the key for the allowance of the specified owner and spender. +func allowanceKey(owner, spender std.Address) string { + return owner.String() + ":" + spender.String() } diff --git a/examples/gno.land/p/demo/grc/grc20/token_test.gno b/examples/gno.land/p/demo/grc/grc20/token_test.gno index 713ad734ed8..c68513554f0 100644 --- a/examples/gno.land/p/demo/grc/grc20/token_test.gno +++ b/examples/gno.land/p/demo/grc/grc20/token_test.gno @@ -1,72 +1,89 @@ package grc20 import ( - "std" "testing" "gno.land/p/demo/testutils" + "gno.land/p/demo/uassert" "gno.land/p/demo/ufmt" "gno.land/p/demo/urequire" ) -func TestUserTokenImpl(t *testing.T) { - bank := NewBanker("Dummy", "DUMMY", 4) - tok := bank.Token() - _ = tok +func TestTestImpl(t *testing.T) { + bank, _ := NewToken("Dummy", "DUMMY", 4) + urequire.False(t, bank == nil, "dummy should not be nil") } -func TestUserApprove(t *testing.T) { - owner := testutils.TestAddress("owner") - spender := testutils.TestAddress("spender") - dest := testutils.TestAddress("dest") - - bank := NewBanker("Dummy", "DUMMY", 6) - tok := bank.Token() - - // Set owner as the original caller - std.TestSetOrigCaller(owner) - // Mint 100000000 tokens for owner - urequire.NoError(t, bank.Mint(owner, 100000000)) - - // Approve spender to spend 5000000 tokens - 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 - urequire.Error(t, - tok.TransferFrom(owner, dest, 10000000), - ErrInsufficientAllowance.Error(), - "should not be able to transfer more than approved", +func TestToken(t *testing.T) { + var ( + alice = testutils.TestAddress("alice") + bob = testutils.TestAddress("bob") + carl = testutils.TestAddress("carl") ) - // Define a set of test data with spend amount and expected remaining allowance - tests := []struct { - spend uint64 // Spend amount - exp uint64 // Remaining allowance - }{ - {3, 4999997}, - {999997, 4000000}, - {4000000, 0}, - } + bank, adm := NewToken("Dummy", "DUMMY", 6) - // perform transfer operation,and check if allowance and balance are correct - for _, tt := range tests { - b0 := tok.BalanceOf(dest) - // Perform transfer from owner to dest - urequire.NoError(t, tok.TransferFrom(owner, dest, tt.spend)) - a := tok.Allowance(owner, spender) - // Check if allowance equals expected value - urequire.True(t, a == tt.exp, ufmt.Sprintf("allowance exp: %d,got %d", tt.exp, a)) - - // Get dest current balance - b := tok.BalanceOf(dest) - // Calculate expected balance ,should be initial balance plus transfer amount - expB := b0 + tt.spend - // Check if balance equals expected value - urequire.True(t, b == expB, ufmt.Sprintf("balance exp: %d,got %d", expB, b)) + checkBalances := func(aliceEB, bobEB, carlEB uint64) { + t.Helper() + exp := ufmt.Sprintf("alice=%d bob=%d carl=%d", aliceEB, bobEB, carlEB) + aliceGB := bank.BalanceOf(alice) + bobGB := bank.BalanceOf(bob) + carlGB := bank.BalanceOf(carl) + got := ufmt.Sprintf("alice=%d bob=%d carl=%d", aliceGB, bobGB, carlGB) + uassert.Equal(t, got, exp, "invalid balances") + } + checkAllowances := func(abEB, acEB, baEB, bcEB, caEB, cbEB uint64) { + t.Helper() + exp := ufmt.Sprintf("ab=%d ac=%d ba=%d bc=%d ca=%d cb=%s", abEB, acEB, baEB, bcEB, caEB, cbEB) + abGB := bank.Allowance(alice, bob) + acGB := bank.Allowance(alice, carl) + baGB := bank.Allowance(bob, alice) + bcGB := bank.Allowance(bob, carl) + caGB := bank.Allowance(carl, alice) + cbGB := bank.Allowance(carl, bob) + got := ufmt.Sprintf("ab=%d ac=%d ba=%d bc=%d ca=%d cb=%s", abGB, acGB, baGB, bcGB, caGB, cbGB) + uassert.Equal(t, got, exp, "invalid allowances") } - // Try to transfer one token from owner to dest ,should fail because no allowance left - urequire.Error(t, tok.TransferFrom(owner, dest, 1), ErrInsufficientAllowance.Error(), "no allowance") + checkBalances(0, 0, 0) + checkAllowances(0, 0, 0, 0, 0, 0) + + urequire.NoError(t, adm.Mint(alice, 1000)) + urequire.NoError(t, adm.Mint(alice, 100)) + checkBalances(1100, 0, 0) + checkAllowances(0, 0, 0, 0, 0, 0) + + urequire.NoError(t, adm.Approve(alice, bob, 99999999)) + checkBalances(1100, 0, 0) + checkAllowances(99999999, 0, 0, 0, 0, 0) + + urequire.NoError(t, adm.Approve(alice, bob, 400)) + checkBalances(1100, 0, 0) + checkAllowances(400, 0, 0, 0, 0, 0) + + urequire.Error(t, adm.TransferFrom(alice, bob, carl, 100000000)) + checkBalances(1100, 0, 0) + checkAllowances(400, 0, 0, 0, 0, 0) + + urequire.NoError(t, adm.TransferFrom(alice, bob, carl, 100)) + checkBalances(1000, 0, 100) + checkAllowances(300, 0, 0, 0, 0, 0) + + urequire.Error(t, adm.SpendAllowance(alice, bob, 2000000)) + checkBalances(1000, 0, 100) + checkAllowances(300, 0, 0, 0, 0, 0) + + urequire.NoError(t, adm.SpendAllowance(alice, bob, 100)) + checkBalances(1000, 0, 100) + checkAllowances(200, 0, 0, 0, 0, 0) +} + +func TestOverflow(t *testing.T) { + alice := testutils.TestAddress("alice") + bob := testutils.TestAddress("bob") + tok, adm := NewToken("Dummy", "DUMMY", 6) + + urequire.NoError(t, adm.Mint(alice, 2<<62)) + urequire.Equal(t, tok.BalanceOf(alice), uint64(2<<62)) + urequire.Error(t, adm.Mint(bob, 2<<62)) } diff --git a/examples/gno.land/p/demo/grc/grc20/types.gno b/examples/gno.land/p/demo/grc/grc20/types.gno index 201c6638914..cf67858ccf3 100644 --- a/examples/gno.land/p/demo/grc/grc20/types.gno +++ b/examples/gno.land/p/demo/grc/grc20/types.gno @@ -4,17 +4,17 @@ import ( "errors" "std" + "gno.land/p/demo/avl" "gno.land/p/demo/grc/exts" ) -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 { +// Teller interface defines the methods that a GRC20 token must implement. It +// extends the TokenMetadata interface to include methods for managing token +// transfers, allowances, and querying balances. +// +// The Teller interface is designed to ensure that any token adhering to this +// standard provides a consistent API for interacting with fungible tokens. +type Teller interface { exts.TokenMetadata // Returns the amount of tokens in existence. @@ -55,9 +55,54 @@ type Token interface { TransferFrom(from, to std.Address, amount uint64) error } +// Token represents a fungible token with a name, symbol, and a certain number +// of decimal places. It maintains a ledger for tracking balances and allowances +// of addresses. +// +// The Token struct provides methods for retrieving token metadata, such as the +// name, symbol, and decimals, as well as methods for interacting with the +// ledger, including checking balances and allowances. +type Token struct { + name string // Name of the token (e.g., "Dummy Token"). + symbol string // Symbol of the token (e.g., "DUMMY"). + decimals uint // Number of decimal places used for the token's precision. + ledger *PrivateLedger // Pointer to the PrivateLedger that manages balances and allowances. +} + +// PrivateLedger is a struct that holds the balances and allowances for the +// token. It provides administrative functions for minting, burning, +// transferring tokens, and managing allowances. +// +// The PrivateLedger is not safe to expose publicly, as it contains sensitive +// information regarding token balances and allowances, and allows direct, +// unrestricted access to all administrative functions. +type PrivateLedger struct { + totalSupply uint64 // Total supply of the token managed by this ledger. + balances avl.Tree // std.Address -> uint64 + allowances avl.Tree // owner.(std.Address)+":"+spender.(std.Address)) -> uint64 + token *Token // Pointer to the associated Token struct +} + +var ( + ErrInsufficientBalance = errors.New("insufficient balance") + ErrInsufficientAllowance = errors.New("insufficient allowance") + ErrInvalidAddress = errors.New("invalid address") + ErrCannotTransferToSelf = errors.New("cannot send transfer to self") + ErrReadonly = errors.New("banker is readonly") + ErrRestrictedTokenOwner = errors.New("restricted to bank owner") + ErrOverflow = errors.New("Mint overflow") +) + const ( MintEvent = "Mint" BurnEvent = "Burn" TransferEvent = "Transfer" ApprovalEvent = "Approval" ) + +type fnTeller struct { + accountFn func() std.Address + *Token +} + +var _ Teller = (*fnTeller)(nil) diff --git a/examples/gno.land/r/demo/bar20/bar20.gno b/examples/gno.land/r/demo/bar20/bar20.gno index 1d6ecd3d378..de51b8b47d9 100644 --- a/examples/gno.land/r/demo/bar20/bar20.gno +++ b/examples/gno.land/r/demo/bar20/bar20.gno @@ -12,18 +12,17 @@ import ( ) var ( - banker *grc20.Banker // private banker. - Token grc20.Token // public safe-object. + Token, adm = grc20.NewToken("Bar", "BAR", 4) + UserTeller = Token.CallerTeller() ) func init() { - banker = grc20.NewBanker("Bar", "BAR", 4) - Token = banker.Token() + // XXX: grc20reg.Register(Token, "") } func Faucet() string { caller := std.PrevRealm().Addr() - if err := banker.Mint(caller, 1_000_000); err != nil { + if err := adm.Mint(caller, 1_000_000); err != nil { return "error: " + err.Error() } return "OK" @@ -35,7 +34,7 @@ func Render(path string) string { switch { case path == "": - return banker.RenderHome() // XXX: should be Token.RenderHome() + return Token.RenderHome() case c == 2 && parts[0] == "balance": owner := std.Address(parts[1]) balance := Token.BalanceOf(owner) diff --git a/examples/gno.land/r/demo/bar20/bar20_test.gno b/examples/gno.land/r/demo/bar20/bar20_test.gno index 20349258c1b..0561d13c865 100644 --- a/examples/gno.land/r/demo/bar20/bar20_test.gno +++ b/examples/gno.land/r/demo/bar20/bar20_test.gno @@ -13,7 +13,7 @@ func TestPackage(t *testing.T) { std.TestSetRealm(std.NewUserRealm(alice)) std.TestSetOrigCaller(alice) // XXX: should not need this - urequire.Equal(t, Token.BalanceOf(alice), uint64(0)) + urequire.Equal(t, UserTeller.BalanceOf(alice), uint64(0)) urequire.Equal(t, Faucet(), "OK") - urequire.Equal(t, Token.BalanceOf(alice), uint64(1_000_000)) + urequire.Equal(t, UserTeller.BalanceOf(alice), uint64(1_000_000)) } diff --git a/examples/gno.land/r/demo/foo20/foo20.gno b/examples/gno.land/r/demo/foo20/foo20.gno index 9d4e5d40193..fe099117215 100644 --- a/examples/gno.land/r/demo/foo20/foo20.gno +++ b/examples/gno.land/r/demo/foo20/foo20.gno @@ -1,5 +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. +// foo20 is a GRC20 token contract where all the grc20.Teller methods are +// proxified with top-level functions. see also gno.land/r/demo/bar20. package foo20 import ( @@ -14,45 +14,45 @@ import ( ) var ( - banker *grc20.Banker - admin *ownable.Ownable - token grc20.Token + Token, privateLedger = grc20.NewToken("Foo", "FOO", 4) + UserTeller = Token.CallerTeller() + owner = ownable.NewWithAddress("g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq") // @manfred ) func init() { - admin = ownable.NewWithAddress("g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq") // @manfred - banker = grc20.NewBanker("Foo", "FOO", 4) - banker.Mint(admin.Owner(), 1000000*10000) // @administrator (1M) - token = banker.Token() + privateLedger.Mint(owner.Owner(), 1_000_000*10_000) // @privateLedgeristrator (1M) + // XXX: grc20reg.Register(Token, "") } -func TotalSupply() uint64 { return token.TotalSupply() } +func TotalSupply() uint64 { + return UserTeller.TotalSupply() +} func BalanceOf(owner pusers.AddressOrName) uint64 { ownerAddr := users.Resolve(owner) - return token.BalanceOf(ownerAddr) + return UserTeller.BalanceOf(ownerAddr) } func Allowance(owner, spender pusers.AddressOrName) uint64 { ownerAddr := users.Resolve(owner) spenderAddr := users.Resolve(spender) - return token.Allowance(ownerAddr, spenderAddr) + return UserTeller.Allowance(ownerAddr, spenderAddr) } func Transfer(to pusers.AddressOrName, amount uint64) { toAddr := users.Resolve(to) - checkErr(token.Transfer(toAddr, amount)) + checkErr(UserTeller.Transfer(toAddr, amount)) } func Approve(spender pusers.AddressOrName, amount uint64) { spenderAddr := users.Resolve(spender) - checkErr(token.Approve(spenderAddr, amount)) + checkErr(UserTeller.Approve(spenderAddr, amount)) } func TransferFrom(from, to pusers.AddressOrName, amount uint64) { fromAddr := users.Resolve(from) toAddr := users.Resolve(to) - checkErr(token.TransferFrom(fromAddr, toAddr, amount)) + checkErr(UserTeller.TransferFrom(fromAddr, toAddr, amount)) } // Faucet is distributing foo20 tokens without restriction (unsafe). @@ -60,19 +60,19 @@ func TransferFrom(from, to pusers.AddressOrName, amount uint64) { func Faucet() { caller := std.PrevRealm().Addr() amount := uint64(1_000 * 10_000) // 1k - checkErr(banker.Mint(caller, amount)) + checkErr(privateLedger.Mint(caller, amount)) } func Mint(to pusers.AddressOrName, amount uint64) { - admin.AssertCallerIsOwner() + owner.AssertCallerIsOwner() toAddr := users.Resolve(to) - checkErr(banker.Mint(toAddr, amount)) + checkErr(privateLedger.Mint(toAddr, amount)) } func Burn(from pusers.AddressOrName, amount uint64) { - admin.AssertCallerIsOwner() + owner.AssertCallerIsOwner() fromAddr := users.Resolve(from) - checkErr(banker.Burn(fromAddr, amount)) + checkErr(privateLedger.Burn(fromAddr, amount)) } func Render(path string) string { @@ -81,11 +81,11 @@ func Render(path string) string { switch { case path == "": - return banker.RenderHome() + return Token.RenderHome() case c == 2 && parts[0] == "balance": owner := pusers.AddressOrName(parts[1]) ownerAddr := users.Resolve(owner) - balance := banker.BalanceOf(ownerAddr) + balance := UserTeller.BalanceOf(ownerAddr) return ufmt.Sprintf("%d\n", balance) default: return "404\n" diff --git a/examples/gno.land/r/demo/foo20/foo20_test.gno b/examples/gno.land/r/demo/foo20/foo20_test.gno index 77c99d0525e..b3346296b04 100644 --- a/examples/gno.land/r/demo/foo20/foo20_test.gno +++ b/examples/gno.land/r/demo/foo20/foo20_test.gno @@ -71,10 +71,18 @@ func TestErrConditions(t *testing.T) { fn func() } - std.TestSetOrigCaller(users.Resolve(admin)) + privateLedger.Mint(std.Address(admin), 10000) { tests := []test{ - {"Transfer(admin, 1)", "cannot send transfer to self", func() { Transfer(admin, 1) }}, + {"Transfer(admin, 1)", "cannot send transfer to self", func() { + // XXX: should replace with: Transfer(admin, 1) + // but there is currently a limitation in manipulating the frame stack and simulate + // calling this package from an outside point of view. + adminAddr := std.Address(admin) + if err := privateLedger.Transfer(adminAddr, adminAddr, 1); err != nil { + panic(err) + } + }}, {"Approve(empty, 1))", "invalid address", func() { Approve(empty, 1) }}, } for _, tc := range tests { diff --git a/examples/gno.land/r/demo/grc20factory/gno.mod b/examples/gno.land/r/demo/grc20factory/gno.mod index 8d0fbd0c46b..bf5e9c9ec96 100644 --- a/examples/gno.land/r/demo/grc20factory/gno.mod +++ b/examples/gno.land/r/demo/grc20factory/gno.mod @@ -4,6 +4,7 @@ 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/testutils v0.0.0-latest gno.land/p/demo/uassert 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 f37a9370a9e..901a9b9f33c 100644 --- a/examples/gno.land/r/demo/grc20factory/grc20factory.gno +++ b/examples/gno.land/r/demo/grc20factory/grc20factory.gno @@ -12,6 +12,13 @@ import ( var instances avl.Tree // symbol -> instance +type instance struct { + token *grc20.Token + ledger *grc20.PrivateLedger + admin *ownable.Ownable + faucet uint64 // per-request amount. disabled if 0. +} + func New(name, symbol string, decimals uint, initialMint, faucet uint64) { caller := std.PrevRealm().Addr() NewWithAdmin(name, symbol, decimals, initialMint, faucet, caller) @@ -23,56 +30,68 @@ func NewWithAdmin(name, symbol string, decimals uint, initialMint, faucet uint64 panic("token already exists") } - banker := grc20.NewBanker(name, symbol, decimals) + token, ledger := grc20.NewToken(name, symbol, decimals) if initialMint > 0 { - banker.Mint(admin, initialMint) + ledger.Mint(admin, initialMint) } inst := instance{ - banker: banker, + token: token, + ledger: ledger, admin: ownable.NewWithAddress(admin), faucet: faucet, } - instances.Set(symbol, &inst) + // XXX: grc20reg.Register(token, symbol) } -type instance struct { - banker *grc20.Banker - admin *ownable.Ownable - faucet uint64 // per-request amount. disabled if 0. +func (inst instance) Token() *grc20.Token { + return inst.token +} + +func (inst instance) CallerTeller() grc20.Teller { + return inst.token.CallerTeller() } -func (inst instance) Token() grc20.Token { return inst.banker.Token() } +func Bank(symbol string) *grc20.Token { + inst := mustGetInstance(symbol) + return inst.token +} func TotalSupply(symbol string) uint64 { inst := mustGetInstance(symbol) - return inst.Token().TotalSupply() + return inst.token.ReadonlyTeller().TotalSupply() } func BalanceOf(symbol string, owner std.Address) uint64 { inst := mustGetInstance(symbol) - return inst.Token().BalanceOf(owner) + return inst.token.ReadonlyTeller().BalanceOf(owner) } func Allowance(symbol string, owner, spender std.Address) uint64 { inst := mustGetInstance(symbol) - return inst.Token().Allowance(owner, spender) + return inst.token.ReadonlyTeller().Allowance(owner, spender) } func Transfer(symbol string, to std.Address, amount uint64) { inst := mustGetInstance(symbol) - checkErr(inst.Token().Transfer(to, amount)) + caller := std.PrevRealm().Addr() + teller := inst.ledger.ImpersonateTeller(caller) + checkErr(teller.Transfer(to, amount)) } func Approve(symbol string, spender std.Address, amount uint64) { inst := mustGetInstance(symbol) - checkErr(inst.Token().Approve(spender, amount)) + caller := std.PrevRealm().Addr() + teller := inst.ledger.ImpersonateTeller(caller) + checkErr(teller.Approve(spender, amount)) } func TransferFrom(symbol string, from, to std.Address, amount uint64) { inst := mustGetInstance(symbol) - checkErr(inst.Token().TransferFrom(from, to, amount)) + caller := std.PrevRealm().Addr() + teller := inst.ledger.ImpersonateTeller(caller) + checkErr(teller.TransferFrom(from, to, amount)) } // faucet. @@ -84,19 +103,19 @@ func Faucet(symbol string) { // FIXME: add limits? // FIXME: add payment in gnot? caller := std.PrevRealm().Addr() - checkErr(inst.banker.Mint(caller, inst.faucet)) + checkErr(inst.ledger.Mint(caller, inst.faucet)) } func Mint(symbol string, to std.Address, amount uint64) { inst := mustGetInstance(symbol) inst.admin.AssertCallerIsOwner() - checkErr(inst.banker.Mint(to, amount)) + checkErr(inst.ledger.Mint(to, amount)) } func Burn(symbol string, from std.Address, amount uint64) { inst := mustGetInstance(symbol) inst.admin.AssertCallerIsOwner() - checkErr(inst.banker.Burn(from, amount)) + checkErr(inst.ledger.Burn(from, amount)) } func Render(path string) string { @@ -109,12 +128,12 @@ func Render(path string) string { case c == 1: symbol := parts[0] inst := mustGetInstance(symbol) - return inst.banker.RenderHome() + return inst.token.RenderHome() case c == 3 && parts[1] == "balance": symbol := parts[0] inst := mustGetInstance(symbol) owner := std.Address(parts[2]) - balance := inst.Token().BalanceOf(owner) + balance := inst.token.CallerTeller().BalanceOf(owner) return ufmt.Sprintf("%d", balance) default: return "404\n" @@ -131,6 +150,6 @@ func mustGetInstance(symbol string) *instance { func checkErr(err error) { if err != nil { - panic(err) + panic(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 5dfb6a760cc..46fc07fabf2 100644 --- a/examples/gno.land/r/demo/grc20factory/grc20factory_test.gno +++ b/examples/gno.land/r/demo/grc20factory/grc20factory_test.gno @@ -4,16 +4,16 @@ import ( "std" "testing" + "gno.land/p/demo/testutils" "gno.land/p/demo/uassert" + "gno.land/p/demo/ufmt" ) func TestReadOnlyPublicMethods(t *testing.T) { - admin := std.Address("g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj") - manfred := std.Address("g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq") - unknown := std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // valid but never used. - 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) + std.TestSetOrigPkgAddr("gno.land/r/demo/grc20factory") + admin := testutils.TestAddress("admin") + bob := testutils.TestAddress("bob") + carl := testutils.TestAddress("carl") type test struct { name string @@ -21,36 +21,52 @@ func TestReadOnlyPublicMethods(t *testing.T) { fn func() uint64 } - // check balances #1. - { + checkBalances := func(step string, totSup, balAdm, balBob, allowAdmBob, balCarl uint64) { tests := []test{ - {"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) }}, + {"TotalSupply", totSup, func() uint64 { return TotalSupply("FOO") }}, + {"BalanceOf(admin)", balAdm, func() uint64 { return BalanceOf("FOO", admin) }}, + {"BalanceOf(bob)", balBob, func() uint64 { return BalanceOf("FOO", bob) }}, + {"Allowance(admin, bob)", allowAdmBob, func() uint64 { return Allowance("FOO", admin, bob) }}, + {"BalanceOf(carl)", balCarl, func() uint64 { return BalanceOf("FOO", carl) }}, } for _, tc := range tests { - uassert.Equal(t, tc.balance, tc.fn(), "balance does not match") + reason := ufmt.Sprintf("%s.%s - %s", step, tc.name, "balances do not match") + uassert.Equal(t, tc.balance, tc.fn(), reason) } } - return - // unknown uses the faucet. - std.TestSetOrigCaller(unknown) + // admin creates FOO and BAR. + std.TestSetOrigCaller(admin) + std.TestSetRealm(std.NewUserRealm(admin)) + NewWithAdmin("Foo", "FOO", 3, 1_111_111_000, 5_555, admin) + NewWithAdmin("Bar", "BAR", 3, 2_222_000, 6_666, admin) + checkBalances("step1", 1_111_111_000, 1_111_111_000, 0, 0, 0) + + // admin mints to bob. + mustGetInstance("FOO").ledger.Mint(bob, 333_333_000) + checkBalances("step2", 1_444_444_000, 1_111_111_000, 333_333_000, 0, 0) + + // carl uses the faucet. + std.TestSetOrigCaller(carl) + std.TestSetRealm(std.NewUserRealm(carl)) Faucet("FOO") + checkBalances("step3", 1_444_449_555, 1_111_111_000, 333_333_000, 0, 5_555) - // check balances #2. - { - tests := []test{ - {"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)", 10_000_000, func() uint64 { return BalanceOf("FOO", unknown) }}, - } - for _, tc := range tests { - uassert.Equal(t, tc.balance, tc.fn(), "balance does not match") - } - } + // admin gives to bob some allowance. + std.TestSetOrigCaller(admin) + std.TestSetRealm(std.NewUserRealm(admin)) + Approve("FOO", bob, 1_000_000) + checkBalances("step4", 1_444_449_555, 1_111_111_000, 333_333_000, 1_000_000, 5_555) + + // bob uses a part of the allowance. + std.TestSetOrigCaller(bob) + std.TestSetRealm(std.NewUserRealm(bob)) + TransferFrom("FOO", admin, carl, 400_000) + checkBalances("step5", 1_444_449_555, 1_110_711_000, 333_333_000, 600_000, 405_555) + + // bob uses a part of the allowance. + std.TestSetOrigCaller(bob) + std.TestSetRealm(std.NewUserRealm(bob)) + TransferFrom("FOO", admin, carl, 600_000) + checkBalances("step6", 1_444_449_555, 1_110_111_000, 333_333_000, 0, 1_005_555) } diff --git a/examples/gno.land/r/demo/wugnot/wugnot.gno b/examples/gno.land/r/demo/wugnot/wugnot.gno index e1028530c8c..bb109644778 100644 --- a/examples/gno.land/r/demo/wugnot/wugnot.gno +++ b/examples/gno.land/r/demo/wugnot/wugnot.gno @@ -10,23 +10,25 @@ import ( "gno.land/r/demo/users" ) -var ( - banker *grc20.Banker = grc20.NewBanker("wrapped GNOT", "wugnot", 0) - Token = banker.Token() -) +var Token, adm = grc20.NewToken("wrapped GNOT", "wugnot", 0) const ( ugnotMinDeposit uint64 = 1000 wugnotMinDeposit uint64 = 1 ) +func init() { + // XXX: grc20reg.Register(Token, "") +} + func Deposit() { caller := std.PrevRealm().Addr() sent := std.GetOrigSend() amount := sent.AmountOf("ugnot") require(uint64(amount) >= ugnotMinDeposit, ufmt.Sprintf("Deposit below minimum: %d/%d ugnot.", amount, ugnotMinDeposit)) - checkErr(banker.Mint(caller, uint64(amount))) + + checkErr(adm.Mint(caller, uint64(amount))) } func Withdraw(amount uint64) { @@ -41,7 +43,7 @@ func Withdraw(amount uint64) { stdBanker := std.GetBanker(std.BankerTypeRealmSend) send := std.Coins{{"ugnot", int64(amount)}} stdBanker.SendCoins(pkgaddr, caller, send) - checkErr(banker.Burn(caller, amount)) + checkErr(adm.Burn(caller, amount)) } func Render(path string) string { @@ -50,7 +52,7 @@ func Render(path string) string { switch { case path == "": - return banker.RenderHome() + return Token.RenderHome() case c == 2 && parts[0] == "balance": owner := std.Address(parts[1]) balance := Token.BalanceOf(owner) @@ -75,18 +77,21 @@ func Allowance(owner, spender pusers.AddressOrName) uint64 { func Transfer(to pusers.AddressOrName, amount uint64) { toAddr := users.Resolve(to) - checkErr(Token.Transfer(toAddr, amount)) + userTeller := Token.CallerTeller() + checkErr(userTeller.Transfer(toAddr, amount)) } func Approve(spender pusers.AddressOrName, amount uint64) { spenderAddr := users.Resolve(spender) - checkErr(Token.Approve(spenderAddr, amount)) + userTeller := Token.CallerTeller() + checkErr(userTeller.Approve(spenderAddr, amount)) } func TransferFrom(from, to pusers.AddressOrName, amount uint64) { fromAddr := users.Resolve(from) toAddr := users.Resolve(to) - checkErr(Token.TransferFrom(fromAddr, toAddr, amount)) + userTeller := Token.CallerTeller() + checkErr(userTeller.TransferFrom(fromAddr, toAddr, amount)) } func require(condition bool, msg string) {