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:
parent
0ca8865402
commit
35551282b0
11 changed files with 353 additions and 32 deletions
2
VERSION
2
VERSION
|
@ -1 +1 @@
|
|||
0.44.0
|
||||
0.44.2
|
BIN
examples/token-sale/token_sale.avm
Executable file
BIN
examples/token-sale/token_sale.avm
Executable file
Binary file not shown.
246
examples/token-sale/token_sale.go
Normal file
246
examples/token-sale/token_sale.go
Normal 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)
|
||||
}
|
|
@ -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{}) {}
|
||||
|
|
|
@ -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
|
||||
|
|
56
pkg/vm/tests/byte_conversion_test.go
Normal file
56
pkg/vm/tests/byte_conversion_test.go
Normal 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"))
|
||||
}
|
3
pkg/vm/tests/foobar/bar.go
Normal file
3
pkg/vm/tests/foobar/bar.go
Normal file
|
@ -0,0 +1,3 @@
|
|||
package foobar
|
||||
|
||||
func getBool() bool { return true }
|
7
pkg/vm/tests/foobar/foo.go
Normal file
7
pkg/vm/tests/foobar/foo.go
Normal file
|
@ -0,0 +1,7 @@
|
|||
package foobar
|
||||
|
||||
// OtherBool ...
|
||||
func OtherBool() bool {
|
||||
ok := getBool()
|
||||
return ok
|
||||
}
|
|
@ -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))
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue