Skip to content
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: Add option to designate Reward Recipient to Lock and Incentives #5281

Merged
merged 18 commits into from
May 29, 2023
Merged
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
* [#4830](https://github.com/osmosis-labs/osmosis/pull/4830) Add gas cost when we AddToGaugeRewards, linearly increase with coins to add
* [#4886](https://github.com/osmosis-labs/osmosis/pull/4886) Implement MsgSplitRouteSwapExactAmountIn and MsgSplitRouteSwapExactAmountOut that supports route splitting.
* [#5000](https://github.com/osmosis-labs/osmosis/pull/5000) osmomath.Power panics for base < 1 to temporarily restrict broken logic for such base.
* [#5281](https://github.com/osmosis-labs/osmosis/pull/5281) Add option to designate Reward Recipient to Lock and Incentives.
* [#4827] (https://github.com/osmosis-labs/osmosis/pull/4827) Protorev: Change highest liquidity pool updating from weekly to daily and change dev fee payout from weekly to after every trade.

### Misc Improvements
Expand Down
5 changes: 5 additions & 0 deletions proto/osmosis/lockup/lock.proto
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,11 @@ message PeriodLock {
(gogoproto.nullable) = false,
(gogoproto.castrepeated) = "github.com/cosmos/cosmos-sdk/types.Coins"
];
// Reward Receiver Address is the address that would be receiving rewards for
// the incentives for the lock. This is set to owner by default and can be
// changed via separate msg.
string reward_receiver_address = 6
[ (gogoproto.moretags) = "yaml:\"reward_receiver_address\"" ];
}

// LockQueryType defines the type of the lock query that can
Expand Down
13 changes: 12 additions & 1 deletion proto/osmosis/lockup/tx.proto
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ service Msg {
// MsgEditLockup edits the existing lockups by lock ID
rpc ExtendLockup(MsgExtendLockup) returns (MsgExtendLockupResponse);
rpc ForceUnlock(MsgForceUnlock) returns (MsgForceUnlockResponse);
// SetRewardReceiverAddress edits the reward receiver for the given lock ID
rpc SetRewardReceiverAddress(MsgSetRewardReceiverAddress)
returns (MsgSetRewardReceiverAddressResponse);
}

message MsgLockTokens {
Expand Down Expand Up @@ -95,4 +98,12 @@ message MsgForceUnlock {
];
}

message MsgForceUnlockResponse { bool success = 1; }
message MsgForceUnlockResponse { bool success = 1; }

message MsgSetRewardReceiverAddress {
string owner = 1 [ (gogoproto.moretags) = "yaml:\"owner\"" ];
uint64 lockID = 2;
string reward_receiver = 3
[ (gogoproto.moretags) = "yaml:\"reward_receiver\"" ];
}
message MsgSetRewardReceiverAddressResponse { bool success = 1; }
47 changes: 29 additions & 18 deletions x/incentives/keeper/distribute.go
Original file line number Diff line number Diff line change
Expand Up @@ -158,53 +158,58 @@ func (k Keeper) FilteredLocksDistributionEst(ctx sdk.Context, gauge types.Gauge,
// distributionInfo stores all of the information for pent up sends for rewards distributions.
// This enables us to lower the number of events and calls to back.
type distributionInfo struct {
nextID int
lockOwnerAddrToID map[string]int
idToBech32Addr []string
idToDecodedAddr []sdk.AccAddress
idToDistrCoins []sdk.Coins
nextID int
lockOwnerAddrToID map[string]int
lockOwnerAddrToRewardReceiver map[string]string
idToBech32Addr []string
idToDecodedRewardReceiverAddr []sdk.AccAddress
idToDistrCoins []sdk.Coins
}

// newDistributionInfo creates a new distributionInfo struct
func newDistributionInfo() distributionInfo {
return distributionInfo{
nextID: 0,
lockOwnerAddrToID: make(map[string]int),
idToBech32Addr: []string{},
idToDecodedAddr: []sdk.AccAddress{},
idToDistrCoins: []sdk.Coins{},
nextID: 0,
lockOwnerAddrToID: make(map[string]int),
lockOwnerAddrToRewardReceiver: make(map[string]string),
idToBech32Addr: []string{},
idToDecodedRewardReceiverAddr: []sdk.AccAddress{},
idToDistrCoins: []sdk.Coins{},
}
}

// addLockRewards adds the provided rewards to the lockID mapped to the provided owner address.
func (d *distributionInfo) addLockRewards(owner string, rewards sdk.Coins) error {
func (d *distributionInfo) addLockRewards(owner, rewardReceiver string, rewards sdk.Coins) error {
// if we have already added current lock owner's info to distribution Info, simply add reward.
if id, ok := d.lockOwnerAddrToID[owner]; ok {
oldDistrCoins := d.idToDistrCoins[id]
d.idToDistrCoins[id] = rewards.Add(oldDistrCoins...)
} else {
} else { // if this is a new owner that we have not added to distributionInfo yet,
// add according information to the distributionInfo maps.
id := d.nextID
d.nextID += 1
d.lockOwnerAddrToID[owner] = id
decodedOwnerAddr, err := sdk.AccAddressFromBech32(owner)
decodedRewardReceiverAddr, err := sdk.AccAddressFromBech32(rewardReceiver)
if err != nil {
return err
}
d.idToBech32Addr = append(d.idToBech32Addr, owner)
d.idToDecodedAddr = append(d.idToDecodedAddr, decodedOwnerAddr)
d.idToBech32Addr = append(d.idToBech32Addr, rewardReceiver)
d.idToDecodedRewardReceiverAddr = append(d.idToDecodedRewardReceiverAddr, decodedRewardReceiverAddr)
d.idToDistrCoins = append(d.idToDistrCoins, rewards)
}
return nil
}

// doDistributionSends utilizes provided distributionInfo to send coins from the module account to various recipients.
func (k Keeper) doDistributionSends(ctx sdk.Context, distrs *distributionInfo) error {
numIDs := len(distrs.idToDecodedAddr)
numIDs := len(distrs.idToDecodedRewardReceiverAddr)
if numIDs > 0 {
ctx.Logger().Debug(fmt.Sprintf("Beginning distribution to %d users", numIDs))
// send rewards from the gauge to the reward receiver address
err := k.bk.SendCoinsFromModuleToManyAccounts(
ctx,
types.ModuleName,
distrs.idToDecodedAddr,
distrs.idToDecodedRewardReceiverAddr,
distrs.idToDistrCoins)
if err != nil {
return err
Expand Down Expand Up @@ -324,7 +329,13 @@ func (k Keeper) distributeInternal(
continue
}
// update the amount for that address
err := distrInfo.addLockRewards(lock.Owner, distrCoins)
rewardReceiver := lock.RewardReceiverAddress

// if the reward receiver stored in state is an empty string, it indicates that the owner is the reward receiver.
if rewardReceiver == "" {
rewardReceiver = lock.Owner
}
err := distrInfo.addLockRewards(lock.Owner, rewardReceiver, distrCoins)
if err != nil {
return nil, err
}
Expand Down
79 changes: 75 additions & 4 deletions x/incentives/keeper/distribute_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,14 @@ func (s *KeeperTestSuite) TestDistribute() {
noRewardCoins := sdk.Coins{}
oneKRewardCoins := sdk.Coins{sdk.NewInt64Coin(defaultRewardDenom, 1000)}
twoKRewardCoins := sdk.Coins{sdk.NewInt64Coin(defaultRewardDenom, 2000)}
threeKRewardCoins := sdk.Coins{sdk.NewInt64Coin(defaultRewardDenom, 3000)}
fiveKRewardCoins := sdk.Coins{sdk.NewInt64Coin(defaultRewardDenom, 5000)}
tests := []struct {
name string
users []userLocks
gauges []perpGaugeDesc
expectedRewards []sdk.Coins
name string
users []userLocks
gauges []perpGaugeDesc
changeRewardReceiver []changeRewardReceiver
expectedRewards []sdk.Coins
}{
// gauge 1 gives 3k coins. three locks, all eligible. 1k coins per lock.
// 1k should go to oneLockupUser and 2k to twoLockupUser.
Expand Down Expand Up @@ -78,13 +80,82 @@ func (s *KeeperTestSuite) TestDistribute() {
gauges: []perpGaugeDesc{noRewardGauge, defaultGauge},
expectedRewards: []sdk.Coins{oneKRewardCoins, twoKRewardCoins},
},
// gauge 1 gives 3k coins. three locks, all eligible. 1k coins per lock.
// we change oneLockupUser lock's reward recepient to the twoLockupUser
// none should go to oneLockupUser and 3k to twoLockupUser.
{
name: "Change Reward Receiver: One user with one lockup, another user with two lockups, single default gauge",
users: []userLocks{oneLockupUser, twoLockupUser},
gauges: []perpGaugeDesc{defaultGauge},
changeRewardReceiver: []changeRewardReceiver{
// change first lock's receiver address to the second account
{
lockId: 1,
newReceiverAccIndex: 1,
},
},
expectedRewards: []sdk.Coins{sdk.NewCoins(), threeKRewardCoins},
},
// gauge 1 gives 3k coins. three locks, all eligible. 1k coins per lock.
// We change oneLockupUser's reward recepient to twoLockupUser, twoLockupUser's reward recepient to OneLockupUser.
// Rewards should be reversed to the original test case, 2k should go to oneLockupUser and 1k to twoLockupUser.
{
name: "Change Reward Receiver: One user with one lockup, another user with two lockups, single default gauge",
users: []userLocks{oneLockupUser, twoLockupUser},
gauges: []perpGaugeDesc{defaultGauge},
changeRewardReceiver: []changeRewardReceiver{
// change first lock's receiver address to the second account
{
lockId: 1,
newReceiverAccIndex: 1,
},
{
lockId: 2,
newReceiverAccIndex: 0,
},
{
lockId: 3,
newReceiverAccIndex: 0,
},
p0mvn marked this conversation as resolved.
Show resolved Hide resolved
},
expectedRewards: []sdk.Coins{twoKRewardCoins, oneKRewardCoins},
},
// gauge 1 gives 3k coins. three locks, all eligible.
// gauge 2 gives 3k coins. one lock, to twoLockupUser.
// Change all of oneLockupUser's reward recepient to twoLockupUser, vice versa.
// Rewards should be reversed, 5k should to oneLockupUser and 1k to twoLockupUser.
{
name: "Change Reward Receiver: One user with one lockup (default gauge), another user with two lockups (double length gauge)",
users: []userLocks{oneLockupUser, twoLockupUser},
gauges: []perpGaugeDesc{defaultGauge, doubleLengthGauge},
changeRewardReceiver: []changeRewardReceiver{
{
lockId: 1,
newReceiverAccIndex: 1,
},
{
lockId: 2,
newReceiverAccIndex: 0,
},
{
lockId: 3,
newReceiverAccIndex: 0,
},
},
expectedRewards: []sdk.Coins{fiveKRewardCoins, oneKRewardCoins},
},
}
for _, tc := range tests {
s.SetupTest()
// setup gauges and the locks defined in the above tests, then distribute to them
gauges := s.SetupGauges(tc.gauges, defaultLPDenom)
addrs := s.SetupUserLocks(tc.users)

// set up reward receiver if not nil
if len(tc.changeRewardReceiver) != 0 {
s.SetupChangeRewardReceiver(tc.changeRewardReceiver, addrs)
}

_, err := s.App.IncentivesKeeper.Distribute(s.Ctx, gauges)
s.Require().NoError(err)
// check expected rewards against actual rewards received
Expand Down
15 changes: 15 additions & 0 deletions x/incentives/keeper/suite_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,11 @@ type perpGaugeDesc struct {
rewardAmount sdk.Coins
}

type changeRewardReceiver struct {
newReceiverAccIndex int
lockId uint64
}

// setupAddr takes a balance, prefix, and address number. Then returns the respective account address byte array.
// If prefix is left blank, it will be replaced with a random prefix.
func (s *KeeperTestSuite) setupAddr(addrNum int, prefix string, balance sdk.Coins) sdk.AccAddress {
Expand Down Expand Up @@ -83,6 +88,16 @@ func (s *KeeperTestSuite) SetupUserLocks(users []userLocks) (accs []sdk.AccAddre
return
}

func (s *KeeperTestSuite) SetupChangeRewardReceiver(changeRewardReceivers []changeRewardReceiver, accs []sdk.AccAddress) {
for _, changeRewardReceiver := range changeRewardReceivers {
lock, err := s.App.LockupKeeper.GetLockByID(s.Ctx, changeRewardReceiver.lockId)
s.Require().NoError(err)

err = s.App.LockupKeeper.SetLockRewardReceiverAddress(s.Ctx, changeRewardReceiver.lockId, lock.OwnerAddress(), accs[changeRewardReceiver.newReceiverAccIndex].String())
s.Require().NoError(err)
}
}

// SetupUserSyntheticLocks takes an array of user locks and creates synthetic locks based on this array, then returns the respective account address byte array.
func (s *KeeperTestSuite) SetupUserSyntheticLocks(users []userLocks) (accs []sdk.AccAddress) {
accs = make([]sdk.AccAddress, len(users))
Expand Down
2 changes: 1 addition & 1 deletion x/lockup/keeper/bench_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ func benchmarkResetLogic(b *testing.B, numLockups int) {
addr := addrs[r.Int()%numAccts]
simCoins := sdk.NewCoins(sdk.NewCoin(denom, sdk.NewInt(r.Int63n(100))))
duration := time.Duration(r.Intn(1*60*60*24*7)) * time.Second
lock := lockuptypes.NewPeriodLock(uint64(i+1), addr, duration, time.Time{}, simCoins)
lock := lockuptypes.NewPeriodLock(uint64(i+1), addr, addr.String(), duration, time.Time{}, simCoins)
mattverse marked this conversation as resolved.
Show resolved Hide resolved
locks[i] = lock
}

Expand Down
77 changes: 42 additions & 35 deletions x/lockup/keeper/genesis_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,25 +24,28 @@ var (
LastLockId: 10,
Locks: []types.PeriodLock{
{
ID: 1,
Owner: acc1.String(),
Duration: time.Second,
EndTime: time.Time{},
Coins: sdk.Coins{sdk.NewInt64Coin("foo", 10000000)},
ID: 1,
Owner: acc1.String(),
RewardReceiverAddress: "",
Duration: time.Second,
EndTime: time.Time{},
Coins: sdk.Coins{sdk.NewInt64Coin("foo", 10000000)},
},
{
ID: 2,
Owner: acc1.String(),
Duration: time.Hour,
EndTime: time.Time{},
Coins: sdk.Coins{sdk.NewInt64Coin("foo", 15000000)},
ID: 2,
Owner: acc1.String(),
RewardReceiverAddress: acc2.String(),
Duration: time.Hour,
EndTime: time.Time{},
Coins: sdk.Coins{sdk.NewInt64Coin("foo", 15000000)},
},
{
ID: 3,
Owner: acc2.String(),
Duration: time.Minute,
EndTime: time.Time{},
Coins: sdk.Coins{sdk.NewInt64Coin("foo", 5000000)},
ID: 3,
Owner: acc2.String(),
RewardReceiverAddress: acc1.String(),
Duration: time.Minute,
EndTime: time.Time{},
Coins: sdk.Coins{sdk.NewInt64Coin("foo", 5000000)},
},
},
}
Expand Down Expand Up @@ -90,32 +93,36 @@ func TestExportGenesis(t *testing.T) {
require.Equal(t, genesisExported.LastLockId, uint64(11))
require.Equal(t, genesisExported.Locks, []types.PeriodLock{
{
ID: 1,
Owner: acc1.String(),
Duration: time.Second,
EndTime: time.Time{},
Coins: sdk.Coins{sdk.NewInt64Coin("foo", 10000000)},
ID: 1,
Owner: acc1.String(),
RewardReceiverAddress: "",
Duration: time.Second,
EndTime: time.Time{},
Coins: sdk.Coins{sdk.NewInt64Coin("foo", 10000000)},
},
{
ID: 11,
Owner: acc2.String(),
Duration: time.Second * 5,
EndTime: time.Time{},
Coins: sdk.Coins{sdk.NewInt64Coin("foo", 5000000)},
ID: 11,
Owner: acc2.String(),
RewardReceiverAddress: "",
Duration: time.Second * 5,
EndTime: time.Time{},
Coins: sdk.Coins{sdk.NewInt64Coin("foo", 5000000)},
},
{
ID: 3,
Owner: acc2.String(),
Duration: time.Minute,
EndTime: time.Time{},
Coins: sdk.Coins{sdk.NewInt64Coin("foo", 5000000)},
ID: 3,
Owner: acc2.String(),
RewardReceiverAddress: acc1.String(),
Duration: time.Minute,
EndTime: time.Time{},
Coins: sdk.Coins{sdk.NewInt64Coin("foo", 5000000)},
},
{
ID: 2,
Owner: acc1.String(),
Duration: time.Hour,
EndTime: time.Time{},
Coins: sdk.Coins{sdk.NewInt64Coin("foo", 15000000)},
ID: 2,
Owner: acc1.String(),
RewardReceiverAddress: acc2.String(),
Duration: time.Hour,
EndTime: time.Time{},
Coins: sdk.Coins{sdk.NewInt64Coin("foo", 15000000)},
},
})
}
Expand Down
Loading