DevStation / LaunchKit / Templates / QIE ID Gated Allowlist

QIE ID Gated Allowlist

An allowlist only holders of a QIE ID (.qie) name can claim a place on, with an optional cap and an onchain member record.

Deploy This Template
QieIdGatedAllowlist.sol
solidity
1// SPDX-License-Identifier: MIT
2pragma solidity ^0.8.20;
3 
4interface IERC721Balance {
5 function balanceOf(address owner) external view returns (uint256);
6}
7 
8/// @title QieIdGatedAllowlist
9/// @notice Access control gated on holding a QIE ID (.qie) name, with an
10/// onchain record of who claimed a place and when.
11/// @dev The gate is deliberately `balanceOf(user) > 0` and nothing else.
12/// QIE ID is an ERC-721 of .qie names: verified onchain: it answers
13/// supportsInterface(0x80ac58cd) and balanceOf(), but ownerOf() reverts
14/// for sequential ids (ids are not sequential), tokenURI is empty, and
15/// the ENS-style addr()/name() resolver calls revert. So "holds at least
16/// one name" is the only sound onchain check; do not add ownerOf or
17/// name-resolution logic on top of it, it will revert.
18///
19/// The registry address is a constructor parameter rather than a
20/// constant so this deploys on any network: pass the QIE ID address on
21/// QIE Mainnet, or any ERC-721 you want to gate on elsewhere.
22contract QieIdGatedAllowlist {
23 IERC721Balance public immutable gateToken;
24 address public owner;
25 uint256 public claimed;
26 uint256 public maxClaims; // 0 = unlimited
27 bool public claimingOpen;
28 
29 mapping(address => uint64) public claimedAt; // 0 = has not claimed
30 address[] private _members;
31 
32 event Claimed(address indexed member, uint256 position);
33 event ClaimingToggled(bool open);
34 event MaxClaimsChanged(uint256 maxClaims);
35 event OwnerChanged(address indexed previousOwner, address indexed newOwner);
36 
37 error NotOwner();
38 error NotAContract();
39 error ZeroAddress();
40 error ClaimingClosed();
41 error NoQieId();
42 error AlreadyClaimed();
43 error AllocationFull();
44 
45 modifier onlyOwner() {
46 if (msg.sender != owner) revert NotOwner();
47 _;
48 }
49 
50 /// @param gateToken_ ERC-721 whose holders may claim (QIE ID on QIE Mainnet).
51 /// @param initialOwner Can open/close claiming and change the cap.
52 /// @param maxClaims_ Maximum places; 0 for unlimited.
53 constructor(address gateToken_, address initialOwner, uint256 maxClaims_) {
54 if (gateToken_ == address(0) || initialOwner == address(0)) revert ZeroAddress();
55 // gateToken is immutable, so a wrong address here bricks the contract
56 // permanently, every read reverts and nobody can ever claim. Reject
57 // anything without code at deploy time rather than after the fact.
58 if (gateToken_.code.length == 0) revert NotAContract();
59 gateToken = IERC721Balance(gateToken_);
60 owner = initialOwner;
61 maxClaims = maxClaims_;
62 claimingOpen = true;
63 emit OwnerChanged(address(0), initialOwner);
64 }
65 
66 /// @notice True when `user` holds at least one gate-token name.
67 /// @dev Deliberately a low-level staticcall rather than a direct call. A
68 /// plain external call bubbles up any revert, which would make
69 /// canClaim() -- a view the UI calls to decide whether to show a
70 /// claim button -- throw instead of returning false. A gate that
71 /// cannot answer is treated as "not a holder", never as an error.
72 function holdsQieId(address user) public view returns (bool) {
73 (bool ok, bytes memory data) = address(gateToken).staticcall(
74 abi.encodeWithSelector(IERC721Balance.balanceOf.selector, user)
75 );
76 if (!ok || data.length < 32) return false;
77 return abi.decode(data, (uint256)) > 0;
78 }
79 
80 /// @notice Whether `user` could claim a place right now.
81 function canClaim(address user) external view returns (bool) {
82 if (!claimingOpen) return false;
83 if (claimedAt[user] != 0) return false;
84 if (maxClaims != 0 && claimed >= maxClaims) return false;
85 return holdsQieId(user);
86 }
87 
88 /// @notice Claim a place. Requires a QIE ID and one claim per address.
89 function claim() external {
90 if (!claimingOpen) revert ClaimingClosed();
91 if (claimedAt[msg.sender] != 0) revert AlreadyClaimed();
92 if (maxClaims != 0 && claimed >= maxClaims) revert AllocationFull();
93 if (!holdsQieId(msg.sender)) revert NoQieId();
94 
95 claimedAt[msg.sender] = uint64(block.timestamp);
96 _members.push(msg.sender);
97 claimed += 1;
98 emit Claimed(msg.sender, claimed);
99 }
100 
101 function setClaimingOpen(bool open) external onlyOwner {
102 claimingOpen = open;
103 emit ClaimingToggled(open);
104 }
105 
106 function setMaxClaims(uint256 maxClaims_) external onlyOwner {
107 maxClaims = maxClaims_;
108 emit MaxClaimsChanged(maxClaims_);
109 }
110 
111 function transferOwnership(address newOwner) external onlyOwner {
112 if (newOwner == address(0)) revert ZeroAddress();
113 emit OwnerChanged(owner, newOwner);
114 owner = newOwner;
115 }
116 
117 function memberCount() external view returns (uint256) {
118 return _members.length;
119 }
120 
121 /// @notice Paged member list. Deliberately paged: an unbounded getter
122 /// grows until it reverts out-of-gas for every caller.
123 function membersPage(uint256 offset, uint256 limit) external view returns (address[] memory page) {
124 uint256 total = _members.length;
125 if (offset >= total) return new address[](0);
126 uint256 end = offset + limit;
127 if (end > total) end = total;
128 page = new address[](end - offset);
129 for (uint256 i = offset; i < end; i++) {
130 page[i - offset] = _members[i];
131 }
132 }
133}
134 
DevStation
Loading console…