Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 10 additions & 4 deletions proto/secret/compute/v1beta1/msg.proto
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ service Msg {
// UpdateParams updates compute module params
rpc UpdateParams(MsgUpdateParams) returns (MsgUpdateParamsResponse);
rpc UpgradeProposalPassed(MsgUpgradeProposalPassed) returns (MsgUpgradeProposalPassedResponse);
rpc MigrateContractProposal(MsgMigrateContractProposal) returns (MsgMigrateContractProposalResponse);
rpc ContractGovernanceProposal(MsgContractGovernanceProposal) returns (MsgContractGovernanceProposalResponse);
rpc SetContractGovernance(MsgSetContractGovernance) returns (MsgSetContractGovernanceResponse);
}

Expand Down Expand Up @@ -222,18 +222,24 @@ message MigrateContractInfo {
uint64 new_code_id = 2;
}

message MsgMigrateContractProposal {
message UpdateAdminInfo {
string address = 1;
string new_admin = 2; // Empty string to remove admin
}

message MsgContractGovernanceProposal {
option (gogoproto.goproto_getters) = false;
option (cosmos.msg.v1.signer) = "authority";
option (amino.name) = "wasm/MsgMigrateContractProposal";
option (amino.name) = "wasm/MsgContractGovernanceProposal";

string authority = 1;
string title = 2;
string description = 3;
repeated MigrateContractInfo contracts = 4;
repeated UpdateAdminInfo admin_updates = 5; // Better field name
}

message MsgMigrateContractProposalResponse {}
message MsgContractGovernanceProposalResponse {}

message MsgSetContractGovernance {
option (gogoproto.goproto_getters) = false;
Expand Down
14 changes: 14 additions & 0 deletions proto/secret/compute/v1beta1/query.proto
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,10 @@ service Query {
rpc AuthorizedMigration(QueryAuthorizedMigrationRequest) returns (QueryAuthorizedMigrationResponse) {
option (google.api.http).get = "/compute/v1beta1/authorized_migration/{contract_address}";
}
// Query authorized admin update for a contract
rpc AuthorizedAdminUpdate(QueryAuthorizedAdminUpdateRequest) returns (QueryAuthorizedAdminUpdateResponse) {
option (google.api.http).get = "/compute/v1beta1/authorized_admin_update/{contract_address}";
}
}

// ParamsRequest is the request type for the Query/Params RPC method.
Expand Down Expand Up @@ -199,4 +203,14 @@ message QueryAuthorizedMigrationRequest {
message QueryAuthorizedMigrationResponse {
// Authorized code ID (if any)
uint64 new_code_id = 1 [ (gogoproto.customname) = "NewCodeID" ];
}

message QueryAuthorizedAdminUpdateRequest {
// Contract address to query
string contract_address = 1;
}

message QueryAuthorizedAdminUpdateResponse {
// Authorized new admin address (empty string if removing admin, or if no authorization)
string new_admin = 1;
}
32 changes: 32 additions & 0 deletions x/compute/client/cli/query.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ func GetQueryCmd() *cobra.Command {
GetCmdDecryptText(),
GetCmdGetContractHistory(),
GetCmdQueryAuthorizedMigration(),
GetCmdQueryAuthorizedAdminUpdate(),
)
return queryCmd
}
Expand Down Expand Up @@ -685,6 +686,37 @@ Examples:
return cmd
}

func GetCmdQueryAuthorizedAdminUpdate() *cobra.Command {
cmd := &cobra.Command{
Use: "authorized-admin-update [contract-address]",
Short: "Query authorized-admin-update for a contract",
Long: `Query whether a contract has an authorized admin-update set.

Examples:
# Check if the contract has authorized admin-update
secretcli query compute authorized-admin-update secret1sscrt123...`,
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
clientCtx := client.GetClientContextFromCmd(cmd)
queryClient := types.NewQueryClient(clientCtx)

contractAddr := args[0]

res, err := queryClient.AuthorizedAdminUpdate(context.Background(), &types.QueryAuthorizedAdminUpdateRequest{
ContractAddress: contractAddr,
})
if err != nil {
return err
}

return clientCtx.PrintProto(res)
},
}

flags.AddQueryFlagsToCmd(cmd)
return cmd
}

