Skip to content

Commit

Permalink
Add Gov/Distribution precompiles (#1323)
Browse files Browse the repository at this point in the history
  • Loading branch information
codchen authored Feb 7, 2024
1 parent ae189ef commit 5691dd5
Show file tree
Hide file tree
Showing 12 changed files with 655 additions and 14 deletions.
19 changes: 14 additions & 5 deletions app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -585,11 +585,6 @@ func New(
if err != nil {
panic(fmt.Sprintf("error reading EVM config due to %s", err))
}
if enableCustomEVMPrecompiles {
if err := precompiles.InitializePrecompiles(&app.EvmKeeper, app.BankKeeper, wasmkeeper.NewDefaultPermissionKeeper(app.WasmKeeper), app.WasmKeeper, stakingkeeper.NewMsgServerImpl(app.StakingKeeper)); err != nil {
panic(err)
}
}

customDependencyGenerators := aclmapping.NewCustomDependencyGenerator()
aclOpts = append(aclOpts, aclkeeper.WithDependencyGeneratorMappings(customDependencyGenerators.GetCustomDependencyGenerators(app.EvmKeeper)))
Expand Down Expand Up @@ -635,6 +630,20 @@ func New(
// this line is used by starport scaffolding # ibc/app/router
app.IBCKeeper.SetRouter(ibcRouter)

if enableCustomEVMPrecompiles {
if err := precompiles.InitializePrecompiles(
&app.EvmKeeper,
app.BankKeeper,
wasmkeeper.NewDefaultPermissionKeeper(app.WasmKeeper),
app.WasmKeeper,
stakingkeeper.NewMsgServerImpl(app.StakingKeeper),
app.GovKeeper,
app.DistrKeeper,
); err != nil {
panic(err)
}
}

/**** Module Options ****/

// NOTE: we may consider parsing `appOpts` inside module constructors. For the moment
Expand Down
11 changes: 11 additions & 0 deletions precompiles/common/expected_keepers.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (

sdk "github.com/cosmos/cosmos-sdk/types"
banktypes "github.com/cosmos/cosmos-sdk/x/bank/types"
govtypes "github.com/cosmos/cosmos-sdk/x/gov/types"
stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types"
"github.com/ethereum/go-ethereum/common"
)
Expand Down Expand Up @@ -43,3 +44,13 @@ type StakingKeeper interface {
BeginRedelegate(goCtx context.Context, msg *stakingtypes.MsgBeginRedelegate) (*stakingtypes.MsgBeginRedelegateResponse, error)
Undelegate(goCtx context.Context, msg *stakingtypes.MsgUndelegate) (*stakingtypes.MsgUndelegateResponse, error)
}

type GovKeeper interface {
AddVote(ctx sdk.Context, proposalID uint64, voterAddr sdk.AccAddress, options govtypes.WeightedVoteOptions) error
AddDeposit(ctx sdk.Context, proposalID uint64, depositorAddr sdk.AccAddress, depositAmount sdk.Coins) (bool, error)
}

type DistributionKeeper interface {
SetWithdrawAddr(ctx sdk.Context, delegatorAddr sdk.AccAddress, withdrawAddr sdk.AccAddress) error
WithdrawDelegationRewards(ctx sdk.Context, delAddr sdk.AccAddress, valAddr sdk.ValAddress) (sdk.Coins, error)
}
15 changes: 15 additions & 0 deletions precompiles/distribution/Distribution.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

address constant DISTR_PRECOMPILE_ADDRESS = 0x0000000000000000000000000000000000001007;

IDistr constant DISTR_CONTRACT = IDistr(
DISTR_PRECOMPILE_ADDRESS
);

interface IDistr {
// Transactions
function setWithdrawAddress(address withdrawAddr) external returns (bool success);

function withdrawDelegationRewards(string memory validator) external returns (bool success);
}
1 change: 1 addition & 0 deletions precompiles/distribution/abi.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
[{"inputs":[{"internalType":"address","name":"withdrawAddr","type":"address"}],"name":"setWithdrawAddress","outputs":[{"internalType":"bool","name":"success","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"string","name":"validator","type":"string"}],"name":"withdrawDelegationRewards","outputs":[{"internalType":"bool","name":"success","type":"bool"}],"stateMutability":"nonpayable","type":"function"}]
145 changes: 145 additions & 0 deletions precompiles/distribution/distribution.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
package distribution

import (
"bytes"
"embed"
"errors"

sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/ethereum/go-ethereum/accounts/abi"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/vm"
pcommon "github.com/sei-protocol/sei-chain/precompiles/common"
)

const (
SetWithdrawAddressMethod = "setWithdrawAddress"
WithdrawDelegationRewardsMethod = "withdrawDelegationRewards"
)

const (
DistrAddress = "0x0000000000000000000000000000000000001007"
)

var _ vm.PrecompiledContract = &Precompile{}

// Embed abi json file to the executable binary. Needed when importing as dependency.
//
//go:embed abi.json
var f embed.FS

func GetABI() abi.ABI {
abiBz, err := f.ReadFile("abi.json")
if err != nil {
panic(err)
}

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

type Precompile struct {
pcommon.Precompile
distrKeeper pcommon.DistributionKeeper
evmKeeper pcommon.EVMKeeper
address common.Address

SetWithdrawAddrID []byte
WithdrawDelegationRewardsID []byte
}

func NewPrecompile(distrKeeper pcommon.DistributionKeeper, evmKeeper pcommon.EVMKeeper) (*Precompile, error) {
newAbi := GetABI()

p := &Precompile{
Precompile: pcommon.Precompile{ABI: newAbi},
distrKeeper: distrKeeper,
evmKeeper: evmKeeper,
address: common.HexToAddress(DistrAddress),
}

for name, m := range newAbi.Methods {
switch name {
case SetWithdrawAddressMethod:
p.SetWithdrawAddrID = m.ID
case WithdrawDelegationRewardsMethod:
p.WithdrawDelegationRewardsID = m.ID
}
}

return p, nil
}

// RequiredGas returns the required bare minimum gas to execute the precompile.
func (p Precompile) RequiredGas(input []byte) uint64 {
methodID := input[:4]

if bytes.Equal(methodID, p.SetWithdrawAddrID) {
return 30000
} else if bytes.Equal(methodID, p.WithdrawDelegationRewardsID) {
return 50000
}
panic("unknown method")
}

func (p Precompile) Address() common.Address {
return p.address
}

func (p Precompile) Run(evm *vm.EVM, caller common.Address, input []byte) (bz []byte, err error) {
ctx, method, args, err := p.Prepare(evm, input)
if err != nil {
return nil, err
}

switch method.Name {
case SetWithdrawAddressMethod:
return p.setWithdrawAddress(ctx, method, caller, args)
case WithdrawDelegationRewardsMethod:
return p.withdrawDelegationRewards(ctx, method, caller, args)
}
return
}

func (p Precompile) setWithdrawAddress(ctx sdk.Context, method *abi.Method, caller common.Address, args []interface{}) ([]byte, error) {
pcommon.AssertArgsLength(args, 1)
delegator := p.evmKeeper.GetSeiAddressOrDefault(ctx, caller)
withdrawAddr, err := p.accAddressFromArg(ctx, args[0])
if err != nil {
return nil, err
}
err = p.distrKeeper.SetWithdrawAddr(ctx, delegator, withdrawAddr)
if err != nil {
return nil, err
}
return method.Outputs.Pack(true)
}

func (p Precompile) withdrawDelegationRewards(ctx sdk.Context, method *abi.Method, caller common.Address, args []interface{}) ([]byte, error) {
pcommon.AssertArgsLength(args, 1)
delegator := p.evmKeeper.GetSeiAddressOrDefault(ctx, caller)
validator, err := sdk.ValAddressFromBech32(args[0].(string))
if err != nil {
return nil, err
}
_, err = p.distrKeeper.WithdrawDelegationRewards(ctx, delegator, validator)
if err != nil {
return nil, err
}
return method.Outputs.Pack(true)
}

func (p Precompile) accAddressFromArg(ctx sdk.Context, arg interface{}) (sdk.AccAddress, error) {
addr := arg.(common.Address)
if addr == (common.Address{}) {
return nil, errors.New("invalid addr")
}
seiAddr, associated := p.evmKeeper.GetSeiAddress(ctx, addr)
if !associated {
return nil, errors.New("cannot use an unassociated address as withdraw address")
}
return seiAddr, nil
}
178 changes: 178 additions & 0 deletions precompiles/distribution/distribution_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
package distribution_test

import (
"encoding/hex"
"math/big"
"testing"
"time"

"github.com/cosmos/cosmos-sdk/crypto/keys/secp256k1"
crptotypes "github.com/cosmos/cosmos-sdk/crypto/types"
sdk "github.com/cosmos/cosmos-sdk/types"
slashingtypes "github.com/cosmos/cosmos-sdk/x/slashing/types"
"github.com/cosmos/cosmos-sdk/x/staking/teststaking"
stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types"
"github.com/ethereum/go-ethereum/common"
ethtypes "github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/crypto"
"github.com/sei-protocol/sei-chain/app"
"github.com/sei-protocol/sei-chain/precompiles/distribution"
"github.com/sei-protocol/sei-chain/precompiles/staking"
testkeeper "github.com/sei-protocol/sei-chain/testutil/keeper"
"github.com/sei-protocol/sei-chain/x/evm/ante"
"github.com/sei-protocol/sei-chain/x/evm/keeper"
"github.com/sei-protocol/sei-chain/x/evm/types"
evmtypes "github.com/sei-protocol/sei-chain/x/evm/types"
"github.com/sei-protocol/sei-chain/x/evm/types/ethtx"
minttypes "github.com/sei-protocol/sei-chain/x/mint/types"
"github.com/stretchr/testify/require"
tmtypes "github.com/tendermint/tendermint/proto/tendermint/types"
)

func TestWithdraw(t *testing.T) {
testApp := testkeeper.EVMTestApp
ctx := testApp.NewContext(false, tmtypes.Header{}).WithBlockHeight(2)
distrParams := testApp.DistrKeeper.GetParams(ctx)
distrParams.WithdrawAddrEnabled = true
testApp.DistrKeeper.SetParams(ctx, distrParams)
k := &testApp.EvmKeeper
valPub1 := secp256k1.GenPrivKey().PubKey()
val := setupValidator(t, ctx, testApp, stakingtypes.Unbonded, valPub1)

// delegate
abi := staking.GetABI()
args, err := abi.Pack("delegate", val.String(), big.NewInt(100))

privKey := testkeeper.MockPrivateKey()
testPrivHex := hex.EncodeToString(privKey.Bytes())
key, _ := crypto.HexToECDSA(testPrivHex)
addr := common.HexToAddress(staking.StakingAddress)
txData := ethtypes.LegacyTx{
GasPrice: big.NewInt(1000000000000),
Gas: 20000000,
To: &addr,
Value: big.NewInt(0),
Data: args,
Nonce: 0,
}
chainID := k.ChainID(ctx)
evmParams := k.GetParams(ctx)
chainCfg := evmParams.GetChainConfig()
ethCfg := chainCfg.EthereumConfig(chainID)
blockNum := big.NewInt(ctx.BlockHeight())
signer := ethtypes.MakeSigner(ethCfg, blockNum, uint64(ctx.BlockTime().Unix()))
tx, err := ethtypes.SignTx(ethtypes.NewTx(&txData), signer, key)
require.Nil(t, err)
txwrapper, err := ethtx.NewLegacyTx(tx)
require.Nil(t, err)
req, err := types.NewMsgEVMTransaction(txwrapper)
require.Nil(t, err)

_, evmAddr := testkeeper.PrivateKeyToAddresses(privKey)
amt := sdk.NewCoins(sdk.NewCoin(k.GetBaseDenom(ctx), sdk.NewInt(200000000)))
require.Nil(t, k.BankKeeper().MintCoins(ctx, evmtypes.ModuleName, sdk.NewCoins(sdk.NewCoin(k.GetBaseDenom(ctx), sdk.NewInt(200000000)))))
require.Nil(t, k.BankKeeper().SendCoinsFromModuleToAccount(ctx, evmtypes.ModuleName, evmAddr[:], amt))

msgServer := keeper.NewMsgServerImpl(k)

ante.Preprocess(ctx, req, k.GetParams(ctx))
res, err := msgServer.EVMTransaction(sdk.WrapSDKContext(ctx), req)
require.Nil(t, err)
require.Empty(t, res.VmError)

d, found := testApp.StakingKeeper.GetDelegation(ctx, evmAddr[:], val)
require.True(t, found)
require.Equal(t, int64(100), d.Shares.RoundInt().Int64())

// set withdraw addr
withdrawSeiAddr, withdrawAddr := testkeeper.MockAddressPair()
k.SetAddressMapping(ctx, withdrawSeiAddr, withdrawAddr)
abi = distribution.GetABI()
args, err = abi.Pack("setWithdrawAddress", withdrawAddr)
require.Nil(t, err)
addr = common.HexToAddress(distribution.DistrAddress)
txData = ethtypes.LegacyTx{
GasPrice: big.NewInt(1000000000000),
Gas: 20000000,
To: &addr,
Value: big.NewInt(0),
Data: args,
Nonce: 1,
}
tx, err = ethtypes.SignTx(ethtypes.NewTx(&txData), signer, key)
require.Nil(t, err)
txwrapper, err = ethtx.NewLegacyTx(tx)
require.Nil(t, err)
req, err = types.NewMsgEVMTransaction(txwrapper)
require.Nil(t, err)

ante.Preprocess(ctx, req, k.GetParams(ctx))
res, err = msgServer.EVMTransaction(sdk.WrapSDKContext(ctx), req)
require.Nil(t, err)
require.Empty(t, res.VmError)
require.Equal(t, withdrawSeiAddr.String(), testApp.DistrKeeper.GetDelegatorWithdrawAddr(ctx, evmAddr[:]).String())

// withdraw
args, err = abi.Pack("withdrawDelegationRewards", val.String())
require.Nil(t, err)
txData = ethtypes.LegacyTx{
GasPrice: big.NewInt(1000000000000),
Gas: 20000000,
To: &addr,
Value: big.NewInt(0),
Data: args,
Nonce: 2,
}
tx, err = ethtypes.SignTx(ethtypes.NewTx(&txData), signer, key)
require.Nil(t, err)
txwrapper, err = ethtx.NewLegacyTx(tx)
require.Nil(t, err)
req, err = types.NewMsgEVMTransaction(txwrapper)
require.Nil(t, err)

ante.Preprocess(ctx, req, k.GetParams(ctx))
res, err = msgServer.EVMTransaction(sdk.WrapSDKContext(ctx), req)
require.Nil(t, err)
require.Empty(t, res.VmError)

// reinitialized
d, found = testApp.StakingKeeper.GetDelegation(ctx, evmAddr[:], val)
require.True(t, found)
}

func setupValidator(t *testing.T, ctx sdk.Context, a *app.App, bondStatus stakingtypes.BondStatus, valPub crptotypes.PubKey) sdk.ValAddress {
valAddr := sdk.ValAddress(valPub.Address())
bondDenom := a.StakingKeeper.GetParams(ctx).BondDenom
selfBond := sdk.NewCoins(sdk.Coin{Amount: sdk.NewInt(100), Denom: bondDenom})

err := a.BankKeeper.MintCoins(ctx, minttypes.ModuleName, selfBond)
require.NoError(t, err)

err = a.BankKeeper.SendCoinsFromModuleToAccount(ctx, minttypes.ModuleName, sdk.AccAddress(valAddr), selfBond)
require.NoError(t, err)

sh := teststaking.NewHelper(t, ctx, a.StakingKeeper)
msg := sh.CreateValidatorMsg(valAddr, valPub, selfBond[0].Amount)
sh.Handle(msg, true)

val, found := a.StakingKeeper.GetValidator(ctx, valAddr)
require.True(t, found)

val = val.UpdateStatus(bondStatus)
a.StakingKeeper.SetValidator(ctx, val)

consAddr, err := val.GetConsAddr()
require.NoError(t, err)

signingInfo := slashingtypes.NewValidatorSigningInfo(
consAddr,
ctx.BlockHeight(),
0,
time.Unix(0, 0),
false,
0,
)
a.SlashingKeeper.SetValidatorSigningInfo(ctx, consAddr, signingInfo)

return valAddr
}
Loading

0 comments on commit 5691dd5

Please sign in to comment.