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

Create Force Unlock Messages #2733

Merged
merged 10 commits into from
Oct 10, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions app/apptesting/superfluid.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package apptesting

import (
sdk "github.com/cosmos/cosmos-sdk/types"
stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types"

gammtypes "github.com/osmosis-labs/osmosis/v12/x/gamm/types"
"github.com/osmosis-labs/osmosis/v12/x/superfluid/types"
)

func (s *KeeperTestHelper) SuperfluidDelegateToDefaultVal(sender sdk.AccAddress, poolId uint64, lockId uint64) error {
valAddr := s.SetupValidator(stakingtypes.Bonded)

poolDenom := gammtypes.GetPoolShareDenom(poolId)
err := s.App.SuperfluidKeeper.AddNewSuperfluidAsset(s.Ctx, types.SuperfluidAsset{
Denom: poolDenom,
AssetType: types.SuperfluidAssetTypeLPShare,
})
s.Require().NoError(err)

return s.App.SuperfluidKeeper.SuperfluidDelegate(s.Ctx, sender.String(), lockId, valAddr.String())
}
3 changes: 2 additions & 1 deletion app/keepers/keepers.go
Original file line number Diff line number Diff line change
Expand Up @@ -255,7 +255,7 @@ func (appKeepers *AppKeepers) InitNormalKeepers(
// TODO: Visit why this needs to be deref'd
*appKeepers.AccountKeeper,
appKeepers.BankKeeper,
appKeepers.DistrKeeper)
appKeepers.DistrKeeper, appKeepers.GetSubspace(lockuptypes.ModuleName))

appKeepers.EpochsKeeper = epochskeeper.NewKeeper(appKeepers.keys[epochstypes.StoreKey])

