Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add variable wallet limit support #119

Merged
merged 8 commits into from
Mar 14, 2024
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
31 changes: 28 additions & 3 deletions contracts/ERC721CM.sol
Original file line number Diff line number Diff line change
Expand Up @@ -353,7 +353,26 @@ contract ERC721CM is IERC721M, ERC721ACQueryable, Ownable, ReentrancyGuard {
uint64 timestamp,
bytes calldata signature
) external payable virtual nonReentrant {
_mintInternal(qty, msg.sender, proof, timestamp, signature);
_mintInternal(qty, msg.sender, 0, proof, timestamp, signature);
}

/**
* @dev Mints token(s) with limit.
*
* qty - number of tokens to mint
* limit - limit for the given minter
* proof - the merkle proof generated on client side. This applies if using whitelist.
* timestamp - the current timestamp
* signature - the signature from cosigner if using cosigner.
*/
function mintWithLimit(
uint32 qty,
uint32 limit,
bytes32[] calldata proof,
uint64 timestamp,
bytes calldata signature
) external payable virtual nonReentrant {
_mintInternal(qty, msg.sender, limit, proof, timestamp, signature);
}

/**
Expand All @@ -377,7 +396,7 @@ contract ERC721CM is IERC721M, ERC721ACQueryable, Ownable, ReentrancyGuard {
// Check the caller is Crossmint
if (msg.sender != _crossmintAddress) revert CrossmintOnly();

_mintInternal(qty, to, proof, timestamp, signature);
_mintInternal(qty, to, 0, proof, timestamp, signature);
}

/**
Expand All @@ -386,6 +405,7 @@ contract ERC721CM is IERC721M, ERC721ACQueryable, Ownable, ReentrancyGuard {
function _mintInternal(
uint32 qty,
address to,
uint32 limit,
bytes32[] calldata proof,
uint64 timestamp,
bytes calldata signature
Expand Down Expand Up @@ -432,9 +452,14 @@ contract ERC721CM is IERC721M, ERC721ACQueryable, Ownable, ReentrancyGuard {
if (
MerkleProof.processProof(
proof,
keccak256(abi.encodePacked(to))
keccak256(abi.encodePacked(to, limit))
) != stage.merkleRoot
) revert InvalidProof();

// Verify merkle proof mint limit
if (limit > 0 && _stageMintedCountsPerWallet[activeStage][to] + qty > limit) {
revert WalletStageLimitExceeded();
}
}

if (_mintCurrency != address(0)) {
Expand Down
31 changes: 28 additions & 3 deletions contracts/ERC721M.sol
Original file line number Diff line number Diff line change
Expand Up @@ -349,7 +349,26 @@ contract ERC721M is IERC721M, ERC721AQueryable, Ownable, ReentrancyGuard {
uint64 timestamp,
bytes calldata signature
) external payable virtual nonReentrant {
_mintInternal(qty, msg.sender, proof, timestamp, signature);
_mintInternal(qty, msg.sender, 0, proof, timestamp, signature);
}

/**
* @dev Mints token(s) with limit.
*
* qty - number of tokens to mint
* limit - limit for the given minter
* proof - the merkle proof generated on client side. This applies if using whitelist.
* timestamp - the current timestamp
* signature - the signature from cosigner if using cosigner.
*/
function mintWithLimit(
uint32 qty,
uint32 limit,
bytes32[] calldata proof,
uint64 timestamp,
bytes calldata signature
) external payable virtual nonReentrant {
_mintInternal(qty, msg.sender, limit, proof, timestamp, signature);
}

