Skip to content

Commit

Permalink
feat(merkle): account allowlist strategy
Browse files Browse the repository at this point in the history
  • Loading branch information
bitbeckers committed Nov 22, 2023
1 parent 84a623c commit 5c2dc7e
Show file tree
Hide file tree
Showing 3 changed files with 161 additions and 3 deletions.
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.17;
pragma solidity ^0.8.17;

// Libraries
import {OrderStructs} from "../libraries/OrderStructs.sol";
Expand Down Expand Up @@ -98,6 +98,46 @@ contract StrategyCollectionOffer is BaseStrategy {
}
}

/**
* @notice This function validates the order under the context of the chosen strategy
* and returns the fulfillable items/amounts/price/nonce invalidation status.
* This strategy executes a collection offer against a taker ask order with the need of a merkle proof
* that the address is allowed to fullfil the ask.
* @param takerAsk Taker ask struct (taker ask-specific parameters for the execution)
* @param makerBid Maker bid struct (maker bid-specific parameters for the execution)
* @dev The transaction reverts if the maker does not include a merkle root in the additionalParameters.
*/
function executeCollectionStrategyWithTakerAskWithAllowlist(
OrderStructs.Taker calldata takerAsk,
OrderStructs.Maker calldata makerBid
)
external
pure
returns (uint256 price, uint256[] memory itemIds, uint256[] calldata amounts, bool isNonceInvalidated)
{
price = makerBid.price;
amounts = makerBid.amounts;

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

(uint256 offeredItemId, bytes32[] memory proof) =
abi.decode(takerAsk.additionalParameters, (uint256, bytes32[]));
itemIds = new uint256[](1);
itemIds[0] = offeredItemId;
isNonceInvalidated = true;

bytes32 root = abi.decode(makerBid.additionalParameters, (bytes32));
bytes32 node = keccak256(abi.encodePacked(takerAsk.recipient));

// Verify the merkle root for the given merkle proof
if (!MerkleProofMemory.verify(proof, root, node)) {
revert MerkleProofInvalid();
}
}