Expand Down Expand Up @@ -430,6 +430,7 @@ func (appKeepers *AppKeepers) initParamsKeeper(appCodec codec.BinaryCodec, legac
paramsKeeper.Subspace(ibchost.ModuleName)
paramsKeeper.Subspace(icahosttypes.SubModuleName)
paramsKeeper.Subspace(incentivestypes.ModuleName)
paramsKeeper.Subspace(lockuptypes.ModuleName)
paramsKeeper.Subspace(poolincentivestypes.ModuleName)
paramsKeeper.Subspace(superfluidtypes.ModuleName)
paramsKeeper.Subspace(gammtypes.ModuleName)
Expand Down
25 changes: 25 additions & 0 deletions app/upgrades/v13/upgrades.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package v13

import (
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/types/module"
upgradetypes "github.com/cosmos/cosmos-sdk/x/upgrade/types"

lockuptypes "github.com/osmosis-labs/osmosis/v12/x/lockup/types"

"github.com/osmosis-labs/osmosis/v12/app/keepers"
"github.com/osmosis-labs/osmosis/v12/app/upgrades"
)

func CreateUpgradeHandler(
mm *module.Manager,
configurator module.Configurator,
bpm upgrades.BaseAppParamManager,
keepers *keepers.AppKeepers,
) upgradetypes.UpgradeHandler {
return func(ctx sdk.Context, plan upgradetypes.Plan, fromVM module.VersionMap) (module.VersionMap, error) {
keepers.LockupKeeper.SetParams(ctx, lockuptypes.DefaultParams())

return mm.RunMigrations(ctx, configurator, fromVM)
}
}
11 changes: 11 additions & 0 deletions proto/osmosis/lockup/params.proto
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
syntax = "proto3";
package osmosis.lockup;

import "gogoproto/gogo.proto";

option go_package = "github.com/osmosis-labs/osmosis/v12/x/lockup/types";

message Params {
repeated string force_unlock_allowed_addresses = 1
[ (gogoproto.moretags) = "yaml:\"force_unlock_allowed_address\"" ];
}
15 changes: 15 additions & 0 deletions proto/osmosis/lockup/tx.proto
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ service Msg {
rpc BeginUnlocking(MsgBeginUnlocking) returns (MsgBeginUnlockingResponse);
// MsgEditLockup edits the existing lockups by lock ID
rpc ExtendLockup(MsgExtendLockup) returns (MsgExtendLockupResponse);
rpc ForceUnlock(MsgForceUnlock) returns (MsgForceUnlockResponse);
}

message MsgLockTokens {
Expand Down Expand Up @@ -71,3 +72,17 @@ message MsgExtendLockup {
}

message MsgExtendLockupResponse { bool success = 1; }

// MsgForceUnlock unlocks locks immediately for
// addresses registered via governance.
message MsgForceUnlock {
mattverse marked this conversation as resolved.
Show resolved Hide resolved
string owner = 1 [ (gogoproto.moretags) = "yaml:\"owner\"" ];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this an SDK address? If so, shouldn't we add (cosmos_proto.scalar) = "cosmos.AddressString"? Maybe our proto tooling doesn't support this yet?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, option (cosmos.msg.v1.signer) = "from_address";

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what does (cosmos_proto.scalar) = "cosmos.AddressString" and option (cosmos.msg.v1.signer) = "from_address"; do?

Orignial intention was to recieve it as string and then convert it to address

uint64 ID = 2;
// Amount of unlocking coins. Unlock all if not set.
repeated cosmos.base.v1beta1.Coin coins = 3 [
(gogoproto.nullable) = false,
(gogoproto.castrepeated) = "github.com/cosmos/cosmos-sdk/types.Coins"
];
}

message MsgForceUnlockResponse { bool success = 1; }
34 changes: 29 additions & 5 deletions x/lockup/keeper/keeper.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"github.com/osmosis-labs/osmosis/v12/x/lockup/types"

sdk "github.com/cosmos/cosmos-sdk/types"
paramtypes "github.com/cosmos/cosmos-sdk/x/params/types"
)

// Keeper provides a way to manage module storage.
Expand All @@ -16,21 +17,44 @@ type Keeper struct {

hooks types.LockupHooks

paramSpace paramtypes.Subspace

ak types.AccountKeeper
bk types.BankKeeper
ck types.CommunityPoolKeeper
}

// NewKeeper returns an instance of Keeper.
func NewKeeper(storeKey sdk.StoreKey, ak types.AccountKeeper, bk types.BankKeeper, ck types.CommunityPoolKeeper) *Keeper {
func NewKeeper(storeKey sdk.StoreKey, ak types.AccountKeeper, bk types.BankKeeper, ck types.CommunityPoolKeeper, paramSpace paramtypes.Subspace) *Keeper {
// set KeyTable if it has not already been set
if !paramSpace.HasKeyTable() {
paramSpace = paramSpace.WithKeyTable(types.ParamKeyTable())
}

return &Keeper{
storeKey: storeKey,
ak: ak,
bk: bk,
ck: ck,
storeKey: storeKey,
paramSpace: paramSpace,
ak: ak,
bk: bk,
ck: ck,
}
}

// GetParams returns the total set of lockup parameters.
func (k Keeper) GetParams(ctx sdk.Context) (params types.Params) {
k.paramSpace.GetParamSet(ctx, &params)
return params
}

// SetParams sets the total set of lockup parameters.
func (k Keeper) SetParams(ctx sdk.Context, params types.Params) {
k.paramSpace.SetParamSet(ctx, &params)
}

func (k Keeper) GetForceUnlockAllowedAddresses(ctx sdk.Context) (forceUnlockAllowedAddresses []string) {
return k.GetParams(ctx).ForceUnlockAllowedAddresses
}

Comment on lines +54 to +57
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

non-blocking nit: I'm generally unsure if this method is needed if we can get the addresses from params

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I originally thought this looked messy(having to call params to call get the addresses), but on the second thought I tink that way makes everything more intuitive, reverting this part so that we call from params directly

// Logger returns a logger instance.
func (k Keeper) Logger(ctx sdk.Context) log.Logger {
return ctx.Logger().With("module", fmt.Sprintf("x/%s", types.ModuleName))
Expand Down
8 changes: 8 additions & 0 deletions x/lockup/keeper/keeper_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,14 @@ type KeeperTestSuite struct {
func (suite *KeeperTestSuite) SetupTest() {
suite.Setup()
suite.querier = keeper.NewQuerier(*suite.App.LockupKeeper)
unbondingDuration := suite.App.StakingKeeper.GetParams(suite.Ctx).UnbondingTime
suite.App.IncentivesKeeper.SetLockableDurations(suite.Ctx, []time.Duration{
time.Hour * 24 * 14,
time.Hour,
time.Hour * 3,
time.Hour * 7,
unbondingDuration,
})
}

func (suite *KeeperTestSuite) SetupTestWithLevelDb() {
Expand Down
32 changes: 29 additions & 3 deletions x/lockup/keeper/lock.go
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,7 @@ func (k Keeper) beginUnlock(ctx sdk.Context, lock types.PeriodLock, coins sdk.Co
// Otherwise, split the lock into two locks, and fully unlock the newly created lock.
// (By virtue, the newly created lock we split into should have the unlock amount)
if len(coins) != 0 && !coins.IsEqual(lock.Coins) {
splitLock, err := k.splitLock(ctx, lock, coins)
splitLock, err := k.splitLock(ctx, lock, coins, false)
if err != nil {
return err
}
Expand Down Expand Up @@ -307,6 +307,28 @@ func (k Keeper) UnlockMaturedLock(ctx sdk.Context, lockID uint64) error {
return k.unlockMaturedLockInternalLogic(ctx, *lock)
}

// PartialForceUnlock begins partial ForceUnlock of given lock for the given amount of coins.
// ForceUnlocks the lock as a whole when provided coins are empty, or coin provided equals amount of coins in the lock.
// This also supports the case of lock in an unbonding status.
func (k Keeper) PartialForceUnlock(ctx sdk.Context, lock types.PeriodLock, coins sdk.Coins) error {
// sanity check
if !coins.IsAllLTE(lock.Coins) {
return fmt.Errorf("requested amount to unlock exceeds locked tokens")
}

// split lock to support partial force unlock.
// (By virtue, the newly created lock we split into should have the unlock amount)
if len(coins) != 0 && !coins.IsEqual(lock.Coins) {
splitLock, err := k.splitLock(ctx, lock, coins, true)
if err != nil {
return err
}
lock = splitLock
}

return k.ForceUnlock(ctx, lock)
}

// ForceUnlock ignores unlock duration and immediately unlocks the lock and refunds tokens to lock owner.
func (k Keeper) ForceUnlock(ctx sdk.Context, lock types.PeriodLock) error {
// Steps:
Expand Down Expand Up @@ -658,20 +680,24 @@ func (k Keeper) deleteLock(ctx sdk.Context, id uint64) {
}

// splitLock splits a lock with the given amount, and stores split new lock to the state.
func (k Keeper) splitLock(ctx sdk.Context, lock types.PeriodLock, coins sdk.Coins) (types.PeriodLock, error) {
if lock.IsUnlocking() {
// Returns the new lock after modifying the state of the old lock.
func (k Keeper) splitLock(ctx sdk.Context, lock types.PeriodLock, coins sdk.Coins, forceUnlock bool) (types.PeriodLock, error) {
if !forceUnlock && lock.IsUnlocking() {
return types.PeriodLock{}, fmt.Errorf("cannot split unlocking lock")
}

lock.Coins = lock.Coins.Sub(coins)
err := k.setLock(ctx, lock)
if err != nil {
return types.PeriodLock{}, err
}

// create a new lock
splitLockID := k.GetLastLockID(ctx) + 1
k.SetLastLockID(ctx, splitLockID)

splitLock := types.NewPeriodLock(splitLockID, lock.OwnerAddress(), lock.Duration, lock.EndTime, coins)

err = k.setLock(ctx, splitLock)
return splitLock, err
}
Expand Down
120 changes: 120 additions & 0 deletions x/lockup/keeper/lock_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -871,3 +871,123 @@ func (suite *KeeperTestSuite) TestEditLockup() {
})
suite.Require().Equal(int64(0), acc.Int64())
}

func (suite *KeeperTestSuite) TestForceUnlock() {
addr1 := sdk.AccAddress([]byte("addr1---------------"))

testCases := []struct {
name string
postLockSetup func()
}{
{
name: "happy path",
},
{
name: "superfluid staked",
postLockSetup: func() {
err := suite.App.LockupKeeper.CreateSyntheticLockup(suite.Ctx, 1, "testDenom", time.Minute, true)
suite.Require().NoError(err)
},
},
}
for _, tc := range testCases {
// set up test and create default lock
suite.SetupTest()
coinsToLock := sdk.NewCoins(sdk.NewCoin("stake", sdk.NewInt(10000000)))
suite.FundAcc(addr1, sdk.NewCoins(coinsToLock...))
lock, err := suite.App.LockupKeeper.CreateLock(suite.Ctx, addr1, coinsToLock, time.Minute)
suite.Require().NoError(err)

// post lock setup
if tc.postLockSetup != nil {
tc.postLockSetup()
}

err = suite.App.LockupKeeper.ForceUnlock(suite.Ctx, lock)
suite.Require().NoError(err)

// check that accumulation store has decreased
accum := suite.App.LockupKeeper.GetPeriodLocksAccumulation(suite.Ctx, types.QueryCondition{
LockQueryType: types.ByDuration,
Denom: "foo",
Duration: time.Minute,
})
suite.Require().Equal(accum.String(), "0")

// check balance of lock account to confirm
balances := suite.App.BankKeeper.GetAllBalances(suite.Ctx, addr1)
suite.Require().Equal(balances, coinsToLock)

// if it was superfluid delegated lock,
// confirm that we don't have associated synth locks
synthLocks := suite.App.LockupKeeper.GetAllSyntheticLockupsByLockup(suite.Ctx, lock.ID)
suite.Require().Equal(0, len(synthLocks))

// check if lock is deleted by checking trying to get lock ID
_, err = suite.App.LockupKeeper.GetLockByID(suite.Ctx, lock.ID)
suite.Require().Error(err)
}
}

func (suite *KeeperTestSuite) TestPartialForceUnlock() {
addr1 := sdk.AccAddress([]byte("addr1---------------"))

defaultDenomToLock := "stake"
defaultAmountToLock := sdk.NewInt(10000000)

testCases := []struct {
name string
coinsToForceUnlock sdk.Coins
expectedPass bool
}{
{
name: "unlock full amount",
coinsToForceUnlock: sdk.Coins{sdk.NewCoin(defaultDenomToLock, defaultAmountToLock)},
expectedPass: true,
},
{
name: "partial unlock",
coinsToForceUnlock: sdk.Coins{sdk.NewCoin(defaultDenomToLock, defaultAmountToLock.Quo(sdk.NewInt(2)))},
expectedPass: true,
},
{
name: "unlock more than locked",
coinsToForceUnlock: sdk.Coins{sdk.NewCoin(defaultDenomToLock, defaultAmountToLock.Add(sdk.NewInt(2)))},
expectedPass: false,
},
{
name: "try unlocking with empty coins",
coinsToForceUnlock: sdk.Coins{},
expectedPass: true,
},
}
for _, tc := range testCases {
// set up test and create default lock
suite.SetupTest()
coinsToLock := sdk.NewCoins(sdk.NewCoin("stake", defaultAmountToLock))
suite.FundAcc(addr1, sdk.NewCoins(coinsToLock...))
// balanceBeforeLock := suite.App.BankKeeper.GetAllBalances(suite.Ctx, addr1)
lock, err := suite.App.LockupKeeper.CreateLock(suite.Ctx, addr1, coinsToLock, time.Minute)
suite.Require().NoError(err)

err = suite.App.LockupKeeper.PartialForceUnlock(suite.Ctx, lock, tc.coinsToForceUnlock)

if tc.expectedPass {
suite.Require().NoError(err)

// check balance
balanceAfterForceUnlock := suite.App.BankKeeper.GetBalance(suite.Ctx, addr1, "stake")

if tc.coinsToForceUnlock.Empty() {
tc.coinsToForceUnlock = coinsToLock
}
suite.Require().Equal(tc.coinsToForceUnlock, sdk.Coins{balanceAfterForceUnlock})
} else {
suite.Require().Error(err)

// check balance
balanceAfterForceUnlock := suite.App.BankKeeper.GetBalance(suite.Ctx, addr1, "stake")
suite.Require().Equal(sdk.NewInt(0), balanceAfterForceUnlock.Amount)
}
}
}
Loading