Skip to content

Commit

Permalink
feat(wasm): add contract access control (#151)
Browse files Browse the repository at this point in the history
* add permission param `ContractStatusAccess`

* add `ContractStatus` type and field
  - add an enum `ContractStatus`
  - add a field `status` into ContractInfo

* add msg `MsgUpdateContractStatus`
  - add a msg `MsgUpdateContractStatus`
  - add handler for it

* reject inactive smart contract call

* add changlog entry to `CHANGELOG_PENDING.md`

* use spaces rather than tabs for json tamplate
  - in function `TestImportContractWithCodeHistoryReset` of `genesis_test.go`

* Add comments for GovAuthorizationPolicy

* Add gov proposal for update contract status

* Add proposal handler for updating contract status
  • Loading branch information
Yongwoo Lee authored May 3, 2021
1 parent c4bf377 commit 62843eb
Show file tree
Hide file tree
Showing 29 changed files with 1,953 additions and 244 deletions.
1 change: 1 addition & 0 deletions CHANGELOG_PENDING.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ Write the changes made after branching from cosmos-sdk v0.42.1.
* (global) [\#97](https://github.com/line/lbm-sdk/pull/97) Add codespace to query error
* (config) [\#114](https://github.com/line/lbm-sdk/pull/114) Add idle-timeout to rest server and rpc server config
* (x/wasm) [\#127](https://github.com/line/lbm-sdk/pull/127) Add wasm with Staragate migration completed.
* (x/wasm) [\#151](https://github.com/line/lbm-sdk/pull/151) Add contract access control.

### IMPROVEMENTS

Expand Down
32 changes: 32 additions & 0 deletions x/wasm/client/cli/new_tx.go
Original file line number Diff line number Diff line change
Expand Up @@ -113,3 +113,35 @@ func ClearContractAdminCmd() *cobra.Command {
flags.AddTxFlagsToCmd(cmd)
return cmd
}

// UpdateContractStatusCmd clears an admin for a contract
func UpdateContractStatusCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "update-contract-status [contract_addr_bech32] [status(Active|Inactive)]",
Short: "Clears admin for a contract to prevent further migrations",
Args: cobra.ExactArgs(2),
RunE: func(cmd *cobra.Command, args []string) error {
clientCtx, err := client.GetClientTxContext(cmd)
if err != nil {
return err
}

status := types.ContractStatusUnspecified
if err := (&status).UnmarshalText([]byte(args[1])); err != nil {
return err
}

msg := types.MsgUpdateContractStatus{
Sender: clientCtx.GetFromAddress().String(),
Contract: args[0],
Status: status,
}
if err := msg.ValidateBasic(); err != nil {
return err
}
return tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), &msg)
},
}
flags.AddTxFlagsToCmd(cmd)
return cmd
}
1 change: 1 addition & 0 deletions x/wasm/client/cli/tx.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ func GetTxCmd() *cobra.Command {
MigrateContractCmd(),
UpdateContractAdminCmd(),
ClearContractAdminCmd(),
UpdateContractStatusCmd(),
)
return txCmd
}
Expand Down
2 changes: 2 additions & 0 deletions x/wasm/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ func NewHandler(k *Keeper) sdk.Handler {
res, err = msgServer.UpdateAdmin(sdk.WrapSDKContext(ctx), msg)
case *MsgClearAdmin:
res, err = msgServer.ClearAdmin(sdk.WrapSDKContext(ctx), msg)
case *types.MsgUpdateContractStatus:
res, err = msgServer.UpdateContractStatus(sdk.WrapSDKContext(ctx), msg)
default:
errMsg := fmt.Sprintf("unrecognized wasm message type: %T", msg)
return nil, sdkerrors.Wrap(sdkerrors.ErrUnknownRequest, errMsg)
Expand Down
14 changes: 14 additions & 0 deletions x/wasm/internal/keeper/authz_policy.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ type AuthorizationPolicy interface {
CanCreateCode(c types.AccessConfig, creator sdk.AccAddress) bool
CanInstantiateContract(c types.AccessConfig, actor sdk.AccAddress) bool
CanModifyContract(admin, actor sdk.AccAddress) bool
CanUpdateContractStatus(c types.AccessConfig, actor sdk.AccAddress) bool
}

type DefaultAuthorizationPolicy struct {
Expand All @@ -26,17 +27,30 @@ func (p DefaultAuthorizationPolicy) CanModifyContract(admin, actor sdk.AccAddres
return admin != nil && admin.Equals(actor)
}

func (p DefaultAuthorizationPolicy) CanUpdateContractStatus(config types.AccessConfig, actor sdk.AccAddress) bool {
return config.Allowed(actor)
}

// GovAuthorizationPolicy is for the gov handler(proposal_handler.go) authorities
type GovAuthorizationPolicy struct {
}

func (p GovAuthorizationPolicy) CanCreateCode(types.AccessConfig, sdk.AccAddress) bool {
// The gov handler can create code regardless of the current access config
return true
}

func (p GovAuthorizationPolicy) CanInstantiateContract(types.AccessConfig, sdk.AccAddress) bool {
// The gov handler can instantiate contract regardless of the code access config
return true
}

func (p GovAuthorizationPolicy) CanModifyContract(sdk.AccAddress, sdk.AccAddress) bool {
// The gov handler can migrate contract regardless of the contract admin
return true
}

func (p GovAuthorizationPolicy) CanUpdateContractStatus(types.AccessConfig, sdk.AccAddress) bool {
// The gov handler can update contract status regardless of the current access config
return true
}
7 changes: 6 additions & 1 deletion x/wasm/internal/keeper/genesis_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -444,6 +444,9 @@ func TestImportContractWithCodeHistoryReset(t *testing.T) {
"permission": "Everybody"
},
"instantiate_default_permission": "Everybody",
"contract_status_access": {
"permission": "Nobody"
},
"max_wasm_code_size": 500000,
"gas_multiplier": 100,
"instance_cost": 40000,
Expand Down Expand Up @@ -472,7 +475,8 @@ func TestImportContractWithCodeHistoryReset(t *testing.T) {
"code_id": "1",
"creator": "link1p0yx9c9q4xsnedlcn24gqfry5dcu6e9xkhv9aj",
"admin": "link1qyqszqgpqyqszqgpqyqszqgpqyqszqgp8apuk5",
"label": "ȀĴnZV芢毤"
"label": "ȀĴnZV芢毤",
"status": "Active"
}
}
],
Expand Down Expand Up @@ -537,6 +541,7 @@ func TestImportContractWithCodeHistoryReset(t *testing.T) {
Admin: adminAddr,
Label: "ȀĴnZV芢毤",
Created: &types.AbsoluteTxPosition{BlockHeight: 0, TxIndex: 0},
Status: wasmTypes.ContractStatusActive,
}
assert.Equal(t, expContractInfo, *gotContractInfo)