func QueryWithData(contractAddress sdk.AccAddress, queryData []byte, clientCtx client.Context) error {
wasmCtx := wasmUtils.WASMContext{CLIContext: clientCtx}

Expand Down
66 changes: 64 additions & 2 deletions x/compute/internal/keeper/keeper.go
Original file line number Diff line number Diff line change
Expand Up @@ -1684,6 +1684,19 @@ func (k Keeper) UpdateContractAdmin(ctx sdk.Context, contractAddress, caller, ne
return sdkerrors.ErrUnauthorized.Wrap("caller is not the admin")
}

// if the contract was set to require governance, check that the new admin matches
// the one set by governance
if contractInfo.RequireGovernance {
storedAdmin, found := k.GetNewAdmin(ctx, contractAddress.String())
if !found {
return sdkerrors.ErrUnauthorized.Wrap("requires governance approval for admin change")
}
if storedAdmin != newAdmin.String() {
return sdkerrors.ErrUnauthorized.Wrapf("admin mismatch: governance authorized '%s', attempting '%s'",
storedAdmin, newAdmin.String())
}
}

signBytes := []byte{}
signMode := sdktxsigning.SignMode_SIGN_MODE_UNSPECIFIED
modeInfoBytes := []byte{}
Expand Down Expand Up @@ -1734,6 +1747,10 @@ func (k Keeper) UpdateContractAdmin(ctx sdk.Context, contractAddress, caller, ne
contractInfo.AdminProof = newAdminProof
k.setContractInfo(ctx, contractAddress, &contractInfo)

if contractInfo.RequireGovernance {
k.ConsumeAdminUpdate(ctx, contractAddress.String())
}

ctx.EventManager().EmitEvent(sdk.NewEvent(
types.EventTypeUpdateContractAdmin,
sdk.NewAttribute(types.AttributeKeyContractAddr, contractAddress.String()),
Expand Down Expand Up @@ -1801,8 +1818,8 @@ func (k Keeper) Migrate(ctx sdk.Context, contractAddress sdk.AccAddress, caller
if contractInfo.Admin == "" && !contractInfo.RequireGovernance {
return nil, errorsmod.Wrap(types.ErrMigrationFailed, "contract is not upgradable")
}
codeID, found := k.GetAuthorizedMigration(ctx, contractAddress.String())
if contractInfo.RequireGovernance {
codeID, found := k.GetAuthorizedMigration(ctx, contractAddress.String())
if !found {
return nil, errorsmod.Wrap(types.ErrMigrationFailed, "requires governance approval for migration")
}
Expand Down Expand Up @@ -1871,7 +1888,7 @@ func (k Keeper) Migrate(ctx sdk.Context, contractAddress sdk.AccAddress, caller
contractInfo.CodeID = newCodeID
k.setContractInfo(ctx, contractAddress, &contractInfo)

if contractInfo.RequireGovernance && found {
if contractInfo.RequireGovernance {
k.ConsumeAuthorizedMigration(ctx, contractAddress.String())
}

Expand Down Expand Up @@ -1990,6 +2007,16 @@ func (k Keeper) SetAuthorizedMigration(ctx sdk.Context, contractAddr string, new
}
}

func (k Keeper) SetAdminUpdate(ctx sdk.Context, contractAddr string, newAdmin string) {
store := k.storeService.OpenKVStore(ctx)
key := types.GetUpdateAdminKey(contractAddr)
value := []byte(newAdmin)
err := store.Set(key, value)
if err != nil {
ctx.Logger().Error("SetAdminUpdate:", err.Error())
}
}

// Get upgrade authorization
func (k Keeper) GetAuthorizedMigration(ctx sdk.Context, contractAddr string) (uint64, bool) {
store := k.storeService.OpenKVStore(ctx)
Expand All @@ -2005,6 +2032,31 @@ func (k Keeper) GetAuthorizedMigration(ctx sdk.Context, contractAddr string) (ui
return sdk.BigEndianToUint64(bz), true
}

// GetNewAdmin returns the authorized new admin for a contract
func (k Keeper) GetNewAdmin(ctx sdk.Context, contractAddr string) (string, bool) {
store := k.storeService.OpenKVStore(ctx)
key := types.GetUpdateAdminKey(contractAddr)

// Check if authorization exists
exists, err := store.Has(key)
if err != nil {
ctx.Logger().Error("GetNewAdmin.Has:", err.Error())
return "", false
}
if !exists {
return "", false // No authorization - this is normal, not an error
}

// Get the authorized admin value
bz, err := store.Get(key)
if err != nil {
ctx.Logger().Error("GetNewAdmin.Get:", err.Error())
return "", false
}

return string(bz), true // Can be empty string (valid: remove admin)
}

// Consume (delete) authorization after use
func (k Keeper) ConsumeAuthorizedMigration(ctx sdk.Context, contractAddr string) {
store := k.storeService.OpenKVStore(ctx)
Expand All @@ -2015,6 +2067,16 @@ func (k Keeper) ConsumeAuthorizedMigration(ctx sdk.Context, contractAddr string)
}
}

// ConsumeAdminUpdate deletes admin update authorization after use
func (k Keeper) ConsumeAdminUpdate(ctx sdk.Context, contractAddr string) {
store := k.storeService.OpenKVStore(ctx)
key := types.GetUpdateAdminKey(contractAddr)
err := store.Delete(key)
if err != nil {
ctx.Logger().Error("ConsumeAdminUpdate:", err.Error())
}
}

// UpdateContractGovernanceRequirement set true to the require_governance field
func (k Keeper) SetContractGovernanceRequirement(ctx sdk.Context, contractAddr sdk.AccAddress) error {
contractInfo := k.GetContractInfo(ctx, contractAddr)
Expand Down
26 changes: 18 additions & 8 deletions x/compute/internal/keeper/msg_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,7 @@ func (m msgServer) UpgradeProposalPassed(goCtx context.Context, msg *types.MsgUp
return &types.MsgUpgradeProposalPassedResponse{}, nil
}

func (m msgServer) MigrateContractProposal(goCtx context.Context, msg *types.MsgMigrateContractProposal) (*types.MsgMigrateContractProposalResponse, error) {
func (m msgServer) ContractGovernanceProposal(goCtx context.Context, msg *types.MsgContractGovernanceProposal) (*types.MsgContractGovernanceProposalResponse, error) {
ctx := sdk.UnwrapSDKContext(goCtx)

if err := msg.ValidateBasic(); err != nil {
Expand All @@ -238,25 +238,35 @@ func (m msgServer) MigrateContractProposal(goCtx context.Context, msg *types.Msg
}

for _, contract := range msg.Contracts {
contractAddr, err := sdk.AccAddressFromBech32(contract.Address)
_, err := sdk.AccAddressFromBech32(contract.Address)
if err != nil {
return nil, errorsmod.Wrap(err, "contract")
}
// Store the authorized migration
m.keeper.SetAuthorizedMigration(ctx, contract.Address, contract.NewCodeId)
err = m.keeper.SetContractGovernanceRequirement(ctx, contractAddr)
if err != nil {
return nil, errorsmod.Wrap(err, "updating contract governance requirement")
}
ctx.EventManager().EmitEvent(sdk.NewEvent(
types.EventTypeMigrateContractProposal,
types.EventTypeContractGovernanceProposal,
sdk.NewAttribute(types.AttributeKeyContractAddr, contract.Address),
sdk.NewAttribute(types.AttributeKeyCodeID, fmt.Sprintf("%d", contract.NewCodeId)),
sdk.NewAttribute(sdk.AttributeKeySender, msg.Authority),
))
}

return &types.MsgMigrateContractProposalResponse{}, nil
for _, adminUpdate := range msg.AdminUpdates {
_, err := sdk.AccAddressFromBech32(adminUpdate.Address)
if err != nil {
return nil, errorsmod.Wrap(err, "admin update contract")
}
m.keeper.SetAdminUpdate(ctx, adminUpdate.Address, adminUpdate.NewAdmin)
ctx.EventManager().EmitEvent(sdk.NewEvent(
types.EventTypeContractGovernanceProposal,
sdk.NewAttribute(types.AttributeKeyContractAddr, adminUpdate.Address),
sdk.NewAttribute("new_admin", adminUpdate.NewAdmin),
sdk.NewAttribute(sdk.AttributeKeySender, msg.Authority),
))
}

return &types.MsgContractGovernanceProposalResponse{}, nil
}

func (m msgServer) SetContractGovernance(goCtx context.Context, msg *types.MsgSetContractGovernance) (*types.MsgSetContractGovernanceResponse, error) {
Expand Down
32 changes: 32 additions & 0 deletions x/compute/internal/keeper/querier.go
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,38 @@ func (q GrpcQuerier) AuthorizedMigration(c context.Context, req *types.QueryAuth
response := &types.QueryAuthorizedMigrationResponse{}
if hasAuth {
response.NewCodeID = codeID
} else {
return nil, status.Error(codes.NotFound, "no authorized migration found for the given contract address")
}

return response, nil
}

// AuthorizedAdminUpdate returns the authorized admin update info for a contract
func (q GrpcQuerier) AuthorizedAdminUpdate(c context.Context, req *types.QueryAuthorizedAdminUpdateRequest) (*types.QueryAuthorizedAdminUpdateResponse, error) {
if req == nil {
return nil, status.Error(codes.InvalidArgument, "invalid request")
}

if req.ContractAddress == "" {
return nil, status.Error(codes.InvalidArgument, "contract address cannot be empty")
}

ctx := sdk.UnwrapSDKContext(c)

// Validate contract address
if _, err := sdk.AccAddressFromBech32(req.ContractAddress); err != nil {
return nil, status.Error(codes.InvalidArgument, "invalid contract address")
}

// Check for authorized admin update
newAdmin, hasAuth := q.keeper.GetNewAdmin(ctx, req.ContractAddress)

response := &types.QueryAuthorizedAdminUpdateResponse{}
if hasAuth {
response.NewAdmin = newAdmin
} else {
return nil, status.Error(codes.NotFound, "no authorized admin update found for the given contract address")
}

return response, nil
Expand Down
22 changes: 11 additions & 11 deletions x/compute/internal/types/events.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,17 @@ const (
// CustomContractEventPrefix contracts can create custom events. To not mix them with other system events they got the `wasm-` prefix.
CustomContractEventPrefix = "wasm-"

EventTypeStoreCode = "store_code"
EventTypeInstantiate = "instantiate"
EventTypeExecute = "execute"
EventTypeMigrate = "migrate"
EventTypePinCode = "pin_code"
EventTypeUnpinCode = "unpin_code"
EventTypeSudo = "sudo"
EventTypeReply = "reply"
EventTypeUpdateContractAdmin = "update_contract_admin"
EventTypeUpgradeProposalPassed = "upgrade_proposal_passed"
EventTypeMigrateContractProposal = "migrate_contract_proposal"
EventTypeStoreCode = "store_code"
EventTypeInstantiate = "instantiate"
EventTypeExecute = "execute"
EventTypeMigrate = "migrate"
EventTypePinCode = "pin_code"
EventTypeUnpinCode = "unpin_code"
EventTypeSudo = "sudo"
EventTypeReply = "reply"
EventTypeUpdateContractAdmin = "update_contract_admin"
EventTypeUpgradeProposalPassed = "upgrade_proposal_passed"
EventTypeContractGovernanceProposal = "contract_governance_proposal"
)

// event attributes returned from contract execution
Expand Down
5 changes: 5 additions & 0 deletions x/compute/internal/types/keys.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,13 +35,18 @@ var (
ContractByCodeIDAndCreatedSecondaryIndexPrefix = []byte{0x0A}
ParamsKey = []byte{0x0B}
UpgradeAuthPrefix = []byte{0x0C}
UpdateAdminPrefix = []byte{0x0D}
RandomPrefix = []byte{0xFF}
ValidatorSetEvidencePrefix = []byte{0xFE}

KeyLastCodeID = append(SequenceKeyPrefix, []byte("lastCodeId")...)
KeyLastInstanceID = append(SequenceKeyPrefix, []byte("lastContractId")...)
)

func GetUpdateAdminKey(contractAddr string) []byte {
return append(UpdateAdminPrefix, []byte(contractAddr)...)
}

// GetUpgradeAuthKey creates the key for upgrade authorization storage
func GetUpgradeAuthKey(contractAddr string) []byte {
return append(UpgradeAuthPrefix, []byte(contractAddr)...)
Expand Down
Loading
Loading