Skip to content

Commit

Permalink
Add variable wallet limit support (#119)
Browse files Browse the repository at this point in the history
* Support variable wallet limit

* Support variable wallet limit

* Update scripts

* update

* fix script

* lint
  • Loading branch information
channing-magiceden authored Mar 14, 2024
1 parent e6fe9fc commit c24133c
Show file tree
Hide file tree
Showing 9 changed files with 419 additions and 60 deletions.
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

0 comments on commit c24133c

Please sign in to comment.