[WP-H0] Unsafe type casting for the order's `denominator` and `numerator` may allow the attacker to buy more than the offered amount from the seller against the seller's will #149
When an order is filled partially, the order's denominator and numerator will be updated in OrderValidator.sol#_validateOrderAndUpdateStatus based on the previous filledNumerator and filledDenominator, and the numerator and denominator of the current fulfill order.
However, in the current implementation, both the order's denominator and numerator in the storage and the buyer's inputs will be converted to uint256 and multiply to the new values.
// the supplied numerator & denominator by current denominator.
numerator *= filledDenominator;
denominator *= filledDenominator;
This makes it possible for the new values to exceed type(uint120).max, and later when updating order status and fill amount, they will be cast to uint120 unsafely.
A sophisticated attacker can send buy orders with specially designed values for denominator and numerator to manipulate the fill amount of the seller's order, then use unsafe type casting to reset the denominator and numerator to 0 and buy the entire offered amount again, effectively buying more against the seller's will.
Impact
One of the most important requirements of a marketplace protocol is that it MUST ensure that orders are fulfilled at the agreed-upon exchange rate (aka price) for an agreed-upon amount (quantity).
To put it another way, if the seller created an order to sell 3 apples for $1 each, the marketplace protocol MUST NOT accept a buy order for 5 cents per apple, nor a buy order for $1 million to buy 1 million apples. Even if there are 1 million apples in the seller's warehouse and the seller did give the key to the marketplace.
As a result, we believe that allowing the buyer to buy more than the offered amount is inherently wrong, and that it "places most or all user funds at risk," where the funds, in this case, are the assets that sit in the seller's wallet and have been approved to the protocol, can be bought and transferred to the buyer/attacker against the seller's will.
The impacted scenarios can be quite common in practice: "flash sale" is very popular among online marketplaces, where sellers sell products at a discounted price for a very limited quantity.
If a seller uses the seaport protocol to host a similar "flash sale" for their tokens and/or NFTs, the financial damage from such an attack can be severe.
Lines of code
https://github.com/code-423n4/2022-05-opensea-seaport/blob/4140473b1f85d0df602548ad260b1739ddd734a5/contracts/lib/OrderValidator.sol#L228-L231
Vulnerability details
When an order is filled partially, the order's
denominator
andnumerator
will be updated inOrderValidator.sol#_validateOrderAndUpdateStatus
based on the previousfilledNumerator
andfilledDenominator
, and thenumerator
anddenominator
of the current fulfill order.However, in the current implementation, both the order's
denominator
andnumerator
in the storage and the buyer's inputs will be converted touint256
and multiply to the new values.https://github.com/code-423n4/2022-05-opensea-seaport/blob/4140473b1f85d0df602548ad260b1739ddd734a5/contracts/lib/OrderValidator.sol#L209-L211
This makes it possible for the new values to exceed
type(uint120).max
, and later when updating order status and fill amount, they will be cast touint120
unsafely.A sophisticated attacker can send buy orders with specially designed values for
denominator
andnumerator
to manipulate the fill amount of the seller's order, then use unsafe type casting to reset thedenominator
andnumerator
to0
and buy the entire offered amount again, effectively buying more against the seller's will.Impact
One of the most important requirements of a marketplace protocol is that it MUST ensure that orders are fulfilled at the agreed-upon exchange rate (aka price) for an agreed-upon amount (quantity).
To put it another way, if the seller created an order to sell 3 apples for $1 each, the marketplace protocol MUST NOT accept a buy order for 5 cents per apple, nor a buy order for $1 million to buy 1 million apples. Even if there are 1 million apples in the seller's warehouse and the seller did give the key to the marketplace.
As a result, we believe that allowing the buyer to buy more than the offered amount is inherently wrong, and that it "places most or all user funds at risk," where the funds, in this case, are the assets that sit in the seller's wallet and have been approved to the protocol, can be bought and transferred to the buyer/attacker against the seller's will.
The impacted scenarios can be quite common in practice: "flash sale" is very popular among online marketplaces, where sellers sell products at a discounted price for a very limited quantity.
If a seller uses the seaport protocol to host a similar "flash sale" for their tokens and/or NFTs, the financial damage from such an attack can be severe.
PoC
Given:
PARTIAL_OPEN
100
The attacker can:
order.numerator =
2**117
order.denominator =
2**119
the attacker successfully bought
25
Updated orderStatus:
166153499473114484112975882535043072
664613997892457936451903530140172288
order.numerator =
4
order.denominator =
16
the attacker successfully bought
25
, total bought amount is now:50
orderStatus.numerator =
0
orderStatus.denominator =
0
This is the test script we wrote for this proof of concept:
Recommendation
Consider removing the casting from
uint120
touint256
before the multiply, making the transaction revert when the multiply overflows.