Platform: Code4rena
Start Date: 08/01/2024
Pot Size: $83,600 USDC
Total HM: 23
Participants: 116
Period: 10 days
Judge: 0xean
Total Solo HM: 1
Id: 317
League: ETH
Rank: 7/116
Findings: 2
Award: $2,601.84
π Selected for report: 0
π Solo Findings: 0
π Selected for report: juancito
Also found by: DeFiHackLabs
2599.1195 USDC - $2,599.12
If the order is Base or Pay, ESCRW will increase the deposit.
// PAYEE orders are considered mirror-images of a PAY order. So, PAYEE orders // do not need to be processed in the same way that other order types do. if ( payload.metadata.orderType.isBaseOrder() || payload.metadata.orderType.isPayOrder() ) { /* .... */ // Interaction: Update storage only if the order is a Base Order or Pay order. STORE.addRentals(orderHash, _convertToStatic(rentalAssetUpdates)); // Interaction: Increase the deposit value on the payment escrow so // it knows how many tokens were sent to it. for (uint256 i = 0; i < items.length; ++i) { if (items[i].isERC20()) { ESCRW.increaseDeposit(items[i].token, items[i].amount); } }
The way Seaport matches orders is as follows: Calling one of two "match" functions, matchOrders and matchAdvancedOrders, where a group of explicit orders are supplied alongside a group of fulfillments specifying which offer items to apply to which consideration itemsγ Note that orders fulfilled in this manner do not have an explicit fulfiller; instead, Seaport will simply ensure coincidence of wants across each order. Consider the following scenario:
Base | Pay | Payee 1 | Payee 2 | |
---|---|---|---|---|
Offer items | Nft A | NFT B and 15 ERC20 | none | none |
Consideration items | 10 ERC20 | none | Nft A and 2 ERC20 | Nft B and 3 ERC20 |
The PAY order rented from the BASE order and corresponds with two PAYEE orders. BASE order increases 10 in escrow. Pay order increases 15 in escrow. In this case, PAY only gives 15 to escrow. The escrow's accounting will be incorrect. This would cause a loss for the escrow, and in the worst-case scenario, it could lead to the escrow being completely depleted.
Context
#0 - c4-pre-sort
2024-01-21T19:07:39Z
141345 marked the issue as sufficient quality report
#1 - c4-pre-sort
2024-01-22T05:22:13Z
141345 marked the issue as duplicate of #387
#2 - c4-judge
2024-01-28T19:05:56Z
0xean marked the issue as satisfactory
#3 - xuwinnie
2024-01-30T14:40:56Z
Hi, I believe this issue is different from #387. #387 basically says if offerer and receipent is the same one, the transfer will not be included in totalExecution and execution invariant check is bypassed. And this issue does not bypass execution invariant check, actually all the execution satisfies the check, the root cause is another accounting logic error, when a base order and pay order co-exists, certain payment will be counted twice. Even if #387 is fixed, this issue will still exist. Thanks!
#4 - stalinMacias
2024-01-31T03:40:20Z
Both issues are actually super similar and have the same exact root cause, that is, relying on accounting errors when utilizing these types of orders. #387 actually demonstrates the actual impact and vuln much better that is, exploiting the accounting error to drain the escrow while this one just mentions the fact that it potentially can be drained
#387 basically says if offerer and receipent is the same one
Yes, the attacker is exploiting this bug by creating and lending himself a malicious order to cause the accounting logic error and drain the payment escrow
Both bugs are very similar in nature: they rely on matching pay & payee orders to exploit an accounting error. If #387 is fixed, this one will be too.
That's the only description of the issue provided in the report
The PAY order rented from the BASE order and corresponds with two PAYEE orders. BASE order increases 10 in escrow. Pay order increases 15 in escrow. In this case, PAY only gives 15 to escrow. The escrow's accounting will be incorrect. This would cause a loss for the escrow, and in the worst-case scenario, it could lead to the escrow being completely depleted.
The description of this bug in this report is extremely shallow and it doesn't seem to pinpoint where the issue is, I don't know exactly where is the vulnerability here. There is also no PoC, unlike #387.
#5 - xuwinnie
2024-01-31T04:00:46Z
Both issues are actually super similar and have the same exact root cause, that is, relying on accounting errors when utilizing these types of orders. #387 actually demonstrates the actual impact better that is, exploiting the accounting error to drain the escrow while this one just mentions the fact that it potentially can be drained
#387 basically says if offerer and receipent is the same one
Yes, the attacker is exploiting this bug by creating and lending himself a malicious order to cause the accounting logic error and drain the payment escrow
Both bugs are very similar in nature: they rely on matching pay & payee orders to exploit an accounting error. If #387 is fixed, this one will be too.
#6 - xuwinnie
2024-01-31T04:42:58Z
The description of this bug in this report is extremely shallow and it doesn't seem to pinpoint where the issue is, I don't know exactly where is the vulnerability here. There is also no PoC, unlike #387.
The issue is, escrow.only receives 15 unit token, but behaves as if it had received 25 unit.
π Selected for report: juancito
Also found by: DeFiHackLabs
2599.1195 USDC - $2,599.12
PAY order have at least one ERC721/ERC1155, at least one ERC20. These orders are paired with PAYEE orders. PAYEE order have at least one ERC721/ERC1155, at least one ERC20. In Create.sol, when utilizing validateOrder(), the execution of _rentFromZone initiates a rental order.
function validateOrder( ZoneParameters calldata zoneParams ) external override onlyRole("SEAPORT") returns (bytes4 validOrderMagicValue) { /* ... */ // Create a payload of seaport data. SeaportPayload memory seaportPayload = SeaportPayload({ orderHash: zoneParams.orderHash, zoneHash: zoneParams.zoneHash, offer: zoneParams.offer, consideration: zoneParams.consideration, totalExecutions: zoneParams.totalExecutions, fulfiller: zoneParams.fulfiller, offerer: zoneParams.offerer }); // Check: The signature from the protocol signer has not expired. _validateProtocolSignatureExpiration(payload.expiration); // Check: The fulfiller is the intended fulfiller. _validateFulfiller(payload.intendedFulfiller, seaportPayload.fulfiller); // Recover the signer from the payload. address signer = _recoverSignerFromPayload( _deriveRentPayloadHash(payload), signature ); // Check: The data matches the signature and that the protocol signer is the one that signed. if (!kernel.hasRole(signer, toRole("CREATE_SIGNER"))) { revert Errors.CreatePolicy_UnauthorizedCreatePolicySigner(); } // Initiate the rental using the rental manager. _rentFromZone(payload, seaportPayload); /* ... */ }
Create a PAYEE order, where the ERC20 is identical to the PAY order. And set the recipient to be the same as the offerer. Can pass _executionInvariantChecks.
function _executionInvariantChecks( ReceivedItem[] memory executions, address expectedRentalSafe ) internal view { for (uint256 i = 0; i < executions.length; ++i) { ReceivedItem memory execution = executions[i]; // ERC20 invariant where the recipient must be the payment escrow. if (execution.isERC20()) { _checkExpectedRecipient(execution, address(ESCRW)); } // ERC721 and ERC1155 invariants where the recipient must // be the expected rental safe. else if (execution.isRental()) { _checkExpectedRecipient(execution, expectedRentalSafe); } // Revert if unsupported item type. else { revert Errors.CreatePolicy_SeaportItemTypeNotSupported( execution.itemType ); } } }
In OrderCombiner.sol, _isFilterableExecution() returns true when the offerer and recipient have the same address.
function _isFilterableExecution(Execution memory execution) internal pure returns (bool filterable) { // Utilize assembly to efficiently determine if execution is filterable. assembly { // Retrieve the received item referenced by the execution. let item := mload(execution) // Determine whether the execution is filterable. filterable := and( // Determine if offerer and recipient are the same address. eq( // Retrieve the recipient's address from the received item. mload(add(item, ReceivedItem_recipient_offset)), // Retrieve the offerer's address from the execution. mload(add(execution, Execution_offerer_offset)) ), // Determine if received item's item type is non-zero, thereby // indicating that the execution does not involve native tokens. iszero(iszero(mload(item))) ) } }
The renter of the pay order can retrieve the escrowed rent, and the lender of the payee can borrow the NFT without paying any ERC20 tokens.
In the worst-case scenario, it could lead to the depletion of the escrow.
Context
#0 - c4-pre-sort
2024-01-21T19:07:06Z
141345 marked the issue as sufficient quality report
#1 - c4-pre-sort
2024-01-23T05:46:08Z
141345 marked the issue as duplicate of #387
#2 - c4-judge
2024-01-28T19:05:09Z
0xean changed the severity to 3 (High Risk)
#3 - c4-judge
2024-01-28T22:50:36Z
0xean marked the issue as satisfactory
π Selected for report: juancito
Also found by: DeFiHackLabs
2599.1195 USDC - $2,599.12
PAY order have at least one ERC721/ERC1155, at least one ERC20. These orders are paired with PAYEE orders. PAYEE order have at least one ERC721/ERC1155, at least one ERC20. In Create.sol, when utilizing validateOrder(), the execution of _rentFromZone initiates a rental order.
function validateOrder( ZoneParameters calldata zoneParams ) external override onlyRole("SEAPORT") returns (bytes4 validOrderMagicValue) { /* ... */ // Create a payload of seaport data. SeaportPayload memory seaportPayload = SeaportPayload({ orderHash: zoneParams.orderHash, zoneHash: zoneParams.zoneHash, offer: zoneParams.offer, consideration: zoneParams.consideration, totalExecutions: zoneParams.totalExecutions, fulfiller: zoneParams.fulfiller, offerer: zoneParams.offerer }); // Check: The signature from the protocol signer has not expired. _validateProtocolSignatureExpiration(payload.expiration); // Check: The fulfiller is the intended fulfiller. _validateFulfiller(payload.intendedFulfiller, seaportPayload.fulfiller); // Recover the signer from the payload. address signer = _recoverSignerFromPayload( _deriveRentPayloadHash(payload), signature ); // Check: The data matches the signature and that the protocol signer is the one that signed. if (!kernel.hasRole(signer, toRole("CREATE_SIGNER"))) { revert Errors.CreatePolicy_UnauthorizedCreatePolicySigner(); } // Initiate the rental using the rental manager. _rentFromZone(payload, seaportPayload); /* ... */ }
Create a PAYEE order, where the ERC721 is identical to the PAY order. And set the recipient to be the same as the offerer. Can pass _executionInvariantChecks.
function _executionInvariantChecks( ReceivedItem[] memory executions, address expectedRentalSafe ) internal view { for (uint256 i = 0; i < executions.length; ++i) { ReceivedItem memory execution = executions[i]; // ERC20 invariant where the recipient must be the payment escrow. if (execution.isERC20()) { _checkExpectedRecipient(execution, address(ESCRW)); } // ERC721 and ERC1155 invariants where the recipient must // be the expected rental safe. else if (execution.isRental()) { _checkExpectedRecipient(execution, expectedRentalSafe); } // Revert if unsupported item type. else { revert Errors.CreatePolicy_SeaportItemTypeNotSupported( execution.itemType ); } } }
In OrderCombiner.sol, _isFilterableExecution() returns true when the offerer and recipient have the same address.
function _isFilterableExecution(Execution memory execution) internal pure returns (bool filterable) { // Utilize assembly to efficiently determine if execution is filterable. assembly { // Retrieve the received item referenced by the execution. let item := mload(execution) // Determine whether the execution is filterable. filterable := and( // Determine if offerer and recipient are the same address. eq( // Retrieve the recipient's address from the received item. mload(add(item, ReceivedItem_recipient_offset)), // Retrieve the offerer's address from the execution. mload(add(execution, Execution_offerer_offset)) ), // Determine if received item's item type is non-zero, thereby // indicating that the execution does not involve native tokens. iszero(iszero(mload(item))) ) } }
The lender of the payee order didn't successfully borrow the NFT but still made the ERC20 payment for the pay order.
Context
#0 - c4-pre-sort
2024-01-21T19:06:52Z
141345 marked the issue as sufficient quality report
#1 - c4-pre-sort
2024-01-23T05:46:34Z
141345 marked the issue as duplicate of #387
#2 - c4-judge
2024-01-28T19:04:53Z
0xean marked the issue as satisfactory
#3 - c4-judge
2024-01-28T19:05:10Z
0xean changed the severity to 3 (High Risk)
π Selected for report: stackachu
Also found by: 0xHelium, 0xabhay, 0xc695, 0xpiken, DeFiHackLabs, EV_om, HSP, J4X, Krace, KupiaSec, Qkite, ZanyBonzy, albertwh1te, cccz, evmboi32, hals, hash, holydevoti0n, krikolkk, ladboy233, lanrebayode77, marqymarq10, oakcobalt, peanuts, peter, rbserver, said, serial-coder, sin1st3r__
2.7205 USDC - $2.72
https://github.com/re-nft/smart-contracts/blob/3ddd32455a849c3c6dc3c3aad7a33a6c9b44c291/src/modules/PaymentEscrow.sol#L175 https://github.com/re-nft/smart-contracts/blob/3ddd32455a849c3c6dc3c3aad7a33a6c9b44c291/src/modules/PaymentEscrow.sol#L178
In case of pay and payee order, lender pays the renter. if the the transfer fails, lenders cannot call stopOrders to retrieve their NFT. There are two scenarios:
function stopRent(RentalOrder calldata order) external { /* ...*/ // Interaction: Transfer rentals from the renter back to lender. _reclaimRentedItems(order); // Interaction: Transfer ERC20 payments from the escrow contract to the respective recipients. ESCRW.settlePayment(order); /* ...*/ // Emit rental order stopped. _emitRentalOrderStopped(order.seaportOrderHash, msg.sender); }
function _safeTransfer(address token, address to, uint256 value) internal {// Call transfer() on the token.(bool success, bytes memory data) = token.call( abi.encodeWithSelector(IERC20.transfer.selector, to, value));/.../ if (!success || (data.length != 0 && !abi.decode(data, (bool)))) { revert Errors.PaymentEscrowModule_PaymentTransferFailed(token, to, value);}}
https://github.com/re-nft/smart-contracts/blob/3ddd32455a849c3c6dc3c3aad7a33a6c9b44c291/src/modules/PaymentEscrow.sol#L175 https://github.com/re-nft/smart-contracts/blob/3ddd32455a849c3c6dc3c3aad7a33a6c9b44c291/src/modules/PaymentEscrow.sol#L178
Token-Transfer
#0 - c4-pre-sort
2024-01-21T17:36:09Z
141345 marked the issue as duplicate of #64
#1 - c4-judge
2024-01-28T21:00:55Z
0xean marked the issue as satisfactory