/**
* @inheritdoc IStrategy
*/
Expand All @@ -110,6 +150,7 @@ contract StrategyCollectionOffer is BaseStrategy {
if (
functionSelector != StrategyCollectionOffer.executeCollectionStrategyWithTakerAskWithProof.selector
&& functionSelector != StrategyCollectionOffer.executeCollectionStrategyWithTakerAsk.selector
&& functionSelector != StrategyCollectionOffer.executeCollectionStrategyWithTakerAskWithAllowlist.selector
) {
return (isValid, FunctionSelectorInvalid.selector);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.17;
pragma solidity ^0.8.17;

// Murky (third-party) library is used to compute Merkle trees in Solidity
import {Merkle} from "murky/Merkle.sol";
Expand Down Expand Up @@ -35,6 +35,8 @@ contract CollectionOrdersTest is ProtocolBase {
StrategyCollectionOffer public strategyCollectionOffer;
bytes4 public selectorNoProof = strategyCollectionOffer.executeCollectionStrategyWithTakerAsk.selector;
bytes4 public selectorWithProof = strategyCollectionOffer.executeCollectionStrategyWithTakerAskWithProof.selector;
bytes4 public selectorWithProofAllowlist =
strategyCollectionOffer.executeCollectionStrategyWithTakerAskWithAllowlist.selector;

uint256 private constant price = 1 ether; // Fixed price of sale
bytes32 private constant mockMerkleRoot = bytes32(keccak256("Mock")); // Mock merkle root
Expand All @@ -48,6 +50,7 @@ contract CollectionOrdersTest is ProtocolBase {
strategyCollectionOffer = new StrategyCollectionOffer();
_addStrategy(address(strategyCollectionOffer), selectorNoProof, true);
_addStrategy(address(strategyCollectionOffer), selectorWithProof, true);
_addStrategy(address(strategyCollectionOffer), selectorWithProofAllowlist, true);
}

function testNewStrategies() public {
Expand Down Expand Up @@ -169,6 +172,8 @@ contract CollectionOrdersTest is ProtocolBase {

/**
* A collection offer with merkle tree criteria
*
* COLLECTION TOKEN IDs
*/
function testTakerAskCollectionOrderWithMerkleTreeERC721() public {
_setUpUsers();
Expand Down Expand Up @@ -255,6 +260,97 @@ contract CollectionOrdersTest is ProtocolBase {
looksRareProtocol.executeTakerAsk(takerAsk, makerBid, signature, _EMPTY_MERKLE_TREE);
}

/**
* TAKER ALLOWLIST
*/
function testTakerAskCollectionOrderWithMerkleTreeERC721AccountAllowlist() public {
_setUpUsers();

OrderStructs.Maker memory makerBid = _createSingleItemMakerOrder({
quoteType: QuoteType.Bid,
globalNonce: 0,
subsetNonce: 0,
strategyId: 3,
collectionType: CollectionType.ERC721,
orderNonce: 0,
collection: address(mockERC721),
currency: address(weth),
signer: makerUser,
price: price,
itemId: 0 // Not used
});

address accountInMerkleTree = takerUser;
uint256 tokenIdInMerkleTree = 2;
(bytes32 merkleRoot, bytes32[] memory proof) = _mintNFTsToOwnerAndGetMerkleRootAndProofAccountAllowlist({
owner: takerUser,
numberOfAccountsInMerkleTree: 5,
accountInMerkleTree: accountInMerkleTree
});

makerBid.additionalParameters = abi.encode(merkleRoot);

// Sign order
bytes memory signature = _signMakerOrder(makerBid, makerUserPK);

// Prepare the taker ask
OrderStructs.Taker memory takerAsk = OrderStructs.Taker(takerUser, abi.encode(tokenIdInMerkleTree, proof));

// Verify validity of maker bid order
_assertOrderIsValid(makerBid, true);
_assertValidMakerOrder(makerBid, signature);

// Execute taker ask transaction
vm.prank(takerUser);
looksRareProtocol.executeTakerAsk(takerAsk, makerBid, signature, _EMPTY_MERKLE_TREE);

_assertSuccessfulTakerAsk(makerBid, tokenIdInMerkleTree);
}

function testTakerAskCannotExecuteWithInvalidProofAccountAllowlist(uint256 itemIdSold) public {
vm.assume(itemIdSold > 5);
_setUpUsers();

OrderStructs.Maker memory makerBid = _createSingleItemMakerOrder({
quoteType: QuoteType.Bid,
globalNonce: 0,
subsetNonce: 0,
strategyId: 3,
collectionType: CollectionType.ERC721,
orderNonce: 0,
collection: address(mockERC721),
currency: address(weth),
signer: makerUser,
price: price,
itemId: 0 // Not used
});

address accountInMerkleTree = takerUser;
uint256 tokenIdInMerkleTree = 2;
(bytes32 merkleRoot, bytes32[] memory proof) = _mintNFTsToOwnerAndGetMerkleRootAndProofAccountAllowlist({
owner: takerUser,
numberOfAccountsInMerkleTree: 5,
// Doesn't matter what itemIdInMerkleTree is as we are are going to tamper with the proof
accountInMerkleTree: accountInMerkleTree
});
makerBid.additionalParameters = abi.encode(merkleRoot);

// Sign order
bytes memory signature = _signMakerOrder(makerBid, makerUserPK);

// Prepare the taker ask
proof[0] = bytes32(0); // Tamper with the proof
OrderStructs.Taker memory takerAsk = OrderStructs.Taker(takerUser, abi.encode(tokenIdInMerkleTree, proof));

// Verify validity of maker bid order
_assertOrderIsValid(makerBid, true);
_assertValidMakerOrder(makerBid, signature);

vm.prank(takerUser);
vm.expectRevert(MerkleProofInvalid.selector);
looksRareProtocol.executeTakerAsk(takerAsk, makerBid, signature, _EMPTY_MERKLE_TREE);
}

function testInvalidAmounts() public {
_setUpUsers();

Expand Down Expand Up @@ -433,6 +529,27 @@ contract CollectionOrdersTest is ProtocolBase {
assertTrue(m.verifyProof(merkleRoot, proof, merkleTreeIds[itemIdInMerkleTree]));
}

function _mintNFTsToOwnerAndGetMerkleRootAndProofAccountAllowlist(
address owner,
uint256 numberOfAccountsInMerkleTree,
address accountInMerkleTree
) private returns (bytes32 merkleRoot, bytes32[] memory proof) {
// Initialize Merkle Tree
Merkle m = new Merkle();

bytes32[] memory merkleTreeAccounts = new bytes32[](numberOfAccountsInMerkleTree);
for (uint256 i; i < numberOfAccountsInMerkleTree; i++) {
mockERC721.mint(owner, i);
merkleTreeAccounts[i] = keccak256(abi.encodePacked(accountInMerkleTree));
}

// Compute merkle root
merkleRoot = m.getRoot(merkleTreeAccounts);
proof = m.getProof(merkleTreeAccounts, 2);

assertTrue(m.verifyProof(merkleRoot, proof, merkleTreeAccounts[0]));
}

function _assertSuccessfulTakerAsk(OrderStructs.Maker memory makerBid, uint256 tokenId) private {
// Taker user has received the asset
assertEq(mockERC721.ownerOf(tokenId), makerUser);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/// SPDX-License-Identifier: MIT
pragma solidity 0.8.17;
pragma solidity ^0.8.17;

// Dependencies
import {BatchOrderTypehashRegistry} from "@hypercerts/marketplace/BatchOrderTypehashRegistry.sol";
Expand Down

0 comments on commit 5c2dc7e

Please sign in to comment.