-
Notifications
You must be signed in to change notification settings - Fork 4k
fix(x/bank): Better handling of negative spendable balances #24060
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 14 commits
6d063ff
e413f78
86f4d18
825fecc
1c9bc0a
e804514
ab257a1
d961824
2c28093
a9022e3
f75d4a2
e830d07
ded9c9a
280c2a3
6f3d40f
ce7d6f0
e5df954
fe0862d
00b2355
cda9f34
8f7103b
f9c8f0f
c7b3797
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -190,11 +190,34 @@ func (k BaseViewKeeper) LockedCoins(ctx context.Context, addr sdk.AccAddress) sd | |
return sdk.NewCoins() | ||
} | ||
|
||
// SpendableCoins returns the total balances of spendable coins for an account | ||
// by address. If the account has no spendable coins, an empty Coins slice is | ||
// returned. | ||
// SpendableCoins returns the total balances of spendable coins (i.e. unlocked) for an account by address. | ||
// It works as follows: | ||
// 1. It retrieves the total balance (all coins) for the account. | ||
// 2. It gets the set of locked coins (coins that are not spendable, e.g. due to vesting). | ||
// 3. If there are no locked coins, it returns the total balance as all coins are spendable. | ||
// 4. Otherwise, it attempts to subtract the locked coins from the total balance using SafeSub. | ||
// - If SafeSub does not produce any negative values, the result (unlocked coins) is returned. | ||
// - If subtraction results in any negative values for a denomination (i.e. there are more locked coins than available in that denom), | ||
// then for that denomination the spendable amount is considered to be zero. In this case, the function iterates over each coin in the | ||
// unlocked result and includes only those coins with a positive amount in the final spendable coins set. | ||
func (k BaseViewKeeper) SpendableCoins(ctx context.Context, addr sdk.AccAddress) sdk.Coins { | ||
spendable, _ := k.spendableCoins(ctx, addr) | ||
total := k.GetAllBalances(ctx, addr) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we add a comment to the function that describes exactly how this works? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. i.e. What we do when we have:
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I updated with specific flow |
||
allLocked := k.LockedCoins(ctx, addr) | ||
if allLocked.IsZero() { | ||
return total | ||
} | ||
|
||
unlocked, hasNeg := total.SafeSub(allLocked...) | ||
if !hasNeg { | ||
return unlocked | ||
} | ||
|
||
spendable := sdk.Coins{} | ||
for _, coin := range unlocked { | ||
if coin.IsPositive() { | ||
spendable = append(spendable, coin) | ||
} | ||
} | ||
return spendable | ||
} | ||
|
||
|
@@ -203,23 +226,14 @@ func (k BaseViewKeeper) SpendableCoins(ctx context.Context, addr sdk.AccAddress) | |
// is returned. | ||
func (k BaseViewKeeper) SpendableCoin(ctx context.Context, addr sdk.AccAddress, denom string) sdk.Coin { | ||
balance := k.GetBalance(ctx, addr, denom) | ||
locked := k.LockedCoins(ctx, addr) | ||
return balance.SubAmount(locked.AmountOf(denom)) | ||
} | ||
|
||
// spendableCoins returns the coins the given address can spend alongside the total amount of coins it holds. | ||
// It exists for gas efficiency, in order to avoid to have to get balance multiple times. | ||
func (k BaseViewKeeper) spendableCoins(ctx context.Context, addr sdk.AccAddress) (spendable, total sdk.Coins) { | ||
total = k.GetAllBalances(ctx, addr) | ||
locked := k.LockedCoins(ctx, addr) | ||
|
||
spendable, hasNeg := total.SafeSub(locked...) | ||
if hasNeg { | ||
spendable = sdk.NewCoins() | ||
return | ||
lockedAmt := k.LockedCoins(ctx, addr).AmountOf(denom) | ||
if !lockedAmt.IsPositive() { | ||
return balance | ||
} | ||
|
||
return | ||
if lockedAmt.LT(balance.Amount) { | ||
return balance.SubAmount(lockedAmt) | ||
} | ||
return sdk.NewCoin(denom, math.ZeroInt()) | ||
} | ||
|
||
// ValidateBalance validates all balances for a given account address returning | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@aljo242 I also added document on what testcases we have. I hope it cover every paths you need.