diff --git a/CHANGELOG.md b/CHANGELOG.md index b840983fd78d63a58164332f38bfbcdad0e8bf5e..276330608a3bb64b2b31be79526f4930719929bb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -40,6 +40,18 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## v10.2.0 + +In v10.1.0 a refactor did changed the ordering of certain function calls in a message execution. +The data read and written to in state were the same, but re-ordered. +This unknowingly caused gas_used incompatabilities, because you could out-of-gas error at any intermediate step, and end in a different gas_used quantity. cref: https://github.com/cosmos/cosmos-sdk/issues/12788 + +The v10.2 line reverts the gas limit state incompatabilities introduced due to function ordering. +Namely this reverts + +* [#1937](https://github.com/osmosis-labs/osmosis/pull/1937) Change `lockupKeeper.ExtendLock` to take in lockID instead of the direct lock struct. +* [#2030](https://github.com/osmosis-labs/osmosis/pull/2030) Rename lockup keeper `ResetAllLocks` to `InitializeAllLocks` and `ResetAllSyntheticLocks` to `InitializeAllSyntheticLocks`. + ## v10.1.1 #### Improvements diff --git a/x/lockup/keeper/admin_keeper_test.go b/x/lockup/keeper/admin_keeper_test.go index 84a4af63362635d0c4fb0f78e6ff39897f578ff5..c10826881e89e467e6f44340cd0281c1e9ad2762 100644 --- a/x/lockup/keeper/admin_keeper_test.go +++ b/x/lockup/keeper/admin_keeper_test.go @@ -4,6 +4,7 @@ import ( "time" "github.com/osmosis-labs/osmosis/v10/x/lockup/keeper" + "github.com/osmosis-labs/osmosis/v10/x/lockup/types" sdk "github.com/cosmos/cosmos-sdk/types" ) @@ -13,10 +14,11 @@ func (suite *KeeperTestSuite) TestRelock() { addr1 := sdk.AccAddress([]byte("addr1---------------")) coins := sdk.Coins{sdk.NewInt64Coin("stake", 10)} + lock := types.NewPeriodLock(1, addr1, time.Second, suite.Ctx.BlockTime().Add(time.Second), coins) // lock with balance suite.FundAcc(addr1, coins) - lock, err := suite.App.LockupKeeper.CreateLock(suite.Ctx, addr1, coins, time.Second) + err := suite.App.LockupKeeper.Lock(suite.Ctx, lock) suite.Require().NoError(err) // lock with balance with same id @@ -36,12 +38,12 @@ func (suite *KeeperTestSuite) BreakLock() { addr1 := sdk.AccAddress([]byte("addr1---------------")) coins := sdk.Coins{sdk.NewInt64Coin("stake", 10)} + lock := types.NewPeriodLock(1, addr1, time.Second, suite.Ctx.BlockTime().Add(time.Second), coins) // lock with balance suite.FundAcc(addr1, coins) - lock, err := suite.App.LockupKeeper.CreateLock(suite.Ctx, addr1, coins, time.Second) - + err := suite.App.LockupKeeper.Lock(suite.Ctx, lock) suite.Require().NoError(err) // break lock diff --git a/x/lockup/keeper/export_test.go b/x/lockup/keeper/export_test.go index 369c6b1812eef4394914bdd3c489cebc679e501e..bbb648e6a1924affb76c7abd600cdf2d1c791f77 100644 --- a/x/lockup/keeper/export_test.go +++ b/x/lockup/keeper/export_test.go @@ -25,7 +25,3 @@ func (k Keeper) SyntheticCoins(coins sdk.Coins, suffix string) sdk.Coins { func (k Keeper) GetCoinsFromLocks(locks []types.PeriodLock) sdk.Coins { return k.getCoinsFromLocks(locks) } - -func (k Keeper) Lock(ctx sdk.Context, lock types.PeriodLock, tokensToLock sdk.Coins) error { - return k.lock(ctx, lock, tokensToLock) -} diff --git a/x/lockup/keeper/lock.go b/x/lockup/keeper/lock.go index 3d43af871cd1c7d39c633018092c459cca277adf..826656ec49ab9cfc11188cf84919be10c5780fe3 100644 --- a/x/lockup/keeper/lock.go +++ b/x/lockup/keeper/lock.go @@ -66,79 +66,100 @@ func (k Keeper) AddToExistingLock(ctx sdk.Context, owner sdk.AccAddress, coin sd // Tokens locked are sent and kept in the module account. // This method alters the lock state in store, thus we do a sanity check to ensure // lock owner matches the given owner. -func (k Keeper) AddTokensToLockByID(ctx sdk.Context, lockID uint64, owner sdk.AccAddress, tokensToAdd sdk.Coin) (*types.PeriodLock, error) { +func (k Keeper) AddTokensToLockByID(ctx sdk.Context, lockID uint64, owner sdk.AccAddress, coin sdk.Coin) (*types.PeriodLock, error) { lock, err := k.GetLockByID(ctx, lockID) - if err != nil { - return nil, err - } if lock.GetOwner() != owner.String() { return nil, types.ErrNotLockOwner } - lock.Coins = lock.Coins.Add(tokensToAdd) - err = k.lock(ctx, *lock, sdk.NewCoins(tokensToAdd)) if err != nil { return nil, err } + if err := k.bk.SendCoinsFromAccountToModule(ctx, lock.OwnerAddress(), types.ModuleName, sdk.NewCoins(coin)); err != nil { + return nil, err + } - for _, synthlock := range k.GetAllSyntheticLockupsByLockup(ctx, lock.ID) { - k.accumulationStore(ctx, synthlock.SynthDenom).Increase(accumulationKey(synthlock.Duration), tokensToAdd.Amount) + err = k.addTokenToLock(ctx, lock, coin) + if err != nil { + return nil, err } if k.hooks == nil { return lock, nil } - k.hooks.AfterAddTokensToLock(ctx, lock.OwnerAddress(), lock.GetID(), sdk.NewCoins(tokensToAdd)) - + k.hooks.OnTokenLocked(ctx, lock.OwnerAddress(), lock.ID, sdk.Coins{coin}, lock.Duration, lock.EndTime) return lock, nil } +// addTokenToLock adds token to lock and modifies the state of the lock and the accumulation store. +func (k Keeper) addTokenToLock(ctx sdk.Context, lock *types.PeriodLock, coin sdk.Coin) error { + lock.Coins = lock.Coins.Add(coin) + + err := k.setLock(ctx, *lock) + if err != nil { + return err + } + + // modifications to accumulation store + k.accumulationStore(ctx, coin.Denom).Increase(accumulationKey(lock.Duration), coin.Amount) + + // CONTRACT: lock will have synthetic lock only if it has a single coin + // accumulation store for its synthetic denom is increased if exists. + lockedCoin, err := lock.SingleCoin() + if err == nil { + for _, synthlock := range k.GetAllSyntheticLockupsByLockup(ctx, lock.ID) { + k.accumulationStore(ctx, synthlock.SynthDenom).Increase(accumulationKey(synthlock.Duration), sdk.NewCoins(coin).AmountOf(lockedCoin.Denom)) + } + } + + k.hooks.AfterAddTokensToLock(ctx, lock.OwnerAddress(), lock.GetID(), sdk.NewCoins(coin)) + + return nil +} + // CreateLock creates a new lock with the specified duration for the owner. -// Returns an error in the following conditions: -// - account does not have enough balance func (k Keeper) CreateLock(ctx sdk.Context, owner sdk.AccAddress, coins sdk.Coins, duration time.Duration) (types.PeriodLock, error) { ID := k.GetLastLockID(ctx) + 1 // unlock time is initially set without a value, gets set as unlock start time + duration // when unlocking starts. lock := types.NewPeriodLock(ID, owner, duration, time.Time{}, coins) - err := k.lock(ctx, lock, lock.Coins) + err := k.Lock(ctx, lock) if err != nil { return lock, err } - - // add lock refs into not unlocking queue - err = k.addLockRefs(ctx, lock) - if err != nil { - return lock, err - } - k.SetLastLockID(ctx, lock.ID) return lock, nil } -// lock is an internal utility to lock coins and set corresponding states. -// This is only called by either of the two possible entry points to lock tokens. -// 1. CreateLock -// 2. AddTokensToLockByID -func (k Keeper) lock(ctx sdk.Context, lock types.PeriodLock, tokensToLock sdk.Coins) error { +// Lock is a utility method to lock tokens into the module account. This method includes setting the +// lock within the state machine and increasing the value of accumulation store. +func (k Keeper) Lock(ctx sdk.Context, lock types.PeriodLock) error { owner, err := sdk.AccAddressFromBech32(lock.Owner) if err != nil { return err } - if err := k.bk.SendCoinsFromAccountToModule(ctx, owner, types.ModuleName, tokensToLock); err != nil { + if err := k.bk.SendCoinsFromAccountToModule(ctx, owner, types.ModuleName, lock.Coins); err != nil { return err } // store lock object into the store - err = k.setLock(ctx, lock) + store := ctx.KVStore(k.storeKey) + bz, err := proto.Marshal(&lock) + if err != nil { + return err + } + store.Set(lockStoreKey(lock.ID), bz) + + // add lock refs into not unlocking queue + err = k.addLockRefs(ctx, lock) if err != nil { return err } // add to accumulation store - for _, coin := range tokensToLock { + for _, coin := range lock.Coins { k.accumulationStore(ctx, coin.Denom).Increase(accumulationKey(lock.Duration), coin.Amount) } @@ -353,16 +374,7 @@ func (k Keeper) unlockMaturedLockInternalLogic(ctx sdk.Context, lock types.Perio // 2. Locks that are unlokcing are not allowed to change duration. // 3. Locks that have synthetic lockup are not allowed to change. // 4. Provided duration should be greater than the original duration. -func (k Keeper) ExtendLockup(ctx sdk.Context, lockID uint64, owner sdk.AccAddress, newDuration time.Duration) error { - lock, err := k.GetLockByID(ctx, lockID) - if err != nil { - return err - } - - if lock.GetOwner() != owner.String() { - return types.ErrNotLockOwner - } - +func (k Keeper) ExtendLockup(ctx sdk.Context, lock types.PeriodLock, newDuration time.Duration) error { if lock.IsUnlocking() { return fmt.Errorf("cannot edit unlocking lockup for lock %d", lock.ID) } @@ -372,15 +384,10 @@ func (k Keeper) ExtendLockup(ctx sdk.Context, lockID uint64, owner sdk.AccAddres return fmt.Errorf("cannot edit lockup with synthetic lock %d", lock.ID) } - // completely delete existing lock refs - err = k.deleteLockRefs(ctx, unlockingPrefix(lock.IsUnlocking()), *lock) - if err != nil { - return err - } + oldLock := lock - oldDuration := lock.GetDuration() if newDuration != 0 { - if newDuration <= oldDuration { + if newDuration <= lock.Duration { return fmt.Errorf("new duration should be greater than the original") } @@ -393,20 +400,25 @@ func (k Keeper) ExtendLockup(ctx sdk.Context, lockID uint64, owner sdk.AccAddres lock.Duration = newDuration } - // add lock refs with the new duration - err = k.addLockRefs(ctx, *lock) + // update lockup + err := k.deleteLockRefs(ctx, unlockingPrefix(oldLock.IsUnlocking()), oldLock) if err != nil { return err } - err = k.setLock(ctx, *lock) + err = k.addLockRefs(ctx, lock) + if err != nil { + return err + } + + err = k.setLock(ctx, lock) if err != nil { return err } k.hooks.OnLockupExtend(ctx, lock.GetID(), - oldDuration, + oldLock.GetDuration(), lock.GetDuration(), ) diff --git a/x/lockup/keeper/lock_test.go b/x/lockup/keeper/lock_test.go index edd22c9df2352b36c58a5e1d778341a26d1524a7..e5ea3ec8e4f680c84da9949da23dcc167838bdeb 100644 --- a/x/lockup/keeper/lock_test.go +++ b/x/lockup/keeper/lock_test.go @@ -144,6 +144,35 @@ func (suite *KeeperTestSuite) TestUnlockPeriodLockByID() { suite.Require().Len(locks, 0) } +func (suite *KeeperTestSuite) TestLock() { + // test for coin locking + suite.SetupTest() + + addr1 := sdk.AccAddress([]byte("addr1---------------")) + coins := sdk.Coins{sdk.NewInt64Coin("stake", 10)} + lock := types.NewPeriodLock(1, addr1, time.Second, suite.Ctx.BlockTime().Add(time.Second), coins) + + // try lock without balance + err := suite.App.LockupKeeper.Lock(suite.Ctx, lock) + suite.Require().Error(err) + + // lock with balance + suite.FundAcc(addr1, coins) + err = suite.App.LockupKeeper.Lock(suite.Ctx, lock) + suite.Require().NoError(err) + + // lock with balance with same id + suite.FundAcc(addr1, coins) + err = suite.App.LockupKeeper.Lock(suite.Ctx, lock) + suite.Require().Error(err) + + // lock with balance with different id + lock = types.NewPeriodLock(2, addr1, time.Second, suite.Ctx.BlockTime().Add(time.Second), coins) + suite.FundAcc(addr1, coins) + err = suite.App.LockupKeeper.Lock(suite.Ctx, lock) + suite.Require().NoError(err) +} + func (suite *KeeperTestSuite) TestUnlock() { // test for coin unlocking suite.SetupTest() @@ -151,10 +180,11 @@ func (suite *KeeperTestSuite) TestUnlock() { addr1 := sdk.AccAddress([]byte("addr1---------------")) coins := sdk.Coins{sdk.NewInt64Coin("stake", 10)} + lock := types.NewPeriodLock(1, addr1, time.Second, time.Time{}, coins) // lock with balance suite.FundAcc(addr1, coins) - lock, err := suite.App.LockupKeeper.CreateLock(suite.Ctx, addr1, coins, time.Second) + err := suite.App.LockupKeeper.Lock(suite.Ctx, lock) suite.Require().NoError(err) // begin unlock with lock object @@ -290,65 +320,6 @@ func (suite *KeeperTestSuite) TestLocksLongerThanDurationDenom() { suite.Require().Len(locks, 1) } -func (suite *KeeperTestSuite) TestCreateLock() { - suite.SetupTest() - - addr1 := sdk.AccAddress([]byte("addr1---------------")) - coins := sdk.Coins{sdk.NewInt64Coin("stake", 10)} - - // test locking without balance - _, err := suite.App.LockupKeeper.CreateLock(suite.Ctx, addr1, coins, time.Second) - suite.Require().Error(err) - - suite.FundAcc(addr1, coins) - - lock, err := suite.App.LockupKeeper.CreateLock(suite.Ctx, addr1, coins, time.Second) - suite.Require().NoError(err) - - // check new lock - suite.Require().Equal(coins, lock.Coins) - suite.Require().Equal(time.Second, lock.Duration) - suite.Require().Equal(time.Time{}, lock.EndTime) - suite.Require().Equal(uint64(1), lock.ID) - - lockID := suite.App.LockupKeeper.GetLastLockID(suite.Ctx) - suite.Require().Equal(uint64(1), lockID) - - // check accumulation store - accum := suite.App.LockupKeeper.GetPeriodLocksAccumulation(suite.Ctx, types.QueryCondition{ - LockQueryType: types.ByDuration, - Denom: "stake", - Duration: time.Second, - }) - suite.Require().Equal(accum.String(), "10") - - // create new lock - coins = sdk.Coins{sdk.NewInt64Coin("stake", 20)} - suite.FundAcc(addr1, coins) - - lock, err = suite.App.LockupKeeper.CreateLock(suite.Ctx, addr1, coins, time.Second) - suite.Require().NoError(err) - - lockID = suite.App.LockupKeeper.GetLastLockID(suite.Ctx) - suite.Require().Equal(uint64(2), lockID) - - // check accumulation store - accum = suite.App.LockupKeeper.GetPeriodLocksAccumulation(suite.Ctx, types.QueryCondition{ - LockQueryType: types.ByDuration, - Denom: "stake", - Duration: time.Second, - }) - suite.Require().Equal(accum.String(), "30") - - // check balance - balance := suite.App.BankKeeper.GetBalance(suite.Ctx, addr1, "stake") - suite.Require().Equal(sdk.ZeroInt(), balance.Amount) - - acc := suite.App.AccountKeeper.GetModuleAccount(suite.Ctx, types.ModuleName) - balance = suite.App.BankKeeper.GetBalance(suite.Ctx, acc.GetAddress(), "stake") - suite.Require().Equal(sdk.NewInt(30), balance.Amount) -} - func (suite *KeeperTestSuite) TestAddTokensToLock() { suite.SetupTest() @@ -410,52 +381,6 @@ func (suite *KeeperTestSuite) TestAddTokensToLock() { suite.Require().Error(err) } -func (suite *KeeperTestSuite) TestLock() { - suite.SetupTest() - - addr1 := sdk.AccAddress([]byte("addr1---------------")) - coins := sdk.Coins{sdk.NewInt64Coin("stake", 10)} - - lock := types.PeriodLock{ - ID: 1, - Owner: addr1.String(), - Duration: time.Second, - EndTime: time.Time{}, - Coins: coins, - } - - // test locking without balance - err := suite.App.LockupKeeper.Lock(suite.Ctx, lock, coins) - suite.Require().Error(err) - - // check accumulation store - accum := suite.App.LockupKeeper.GetPeriodLocksAccumulation(suite.Ctx, types.QueryCondition{ - LockQueryType: types.ByDuration, - Denom: "stake", - Duration: time.Second, - }) - suite.Require().Equal(accum.String(), "0") - - suite.FundAcc(addr1, coins) - err = suite.App.LockupKeeper.Lock(suite.Ctx, lock, coins) - suite.Require().NoError(err) - - // check accumulation store - accum = suite.App.LockupKeeper.GetPeriodLocksAccumulation(suite.Ctx, types.QueryCondition{ - LockQueryType: types.ByDuration, - Denom: "stake", - Duration: time.Second, - }) - suite.Require().Equal(accum.String(), "10") - - balance := suite.App.BankKeeper.GetBalance(suite.Ctx, addr1, "stake") - suite.Require().Equal(sdk.ZeroInt(), balance.Amount) - - acc := suite.App.AccountKeeper.GetModuleAccount(suite.Ctx, types.ModuleName) - balance = suite.App.BankKeeper.GetBalance(suite.Ctx, acc.GetAddress(), "stake") - suite.Require().Equal(sdk.NewInt(10), balance.Amount) -} - func (suite *KeeperTestSuite) AddTokensToLockForSynth() { suite.SetupTest() @@ -741,14 +666,14 @@ func (suite *KeeperTestSuite) TestEditLockup() { lock, _ := suite.App.LockupKeeper.GetLockByID(suite.Ctx, 1) // duration decrease should fail - err = suite.App.LockupKeeper.ExtendLockup(suite.Ctx, lock.ID, addr, time.Second/2) + err = suite.App.LockupKeeper.ExtendLockup(suite.Ctx, *lock, time.Second/2) suite.Require().Error(err) // extending lock with same duration should fail - err = suite.App.LockupKeeper.ExtendLockup(suite.Ctx, lock.ID, addr, time.Second) + err = suite.App.LockupKeeper.ExtendLockup(suite.Ctx, *lock, time.Second) suite.Require().Error(err) // duration increase should success - err = suite.App.LockupKeeper.ExtendLockup(suite.Ctx, lock.ID, addr, time.Second*2) + err = suite.App.LockupKeeper.ExtendLockup(suite.Ctx, *lock, time.Second*2) suite.Require().NoError(err) // check queries diff --git a/x/lockup/keeper/msg_server.go b/x/lockup/keeper/msg_server.go index 51b704114563cde90bcc96a3b3cdc6c152bb0e28..bb7e478e93fb0873e14131e296f90a2a0cca22f1 100644 --- a/x/lockup/keeper/msg_server.go +++ b/x/lockup/keeper/msg_server.go @@ -155,19 +155,18 @@ func createBeginUnlockEvent(lock *types.PeriodLock) sdk.Event { func (server msgServer) ExtendLockup(goCtx context.Context, msg *types.MsgExtendLockup) (*types.MsgExtendLockupResponse, error) { ctx := sdk.UnwrapSDKContext(goCtx) - owner, err := sdk.AccAddressFromBech32(msg.Owner) + lock, err := server.keeper.GetLockByID(ctx, msg.ID) if err != nil { - return nil, err + return nil, sdkerrors.Wrapf(sdkerrors.ErrInvalidRequest, err.Error()) } - err = server.keeper.ExtendLockup(ctx, msg.ID, owner, msg.Duration) - if err != nil { - return nil, sdkerrors.Wrapf(sdkerrors.ErrInvalidRequest, err.Error()) + if msg.Owner != lock.Owner { + return nil, sdkerrors.Wrapf(types.ErrNotLockOwner, fmt.Sprintf("msg sender (%s) and lock owner (%s) does not match", msg.Owner, lock.Owner)) } - lock, err := server.keeper.GetLockByID(ctx, msg.ID) + err = server.keeper.ExtendLockup(ctx, *lock, msg.Duration) if err != nil { - return nil, sdkerrors.Wrap(sdkerrors.ErrInvalidRequest, err.Error()) + return nil, sdkerrors.Wrapf(sdkerrors.ErrInvalidRequest, err.Error()) } ctx.EventManager().EmitEvents(sdk.Events{