Putty contest - zzzitron's results

An order-book based american options market for NFTs and ERC20s.

General Information

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

Putty

Findings Distribution

Researcher Performance

Rank: 7/133

Findings: 5

Award: $1,816.25

🌟 Selected for report: 1

πŸš€ Solo Findings: 0

Findings Information

🌟 Selected for report: zishansami

Also found by: 0x52, 0xsanson, auditor0517, berndartmueller, csanuragjain, zzzitron

Labels

bug
duplicate
3 (High Risk)
old-submission-method

Awards

583.9233 USDC - $583.92

External Links

Lines of code

https://github.com/code-423n4/2022-06-putty/blob/3b6b844bc39e897bd0bbb69897f2deff12dc3893/contracts/src/PuttyV2.sol#L135

Vulnerability details

Impact

MEDIUM - functions of the protocol could be impacted

For put options, the fees are not paid as intended.

Proof of Concept

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;
        }

Tools Used

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

Findings Information

🌟 Selected for report: zzzitron

Also found by: Kenshin, Metatron, PwnedNoMore, danb, minhquanym

Labels

bug
3 (High Risk)
resolved
sponsor confirmed
old-submission-method

Awards

756.9377 USDC - $756.94

External Links

Lines of code

https://github.com/code-423n4/2022-06-putty/blob/3b6b844bc39e897bd0bbb69897f2deff12dc3893/contracts/src/PuttyV2.sol#L296-L298

Vulnerability details

Impact

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.

Proof of Concept

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.

  • case 1: he calls exercise with empty floorAssetTokenIds array
  • case 2: he calls exercise with non-empty floorAssetTokenIds 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:

  • The test for case1 is commented out because foundry could not catch the revert. But by running the test with un-commenting these lines will show that the call reverts with Index out of bounds.
  • For the same reason the withdraw also is commented out
  • The reference case just shows that it works as intended when the order does not contain non-empty floorTokens.

Tools Used

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

Findings Information

🌟 Selected for report: IllIllI

Also found by: 0xNineDec, exd0tpy, zzzitron

Labels

bug
duplicate
2 (Med Risk)
disagree with severity
old-submission-method

Awards

420.5209 USDC - $420.52

External Links

Lines of code

https://github.com/code-423n4/2022-06-putty/blob/3b6b844bc39e897bd0bbb69897f2deff12dc3893/contracts/src/PuttyV2.sol#L599

Vulnerability details

Impact

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.

Proof of Concept

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:

Tools Used

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

Awards

5.5216 USDC - $5.52

Labels

bug
duplicate
2 (Med Risk)
disagree with severity
old-submission-method

External Links

Lines of code

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

Vulnerability details

Impact

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.

Proof of Concept

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 typefunction callcondition
long putfillOrdernon weth baseAsset
long callfillOrder
short putfillOrdernon weth baseAsset
short callfillOrdernon weth baseAsset
long putexercise
long callexercisenon weth baseAsset

Tools Used

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

PuttyV2 QA Report

Summary / Impression

  • The code is clean and well documented. It was enjoyable to work on.
RiskTitle
L01Cancel the same order will emit event again
N01Usage of magic number
OOS01Mismatch to the spec file
OOS02The frontend error for minus values

Low

L01: Cancel the same order will emit event again

github link

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);
    }

Non critical

N01: Usage of magic number

github link

The precision of the fee, 1000, can be stored as a constant for easy referring.

 feeAmount = (order.strike * fee) / 1000;

Out Of Scope

OOS01: Mismatch to the spec file

github link

// 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 {

OOS02: The frontend error for minus values

link

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.

AuditHub

A portfolio for auditors, a security profile for protocols, a hub for web3 security.

Built bymalatrax Β© 2024

Auditors

Browse

Contests

Browse

Get in touch

ContactTwitter