-
Notifications
You must be signed in to change notification settings - Fork 6
/
Copy pathvoteEscrow.sol
225 lines (189 loc) · 8.38 KB
/
voteEscrow.sol
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
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.7;
import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Burnable.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Votes.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import "../interfaces/IVotingEscrow.sol";
contract VoteEscrow is ERC20Votes, ReentrancyGuard, Ownable, IVotingEscrow {
using SafeERC20 for IERC20;
struct LockedBalance {
uint256 amount;
uint256 end;
}
// flags
uint256 public constant MINDAYS = 7;
uint256 public constant MAXDAYS = 3 * 365;
uint256 public constant MAXTIME = MAXDAYS * 1 days; // 3 years
uint256 public constant MAX_WITHDRAWAL_PENALTY = 50000; // 50%
uint256 public constant PRECISION = 100000; // 5 decimals
address public lockedToken;
address public penaltyCollector;
uint256 public minLockedAmount;
uint256 public earlyWithdrawPenaltyRate;
uint256 public override supply;
mapping(address => LockedBalance) public locked;
mapping(address => uint256) public mintedForLock;
address public constant burn = 0x000000000000000000000000000000000000dEaD;
/* =============== EVENTS ==================== */
event Deposit(address indexed provider, uint256 value, uint256 locktime, uint256 timestamp);
event Withdraw(address indexed provider, uint256 value, uint256 timestamp);
event PenaltyCollectorSet(address indexed addr);
event EarlyWithdrawPenaltySet(uint256 indexed penalty);
event MinLockedAmountSet(uint256 indexed amount);
constructor(
string memory _name,
string memory _symbol,
address _lockedToken,
uint256 _minLockedAmount
) ERC20(_name, _symbol) ERC20Permit(_name) {
lockedToken = _lockedToken;
minLockedAmount = _minLockedAmount;
earlyWithdrawPenaltyRate = 30000; // 30%
}
function deposit_for(address _addr, uint256 _value) external override {
require(_value >= minLockedAmount, "VE:VL0");
_deposit_for(_addr, _value, 0);
}
function create_lock(uint256 _value, uint256 _days) external override {
require(_value >= minLockedAmount, "less than min amount");
require(locked[_msgSender()].amount == 0, "Withdraw old tokens first");
require(_days >= MINDAYS, "Voting lock can be 7 days min");
require(_days <= MAXDAYS, "Voting lock can be 4 years max");
_deposit_for(_msgSender(), _value, _days);
}
function increase_amount(uint256 _value) external override {
require(_value >= minLockedAmount, "less than min amount");
_deposit_for(_msgSender(), _value, 0);
}
function increase_unlock_time(uint256 _days) external override {
require(_days >= MINDAYS, "Voting lock can be 7 days min");
require(_days <= MAXDAYS, "Voting lock can be 4 years max");
_deposit_for(_msgSender(), 0, _days);
}
function withdraw() external override nonReentrant {
LockedBalance storage _locked = locked[_msgSender()];
uint256 _now = block.timestamp;
require(_locked.amount > 0, "Nothing to withdraw");
require(_now >= _locked.end, "The lock didn't expire");
uint256 _amount = _locked.amount;
_locked.end = 0;
_locked.amount = 0;
_burn(_msgSender(), mintedForLock[_msgSender()]);
mintedForLock[_msgSender()] = 0;
IERC20(lockedToken).safeTransfer(_msgSender(), _amount);
emit Withdraw(_msgSender(), _amount, _now);
}
// This will charge PENALTY if lock is not expired yet
function emergencyWithdraw() external nonReentrant {
LockedBalance storage _locked = locked[_msgSender()];
uint256 _now = block.timestamp;
require(_locked.amount > 0, "Nothing to withdraw");
uint256 _amount = _locked.amount;
if (_now < _locked.end) {
uint256 _fee = (_amount * earlyWithdrawPenaltyRate) / PRECISION;
_penalize(_fee);
_amount = _amount - _fee;
}
_locked.end = 0;
supply -= _locked.amount;
_locked.amount = 0;
_burn(_msgSender(), mintedForLock[_msgSender()]);
mintedForLock[_msgSender()] = 0;
IERC20(lockedToken).safeTransfer(_msgSender(), _amount);
emit Withdraw(_msgSender(), _amount, _now);
}
/* ========== RESTRICTED FUNCTIONS ========== */
function setMinLockedAmount(uint256 _minLockedAmount) external onlyOwner {
minLockedAmount = _minLockedAmount;
emit MinLockedAmountSet(_minLockedAmount);
}
function setEarlyWithdrawPenaltyRate(uint256 _earlyWithdrawPenaltyRate) external onlyOwner {
require(_earlyWithdrawPenaltyRate <= MAX_WITHDRAWAL_PENALTY, "withdrawal penalty is too high"); // <= 50%
earlyWithdrawPenaltyRate = _earlyWithdrawPenaltyRate;
emit EarlyWithdrawPenaltySet(_earlyWithdrawPenaltyRate);
}
function setPenaltyCollector(address _addr) external onlyOwner {
penaltyCollector = _addr;
emit PenaltyCollectorSet(_addr);
}
/* ========== PUBLIC FUNCTIONS ========== */
function locked__of(address _addr) external view returns (uint256) {
return locked[_addr].amount;
}
function locked__end(address _addr) external view returns (uint256) {
return locked[_addr].end;
}
function voting_power_unlock_time(uint256 _value, uint256 _unlockTime) public view returns (uint256) {
uint256 _now = block.timestamp;
if (_unlockTime <= _now) return 0;
uint256 _lockedSeconds = _unlockTime - _now;
if (_lockedSeconds >= MAXTIME) {
return _value;
}
return (_value * _lockedSeconds) / MAXTIME;
}
function voting_power_locked_days(uint256 _value, uint256 _days) public view returns (uint256) {
if (_days >= MAXDAYS) {
return _value;
}
return (_value * _days) / MAXDAYS;
}
/* ========== INTERNAL FUNCTIONS ========== */
function _deposit_for(address _addr, uint256 _value, uint256 _days) internal nonReentrant {
LockedBalance storage _locked = locked[_addr];
uint256 _now = block.timestamp;
uint256 _amount = _locked.amount;
uint256 _end = _locked.end;
uint256 _vp;
if (_amount == 0) {
_vp = voting_power_locked_days(_value, _days);
_locked.amount = _value;
_locked.end = _now + _days * 1 days;
} else if (_days == 0) {
_vp = voting_power_unlock_time(_value, _end);
_locked.amount = _amount + _value;
} else {
require(_value == 0, "Cannot increase amount and extend lock in the same time");
_vp = voting_power_locked_days(_amount, _days);
_locked.end = _end + _days * 1 days;
require(_locked.end - _now <= MAXTIME, "Cannot extend lock to more than 4 years");
}
require(_vp > 0, "No benefit to lock");
if (_value > 0) {
IERC20(lockedToken).safeTransferFrom(_msgSender(), address(this), _value);
}
_mint(_addr, _vp);
mintedForLock[_addr] += _vp;
supply += _value;
emit Deposit(_addr, _locked.amount, _locked.end, _now);
}
function _penalize(uint256 _amount) internal {
// TODO: we cannot burn univ2/sushi LP tokens, therefore they need to be sent to 0xdead or this needs to change
if (penaltyCollector != address(0)) {
// send to collector if `penaltyCollector` set
IERC20(lockedToken).safeTransfer(penaltyCollector, _amount);
} else {
ERC20Burnable(lockedToken).burn(_amount);
}
}
function _withdraw() internal {
LockedBalance storage _locked = locked[_msgSender()];
uint256 _now = block.timestamp;
require(_locked.amount > 0, "Nothing to withdraw");
uint256 _amount = _locked.amount;
if (_now < _locked.end) {
uint256 _fee = (_amount * earlyWithdrawPenaltyRate) / PRECISION;
_penalize(_fee);
_amount = _amount - _fee;
}
_locked.end = 0;
_locked.amount = 0;
_burn(_msgSender(), mintedForLock[_msgSender()]);
mintedForLock[_msgSender()] = 0;
supply -= _amount;
IERC20(lockedToken).safeTransfer(_msgSender(), _amount);
emit Withdraw(_msgSender(), _amount, _now);
}
}