Platform: Code4rena
Start Date: 27/11/2023
Pot Size: $60,500 USDC
Total HM: 7
Participants: 72
Period: 7 days
Judge: Picodes
Total Solo HM: 2
Id: 309
League: ETH
Rank: 14/72
Findings: 2
Award: $1,122.43
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: osmanozdemir1
Also found by: 0xDING99YA, 0xloscar01, KupiaSec, SpicyMeatball, ether_sky, fnanni, hash, minhtrng
1111.1061 USDC - $1,111.11
When a token is transferred, it checks if the entire balance is being transferred by comparing liquidity chunk and net liquidity.
uint256 fromLiq = s_accountLiquidity[positionKey_from]; if (fromLiq.rightSlot() != liquidityChunk.liquidity()) revert Errors.TransferFailed();
This causes an issue where invalid token is allowed to be transferred and it makes other token can't be burnt forever. For example, Alice creates an option for selling T amount of call at K, also creates another option(not a leg but separate token) for buying T/2 amount of call at K. In this case, R = T/2, N = T - R = T/2, thus make it possible for long call option to be transferred to another address. Once long call is transferred, liquidity information is reset to zero, making it impossible for the short call being burnt or transferred and the short call's position is stuck on the smart contract forever.
Here's a test case written in Foundry that shows above example:
function test_Audit_token_transfer( uint256 x, uint256 widthSeed, int256 strikeSeed ) public { uint256 positionSizeSeed = 1e19; _initPool(x); (int24 width, int24 strike) = PositionUtils.getInRangeSW( widthSeed, strikeSeed, uint24(tickSpacing), currentTick ); populatePositionData(width, strike, positionSizeSeed); // tokenId1 - short call uint256 tokenId1 = uint256(0).addUniv3pool(poolId).addLeg(0, 1, isWETH == 0 ? 1 : 0, 0, 0, 0, strike, width); // tokenId2 - long call uint256 tokenId2 = uint256(0).addUniv3pool(poolId).addLeg(0, 1, isWETH == 0 ? 1 : 0, 1, 0, 0, strike, width); int256 totalCollected; int256 totalSwapped; int24 newTickk; changePrank(Alice); positionSize = 1e20; // Write long call option - Option1 (totalCollected, totalSwapped, newTickk) = sfpm.mintTokenizedPosition( tokenId1, positionSize, TickMath.MAX_TICK - 1, TickMath.MIN_TICK + 1 ); // Write short call option - Option2 (totalCollected, totalSwapped, newTickk) = sfpm.mintTokenizedPosition( tokenId2, positionSize / 2, TickMath.MAX_TICK - 1, TickMath.MIN_TICK + 1 ); // int24 tmp = width * tickSpacing / 2; // uint256 liq = sfpm.getAccountLiquidity(address(pool), Alice, 0, strike - tmp, strike + tmp); // assertEq(liq, 0); // return ; // Option2 can be transferred because `liquidityChunk = T/2 and N = T- R = T/2 as well` sfpm.safeTransferFrom( Alice, Bob, tokenId2, positionSize / 2, "" ); // Option1 fails to burn, forever fails vm.expectRevert(Errors.NotEnoughLiquidity.selector); sfpm.burnTokenizedPosition( tokenId1, positionSize, TickMath.MAX_TICK - 1, TickMath.MIN_TICK + 1 ); }
Manual Review, Foundry
When a token is transferred, T and R need to be calculated for all legs and compare it with the account liquidity. Another possible mitigation would be tieng position key to tokenId.
Invalid Validation
#0 - dyedm1
2023-12-17T21:59:29Z
dup #256
#1 - c4-sponsor
2023-12-17T21:59:37Z
dyedm1 (sponsor) confirmed
#2 - c4-judge
2023-12-21T18:59:05Z
Picodes marked the issue as duplicate of #256
#3 - c4-judge
2023-12-26T22:47:12Z
Picodes marked the issue as satisfactory
🌟 Selected for report: osmanozdemir1
Also found by: 0xCiphky, Audinarey, Banditx0x, CRYP70, Cryptor, D1r3Wolf, KupiaSec, LokiThe5th, Sathish9098, Skylice, ThenPuli, Topmark, Udsen, ZanyBonzy, baice, ether_sky, fatherOfBlocks, foxb868, grearlake, hihen, hubble, hunter_w3b, lanrebayode77, leegh, lsaudit, minhtrng, nocoder, onchain-guardians, ptsanev, ro1sharkm, seaton0x1, sivanesh_808, t4sk, tapir, tpiliposian, ustas
11.3163 USDC - $11.32
For writing options, call writers only require asset(ETH) to deposit and put writers only require cash(USDC) to deposit, they don't have to hold both asset and cash to write an option. However, by the nature of the minting logic, when the positin is in the money, call writers need to hold both asset and cash even though one side is returned after swap.
Recommendation: Implementation could be upgraded by using flashloan, without requiring option writers to hold both asset and cash.
Recommendation: Use <=
and >=
for comparison.
#0 - c4-judge
2023-12-14T16:55:49Z
Picodes marked the issue as grade-b