Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

BTT tests: Direct Listings #546

Merged
merged 27 commits into from
Oct 18, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
306e72f
tree file: buyFromListing
nkrishang Sep 29, 2023
d1dd3a9
tree file: createListing
nkrishang Oct 2, 2023
f4f4b8f
tree file: updateListing
nkrishang Oct 2, 2023
0792c9e
tree file: cancelListing
nkrishang Oct 2, 2023
d5077b1
tree file: approveCurrencyForListing
nkrishang Oct 2, 2023
adeda50
tree file: approveBuyerForListing
nkrishang Oct 2, 2023
4ab0ab5
tree file: _validateOwnershipAndApproval
nkrishang Oct 2, 2023
6ea6f4b
tree file: _validateNewListing
nkrishang Oct 2, 2023
ee10d68
tree file: _validateERC20BalAndAllowance
nkrishang Oct 2, 2023
1951e8e
_transferListingTokens
nkrishang Oct 2, 2023
974a55a
tree file: _payout
nkrishang Oct 2, 2023
c7941be
Merge branch 'main' into nkrishang/direct-listings-btt-tests
nkrishang Oct 16, 2023
1df66e7
btt tests: createListing
nkrishang Oct 16, 2023
d991d10
_validateNewListing WIP tests
nkrishang Oct 16, 2023
4a24a38
_validateNewListing WIP tests 2
nkrishang Oct 16, 2023
e5070b7
btt tests: _validateNewListing
nkrishang Oct 16, 2023
54ef4ba
btt tests: _validateOwnershipAndApproval
nkrishang Oct 17, 2023
bc43398
btt tests: updateListing
nkrishang Oct 17, 2023
6eda227
Mark validateOwnershipAndApproval tree as complete
nkrishang Oct 17, 2023
a3d5ee5
btt tests: cancelListing
nkrishang Oct 17, 2023
ed2e184
btt tests: _validateERC20BalAndAllowance
nkrishang Oct 17, 2023
1fb86b0
btt tests: transferListingTokens
nkrishang Oct 17, 2023
93d97da
btt tests: approveCurrencyForListing
nkrishang Oct 17, 2023
43f21e7
btt tests: approveBuyerForListing
nkrishang Oct 17, 2023
beae771
btt tests: payout
nkrishang Oct 17, 2023
85abbf6
btt tests: buyFromListing
nkrishang Oct 18, 2023
8e41c5c
Merge branch 'main' into nkrishang/direct-listings-btt-tests
nkrishang Oct 18, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
343 changes: 343 additions & 0 deletions src/test/marketplace/direct-listings/_payout/_payout.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,343 @@
// SPDX-License-Identifier: Apache-2.0
pragma solidity ^0.8.0;

import "../../../utils/BaseTest.sol";
import "@thirdweb-dev/dynamic-contracts/src/interface/IExtension.sol";

import { RoyaltyPaymentsLogic } from "contracts/extension/plugin/RoyaltyPayments.sol";
import { PlatformFee } from "contracts/extension/PlatformFee.sol";
import { TWProxy } from "contracts/infra/TWProxy.sol";
import { MarketplaceV3 } from "contracts/prebuilts/marketplace/entrypoint/MarketplaceV3.sol";
import { DirectListingsLogic } from "contracts/prebuilts/marketplace/direct-listings/DirectListingsLogic.sol";
import { IDirectListings } from "contracts/prebuilts/marketplace/IMarketplace.sol";
import { MockRoyaltyEngineV1 } from "../../../mocks/MockRoyaltyEngineV1.sol";