/**
Expand All @@ -373,7 +392,7 @@ contract ERC721M is IERC721M, ERC721AQueryable, Ownable, ReentrancyGuard {
// Check the caller is Crossmint
if (msg.sender != _crossmintAddress) revert CrossmintOnly();

_mintInternal(qty, to, proof, timestamp, signature);
_mintInternal(qty, to, 0, proof, timestamp, signature);
}

/**
Expand All @@ -382,6 +401,7 @@ contract ERC721M is IERC721M, ERC721AQueryable, Ownable, ReentrancyGuard {
function _mintInternal(
uint32 qty,
address to,
uint32 limit,
bytes32[] calldata proof,
uint64 timestamp,
bytes calldata signature
Expand Down Expand Up @@ -428,9 +448,14 @@ contract ERC721M is IERC721M, ERC721AQueryable, Ownable, ReentrancyGuard {
if (
MerkleProof.processProof(
proof,
keccak256(abi.encodePacked(to))
keccak256(abi.encodePacked(to, limit))
) != stage.merkleRoot
) revert InvalidProof();

// Verify merkle proof mint limit
if (limit > 0 && _stageMintedCountsPerWallet[activeStage][to] + qty > limit) {
revert WalletStageLimitExceeded();
}
}

if (_mintCurrency != address(0)) {
Expand Down
2 changes: 1 addition & 1 deletion contracts/ERC721MAutoApprover.sol
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ contract ERC721MAutoApprover is ERC721M {
uint64 timestamp,
bytes calldata signature
) external payable override nonReentrant {
_mintInternal(qty, msg.sender, proof, timestamp, signature);
_mintInternal(qty, msg.sender, 0, proof, timestamp, signature);

// if auto approve address is not all zero, check if the address is already approved
if (
Expand Down
2 changes: 1 addition & 1 deletion contracts/ERC721MOperatorFiltererAutoApprover.sol
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ contract ERC721MOperatorFiltererAutoApprover is ERC721MOperatorFilterer {
uint64 timestamp,
bytes calldata signature
) external payable override nonReentrant {
_mintInternal(qty, msg.sender, proof, timestamp, signature);
_mintInternal(qty, msg.sender, 0, proof, timestamp, signature);

// if auto approve address is not all zero, check if the address is already approved
if (
Expand Down
7 changes: 7 additions & 0 deletions contracts/IERC721M.sol
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ interface IERC721M is IERC721AQueryable {
error WalletStageLimitExceeded();
error WithdrawFailed();
error WrongMintCurrency();
error NotSupported();

struct MintStageInfo {
uint80 price;
Expand Down Expand Up @@ -70,4 +71,10 @@ interface IERC721M is IERC721AQueryable {
function getStageInfo(
uint256 index
) external view returns (MintStageInfo memory, uint32, uint256);

function mint(uint32 qty, bytes32[] calldata proof, uint64 timestamp, bytes calldata signature) external payable;

function mintWithLimit(uint32 qty, uint32 limit, bytes32[] calldata proof, uint64 timestamp, bytes calldata signature) external payable;

function crossmint(uint32 qty, address to, bytes32[] calldata proof, uint64 timestamp, bytes calldata signature) external payable;
}
24 changes: 23 additions & 1 deletion contracts/onft/ERC721MLite.sol
Original file line number Diff line number Diff line change
Expand Up @@ -294,7 +294,7 @@ contract ERC721MLite is
if (
MerkleProof.processProof(
proof,
keccak256(abi.encodePacked(msg.sender))
keccak256(abi.encodePacked(msg.sender, uint32(0))) // limit = 0 for consistency
) != stage.merkleRoot
) revert InvalidProof();
}
Expand All @@ -304,6 +304,28 @@ contract ERC721MLite is
_safeMint(msg.sender, qty);
}

/** NOT SUPPORTED */
function mintWithLimit(
uint32 qty,
uint32 limit,
bytes32[] calldata proof,
uint64 timestamp,
bytes calldata signature
) external payable virtual nonReentrant {
revert NotSupported();
}

/** NOT SUPPORTED */
function crossmint(
uint32 qty,
address to,
bytes32[] calldata proof,
uint64 timestamp,
bytes calldata signature
) external payable nonReentrant {
revert NotSupported();
}

