Skip to content

Commit 8e3c362

Browse files
authored
extracted claim interface (#32)
1 parent 7c3411c commit 8e3c362

File tree

3 files changed

+145
-30
lines changed

3 files changed

+145
-30
lines changed
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity 0.8.28;
3+
4+
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
5+
6+
/**
7+
* @title IZoraTokenCommunityClaim
8+
* @notice Interface for the Zora Token Community Claim contract
9+
*/
10+
interface IZoraTokenCommunityClaim {
11+
/**
12+
* @notice Emitted when allocations are set
13+
* @param allocations The packed allocation data (address + amount)
14+
*/
15+
event AllocationsSet(bytes32[] indexed allocations);
16+
17+
/**
18+
* @notice Emitted when tokens are claimed
19+
* @param account The account that had the allocation
20+
* @param claimTo The address that received the tokens
21+
* @param amount The amount of tokens claimed
22+
*/
23+
event Claimed(address indexed account, address indexed claimTo, uint256 amount);
24+
25+
/**
26+
* @notice Error thrown when a non-admin tries to perform an admin-only action
27+
*/
28+
error OnlyAdmin();
29+
30+
/**
31+
* @notice Error thrown when trying to claim before the claim period has started
32+
*/
33+
error ClaimNotOpen();
34+
35+
/**
36+
* @notice Error thrown when trying to set allocations after the claim period has started
37+
*/
38+
error ClaimOpened();
39+
40+
/**
41+
* @notice Error thrown when a user tries to claim without having an allocation
42+
*/
43+
error NoAllocation();
44+
45+
/**
46+
* @notice Error thrown when a user tries to claim more than once
47+
*/
48+
error AlreadyClaimed();
49+
50+
/**
51+
* @notice Error thrown when a signature is invalid
52+
*/
53+
error InvalidSignature();
54+
55+
/**
56+
* @notice Error thrown when a signature has expired
57+
*/
58+
error SignatureExpired();
59+
60+
/**
61+
* @notice Returns the admin address
62+
* @return The admin address
63+
*/
64+
function admin() external view returns (address);
65+
66+
/**
67+
* @notice Returns the claim start timestamp
68+
* @return The claim start timestamp
69+
*/
70+
function claimStart() external view returns (uint256);
71+
72+
/**
73+
* @notice Returns the token contract address
74+
* @return The token contract address
75+
*/
76+
function token() external view returns (IERC20);
77+
78+
/**
79+
* @notice Returns the allocation for a user
80+
* @param user The user address
81+
* @return The allocation amount
82+
*/
83+
function allocations(address user) external view returns (uint256);
84+
85+
/**
86+
* @notice Returns whether a user has claimed their allocation
87+
* @param user The user address
88+
* @return Whether the user has claimed
89+
*/
90+
function hasClaimed(address user) external view returns (bool);
91+
92+
/**
93+
* @notice Sets allocations using packed data format for gas efficiency
94+
* @dev Each bytes32 contains an address (160 bits) and allocation (96 bits)
95+
* @param packedData Array of packed address+allocation data
96+
*/
97+
function setAllocations(bytes32[] calldata packedData) external;
98+
99+
/**
100+
* @notice Returns whether claiming is open
101+
* @return Whether claiming is open
102+
*/
103+
function claimIsOpen() external view returns (bool);
104+
105+
/**
106+
* @notice Claims tokens for the caller
107+
* @param _claimTo The address to send the tokens to
108+
*/
109+
function claim(address _claimTo) external;
110+
111+
/**
112+
* @notice Claims tokens on behalf of a user with their signature
113+
* @param _user The user who is delegating their claim
114+
* @param _claimTo The address to send the tokens to
115+
* @param _deadline The deadline for the signature to be valid
116+
* @param _signature The signature authorizing the claim
117+
*/
118+
function claimWithSignature(address _user, address _claimTo, uint256 _deadline, bytes calldata _signature) external;
119+
120+
/**
121+
* @notice Returns the domain separator used for EIP-712 signatures
122+
* @return The domain separator
123+
*/
124+
function getDomainSeparator() external view returns (bytes32);
125+
}

src/claim/ZoraTokenCommunityClaim.sol

Lines changed: 12 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -5,37 +5,26 @@ import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
55
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
66
import {SignatureChecker} from "@openzeppelin/contracts/utils/cryptography/SignatureChecker.sol";
77
import {EIP712} from "@openzeppelin/contracts/utils/cryptography/EIP712.sol";
8+
import {IZoraTokenCommunityClaim} from "./IZoraTokenCommunityClaim.sol";
89

910
/// @title Zora Token Claim
1011
/// @notice Contract for distributing tokens to the Zora community
1112
/// @dev Allows an admin to set allocations in advance using a storage mapping of addresses to amounts,
1213
/// avoiding the blockspace congestion that can occur with merkle proofs during claiming
13-
contract ZoraTokenCommunityClaim is EIP712 {
14+
contract ZoraTokenCommunityClaim is IZoraTokenCommunityClaim, EIP712 {
1415
string private constant DOMAIN_NAME = "ZoraTokenCommunityClaim";
1516
string private constant DOMAIN_VERSION = "1";
1617

1718
// Type hash for the ClaimWithSignature struct
1819
bytes32 private constant CLAIM_TYPEHASH = keccak256("ClaimWithSignature(address user,address claimTo,uint256 deadline)");
1920

20-
address public immutable admin;
21-
uint256 public immutable claimStart;
21+
address public immutable override admin;
22+
uint256 public immutable override claimStart;
2223
IERC20 public immutable token;
2324

2425
// Compact allocations stored as uint96 (token count, will be multiplied by 1e18)
2526
mapping(address => uint96) private compactAllocations;
26-
mapping(address => bool) public hasClaimed;
27-
28-
error OnlyAdmin();
29-
error ClaimNotOpen();
30-
error ClaimOpened();
31-
error ArrayLengthMismatch();
32-
error NoAllocation();
33-
error AlreadyClaimed();
34-
error InvalidSignature();
35-
error SignatureExpired();
36-
37-
event AllocationsSet(bytes32[] indexed allocations);
38-
event Claimed(address indexed account, address indexed claimTo, uint256 amount);
27+
mapping(address => bool) public override hasClaimed;
3928

4029
constructor(address _admin, uint256 _claimStart, address _token) EIP712(DOMAIN_NAME, DOMAIN_VERSION) {
4130
admin = _admin;
@@ -44,14 +33,14 @@ contract ZoraTokenCommunityClaim is EIP712 {
4433
}
4534

4635
// Public view function to get the full allocation amount with 18 decimals
47-
function allocations(address user) public view returns (uint256) {
36+
function allocations(address user) public view override returns (uint256) {
4837
return uint256(compactAllocations[user]);
4938
}
5039

5140
/// @notice Sets allocations using packed data format for gas efficiency
5241
/// @dev Each bytes32 contains an address (160 bits) and allocation (96 bits)
5342
/// @param packedData Array of packed address+allocation data
54-
function setAllocations(bytes32[] calldata packedData) external {
43+
function setAllocations(bytes32[] calldata packedData) external override {
5544
require(msg.sender == admin, OnlyAdmin());
5645
require(!claimIsOpen(), ClaimOpened());
5746

@@ -69,11 +58,11 @@ contract ZoraTokenCommunityClaim is EIP712 {
6958
emit AllocationsSet(packedData);
7059
}
7160

72-
function claimIsOpen() public view returns (bool) {
61+
function claimIsOpen() public view override returns (bool) {
7362
return block.timestamp >= claimStart;
7463
}
7564

76-
function claim(address _claimTo) external {
65+
function claim(address _claimTo) external override {
7766
_claim(msg.sender, _claimTo);
7867
}
7968

@@ -82,7 +71,7 @@ contract ZoraTokenCommunityClaim is EIP712 {
8271
/// @param _claimTo The address to send the tokens to
8372
/// @param _deadline The deadline for the signature to be valid
8473
/// @param _signature The signature authorizing the claim
85-
function claimWithSignature(address _user, address _claimTo, uint256 _deadline, bytes calldata _signature) external {
74+
function claimWithSignature(address _user, address _claimTo, uint256 _deadline, bytes calldata _signature) external override {
8675
require(block.timestamp <= _deadline, SignatureExpired());
8776

8877
// Verify signature
@@ -113,8 +102,8 @@ contract ZoraTokenCommunityClaim is EIP712 {
113102
SafeERC20.safeTransfer(token, _claimTo, amount);
114103
}
115104

116-
// Make the domain separator accessible for testing
117-
function getDomainSeparator() public view returns (bytes32) {
105+
/// @notice Make the domain separator accessible for testing
106+
function getDomainSeparator() public view override returns (bytes32) {
118107
return _domainSeparatorV4();
119108
}
120109
}

test/ZoraTokenCommunityClaim.t.sol

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ pragma solidity ^0.8.28;
44
import {Test} from "forge-std/Test.sol";
55
import {Zora} from "../src/zora/Zora.sol";
66
import {ZoraTokenCommunityClaim} from "../src/claim/ZoraTokenCommunityClaim.sol";
7+
import {IZoraTokenCommunityClaim} from "../src/claim/IZoraTokenCommunityClaim.sol";
78
import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
89

910
interface IMultiOwnable {
@@ -98,7 +99,7 @@ contract ZoraTokenCommunityClaimTest is Test {
9899
bytes32[] memory compactAllocations = toCompactAllocations(accounts, amounts);
99100

100101
vm.prank(makeAddr("not-admin"));
101-
vm.expectRevert(ZoraTokenCommunityClaim.OnlyAdmin.selector);
102+
vm.expectRevert(IZoraTokenCommunityClaim.OnlyAdmin.selector);
102103
claim.setAllocations(compactAllocations);
103104
}
104105

@@ -115,7 +116,7 @@ contract ZoraTokenCommunityClaimTest is Test {
115116
vm.warp(claimStart + 1);
116117

117118
vm.prank(deployer);
118-
vm.expectRevert(ZoraTokenCommunityClaim.ClaimOpened.selector);
119+
vm.expectRevert(IZoraTokenCommunityClaim.ClaimOpened.selector);
119120
claim.setAllocations(compactAllocations);
120121
}
121122

@@ -161,7 +162,7 @@ contract ZoraTokenCommunityClaimTest is Test {
161162
claim.setAllocations(compactAllocations);
162163

163164
vm.prank(user1);
164-
vm.expectRevert(ZoraTokenCommunityClaim.ClaimNotOpen.selector);
165+
vm.expectRevert(IZoraTokenCommunityClaim.ClaimNotOpen.selector);
165166
claim.claim(user1);
166167
}
167168

@@ -234,7 +235,7 @@ contract ZoraTokenCommunityClaimTest is Test {
234235
claim.claimWithSignature(signer, recipient, deadline, signature);
235236

236237
// Second claim should fail due to no allocation
237-
vm.expectRevert(ZoraTokenCommunityClaim.AlreadyClaimed.selector);
238+
vm.expectRevert(IZoraTokenCommunityClaim.AlreadyClaimed.selector);
238239
claim.claimWithSignature(signer, recipient, deadline, signature);
239240
}
240241

@@ -261,7 +262,7 @@ contract ZoraTokenCommunityClaimTest is Test {
261262
(uint8 v, bytes32 r, bytes32 s) = vm.sign(privateKey, digest);
262263
bytes memory signature = abi.encodePacked(r, s, v);
263264

264-
vm.expectRevert(ZoraTokenCommunityClaim.SignatureExpired.selector);
265+
vm.expectRevert(IZoraTokenCommunityClaim.SignatureExpired.selector);
265266
claim.claimWithSignature(signer, recipient, deadline, signature);
266267
}
267268

@@ -288,7 +289,7 @@ contract ZoraTokenCommunityClaimTest is Test {
288289
(uint8 v, bytes32 r, bytes32 s) = vm.sign(wrongPrivateKey, digest);
289290
bytes memory signature = abi.encodePacked(r, s, v);
290291

291-
vm.expectRevert(ZoraTokenCommunityClaim.InvalidSignature.selector);
292+
vm.expectRevert(IZoraTokenCommunityClaim.InvalidSignature.selector);
292293
claim.claimWithSignature(signer, recipient, deadline, signature);
293294
}
294295

@@ -365,7 +366,7 @@ contract ZoraTokenCommunityClaimTest is Test {
365366
bytes memory signature = abi.encodePacked(r, s, v);
366367

367368
// Attempt to claim with invalid signature should fail
368-
vm.expectRevert(ZoraTokenCommunityClaim.InvalidSignature.selector);
369+
vm.expectRevert(IZoraTokenCommunityClaim.InvalidSignature.selector);
369370
claim.claimWithSignature(address(smartWallet), recipient, deadline, signature);
370371
}
371372
}

0 commit comments

Comments
 (0)