Skip to content

Commit

Permalink
feat(hc): hypercert order routing and validation
Browse files Browse the repository at this point in the history
  • Loading branch information
bitbeckers committed Dec 2, 2023
1 parent 32fb042 commit 6ec4006
Show file tree
Hide file tree
Showing 7 changed files with 288 additions and 71 deletions.
24 changes: 21 additions & 3 deletions contracts/src/marketplace/LooksRareProtocol.sol
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,13 @@ import {
// Direct dependencies
import {TransferSelectorNFT} from "./TransferSelectorNFT.sol";
import {BatchOrderTypehashRegistry} from "./BatchOrderTypehashRegistry.sol";
import {StrategyHypercertFractionOffer} from "./executionStrategies/StrategyHypercertFractionOffer.sol";

// Constants
import {MAX_CALLDATA_PROOF_LENGTH, ONE_HUNDRED_PERCENT_IN_BP} from "./constants/NumericConstants.sol";

// Enums
import {QuoteType} from "./enums/QuoteType.sol";
import {CollectionType} from "./enums/CollectionType.sol";

/**
* @title LooksRareProtocol
Expand Down Expand Up @@ -371,7 +371,7 @@ contract LooksRareProtocol is
_updateUserOrderNonce(isNonceInvalidated, signer, makerBid.orderNonce, orderHash);

// Taker action goes first
_transferNFT(makerBid.collection, makerBid.collectionType, msg.sender, signer, itemIds, amounts);
_executeTakerAskTakerAction(makerBid, takerAsk, msg.sender, signer, itemIds, amounts);

// Maker action goes second
_transferToAskRecipientAndCreatorIfAny(recipients, feeAmounts, makerBid.currency, signer);
Expand Down Expand Up @@ -467,6 +467,23 @@ contract LooksRareProtocol is
return feeAmounts[2];
}

function _executeTakerAskTakerAction(
OrderStructs.Maker calldata makerBid,
OrderStructs.Taker calldata takerAsk,
address sender,
address recipient,
uint256[] memory itemIds,
uint256[] memory amounts
) internal {
if (makerBid.collectionType == CollectionType.Hypercert) {
_transferHypercertFraction(
makerBid.collection, makerBid.collectionType, makerBid.strategyId, sender, recipient, itemIds, amounts
);
} else {
_transferNFT(makerBid.collection, makerBid.collectionType, sender, recipient, itemIds, amounts);
}
}

function _executeTakerBidMakerAction(
OrderStructs.Maker calldata makerAsk,
OrderStructs.Taker calldata takerBid,
Expand All @@ -475,10 +492,11 @@ contract LooksRareProtocol is
uint256[] memory itemIds,
uint256[] memory amounts
) internal {
if (makerAsk.collectionType == 3) {
if (makerAsk.collectionType == CollectionType.Hypercert) {
_transferHypercertFraction(
makerAsk.collection,
makerAsk.collectionType,
makerAsk.strategyId,
sender,
takerBid.recipient == address(0) ? recipient : takerBid.recipient,
itemIds,
Expand Down
25 changes: 14 additions & 11 deletions contracts/src/marketplace/TransferManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ import {CollectionType} from "./enums/CollectionType.sol";
* Collection type "3" refers to Hyperboard transfer functions.
* @dev "Safe" transfer functions for ERC721 are not implemented since they come with added gas costs
* to verify if the recipient is a contract as it requires verifying the receiver interface is valid.
* @author LooksRare protocol team (👀,💎)
* @author LooksRare protocol team (👀,💎); bitbeckers
*/
contract TransferManager is
ITransferManager,
Expand Down Expand Up @@ -177,13 +177,13 @@ contract TransferManager is
}

