2020-09-21 14:00:33 +00:00
package contract
import (
"errors"
"fmt"
"strings"
2021-08-17 12:18:11 +00:00
"github.com/nspcc-dev/neo-go/pkg/core/dao"
2020-09-21 14:00:33 +00:00
"github.com/nspcc-dev/neo-go/pkg/core/interop"
2021-08-17 12:18:11 +00:00
"github.com/nspcc-dev/neo-go/pkg/core/native/nativenames"
2020-10-02 09:56:51 +00:00
"github.com/nspcc-dev/neo-go/pkg/core/state"
2020-12-29 10:44:07 +00:00
"github.com/nspcc-dev/neo-go/pkg/smartcontract"
2020-12-29 10:45:49 +00:00
"github.com/nspcc-dev/neo-go/pkg/smartcontract/callflag"
2020-09-21 14:00:33 +00:00
"github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest"
"github.com/nspcc-dev/neo-go/pkg/util"
2020-10-12 11:32:27 +00:00
"github.com/nspcc-dev/neo-go/pkg/vm"
2020-09-21 14:00:33 +00:00
"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
)
2021-08-17 12:18:11 +00:00
type policyChecker interface {
2022-04-19 15:57:46 +00:00
IsBlocked ( * dao . Simple , util . Uint160 ) bool
2021-08-17 12:18:11 +00:00
}
2022-04-20 18:30:09 +00:00
// LoadToken calls method specified by the token id.
2022-06-06 18:53:03 +00:00
func LoadToken ( ic * interop . Context , id int32 ) error {
ctx := ic . VM . Context ( )
if ! ctx . GetCallFlags ( ) . Has ( callflag . ReadStates | callflag . AllowCall ) {
return errors . New ( "invalid call flags" )
2021-01-19 08:23:39 +00:00
}
2022-06-06 18:53:03 +00:00
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 , false )
2021-01-19 08:23:39 +00:00
}
2020-12-29 10:44:07 +00:00
// Call calls a contract with flags.
2020-09-21 14:00:33 +00:00
func Call ( ic * interop . Context ) error {
h := ic . VM . Estack ( ) . Pop ( ) . Bytes ( )
method := ic . VM . Estack ( ) . Pop ( ) . String ( )
2020-12-29 10:45:49 +00:00
fs := callflag . CallFlag ( int32 ( ic . VM . Estack ( ) . Pop ( ) . BigInt ( ) . Int64 ( ) ) )
if fs &^ callflag . All != 0 {
2020-09-21 14:00:33 +00:00
return errors . New ( "call flags out of range" )
}
2020-12-29 10:44:07 +00:00
args := ic . VM . Estack ( ) . Pop ( ) . Array ( )
2020-09-21 14:00:33 +00:00
u , err := util . Uint160DecodeBytesBE ( h )
if err != nil {
return errors . New ( "invalid contract hash" )
}
2020-12-13 15:26:35 +00:00
cs , err := ic . GetContract ( u )
2020-09-21 14:00:33 +00:00
if err != nil {
2021-05-17 17:12:19 +00:00
return fmt . Errorf ( "called contract %s not found: %w" , u . StringLE ( ) , err )
2020-09-21 14:00:33 +00:00
}
2021-01-19 08:23:39 +00:00
if strings . HasPrefix ( method , "_" ) {
2020-09-21 14:00:33 +00:00
return errors . New ( "invalid method name (starts with '_')" )
}
2021-01-26 14:37:34 +00:00
md := cs . Manifest . ABI . GetMethod ( method , len ( args ) )
2020-12-08 10:27:41 +00:00
if md == nil {
2021-10-28 14:41:35 +00:00
return fmt . Errorf ( "method not found: %s/%d" , method , len ( args ) )
2020-12-08 10:27:41 +00:00
}
2020-12-29 10:44:07 +00:00
hasReturn := md . ReturnType != smartcontract . VoidType
2022-05-25 07:00:02 +00:00
return callInternal ( ic , cs , method , fs , hasReturn , args , ! hasReturn )
2021-01-19 08:23:39 +00:00
}
func callInternal ( ic * interop . Context , cs * state . Contract , name string , f callflag . CallFlag ,
2022-05-25 07:00:02 +00:00
hasReturn bool , args [ ] stackitem . Item , pushNullOnUnloading bool ) error {
2021-01-26 14:37:34 +00:00
md := cs . Manifest . ABI . GetMethod ( name , len ( args ) )
2020-12-08 10:27:41 +00:00
if md . Safe {
2021-03-16 19:47:49 +00:00
f &^= ( callflag . WriteStates | callflag . AllowNotify )
2020-12-08 10:27:41 +00:00
} else if ctx := ic . VM . Context ( ) ; ctx != nil && ctx . IsDeployed ( ) {
2020-12-13 15:26:35 +00:00
curr , err := ic . GetContract ( ic . VM . GetCurrentScriptHash ( ) )
2020-11-26 20:02:00 +00:00
if err == nil {
2021-01-19 08:23:39 +00:00
if ! curr . Manifest . CanCall ( cs . Hash , & cs . Manifest , name ) {
2020-11-26 20:02:00 +00:00
return errors . New ( "disallowed method call" )
}
2020-09-21 14:00:33 +00:00
}
}
2022-05-26 08:44:26 +00:00
return callExFromNative ( ic , ic . VM . GetCurrentScriptHash ( ) , cs , name , args , f , hasReturn , pushNullOnUnloading , false )
2020-12-09 12:16:49 +00:00
}
2022-04-20 18:30:09 +00:00
// callExFromNative calls a contract with flags using the provided calling hash.
2020-12-09 12:16:49 +00:00
func callExFromNative ( ic * interop . Context , caller util . Uint160 , cs * state . Contract ,
2022-05-26 08:44:26 +00:00
name string , args [ ] stackitem . Item , f callflag . CallFlag , hasReturn bool , pushNullOnUnloading bool , callFromNative bool ) error {
2021-08-17 12:18:11 +00:00
for _ , nc := range ic . Natives {
if nc . Metadata ( ) . Name == nativenames . Policy {
var pch = nc . ( policyChecker )
2022-04-19 15:57:46 +00:00
if pch . IsBlocked ( ic . DAO , cs . Hash ) {
2021-08-17 12:18:11 +00:00
return fmt . Errorf ( "contract %s is blocked" , cs . Hash . StringLE ( ) )
}
break
}
}
2021-01-26 14:37:34 +00:00
md := cs . Manifest . ABI . GetMethod ( name , len ( args ) )
2020-10-02 09:56:51 +00:00
if md == nil {
return fmt . Errorf ( "method '%s' not found" , name )
}
2020-09-21 14:00:33 +00:00
if len ( args ) != len ( md . Parameters ) {
return fmt . Errorf ( "invalid argument count: %d (expected %d)" , len ( args ) , len ( md . Parameters ) )
}
2021-11-19 17:02:32 +00:00
methodOff := md . Offset
initOff := - 1
2021-01-26 14:37:34 +00:00
md = cs . Manifest . ABI . GetMethod ( manifest . MethodInit , 0 )
2020-09-21 14:00:33 +00:00
if md != nil {
2021-11-19 17:02:32 +00:00
initOff = md . Offset
2020-09-21 14:00:33 +00:00
}
2021-11-19 17:25:58 +00:00
ic . Invocations [ cs . Hash ] ++
2022-05-25 07:00:02 +00:00
f = ic . VM . Context ( ) . GetCallFlags ( ) & f
2022-05-31 08:44:12 +00:00
wrapped := ic . VM . Context ( ) . HasTryBlock ( ) && // If the method is not wrapped into try-catch block, then changes should be discarded anyway if exception occurs.
f & ( callflag . All ^ callflag . ReadOnly ) != 0 // If the method is safe, then it's read-only and doesn't perform storage changes or emit notifications.
2022-05-25 07:00:02 +00:00
baseNtfCount := len ( ic . Notifications )
baseDAO := ic . DAO
if wrapped {
ic . DAO = ic . DAO . GetPrivate ( )
}
onUnload := func ( commit bool ) error {
if wrapped {
if commit {
_ , err := ic . DAO . Persist ( )
if err != nil {
return fmt . Errorf ( "failed to persist changes %w" , err )
}
} else {
ic . Notifications = ic . Notifications [ : baseNtfCount ] // Rollback all notification changes made by current context.
}
ic . DAO = baseDAO
}
if pushNullOnUnloading && commit {
ic . VM . Context ( ) . Estack ( ) . PushItem ( stackitem . Null { } ) // Must use current context stack.
}
2022-05-26 08:44:26 +00:00
if callFromNative && ! commit {
return fmt . Errorf ( "unhandled exception" )
}
2022-05-25 07:00:02 +00:00
return nil
}
ic . VM . LoadNEFMethod ( & cs . NEF , caller , cs . Hash , f ,
hasReturn , methodOff , initOff , onUnload )
2020-09-21 14:00:33 +00:00
2021-11-19 17:02:32 +00:00
for e , i := ic . VM . Estack ( ) , len ( args ) - 1 ; i >= 0 ; i -- {
e . PushItem ( args [ i ] )
}
2020-09-21 14:00:33 +00:00
return nil
}
2020-12-09 12:16:49 +00:00
// ErrNativeCall is returned for failed calls from native.
2021-07-23 07:08:09 +00:00
var ErrNativeCall = errors . New ( "failed native call" )
2020-12-09 12:16:49 +00:00
// CallFromNative performs synchronous call from native contract.
2020-12-29 10:44:07 +00:00
func CallFromNative ( ic * interop . Context , caller util . Uint160 , cs * state . Contract , method string , args [ ] stackitem . Item , hasReturn bool ) error {
2020-12-09 12:16:49 +00:00
startSize := ic . VM . Istack ( ) . Len ( )
2022-05-26 08:44:26 +00:00
if err := callExFromNative ( ic , caller , cs , method , args , callflag . All , hasReturn , false , true ) ; err != nil {
2020-12-09 12:16:49 +00:00
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
}