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
This commit is contained in:
Anthony De Meulemeester 2018-05-06 08:03:26 +02:00 committed by GitHub
parent 0ca8865402
commit 35551282b0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 353 additions and 32 deletions

View file

@ -1 +1 @@
0.44.0 0.44.2

Binary file not shown.

View file

@ -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)
}

View file

@ -1,13 +1,16 @@
package storage package storage
// Context .. // Context represents the storage context.
func Context() interface{} { return 0 } type Context interface{}
// GetContext returns the storage context.
func GetContext() interface{} { return nil }
// Put stores a value in to the storage. // 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. // 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. // Delete removes a stored key value pair.
func Delete(ctx interface{}, key string) {} func Delete(ctx interface{}, key interface{}) {}

View file

@ -395,6 +395,13 @@ func (c *codegen) Visit(node ast.Node) ast.Visitor {
if !ok { if !ok {
log.Fatalf("could not resolve function %s", fun.Sel.Name) 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 // Handle the arguments

View file

@ -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"))
}

View file

@ -0,0 +1,3 @@
package foobar
func getBool() bool { return true }

View file

@ -0,0 +1,7 @@
package foobar
// OtherBool ...
func OtherBool() bool {
ok := getBool()
return ok
}

View file

@ -34,3 +34,17 @@ func TestImportStruct(t *testing.T) {
` `
eval(t, src, big.NewInt(0)) 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))
}

View file

@ -11,9 +11,9 @@ func TestStoragePutGet(t *testing.T) {
import "github.com/CityOfZion/neo-go/pkg/vm/api/storage" import "github.com/CityOfZion/neo-go/pkg/vm/api/storage"
func Main() string { func Main() string {
ctx := storage.Context() ctx := storage.GetContext()
key := "token" key := []byte("token")
storage.Put(ctx, key, "foo") storage.Put(ctx, key, []byte("foo"))
x := storage.Get(ctx, key) x := storage.Get(ctx, key)
return x.(string) return x.(string)
} }

View file

@ -41,6 +41,7 @@ func vmAndCompile(t *testing.T, src string) *vm.VM {
storePlugin := newStoragePlugin() storePlugin := newStoragePlugin()
vm.RegisterInteropFunc("Neo.Storage.Get", storePlugin.Get) vm.RegisterInteropFunc("Neo.Storage.Get", storePlugin.Get)
vm.RegisterInteropFunc("Neo.Storage.Put", storePlugin.Put) vm.RegisterInteropFunc("Neo.Storage.Put", storePlugin.Put)
vm.RegisterInteropFunc("Neo.Storage.GetContext", storePlugin.GetContext)
b, err := compiler.Compile(strings.NewReader(src), &compiler.Options{}) b, err := compiler.Compile(strings.NewReader(src), &compiler.Options{})
if err != nil { if err != nil {
@ -50,29 +51,6 @@ func vmAndCompile(t *testing.T, src string) *vm.VM {
return 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 { type storagePlugin struct {
mem map[string][]byte mem map[string][]byte
} }
@ -107,3 +85,10 @@ func (s *storagePlugin) Get(vm *vm.VM) error {
} }
return fmt.Errorf("could not find %+v", item) 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
}