Platform: Code4rena
Start Date: 29/06/2022
Pot Size: $50,000 USDC
Total HM: 20
Participants: 133
Period: 5 days
Judge: hickuphh3
Total Solo HM: 1
Id: 142
League: ETH
Rank: 48/133
Findings: 3
Award: $103.01
π Selected for report: 0
π Solo Findings: 0
π Selected for report: IllIllI
Also found by: 0x29A, 0xDjango, 0xc0ffEE, AmitN, BowTiedWardens, StErMi, auditor0517, berndartmueller, cccz, danb, dipp, dirk_y, hansfriese, horsefacts, hyh, kirk-baird, oyc_109, peritoflores, rfa, sseefried, swit, xiaoming90, zzzitron
5.5216 USDC - $5.52
https://github.com/code-423n4/2022-06-putty/blob/main/contracts/src/PuttyV2.sol#L323-L379
The fillOrder
method allows the caller to pay with ETH that is then wrapped to WETH. This only works when the order base asset is WETH. However, if the base asset isn't WETH or if the type of order doesn't transfer value in from the sender, then the transaction doesn't revert or refund the value back to the sender.
I consider this to be of high severity since users of the protocol can accidentally lose funds. A properly functioning protocol should not allow this to occur even by accident.
To highlight this behaviour I wrote a new test case in FillOrder.t.sol
:
function testNativeEthCanBeLost() public { PuttyV2.Order memory order = defaultOrder(); order.isLong = true; order.isCall = false; MockERC20 randomToken = new MockERC20("Mock Token", "MOCK", 18); order.baseAsset = address(randomToken); bytes memory signature = signOrder(babePrivateKey, order); randomToken.mint(address(this), 0xffffffff); randomToken.mint(babe, 0xffffffff); vm.startPrank(babe); randomToken.approve(address(p), type(uint256).max); vm.stopPrank(); randomToken.approve(address(p), type(uint256).max); deal(address(this), order.strike); uint256 takerBalanceBefore = address(this).balance; uint256 contractWethBefore = weth.balanceOf(address(p)); uint256 contractBalanceBefore = address(p).balance; // act p.fillOrder{value: order.strike}(order, signature, floorAssetTokenIds); // assert assertEq( weth.balanceOf(address(p)) - contractWethBefore, 0, "No ETH should have been converted to WETH" ); assertEq( address(p).balance - contractBalanceBefore, order.strike, "Taker should have lost ETH" ); assertEq( takerBalanceBefore - address(this).balance, order.strike, "Should have transferred strike ETH from taker to contract" ); }
VSCode + Foundry
I consider there to be two options to resolve this issue:
I think option 2 is probably the better option given the limited utility the extra logic provides. Especially consider the batchFillOrder method can't be payable given the current logic in fillOrder. By removing the ability to pay with native ETH it will not be possible for users to lose any funds accidentally in this manner. You could also add in a fallback function to the contract to revert on any incoming ETH transfers to prevent any ETH from being locked in the contract by external senders (or alternatively add in an admin only method to withdraw any ETH transferred to the contract).
#0 - rotcivegaf
2022-07-04T23:55:52Z
Move to 2 (Med Risk)
label
A part duplicate of #226
#1 - outdoteth
2022-07-06T19:29:33Z
Duplicate: Native ETH can be lost if itβs not utilised in exercise and fillOrder: https://github.com/code-423n4/2022-06-putty-findings/issues/226
π Selected for report: Picodes
Also found by: 0xNineDec, 0xsanson, Metatron, antonttc, berndartmueller, catchup, dirk_y, sseefried, unforgiven
https://github.com/code-423n4/2022-06-putty/blob/main/contracts/src/PuttyV2.sol#L497-L501
Anyone using American options has to understand their profit/loss potential under certain market conditions. When signing an order, the order maker and filler will be very clear on the profit/loss potential of the order and will be taking into account the protocol fee when setting up the order parameters. However, when an option is exercised and the subsequent parties withdraw from the protocol, the fee percentage is taken as the current fee percentage of the contract rather than the fee when the option was filled. Thus, if the fee percentage were increased then one party of the option would receive less value than expected. The lack of certainty around the fee percentage is an issue for the users of the protocol and provides a path for a potential shock when it comes to time of withdrawal.
When withdrawing by calling withdraw
the strike is decremented by the fee percentage defined in the fee
storage slot. The admin can set the fee
variable at any time by calling setFee
. Thus it is possible to have a different value in fee
between filling the order and withdrawing after order exercise/expiration.
VSCode
To provide stability for users of the protocol the fee percentage should be fixed at the time the option is filled and saved in storage. Below is a diff with a suggestion for how this could work:
diff --git a/contracts/src/PuttyV2.sol b/contracts/src/PuttyV2.sol index 04cf2fb..0bb0397 100644 --- a/contracts/src/PuttyV2.sol +++ b/contracts/src/PuttyV2.sol @@ -136,6 +136,11 @@ contract PuttyV2 is PuttyV2Nft, EIP712("Putty", "2.0"), ERC721TokenReceiver, Own */ uint256 public fee; + /** + @notice Fee rate when option is filled + */ + mapping(uint256 => uint256) public positionFee; + /** @notice Whether or not an order has been cancelled. Maps from orderHash to isCancelled. @@ -315,6 +320,9 @@ contract PuttyV2 is PuttyV2Nft, EIP712("Putty", "2.0"), ERC721TokenReceiver, Own // save the long position expiration positionExpirations[order.isLong ? uint256(orderHash) : positionId] = block.timestamp + order.duration; + // save the short position fee percentage + positionFee[!order.isLong ? uint256(orderHash) : positionId] = fee; + emit FilledOrder(orderHash, floorAssetTokenIds, order); /* ~~~ INTERACTIONS ~~~ */ @@ -491,12 +502,14 @@ contract PuttyV2 is PuttyV2Nft, EIP712("Putty", "2.0"), ERC721TokenReceiver, Own /* ~~~ INTERACTIONS ~~~ */ + uint256 fillFee = positionFee[uint256(orderHash)] + // transfer strike to owner if put is expired or call is exercised if ((order.isCall && isExercised) || (!order.isCall && !isExercised)) { // send the fee to the admin/DAO if fee is greater than 0% uint256 feeAmount = 0; - if (fee > 0) { - feeAmount = (order.strike * fee) / 1000; + if (fillFee > 0) { + feeAmount = (order.strike * fillFee) / 1000; ERC20(order.baseAsset).safeTransfer(owner(), feeAmount); }
#0 - outdoteth
2022-07-06T18:35:38Z
Duplicate: Admin can change fee at any time for existing orders: https://github.com/code-423n4/2022-06-putty-findings/issues/422
π Selected for report: xiaoming90
Also found by: 0x1f8b, 0x29A, 0x52, 0xDjango, 0xNazgul, 0xNineDec, 0xSolus, 0xf15ers, 0xsanson, AmitN, Bnke0x0, BowTiedWardens, Chom, David_, ElKu, Funen, GalloDaSballo, GimelSec, Hawkeye, IllIllI, JC, JohnSmith, Kaiziron, Kenshin, Lambda, Limbooo, MadWookie, Metatron, MiloTruck, Nethermind, Picodes, ReyAdmirado, Sneakyninja0129, StErMi, TomJ, Treasure-Seeker, TrungOre, Waze, Yiko, _Adam, __141345__, antonttc, async, aysha, catchup, cccz, cryptphi, csanuragjain, danb, datapunk, defsec, delfin454000, dirk_y, doddle0x, durianSausage, exd0tpy, fatherOfBlocks, gogo, hake, hansfriese, horsefacts, hubble, itsmeSTYJ, joestakey, oyc_109, pedroais, peritoflores, rajatbeladiya, reassor, robee, rokinot, samruna, saneryee, sashik_eth, shenwilly, shung, simon135, sseefried, unforgiven, zer0dot, zzzitron
47.3626 USDC - $47.36
During my audit of this project I found 2 low severity issues I thought were worth calling out:
ORDER_TYPE_HASH
is incorrect. It should be something like: Order type hash used for EIP-712 encoding.
setBaseURI
and setFee
are payable for no reason. There is no reason why these methods should accept value and it opens up the potential for accidentally locking Ether inside the contract.