-
Notifications
You must be signed in to change notification settings - Fork 5
Expand file tree
/
Copy pathGitlawbStaking.sol
More file actions
281 lines (217 loc) · 11.3 KB
/
Copy pathGitlawbStaking.sol
File metadata and controls
281 lines (217 loc) · 11.3 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
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import {IERC20} from "lib/forge-std/src/interfaces/IERC20.sol";
/// @title GitlawbStaking
/// @notice Stake $GITLAWB to earn protocol fee revenue share on Base L2.
///
/// Four tiers with multiplied rewards:
/// Observer (1,000+) → 1x
/// Curator (10,000+) → 2x
/// Steward (100,000+) → 4x
/// Validator (1,000,000+) → 8x
///
/// Revenue is deposited by the treasury (from bounty fees, etc.)
/// and distributed pro-rata to stakers weighted by tier multiplier.
/// 7-day cooldown on unstaking to prevent reward sniping.
contract GitlawbStaking {
// ── Types ────────────────────────────────────────────────────────────────
enum Tier { None, Observer, Curator, Steward, Validator }
struct StakeInfo {
uint256 amount;
uint256 rewardDebt; // accumulated reward offset (for pro-rata calc)
uint256 pendingRewards; // unclaimed rewards
uint256 unstakeRequestAt; // timestamp of unstake request (0 = none)
uint256 unstakeAmount; // amount requested to unstake
}
// ── Constants ────────────────────────────────────────────────────────────
uint256 public constant OBSERVER_THRESHOLD = 1_000 * 1e18;
uint256 public constant CURATOR_THRESHOLD = 10_000 * 1e18;
uint256 public constant STEWARD_THRESHOLD = 100_000 * 1e18;
uint256 public constant VALIDATOR_THRESHOLD = 1_000_000 * 1e18;
uint256 public constant COOLDOWN_PERIOD = 7 days;
// Precision for reward-per-share calculations
uint256 private constant ACC_PRECISION = 1e18;
// ── Storage ──────────────────────────────────────────────────────────────
IERC20 public immutable token;
address public owner;
mapping(address => StakeInfo) public stakes;
uint256 public totalWeightedStake; // sum of (amount * multiplier) across all stakers
uint256 public accRewardPerShare; // accumulated rewards per weighted share
uint256 public totalStaked; // total raw tokens staked
uint256 public totalRewardsDistributed; // lifetime rewards deposited
// ── Events ───────────────────────────────────────────────────────────────
event Staked(address indexed staker, uint256 amount, Tier tier);
event UnstakeRequested(address indexed staker, uint256 amount, uint256 availableAt);
event Unstaked(address indexed staker, uint256 amount);
event RewardsClaimed(address indexed staker, uint256 amount);
event RevenueDeposited(address indexed depositor, uint256 amount);
// ── Errors ───────────────────────────────────────────────────────────────
error NotOwner();
error InvalidAmount();
error InsufficientStake();
error CooldownNotElapsed();
error NoPendingUnstake();
error NoRewards();
error TransferFailed();
error ZeroAddress();
error BelowMinimumStake();
// ── Constructor ──────────────────────────────────────────────────────────
constructor(address _token) {
token = IERC20(_token);
owner = msg.sender;
}
// ── Core functions ───────────────────────────────────────────────────────
/// Stake $GITLAWB tokens. Must have approved this contract first.
function stake(uint256 amount) external {
if (amount == 0) revert InvalidAmount();
StakeInfo storage info = stakes[msg.sender];
// Harvest pending rewards before changing stake
_harvest(msg.sender);
bool ok = token.transferFrom(msg.sender, address(this), amount);
if (!ok) revert TransferFailed();
// Remove old weighted stake
uint256 oldWeight = _weightedAmount(info.amount);
totalWeightedStake -= oldWeight;
info.amount += amount;
totalStaked += amount;
// Must meet minimum tier threshold
if (info.amount < OBSERVER_THRESHOLD) revert BelowMinimumStake();
// Add new weighted stake
uint256 newWeight = _weightedAmount(info.amount);
totalWeightedStake += newWeight;
// Reset reward debt to current accumulated value
info.rewardDebt = (newWeight * accRewardPerShare) / ACC_PRECISION;
emit Staked(msg.sender, amount, getTier(msg.sender));
}
/// Request unstake — starts 7-day cooldown.
function requestUnstake(uint256 amount) external {
StakeInfo storage info = stakes[msg.sender];
if (amount == 0) revert InvalidAmount();
if (amount > info.amount) revert InsufficientStake();
// Harvest pending rewards first
_harvest(msg.sender);
info.unstakeRequestAt = block.timestamp;
info.unstakeAmount = amount;
emit UnstakeRequested(msg.sender, amount, block.timestamp + COOLDOWN_PERIOD);
}
/// Complete unstake after cooldown has elapsed.
function unstake() external {
StakeInfo storage info = stakes[msg.sender];
if (info.unstakeAmount == 0) revert NoPendingUnstake();
if (block.timestamp < info.unstakeRequestAt + COOLDOWN_PERIOD) revert CooldownNotElapsed();
uint256 amount = info.unstakeAmount;
// Remove old weighted stake
uint256 oldWeight = _weightedAmount(info.amount);
totalWeightedStake -= oldWeight;
info.amount -= amount;
totalStaked -= amount;
// Add new weighted stake (may be 0)
uint256 newWeight = _weightedAmount(info.amount);
totalWeightedStake += newWeight;
// Reset reward debt
info.rewardDebt = (newWeight * accRewardPerShare) / ACC_PRECISION;
// Clear unstake request
info.unstakeAmount = 0;
info.unstakeRequestAt = 0;
bool ok = token.transfer(msg.sender, amount);
if (!ok) revert TransferFailed();
emit Unstaked(msg.sender, amount);
}
/// Claim accumulated rewards.
function claimRewards() external {
_harvest(msg.sender);
StakeInfo storage info = stakes[msg.sender];
uint256 rewards = info.pendingRewards;
if (rewards == 0) revert NoRewards();
info.pendingRewards = 0;
bool ok = token.transfer(msg.sender, rewards);
if (!ok) revert TransferFailed();
emit RewardsClaimed(msg.sender, rewards);
}
/// Deposit revenue (protocol fees) for distribution to stakers.
/// Called by treasury/bounty contract when fees are collected.
function depositRevenue(uint256 amount) external {
if (amount == 0) revert InvalidAmount();
if (totalWeightedStake == 0) revert InvalidAmount(); // no stakers
bool ok = token.transferFrom(msg.sender, address(this), amount);
if (!ok) revert TransferFailed();
accRewardPerShare += (amount * ACC_PRECISION) / totalWeightedStake;
totalRewardsDistributed += amount;
emit RevenueDeposited(msg.sender, amount);
}
// ── View functions ───────────────────────────────────────────────────────
/// Get the staking tier for an address.
function getTier(address staker) public view returns (Tier) {
uint256 amount = stakes[staker].amount;
if (amount >= VALIDATOR_THRESHOLD) return Tier.Validator;
if (amount >= STEWARD_THRESHOLD) return Tier.Steward;
if (amount >= CURATOR_THRESHOLD) return Tier.Curator;
if (amount >= OBSERVER_THRESHOLD) return Tier.Observer;
return Tier.None;
}
/// Get tier multiplier (1x, 2x, 4x, 8x).
function getTierMultiplier(Tier tier) public pure returns (uint256) {
if (tier == Tier.Validator) return 8;
if (tier == Tier.Steward) return 4;
if (tier == Tier.Curator) return 2;
if (tier == Tier.Observer) return 1;
return 0;
}
/// Pending (unclaimed) rewards for a staker.
function pendingRewards(address staker) external view returns (uint256) {
StakeInfo storage info = stakes[staker];
uint256 weight = _weightedAmount(info.amount);
uint256 accumulated = (weight * accRewardPerShare) / ACC_PRECISION;
return info.pendingRewards + accumulated - info.rewardDebt;
}
/// Full staker info.
function getStakeInfo(address staker) external view returns (
uint256 amount,
Tier tier,
uint256 multiplier,
uint256 _pendingRewards,
uint256 unstakeRequestAt,
uint256 unstakeAmount
) {
StakeInfo storage info = stakes[staker];
Tier t = getTier(staker);
uint256 weight = _weightedAmount(info.amount);
uint256 accumulated = (weight * accRewardPerShare) / ACC_PRECISION;
uint256 pending = info.pendingRewards + accumulated - info.rewardDebt;
return (info.amount, t, getTierMultiplier(t), pending, info.unstakeRequestAt, info.unstakeAmount);
}
/// Protocol-level stats.
function getProtocolStats() external view returns (
uint256 _totalStaked,
uint256 _totalWeightedStake,
uint256 _totalRewardsDistributed,
uint256 _accRewardPerShare
) {
return (totalStaked, totalWeightedStake, totalRewardsDistributed, accRewardPerShare);
}
// ── Internal ─────────────────────────────────────────────────────────────
function _harvest(address staker) internal {
StakeInfo storage info = stakes[staker];
if (info.amount == 0) return;
uint256 weight = _weightedAmount(info.amount);
uint256 accumulated = (weight * accRewardPerShare) / ACC_PRECISION;
uint256 pending = accumulated - info.rewardDebt;
if (pending > 0) {
info.pendingRewards += pending;
}
info.rewardDebt = accumulated;
}
function _weightedAmount(uint256 amount) internal pure returns (uint256) {
if (amount >= VALIDATOR_THRESHOLD) return amount * 8;
if (amount >= STEWARD_THRESHOLD) return amount * 4;
if (amount >= CURATOR_THRESHOLD) return amount * 2;
if (amount >= OBSERVER_THRESHOLD) return amount;
return 0;
}
// ── Admin ────────────────────────────────────────────────────────────────
function transferOwnership(address newOwner) external {
if (msg.sender != owner) revert NotOwner();
if (newOwner == address(0)) revert ZeroAddress();
owner = newOwner;
}
}