From 1e0b45d31d607c34059d3f22e990d9906ac3276a Mon Sep 17 00:00:00 2001 From: Manfred Touron <94029+moul@users.noreply.github.com> Date: Fri, 22 Apr 2022 23:59:03 +0000 Subject: [PATCH] chore: add a grc20 example smart contract Signed-off-by: Manfred Touron <94029+moul@users.noreply.github.com> --- examples/gno.land/p/grc/grc20/grc20.gno | 64 +++++++ examples/gno.land/r/grc20-foo/foo.gno | 239 ++++++++++++++++++++++++ examples/go.mod | 25 +++ 3 files changed, 328 insertions(+) create mode 100644 examples/gno.land/p/grc/grc20/grc20.gno create mode 100644 examples/gno.land/r/grc20-foo/foo.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/r/grc20-foo/foo.gno b/examples/gno.land/r/grc20-foo/foo.gno new file mode 100644 index 00000000000..623b3dc4bae --- /dev/null +++ b/examples/gno.land/r/grc20-foo/foo.gno @@ -0,0 +1,239 @@ +package foo + +import ( + "std" + + "gno.land/p/avl" + "gno.land/p/grc/grc20" +) + +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 = NewToken("Foo", "FOO", 4) +var admin std.Address = "g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj" + +const zeroAddress = std.Address("") + +func init() { + 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) { + std.AssertOriginCall() + caller := std.GetCallerAt(2) + 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) { + std.AssertOriginCall() + owner := std.GetCallerAt(2) + t.approve(owner, spender, amount) +} + +func (t *Token) TransferFrom(from, to std.Address, amount uint64) { + std.AssertOriginCall() + spender := std.GetCallerAt(2) + t.spendAllowance(from, spender, amount) + t.transfer(from, to, amount) +} + +// Administration helpers implementation. +// + +func (t *Token) Mint(address std.Address, amount uint64) { + std.AssertOriginCall() + caller := std.GetCallerAt(2) + + if caller != admin { + panic("restricted access") + } + t.mint(address, amount) +} + +func (t *Token) Burn(address std.Address, amount uint64) { + std.AssertOriginCall() + caller := std.GetCallerAt(2) + + if caller != admin { + panic("restricted access") + } + 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 + + t.totalSupply -= amount + currentBalance := t.balanceOf(address) + 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) + + 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") + } +} + +// method proxies as public functions. +// + +// getters. + +func GetFoo() *Token { return foo } +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) { foo.Transfer(to, amount) } +func Approve(spender std.Address, amount uint64) { foo.Approve(spender, amount) } +func TransferFrom(from, to std.Address, amount uint64) { foo.TransferFrom(from, to, amount) } + +// administration. + +func Mint(address std.Address, amount uint64) { foo.Mint(address, amount) } +func Burn(address std.Address, amount uint64) { foo.Burn(address, amount) } + +// placeholders +// + +func emit(event interface{}) { + // TODO: should we do something there? + // noop +} diff --git a/examples/go.mod b/examples/go.mod index e69de29bb2d..6d97441b986 100644 --- a/examples/go.mod +++ b/examples/go.mod @@ -0,0 +1,25 @@ +module github.com/gnolang/gno/examples + +go 1.17 + +require github.com/gnolang/gno v0.0.0 + +require ( + github.com/btcsuite/btcutil v1.0.2 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/gnolang/overflow v0.0.0-20170615021017-4d914c927216 // indirect + github.com/golang/protobuf v1.5.0 // indirect + github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db // indirect + github.com/jmhodges/levigo v1.0.0 // indirect + github.com/syndtr/goleveldb v1.0.0 // indirect + github.com/tecbot/gorocksdb v0.0.0-20191217155057-f0fad39f321c // indirect + go.etcd.io/bbolt v1.3.6 // indirect + go.uber.org/atomic v1.7.0 // indirect + go.uber.org/multierr v1.8.0 // indirect + golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 // indirect + golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 // indirect + golang.org/x/tools v0.1.0 // indirect + google.golang.org/protobuf v1.27.1 // indirect +) + +replace github.com/gnolang/gno => /home/moul/go/src/moul.io/gno/gno/