Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 17 additions & 4 deletions src/tokens/ERC1155/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,16 +18,29 @@ This folder contains contracts that are pre-configured for specific use cases.

The `ERC1155Items` contract is a preset that configures the `ERC1155BaseToken` contract to allow minting of tokens. It adds a `MINTER_ROLE` and a `mint(address to, uint256 amount)` function that can only be called by accounts with the `MINTER_ROLE`.

## Utilities

This folder contains contracts that work in conjunction with other ERC1155 contracts.

### Sale

The `ERC1155Sale` contract is a preset that configures the `ERC1155BaseToken` contract to allow for the sale of tokens. It adds a `mint(address to, , uint256[] memory tokenIds, uint256[] memory amounts, bytes memory data, bytes32[] calldata proof)` function allows for the minting of tokens under various conditions.
The `ERC1155Sale` contract is a utility contract that provides sale functionality for ERC-1155 tokens. It works in conjunction with an `ERC1155Items` contract to handle the minting and sale of tokens under various conditions.

The contract supports multiple sale configurations through a sale details system. Each sale configuration includes:

- Token ID range (minTokenId to maxTokenId)
- Cost per token
- Payment token (ETH or ERC20)
- Supply limit per token ID
- Sale time window (startTime to endTime)
- Optional merkle root for allowlist minting

Conditions may be set by the contract owner. Set the payment token with `setPaymentToken(address paymentTokenAddr)`, then use either the `setTokenSaleDetails(uint256 tokenId, uint256 cost, uint256 remainingSupply, uint64 startTime, uint64 endTime, bytes32 merkleRoot)` function for single token settings or the `setGlobalSaleDetails(uint256 cost, uint256 remainingSupply, uint64 startTime, uint64 endTime, bytes32 merkleRoot)` function for global settings. These functions can only be called by accounts with the `MINT_ADMIN_ROLE`.
Conditions may be set by the contract owner using the `addSaleDetails(SaleDetails calldata details)` function for new configurations or `updateSaleDetails(uint256 saleIndex, SaleDetails calldata details)` for existing ones. These functions can only be called by accounts with the `MINT_ADMIN_ROLE`.

When using a merkle proof, each caller may only use each root once. To prevent collisions ensure the same root is not used for multiple sale details.
Leaves are defined as `keccak256(abi.encodePacked(caller, tokenId))`. The `caller` is the message sender, who will also receive the tokens. The `tokenId` is the id of the token that will be minted, (for global sales `type(uint256).max` is used).
Leaves are defined as `keccak256(abi.encodePacked(caller, tokenId))`. The `caller` is the message sender, who will also receive the tokens. The `tokenId` is the id of the token that will be minted.

For information about the function parameters, please refer to the function specification in `presets/sale/IERC1155Sale.sol`.
For information about the function parameters, please refer to the function specification in `utility/sale/IERC1155Sale.sol`.

## Usage

Expand Down
341 changes: 109 additions & 232 deletions src/tokens/ERC1155/utility/sale/ERC1155Sale.sol

Large diffs are not rendered by default.

