Skip to content

Commit

Permalink
Merge pull request #153 from ourzora/call-sale-patch
Browse files Browse the repository at this point in the history
refactor: validate encoded token id for calls to fixed price minter
  • Loading branch information
kulkarohan authored Sep 26, 2023
2 parents 8651ee2 + a170f1f commit a8f33a8
Show file tree
Hide file tree
Showing 13 changed files with 142 additions and 61 deletions.
5 changes: 5 additions & 0 deletions .changeset/pink-pants-decide.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@zoralabs/zora-1155-contracts": minor
---

Add TokenId to redeemInstructionsHashIsAllowed for Redeem Contracts
6 changes: 6 additions & 0 deletions .changeset/thin-geese-flash.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@zoralabs/zora-1155-contracts": patch
---

- Ensures sales configs can only be updated for the token ids specified
- Deprecates support with 'ZoraCreatorRedeemMinterStrategy' v1.0.1
6 changes: 6 additions & 0 deletions .changeset/witty-numbers-share.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@zoralabs/zora-1155-contracts": minor
---

- Patches the 1155 `callSale` function to ensure that the token id passed matches the token id encoded in the generic calldata to forward
- Updates the redeem minter to v1.1.0 to support b2r per an 1155 token id
6 changes: 6 additions & 0 deletions .github/workflows/coverage.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,12 @@ jobs:
- name: Setup LCOV
uses: hrishikesh-kadam/setup-lcov@v1

- name: Filter files to ignore
run: |
lcov --rc lcov_branch_coverage=1 \
--remove lcov.info \
--output-file lcov.info "*node_modules*" "*test*" "*script*" "*DeploymentConfig*"
- name: Report code coverage
uses: zgosalvez/github-actions-report-lcov@v2
with:
Expand Down
2 changes: 1 addition & 1 deletion foundry.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
fs_permissions = [{access = "read", path = "./addresses"}, {access = "read", path = "./chainConfigs"}, {access = "read", path = "./package.json"}]
libs = ['_imagine', 'node_modules', 'script']
optimizer = true
optimizer_runs = 3000
optimizer_runs = 500
out = 'out'
solc_version = '0.8.17'
src = 'src'
Expand Down
1 change: 1 addition & 0 deletions src/interfaces/IZoraCreator1155.sol
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ interface IZoraCreator1155 is IZoraCreator1155TypesV1, IVersionedContract, IOwna

error Sale_CannotCallNonSalesContract(address targetContract);

error Call_TokenIdMismatch();
error CallFailed(bytes reason);
error Renderer_NotValidRendererContract();

Expand Down
2 changes: 1 addition & 1 deletion src/minters/redeem/ZoraCreatorRedeemMinterFactory.sol
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ contract ZoraCreatorRedeemMinterFactory is Enjoy, IContractMetadata, SharedBaseC

/// @notice Factory contract version
function contractVersion() external pure override returns (string memory) {
return "1.0.1";
return "1.1.0";
}

