Skip to content

Commit

Permalink
interop: use currently executing contract state for permissions check
Browse files Browse the repository at this point in the history
It's not correct to use an updated contract state got from Management to
check for the allowed method call. We need to use manifest from the
currently executing context for that. It may be critical for cases when
executing contract is being updated firstly, and after that calls
another contract. So we need an old (executing) contract manifest for
this check.

This change likely does not affect the mainnet's state since it's hard
to meet the trigger criteria, but I'd put it under the hardfork anyway.

Signed-off-by: Anna Shaleva <[email protected]>
  • Loading branch information
AnnaShaleva committed Jun 4, 2024
1 parent e0825e7 commit dc4d5a2
Show file tree
Hide file tree
Showing 2 changed files with 123 additions and 4 deletions.
15 changes: 11 additions & 4 deletions pkg/core/interop/contract/call.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"math/big"
"strings"

"github.com/nspcc-dev/neo-go/pkg/config"
"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"
Expand Down Expand Up @@ -77,12 +78,18 @@ func callInternal(ic *interop.Context, cs *state.Contract, name string, f callfl
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")
var mfst *manifest.Manifest
if ic.IsHardforkEnabled(config.HFDomovoi) {
mfst = ctx.GetManifest()
} else {
curr, err := ic.GetContract(ic.VM.GetCurrentScriptHash())
if err == nil {
mfst = &curr.Manifest
}
}
if mfst != nil && !mfst.CanCall(cs.Hash, &cs.Manifest, name) {
return errors.New("disallowed method call")
}
}
return callExFromNative(ic, ic.VM.GetCurrentScriptHash(), cs, name, args, f, hasReturn, isDynamic, false)
}
Expand Down
112 changes: 112 additions & 0 deletions pkg/core/interop/contract/call_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"github.com/nspcc-dev/neo-go/internal/contracts"
"github.com/nspcc-dev/neo-go/internal/random"
"github.com/nspcc-dev/neo-go/pkg/compiler"
"github.com/nspcc-dev/neo-go/pkg/config"
"github.com/nspcc-dev/neo-go/pkg/core/block"
"github.com/nspcc-dev/neo-go/pkg/core/interop"
"github.com/nspcc-dev/neo-go/pkg/core/interop/contract"
Expand Down Expand Up @@ -173,6 +174,117 @@ func TestCall(t *testing.T) {
})
}

func TestSystemContractCall_Permissions(t *testing.T) {
check := func(t *testing.T, cfg func(*config.Blockchain), shouldUpdateFail bool) {
bc, acc := chain.NewSingleWithCustomConfig(t, cfg)
e := neotest.NewExecutor(t, bc, acc, acc)

// Contract A has an unsafe method.
srcA := `package contractA
func RetOne() int {
return 1
}`
ctrA := neotest.CompileSource(t, acc.ScriptHash(), strings.NewReader(srcA), &compiler.Options{
NoEventsCheck: true,
NoPermissionsCheck: true,
Name: "contractA",
})
e.DeployContract(t, ctrA, nil)

var hashAStr string
for i := 0; i < util.Uint160Size; i++ {
hashAStr += fmt.Sprintf("%#x", ctrA.Hash[i])
if i != util.Uint160Size-1 {
hashAStr += ", "
}
}

// Contract B has a method that calls contract's A and another update method that
// calls contract's A after B's update.
srcB := `package contractB
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/native/management"
)
func CallRetOne() int {
res := contract.Call(interop.Hash160{` + hashAStr + `}, "retOne", contract.All).(int)
return res
}
func Update(nef []byte, manifest []byte) int {
management.Update(nef, manifest)
res := contract.Call(interop.Hash160{` + hashAStr + `}, "retOne", contract.All).(int)
return res
}`
ctrB := neotest.CompileSource(t, acc.ScriptHash(), strings.NewReader(srcB), &compiler.Options{
Name: "contractB",
NoEventsCheck: true,
NoPermissionsCheck: true,
Permissions: []manifest.Permission{
{
Contract: manifest.PermissionDesc{Type: manifest.PermissionHash, Value: ctrA.Hash},
Methods: manifest.WildStrings{Value: []string{"retOne"}},
},
{
Methods: manifest.WildStrings{Value: []string{"update"}},
},
},
})
e.DeployContract(t, ctrB, nil)
ctrBInvoker := e.ValidatorInvoker(ctrB.Hash)

// ctrBUpdated differs from ctrB in that it has no permission to call retOne method of ctrA
ctrBUpdated := neotest.CompileSource(t, acc.ScriptHash(), strings.NewReader(srcB), &compiler.Options{
Name: "contractB",
NoEventsCheck: true,
NoPermissionsCheck: true,
Permissions: []manifest.Permission{
{
Contract: manifest.PermissionDesc{Type: manifest.PermissionHash, Value: ctrA.Hash},
Methods: manifest.WildStrings{Value: []string{}},
},
{
Methods: manifest.WildStrings{Value: []string{"update"}},
},
},
})

// Call to A before B update should HALT.
ctrBInvoker.Invoke(t, stackitem.Make(1), "callRetOne")

// Call to A in the same context as B update should HALT.
n, err := ctrBUpdated.NEF.Bytes()
require.NoError(t, err)
m, err := json.Marshal(ctrBUpdated.Manifest)
require.NoError(t, err)
if shouldUpdateFail {
ctrBInvoker.InvokeFail(t, "System.Contract.Call failed: disallowed method call", "update", n, m)
} else {
ctrBInvoker.Invoke(t, stackitem.Make(1), "update", n, m)
}

// If contract is updated, then all to A after B update should FAULT (manifest
// is updated, no permission to call retOne method).
if !shouldUpdateFail {
ctrBInvoker.InvokeFail(t, "System.Contract.Call failed: disallowed method call", "callRetOne")
}
}

// Pre-Domovoi behaviour: an updated contract state is used for permissions check.
check(t, func(cfg *config.Blockchain) {
cfg.Hardforks = map[string]uint32{
config.HFDomovoi.String(): 100500,
}
}, true)

// Post-Domovoi behaviour: an executing contract state is used for permissions check.
check(t, func(cfg *config.Blockchain) {
cfg.Hardforks = map[string]uint32{
config.HFDomovoi.String(): 0,
}
}, false)
}

func TestLoadToken(t *testing.T) {
bc, acc := chain.NewSingle(t)
e := neotest.NewExecutor(t, bc, acc, acc)
Expand Down

0 comments on commit dc4d5a2

Please sign in to comment.