Skip to content

Commit

Permalink
feat: staking precompile (#2784)
Browse files Browse the repository at this point in the history
* add boilerplate for staking precompile with empty delegate method

* add simple undelegate and redelegate methods

* add new methods to run

* add simple tests

* add delegator caller checks

* fix del addr origin check

* add unit tests for delegate method

* changelog

* add undelegate tests

* add redelegate unit tests

* renamings

* generate

* pr comments

* codecov

* renaming

* e2e test and fixes

* generate

* fix tests

* fix view methods required gas

* add queries unit tests

* add more unit tests and refactor a bit

* fmt

* fixes after merge

* cleanup

* PR comments

* add todo with issue

* PR comment and bug fix with validators append
  • Loading branch information
skosito authored Sep 5, 2024
1 parent ebeedfc commit 274c1bf
Show file tree
Hide file tree
Showing 21 changed files with 2,341 additions and 20 deletions.
7 changes: 6 additions & 1 deletion app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -567,7 +567,12 @@ func New(
&app.FeeMarketKeeper,
tracer,
evmSs,
precompiles.StatefulContracts(&app.FungibleKeeper, appCodec, storetypes.TransientGasConfig()),
precompiles.StatefulContracts(
&app.FungibleKeeper,
app.StakingKeeper,
appCodec,
storetypes.TransientGasConfig(),
),
app.ConsensusParamsKeeper,
aggregateAllKeys(keys, tKeys, memKeys),
)
Expand Down
3 changes: 2 additions & 1 deletion changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,9 @@
* [2681](https://github.com/zeta-chain/node/pull/2681) - implement `MsgUpdateERC20CustodyPauseStatus` to pause or unpause ERC20 Custody contract (to be used for the migration process for smart contract V2)
* [2644](https://github.com/zeta-chain/node/pull/2644) - add created_timestamp to cctx status
* [2673](https://github.com/zeta-chain/node/pull/2673) - add relayer key importer, encryption and decryption
* [2633](https://github.com/zeta-chain/node/pull/2633) - support for stateful precompiled contracts.
* [2633](https://github.com/zeta-chain/node/pull/2633) - support for stateful precompiled contracts
* [2788](https://github.com/zeta-chain/node/pull/2788) - add common importable zetacored rpc package
* [2784](https://github.com/zeta-chain/node/pull/2784) - staking precompiled contract

### Refactor

Expand Down
3 changes: 2 additions & 1 deletion cmd/zetae2e/local/local.go
Original file line number Diff line number Diff line change
Expand Up @@ -309,7 +309,8 @@ func localE2ETest(cmd *cobra.Command, _ []string) {

if !skipPrecompiles {
precompiledContractTests = []string{
e2etests.TestZetaPrecompilesPrototypeName,
e2etests.TestPrecompilesPrototypeName,
e2etests.TestPrecompilesStakingName,
}
}

Expand Down
1 change: 1 addition & 0 deletions codecov.yml
Original file line number Diff line number Diff line change
Expand Up @@ -81,3 +81,4 @@ ignore:
- "precompiles/**/*.json"
- "precompiles/**/*.sol"
- "precompiles/prototype/IPrototype.go"
- "precompiles/staking/IStaking.go"
5 changes: 5 additions & 0 deletions contrib/localnet/orchestrator/start-zetae2e.sh
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,11 @@ fund_eth_from_config '.additional_accounts.user_v2_ether_revert.evm_address' 100
# unlock v2 erc20 revert tests accounts
fund_eth_from_config '.additional_accounts.user_v2_erc20_revert.evm_address' 10000 "V2 ERC20 revert tester"

# unlock precompile tests accounts
address=$(yq -r '.additional_accounts.user_precompile.evm_address' config.yml)
echo "funding precompile tester address ${address} with 10000 Ether"
geth --exec "eth.sendTransaction({from: eth.coinbase, to: '${address}', value: web3.toWei(10000,'ether')})" attach http://eth:8545 > /dev/null

# unlock local solana relayer accounts
if host solana > /dev/null; then
solana_url=$(config_str '.rpcs.solana')
Expand Down
3 changes: 3 additions & 0 deletions contrib/localnet/scripts/start-zetacored.sh
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,9 @@ then
# migration tester
address=$(yq -r '.additional_accounts.user_migration.bech32_address' /root/config.yml)
zetacored add-genesis-account "$address" 100000000000000000000000000azeta
# precompiles tester
address=$(yq -r '.additional_accounts.user_precompile.bech32_address' /root/config.yml)
zetacored add-genesis-account "$address" 100000000000000000000000000azeta
# v2 ether tester
address=$(yq -r '.additional_accounts.user_v2_ether.bech32_address' /root/config.yml)
zetacored add-genesis-account "$address" 100000000000000000000000000azeta
Expand Down
13 changes: 10 additions & 3 deletions e2e/e2etests/e2etests.go
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,8 @@ const (
/*
Stateful precompiled contracts tests
*/
TestZetaPrecompilesPrototypeName = "precompile_contracts_prototype"
TestPrecompilesPrototypeName = "precompile_contracts_prototype"
TestPrecompilesStakingName = "precompile_contracts_staking"
)

// AllE2ETests is an ordered list of all e2e tests
Expand Down Expand Up @@ -814,9 +815,15 @@ var AllE2ETests = []runner.E2ETest{
Stateful precompiled contracts tests
*/
runner.NewE2ETest(
TestZetaPrecompilesPrototypeName,
TestPrecompilesPrototypeName,
"test stateful precompiled contracts prototype",
[]runner.ArgDefinition{},
TestPrecompilesRegular,
TestPrecompilesPrototype,
),
runner.NewE2ETest(
TestPrecompilesStakingName,
"test stateful precompiled contracts staking",
[]runner.ArgDefinition{},
TestPrecompilesStaking,
),
}
2 changes: 1 addition & 1 deletion e2e/e2etests/test_precompiles_prototype.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import (
"github.com/zeta-chain/node/precompiles/prototype"
)

func TestPrecompilesRegular(r *runner.E2ERunner, args []string) {
func TestPrecompilesPrototype(r *runner.E2ERunner, args []string) {
require.Len(r, args, 0, "No arguments expected")

iPrototype, err := prototype.NewIPrototype(prototype.ContractAddress, r.ZEVMClient)
Expand Down
79 changes: 79 additions & 0 deletions e2e/e2etests/test_precompiles_staking.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package e2etests

import (
"math/big"

"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/stretchr/testify/require"

"github.com/zeta-chain/node/e2e/runner"
"github.com/zeta-chain/node/e2e/utils"
"github.com/zeta-chain/node/precompiles/staking"
)

func TestPrecompilesStaking(r *runner.E2ERunner, args []string) {
require.Len(r, args, 0, "No arguments expected")

stakingContract, err := staking.NewIStaking(staking.ContractAddress, r.ZEVMClient)
require.NoError(r, err, "Failed to create staking contract caller")

previousGasLimit := r.ZEVMAuth.GasLimit
r.ZEVMAuth.GasLimit = 10000000
defer func() {
r.ZEVMAuth.GasLimit = previousGasLimit
}()

validators, err := stakingContract.GetAllValidators(&bind.CallOpts{})
require.NoError(r, err)
require.GreaterOrEqual(r, len(validators), 2)

// shares are 0 for both validators at the start
sharesBeforeVal1, err := stakingContract.GetShares(&bind.CallOpts{}, r.ZEVMAuth.From, validators[0].OperatorAddress)
require.NoError(r, err)
require.Equal(r, int64(0), sharesBeforeVal1.Int64())

sharesBeforeVal2, err := stakingContract.GetShares(&bind.CallOpts{}, r.ZEVMAuth.From, validators[1].OperatorAddress)
require.NoError(r, err)
require.Equal(r, int64(0), sharesBeforeVal2.Int64())

// stake 3 to validator1
tx, err := stakingContract.Stake(r.ZEVMAuth, r.ZEVMAuth.From, validators[0].OperatorAddress, big.NewInt(3))
require.NoError(r, err)
utils.MustWaitForTxReceipt(r.Ctx, r.ZEVMClient, tx, r.Logger, r.ReceiptTimeout)

// check shares are set to 3
sharesAfterVal1, err := stakingContract.GetShares(&bind.CallOpts{}, r.ZEVMAuth.From, validators[0].OperatorAddress)
require.NoError(r, err)
require.Equal(r, big.NewInt(3e18).String(), sharesAfterVal1.String())

// unstake 1 from validator1
tx, err = stakingContract.Unstake(r.ZEVMAuth, r.ZEVMAuth.From, validators[0].OperatorAddress, big.NewInt(1))
require.NoError(r, err)
utils.MustWaitForTxReceipt(r.Ctx, r.ZEVMClient, tx, r.Logger, r.ReceiptTimeout)

// check shares are set to 2
sharesAfterVal1, err = stakingContract.GetShares(&bind.CallOpts{}, r.ZEVMAuth.From, validators[0].OperatorAddress)
require.NoError(r, err)
require.Equal(r, big.NewInt(2e18).String(), sharesAfterVal1.String())

// move 1 stake from validator1 to validator2
tx, err = stakingContract.MoveStake(
r.ZEVMAuth,
r.ZEVMAuth.From,
validators[0].OperatorAddress,
validators[1].OperatorAddress,
big.NewInt(1),
)
require.NoError(r, err)

utils.MustWaitForTxReceipt(r.Ctx, r.ZEVMClient, tx, r.Logger, r.ReceiptTimeout)

// check shares for both validator1 and validator2 are 1
sharesAfterVal1, err = stakingContract.GetShares(&bind.CallOpts{}, r.ZEVMAuth.From, validators[0].OperatorAddress)
require.NoError(r, err)
require.Equal(r, big.NewInt(1e18).String(), sharesAfterVal1.String())

sharesAfterVal2, err := stakingContract.GetShares(&bind.CallOpts{}, r.ZEVMAuth.From, validators[1].OperatorAddress)
require.NoError(r, err)
require.Equal(r, big.NewInt(1e18).String(), sharesAfterVal2.String())
}
22 changes: 18 additions & 4 deletions precompiles/precompiles.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,40 +4,54 @@ import (
"github.com/cosmos/cosmos-sdk/codec"
storetypes "github.com/cosmos/cosmos-sdk/store/types"
sdktypes "github.com/cosmos/cosmos-sdk/types"
stakingkeeper "github.com/cosmos/cosmos-sdk/x/staking/keeper"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/vm"
ethparams "github.com/ethereum/go-ethereum/params"
evmkeeper "github.com/zeta-chain/ethermint/x/evm/keeper"

"github.com/zeta-chain/node/precompiles/prototype"
"github.com/zeta-chain/node/x/fungible/keeper"
"github.com/zeta-chain/node/precompiles/staking"
fungiblekeeper "github.com/zeta-chain/node/x/fungible/keeper"
)

// EnabledStatefulContracts contains the list of all enabled stateful precompiles.
// This is useful for listing and reading from other packages, such as BlockedAddrs() function.
// Setting to false a contract here will disable it, not being included in the blockchain.
var EnabledStatefulContracts = map[common.Address]bool{
prototype.ContractAddress: true,
staking.ContractAddress: true,
}

// StatefulContracts returns all the registered precompiled contracts.
func StatefulContracts(
fungibleKeeper *keeper.Keeper,
fungibleKeeper *fungiblekeeper.Keeper,
stakingKeeper *stakingkeeper.Keeper,
cdc codec.Codec,
gasConfig storetypes.GasConfig,
) (precompiledContracts []evmkeeper.CustomContractFn) {
// Initialize at 0 the custom compiled contracts and the addresses.
precompiledContracts = make([]evmkeeper.CustomContractFn, 0)

// Define the regular contract function.
// Define the prototype contract function.
if EnabledStatefulContracts[prototype.ContractAddress] {
prototypeContract := func(_ sdktypes.Context, _ ethparams.Rules) vm.PrecompiledContract {
return prototype.NewIPrototypeContract(fungibleKeeper, cdc, gasConfig)
}

// Append the regular contract to the precompiledContracts slice.
// Append the prototype contract to the precompiledContracts slice.
precompiledContracts = append(precompiledContracts, prototypeContract)
}

// Define the staking contract function.
if EnabledStatefulContracts[staking.ContractAddress] {
stakingContract := func(_ sdktypes.Context, _ ethparams.Rules) vm.PrecompiledContract {
return staking.NewIStakingContract(stakingKeeper, cdc, gasConfig)
}

// Append the staking contract to the precompiledContracts slice.
precompiledContracts = append(precompiledContracts, stakingContract)
}

return precompiledContracts
}
17 changes: 9 additions & 8 deletions precompiles/precompiles_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import (
)

func Test_StatefulContracts(t *testing.T) {
k, ctx, _, _ := keeper.FungibleKeeper(t)
k, ctx, sdkk, _ := keeper.FungibleKeeper(t)
gasConfig := storetypes.TransientGasConfig()

var encoding ethermint.EncodingConfig
Expand All @@ -25,15 +25,16 @@ func Test_StatefulContracts(t *testing.T) {
}

// StatefulContracts() should return all the enabled contracts.
contracts := StatefulContracts(k, appCodec, gasConfig)
contracts := StatefulContracts(k, &sdkk.StakingKeeper, appCodec, gasConfig)
require.NotNil(t, contracts, "StatefulContracts() should not return a nil slice")
require.Len(t, contracts, expectedContracts, "StatefulContracts() should return all the enabled contracts")

// Extract the contract function from the first contract.
customContractFn := contracts[0]
contract := customContractFn(ctx, ethparams.Rules{})
for _, customContractFn := range contracts {
// Extract the contract function.
contract := customContractFn(ctx, ethparams.Rules{})

// Check the contract function returns a valid address.
contractAddr := contract.Address()
require.NotNil(t, contractAddr, "The called contract should have a valid address")
// Check the contract function returns a valid address.
contractAddr := contract.Address()
require.NotNil(t, contractAddr, "The called contract should have a valid address")
}
}
1 change: 1 addition & 0 deletions precompiles/prototype/prototype.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ func initABI() {
var methodID [4]byte
copy(methodID[:], ABI.Methods[methodName].ID[:4])
switch methodName {
// TODO: https://github.com/zeta-chain/node/issues/2812
case Bech32ToHexAddrMethodName:
GasRequiredByMethod[methodID] = 500
case Bech32ifyMethodName:
Expand Down
1 change: 0 additions & 1 deletion precompiles/prototype/prototype_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -296,7 +296,6 @@ func Test_GetGasStabilityPoolBalance(t *testing.T) {
)

t.Run("should fail with invalid arguments", func(t *testing.T) {

t.Run("invalid number of arguments", func(t *testing.T) {
args := []interface{}{int64(1337), "second argument"}
_, err := contract.GetGasStabilityPoolBalance(ctx, &methodID, args)
Expand Down
Loading

0 comments on commit 274c1bf

Please sign in to comment.