/**
* @notice This function transfers items for a single Hypercert.
* @notice This function splits and transfers a fraction of a hypercert.
* @param collection Collection address
* @param from Sender address
* @param to Recipient address
* @param itemIds Array of itemIds
* @param amounts Array of amounts
* @dev It does not allow batch transferring if from = msg.sender since native function should be used.
* @dev It does not allow batch transferring.
*/
function splitItemsHypercert(
address collection,
Expand All @@ -192,8 +192,6 @@ contract TransferManager is
uint256[] calldata itemIds,
uint256[] calldata amounts
) external {
IHypercertToken hypercert = IHypercertToken(collection);

if (itemIds.length != 1 || amounts.length != 1) {
revert LengthsInvalid();
}
Expand All @@ -204,14 +202,19 @@ contract TransferManager is
revert AmountInvalid();
}

if (IHypercertToken(collection).unitsOf(itemIds[0]) <= amounts[0]) {
revert UnitAmountInvalid();
}

uint256[] memory newAmounts = new uint256[](2);
newAmounts[0] = hypercert.unitsOf(itemIds[0]) - amounts[0];

//The new amount is the difference between the total amount and the amount being split.
//This will underflow if the amount being split is greater than the total amount.
newAmounts[0] = IHypercertToken(collection).unitsOf(itemIds[0]) - amounts[0];
newAmounts[1] = amounts[0];
_executeHypercertSplitFraction(collection, from, to, itemIds[0], newAmounts);

// If the new amount is 0, then the split is will revert but the whole fraction can be transferred.
if (newAmounts[0] == 0) {
_executeERC1155SafeTransferFrom(collection, from, to, itemIds[0], 1);
} else {
_executeHypercertSplitFraction(collection, to, itemIds[0], newAmounts);
}
}

/**
Expand Down
12 changes: 6 additions & 6 deletions contracts/src/marketplace/TransferSelectorNFT.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ pragma solidity ^0.8.17;
import {PackableReentrancyGuard} from "@looksrare/contracts-libs/contracts/PackableReentrancyGuard.sol";
import {ExecutionManager} from "./ExecutionManager.sol";
import {TransferManager} from "./TransferManager.sol";
import {StrategyHypercertFractionOffer} from "./executionStrategies/StrategyHypercertFractionOffer.sol";

// Libraries
import {OrderStructs} from "./libraries/OrderStructs.sol";
Expand All @@ -23,7 +24,7 @@ import {IHypercertToken} from "../protocol/interfaces/IHypercertToken.sol";
contract TransferSelectorNFT is ExecutionManager, PackableReentrancyGuard {
error UnsupportedCollectionType();
/**
* @notice Transfer manager for ERC721 and ERC1155.
* @notice Transfer manager for ERC721, ERC1155 and Hypercerts.
*/

