-
Notifications
You must be signed in to change notification settings - Fork 624
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
feat(osmoutils): custom accumulator position updates #3903
Changes from all commits
b50279a
cdb33cf
df87515
7774a8b
cc20a10
54e8b25
c00cc76
0cd22bc
88842df
782600e
6b2a9ab
25536f5
367a76c
54042ac
5b40a4f
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 | ||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|
|
@@ -77,10 +77,27 @@ func (accum *AccumulatorObject) UpdateAccumulator(amt sdk.DecCoins) { | |||||||||||
// The position is initialized with empty unclaimed rewards | ||||||||||||
// If there is an existing position for the given address, it is overwritten. | ||||||||||||
func (accum AccumulatorObject) NewPosition(name string, numShareUnits sdk.Dec, options *Options) error { | ||||||||||||
return accum.newPosition(name, numShareUnits, accum.value, options) | ||||||||||||
} | ||||||||||||
|
||||||||||||
// NewPositionCustomAcc creates a new position for the given name, with the given number of share units. | ||||||||||||
// The name can be an owner's address, or any other unique identifier for a position. | ||||||||||||
// It sets the position's accumulator to the given value of customAccumulatorValue. | ||||||||||||
// All custom accumulator values must be non-negative. | ||||||||||||
// The position is initialized with empty unclaimed rewards | ||||||||||||
// If there is an existing position for the given address, it is overwritten. | ||||||||||||
func (accum AccumulatorObject) NewPositionCustomAcc(name string, numShareUnits sdk.Dec, customAccumulatorValue sdk.DecCoins, options *Options) error { | ||||||||||||
if customAccumulatorValue.IsAnyNegative() { | ||||||||||||
return NegativeCustomAccError{customAccumulatorValue} | ||||||||||||
} | ||||||||||||
return accum.newPosition(name, numShareUnits, customAccumulatorValue, options) | ||||||||||||
} | ||||||||||||
|
||||||||||||
func (accum AccumulatorObject) newPosition(name string, numShareUnits sdk.Dec, positionAccumulatorInit sdk.DecCoins, options *Options) error { | ||||||||||||
if err := options.validate(); err != nil { | ||||||||||||
return err | ||||||||||||
} | ||||||||||||
createNewPosition(accum, name, numShareUnits, sdk.NewDecCoins(), options) | ||||||||||||
createNewPosition(accum, positionAccumulatorInit, name, numShareUnits, sdk.NewDecCoins(), options) | ||||||||||||
return nil | ||||||||||||
} | ||||||||||||
|
||||||||||||
|
@@ -99,6 +116,26 @@ func (accum AccumulatorObject) NewPosition(name string, numShareUnits sdk.Dec, o | |||||||||||
// - there is no existing position at the given address | ||||||||||||
// - other internal or database error occurs. | ||||||||||||
func (accum AccumulatorObject) AddToPosition(name string, newShares sdk.Dec) error { | ||||||||||||
return accum.AddToPositionCustomAcc(name, newShares, accum.value) | ||||||||||||
} | ||||||||||||
|
||||||||||||
// AddToPositionCustomAcc adds newShares of shares to an existing position with the given name. | ||||||||||||
// This is functionally equivalent to claiming rewards, closing down the position, and | ||||||||||||
// opening a fresh one with the new number of shares. We can represent this behavior by | ||||||||||||
// claiming rewards. The accumulator of the new position is set to given customAccumulatorValue. | ||||||||||||
// All custom accumulator values must be non-negative. They must also be a superset of the | ||||||||||||
// old accumulator value associated with the position. | ||||||||||||
// | ||||||||||||
// An alternative approach is to simply generate an additional position every time an | ||||||||||||
// address adds to its position. We do not pursue this path because we want to ensure | ||||||||||||
// that withdrawal and claiming functions remain constant time and do not scale with the | ||||||||||||
// number of times a user has added to their position. | ||||||||||||
Comment on lines
+128
to
+132
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. What do you think of removing this part?
Suggested change
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 think this is useful context |
||||||||||||
// | ||||||||||||
// Returns nil on success. Returns error when: | ||||||||||||
// - newShares are negative or zero. | ||||||||||||
// - there is no existing position at the given address | ||||||||||||
// - other internal or database error occurs. | ||||||||||||
func (accum AccumulatorObject) AddToPositionCustomAcc(name string, newShares sdk.Dec, customAccumulatorValue sdk.DecCoins) error { | ||||||||||||
if !newShares.IsPositive() { | ||||||||||||
return errors.New("Attempted to add a non-zero and non-negative number of shares to a position") | ||||||||||||
} | ||||||||||||
|
@@ -109,6 +146,10 @@ func (accum AccumulatorObject) AddToPosition(name string, newShares sdk.Dec) err | |||||||||||
return err | ||||||||||||
} | ||||||||||||
|
||||||||||||
if err := validateAccumulatorValue(customAccumulatorValue, position.InitAccumValue); err != nil { | ||||||||||||
return err | ||||||||||||
} | ||||||||||||
|
||||||||||||
// Save current number of shares and unclaimed rewards | ||||||||||||
unclaimedRewards := getTotalRewards(accum, position) | ||||||||||||
oldNumShares, err := accum.GetPositionSize(name) | ||||||||||||
|
@@ -118,7 +159,7 @@ func (accum AccumulatorObject) AddToPosition(name string, newShares sdk.Dec) err | |||||||||||
|
||||||||||||
// Update user's position with new number of shares while moving its unaccrued rewards | ||||||||||||
// into UnclaimedRewards. Starting accumulator value is moved up to accum'scurrent value | ||||||||||||
createNewPosition(accum, name, oldNumShares.Add(newShares), unclaimedRewards, position.Options) | ||||||||||||
createNewPosition(accum, customAccumulatorValue, name, oldNumShares.Add(newShares), unclaimedRewards, position.Options) | ||||||||||||
|
||||||||||||
return nil | ||||||||||||
} | ||||||||||||
|
@@ -128,6 +169,16 @@ func (accum AccumulatorObject) AddToPosition(name string, newShares sdk.Dec) err | |||||||||||
// overwrites the position record with the updated number of shares. Since it accrues rewards, it | ||||||||||||
// also moves up the position's accumulator value to the current accum val. | ||||||||||||
func (accum AccumulatorObject) RemoveFromPosition(name string, numSharesToRemove sdk.Dec) error { | ||||||||||||
return accum.RemoveFromPositionCustomAcc(name, numSharesToRemove, accum.value) | ||||||||||||
} | ||||||||||||
|
||||||||||||
// RemovePositionCustomAcc removes the specified number of shares from a position. Specifically, it claims | ||||||||||||
// the unclaimed and newly accrued rewards and returns them alongside the redeemed shares. Then, it | ||||||||||||
// overwrites the position record with the updated number of shares. Since it accrues rewards, it | ||||||||||||
// also resets the position's accumulator value to the given customAccumulatorValue. | ||||||||||||
// All custom accumulator values must be non-negative. They must also be a superset of the | ||||||||||||
// old accumulator value associated with the position. | ||||||||||||
func (accum AccumulatorObject) RemoveFromPositionCustomAcc(name string, numSharesToRemove sdk.Dec, customAccumulatorValue sdk.DecCoins) error { | ||||||||||||
// Cannot remove zero or negative shares | ||||||||||||
if numSharesToRemove.LTE(sdk.ZeroDec()) { | ||||||||||||
return fmt.Errorf("Attempted to remove no/negative shares (%s)", numSharesToRemove) | ||||||||||||
|
@@ -139,6 +190,10 @@ func (accum AccumulatorObject) RemoveFromPosition(name string, numSharesToRemove | |||||||||||
return err | ||||||||||||
} | ||||||||||||
|
||||||||||||
if err := validateAccumulatorValue(customAccumulatorValue, position.InitAccumValue); err != nil { | ||||||||||||
return err | ||||||||||||
} | ||||||||||||
|
||||||||||||
// Ensure not removing more shares than exist | ||||||||||||
if numSharesToRemove.GT(position.NumShares) { | ||||||||||||
return fmt.Errorf("Attempted to remove more shares (%s) than exist in the position (%s)", numSharesToRemove, position.NumShares) | ||||||||||||
|
@@ -151,13 +206,41 @@ func (accum AccumulatorObject) RemoveFromPosition(name string, numSharesToRemove | |||||||||||
return err | ||||||||||||
} | ||||||||||||
|
||||||||||||
createNewPosition(accum, name, oldNumShares.Sub(numSharesToRemove), unclaimedRewards, position.Options) | ||||||||||||
createNewPosition(accum, customAccumulatorValue, name, oldNumShares.Sub(numSharesToRemove), unclaimedRewards, position.Options) | ||||||||||||
|
||||||||||||
return nil | ||||||||||||
} | ||||||||||||
|
||||||||||||
// GetPositionSize returns the number of shares the position corresponding to postion's name | ||||||||||||
// or an error if no position exists. | ||||||||||||
// UpdatePosition updates the position with the given name by adding or removing | ||||||||||||
// the given number of shares. If numShares is positive, it is equivalent to calling | ||||||||||||
// AddToPosition. If numShares is negative, it is equivalent to calling RemoveFromPosition. | ||||||||||||
// Also, it moves up the position's accumulator value to the current accum value. | ||||||||||||
// Fails with error if numShares is zero. Returns nil on success. | ||||||||||||
func (accum AccumulatorObject) UpdatePosition(name string, numShares sdk.Dec) error { | ||||||||||||
return accum.UpdatePositionCustomAcc(name, numShares, accum.value) | ||||||||||||
} | ||||||||||||
|
||||||||||||
// UpdatePositionCustomAcc updates the position with the given name by adding or removing | ||||||||||||
// the given number of shares. If numShares is positive, it is equivalent to calling | ||||||||||||
// AddToPositionCustomAcc. If numShares is negative, it is equivalent to calling RemoveFromPositionCustomAcc. | ||||||||||||
// Fails with error if numShares is zero. Returns nil on success. | ||||||||||||
// It also resets the position's accumulator value to the given customAccumulatorValue. | ||||||||||||
// All custom accumulator values must be non-negative. They must also be a superset of the | ||||||||||||
// old accumulator value associated with the position. | ||||||||||||
func (accum AccumulatorObject) UpdatePositionCustomAcc(name string, numShares sdk.Dec, customAccumulatorValue sdk.DecCoins) error { | ||||||||||||
if numShares.Equal(sdk.ZeroDec()) { | ||||||||||||
return ZeroSharesError | ||||||||||||
} | ||||||||||||
|
||||||||||||
if numShares.IsNegative() { | ||||||||||||
return accum.RemoveFromPositionCustomAcc(name, numShares.Neg(), customAccumulatorValue) | ||||||||||||
} | ||||||||||||
|
||||||||||||
return accum.AddToPositionCustomAcc(name, numShares, customAccumulatorValue) | ||||||||||||
} | ||||||||||||
|
||||||||||||
// GetPositionSize returns the number of shares the position corresponding to `addr` | ||||||||||||
// in accumulator `accum` has, or an error if no position exists. | ||||||||||||
func (accum AccumulatorObject) GetPositionSize(name string) (sdk.Dec, error) { | ||||||||||||
position, err := getPosition(accum, name) | ||||||||||||
if err != nil { | ||||||||||||
|
@@ -192,7 +275,7 @@ func (accum AccumulatorObject) ClaimRewards(positionName string) (sdk.Coins, err | |||||||||||
|
||||||||||||
// Create a completely new position, with no rewards | ||||||||||||
// TODO: remove the position from state entirely if numShares = zero | ||||||||||||
createNewPosition(accum, positionName, position.NumShares, sdk.NewDecCoins(), position.Options) | ||||||||||||
createNewPosition(accum, accum.value, positionName, position.NumShares, sdk.NewDecCoins(), position.Options) | ||||||||||||
|
||||||||||||
return truncatedRewards, nil | ||||||||||||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -6,11 +6,15 @@ import ( | |
"github.com/osmosis-labs/osmosis/osmoutils" | ||
) | ||
|
||
var ( | ||
minusOne = sdk.NewDec(-1) | ||
) | ||
|
||
// Creates a new position at accumulator's current value with a specific number of shares and unclaimed rewards | ||
func createNewPosition(accum AccumulatorObject, index string, numShareUnits sdk.Dec, unclaimedRewards sdk.DecCoins, options *Options) { | ||
func createNewPosition(accum AccumulatorObject, accumulatorValue sdk.DecCoins, index string, numShareUnits sdk.Dec, unclaimedRewards sdk.DecCoins, options *Options) { | ||
position := Record{ | ||
NumShares: numShareUnits, | ||
InitAccumValue: accum.value, | ||
InitAccumValue: accumulatorValue, | ||
UnclaimedRewards: unclaimedRewards, | ||
Options: options, | ||
} | ||
|
@@ -35,8 +39,24 @@ func getPosition(accum AccumulatorObject, name string) (Record, error) { | |
func getTotalRewards(accum AccumulatorObject, position Record) sdk.DecCoins { | ||
totalRewards := position.UnclaimedRewards | ||
|
||
// TODO: add a check that accum.value is greater than position.InitAccumValue | ||
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 reckon this is small enough to be included in this PR, was there a separate reason or further investigation you wanted to do before adding this check? 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. This is non-blocking for fees. I don't want to get too wrapped up in the details to block further fee work. We have a bunch of TODOs in this package since we still are iterating and prototyping the final design. I think it would make sense to come back and address all the details once we finalize the Let me know if that doesn't convince you to keep this as todo for now |
||
accumulatorRewards := accum.value.Sub(position.InitAccumValue).MulDec(position.NumShares) | ||
totalRewards = totalRewards.Add(accumulatorRewards...) | ||
|
||
return totalRewards | ||
} | ||
|
||
// validateAccumulatorValue validates the provided accumulator. | ||
// All coins in custom accumulator value must be non-negative. | ||
// Custom accumulator value must be a superset of the old accumulator value. | ||
// Fails if any coin is negative. On success, returns nil. | ||
func validateAccumulatorValue(customAccumulatorValue, oldPositionAccumulatorValue sdk.DecCoins) error { | ||
if customAccumulatorValue.IsAnyNegative() { | ||
return NegativeCustomAccError{customAccumulatorValue} | ||
} | ||
newValue, IsAnyNegative := customAccumulatorValue.SafeSub(oldPositionAccumulatorValue) | ||
if IsAnyNegative { | ||
return NegativeAccDifferenceError{newValue.MulDec(minusOne)} | ||
} | ||
return nil | ||
} |
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -0,0 +1,50 @@ | ||||||
package accum_test | ||||||
|
||||||
import ( | ||||||
sdk "github.com/cosmos/cosmos-sdk/types" | ||||||
|
||||||
"github.com/osmosis-labs/osmosis/osmoutils/accum" | ||||||
) | ||||||
|
||||||
func (suite *AccumTestSuite) TestValidateAccumulatorValue() { | ||||||
tests := map[string]struct { | ||||||
customAccumulatorValue sdk.DecCoins | ||||||
oldPositionAccumulatorValue sdk.DecCoins | ||||||
expectError error | ||||||
}{ | ||||||
"negative custom coins - error": { | ||||||
customAccumulatorValue: initialCoinsDenomOne.MulDec(sdk.NewDec(-1)), | ||||||
oldPositionAccumulatorValue: emptyCoins, | ||||||
expectError: accum.NegativeCustomAccError{initialCoinsDenomOne.MulDec(sdk.NewDec(-1))}, | ||||||
}, | ||||||
"old accumulator coins are greater than new - error": { | ||||||
customAccumulatorValue: initialCoinsDenomOne, | ||||||
oldPositionAccumulatorValue: initialCoinsDenomOne.Add(sdk.NewDecCoin(initialCoinDenomOne.Denom, sdk.OneInt())), | ||||||
expectError: accum.NegativeAccDifferenceError{sdk.NewDecCoins(sdk.NewDecCoin(initialCoinDenomOne.Denom, sdk.OneInt()))}, | ||||||
}, | ||||||
"old accumulator coins are a superset of new - error": { | ||||||
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.
Suggested change
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. but they are in this test case. old coins contain coins from |
||||||
customAccumulatorValue: initialCoinsDenomOne, | ||||||
oldPositionAccumulatorValue: initialCoinsDenomOne.Add(initialCoinDenomTwo), | ||||||
expectError: accum.NegativeAccDifferenceError{sdk.NewDecCoins(initialCoinDenomTwo)}, | ||||||
}, | ||||||
"new accumulator coins are a superset of old - success": { | ||||||
customAccumulatorValue: initialCoinsDenomOne.Add(initialCoinDenomTwo), | ||||||
oldPositionAccumulatorValue: initialCoinsDenomOne, | ||||||
}, | ||||||
} | ||||||
|
||||||
for name, tc := range tests { | ||||||
suite.Run(name, func() { | ||||||
suite.SetupTest() | ||||||
|
||||||
err := accum.ValidateAccumulatorValue(tc.customAccumulatorValue, tc.oldPositionAccumulatorValue) | ||||||
|
||||||
if tc.expectError != nil { | ||||||
suite.Require().Error(err) | ||||||
suite.Require().Equal(tc.expectError, err) | ||||||
return | ||||||
} | ||||||
suite.Require().NoError(err) | ||||||
}) | ||||||
} | ||||||
} |
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.
Nit: consider changing method name to
NewPositionCustomizedAcc
it was after reading through all other codes that I understood what this method meant. (Just a personal opinion tho so please feel free to resolve this issue if you think the current method name is more intuitive)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.
I've considered this but tbh I prefer the original
NewPositionCustomAcc
better because it is shorter. To me, there is no semantical difference betweenCustom
andCustomized
.If you don't mind, I will keep the old name for shortness but lmk if any concerns