-
Notifications
You must be signed in to change notification settings - Fork 0
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
Partial transfers are still possible, leading to incorrect storage updates, and the calculated account premiums will be significantly different from what they should be #256
Comments
Picodes marked the issue as primary issue |
dyedm1 (sponsor) disputed |
dyedm1 (sponsor) confirmed |
dyedm1 marked the issue as disagree with severity |
dyedm1 marked the issue as agree with severity |
Picodes marked the issue as satisfactory |
Picodes marked the issue as selected for report |
Hi @Picodes, I think some of the duplicates of this finding found the root cause but failed to demonstrate the full impact, and deserve partial credit. For example:
I would appreciate if you could reevaluate the satisfactory level of these findings. |
Lines of code
https://github.com/code-423n4/2023-11-panoptic/blob/f75d07c345fd795f907385868c39bafcd6a56624/contracts/SemiFungiblePositionManager.sol#L619-L621
https://github.com/code-423n4/2023-11-panoptic/blob/f75d07c345fd795f907385868c39bafcd6a56624/contracts/SemiFungiblePositionManager.sol#L626-L630
Vulnerability details
Bug description
The positions in this protocol are ERC1155 tokens and they can be minted or burned.
Token transfers are extremely limited in the protocol:
The sender must transfer all of its liquidity
The recipient must not have a position in that tick range and token type
Users' current liquidity in their positions is tracked with a storage variable called
s_accountLiquidity
. This mapping is overwritten during transfers and the whole value is transferred from to to.The reason for not allowing partial transfers is that partial transfers will mess up the whole storage updating mechanism.
The requirements mentioned above are checked here:
https://github.com/code-423n4/2023-11-panoptic/blob/f75d07c345fd795f907385868c39bafcd6a56624/contracts/SemiFungiblePositionManager.sol#L613C13-L621C99
The check related to whether all balance is transferred or not is made by checking the right slot of the sender's liquidity using
fromLiq.rightSlot()
. Right now, I want to point out there is no check related to the left slot. I'll get there later.Now, we have to understand how position keys are constructed, and how the left slot and right slot work. Let's start with the position keys:
https://github.com/code-423n4/2023-11-panoptic/blob/f75d07c345fd795f907385868c39bafcd6a56624/contracts/SemiFungiblePositionManager.sol#L593C13-L601C18
They are constructed with pool address, user address, token type, lower tick and upper tick. The most important thing I want to mention here is that whether the position is long or short is not in the position key. The thing that matters is the token type (put or call). Which means:
The second thing we need to know is the left and right slot mechanism.
The left slot holds the removed liquidity values and the right slot holds the net liquidity values.
These values are updated in the
_createLegInAMM
during minting and burning depending on whether the action is short or long or mint or burn etc.https://github.com/code-423n4/2023-11-panoptic/blob/f75d07c345fd795f907385868c39bafcd6a56624/contracts/SemiFungiblePositionManager.sol#L971C13-L1000C18
As I mentioned above, only the right slot is checked during transfers. If a user mints a short put (deposits tokens), and then mints a long put (withdraws tokens) in the same ticks, the right slot will be a very small number but the user will have two different ERC1155 tokens (token Ids are different for short and long positions, but position key is the same). Then that user can transfer just the partial amount of short put tokens that correspond to the right slot. It may sound complicated but don't worry, I'll give examples now :)
I'll provide two different scenarios here where the sender is malicious in one of them, and a naive user in another one. You can also find a coded PoC down below that shows all of these scenarios.
Scenario 1: Alice(sender) is a malicious user
Alice mints 100 Short put tokens.
Alice mints 90 Long put tokens in the same ticks (not burn).
At this moment Alice has 100 short put tokens and 90 long put tokens (
tokenId
s are different but theposition key
is the same)Alice transfers only 10 short put tokens to Bob.
This transaction succeeds as the 10 short put token liquidity is the same as Alice's right slot liquidity. (The net liquidity is totally transferred)
After the transfer, Alice still has 90 short put tokens and 90 long put tokens but Alice's
s_accountLiquidity
storage variable is updated to 0.At this moment Bob only has 10 short put tokens. However, the storage is updated.
Bob didn't remove any tokens but his
s_accountLiquidity
left slot is 90, and it looks like Bob has removed 90 tokens.There are two big problems here.
Bob has no way to update his
removedLiquidity
variable other than burning long put tokens. However, he doesn't have these tokens. They are still in Alice's wallet.All of Bob's account premiums and owed premiums are calculated based on the ratio of removed, net and total liquidity. All of his account premiums for that option will be completely incorrect.
https://github.com/code-423n4/2023-11-panoptic/blob/f75d07c345fd795f907385868c39bafcd6a56624/contracts/SemiFungiblePositionManager.sol#L1279C17-L1306C14
Now imagine a malicious user minting a huge amount of short put (deposit), minting 99% of that amount of long put (withdraw), and transferring that 1% to the victim. It's basically setting traps for the victim by transferring tiny amount of net liquidities. The victim's account looks like he removed a lot of liquidity, and if the victim mints positions in the same range in the future, the
owed premiums
for this position will be extremely different, and much higher than it should be.Scenario 2: Alice(sender) is a naive user
The initial steps are the same as those above. She is just a regular user.
She mints 100 short put
Then mints 90 long put.
She knows she has some liquidity left and transfers 10 short put tokens to her friend.
Right now, she has 90 short put tokens and 90 long put tokens.
Her account liquidity is overwritten and updated to 0, but she doesn't know that. She is just a regular user. From her perspective, she still has these 90 short put and 90 long put tokens in her wallet.
She wants to burn her tokens (She has to burn the long ones first).
She burns 90 long put tokens.
https://github.com/code-423n4/2023-11-panoptic/blob/f75d07c345fd795f907385868c39bafcd6a56624/contracts/SemiFungiblePositionManager.sol#L961C9-L980C18
Her account liquidity storage was updated before (step 4) and
removedLiquidity
was 0. After burning her long put tokens, the newremovedLiquidity
(L979 above) will be an enormous number since it is inside the unchecked block.Impact
Partial transfers are still possible since the function only checks whether the net liquidity (right slot) is transferred.
This will disrupt the whole storage updates related to account liquidities.
Malicious users might transfer a tiny bit of their balance, and cause the recipient to pay much more premium.
Naive users might unintentionally mess up their accounts.
Proof of Concept
Down below you can find a coded PoC that proves all scenarios explained above. You can use the protocol's own setup to test this issue:
- Copy and paste the snippet in the
SemiFungiblePositionManager.t.sol
file- Run it with
forge test --match-test test_transferpartial -vvv
The result after running the test:
Tools Used
Manual review, Foundry
Recommended Mitigation Steps
At the moment, the protocol checks if the whole net liquidity is transferred by checking the right slot. However, this restriction is not enough and the situation of the left slot is not checked at all.
The transfer restriction should be widened and users should not be able to transfer if their removed liquidity (left slot) is greater than zero.
Assessed type
Token-Transfer
The text was updated successfully, but these errors were encountered: