diff --git a/examples/gno.land/r/demo/tests/test20/gno.mod b/examples/gno.land/r/demo/tests/test20/gno.mod new file mode 100644 index 00000000000..5be8c8d409f --- /dev/null +++ b/examples/gno.land/r/demo/tests/test20/gno.mod @@ -0,0 +1,3 @@ +module gno.land/r/demo/tests/test20 + +require gno.land/p/demo/grc/grc20 v0.0.0-latest diff --git a/examples/gno.land/r/demo/tests/test20/test20.gno b/examples/gno.land/r/demo/tests/test20/test20.gno new file mode 100644 index 00000000000..6d1aaf108bf --- /dev/null +++ b/examples/gno.land/r/demo/tests/test20/test20.gno @@ -0,0 +1,18 @@ +// Package test20 implements a deliberately insecure ERC20 token for testing purposes. +// The Test20 token allows anyone to mint any amount of tokens to any address, making +// it unsuitable for production use. The primary goal of this package is to facilitate +// testing and experimentation without any security measures or restrictions. +// +// WARNING: This token is highly insecure and should not be used in any +// production environment. It is intended solely for testing and +// educational purposes. +package test20 + +import "gno.land/p/demo/grc/grc20" + +var ( + Banker = grc20.NewBanker("Test20", "TST", 4) + Token = Banker.Token() +) + +// XXX func init() { grc20reg.Register(Pub, "") } // Depends on #2516 diff --git a/examples/gno.land/r/demo/vault/gno.mod b/examples/gno.land/r/demo/vault/gno.mod new file mode 100644 index 00000000000..a6a5907e35f --- /dev/null +++ b/examples/gno.land/r/demo/vault/gno.mod @@ -0,0 +1,6 @@ +module gno.land/r/demo/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/r/demo/vault/types.gno b/examples/gno.land/r/demo/vault/types.gno new file mode 100644 index 00000000000..82cc35e1f58 --- /dev/null +++ b/examples/gno.land/r/demo/vault/types.gno @@ -0,0 +1,8 @@ +package vault + +import "errors" + +var ( + ErrTooEarlyToRedeem = errors.New("too early to redeem") + ErrNoSuchVault = errors.New("no such vault") +) diff --git a/examples/gno.land/r/demo/vault/vault.gno b/examples/gno.land/r/demo/vault/vault.gno new file mode 100644 index 00000000000..ea24a12a393 --- /dev/null +++ b/examples/gno.land/r/demo/vault/vault.gno @@ -0,0 +1,121 @@ +package vault + +import ( + "std" + + "gno.land/p/demo/avl" + "gno.land/p/demo/grc/grc20" +) + +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.Banker) Vault { + return &impl{ + adminToken: adminToken, + users: avl.Tree{}, + } +} + +type impl struct { + adminToken *grc20.Banker + 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 := v.adminToken.BalanceOf(caller) + 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/r/demo/vault/vault_test.gno b/examples/gno.land/r/demo/vault/vault_test.gno new file mode 100644 index 00000000000..c0dc6499300 --- /dev/null +++ b/examples/gno.land/r/demo/vault/vault_test.gno @@ -0,0 +1,3 @@ +package vault + +// TODO: unit tests, edge cases. diff --git a/examples/gno.land/r/demo/vault/z1_filetest.gno b/examples/gno.land/r/demo/vault/z1_filetest.gno new file mode 100644 index 00000000000..4f2c3b1e597 --- /dev/null +++ b/examples/gno.land/r/demo/vault/z1_filetest.gno @@ -0,0 +1,78 @@ +package main + +import ( + "std" + + "gno.land/p/demo/testutils" + "gno.land/p/demo/ufmt" + "gno.land/r/demo/tests/test20" + "gno.land/r/demo/vault" +) + +func main() { + var ( + alice = testutils.TestAddress("alice") + bob = testutils.TestAddress("bob") // recovery request address (cold wallet). + charly = testutils.TestAddress("charly") // recovery dest. + pkgaddr = std.GetOrigPkgAddr() + t20token = test20.Token + ) + + printBalances := func() { + aliceBalance := t20token.BalanceOf(alice) + bobBalance := t20token.BalanceOf(bob) + charlyBalance := t20token.BalanceOf(charly) + pkgBalance := t20token.BalanceOf(pkgaddr) + height := std.GetHeight() + println(ufmt.Sprintf( + "balances: alice=%d, bob=%d, charly=%d, pkg=%d, height=%d", + aliceBalance, bobBalance, charlyBalance, pkgBalance, height)) + } + + // create a vault for t20token. + v := vault.New(t20token) + printBalances() + + return // XXX + + // 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 t20tokens. + checkErr(v.Unvault(200)) + printBalances() + + // alice waits for few blocks. + std.TestSkipHeights(int64(lockDuration) + 1) + printBalances() + + // alice redeems 200 t20tokens. + 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