Skip to content

Commit

Permalink
feat(evmstaking): implement epoch staking (#96)
Browse files Browse the repository at this point in the history
* feat(evmstaking): implement epoched staking

* fix(evmstaking): fix tests with implementation of epoched staking

* perf(evmstaking): improve processing queued msg

* fix(evmstaking): fix tests by adding epoch identifier

* test(evmstaking): add tests for epoch identifier param

* chore(evmstaking): change log level of withdraw

* feat(evmstaking): add validation of epoch duration

* chore(evmstaking): continue processing msg when failed

* chore(evmstaking): process mature unbonding every epoch

* feat(evmstaking): move partial withdraw out of epoch
  • Loading branch information
Narangde authored Oct 10, 2024
1 parent 9e64242 commit 91a4806
Show file tree
Hide file tree
Showing 39 changed files with 1,958 additions and 257 deletions.
28 changes: 0 additions & 28 deletions client/x/evmengine/testutil/expected_keepers_mocks.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 0 additions & 2 deletions client/x/evmengine/types/expected_keepers.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,6 @@ type AccountKeeper interface {
}

type EvmStakingKeeper interface {
ProcessDeposit(ctx context.Context, ev *bindings.IPTokenStakingDeposit) error
ProcessWithdraw(ctx context.Context, ev *bindings.IPTokenStakingWithdraw) error
DequeueEligibleWithdrawals(ctx context.Context) (ethtypes.Withdrawals, error)
ParseDepositLog(ethlog ethtypes.Log) (*bindings.IPTokenStakingDeposit, error)
ParseWithdrawLog(ethlog ethtypes.Log) (*bindings.IPTokenStakingWithdraw, error)
Expand Down
170 changes: 98 additions & 72 deletions client/x/evmstaking/keeper/abci.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,99 +28,125 @@ func (k *Keeper) EndBlock(ctx context.Context) (abci.ValidatorUpdates, error) {
log.Debug(ctx, "EndBlock.evmstaking")
defer telemetry.ModuleMeasureSince(types.ModuleName, time.Now(), telemetry.MetricKeyEndBlocker)

sdkCtx := sdk.UnwrapSDKContext(ctx)
ctxTime := sdkCtx.BlockHeader().Time
blockHeight := sdkCtx.BlockHeader().Height
var valUpdates []abci.ValidatorUpdate

matureUnbonds, err := k.GetMatureUnbondedDelegations(ctx)
log.Debug(ctx, "Processing mature unbonding delegations", "count", len(matureUnbonds))
isNextEpoch, err := k.IsNextEpoch(ctx)
if err != nil {
return nil, err
return nil, errors.Wrap(err, "check if the next epoch starts")
}

// make an array with each entry being the validator address, delegator address, and the amount
var unbondedEntries []UnbondedEntry
if isNextEpoch { //nolint:nestif // readability
sdkCtx := sdk.UnwrapSDKContext(ctx)
ctxTime := sdkCtx.BlockHeader().Time
blockHeight := sdkCtx.BlockHeader().Height

for _, dvPair := range matureUnbonds {
validatorAddr, err := k.validatorAddressCodec.StringToBytes(dvPair.ValidatorAddress)
matureUnbonds, err := k.GetMatureUnbondedDelegations(ctx)
log.Debug(ctx, "Processing mature unbonding delegations", "count", len(matureUnbonds))
if err != nil {
return nil, errors.Wrap(err, "validator address from bech32")
return nil, err
}

delegatorAddr, err := k.authKeeper.AddressCodec().StringToBytes(dvPair.DelegatorAddress)
if err != nil {
return nil, errors.Wrap(err, "delegator address from bech32")
}
// make an array with each entry being the validator address, delegator address, and the amount
var unbondedEntries []UnbondedEntry

ubd, err := (k.stakingKeeper).GetUnbondingDelegation(ctx, delegatorAddr, validatorAddr)
if err != nil {
return nil, err
}
for _, dvPair := range matureUnbonds {
validatorAddr, err := k.validatorAddressCodec.StringToBytes(dvPair.ValidatorAddress)
if err != nil {
return nil, errors.Wrap(err, "validator address from bech32")
}

// TODO: parameterized bondDenom
bondDenom := sdk.DefaultBondDenom

// loop through all the entries and process unbonding mature entries
for i := range len(ubd.Entries) {
entry := ubd.Entries[i]
if entry.IsMature(ctxTime) && !entry.OnHold() {
// track undelegation only when remaining or truncated shares are non-zero
if !entry.Balance.IsZero() {
amt := sdk.NewCoin(bondDenom, entry.Balance)
// TODO: check if it's possible to add a double entry in the unbondedEntries array
unbondedEntries = append(unbondedEntries, UnbondedEntry{
validatorAddress: dvPair.ValidatorAddress,
delegatorAddress: dvPair.DelegatorAddress,
amount: amt.Amount,
})
}
delegatorAddr, err := k.authKeeper.AddressCodec().StringToBytes(dvPair.DelegatorAddress)
if err != nil {
return nil, errors.Wrap(err, "delegator address from bech32")
}
}
}

valUpdates, err := k.stakingKeeper.EndBlocker(ctx)
if err != nil {
return nil, err
}
ubd, err := (k.stakingKeeper).GetUnbondingDelegation(ctx, delegatorAddr, validatorAddr)
if err != nil {
return nil, err
}

for _, entry := range unbondedEntries {
log.Debug(ctx, "Adding undelegation to withdrawal queue",
"delegator", entry.delegatorAddress,
"validator", entry.validatorAddress,
"amount", entry.amount.String())
// TODO: parameterized bondDenom
bondDenom := sdk.DefaultBondDenom

// loop through all the entries and process unbonding mature entries
for i := range len(ubd.Entries) {
entry := ubd.Entries[i]
if entry.IsMature(ctxTime) && !entry.OnHold() {
// track undelegation only when remaining or truncated shares are non-zero
if !entry.Balance.IsZero() {
amt := sdk.NewCoin(bondDenom, entry.Balance)
// TODO: check if it's possible to add a double entry in the unbondedEntries array
unbondedEntries = append(unbondedEntries, UnbondedEntry{
validatorAddress: dvPair.ValidatorAddress,
delegatorAddress: dvPair.DelegatorAddress,
amount: amt.Amount,
})
}
}
}
}

delegatorAddr, err := k.authKeeper.AddressCodec().StringToBytes(entry.delegatorAddress)
if err != nil {
return nil, errors.Wrap(err, "delegator address from bech32")
// Update validator set
// process all queued messages
if err := k.ProcessAllMsgs(ctx); err != nil {
return nil, errors.Wrap(err, "get current epoch queued messages")
}
// Burn tokens from the delegator
_, coins := IPTokenToBondCoin(entry.amount.BigInt())
err = k.bankKeeper.SendCoinsFromAccountToModule(ctx, delegatorAddr, types.ModuleName, coins)
if err != nil {
return nil, errors.Wrap(err, "send coins from account to module")

// if it is epoch based, increase epoch number
if err := k.IncCurEpochNumber(ctx); err != nil {
return nil, errors.Wrap(err, "increase current epoch number")
}
err = k.bankKeeper.BurnCoins(ctx, types.ModuleName, coins)

// update val set
valUpdates, err = k.stakingKeeper.EndBlocker(ctx)
if err != nil {
return nil, errors.Wrap(err, "burn coins")
return nil, errors.Wrap(err, "update validator set")
}

// This should not produce error, as all delegations are done via the evmstaking module via EL.
// However, we should gracefully handle in case Get fails.
delEvmAddr, err := k.DelegatorMap.Get(ctx, entry.delegatorAddress)
if err != nil {
return nil, errors.Wrap(err, "map delegator pubkey to evm address")
// init message queue
if err := k.MessageQueue.Initialize(ctx); err != nil {
return nil, errors.Wrap(err, "initialize message queue")
}

// push the undelegation to the withdrawal queue
err = k.AddWithdrawalToQueue(ctx, types.NewWithdrawal(
uint64(blockHeight),
entry.delegatorAddress,
entry.validatorAddress,
delEvmAddr,
entry.amount.Uint64(),
))
if err != nil {
return nil, err
for _, entry := range unbondedEntries {
log.Debug(ctx, "Adding undelegation to withdrawal queue",
"delegator", entry.delegatorAddress,
"validator", entry.validatorAddress,
"amount", entry.amount.String())

delegatorAddr, err := k.authKeeper.AddressCodec().StringToBytes(entry.delegatorAddress)
if err != nil {
return nil, errors.Wrap(err, "delegator address from bech32")
}
// Burn tokens from the delegator
_, coins := IPTokenToBondCoin(entry.amount.BigInt())
err = k.bankKeeper.SendCoinsFromAccountToModule(ctx, delegatorAddr, types.ModuleName, coins)
if err != nil {
return nil, errors.Wrap(err, "send coins from account to module")
}
err = k.bankKeeper.BurnCoins(ctx, types.ModuleName, coins)
if err != nil {
return nil, errors.Wrap(err, "burn coins")
}

// This should not produce error, as all delegations are done via the evmstaking module via EL.
// However, we should gracefully handle in case Get fails.
delEvmAddr, err := k.DelegatorMap.Get(ctx, entry.delegatorAddress)
if err != nil {
return nil, errors.Wrap(err, "map delegator pubkey to evm address")
}

// push the undelegation to the withdrawal queue
err = k.AddWithdrawalToQueue(ctx, types.NewWithdrawal(
uint64(blockHeight),
entry.delegatorAddress,
entry.validatorAddress,
delEvmAddr,
entry.amount.Uint64(),
))
if err != nil {
return nil, err
}
}
}

Expand Down
6 changes: 2 additions & 4 deletions client/x/evmstaking/keeper/abci_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -242,9 +242,8 @@ func (s *TestSuite) TestEndBlock() {
pastHeader.Time = pastHeader.Time.Add(-ubdTime).Add(-time.Minute)
s.setupUnbonding(sdkCtx.WithBlockHeader(pastHeader), delAddr, valAddr1, "10")

// Mock staking.EndBlocker
s.BankKeeper.EXPECT().UndelegateCoinsFromModuleToAccount(gomock.Any(), stypes.NotBondedPoolName, delAddr, gomock.Any()).Return(nil)
// Mock evmstaking.EndBlocker
s.BankKeeper.EXPECT().UndelegateCoinsFromModuleToAccount(gomock.Any(), stypes.NotBondedPoolName, delAddr, gomock.Any()).Return(nil)
s.BankKeeper.EXPECT().SendCoinsFromAccountToModule(gomock.Any(), delAddr, types.ModuleName, gomock.Any()).Return(errors.New("failed to send coins to module"))

return nil, []abcitypes.ValidatorUpdate{
Expand All @@ -268,9 +267,8 @@ func (s *TestSuite) TestEndBlock() {
pastHeader.Time = pastHeader.Time.Add(-ubdTime).Add(-time.Minute)
s.setupUnbonding(sdkCtx.WithBlockHeader(pastHeader), delAddr, valAddr1, "10")

// Mock staking.EndBlocker
s.BankKeeper.EXPECT().UndelegateCoinsFromModuleToAccount(gomock.Any(), stypes.NotBondedPoolName, delAddr, gomock.Any()).Return(nil)
// Mock evmstaking.EndBlocker
s.BankKeeper.EXPECT().UndelegateCoinsFromModuleToAccount(gomock.Any(), stypes.NotBondedPoolName, delAddr, gomock.Any()).Return(nil)
s.BankKeeper.EXPECT().SendCoinsFromAccountToModule(gomock.Any(), delAddr, types.ModuleName, gomock.Any()).Return(nil)
s.BankKeeper.EXPECT().BurnCoins(gomock.Any(), types.ModuleName, gomock.Any()).Return(errors.New("failed to burn coins"))

Expand Down
74 changes: 49 additions & 25 deletions client/x/evmstaking/keeper/deposit.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package keeper
import (
"context"

"github.com/cometbft/cometbft/crypto/tmhash"
sdk "github.com/cosmos/cosmos-sdk/types"
skeeper "github.com/cosmos/cosmos-sdk/x/staking/keeper"
stypes "github.com/cosmos/cosmos-sdk/x/staking/types"
Expand All @@ -15,7 +16,8 @@ import (
"github.com/piplabs/story/lib/log"
)

func (k Keeper) ProcessDeposit(ctx context.Context, ev *bindings.IPTokenStakingDeposit) error {
// HandleDepositEvent handles Deposit event. It converts the event to sdk.Msg and enqueues for epoched staking.
func (k Keeper) HandleDepositEvent(ctx context.Context, ev *bindings.IPTokenStakingDeposit) error {
depositorPubkey, err := k1util.PubKeyBytesToCosmos(ev.DelegatorCmpPubkey)
if err != nil {
return errors.Wrap(err, "depositor pubkey to cosmos")
Expand All @@ -38,41 +40,65 @@ func (k Keeper) ProcessDeposit(ctx context.Context, ev *bindings.IPTokenStakingD
return errors.Wrap(err, "delegator pubkey to evm address")
}

amountCoin, amountCoins := IPTokenToBondCoin(ev.Amount)
amountCoin, _ := IPTokenToBondCoin(ev.Amount)

// Note that, after minting, we save the mapping between delegator bech32 address and evm address, which will be used in the withdrawal queue.
// The saving is done regardless of any error below, as the money is already minted and sent to the delegator, who can withdraw the minted amount.
// TODO: Confirm that bech32 address and evm address can be used interchangeably. Must be one-to-one or many-bech32-to-one-evm.
if err := k.DelegatorMap.Set(ctx, depositorAddr.String(), delEvmAddr.String()); err != nil {
return errors.Wrap(err, "set delegator map")
}

log.Debug(ctx, "EVM staking deposit detected, delegating to validator",
"del_story", depositorAddr.String(),
"val_story", validatorAddr.String(),
"del_evm_addr", delEvmAddr.String(),
"val_evm_addr", valEvmAddr.String(),
"amount_coin", amountCoin.String(),
)

sdkCtx := sdk.UnwrapSDKContext(ctx)
header := sdkCtx.BlockHeader()
txID := tmhash.Sum(sdkCtx.TxBytes())

msg := stypes.NewMsgDelegate(depositorAddr.String(), validatorAddr.String(), amountCoin)
qMsg, err := types.NewQueuedMessage(uint64(header.Height), header.Time, txID, msg)
if err != nil {
return errors.Wrap(err, "new queued message for Delegate event")
}

if err := k.EnqueueMsg(ctx, qMsg); err != nil {
return errors.Wrap(err, "enqueue Delegate message")
}

return nil
}

// ProcessDepositMsg processes the Delegate message. It makes delegation.
func (k Keeper) ProcessDepositMsg(ctx context.Context, msg *stypes.MsgDelegate) error {
amountCoins := sdk.Coins{msg.Amount}
delegatorAddr, err := sdk.AccAddressFromBech32(msg.DelegatorAddress)
if err != nil {
return errors.Wrap(err, "acc address from bech32", "delegator_addr", msg.DelegatorAddress)
}

// Create account if not exists
if !k.authKeeper.HasAccount(ctx, depositorAddr) {
acc := k.authKeeper.NewAccountWithAddress(ctx, depositorAddr)
if !k.authKeeper.HasAccount(ctx, delegatorAddr) {
acc := k.authKeeper.NewAccountWithAddress(ctx, delegatorAddr)
k.authKeeper.SetAccount(ctx, acc)
log.Debug(ctx, "Created account for depositor",
"address", depositorAddr.String(),
"evm_address", delEvmAddr.String(),
"address", delegatorAddr.String(),
)
}

if err := k.bankKeeper.MintCoins(ctx, types.ModuleName, amountCoins); err != nil {
return errors.Wrap(err, "create stake coin for depositor: mint coins")
}

if err := k.bankKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, depositorAddr, amountCoins); err != nil {
if err := k.bankKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, delegatorAddr, amountCoins); err != nil {
return errors.Wrap(err, "create stake coin for depositor: send coins")
}

log.Info(ctx, "EVM staking deposit detected, delegating to validator",
"del_story", depositorAddr.String(),
"val_story", validatorAddr.String(),
"del_evm_addr", delEvmAddr.String(),
"val_evm_addr", valEvmAddr.String(),
"amount_coin", amountCoin.String(),
)

// Note that, after minting, we save the mapping between delegator bech32 address and evm address, which will be used in the withdrawal queue.
// The saving is done regardless of any error below, as the money is already minted and sent to the delegator, who can withdraw the minted amount.
// TODO: Confirm that bech32 address and evm address can be used interchangeably. Must be one-to-one or many-bech32-to-one-evm.
if err := k.DelegatorMap.Set(ctx, depositorAddr.String(), delEvmAddr.String()); err != nil {
return errors.Wrap(err, "set delegator map")
}

// TODO: Check if we can instantiate the msgServer without type assertion
evmstakingSKeeper, ok := k.stakingKeeper.(*skeeper.Keeper)
if !ok {
Expand All @@ -81,9 +107,7 @@ func (k Keeper) ProcessDeposit(ctx context.Context, ev *bindings.IPTokenStakingD
skeeperMsgServer := skeeper.NewMsgServerImpl(evmstakingSKeeper)

// Delegation by the depositor on the validator (validator existence is checked in msgServer.Delegate)
msg := stypes.NewMsgDelegate(depositorAddr.String(), validatorAddr.String(), amountCoin)
_, err = skeeperMsgServer.Delegate(ctx, msg)
if err != nil {
if _, err = skeeperMsgServer.Delegate(ctx, msg); err != nil {
return errors.Wrap(err, "delegate")
}

Expand Down
6 changes: 5 additions & 1 deletion client/x/evmstaking/keeper/deposit_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,11 @@ func (s *TestSuite) TestProcessDeposit() {
tc.settingMock()
}
cachedCtx, _ := ctx.CacheContext()
err := keeper.ProcessDeposit(cachedCtx, tc.deposit)
var err error
err = keeper.HandleDepositEvent(cachedCtx, tc.deposit)
if !keeper.MessageQueue.IsEmpty(cachedCtx) {
err = s.processQueuedMessage(cachedCtx)
}
if tc.expectedErr != "" {
require.ErrorContains(err, tc.expectedErr)
} else {
Expand Down
Loading

0 comments on commit 91a4806

Please sign in to comment.