-
Notifications
You must be signed in to change notification settings - Fork 8
Expand file tree
/
Copy pathERC20stakingg
More file actions
150 lines (120 loc) · 5.23 KB
/
ERC20stakingg
File metadata and controls
150 lines (120 loc) · 5.23 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
// SPDX-License-Identifier: MIT
// Compatible with OpenZeppelin Contracts ^5.6.0
pragma solidity ^0.8.27;
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
contract ERC20Staking is Ownable {
struct Staking {
uint256 totalAmount;
uint256 claimed;
uint64 startTime;
uint64 endTime;
uint64 cliff;
uint64 minimalAmountToClaim;
uint64 cooldownEndsAt;
}
/// @notice Vesting data per beneficiary
mapping(address => Staking) public stakes;
/// @notice ERC20 token used for payouts
IERC20 public token;
/// @notice Global vesting schedule parameters (shared by all beneficiaries)
uint64 public globalStartTime;
uint64 public globalEndTime;
uint64 public globalCliff;
uint64 public globalMinimalAmountToClaim;
/// @notice Cooldown duration (in seconds) after each claim
uint64 public cooldownDuration;
/// @notice Total amount allocated across all beneficiaries
uint256 public totalAllocated;
event VestingStarted(address indexed beneficiary, uint256 amount);
event Claimed(address indexed beneficiary, uint256 amount);
event CooldownSet(address indexed beneficiary, uint64 until);
constructor(
address token_,
uint64 startTime_,
uint64 endTime_,
uint64 cliff_,
uint64 minimalAmountToClaim_,
uint64 cooldownDuration_
) Ownable(msg.sender) {
require(token_ != address(0), "Token is zero");
require(startTime_ < endTime_, "Invalid time range");
require(cliff_ >= startTime_, "Cliff before start");
token = IERC20(token_);
globalStartTime = startTime_;
globalEndTime = endTime_;
globalCliff = cliff_;
globalMinimalAmountToClaim = minimalAmountToClaim_;
cooldownDuration = cooldownDuration_;
}
/// @notice Fill or extend vesting stakes for multiple beneficiaries.
/// @dev Assumes the contract is already funded with enough tokens.
function startVesting(
address[] calldata beneficiaries,
uint256[] calldata amounts
) external onlyOwner {
uint256 length = beneficiaries.length;
require(length == amounts.length && length > 0, "Invalid input");
uint256 addedTotal;
for (uint256 i = 0; i < length; i++) {
address beneficiary = beneficiaries[i];
uint256 amount = amounts[i];
require(beneficiary != address(0), "Zero beneficiary");
require(amount > 0, "Zero amount");
Staking storage s = stakes[beneficiary];
// First time configuration for this beneficiary
if (s.totalAmount == 0 && s.claimed == 0) {
s.startTime = globalStartTime;
s.endTime = globalEndTime;
s.cliff = globalCliff;
s.minimalAmountToClaim = globalMinimalAmountToClaim;
} else {
// Ensure that all schedules remain aligned with the global one
require(
s.startTime == globalStartTime &&
s.endTime == globalEndTime &&
s.cliff == globalCliff &&
s.minimalAmountToClaim == globalMinimalAmountToClaim,
"Schedule mismatch"
);
}
s.totalAmount += amount;
addedTotal += amount;
emit VestingStarted(beneficiary, amount);
}
totalAllocated += addedTotal;
require(totalAllocated <= token.balanceOf(address(this)), "Insufficient pool");
}
/// @notice Claim vested tokens for the caller.
/// @param requestedAmount If 0, claims full available amount, otherwise
/// claims the minimum of requestedAmount and the currently claimable amount.
function claim(uint256 requestedAmount) external {
Staking storage s = stakes[msg.sender];
require(s.totalAmount > 0, "No vesting");
require(block.timestamp >= s.startTime, "Vesting not started");
require(block.timestamp >= s.cliff, "Cliff not reached");
require(block.timestamp < s.endTime, "Vesting ended");
require(block.timestamp >= s.cooldownEndsAt, "Cooldown active");
uint256 duration = uint256(s.endTime) - uint256(s.startTime);
require(duration > 0, "Invalid duration");
uint256 timePassed = block.timestamp - uint256(s.startTime);
if (timePassed > duration) {
timePassed = duration;
}
uint256 vestedTotal = (s.totalAmount * timePassed) / duration;
if (vestedTotal <= s.claimed) {
revert("Nothing to claim");
}
uint256 claimable = vestedTotal - s.claimed;
require(claimable >= s.minimalAmountToClaim, "Too small");
uint256 claimAmount = claimable;
if (requestedAmount > 0 && requestedAmount < claimable) {
claimAmount = requestedAmount;
}
s.claimed += claimAmount;
s.cooldownEndsAt = uint64(block.timestamp + cooldownDuration);
require(token.transfer(msg.sender, claimAmount), "Transfer failed");
emit Claimed(msg.sender, claimAmount);
emit CooldownSet(msg.sender, s.cooldownEndsAt);
}
}