From ce687664db53dc1f3be4f467877425cdbf352db0 Mon Sep 17 00:00:00 2001 From: nikki-kiga <42276368+nikki-kiga@users.noreply.github.com> Date: Fri, 7 Jun 2024 11:23:53 -0700 Subject: [PATCH] feat: [CON-944] Gacha extension (#88) * docs: add instructions to install foundry * chore: use v4 node actions * initial scaffolding Co-authored-by: jaxonL * add string tier * update gacha * small changes Co-authored-by: jaxonL * updates except mintReserve * add mintReserve and init test * more tests * update deliverMints test * update for feedback * add tokenURI test and rename itemVariations * add script and update for feedback * update structs and tests * add creatorAdmin * add test and case to reserve when totalMax is 0 * add more test cases * update tests and checks * remove extra line * clean up mintReserve logic * use numbers other than diff of 1 * modify updateClaim assertions and add extension balance assertion * update the max-uint-80 value --------- Co-authored-by: Jaxon Lin <11150920+jaxonL@users.noreply.github.com> Co-authored-by: jaxonL --- .github/workflows/ci.yml | 8 +- packages/manifold/README.md | 3 + .../gachaclaims/ERC1155GachaLazyClaim.sol | 267 +++++++ .../contracts/gachaclaims/GachaLazyClaim.sol | 80 +++ .../gachaclaims/IERC1155GachaLazyClaim.sol | 102 +++ .../contracts/gachaclaims/IGachaLazyClaim.sol | 106 +++ .../script/ERC1155GachaLazyClaim.s.sol | 41 ++ .../test/gacha/ERC1155GachaLazyClaim.t.sol | 657 ++++++++++++++++++ 8 files changed, 1260 insertions(+), 4 deletions(-) create mode 100644 packages/manifold/contracts/gachaclaims/ERC1155GachaLazyClaim.sol create mode 100644 packages/manifold/contracts/gachaclaims/GachaLazyClaim.sol create mode 100644 packages/manifold/contracts/gachaclaims/IERC1155GachaLazyClaim.sol create mode 100644 packages/manifold/contracts/gachaclaims/IGachaLazyClaim.sol create mode 100644 packages/manifold/script/ERC1155GachaLazyClaim.s.sol create mode 100644 packages/manifold/test/gacha/ERC1155GachaLazyClaim.t.sol diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 25a2dcc3..8a4aa0a5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,13 +15,13 @@ jobs: folder: ["edition", "lazywhitelist", "redeem", "manifold"] steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v4 with: ref: ${{ github.event.pull_request.head.ref }} repository: ${{ github.event.pull_request.head.repo.full_name }} - name: Set Node to v18 - uses: actions/setup-node@v2 + uses: actions/setup-node@v4 with: node-version: "18" @@ -66,13 +66,13 @@ jobs: folder: ["dynamic", "enumerable", "manifold"] steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v4 with: ref: ${{ github.event.pull_request.head.ref }} repository: ${{ github.event.pull_request.head.repo.full_name }} - name: Set Node to v8 - uses: actions/setup-node@v2 + uses: actions/setup-node@v4 with: node-version: "18" diff --git a/packages/manifold/README.md b/packages/manifold/README.md index 08cda1d3..4da565e6 100644 --- a/packages/manifold/README.md +++ b/packages/manifold/README.md @@ -87,6 +87,9 @@ See the [developer documentation](https://docs.manifold.xyz/v/manifold-for-devel npm install ``` +Make sure you have foundry installed. If not, follow the instructions here: https://book.getfoundry.sh/getting-started/installation. + + ### Compile ``` forge build diff --git a/packages/manifold/contracts/gachaclaims/ERC1155GachaLazyClaim.sol b/packages/manifold/contracts/gachaclaims/ERC1155GachaLazyClaim.sol new file mode 100644 index 00000000..3eeb5b3b --- /dev/null +++ b/packages/manifold/contracts/gachaclaims/ERC1155GachaLazyClaim.sol @@ -0,0 +1,267 @@ +// SPDX-License-Identifier: MIT +// solhint-disable reason-string +pragma solidity ^0.8.0; + +import "@manifoldxyz/creator-core-solidity/contracts/core/IERC1155CreatorCore.sol"; +import "@manifoldxyz/creator-core-solidity/contracts/extensions/ICreatorExtensionTokenURI.sol"; + +import "@openzeppelin/contracts/utils/introspection/IERC165.sol"; +import "@openzeppelin/contracts/utils/Strings.sol"; +import "@openzeppelin/contracts/utils/Address.sol"; +import "@openzeppelin/contracts/utils/math/Math.sol"; + +import "./GachaLazyClaim.sol"; +import "./IERC1155GachaLazyClaim.sol"; + +/** + * @title Gacha Lazy 1155 Payable Claim + * @author manifold.xyz + * @notice + */ +contract ERC1155GachaLazyClaim is IERC165, IERC1155GachaLazyClaim, ICreatorExtensionTokenURI, GachaLazyClaim { + using Strings for uint256; + + // stores mapping from contractAddress/instanceId to the claim it represents + // { contractAddress => { instanceId => Claim } } + mapping(address => mapping(uint256 => Claim)) private _claims; + + // { contractAddress => { tokenId => { instanceId } } + mapping(address => mapping(uint256 => uint256)) private _tokenInstances; + + function supportsInterface(bytes4 interfaceId) public view virtual override(IERC165, AdminControl) returns (bool) { + return + interfaceId == type(IERC1155GachaLazyClaim).interfaceId || + interfaceId == type(IGachaLazyClaim).interfaceId || + interfaceId == type(ICreatorExtensionTokenURI).interfaceId || + interfaceId == type(IAdminControl).interfaceId || + interfaceId == type(IERC165).interfaceId; + } + + constructor(address initialOwner) GachaLazyClaim(initialOwner) {} + + /** + * See {IERC1155GachaLazyClaim-initializeClaim}. + */ + function initializeClaim( + address creatorContractAddress, + uint256 instanceId, + ClaimParameters calldata claimParameters + ) external payable override creatorAdminRequired(creatorContractAddress) { + if (instanceId == 0 || instanceId > MAX_UINT_56) revert IGachaLazyClaim.InvalidInstance(); + if (_claims[creatorContractAddress][instanceId].storageProtocol != StorageProtocol.INVALID) + revert IGachaLazyClaim.ClaimAlreadyInitialized(); + // Checks + if (claimParameters.storageProtocol == StorageProtocol.INVALID) revert IGachaLazyClaim.InvalidStorageProtocol(); + if (claimParameters.endDate != 0 && claimParameters.startDate >= claimParameters.endDate) + revert IGachaLazyClaim.InvalidDate(); + if (claimParameters.totalMax > MAX_UINT_32) revert IGachaLazyClaim.InvalidInput(); + if (claimParameters.tokenVariations > MAX_UINT_8) revert IGachaLazyClaim.InvalidInput(); + if (claimParameters.cost > MAX_UINT_96) revert IGachaLazyClaim.InvalidInput(); + + address[] memory receivers = new address[](1); + receivers[0] = msg.sender; + uint256[] memory amounts = new uint256[](claimParameters.tokenVariations); + string[] memory uris = new string[](claimParameters.tokenVariations); + uint256[] memory newTokenIds = IERC1155CreatorCore(creatorContractAddress).mintExtensionNew(receivers, amounts, uris); + + if (newTokenIds[0] > MAX_UINT_80) revert IGachaLazyClaim.InvalidStartingTokenId(); + + // Create the claim + _claims[creatorContractAddress][instanceId] = Claim({ + storageProtocol: claimParameters.storageProtocol, + total: 0, + totalMax: claimParameters.totalMax, + startDate: claimParameters.startDate, + endDate: claimParameters.endDate, + startingTokenId: uint80(newTokenIds[0]), + tokenVariations: claimParameters.tokenVariations, + location: claimParameters.location, + paymentReceiver: claimParameters.paymentReceiver, + cost: claimParameters.cost, + erc20: claimParameters.erc20 + }); + for (uint256 i; i < claimParameters.tokenVariations; ) { + _tokenInstances[creatorContractAddress][newTokenIds[i]] = instanceId; + unchecked { + ++i; + } + } + + emit GachaClaimInitialized(creatorContractAddress, instanceId, msg.sender); + } + + /** + * See {IERC1155GachaLazyClaim-updateClaim}. + */ + function updateClaim( + address creatorContractAddress, + uint256 instanceId, + UpdateClaimParameters memory updateClaimParameters + ) external override creatorAdminRequired(creatorContractAddress) { + Claim memory claim = _getClaim(creatorContractAddress, instanceId); + if (instanceId == 0 || instanceId > MAX_UINT_56) revert IGachaLazyClaim.InvalidInstance(); + if (updateClaimParameters.endDate != 0 && updateClaimParameters.startDate >= updateClaimParameters.endDate) + revert IGachaLazyClaim.InvalidDate(); + if (updateClaimParameters.totalMax < claim.total) revert IGachaLazyClaim.CannotLowerTotalMaxBeyondTotal(); + if (updateClaimParameters.totalMax > MAX_UINT_32) revert IGachaLazyClaim.InvalidInput(); + if (updateClaimParameters.storageProtocol == StorageProtocol.INVALID) revert IGachaLazyClaim.InvalidStorageProtocol(); + if (updateClaimParameters.cost > MAX_UINT_96) revert IGachaLazyClaim.InvalidInput(); + + + // Overwrite the existing values + _claims[creatorContractAddress][instanceId] = Claim({ + storageProtocol: updateClaimParameters.storageProtocol, + total: claim.total, + totalMax: updateClaimParameters.totalMax, + startDate: updateClaimParameters.startDate, + endDate: updateClaimParameters.endDate, + startingTokenId: claim.startingTokenId, + tokenVariations: claim.tokenVariations, + location: updateClaimParameters.location, + paymentReceiver: updateClaimParameters.paymentReceiver, + cost: updateClaimParameters.cost, + erc20: claim.erc20 + }); + emit GachaClaimUpdated(creatorContractAddress, instanceId); + } + + /** + * See {IERC1155GachaLazyClaim-getClaim}. + */ + function getClaim(address creatorContractAddress, uint256 instanceId) public view override returns (Claim memory) { + return _getClaim(creatorContractAddress, instanceId); + } + + /** + * See {IERC1155GachaLazyClaim-getClaimForToken}. + */ + function getClaimForToken( + address creatorContractAddress, + uint256 tokenId + ) external view override returns (uint256 instanceId, Claim memory claim) { + instanceId = _tokenInstances[creatorContractAddress][tokenId]; + claim = _getClaim(creatorContractAddress, instanceId); + } + + function _getClaim(address creatorContractAddress, uint256 instanceId) private view returns (Claim storage claim) { + claim = _claims[creatorContractAddress][instanceId]; + if (claim.storageProtocol == StorageProtocol.INVALID) revert IGachaLazyClaim.ClaimNotInitialized(); + } + + /** + * See {IGachaLazyClaim-mintReserve}. + */ + function mintReserve(address creatorContractAddress, uint256 instanceId, uint32 mintCount) external payable override { + if (Address.isContract(msg.sender)) revert IGachaLazyClaim.CannotMintFromContract(); + Claim storage claim = _getClaim(creatorContractAddress, instanceId); + // Checks for reserving + if (mintCount == 0 || mintCount >= MAX_UINT_32) revert IGachaLazyClaim.InvalidMintCount(); + if (claim.startDate > block.timestamp || (claim.endDate > 0 && claim.endDate < block.timestamp)) revert IGachaLazyClaim.ClaimInactive(); + if (claim.totalMax != 0 && claim.total == claim.totalMax) revert IGachaLazyClaim.ClaimSoldOut(); + if (claim.total == MAX_UINT_32) revert IGachaLazyClaim.TooManyRequested(); + if (msg.value != (claim.cost + MINT_FEE) * mintCount) revert IGachaLazyClaim.InvalidPayment(); + // calculate the amount to reserve and update totals + uint32 amountToReserve = mintCount; + if (claim.totalMax != 0) { + amountToReserve = uint32(Math.min(mintCount, claim.totalMax - claim.total)); + } + claim.total += amountToReserve; + _mintDetailsPerWallet[creatorContractAddress][instanceId][msg.sender].reservedCount += amountToReserve; + if (claim.cost > 0) { + _sendFunds(claim.paymentReceiver, claim.cost * amountToReserve); + } + // Refund any overpayment + if (amountToReserve != mintCount) { + uint256 refundAmount = msg.value - (claim.cost + MINT_FEE) * amountToReserve; + _sendFunds(payable(msg.sender), refundAmount); + } + emit GachaClaimMintReserved(creatorContractAddress, instanceId, msg.sender, amountToReserve); + } + + /** + * See {IGachaLazyClaim-deliverMints}. + */ + function deliverMints(IGachaLazyClaim.ClaimMint[] calldata mints) external override { + _validateSigner(); + for (uint256 i; i < mints.length; ) { + ClaimMint calldata mintData = mints[i]; + Claim memory claim = _getClaim(mintData.creatorContractAddress, mintData.instanceId); + address[] memory receivers = new address[](mintData.variationMints.length); + uint256[] memory amounts = new uint256[](mintData.variationMints.length); + uint256[] memory tokenIds = new uint256[](mintData.variationMints.length); + + for (uint256 j; j < mintData.variationMints.length; ) { + VariationMint calldata variationMint = mintData.variationMints[j]; + if (variationMint.variationIndex > MAX_UINT_8) revert IGachaLazyClaim.InvalidVariationIndex(); + uint8 variationIndex = variationMint.variationIndex; + if (variationIndex > claim.tokenVariations || variationIndex < 1) revert IGachaLazyClaim.InvalidVariationIndex(); + address recipient = variationMint.recipient; + if (variationMint.amount > MAX_UINT_32) revert IGachaLazyClaim.TooManyRequested(); + uint32 amount = variationMint.amount; + UserMintDetails storage userMintDetails = _mintDetailsPerWallet[mintData.creatorContractAddress][ + mintData.instanceId + ][recipient]; + + if (userMintDetails.deliveredCount + amount > userMintDetails.reservedCount) + revert IGachaLazyClaim.CannotMintMoreThanReserved(); + if (claim.startingTokenId > MAX_UINT_80) revert IGachaLazyClaim.InvalidStartingTokenId(); + tokenIds[j] = claim.startingTokenId + variationIndex - 1; + amounts[j] = amount; + receivers[j] = recipient; + userMintDetails.deliveredCount += amount; + unchecked { + j++; + } + } + + IERC1155CreatorCore(mintData.creatorContractAddress).mintExtensionExisting(receivers, tokenIds, amounts); + unchecked { + i++; + } + } + } + + /** + * See {IGachaLazyClaim-getUserMints}. + */ + function getUserMints( + address minter, + address creatorContractAddress, + uint256 instanceId + ) external view override returns (UserMintDetails memory) { + return _getUserMints(minter, creatorContractAddress, instanceId); + } + + /** + * See {ICreatorExtensionTokenURI-tokenURI}. + */ + function tokenURI(address creatorContractAddress, uint256 tokenId) external view override returns (string memory uri) { + uint256 instanceId = _tokenInstances[creatorContractAddress][tokenId]; + if (instanceId == 0) revert IGachaLazyClaim.TokenDNE(); + Claim memory claim = _getClaim(creatorContractAddress, instanceId); + + string memory prefix = ""; + if (claim.storageProtocol == StorageProtocol.ARWEAVE) { + prefix = ARWEAVE_PREFIX; + } else if (claim.storageProtocol == StorageProtocol.IPFS) { + prefix = IPFS_PREFIX; + } + uri = string(abi.encodePacked(prefix, claim.location, "/", Strings.toString(tokenId - claim.startingTokenId + 1))); + } + + /** + * See {IERC1155GachaLazyClaim-updateTokenURIParams}. + */ + function updateTokenURIParams( + address creatorContractAddress, + uint256 instanceId, + StorageProtocol storageProtocol, + string calldata location + ) external override creatorAdminRequired(creatorContractAddress) { + Claim storage claim = _getClaim(creatorContractAddress, instanceId); + if (storageProtocol == StorageProtocol.INVALID) revert IGachaLazyClaim.InvalidStorageProtocol(); + claim.storageProtocol = storageProtocol; + claim.location = location; + emit GachaClaimUpdated(creatorContractAddress, instanceId); + } +} diff --git a/packages/manifold/contracts/gachaclaims/GachaLazyClaim.sol b/packages/manifold/contracts/gachaclaims/GachaLazyClaim.sol new file mode 100644 index 00000000..50fc256e --- /dev/null +++ b/packages/manifold/contracts/gachaclaims/GachaLazyClaim.sol @@ -0,0 +1,80 @@ +// SPDX-License-Identifier: MIT +// solhint-disable reason-string +pragma solidity ^0.8.0; + +import "@manifoldxyz/libraries-solidity/contracts/access/AdminControl.sol"; + +import "./IGachaLazyClaim.sol"; + +/** + * @title Gacha Lazy Claim + * @author manifold.xyz + */ +abstract contract GachaLazyClaim is IGachaLazyClaim, AdminControl { + using EnumerableSet for EnumerableSet.AddressSet; + + string internal constant ARWEAVE_PREFIX = "https://arweave.net/"; + string internal constant IPFS_PREFIX = "ipfs://"; + address internal _signer; + + uint256 internal constant MAX_UINT_8 = 0xff; + uint256 internal constant MAX_UINT_32 = 0xffffffff; + uint256 internal constant MAX_UINT_48 = 0xffffffffffff; + uint256 internal constant MAX_UINT_56 = 0xffffffffffffff; + uint256 internal constant MAX_UINT_80 = 0xffffffffffffffffffff; + uint256 internal constant MAX_UINT_96 = 0xffffffffffffffffffffffff; + address internal constant ADDRESS_ZERO = 0x0000000000000000000000000000000000000000; + + uint256 public constant MINT_FEE = 500000000000000; + + // { contractAddress => { instanceId => { walletAddress => UserMintDetails } } } + mapping(address => mapping(uint256 => mapping(address => UserMintDetails))) internal _mintDetailsPerWallet; + + /** + * @notice This extension is shared, not single-creator. So we must ensure + * that a claim's initializer is an admin on the creator contract + * @param creatorContractAddress the address of the creator contract to check the admin against + */ + modifier creatorAdminRequired(address creatorContractAddress) { + AdminControl creatorCoreContract = AdminControl(creatorContractAddress); + require(creatorCoreContract.isAdmin(msg.sender), "Wallet is not an administrator for contract"); + _; + } + + constructor(address initialOwner) { + _transferOwnership(initialOwner); + } + + /** + * See {IGachaLazyClaim-withdraw}. + */ + function withdraw(address payable receiver, uint256 amount) external override adminRequired { + (bool sent, ) = receiver.call{ value: amount }(""); + if (!sent) revert IGachaLazyClaim.FailedToTransfer(); + } + + /** + * See {IGachaLazyClaim-setSigner}. + */ + function setSigner(address signer) external override adminRequired { + _signer = signer; + } + + function _validateSigner() internal view { + if (msg.sender != _signer) revert IGachaLazyClaim.InvalidSignature(); + } + + function _getUserMints( + address minter, + address creatorContractAddress, + uint256 instanceId + ) internal view returns (UserMintDetails memory) { + return (_mintDetailsPerWallet[creatorContractAddress][instanceId][minter]); + } + + function _sendFunds(address payable recipient, uint256 amount) internal { + if (recipient == ADDRESS_ZERO) revert FailedToTransfer(); + (bool sent, ) = recipient.call{ value: amount }(""); + if (!sent) revert FailedToTransfer(); + } +} diff --git a/packages/manifold/contracts/gachaclaims/IERC1155GachaLazyClaim.sol b/packages/manifold/contracts/gachaclaims/IERC1155GachaLazyClaim.sol new file mode 100644 index 00000000..98e41406 --- /dev/null +++ b/packages/manifold/contracts/gachaclaims/IERC1155GachaLazyClaim.sol @@ -0,0 +1,102 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +/// @author: manifold.xyz + +import "./IGachaLazyClaim.sol"; + +/** + * Gacha 1155 Lazy Claim interface + */ +interface IERC1155GachaLazyClaim is IGachaLazyClaim { + struct Claim { + StorageProtocol storageProtocol; + uint32 total; + uint32 totalMax; + uint48 startDate; + uint48 endDate; + uint80 startingTokenId; + uint8 tokenVariations; + string location; + address payable paymentReceiver; + uint96 cost; + address erc20; + } + + struct ClaimParameters { + StorageProtocol storageProtocol; + uint32 totalMax; + uint48 startDate; + uint48 endDate; + uint8 tokenVariations; + string location; + address payable paymentReceiver; + uint96 cost; + address erc20; + } + + struct UpdateClaimParameters { + StorageProtocol storageProtocol; + address payable paymentReceiver; + uint32 totalMax; + uint48 startDate; + uint48 endDate; + uint96 cost; + string location; + } + + /** + * @notice initialize a new claim, emit initialize event + * @param creatorContractAddress the creator contract the claim will mint tokens for + * @param instanceId the claim instanceId for the creator contract + * @param claimParameters the parameters which will affect the minting behavior of the claim + */ + function initializeClaim( + address creatorContractAddress, + uint256 instanceId, + ClaimParameters calldata claimParameters + ) external payable; + + /** + * @notice update an existing claim at instanceId + * @param creatorContractAddress the creator contract corresponding to the claim + * @param instanceId the claim instanceId for the creator contract + * @param updateClaimParameters the updateable parameters that affect the minting behavior of the claim + */ + function updateClaim( + address creatorContractAddress, + uint256 instanceId, + UpdateClaimParameters calldata updateClaimParameters + ) external; + + /** + * @notice get a claim corresponding to a creator contract and instanceId + * @param creatorContractAddress the address of the creator contract + * @param instanceId the claim instanceId for the creator contract + * @return the claim object + */ + function getClaim(address creatorContractAddress, uint256 instanceId) external view returns (Claim memory); + + /** + * @notice get a claim corresponding to a token + * @param creatorContractAddress the address of the creator contract + * @param tokenId the tokenId of the claim + * @return the claim instanceId and claim object + */ + function getClaimForToken(address creatorContractAddress, uint256 tokenId) external view returns (uint256, Claim memory); + + /** + * @notice update tokenURI for an existing token + * @param creatorContractAddress the creator contract corresponding to the burn redeem + * @param instanceId the instanceId of the burnRedeem for the creator contract + * @param storageProtocol the storage protocol for the metadata + * @param location the location of the metadata + */ + function updateTokenURIParams( + address creatorContractAddress, + uint256 instanceId, + StorageProtocol storageProtocol, + string calldata location + ) external; +} diff --git a/packages/manifold/contracts/gachaclaims/IGachaLazyClaim.sol b/packages/manifold/contracts/gachaclaims/IGachaLazyClaim.sol new file mode 100644 index 00000000..708c13e8 --- /dev/null +++ b/packages/manifold/contracts/gachaclaims/IGachaLazyClaim.sol @@ -0,0 +1,106 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +/// @author: manifold.xyz + +/** + * Gacha Lazy Claim interface + */ +interface IGachaLazyClaim { + enum StorageProtocol { + INVALID, + NONE, + ARWEAVE, + IPFS + } + + error InvalidStorageProtocol(); + error InvalidDate(); + error InvalidInstance(); + error InvalidInput(); + error InvalidPayment(); + error InvalidSignature(); + error InvalidMintCount(); + error InvalidVariationIndex(); + error InvalidStartingTokenId(); + error ClaimAlreadyInitialized(); + error ClaimNotInitialized(); + error ClaimInactive(); + error ClaimSoldOut(); + error TokenDNE(); + error FailedToTransfer(); + error TooManyRequested(); + error CannotLowerTotalMaxBeyondTotal(); + error CannotChangeTokenVariations(); + error CannotChangePaymentToken(); + error CannotLowertokenVariationsBeyondVariations(); + error CannotMintMoreThanReserved(); + error CannotMintFromContract(); + + event GachaClaimInitialized(address indexed creatorContract, uint256 indexed instanceId, address initializer); + event GachaClaimUpdated(address indexed creatorContract, uint256 indexed instanceId); + event GachaClaimMintReserved( + address indexed creatorContract, + uint256 indexed instanceId, + address indexed collector, + uint32 mintCount + ); + + struct VariationMint { + uint8 variationIndex; + uint32 amount; + address recipient; + } + + struct ClaimMint { + address creatorContractAddress; + uint256 instanceId; + VariationMint[] variationMints; + } + + struct UserMintDetails { + uint32 reservedCount; + uint32 deliveredCount; + } + + /** + * @notice Set the signing address + * @param signer the signer address + */ + function setSigner(address signer) external; + + /** + * @notice Withdraw funds + */ + function withdraw(address payable receiver, uint256 amount) external; + + /** + * @notice minting request + * @param creatorContractAddress the creator contract address + * @param instanceId the claim instanceId for the creator contract + * @param mintCount the number of claims to mint + */ + function mintReserve(address creatorContractAddress, uint256 instanceId, uint32 mintCount) external payable; + + /** + * @notice Deliver NFTs + * initiated after be has handled randomization + * @param mints the mints to deliver with creatorcontractaddress, instanceId and variationMints + */ + function deliverMints(ClaimMint[] calldata mints) external; + + /** + * @notice get mints made for a wallet + * + * @param minter the address of the minting address + * @param creatorContractAddress the address of the creator contract for the claim + * @param instanceId the claim instance for the creator contract + * @return userMintdetails the wallet's reservedCount and deliveredCount + */ + function getUserMints( + address minter, + address creatorContractAddress, + uint256 instanceId + ) external view returns (UserMintDetails memory); +} diff --git a/packages/manifold/script/ERC1155GachaLazyClaim.s.sol b/packages/manifold/script/ERC1155GachaLazyClaim.s.sol new file mode 100644 index 00000000..8cffe1eb --- /dev/null +++ b/packages/manifold/script/ERC1155GachaLazyClaim.s.sol @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import "forge-std/Script.sol"; +import "../contracts/gachaclaims/ERC1155GachaLazyClaim.sol"; + +/** + Pro tip for testing! The private key can be whatever. This is what I did to test. + + 1. Uncomment the lines below and follow the instructions there to change the code. + 2. Run the script, like `forge script script/ERC1155GachaLazyClaim.s.sol:DeployERC1155GachaLazyClaim --rpc-url https://eth-sepolia.g.alchemy.com/v2/xxx --broadcast` + 3. It will print out the address, but give you an out of eth error. + 4. Now you have the address, use your real wallet and send it some sepolia eth. + 5. Now, run the script again. It will deploy and transfer the contract to your wallet. + + In the end, you just basically used a random pk in the moment to deploy. You never had + to expose your personal pk to your mac's environment variable or anything. + */ +contract DeployERC1155GachaLazyClaim is Script { + function run() external { + address initialOwner = 0x07297ddf5AAa3Fa3846D258EED663eb76C18D794; // uncomment this and put in your printed out wallet address based on fake pk on sepolia + // address initialOwner = vm.envAddress("INITIAL_OWNER"); // comment this out on sepolia + + uint pk = 69696969696969996969996969; + address addr = vm.addr(pk); + console.log(addr); + + require(initialOwner != address(0), "Initial owner address not set. Please configure INITIAL_OWNER."); + + uint256 deployerPrivateKey = pk; // uncomment this when testing on sepolia + // uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY"); // comment this out when testing on sepolia + vm.startBroadcast(deployerPrivateKey); + + // forge script script/ERC1155GachaLazyClaim.s.sol:DeployERC1155GachaLazyClaim --optimizer-runs 1000 --rpc-url --broadcast + // forge verify-contract --compiler-version 0.8.17 --optimizer-runs 1000 --chain sepolia 0x6664775828c892d06a18cc5599bff9c7781f018f contracts/gachaclaims/ERC1155GachaLazyClaim.sol:ERC1155GachaLazyClaim --constructor-args $(cast abi-encode "constructor(address)" "${INITIAL_OWNER}") --watch + new ERC1155GachaLazyClaim{salt: 0x16091cc3cd908d7d973f650f59bd476ac79090f0358f87c50a7f5caee0835a84}(initialOwner); + vm.stopBroadcast(); + } +} + +// forge verify-contract --compiler-version 0.8.17 --optimizer-runs 1000 --chain sepolia 0xa160a0a48b6ddb9ffd01a7b85e0c2b331c912e29 contracts/gachaclaims/ERC1155GachaLazyClaim.sol:ERC1155GachaLazyClaim --constructor-args $(cast abi-encode "constructor(address)" "0x07297ddf5AAa3Fa3846D258EED663eb76C18D794") --watch \ No newline at end of file diff --git a/packages/manifold/test/gacha/ERC1155GachaLazyClaim.t.sol b/packages/manifold/test/gacha/ERC1155GachaLazyClaim.t.sol new file mode 100644 index 00000000..80fa4f46 --- /dev/null +++ b/packages/manifold/test/gacha/ERC1155GachaLazyClaim.t.sol @@ -0,0 +1,657 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.0; + +import "forge-std/Test.sol"; +import "../../contracts/gachaclaims/IERC1155GachaLazyClaim.sol"; +import "../../contracts/gachaclaims/ERC1155GachaLazyClaim.sol"; +import "../../contracts/gachaclaims/IGachaLazyClaim.sol"; +import "../../contracts/gachaclaims/GachaLazyClaim.sol"; + +import "@manifoldxyz/creator-core-solidity/contracts/ERC1155Creator.sol"; +import "@openzeppelin/contracts/utils/math/SafeMath.sol"; +import "../mocks/Mock.sol"; + +contract ERC1155GachaLazyClaimTest is Test { + using SafeMath for uint256; + + ERC1155GachaLazyClaim public example; + ERC1155 public erc1155; + ERC1155Creator public creatorCore1; + ERC1155Creator public creatorCore2; + + address public creator = 0xc78Dc443c126af6E4f6Ed540c1e740C1b5be09cd; + address public owner = 0x6140F00e4Ff3936702E68744f2b5978885464cbB; + address public signingAddress = 0xc78dC443c126Af6E4f6eD540C1E740c1B5be09CE; + address public other = 0x5174cD462b60c536eb51D4ceC1D561D3Ea31004F; + address public other2 = 0x80AAC46bbd3C2FcE33681541a52CacBEd14bF425; + + address public zeroAddress = address(0); + + uint256 privateKey = 0x1010101010101010101010101010101010101010101010101010101010101010; + + uint32 MAX_UINT_32 = 0xffffffff; + uint256 public constant MINT_FEE = 500000000000000; + + // Test setup + function setUp() public { + vm.startPrank(creator); + creatorCore1 = new ERC1155Creator("Token1", "NFT1"); + creatorCore2 = new ERC1155Creator("Token2", "NFT2"); + vm.stopPrank(); + + vm.startPrank(owner); + example = new ERC1155GachaLazyClaim(owner); + example.setSigner(address(signingAddress)); + vm.stopPrank(); + + vm.startPrank(creator); + creatorCore1.registerExtension(address(example), "override"); + creatorCore2.registerExtension(address(example), "override"); + vm.stopPrank(); + + vm.deal(creator, 2147483647500004294967295); + vm.deal(other, 10 ether); + vm.deal(other2, 10 ether); + } + + function testAccess() public { + vm.startPrank(other); + // Must be admin + vm.expectRevert(); + example.withdraw(payable(other), 20); + vm.expectRevert("AdminControl: Must be owner or admin"); + example.setSigner(other); + // Must be admin + vm.expectRevert(); + + uint48 nowC = uint48(block.timestamp); + uint48 later = nowC + 1000; + + IERC1155GachaLazyClaim.ClaimParameters memory claimP = IERC1155GachaLazyClaim.ClaimParameters({ + storageProtocol: IGachaLazyClaim.StorageProtocol.IPFS, + totalMax: 100, + startDate: nowC, + endDate: later, + tokenVariations: 5, + location: "arweaveHash1", + paymentReceiver: payable(creator), + cost: 0.01 ether, + erc20: zeroAddress + }); + // Must be admin + vm.expectRevert(); + example.initializeClaim(address(creatorCore1), 1, claimP); + // Succeeds because is admin + vm.stopPrank(); + vm.startPrank(creator); + example.initializeClaim(address(creatorCore1), 1, claimP); + // Try as a different non admin + vm.stopPrank(); + vm.startPrank(other2); + vm.expectRevert(); + example.initializeClaim(address(creatorCore1), 2, claimP); + vm.expectRevert(); + + vm.stopPrank(); + } + + function testinitializeClaimSanitization() public { + vm.startPrank(creator); + + uint48 nowC = uint48(block.timestamp); + uint48 later = nowC + 1000; + + IERC1155GachaLazyClaim.ClaimParameters memory claimP = IERC1155GachaLazyClaim.ClaimParameters({ + storageProtocol: IGachaLazyClaim.StorageProtocol.INVALID, + location: "arweaveHash1", + totalMax: 100, + startDate: nowC, + endDate: later, + tokenVariations: 5, + paymentReceiver: payable(other), + cost: 1, + erc20: zeroAddress + }); + + vm.expectRevert(IGachaLazyClaim.InvalidStorageProtocol.selector); + example.initializeClaim(address(creatorCore1), 1, claimP); + + claimP.storageProtocol = IGachaLazyClaim.StorageProtocol.ARWEAVE; + claimP.startDate = nowC + 2000; + vm.expectRevert(IGachaLazyClaim.InvalidDate.selector); + example.initializeClaim(address(creatorCore1), 1, claimP); + + // successful initialization with no end date + claimP.endDate = 0; + example.initializeClaim(address(creatorCore1), 1, claimP); + vm.stopPrank(); + } + + function testUpdateClaimSanitization() public { + vm.startPrank(creator); + + uint48 nowC = uint48(block.timestamp); + uint48 later = nowC + 1000; + IERC1155GachaLazyClaim.ClaimParameters memory claimP = IERC1155GachaLazyClaim.ClaimParameters({ + storageProtocol: IGachaLazyClaim.StorageProtocol.IPFS, + location: "arweaveHash1", + totalMax: 100, + startDate: nowC, + endDate: later, + tokenVariations: 5, + paymentReceiver: payable(other), + cost: 1, + erc20: zeroAddress + }); + example.initializeClaim(address(creatorCore1), 1, claimP); + + IERC1155GachaLazyClaim.UpdateClaimParameters memory claimU = IERC1155GachaLazyClaim.UpdateClaimParameters({ + storageProtocol: IGachaLazyClaim.StorageProtocol.ARWEAVE, + totalMax: 100, + startDate: nowC, + endDate: later, + location: "arweaveHash1", + paymentReceiver: payable(creator), + cost: 1 + }); + + example.mintReserve{ value: 1 + MINT_FEE }(address(creatorCore1), 1, 1); + + claimU.storageProtocol = IGachaLazyClaim.StorageProtocol.INVALID; + vm.expectRevert(IGachaLazyClaim.InvalidStorageProtocol.selector); + example.updateClaim(address(creatorCore1), 1, claimU); + + claimU.storageProtocol = IGachaLazyClaim.StorageProtocol.ARWEAVE; + claimU.totalMax = 0; + vm.expectRevert(IGachaLazyClaim.CannotLowerTotalMaxBeyondTotal.selector); + example.updateClaim(address(creatorCore1), 1, claimU); + + claimU.totalMax = 100; + claimU.startDate = nowC + 2000; + vm.expectRevert(IGachaLazyClaim.InvalidDate.selector); + example.updateClaim(address(creatorCore1), 1, claimU); + + claimU.endDate = nowC; + vm.expectRevert(IGachaLazyClaim.InvalidDate.selector); + example.updateClaim(address(creatorCore1), 1, claimU); + claimU.endDate = later; + + //successful data and cost update + claimU.cost = 2; + claimU.storageProtocol = IGachaLazyClaim.StorageProtocol.IPFS; + claimU.startDate = nowC + 1000; + claimU.endDate = later + 3000; + claimU.totalMax = 1; + example.updateClaim(address(creatorCore1), 1, claimU); + IERC1155GachaLazyClaim.Claim memory claim = example.getClaim(address(creatorCore1), 1); + assertEq(claim.cost, 2); + // storage protocol for IPFS is 3 + assertEq(uint(claim.storageProtocol), 3); + assertEq(claim.startDate, nowC + 1000); + assertEq(claim.endDate, later + 3000); + assertEq(claim.totalMax, 1); + + // able to update to no end or start date + claimU.startDate = 0; + claimU.endDate = 0; + example.updateClaim(address(creatorCore1), 1, claimU); + claim = example.getClaim(address(creatorCore1), 1); + assertEq(claim.startDate, 0); + assertEq(claim.endDate, 0); + + claimU.startDate = nowC; + claimU.endDate = 0; + example.updateClaim(address(creatorCore1), 1, claimU); + claim = example.getClaim(address(creatorCore1), 1); + assertEq(claim.startDate, nowC); + assertEq(claim.endDate, 0); + + vm.stopPrank(); + } + + function testMintReserveLowPayment() public { + vm.startPrank(creator); + + uint48 nowC = 0; + uint48 later = uint48(block.timestamp) + 2000; + + IERC1155GachaLazyClaim.ClaimParameters memory claimP = IERC1155GachaLazyClaim.ClaimParameters({ + storageProtocol: IGachaLazyClaim.StorageProtocol.ARWEAVE, + totalMax: 100, + startDate: nowC, + endDate: later, + tokenVariations: 5, + location: "arweaveHash1", + paymentReceiver: payable(creator), + cost: 1, + erc20: zeroAddress + }); + + example.initializeClaim(address(creatorCore1), 1, claimP); + + // Insufficient payment + vm.expectRevert(IGachaLazyClaim.InvalidPayment.selector); + example.mintReserve{ value: 1 }(address(creatorCore1), 1, 2); + + vm.stopPrank(); + } + + function testMintReserveEarly() public { + // claim hasn't started yet + vm.startPrank(creator); + + uint48 start = uint48(block.timestamp) + 2000; + uint48 end = 0; + + IERC1155GachaLazyClaim.ClaimParameters memory claimP = IERC1155GachaLazyClaim.ClaimParameters({ + storageProtocol: IGachaLazyClaim.StorageProtocol.ARWEAVE, + totalMax: 100, + startDate: start, + endDate: end, + tokenVariations: 5, + location: "arweaveHash1", + paymentReceiver: payable(creator), + cost: 1, + erc20: zeroAddress + }); + example.initializeClaim(address(creatorCore1), 1, claimP); + vm.expectRevert(IGachaLazyClaim.ClaimInactive.selector); + example.mintReserve{ value: 3 }(address(creatorCore1), 1, 1); + vm.stopPrank(); + } + + function testMintReserveLate() public { + vm.startPrank(creator); + + uint48 start = 0; + uint48 end = uint48(block.timestamp.sub(1)); + + IERC1155GachaLazyClaim.ClaimParameters memory claimP = IERC1155GachaLazyClaim.ClaimParameters({ + storageProtocol: IGachaLazyClaim.StorageProtocol.ARWEAVE, + totalMax: 100, + startDate: start, + endDate: end, + tokenVariations: 5, + location: "arweaveHash1", + paymentReceiver: payable(creator), + cost: 1, + erc20: zeroAddress + }); + example.initializeClaim(address(creatorCore1), 1, claimP); + vm.stopPrank(); + + vm.startPrank(other); + example.mintReserve{ value: 1 + MINT_FEE }(address(creatorCore1), 1, 1); + vm.stopPrank(); + } + + function testMintReserveSoldout() public { + vm.startPrank(creator); + + uint48 start = 0; + uint48 end = uint48(block.timestamp) + 2000; + + IERC1155GachaLazyClaim.ClaimParameters memory claimP = IERC1155GachaLazyClaim.ClaimParameters({ + storageProtocol: IGachaLazyClaim.StorageProtocol.ARWEAVE, + totalMax: 1, + startDate: start, + endDate: end, + tokenVariations: 5, + location: "arweaveHash1", + paymentReceiver: payable(creator), + cost: 1, + erc20: zeroAddress + }); + example.initializeClaim(address(creatorCore1), 1, claimP); + example.mintReserve{ value: 1 + MINT_FEE }(address(creatorCore1), 1, 1); + vm.stopPrank(); + + vm.startPrank(other); + vm.expectRevert(IGachaLazyClaim.ClaimSoldOut.selector); + example.mintReserve{ value: 1 + MINT_FEE }(address(creatorCore1), 1, 1); + vm.stopPrank(); + } + + function testMintReserveNone() public { + vm.startPrank(creator); + + uint48 start = 0; + uint48 end = uint48(block.timestamp) + 2000; + + IERC1155GachaLazyClaim.ClaimParameters memory claimP = IERC1155GachaLazyClaim.ClaimParameters({ + storageProtocol: IGachaLazyClaim.StorageProtocol.ARWEAVE, + totalMax: 1, + startDate: start, + endDate: end, + tokenVariations: 5, + location: "arweaveHash1", + paymentReceiver: payable(creator), + cost: 1, + erc20: zeroAddress + }); + example.initializeClaim(address(creatorCore1), 1, claimP); + vm.expectRevert(IGachaLazyClaim.InvalidMintCount.selector); + example.mintReserve{ value: 1 + MINT_FEE }(address(creatorCore1), 1, 0); + vm.stopPrank(); + } + + function testMintReserveTooMany() public { + vm.startPrank(creator); + + uint48 start = 0; + uint48 end = uint48(block.timestamp) + 2000; + + IERC1155GachaLazyClaim.ClaimParameters memory claimP = IERC1155GachaLazyClaim.ClaimParameters({ + storageProtocol: IGachaLazyClaim.StorageProtocol.ARWEAVE, + totalMax: 0, + startDate: start, + endDate: end, + tokenVariations: 2, + location: "arweaveHash1", + paymentReceiver: payable(creator), + cost: 1, + erc20: zeroAddress + }); + example.initializeClaim(address(creatorCore1), 1, claimP); + vm.stopPrank(); + vm.startPrank(other); + example.mintReserve{ value: 1 + MINT_FEE }(address(creatorCore1), 1, 1); + vm.stopPrank(); + + vm.startPrank(creator); + vm.expectRevert(IGachaLazyClaim.InvalidMintCount.selector); + example.mintReserve{ value: (1 + MINT_FEE) * MAX_UINT_32 }(address(creatorCore1), 1, MAX_UINT_32); + + // max out mints + example.mintReserve{ value: (1 + MINT_FEE) * (MAX_UINT_32 - 1) }(address(creatorCore1), 1, MAX_UINT_32 - 1); + + // try to mint one more + vm.expectRevert(IGachaLazyClaim.TooManyRequested.selector); + example.mintReserve{ value: 1 + MINT_FEE }(address(creatorCore1), 1, 1); + vm.stopPrank(); + } + + function testMintReserveDeliverTotalMax0() public { + vm.startPrank(creator); + + uint48 start = 0; + uint48 end = 0; + + IERC1155GachaLazyClaim.ClaimParameters memory claimP = IERC1155GachaLazyClaim.ClaimParameters({ + storageProtocol: IGachaLazyClaim.StorageProtocol.ARWEAVE, + totalMax: 0, + startDate: start, + endDate: end, + tokenVariations: 5, + location: "arweaveHash1", + paymentReceiver: payable(creator), + cost: 1, + erc20: zeroAddress + }); + example.initializeClaim(address(creatorCore1), 1, claimP); + vm.stopPrank(); + + // should be able to reserve mint even if totalMax is 0 + vm.startPrank(other); + example.mintReserve{ value: 1 + MINT_FEE }(address(creatorCore1), 1, 1); + vm.stopPrank(); + + vm.startPrank(signingAddress); + IGachaLazyClaim.ClaimMint[] memory mints = new IGachaLazyClaim.ClaimMint[](1); + IGachaLazyClaim.VariationMint[] memory variationMints = new IGachaLazyClaim.VariationMint[](1); + variationMints[0] = IGachaLazyClaim.VariationMint({ variationIndex: 1, amount: 1, recipient: other }); + mints[0] = IGachaLazyClaim.ClaimMint({ + creatorContractAddress: address(creatorCore1), + instanceId: 1, + variationMints: variationMints + }); + example.deliverMints(mints); + vm.stopPrank(); + } + + function testMintReserveMoreThanAvailable() public { + vm.startPrank(creator); + + uint48 start = 0; + uint48 end = uint48(block.timestamp) + 2000; + + uint256 collectorBalanceBefore = address(other).balance; + uint96 mintPrice = 1 ether; + + IERC1155GachaLazyClaim.ClaimParameters memory claimP = IERC1155GachaLazyClaim.ClaimParameters({ + storageProtocol: IGachaLazyClaim.StorageProtocol.ARWEAVE, + totalMax: 4, + startDate: start, + endDate: end, + tokenVariations: 5, + location: "arweaveHash1", + paymentReceiver: payable(creator), + cost: mintPrice, + erc20: zeroAddress + }); + example.initializeClaim(address(creatorCore1), 1, claimP); + example.mintReserve{ value: mintPrice + MINT_FEE }(address(creatorCore1), 1, 1); + uint256 creatorBalanceBefore = address(creator).balance; + // the manifoled fee is saved to the extension contract + uint256 extensionBalanceBefore = address(example).balance; + vm.stopPrank(); + + vm.startPrank(other); + example.mintReserve{ value: (mintPrice + MINT_FEE) * 5 }(address(creatorCore1), 1, 5); + vm.stopPrank(); + + //confirm user + GachaLazyClaim.UserMintDetails memory userMintDetails = example.getUserMints(other, address(creatorCore1), 1); + assertEq(userMintDetails.reservedCount, 3); + + //check payment balances: for creator and collector, difference should be for only three mints instead of 5 + uint creatorBalanceAfter = address(creator).balance; + assertEq(creatorBalanceAfter, creatorBalanceBefore + mintPrice * 3); + assertEq(address(other).balance, collectorBalanceBefore - ((mintPrice + MINT_FEE) * 3)); + assertEq(address(example).balance, extensionBalanceBefore + MINT_FEE * 3); + } + + function testInvalidSigner() public { + vm.startPrank(creator); + uint48 nowC = uint48(block.timestamp); + uint48 later = nowC + 1000; + + IERC1155GachaLazyClaim.ClaimParameters memory claimP = IERC1155GachaLazyClaim.ClaimParameters({ + storageProtocol: IGachaLazyClaim.StorageProtocol.ARWEAVE, + totalMax: 100, + startDate: nowC, + endDate: later, + tokenVariations: 5, + location: "arweaveHash1", + paymentReceiver: payable(creator), + cost: 1, + erc20: zeroAddress + }); + example.initializeClaim(address(creatorCore1), 1, claimP); + vm.stopPrank(); + + vm.startPrank(other); + IGachaLazyClaim.ClaimMint[] memory mints = new IGachaLazyClaim.ClaimMint[](1); + IGachaLazyClaim.VariationMint[] memory variationMints = new IGachaLazyClaim.VariationMint[](1); + variationMints[0] = IGachaLazyClaim.VariationMint({ variationIndex: 1, amount: 1, recipient: other }); + mints[0] = IGachaLazyClaim.ClaimMint({ + creatorContractAddress: address(creatorCore1), + instanceId: 1, + variationMints: variationMints + }); + + vm.expectRevert(); + example.deliverMints(mints); + vm.stopPrank(); + } + + function testDeliverMints() public { + vm.startPrank(creator); + + uint48 nowC = uint48(block.timestamp); + uint48 later = nowC + 1000; + + IERC1155GachaLazyClaim.ClaimParameters memory claimP = IERC1155GachaLazyClaim.ClaimParameters({ + storageProtocol: IGachaLazyClaim.StorageProtocol.ARWEAVE, + totalMax: 100, + startDate: nowC, + endDate: later, + tokenVariations: 5, + location: "arweaveHash1", + paymentReceiver: payable(creator), + cost: 1, + erc20: zeroAddress + }); + example.initializeClaim(address(creatorCore1), 1, claimP); + example.mintReserve{ value: (1 + MINT_FEE) * 2 }(address(creatorCore1), 1, 2); + vm.stopPrank(); + + vm.startPrank(other2); + example.mintReserve{ value: (1 + MINT_FEE) * 4 }(address(creatorCore1), 1, 4); + vm.stopPrank(); + + vm.startPrank(signingAddress); + IGachaLazyClaim.ClaimMint[] memory mints = new IGachaLazyClaim.ClaimMint[](2); + IGachaLazyClaim.VariationMint[] memory variationMints = new IGachaLazyClaim.VariationMint[](2); + variationMints[0] = IGachaLazyClaim.VariationMint({ variationIndex: 1, amount: 2, recipient: other2 }); + variationMints[1] = IGachaLazyClaim.VariationMint({ variationIndex: 2, amount: 1, recipient: other }); + mints[0] = IGachaLazyClaim.ClaimMint({ + creatorContractAddress: address(creatorCore1), + instanceId: 1, + variationMints: variationMints + }); + mints[1] = IGachaLazyClaim.ClaimMint({ + creatorContractAddress: address(creatorCore1), + instanceId: 1, + variationMints: variationMints + }); + // revert for receiver with no reserved mints + vm.expectRevert(IGachaLazyClaim.CannotMintMoreThanReserved.selector); + example.deliverMints(mints); + GachaLazyClaim.UserMintDetails memory otherMint = example.getUserMints(other, address(creatorCore1), 1); + assertEq(otherMint.reservedCount, 0); + assertEq(otherMint.deliveredCount, 0); + GachaLazyClaim.UserMintDetails memory other2Mint = example.getUserMints(other2, address(creatorCore1), 1); + assertEq(other2Mint.reservedCount, 4); + assertEq(other2Mint.deliveredCount, 0); + vm.stopPrank(); + + // deliver for valid receivers and mintCount + vm.startPrank(signingAddress); + variationMints[0] = IGachaLazyClaim.VariationMint({ variationIndex: 1, amount: 1, recipient: creator }); + variationMints[1] = IGachaLazyClaim.VariationMint({ variationIndex: 2, amount: 2, recipient: other2 }); + mints[0] = IGachaLazyClaim.ClaimMint({ + creatorContractAddress: address(creatorCore1), + instanceId: 1, + variationMints: variationMints + }); + mints[1] = IGachaLazyClaim.ClaimMint({ + creatorContractAddress: address(creatorCore1), + instanceId: 1, + variationMints: variationMints + }); + example.deliverMints(mints); + GachaLazyClaim.UserMintDetails memory creatorMints = example.getUserMints(creator, address(creatorCore1), 1); + assertEq(creatorMints.deliveredCount, 2); + assertEq(creatorMints.reservedCount, 2); + GachaLazyClaim.UserMintDetails memory other2Mints = example.getUserMints(other2, address(creatorCore1), 1); + assertEq(other2Mints.reservedCount, 4); + assertEq(other2Mints.deliveredCount, 4); + vm.stopPrank(); + } + + function testTokenURI() public { + vm.startPrank(creator); + uint48 nowC = uint48(block.timestamp); + uint48 later = nowC + 1000; + uint totalMintPrice = 1 + MINT_FEE; + + IERC1155GachaLazyClaim.ClaimParameters memory claimP = IERC1155GachaLazyClaim.ClaimParameters({ + storageProtocol: IGachaLazyClaim.StorageProtocol.ARWEAVE, + totalMax: 100, + startDate: nowC, + endDate: later, + tokenVariations: 5, + location: "arweaveHash1", + paymentReceiver: payable(creator), + cost: 1, + erc20: zeroAddress + }); + + example.initializeClaim(address(creatorCore1), 1, claimP); + example.mintReserve{ value: totalMintPrice }(address(creatorCore1), 1, 1); + + // mint in between on another extension + address[] memory receivers = new address[](1); + receivers[0] = signingAddress; + uint[] memory amounts = new uint[](1); + amounts[0] = 1; + string[] memory uris = new string[](1); + uris[0] = "0x0"; + creatorCore1.mintBaseNew(receivers, amounts, uris); + vm.stopPrank(); + + // mintreserving should have no effect + vm.startPrank(other); + example.mintReserve{ value: totalMintPrice * 2 }(address(creatorCore1), 1, 2); + vm.stopPrank(); + vm.startPrank(other2); + example.mintReserve{ value: totalMintPrice }(address(creatorCore1), 1, 1); + vm.stopPrank(); + + vm.startPrank(creator); + claimP.tokenVariations = 3; + claimP.location = "arweaveHash2"; + // create another gacha claim + example.initializeClaim(address(creatorCore1), 2, claimP); + vm.stopPrank(); + + assertEq("https://arweave.net/arweaveHash1/1", creatorCore1.uri(1)); + assertEq("https://arweave.net/arweaveHash1/2", creatorCore1.uri(2)); + assertEq("https://arweave.net/arweaveHash1/3", creatorCore1.uri(3)); + assertEq("https://arweave.net/arweaveHash1/4", creatorCore1.uri(4)); + assertEq("https://arweave.net/arweaveHash1/5", creatorCore1.uri(5)); + assertTrue( + keccak256(bytes("https://arweave.net/arweaveHash1/6")) != keccak256(bytes(creatorCore1.uri(6))), + "URI should not match the specified value." + ); + assertEq("https://arweave.net/arweaveHash2/1", creatorCore1.uri(7)); + assertEq("https://arweave.net/arweaveHash2/2", creatorCore1.uri(8)); + assertEq("https://arweave.net/arweaveHash2/3", creatorCore1.uri(9)); + } + + function testUpdateTokenURI() public { + vm.startPrank(creator); + uint48 nowC = uint48(block.timestamp); + uint48 later = nowC + 1000; + + IERC1155GachaLazyClaim.ClaimParameters memory claimP = IERC1155GachaLazyClaim.ClaimParameters({ + storageProtocol: IGachaLazyClaim.StorageProtocol.ARWEAVE, + totalMax: 100, + startDate: nowC, + endDate: later, + tokenVariations: 5, + location: "arweaveHash1", + paymentReceiver: payable(creator), + cost: 1, + erc20: zeroAddress + }); + example.initializeClaim(address(creatorCore1), 1, claimP); + + // tokens with original location + assertEq("https://arweave.net/arweaveHash1/1", creatorCore1.uri(1)); + assertEq("https://arweave.net/arweaveHash1/2", creatorCore1.uri(2)); + assertEq("https://arweave.net/arweaveHash1/3", creatorCore1.uri(3)); + assertEq("https://arweave.net/arweaveHash1/4", creatorCore1.uri(4)); + assertEq("https://arweave.net/arweaveHash1/5", creatorCore1.uri(5)); + + // update tokenURI + example.updateTokenURIParams(address(creatorCore1), 1, IGachaLazyClaim.StorageProtocol.ARWEAVE, "arweaveHashNEW"); + assertEq("https://arweave.net/arweaveHashNEW/1", creatorCore1.uri(1)); + assertEq("https://arweave.net/arweaveHashNEW/2", creatorCore1.uri(2)); + assertEq("https://arweave.net/arweaveHashNEW/3", creatorCore1.uri(3)); + assertEq("https://arweave.net/arweaveHashNEW/4", creatorCore1.uri(4)); + assertEq("https://arweave.net/arweaveHashNEW/5", creatorCore1.uri(5)); + vm.stopPrank(); + } +}