From 1d4e9a472c066ba73685772f3637387ff4806b20 Mon Sep 17 00:00:00 2001 From: Manfred Touron <94029+moul@users.noreply.github.com> Date: Thu, 28 Apr 2022 01:36:15 +0200 Subject: [PATCH] chore: add a grc20 example smart contract (#136) Signed-off-by: Manfred Touron <94029+moul@users.noreply.github.com> --- examples/gno.land/p/grc/grc20/grc20.gno | 64 ++++++ examples/gno.land/p/ufmt/ufmt.gno | 45 ++++ examples/gno.land/r/foo20/foo.gno | 286 ++++++++++++++++++++++++ tests/files2/zrealm_foo200.gno | 30 +++ tests/files2/zrealm_foo201.gno | 98 ++++++++ tests/files2/zrealm_foo202.gno | 68 ++++++ tests/files2/zrealm_foo203.gno | 54 +++++ 7 files changed, 645 insertions(+) create mode 100644 examples/gno.land/p/grc/grc20/grc20.gno create mode 100644 examples/gno.land/p/ufmt/ufmt.gno create mode 100644 examples/gno.land/r/foo20/foo.gno create mode 100644 tests/files2/zrealm_foo200.gno create mode 100644 tests/files2/zrealm_foo201.gno create mode 100644 tests/files2/zrealm_foo202.gno create mode 100644 tests/files2/zrealm_foo203.gno diff --git a/examples/gno.land/p/grc/grc20/grc20.gno b/examples/gno.land/p/grc/grc20/grc20.gno new file mode 100644 index 00000000000..ba6013a1272 --- /dev/null +++ b/examples/gno.land/p/grc/grc20/grc20.gno @@ -0,0 +1,64 @@ +package grc20 + +import "std" + +type GRC20 interface { + // Returns the amount of tokens in existence. + TotalSupply() uint64 + + // Returns the amount of tokens owned by `account`. + BalanceOf(address std.Address) uint64 + + // Moves `amount` tokens from the caller's account to `to`. + // + // Returns a boolean value indicating whether the operation succeeded. + // + // Emits a {EventTransfer} event. + Transfer(to std.Address, amount uint64) bool + + // 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 a boolean value indicating whether the operation succeeded. + // + // 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 + // + // Emits an {EventApproval} event. + Approve(spender std.Address, amount uint64) bool + + // Moves `amount` tokens from `from` to `to` using the + // allowance mechanism. `amount` is then deducted from the caller's + // allowance. + // + // Returns a boolean value indicating whether the operation succeeded. + // + // Emits a {EventTransfer} event. + TransferFrom(from, to std.Address, amount uint64) bool +} + +// 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 +} diff --git a/examples/gno.land/p/ufmt/ufmt.gno b/examples/gno.land/p/ufmt/ufmt.gno new file mode 100644 index 00000000000..b5b4795d09b --- /dev/null +++ b/examples/gno.land/p/ufmt/ufmt.gno @@ -0,0 +1,45 @@ +package ufmt + +import "strconv" + +func Sprintf(format string, values ...interface{}) string { + offset := 0 + fmtlen := len(format) + r := "" + + for i := 0; i < fmtlen; i++ { + k := format[i] + isLast := i == fmtlen-1 + switch { + case k == '%' && !isLast: + value := values[offset] + switch format[i+1] { + case 's': + r += value.(string) + case 'd': + switch v := value.(type) { + case int: + r += strconv.Itoa(v) + case int64: + r += strconv.Itoa(int(v)) + case uint: + r += strconv.FormatUint(uint64(v), 10) + case uint64: + r += strconv.FormatUint(v, 10) + default: + r += "(unhandled)" + } + case '%': + r += "%" + default: + r += "(unhandled)" + } + + i++ + offset++ + default: + r += string(k) + } + } + return r +} diff --git a/examples/gno.land/r/foo20/foo.gno b/examples/gno.land/r/foo20/foo.gno new file mode 100644 index 00000000000..bd5cf2dc464 --- /dev/null +++ b/examples/gno.land/r/foo20/foo.gno @@ -0,0 +1,286 @@ +package foo20 + +import ( + "std" + "strings" + + "gno.land/p/avl" + "gno.land/p/grc/grc20" + "gno.land/p/ufmt" +) + +type Token struct { + // TODO: use big.Int or a custom uint256 instead of uint64? + + grc20.GRC20 // implements the GRC20 interface + + name string + symbol string + decimals uint + totalSupply uint64 + + balances *avl.MutTree // std.Address(owner) -> uint64 + allowances *avl.MutTree // string(owner+":"+spender) -> uint64 +} + +func newToken(name, symbol string, decimals uint) *Token { + // FIXME: check for limits + + return &Token{ + name: name, + symbol: symbol, + decimals: decimals, + + balances: avl.NewMutTree(), + allowances: avl.NewMutTree(), + } +} + +var foo *Token +var admin std.Address = "g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj" // TODO: helper to change admin + +const zeroAddress = std.Address("") + +func init() { + foo = newToken("Foo", "FOO", 4) + foo.mint("g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj", 1000000*10000) // @administrator (1M) + foo.mint("g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq", 10000*10000) // @manfred (10k) +} + +// GRC20 implementation. +// + +// TODO: create a reusable interface with optional hooks. +// TODO: simplify the API and try to use events when available. +// TODO: useful Render() method. +// TODO: add a lot of unit tests, really a lot. + +func (t *Token) TotalSupply() uint64 { + return t.totalSupply +} + +func (t *Token) BalanceOf(address std.Address) uint64 { + return t.balanceOf(address) +} + +func (t *Token) Transfer(to std.Address, amount uint64) { + caller := std.GetCallerAt(3) + t.transfer(caller, to, amount) +} + +func (t *Token) Allowance(owner, spender std.Address) uint64 { + return t.allowance(owner, spender) +} + +func (t *Token) Approve(spender std.Address, amount uint64) { + owner := std.GetCallerAt(3) + t.approve(owner, spender, amount) +} + +func (t *Token) TransferFrom(from, to std.Address, amount uint64) { + spender := std.GetCallerAt(3) + t.spendAllowance(from, spender, amount) + t.transfer(from, to, amount) +} + +// Administration helpers implementation. +// + +func (t *Token) Mint(address std.Address, amount uint64) { + caller := std.GetCallerAt(3) + assertIsAdmin(caller) + t.mint(address, amount) +} + +func (t *Token) Burn(address std.Address, amount uint64) { + caller := std.GetCallerAt(3) + assertIsAdmin(caller) + t.burn(address, amount) +} + +// private helpers +// + +func (t *Token) mint(address std.Address, amount uint64) { + checkIsValidAddress(address) + // TODO: check for overflow + + t.totalSupply += amount + currentBalance := t.balanceOf(address) + newBalance := currentBalance + amount + + t.balances.Set(string(address), newBalance) + + event := grc20.TransferEvent{zeroAddress, address, amount} + emit(&event) +} + +func (t *Token) burn(address std.Address, amount uint64) { + checkIsValidAddress(address) + // TODO: check for overflow + + currentBalance := t.balanceOf(address) + if currentBalance < amount { + panic("insufficient balance") + } + + t.totalSupply -= amount + newBalance := currentBalance - amount + + t.balances.Set(string(address), newBalance) + + event := grc20.TransferEvent{address, zeroAddress, amount} + emit(&event) +} + +func (t *Token) balanceOf(address std.Address) uint64 { + checkIsValidAddress(address) + + balance, found := t.balances.Get(address.String()) + if !found { + return 0 + } + return balance.(uint64) +} + +func (t *Token) spendAllowance(owner, spender std.Address, amount uint64) { + checkIsValidAddress(owner) + checkIsValidAddress(spender) + + currentAllowance := t.allowance(owner, spender) + if currentAllowance < amount { + panic("insufficient allowance") + } +} + +func (t *Token) transfer(from, to std.Address, amount uint64) { + checkIsValidAddress(from) + checkIsValidAddress(to) + + if from == to { + panic("cannot send transfer to self") + } + + toBalance := t.balanceOf(to) + fromBalance := t.balanceOf(from) + + if fromBalance < amount { + panic("insufficient balance") + } + + newToBalance := toBalance + amount + newFromBalance := fromBalance - amount + + t.balances.Set(string(to), newToBalance) + t.balances.Set(string(from), newFromBalance) + + event := grc20.TransferEvent{from, to, amount} + emit(&event) +} + +func (t *Token) allowance(owner, spender std.Address) uint64 { + checkIsValidAddress(owner) + checkIsValidAddress(spender) + + key := owner.String() + ":" + spender.String() + + allowance, found := t.allowances.Get(key) + if !found { + return 0 + } + + return allowance.(uint64) +} + +func (t *Token) approve(owner, spender std.Address, amount uint64) { + checkIsValidAddress(owner) + checkIsValidAddress(spender) + + key := owner.String() + ":" + spender.String() + t.allowances.Set(key, amount) + + event := grc20.ApprovalEvent{owner, spender, amount} + emit(&event) +} + +func checkIsValidAddress(addr std.Address) { + if addr.String() == "" { + panic("invalid address") + } +} + +func assertIsAdmin(address std.Address) { + if address != admin { + panic("restricted access") + } +} + +// method proxies as public functions. +// + +// getters. + +func TotalSupply() uint64 { return foo.TotalSupply() } +func BalanceOf(address std.Address) uint64 { return foo.BalanceOf(address) } +func Allowance(owner, spender std.Address) uint64 { return foo.Allowance(owner, spender) } + +// setters. + +func Transfer(to std.Address, amount uint64) { + //std.AssertOriginCall() // FIXME: inconsistent + foo.Transfer(to, amount) +} + +func Approve(spender std.Address, amount uint64) { + //std.AssertOriginCall() // FIXME: inconsistent + foo.Approve(spender, amount) +} + +func TransferFrom(from, to std.Address, amount uint64) { + //std.AssertOriginCall() // FIXME: inconsistent + foo.TransferFrom(from, to, amount) +} + +// administration. + +func Mint(address std.Address, amount uint64) { + //std.AssertOriginCall() // FIXME: inconsistent + foo.Mint(address, amount) +} + +func Burn(address std.Address, amount uint64) { + //std.AssertOriginCall() // FIXME: inconsistent + foo.Burn(address, amount) +} + +// placeholders. +// + +func emit(event interface{}) { + // TODO: should we do something there? + // noop +} + +// render. +// + +func Render(path string) string { + parts := strings.Split(path, "/") + c := len(parts) + + switch { + case path == "": + str := "" + str += ufmt.Sprintf("# %s ($%s)\n\n", foo.name, foo.symbol) + str += ufmt.Sprintf("* **Decimals**: %d\n", foo.decimals) + str += ufmt.Sprintf("* **Total supply**: %d\n", foo.totalSupply) + str += ufmt.Sprintf("* **Known accounts**: %d\n", foo.balances.Size()) + return str + case c == 2 && parts[0] == "balance": + addr := std.Address(parts[1]) + balance := foo.BalanceOf(addr) + return ufmt.Sprintf("%d\n", balance) + default: + return "404\n" + } +} diff --git a/tests/files2/zrealm_foo200.gno b/tests/files2/zrealm_foo200.gno new file mode 100644 index 00000000000..9f6e7c89761 --- /dev/null +++ b/tests/files2/zrealm_foo200.gno @@ -0,0 +1,30 @@ +package main + +import ( + "std" + + "gno.land/r/foo20" +) + +var admin = std.Address("g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj") +var manfred = std.Address("g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq") +var unknown = std.Address("g1u0000000000000000000000000000000000000") + +func main() { + // testing read-only public methods + + println(foo20.TotalSupply()) + println(foo20.BalanceOf(admin)) + println(foo20.BalanceOf(manfred)) + println(foo20.BalanceOf(unknown)) + println(foo20.Allowance(admin, manfred)) + println("done") +} + +// Output: +// 10100000000 +// 10000000000 +// 100000000 +// 0 +// 0 +// done diff --git a/tests/files2/zrealm_foo201.gno b/tests/files2/zrealm_foo201.gno new file mode 100644 index 00000000000..9978ff144c9 --- /dev/null +++ b/tests/files2/zrealm_foo201.gno @@ -0,0 +1,98 @@ +package main + +import ( + "std" + + "gno.land/p/testutils" + "gno.land/r/foo20" +) + +const admin = std.Address("g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj") +const test1 = testutils.TestAddress("test1") +const test2 = testutils.TestAddress("test2") +const test3 = testutils.TestAddress("test3") +const test4 = testutils.TestAddress("test4") + +func init() { + std.TestSetOrigCaller(admin) + foo20.Transfer(test1, 1000) + foo20.Transfer(test2, 1000) +} + +func main() { + // testing errors. + + printBalances() + + // balance of invalid address + rec(func() { + println(foo20.BalanceOf("")) // invalid address + }) + + // transfer of a fresh account + rec(func() { + std.TestSetOrigCaller(test3) + foo20.Transfer(test4, 1) // insufficient balance + }) + + // transfer too much + rec(func() { + std.TestSetOrigCaller(test1) + foo20.Transfer(test4, 1001) // insufficient balance + + }) + + // transferFrom without allowance + rec(func() { + std.TestSetOrigCaller(test3) + foo20.TransferFrom(test1, test4, 1) // insufficient allowance + }) + + // transferFrom without enough allowance + std.TestSetOrigCaller(test1) + foo20.Approve(test3, 10) + rec(func() { + std.TestSetOrigCaller(test3) + foo20.TransferFrom(test1, test4, 11) // insufficient allowance + }) + + // TODO + // burn too much + // mint too much (overflow) + + printBalances() + println("done") +} + +func assertBalanceOfInvalidAddress() { +} + +func rec(handler func()) { + defer func() { + r := recover() + if r != nil { + println(r) + } + }() + handler() +} + +func printBalances() { + println( + foo20.BalanceOf(admin), + foo20.BalanceOf(test1), + foo20.BalanceOf(test2), + foo20.BalanceOf(test3), + foo20.BalanceOf(test4), + ) +} + +// Output: +// 9999998000 1000 1000 0 0 +// invalid address +// insufficient balance +// insufficient balance +// insufficient allowance +// insufficient allowance +// 9999998000 1000 1000 0 0 +// done diff --git a/tests/files2/zrealm_foo202.gno b/tests/files2/zrealm_foo202.gno new file mode 100644 index 00000000000..b2bebf72328 --- /dev/null +++ b/tests/files2/zrealm_foo202.gno @@ -0,0 +1,68 @@ +package main + +// SEND: 100gnot + +import ( + "std" + + "gno.land/p/testutils" + "gno.land/r/foo20" +) + +const admin = std.Address("g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj") +const test1 = testutils.TestAddress("test1") +const test2 = testutils.TestAddress("test2") +const test3 = testutils.TestAddress("test3") +const test4 = testutils.TestAddress("test4") + +func main() { + caller := std.GetOrigCaller() // main + printBalances() + + std.TestSetOrigCaller(admin) + foo20.Mint(test1, 100000) + printBalances() + + std.TestSetOrigCaller(test1) + foo20.Transfer(test2, 1000) + printBalances() + + std.TestSetOrigCaller(admin) + foo20.Transfer(test2, 1000) + printBalances() + + std.TestSetOrigCaller(test2) + foo20.Transfer(test3, 100) + printBalances() + + std.TestSetOrigCaller(test1) + println(foo20.Allowance(test1, test3)) + foo20.Approve(test3, 10000) + println(foo20.Allowance(test1, test3)) + printBalances() + + std.TestSetOrigCaller(test3) + foo20.TransferFrom(test1, test4, 10000) + printBalances() +} + +func printBalances() { + println( + foo20.BalanceOf(admin), + foo20.BalanceOf(test1), + foo20.BalanceOf(test2), + foo20.BalanceOf(test3), + foo20.BalanceOf(test4), + ) +} + +// Output: +// 10000000000 0 0 0 0 +// 10000000000 100000 0 0 0 +// 10000000000 99000 1000 0 0 +// 9999999000 99000 2000 0 0 +// 9999999000 99000 1900 100 0 +// 0 +// 10000 +// 9999999000 99000 1900 100 0 +// 9999999000 89000 1900 100 10000 diff --git a/tests/files2/zrealm_foo203.gno b/tests/files2/zrealm_foo203.gno new file mode 100644 index 00000000000..9d4fde8408c --- /dev/null +++ b/tests/files2/zrealm_foo203.gno @@ -0,0 +1,54 @@ +package main + +// SEND: 100gnot + +import ( + "std" + + "gno.land/p/testutils" + "gno.land/r/foo20" +) + +const admin = std.Address("g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj") +const test1 = testutils.TestAddress("test1") +const test2 = testutils.TestAddress("test2") +const test3 = testutils.TestAddress("test3") +const test4 = testutils.TestAddress("test4") + +func main() { + caller := std.GetOrigCaller() // main + + std.TestSetOrigCaller(admin) + foo20.Mint(test1, 100000) + foo20.Mint(test2, 1000000) + foo20.Mint(test3, 10000000) + + println("----- /") + println(foo20.Render("")) + + println("----- /balance/") + println(foo20.Render("balance/" + test1.String())) + + println("----- /balance/") + println(foo20.Render("balance/g1ublahblahblah")) + + println("----- /404") + println(foo20.Render("404")) +} + +// Output: +// ----- / +// # Foo ($FOO) +// +// * **Decimals**: 4 +// * **Total supply**: 10111100000 +// * **Known accounts**: 5 +// +// ----- /balance/ +// 100000 +// +// ----- /balance/ +// 0 +// +// ----- /404 +// 404