/**
* @dev Mints token(s) by owner.
*
Expand Down
119 changes: 84 additions & 35 deletions scripts/setStages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ interface StageConfig {
walletLimit?: number;
maxSupply?: number;
whitelistPath?: string;
variableWalletLimitPath?: string;
}

export const setStages = async (
Expand All @@ -41,46 +42,94 @@ export const setStages = async (
if (args.gaslimit) {
overrides.gasLimit = ethers.BigNumber.from(args.gaslimit);
}

/*
* Merkle root generation logic:
* - for `whitelist`, leaves are `solidityKeccak256(['address', 'uint32'], [address, 0])`
* - for `variable wallet limit list`, leaves are `solidityKeccak256(['address', 'uint32'], [address, limit])`
*/
const merkleRoots = await Promise.all(
stagesConfig.map((stage) => {
if (!stage.whitelistPath) {
return ethers.utils.hexZeroPad('0x', 32);
}
const whitelist = JSON.parse(
fs.readFileSync(stage.whitelistPath, 'utf-8'),
);

// Clean up whitelist
const filteredWhitelist = whitelist.filter((address: string) =>
ethers.utils.isAddress(address),
);
console.log(
`Filtered whitelist: ${filteredWhitelist.length} addresses. ${whitelist.length - filteredWhitelist.length} invalid addresses removed.`,
);
const invalidWhitelist = whitelist.filter(
(address: string) => !ethers.utils.isAddress(address),
);
console.log(
`❌ Invalid whitelist: ${invalidWhitelist.length} addresses.\r\n${invalidWhitelist.join(', \r\n')}`,
);

if (invalidWhitelist.length > 0) {
console.log(`🔄 🚨 updating whitelist file: ${stage.whitelistPath}`);
fs.writeFileSync(
stage.whitelistPath,
JSON.stringify(filteredWhitelist, null, 2),
if (stage.whitelistPath) {
const whitelist = JSON.parse(
fs.readFileSync(stage.whitelistPath, 'utf-8'),
);
}

const mt = new MerkleTree(
filteredWhitelist.map(ethers.utils.getAddress),
ethers.utils.keccak256,
{
// Clean up whitelist
const filteredWhitelist = whitelist.filter((address: string) =>
ethers.utils.isAddress(address),
);
console.log(
`Filtered whitelist: ${filteredWhitelist.length} addresses. ${whitelist.length - filteredWhitelist.length} invalid addresses removed.`,
);
const invalidWhitelist = whitelist.filter(
(address: string) => !ethers.utils.isAddress(address),
);
console.log(
`❌ Invalid whitelist: ${invalidWhitelist.length} addresses.\r\n${invalidWhitelist.join(', \r\n')}`,
);

if (invalidWhitelist.length > 0) {
console.log(`🔄 🚨 updating whitelist file: ${stage.whitelistPath}`);
fs.writeFileSync(
stage.whitelistPath,
JSON.stringify(filteredWhitelist, null, 2),
);
}

const mt = new MerkleTree(
filteredWhitelist.map((address: string) =>
ethers.utils.solidityKeccak256(
['address', 'uint32'],
[ethers.utils.getAddress(address), 0],
),
),
ethers.utils.keccak256,
{
sortPairs: true,
hashLeaves: true,
},
);
return mt.getHexRoot();
} else if (stage.variableWalletLimitPath) {
const leaves: any[] = [];
const file = fs.readFileSync(stage.variableWalletLimitPath, 'utf-8');
file
.split('\n')
.filter((line) => line)
.forEach((line) => {
const [addressStr, limitStr] = line.split(',');

if (!ethers.utils.isAddress(addressStr.trim().toLowerCase())) {
console.log(`Ignored invalid address: ${addressStr}`);
return;
}

const address = ethers.utils.getAddress(
addressStr.trim().toLowerCase(),
);
const limit = parseInt(limitStr, 10);

if (!Number.isInteger(limit)) {
console.log(`Ignored invalid limit for address: ${addressStr}`);
return;
}

const digest = ethers.utils.solidityKeccak256(
['address', 'uint32'],
[address, limit],
);
leaves.push(digest);
});

const mt = new MerkleTree(leaves, ethers.utils.keccak256, {
sortPairs: true,
hashLeaves: true,
},
);
return mt.getHexRoot();
hashLeaves: false,
});
return mt.getHexRoot();
}

return ethers.utils.hexZeroPad('0x', 32);
}),
);

Expand Down
Loading
Loading