package contract import ( "errors" "fmt" "strings" "github.com/nspcc-dev/neo-go/pkg/core/dao" "github.com/nspcc-dev/neo-go/pkg/core/interop" "github.com/nspcc-dev/neo-go/pkg/core/native/nativenames" "github.com/nspcc-dev/neo-go/pkg/core/state" "github.com/nspcc-dev/neo-go/pkg/smartcontract" "github.com/nspcc-dev/neo-go/pkg/smartcontract/callflag" "github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest" "github.com/nspcc-dev/neo-go/pkg/util" "github.com/nspcc-dev/neo-go/pkg/vm" "github.com/nspcc-dev/neo-go/pkg/vm/stackitem" ) type policyChecker interface { IsBlockedInternal(dao.DAO, util.Uint160) bool } // LoadToken calls method specified by token id. func LoadToken(ic *interop.Context) func(id int32) error { return func(id int32) error { ctx := ic.VM.Context() if !ctx.GetCallFlags().Has(callflag.ReadStates | callflag.AllowCall) { return errors.New("invalid call flags") } tok := ctx.NEF.Tokens[id] if int(tok.ParamCount) > ctx.Estack().Len() { return errors.New("stack is too small") } args := make([]stackitem.Item, tok.ParamCount) for i := range args { args[i] = ic.VM.Estack().Pop().Item() } cs, err := ic.GetContract(tok.Hash) if err != nil { return fmt.Errorf("token contract %s not found: %w", tok.Hash.StringLE(), err) } return callInternal(ic, cs, tok.Method, tok.CallFlag, tok.HasReturn, args) } } // Call calls a contract with flags. func Call(ic *interop.Context) error { h := ic.VM.Estack().Pop().Bytes() method := ic.VM.Estack().Pop().String() fs := callflag.CallFlag(int32(ic.VM.Estack().Pop().BigInt().Int64())) if fs&^callflag.All != 0 { return errors.New("call flags out of range") } args := ic.VM.Estack().Pop().Array() u, err := util.Uint160DecodeBytesBE(h) if err != nil { return errors.New("invalid contract hash") } cs, err := ic.GetContract(u) if err != nil { return fmt.Errorf("called contract %s not found: %w", u.StringLE(), err) } if strings.HasPrefix(method, "_") { return errors.New("invalid method name (starts with '_')") } md := cs.Manifest.ABI.GetMethod(method, len(args)) if md == nil { return errors.New("method not found") } hasReturn := md.ReturnType != smartcontract.VoidType if !hasReturn { ic.VM.Estack().PushItem(stackitem.Null{}) } return callInternal(ic, cs, method, fs, hasReturn, args) } func callInternal(ic *interop.Context, cs *state.Contract, name string, f callflag.CallFlag, hasReturn bool, args []stackitem.Item) error { md := cs.Manifest.ABI.GetMethod(name, len(args)) if md.Safe { f &^= (callflag.WriteStates | callflag.AllowNotify) } else if ctx := ic.VM.Context(); ctx != nil && ctx.IsDeployed() { curr, err := ic.GetContract(ic.VM.GetCurrentScriptHash()) if err == nil { if !curr.Manifest.CanCall(cs.Hash, &cs.Manifest, name) { return errors.New("disallowed method call") } } } return callExFromNative(ic, ic.VM.GetCurrentScriptHash(), cs, name, args, f, hasReturn) } // callExFromNative calls a contract with flags using provided calling hash. func callExFromNative(ic *interop.Context, caller util.Uint160, cs *state.Contract, name string, args []stackitem.Item, f callflag.CallFlag, hasReturn bool) error { for _, nc := range ic.Natives { if nc.Metadata().Name == nativenames.Policy { var pch = nc.(policyChecker) if pch.IsBlockedInternal(ic.DAO, cs.Hash) { return fmt.Errorf("contract %s is blocked", cs.Hash.StringLE()) } break } } md := cs.Manifest.ABI.GetMethod(name, len(args)) if md == nil { return fmt.Errorf("method '%s' not found", name) } if len(args) != len(md.Parameters) { return fmt.Errorf("invalid argument count: %d (expected %d)", len(args), len(md.Parameters)) } ic.VM.Invocations[cs.Hash]++ ic.VM.LoadScriptWithCallingHash(caller, cs.NEF.Script, cs.Hash, ic.VM.Context().GetCallFlags()&f, hasReturn, uint16(len(args))) ic.VM.Context().NEF = &cs.NEF for i := len(args) - 1; i >= 0; i-- { ic.VM.Estack().PushItem(args[i]) } // use Jump not Call here because context was loaded in LoadScript above. ic.VM.Jump(ic.VM.Context(), md.Offset) if hasReturn { ic.VM.Context().RetCount = 1 } else { ic.VM.Context().RetCount = 0 } md = cs.Manifest.ABI.GetMethod(manifest.MethodInit, 0) if md != nil { ic.VM.Call(ic.VM.Context(), md.Offset) } return nil } // ErrNativeCall is returned for failed calls from native. var ErrNativeCall = errors.New("failed native call") // CallFromNative performs synchronous call from native contract. func CallFromNative(ic *interop.Context, caller util.Uint160, cs *state.Contract, method string, args []stackitem.Item, hasReturn bool) error { startSize := ic.VM.Istack().Len() if err := callExFromNative(ic, caller, cs, method, args, callflag.All, hasReturn); err != nil { return err } for !ic.VM.HasStopped() && ic.VM.Istack().Len() > startSize { if err := ic.VM.Step(); err != nil { return fmt.Errorf("%w: %v", ErrNativeCall, err) } } if ic.VM.State() == vm.FaultState { return ErrNativeCall } return nil }