Handling NFT Presale or Allowing Lists Off-chain | by Humans Of NFT | Jan, 2022

A novel approach to using signed coupons generated off-chain instead of an on-chain allow list.

Humans Of NFT
https://etherscan.io/address/0x8575B2Dbbd7608A1629aDAA952abA74Bcc53d22A#code
mapping(address => uint8) _allowList;function setAllowList(
address[] calldata addresses,
uint8 numAllowedToMint
) external onlyOwner {
for (uint256 i = 0; i < addresses.length; i++) {
_allowList[addresses[i]] = numAllowedToMint;
}
}
Diagram of a Merkle Tree from the aforementioned Openzeppelin presentation.
import '@openzeppelin/contracts/utils/cryptography/MerkleProof.sol';...// declare bytes32 variables to store each root (a hash)bytes32 public genesisMerkleRoot; 
bytes32 public authorsMerkleRoot;
bytes32 public presaleMerkleRoot;
...// separate functions to set the roots of each individual Merkle Treefunction setGenesisMerkleRoot(bytes32 _root) external onlyOwner {
genesisMerkleRoot = _root;
}
function setAuthorsMerkleRoot(bytes32 _root) external onlyOwner {
authorsMerkleRoot = _root;
}
function setPresaleMerkleRoot(bytes32 _root) external onlyOwner {
presaleMerkleRoot = _root;
}
...// create merkle leaves from supplied datafunction _generateGenesisMerkleLeaf(
address _account,
uint256 _tokenId
) internal pure returns (bytes32) {
return keccak256(abi.encodePacked(_tokenId, _account));
}
function _generateAuthorsMerkleLeaf(
address _account,
uint256 _tokenCount
) internal pure returns (bytes32) {
return keccak256(abi.encodePacked(_account, _tokenCount));
}
function _generatePresaleMerkleLeaf(
address _account,
uint256 _max
) internal pure returns (bytes32) {
return keccak256(abi.encodePacked(_max, _account));
}
...// function to verify that the given leaf belongs to a given tree using its root for comparisonfunction _verifyMerkleLeaf(
bytes32 _leafNode,
bytes32 _merkleRoot,
bytes32[] memory _proof ) internal view returns (bool) {
return MerkleProof.verify(_proof, _merkleRoot, _leafNode);
}
require(     
_verifyMerkleLeaf(
_generateGenesisMerkleLeaf(
msg.sender,
_tokenIds[i]),
genesisMerkleRoot,
_proofs[i]
), "Invalid proof, you don't own that Token ID");
struct Coupon {
bytes32 r;
bytes32 s;
uint8 v;
}
enum CouponType {
Genesis,
Author,
Presale
}
enum SalePhase {
Locked,
PreSale,
PublicSale
}
 /// Mint during presale
/// @dev mints by addresses validated using verified coupons signed by an admin signer
/// @notice mints tokens with randomized token IDs to addresses eligible for presale
/// @param count number of tokens to mint in transaction
/// @param coupon coupon signed by an admin coupon
function mintPresale(uint256 count, Coupon memory coupon)
external
payable
ensureAvailabilityFor(count)
validateEthPayment(count)
{
require(
phase == SalePhase.PreSale,
'Presale event is not active'
); // 1

require(
count + addressToMints[msg.sender]._numberOfMintsByAddress <=
MAX_PRESALE_MINTS_PER_ADDRESS,
'Exceeds number of presale mints allowed'
); // 2

bytes32 digest = keccak256(
abi.encode(CouponType.Presale, msg.sender)
); // 3

require(
_isVerifiedCoupon(digest, coupon),
'Invalid coupon'
); // 4

...}
bytes32 digest = keccak256(
abi.encode(
2,
0x8575B2Dbbd7608A1629aDAA952abA74Bcc53d22A
)
);
 /// @dev check that the coupon sent was signed by the admin signer
function _isVerifiedCoupon(bytes32 digest, Coupon memory coupon)
internal
view
returns (bool)
{
address signer = ecrecover(digest, coupon.v, coupon.r, coupon.s);
require(signer != address(0), 'ECDSA: invalid signature');
return signer == _adminSigner;
}

So where does the Coupon come from?

A user validating their position on the list by fetching a coupon
Coupon Lifecycle
{
"0x1813183E1A2a5a...a868A4e1b7610c0": {
"coupon": {
"r": "0x77b675bb4808.....674c42bde11618a",
"s": "0x17baa76756fed.....4b0b9f4a380b8a9",
"v": 27
}
}
mintPresale(
qty: number,
priceInEth: number,
coupon: ICoupon
) {
const mintPriceBn = utils.parseEther(priceInEth.toString());
return this.contract.mintPresale(qty, coupon, {
value: mintPriceBn.mul(qty),
gasLimit: GAS_LIMIT_PER * qty
});
}
const {
keccak256,
toBuffer,
ecsign,
bufferToHex,
} = require("ethereumjs-utils");
const { ethers } = require('ethers');...// create an object to match the contracts struct
const CouponTypeEnum = {
Genesis: 0,
Author: 1,
Presale: 2,
};
let coupons = {};for (let i = 0; i < presaleAddresses.length; i++) {
const userAddress = ethers.utils.getAddress(presaleAddresses[i]);
const hashBuffer = generateHashBuffer(
["uint256", "address"],
[CouponTypeEnum["Presale"], userAddress]
);
const coupon = createCoupon(hashBuffer, signerPvtKey);

coupons[userAddress] = {
coupon: serializeCoupon(coupon)
};
}

// HELPER FUNCTIONS
function createCoupon(hash, signerPvtKey) {
return ecsign(hash, signerPvtKey);
}
function generateHashBuffer(typesArray, valueArray) {
return keccak256(
toBuffer(ethers.utils.defaultAbiCoder.encode(typesArray,
valueArray))
);
}
function serializeCoupon(coupon) {
return {
r: bufferToHex(coupon.r),
s: bufferToHex(coupon.s),
v: coupon.v,
};
}
// [solidity] recreating the digest in the contract 
bytes32 digest = keccak256(
abi.encode(CouponType.Presale, msg.sender)
);
// [javascript] Creating the digest for the coupon off-chain
const hashBuffer = generateHashBuffer(
["uint256", "address"],
[CouponTypeEnum["Presale"], userAddress]
);
function generateHashBuffer(typesArray, valueArray) {
return keccak256(
toBuffer(ethers.utils.defaultAbiCoder.encode(typesArray,
valueArray))
);
}
function createCoupon(hash, signerPvtKey) {
return ecsign(hash, signerPvtKey);
}
const signerPvtKeyString = process.env.ADMIN_SIGNER_PRIVATE_KEY || "";const signerPvtKey = Buffer.from(signerPvtKeyString, "hex");
const { privateToAddress } = require("ethereumjs-utils");
const { ethers } = require("ethers");
const crypto = require("crypto");
const pvtKey = crypto.randomBytes(32);
const pvtKeyString = pvtKey.toString("hex");
const signerAddress = ethers.utils.getAddress(
privateToAddress(pvtKey).toString("hex"));
console.log({ signerAddress, pvtKeyString });

Leave a Comment