RemoveStakes and RemoveDelegateStakes silently handle errors in EndBlocker
Summary
RemoveStakes and RemoveDelegateStakes silently handle errors in EndBlocker.
Vulnerability Detail
When finalizing stake removals in the EndBlocker, for each removal :
1) First, the unstaked amount is sent to the staker.
2) Then the state is updated with RemoveReputerStake / RemoveDelegateStake to reflect the removal (.i.e : update total stakes, update topic stakes, update reputer stakes, delete the processed removal, ...).
If an error happens at any of these stages, it simply continues to the next removal.
Impact
An error in the first step doesn't lead to an issue, however, an error in the second step at any stage leads to :
An inconsistent written state as some changes will be written while the ones that are meant to happen after the error will not be written.
The staker will receive his stakes and still be able to re queue another removal for the same stakes.
func RemoveStakes(
sdkCtx sdk.Context,
currentBlock int64,
k emissionskeeper.Keeper,
) {
removals, err := k.GetStakeRemovalsForBlock(sdkCtx, currentBlock)
if err != nil {
sdkCtx.Logger().Error(fmt.Sprintf(
"Unable to get stake removals for block %d, skipping removing stakes: %v",
currentBlock,
err,
))
return
}
for _, stakeRemoval := range removals {
// do no checking that the stake removal struct is valid. In order to have a stake removal
// it would have had to be created in msgServer.RemoveStake which would have done
// validation of validity up front before scheduling the delay
// Check the module has enough funds to send back to the sender
// Bank module does this for us in module SendCoins / subUnlockedCoins so we don't need to check
// Send the funds
coins := sdk.NewCoins(sdk.NewCoin(chainParams.DefaultBondDenom, stakeRemoval.Amount))
err = k.SendCoinsFromModuleToAccount(sdkCtx, emissionstypes.AlloraStakingAccountName, stakeRemoval.Reputer, coins)
if err != nil {
sdkCtx.Logger().Error(fmt.Sprintf(
"Error removing stake: %v | %v",
stakeRemoval,
err,
))
continue
}
// Update the stake data structures
err = k.RemoveReputerStake( // <===== Audit
sdkCtx,
currentBlock,
stakeRemoval.TopicId,
stakeRemoval.Reputer,
stakeRemoval.Amount,
)
if err != nil { // <===== Audit
sdkCtx.Logger().Error(fmt.Sprintf(
"Error removing stake: %v | %v",
stakeRemoval,
err,
))
continue // <===== Audit
}
}
}
func RemoveDelegateStakes(
sdkCtx sdk.Context,
currentBlock int64,
k emissionskeeper.Keeper,
) {
removals, err := k.GetDelegateStakeRemovalsForBlock(sdkCtx, currentBlock)
if err != nil {
sdkCtx.Logger().Error(
fmt.Sprintf(
"Unable to get stake removals for block %d, skipping removing stakes: %v",
currentBlock,
err,
))
return
}
for _, stakeRemoval := range removals {
// do no checking that the stake removal struct is valid. In order to have a stake removal
// it would have had to be created in msgServer.RemoveDelegateStake which would have done
// validation of validity up front before scheduling the delay
// Check the module has enough funds to send back to the sender
// Bank module does this for us in module SendCoins / subUnlockedCoins so we don't need to check
// Send the funds
coins := sdk.NewCoins(sdk.NewCoin(chainParams.DefaultBondDenom, stakeRemoval.Amount))
err = k.SendCoinsFromModuleToAccount(sdkCtx, emissionstypes.AlloraStakingAccountName, stakeRemoval.Delegator, coins)
if err != nil {
sdkCtx.Logger().Error(fmt.Sprintf(
"Error removing stake: %v | %v",
stakeRemoval,
err,
))
continue
}
// Update the stake data structures
err = k.RemoveDelegateStake( // <===== Audit
sdkCtx,
currentBlock,
stakeRemoval.TopicId,
stakeRemoval.Delegator,
stakeRemoval.Reputer,
stakeRemoval.Amount,
)
if err != nil { // <===== Audit
sdkCtx.Logger().Error(fmt.Sprintf(
"Error removing stake: %v | %v",
stakeRemoval,
err,
))
continue // <===== Audit
}
}
}
Try to update the state first using a cache context and only write the changes if there are no errors.
diff --git a/allora-chain/x/emissions/module/stake_removals.go b/allora-chain/x/emissions/module/stake_removals.go
index 14d45c6..1f235e9 100644
--- a/allora-chain/x/emissions/module/stake_removals.go
+++ b/allora-chain/x/emissions/module/stake_removals.go
@@ -25,15 +25,16 @@ func RemoveStakes(
return
}
for _, stakeRemoval := range removals {
- // do no checking that the stake removal struct is valid. In order to have a stake removal
- // it would have had to be created in msgServer.RemoveStake which would have done
- // validation of validity up front before scheduling the delay
+ cacheSdkCtx, write := sdkCtx.CacheContext()
- // Check the module has enough funds to send back to the sender
- // Bank module does this for us in module SendCoins / subUnlockedCoins so we don't need to check
- // Send the funds
- coins := sdk.NewCoins(sdk.NewCoin(chainParams.DefaultBondDenom, stakeRemoval.Amount))
- err = k.SendCoinsFromModuleToAccount(sdkCtx, emissionstypes.AlloraStakingAccountName, stakeRemoval.Reputer, coins)
+ // Update the stake data structures
+ err = k.RemoveReputerStake(
+ cacheSdkCtx,
+ currentBlock,
+ stakeRemoval.TopicId,
+ stakeRemoval.Reputer,
+ stakeRemoval.Amount,
+ )
if err != nil {
sdkCtx.Logger().Error(fmt.Sprintf(
"Error removing stake: %v | %v",
@@ -43,14 +44,15 @@ func RemoveStakes(
continue
}
- // Update the stake data structures
- err = k.RemoveReputerStake(
- sdkCtx,
- currentBlock,
- stakeRemoval.TopicId,
- stakeRemoval.Reputer,
- stakeRemoval.Amount,
- )
+ // do no checking that the stake removal struct is valid. In order to have a stake removal
+ // it would have had to be created in msgServer.RemoveStake which would have done
+ // validation of validity up front before scheduling the delay
+
+ // Check the module has enough funds to send back to the sender
+ // Bank module does this for us in module SendCoins / subUnlockedCoins so we don't need to check
+ // Send the funds
+ coins := sdk.NewCoins(sdk.NewCoin(chainParams.DefaultBondDenom, stakeRemoval.Amount))
+ err = k.SendCoinsFromModuleToAccount(sdkCtx, emissionstypes.AlloraStakingAccountName, stakeRemoval.Reputer, coins)
if err != nil {
sdkCtx.Logger().Error(fmt.Sprintf(
"Error removing stake: %v | %v",
@@ -59,6 +61,8 @@ func RemoveStakes(
))
continue
}
+
+ write()
}
}
@@ -79,15 +83,18 @@ func RemoveDelegateStakes(
return
}
for _, stakeRemoval := range removals {
- // do no checking that the stake removal struct is valid. In order to have a stake removal
- // it would have had to be created in msgServer.RemoveDelegateStake which would have done
- // validation of validity up front before scheduling the delay
+ cacheSdkCtx, write := sdkCtx.CacheContext()
+
+ // Update the stake data structures
+ err = k.RemoveDelegateStake(
+ cacheSdkCtx,
+ currentBlock,
+ stakeRemoval.TopicId,
+ stakeRemoval.Delegator,
+ stakeRemoval.Reputer,
+ stakeRemoval.Amount,
+ )
- // Check the module has enough funds to send back to the sender
- // Bank module does this for us in module SendCoins / subUnlockedCoins so we don't need to check
- // Send the funds
- coins := sdk.NewCoins(sdk.NewCoin(chainParams.DefaultBondDenom, stakeRemoval.Amount))
- err = k.SendCoinsFromModuleToAccount(sdkCtx, emissionstypes.AlloraStakingAccountName, stakeRemoval.Delegator, coins)
if err != nil {
sdkCtx.Logger().Error(fmt.Sprintf(
"Error removing stake: %v | %v",
@@ -97,15 +104,15 @@ func RemoveDelegateStakes(
continue
}
- // Update the stake data structures
- err = k.RemoveDelegateStake(
- sdkCtx,
- currentBlock,
- stakeRemoval.TopicId,
- stakeRemoval.Delegator,
- stakeRemoval.Reputer,
- stakeRemoval.Amount,
- )
+ // do no checking that the stake removal struct is valid. In order to have a stake removal
+ // it would have had to be created in msgServer.RemoveDelegateStake which would have done
+ // validation of validity up front before scheduling the delay
+
+ // Check the module has enough funds to send back to the sender
+ // Bank module does this for us in module SendCoins / subUnlockedCoins so we don't need to check
+ // Send the funds
+ coins := sdk.NewCoins(sdk.NewCoin(chainParams.DefaultBondDenom, stakeRemoval.Amount))
+ err = k.SendCoinsFromModuleToAccount(sdkCtx, emissionstypes.AlloraStakingAccountName, stakeRemoval.Delegator, coins)
if err != nil {
sdkCtx.Logger().Error(fmt.Sprintf(
"Error removing stake: %v | %v",
@@ -114,5 +121,7 @@ func RemoveDelegateStakes(
))
continue
}
+
+ write()
}
}
imsrybr0
High
RemoveStakes and RemoveDelegateStakes silently handle errors in EndBlocker
Summary
RemoveStakes and RemoveDelegateStakes silently handle errors in EndBlocker.
Vulnerability Detail
When finalizing stake removals in the EndBlocker, for each removal : 1) First, the unstaked amount is sent to the staker. 2) Then the state is updated with
RemoveReputerStake
/RemoveDelegateStake
to reflect the removal (.i.e : update total stakes, update topic stakes, update reputer stakes, delete the processed removal, ...).If an error happens at any of these stages, it simply continues to the next removal.
Impact
An error in the first step doesn't lead to an issue, however, an error in the second step at any stage leads to :
Code Snippet
EndBlocker
RemoveStakes
RemoveDelegateStakes
RemoveReputerStake
RemoveDelegateStake
Tool used
Manual Review
Recommendation
Try to update the state first using a cache context and only write the changes if there are no errors.