From 35551282b02e9de7359d049350794746c665a8ed Mon Sep 17 00:00:00 2001 From: Anthony De Meulemeester Date: Sun, 6 May 2018 08:03:26 +0200 Subject: [PATCH] Porting the NEX ICO template to neo-go as first class smart contract example (#78) * Initial draft of the ICO template ported from NEX. * filled in token configuration * added kyc storage prefix * fixed byte array conversion + added tests * fixed broken test + made 1 file for the token sale example. * implemented the NEP5 handlers * bumped version --- VERSION | 2 +- examples/token-sale/token_sale.avm | Bin 0 -> 2722 bytes examples/token-sale/token_sale.go | 246 +++++++++++++++++++++++++++ pkg/vm/api/storage/storage.go | 13 +- pkg/vm/compiler/codegen.go | 7 + pkg/vm/tests/byte_conversion_test.go | 56 ++++++ pkg/vm/tests/foobar/bar.go | 3 + pkg/vm/tests/foobar/foo.go | 7 + pkg/vm/tests/import_test.go | 14 ++ pkg/vm/tests/syscall_test.go | 6 +- pkg/vm/tests/vm_test.go | 31 +--- 11 files changed, 353 insertions(+), 32 deletions(-) create mode 100755 examples/token-sale/token_sale.avm create mode 100644 examples/token-sale/token_sale.go create mode 100644 pkg/vm/tests/byte_conversion_test.go create mode 100644 pkg/vm/tests/foobar/bar.go create mode 100644 pkg/vm/tests/foobar/foo.go diff --git a/VERSION b/VERSION index a8ab6c966..8b8043350 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.44.0 +0.44.2 \ No newline at end of file diff --git a/examples/token-sale/token_sale.avm b/examples/token-sale/token_sale.avm new file mode 100755 index 0000000000000000000000000000000000000000..790e1bdc8157940b6bac27c1c5be518d9b1e58e3 GIT binary patch literal 2722 zcmcIm%}&%%6m9_#1QiHDcM_usae;B63wH*iD}-r*0frwh?S+|4XG%IPh`Mm$N>@gf z6VL^TJ6-`(FeW~LFX1D&@O-DYoia`*nBb=OwCCLO^PTUWTYd1P>}-`ajxx%}^Atr(;YMrB%+C z{HmAtTGu%JDSADJ9wPebW5@K`$1mpoUM{Vv&of3z?&BSMCR z*Xh$U9Yo_;jmxYQF?Pt+B8Y2-AL)sa6FcX27c!hvV|eK5H_pE?r=e2B5Z@`U1XJqS z*kAmDLCX<kk~o36*0p0Y8%fwWk=#j{M125SCG&8NGq K5jWlwtp5SF>p#N) literal 0 HcmV?d00001 diff --git a/examples/token-sale/token_sale.go b/examples/token-sale/token_sale.go new file mode 100644 index 000000000..0e150925f --- /dev/null +++ b/examples/token-sale/token_sale.go @@ -0,0 +1,246 @@ +package tokensale + +import ( + "github.com/CityOfZion/neo-go/pkg/vm/api/runtime" + "github.com/CityOfZion/neo-go/pkg/vm/api/storage" +) + +const ( + decimals = 8 + multiplier = decimals * 10 +) + +var owner = []byte{0xaf, 0x12, 0xa8, 0x68, 0x7b, 0x14, 0x94, 0x8b, 0xc4, 0xa0, 0x08, 0x12, 0x8a, 0x55, 0x0a, 0x63, 0x69, 0x5b, 0xc1, 0xa5} + +// TokenConfig holds information about the token we want to use for the sale. +type TokenConfig struct { + // Name of the token. + Name string + // 3 letter abreviation of the token. + Symbol string + // How decimals this token will have. + Decimals int + // Address of the token owner. This is the Uint160 hash. + Owner []byte + // The total amount of tokens created. Notice that we need to multiply the + // amount by 100000000. (10^8) + TotalSupply int + // Initial amount is number of tokens that are available for the token sale. + InitialAmount int + // How many NEO will be worth 1 token. For example: + // Lets say 1 euro per token, where 1 NEO is 60 euro. This means buyers + // will get (60 * 10^8) tokens for 1 NEO. + AmountPerNEO int + // How many Gas will be worth 1 token. This is the same calculation as + // for the AmountPerNEO, except Gas price will have a different value. + AmountPerGas int + // The maximum amount you can mint in the limited round. For example: + // 500 NEO/buyer * 60 tokens/NEO * 10^8 + MaxExchangeLimitRound int + // When to start the token sale. + SaleStart int + // When to end the initial limited round if there is one. For example: + // SaleStart + 10000 + LimitRoundEnd int + // The prefix used to store how many tokens there are in circulation. + CirculationKey []byte + // The prefix used to store how many tokens there are in the limited round. + LimitRoundKey []byte + // The prefix used to store the addresses that are registered with KYC. + KYCKey []byte +} + +// NewTokenConfig returns the initialized TokenConfig. +func NewTokenConfig() TokenConfig { + return TokenConfig{ + Name: "My awesome token", + Symbol: "MAT", + Decimals: decimals, + Owner: owner, + TotalSupply: 10000000 * multiplier, + InitialAmount: 5000000 * multiplier, + AmountPerNEO: 60 * multiplier, + AmountPerGas: 40 * multiplier, + MaxExchangeLimitRound: 500 * 60 * multiplier, + SaleStart: 75500, + LimitRoundEnd: 75500 + 10000, + CirculationKey: []byte("in_circulation"), + LimitRoundKey: []byte("r1"), + KYCKey: []byte("kyc_ok"), + } +} + +// InCirculation return the amount of total tokens that are in circulation. +func (t TokenConfig) InCirculation(ctx storage.Context) int { + amount := storage.Get(ctx, t.CirculationKey) + return amount.(int) +} + +// AddToCirculation sets the given amount as "in circulation" in the storage. +func (t TokenConfig) AddToCirculation(ctx storage.Context, amount int) bool { + supply := storage.Get(ctx, t.CirculationKey).(int) + supply += amount + storage.Put(ctx, t.CirculationKey, supply) + return true +} + +// TokenSaleAvailableAmount returns the total amount of available tokens left +// to be distributed. +func (t TokenConfig) TokenSaleAvailableAmount(ctx storage.Context) int { + inCirc := storage.Get(ctx, t.CirculationKey) + return t.TotalSupply - inCirc.(int) +} + +// Main smart contract entry point. +func Main(operation string, args []interface{}) interface{} { + var ( + trigger = runtime.GetTrigger() + cfg = NewTokenConfig() + ctx = storage.GetContext() + ) + + // This is used to verify if a transfer of system assets (NEO and Gas) + // involving this contract's address can proceed. + if trigger == runtime.Verification() { + // Check if the invoker is the owner of the contract. + if runtime.CheckWitness(cfg.Owner) { + return true + } + // Otherwise TODO + return false + } + if trigger == runtime.Application() { + return handleOperation(operation, args, ctx, cfg) + } + return true +} + +func handleOperation(op string, args []interface{}, ctx storage.Context, cfg TokenConfig) interface{} { + // NEP-5 handlers + if op == "name" { + return cfg.Name + } + if op == "decimals" { + return cfg.Decimals + } + if op == "symbol" { + return cfg.Symbol + } + if op == "totalSupply" { + return storage.Get(ctx, cfg.CirculationKey) + } + if op == "balanceOf" { + if len(args) == 1 { + return storage.Get(ctx, args[0].([]byte)) + } + } + if op == "transfer" { + if len(args) != 3 { + return false + } + from := args[0].([]byte) + to := args[1].([]byte) + amount := args[2].(int) + return transfer(cfg, ctx, from, to, amount) + } + if op == "transferFrom" { + if len(args) != 3 { + return false + } + from := args[0].([]byte) + to := args[1].([]byte) + amount := args[2].(int) + return transferFrom(cfg, ctx, from, to, amount) + } + if op == "approve" { + if len(args) != 3 { + return false + } + from := args[0].([]byte) + to := args[1].([]byte) + amount := args[2].(int) + return approve(ctx, from, to, amount) + } + if op == "allowance" { + if len(args) != 2 { + return false + } + from := args[0].([]byte) + to := args[1].([]byte) + return allowance(ctx, from, to) + } + return false +} + +func transfer(cfg TokenConfig, ctx storage.Context, from, to []byte, amount int) bool { + if amount <= 0 || len(to) != 20 || !runtime.CheckWitness(from) { + return false + } + amountFrom := storage.Get(ctx, from).(int) + if amountFrom < amount { + return false + } + if amountFrom == amount { + storage.Delete(ctx, from) + } else { + diff := amountFrom - amount + storage.Put(ctx, from, diff) + } + amountTo := storage.Get(ctx, to).(int) + totalAmountTo := amountTo + amount + storage.Put(ctx, to, totalAmountTo) + return true +} + +func transferFrom(cfg TokenConfig, ctx storage.Context, from, to []byte, amount int) bool { + if amount <= 0 { + return false + } + availableKey := append(from, to...) + if len(availableKey) != 40 { + return false + } + availableTo := storage.Get(ctx, availableKey).(int) + if availableTo < amount { + return false + } + fromBalance := storage.Get(ctx, from).(int) + if fromBalance < amount { + return false + } + toBalance := storage.Get(ctx, to).(int) + newFromBalance := fromBalance - amount + newToBalance := toBalance + amount + storage.Put(ctx, to, newToBalance) + storage.Put(ctx, from, newFromBalance) + + newAllowance := availableTo - amount + if newAllowance == 0 { + storage.Delete(ctx, availableKey) + } else { + storage.Put(ctx, availableKey, newAllowance) + } + return true +} + +func approve(ctx storage.Context, owner, spender []byte, amount int) bool { + if !runtime.CheckWitness(owner) || amount < 0 { + return false + } + toSpend := storage.Get(ctx, owner).(int) + if toSpend < amount { + return false + } + approvalKey := append(owner, spender...) + if amount == 0 { + storage.Delete(ctx, approvalKey) + } else { + storage.Put(ctx, approvalKey, amount) + } + return true +} + +func allowance(ctx storage.Context, from, to []byte) int { + key := append(from, to...) + return storage.Get(ctx, key).(int) +} diff --git a/pkg/vm/api/storage/storage.go b/pkg/vm/api/storage/storage.go index 73c09e9aa..f253a07fe 100644 --- a/pkg/vm/api/storage/storage.go +++ b/pkg/vm/api/storage/storage.go @@ -1,13 +1,16 @@ package storage -// Context .. -func Context() interface{} { return 0 } +// Context represents the storage context. +type Context interface{} + +// GetContext returns the storage context. +func GetContext() interface{} { return nil } // Put stores a value in to the storage. -func Put(ctx interface{}, key string, value interface{}) {} +func Put(ctx interface{}, key interface{}, value interface{}) {} // Get returns the value from the storage. -func Get(ctx interface{}, key string) interface{} { return 0 } +func Get(ctx interface{}, key interface{}) interface{} { return 0 } // Delete removes a stored key value pair. -func Delete(ctx interface{}, key string) {} +func Delete(ctx interface{}, key interface{}) {} diff --git a/pkg/vm/compiler/codegen.go b/pkg/vm/compiler/codegen.go index b8ac0ae3b..f1a90aba3 100644 --- a/pkg/vm/compiler/codegen.go +++ b/pkg/vm/compiler/codegen.go @@ -395,6 +395,13 @@ func (c *codegen) Visit(node ast.Node) ast.Visitor { if !ok { log.Fatalf("could not resolve function %s", fun.Sel.Name) } + case *ast.ArrayType: + // For now we will assume that there is only 1 argument passed which + // will be a basic literal (string kind). This only to handle string + // to byte slice conversions. E.G. []byte("foobar") + arg := n.Args[0].(*ast.BasicLit) + c.emitLoadConst(c.typeInfo.Types[arg]) + return nil } // Handle the arguments diff --git a/pkg/vm/tests/byte_conversion_test.go b/pkg/vm/tests/byte_conversion_test.go new file mode 100644 index 000000000..1a1fde605 --- /dev/null +++ b/pkg/vm/tests/byte_conversion_test.go @@ -0,0 +1,56 @@ +package vm_test + +import "testing" + +func TestStringToByteConversion(t *testing.T) { + src := ` + package foo + func Main() []byte { + b := []byte("foo") + return b + } + ` + eval(t, src, []byte("foo")) +} + +func TestStringToByteAppend(t *testing.T) { + src := ` + package foo + func Main() []byte { + b := []byte("foo") + c := []byte("bar") + e := append(b, c...) + return e + } + ` + eval(t, src, []byte("foobar")) +} + +func TestByteConversionInFunctionCall(t *testing.T) { + src := ` + package foo + func Main() []byte { + b := []byte("foo") + return handle(b) + } + + func handle(b []byte) []byte { + return b + } + ` + eval(t, src, []byte("foo")) +} + +func TestByteConversionDirectlyInFunctionCall(t *testing.T) { + src := ` + package foo + func Main() []byte { + return handle([]byte("foo")) + } + + func handle(b []byte) []byte { + return b + } + ` + eval(t, src, []byte("foo")) +} diff --git a/pkg/vm/tests/foobar/bar.go b/pkg/vm/tests/foobar/bar.go new file mode 100644 index 000000000..b1624c196 --- /dev/null +++ b/pkg/vm/tests/foobar/bar.go @@ -0,0 +1,3 @@ +package foobar + +func getBool() bool { return true } diff --git a/pkg/vm/tests/foobar/foo.go b/pkg/vm/tests/foobar/foo.go new file mode 100644 index 000000000..877b06556 --- /dev/null +++ b/pkg/vm/tests/foobar/foo.go @@ -0,0 +1,7 @@ +package foobar + +// OtherBool ... +func OtherBool() bool { + ok := getBool() + return ok +} diff --git a/pkg/vm/tests/import_test.go b/pkg/vm/tests/import_test.go index eb5fe3e8e..c969ba71f 100644 --- a/pkg/vm/tests/import_test.go +++ b/pkg/vm/tests/import_test.go @@ -34,3 +34,17 @@ func TestImportStruct(t *testing.T) { ` eval(t, src, big.NewInt(0)) } + +func TestMultipleDirFileImport(t *testing.T) { + src := ` + package hello + + import "github.com/CityOfZion/neo-go/pkg/vm/tests/foobar" + + func Main() bool { + ok := foobar.OtherBool() + return ok + } + ` + eval(t, src, big.NewInt(1)) +} diff --git a/pkg/vm/tests/syscall_test.go b/pkg/vm/tests/syscall_test.go index db28cb628..5b3e4c883 100644 --- a/pkg/vm/tests/syscall_test.go +++ b/pkg/vm/tests/syscall_test.go @@ -11,9 +11,9 @@ func TestStoragePutGet(t *testing.T) { import "github.com/CityOfZion/neo-go/pkg/vm/api/storage" func Main() string { - ctx := storage.Context() - key := "token" - storage.Put(ctx, key, "foo") + ctx := storage.GetContext() + key := []byte("token") + storage.Put(ctx, key, []byte("foo")) x := storage.Get(ctx, key) return x.(string) } diff --git a/pkg/vm/tests/vm_test.go b/pkg/vm/tests/vm_test.go index 66b2f8e59..ac504c945 100644 --- a/pkg/vm/tests/vm_test.go +++ b/pkg/vm/tests/vm_test.go @@ -41,6 +41,7 @@ func vmAndCompile(t *testing.T, src string) *vm.VM { storePlugin := newStoragePlugin() vm.RegisterInteropFunc("Neo.Storage.Get", storePlugin.Get) vm.RegisterInteropFunc("Neo.Storage.Put", storePlugin.Put) + vm.RegisterInteropFunc("Neo.Storage.GetContext", storePlugin.GetContext) b, err := compiler.Compile(strings.NewReader(src), &compiler.Options{}) if err != nil { @@ -50,29 +51,6 @@ func vmAndCompile(t *testing.T, src string) *vm.VM { return vm } -func TestVMAndCompilerCases(t *testing.T) { - vm := vm.New(vm.ModeMute) - - storePlugin := newStoragePlugin() - vm.RegisterInteropFunc("Neo.Storage.Get", storePlugin.Get) - - testCases := []testCase{} - testCases = append(testCases, numericTestCases...) - testCases = append(testCases, assignTestCases...) - testCases = append(testCases, binaryExprTestCases...) - testCases = append(testCases, structTestCases...) - - for _, tc := range testCases { - b, err := compiler.Compile(strings.NewReader(tc.src), &compiler.Options{}) - if err != nil { - t.Fatal(err) - } - vm.Load(b) - vm.Run() - assert.Equal(t, tc.result, vm.PopResult()) - } -} - type storagePlugin struct { mem map[string][]byte } @@ -107,3 +85,10 @@ func (s *storagePlugin) Get(vm *vm.VM) error { } return fmt.Errorf("could not find %+v", item) } + +func (s *storagePlugin) GetContext(vm *vm.VM) error { + // Pushing anything on the stack here will work. This is just to satisfy + // the compiler, thinking it has pushed the context ^^. + vm.Estack().PushVal(10) + return nil +}