-
Notifications
You must be signed in to change notification settings - Fork 6
/
RevenueHandler.sol
327 lines (277 loc) · 13.7 KB
/
RevenueHandler.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
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
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
// SPDX-License-Identifier: GPL-3
pragma solidity ^0.8.15;
import "src/interfaces/IRevenueHandler.sol";
import "src/interfaces/IPoolAdapter.sol";
import "src/interfaces/IVotingEscrow.sol";
import "lib/v2-foundry/src/interfaces/IAlchemistV2.sol";
import "lib/v2-foundry/src/base/ErrorMessages.sol";
import "openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol";
import "openzeppelin-contracts/contracts/access/Ownable.sol";
import "openzeppelin-contracts/contracts/token/ERC20/IERC20.sol";
/// @title RevenueHandler
/*
This contract is meant to receive all revenue from the Alchemix protocol, and allow
veALCX stakers to claim it, primarily as a form of debt repayment.
IPoolAdapter contracts are used to plug into various DEXes so that revenue tokens (dai, usdc, weth, etc)
can be traded for alAssets (alUSD, alETH, etc). Once per epoch (at the beginning of the epoch)
the `checkpoint()` function needs to be called so that any revenue accrued since the last checkpoint
can be melted into its relative alAsset. After `checkpoint()` is called, the current epoch's revenue
is available to be claimed by veALCX stakers (as long as they were staked before `checkpoint()` was
called).
veALCX stakers can claim some or all of their available revenue. When a staker calls `claim()`, they
choose an amount, target alchemist, and target recipient. The RevenueHandler will `burn()` up to
`amount` of the alAsset used by `alchemist` on `recipient`'s account. Any leftover revenue that
is not burned will be sent directly to `recipient.
Any revenue that is not an Alchemix protocol debt token will be sent directly to users
*/
contract RevenueHandler is IRevenueHandler, Ownable {
using SafeERC20 for IERC20;
/// @notice Parameters to define actions with respect to melting a revenue token for alchemic-tokens.
struct RevenueTokenConfig {
/// The target alchemic-token.
address debtToken;
/// A IPoolAdapter that can be used to trade revenue token for `debtToken`.
address poolAdapter;
/// A flag to enable or disable the revenue token
bool disabled;
}
/// @notice A checkpoint on the state of a user's account for a given debtToken
struct Claimable {
/// An amount of the target debtToken that the user can currently claim (leftover from an incomplete claim).
uint256 unclaimed;
/// The last epoch that the user claimed the target debtToken.
uint256 lastClaimEpoch;
}
uint256 internal constant WEEK = 2 weeks;
uint256 internal constant BPS = 10_000;
/// @notice The veALCX contract address.
address public immutable veALCX;
/// @notice The list of revenue tokens.
address[] public revenueTokens;
/// @notice A mapping of alchemists to their alchemic-tokens.
mapping(address => address) public alchemists;
/// @notice A mapping of revenue tokens to their configurations.
mapping(address => RevenueTokenConfig) public revenueTokenConfigs;
/// @notice A mapping of epoch to a mapping of debtToken to epoch revenue.
mapping(uint256 => mapping(address => uint256)) public epochRevenues;
/// @notice A mapping of tokenId to a mapping of debtToken to a user's claimable amount.
mapping(uint256 => mapping(address => Claimable)) public userCheckpoints;
/// @notice The current epoch.
uint256 public currentEpoch;
/// @notice The address of the treasury.
address public treasury;
/// @notice The percentage of revenue that goes to the treasury.
uint256 public treasuryPct;
constructor(address _veALCX, address _treasury, uint256 _treasuryPct) Ownable() {
veALCX = _veALCX;
require(_treasury != address(0), "treasury cannot be 0x0");
treasury = _treasury;
require(treasuryPct <= BPS, "treasury pct too large");
treasuryPct = _treasuryPct;
}
/*
View functions
*/
/// @inheritdoc IRevenueHandler
function claimable(uint256 tokenId, address token) external view override returns (uint256) {
return _claimable(tokenId, token);
}
/*
Admin functions
*/
/// @inheritdoc IRevenueHandler
function addRevenueToken(address revenueToken) external override onlyOwner {
uint256 length = revenueTokens.length;
for (uint256 i = 0; i < length; i++) {
if (revenueTokens[i] == revenueToken) {
revert("revenue token already exists");
}
}
revenueTokens.push(revenueToken);
emit RevenueTokenTokenAdded(revenueToken);
}
/// @inheritdoc IRevenueHandler
function removeRevenueToken(address revenueToken) external override onlyOwner {
uint256 length = revenueTokens.length;
for (uint256 i = 0; i < length; i++) {
if (revenueTokens[i] == revenueToken) {
revenueTokens[i] = revenueTokens[length - 1];
revenueTokens.pop();
emit RevenueTokenTokenRemoved(revenueToken);
return;
}
}
revert("revenue token does not exist");
}
/// @inheritdoc IRevenueHandler
function addAlchemicToken(address alchemist) external override onlyOwner {
require(alchemists[alchemist] == address(0), "alchemic token already exists");
address alchemicToken = IAlchemistV2(alchemist).debtToken();
alchemists[alchemist] = alchemicToken;
emit AlchemicTokenAdded(alchemist, alchemicToken);
}
/// @inheritdoc IRevenueHandler
function removeAlchemicToken(address alchemist) external override onlyOwner {
address alchemicToken = alchemists[alchemist];
require(alchemicToken != address(0), "alchemic token does not exist");
alchemists[alchemist] = address(0);
emit AlchemicTokenRemoved(alchemist, alchemicToken);
}
/// @inheritdoc IRevenueHandler
function setDebtToken(address revenueToken, address debtToken) external override onlyOwner {
revenueTokenConfigs[revenueToken].debtToken = debtToken;
emit SetDebtToken(revenueToken, debtToken);
}
/// @inheritdoc IRevenueHandler
function setPoolAdapter(address revenueToken, address poolAdapter) external override onlyOwner {
revenueTokenConfigs[revenueToken].poolAdapter = poolAdapter;
emit SetPoolAdapter(revenueToken, poolAdapter);
}
/// @inheritdoc IRevenueHandler
function disableRevenueToken(address revenueToken) external override onlyOwner {
require(!revenueTokenConfigs[revenueToken].disabled, "Token disabled");
revenueTokenConfigs[revenueToken].disabled = true;
}
/// @inheritdoc IRevenueHandler
function enableRevenueToken(address revenueToken) external override onlyOwner {
require(revenueTokenConfigs[revenueToken].disabled, "Token enabled");
revenueTokenConfigs[revenueToken].disabled = false;
}
/// @inheritdoc IRevenueHandler
function setTreasury(address _treasury) external override onlyOwner {
require(_treasury != address(0), "treasury cannot be 0x0");
treasury = _treasury;
emit TreasuryUpdated(_treasury);
}
/// @inheritdoc IRevenueHandler
function setTreasuryPct(uint256 _treasuryPct) external override onlyOwner {
require(_treasuryPct <= BPS, "treasury pct too large");
require(_treasuryPct != treasuryPct, "treasury pct unchanged");
treasuryPct = _treasuryPct;
emit TreasuryPctUpdated(_treasuryPct);
}
/*
User functions
*/
/// @inheritdoc IRevenueHandler
function claim(
uint256 tokenId,
address token,
address alchemist,
uint256 amount,
address recipient
) external override {
require(IVotingEscrow(veALCX).isApprovedOrOwner(msg.sender, tokenId), "Not approved or owner");
uint256 amountBurned = 0;
uint256 amountClaimable = _claimable(tokenId, token);
require(amount <= amountClaimable, "Not enough claimable");
require(amount > 0, "Amount must be greater than 0");
require(amount <= IERC20(token).balanceOf(address(this)), "Not enough revenue to claim");
userCheckpoints[tokenId][token].lastClaimEpoch = currentEpoch;
userCheckpoints[tokenId][token].unclaimed = amountClaimable - amount;
// If the alchemist is defined we know it has an alchemic-token
if (alchemists[alchemist] != address(0)) {
require(token == IAlchemistV2(alchemist).debtToken(), "Invalid alchemist/alchemic-token pair");
(, address[] memory deposits) = IAlchemistV2(alchemist).accounts(recipient);
IERC20(token).approve(alchemist, amount);
// Only burn if there are deposits
amountBurned = deposits.length > 0 ? IAlchemistV2(alchemist).burn(amount, recipient) : 0;
}
/*
burn() will only burn up to total cdp debt
send the leftover directly to the user
*/
if (amountBurned < amount) {
IERC20(token).safeTransfer(recipient, amount - amountBurned);
}
emit ClaimRevenue(tokenId, token, amount, recipient);
}
/// @inheritdoc IRevenueHandler
function checkpoint() public {
// only run checkpoint() once per epoch
if (block.timestamp >= currentEpoch + WEEK /* && initializer == address(0) */) {
currentEpoch = (block.timestamp / WEEK) * WEEK;
uint256 length = revenueTokens.length;
for (uint256 i = 0; i < length; i++) {
// These will be zero if the revenue token is not an alchemic-token
uint256 treasuryAmt = 0;
uint256 amountReceived = 0;
address token = revenueTokens[i];
RevenueTokenConfig memory tokenConfig = revenueTokenConfigs[token];
// If a revenue token is disabled, skip it.
if (tokenConfig.disabled) continue;
uint256 thisBalance = IERC20(token).balanceOf(address(this));
// If poolAdapter is set, the revenue token is an alchemic-token
if (tokenConfig.poolAdapter != address(0)) {
// Treasury only receives revenue if the token is an alchemic-token
treasuryAmt = (thisBalance * treasuryPct) / BPS;
IERC20(token).safeTransfer(treasury, treasuryAmt);
// Only melt if there is an alchemic-token to melt to
amountReceived = _melt(token);
// Update amount of alchemic-token revenue received for this epoch
epochRevenues[currentEpoch][tokenConfig.debtToken] += amountReceived;
} else {
// If the revenue token doesn't have a poolAdapter, it is not an alchemic-token
amountReceived = thisBalance;
// Update amount of non-alchemic-token revenue received for this epoch
epochRevenues[currentEpoch][token] += amountReceived;
}
emit RevenueRealized(currentEpoch, token, tokenConfig.debtToken, amountReceived, treasuryAmt);
}
}
}
/*
Internal functions
*/
function _melt(address revenueToken) internal returns (uint256) {
RevenueTokenConfig storage tokenConfig = revenueTokenConfigs[revenueToken];
address poolAdapter = tokenConfig.poolAdapter;
uint256 revenueTokenBalance = IERC20(revenueToken).balanceOf(address(this));
if (revenueTokenBalance == 0) {
return 0;
}
IERC20(revenueToken).safeTransfer(poolAdapter, revenueTokenBalance);
/*
minimumAmountOut == inputAmount
Here we are making the assumption that the price of the alAsset will always be at or below the price of the revenue token.
This is currently a safe assumption since this imbalance has always held true for alUSD and alETH since their inceptions.
*/
return
IPoolAdapter(poolAdapter).melt(
revenueToken,
tokenConfig.debtToken,
revenueTokenBalance,
revenueTokenBalance
);
}
function _claimable(uint256 tokenId, address token) internal view returns (uint256) {
uint256 totalClaimable = 0;
uint256 lastClaimEpochTimestamp = userCheckpoints[tokenId][token].lastClaimEpoch;
if (lastClaimEpochTimestamp == 0) {
/*
If we get here, the user has not yet claimed anything from the RevenueHandler.
We need to get the first epoch that they deposited so we know where to start tallying from.
*/
// Get index of first epoch
uint256 lastUserEpoch = IVotingEscrow(veALCX).userFirstEpoch(tokenId);
// Get timestamp from index
lastClaimEpochTimestamp = (IVotingEscrow(veALCX).pointHistoryTimestamp(lastUserEpoch) / WEEK) * WEEK - WEEK;
}
/*
Start tallying from the "next" epoch after the last epoch that they claimed, since they already
claimed their revenue from "lastClaimEpochTimestamp".
*/
for (
uint256 epochTimestamp = lastClaimEpochTimestamp + WEEK;
epochTimestamp <= currentEpoch;
epochTimestamp += WEEK
) {
uint256 epochTotalVeSupply = IVotingEscrow(veALCX).totalSupplyAtT(epochTimestamp);
if (epochTotalVeSupply == 0) continue;
uint256 epochRevenue = epochRevenues[epochTimestamp][token];
uint256 epochUserVeBalance = IVotingEscrow(veALCX).balanceOfTokenAt(tokenId, epochTimestamp);
totalClaimable += (epochRevenue * epochUserVeBalance) / epochTotalVeSupply;
}
return totalClaimable + userCheckpoints[tokenId][token].unclaimed;
}
}