-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathRewardClaimV2.sol
More file actions
165 lines (143 loc) · 6.14 KB
/
RewardClaimV2.sol
File metadata and controls
165 lines (143 loc) · 6.14 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
/**
* --------------------------------------------------------------------
* Athena RewardClaimV2 — Final Audit-Grade Version
* Network: Base (Chain ID 8453)
* Token: $ATA
*
* Key Features:
* • Treasury-only funding (no admin drains)
* • Sorted-pair keccak256 Merkle verification
* • Tracks total claimed + sweep of unclaimed ATA
* • calldata optimization (~200 gas saved per claim)
* • Compatible with Athena Epoch Runner automation
* --------------------------------------------------------------------
*/
/// @notice Minimal ERC-20 interface (no mint/burn)
interface IERC20 {
function transfer(address to, uint256 value) external returns (bool);
function transferFrom(address from, address to, uint256 value) external returns (bool);
}
library Merkle {
/// @notice Verify Merkle proof for a given leaf and root (sorted-pair)
/// @dev Uses calldata to reduce gas costs.
function verify(bytes32[] calldata proof, bytes32 root, bytes32 leaf)
internal
pure
returns (bool ok)
{
bytes32 computed = leaf;
for (uint256 i = 0; i < proof.length; i++) {
bytes32 p = proof[i];
if (computed < p) {
computed = keccak256(abi.encodePacked(computed, p));
} else {
computed = keccak256(abi.encodePacked(p, computed));
}
}
return computed == root;
}
}
contract RewardClaimV2 {
using Merkle for bytes32[];
// ------------------------------------------------------------------
// Immutable references
// ------------------------------------------------------------------
IERC20 public immutable token;
address public immutable treasury;
// Admin (for setting roots, not funding)
address public admin;
uint256 public constant CLAIM_WINDOW = 48 hours;
struct Epoch {
bytes32 root; // Merkle root for epoch
uint256 funded; // total ATA funded for epoch
uint256 start; // epoch start timestamp
uint256 claimsOpenAt; // when claim window opened
}
mapping(uint256 => Epoch) public epochs;
mapping(uint256 => mapping(address => bool)) public claimed;
mapping(uint256 => uint256) public totalClaimed; // per-epoch claimed amount
// ------------------------------------------------------------------
// Events
// ------------------------------------------------------------------
event AdminChanged(address indexed admin);
event Funded(uint256 indexed epoch, uint256 amount);
event RootSet(uint256 indexed epoch, bytes32 root);
event Claimed(uint256 indexed epoch, address indexed user, uint256 amount);
event UnclaimedSwept(uint256 indexed epoch, uint256 amount);
// ------------------------------------------------------------------
// Modifiers
// ------------------------------------------------------------------
modifier onlyAdmin() {
require(msg.sender == admin, "not admin");
_;
}
modifier onlyTreasury() {
require(msg.sender == treasury, "only treasury");
_;
}
// ------------------------------------------------------------------
// Constructor
// ------------------------------------------------------------------
constructor(address token_, address treasury_) {
require(token_ != address(0) && treasury_ != address(0), "zero");
token = IERC20(token_);
treasury = treasury_;
admin = msg.sender;
emit AdminChanged(admin);
}
// ------------------------------------------------------------------
// Admin controls
// ------------------------------------------------------------------
/// @notice Transfer admin privileges to a new address.
function setAdmin(address a) external onlyAdmin {
require(a != address(0), "zero");
admin = a;
emit AdminChanged(a);
}
/// @notice Treasury funds a given epoch with pre-computed ATA amount.
/// @dev ATA must be approved beforehand.
function fund(uint256 epoch, uint256 amount) external onlyTreasury {
require(amount > 0, "amount");
epochs[epoch].funded += amount;
require(token.transferFrom(treasury, address(this), amount), "transfer");
emit Funded(epoch, amount);
}
/// @notice Publish the Merkle root for an epoch; opens a 48h claim window.
/// @dev Callable only by admin (automation script uses this).
function setMerkleRoot(uint256 epoch, bytes32 root) external onlyAdmin {
require(root != bytes32(0), "root");
Epoch storage e = epochs[epoch];
e.root = root;
e.claimsOpenAt = block.timestamp;
if (e.start == 0) e.start = block.timestamp;
emit RootSet(epoch, root);
}
// ------------------------------------------------------------------
// Claim Logic
// ------------------------------------------------------------------
/// @notice Claim ATA reward for a given epoch using a valid proof.
function claim(uint256 epoch, uint256 amount, bytes32[] calldata proof) external {
Epoch storage e = epochs[epoch];
require(e.root != bytes32(0), "no root");
require(block.timestamp <= e.claimsOpenAt + CLAIM_WINDOW, "window closed");
require(!claimed[epoch][msg.sender], "claimed");
bytes32 leaf = keccak256(abi.encodePacked(msg.sender, amount, epoch));
require(proof.verify(e.root, leaf), "bad proof");
claimed[epoch][msg.sender] = true;
totalClaimed[epoch] += amount;
require(token.transfer(msg.sender, amount), "transfer");
emit Claimed(epoch, msg.sender, amount);
}
/// @notice After claim window, admin can sweep unclaimed rewards back to treasury.
function sweepUnclaimed(uint256 epoch) external onlyAdmin {
Epoch storage e = epochs[epoch];
require(block.timestamp > e.claimsOpenAt + CLAIM_WINDOW, "window open");
uint256 unclaimed = e.funded - totalClaimed[epoch];
if (unclaimed > 0) {
require(token.transfer(treasury, unclaimed), "sweep");
emit UnclaimedSwept(epoch, unclaimed);
}
}
}