/// @notice No-op function for IMinter1155 compatibility
Expand Down
28 changes: 19 additions & 9 deletions src/minters/redeem/ZoraCreatorRedeemMinterStrategy.sol
Original file line number Diff line number Diff line change
Expand Up @@ -120,8 +120,8 @@ contract ZoraCreatorRedeemMinterStrategy is Enjoy, SaleStrategy, Initializable {
error MintTokenContractMustBeCreatorContract();
error SenderIsNotTokenOwner();

/// @notice keccak256(abi.encode(RedeemInstructions)) => redeem instructions are allowed
mapping(bytes32 => bool) public redeemInstructionsHashIsAllowed;
/// @notice tokenId, keccak256(abi.encode(RedeemInstructions)) => redeem instructions are allowed
mapping(uint256 => mapping(bytes32 => bool)) public redeemInstructionsHashIsAllowed;

/// @notice Zora creator contract
address public creatorContract;
Expand Down Expand Up @@ -152,7 +152,7 @@ contract ZoraCreatorRedeemMinterStrategy is Enjoy, SaleStrategy, Initializable {

/// @notice Redeem Minter Strategy contract version
function contractVersion() external pure override returns (string memory) {
return "1.0.1";
return "1.1.0";
}

/// @notice Redeem instructions object hash
Expand Down Expand Up @@ -210,27 +210,33 @@ contract ZoraCreatorRedeemMinterStrategy is Enjoy, SaleStrategy, Initializable {
}

/// @notice Set redeem instructions
/// @param tokenId The token id to set redeem instructions for
/// @param _redeemInstructions The redeem instructions object
function setRedeem(RedeemInstructions calldata _redeemInstructions) external onlyCreatorContract {
function setRedeem(uint256 tokenId, RedeemInstructions calldata _redeemInstructions) external onlyCreatorContract {
if (_redeemInstructions.mintToken.tokenId != tokenId) {
revert InvalidTokenIdsForTokenType();
}

validateRedeemInstructions(_redeemInstructions);

bytes32 hash = redeemInstructionsHash(_redeemInstructions);
if (redeemInstructionsHashIsAllowed[hash]) {
if (redeemInstructionsHashIsAllowed[tokenId][hash]) {
revert RedeemInstructionAlreadySet();
}
redeemInstructionsHashIsAllowed[hash] = true;
redeemInstructionsHashIsAllowed[tokenId][hash] = true;

emit RedeemSet(creatorContract, hash, _redeemInstructions);
}

/// @notice Clear redeem instructions
/// @param tokenId The token id to clear redeem instructions for
/// @param hashes Array of redeem instructions hashes to clear
function clearRedeem(bytes32[] calldata hashes) external onlyCreatorContract {
function clearRedeem(uint256 tokenId, bytes32[] calldata hashes) external onlyCreatorContract {
uint256 numHashes = hashes.length;

unchecked {
for (uint256 i; i < numHashes; ++i) {
redeemInstructionsHashIsAllowed[hashes[i]] = false;
redeemInstructionsHashIsAllowed[tokenId][hashes[i]] = false;
}
}

Expand All @@ -254,7 +260,11 @@ contract ZoraCreatorRedeemMinterStrategy is Enjoy, SaleStrategy, Initializable {
(RedeemInstructions, uint256[][], uint256[][])
);
bytes32 hash = redeemInstructionsHash(redeemInstructions);
if (!redeemInstructionsHashIsAllowed[hash]) {

if (tokenId != redeemInstructions.mintToken.tokenId) {
revert InvalidTokenIdsForTokenType();
}
if (!redeemInstructionsHashIsAllowed[tokenId][hash]) {
revert RedeemInstructionNotAllowed();
}
if (redeemInstructions.saleStart > block.timestamp) {
Expand Down
12 changes: 11 additions & 1 deletion src/nft/ZoraCreator1155Impl.sol
Original file line number Diff line number Diff line change
Expand Up @@ -506,11 +506,21 @@ contract ZoraCreator1155Impl is
/// @param tokenId The token ID to call the sale contract with
/// @param salesConfig The sales config contract to call
/// @param data The data to pass to the sales config contract
function callSale(uint256 tokenId, IMinter1155 salesConfig, bytes memory data) external onlyAdminOrRole(tokenId, PERMISSION_BIT_SALES) {
function callSale(uint256 tokenId, IMinter1155 salesConfig, bytes calldata data) external onlyAdminOrRole(tokenId, PERMISSION_BIT_SALES) {
_requireAdminOrRole(address(salesConfig), tokenId, PERMISSION_BIT_MINTER);
if (!salesConfig.supportsInterface(type(IMinter1155).interfaceId)) {
revert Sale_CannotCallNonSalesContract(address(salesConfig));
}

// Get the token id encoded in the calldata for the sales config
// Assume that it is the first 32 bytes following the function selector
uint256 encodedTokenId = uint256(bytes32(data[4:36]));

// Ensure the encoded token id matches the passed token id
if (encodedTokenId != tokenId) {
revert Call_TokenIdMismatch();
}

(bool success, bytes memory why) = address(salesConfig).call(data);
if (!success) {
revert CallFailed(why);
Expand Down
40 changes: 40 additions & 0 deletions test/minters/fixed-price/ZoraCreatorFixedPriceSaleStrategy.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -420,4 +420,44 @@ contract ZoraCreatorFixedPriceSaleStrategyTest is Test {
assertTrue(fixedPrice.supportsInterface(0x01ffc9a7));
assertFalse(fixedPrice.supportsInterface(0x0));
}

function testRevert_CannotSetSaleOfDifferentTokenId() public {
vm.startPrank(admin);
uint256 tokenId1 = target.setupNewToken("https://zora.co/testing/token.json", 10);
uint256 tokenId2 = target.setupNewToken("https://zora.co/testing/token.json", 5);

target.addPermission(tokenId1, address(fixedPrice), target.PERMISSION_BIT_MINTER());
target.addPermission(tokenId2, address(fixedPrice), target.PERMISSION_BIT_MINTER());

vm.expectRevert(abi.encodeWithSignature("Call_TokenIdMismatch()"));
target.callSale(
tokenId1,
fixedPrice,
abi.encodeWithSelector(
ZoraCreatorFixedPriceSaleStrategy.setSale.selector,
tokenId2,
ZoraCreatorFixedPriceSaleStrategy.SalesConfig({
pricePerToken: 1 ether,
saleStart: 0,
saleEnd: type(uint64).max,
maxTokensPerAddress: 0,
fundsRecipient: address(0)
})
)
);
vm.stopPrank();
}

function testRevert_CannotResetSaleOfDifferentTokenId() public {
vm.startPrank(admin);
uint256 tokenId1 = target.setupNewToken("https://zora.co/testing/token.json", 10);
uint256 tokenId2 = target.setupNewToken("https://zora.co/testing/token.json", 5);

target.addPermission(tokenId1, address(fixedPrice), target.PERMISSION_BIT_MINTER());
target.addPermission(tokenId2, address(fixedPrice), target.PERMISSION_BIT_MINTER());

vm.expectRevert(abi.encodeWithSignature("Call_TokenIdMismatch()"));
target.callSale(tokenId1, fixedPrice, abi.encodeWithSelector(ZoraCreatorFixedPriceSaleStrategy.resetSale.selector, tokenId2));
vm.stopPrank();
}
}
5 changes: 3 additions & 2 deletions test/minters/redeem/ZoraCreatorRedeemMinterFactory.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {IZoraCreator1155Factory} from "../../../src/interfaces/IZoraCreator1155F
import {ZoraCreatorRedeemMinterStrategy} from "../../../src/minters/redeem/ZoraCreatorRedeemMinterStrategy.sol";
import {ZoraCreatorRedeemMinterFactory} from "../../../src/minters/redeem/ZoraCreatorRedeemMinterFactory.sol";

/// @notice Contract versions after v1.4.0 will not support burn to redeem
contract ZoraCreatorRedeemMinterFactoryTest is Test {
ProtocolRewards internal protocolRewards;
ZoraCreator1155Impl internal target;
Expand All @@ -39,7 +40,7 @@ contract ZoraCreatorRedeemMinterFactoryTest is Test {
}

function test_contractVersion() public {
assertEq(minterFactory.contractVersion(), "1.0.1");
assertEq(minterFactory.contractVersion(), "1.1.0");
}

function test_createMinterIfNoneExists() public {
Expand All @@ -48,7 +49,7 @@ contract ZoraCreatorRedeemMinterFactoryTest is Test {
address predictedAddress = minterFactory.predictMinterAddress(address(target));
vm.expectEmit(false, false, false, false);
emit RedeemMinterDeployed(address(target), predictedAddress);
target.callSale(0, minterFactory, abi.encodeWithSelector(ZoraCreatorRedeemMinterFactory.createMinterIfNoneExists.selector));
target.callSale(0, minterFactory, abi.encodeWithSelector(ZoraCreatorRedeemMinterFactory.createMinterIfNoneExists.selector, 0));
vm.stopPrank();

ZoraCreatorRedeemMinterStrategy minter = ZoraCreatorRedeemMinterStrategy(predictedAddress);
Expand Down
Loading

0 comments on commit a8f33a8

Please sign in to comment.