6 changes: 4 additions & 2 deletions src/tokens/ERC1155/utility/sale/ERC1155SaleFactory.sol
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,14 @@ contract ERC1155SaleFactory is IERC1155SaleFactory, SequenceProxyFactory {

/// @inheritdoc IERC1155SaleFactoryFunctions
function deploy(
uint256 nonce,
address proxyOwner,
address tokenOwner,
address items,
address implicitModeValidator,
bytes32 implicitModeProjectId
) external returns (address proxyAddr) {
bytes32 salt = keccak256(abi.encode(tokenOwner, items, implicitModeValidator, implicitModeProjectId));
bytes32 salt = keccak256(abi.encode(nonce, tokenOwner, items, implicitModeValidator, implicitModeProjectId));
proxyAddr = _createProxy(salt, proxyOwner, "");
ERC1155Sale(proxyAddr).initialize(tokenOwner, items, implicitModeValidator, implicitModeProjectId);
emit ERC1155SaleDeployed(proxyAddr);
Expand All @@ -38,13 +39,14 @@ contract ERC1155SaleFactory is IERC1155SaleFactory, SequenceProxyFactory {

/// @inheritdoc IERC1155SaleFactoryFunctions
function determineAddress(
uint256 nonce,
address proxyOwner,
address tokenOwner,
address items,
address implicitModeValidator,
bytes32 implicitModeProjectId
) external view returns (address proxyAddr) {
bytes32 salt = keccak256(abi.encode(tokenOwner, items, implicitModeValidator, implicitModeProjectId));
bytes32 salt = keccak256(abi.encode(nonce, tokenOwner, items, implicitModeValidator, implicitModeProjectId));
return _computeProxyAddress(salt, proxyOwner, "");
}

Expand Down
149 changes: 94 additions & 55 deletions src/tokens/ERC1155/utility/sale/IERC1155Sale.sol
Original file line number Diff line number Diff line change
@@ -1,85 +1,117 @@
// SPDX-License-Identifier: Apache-2.0
pragma solidity ^0.8.19;

interface IERC1155SaleFunctions {
interface IERC1155Sale {

/**
* Sale details.
* @param minTokenId Minimum token ID for this sale. (Inclusive)
* @param maxTokenId Maximum token ID for this sale. (Inclusive)
* @param cost Cost per token.
* @param paymentToken ERC20 token address to accept payment in. address(0) indicates payment in ETH.
* @param supply Maximum number of tokens that can be minted (per token ID).
* @param startTime Start time of the sale. (Inclusive)
* @param endTime End time of the sale. (Inclusive)
* @param merkleRoot Merkle root for allowlist minting. 0 indicates no proof required.
*/
struct SaleDetails {
uint256 minTokenId;
uint256 maxTokenId;
uint256 cost;
uint256 remainingSupply;
address paymentToken;
uint256 supply;
uint64 startTime;
uint64 endTime; // 0 end time indicates sale inactive
bytes32 merkleRoot; // Root of allowed addresses
uint64 endTime;
bytes32 merkleRoot;
}

/**
* Get global sales details.
* @return Sale details.
* @notice Global sales details apply to all tokens.
* @notice Global sales details are overriden when token sale is active.
* Get the total number of sale details.
* @return Total number of sale details.
*/
function globalSaleDetails() external view returns (SaleDetails memory);
function saleDetailsCount() external view returns (uint256);

/**
* Get token sale details.
* @param tokenId Token ID to get sale details for.
* @return Sale details.
* @notice Token sale details override global sale details.
* Get sale details.
* @param saleIndex Index of the sale details to get.
* @return details Sale details.
*/
function tokenSaleDetails(
uint256 tokenId
) external view returns (SaleDetails memory);
function saleDetails(
uint256 saleIndex
) external view returns (SaleDetails memory details);

/**
* Get sale details for multiple tokens.
* @param tokenIds Array of token IDs to retrieve sale details for.
* @return Array of sale details corresponding to each token ID.
* @notice Each token's sale details override the global sale details if set.
* Get sale details for multiple sale indexes.
* @param saleIndexes Array of sale indexes to retrieve sale details for.
* @return details Array of sale details corresponding to each sale index.
*/
function tokenSaleDetailsBatch(
uint256[] calldata tokenIds
) external view returns (SaleDetails[] memory);
function saleDetailsBatch(
uint256[] calldata saleIndexes
) external view returns (SaleDetails[] memory details);

/**
* Get payment token.
* @return Payment token address.
* @notice address(0) indicates payment in ETH.
* Add new sale details.
* @param details Sale details to add.
* @return saleIndex Index of the newly added sale details.
*/
function paymentToken() external view returns (address);
function addSaleDetails(
SaleDetails calldata details
) external returns (uint256 saleIndex);

/**
* Update existing sale details.
* @param saleIndex Index of the sale details to update.
* @param details Sale details to update.
*/
function updateSaleDetails(uint256 saleIndex, SaleDetails calldata details) external;

/**
* Mint tokens.
* @param to Address to mint tokens to.
* @param tokenIds Token IDs to mint.
* @param amounts Amounts of tokens to mint.
* @param data Data to pass if receiver is contract.
* @param paymentToken ERC20 token address to accept payment in. address(0) indicates ETH.
* @param saleIndexes Sale indexes for each token. Must match tokenIds length.
* @param expectedPaymentToken ERC20 token address to accept payment in. address(0) indicates ETH.
* @param maxTotal Maximum amount of payment tokens.
* @param proof Merkle proof for allowlist minting.
* @param proofs Merkle proofs for allowlist minting. Must match tokenIds length.
* @notice Sale must be active for all tokens.
* @dev tokenIds must be sorted ascending without duplicates.
* @dev All sales must use the same payment token.
* @dev An empty proof is supplied when no proof is required.
*/
function mint(
address to,
uint256[] memory tokenIds,
uint256[] memory amounts,
bytes memory data,
address paymentToken,
uint256[] calldata tokenIds,
uint256[] calldata amounts,
bytes calldata data,
uint256[] calldata saleIndexes,
address expectedPaymentToken,
uint256 maxTotal,
bytes32[] calldata proof
bytes32[][] calldata proofs
) external payable;

}
/**
* Emitted when sale details are added.
* @param saleIndex Index of the sale details that were added.
* @param details Sale details that were added.
*/
event SaleDetailsAdded(uint256 saleIndex, SaleDetails details);

interface IERC1155SaleSignals {
/**
* Emitted when sale details are updated.
* @param saleIndex Index of the sale details that were updated.
* @param details Sale details that were updated.
*/
event SaleDetailsUpdated(uint256 saleIndex, SaleDetails details);

event GlobalSaleDetailsUpdated(
uint256 cost, uint256 remainingSupply, uint64 startTime, uint64 endTime, bytes32 merkleRoot
);
event TokenSaleDetailsUpdated(
uint256 tokenId, uint256 cost, uint256 remainingSupply, uint64 startTime, uint64 endTime, bytes32 merkleRoot
);
event ItemsMinted(address to, uint256[] tokenIds, uint256[] amounts);
/**
* Emitted when tokens are minted.
* @param to Address that minted the tokens.
* @param tokenIds Token IDs that were minted.
* @param amounts Amounts of tokens that were minted.
* @param saleIndexes Sale indexes that were minted from.
*/
event ItemsMinted(address to, uint256[] tokenIds, uint256[] amounts, uint256[] saleIndexes);

/**
* Contract already initialized.
Expand All @@ -92,15 +124,14 @@ interface IERC1155SaleSignals {
error InvalidSaleDetails();

/**
* Sale is not active globally.
* Sale details index does not exist.
*/
error GlobalSaleInactive();
error SaleDetailsNotFound(uint256 index);

/**
* Sale is not active.
* @param tokenId Invalid Token ID.
*/
error SaleInactive(uint256 tokenId);
error SaleInactive();

/**
* Insufficient tokens for payment.
Expand All @@ -110,18 +141,26 @@ interface IERC1155SaleSignals {
*/
error InsufficientPayment(address currency, uint256 expected, uint256 actual);

/**
* Invalid token IDs.
*/
error InvalidTokenIds();

/**
* Insufficient supply of tokens.
* @param remainingSupply Remaining supply.
* @param amount Amount to mint.
*/
error InsufficientSupply(uint256 remainingSupply, uint256 amount);

}
/**
* Invalid array lengths.
*/
error InvalidArrayLengths();

/**
* Invalid amount.
*/
error InvalidAmount();

/**
* Payment token mismatch between sales.
*/
error PaymentTokenMismatch();

interface IERC1155Sale is IERC1155SaleFunctions, IERC1155SaleSignals { }
}
4 changes: 4 additions & 0 deletions src/tokens/ERC1155/utility/sale/IERC1155SaleFactory.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ interface IERC1155SaleFactoryFunctions {

/**
* Creates an ERC-1155 Sale proxy contract
* @param nonce Extra salt to add to the salt used to compute the address
* @param proxyOwner The owner of the ERC-1155 Sale proxy
* @param tokenOwner The owner of the ERC-1155 Sale implementation
* @param items The ERC-1155 Items contract address
Expand All @@ -14,6 +15,7 @@ interface IERC1155SaleFactoryFunctions {
* @notice The deployed contract must be granted the MINTER_ROLE on the ERC-1155 Items contract.
*/
function deploy(
uint256 nonce,
address proxyOwner,
address tokenOwner,
address items,
Expand All @@ -23,6 +25,7 @@ interface IERC1155SaleFactoryFunctions {

/**
* Computes the address of a proxy instance.
* @param nonce Extra salt to add to the salt used to compute the address
* @param proxyOwner The owner of the ERC-1155 Sale proxy
* @param tokenOwner The owner of the ERC-1155 Sale implementation
* @param items The ERC-1155 Items contract address
Expand All @@ -31,6 +34,7 @@ interface IERC1155SaleFactoryFunctions {
* @return proxyAddr The address of the ERC-1155 Sale Proxy
*/
function determineAddress(
uint256 nonce,
address proxyOwner,
address tokenOwner,
address items,
Expand Down
21 changes: 15 additions & 6 deletions src/tokens/common/MerkleProofSingleUse.sol
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,12 @@ import { IMerkleProofSingleUse } from "./IMerkleProofSingleUse.sol";
import { MerkleProof } from "openzeppelin-contracts/contracts/utils/cryptography/MerkleProof.sol";

/**
* Require single use merkle proofs per address.
* Require single use merkle proofs per root.
*/
abstract contract MerkleProofSingleUse is IMerkleProofSingleUse {

// Stores proofs used by an address
mapping(address => mapping(bytes32 => bool)) private _proofUsed;
// Stores proofs used per root
mapping(bytes32 => mapping(bytes32 => bool)) private _proofUsed;

/**
* Requires the given merkle proof to be valid.
Expand All @@ -24,10 +24,11 @@ abstract contract MerkleProofSingleUse is IMerkleProofSingleUse {
*/
function requireMerkleProof(bytes32 root, bytes32[] calldata proof, address addr, bytes32 salt) internal {
if (root != bytes32(0)) {
if (!checkMerkleProof(root, proof, addr, salt)) {
bytes32 leaf = _getLeaf(addr, salt);
if (!_checkMerkleProof(root, proof, leaf)) {
revert MerkleProofInvalid(root, proof, addr, salt);
}
_proofUsed[addr][root] = true;
_proofUsed[root][leaf] = true;
}
}

Expand All @@ -45,7 +46,15 @@ abstract contract MerkleProofSingleUse is IMerkleProofSingleUse {
address addr,
bytes32 salt
) public view returns (bool) {
return !_proofUsed[addr][root] && MerkleProof.verify(proof, root, keccak256(abi.encodePacked(addr, salt)));
return _checkMerkleProof(root, proof, _getLeaf(addr, salt));
}

function _getLeaf(address addr, bytes32 salt) internal pure returns (bytes32) {
return keccak256(abi.encodePacked(addr, salt));
}

function _checkMerkleProof(bytes32 root, bytes32[] calldata proof, bytes32 leaf) internal view returns (bool) {
return !_proofUsed[root][leaf] && MerkleProof.verify(proof, root, leaf);
}

}
Loading
Loading