Root Cause & issue Loc.
In the _mint()
function, there is special-case logic for when the vault is in a 1x long position (_isLongOneLeverage(beenLong) == true
). Specifically:
When a user deposits collateral, that collateral eventually gets swapped into indexToken
(since the vault is 1x long). >BUT, the code subtracts the deposit’s final amount
from the old indexToken
balance instead of adding it to the vault’s total. This incorrectly lowers the denominator in the share-minting formula and causes the depositor to receive more shares than they really deserve.
Assume the vault is already open in a 1x long position.
There is some existing balance of indexToken
in the vault (e.g. 100 tokens) and some nonzero totalShares
(e.g. 100 shares).
2. Deposit:
An attacker deposits X
units of the collateral token, which are swapped 1:1 into X
indexToken
.
Now the actual total indexToken
holdings in the vault should be 100 + X
.
Faulty Calculation:
Instead of recognizing that the vault’s “old” total was 100 and the “new” total is 100 + X
, the code does:
Then it mints shares as:
Because totalAmountBefore
is artificially reduced by X
, the fraction (_shares / totalShares)
ends up being larger than the correct fraction.
Proof by Example
Initial State:
Vault has 100
indexToken
.
totalShares = 100
.
So, each share should be worth 1
indexToken
at this moment.
User Deposits 10 Collateral, which the vault swaps into 10
indexToken
:
Real new total: 110
indexToken
.
Fair share: The user should get shares equal to (10 / 110) * (old totalShares + ???)
, which is about 9.09
shares out of ~109.09
total. They end up with roughly 9.09%
of the vault if done correctly.
Faulty Code Path:
It sets totalAmountBefore = 100 - 10 = 90
(subtracting the deposit from the old indexToken
balance).
The minted shares = 10 * 100 / 90 = 11.111...
.
After minting, totalShares = 100 + 11.111... = 111.111...
, so the user’s fractional ownership is 11.111 / 111.111 = 10%
.
In terms of the vault’s actual 110 tokens, the user effectively controls 110 * 0.1 = 11
tokens’ worth—a free ~1 token.
Because the user is systematically over-minted shares, they effectively steal value from other participants. Moreover, an attacker can repeat this deposit process multiple times to compound the exploit.
Below is a minimal Foundry proof-of-concept (PoC) demonstrating the “Over-Minting of Shares in 1x Long Scenario” issue in PerpetualVault
. The key idea is:
Deploy a simplified PerpetualVault
with leverage = 1x (leverage = 10_000
).
Mock the vault so it believes it is in a “1x long, position open” state (beenLong = true; positionIsClosed = false
).
Have a user deposit a certain amount of collateral.
Show that the minted shares exceed the correct, “expected” shares, proving an inflation in user share issuance.
Note: This is a minimal test. It does not compile against the entire protocol code, but focuses on the
_mint()
over-mint logic.
Severity by likelyhood & impact
This is a high-severity issue because it allows one depositor to acquire a disproportionately large share of the vault with no special permissions. Over multiple deposits, the attacker can dilute and siphon value from honest depositors.
In _mint()
, remove the special subtraction of amount
from the vault’s indexToken balance whenever the vault is in a 1x long position. Instead, the contract should compute the vault’s total value before adding the new deposit, then use that to calculate the correct fraction of shares to mint.
A safer approach is to always use a reliable “vault total value” call (e.g., _totalAmount(prices)
) before the deposit is added. Then, once the deposit is fully swapped into indexToken
, you mint shares relative to the vault’s pre-deposit state.
In other words:
There is no real proof, concrete root cause, specific impact, or enough details in those submissions. Examples include: "It could happen" without specifying when, "If this impossible case happens," "Unexpected behavior," etc. Make a Proof of Concept (PoC) using external functions and realistic parameters. Do not test only the internal function where you think you found something.
The contest is live. Earn rewards by submitting a finding.
This is your time to appeal against judgements on your submissions.
Appeals are being carefully reviewed by our judges.