diff --git a/giga/evmonly/README.md b/giga/evmonly/README.md index 928a203ce3..e0445932f5 100644 --- a/giga/evmonly/README.md +++ b/giga/evmonly/README.md @@ -15,9 +15,10 @@ The target execution model is based on the `sei-v3` executor: The current implementation executes raw RLP transactions with go-ethereum against an EVM-native state backend, then returns a changeset plus Ethereum -receipts. Custom precompiles are still placeholders. The open work is to port -them behind an EVM-native context that is visible to the executor's conflict -tracking without reintroducing Cosmos keeper dependencies. +receipts. The staking custom precompile is the first SDK-free implementation; +other custom precompiles are still placeholders. The open work is to port them +behind an EVM-native context that is visible to the executor's conflict tracking +without reintroducing Cosmos keeper dependencies. ## Current implementation @@ -31,7 +32,7 @@ The `evmonly` package currently provides: - Ethereum receipt construction with logs, bloom, gas, tx hash, block metadata, contract address, and effective gas price - a map-backed `MemoryState` for tests and early integration -- fail-closed custom precompile placeholders +- fail-closed custom precompile placeholders plus an SDK-free staking precompile The executor accepts config for nonce checks, gas-price checks, minimum gas price, chain config, and the custom precompile registry. @@ -109,7 +110,7 @@ receipts and RPC responses. `GasUsed` is the total EVM gas consumed by the block ## Open precompile work -Native custom precompiles still need a separate design. If they introduce state +Most native custom precompiles still need a separate design. If they introduce state outside balance, nonce, code, and storage, that state must either become part of the EVM-native changeset or be represented through an explicit extension that is visible to the OCC conflict tracker. @@ -119,9 +120,10 @@ state as contract storage owned by that precompile address. With no range reads and no side state, precompile reads and writes can then flow through ordinary `(address, slot)` storage tracking. -Until that design is implemented, the `evmonly` executor accepts a custom -precompile registry only as a fail-closed placeholder. Calls to registered -custom precompile addresses return `ErrCustomPrecompilesOpen`. +The staking precompile under `giga/evmonly/precompiles/staking` follows this +shape with a byte-key store backed by storage slots owned by the staking +precompile address. Registry entries without an implementation still fail +closed with `ErrCustomPrecompilesOpen`. ## Current limitations diff --git a/giga/evmonly/executor.go b/giga/evmonly/executor.go index ad49d485f7..fda418ed0b 100644 --- a/giga/evmonly/executor.go +++ b/giga/evmonly/executor.go @@ -8,12 +8,10 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core" - "github.com/ethereum/go-ethereum/core/tracing" ethtypes "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/vm" "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/params" - "github.com/sei-protocol/sei-chain/giga/evmonly/precompiles" ) // Executor runs raw EVM transactions against an EVM-native state backend. @@ -48,10 +46,6 @@ func (e *Executor) Config() Config { } func (e *Executor) ExecuteBlock(ctx context.Context, req BlockRequest) (*BlockResult, error) { - if len(req.Txs) == 0 { - return &BlockResult{}, nil - } - chainConfig := e.chainConfig(req.Context) signer := ethtypes.MakeSigner(chainConfig, new(big.Int).SetUint64(req.Context.Number), req.Context.Time) parsed, err := parseBlockTxs(ctx, req.Txs, signer) @@ -93,6 +87,11 @@ func (e *Executor) ExecuteBlock(ctx context.Context, req BlockRequest) (*BlockRe result.GasUsed += txResult.GasUsed txIndexUint++ } + validatorUpdates, err := runCustomPrecompileEndBlock(e.cfg.CustomPrecompiles, evm) + if err != nil { + return nil, fmt.Errorf("run custom precompile end block: %w", err) + } + result.ValidatorUpdates = validatorUpdates stateDB.Finalise(true) result.ChangeSet = stateDB.ChangeSet() return result, nil @@ -205,31 +204,6 @@ func buildBlockContext(ctx BlockContext) vm.BlockContext { } } -type unresolvedCustomPrecompile struct{} - -func (unresolvedCustomPrecompile) RequiredGas([]byte) uint64 { - return 0 -} - -func (unresolvedCustomPrecompile) Run(*vm.EVM, common.Address, common.Address, []byte, *big.Int, bool, bool, *tracing.Hooks) ([]byte, error) { - return nil, precompiles.ErrCustomPrecompilesOpen -} - -func customPrecompileMap(registry precompiles.Registry) map[common.Address]vm.PrecompiledContract { - if registry == nil { - return nil - } - addresses := registry.Addresses() - if len(addresses) == 0 { - return nil - } - contracts := make(map[common.Address]vm.PrecompiledContract, len(addresses)) - for _, addr := range addresses { - contracts[addr] = unresolvedCustomPrecompile{} - } - return contracts -} - func (e *Executor) chainConfig(ctx BlockContext) *params.ChainConfig { var cfg params.ChainConfig if e.cfg.ChainConfig != nil { diff --git a/giga/evmonly/executor_test.go b/giga/evmonly/executor_test.go index 3d7f4d000e..8c22868fd5 100644 --- a/giga/evmonly/executor_test.go +++ b/giga/evmonly/executor_test.go @@ -14,6 +14,8 @@ import ( "github.com/stretchr/testify/require" "github.com/sei-protocol/sei-chain/giga/evmonly/precompiles" + stakingprecompile "github.com/sei-protocol/sei-chain/giga/evmonly/precompiles/staking" + precompileutil "github.com/sei-protocol/sei-chain/giga/evmonly/precompiles/util" ) func TestExecutorEmptyBlock(t *testing.T) { @@ -180,6 +182,243 @@ func TestExecutorCustomPrecompilePlaceholder(t *testing.T) { require.True(t, errors.Is(result.Txs[0].Err, precompiles.ErrCustomPrecompilesOpen)) } +func TestExecutorRegisteredCustomPrecompile(t *testing.T) { + chainID := big.NewInt(713715) + key, err := crypto.GenerateKey() + require.NoError(t, err) + sender := crypto.PubkeyToAddress(key.PublicKey) + customAddr := common.HexToAddress("0x0000000000000000000000000000000000001005") + + state := NewMemoryState() + state.SetBalance(sender, big.NewInt(200_000_000_000_000)) + + rawTx := signLegacyTx(t, key, chainID, 0, &customAddr, big.NewInt(0), []byte{0x01}) + executor := NewExecutor(Config{ + CustomPrecompiles: contractPrecompileRegistry{ + customAddr: storeWritePrecompile{}, + }, + }, WithState(state)) + + result, err := executor.ExecuteBlock(context.Background(), BlockRequest{ + Context: blockContext(chainID), + Txs: [][]byte{rawTx}, + }) + + require.NoError(t, err) + require.Len(t, result.Txs, 1) + require.Equal(t, ethtypes.ReceiptStatusSuccessful, result.Txs[0].Status) + require.NotEmpty(t, result.ChangeSet.Storage) + + state.ApplyChangeSet(result.ChangeSet) + require.Equal(t, encodedStoredLength(2), state.GetState(customAddr, storeBaseSlot([]byte("seen")))) +} + +func TestExecutorStakingPrecompileForwardsPayableValue(t *testing.T) { + chainID := big.NewInt(713715) + key, err := crypto.GenerateKey() + require.NoError(t, err) + sender := crypto.PubkeyToAddress(key.PublicKey) + stakingAddr := common.HexToAddress(stakingprecompile.StakingAddress) + + state := NewMemoryState() + initialBalance := big.NewInt(1_000_000_000_000_000_000) + state.SetBalance(sender, initialBalance) + + contract, err := stakingprecompile.NewPrecompile() + require.NoError(t, err) + registry, err := stakingprecompile.NewRegistry() + require.NoError(t, err) + input, err := contract.ABI().Pack( + stakingprecompile.CreateValidatorMethod, + "01020304", + "validator-one", + "0.100000000000000000", + "0.200000000000000000", + "0.010000000000000000", + big.NewInt(1), + ) + require.NoError(t, err) + value := new(big.Int).Mul(big.NewInt(5), big.NewInt(1_000_000_000_000)) + rawTx := signLegacyTx(t, key, chainID, 0, &stakingAddr, value, input) + executor := NewExecutor(Config{ + CustomPrecompiles: registry, + }, WithState(state)) + + result, err := executor.ExecuteBlock(context.Background(), BlockRequest{ + Context: blockContext(chainID), + Txs: [][]byte{rawTx}, + }) + + require.NoError(t, err) + require.Len(t, result.Txs, 1) + require.Equal(t, ethtypes.ReceiptStatusSuccessful, result.Txs[0].Status) + require.Equal(t, []ValidatorUpdate{{PubKey: []byte{0x01, 0x02, 0x03, 0x04}, Power: 5}}, result.ValidatorUpdates) + + state.ApplyChangeSet(result.ChangeSet) + gasCost := new(big.Int).Mul(new(big.Int).SetUint64(result.Txs[0].GasUsed), result.Txs[0].EffectiveGasPrice) + require.Equal(t, new(big.Int).Sub(new(big.Int).Sub(initialBalance, value), gasCost), state.GetBalance(sender)) + require.Zero(t, state.GetBalance(stakingAddr).Sign()) + require.Equal(t, value, state.GetBalance(stakingprecompile.EscrowAddress())) +} + +func TestExecutorStakingDelegationLifecycleE2E(t *testing.T) { + chainID := big.NewInt(713715) + sourceKey, err := crypto.GenerateKey() + require.NoError(t, err) + dstKey, err := crypto.GenerateKey() + require.NoError(t, err) + delegatorKey, err := crypto.GenerateKey() + require.NoError(t, err) + + source := crypto.PubkeyToAddress(sourceKey.PublicKey) + destination := crypto.PubkeyToAddress(dstKey.PublicKey) + delegator := crypto.PubkeyToAddress(delegatorKey.PublicKey) + stakingAddr := common.HexToAddress(stakingprecompile.StakingAddress) + escrowAddr := stakingprecompile.EscrowAddress() + + state := NewMemoryState() + initialBalance := big.NewInt(1_000_000_000_000_000_000) + state.SetBalance(source, initialBalance) + state.SetBalance(destination, initialBalance) + state.SetBalance(delegator, initialBalance) + + registry, err := stakingprecompile.NewRegistry() + require.NoError(t, err) + contract, err := stakingprecompile.NewPrecompile() + require.NoError(t, err) + executor := NewExecutor(Config{CustomPrecompiles: registry}, WithState(state)) + + nonces := map[common.Address]uint64{} + signStakingTx := func(key *ecdsa.PrivateKey, value *big.Int, input []byte) []byte { + sender := crypto.PubkeyToAddress(key.PublicKey) + raw := signLegacyTx(t, key, chainID, nonces[sender], &stakingAddr, value, input) + nonces[sender]++ + return raw + } + expectedBalances := map[common.Address]*big.Int{ + source: new(big.Int).Set(initialBalance), + destination: new(big.Int).Set(initialBalance), + delegator: new(big.Int).Set(initialBalance), + stakingAddr: new(big.Int), + escrowAddr: new(big.Int), + } + + sourceSelfStake := usei(10) + destinationSelfStake := usei(5) + sourceSetupResult := executeBlockAndApply(t, executor, state, blockContextAt(chainID, 1, 100), [][]byte{ + signStakingTx(sourceKey, sourceSelfStake, mustPackStaking(t, contract, stakingprecompile.CreateValidatorMethod, + "01020304", + "source-validator", + "0.100000000000000000", + "0.200000000000000000", + "0.010000000000000000", + big.NewInt(1), + )), + }) + requireTxsSuccessful(t, sourceSetupResult, 1) + debitExpectedBalance(expectedBalances, source, sourceSelfStake, sourceSetupResult.Txs[0]) + addExpectedBalance(expectedBalances, escrowAddr, sourceSelfStake) + requireNativeBalances(t, state, expectedBalances) + require.Equal(t, []ValidatorUpdate{{PubKey: []byte{0x01, 0x02, 0x03, 0x04}, Power: 10}}, sourceSetupResult.ValidatorUpdates) + requireStakingPool(t, state, "10", "0") + requireStakingValidator(t, state, source, "10", "10", 3) + + destinationSetupResult := executeBlockAndApply(t, executor, state, blockContextAt(chainID, 2, 125), [][]byte{ + signStakingTx(dstKey, destinationSelfStake, mustPackStaking(t, contract, stakingprecompile.CreateValidatorMethod, + "05060708", + "destination-validator", + "0.100000000000000000", + "0.200000000000000000", + "0.010000000000000000", + big.NewInt(1), + )), + }) + requireTxsSuccessful(t, destinationSetupResult, 1) + debitExpectedBalance(expectedBalances, destination, destinationSelfStake, destinationSetupResult.Txs[0]) + addExpectedBalance(expectedBalances, escrowAddr, destinationSelfStake) + requireNativeBalances(t, state, expectedBalances) + require.Equal(t, []ValidatorUpdate{{PubKey: []byte{0x05, 0x06, 0x07, 0x08}, Power: 5}}, destinationSetupResult.ValidatorUpdates) + requireStakingPool(t, state, "15", "0") + requireStakingValidator(t, state, source, "10", "10", 3) + requireStakingValidator(t, state, destination, "5", "5", 3) + + delegationValue := usei(7) + delegateResult := executeBlockAndApply(t, executor, state, blockContextAt(chainID, 3, 150), [][]byte{ + signStakingTx(delegatorKey, delegationValue, mustPackStaking(t, contract, stakingprecompile.DelegateMethod, source.Hex())), + }) + requireTxsSuccessful(t, delegateResult, 1) + debitExpectedBalance(expectedBalances, delegator, delegationValue, delegateResult.Txs[0]) + addExpectedBalance(expectedBalances, escrowAddr, delegationValue) + requireNativeBalances(t, state, expectedBalances) + require.Equal(t, []ValidatorUpdate{{PubKey: []byte{0x01, 0x02, 0x03, 0x04}, Power: 17}}, delegateResult.ValidatorUpdates) + requireStakingPool(t, state, "22", "0") + requireStakingValidator(t, state, source, "17", "17", 3) + requireStakingValidator(t, state, destination, "5", "5", 3) + requireStakingDelegation(t, state, delegator, source, "7") + + redelegationAmount := big.NewInt(3) + redelegationTime := uint64(200) + redelegationCompletion := int64(redelegationTime + 1_814_400) + redelegateResult := executeBlockAndApply(t, executor, state, blockContextAt(chainID, 4, redelegationTime), [][]byte{ + signStakingTx(delegatorKey, nil, mustPackStaking(t, contract, stakingprecompile.RedelegateMethod, source.Hex(), destination.Hex(), redelegationAmount)), + }) + requireTxsSuccessful(t, redelegateResult, 1) + debitExpectedBalance(expectedBalances, delegator, nil, redelegateResult.Txs[0]) + requireNativeBalances(t, state, expectedBalances) + require.Equal(t, []ValidatorUpdate{ + {PubKey: []byte{0x01, 0x02, 0x03, 0x04}, Power: 14}, + {PubKey: []byte{0x05, 0x06, 0x07, 0x08}, Power: 8}, + }, redelegateResult.ValidatorUpdates) + requireStakingPool(t, state, "22", "0") + requireStakingValidator(t, state, source, "14", "14", 3) + requireStakingValidator(t, state, destination, "8", "8", 3) + requireStakingDelegation(t, state, delegator, source, "4") + requireStakingDelegation(t, state, delegator, destination, "3") + requireStakingRedelegation(t, state, delegator, source, destination, "3", redelegationCompletion) + + undelegationAmount := big.NewInt(2) + undelegationTime := uint64(300) + undelegationCompletion := int64(undelegationTime + 1_814_400) + undelegateResult := executeBlockAndApply(t, executor, state, blockContextAt(chainID, 5, undelegationTime), [][]byte{ + signStakingTx(delegatorKey, nil, mustPackStaking(t, contract, stakingprecompile.UndelegateMethod, destination.Hex(), undelegationAmount)), + }) + requireTxsSuccessful(t, undelegateResult, 1) + debitExpectedBalance(expectedBalances, delegator, nil, undelegateResult.Txs[0]) + requireNativeBalances(t, state, expectedBalances) + require.Equal(t, []ValidatorUpdate{{PubKey: []byte{0x05, 0x06, 0x07, 0x08}, Power: 6}}, undelegateResult.ValidatorUpdates) + requireStakingPool(t, state, "20", "2") + requireStakingValidator(t, state, source, "14", "14", 3) + requireStakingValidator(t, state, destination, "6", "6", 3) + requireStakingDelegation(t, state, delegator, source, "4") + requireStakingDelegation(t, state, delegator, destination, "1") + requireStakingRedelegation(t, state, delegator, source, destination, "3", redelegationCompletion) + requireStakingUnbonding(t, state, delegator, destination, "2", undelegationCompletion) + + redelegationMaturityResult := executeBlockAndApply(t, executor, state, blockContextAt(chainID, 6, uint64(redelegationCompletion)), nil) + require.Empty(t, redelegationMaturityResult.ValidatorUpdates) + requireNativeBalances(t, state, expectedBalances) + requireStakingPool(t, state, "20", "2") + requireStakingValidator(t, state, source, "14", "14", 3) + requireStakingValidator(t, state, destination, "6", "6", 3) + requireStakingDelegation(t, state, delegator, source, "4") + requireStakingDelegation(t, state, delegator, destination, "1") + requireNoStakingRedelegation(t, state, delegator, source, destination) + requireStakingUnbonding(t, state, delegator, destination, "2", undelegationCompletion) + + undelegationMaturityResult := executeBlockAndApply(t, executor, state, blockContextAt(chainID, 7, uint64(undelegationCompletion)), nil) + require.Empty(t, undelegationMaturityResult.ValidatorUpdates) + addExpectedBalance(expectedBalances, delegator, usei(2)) + addExpectedBalance(expectedBalances, escrowAddr, new(big.Int).Neg(usei(2))) + requireNativeBalances(t, state, expectedBalances) + requireStakingPool(t, state, "20", "0") + requireStakingValidator(t, state, source, "14", "14", 3) + requireStakingValidator(t, state, destination, "6", "6", 3) + requireStakingDelegation(t, state, delegator, source, "4") + requireStakingDelegation(t, state, delegator, destination, "1") + requireNoStakingRedelegation(t, state, delegator, source, destination) + requireNoStakingUnbonding(t, state, delegator, destination) +} + func signLegacyTx(t *testing.T, key *ecdsa.PrivateKey, chainID *big.Int, nonce uint64, to *common.Address, value *big.Int, data []byte) []byte { t.Helper() tx := ethtypes.NewTx(ðtypes.LegacyTx{ @@ -227,6 +466,158 @@ func blockContext(chainID *big.Int) BlockContext { } } +func blockContextAt(chainID *big.Int, number uint64, blockTime uint64) BlockContext { + ctx := blockContext(chainID) + ctx.Number = number + ctx.Time = blockTime + return ctx +} + +func executeBlockAndApply(t *testing.T, executor *Executor, state StateWriter, block BlockContext, txs [][]byte) *BlockResult { + t.Helper() + result, err := executor.ExecuteBlock(context.Background(), BlockRequest{ + Context: block, + Txs: txs, + }) + require.NoError(t, err) + state.ApplyChangeSet(result.ChangeSet) + return result +} + +func requireTxsSuccessful(t *testing.T, result *BlockResult, count int) { + t.Helper() + require.Len(t, result.Txs, count) + require.Len(t, result.Receipts, count) + for _, tx := range result.Txs { + require.Equal(t, ethtypes.ReceiptStatusSuccessful, tx.Status) + require.NoError(t, tx.Err) + } +} + +func mustPackStaking(t *testing.T, contract *stakingprecompile.Precompile, method string, args ...interface{}) []byte { + t.Helper() + input, err := contract.ABI().Pack(method, args...) + require.NoError(t, err) + return input +} + +func usei(amount int64) *big.Int { + return new(big.Int).Mul(big.NewInt(amount), big.NewInt(1_000_000_000_000)) +} + +func debitExpectedBalance(expected map[common.Address]*big.Int, sender common.Address, value *big.Int, tx TxResult) { + gasCost := new(big.Int).Mul(new(big.Int).SetUint64(tx.GasUsed), tx.EffectiveGasPrice) + total := new(big.Int).Add(cloneBig(value), gasCost) + addExpectedBalance(expected, sender, new(big.Int).Neg(total)) +} + +func addExpectedBalance(expected map[common.Address]*big.Int, addr common.Address, amount *big.Int) { + if amount == nil || amount.Sign() == 0 { + return + } + current := expected[addr] + if current == nil { + current = new(big.Int) + } + expected[addr] = new(big.Int).Add(current, amount) +} + +func requireNativeBalances(t *testing.T, state StateReader, expected map[common.Address]*big.Int) { + t.Helper() + for addr, balance := range expected { + require.Equal(t, balance, state.GetBalance(addr), "balance %s", addr.Hex()) + } +} + +type stakingDelegationRecordForTest struct { + DelegatorAddress string `json:"delegator_address"` + ValidatorAddress string `json:"validator_address"` + Amount string `json:"amount"` +} + +func requireStakingPool(t *testing.T, state StateReader, bonded string, notBonded string) { + t.Helper() + pool, ok := loadStakingJSON[stakingprecompile.Pool](t, state, []byte("pool")) + require.True(t, ok) + require.Equal(t, bonded, pool.BondedTokens) + require.Equal(t, notBonded, pool.NotBondedTokens) +} + +func requireStakingValidator(t *testing.T, state StateReader, validator common.Address, tokens string, shares string, status int32) { + t.Helper() + record, ok := loadStakingJSON[stakingprecompile.Validator](t, state, []byte("validator/"+validator.Hex())) + require.True(t, ok) + require.Equal(t, validator.Hex(), record.OperatorAddress) + require.Equal(t, tokens, record.Tokens) + require.Equal(t, shares, record.DelegatorShares) + require.Equal(t, status, record.Status) +} + +func requireStakingDelegation(t *testing.T, state StateReader, delegator common.Address, validator common.Address, amount string) { + t.Helper() + record, ok := loadStakingJSON[stakingDelegationRecordForTest](t, state, []byte("delegation/"+delegator.Hex()+"/"+validator.Hex())) + require.True(t, ok) + require.Equal(t, delegator.Hex(), record.DelegatorAddress) + require.Equal(t, validator.Hex(), record.ValidatorAddress) + require.Equal(t, amount, record.Amount) +} + +func requireStakingRedelegation(t *testing.T, state StateReader, delegator common.Address, src common.Address, dst common.Address, amount string, completionTime int64) { + t.Helper() + record, ok := loadStakingJSON[stakingprecompile.Redelegation](t, state, stakingRedelegationKey(delegator, src, dst)) + require.True(t, ok) + require.Equal(t, delegator.Hex(), record.DelegatorAddress) + require.Equal(t, src.Hex(), record.ValidatorSrcAddress) + require.Equal(t, dst.Hex(), record.ValidatorDstAddress) + require.Len(t, record.Entries, 1) + require.Equal(t, amount, record.Entries[0].InitialBalance) + require.Equal(t, amount, record.Entries[0].SharesDst) + require.Equal(t, completionTime, record.Entries[0].CompletionTime) +} + +func requireNoStakingRedelegation(t *testing.T, state StateReader, delegator common.Address, src common.Address, dst common.Address) { + t.Helper() + _, ok := loadStakingJSON[stakingprecompile.Redelegation](t, state, stakingRedelegationKey(delegator, src, dst)) + require.False(t, ok) +} + +func requireStakingUnbonding(t *testing.T, state StateReader, delegator common.Address, validator common.Address, amount string, completionTime int64) { + t.Helper() + record, ok := loadStakingJSON[stakingprecompile.UnbondingDelegation](t, state, stakingUnbondingKey(delegator, validator)) + require.True(t, ok) + require.Equal(t, delegator.Hex(), record.DelegatorAddress) + require.Equal(t, validator.Hex(), record.ValidatorAddress) + require.Len(t, record.Entries, 1) + require.Equal(t, amount, record.Entries[0].InitialBalance) + require.Equal(t, amount, record.Entries[0].Balance) + require.Equal(t, completionTime, record.Entries[0].CompletionTime) +} + +func requireNoStakingUnbonding(t *testing.T, state StateReader, delegator common.Address, validator common.Address) { + t.Helper() + _, ok := loadStakingJSON[stakingprecompile.UnbondingDelegation](t, state, stakingUnbondingKey(delegator, validator)) + require.False(t, ok) +} + +func loadStakingJSON[T any](t *testing.T, state StateReader, key []byte) (T, bool) { + t.Helper() + store := storageBackedStore{ + db: newNativeStateDB(state), + address: common.HexToAddress(stakingprecompile.StakingAddress), + } + value, ok, err := precompileutil.GetJSON[T](store, key) + require.NoError(t, err) + return value, ok +} + +func stakingRedelegationKey(delegator common.Address, src common.Address, dst common.Address) []byte { + return []byte("redelegation/" + delegator.Hex() + "\x00" + src.Hex() + "\x00" + dst.Hex()) +} + +func stakingUnbondingKey(delegator common.Address, validator common.Address) []byte { + return []byte("unbonding/" + delegator.Hex() + "/" + validator.Hex()) +} + func legacySelfDestructChainConfig(chainID *big.Int) *params.ChainConfig { return ¶ms.ChainConfig{ ChainID: chainID, @@ -261,3 +652,29 @@ func (r staticPrecompileRegistry) Get(addr common.Address) (precompiles.Contract func (r staticPrecompileRegistry) Addresses() []common.Address { return []common.Address{r.addr} } + +type contractPrecompileRegistry map[common.Address]precompiles.Contract + +func (r contractPrecompileRegistry) Get(addr common.Address) (precompiles.Contract, bool) { + contract, ok := r[addr] + return contract, ok +} + +func (r contractPrecompileRegistry) Addresses() []common.Address { + addresses := make([]common.Address, 0, len(r)) + for addr := range r { + addresses = append(addresses, addr) + } + return addresses +} + +type storeWritePrecompile struct{} + +func (storeWritePrecompile) RequiredGas([]byte) uint64 { + return 100 +} + +func (storeWritePrecompile) Run(ctx *precompiles.Context, _ []byte) ([]byte, error) { + ctx.Store.Set([]byte("seen"), []byte{0xaa, 0xbb}) + return []byte{0x01}, nil +} diff --git a/giga/evmonly/precompile_adapter.go b/giga/evmonly/precompile_adapter.go new file mode 100644 index 0000000000..d023f88399 --- /dev/null +++ b/giga/evmonly/precompile_adapter.go @@ -0,0 +1,302 @@ +package evmonly + +import ( + "encoding/binary" + "errors" + "math/big" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/tracing" + "github.com/ethereum/go-ethereum/core/vm" + "github.com/ethereum/go-ethereum/crypto" + "github.com/holiman/uint256" + + "github.com/sei-protocol/sei-chain/giga/evmonly/precompiles" +) + +var errInvalidPrecompileStateDB = errors.New("evm-only precompile requires native state db") + +type unresolvedCustomPrecompile struct{} + +func (unresolvedCustomPrecompile) RequiredGas([]byte) uint64 { + return 0 +} + +func (unresolvedCustomPrecompile) Run(*vm.EVM, common.Address, common.Address, []byte, *big.Int, bool, bool, *tracing.Hooks) ([]byte, error) { + return nil, precompiles.ErrCustomPrecompilesOpen +} + +type registeredCustomPrecompile struct { + address common.Address + contract precompiles.Contract +} + +func (p registeredCustomPrecompile) RequiredGas(input []byte) uint64 { + return p.contract.RequiredGas(input) +} + +func (p registeredCustomPrecompile) Run(evm *vm.EVM, caller common.Address, _ common.Address, input []byte, value *big.Int, readOnly bool, isFromDelegateCall bool, _ *tracing.Hooks) ([]byte, error) { + return p.run(evm, caller, input, value, readOnly, isFromDelegateCall, 0) +} + +func (p registeredCustomPrecompile) RunAndCalculateGas(evm *vm.EVM, caller common.Address, _ common.Address, input []byte, suppliedGas uint64, value *big.Int, hooks *tracing.Hooks, readOnly bool, isFromDelegateCall bool) ([]byte, uint64, error) { + gasCost := p.RequiredGas(input) + if suppliedGas < gasCost { + return nil, 0, vm.ErrOutOfGas + } + remainingGas := suppliedGas - gasCost + if hooks != nil && hooks.OnGasChange != nil { + hooks.OnGasChange(suppliedGas, remainingGas, tracing.GasChangeCallPrecompiledContract) + } + ret, err := p.run(evm, caller, input, value, readOnly, isFromDelegateCall, remainingGas) + return ret, remainingGas, err +} + +func (p registeredCustomPrecompile) run(evm *vm.EVM, caller common.Address, input []byte, value *big.Int, readOnly bool, isFromDelegateCall bool, remainingGas uint64) ([]byte, error) { + stateDB, ok := evm.StateDB.(*nativeStateDB) + if !ok { + return nil, errInvalidPrecompileStateDB + } + ctx := &precompiles.Context{ + Caller: caller, + Address: p.address, + ApparentValue: cloneBig(value), + ReadOnly: readOnly, + DelegateCall: isFromDelegateCall, + GasRemaining: remainingGas, + Block: evmPrecompileBlockContext(evm), + Store: storageBackedStore{db: stateDB, address: p.address}, + Balances: nativeBalanceTransfer{db: stateDB}, + Logs: stateDB, + } + return p.contract.Run(ctx, input) +} + +func customPrecompileMap(registry precompiles.Registry) map[common.Address]vm.PrecompiledContract { + if registry == nil { + return nil + } + addresses := registry.Addresses() + if len(addresses) == 0 { + return nil + } + contracts := make(map[common.Address]vm.PrecompiledContract, len(addresses)) + for _, addr := range addresses { + contract, ok := registry.Get(addr) + if !ok || contract == nil { + contracts[addr] = unresolvedCustomPrecompile{} + continue + } + contracts[addr] = registeredCustomPrecompile{ + address: addr, + contract: contract, + } + } + return contracts +} + +func runCustomPrecompileEndBlock(registry precompiles.Registry, evm *vm.EVM) ([]precompiles.ValidatorUpdate, error) { + if registry == nil { + return nil, nil + } + stateDB, ok := evm.StateDB.(*nativeStateDB) + if !ok { + return nil, errInvalidPrecompileStateDB + } + addresses := registry.Addresses() + updates := make([]precompiles.ValidatorUpdate, 0) + for _, addr := range addresses { + contract, ok := registry.Get(addr) + if !ok || contract == nil { + continue + } + endBlocker, ok := contract.(precompiles.EndBlocker) + if !ok { + continue + } + ctx := &precompiles.EndBlockContext{ + Address: addr, + Block: evmPrecompileBlockContext(evm), + Store: storageBackedStore{db: stateDB, address: addr}, + Balances: nativeBalanceTransfer{db: stateDB}, + Logs: stateDB, + } + contractUpdates, err := endBlocker.EndBlock(ctx) + if err != nil { + return nil, err + } + updates = append(updates, contractUpdates...) + } + return updates, nil +} + +func evmPrecompileBlockContext(evm *vm.EVM) precompiles.BlockContext { + var number uint64 + if evm.Context.BlockNumber != nil { + number = evm.Context.BlockNumber.Uint64() + } + var chainID *big.Int + if cfg := evm.ChainConfig(); cfg != nil && cfg.ChainID != nil { + chainID = new(big.Int).Set(cfg.ChainID) + } + var prevRandao common.Hash + if evm.Context.Random != nil { + prevRandao = *evm.Context.Random + } + return precompiles.BlockContext{ + Number: number, + Time: evm.Context.Time, + ChainID: chainID, + BaseFee: cloneBig(evm.Context.BaseFee), + BlobBaseFee: cloneBig(evm.Context.BlobBaseFee), + Coinbase: evm.Context.Coinbase, + PrevRandao: prevRandao, + } +} + +type nativeBalanceTransfer struct { + db *nativeStateDB +} + +func (t nativeBalanceTransfer) Transfer(from common.Address, to common.Address, amount *big.Int) error { + if amount == nil || amount.Sign() == 0 { + return nil + } + if t.db.err != nil { + return t.db.err + } + u, err := uint256FromBigChecked(amount) + if err != nil { + t.db.err = err + return err + } + if t.db.GetBalance(from).Cmp(u) < 0 { + t.db.err = errInsufficientBalance + return errInsufficientBalance + } + t.db.SubBalance(from, u, tracing.BalanceChangeTransfer) + if t.db.err != nil { + return t.db.err + } + t.db.AddBalance(to, u, tracing.BalanceChangeTransfer) + return t.db.err +} + +func uint256FromBigChecked(v *big.Int) (*uint256.Int, error) { + if v == nil { + return uint256.NewInt(0), nil + } + if v.Sign() < 0 { + return nil, errors.New("negative amount") + } + u, overflow := uint256.FromBig(v) + if overflow { + return nil, errors.New("amount exceeds uint256") + } + if u == nil { + return uint256.NewInt(0), nil + } + return u, nil +} + +const ( + storeLengthDomain = "sei/evmonly/precompile-store/length/v1" + storeChunkDomain = "sei/evmonly/precompile-store/chunk/v1" +) + +type storageBackedStore struct { + db *nativeStateDB + address common.Address +} + +func (s storageBackedStore) Get(key []byte) ([]byte, bool) { + baseSlot := storeBaseSlot(key) + length, ok := s.length(baseSlot) + if !ok { + return nil, false + } + if length > uint64(^uint(0)>>1) { + return nil, false + } + chunks := chunkCount(length) + out := make([]byte, 0, int(chunks*32)) //nolint:gosec // length was bounded by max int above. + for i := uint64(0); i < chunks; i++ { + chunk := s.db.GetState(s.address, storeChunkSlot(baseSlot, i)) + out = append(out, chunk.Bytes()...) + } + return out[:int(length)], true //nolint:gosec // length was bounded by max int above. +} + +func (s storageBackedStore) Set(key []byte, value []byte) { + baseSlot := storeBaseSlot(key) + oldLength, oldOK := s.length(baseSlot) + oldChunks := uint64(0) + if oldOK { + oldChunks = chunkCount(oldLength) + } + newLength := uint64(len(value)) //nolint:gosec // slices cannot exceed max int. + newChunks := chunkCount(newLength) + s.db.SetState(s.address, baseSlot, encodedStoredLength(newLength)) + for i := uint64(0); i < newChunks; i++ { + start := int(i * 32) //nolint:gosec // i is bounded by len(value) chunks. + end := start + 32 + if end > len(value) { + end = len(value) + } + var chunk common.Hash + copy(chunk[:], value[start:end]) + s.db.SetState(s.address, storeChunkSlot(baseSlot, i), chunk) + } + for i := newChunks; i < oldChunks; i++ { + s.db.SetState(s.address, storeChunkSlot(baseSlot, i), common.Hash{}) + } +} + +func (s storageBackedStore) Delete(key []byte) { + baseSlot := storeBaseSlot(key) + length, ok := s.length(baseSlot) + if !ok { + return + } + for i := uint64(0); i < chunkCount(length); i++ { + s.db.SetState(s.address, storeChunkSlot(baseSlot, i), common.Hash{}) + } + s.db.SetState(s.address, baseSlot, common.Hash{}) +} + +func (s storageBackedStore) length(baseSlot common.Hash) (uint64, bool) { + encoded := s.db.GetState(s.address, baseSlot) + if encoded == (common.Hash{}) { + return 0, false + } + n := encoded.Big() + if n.Sign() == 0 { + return 0, false + } + n.Sub(n, big.NewInt(1)) + if !n.IsUint64() { + return 0, false + } + return n.Uint64(), true +} + +func storeBaseSlot(key []byte) common.Hash { + return crypto.Keccak256Hash([]byte(storeLengthDomain), key) +} + +func storeChunkSlot(baseSlot common.Hash, index uint64) common.Hash { + var indexBz [8]byte + binary.BigEndian.PutUint64(indexBz[:], index) + return crypto.Keccak256Hash([]byte(storeChunkDomain), baseSlot.Bytes(), indexBz[:]) +} + +func encodedStoredLength(length uint64) common.Hash { + return common.BigToHash(new(big.Int).SetUint64(length + 1)) +} + +func chunkCount(length uint64) uint64 { + if length == 0 { + return 0 + } + return (length + 31) / 32 +} diff --git a/giga/evmonly/precompiles/context.go b/giga/evmonly/precompiles/context.go index 828937d6b1..390566ff5f 100644 --- a/giga/evmonly/precompiles/context.go +++ b/giga/evmonly/precompiles/context.go @@ -22,6 +22,12 @@ type Contract interface { Run(*Context, []byte) ([]byte, error) } +// EndBlocker is implemented by custom precompiles that need per-block work +// after all transactions have executed. +type EndBlocker interface { + EndBlock(*EndBlockContext) ([]ValidatorUpdate, error) +} + // Context is the only execution context custom precompiles should receive in // the EVM-only path. It deliberately excludes sdk.Context and Cosmos keepers. type Context struct { @@ -32,10 +38,21 @@ type Context struct { DelegateCall bool GasRemaining uint64 Block BlockContext - State State + Store Store + Balances BalanceTransfer Logs LogSink } +// EndBlockContext is the SDK-free context custom precompiles receive after all +// transactions in a block have executed. +type EndBlockContext struct { + Address common.Address + Block BlockContext + Store Store + Balances BalanceTransfer + Logs LogSink +} + // BlockContext is the block data custom precompiles may read. type BlockContext struct { Number uint64 @@ -47,19 +64,25 @@ type BlockContext struct { PrevRandao common.Hash } -// State is the precompile-facing state API. Implementations must make these -// reads and writes visible to the executor's conflict tracking. -type State interface { - GetBalance(common.Address) *big.Int - AddBalance(common.Address, *big.Int) - SubBalance(common.Address, *big.Int) error - GetNonce(common.Address) uint64 - SetNonce(common.Address, uint64) - GetCode(common.Address) []byte - GetState(common.Address, common.Hash) common.Hash - SetState(common.Address, common.Hash, common.Hash) - GetCustom([]byte) ([]byte, bool) - SetCustom([]byte, []byte) +// ValidatorUpdate is the EVM-only validator set update shape. +type ValidatorUpdate struct { + PubKey []byte + Power int64 +} + +// Store is the byte-keyed state boundary custom precompiles use for module-like +// data. Implementations should make Get/Set/Delete visible through the same +// read/write tracking as ordinary EVM storage. +type Store interface { + Get([]byte) ([]byte, bool) + Set([]byte, []byte) + Delete([]byte) +} + +// BalanceTransfer moves native EVM value for precompiles that need to forward +// payable call value or adjust native balances alongside module-like state. +type BalanceTransfer interface { + Transfer(from common.Address, to common.Address, amount *big.Int) error } // LogSink lets custom precompiles emit Ethereum logs without Cosmos events. diff --git a/giga/evmonly/precompiles/staking/abi.json b/giga/evmonly/precompiles/staking/abi.json new file mode 100644 index 0000000000..a6584f25c5 --- /dev/null +++ b/giga/evmonly/precompiles/staking/abi.json @@ -0,0 +1 @@ +[{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"delegator","type":"address"},{"indexed":false,"internalType":"string","name":"validator","type":"string"},{"indexed":false,"internalType":"uint256","name":"amount","type":"uint256"}],"name":"Delegate","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"delegator","type":"address"},{"indexed":false,"internalType":"string","name":"validator","type":"string"},{"indexed":false,"internalType":"uint256","name":"amount","type":"uint256"}],"name":"DelegationRewardsWithdrawn","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"delegator","type":"address"},{"indexed":false,"internalType":"string","name":"srcValidator","type":"string"},{"indexed":false,"internalType":"string","name":"dstValidator","type":"string"},{"indexed":false,"internalType":"uint256","name":"amount","type":"uint256"}],"name":"Redelegate","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"delegator","type":"address"},{"indexed":false,"internalType":"string","name":"validator","type":"string"},{"indexed":false,"internalType":"uint256","name":"amount","type":"uint256"}],"name":"Undelegate","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"creator","type":"address"},{"indexed":false,"internalType":"string","name":"validatorAddress","type":"string"},{"indexed":false,"internalType":"string","name":"moniker","type":"string"}],"name":"ValidatorCreated","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"editor","type":"address"},{"indexed":false,"internalType":"string","name":"validatorAddress","type":"string"},{"indexed":false,"internalType":"string","name":"moniker","type":"string"}],"name":"ValidatorEdited","type":"event"},{"inputs":[{"internalType":"string","name":"pubKeyHex","type":"string"},{"internalType":"string","name":"moniker","type":"string"},{"internalType":"string","name":"commissionRate","type":"string"},{"internalType":"string","name":"commissionMaxRate","type":"string"},{"internalType":"string","name":"commissionMaxChangeRate","type":"string"},{"internalType":"uint256","name":"minSelfDelegation","type":"uint256"}],"name":"createValidator","outputs":[{"internalType":"bool","name":"success","type":"bool"}],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"string","name":"valAddress","type":"string"}],"name":"delegate","outputs":[{"internalType":"bool","name":"success","type":"bool"}],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"address","name":"delegator","type":"address"},{"internalType":"string","name":"valAddress","type":"string"}],"name":"delegation","outputs":[{"components":[{"components":[{"internalType":"uint256","name":"amount","type":"uint256"},{"internalType":"string","name":"denom","type":"string"}],"internalType":"struct IStaking.Balance","name":"balance","type":"tuple"},{"components":[{"internalType":"string","name":"delegator_address","type":"string"},{"internalType":"uint256","name":"shares","type":"uint256"},{"internalType":"uint256","name":"decimals","type":"uint256"},{"internalType":"string","name":"validator_address","type":"string"}],"internalType":"struct IStaking.DelegationDetails","name":"delegation","type":"tuple"}],"internalType":"struct IStaking.Delegation","name":"delegation","type":"tuple"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"delegator","type":"address"},{"internalType":"bytes","name":"nextKey","type":"bytes"}],"name":"delegatorDelegations","outputs":[{"components":[{"components":[{"components":[{"internalType":"uint256","name":"amount","type":"uint256"},{"internalType":"string","name":"denom","type":"string"}],"internalType":"struct IStaking.Balance","name":"balance","type":"tuple"},{"components":[{"internalType":"string","name":"delegator_address","type":"string"},{"internalType":"uint256","name":"shares","type":"uint256"},{"internalType":"uint256","name":"decimals","type":"uint256"},{"internalType":"string","name":"validator_address","type":"string"}],"internalType":"struct IStaking.DelegationDetails","name":"delegation","type":"tuple"}],"internalType":"struct IStaking.Delegation[]","name":"delegations","type":"tuple[]"},{"internalType":"bytes","name":"nextKey","type":"bytes"}],"internalType":"struct IStaking.DelegationsResponse","name":"response","type":"tuple"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"delegator","type":"address"},{"internalType":"bytes","name":"nextKey","type":"bytes"}],"name":"delegatorUnbondingDelegations","outputs":[{"components":[{"components":[{"internalType":"string","name":"delegatorAddress","type":"string"},{"internalType":"string","name":"validatorAddress","type":"string"},{"components":[{"internalType":"int64","name":"creationHeight","type":"int64"},{"internalType":"int64","name":"completionTime","type":"int64"},{"internalType":"string","name":"initialBalance","type":"string"},{"internalType":"string","name":"balance","type":"string"}],"internalType":"struct IStaking.UnbondingDelegationEntry[]","name":"entries","type":"tuple[]"}],"internalType":"struct IStaking.UnbondingDelegation[]","name":"unbondingDelegations","type":"tuple[]"},{"internalType":"bytes","name":"nextKey","type":"bytes"}],"internalType":"struct IStaking.UnbondingDelegationsResponse","name":"response","type":"tuple"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"delegator","type":"address"},{"internalType":"string","name":"validatorAddress","type":"string"}],"name":"delegatorValidator","outputs":[{"components":[{"internalType":"string","name":"operatorAddress","type":"string"},{"internalType":"bytes","name":"consensusPubkey","type":"bytes"},{"internalType":"bool","name":"jailed","type":"bool"},{"internalType":"int32","name":"status","type":"int32"},{"internalType":"string","name":"tokens","type":"string"},{"internalType":"string","name":"delegatorShares","type":"string"},{"internalType":"string","name":"description","type":"string"},{"internalType":"int64","name":"unbondingHeight","type":"int64"},{"internalType":"int64","name":"unbondingTime","type":"int64"},{"internalType":"string","name":"commissionRate","type":"string"},{"internalType":"string","name":"commissionMaxRate","type":"string"},{"internalType":"string","name":"commissionMaxChangeRate","type":"string"},{"internalType":"int64","name":"commissionUpdateTime","type":"int64"},{"internalType":"string","name":"minSelfDelegation","type":"string"}],"internalType":"struct IStaking.Validator","name":"validator","type":"tuple"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"delegator","type":"address"},{"internalType":"bytes","name":"nextKey","type":"bytes"}],"name":"delegatorValidators","outputs":[{"components":[{"components":[{"internalType":"string","name":"operatorAddress","type":"string"},{"internalType":"bytes","name":"consensusPubkey","type":"bytes"},{"internalType":"bool","name":"jailed","type":"bool"},{"internalType":"int32","name":"status","type":"int32"},{"internalType":"string","name":"tokens","type":"string"},{"internalType":"string","name":"delegatorShares","type":"string"},{"internalType":"string","name":"description","type":"string"},{"internalType":"int64","name":"unbondingHeight","type":"int64"},{"internalType":"int64","name":"unbondingTime","type":"int64"},{"internalType":"string","name":"commissionRate","type":"string"},{"internalType":"string","name":"commissionMaxRate","type":"string"},{"internalType":"string","name":"commissionMaxChangeRate","type":"string"},{"internalType":"int64","name":"commissionUpdateTime","type":"int64"},{"internalType":"string","name":"minSelfDelegation","type":"string"}],"internalType":"struct IStaking.Validator[]","name":"validators","type":"tuple[]"},{"internalType":"bytes","name":"nextKey","type":"bytes"}],"internalType":"struct IStaking.ValidatorsResponse","name":"response","type":"tuple"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"string","name":"moniker","type":"string"},{"internalType":"string","name":"commissionRate","type":"string"},{"internalType":"uint256","name":"minSelfDelegation","type":"uint256"}],"name":"editValidator","outputs":[{"internalType":"bool","name":"success","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"int64","name":"height","type":"int64"}],"name":"historicalInfo","outputs":[{"components":[{"internalType":"int64","name":"height","type":"int64"},{"components":[{"internalType":"string","name":"operatorAddress","type":"string"},{"internalType":"bytes","name":"consensusPubkey","type":"bytes"},{"internalType":"bool","name":"jailed","type":"bool"},{"internalType":"int32","name":"status","type":"int32"},{"internalType":"string","name":"tokens","type":"string"},{"internalType":"string","name":"delegatorShares","type":"string"},{"internalType":"string","name":"description","type":"string"},{"internalType":"int64","name":"unbondingHeight","type":"int64"},{"internalType":"int64","name":"unbondingTime","type":"int64"},{"internalType":"string","name":"commissionRate","type":"string"},{"internalType":"string","name":"commissionMaxRate","type":"string"},{"internalType":"string","name":"commissionMaxChangeRate","type":"string"},{"internalType":"int64","name":"commissionUpdateTime","type":"int64"},{"internalType":"string","name":"minSelfDelegation","type":"string"}],"internalType":"struct IStaking.Validator[]","name":"validators","type":"tuple[]"}],"internalType":"struct IStaking.HistoricalInfo","name":"historicalInfo","type":"tuple"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"params","outputs":[{"components":[{"internalType":"uint64","name":"unbondingTime","type":"uint64"},{"internalType":"uint32","name":"maxValidators","type":"uint32"},{"internalType":"uint32","name":"maxEntries","type":"uint32"},{"internalType":"uint32","name":"historicalEntries","type":"uint32"},{"internalType":"string","name":"bondDenom","type":"string"},{"internalType":"string","name":"minCommissionRate","type":"string"},{"internalType":"string","name":"maxVotingPowerRatio","type":"string"},{"internalType":"string","name":"maxVotingPowerEnforcementThreshold","type":"string"}],"internalType":"struct IStaking.Params","name":"params","type":"tuple"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"pool","outputs":[{"components":[{"internalType":"string","name":"notBondedTokens","type":"string"},{"internalType":"string","name":"bondedTokens","type":"string"}],"internalType":"struct IStaking.Pool","name":"pool","type":"tuple"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"string","name":"srcAddress","type":"string"},{"internalType":"string","name":"dstAddress","type":"string"},{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"redelegate","outputs":[{"internalType":"bool","name":"success","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"string","name":"delegator","type":"string"},{"internalType":"string","name":"srcValidator","type":"string"},{"internalType":"string","name":"dstValidator","type":"string"},{"internalType":"bytes","name":"nextKey","type":"bytes"}],"name":"redelegations","outputs":[{"components":[{"components":[{"internalType":"string","name":"delegatorAddress","type":"string"},{"internalType":"string","name":"validatorSrcAddress","type":"string"},{"internalType":"string","name":"validatorDstAddress","type":"string"},{"components":[{"internalType":"int64","name":"creationHeight","type":"int64"},{"internalType":"int64","name":"completionTime","type":"int64"},{"internalType":"string","name":"initialBalance","type":"string"},{"internalType":"string","name":"sharesDst","type":"string"}],"internalType":"struct IStaking.RedelegationEntry[]","name":"entries","type":"tuple[]"}],"internalType":"struct IStaking.Redelegation[]","name":"redelegations","type":"tuple[]"},{"internalType":"bytes","name":"nextKey","type":"bytes"}],"internalType":"struct IStaking.RedelegationsResponse","name":"response","type":"tuple"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"delegator","type":"address"},{"internalType":"string","name":"validatorAddress","type":"string"}],"name":"unbondingDelegation","outputs":[{"components":[{"internalType":"string","name":"delegatorAddress","type":"string"},{"internalType":"string","name":"validatorAddress","type":"string"},{"components":[{"internalType":"int64","name":"creationHeight","type":"int64"},{"internalType":"int64","name":"completionTime","type":"int64"},{"internalType":"string","name":"initialBalance","type":"string"},{"internalType":"string","name":"balance","type":"string"}],"internalType":"struct IStaking.UnbondingDelegationEntry[]","name":"entries","type":"tuple[]"}],"internalType":"struct IStaking.UnbondingDelegation","name":"unbondingDelegation","type":"tuple"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"string","name":"valAddress","type":"string"},{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"undelegate","outputs":[{"internalType":"bool","name":"success","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"string","name":"validatorAddress","type":"string"}],"name":"validator","outputs":[{"components":[{"internalType":"string","name":"operatorAddress","type":"string"},{"internalType":"bytes","name":"consensusPubkey","type":"bytes"},{"internalType":"bool","name":"jailed","type":"bool"},{"internalType":"int32","name":"status","type":"int32"},{"internalType":"string","name":"tokens","type":"string"},{"internalType":"string","name":"delegatorShares","type":"string"},{"internalType":"string","name":"description","type":"string"},{"internalType":"int64","name":"unbondingHeight","type":"int64"},{"internalType":"int64","name":"unbondingTime","type":"int64"},{"internalType":"string","name":"commissionRate","type":"string"},{"internalType":"string","name":"commissionMaxRate","type":"string"},{"internalType":"string","name":"commissionMaxChangeRate","type":"string"},{"internalType":"int64","name":"commissionUpdateTime","type":"int64"},{"internalType":"string","name":"minSelfDelegation","type":"string"}],"internalType":"struct IStaking.Validator","name":"validator","type":"tuple"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"string","name":"validatorAddress","type":"string"},{"internalType":"bytes","name":"nextKey","type":"bytes"}],"name":"validatorDelegations","outputs":[{"components":[{"components":[{"components":[{"internalType":"uint256","name":"amount","type":"uint256"},{"internalType":"string","name":"denom","type":"string"}],"internalType":"struct IStaking.Balance","name":"balance","type":"tuple"},{"components":[{"internalType":"string","name":"delegator_address","type":"string"},{"internalType":"uint256","name":"shares","type":"uint256"},{"internalType":"uint256","name":"decimals","type":"uint256"},{"internalType":"string","name":"validator_address","type":"string"}],"internalType":"struct IStaking.DelegationDetails","name":"delegation","type":"tuple"}],"internalType":"struct IStaking.Delegation[]","name":"delegations","type":"tuple[]"},{"internalType":"bytes","name":"nextKey","type":"bytes"}],"internalType":"struct IStaking.DelegationsResponse","name":"response","type":"tuple"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"string","name":"validatorAddress","type":"string"},{"internalType":"bytes","name":"nextKey","type":"bytes"}],"name":"validatorUnbondingDelegations","outputs":[{"components":[{"components":[{"internalType":"string","name":"delegatorAddress","type":"string"},{"internalType":"string","name":"validatorAddress","type":"string"},{"components":[{"internalType":"int64","name":"creationHeight","type":"int64"},{"internalType":"int64","name":"completionTime","type":"int64"},{"internalType":"string","name":"initialBalance","type":"string"},{"internalType":"string","name":"balance","type":"string"}],"internalType":"struct IStaking.UnbondingDelegationEntry[]","name":"entries","type":"tuple[]"}],"internalType":"struct IStaking.UnbondingDelegation[]","name":"unbondingDelegations","type":"tuple[]"},{"internalType":"bytes","name":"nextKey","type":"bytes"}],"internalType":"struct IStaking.UnbondingDelegationsResponse","name":"response","type":"tuple"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"string","name":"status","type":"string"},{"internalType":"bytes","name":"nextKey","type":"bytes"}],"name":"validators","outputs":[{"components":[{"components":[{"internalType":"string","name":"operatorAddress","type":"string"},{"internalType":"bytes","name":"consensusPubkey","type":"bytes"},{"internalType":"bool","name":"jailed","type":"bool"},{"internalType":"int32","name":"status","type":"int32"},{"internalType":"string","name":"tokens","type":"string"},{"internalType":"string","name":"delegatorShares","type":"string"},{"internalType":"string","name":"description","type":"string"},{"internalType":"int64","name":"unbondingHeight","type":"int64"},{"internalType":"int64","name":"unbondingTime","type":"int64"},{"internalType":"string","name":"commissionRate","type":"string"},{"internalType":"string","name":"commissionMaxRate","type":"string"},{"internalType":"string","name":"commissionMaxChangeRate","type":"string"},{"internalType":"int64","name":"commissionUpdateTime","type":"int64"},{"internalType":"string","name":"minSelfDelegation","type":"string"}],"internalType":"struct IStaking.Validator[]","name":"validators","type":"tuple[]"},{"internalType":"bytes","name":"nextKey","type":"bytes"}],"internalType":"struct IStaking.ValidatorsResponse","name":"response","type":"tuple"}],"stateMutability":"view","type":"function"}] \ No newline at end of file diff --git a/giga/evmonly/precompiles/staking/balances.go b/giga/evmonly/precompiles/staking/balances.go new file mode 100644 index 0000000000..40a71c204a --- /dev/null +++ b/giga/evmonly/precompiles/staking/balances.go @@ -0,0 +1,52 @@ +package staking + +import ( + "fmt" + "math/big" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" + + "github.com/sei-protocol/sei-chain/giga/evmonly/precompiles" +) + +const escrowAddressSeed = "sei/evmonly/staking/escrow/v1" + +var escrowAddress = common.BytesToAddress(crypto.Keccak256([]byte(escrowAddressSeed))[12:]) + +// EscrowAddress is the module-account-like address that holds bonded stake. +func EscrowAddress() common.Address { + return escrowAddress +} + +func transferPrecompileValueToEscrow(ctx *precompiles.Context) error { + return transferNativeValue(ctx, ctx.Address, escrowAddress, ctx.ApparentValue) +} + +func transferStakeFromEscrowToAddress(balances precompiles.BalanceTransfer, delegator string, amount *big.Int) error { + if !common.IsHexAddress(delegator) { + return fmt.Errorf("delegator address %q is not an EVM address", delegator) + } + return transferNativeValueWithBalances(balances, escrowAddress, common.HexToAddress(delegator), sweiFromUsei(amount)) +} + +func transferNativeValue(ctx *precompiles.Context, from common.Address, to common.Address, amount *big.Int) error { + return transferNativeValueWithBalances(ctx.Balances, from, to, amount) +} + +func transferNativeValueWithBalances(balances precompiles.BalanceTransfer, from common.Address, to common.Address, amount *big.Int) error { + if amount == nil || amount.Sign() == 0 { + return nil + } + if balances == nil { + return errMissingBalanceTransfer + } + return balances.Transfer(from, to, amount) +} + +func sweiFromUsei(amount *big.Int) *big.Int { + if amount == nil { + return new(big.Int) + } + return new(big.Int).Mul(amount, useiToSwei) +} diff --git a/giga/evmonly/precompiles/staking/endblock.go b/giga/evmonly/precompiles/staking/endblock.go new file mode 100644 index 0000000000..125bdc2d43 --- /dev/null +++ b/giga/evmonly/precompiles/staking/endblock.go @@ -0,0 +1,367 @@ +package staking + +import ( + "errors" + "math" + "math/big" + "sort" + + "github.com/sei-protocol/sei-chain/giga/evmonly/precompiles" + "github.com/sei-protocol/sei-chain/giga/evmonly/precompiles/util" +) + +// EndBlock runs the SDK-free staking end-block transition. +func (p *Precompile) EndBlock(ctx *precompiles.EndBlockContext) ([]precompiles.ValidatorUpdate, error) { + if ctx.Store == nil { + return nil, errMissingStore + } + updates, err := applyAndReturnValidatorSetUpdates(ctx.Store, ctx.Block) + if err != nil { + return nil, err + } + if err := unbondAllMatureValidators(ctx.Store, ctx.Block); err != nil { + return nil, err + } + if err := completeMatureUnbondings(ctx); err != nil { + return nil, err + } + if err := completeMatureRedelegations(ctx.Store, ctx.Block); err != nil { + return nil, err + } + return updates, nil +} + +func applyAndReturnValidatorSetUpdates(store precompiles.Store, block precompiles.BlockContext) ([]precompiles.ValidatorUpdate, error) { + params, err := loadParams(store) + if err != nil { + return nil, err + } + last, err := getLastValidatorPowers(store) + if err != nil { + return nil, err + } + candidates, err := validatorsByPower(store) + if err != nil { + return nil, err + } + maxValidators := int(params.MaxValidators) + if maxValidators < 0 { + maxValidators = 0 + } + if maxValidators > len(candidates) { + maxValidators = len(candidates) + } + + updates := make([]precompiles.ValidatorUpdate, 0) + totalPower := int64(0) + amtFromBondedToNotBonded := new(big.Int) + amtFromNotBondedToBonded := new(big.Int) + + for i := 0; i < maxValidators; i++ { + validator := candidates[i] + newPower, err := validatorPower(validator) + if err != nil { + return nil, err + } + if newPower == 0 { + break + } + tokens, err := util.ParseAmount(validator.Tokens) + if err != nil { + return nil, err + } + switch validator.Status { + case bondStatusUnbonded: + validator.Status = bondStatusBonded + amtFromNotBondedToBonded.Add(amtFromNotBondedToBonded, tokens) + case bondStatusUnbonding: + if err := deleteValidatorQueue(store, validator.UnbondingTime, validator.UnbondingHeight, validator.OperatorAddress); err != nil { + return nil, err + } + validator.Status = bondStatusBonded + validator.UnbondingHeight = 0 + validator.UnbondingTime = 0 + amtFromNotBondedToBonded.Add(amtFromNotBondedToBonded, tokens) + case bondStatusBonded: + default: + return nil, errors.New("unexpected validator status") + } + if err := setValidator(store, validator); err != nil { + return nil, err + } + + oldPower, found := last[validator.OperatorAddress] + if !found || oldPower != newPower { + updates = append(updates, validatorUpdate(validator, newPower)) + if err := setLastValidatorPower(store, validator.OperatorAddress, newPower); err != nil { + return nil, err + } + } + delete(last, validator.OperatorAddress) + if totalPower > math.MaxInt64-newPower { + return nil, errors.New("validator power overflow") + } + totalPower += newPower + } + + noLongerBonded := make([]string, 0, len(last)) + for validator := range last { + noLongerBonded = append(noLongerBonded, validator) + } + sort.Strings(noLongerBonded) + for _, validatorAddress := range noLongerBonded { + validator, ok, err := getValidator(store, validatorAddress) + if err != nil { + return nil, err + } + if !ok { + return nil, errValidatorMissing + } + tokens, err := util.ParseAmount(validator.Tokens) + if err != nil { + return nil, err + } + if validator.Status == bondStatusBonded { + validator.Status = bondStatusUnbonding + validator.UnbondingTime = util.SaturatingCompletionTime(block.Time, params.UnbondingTime) + validator.UnbondingHeight = saturatingInt64FromUint64(block.Number) + if err := setValidator(store, validator); err != nil { + return nil, err + } + if err := insertValidatorQueue(store, validator.UnbondingTime, validator.UnbondingHeight, validator.OperatorAddress); err != nil { + return nil, err + } + amtFromBondedToNotBonded.Add(amtFromBondedToNotBonded, tokens) + } + if err := deleteLastValidatorPower(store, validator.OperatorAddress); err != nil { + return nil, err + } + updates = append(updates, validatorUpdate(validator, 0)) + } + + if amtFromNotBondedToBonded.Cmp(amtFromBondedToNotBonded) > 0 { + delta := new(big.Int).Sub(amtFromNotBondedToBonded, amtFromBondedToNotBonded) + if err := addPoolNotBonded(store, new(big.Int).Neg(delta)); err != nil { + return nil, err + } + if err := addPoolBonded(store, delta); err != nil { + return nil, err + } + } else if amtFromBondedToNotBonded.Cmp(amtFromNotBondedToBonded) > 0 { + delta := new(big.Int).Sub(amtFromBondedToNotBonded, amtFromNotBondedToBonded) + if err := addPoolBonded(store, new(big.Int).Neg(delta)); err != nil { + return nil, err + } + if err := addPoolNotBonded(store, delta); err != nil { + return nil, err + } + } + + if len(updates) > 0 { + if err := setLastTotalPower(store, totalPower); err != nil { + return nil, err + } + } + return updates, nil +} + +func validatorsByPower(store precompiles.Store) ([]Validator, error) { + validatorAddresses, err := getStringList(store, validatorsIndexKey()) + if err != nil { + return nil, err + } + validators := make([]Validator, 0, len(validatorAddresses)) + for _, validatorAddress := range validatorAddresses { + validator, ok, err := getValidator(store, validatorAddress) + if err != nil { + return nil, err + } + if !ok || validator.Jailed { + continue + } + power, err := validatorPower(validator) + if err != nil { + return nil, err + } + if power == 0 { + continue + } + validators = append(validators, validator) + } + sort.SliceStable(validators, func(i, j int) bool { + left, _ := validatorPower(validators[i]) + right, _ := validatorPower(validators[j]) + if left != right { + return left > right + } + return validators[i].OperatorAddress < validators[j].OperatorAddress + }) + return validators, nil +} + +func validatorPower(validator Validator) (int64, error) { + tokens, err := util.ParseAmount(validator.Tokens) + if err != nil { + return 0, err + } + if powerReduction <= 0 { + return 0, errors.New("invalid power reduction") + } + power := new(big.Int).Quo(tokens, big.NewInt(powerReduction)) + if !power.IsInt64() { + return 0, errors.New("validator power exceeds int64") + } + return power.Int64(), nil +} + +func validatorUpdate(validator Validator, power int64) precompiles.ValidatorUpdate { + return precompiles.ValidatorUpdate{ + PubKey: append([]byte(nil), validator.ConsensusPubkey...), + Power: power, + } +} + +func unbondAllMatureValidators(store precompiles.Store, block precompiles.BlockContext) error { + ids, err := matureValidatorQueueIDs(store, block.Time, block.Number) + if err != nil { + return err + } + for _, id := range ids { + validators, err := getStringList(store, validatorQueueKey(id)) + if err != nil { + return err + } + for _, validatorAddress := range validators { + validator, ok, err := getValidator(store, validatorAddress) + if err != nil { + return err + } + if !ok { + return errValidatorMissing + } + if validator.Status != bondStatusUnbonding { + return errors.New("validator queue contains a validator that is not unbonding") + } + validator.Status = bondStatusUnbonded + if err := setValidator(store, validator); err != nil { + return err + } + shares, err := util.ParseAmount(validator.DelegatorShares) + if err != nil { + return err + } + if shares.Sign() == 0 { + if err := removeValidator(store, validator.OperatorAddress); err != nil { + return err + } + } + } + store.Delete(validatorQueueKey(id)) + if err := removeStringListItem(store, validatorQueueIndexKey(), id); err != nil { + return err + } + } + return nil +} + +func completeMatureUnbondings(ctx *precompiles.EndBlockContext) error { + ids, err := matureTimeQueueIDs(ctx.Store, unbondingQueueIndexKey(), ctx.Block.Time) + if err != nil { + return err + } + for _, id := range ids { + pairs, err := getDelegationPairList(ctx.Store, unbondingQueueKey(id)) + if err != nil { + return err + } + for _, pair := range pairs { + if err := completeUnbonding(ctx, pair); err != nil { + return err + } + } + ctx.Store.Delete(unbondingQueueKey(id)) + if err := removeStringListItem(ctx.Store, unbondingQueueIndexKey(), id); err != nil { + return err + } + } + return nil +} + +func completeUnbonding(ctx *precompiles.EndBlockContext, pair delegationPair) error { + record, ok, err := getUnbondingDelegation(ctx.Store, pair.DelegatorAddress, pair.ValidatorAddress) + if err != nil || !ok { + return err + } + remaining := record.Entries[:0] + blockTime := saturatingInt64FromUint64(ctx.Block.Time) + for _, entry := range record.Entries { + if entry.CompletionTime > blockTime { + remaining = append(remaining, entry) + continue + } + amount, err := util.ParseAmount(entry.Balance) + if err != nil { + return err + } + if amount.Sign() != 0 { + if err := transferStakeFromEscrowToAddress(ctx.Balances, record.DelegatorAddress, amount); err != nil { + return err + } + if err := addPoolNotBonded(ctx.Store, new(big.Int).Neg(amount)); err != nil { + return err + } + } + } + if len(remaining) == 0 { + ctx.Store.Delete(unbondingDelegationKey(pair.DelegatorAddress, pair.ValidatorAddress)) + if err := removeStringListItem(ctx.Store, delegatorUnbondingsIndexKey(pair.DelegatorAddress), pair.ValidatorAddress); err != nil { + return err + } + return removeStringListItem(ctx.Store, validatorUnbondingsIndexKey(pair.ValidatorAddress), pair.DelegatorAddress) + } + record.Entries = remaining + return util.SetJSON(ctx.Store, unbondingDelegationKey(pair.DelegatorAddress, pair.ValidatorAddress), record) +} + +func completeMatureRedelegations(store precompiles.Store, block precompiles.BlockContext) error { + ids, err := matureTimeQueueIDs(store, redelegationQueueIndexKey(), block.Time) + if err != nil { + return err + } + for _, id := range ids { + triplets, err := getRedelegationTripletList(store, redelegationQueueKey(id)) + if err != nil { + return err + } + for _, triplet := range triplets { + if err := completeRedelegation(store, triplet, block.Time); err != nil { + return err + } + } + store.Delete(redelegationQueueKey(id)) + if err := removeStringListItem(store, redelegationQueueIndexKey(), id); err != nil { + return err + } + } + return nil +} + +func completeRedelegation(store precompiles.Store, triplet redelegationTriplet, blockTime uint64) error { + record, ok, err := getRedelegation(store, triplet.DelegatorAddress, triplet.ValidatorSrcAddress, triplet.ValidatorDstAddress) + if err != nil || !ok { + return err + } + remaining := record.Entries[:0] + completionTime := saturatingInt64FromUint64(blockTime) + for _, entry := range record.Entries { + if entry.CompletionTime > completionTime { + remaining = append(remaining, entry) + } + } + if len(remaining) == 0 { + store.Delete(redelegationKey(triplet.DelegatorAddress, triplet.ValidatorSrcAddress, triplet.ValidatorDstAddress)) + return removeStringListItem(store, redelegationsIndexKey(), redelegationID(triplet.DelegatorAddress, triplet.ValidatorSrcAddress, triplet.ValidatorDstAddress)) + } + record.Entries = remaining + return util.SetJSON(store, redelegationKey(triplet.DelegatorAddress, triplet.ValidatorSrcAddress, triplet.ValidatorDstAddress), record) +} diff --git a/giga/evmonly/precompiles/staking/events.go b/giga/evmonly/precompiles/staking/events.go new file mode 100644 index 0000000000..0d98fa0cdf --- /dev/null +++ b/giga/evmonly/precompiles/staking/events.go @@ -0,0 +1,16 @@ +package staking + +import ( + "github.com/ethereum/go-ethereum/common" + + "github.com/sei-protocol/sei-chain/giga/evmonly/precompiles" + "github.com/sei-protocol/sei-chain/giga/evmonly/precompiles/util" +) + +func (p *Precompile) emit(ctx *precompiles.Context, name string, indexed common.Address, args ...interface{}) { + event, ok := p.abi.Events[name] + if !ok { + return + } + util.EmitEvent(ctx.Logs, p.address, event, indexed, args...) +} diff --git a/giga/evmonly/precompiles/staking/helpers.go b/giga/evmonly/precompiles/staking/helpers.go new file mode 100644 index 0000000000..3ca29e2260 --- /dev/null +++ b/giga/evmonly/precompiles/staking/helpers.go @@ -0,0 +1,63 @@ +package staking + +import ( + "errors" + "fmt" + "math/big" + "strconv" + "strings" + + "github.com/sei-protocol/sei-chain/giga/evmonly/precompiles" +) + +func stakingValue(value *big.Int) (*big.Int, error) { + if value == nil || value.Sign() == 0 { + return nil, errors.New("set `value` field to non-zero to send delegate fund") + } + if value.Sign() < 0 { + return nil, errors.New("staking value cannot be negative") + } + usei, remainder := new(big.Int).QuoRem(value, useiToSwei, new(big.Int)) + if remainder.Sign() != 0 { + return nil, fmt.Errorf("selected precompile function does not allow payment with non-zero wei remainder: received %s", value) + } + if usei.Sign() == 0 { + return nil, errors.New("staking value is below one usei") + } + return usei, nil +} + +func validateWritable(ctx *precompiles.Context) error { + if ctx.ReadOnly { + return errReadOnly + } + return nil +} + +func statusMatches(filter string, status int32) bool { + if filter == "" { + return true + } + switch strings.ToUpper(filter) { + case "BOND_STATUS_UNSPECIFIED": + return status == 0 + case "BOND_STATUS_UNBONDED": + return status == 1 + case "BOND_STATUS_UNBONDING": + return status == 2 + case "BOND_STATUS_BONDED": + return status == 3 + default: + parsed, err := strconv.ParseInt(filter, 10, 32) + return err == nil && int32(parsed) == status //nolint:gosec // parsed is limited to 32 bits. + } +} + +func isTransaction(method string) bool { + switch method { + case DelegateMethod, RedelegateMethod, UndelegateMethod, CreateValidatorMethod, EditValidatorMethod: + return true + default: + return false + } +} diff --git a/giga/evmonly/precompiles/staking/staking.go b/giga/evmonly/precompiles/staking/staking.go new file mode 100644 index 0000000000..ff792641af --- /dev/null +++ b/giga/evmonly/precompiles/staking/staking.go @@ -0,0 +1,872 @@ +package staking + +import ( + "bytes" + "embed" + "encoding/hex" + "errors" + "fmt" + "math/big" + + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/common" + + "github.com/sei-protocol/sei-chain/giga/evmonly/precompiles" + "github.com/sei-protocol/sei-chain/giga/evmonly/precompiles/util" +) + +const ( + DelegateMethod = "delegate" + RedelegateMethod = "redelegate" + UndelegateMethod = "undelegate" + DelegationMethod = "delegation" + CreateValidatorMethod = "createValidator" + EditValidatorMethod = "editValidator" + ValidatorsMethod = "validators" + ValidatorMethod = "validator" + ValidatorDelegationsMethod = "validatorDelegations" + ValidatorUnbondingDelegationsMethod = "validatorUnbondingDelegations" + UnbondingDelegationMethod = "unbondingDelegation" + DelegatorDelegationsMethod = "delegatorDelegations" + DelegatorValidatorMethod = "delegatorValidator" + DelegatorUnbondingDelegationsMethod = "delegatorUnbondingDelegations" + RedelegationsMethod = "redelegations" + DelegatorValidatorsMethod = "delegatorValidators" + HistoricalInfoMethod = "historicalInfo" + PoolMethod = "pool" + ParamsMethod = "params" +) + +const ( + StakingAddress = "0x0000000000000000000000000000000000001005" + + unknownMethodGas uint64 = 3000 + readGas uint64 = 3000 + writeGas uint64 = 20000 + inputByteGas uint64 = 16 + + bondDenom = "usei" + precision int64 = 18 + pageLimit = 100 +) + +const ( + bondStatusUnspecified int32 = 0 + bondStatusUnbonded int32 = 1 + bondStatusUnbonding int32 = 2 + bondStatusBonded int32 = 3 + + powerReduction int64 = 1 +) + +var ( + address = common.HexToAddress(StakingAddress) + useiToSwei = big.NewInt(1_000_000_000_000) + errReadOnly = errors.New("cannot call staking precompile from staticcall") + errDelegateCall = errors.New("cannot delegatecall staking") + errMissingStore = errors.New("staking precompile requires a store") + errMissingBalanceTransfer = errors.New("staking precompile requires balance transfer") + errValidatorMissing = errors.New("validator not found") +) + +//go:embed abi.json +var abiFS embed.FS + +// Precompile is the SDK-free staking custom precompile for the evm-only path. +type Precompile struct { + abi abi.ABI + address common.Address +} + +// Registry exposes only the staking precompile to the evm-only executor. +type Registry struct { + contract *Precompile +} + +// NewPrecompile constructs the staking precompile without Cosmos keepers or +// sdk.Context dependencies. +func NewPrecompile() (*Precompile, error) { + abiBz, err := abiFS.ReadFile("abi.json") + if err != nil { + return nil, err + } + parsedABI, err := abi.JSON(bytes.NewReader(abiBz)) + if err != nil { + return nil, err + } + return &Precompile{abi: parsedABI, address: address}, nil +} + +// NewRegistry returns a registry containing the staking precompile. +func NewRegistry() (Registry, error) { + contract, err := NewPrecompile() + if err != nil { + return Registry{}, err + } + return Registry{contract: contract}, nil +} + +func (r Registry) Get(addr common.Address) (precompiles.Contract, bool) { + if addr != address || r.contract == nil { + return nil, false + } + return r.contract, true +} + +func (r Registry) Addresses() []common.Address { + return []common.Address{address} +} + +func (p *Precompile) Address() common.Address { + return p.address +} + +func (p *Precompile) ABI() abi.ABI { + return p.abi +} + +func (p *Precompile) RequiredGas(input []byte) uint64 { + method, _, err := p.prepare(input) + if err != nil { + return unknownMethodGas + } + gas := readGas + if isTransaction(method.Name) { + gas = writeGas + } + return gas + inputByteGas*uint64(len(input)) //nolint:gosec // input length is bounded by memory. +} + +func (p *Precompile) Run(ctx *precompiles.Context, input []byte) ([]byte, error) { + if ctx.DelegateCall { + return nil, errDelegateCall + } + if ctx.Store == nil { + return nil, errMissingStore + } + method, args, err := p.prepare(input) + if err != nil { + return nil, err + } + switch method.Name { + case DelegateMethod: + return p.delegate(ctx, method, args) + case RedelegateMethod: + return p.redelegate(ctx, method, args) + case UndelegateMethod: + return p.undelegate(ctx, method, args) + case CreateValidatorMethod: + return p.createValidator(ctx, method, args) + case EditValidatorMethod: + return p.editValidator(ctx, method, args) + case DelegationMethod: + return p.delegation(ctx, method, args) + case ValidatorsMethod: + return p.validators(ctx, method, args) + case ValidatorMethod: + return p.validator(ctx, method, args) + case ValidatorDelegationsMethod: + return p.validatorDelegations(ctx, method, args) + case ValidatorUnbondingDelegationsMethod: + return p.validatorUnbondingDelegations(ctx, method, args) + case UnbondingDelegationMethod: + return p.unbondingDelegation(ctx, method, args) + case DelegatorDelegationsMethod: + return p.delegatorDelegations(ctx, method, args) + case DelegatorValidatorMethod: + return p.delegatorValidator(ctx, method, args) + case DelegatorUnbondingDelegationsMethod: + return p.delegatorUnbondingDelegations(ctx, method, args) + case RedelegationsMethod: + return p.redelegations(ctx, method, args) + case DelegatorValidatorsMethod: + return p.delegatorValidators(ctx, method, args) + case HistoricalInfoMethod: + return p.historicalInfo(ctx, method, args) + case PoolMethod: + return p.pool(ctx, method) + case ParamsMethod: + return p.params(ctx, method) + default: + return nil, fmt.Errorf("unsupported staking method %s", method.Name) + } +} + +func (p *Precompile) prepare(input []byte) (*abi.Method, []interface{}, error) { + if len(input) < 4 { + return nil, nil, errors.New("input too short to extract method ID") + } + method, err := p.abi.MethodById(input[:4]) + if err != nil { + return nil, nil, err + } + args, err := method.Inputs.Unpack(input[4:]) + if err != nil { + return nil, nil, err + } + return method, args, nil +} + +func (p *Precompile) delegate(ctx *precompiles.Context, method *abi.Method, args []interface{}) ([]byte, error) { + if err := validateWritable(ctx); err != nil { + return nil, err + } + if err := util.ValidateArgsLength(args, 1); err != nil { + return nil, err + } + validatorAddress := args[0].(string) + validator, ok, err := getValidator(ctx.Store, validatorAddress) + if err != nil { + return nil, err + } + if !ok { + return nil, errValidatorMissing + } + useiAmount, err := stakingValue(ctx.ApparentValue) + if err != nil { + return nil, err + } + if err := transferPrecompileValueToEscrow(ctx); err != nil { + return nil, err + } + delegator := util.AddressString(ctx.Caller) + if err := addDelegation(ctx.Store, delegator, validatorAddress, useiAmount); err != nil { + return nil, err + } + if err := addValidatorTokens(ctx.Store, validatorAddress, useiAmount); err != nil { + return nil, err + } + if validator.Status == bondStatusBonded { + if err := addPoolBonded(ctx.Store, useiAmount); err != nil { + return nil, err + } + } else if err := addPoolNotBonded(ctx.Store, useiAmount); err != nil { + return nil, err + } + p.emit(ctx, "Delegate", ctx.Caller, validatorAddress, util.CloneBig(ctx.ApparentValue)) + p.emit(ctx, "DelegationRewardsWithdrawn", ctx.Caller, validatorAddress, new(big.Int)) + return method.Outputs.Pack(true) +} + +func (p *Precompile) redelegate(ctx *precompiles.Context, method *abi.Method, args []interface{}) ([]byte, error) { + if err := validateWritable(ctx); err != nil { + return nil, err + } + if err := util.ValidateNonPayable(ctx.ApparentValue); err != nil { + return nil, err + } + if err := util.ValidateArgsLength(args, 3); err != nil { + return nil, err + } + delegator := util.AddressString(ctx.Caller) + srcValidator := args[0].(string) + dstValidator := args[1].(string) + amount := args[2].(*big.Int) + if err := util.ValidatePositiveAmount(amount, "redelegation amount"); err != nil { + return nil, err + } + src, ok, err := getValidator(ctx.Store, srcValidator) + if err != nil { + return nil, err + } + if !ok { + return nil, fmt.Errorf("source %w", errValidatorMissing) + } + dst, ok, err := getValidator(ctx.Store, dstValidator) + if err != nil { + return nil, err + } + if !ok { + return nil, fmt.Errorf("destination %w", errValidatorMissing) + } + if err := validateDelegationAmount(ctx.Store, delegator, srcValidator, amount); err != nil { + return nil, err + } + if err := addDelegation(ctx.Store, delegator, srcValidator, new(big.Int).Neg(amount)); err != nil { + return nil, err + } + if err := addDelegation(ctx.Store, delegator, dstValidator, amount); err != nil { + return nil, err + } + if err := addValidatorTokens(ctx.Store, srcValidator, new(big.Int).Neg(amount)); err != nil { + return nil, err + } + if err := addValidatorTokens(ctx.Store, dstValidator, amount); err != nil { + return nil, err + } + if err := movePoolsForRedelegation(ctx.Store, src.Status, dst.Status, amount); err != nil { + return nil, err + } + params, err := loadParams(ctx.Store) + if err != nil { + return nil, err + } + if err := addRedelegation(ctx.Store, delegator, srcValidator, dstValidator, amount, util.SaturatingCompletionTime(ctx.Block.Time, params.UnbondingTime)); err != nil { + return nil, err + } + p.emit(ctx, "Redelegate", ctx.Caller, srcValidator, dstValidator, amount) + p.emit(ctx, "DelegationRewardsWithdrawn", ctx.Caller, srcValidator, new(big.Int)) + p.emit(ctx, "DelegationRewardsWithdrawn", ctx.Caller, dstValidator, new(big.Int)) + return method.Outputs.Pack(true) +} + +func (p *Precompile) undelegate(ctx *precompiles.Context, method *abi.Method, args []interface{}) ([]byte, error) { + if err := validateWritable(ctx); err != nil { + return nil, err + } + if err := util.ValidateNonPayable(ctx.ApparentValue); err != nil { + return nil, err + } + if err := util.ValidateArgsLength(args, 2); err != nil { + return nil, err + } + delegator := util.AddressString(ctx.Caller) + validatorAddress := args[0].(string) + amount := args[1].(*big.Int) + if err := util.ValidatePositiveAmount(amount, "undelegation amount"); err != nil { + return nil, err + } + validator, ok, err := getValidator(ctx.Store, validatorAddress) + if err != nil { + return nil, err + } + if !ok { + return nil, errValidatorMissing + } + if err := validateDelegationAmount(ctx.Store, delegator, validatorAddress, amount); err != nil { + return nil, err + } + if err := addDelegation(ctx.Store, delegator, validatorAddress, new(big.Int).Neg(amount)); err != nil { + return nil, err + } + if err := addValidatorTokens(ctx.Store, validatorAddress, new(big.Int).Neg(amount)); err != nil { + return nil, err + } + params, err := loadParams(ctx.Store) + if err != nil { + return nil, err + } + if err := addUnbondingDelegation(ctx.Store, delegator, validatorAddress, amount, ctx.Block.Number, util.SaturatingCompletionTime(ctx.Block.Time, params.UnbondingTime)); err != nil { + return nil, err + } + if validator.Status == bondStatusBonded { + if err := addPoolBonded(ctx.Store, new(big.Int).Neg(amount)); err != nil { + return nil, err + } + if err := addPoolNotBonded(ctx.Store, amount); err != nil { + return nil, err + } + } + p.emit(ctx, "Undelegate", ctx.Caller, validatorAddress, amount) + p.emit(ctx, "DelegationRewardsWithdrawn", ctx.Caller, validatorAddress, new(big.Int)) + return method.Outputs.Pack(true) +} + +func (p *Precompile) createValidator(ctx *precompiles.Context, method *abi.Method, args []interface{}) ([]byte, error) { + if err := validateWritable(ctx); err != nil { + return nil, err + } + if err := util.ValidateArgsLength(args, 6); err != nil { + return nil, err + } + pubKeyHex := args[0].(string) + moniker := args[1].(string) + commissionRate := args[2].(string) + commissionMaxRate := args[3].(string) + commissionMaxChangeRate := args[4].(string) + minSelfDelegation := args[5].(*big.Int) + pubKey, err := hex.DecodeString(pubKeyHex) + if err != nil { + return nil, errors.New("invalid public key hex format") + } + if err := util.ValidateDecimal(commissionRate, "commission rate"); err != nil { + return nil, err + } + if err := util.ValidateDecimal(commissionMaxRate, "commission max rate"); err != nil { + return nil, err + } + if err := util.ValidateDecimal(commissionMaxChangeRate, "commission max change rate"); err != nil { + return nil, err + } + if err := util.ValidatePositiveAmount(minSelfDelegation, "minimum self delegation"); err != nil { + return nil, errors.New("minimum self delegation must be a positive integer: invalid request") + } + selfDelegation, err := stakingValue(ctx.ApparentValue) + if err != nil { + return nil, err + } + if selfDelegation.Cmp(minSelfDelegation) < 0 { + return nil, errors.New("self delegation is below minimum self delegation") + } + validatorAddress := util.AddressString(ctx.Caller) + if _, exists, err := getValidator(ctx.Store, validatorAddress); err != nil { + return nil, err + } else if exists { + return nil, errors.New("validator already exists") + } + if err := transferPrecompileValueToEscrow(ctx); err != nil { + return nil, err + } + validator := Validator{ + OperatorAddress: validatorAddress, + ConsensusPubkey: pubKey, + Jailed: false, + Status: bondStatusUnbonded, + Tokens: selfDelegation.String(), + DelegatorShares: selfDelegation.String(), + Description: moniker, + UnbondingHeight: 0, + UnbondingTime: 0, + CommissionRate: commissionRate, + CommissionMaxRate: commissionMaxRate, + CommissionMaxChangeRate: commissionMaxChangeRate, + CommissionUpdateTime: saturatingInt64FromUint64(ctx.Block.Time), + MinSelfDelegation: minSelfDelegation.String(), + } + if err := setValidator(ctx.Store, validator); err != nil { + return nil, err + } + if err := addDelegation(ctx.Store, validatorAddress, validatorAddress, selfDelegation); err != nil { + return nil, err + } + if err := addPoolNotBonded(ctx.Store, selfDelegation); err != nil { + return nil, err + } + if err := setHistoricalInfo(ctx.Store, ctx.Block.Number); err != nil { + return nil, err + } + p.emit(ctx, "ValidatorCreated", ctx.Caller, validatorAddress, moniker) + return method.Outputs.Pack(true) +} + +func (p *Precompile) editValidator(ctx *precompiles.Context, method *abi.Method, args []interface{}) ([]byte, error) { + if err := validateWritable(ctx); err != nil { + return nil, err + } + if err := util.ValidateNonPayable(ctx.ApparentValue); err != nil { + return nil, err + } + if err := util.ValidateArgsLength(args, 3); err != nil { + return nil, err + } + validatorAddress := util.AddressString(ctx.Caller) + validator, ok, err := getValidator(ctx.Store, validatorAddress) + if err != nil { + return nil, err + } + if !ok { + return nil, errValidatorMissing + } + moniker := args[0].(string) + commissionRate := args[1].(string) + minSelfDelegation := args[2].(*big.Int) + if moniker != "" { + validator.Description = moniker + } + if commissionRate != "" { + if err := util.ValidateDecimal(commissionRate, "commission rate"); err != nil { + return nil, err + } + validator.CommissionRate = commissionRate + validator.CommissionUpdateTime = saturatingInt64FromUint64(ctx.Block.Time) + } + if minSelfDelegation != nil && minSelfDelegation.Sign() > 0 { + validator.MinSelfDelegation = minSelfDelegation.String() + } + if err := setValidator(ctx.Store, validator); err != nil { + return nil, err + } + if err := setHistoricalInfo(ctx.Store, ctx.Block.Number); err != nil { + return nil, err + } + p.emit(ctx, "ValidatorEdited", ctx.Caller, validatorAddress, moniker) + return method.Outputs.Pack(true) +} + +func (p *Precompile) delegation(ctx *precompiles.Context, method *abi.Method, args []interface{}) ([]byte, error) { + if err := util.ValidateNonPayable(ctx.ApparentValue); err != nil { + return nil, err + } + if err := util.ValidateArgsLength(args, 2); err != nil { + return nil, err + } + delegator := util.AddressString(args[0].(common.Address)) + validatorAddress := args[1].(string) + record, ok, err := getDelegation(ctx.Store, delegator, validatorAddress) + if err != nil { + return nil, err + } + if !ok { + return nil, errors.New("delegation not found") + } + delegation, err := delegationFromRecord(record) + if err != nil { + return nil, err + } + return method.Outputs.Pack(delegation) +} + +func (p *Precompile) validators(ctx *precompiles.Context, method *abi.Method, args []interface{}) ([]byte, error) { + if err := util.ValidateNonPayable(ctx.ApparentValue); err != nil { + return nil, err + } + if err := util.ValidateArgsLength(args, 2); err != nil { + return nil, err + } + status := args[0].(string) + nextKey := args[1].([]byte) + validatorAddresses, err := getStringList(ctx.Store, validatorsIndexKey()) + if err != nil { + return nil, err + } + filtered := make([]string, 0, len(validatorAddresses)) + for _, validatorAddress := range validatorAddresses { + validator, ok, err := getValidator(ctx.Store, validatorAddress) + if err != nil { + return nil, err + } + if ok && statusMatches(status, validator.Status) { + filtered = append(filtered, validatorAddress) + } + } + page, outNextKey, err := pageStrings(filtered, nextKey) + if err != nil { + return nil, err + } + result := ValidatorsResponse{Validators: make([]Validator, 0, len(page)), NextKey: outNextKey} + for _, validatorAddress := range page { + validator, ok, err := getValidator(ctx.Store, validatorAddress) + if err != nil { + return nil, err + } + if ok { + result.Validators = append(result.Validators, validator) + } + } + return method.Outputs.Pack(result) +} + +func (p *Precompile) validator(ctx *precompiles.Context, method *abi.Method, args []interface{}) ([]byte, error) { + if err := util.ValidateNonPayable(ctx.ApparentValue); err != nil { + return nil, err + } + if err := util.ValidateArgsLength(args, 1); err != nil { + return nil, err + } + validator, ok, err := getValidator(ctx.Store, args[0].(string)) + if err != nil { + return nil, err + } + if !ok { + return nil, errValidatorMissing + } + return method.Outputs.Pack(validator) +} + +func (p *Precompile) validatorDelegations(ctx *precompiles.Context, method *abi.Method, args []interface{}) ([]byte, error) { + if err := util.ValidateNonPayable(ctx.ApparentValue); err != nil { + return nil, err + } + if err := util.ValidateArgsLength(args, 2); err != nil { + return nil, err + } + validatorAddress := args[0].(string) + nextKey := args[1].([]byte) + delegators, err := getStringList(ctx.Store, validatorDelegationsIndexKey(validatorAddress)) + if err != nil { + return nil, err + } + page, outNextKey, err := pageStrings(delegators, nextKey) + if err != nil { + return nil, err + } + result := DelegationsResponse{Delegations: make([]Delegation, 0, len(page)), NextKey: outNextKey} + for _, delegator := range page { + record, ok, err := getDelegation(ctx.Store, delegator, validatorAddress) + if err != nil { + return nil, err + } + if !ok { + continue + } + delegation, err := delegationFromRecord(record) + if err != nil { + return nil, err + } + result.Delegations = append(result.Delegations, delegation) + } + return method.Outputs.Pack(result) +} + +func (p *Precompile) validatorUnbondingDelegations(ctx *precompiles.Context, method *abi.Method, args []interface{}) ([]byte, error) { + if err := util.ValidateNonPayable(ctx.ApparentValue); err != nil { + return nil, err + } + if err := util.ValidateArgsLength(args, 2); err != nil { + return nil, err + } + validatorAddress := args[0].(string) + nextKey := args[1].([]byte) + delegators, err := getStringList(ctx.Store, validatorUnbondingsIndexKey(validatorAddress)) + if err != nil { + return nil, err + } + page, outNextKey, err := pageStrings(delegators, nextKey) + if err != nil { + return nil, err + } + result := UnbondingDelegationsResponse{UnbondingDelegations: make([]UnbondingDelegation, 0, len(page)), NextKey: outNextKey} + for _, delegator := range page { + record, ok, err := getUnbondingDelegation(ctx.Store, delegator, validatorAddress) + if err != nil { + return nil, err + } + if ok { + result.UnbondingDelegations = append(result.UnbondingDelegations, record) + } + } + return method.Outputs.Pack(result) +} + +func (p *Precompile) unbondingDelegation(ctx *precompiles.Context, method *abi.Method, args []interface{}) ([]byte, error) { + if err := util.ValidateNonPayable(ctx.ApparentValue); err != nil { + return nil, err + } + if err := util.ValidateArgsLength(args, 2); err != nil { + return nil, err + } + delegator := util.AddressString(args[0].(common.Address)) + validatorAddress := args[1].(string) + record, ok, err := getUnbondingDelegation(ctx.Store, delegator, validatorAddress) + if err != nil { + return nil, err + } + if !ok { + return nil, errors.New("unbonding delegation not found") + } + return method.Outputs.Pack(record) +} + +func (p *Precompile) delegatorDelegations(ctx *precompiles.Context, method *abi.Method, args []interface{}) ([]byte, error) { + if err := util.ValidateNonPayable(ctx.ApparentValue); err != nil { + return nil, err + } + if err := util.ValidateArgsLength(args, 2); err != nil { + return nil, err + } + delegator := util.AddressString(args[0].(common.Address)) + nextKey := args[1].([]byte) + validators, err := getStringList(ctx.Store, delegatorDelegationsIndexKey(delegator)) + if err != nil { + return nil, err + } + page, outNextKey, err := pageStrings(validators, nextKey) + if err != nil { + return nil, err + } + result := DelegationsResponse{Delegations: make([]Delegation, 0, len(page)), NextKey: outNextKey} + for _, validatorAddress := range page { + record, ok, err := getDelegation(ctx.Store, delegator, validatorAddress) + if err != nil { + return nil, err + } + if !ok { + continue + } + delegation, err := delegationFromRecord(record) + if err != nil { + return nil, err + } + result.Delegations = append(result.Delegations, delegation) + } + return method.Outputs.Pack(result) +} + +func (p *Precompile) delegatorValidator(ctx *precompiles.Context, method *abi.Method, args []interface{}) ([]byte, error) { + if err := util.ValidateNonPayable(ctx.ApparentValue); err != nil { + return nil, err + } + if err := util.ValidateArgsLength(args, 2); err != nil { + return nil, err + } + delegator := util.AddressString(args[0].(common.Address)) + validatorAddress := args[1].(string) + if _, ok, err := getDelegation(ctx.Store, delegator, validatorAddress); err != nil { + return nil, err + } else if !ok { + return nil, errors.New("delegation not found") + } + validator, ok, err := getValidator(ctx.Store, validatorAddress) + if err != nil { + return nil, err + } + if !ok { + return nil, errValidatorMissing + } + return method.Outputs.Pack(validator) +} + +func (p *Precompile) delegatorUnbondingDelegations(ctx *precompiles.Context, method *abi.Method, args []interface{}) ([]byte, error) { + if err := util.ValidateNonPayable(ctx.ApparentValue); err != nil { + return nil, err + } + if err := util.ValidateArgsLength(args, 2); err != nil { + return nil, err + } + delegator := util.AddressString(args[0].(common.Address)) + nextKey := args[1].([]byte) + validators, err := getStringList(ctx.Store, delegatorUnbondingsIndexKey(delegator)) + if err != nil { + return nil, err + } + page, outNextKey, err := pageStrings(validators, nextKey) + if err != nil { + return nil, err + } + result := UnbondingDelegationsResponse{UnbondingDelegations: make([]UnbondingDelegation, 0, len(page)), NextKey: outNextKey} + for _, validatorAddress := range page { + record, ok, err := getUnbondingDelegation(ctx.Store, delegator, validatorAddress) + if err != nil { + return nil, err + } + if ok { + result.UnbondingDelegations = append(result.UnbondingDelegations, record) + } + } + return method.Outputs.Pack(result) +} + +func (p *Precompile) redelegations(ctx *precompiles.Context, method *abi.Method, args []interface{}) ([]byte, error) { + if err := util.ValidateNonPayable(ctx.ApparentValue); err != nil { + return nil, err + } + if err := util.ValidateArgsLength(args, 4); err != nil { + return nil, err + } + delegatorFilter := args[0].(string) + srcFilter := args[1].(string) + dstFilter := args[2].(string) + nextKey := args[3].([]byte) + ids, err := getStringList(ctx.Store, redelegationsIndexKey()) + if err != nil { + return nil, err + } + filtered := make([]string, 0, len(ids)) + for _, id := range ids { + delegator, src, dst, ok := splitRedelegationID(id) + if !ok { + continue + } + if delegatorFilter != "" && delegatorFilter != delegator { + continue + } + if srcFilter != "" && srcFilter != src { + continue + } + if dstFilter != "" && dstFilter != dst { + continue + } + filtered = append(filtered, id) + } + page, outNextKey, err := pageStrings(filtered, nextKey) + if err != nil { + return nil, err + } + result := RedelegationsResponse{Redelegations: make([]Redelegation, 0, len(page)), NextKey: outNextKey} + for _, id := range page { + delegator, src, dst, ok := splitRedelegationID(id) + if !ok { + continue + } + record, ok, err := getRedelegation(ctx.Store, delegator, src, dst) + if err != nil { + return nil, err + } + if ok { + result.Redelegations = append(result.Redelegations, record) + } + } + return method.Outputs.Pack(result) +} + +func (p *Precompile) delegatorValidators(ctx *precompiles.Context, method *abi.Method, args []interface{}) ([]byte, error) { + if err := util.ValidateNonPayable(ctx.ApparentValue); err != nil { + return nil, err + } + if err := util.ValidateArgsLength(args, 2); err != nil { + return nil, err + } + delegator := util.AddressString(args[0].(common.Address)) + nextKey := args[1].([]byte) + validators, err := getStringList(ctx.Store, delegatorDelegationsIndexKey(delegator)) + if err != nil { + return nil, err + } + page, outNextKey, err := pageStrings(validators, nextKey) + if err != nil { + return nil, err + } + result := ValidatorsResponse{Validators: make([]Validator, 0, len(page)), NextKey: outNextKey} + for _, validatorAddress := range page { + validator, ok, err := getValidator(ctx.Store, validatorAddress) + if err != nil { + return nil, err + } + if ok { + result.Validators = append(result.Validators, validator) + } + } + return method.Outputs.Pack(result) +} + +func (p *Precompile) historicalInfo(ctx *precompiles.Context, method *abi.Method, args []interface{}) ([]byte, error) { + if err := util.ValidateNonPayable(ctx.ApparentValue); err != nil { + return nil, err + } + if err := util.ValidateArgsLength(args, 1); err != nil { + return nil, err + } + height := args[0].(int64) + info, ok, err := getHistoricalInfo(ctx.Store, height) + if err != nil { + return nil, err + } + if !ok { + if height < 0 || uint64(height) != ctx.Block.Number { + return nil, errors.New("historical info not found") + } + if err := setHistoricalInfo(ctx.Store, ctx.Block.Number); err != nil { + return nil, err + } + info, ok, err = getHistoricalInfo(ctx.Store, height) + if err != nil { + return nil, err + } + if !ok { + return nil, errors.New("historical info not found") + } + } + return method.Outputs.Pack(info) +} + +func (p *Precompile) pool(ctx *precompiles.Context, method *abi.Method) ([]byte, error) { + if err := util.ValidateNonPayable(ctx.ApparentValue); err != nil { + return nil, err + } + pool, err := loadPool(ctx.Store) + if err != nil { + return nil, err + } + return method.Outputs.Pack(pool) +} + +func (p *Precompile) params(ctx *precompiles.Context, method *abi.Method) ([]byte, error) { + if err := util.ValidateNonPayable(ctx.ApparentValue); err != nil { + return nil, err + } + params, err := loadParams(ctx.Store) + if err != nil { + return nil, err + } + return method.Outputs.Pack(params) +} diff --git a/giga/evmonly/precompiles/staking/staking_test.go b/giga/evmonly/precompiles/staking/staking_test.go new file mode 100644 index 0000000000..321a5dbbb9 --- /dev/null +++ b/giga/evmonly/precompiles/staking/staking_test.go @@ -0,0 +1,295 @@ +package staking + +import ( + "errors" + "math/big" + "testing" + + "github.com/ethereum/go-ethereum/common" + ethtypes "github.com/ethereum/go-ethereum/core/types" + "github.com/stretchr/testify/require" + + "github.com/sei-protocol/sei-chain/giga/evmonly/precompiles" +) + +func TestPrecompileCreateDelegateAndQuery(t *testing.T) { + p, err := NewPrecompile() + require.NoError(t, err) + + caller := common.HexToAddress("0x0000000000000000000000000000000000000abc") + store := newMemoryStore() + logs := &memoryLogs{} + balances := newMemoryBalances() + ctx := &precompiles.Context{ + Caller: caller, + Address: address, + ApparentValue: new(big.Int).Mul(big.NewInt(5), useiToSwei), + Block: precompiles.BlockContext{Number: 7, Time: 100}, + Store: store, + Balances: balances, + Logs: logs, + } + + input, err := p.abi.Pack( + CreateValidatorMethod, + "01020304", + "validator-one", + "0.100000000000000000", + "0.200000000000000000", + "0.010000000000000000", + big.NewInt(1), + ) + require.NoError(t, err) + balances.add(address, ctx.ApparentValue) + ret, err := p.Run(ctx, input) + require.NoError(t, err) + requireBoolReturn(t, p, CreateValidatorMethod, ret, true) + require.Len(t, logs.logs, 1) + require.Equal(t, new(big.Int).Mul(big.NewInt(5), useiToSwei), balances.balance(EscrowAddress())) + require.Zero(t, balances.balance(caller).Sign()) + require.Zero(t, balances.balance(address).Sign()) + + ctx.ApparentValue = new(big.Int).Mul(big.NewInt(2), useiToSwei) + input, err = p.abi.Pack(DelegateMethod, caller.Hex()) + require.NoError(t, err) + balances.add(address, ctx.ApparentValue) + ret, err = p.Run(ctx, input) + require.NoError(t, err) + requireBoolReturn(t, p, DelegateMethod, ret, true) + require.Len(t, logs.logs, 3) + require.Equal(t, new(big.Int).Mul(big.NewInt(7), useiToSwei), balances.balance(EscrowAddress())) + require.Zero(t, balances.balance(caller).Sign()) + require.Zero(t, balances.balance(address).Sign()) + + updates, err := p.EndBlock(&precompiles.EndBlockContext{ + Address: address, + Block: ctx.Block, + Store: store, + Balances: balances, + Logs: logs, + }) + require.NoError(t, err) + require.Len(t, updates, 1) + + ctx.ApparentValue = nil + input, err = p.abi.Pack(DelegationMethod, caller, caller.Hex()) + require.NoError(t, err) + ret, err = p.Run(ctx, input) + require.NoError(t, err) + var delegationOut struct { + Delegation Delegation + } + require.NoError(t, p.abi.UnpackIntoInterface(&delegationOut, DelegationMethod, ret)) + delegation := delegationOut.Delegation + require.Equal(t, big.NewInt(7), delegation.Balance.Amount) + require.Equal(t, "usei", delegation.Balance.Denom) + require.Equal(t, caller.Hex(), delegation.Delegation.DelegatorAddress) + require.Equal(t, caller.Hex(), delegation.Delegation.ValidatorAddress) + + input, err = p.abi.Pack(PoolMethod) + require.NoError(t, err) + ret, err = p.Run(ctx, input) + require.NoError(t, err) + var poolOut struct { + Pool Pool + } + require.NoError(t, p.abi.UnpackIntoInterface(&poolOut, PoolMethod, ret)) + pool := poolOut.Pool + require.Equal(t, "7", pool.BondedTokens) + require.Equal(t, "0", pool.NotBondedTokens) + + input, err = p.abi.Pack(ValidatorsMethod, "BOND_STATUS_BONDED", []byte{}) + require.NoError(t, err) + ret, err = p.Run(ctx, input) + require.NoError(t, err) + var validatorsOut struct { + Response ValidatorsResponse + } + require.NoError(t, p.abi.UnpackIntoInterface(&validatorsOut, ValidatorsMethod, ret)) + validators := validatorsOut.Response + require.Len(t, validators.Validators, 1) + require.Equal(t, caller.Hex(), validators.Validators[0].OperatorAddress) + require.Empty(t, validators.NextKey) +} + +func TestPrecompileMovesNativeBalancesForStakingTransitions(t *testing.T) { + p, err := NewPrecompile() + require.NoError(t, err) + + delegator := common.HexToAddress("0x0000000000000000000000000000000000000abc") + dstValidator := common.HexToAddress("0x0000000000000000000000000000000000000def") + store := newMemoryStore() + balances := newMemoryBalances() + ctx := &precompiles.Context{ + Caller: delegator, + Address: address, + ApparentValue: new(big.Int).Mul(big.NewInt(5), useiToSwei), + Block: precompiles.BlockContext{Number: 7, Time: 100}, + Store: store, + Balances: balances, + Logs: &memoryLogs{}, + } + + balances.add(address, ctx.ApparentValue) + input, err := p.abi.Pack( + CreateValidatorMethod, + "01020304", + "validator-one", + "0.100000000000000000", + "0.200000000000000000", + "0.010000000000000000", + big.NewInt(1), + ) + require.NoError(t, err) + _, err = p.Run(ctx, input) + require.NoError(t, err) + + ctx.Caller = dstValidator + ctx.ApparentValue = new(big.Int).Mul(big.NewInt(1), useiToSwei) + balances.add(address, ctx.ApparentValue) + input, err = p.abi.Pack( + CreateValidatorMethod, + "05060708", + "validator-two", + "0.100000000000000000", + "0.200000000000000000", + "0.010000000000000000", + big.NewInt(1), + ) + require.NoError(t, err) + _, err = p.Run(ctx, input) + require.NoError(t, err) + + ctx.Caller = delegator + ctx.ApparentValue = nil + input, err = p.abi.Pack(RedelegateMethod, delegator.Hex(), dstValidator.Hex(), big.NewInt(2)) + require.NoError(t, err) + _, err = p.Run(ctx, input) + require.NoError(t, err) + src, ok, err := getValidator(store, delegator.Hex()) + require.NoError(t, err) + require.True(t, ok) + require.Equal(t, "3", src.Tokens) + dst, ok, err := getValidator(store, dstValidator.Hex()) + require.NoError(t, err) + require.True(t, ok) + require.Equal(t, "3", dst.Tokens) + require.Equal(t, new(big.Int).Mul(big.NewInt(6), useiToSwei), balances.balance(EscrowAddress())) + require.Zero(t, balances.balance(delegator).Sign()) + require.Zero(t, balances.balance(dstValidator).Sign()) + + input, err = p.abi.Pack(UndelegateMethod, dstValidator.Hex(), big.NewInt(1)) + require.NoError(t, err) + _, err = p.Run(ctx, input) + require.NoError(t, err) + require.Equal(t, new(big.Int).Mul(big.NewInt(6), useiToSwei), balances.balance(EscrowAddress())) + require.Zero(t, balances.balance(delegator).Sign()) + require.Zero(t, balances.balance(dstValidator).Sign()) + require.Zero(t, balances.balance(address).Sign()) + + _, err = p.EndBlock(&precompiles.EndBlockContext{ + Address: address, + Block: precompiles.BlockContext{ + Number: 8, + Time: 100 + 1_814_400, + }, + Store: store, + Balances: balances, + Logs: &memoryLogs{}, + }) + require.NoError(t, err) + require.Equal(t, new(big.Int).Mul(big.NewInt(5), useiToSwei), balances.balance(EscrowAddress())) + require.Equal(t, new(big.Int).Mul(big.NewInt(1), useiToSwei), balances.balance(delegator)) +} + +func TestPrecompileRejectsDelegateCall(t *testing.T) { + p, err := NewPrecompile() + require.NoError(t, err) + + input, err := p.abi.Pack(PoolMethod) + require.NoError(t, err) + _, err = p.Run(&precompiles.Context{ + DelegateCall: true, + Store: newMemoryStore(), + }, input) + require.ErrorIs(t, err, errDelegateCall) +} + +func requireBoolReturn(t *testing.T, p *Precompile, method string, ret []byte, expected bool) { + t.Helper() + values, err := p.abi.Unpack(method, ret) + require.NoError(t, err) + require.Len(t, values, 1) + require.Equal(t, expected, values[0]) +} + +type memoryStore struct { + values map[string][]byte +} + +func newMemoryStore() *memoryStore { + return &memoryStore{values: map[string][]byte{}} +} + +func (s *memoryStore) Get(key []byte) ([]byte, bool) { + value, ok := s.values[string(key)] + if !ok { + return nil, false + } + return append([]byte(nil), value...), true +} + +func (s *memoryStore) Set(key []byte, value []byte) { + s.values[string(key)] = append([]byte(nil), value...) +} + +func (s *memoryStore) Delete(key []byte) { + delete(s.values, string(key)) +} + +type memoryLogs struct { + logs []*ethtypes.Log +} + +func (l *memoryLogs) AddLog(log *ethtypes.Log) { + l.logs = append(l.logs, log) +} + +type memoryBalances struct { + balances map[common.Address]*big.Int +} + +func newMemoryBalances() *memoryBalances { + return &memoryBalances{balances: map[common.Address]*big.Int{}} +} + +func (b *memoryBalances) Transfer(from common.Address, to common.Address, amount *big.Int) error { + if amount == nil || amount.Sign() == 0 { + return nil + } + if amount.Sign() < 0 { + return errors.New("negative amount") + } + fromBalance := b.balance(from) + if fromBalance.Cmp(amount) < 0 { + return errors.New("insufficient balance") + } + b.balances[from] = new(big.Int).Sub(fromBalance, amount) + b.balances[to] = new(big.Int).Add(b.balance(to), amount) + return nil +} + +func (b *memoryBalances) add(addr common.Address, amount *big.Int) { + if amount == nil || amount.Sign() == 0 { + return + } + b.balances[addr] = new(big.Int).Add(b.balance(addr), amount) +} + +func (b *memoryBalances) balance(addr common.Address) *big.Int { + balance, ok := b.balances[addr] + if !ok { + return new(big.Int) + } + return new(big.Int).Set(balance) +} diff --git a/giga/evmonly/precompiles/staking/state.go b/giga/evmonly/precompiles/staking/state.go new file mode 100644 index 0000000000..b551ab14dd --- /dev/null +++ b/giga/evmonly/precompiles/staking/state.go @@ -0,0 +1,677 @@ +package staking + +import ( + "errors" + "math/big" + "sort" + "strconv" + "strings" + + "github.com/sei-protocol/sei-chain/giga/evmonly/precompiles" + "github.com/sei-protocol/sei-chain/giga/evmonly/precompiles/util" +) + +func addDelegation(store precompiles.Store, delegator string, validator string, delta *big.Int) error { + record, ok, err := getDelegation(store, delegator, validator) + if err != nil { + return err + } + current := new(big.Int) + if ok { + current, err = util.ParseAmount(record.Amount) + if err != nil { + return err + } + } + next := new(big.Int).Add(current, delta) + if next.Sign() < 0 { + return errors.New("delegation amount is insufficient") + } + if next.Sign() == 0 { + store.Delete(delegationKey(delegator, validator)) + if err := removeStringListItem(store, delegatorDelegationsIndexKey(delegator), validator); err != nil { + return err + } + return removeStringListItem(store, validatorDelegationsIndexKey(validator), delegator) + } + record = delegationRecord{ + DelegatorAddress: delegator, + ValidatorAddress: validator, + Amount: next.String(), + } + if err := util.SetJSON(store, delegationKey(delegator, validator), record); err != nil { + return err + } + if err := addStringListItem(store, delegatorDelegationsIndexKey(delegator), validator); err != nil { + return err + } + return addStringListItem(store, validatorDelegationsIndexKey(validator), delegator) +} + +func getDelegation(store precompiles.Store, delegator string, validator string) (delegationRecord, bool, error) { + return util.GetJSON[delegationRecord](store, delegationKey(delegator, validator)) +} + +func validateDelegationAmount(store precompiles.Store, delegator string, validator string, amount *big.Int) error { + record, ok, err := getDelegation(store, delegator, validator) + if err != nil { + return err + } + if !ok { + return errors.New("delegation amount is insufficient") + } + current, err := util.ParseAmount(record.Amount) + if err != nil { + return err + } + if current.Cmp(amount) < 0 { + return errors.New("delegation amount is insufficient") + } + return nil +} + +func delegationFromRecord(record delegationRecord) (Delegation, error) { + amount, err := util.ParseAmount(record.Amount) + if err != nil { + return Delegation{}, err + } + return Delegation{ + Balance: Balance{ + Amount: amount, + Denom: bondDenom, + }, + Delegation: DelegationDetails{ + DelegatorAddress: record.DelegatorAddress, + Shares: new(big.Int).Set(amount), + Decimals: big.NewInt(precision), + ValidatorAddress: record.ValidatorAddress, + }, + }, nil +} + +func setValidator(store precompiles.Store, validator Validator) error { + if err := util.SetJSON(store, validatorKey(validator.OperatorAddress), validator); err != nil { + return err + } + return addStringListItem(store, validatorsIndexKey(), validator.OperatorAddress) +} + +func getValidator(store precompiles.Store, validatorAddress string) (Validator, bool, error) { + return util.GetJSON[Validator](store, validatorKey(validatorAddress)) +} + +func removeValidator(store precompiles.Store, validatorAddress string) error { + store.Delete(validatorKey(validatorAddress)) + return removeStringListItem(store, validatorsIndexKey(), validatorAddress) +} + +func addValidatorTokens(store precompiles.Store, validatorAddress string, delta *big.Int) error { + validator, ok, err := getValidator(store, validatorAddress) + if err != nil { + return err + } + if !ok { + return errValidatorMissing + } + tokens, err := util.ParseAmount(validator.Tokens) + if err != nil { + return err + } + shares, err := util.ParseAmount(validator.DelegatorShares) + if err != nil { + return err + } + tokens.Add(tokens, delta) + shares.Add(shares, delta) + if tokens.Sign() < 0 || shares.Sign() < 0 { + return errors.New("validator tokens are insufficient") + } + validator.Tokens = tokens.String() + validator.DelegatorShares = shares.String() + return setValidator(store, validator) +} + +func addUnbondingDelegation(store precompiles.Store, delegator string, validator string, amount *big.Int, creationHeight uint64, completionTime int64) error { + record, _, err := getUnbondingDelegation(store, delegator, validator) + if err != nil { + return err + } + record.DelegatorAddress = delegator + record.ValidatorAddress = validator + record.Entries = append(record.Entries, UnbondingDelegationEntry{ + CreationHeight: saturatingInt64FromUint64(creationHeight), + CompletionTime: completionTime, + InitialBalance: amount.String(), + Balance: amount.String(), + }) + if err := util.SetJSON(store, unbondingDelegationKey(delegator, validator), record); err != nil { + return err + } + if err := addStringListItem(store, delegatorUnbondingsIndexKey(delegator), validator); err != nil { + return err + } + if err := addStringListItem(store, validatorUnbondingsIndexKey(validator), delegator); err != nil { + return err + } + return insertUnbondingQueue(store, completionTime, delegator, validator) +} + +func getUnbondingDelegation(store precompiles.Store, delegator string, validator string) (UnbondingDelegation, bool, error) { + return util.GetJSON[UnbondingDelegation](store, unbondingDelegationKey(delegator, validator)) +} + +func addRedelegation(store precompiles.Store, delegator string, srcValidator string, dstValidator string, amount *big.Int, completionTime int64) error { + record, _, err := getRedelegation(store, delegator, srcValidator, dstValidator) + if err != nil { + return err + } + record.DelegatorAddress = delegator + record.ValidatorSrcAddress = srcValidator + record.ValidatorDstAddress = dstValidator + record.Entries = append(record.Entries, RedelegationEntry{ + CreationHeight: 0, + CompletionTime: completionTime, + InitialBalance: amount.String(), + SharesDst: amount.String(), + }) + if err := util.SetJSON(store, redelegationKey(delegator, srcValidator, dstValidator), record); err != nil { + return err + } + if err := addStringListItem(store, redelegationsIndexKey(), redelegationID(delegator, srcValidator, dstValidator)); err != nil { + return err + } + return insertRedelegationQueue(store, completionTime, delegator, srcValidator, dstValidator) +} + +func getRedelegation(store precompiles.Store, delegator string, srcValidator string, dstValidator string) (Redelegation, bool, error) { + return util.GetJSON[Redelegation](store, redelegationKey(delegator, srcValidator, dstValidator)) +} + +func loadParams(store precompiles.Store) (Params, error) { + params, ok, err := util.GetJSON[Params](store, paramsKey()) + if err != nil { + return Params{}, err + } + if ok { + params.BondDenom = bondDenom + return params, nil + } + return Params{ + UnbondingTime: 1_814_400, + MaxValidators: 100, + MaxEntries: 7, + HistoricalEntries: 10_000, + BondDenom: bondDenom, + MinCommissionRate: "0.000000000000000000", + MaxVotingPowerRatio: "0.000000000000000000", + MaxVotingPowerEnforcementThreshold: "0.000000000000000000", + }, nil +} + +func loadPool(store precompiles.Store) (Pool, error) { + pool, ok, err := util.GetJSON[Pool](store, poolKey()) + if err != nil { + return Pool{}, err + } + if ok { + return pool, nil + } + return Pool{NotBondedTokens: "0", BondedTokens: "0"}, nil +} + +func addPoolBonded(store precompiles.Store, delta *big.Int) error { + pool, err := loadPool(store) + if err != nil { + return err + } + bonded, err := util.ParseAmount(pool.BondedTokens) + if err != nil { + return err + } + bonded.Add(bonded, delta) + if bonded.Sign() < 0 { + return errors.New("bonded pool tokens are insufficient") + } + pool.BondedTokens = bonded.String() + return util.SetJSON(store, poolKey(), pool) +} + +func addPoolNotBonded(store precompiles.Store, delta *big.Int) error { + pool, err := loadPool(store) + if err != nil { + return err + } + notBonded, err := util.ParseAmount(pool.NotBondedTokens) + if err != nil { + return err + } + notBonded.Add(notBonded, delta) + if notBonded.Sign() < 0 { + return errors.New("not bonded pool tokens are insufficient") + } + pool.NotBondedTokens = notBonded.String() + return util.SetJSON(store, poolKey(), pool) +} + +func movePoolsForRedelegation(store precompiles.Store, srcStatus int32, dstStatus int32, amount *big.Int) error { + srcBonded := srcStatus == bondStatusBonded + dstBonded := dstStatus == bondStatusBonded + switch { + case srcBonded == dstBonded: + return nil + case srcBonded: + if err := addPoolBonded(store, new(big.Int).Neg(amount)); err != nil { + return err + } + return addPoolNotBonded(store, amount) + default: + if err := addPoolNotBonded(store, new(big.Int).Neg(amount)); err != nil { + return err + } + return addPoolBonded(store, amount) + } +} + +func setHistoricalInfo(store precompiles.Store, height uint64) error { + if height > uint64(1<<63-1) { + return nil + } + validatorAddresses, err := getStringList(store, validatorsIndexKey()) + if err != nil { + return err + } + info := HistoricalInfo{ + Height: int64(height), //nolint:gosec // bounded above. + Validators: make([]Validator, 0, len(validatorAddresses)), + } + for _, validatorAddress := range validatorAddresses { + validator, ok, err := getValidator(store, validatorAddress) + if err != nil { + return err + } + if ok { + info.Validators = append(info.Validators, validator) + } + } + return util.SetJSON(store, historicalInfoKey(info.Height), info) +} + +func getHistoricalInfo(store precompiles.Store, height int64) (HistoricalInfo, bool, error) { + return util.GetJSON[HistoricalInfo](store, historicalInfoKey(height)) +} + +func getStringList(store precompiles.Store, key []byte) ([]string, error) { + items, ok, err := util.GetJSON[[]string](store, key) + if err != nil || !ok { + return nil, err + } + sort.Strings(items) + return items, nil +} + +func setStringList(store precompiles.Store, key []byte, items []string) error { + if len(items) == 0 { + store.Delete(key) + return nil + } + sort.Strings(items) + return util.SetJSON(store, key, items) +} + +func addStringListItem(store precompiles.Store, key []byte, item string) error { + items, err := getStringList(store, key) + if err != nil { + return err + } + for _, existing := range items { + if existing == item { + return nil + } + } + items = append(items, item) + sort.Strings(items) + return util.SetJSON(store, key, items) +} + +func removeStringListItem(store precompiles.Store, key []byte, item string) error { + items, err := getStringList(store, key) + if err != nil { + return err + } + out := items[:0] + for _, existing := range items { + if existing != item { + out = append(out, existing) + } + } + if len(out) == 0 { + store.Delete(key) + return nil + } + return setStringList(store, key, out) +} + +func insertUnbondingQueue(store precompiles.Store, completionTime int64, delegator string, validator string) error { + id := timeQueueID(completionTime) + items, err := getDelegationPairList(store, unbondingQueueKey(id)) + if err != nil { + return err + } + item := delegationPair{DelegatorAddress: delegator, ValidatorAddress: validator} + for _, existing := range items { + if existing == item { + return nil + } + } + items = append(items, item) + if err := util.SetJSON(store, unbondingQueueKey(id), items); err != nil { + return err + } + return addStringListItem(store, unbondingQueueIndexKey(), id) +} + +func insertRedelegationQueue(store precompiles.Store, completionTime int64, delegator string, srcValidator string, dstValidator string) error { + id := timeQueueID(completionTime) + items, err := getRedelegationTripletList(store, redelegationQueueKey(id)) + if err != nil { + return err + } + item := redelegationTriplet{DelegatorAddress: delegator, ValidatorSrcAddress: srcValidator, ValidatorDstAddress: dstValidator} + for _, existing := range items { + if existing == item { + return nil + } + } + items = append(items, item) + if err := util.SetJSON(store, redelegationQueueKey(id), items); err != nil { + return err + } + return addStringListItem(store, redelegationQueueIndexKey(), id) +} + +func insertValidatorQueue(store precompiles.Store, completionTime int64, completionHeight int64, validator string) error { + id := validatorQueueID(completionTime, completionHeight) + if err := addStringListItem(store, validatorQueueKey(id), validator); err != nil { + return err + } + return addStringListItem(store, validatorQueueIndexKey(), id) +} + +func deleteValidatorQueue(store precompiles.Store, completionTime int64, completionHeight int64, validator string) error { + id := validatorQueueID(completionTime, completionHeight) + if err := removeStringListItem(store, validatorQueueKey(id), validator); err != nil { + return err + } + if items, err := getStringList(store, validatorQueueKey(id)); err != nil { + return err + } else if len(items) == 0 { + return removeStringListItem(store, validatorQueueIndexKey(), id) + } + return nil +} + +func getDelegationPairList(store precompiles.Store, key []byte) ([]delegationPair, error) { + items, ok, err := util.GetJSON[[]delegationPair](store, key) + if err != nil || !ok { + return nil, err + } + return items, nil +} + +func getRedelegationTripletList(store precompiles.Store, key []byte) ([]redelegationTriplet, error) { + items, ok, err := util.GetJSON[[]redelegationTriplet](store, key) + if err != nil || !ok { + return nil, err + } + return items, nil +} + +func matureTimeQueueIDs(store precompiles.Store, indexKey []byte, blockTime uint64) ([]string, error) { + ids, err := getStringList(store, indexKey) + if err != nil { + return nil, err + } + mature := ids[:0] + completionTime := saturatingInt64FromUint64(blockTime) + for _, id := range ids { + time, err := parseTimeQueueID(id) + if err != nil { + return nil, err + } + if time <= completionTime { + mature = append(mature, id) + } + } + sort.SliceStable(mature, func(i, j int) bool { + left, _ := parseTimeQueueID(mature[i]) + right, _ := parseTimeQueueID(mature[j]) + if left == right { + return mature[i] < mature[j] + } + return left < right + }) + return mature, nil +} + +func saturatingInt64FromUint64(value uint64) int64 { + if value > uint64(1<<63-1) { + return int64(1<<63 - 1) + } + return int64(value) //nolint:gosec // bounded above. +} + +func matureValidatorQueueIDs(store precompiles.Store, blockTime uint64, blockHeight uint64) ([]string, error) { + ids, err := getStringList(store, validatorQueueIndexKey()) + if err != nil { + return nil, err + } + mature := ids[:0] + completionTime := saturatingInt64FromUint64(blockTime) + completionHeight := saturatingInt64FromUint64(blockHeight) + for _, id := range ids { + time, height, err := parseValidatorQueueID(id) + if err != nil { + return nil, err + } + if time <= completionTime && height <= completionHeight { + mature = append(mature, id) + } + } + sort.SliceStable(mature, func(i, j int) bool { + leftTime, leftHeight, _ := parseValidatorQueueID(mature[i]) + rightTime, rightHeight, _ := parseValidatorQueueID(mature[j]) + if leftTime != rightTime { + return leftTime < rightTime + } + if leftHeight != rightHeight { + return leftHeight < rightHeight + } + return mature[i] < mature[j] + }) + return mature, nil +} + +func setLastValidatorPower(store precompiles.Store, validator string, power int64) error { + if err := util.SetJSON(store, lastValidatorPowerKey(validator), power); err != nil { + return err + } + return addStringListItem(store, lastValidatorsIndexKey(), validator) +} + +func deleteLastValidatorPower(store precompiles.Store, validator string) error { + store.Delete(lastValidatorPowerKey(validator)) + return removeStringListItem(store, lastValidatorsIndexKey(), validator) +} + +func getLastValidatorPowers(store precompiles.Store) (map[string]int64, error) { + validators, err := getStringList(store, lastValidatorsIndexKey()) + if err != nil { + return nil, err + } + out := make(map[string]int64, len(validators)) + for _, validator := range validators { + power, ok, err := util.GetJSON[int64](store, lastValidatorPowerKey(validator)) + if err != nil { + return nil, err + } + if ok { + out[validator] = power + } + } + return out, nil +} + +func setLastTotalPower(store precompiles.Store, power int64) error { + return util.SetJSON(store, lastTotalPowerKey(), power) +} + +func pageStrings(items []string, nextKey []byte) ([]string, []byte, error) { + start := 0 + if len(nextKey) != 0 { + parsed, err := strconv.Atoi(string(nextKey)) + if err != nil || parsed < 0 { + return nil, nil, errors.New("invalid pagination key") + } + start = parsed + } + if start >= len(items) { + return nil, nil, nil + } + end := start + pageLimit + if end > len(items) { + end = len(items) + } + var outNextKey []byte + if end < len(items) { + outNextKey = []byte(strconv.Itoa(end)) + } + return items[start:end], outNextKey, nil +} + +func paramsKey() []byte { + return []byte("params") +} + +func poolKey() []byte { + return []byte("pool") +} + +func validatorsIndexKey() []byte { + return []byte("validators/index") +} + +func validatorKey(validator string) []byte { + return []byte("validator/" + validator) +} + +func delegationKey(delegator string, validator string) []byte { + return []byte("delegation/" + delegator + "/" + validator) +} + +func delegatorDelegationsIndexKey(delegator string) []byte { + return []byte("delegator-delegations/" + delegator) +} + +func validatorDelegationsIndexKey(validator string) []byte { + return []byte("validator-delegations/" + validator) +} + +func unbondingDelegationKey(delegator string, validator string) []byte { + return []byte("unbonding/" + delegator + "/" + validator) +} + +func delegatorUnbondingsIndexKey(delegator string) []byte { + return []byte("delegator-unbondings/" + delegator) +} + +func validatorUnbondingsIndexKey(validator string) []byte { + return []byte("validator-unbondings/" + validator) +} + +func redelegationsIndexKey() []byte { + return []byte("redelegations/index") +} + +func redelegationKey(delegator string, srcValidator string, dstValidator string) []byte { + return []byte("redelegation/" + redelegationID(delegator, srcValidator, dstValidator)) +} + +func redelegationID(delegator string, srcValidator string, dstValidator string) string { + return delegator + "\x00" + srcValidator + "\x00" + dstValidator +} + +func splitRedelegationID(id string) (string, string, string, bool) { + parts := strings.Split(id, "\x00") + if len(parts) != 3 { + return "", "", "", false + } + return parts[0], parts[1], parts[2], true +} + +func historicalInfoKey(height int64) []byte { + return []byte("historical/" + strconv.FormatInt(height, 10)) +} + +func lastValidatorsIndexKey() []byte { + return []byte("last-validators/index") +} + +func lastValidatorPowerKey(validator string) []byte { + return []byte("last-validators/power/" + validator) +} + +func lastTotalPowerKey() []byte { + return []byte("last-validators/total-power") +} + +func unbondingQueueIndexKey() []byte { + return []byte("unbonding-queue/index") +} + +func unbondingQueueKey(id string) []byte { + return []byte("unbonding-queue/" + id) +} + +func redelegationQueueIndexKey() []byte { + return []byte("redelegation-queue/index") +} + +func redelegationQueueKey(id string) []byte { + return []byte("redelegation-queue/" + id) +} + +func validatorQueueIndexKey() []byte { + return []byte("validator-queue/index") +} + +func validatorQueueKey(id string) []byte { + return []byte("validator-queue/" + id) +} + +func timeQueueID(completionTime int64) string { + return strconv.FormatInt(completionTime, 10) +} + +func parseTimeQueueID(id string) (int64, error) { + return strconv.ParseInt(id, 10, 64) +} + +func validatorQueueID(completionTime int64, completionHeight int64) string { + return strconv.FormatInt(completionTime, 10) + "/" + strconv.FormatInt(completionHeight, 10) +} + +func parseValidatorQueueID(id string) (int64, int64, error) { + timePart, heightPart, ok := strings.Cut(id, "/") + if !ok { + return 0, 0, errors.New("invalid validator queue id") + } + completionTime, err := strconv.ParseInt(timePart, 10, 64) + if err != nil { + return 0, 0, err + } + completionHeight, err := strconv.ParseInt(heightPart, 10, 64) + if err != nil { + return 0, 0, err + } + return completionTime, completionHeight, nil +} diff --git a/giga/evmonly/precompiles/staking/types.go b/giga/evmonly/precompiles/staking/types.go new file mode 100644 index 0000000000..c1328a91cc --- /dev/null +++ b/giga/evmonly/precompiles/staking/types.go @@ -0,0 +1,122 @@ +package staking + +import "math/big" + +type Delegation struct { + Balance Balance + Delegation DelegationDetails +} + +type Balance struct { + Amount *big.Int + Denom string +} + +type DelegationDetails struct { + DelegatorAddress string + Shares *big.Int + Decimals *big.Int + ValidatorAddress string +} + +type ValidatorsResponse struct { + Validators []Validator + NextKey []byte +} + +type DelegationsResponse struct { + Delegations []Delegation + NextKey []byte +} + +type UnbondingDelegationsResponse struct { + UnbondingDelegations []UnbondingDelegation + NextKey []byte +} + +type RedelegationsResponse struct { + Redelegations []Redelegation + NextKey []byte +} + +type Validator struct { + OperatorAddress string + ConsensusPubkey []byte + Jailed bool + Status int32 + Tokens string + DelegatorShares string + Description string + UnbondingHeight int64 + UnbondingTime int64 + CommissionRate string + CommissionMaxRate string + CommissionMaxChangeRate string + CommissionUpdateTime int64 + MinSelfDelegation string +} + +type delegationRecord struct { + DelegatorAddress string `json:"delegator_address"` + ValidatorAddress string `json:"validator_address"` + Amount string `json:"amount"` +} + +type UnbondingDelegationEntry struct { + CreationHeight int64 + CompletionTime int64 + InitialBalance string + Balance string +} + +type UnbondingDelegation struct { + DelegatorAddress string + ValidatorAddress string + Entries []UnbondingDelegationEntry +} + +type RedelegationEntry struct { + CreationHeight int64 + CompletionTime int64 + InitialBalance string + SharesDst string +} + +type Redelegation struct { + DelegatorAddress string + ValidatorSrcAddress string + ValidatorDstAddress string + Entries []RedelegationEntry +} + +type HistoricalInfo struct { + Height int64 + Validators []Validator +} + +type Pool struct { + NotBondedTokens string + BondedTokens string +} + +type Params struct { + UnbondingTime uint64 + MaxValidators uint32 + MaxEntries uint32 + HistoricalEntries uint32 + BondDenom string + MinCommissionRate string + MaxVotingPowerRatio string + MaxVotingPowerEnforcementThreshold string +} + +type delegationPair struct { + DelegatorAddress string + ValidatorAddress string +} + +type redelegationTriplet struct { + DelegatorAddress string + ValidatorSrcAddress string + ValidatorDstAddress string +} diff --git a/giga/evmonly/precompiles/util/events.go b/giga/evmonly/precompiles/util/events.go new file mode 100644 index 0000000000..0d8e3b1590 --- /dev/null +++ b/giga/evmonly/precompiles/util/events.go @@ -0,0 +1,27 @@ +package util + +import ( + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/common" + ethtypes "github.com/ethereum/go-ethereum/core/types" + + "github.com/sei-protocol/sei-chain/giga/evmonly/precompiles" +) + +func EmitEvent(logs precompiles.LogSink, address common.Address, event abi.Event, indexed common.Address, args ...interface{}) { + if logs == nil { + return + } + data, err := event.Inputs.NonIndexed().Pack(args...) + if err != nil { + return + } + logs.AddLog(ðtypes.Log{ + Address: address, + Topics: []common.Hash{ + event.ID, + common.BytesToHash(indexed.Bytes()), + }, + Data: data, + }) +} diff --git a/giga/evmonly/precompiles/util/helpers.go b/giga/evmonly/precompiles/util/helpers.go new file mode 100644 index 0000000000..ae2b2704a2 --- /dev/null +++ b/giga/evmonly/precompiles/util/helpers.go @@ -0,0 +1,69 @@ +package util + +import ( + "errors" + "fmt" + "math/big" + + "github.com/ethereum/go-ethereum/common" +) + +func ParseAmount(s string) (*big.Int, error) { + if s == "" { + return new(big.Int), nil + } + amount, ok := new(big.Int).SetString(s, 10) + if !ok { + return nil, fmt.Errorf("invalid integer amount %q", s) + } + return amount, nil +} + +func CloneBig(v *big.Int) *big.Int { + if v == nil { + return new(big.Int) + } + return new(big.Int).Set(v) +} + +func ValidateNonPayable(value *big.Int) error { + if value != nil && value.Sign() != 0 { + return errors.New("sending funds to a non-payable function") + } + return nil +} + +func ValidateArgsLength(args []interface{}, length int) error { + if len(args) != length { + return fmt.Errorf("expected %d arguments but got %d", length, len(args)) + } + return nil +} + +func ValidatePositiveAmount(amount *big.Int, name string) error { + if amount == nil || amount.Sign() <= 0 { + return fmt.Errorf("%s must be a positive integer", name) + } + return nil +} + +func ValidateDecimal(value string, name string) error { + if value == "" { + return fmt.Errorf("invalid %s", name) + } + if _, ok := new(big.Rat).SetString(value); !ok { + return fmt.Errorf("invalid %s", name) + } + return nil +} + +func SaturatingCompletionTime(blockTime uint64, offset uint64) int64 { + if blockTime > uint64(1<<63-1) || offset > uint64(1<<63-1)-blockTime { + return int64(1<<63 - 1) + } + return int64(blockTime + offset) //nolint:gosec // bounded above. +} + +func AddressString(addr common.Address) string { + return addr.Hex() +} diff --git a/giga/evmonly/precompiles/util/json.go b/giga/evmonly/precompiles/util/json.go new file mode 100644 index 0000000000..0fba6de350 --- /dev/null +++ b/giga/evmonly/precompiles/util/json.go @@ -0,0 +1,28 @@ +package util + +import ( + "encoding/json" + + "github.com/sei-protocol/sei-chain/giga/evmonly/precompiles" +) + +func GetJSON[T any](store precompiles.Store, key []byte) (T, bool, error) { + var out T + bz, ok := store.Get(key) + if !ok { + return out, false, nil + } + if err := json.Unmarshal(bz, &out); err != nil { + return out, false, err + } + return out, true, nil +} + +func SetJSON[T any](store precompiles.Store, key []byte, value T) error { + bz, err := json.Marshal(value) + if err != nil { + return err + } + store.Set(key, bz) + return nil +} diff --git a/giga/evmonly/types.go b/giga/evmonly/types.go index 708c10e91f..24a9f9ba01 100644 --- a/giga/evmonly/types.go +++ b/giga/evmonly/types.go @@ -6,6 +6,8 @@ import ( "github.com/ethereum/go-ethereum/common" ethtypes "github.com/ethereum/go-ethereum/core/types" + + "github.com/sei-protocol/sei-chain/giga/evmonly/precompiles" ) // BlockExecutor is the Cosmos-free block execution boundary for the EVM-only path. @@ -36,12 +38,15 @@ type BlockContext struct { // BlockResult is the executor output consumed by the new runtime boundary. type BlockResult struct { - ChangeSet StateChangeSet - Txs []TxResult - Receipts ethtypes.Receipts - GasUsed uint64 + ChangeSet StateChangeSet + ValidatorUpdates []ValidatorUpdate + Txs []TxResult + Receipts ethtypes.Receipts + GasUsed uint64 } +type ValidatorUpdate = precompiles.ValidatorUpdate + // StateChangeSet is the deterministic EVM-native state output for a block. // Values are post-block values, not deltas. type StateChangeSet struct {