-
Notifications
You must be signed in to change notification settings - Fork 8
/
Copy pathLendingLedger.sol
210 lines (190 loc) · 11.6 KB
/
LendingLedger.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
// SPDX-License-Identifier: AGPL-3.0-or-later
pragma solidity ^0.8.16;
import {VotingEscrow} from "./VotingEscrow.sol";
import {GaugeController} from "./GaugeController.sol";
import {Math} from "@openzeppelin/contracts/utils/math/Math.sol";
contract LendingLedger {
// Constants
uint256 public constant WEEK = 7 days;
// State
address public governance;
GaugeController public gaugeController;
mapping(address => bool) public lendingMarketWhitelist;
/// @dev Lending Market => Lender => Epoch => Balance
mapping(address => mapping(address => mapping(uint256 => uint256))) public lendingMarketBalances; // cNote balances of users within the lending markets, indexed by epoch
/// @dev Lending Market => Lender => Epoch
mapping(address => mapping(address => uint256)) public lendingMarketBalancesEpoch; // Epoch when the last update happened
/// @dev Lending Market => Epoch => Balance
mapping(address => mapping(uint256 => uint256)) public lendingMarketTotalBalance; // Total balance locked within the market, i.e. sum of lendingMarketBalances for all
/// @dev Lending Market => Epoch
mapping(address => uint256) public lendingMarketTotalBalanceEpoch; // Epoch when the last update happened
/// @dev Lending Market => Lender => Epoch
mapping(address => mapping(address => uint256)) public userClaimedEpoch; // Until which epoch a user has claimed for a particular market (exclusive this value)
struct RewardInformation {
bool set;
uint248 amount;
}
mapping(uint256 => RewardInformation) public rewardInformation;
/// @notice Check that a provided timestamp is a valid epoch (divisible by WEEK) or infinity
/// @param _timestamp Timestamp to check
modifier is_valid_epoch(uint256 _timestamp) {
require(_timestamp % WEEK == 0 || _timestamp == type(uint256).max, "Invalid timestamp");
_;
}
modifier onlyGovernance() {
require(msg.sender == governance);
_;
}
constructor(address _gaugeController, address _governance) {
gaugeController = GaugeController(_gaugeController);
governance = _governance; // TODO: Maybe change to Oracle
}
/// @notice Fill in gaps in the user market balances history (if any exist)
/// @param _market Address of the market
/// @param _lender Address of the lender
/// @param _forwardTimestampLimit Until which epoch (provided as timestamp) should the update be applied. If it is higher than the current epoch timestamp, this will be used.
function _checkpoint_lender(
address _market,
address _lender,
uint256 _forwardTimestampLimit
) private {
uint256 currEpoch = (block.timestamp / WEEK) * WEEK;
uint256 lastUserUpdateEpoch = lendingMarketBalancesEpoch[_market][_lender];
uint256 updateUntilEpoch = Math.min(currEpoch, _forwardTimestampLimit);
if (lastUserUpdateEpoch == 0) {
// Store epoch of first deposit
userClaimedEpoch[_market][_lender] = currEpoch;
lendingMarketBalancesEpoch[_market][_lender] = currEpoch;
} else if (lastUserUpdateEpoch < currEpoch) {
// Fill in potential gaps in the user balances history
uint256 lastUserBalance = lendingMarketBalances[_market][_lender][lastUserUpdateEpoch];
for (uint256 i = lastUserUpdateEpoch; i <= updateUntilEpoch; i += WEEK) {
lendingMarketBalances[_market][_lender][i] = lastUserBalance;
}
if (updateUntilEpoch > lastUserUpdateEpoch) {
lendingMarketBalancesEpoch[_market][_lender] = updateUntilEpoch;
}
}
}
/// @notice Fill in gaps in the market total balances history (if any exist)
/// @param _market Address of the market
/// @param _forwardTimestampLimit Until which epoch (provided as timestamp) should the update be applied. If it is higher than the current epoch timestamp, this will be used.
function _checkpoint_market(address _market, uint256 _forwardTimestampLimit) private {
uint256 currEpoch = (block.timestamp / WEEK) * WEEK;
uint256 lastMarketUpdateEpoch = lendingMarketTotalBalanceEpoch[_market];
uint256 updateUntilEpoch = Math.min(currEpoch, _forwardTimestampLimit);
if (lastMarketUpdateEpoch == 0) {
lendingMarketTotalBalanceEpoch[_market] = currEpoch;
} else if (lastMarketUpdateEpoch < currEpoch) {
// Fill in potential gaps in the market total balances history
uint256 lastMarketBalance = lendingMarketTotalBalance[_market][lastMarketUpdateEpoch];
for (uint256 i = lastMarketUpdateEpoch; i <= updateUntilEpoch; i += WEEK) {
lendingMarketTotalBalance[_market][i] = lastMarketBalance;
}
if (updateUntilEpoch > lastMarketUpdateEpoch) {
// Only update epoch when we actually checkpointed to avoid decreases
lendingMarketTotalBalanceEpoch[_market] = updateUntilEpoch;
}
}
}
/// @notice Trigger a checkpoint explicitly.
/// Never needs to be called explicitly, but could be used to ensure the checkpoints within the other functions consume less gas (because they need to forward less epochs)
/// @param _market Address of the market
/// @param _forwardTimestampLimit Until which epoch (provided as timestamp) should the update be applied. If it is higher than the current epoch timestamp, this will be used.
function checkpoint_market(address _market, uint256 _forwardTimestampLimit)
external
is_valid_epoch(_forwardTimestampLimit)
{
require(lendingMarketTotalBalanceEpoch[_market] > 0, "No deposits for this market");
_checkpoint_market(_market, _forwardTimestampLimit);
}
/// @param _market Address of the market
/// @param _lender Address of the lender
/// @param _forwardTimestampLimit Until which epoch (provided as timestamp) should the update be applied. If it is higher than the current epoch timestamp, this will be used.
function checkpoint_lender(
address _market,
address _lender,
uint256 _forwardTimestampLimit
) external is_valid_epoch(_forwardTimestampLimit) {
require(lendingMarketBalancesEpoch[_market][_lender] > 0, "No deposits for this lender in this market");
_checkpoint_lender(_market, _lender, _forwardTimestampLimit);
}
/// @notice Function that is called by the lending market on cNOTE deposits / withdrawals
/// @param _lender The address of the lender
/// @param _delta The amount of cNote deposited (positive) or withdrawn (negative)
function sync_ledger(address _lender, int256 _delta) external {
address lendingMarket = msg.sender;
require(lendingMarketWhitelist[lendingMarket], "Market not whitelisted");
_checkpoint_lender(lendingMarket, _lender, type(uint256).max);
uint256 currEpoch = (block.timestamp / WEEK) * WEEK;
int256 updatedLenderBalance = int256(lendingMarketBalances[lendingMarket][_lender][currEpoch]) + _delta;
require(updatedLenderBalance >= 0, "Lender balance underflow"); // Sanity check performed here, but the market should ensure that this never happens
lendingMarketBalances[lendingMarket][_lender][currEpoch] = uint256(updatedLenderBalance);
_checkpoint_market(lendingMarket, type(uint256).max);
int256 updatedMarketBalance = int256(lendingMarketTotalBalance[lendingMarket][currEpoch]) + _delta;
require(updatedMarketBalance >= 0, "Market balance underflow"); // Sanity check performed here, but the market should ensure that this never happens
lendingMarketTotalBalance[lendingMarket][currEpoch] = uint256(updatedMarketBalance);
}
/// @notice Claim the CANTO for a given market. Can only be performed for prior (i.e. finished) epochs, not the current one
/// @param _market Address of the market
/// @param _claimFromTimestamp From which epoch (provided as timestmap) should the claim start. Usually, this parameter should be set to 0, in which case the epoch of the last claim will be used.
/// However, it can be useful to skip certain epochs, e.g. when the balance was very low or 0 (after everything was withdrawn) and the gas usage should be reduced.
/// Note that all rewards are forfeited forever if epochs are explicitly skipped by providing this parameter
/// @param _claimUpToTimestamp Until which epoch (provided as timestamp) should the claim be applied. If it is higher than the timestamp of the previous epoch, this will be used
/// Set to type(uint256).max to claim all possible epochs
function claim(
address _market,
uint256 _claimFromTimestamp,
uint256 _claimUpToTimestamp
) external is_valid_epoch(_claimFromTimestamp) is_valid_epoch(_claimUpToTimestamp) {
address lender = msg.sender;
uint256 userLastClaimed = userClaimedEpoch[_market][lender];
require(userLastClaimed > 0, "No deposits for this user");
_checkpoint_lender(_market, lender, _claimUpToTimestamp);
_checkpoint_market(_market, _claimUpToTimestamp);
uint256 currEpoch = (block.timestamp / WEEK) * WEEK;
uint256 claimStart = Math.max(userLastClaimed, _claimFromTimestamp);
uint256 claimEnd = Math.min(currEpoch - WEEK, _claimUpToTimestamp);
uint256 cantoToSend;
if (claimEnd >= claimStart) {
// This ensures that we only set userClaimedEpoch when a claim actually happened
for (uint256 i = claimStart; i <= claimEnd; i += WEEK) {
uint256 userBalance = lendingMarketBalances[_market][lender][i];
uint256 marketBalance = lendingMarketTotalBalance[_market][i];
RewardInformation memory ri = rewardInformation[i];
require(ri.set, "Reward not set yet"); // Can only claim for epochs where rewards are set, even if it is set to 0
uint256 marketWeight = gaugeController.gauge_relative_weight_write(_market, i); // Normalized to 1e18
cantoToSend += (marketWeight * userBalance * ri.amount) / (1e18 * marketBalance); // (marketWeight / 1e18) * (userBalance / marketBalance) * ri.amount;
}
userClaimedEpoch[_market][lender] = claimEnd + WEEK;
}
if (cantoToSend > 0) {
(bool success, ) = msg.sender.call{value: cantoToSend}("");
require(success, "Failed to send CANTO");
}
}
/// @notice Used by governance to set the overall CANTO rewards per epoch
/// @param _fromEpoch From which epoch (provided as timestamp) to set the rewards from
/// @param _toEpoch Until which epoch (provided as timestamp) to set the rewards to
/// @param _amountPerEpoch The amount per epoch
function setRewards(
uint256 _fromEpoch,
uint256 _toEpoch,
uint248 _amountPerEpoch
) external is_valid_epoch(_fromEpoch) is_valid_epoch(_toEpoch) onlyGovernance {
for (uint256 i = _fromEpoch; i <= _toEpoch; i += WEEK) {
RewardInformation storage ri = rewardInformation[i];
require(!ri.set, "Rewards already set");
ri.set = true;
ri.amount = _amountPerEpoch;
}
}
/// @notice Used by governance to whitelist a lending market
/// @param _market Address of the market to whitelist
/// @param _isWhiteListed Whether the market is whitelisted or not
function whiteListLendingMarket(address _market, bool _isWhiteListed) external onlyGovernance {
require(lendingMarketWhitelist[_market] != _isWhiteListed, "No change");
lendingMarketWhitelist[_market] = _isWhiteListed;
}
receive() external payable {}
}