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: 7/133
Findings: 5
Award: $1,816.25
π Selected for report: 1
π Solo Findings: 0
π Selected for report: zishansami
Also found by: 0x52, 0xsanson, auditor0517, berndartmueller, csanuragjain, zzzitron
583.9233 USDC - $583.92
MEDIUM - functions of the protocol could be impacted
For put options, the fees are not paid as intended.
As this comment says, fee is applied on exercise.
@notice Fee rate that is applied on exercise.
But the current implementation applies the fee in withdraw
:
// withdraw function, where the fee is applied // 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; ERC20(order.baseAsset).safeTransfer(owner(), feeAmount); } ERC20(order.baseAsset).safeTransfer(msg.sender, order.strike - feeAmount); return; }
foundry
The fees can be applied in the exercise.
#0 - outdoteth
2022-07-06T18:25:35Z
Duplicate: Fees are only applied on puts if they are expired: https://github.com/code-423n4/2022-06-putty-findings/issues/269
π Selected for report: zzzitron
Also found by: Kenshin, Metatron, PwnedNoMore, danb, minhquanym
756.9377 USDC - $756.94
HIGH - assets can be lost
If a short call order is created with non empty floorTokens array, the taker cannot exercise. Also, the maker cannot withdraw after the expiration. The maker will still get premium when the order is filled. If the non empty floorTokens array was included as an accident, it is a loss for both parties: the taker loses premium without possible exercise, the maker loses the locked ERC20s and ERC721s.
This bug is not suitable for exploitation to get a 'free' premium by creating not exercisable options, because the maker will lose the ERC20s and ERC721s without getting any strike. In that sense it is similar but different issue to the Create a short put order with zero tokenAmount makes the option impossible to exercise
, therefore reported separately.
The proof of concept shows a scenario where babe makes an short call order with non empty floorTokens
array. Bob filled the order, and now he has long call option NFT. He wants to exercise his option and calls exercise
. There are two cases.
floorAssetTokenIds
arrayfloorAssetTokenIds
array with matching length to the orders.floorTokens
In the case1, the input floorAssetTokenIds
were checked to be empty for put orders, and his call passes this requirement. But eventually _transferFloorsIn
was called and he gets Index out of bounds
error, because floorTokens
is not empty which does not match with empty floorAssetTokenIds
.
// case 1 // PuttyV2.sol: _transferFloorsIn called by exercise // The floorTokens and floorTokenIds do not match the lenghts // floorTokens.length is not zero, while floorTokenIds.length is zero ERC721(floorTokens[i]).safeTransferFrom(from, address(this), floorTokenIds[i]);
In the case2, the input floorAssetTokenIds
were checked to be empty for put orders, but it is not empty. So it reverts.
// case2 // PuttyV2.sol: exercise // non empty floorAssetTokenIds array is passed for put option, it will revert !order.isCall ? require(floorAssetTokenIds.length == order.floorTokens.length, "Wrong amount of floor tokenIds") : require(floorAssetTokenIds.length == 0, "Invalid floor tokenIds length");
After the option is expired, the maker - babe is trying to withdraw but fails due to the same issue with the case1.
// maker trying to withdraw // PuttyV2.sol: withdraw _transferFloorsOut(order.floorTokens, positionFloorAssetTokenIds[floorPositionId]);
Note on the poc:
Index out of bounds
.floorTokens
.foundry
It happens because the fillOrder
does not ensure the order.floorTokens
to be empty when the order is short call.
#0 - 0xlgtm
2022-07-05T08:22:39Z
Note that it is possible to cause loss of funds for others through this.
Assume that maker (A) creates a long call and taker (B) fills it, transferring floor tokens (XYZ) into putty.
If maker (C) creates a short call with floorTokens (XYZ), taker (D) is able to fill and exercise his long call since XYZ already resides on Putty. This will however invalidate the options pair that was created between A and B since A cannot exercise and B cannot withdraw.
#1 - outdoteth
2022-07-08T13:33:29Z
Agree that this should be marked as high severity given the exploit scenario provided by @STYJ above.
#2 - outdoteth
2022-07-08T13:33:43Z
Report: Short call with floorTokens will result in a revert when exercising
#3 - HickupHH3
2022-07-12T13:54:27Z
Agreed, all wardens gave the same scenario that leads to a direct loss of NFTs and premium, but @STYJ's exploit scenario raises the gravity of the situation since users can be griefed.
#4 - outdoteth
2022-07-15T10:30:22Z
PR with fix: https://github.com/outdoteth/putty-v2/pull/1
420.5209 USDC - $420.52
HIGH - assets can be compromised By creating an order which can be filled but not possible to exercise, the maker of this order is basically getting free premium. Even if the taker wants to exercise the long put option, it will revert. After the option is expired, the maker can withdraw strike. Malicious users can make attractive deals to lure people to take their orders then wait until it expires and takes back all strikes. This will also hurt the reputation of the platform.
It is similar case with Create an short call order with non empty floor makes the option impossible to exercise and withdraw
but the root cause is different, also the maker cannot withdraw after expiration.
The proof of concept shows a scenario, where babe makes a short put order with 0 tokenAmount. Bob filled the order and he has an NFT of long put.
When he calls the exercise, the putty contract tries to transfer ERC20s from bob to itself. However the _transferERC20sIn
function reverts, because it requires the tokenAmount to be greater than zero.
// When bob calls exercise // PuttyV2.sol: _transferERC20sIn function called by exercise // require(tokenAmount > 0, "ERC20: Amount too small"); function _transferERC20sIn(ERC20Asset[] memory assets, address from) internal { for (uint256 i = 0; i < assets.length; i++) { address token = assets[i].token; uint256 tokenAmount = assets[i].tokenAmount; require(token.code.length > 0, "ERC20: Token is not contract"); require(tokenAmount > 0, "ERC20: Amount too small"); ERC20(token).safeTransferFrom(from, address(this), tokenAmount); } }
Since bob can never exercise his option, babe does not need to worry about the price movement. She can also make attractive deals without worrying it will realize. Basically she is taking free premium.
After the option is expired, the maker of the order, babe, can now withdraw the strike (minus fees, based on the current implementation, but the fee should not be paid when not exercised - see the issue The fee is not paid as intended for put orders
).
Note:
foundry
The easy mitigation would be getting rid of the requirement: require(tokenAmount > 0, "ERC20: Amount too small");
Alternatively, ensure the orders to have non zero tokenAmount.
#0 - outdoteth
2022-07-07T13:41:41Z
Duplicate: Setting an erc20Asset with a zero amount or with no code at the address will result in a revert when exercising a put option: https://github.com/code-423n4/2022-06-putty-findings/issues/223
π 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/3b6b844bc39e897bd0bbb69897f2deff12dc3893/contracts/src/PuttyV2.sol#L268 https://github.com/code-423n4/2022-06-putty/blob/3b6b844bc39e897bd0bbb69897f2deff12dc3893/contracts/src/PuttyV2.sol#L389
HIGH - assets can be lost
When an user is calling fillOrder
or exercise
functions, if the user sent some ETH accidentally, the ETH can be lost.
In the proof of concept, babe creates a short put order. The baseAsset for this order is set to some erc20 other than WETH.
Bob calls fillOrder
but sends some ETH accidentally. All transaction passes without failing and Bob lost the ETH he sent in.
This can happen in both fillOrder
and exercise
. Below is a table for the possible cases (, but not tested).
Order type | function call | condition |
---|---|---|
long put | fillOrder | non weth baseAsset |
long call | fillOrder | |
short put | fillOrder | non weth baseAsset |
short call | fillOrder | non weth baseAsset |
long put | exercise | |
long call | exercise | non weth baseAsset |
foundry
The check for msg.value
should be added for cases, where user is not supposed to send any ETH in.
#0 - rotcivegaf
2022-07-04T23:48:56Z
Move to 2 (Med Risk)
label
Duplicate of #226
#1 - GalloDaSballo
2022-07-05T01:47:10Z
Why would the caller be so generous to send eth when they don't need to?
#2 - outdoteth
2022-07-06T19:28:38Z
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: 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
49.346 USDC - $49.35
Risk | Title |
---|---|
L01 | Cancel the same order will emit event again |
N01 | Usage of magic number |
OOS01 | Mismatch to the spec file |
OOS02 | The frontend error for minus values |
One can cancel the same order and emit CancelledOrder
again.
function cancel(Order memory order) public { require(msg.sender == order.maker, "Not your order"); bytes32 orderHash = hashOrder(order); // mark the order as cancelled cancelledOrders[orderHash] = true; emit CancelledOrder(orderHash, order); }
The precision of the fee, 1000
, can be stored as a constant for easy referring.
feeAmount = (order.strike * fee) / 1000;
// spec/withdraw.md input β *check position side is equal to short β βββββββββββββββββββββββββββββββ β β β order: Order ββββββββββββββββΊ *check position has expired OR it has been β β floorAssetTokeIds: number[] β β exercised β βββββββββββββββββββββββββββββββ β β
The spec for withdraw shows that the function takes two inputs: order
and floorAssetTokeIds
.
But the implementation only takes order
as an input.
function withdraw(Order memory order) public {
In the create order, the asset type NFT will allow to go to minus id. The asset type ERC20 will allow to go to minus values. The asset type Floor will error out when the token amount goes to minus. Disclaimer: Using firefox.