package balance import ( "github.com/nspcc-dev/neo-go/pkg/interop" "github.com/nspcc-dev/neo-go/pkg/interop/contract" "github.com/nspcc-dev/neo-go/pkg/interop/iterator" "github.com/nspcc-dev/neo-go/pkg/interop/native/management" "github.com/nspcc-dev/neo-go/pkg/interop/native/std" "github.com/nspcc-dev/neo-go/pkg/interop/runtime" "github.com/nspcc-dev/neo-go/pkg/interop/storage" "github.com/nspcc-dev/neofs-contract/common" ) type ( // Token holds all token info. Token struct { // Ticker symbol Symbol string // Amount of decimals Decimals int // Storage key for circulation value CirculationKey string } // Account structure stores metadata of each NeoFS balance account. Account struct { // Active balance Balance int // Until valid for lock accounts Until int // Parent field used in lock accounts, used to return assets back if // account wasn't burnt. Parent []byte } ) const ( symbol = "NEOFS" decimals = 12 circulation = "MainnetGAS" netmapContractKey = "netmapScriptHash" containerContractKey = "containerScriptHash" notaryDisabledKey = "notary" ) var token Token func createToken() Token { return Token{ Symbol: symbol, Decimals: decimals, CirculationKey: circulation, } } func init() { token = createToken() } func _deploy(data interface{}, isUpdate bool) { if isUpdate { return } args := data.([]interface{}) notaryDisabled := args[0].(bool) owner := args[1].(interop.Hash160) addrNetmap := args[2].(interop.Hash160) addrContainer := args[3].(interop.Hash160) ctx := storage.GetContext() if !common.HasUpdateAccess(ctx) { panic("only owner can reinitialize contract") } if len(addrNetmap) != 20 || len(addrContainer) != 20 { panic("init: incorrect length of contract script hash") } storage.Put(ctx, common.OwnerKey, owner) storage.Put(ctx, netmapContractKey, addrNetmap) storage.Put(ctx, containerContractKey, addrContainer) // initialize the way to collect signatures storage.Put(ctx, notaryDisabledKey, notaryDisabled) if notaryDisabled { common.InitVote(ctx) runtime.Log("balance contract notary disabled") } runtime.Log("balance contract initialized") } // Migrate method updates contract source code and manifest. Can be invoked // only by contract owner. func Migrate(script []byte, manifest []byte, data interface{}) bool { ctx := storage.GetReadOnlyContext() if !common.HasUpdateAccess(ctx) { runtime.Log("only owner can update contract") return false } contract.Call(interop.Hash160(management.Hash), "update", contract.All, script, manifest, data) runtime.Log("balance contract updated") return true } // Symbol is a NEP-17 standard method that returns NEOFS token symbol. func Symbol() string { return token.Symbol } // Decimals is a NEP-17 standard method that returns precision of NeoFS // balances. func Decimals() int { return token.Decimals } // TotalSupply is a NEP-17 standard method that returns total amount of main // chain GAS in the NeoFS network. func TotalSupply() int { ctx := storage.GetReadOnlyContext() return token.getSupply(ctx) } // BalanceOf is a NEP-17 standard method that returns NeoFS balance of specified // account. func BalanceOf(account interop.Hash160) int { ctx := storage.GetReadOnlyContext() return token.balanceOf(ctx, account) } // Transfer is a NEP-17 standard method that transfers NeoFS balance from one // account to other. Can be invoked only by account owner. // // Produces Transfer and TransferX notifications. TransferX notification // will have empty details field. func Transfer(from, to interop.Hash160, amount int, data interface{}) bool { ctx := storage.GetContext() return token.transfer(ctx, from, to, amount, false, nil) } // TransferX is a method for NeoFS balance transfers from one account to // another. Can be invoked by account owner or by Alphabet nodes. // // Produces Transfer and TransferX notifications. // // TransferX method expands Transfer method by having extra details argument. // Also TransferX method allows to transfer assets by Alphabet nodes of the // Inner Ring with multi signature. func TransferX(from, to interop.Hash160, amount int, details []byte) { ctx := storage.GetContext() notaryDisabled := storage.Get(ctx, notaryDisabledKey).(bool) var ( // for invocation collection without notary alphabet []common.IRNode nodeKey []byte indirectCall bool ) if notaryDisabled { alphabet = common.AlphabetNodes() nodeKey = common.InnerRingInvoker(alphabet) if len(nodeKey) == 0 { panic("transferX: this method must be invoked from inner ring") } indirectCall = common.FromKnownContract( ctx, runtime.GetCallingScriptHash(), containerContractKey, ) } else { multiaddr := common.AlphabetAddress() if !runtime.CheckWitness(multiaddr) { panic("transferX: this method must be invoked from inner ring") } } if notaryDisabled && !indirectCall { threshold := len(alphabet)*2/3 + 1 id := common.InvokeID([]interface{}{from, to, amount}, []byte("transfer")) n := common.Vote(ctx, id, nodeKey) if n < threshold { return } common.RemoveVotes(ctx, id) } result := token.transfer(ctx, from, to, amount, true, details) if !result { panic("transferX: fail") } runtime.Log("transferX: success") } // Lock is a method that transfers assets from user account to lock account // related to the user. Can be invoked only by Alphabet nodes of the Inner Ring. // // Produces Lock, Transfer and TransferX notifications. // // Lock method invoked by Alphabet nodes of the Inner Ring when they process // Withdraw notification from NeoFS contract. This should transfer assets // to new lock account that won't be used for anything besides Unlock and Burn. func Lock(txDetails []byte, from, to interop.Hash160, amount, until int) { ctx := storage.GetContext() notaryDisabled := storage.Get(ctx, notaryDisabledKey).(bool) var ( // for invocation collection without notary alphabet []common.IRNode nodeKey []byte ) if notaryDisabled { alphabet = common.AlphabetNodes() nodeKey = common.InnerRingInvoker(alphabet) if len(nodeKey) == 0 { panic("lock: this method must be invoked from inner ring") } } else { multiaddr := common.AlphabetAddress() if !runtime.CheckWitness(multiaddr) { panic("lock: this method must be invoked from inner ring") } } details := common.LockTransferDetails(txDetails) lockAccount := Account{ Balance: 0, Until: until, Parent: from, } if notaryDisabled { threshold := len(alphabet)*2/3 + 1 id := common.InvokeID([]interface{}{txDetails}, []byte("lock")) n := common.Vote(ctx, id, nodeKey) if n < threshold { return } common.RemoveVotes(ctx, id) } common.SetSerialized(ctx, to, lockAccount) result := token.transfer(ctx, from, to, amount, true, details) if !result { // consider using `return false` to remove votes panic("lock: can't lock funds") } runtime.Log("lock: created lock account") runtime.Notify("Lock", txDetails, from, to, amount, until) } // NewEpoch is a method that checks timeout on lock accounts and return assets // if lock is not available anymore. Can be invoked only by NewEpoch method // of Netmap contract. // // Produces Transfer and TransferX notifications. func NewEpoch(epochNum int) { ctx := storage.GetContext() notaryDisabled := storage.Get(ctx, notaryDisabledKey).(bool) if notaryDisabled { indirectCall := common.FromKnownContract( ctx, runtime.GetCallingScriptHash(), netmapContractKey, ) if !indirectCall { panic("newEpoch: this method must be invoked from inner ring") } } else { multiaddr := common.AlphabetAddress() if !runtime.CheckWitness(multiaddr) { panic("newEpoch: this method must be invoked from inner ring") } } it := storage.Find(ctx, []byte{}, storage.KeysOnly) for iterator.Next(it) { addr := iterator.Value(it).(interop.Hash160) // it MUST BE `storage.KeysOnly` if len(addr) != 20 { continue } acc := getAccount(ctx, addr) if acc.Until == 0 { continue } if epochNum >= acc.Until { details := common.UnlockTransferDetails(epochNum) // return assets back to the parent token.transfer(ctx, addr, acc.Parent, acc.Balance, true, details) } } } // Mint is a method that transfers assets to user account from empty account. // Can be invoked only by Alphabet nodes of the Inner Ring. // // Produces Mint, Transfer and TransferX notifications. // // Mint method invoked by Alphabet nodes of the Inner Ring when they process // Deposit notification from NeoFS contract. Before that Alphabet nodes should // synchronize precision of main chain GAS contract and Balance contract. // Mint increases total supply of NEP-17 compatible NeoFS token. func Mint(to interop.Hash160, amount int, txDetails []byte) { ctx := storage.GetContext() notaryDisabled := storage.Get(ctx, notaryDisabledKey).(bool) var ( // for invocation collection without notary alphabet []common.IRNode nodeKey []byte ) if notaryDisabled { alphabet = common.AlphabetNodes() nodeKey = common.InnerRingInvoker(alphabet) if len(nodeKey) == 0 { panic("mint: this method must be invoked from inner ring") } } else { multiaddr := common.AlphabetAddress() if !runtime.CheckWitness(multiaddr) { panic("mint: this method must be invoked from inner ring") } } details := common.MintTransferDetails(txDetails) if notaryDisabled { threshold := len(alphabet)*2/3 + 1 id := common.InvokeID([]interface{}{txDetails}, []byte("mint")) n := common.Vote(ctx, id, nodeKey) if n < threshold { return } common.RemoveVotes(ctx, id) } ok := token.transfer(ctx, nil, to, amount, true, details) if !ok { panic("mint: can't transfer assets") } supply := token.getSupply(ctx) supply = supply + amount storage.Put(ctx, token.CirculationKey, supply) runtime.Log("mint: assets were minted") runtime.Notify("Mint", to, amount) } // Burn is a method that transfers assets from user account to empty account. // Can be invoked only by Alphabet nodes of the Inner Ring. // // Produces Burn, Transfer and TransferX notifications. // // Burn method invoked by Alphabet nodes of the Inner Ring when they process // Cheque notification from NeoFS contract. It means that locked assets were // transferred to user in main chain, therefore lock account should be destroyed. // Before that Alphabet nodes should synchronize precision of main chain GAS // contract and Balance contract. Burn decreases total supply of NEP-17 // compatible NeoFS token. func Burn(from interop.Hash160, amount int, txDetails []byte) { ctx := storage.GetContext() notaryDisabled := storage.Get(ctx, notaryDisabledKey).(bool) var ( // for invocation collection without notary alphabet []common.IRNode nodeKey []byte ) if notaryDisabled { alphabet = common.AlphabetNodes() nodeKey = common.InnerRingInvoker(alphabet) if len(nodeKey) == 0 { panic("burn: this method must be invoked from inner ring") } } else { multiaddr := common.AlphabetAddress() if !runtime.CheckWitness(multiaddr) { panic("burn: this method must be invoked from inner ring") } } details := common.BurnTransferDetails(txDetails) if notaryDisabled { threshold := len(alphabet)*2/3 + 1 id := common.InvokeID([]interface{}{txDetails}, []byte("burn")) n := common.Vote(ctx, id, nodeKey) if n < threshold { return } common.RemoveVotes(ctx, id) } ok := token.transfer(ctx, from, nil, amount, true, details) if !ok { panic("burn: can't transfer assets") } supply := token.getSupply(ctx) if supply < amount { panic("panic, negative supply after burn") } supply = supply - amount storage.Put(ctx, token.CirculationKey, supply) runtime.Log("burn: assets were burned") runtime.Notify("Burn", from, amount) } // Version returns version of the contract. func Version() int { return common.Version } // getSupply gets the token totalSupply value from VM storage. func (t Token) getSupply(ctx storage.Context) int { supply := storage.Get(ctx, t.CirculationKey) if supply != nil { return supply.(int) } return 0 } // BalanceOf gets the token balance of a specific address. func (t Token) balanceOf(ctx storage.Context, holder interop.Hash160) int { acc := getAccount(ctx, holder) return acc.Balance } func (t Token) transfer(ctx storage.Context, from, to interop.Hash160, amount int, innerRing bool, details []byte) bool { amountFrom, ok := t.canTransfer(ctx, from, to, amount, innerRing) if !ok { return false } if len(from) == 20 { if amountFrom.Balance == amount { storage.Delete(ctx, from) } else { amountFrom.Balance = amountFrom.Balance - amount // neo-go#953 common.SetSerialized(ctx, from, amountFrom) } } if len(to) == 20 { amountTo := getAccount(ctx, to) amountTo.Balance = amountTo.Balance + amount // neo-go#953 common.SetSerialized(ctx, to, amountTo) } runtime.Notify("Transfer", from, to, amount) runtime.Notify("TransferX", from, to, amount, details) return true } // canTransfer returns the amount it can transfer. func (t Token) canTransfer(ctx storage.Context, from, to interop.Hash160, amount int, innerRing bool) (Account, bool) { var ( emptyAcc = Account{} ) if !innerRing { if len(to) != 20 || !isUsableAddress(from) { runtime.Log("transfer: bad script hashes") return emptyAcc, false } } else if len(from) == 0 { return emptyAcc, true } amountFrom := getAccount(ctx, from) if amountFrom.Balance < amount { runtime.Log("transfer: not enough assets") return emptyAcc, false } // return amountFrom value back to transfer, reduces extra Get return amountFrom, true } // isUsableAddress checks if the sender is either the correct NEO address or SC address. func isUsableAddress(addr interop.Hash160) bool { if len(addr) == 20 { if runtime.CheckWitness(addr) { return true } // Check if a smart contract is calling script hash callingScriptHash := runtime.GetCallingScriptHash() if common.BytesEqual(callingScriptHash, addr) { return true } } return false } func getAccount(ctx storage.Context, key interface{}) Account { data := storage.Get(ctx, key) if data != nil { return std.Deserialize(data.([]byte)).(Account) } return Account{} }