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: 18/133
Findings: 5
Award: $902.32
π Selected for report: 0
π Solo Findings: 0
π Selected for report: zzzitron
Also found by: Kenshin, Metatron, PwnedNoMore, danb, minhquanym
756.9377 USDC - $756.94
https://github.com/code-423n4/2022-06-putty/blob/3b6b844bc39e897bd0bbb69897f2deff12dc3893/contracts/src/PuttyV2.sol#L442 https://github.com/code-423n4/2022-06-putty/blob/3b6b844bc39e897bd0bbb69897f2deff12dc3893/contracts/src/PuttyV2.sol#L515-L516
The fillOrder()
function only checks that the floorAssetTokenIds.length
must be 0 when a taker fill a short call order. In other words, it does not check whether order.floorTokens.length
is 0
or not, which means that if the maker includes any address in order.floorTokens
, the maker will be unable to withdraw their assets when the order expires. The function withdraw()
and exercise()
will run into uninitialize variable, which causes the Index out of bound revert due to length inconsistency between two arrays.
Run the test with Foundry.
function testShortCallNonWithdrawable() public { PuttyV2.Order memory babeOrder = defaultOrder(); // short call order babeOrder.isLong = false; babeOrder.isCall = true; // add BAYC #1 as an asset erc721Assets.push(PuttyV2.ERC721Asset({token: address(bayc), tokenId: 1})); babeOrder.erc721Assets = erc721Assets; // include BAYC address to floorTokens array floorTokens.push(address(bayc)); babeOrder.floorTokens = floorTokens; //Babe sign order bytes memory signature = signOrder(babePrivateKey, babeOrder); // Mint BAYC #1 to Babe and Approve to Putty bayc.mint(babe, 1); vm.prank(babe); bayc.approve(address(p), 1); //Someone fill Babe order, floorAssetTokenIds must be empty p.fillOrder(babeOrder, signature, floorAssetTokenIds); assertEq(bayc.ownerOf(1), address(p), "Babe's BAYC #1 should be transferred to Putty"); /* Exercise revert */ babeOrder.isLong = !babeOrder.isLong; vm.expectRevert(stdError.indexOOBError); p.exercise(babeOrder, floorAssetTokenIds); /* Withdraw revert */ //Let it expires vm.warp(block.timestamp + babeOrder.duration + 1 days); babeOrder.isLong = !babeOrder.isLong; // It should revert with "Index out of bounds" vm.expectRevert(stdError.indexOOBError); vm.prank(babe); p.withdraw(babeOrder); assertEq(bayc.ownerOf(1), address(p), "BAYC #1 should be stuck in Putty");
Foundry
Add an empty array validation to invoke floorToken transfer only when positionFloorAssetTokenIds[floorPositionId]
is not empty.
if (positionFloorAssetTokenIds[floorPositionId].length > 0) _transferFloorsOut(order.floorTokens, ...);
#0 - 0xlgtm
2022-07-05T08:28:26Z
#1 - outdoteth
2022-07-06T18:05:52Z
Short call with floorTokens will result in a revert when exercising: https://github.com/code-423n4/2022-06-putty-findings/issues/369
π Selected for report: kirk-baird
Also found by: 0xA5DF, Kenshin, cccz, chatch, csanuragjain, hansfriese, hyh, itsmeSTYJ, pedroais, sashik_eth, unforgiven, xiaoming90
41.8933 USDC - $41.89
Judge has assessed an item in Issue #219 as Medium risk. The relevant finding follows:
#0 - HickupHH3
2022-07-15T15:00:36Z
Any ERC20 Token Can Be Used As baseAsset
dup of #50
π 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.1306 USDC - $47.13
It is possible that either the maker or the taker is a smart contract that does not support ERC721. This case is possible in 3 out of 4 types, short call is the exception one because the unsupported contract should unable to hold an NFT. Even if it has no significant impact, a user who filled the order with this type of contract may lose opportunities because their underlying will be locked in Putty's contract. Particularly if the user holds a long position, they will have a limited time to exercise the option before it expires, after which their underlying may be locked forever.
There are 2 ways to fix this, the second takes less effort but I'm not recommend:
PuttyV2Nft._mint()
to check that the receiver is a contract that support ERC721 or not.require( to.code.length == 0 || ERC721TokenReceiver(to).onERC721Received(msg.sender, address(0), id, "") == ERC721TokenReceiver.onERC721Received.selector, "UNSAFE_RECIPIENT" );
Address.isContract()
. Please note that the checks may not 100% restrict contracts out of the platform.Remark:
If you decide to support smart contract users, including ERC721TokenReceiver(to).onERC721Received
call could lead to reentrancy attack. As it is not possible from the current code base, so I did not focus on finding any.
baseAsset
Refer to the provided demo site, the baseAsset
is implicitly assumed to be ETH/WETH in the most case, so no baseAsset
's address is displayed on the front-end. However, creating an order with a custom baseAsset
is possible, an attacker could create a fake ERC20 with same name and symbol as the well-known token, USDT, USDC, DAI, for example, and use it as baseAsset
. In this case, it is dependent on how the front-end displays it; if this information is displayed imprecisely, users may be deceived. The likely attack in my opinion is long call, in which the attacker uses a fake token as bait to steal ERC20 and/or ERC721 asset(s) from the taker.
Display the baseAsset
information clearly on the front end; this should help reduce the risk of users being deceived, but it is still possible. Or else, implement a list of allowed baseAsset
so that if any order is placed using a token that is not on the allowed list, the contract will revert, but gas consumption will definitely increase.
There are no restrictions on the size of the Order.whitelist
, which can result in DoS with block gas limit. A user could create an order with an extremely long whitelist address, causing anyone attempting to fill the order to fail and lose their transaction fee.
The array size can be limited by using the reasonable lowest uint
size to ensure that no one can include an extremely large array, or by adding a validation check against the Order.whitelist
that should not be longer than n
length, where n
is a reasonable number that will not cause the gas limit to revert while still being sufficient for usage.
Although the premium is expected to be less than the strike amount, there is no safe check. This could result in some attack scenarios, such as when an attacker (maker) creates a short put order with premium = 1 ETH
and strike = 0.1 ETH
, and when someone takes the attacker order and becomes the taker, the taker pays 1 ETH
to the attacker, but can exercise to receive only 0.1 ETH
(fee is excluded) back. I noted that such attack scenarios make no sense and are unlikely to occur; however, the contract allows for such an order.
I recommended adding a validation that premium should be less than or equal strike to prevent this kind of order to be happened.
#0 - outdoteth
2022-07-07T18:59:59Z
No Verification For Unsupported ERC721 Receiver
Duplicate: Contracts that canβt handle ERC721 tokens will lose their Putty ERC721 position tokens: https://github.com/code-423n4/2022-06-putty-findings/issues/327
π Selected for report: GalloDaSballo
Also found by: 0v3rf10w, 0x1f8b, 0xA5DF, 0xDjango, 0xHarry, 0xKitsune, 0xNazgul, 0xNineDec, 0xc0ffEE, 0xf15ers, 0xkatana, 0xsanson, ACai, Aymen0909, Bnke0x0, BowTiedWardens, Chom, ElKu, Fitraldys, Funen, Haruxe, Hawkeye, IllIllI, JC, JohnSmith, Kaiziron, Kenshin, Lambda, Limbooo, MadWookie, Metatron, MiloTruck, Picodes, PwnedNoMore, Randyyy, RedOneN, ReyAdmirado, Ruhum, Sm4rty, StErMi, StyxRave, TerrierLover, TomJ, Tomio, UnusualTurtle, Waze, Yiko, _Adam, __141345__, ajtra, ak1, apostle0x01, asutorufos, c3phas, cRat1st0s, catchup, codetilda, cryptphi, datapunk, defsec, delfin454000, durianSausage, exd0tpy, fatherOfBlocks, gogo, grrwahrr, hake, hansfriese, horsefacts, ignacio, jayfromthe13th, joestakey, ladboy233, m_Rassska, mektigboy, minhquanym, mrpathfindr, natzuu, oyc_109, rajatbeladiya, reassor, rfa, robee, rokinot, sach1r0, saian, sashik_eth, simon135, slywaters, swit, z3s, zeesaw, zer0dot
21.1707 USDC - $21.17
Transferring assets in and out of Putty always be executed even the asset array is empty. Checking and skip transferring call over empty asset array can help save some gas.
Add a check against asset array and skip the transferring if it is empty. Here is some example:
//PuttyV2.sol:L375-L377 if (order.erc20Assets.length > 0) _transferERC20sIn(order.erc20Assets, msg.sender); if (order.erc721Assets.length > 0) _transferERC721sIn(order.erc721Assets, msg.sender); if (order.floorTokens.length > 0) _transferFloorsIn(order.floorTokens, floorAssetTokenIds, msg.sender);
Prefix incremental ++i
uses slightly less gas than postfix incremental i++
. And on arithmetic operation that can be guarantee to not overflow/underflow, the unchecked
block should be apply for gas optimization opportunity.
Use prefix instead of postfix incremental, and apply the unchecked
block over ++i
. Here is an example:
//PuttyV2.sol:L610-L614 function _transferERC721sIn(ERC721Asset[] memory assets, address from) internal { for (uint256 i = 0; i < assets.length; ) { ERC721(assets[i].token).safeTransferFrom(from, address(this), assets[i].tokenId); unchecked { ++i; } } }