-
Notifications
You must be signed in to change notification settings - Fork 608
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
lockup: Allow rebonding of unbonding tokens #4713
Conversation
Questions to reviewers:
|
x/lockup/keeper/lock.go
Outdated
func (k Keeper) RebondTokens(ctx sdk.Context, lockID uint64, owner sdk.AccAddress, coins sdk.Coins) error { | ||
lock, err := k.GetLockByID(ctx, lockID) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
if lock.Owner != owner.String() { | ||
return fmt.Errorf("lock %d is not owned by %s", lockID, owner) | ||
} | ||
|
||
if !lock.IsUnlocking() { | ||
return fmt.Errorf("lock %d is not unlocking, rebonding only possible in unlocking stage", lockID) | ||
} | ||
|
||
// If all checks pass, we can rebond the tokens | ||
err = k.rebondTokens(ctx, owner, *lock, coins) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
return nil | ||
} |
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.
What is the reason for having 2 methods? I think we only need one unexported method
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.
was following the convention of this example. Also, it made sense to me to separate the function that asserts the provided parameters are valid from the function that actually executes the rebonding logic. wdyt, should we do it in one function?
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 personally think that these can be grouped into one since we would not have further instances where we would have to call rebondTokens
separately
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.
Nice! Thanks for this PR, left some comments, please take a look!
x/lockup/keeper/lock.go
Outdated
func (k Keeper) RebondTokens(ctx sdk.Context, lockID uint64, owner sdk.AccAddress, coins sdk.Coins) error { | ||
lock, err := k.GetLockByID(ctx, lockID) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
if lock.Owner != owner.String() { | ||
return fmt.Errorf("lock %d is not owned by %s", lockID, owner) | ||
} | ||
|
||
if !lock.IsUnlocking() { | ||
return fmt.Errorf("lock %d is not unlocking, rebonding only possible in unlocking stage", lockID) | ||
} | ||
|
||
// If all checks pass, we can rebond the tokens | ||
err = k.rebondTokens(ctx, owner, *lock, coins) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
return nil | ||
} |
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 personally think that these can be grouped into one since we would not have further instances where we would have to call rebondTokens
separately
x/lockup/keeper/lock.go
Outdated
} | ||
|
||
// Remove original lock's refs from state | ||
err = k.deleteLockRefs(ctx, types.KeyPrefixUnlocking, lock) |
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.
Question: Do we not need to use the following?
err = k.deleteLockRefs(ctx, types.KeyPrefixUnlocking, lock) | |
err = k.deleteLockRefs(ctx, unlockingPrefix(lock.IsUnlocking()), lock) |
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.
not really, because we are already assured at this point that the lock is unlocking, but we can still add that, but currently there will be no purpose to that
This pull request has been automatically marked as stale because it has not had any recent activity. It will be closed if no further activity occurs. Thank you! |
I'm pretty sure this can't be backported as it is not state compatible |
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.
This looks good to me. I think we need tests for the splitting locks case (added a comment about it in the code). Once those are in I'm happy to merge it
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.
Nice work! @AlpinYukseloglu is going to give a final blessing on this later but looks good for merge to me!
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.
Thanks for this PR! I reviewed the changes quite thoroughly and the logic looks sound, but given how this PR touches critical parts of our codebase and seems quite sparsely tested, we've opted to push this to v21.
I've included a list of the classes of tests that I believe are important to include in code comments. I was originally going to commit these directly to the PR, but the scope was large enough to the point where this was blocking v20 release, so we opted to simply include the list here and aim to get this change in for v21.
x/lockup/keeper/lock_test.go
Outdated
|
||
// a sanity check to make sure the lock is in the correct state | ||
if tc.unlock { | ||
suite.Require().True(initialLock.IsUnlocking()) |
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.
Would you be able to add an additional set of test cases (or extend these ones by adding a new test field for time elapsed) that covers cases where time has elapsed since the lock started unlocking? Specifically, it is important we test behavior around when:
- the lock is partially finished unlocking (should function same as current tests)
- the lock is exactly finished unlocking (should not allow rebonding, as it should no longer be unlocking)
- the lock is past the unlocking period (should fail in all cases)
It would also be great to have cases covering behavior where BeginUnlock
is run on part (not all) of lockedCoins
, as this triggers a different logic branch that uses SplitLock
that is currently untested.
Finally, we should also have test cases that include the creation of other locks and assert that they are unchanged.
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.
hey @AlpinYukseloglu! Those are great points, thank you for your review!
Note: I changed the logic of tests to make it as readable as possible. Now, in a test case struct I include so called setupFunctions
that are being ran prior to testing. These include: creation of initial lock, unlocking the lock, creating synthetic lockup, etc.
I addressed your comments in latest commits, here is a breakdown of everything for an easier review process:
- Partially finished unlocking lock
- Exactly finished unlocking lock
- Past unlocking period
- BeginUnlock on some fraction of locked coins
- Assertion that other locks are unchanged
Though, I have one concern about exactly unlocked lock (2). (2) works because I run EndBlocker
in the test prior to rebonding a lock. Because of that, it automatically removes the lock if the current block time is exactly the endTime
of a lock (matured).
However, in real scenario, when transactions of a block with block time equal to endTime
of an unlocking lock are being executed, if someone tries to rebond this lock, they will be able to do so, since it was not yet removed from the store. I am a little confused as to why in case (2), we should fail? Since unlocking of maturing locks happens in EndBlocker
, I do not see the reason for forbidding case (2) from passing
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.
tagging for re-review of the new test
cc: @mattverse @p0mvn
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.
Adding on case (2): basically, the test works right now by simulating an unreal scenario - by calling endBlocker and then running rebonding logic both with the same blocktime is essentially equivalent to a scenario, when two consecutive blocks have the same block time
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 think case 2 correct way would be being able to rebond if endblocker has not been called yet
defaultDuration := time.Second | ||
|
||
// autoUnlockingStartHeight is a height at which EndBlocker starts automatically unlocking matured locks | ||
autoUnlockingStartHeight := int64(7) |
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.
note: auto withdrawing of tokens does not begin prior to height 7
suite.Require().NoError(err) | ||
|
||
// calling end blocker to automatically remove matured locks (no-op if setup functions did not modify blocktime) | ||
lockup.EndBlocker(suite.Ctx, *suite.App.LockupKeeper) |
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.
note: this is needed for test cases that assert rebonding of unlocking lock at some stage of it's unlocking period (ex: half of unlocking time has elapsed, exactly the unlocking time has elapsed, unlocking time has elapsed in the past)
This pull request has been automatically marked as stale because it has not had any recent activity. It will be closed if no further activity occurs. Thank you! |
keeping alive |
Thank you so much for the huge amount of work that went into this 🙏 , really appreciate it. Unfortunately don't think it makes sense to merge given that its a breaking change that will likely be unused, and its better to have less code to maintain. Really sorry, this should have been cut off much earlier. Would still love to give you a bounty for the work on this |
hey @ValarDragon ! Thank you for letting me know, it is totally fine and I understand it! I will reach out to your @osmosis.team email, and thank you again! |
Closes: #382
What is the purpose of the change
Adds a tx to allow users
rebond
anunbonding
lockThere are 2 scenarios:
Will create a new non-unbonding lock if the amount of coins is not nil and not equal to the amount of coins in the lock. The rebonded tokens will be subtracted from the original lock, otherwise it will be unchanged
If the amount of tokens specified is nil or equal to the amount of coins in that lock, that means we will simply change the original lock's status from unlocking to non-unlocking (by setting the
endTime
totime.Time{}
)Checklist
Brief Changelog
osmosisd tx lockup rebond-tokens [ID]
Testing and Verifying
Documentation and Release Note
added to CHANGELOG.md