IllIllI - Illuminate's PT doesn't respect users' slippage specifications
Opened this issue · 9 comments
IllIllI
high
Illuminate's PT doesn't respect users' slippage specifications
Summary
Illuminate's PT doesn't respect users' slippage specifications, and allows more slippage than is requested
Vulnerability Detail
ERC5095.withdraw()/redeem()
's code adds extra slippage on top of what the user requests
Impact
Direct theft of any user funds, whether at-rest or in-motion, other than unclaimed yield
Miner-extractable value (MEV)
At the end of withdrawal/redemption, the user will end up losing more underlying than they wished to, due to slippage. If the user had used a external PT to mint the Illuminate PT, they will have lost part of their principal.
Code Snippet
The NatSpec says Before maturity, sends 'assets' by selling shares of PT on a YieldSpace AMM.
, so it's clear that the intention is to send back the amount of tokens specified in the input argument. In spite of this, extra slippage is allowed for the amount:
// File: src/tokens/ERC5095.sol : ERC5095.withdraw() #1
219 uint128 returned = IMarketPlace(marketplace).sellPrincipalToken(
220 underlying,
221 maturity,
222 shares,
223 @> Cast.u128(a - (a / 100))
224 );
225 Safe.transfer(IERC20(underlying), r, returned);
226: return returned;
https://github.com/sherlock-audit/2022-10-illuminate/blob/main/src/tokens/ERC5095.sol#L219-L226
// File: src/tokens/ERC5095.sol : ERC5095.withdraw() #2
240 uint128 returned = IMarketPlace(marketplace).sellPrincipalToken(
241 underlying,
242 maturity,
243 Cast.u128(shares),
244 @> Cast.u128(a - (a / 100))
245 );
246 Safe.transfer(IERC20(underlying), r, returned);
247: return returned;
https://github.com/sherlock-audit/2022-10-illuminate/blob/main/src/tokens/ERC5095.sol#L240-L247
(redeem()
has the same issue
and IMarketPlace.sellPrincipalToken()
also considers the amount as an amount that already includes slippage:
// File: src/MarketPlace.sol : MarketPlace.a #3
279 /// @notice sells the PT for the underlying via the pool
280 /// @param u address of an underlying asset
281 /// @param m maturity (timestamp) of the market
282 /// @param a amount of PTs to sell
283 @> /// @param s slippage cap, minimum amount of underlying that must be received
284 /// @return uint128 amount of underlying bought
285 function sellPrincipalToken(
286 address u,
287 uint256 m,
288 uint128 a,
289 @> uint128 s
290: ) external returns (uint128) {
https://github.com/sherlock-audit/2022-10-illuminate/blob/main/src/Marketplace.sol#L285-L298
Tool used
Manual Review
Recommendation
Pass Cast.u128(a)
to the two calls instead
Unfortunately, there isn't a clean solution here for users. When using the preview methods as suggested in the recommendation, an invalid slippage is used for the fourth parameter to sellPrincipalToken
. As a result, we are finding that this does not work on fork-mode tests. For now, we're going to keep the method in place with the knowledge that users should have other avenues available to them (via using the pool directly) to reduce their slippage risk.
In addition, I would disagree with the severity of this issue on that basis as well, and I am open to hearing other ideas the judges have.
Even if there are other paths that the user can take, the presence of a path where they lose principal means there's still a high-severity issue. I believe the problem you're facing is that convert* is using preview*, when it's supposed to be a flash-resistant method of getting the value. The ERC5095 spec specifically only requires that convertToUnderlying() only work at maturity, because according to one of the spec authors, it's an 'open question' about how to get a valid price before then.
As an alternative solution, should we have user provide the slippage then? Given that withdrawPreview()
tells us how many shares
will be required and the fact that it could change depending on where in the block the swap occurs, I think that's the only way around this (other than providing the 1% slippage built-in value we provide)
The 5095 standard has specific arguments for withdraw()
, so I don't think you should add another argument. The standard also mentions Note that some implementations will require pre-requesting to the principal token contract before a withdrawal may be performed. Those methods should be performed separately.
so you could have a function that pre-specifies the slippage before each withdrawal, or you could have a completely separate pre-maturity withdrawal function, and have the normal withdraw()
revert before maturity, as is done in the sample contract in the EIP
Yeah unfortunately we also intended some backwards compatability with 4626 (specifically for some integrations like the aztec 4626 bridge as this product targets their sort of batched design in particular). W/ that context we cant quite remove that sort of integration compatibility with pre-maturity redemptions, nor can add any params without breaking those integrations.
So its difficult to find a great solution other than writing overrides that do include additional parameters for slippage protection. IIRC we had issues with bytecode size limits and didnt want to add them but perhaps through other efforts we reduced some headroom there?
Escalate for 10 USDC
Slight-moderate incorrect slippage controls historically have been graded as medium not high. Seems like there is still ongoing discussion about this issue, but medium seems appropriate if deemed valid.
Escalate for 10 USDC
Slight-moderate incorrect slippage controls historically have been graded as medium not high. Seems like there is still ongoing discussion about this issue, but medium seems appropriate if deemed valid.
You've created a valid escalation for 10 USDC!
To remove the escalation from consideration: Delete your comment.
To change the amount you've staked on this escalation: Edit your comment (do not create a new comment).
You may delete or edit your escalation comment anytime before the 48-hour escalation window closes. After that, the escalation becomes final.
Escalate for 10 USDC
Just to add another point: this should be open/close simultaneously with #181 as the root issue is the same.
Escalate for 10 USDC
Just to add another point: this should be open/close simultaneously with #181 as the root issue is the same.
You've created a valid escalation for 10 USDC!
To remove the escalation from consideration: Delete your comment.
To change the amount you've staked on this escalation: Edit your comment (do not create a new comment).
You may delete or edit your escalation comment anytime before the 48-hour escalation window closes. After that, the escalation becomes final.