Expand Down
38 changes: 37 additions & 1 deletion x/wasm/internal/keeper/keeper.go
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,12 @@ func (k Keeper) getInstantiateAccessConfig(ctx sdk.Context) types.AccessType {
return a
}

func (k Keeper) getContractStatusAccessConfig(ctx sdk.Context) types.AccessConfig {
var a types.AccessConfig
k.paramSpace.Get(ctx, types.ParamStoreKeyContractStatusAccess, &a)
return a
}

func (k Keeper) getMaxWasmCodeSize(ctx sdk.Context) uint64 {
var a uint64
k.paramSpace.Get(ctx, types.ParamStoreKeyMaxWasmCodeSize, &a)
Expand Down Expand Up @@ -289,7 +295,7 @@ func (k Keeper) instantiate(ctx sdk.Context, codeID uint64, creator, admin sdk.A

// persist instance first
createdAt := types.NewAbsoluteTxPosition(ctx)
contractInfo := types.NewContractInfo(codeID, creator, admin, label, createdAt)
contractInfo := types.NewContractInfo(codeID, creator, admin, label, createdAt, types.ContractStatusActive)

// check for IBC flag
report, err := k.wasmer.AnalyzeCode(codeInfo.CodeHash)
Expand Down Expand Up @@ -324,6 +330,9 @@ func (k Keeper) Execute(ctx sdk.Context, contractAddress sdk.AccAddress, caller
if err != nil {
return nil, err
}
if contractInfo.Status != types.ContractStatusActive {
return nil, sdkerrors.Wrap(types.ErrInvalid, "inactive contract")
}

if !k.IsPinnedCode(ctx, contractInfo.CodeID) {
ctx.GasMeter().ConsumeGas(k.getInstanceCost(ctx), "Loading CosmWasm module: execute")
Expand Down Expand Up @@ -377,6 +386,9 @@ func (k Keeper) migrate(ctx sdk.Context, contractAddress sdk.AccAddress, caller
if contractInfo == nil {
return nil, sdkerrors.Wrap(sdkerrors.ErrInvalidRequest, "unknown contract")
}
if contractInfo.Status != types.ContractStatusActive {
return nil, sdkerrors.Wrap(types.ErrInvalid, "inactive contract")
}
if !authZ.CanModifyContract(contractInfo.AdminAddr(), caller) {
return nil, sdkerrors.Wrap(sdkerrors.ErrUnauthorized, "can not migrate")
}
Expand Down Expand Up @@ -534,11 +546,35 @@ func (k Keeper) ClearContractAdmin(ctx sdk.Context, contractAddress sdk.AccAddre
return k.setContractAdmin(ctx, contractAddress, caller, nil, k.authZPolicy)
}

// UpdateContractStatus sets a new status of the contract on the ContractInfo.
func (k Keeper) UpdateContractStatus(ctx sdk.Context, contractAddress sdk.AccAddress, caller sdk.AccAddress, status types.ContractStatus) error {
return k.updateContractStatus(ctx, contractAddress, caller, status, k.authZPolicy)
}

func (k Keeper) updateContractStatus(ctx sdk.Context, contractAddress sdk.AccAddress, caller sdk.AccAddress, status types.ContractStatus, authZ AuthorizationPolicy) error {
if !authZ.CanUpdateContractStatus(k.getContractStatusAccessConfig(ctx), caller) {
return sdkerrors.Wrap(sdkerrors.ErrUnauthorized, "can not update contract status")
}

contractInfo := k.GetContractInfo(ctx, contractAddress)
if contractInfo == nil {
return sdkerrors.Wrap(sdkerrors.ErrInvalidRequest, "unknown contract")
}
if contractInfo.Status != status {
contractInfo.Status = status
k.storeContractInfo(ctx, contractAddress, contractInfo)
}
return nil
}

func (k Keeper) setContractAdmin(ctx sdk.Context, contractAddress, caller, newAdmin sdk.AccAddress, authZ AuthorizationPolicy) error {
contractInfo := k.GetContractInfo(ctx, contractAddress)
if contractInfo == nil {
return sdkerrors.Wrap(sdkerrors.ErrInvalidRequest, "unknown contract")
}
if contractInfo.Status != types.ContractStatusActive {
return sdkerrors.Wrap(types.ErrInvalid, "inactive contract")
}
if !authZ.CanModifyContract(contractInfo.AdminAddr(), caller) {
return sdkerrors.Wrap(sdkerrors.ErrUnauthorized, "can not modify contract")
}
Expand Down
Loading

0 comments on commit 62843eb

Please sign in to comment.