Skip to content

Commit

Permalink
fix(evmstaking): fix double spent issue of claimed reward
Browse files Browse the repository at this point in the history
  • Loading branch information
0xHansLee committed Oct 30, 2024
1 parent 90356a4 commit d47588c
Showing 1 changed file with 131 additions and 146 deletions.
277 changes: 131 additions & 146 deletions client/x/evmstaking/keeper/withdraw.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,6 @@ import (
"github.com/piplabs/story/lib/log"
)

type RewardWithdrawal struct {
DelegatorAddress string
ValidatorAddress string
WithdrawalEntry types.Withdrawal
RemainedBalance uint64
}

func (k Keeper) ProcessUnbondingWithdrawals(ctx context.Context, unbondedEntries []stypes.UnbondedEntry) error {
log.Debug(ctx, "Processing mature unbonding delegations", "count", len(unbondedEntries))

Expand Down Expand Up @@ -77,30 +70,17 @@ func (k Keeper) ProcessUnbondingWithdrawals(ctx context.Context, unbondedEntries
func (k Keeper) ProcessRewardWithdrawals(ctx context.Context) error {
log.Debug(ctx, "Processing reward withdrawals")

rewardWithdrawals, err := k.ExpectedRewardWithdrawals(ctx)
if err != nil {
return errors.Wrap(err, "get expected reward withdrawals")
}

if err := k.EnqueueEligibleRewardWithdrawal(ctx, rewardWithdrawals); err != nil {
return errors.Wrap(err, "enqueue eligible reward withdrawals")
}

return nil
}

func (k Keeper) ExpectedRewardWithdrawals(ctx context.Context) ([]RewardWithdrawal, error) {
validatorSweepIndex, err := k.GetValidatorSweepIndex(ctx)
if err != nil {
return nil, err
return errors.Wrap(err, "get validator sweep index")
}

nextValIndex, nextValDelIndex := validatorSweepIndex.NextValIndex, validatorSweepIndex.NextValDelIndex

// Get all validators first, and then do a circular sweep
validatorSet, err := (k.stakingKeeper.(*skeeper.Keeper)).GetAllValidators(ctx)
if err != nil {
return nil, errors.Wrap(err, "get all validators")
return errors.Wrap(err, "get all validators")
}

if nextValIndex >= uint64(len(validatorSet)) {
Expand All @@ -116,21 +96,18 @@ func (k Keeper) ExpectedRewardWithdrawals(ctx context.Context) ([]RewardWithdraw
}

// Iterate all validators from `nextValidatorIndex` to find out eligible partial withdrawals.
var (
swept uint32
withdrawals []RewardWithdrawal
)
var swept uint32

// Get sweep limit per block.
sweepBound, err := k.MaxSweepPerBlock(ctx)
if err != nil {
return nil, err
return errors.Wrap(err, "get max sweep per block")
}

// Get minimal partial withdrawal amount.
minPartialWithdrawalAmount, err := k.MinPartialWithdrawalAmount(ctx)
// Get minimal reward withdrawal amount.
minRewardWithdrawalAmount, err := k.MinPartialWithdrawalAmount(ctx)
if err != nil {
return nil, err
return errors.Wrap(err, "get minimum partial withdrawal amount")
}

// Sweep and get eligible partial withdrawals.
Expand All @@ -143,86 +120,38 @@ func (k Keeper) ExpectedRewardWithdrawals(ctx context.Context) ([]RewardWithdraw
continue
}

// Get validator's address.
valBz, err := k.stakingKeeper.ValidatorAddressCodec().StringToBytes(validatorSet[nextValIndex].GetOperator())
if err != nil {
return nil, errors.Wrap(err, "validator address from bech32")
}
valAddr := sdk.ValAddress(valBz)
valAccAddr := sdk.AccAddress(valAddr)

// Get validator commissions.
valCommission, err := k.distributionKeeper.GetValidatorAccumulatedCommission(ctx, valAddr)
valAddr, err := k.stakingKeeper.ValidatorAddressCodec().StringToBytes(validatorSet[nextValIndex].GetOperator())
if err != nil {
return nil, err
return errors.Wrap(err, "convert validator address from string to bytes")
}

// Get all delegators of the validator.
delegators, err := (k.stakingKeeper.(*skeeper.Keeper)).GetValidatorDelegations(ctx, valAddr)
delegations, err := (k.stakingKeeper.(*skeeper.Keeper)).GetValidatorDelegations(ctx, sdk.ValAddress(valAddr))
if err != nil {
return nil, errors.Wrap(err, "get validator delegations")
return errors.Wrap(err, "get validator delegations")
}

if nextValDelIndex >= uint64(len(delegators)) {
if nextValDelIndex >= uint64(len(delegations)) {
nextValIndex = (nextValIndex + 1) % uint64(len(validatorSet))
nextValDelIndex = 0

continue
}

nextDelegators := delegators[nextValDelIndex:]
nextDelegations := delegations[nextValDelIndex:]
var shouldStopPrematurely bool

// Check if the sweep should stop prematurely as the current delegator loop exceeds the sweep bound while sweeping.
remainingSweep := sweepBound - swept
if uint32(len(nextDelegators)) > remainingSweep {
nextDelegators = nextDelegators[:remainingSweep]
if uint32(len(nextDelegations)) > remainingSweep {
nextDelegations = nextDelegations[:remainingSweep]
shouldStopPrematurely = true
}

// Iterate on the validator's delegator rewards in the range [nextValDelIndex, len(delegators)].
for i := range nextDelegators {
// Get end current period and calculate rewards.
endingPeriod, err := k.distributionKeeper.IncrementValidatorPeriod(ctx, validatorSet[nextValIndex])
if err != nil {
return nil, err
}

delRewards, err := k.distributionKeeper.CalculateDelegationRewards(ctx, validatorSet[nextValIndex], nextDelegators[i], endingPeriod)
if err != nil {
return nil, err
}

if nextDelegators[i].DelegatorAddress == valAccAddr.String() {
delRewards = delRewards.Add(valCommission.Commission...)
}

addr, err := sdk.AccAddressFromBech32(nextDelegators[i].DelegatorAddress)
if err != nil {
return nil, errors.Wrap(err, "convert acc address from bech32 address")
}

delRewardsTruncated, _ := delRewards.TruncateDecimal()
rewardInDistrKeeper := delRewardsTruncated.AmountOf(sdk.DefaultBondDenom).Uint64()
rewardInBankKeeper := k.bankKeeper.GetBalance(ctx, addr, sdk.DefaultBondDenom).Amount.Uint64()
totalReward := rewardInDistrKeeper + rewardInBankKeeper

if totalReward >= minPartialWithdrawalAmount {
delEvmAddr, err := k.DelegatorRewardAddress.Get(ctx, nextDelegators[i].DelegatorAddress)
if err != nil {
return nil, errors.Wrap(err, "map delegator pubkey to evm reward address")
}

withdrawals = append(withdrawals, RewardWithdrawal{
DelegatorAddress: nextDelegators[i].DelegatorAddress,
ValidatorAddress: valAddr.String(),
WithdrawalEntry: types.NewWithdrawal(
uint64(sdk.UnwrapSDKContext(ctx).BlockHeight()),
delEvmAddr,
rewardInDistrKeeper,
),
RemainedBalance: rewardInBankKeeper,
})
for _, delegation := range nextDelegations {
if err := k.ProcessEligibleRewardWithdrawal(ctx, delegation, validatorSet[nextValIndex], minRewardWithdrawalAmount); err != nil {
return errors.Wrap(err, "process eligible reward withdrawal")
}

nextValDelIndex++
Expand All @@ -239,92 +168,148 @@ func (k Keeper) ExpectedRewardWithdrawals(ctx context.Context) ([]RewardWithdraw
nextValDelIndex = 0

// Increase the total swept amount.
swept += uint32(len(nextDelegators))
swept += uint32(len(nextDelegations))
}

// Update the validator sweep index.
if err := k.SetValidatorSweepIndex(ctx, types.NewValidatorSweepIndex(nextValIndex, nextValDelIndex)); err != nil {
return nil, err
return errors.Wrap(err, "set validator sweep index")
}

log.Debug(
ctx, "Finish validator sweep for partial withdrawals",
"next_validator_index", nextValIndex,
"next_validator_delegation_index", nextValDelIndex,
"partial_withdrawals", len(withdrawals),
)

return withdrawals, nil
return nil
}

func (k Keeper) EnqueueEligibleRewardWithdrawal(ctx context.Context, rewardWithdrawals []RewardWithdrawal) error {
for i := range rewardWithdrawals {
valAddr, err := sdk.ValAddressFromBech32(rewardWithdrawals[i].ValidatorAddress)
if err != nil {
return errors.Wrap(err, "validator address from bech32")
}
// ProcessEligibleRewardWithdrawal processes the reward withdrawal of delegation.
func (k Keeper) ProcessEligibleRewardWithdrawal(ctx context.Context, delegation stypes.Delegation, validator stypes.Validator, minRewardWithdrawalAmount uint64) error {
// Get validator's address.
valBz, err := k.stakingKeeper.ValidatorAddressCodec().StringToBytes(validator.GetOperator())
if err != nil {
return errors.Wrap(err, "validator address from bech32")
}
valAddr := sdk.ValAddress(valBz)
valAccAddr := sdk.AccAddress(valAddr)

// Get end current period and calculate rewards.
endingPeriod, err := k.distributionKeeper.IncrementValidatorPeriod(ctx, validator)
if err != nil {
return err
}

valAccAddr := sdk.AccAddress(valAddr).String()
delRewards, err := k.distributionKeeper.CalculateDelegationRewards(ctx, validator, delegation, endingPeriod)
if err != nil {
return err
}

// Withdraw delegation rewards.
delAddr := sdk.MustAccAddressFromBech32(rewardWithdrawals[i].DelegatorAddress)
delRewards, err := k.distributionKeeper.WithdrawDelegationRewards(ctx, delAddr, valAddr)
// if it is self-delegation, add commission
if delegation.DelegatorAddress == valAccAddr.String() {
// Get validator commissions.
valCommission, err := k.distributionKeeper.GetValidatorAccumulatedCommission(ctx, valAddr)
if err != nil {
return err
}

// Withdraw commission if it is a self delegation.
if rewardWithdrawals[i].DelegatorAddress == valAccAddr {
commissionRewards, err := k.distributionKeeper.WithdrawValidatorCommission(ctx, valAddr)
if errors.Is(err, dtypes.ErrNoValidatorCommission) {
log.Debug(
ctx, "No validator commission",
"validator_addr", rewardWithdrawals[i].ValidatorAddress,
"validator_account_addr", valAccAddr,
)
} else if err != nil {
return err
} else {
delRewards = delRewards.Add(commissionRewards...)
}
}
delRewards = delRewards.Add(valCommission.Commission...)
}

if rewardWithdrawals[i].RemainedBalance > 0 {
rewardCoins := sdk.NewCoin(sdk.DefaultBondDenom, math.NewInt(int64(rewardWithdrawals[i].RemainedBalance)))
delRewards = delRewards.Add(rewardCoins)
}
addr, err := sdk.AccAddressFromBech32(delegation.DelegatorAddress)
if err != nil {
return errors.Wrap(err, "convert acc address from bech32 address")
}

delRewardUint64 := delRewards.AmountOf(sdk.DefaultBondDenom).Uint64()
log.Debug(
ctx, "Withdraw delegator rewards",
"validator_addr", rewardWithdrawals[i].ValidatorAddress,
"validator_account_addr", valAccAddr,
"delegator_addr", rewardWithdrawals[i].DelegatorAddress,
"amount_calculate", rewardWithdrawals[i].WithdrawalEntry.Amount,
"amount_deposited", rewardWithdrawals[i].RemainedBalance,
"amount_total_withdrawal", delRewardUint64,
)
delRewardsTruncated, _ := delRewards.TruncateDecimal()
unclaimedReward := delRewardsTruncated.AmountOf(sdk.DefaultBondDenom).Uint64()
claimedReward := k.bankKeeper.SpendableCoin(ctx, addr, sdk.DefaultBondDenom).Amount.Uint64()
totalReward := unclaimedReward + claimedReward

// Burn tokens from the delegator
err = k.bankKeeper.SendCoinsFromAccountToModule(ctx, delAddr, types.ModuleName, delRewards)
if err != nil {
return err
}
err = k.bankKeeper.BurnCoins(ctx, types.ModuleName, delRewards)
if err != nil {
return err
// if total reward is greater than or equal to min reward withdrawal amount, enqueue the reward withdrawal
if totalReward >= minRewardWithdrawalAmount {
if err := k.EnqueueRewardWithdrawal(ctx, delegation.DelegatorAddress, valAddr.String(), claimedReward); err != nil {
return errors.Wrap(err, "enqueue reward withdrawal")
}
}

// Enqueue to the global withdrawal queue.
if err := k.AddRewardWithdrawalToQueue(ctx, types.NewWithdrawal(
uint64(sdk.UnwrapSDKContext(ctx).BlockHeight()),
rewardWithdrawals[i].WithdrawalEntry.ExecutionAddress,
delRewardUint64,
)); err != nil {
return nil
}

// EnqueueRewardWithdrawal enqueues the reward withdrawal to mint reward IP token on EL side.
func (k Keeper) EnqueueRewardWithdrawal(ctx context.Context, delAddrBech32, valAddrBech32 string, claimedReward uint64) error {
valAddr, err := sdk.ValAddressFromBech32(valAddrBech32)
if err != nil {
return errors.Wrap(err, "validator address from bech32")
}

valAccAddr := sdk.AccAddress(valAddr).String()

// Withdraw delegation rewards.
delAddr := sdk.MustAccAddressFromBech32(delAddrBech32)
delRewards, err := k.distributionKeeper.WithdrawDelegationRewards(ctx, delAddr, valAddr)
if err != nil {
return err
}

// Withdraw commission if it is a self delegation.
if delAddrBech32 == valAccAddr {
commissionRewards, err := k.distributionKeeper.WithdrawValidatorCommission(ctx, valAddr)
if errors.Is(err, dtypes.ErrNoValidatorCommission) {
log.Debug(
ctx, "No validator commission",
"validator_addr", valAddrBech32,
"validator_account_addr", valAccAddr,
)
} else if err != nil {
return err
} else {
delRewards = delRewards.Add(commissionRewards...)
}
}

if claimedReward > 0 {
rewardCoins := sdk.NewCoin(sdk.DefaultBondDenom, math.NewInt(int64(claimedReward)))
delRewards = delRewards.Add(rewardCoins)
}

delRewardUint64 := delRewards.AmountOf(sdk.DefaultBondDenom).Uint64()

withdrawalEVMAddr, err := k.DelegatorRewardAddress.Get(ctx, delAddrBech32)
if err != nil {
return errors.Wrap(err, "map delegator pubkey to evm reward address")
}

log.Debug(
ctx, "Withdraw delegator rewards",
"validator_addr", valAddrBech32,
"validator_account_addr", valAccAddr,
"delegator_addr", delAddrBech32,
"amount_calculate", withdrawalEVMAddr,
"amount_claimed", claimedReward,
"amount_total_withdrawal", delRewardUint64,
)

// Burn tokens from the delegator
err = k.bankKeeper.SendCoinsFromAccountToModule(ctx, delAddr, types.ModuleName, delRewards)
if err != nil {
return err
}
err = k.bankKeeper.BurnCoins(ctx, types.ModuleName, delRewards)
if err != nil {
return err
}

// Enqueue to the global withdrawal queue.
if err := k.AddRewardWithdrawalToQueue(ctx, types.NewWithdrawal(
uint64(sdk.UnwrapSDKContext(ctx).BlockHeight()),
withdrawalEVMAddr,
delRewardUint64,
)); err != nil {
return errors.Wrap(err, "add reward withdrawal to queue")
}

return nil
}

Expand Down

0 comments on commit d47588c

Please sign in to comment.