Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add precompiled staking contract for EVM #19

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
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: 13 additions & 1 deletion app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ import (
ibckeeper "github.com/cosmos/ibc-go/v8/modules/core/keeper"
solomachine "github.com/cosmos/ibc-go/v8/modules/light-clients/06-solomachine"
ibctm "github.com/cosmos/ibc-go/v8/modules/light-clients/07-tendermint"
ethparams "github.com/ethereum/go-ethereum/params"
srvflags "github.com/evmos/ethermint/server/flags"
ethermint "github.com/evmos/ethermint/types"
"github.com/evmos/ethermint/x/evm"
Expand All @@ -134,8 +135,10 @@ import (
"github.com/Galactica-corp/galactica/docs"
runtimeservices "github.com/cosmos/cosmos-sdk/runtime/services"

stakingprecompile "github.com/Galactica-corp/galactica/precompiles/staking"
capabilitytypes "github.com/cosmos/ibc-go/modules/capability/types"
icatypes "github.com/cosmos/ibc-go/v8/modules/apps/27-interchain-accounts/types"
"github.com/ethereum/go-ethereum/core/vm"
)

const (
Expand Down Expand Up @@ -463,6 +466,11 @@ func New(
)
app.FeeMarketKeeper = &feeMarketKeeper

stakingPrecompile, err := stakingprecompile.NewPrecompile(*app.StakingKeeper, app.AuthzKeeper)
if err != nil {
panic(fmt.Errorf("failed to load staking precompile: %w", err))
}

// Set authority to x/gov module account to only expect the module account to update params
evmS := app.GetSubspace(evmtypes.ModuleName)
app.EvmKeeper = evmkeeper.NewKeeper(
Expand All @@ -472,7 +480,11 @@ func New(
app.AccountKeeper, app.BankKeeper, app.StakingKeeper, app.FeeMarketKeeper,
tracer,
evmS,
nil,
[]evmkeeper.CustomContractFn{
func(_ sdk.Context, rules ethparams.Rules) vm.PrecompiledContract {
return stakingPrecompile
},
},
)

app.CapabilityKeeper = capabilitykeeper.NewKeeper(app.appCodec, capKVStoreKey, capKVMemKey)
Expand Down
60 changes: 60 additions & 0 deletions precompiles/staking/abi.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
[
{
"inputs": [
{
"internalType": "address",
"name": "delegatorAddress",
"type": "address"
},
{
"internalType": "string",
"name": "validatorAddress",
"type": "string"
},
{
"internalType": "uint256",
"name": "amount",
"type": "uint256"
}
],
"name": "delegate",
"outputs": [
{
"internalType": "bool",
"name": "success",
"type": "bool"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "delegatorAddress",
"type": "address"
},
{
"internalType": "string",
"name": "validatorAddress",
"type": "string"
},
{
"internalType": "uint256",
"name": "amount",
"type": "uint256"
}
],
"name": "undelegate",
"outputs": [
{
"internalType": "int64",
"name": "completionTime",
"type": "int64"
}
],
"stateMutability": "nonpayable",
"type": "function"
}
]
186 changes: 186 additions & 0 deletions precompiles/staking/staking.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
// Copyright Tharsis Labs Ltd.(Evmos)
// SPDX-License-Identifier:ENCL-1.0(https://github.com/evmos/evmos/blob/main/LICENSE)

package staking

import (
"bytes"
"embed"
"fmt"

sdk "github.com/cosmos/cosmos-sdk/types"

"cosmossdk.io/log"
storetypes "cosmossdk.io/store/types"
authzkeeper "github.com/cosmos/cosmos-sdk/x/authz/keeper"
stakingkeeper "github.com/cosmos/cosmos-sdk/x/staking/keeper"
"github.com/ethereum/go-ethereum/accounts/abi"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/vm"
"github.com/evmos/ethermint/x/evm/statedb"
)

var (
stakingContractAddress = common.BytesToAddress([]byte{100})
)

type (
ErrorOutOfGas = storetypes.ErrorOutOfGas
ErrorGasOverflow = storetypes.ErrorGasOverflow
)

const (
ErrDecreaseAmountTooBig = "amount by which the allowance should be decreased is greater than the authorization limit: %s > %s"
ErrDifferentOriginFromDelegator = "origin address %s is not the same as delegator address %s"
ErrNoDelegationFound = "delegation with delegator %s not found for validator %s"
)

var _ vm.PrecompiledContract = &Precompile{}

//go:embed abi.json
var f embed.FS

type Precompile struct {
abi abi.ABI
AuthzKeeper authzkeeper.Keeper
stakingKeeper stakingkeeper.Keeper
addressContract common.Address
kvGasConfig storetypes.GasConfig
transientKVGasConfig storetypes.GasConfig
}

// TODO
func (p Precompile) RequiredGas(input []byte) uint64 {
return 0
}

func NewPrecompile(
stakingKeeper stakingkeeper.Keeper,
authzKeeper authzkeeper.Keeper,
) (*Precompile, error) {
abiBz, err := f.ReadFile("abi.json")
if err != nil {
return nil, fmt.Errorf("error loading the staking ABI %s", err)
}

newAbi, err := abi.JSON(bytes.NewReader(abiBz))
if err != nil {
return nil, err
}

return &Precompile{
stakingKeeper: stakingKeeper,
AuthzKeeper: authzKeeper,
abi: newAbi,
addressContract: stakingContractAddress,
kvGasConfig: storetypes.KVGasConfig(),
transientKVGasConfig: storetypes.TransientGasConfig(),
}, nil
}

func (Precompile) Address() common.Address {
return stakingContractAddress
}

func (p Precompile) Run(evm *vm.EVM, contract *vm.Contract, readOnly bool) (bz []byte, err error) {
ctx, stateDB, method, initialGas, args, err := p.RunSetup(evm, contract, readOnly, p.IsTransaction)
if err != nil {
return nil, err
}

if err := stateDB.Commit(); err != nil {
return nil, err
}

switch method.Name {
case DelegateMethod:
bz, err = p.Delegate(ctx, evm.Origin, contract, stateDB, method, args)
case UndelegateMethod:
bz, err = p.Undelegate(ctx, evm.Origin, contract, stateDB, method, args)
}

if err != nil {
return nil, err
}

cost := ctx.GasMeter().GasConsumed() - initialGas

if !contract.UseGas(cost) {
return nil, vm.ErrOutOfGas
}

return bz, nil
}

func (Precompile) IsTransaction(method string) bool {
switch method {
case DelegateMethod,
UndelegateMethod:
return true
default:
return false
}
}

func (p Precompile) Logger(ctx sdk.Context) log.Logger {
return ctx.Logger().With("evm extension", fmt.Sprintf("x/%s", "staking"))
}

func (p Precompile) RunSetup(
evm *vm.EVM,
contract *vm.Contract,
readOnly bool,
isTransaction func(name string) bool,
) (ctx sdk.Context, stateDB *statedb.StateDB, method *abi.Method, gasConfig storetypes.Gas, args []interface{}, err error) {
stateDB, ok := evm.StateDB.(*statedb.StateDB)
if !ok {
return sdk.Context{}, nil, nil, uint64(0), nil, fmt.Errorf(ErrNotRunInEvm)
}
ctx = stateDB.Context()

methodID := contract.Input[:4]
method, err = p.abi.MethodById(methodID)
if err != nil {
return sdk.Context{}, nil, nil, uint64(0), nil, err
}

// return error if trying to write to state during a read-only call
if readOnly && isTransaction(method.Name) {
return sdk.Context{}, nil, nil, uint64(0), nil, vm.ErrWriteProtection
}

argsBz := contract.Input[4:]
args, err = method.Inputs.Unpack(argsBz)
if err != nil {
return sdk.Context{}, nil, nil, uint64(0), nil, err
}

initialGas := ctx.GasMeter().GasConsumed()

defer HandleGasError(ctx, contract, initialGas, &err)()

ctx = ctx.WithGasMeter(storetypes.NewGasMeter(contract.Gas)).WithKVGasConfig(p.kvGasConfig).
WithTransientKVGasConfig(p.transientKVGasConfig)

ctx.GasMeter().ConsumeGas(initialGas, "creating a new gas meter")

return ctx, stateDB, method, initialGas, args, nil
}

func HandleGasError(ctx sdk.Context, contract *vm.Contract, initialGas storetypes.Gas, err *error) func() {
return func() {
if r := recover(); r != nil {
switch r.(type) {
case ErrorOutOfGas:
usedGas := ctx.GasMeter().GasConsumed() - initialGas
_ = contract.UseGas(usedGas)

*err = vm.ErrOutOfGas
ctx = ctx.WithKVGasConfig(storetypes.GasConfig{}).
WithTransientKVGasConfig(storetypes.GasConfig{})
default:
panic(r)
}
}
}
}
Loading