TransferManager public immutable transferManager;
Expand Down Expand Up @@ -61,8 +62,6 @@ contract TransferSelectorNFT is ExecutionManager, PackableReentrancyGuard {
transferManager.transferItemsERC721(collection, sender, recipient, itemIds, amounts);
} else if (collectionType == CollectionType.ERC1155) {
transferManager.transferItemsERC1155(collection, sender, recipient, itemIds, amounts);
} else if (collectionType == CollectionType.Hypercert) {
transferManager.transferItemsHypercert(collection, sender, recipient, itemIds, amounts);
} else {
revert UnsupportedCollectionType();
}
Expand All @@ -80,6 +79,7 @@ contract TransferSelectorNFT is ExecutionManager, PackableReentrancyGuard {
function _transferHypercertFraction(
address collection,
CollectionType collectionType,
uint256 strategyId,
address sender,
address recipient,
uint256[] memory itemIds,
Expand All @@ -90,13 +90,13 @@ contract TransferSelectorNFT is ExecutionManager, PackableReentrancyGuard {
}

if (
strategyInfo[makerAsk.strategyId].selector
strategyInfo[strategyId].selector
== StrategyHypercertFractionOffer.executeHypercertFractionStrategyWithTakerBid.selector
|| strategyInfo[makerAsk.strategyId].selector
|| strategyInfo[strategyId].selector
== StrategyHypercertFractionOffer.executeHypercertFractionStrategyWithTakerBidWithAllowlist.selector
) {
transferManager.splitItemsHypercert(collection, sender, recipient, itemIds, amounts);
} else if (amounts[0] == 1) {
} else {
transferManager.transferItemsHypercert(collection, sender, recipient, itemIds, amounts);
}
}
Expand Down
2 changes: 1 addition & 1 deletion contracts/src/marketplace/errors/SharedErrors.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ pragma solidity 0.8.17;

/**
* @notice It is returned if the amount is invalid.
* For ERC721, any number that is not 1. For ERC1155, if amount is 0.
* For ERC721, any number that is not 1. For ERC1155 and Hypercert, if amount is 0.
*/
error AmountInvalid();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,14 @@ import {MerkleProofMemory} from "../libraries/OpenZeppelin/MerkleProofMemory.sol
import {QuoteType} from "../enums/QuoteType.sol";

// Shared errors
import {OrderInvalid, FunctionSelectorInvalid, MerkleProofInvalid, QuoteTypeInvalid} from "../errors/SharedErrors.sol";
import {
AmountInvalid,
LengthsInvalid,
OrderInvalid,
FunctionSelectorInvalid,
MerkleProofInvalid,
QuoteTypeInvalid
} from "../errors/SharedErrors.sol";

// Base strategy contracts
import {BaseStrategy, IStrategy} from "./BaseStrategy.sol";
Expand Down Expand Up @@ -58,35 +65,42 @@ contract StrategyHypercertFractionOffer is BaseStrategy {
)
external
view
returns (uint256 price, uint256[] memory itemIds, uint256[] calldata amounts, bool isNonceInvalidated)
returns (uint256 price, uint256[] memory itemIds, uint256[] memory amounts, bool isNonceInvalidated)
{
amounts = makerAsk.amounts;
itemIds = makerAsk.itemIds;

// A collection order can only be executable for 1 itemId but the actual quantity to fill can vary
if (amounts.length != 1 || itemIds.length != 1) {
revert OrderInvalid();
if (makerAsk.amounts.length != 1 || itemIds.length != 1) {
revert LengthsInvalid();
}

//units, amount, proof[]
(uint256 unitAmount, uint256 acceptedTokenAmount) =
abi.decode(takerBid.additionalParameters, (uint256, uint256));
if (makerAsk.amounts[0] == 0) {
revert AmountInvalid();
}

//units, pricePerUnit
(uint256 unitAmount, uint256 pricePerUnit) = abi.decode(takerBid.additionalParameters, (uint256, uint256));

//minUnitAmount, maxUnitAmount, root
(uint256 minUnitAmount, uint256 maxUnitAmount) = abi.decode(makerAsk.additionalParameters, (uint256, uint256));

// A collection order can only be executable for 1 itemId but quantity to fill can vary
if (
makerAsk.amounts.length != 1 || makerAsk.itemIds.length != 1 || minUnitAmount > maxUnitAmount
|| unitAmount < minUnitAmount || makerAsk.price > acceptedTokenAmount || makerAsk.price == 0
|| IHypercertToken(makerAsk.collection).unitsOf(itemIds[0]) < amounts[0]
minUnitAmount > maxUnitAmount || unitAmount == 0 || unitAmount < minUnitAmount || unitAmount > maxUnitAmount
|| pricePerUnit < makerAsk.price || makerAsk.price == 0
|| IHypercertToken(makerAsk.collection).unitsOf(itemIds[0]) < unitAmount
) {
revert OrderInvalid();
}

price = acceptedTokenAmount * unitAmount;
uint256[] memory amountsToFill = new uint256[](1);
amountsToFill[0] = unitAmount;
amounts = amountsToFill;

isNonceInvalidated = true;
price = unitAmount * pricePerUnit;
// If the amount to fill is equal to the amount of units in the hypercert, we transfer the fraction.
// Otherwise, we do not invalidate the nonce because it is a partial fill.
isNonceInvalidated = IHypercertToken(makerAsk.collection).unitsOf(itemIds[0]) == unitAmount;
}

/**
Expand All @@ -103,42 +117,46 @@ contract StrategyHypercertFractionOffer is BaseStrategy {
OrderStructs.Maker calldata makerAsk
)
external
pure
view
returns (uint256 price, uint256[] memory itemIds, uint256[] memory amounts, bool isNonceInvalidated)
{
itemIds = makerAsk.itemIds;

// A collection order can only be executable for 1 itemId but the actual quantity to fill can vary
if (makerAsk.amounts.length != 1 || itemIds.length != 1) {
revert OrderInvalid();
revert LengthsInvalid();
}

if (makerAsk.amounts[0] == 0) {
revert AmountInvalid();
}

//units, amount, proof[]
//units, pricePerUnit, proof[]
(uint256 unitAmount, uint256 pricePerUnit, bytes32[] memory proof) =
abi.decode(takerBid.additionalParameters, (uint256, uint256, bytes32[]));

// A bid needs to at least match the minimum price per unit
if (pricePerUnit < makerAsk.price) {
revert OrderInvalid();
}

price = unitAmount * pricePerUnit;
amounts = new uint256[](1);
amounts[0] = unitAmount;
//minUnitAmount, maxUnitAmount, root
(uint256 minUnitAmount, uint256 maxUnitAmount, bytes32 root) =
abi.decode(makerAsk.additionalParameters, (uint256, uint256, bytes32));

// A collection order can only be executable for 1 itemId but the actual quantity to fill can vary
if (amounts.length != 1 || itemIds.length != 1) {
// A collection order can only be executable for 1 itemId but quantity to fill can vary
if (
minUnitAmount > maxUnitAmount || unitAmount == 0 || unitAmount < minUnitAmount || unitAmount > maxUnitAmount
|| pricePerUnit < makerAsk.price || makerAsk.price == 0
|| IHypercertToken(makerAsk.collection).unitsOf(itemIds[0]) < unitAmount
) {
revert OrderInvalid();
}

//minUnitAmount, maxUnitAmount, root
(uint256 minUnitAmount, uint256 maxUnitAmount, bytes32 root) =
abi.decode(makerAsk.additionalParameters, (uint256, uint256, bytes32));
uint256[] memory amountsToFill = new uint256[](1);
amountsToFill[0] = unitAmount;
amounts = amountsToFill;

price = unitAmount * pricePerUnit;

// Nonce is not invalidated because it can be a partial fill
// @dev This strategy represents a partial fill. The protocol will call transfer if the bid would clear the
// offered fraction.
isNonceInvalidated = false;
// If the amount to fill is equal to the amount of units in the hypercert, we transfer the fraction.
// Otherwise, we do not invalidate the nonce because it is a partial fill.
isNonceInvalidated = IHypercertToken(makerAsk.collection).unitsOf(itemIds[0]) == unitAmount;

bytes32 node = keccak256(abi.encodePacked(takerBid.recipient));

Expand Down
11 changes: 3 additions & 8 deletions contracts/src/marketplace/libraries/LowLevelHypercertCaller.sol
Original file line number Diff line number Diff line change
Expand Up @@ -16,19 +16,14 @@ contract LowLevelHypercertCaller {
/**
* @notice Execute Hypercert splitFraction
* @param collection Address of the collection
* @param from Address of the sender
* @param to Address of the recipient
* @param tokenId tokenId to transfer
* @param amounts split distribution
*/

function _executeHypercertSplitFraction(
address collection,
address from,
address to,
uint256 tokenId,
uint256[] memory amounts
) internal {
function _executeHypercertSplitFraction(address collection, address to, uint256 tokenId, uint256[] memory amounts)
internal
{
if (collection.code.length == 0) {
revert NotAContract();
}
Expand Down
Loading

0 comments on commit 6ec4006

Please sign in to comment.