contract PayoutTest is BaseTest, IExtension {
// Target contract
address public marketplace;

// Participants
address public marketplaceDeployer;
address public seller;
address public buyer;

// Default listing parameters
IDirectListings.ListingParameters internal listingParams;
uint256 internal listingId = 0;

// Events to test

/// @notice Emitted when a listing is updated.
event UpdatedListing(
address indexed listingCreator,
uint256 indexed listingId,
address indexed assetContract,
IDirectListings.Listing listing
);

function setUp() public override {
super.setUp();

marketplaceDeployer = getActor(1);
seller = getActor(2);
buyer = getActor(3);

// Deploy implementation.
Extension[] memory extensions = _setupExtensions();
address impl = address(
new MarketplaceV3(MarketplaceV3.MarketplaceConstructorParams(extensions, address(0), address(weth)))
);

vm.prank(marketplaceDeployer);
marketplace = address(
new TWProxy(
impl,
abi.encodeCall(
MarketplaceV3.initialize,
(marketplaceDeployer, "", new address[](0), platformFeeRecipient, uint16(platformFeeBps))
)
)
);

// Setup listing params
address assetContract = address(erc721);
uint256 tokenId = 0;
uint256 quantity = 1;
address currency = address(erc20);
uint256 pricePerToken = 1 ether;
uint128 startTimestamp = 100 minutes;
uint128 endTimestamp = 200 minutes;
bool reserved = false;

listingParams = IDirectListings.ListingParameters(
assetContract,
tokenId,
quantity,
currency,
pricePerToken,
startTimestamp,
endTimestamp,
reserved
);

// Mint 1 ERC721 NFT to seller
erc721.mint(seller, listingParams.quantity);

vm.label(impl, "MarketplaceV3_Impl");
vm.label(marketplace, "Marketplace");
vm.label(seller, "Seller");
vm.label(address(erc721), "ERC721_Token");
vm.label(address(erc1155), "ERC1155_Token");
}

function _setupExtensions() internal returns (Extension[] memory extensions) {
extensions = new Extension[](1);

// Deploy `DirectListings`
address directListings = address(new DirectListingsLogic(address(weth)));
vm.label(directListings, "DirectListings_Extension");

// Extension: DirectListingsLogic
Extension memory extension_directListings;
extension_directListings.metadata = ExtensionMetadata({
name: "DirectListingsLogic",
metadataURI: "ipfs://DirectListings",
implementation: directListings
});

extension_directListings.functions = new ExtensionFunction[](13);
extension_directListings.functions[0] = ExtensionFunction(
DirectListingsLogic.totalListings.selector,
"totalListings()"
);
extension_directListings.functions[1] = ExtensionFunction(
DirectListingsLogic.isBuyerApprovedForListing.selector,
"isBuyerApprovedForListing(uint256,address)"
);
extension_directListings.functions[2] = ExtensionFunction(
DirectListingsLogic.isCurrencyApprovedForListing.selector,
"isCurrencyApprovedForListing(uint256,address)"
);
extension_directListings.functions[3] = ExtensionFunction(
DirectListingsLogic.currencyPriceForListing.selector,
"currencyPriceForListing(uint256,address)"
);
extension_directListings.functions[4] = ExtensionFunction(
DirectListingsLogic.createListing.selector,
"createListing((address,uint256,uint256,address,uint256,uint128,uint128,bool))"
);
extension_directListings.functions[5] = ExtensionFunction(
DirectListingsLogic.updateListing.selector,
"updateListing(uint256,(address,uint256,uint256,address,uint256,uint128,uint128,bool))"
);
extension_directListings.functions[6] = ExtensionFunction(
DirectListingsLogic.cancelListing.selector,
"cancelListing(uint256)"
);
extension_directListings.functions[7] = ExtensionFunction(
DirectListingsLogic.approveBuyerForListing.selector,
"approveBuyerForListing(uint256,address,bool)"
);
extension_directListings.functions[8] = ExtensionFunction(
DirectListingsLogic.approveCurrencyForListing.selector,
"approveCurrencyForListing(uint256,address,uint256)"
);
extension_directListings.functions[9] = ExtensionFunction(
DirectListingsLogic.buyFromListing.selector,
"buyFromListing(uint256,address,uint256,address,uint256)"
);
extension_directListings.functions[10] = ExtensionFunction(
DirectListingsLogic.getAllListings.selector,
"getAllListings(uint256,uint256)"
);
extension_directListings.functions[11] = ExtensionFunction(
DirectListingsLogic.getAllValidListings.selector,
"getAllValidListings(uint256,uint256)"
);
extension_directListings.functions[12] = ExtensionFunction(
DirectListingsLogic.getListing.selector,
"getListing(uint256)"
);

extensions[0] = extension_directListings;
}

address payable[] internal mockRecipients;
uint256[] internal mockAmounts;
MockRoyaltyEngineV1 internal royaltyEngine;

function _setupRoyaltyEngine() private {
mockRecipients.push(payable(address(0x12345)));
mockRecipients.push(payable(address(0x56789)));

mockAmounts.push(10 ether);
mockAmounts.push(15 ether);

royaltyEngine = new MockRoyaltyEngineV1(mockRecipients, mockAmounts);
}

function _setupListingForRoyaltyTests(address erc721TokenAddress) private returns (uint256 _listingId) {
// Sample listing parameters.
address assetContract = erc721TokenAddress;
uint256 tokenId = 0;
uint256 quantity = 1;
address currency = address(erc20);
uint256 pricePerToken = 100 ether;
uint128 startTimestamp = 100;
uint128 endTimestamp = 200;
bool reserved = false;

// Approve Marketplace to transfer token.
vm.prank(seller);
IERC721(erc721TokenAddress).setApprovalForAll(marketplace, true);

// List tokens.
IDirectListings.ListingParameters memory listingParameters = IDirectListings.ListingParameters(
assetContract,
tokenId,
quantity,
currency,
pricePerToken,
startTimestamp,
endTimestamp,
reserved
);

vm.prank(seller);
_listingId = DirectListingsLogic(marketplace).createListing(listingParameters);
}

function _buyFromListingForRoyaltyTests(uint256 _listingId) private returns (uint256 totalPrice) {
IDirectListings.Listing memory listing = DirectListingsLogic(marketplace).getListing(_listingId);

address buyFor = buyer;
uint256 quantityToBuy = listing.quantity;
address currency = listing.currency;
uint256 pricePerToken = listing.pricePerToken;
totalPrice = pricePerToken * quantityToBuy;

// Mint requisite total price to buyer.
erc20.mint(buyer, totalPrice);

// Approve marketplace to transfer currency
vm.prank(buyer);
erc20.increaseAllowance(marketplace, totalPrice);

// Buy tokens from listing.
vm.warp(listing.startTimestamp);
vm.prank(buyer);
DirectListingsLogic(marketplace).buyFromListing(_listingId, buyFor, quantityToBuy, currency, totalPrice);
}

function test_payout_whenZeroRoyaltyRecipients() public {
// 1. ========= Create listing =========
vm.startPrank(seller);
erc721.setApprovalForAll(marketplace, true);
listingId = DirectListingsLogic(marketplace).createListing(listingParams);
vm.stopPrank();

// 2. ========= Buy from listing =========

uint256 totalPrice = listingParams.pricePerToken;

// Mint requisite total price to buyer.
erc20.mint(buyer, totalPrice);

// Approve marketplace to transfer currency
vm.prank(buyer);
erc20.increaseAllowance(marketplace, totalPrice);

// Buy tokens from listing.
vm.warp(listingParams.startTimestamp);
vm.prank(buyer);
DirectListingsLogic(marketplace).buyFromListing(
listingId,
buyer,
listingParams.quantity,
listingParams.currency,
totalPrice
);

// 3. ======== Check balances after royalty payments ========

uint256 platformFees = (totalPrice * platformFeeBps) / 10_000;

{
// Platform fee recipient receives correct amount
assertBalERC20Eq(address(erc20), platformFeeRecipient, platformFees);

// Seller gets total price minus royalty amounts
assertBalERC20Eq(address(erc20), seller, totalPrice - platformFees);
}
}

modifier whenNonZeroRoyaltyRecipients() {
_setupRoyaltyEngine();

// Add RoyaltyEngine to marketplace
vm.prank(marketplaceDeployer);
RoyaltyPaymentsLogic(marketplace).setRoyaltyEngine(address(royaltyEngine));

_;
}

function test_payout_whenInsufficientFundsToPayRoyaltyAfterPlatformFeePayout() public whenNonZeroRoyaltyRecipients {
vm.prank(marketplaceDeployer);
PlatformFee(marketplace).setPlatformFeeInfo(platformFeeRecipient, 9999); // 99.99% fees

// Mint the ERC721 tokens to seller. These tokens will be listed.
erc721.mint(seller, 1);
listingId = _setupListingForRoyaltyTests(address(erc721));

IDirectListings.Listing memory listing = DirectListingsLogic(marketplace).getListing(listingId);

address buyFor = buyer;
uint256 quantityToBuy = listing.quantity;
address currency = listing.currency;
uint256 pricePerToken = listing.pricePerToken;
uint256 totalPrice = pricePerToken * quantityToBuy;

// Mint requisite total price to buyer.
erc20.mint(buyer, totalPrice);

// Approve marketplace to transfer currency
vm.prank(buyer);
erc20.increaseAllowance(marketplace, totalPrice);

// Buy tokens from listing.
vm.warp(listing.startTimestamp);
vm.prank(buyer);
vm.expectRevert("fees exceed the price");
DirectListingsLogic(marketplace).buyFromListing(listingId, buyFor, quantityToBuy, currency, totalPrice);
}

function test_payout_whenSufficientFundsToPayRoyaltyAfterPlatformFeePayout() public whenNonZeroRoyaltyRecipients {
assertEq(RoyaltyPaymentsLogic(marketplace).getRoyaltyEngineAddress(), address(royaltyEngine));

// 1. ========= Create listing =========

// Mint the ERC721 tokens to seller. These tokens will be listed.
erc721.mint(seller, 1);
listingId = _setupListingForRoyaltyTests(address(erc721));

// 2. ========= Buy from listing =========

uint256 totalPrice = _buyFromListingForRoyaltyTests(listingId);

// 3. ======== Check balances after royalty payments ========

uint256 platformFees = (totalPrice * platformFeeBps) / 10_000;

{
// Royalty recipients receive correct amounts
assertBalERC20Eq(address(erc20), mockRecipients[0], mockAmounts[0]);
assertBalERC20Eq(address(erc20), mockRecipients[1], mockAmounts[1]);

// Platform fee recipient receives correct amount
assertBalERC20Eq(address(erc20), platformFeeRecipient, platformFees);

// Seller gets total price minus royalty amounts
assertBalERC20Eq(address(erc20), seller, totalPrice - mockAmounts[0] - mockAmounts[1] - platformFees);
}
}
}
17 changes: 17 additions & 0 deletions src/test/marketplace/direct-listings/_payout/_payout.tree
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
function _payout(
address _payer,
address _payee,
address _currencyToUse,
uint256 _totalPayoutAmount,
Listing memory _listing
)
├── when there are zero royalty recipients ✅
│ ├── it should transfer platform fee from payer to platform fee recipient
│ └── it should transfer remainder of currency from payer to payee
└── when there are non-zero royalty recipients
├── when the total royalty payout exceeds remainder payout after having paid platform fee
│ └── it should revert ✅
└── when the total royalty payout does not exceed remainder payout after having paid platform fee ✅
├── it should transfer platform fee from payer to platform fee recipient
├── it should transfer royalty fee from payer to royalty recipients
└── it should transfer remainder of currency from payer to payee
Loading