How To Do Partial Fills With The Seaport Protocol

Senudajayalath
Coinmonks
5 min readJan 2, 2023

--

A guide to enable partial fulfilment of orders when using the Seaport Protocol.

The Seaport Protocol is a new standard introduced by Opensea to be used as an accelerator for anyone trying to build a NFT marketplace on any EVM compatible chain. Partial fulfilment of orders is also a special feature that can be seen in most of the NFT marketplaces. In a partial fulfilment scenario, only a part of what is offered in the offer is bought by the buyer. The buyer in return only pays for what he/she buys. Although this functionality is not yet supported in the Opensea marketplace, the Seaport Protocol has been developed so that it can accommodate partial fills in the future. In this tutorial I will guide you on how to do partial fills using the main two functions fulfillAdvancedOrder and executeMatchAdvancedOrders in the Seaport Protocol.

Photo by Rob Wicks on Unsplash

Before we get into the code, it is important to know how Seaport distinguishes a partial fill enabled order from an order that is not. It all depends on the criteria that gets passed when the order is constructed. The official Seaport docs says the following.

The orderType designates one of four types for the order depending on two distinct preferences:

FULL indicates that the order does not support partial fills, whereas PARTIAL enables filling some fraction of the order, with the important caveat that each item must be cleanly divisible by the supplied fraction (i.e. no remainder after division).

OPEN indicates that the call to execute the order can be submitted by any account, whereas RESTRICTED requires that the order either be executed by the offerer or the zone of the order, or that a magic value indicating that the order is approved is returned upon calling an isValidOrder or isValidOrderIncludingExtraData view function on the zone.

According to the above paragraph, orderType can be either 0,1,2 or 3. By convention all the FULL orders will be either 0 or 2 (even), while all the PARTIAL orders will be either 1 or 3 (odd).

Now let us see how partial fills can be enabled for fixed price sales and auctions.

New to trading? Try crypto trading bots or copy trading on best crypto exchanges

Partial Fills for Fixed Price Sales

In a fixed price sale the price is fixed. Let us say that the seller wants to sell 10 copies of a certain ERC1155 token. Let us consider a situation where the buyer doesn't want to buy the whole lot, but is willing to buy a fraction of the tokens up for sale. In return the buyer will pay a fraction of the asking amount as well.

This is all possible because of the orderType variable and the concept of denominator and numerator in a Seaport order. Let us see how this works in more detail from the given code snippet.

  it("Seller creates an order", async () => {
const nftID = randomBN();
// conduitOne->updateChannel is called in seaportFixtures contructor
// So, marketplace.address has an opened channel to conduitOne
await testERC1155
.connect(seller)
.setMintApproval(conduitOne.address, true);

const offerOne = [
getTestItem1155(nftId, toBN(10), toBN(10), testERC1155.address, undefined),
];
const considerationOne = [
getItemETH(parseEther("10"), parseEther("10"), seller.address), // payment for seller
getItemETH(parseEther("10"), parseEther("10"), owner.address), // Platform fees
];

let { order, orderHash, value } = await createOrder(
seller,
zoneAddr,
offerOne,
considerationOne,
1, // PARTIAL_OPEN
[],
null,
seller,
ethers.constants.HashZero,
conduitKeyOne
);

orderSeller = order;
valueSeller = value;
orderHashSeller = orderHash;
});

it("Buyer executes the Order", async () => {
let balanceBeforeTransaction = await provider.getBalance(seller.address)

orderSeller.denominator = 10;
orderSeller.numerator = 3;

const tx = await marketplaceContract
.connect(buyer) // Buyer sign and send the seller's order to the seaport contract
.fulfillAdvancedOrder(
orderSeller,
[],
toKey(0),
ethers.constants.AddressZero,
{
value:valueSeller,
}
);
const receipt = await tx.wait();

let tokenBalanceBuyer = await testERC1155.balanceOf(buyer.address, nftId)
let ethBalance = await provider.getBalance(seller.address)
assert(tokenBalanceBuyer.eq(toBN(3)), "Tx did not happen")
assert(ethBalance.eq(balanceBeforeTransaction.add(parseEther("3"))), "Eth transfer did not happen")
});

As you can see in the above code, the orderType is set to 1 (i.e partial open). Another thing to note is how the buyer has changed the numerator and denominator of the order. Since the buyer is willing to buy only 3 copies of that given NFT, the buyer has set the denominator to 10 and the numerator to 3, meaning that he is willing to buy 3/10 of the offer amount and is willing to pay 3/10 of the consideration amount. It is important to note that changing the numerator and the denominator will not affect the validity of the order. Another important point to note is that if the numerator and denominator are not equal (as in the above case), you can no longer use the fulfilOrder function. Instead you will have to use the fulfilAdvancedOrder function.

