MarketplaceIntegration.sol
Contract to be deployed be integrating marketplaces. This contract allows integrating marketplaces to set a marketplaceFeeBps and marketplaceFeeRecipient if desired.
The buyWithFinancingBatch and instantSellBatch functions are unaudited as of May 15, 2023
//SPDX-License-Identifier: MIT
pragma solidity 0.8.18;
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/security/Pausable.sol";
import "@openzeppelin/contracts/utils/Address.sol";
import "@openzeppelin/contracts/token/ERC721/IERC721.sol";
import "@openzeppelin/contracts/token/ERC721/utils/ERC721Holder.sol";
import "../interfaces/sellerFinancing/ISellerFinancing.sol";
/// @title MarketplaceIntegration
/// @custom:version 1.0
/// @author zishansami102 (zishansami.eth)
/// @custom:contributor captnseagraves (captnseagraves.eth)
contract MarketplaceIntegration is Ownable, Pausable, ERC721Holder {
using Address for address payable;
/// @notice The base value for fees in the protocol.
uint256 private constant BASE_BPS = 10_000;
/// @dev The status of sanctions checks
bool internal _sanctionsPause;
uint256 public marketplaceFeeBps;
address payable public marketplaceFeeRecipient;
address public sellerFinancingContractAddress;
error ZeroAddress();
error InsufficientMsgValue(uint256 given, uint256 expected);
error SanctionedAddress(address account);
error InvalidInputLength();
error InstantSellCallRevertedAt(uint256 index);
error BuyerTicketTransferRevertedAt(uint256 index, address from, address to);
constructor(
address _sellerFinancingContractAddress,
address _marketplaceFeeRecipient,
uint256 _marketplaceFeeBps
) {
_requireNonZeroAddress(_sellerFinancingContractAddress);
_requireNonZeroAddress(_marketplaceFeeRecipient);
sellerFinancingContractAddress = _sellerFinancingContractAddress;
marketplaceFeeRecipient = payable(_marketplaceFeeRecipient);
marketplaceFeeBps = _marketplaceFeeBps;
}
/// @param newSellerFinancingContractAddress New address for SellerFinancing contract
function updateSellerFinancingContractAddress(
address newSellerFinancingContractAddress
) external onlyOwner {
_requireNonZeroAddress(newSellerFinancingContractAddress);
sellerFinancingContractAddress = newSellerFinancingContractAddress;
}
/// @param newMarketplaceFeeRecipient New address for MarketplaceFeeRecipient
function updateMarketplaceFeeRecipient(address newMarketplaceFeeRecipient) external onlyOwner {
_requireNonZeroAddress(newMarketplaceFeeRecipient);
marketplaceFeeRecipient = payable(newMarketplaceFeeRecipient);
}
/// @param newMarketplaceFeeBps New value for marketplaceFeeBps
function updateMarketplaceFeeBps(uint256 newMarketplaceFeeBps) external onlyOwner {
marketplaceFeeBps = newMarketplaceFeeBps;
}
function pause() external onlyOwner {
_pause();
}
function unpause() external onlyOwner {
_unpause();
}
function pauseSanctions() external onlyOwner {
_sanctionsPause = true;
}
function unpauseSanctions() external onlyOwner {
_sanctionsPause = false;
}
function buyWithFinancing(
ISellerFinancing.Offer memory offer,
bytes calldata signature,
address buyer,
uint256 nftId
) external payable whenNotPaused {
uint256 marketplaceFeeAmount = (offer.price * marketplaceFeeBps) / BASE_BPS;
if (msg.value < offer.downPaymentAmount + marketplaceFeeAmount) {
revert InsufficientMsgValue(msg.value, offer.downPaymentAmount + marketplaceFeeAmount);
}
marketplaceFeeRecipient.sendValue(marketplaceFeeAmount);
ISellerFinancing(sellerFinancingContractAddress).buyWithFinancing{
value: msg.value - marketplaceFeeAmount
}(offer, signature, buyer, nftId);
}
/// @notice Execute loan offers in batch for buyer
/// @param offers The list of the offers to execute
/// @param signatures The list of corresponding signatures from the offer creators
/// @param buyer The address of the buyer
/// @param nftIds The nftIds of the nfts the buyer intends to buy
/// @param partialExecution If set to true, will continue to attempt transaction executions regardless
/// if previous transactions have failed or had insufficient value available
function buyWithFinancingBatch(
ISellerFinancing.Offer[] memory offers,
bytes[] calldata signatures,
address buyer,
uint256[] calldata nftIds,
bool partialExecution
) external payable whenNotPaused {
uint256 offersLength = offers.length;
// requireLengthOfAllInputArraysAreEqual
if (offersLength != signatures.length || offersLength != nftIds.length) {
revert InvalidInputLength();
}
uint256 marketplaceFeeAccumulated;
uint256 valueConsumed;
// loop through list of offers to execute
for (uint256 i; i < offersLength; ++i) {
// instantiate ith offer
ISellerFinancing.Offer memory offer = offers[i];
// calculate marketplace fee for ith offer
uint256 marketplaceFeeAmount = (offer.price * marketplaceFeeBps) / BASE_BPS;
// if remaining value is not sufficient to execute ith offer
if (msg.value - valueConsumed < offer.downPaymentAmount + marketplaceFeeAmount) {
// if partial execution is allowed then move to next offer
if (partialExecution) {
continue;
}
// else revert
else {
revert InsufficientMsgValue(
msg.value,
valueConsumed + offer.downPaymentAmount + marketplaceFeeAmount
);
}
}
// try executing current offer,
try
ISellerFinancing(sellerFinancingContractAddress).buyWithFinancing{
value: offer.downPaymentAmount
}(offer, signatures[i], buyer, nftIds[i])
{
// if successful
// increment marketplaceFeeAccumulated
marketplaceFeeAccumulated += marketplaceFeeAmount;
// increment valueConsumed
valueConsumed += offer.downPaymentAmount + marketplaceFeeAmount;
} catch {
// if failed
// if partial execution is not allowed, revert
if (!partialExecution) {
revert BuyWithFinancingCallRevertedAt(i);
}
}
}
// send accumulated marketplace fee to marketplace fee recipient
marketplaceFeeRecipient.sendValue(marketplaceFeeAccumulated);
// send any unused value back to msg.sender
if (msg.value - valueConsumed > 0) {
payable(msg.sender).sendValue(msg.value - valueConsumed);
}
}
/// @notice Execute instantSell on all the NFTs in the provided input
/// @param nftContractAddresses The list of all the nft contract addresses
/// @param nftIds The list of all the nft IDs
/// @param minProfitAmounts List of minProfitAmount for each `instantSell` call
/// @param data The list of data to be passed to each `instantSell` call
/// @param partialExecution If set to true, will continue to attempt next request in the loop
/// when one `instantSell` or transfer ticket call fails
function instantSellBatch(
address[] memory nftContractAddresses,
uint256[] memory nftIds,
uint256[] memory minProfitAmounts,
bytes[] calldata data,
bool partialExecution
) external whenNotPaused {
uint256 executionCount = nftContractAddresses.length;
// requireLengthOfAllInputArraysAreEqual
if(nftIds.length != executionCount || minProfitAmounts.length != executionCount || data.length != executionCount) {
revert InvalidInputLength();
}
uint256 contractBalanceBefore = address(this).balance;
for (uint256 i; i < executionCount; ++i) {
// intantiate NFT details
address nftContractAddress = nftContractAddresses[i];
uint256 nftId = nftIds[i];
// fetech active loan details
ISellerFinancing.Loan memory loan = ISellerFinancing(sellerFinancingContractAddress).getLoan(nftContractAddress, nftId);
// transfer buyerNft from caller to this contract.
// this call also ensures that loan exists and caller is the current buyer
try IERC721(sellerFinancingContractAddress).safeTransferFrom(msg.sender, address(this), loan.buyerNftId) {
// call instantSell to close the loan
try ISellerFinancing(sellerFinancingContractAddress).instantSell(nftContractAddress, nftId, minProfitAmounts[i], data[i]) {}
catch {
if (!partialExecution) {
revert InstantSellCallRevertedAt(i);
} else {
IERC721(sellerFinancingContractAddress).safeTransferFrom(address(this), msg.sender, loan.buyerNftId);
}
}
} catch {
if (!partialExecution) {
revert BuyerTicketTransferRevertedAt(i, msg.sender, address(this));
}
}
}
// accumulate value received
uint256 valueReceived = address(this).balance - contractBalanceBefore;
// send all the amount received to the caller
if( valueReceived > 0) {
payable(msg.sender).sendValue(valueReceived);
}
}
function _requireNonZeroAddress(address given) internal pure {
if (given == address(0)) {
revert ZeroAddress();
}
}
/// @notice This contract needs to accept ETH from `instantSell` calls
receive() external payable {}
}
Updated over 1 year ago