Skip to content

Commit

Permalink
fix: ensure delegation plan respects maxCanAllocate slice (#979)
Browse files Browse the repository at this point in the history
* fix: ensure delegation plan respects maxCanAllocate slice

* lint

---------

Co-authored-by: Jacob Gadikian <[email protected]>
  • Loading branch information
Joe Bowman and faddat authored Dec 30, 2023
1 parent ccfea16 commit 3cec0a5
Show file tree
Hide file tree
Showing 3 changed files with 164 additions and 19 deletions.
105 changes: 105 additions & 0 deletions x/interchainstaking/keeper/delegation_test.go

Large diffs are not rendered by default.

10 changes: 5 additions & 5 deletions x/interchainstaking/types/allocation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -208,17 +208,17 @@ func TestCurrentAllocationsMoreValidators(t *testing.T) {
}
currentSum := sdkmath.NewInt(600)
targetAllocations := types.ValidatorIntents{
{ValoperAddress: "validator1", Weight: sdk.NewDecWithPrec(3, 1)},
{ValoperAddress: "validator2", Weight: sdk.NewDecWithPrec(4, 1)},
{ValoperAddress: "validator1", Weight: sdk.NewDecWithPrec(43, 2)},
{ValoperAddress: "validator2", Weight: sdk.NewDecWithPrec(57, 2)},
}
amount := sdk.Coins{sdk.NewCoin("token", sdk.NewInt(1000))}

expectedAllocations := map[string]sdkmath.Int{
"validator1": sdkmath.NewInt(489),
"validator2": sdkmath.NewInt(511),
"validator1": sdkmath.NewInt(453),
"validator2": sdkmath.NewInt(547),
}

result, err := types.DetermineAllocationsForDelegation(currentAllocations, currentSum, targetAllocations, amount, make(map[string]sdkmath.Int))
result, err := types.DetermineAllocationsForDelegation(currentAllocations, currentSum, targetAllocations.Normalize(), amount, make(map[string]sdkmath.Int))
require.NoError(t, err)

if !reflect.DeepEqual(result, expectedAllocations) {
Expand Down
68 changes: 54 additions & 14 deletions x/interchainstaking/types/delegation.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,16 @@ func (vi ValidatorIntents) Sort() ValidatorIntents {
return vi
}

func (vi ValidatorIntents) Remove(valoper string) ValidatorIntents {
for i, v := range vi {
if v.ValoperAddress == valoper {
vi[i] = vi[len(vi)-1]
return vi[:len(vi)-1]
}
}
return vi
}

func (vi ValidatorIntents) GetForValoper(valoper string) (*ValidatorIntent, bool) {
for _, i := range vi {
if i.ValoperAddress == valoper {
Expand Down Expand Up @@ -136,29 +146,59 @@ func DetermineAllocationsForDelegation(currentAllocations map[string]sdkmath.Int
deltas, _ := CalculateAllocationDeltas(currentAllocations, map[string]bool{}, currentSum.Add(amount[0].Amount), targetAllocations, maxCanAllocate)
sum := deltas.Sum()

// unequalSplit is the portion of input that should be distributed in attempt to make targets == 0
unequalSplit := sdk.MinInt(sum, input)
// unequalAllocation is the portion of input that should be distributed in attempt to make targets == 0 (that is, in line with intent).
unequalAllocation := sdk.MinInt(sum, input)

if !unequalSplit.IsZero() {
if !unequalAllocation.IsZero() {
for idx := range deltas {
deltas[idx].Amount = sdk.NewDecFromInt(deltas[idx].Amount).QuoInt(sum).MulInt(unequalSplit).TruncateInt()
deltas[idx].Amount = sdk.NewDecFromInt(deltas[idx].Amount).QuoInt(sum).MulInt(unequalAllocation).TruncateInt()
}
}

// equalSplit is the portion of input that should be distributed equally across all validators, once targets are zero.
equalSplit := sdk.NewDecFromInt(input.Sub(unequalSplit))

// replace this portion with allocation proportional to targetAllocations!
if !equalSplit.IsZero() {
for _, targetAllocation := range targetAllocations.Sort() {
// proportionalAllocation is the portion of input that should be distributed proportionally to intent, once targets are zero, respecting caps.
proportionalAllocation := sdk.NewDecFromInt(input.Sub(unequalAllocation))

rounds := 0
// set maximum number of rounds, in case we get stuck in a weird loop we cannot resolve. If we exit the after this point, the remainder will be treated as dust.
maxRounds := 10
for ok := proportionalAllocation.IsPositive(); ok; ok = proportionalAllocation.IsPositive() && rounds < maxRounds {
// normalise targetAllocations, so maxed caps are handled nicely.
targetAllocations = targetAllocations.Normalize().Sort()
// initialise roundAllocation
roundAllocation := sdk.ZeroInt()
// for each target
for _, targetAllocation := range targetAllocations {
// does this target validator have a cap?
max, hasMax := maxCanAllocate[targetAllocation.ValoperAddress]
// does it have an existing allocation?
delta, found := deltas.GetForValoper(targetAllocation.GetValoperAddress())
if found {
delta.Amount = delta.Amount.Add(equalSplit.Mul(targetAllocation.Weight).TruncateInt())
} else {
delta = &AllocationDelta{ValoperAddress: targetAllocation.GetValoperAddress(), Amount: equalSplit.Mul(targetAllocation.Weight).TruncateInt()}
if !found {
// no existing delta, create new delta with zero
delta = &AllocationDelta{ValoperAddress: targetAllocation.GetValoperAddress(), Amount: sdk.ZeroInt()}
deltas = append(deltas, delta)
}
// allocate to this validator based on weight
thisAllocation := proportionalAllocation.Mul(targetAllocation.Weight).TruncateInt()
// if there is a cap...
if hasMax {
// determine if cap is breached
if delta.Amount.Add(thisAllocation).GTE(max) {
// if so, truncate and remove from target allocations for next round
thisAllocation = max.Sub(delta.Amount)
delta.Amount = max
targetAllocations = targetAllocations.Remove(delta.ValoperAddress)
} else {
// if not, increase delta
delta.Amount = delta.Amount.Add(thisAllocation)
}
}
// track round allocations to deduct from running total
roundAllocation = roundAllocation.Add(thisAllocation)
}
// deduct from running total
proportionalAllocation = proportionalAllocation.Sub(sdk.NewDecFromInt(roundAllocation))
// bail after N rounds
rounds++
}

// dust is the portion of the input that was truncated in previous calculations; add this to the first validator in the list,
Expand Down

0 comments on commit 3cec0a5

Please sign in to comment.