Skip to content

Commit 81a6a1e

Browse files
authored
BTT tests: Direct Listings (#546)
* tree file: buyFromListing * tree file: createListing * tree file: updateListing * tree file: cancelListing * tree file: approveCurrencyForListing * tree file: approveBuyerForListing * tree file: _validateOwnershipAndApproval * tree file: _validateNewListing * tree file: _validateERC20BalAndAllowance * _transferListingTokens * tree file: _payout * btt tests: createListing * _validateNewListing WIP tests * _validateNewListing WIP tests 2 * btt tests: _validateNewListing * btt tests: _validateOwnershipAndApproval * btt tests: updateListing * Mark validateOwnershipAndApproval tree as complete * btt tests: cancelListing * btt tests: _validateERC20BalAndAllowance * btt tests: transferListingTokens * btt tests: approveCurrencyForListing * btt tests: approveBuyerForListing * btt tests: payout * btt tests: buyFromListing
1 parent 987c765 commit 81a6a1e

22 files changed

+3754
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,343 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
pragma solidity ^0.8.0;
3+
4+
import "../../../utils/BaseTest.sol";
5+
import "@thirdweb-dev/dynamic-contracts/src/interface/IExtension.sol";
6+
7+
import { RoyaltyPaymentsLogic } from "contracts/extension/plugin/RoyaltyPayments.sol";
8+
import { PlatformFee } from "contracts/extension/PlatformFee.sol";
9+
import { TWProxy } from "contracts/infra/TWProxy.sol";
10+
import { MarketplaceV3 } from "contracts/prebuilts/marketplace/entrypoint/MarketplaceV3.sol";
11+
import { DirectListingsLogic } from "contracts/prebuilts/marketplace/direct-listings/DirectListingsLogic.sol";
12+
import { IDirectListings } from "contracts/prebuilts/marketplace/IMarketplace.sol";
13+
import { MockRoyaltyEngineV1 } from "../../../mocks/MockRoyaltyEngineV1.sol";
14+
15+
contract PayoutTest is BaseTest, IExtension {
16+
// Target contract
17+
address public marketplace;
18+
19+
// Participants
20+
address public marketplaceDeployer;
21+
address public seller;
22+
address public buyer;
23+
24+
// Default listing parameters
25+
IDirectListings.ListingParameters internal listingParams;
26+
uint256 internal listingId = 0;
27+
28+
// Events to test
29+
30+
/// @notice Emitted when a listing is updated.
31+
event UpdatedListing(
32+
address indexed listingCreator,
33+
uint256 indexed listingId,
34+
address indexed assetContract,
35+
IDirectListings.Listing listing
36+
);
37+
38+
function setUp() public override {
39+
super.setUp();
40+
41+
marketplaceDeployer = getActor(1);
42+
seller = getActor(2);
43+
buyer = getActor(3);
44+
45+
// Deploy implementation.
46+
Extension[] memory extensions = _setupExtensions();
47+
address impl = address(
48+
new MarketplaceV3(MarketplaceV3.MarketplaceConstructorParams(extensions, address(0), address(weth)))
49+
);
50+
51+
vm.prank(marketplaceDeployer);
52+
marketplace = address(
53+
new TWProxy(
54+
impl,
55+
abi.encodeCall(
56+
MarketplaceV3.initialize,
57+
(marketplaceDeployer, "", new address[](0), platformFeeRecipient, uint16(platformFeeBps))
58+
)
59+
)
60+
);
61+
62+
// Setup listing params
63+
address assetContract = address(erc721);
64+
uint256 tokenId = 0;
65+
uint256 quantity = 1;
66+
address currency = address(erc20);
67+
uint256 pricePerToken = 1 ether;
68+
uint128 startTimestamp = 100 minutes;
69+
uint128 endTimestamp = 200 minutes;
70+
bool reserved = false;
71+
72+
listingParams = IDirectListings.ListingParameters(
73+
assetContract,
74+
tokenId,
75+
quantity,
76+
currency,
77+
pricePerToken,
78+
startTimestamp,
79+
endTimestamp,
80+
reserved
81+
);
82+
83+
// Mint 1 ERC721 NFT to seller
84+
erc721.mint(seller, listingParams.quantity);
85+
86+
vm.label(impl, "MarketplaceV3_Impl");
87+
vm.label(marketplace, "Marketplace");
88+
vm.label(seller, "Seller");
89+
vm.label(address(erc721), "ERC721_Token");
90+
vm.label(address(erc1155), "ERC1155_Token");
91+
}
92+
93+
function _setupExtensions() internal returns (Extension[] memory extensions) {
94+
extensions = new Extension[](1);
95+
96+
// Deploy `DirectListings`
97+
address directListings = address(new DirectListingsLogic(address(weth)));
98+
vm.label(directListings, "DirectListings_Extension");
99+
100+
// Extension: DirectListingsLogic
101+
Extension memory extension_directListings;
102+
extension_directListings.metadata = ExtensionMetadata({
103+
name: "DirectListingsLogic",
104+
metadataURI: "ipfs://DirectListings",
105+
implementation: directListings
106+
});
107+
108+
extension_directListings.functions = new ExtensionFunction[](13);
109+
extension_directListings.functions[0] = ExtensionFunction(
110+
DirectListingsLogic.totalListings.selector,
111+
"totalListings()"
112+
);
113+
extension_directListings.functions[1] = ExtensionFunction(
114+
DirectListingsLogic.isBuyerApprovedForListing.selector,
115+
"isBuyerApprovedForListing(uint256,address)"
116+
);
117+
extension_directListings.functions[2] = ExtensionFunction(
118+
DirectListingsLogic.isCurrencyApprovedForListing.selector,
119+
"isCurrencyApprovedForListing(uint256,address)"
120+
);
121+
extension_directListings.functions[3] = ExtensionFunction(
122+
DirectListingsLogic.currencyPriceForListing.selector,
123+
"currencyPriceForListing(uint256,address)"
124+
);
125+
extension_directListings.functions[4] = ExtensionFunction(
126+
DirectListingsLogic.createListing.selector,
127+
"createListing((address,uint256,uint256,address,uint256,uint128,uint128,bool))"
128+
);
129+
extension_directListings.functions[5] = ExtensionFunction(
130+
DirectListingsLogic.updateListing.selector,
131+
"updateListing(uint256,(address,uint256,uint256,address,uint256,uint128,uint128,bool))"
132+
);
133+
extension_directListings.functions[6] = ExtensionFunction(
134+
DirectListingsLogic.cancelListing.selector,
135+
"cancelListing(uint256)"
136+
);
137+
extension_directListings.functions[7] = ExtensionFunction(
138+
DirectListingsLogic.approveBuyerForListing.selector,
139+
"approveBuyerForListing(uint256,address,bool)"
140+
);
141+
extension_directListings.functions[8] = ExtensionFunction(
142+
DirectListingsLogic.approveCurrencyForListing.selector,
143+
"approveCurrencyForListing(uint256,address,uint256)"
144+
);
145+
extension_directListings.functions[9] = ExtensionFunction(
146+
DirectListingsLogic.buyFromListing.selector,
147+
"buyFromListing(uint256,address,uint256,address,uint256)"
148+
);
149+
extension_directListings.functions[10] = ExtensionFunction(
150+
DirectListingsLogic.getAllListings.selector,
151+
"getAllListings(uint256,uint256)"
152+
);
153+
extension_directListings.functions[11] = ExtensionFunction(
154+
DirectListingsLogic.getAllValidListings.selector,
155+
"getAllValidListings(uint256,uint256)"
156+
);
157+
extension_directListings.functions[12] = ExtensionFunction(
158+
DirectListingsLogic.getListing.selector,
159+
"getListing(uint256)"
160+
);
161+
162+
extensions[0] = extension_directListings;
163+
}
164+
165+
address payable[] internal mockRecipients;
166+
uint256[] internal mockAmounts;
167+
MockRoyaltyEngineV1 internal royaltyEngine;
168+
169+
function _setupRoyaltyEngine() private {
170+
mockRecipients.push(payable(address(0x12345)));
171+
mockRecipients.push(payable(address(0x56789)));
172+
173+
mockAmounts.push(10 ether);
174+
mockAmounts.push(15 ether);
175+
176+
royaltyEngine = new MockRoyaltyEngineV1(mockRecipients, mockAmounts);
177+
}
178+
179+
function _setupListingForRoyaltyTests(address erc721TokenAddress) private returns (uint256 _listingId) {
180+
// Sample listing parameters.
181+
address assetContract = erc721TokenAddress;
182+
uint256 tokenId = 0;
183+
uint256 quantity = 1;
184+
address currency = address(erc20);
185+
uint256 pricePerToken = 100 ether;
186+
uint128 startTimestamp = 100;
187+
uint128 endTimestamp = 200;
188+
bool reserved = false;
189+
190+
// Approve Marketplace to transfer token.
191+
vm.prank(seller);
192+
IERC721(erc721TokenAddress).setApprovalForAll(marketplace, true);
193+
194+
// List tokens.
195+
IDirectListings.ListingParameters memory listingParameters = IDirectListings.ListingParameters(
196+
assetContract,
197+
tokenId,
198+
quantity,
199+
currency,
200+
pricePerToken,
201+
startTimestamp,
202+
endTimestamp,
203+
reserved
204+
);
205+
206+
vm.prank(seller);
207+
_listingId = DirectListingsLogic(marketplace).createListing(listingParameters);
208+
}
209+
210+
function _buyFromListingForRoyaltyTests(uint256 _listingId) private returns (uint256 totalPrice) {
211+
IDirectListings.Listing memory listing = DirectListingsLogic(marketplace).getListing(_listingId);
212+
213+
address buyFor = buyer;
214+
uint256 quantityToBuy = listing.quantity;
215+
address currency = listing.currency;
216+
uint256 pricePerToken = listing.pricePerToken;
217+
totalPrice = pricePerToken * quantityToBuy;
218+
219+
// Mint requisite total price to buyer.
220+
erc20.mint(buyer, totalPrice);
221+
222+
// Approve marketplace to transfer currency
223+
vm.prank(buyer);
224+
erc20.increaseAllowance(marketplace, totalPrice);
225+
226+
// Buy tokens from listing.
227+
vm.warp(listing.startTimestamp);
228+
vm.prank(buyer);
229+
DirectListingsLogic(marketplace).buyFromListing(_listingId, buyFor, quantityToBuy, currency, totalPrice);
230+
}
231+
232+
function test_payout_whenZeroRoyaltyRecipients() public {
233+
// 1. ========= Create listing =========
234+
vm.startPrank(seller);
235+
erc721.setApprovalForAll(marketplace, true);
236+
listingId = DirectListingsLogic(marketplace).createListing(listingParams);
237+
vm.stopPrank();
238+
239+
// 2. ========= Buy from listing =========
240+
241+
uint256 totalPrice = listingParams.pricePerToken;
242+
243+
// Mint requisite total price to buyer.
244+
erc20.mint(buyer, totalPrice);
245+
246+
// Approve marketplace to transfer currency
247+
vm.prank(buyer);
248+
erc20.increaseAllowance(marketplace, totalPrice);
249+
250+
// Buy tokens from listing.
251+
vm.warp(listingParams.startTimestamp);
252+
vm.prank(buyer);
253+
DirectListingsLogic(marketplace).buyFromListing(
254+
listingId,
255+
buyer,
256+
listingParams.quantity,
257+
listingParams.currency,
258+
totalPrice
259+
);
260+
261+
// 3. ======== Check balances after royalty payments ========
262+
263+
uint256 platformFees = (totalPrice * platformFeeBps) / 10_000;
264+
265+
{
266+
// Platform fee recipient receives correct amount
267+
assertBalERC20Eq(address(erc20), platformFeeRecipient, platformFees);
268+
269+
// Seller gets total price minus royalty amounts
270+
assertBalERC20Eq(address(erc20), seller, totalPrice - platformFees);
271+
}
272+
}
273+
274+
modifier whenNonZeroRoyaltyRecipients() {
275+
_setupRoyaltyEngine();
276+
277+
// Add RoyaltyEngine to marketplace
278+
vm.prank(marketplaceDeployer);
279+
RoyaltyPaymentsLogic(marketplace).setRoyaltyEngine(address(royaltyEngine));
280+
281+
_;
282+
}
283+
284+
function test_payout_whenInsufficientFundsToPayRoyaltyAfterPlatformFeePayout() public whenNonZeroRoyaltyRecipients {
285+
vm.prank(marketplaceDeployer);
286+
PlatformFee(marketplace).setPlatformFeeInfo(platformFeeRecipient, 9999); // 99.99% fees
287+
288+
// Mint the ERC721 tokens to seller. These tokens will be listed.
289+
erc721.mint(seller, 1);
290+
listingId = _setupListingForRoyaltyTests(address(erc721));
291+
292+
IDirectListings.Listing memory listing = DirectListingsLogic(marketplace).getListing(listingId);
293+
294+
address buyFor = buyer;
295+
uint256 quantityToBuy = listing.quantity;
296+
address currency = listing.currency;
297+
uint256 pricePerToken = listing.pricePerToken;
298+
uint256 totalPrice = pricePerToken * quantityToBuy;
299+
300+
// Mint requisite total price to buyer.
301+
erc20.mint(buyer, totalPrice);
302+
303+
// Approve marketplace to transfer currency
304+
vm.prank(buyer);
305+
erc20.increaseAllowance(marketplace, totalPrice);
306+
307+
// Buy tokens from listing.
308+
vm.warp(listing.startTimestamp);
309+
vm.prank(buyer);
310+
vm.expectRevert("fees exceed the price");
311+
DirectListingsLogic(marketplace).buyFromListing(listingId, buyFor, quantityToBuy, currency, totalPrice);
312+
}
313+
314+
function test_payout_whenSufficientFundsToPayRoyaltyAfterPlatformFeePayout() public whenNonZeroRoyaltyRecipients {
315+
assertEq(RoyaltyPaymentsLogic(marketplace).getRoyaltyEngineAddress(), address(royaltyEngine));
316+
317+
// 1. ========= Create listing =========
318+
319+
// Mint the ERC721 tokens to seller. These tokens will be listed.
320+
erc721.mint(seller, 1);
321+
listingId = _setupListingForRoyaltyTests(address(erc721));
322+
323+
// 2. ========= Buy from listing =========
324+
325+
uint256 totalPrice = _buyFromListingForRoyaltyTests(listingId);
326+
327+
// 3. ======== Check balances after royalty payments ========
328+
329+
uint256 platformFees = (totalPrice * platformFeeBps) / 10_000;
330+
331+
{
332+
// Royalty recipients receive correct amounts
333+
assertBalERC20Eq(address(erc20), mockRecipients[0], mockAmounts[0]);
334+
assertBalERC20Eq(address(erc20), mockRecipients[1], mockAmounts[1]);
335+
336+
// Platform fee recipient receives correct amount
337+
assertBalERC20Eq(address(erc20), platformFeeRecipient, platformFees);
338+
339+
// Seller gets total price minus royalty amounts
340+
assertBalERC20Eq(address(erc20), seller, totalPrice - mockAmounts[0] - mockAmounts[1] - platformFees);
341+
}
342+
}
343+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
function _payout(
2+
address _payer,
3+
address _payee,
4+
address _currencyToUse,
5+
uint256 _totalPayoutAmount,
6+
Listing memory _listing
7+
)
8+
├── when there are zero royalty recipients ✅
9+
│ ├── it should transfer platform fee from payer to platform fee recipient
10+
│ └── it should transfer remainder of currency from payer to payee
11+
└── when there are non-zero royalty recipients
12+
├── when the total royalty payout exceeds remainder payout after having paid platform fee
13+
│ └── it should revert ✅
14+
└── when the total royalty payout does not exceed remainder payout after having paid platform fee ✅
15+
├── it should transfer platform fee from payer to platform fee recipient
16+
├── it should transfer royalty fee from payer to royalty recipients
17+
└── it should transfer remainder of currency from payer to payee

0 commit comments

Comments
 (0)