Partial Fills for Auctions

    let orderSeller: any, orderHashSeller: any, valueSeller: BigNumber;
let orderBuyer: any, orderHashBuyer: any, valueBuyer: BigNumber;
const nftId = randomBN();
let fulfillments: any;
it("Seller Creates an Order", async () => {
const offerSeller = [
getTestItem1155(nftId, toBN(10), toBN(10), undefined, testERC1155.address),
];
const considerationSeller = [
getTestItem20(50, 50, seller.address), // Payment to seller
getTestItem20(10, 10, admin_Acc.address), // Platform charges

];
let { order, orderHash, value } = await createOrder(
seller,
zoneAddr,
offerSeller,
considerationSeller,
3, // PARTIAL_CLOSE
);
orderSeller = order;
valueSeller = value;
});

it("Buyer_1 makes a successfull offer", async () => {
await mintAndApproveERC20(buyer_1, marketplaceContract.address, 70);
const offerBuyer1 = [getTestItem20(60, 60), getTestItem20(10, 10),];
const considerationBuyer1 = [
getTestItem1155(
nftId,
toBN(6),
toBN(6),
buyer_1.address,
testERC1155.address
),
];
let { order, orderHash, value } = await createOrder(
buyer_1,
zoneAddr,
offerBuyer1,
considerationBuyer1,
3, // PARTIAL_CLOSE
);
orderBuyer = order;
valueBuyer = value;

// CHECK to see if this is the highest bid
// If so, add the amount to consideration
orderSeller.parameters.consideration.push(
getTestItem20(10, 10,seller.address)
)
});

it("Buyer_2 makes successfull offer", async () => {
await mintAndApproveERC20(buyer_2, marketplaceContract.address,90);
const offerBuyer2 = [getTestItem20(80, 80), getTestItem20(10, 10)];
const considerationBuyer2 = [
getTestItem1155(
nftId,
toBN(6),
toBN(6),
buyer_2.address,
testERC1155.address
),
];
let { order, orderHash, value } = await createOrder(
buyer_2,
zoneAddr,
offerBuyer2,
considerationBuyer2,
3, // PARTIAL_CLOSE
);
orderBuyer = order;
valueBuyer = value;

// CHECK to see if this is the highest bid
// If so, add the amount to consideration

orderSeller.parameters.consideration.pop()
orderSeller.parameters.consideration.push(
getTestItem20(30, 30,seller.address)
)
});

// Before executing the backend should make the correct match and provide the two orders to be matched
it("Executes Order", async () => {

orderSeller.denominator = 10;
orderSeller.numerator = 6;

orderBuyer.denominator = 10;
orderBuyer.numerator = 6;

await testERC1155
.connect(seller)
.setMintApproval(marketplaceContract.address, true);

fulfillments = [
[[[0, 0]], [[1, 0]]], // nft
[[[1, 1]], [[0, 1]]], // platform fees
[[[1, 0]], [[0, 0]]], //floor price
[[[1, 0]], [[0, 2]]], // price increase to floor price
].map(([offerArr, considerationArr]) =>
toFulfillment(offerArr, considerationArr)
);

const tx = await zoneContract
.connect(business_Acc)
.executeMatchAdvancedOrders(
marketplaceContract.address,
[orderSeller, orderBuyer],
fulfillments
);
const events = await decodeEvents(tx, [
{ eventName: "Test1", contract: testERC1155 },
])
const receipt = await tx.wait();

assert(await testERC1155.ownerOf(nftId) == buyer_2.address, "Tx did not happen")
});

Something important to note here is how the numerator and denominator of both the sellorder and the buyorder has to be adjusted according to the fraction the buyer is willing to buy. Like in the previous scenario, the function executeMatchOrders cannot be used. Instead the function executeMatchAdvancedorders has to be used.

NOTE —

It is important to note that using any fraction is not practical when it comes to partial fills. The consideration as well as the offer amount should be perfectly divisible by the fraction chosen. Otherwise the order will not get executed and an exception will be thrown.

Happy Coding !!

References

  1. Official Seaport Docs — https://docs.opensea.io/v2.0/reference/seaport-overview
  2. Seaport Github — https://github.com/ProjectOpenSea/seaport

--

--