-
Notifications
You must be signed in to change notification settings - Fork 49
/
Reserve.sol
265 lines (224 loc) Β· 9.92 KB
/
Reserve.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
// SPDX-License-Identifier: GPL-3.0
pragma solidity 0.8.6;
import "@pooltogether/owner-manager-contracts/contracts/Manageable.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "./interfaces/IReserve.sol";
import "./libraries/ObservationLib.sol";
import "./libraries/RingBufferLib.sol";
/**
* @title PoolTogether V4 Reserve
* @author PoolTogether Inc Team
* @notice The Reserve contract provides historical lookups of a token balance increase during a target timerange.
As the Reserve contract transfers OUT tokens, the withdraw accumulator is increased. When tokens are
transfered IN new checkpoint *can* be created if checkpoint() is called after transfering tokens.
By using the reserve and withdraw accumulators to create a new checkpoint, any contract or account
can lookup the balance increase of the reserve for a target timerange.
* @dev By calculating the total held tokens in a specific time range, contracts that require knowledge
of captured interest during a draw period, can easily call into the Reserve and deterministically
determine the newly aqcuired tokens for that time range.
*/
contract Reserve is IReserve, Manageable {
using SafeERC20 for IERC20;
/// @notice ERC20 token
IERC20 public immutable token;
/// @notice Total withdraw amount from reserve
uint224 public withdrawAccumulator;
uint32 private _gap;
uint24 internal nextIndex;
uint24 internal cardinality;
/// @notice The maximum number of twab entries
uint24 internal constant MAX_CARDINALITY = 16777215; // 2**24 - 1
ObservationLib.Observation[MAX_CARDINALITY] internal reserveAccumulators;
/* ============ Events ============ */
event Deployed(IERC20 indexed token);
/* ============ Constructor ============ */
/**
* @notice Constructs Ticket with passed parameters.
* @param _owner Owner address
* @param _token ERC20 address
*/
constructor(address _owner, IERC20 _token) Ownable(_owner) {
token = _token;
emit Deployed(_token);
}
/* ============ External Functions ============ */
/// @inheritdoc IReserve
function checkpoint() external override {
_checkpoint();
}
/// @inheritdoc IReserve
function getToken() external view override returns (IERC20) {
return token;
}
/// @inheritdoc IReserve
function getReserveAccumulatedBetween(uint32 _startTimestamp, uint32 _endTimestamp)
external
view
override
returns (uint224)
{
require(_startTimestamp < _endTimestamp, "Reserve/start-less-than-end");
uint24 _cardinality = cardinality;
uint24 _nextIndex = nextIndex;
(uint24 _newestIndex, ObservationLib.Observation memory _newestObservation) = _getNewestObservation(_nextIndex);
(uint24 _oldestIndex, ObservationLib.Observation memory _oldestObservation) = _getOldestObservation(_nextIndex);
uint224 _start = _getReserveAccumulatedAt(
_newestObservation,
_oldestObservation,
_newestIndex,
_oldestIndex,
_cardinality,
_startTimestamp
);
uint224 _end = _getReserveAccumulatedAt(
_newestObservation,
_oldestObservation,
_newestIndex,
_oldestIndex,
_cardinality,
_endTimestamp
);
return _end - _start;
}
/// @inheritdoc IReserve
function withdrawTo(address _recipient, uint256 _amount) external override onlyManagerOrOwner {
_checkpoint();
withdrawAccumulator += uint224(_amount);
token.safeTransfer(_recipient, _amount);
emit Withdrawn(_recipient, _amount);
}
/* ============ Internal Functions ============ */
/**
* @notice Find optimal observation checkpoint using target timestamp
* @dev Uses binary search if target timestamp is within ring buffer range.
* @param _newestObservation ObservationLib.Observation
* @param _oldestObservation ObservationLib.Observation
* @param _newestIndex The index of the newest observation
* @param _oldestIndex The index of the oldest observation
* @param _cardinality RingBuffer Range
* @param _timestamp Timestamp target
*
* @return Optimal reserveAccumlator for timestamp.
*/
function _getReserveAccumulatedAt(
ObservationLib.Observation memory _newestObservation,
ObservationLib.Observation memory _oldestObservation,
uint24 _newestIndex,
uint24 _oldestIndex,
uint24 _cardinality,
uint32 _timestamp
) internal view returns (uint224) {
uint32 timeNow = uint32(block.timestamp);
// IF empty ring buffer exit early.
if (_cardinality == 0) return 0;
/**
* Ring Buffer Search Optimization
* Before performing binary search on the ring buffer check
* to see if timestamp is within range of [o T n] by comparing
* the target timestamp to the oldest/newest observation.timestamps
* IF the timestamp is out of the ring buffer range avoid starting
* a binary search, because we can return NULL or oldestObservation.amount
*/
/**
* IF oldestObservation.timestamp is after timestamp: T[old ]
* the Reserve did NOT have a balance or the ring buffer
* no longer contains that timestamp checkpoint.
*/
if (_oldestObservation.timestamp > _timestamp) {
return 0;
}
/**
* IF newestObservation.timestamp is before timestamp: [ new]T
* return _newestObservation.amount since observation
* contains the highest checkpointed reserveAccumulator.
*/
if (_newestObservation.timestamp <= _timestamp) {
return _newestObservation.amount;
}
// IF the timestamp is witin range of ring buffer start/end: [new T old]
// FIND the closest observation to the left(or exact) of timestamp: [OT ]
(
ObservationLib.Observation memory beforeOrAt,
ObservationLib.Observation memory atOrAfter
) = ObservationLib.binarySearch(
reserveAccumulators,
_newestIndex,
_oldestIndex,
_timestamp,
_cardinality,
timeNow
);
// IF target timestamp is EXACT match for atOrAfter.timestamp observation return amount.
// NOT having an exact match with atOrAfter means values will contain accumulator value AFTER the searchable range.
// ELSE return observation.totalDepositedAccumulator closest to LEFT of target timestamp.
if (atOrAfter.timestamp == _timestamp) {
return atOrAfter.amount;
} else {
return beforeOrAt.amount;
}
}
/// @notice Records the currently accrued reserve amount.
function _checkpoint() internal {
uint24 _cardinality = cardinality;
uint24 _nextIndex = nextIndex;
uint256 _balanceOfReserve = token.balanceOf(address(this));
uint224 _withdrawAccumulator = withdrawAccumulator; //sload
(uint24 newestIndex, ObservationLib.Observation memory newestObservation) = _getNewestObservation(_nextIndex);
/**
* IF tokens have been deposited into Reserve contract since the last checkpoint
* create a new Reserve balance checkpoint. The will will update multiple times in a single block.
*/
if (_balanceOfReserve + _withdrawAccumulator > newestObservation.amount) {
uint32 nowTime = uint32(block.timestamp);
// checkpointAccumulator = currentBalance + totalWithdraws
uint224 newReserveAccumulator = uint224(_balanceOfReserve) + _withdrawAccumulator;
// IF newestObservation IS NOT in the current block.
// CREATE observation in the accumulators ring buffer.
if (newestObservation.timestamp != nowTime) {
reserveAccumulators[_nextIndex] = ObservationLib.Observation({
amount: newReserveAccumulator,
timestamp: nowTime
});
nextIndex = uint24(RingBufferLib.nextIndex(_nextIndex, MAX_CARDINALITY));
if (_cardinality < MAX_CARDINALITY) {
cardinality = _cardinality + 1;
}
}
// ELSE IF newestObservation IS in the current block.
// UPDATE the checkpoint previously created in block history.
else {
reserveAccumulators[newestIndex] = ObservationLib.Observation({
amount: newReserveAccumulator,
timestamp: nowTime
});
}
emit Checkpoint(newReserveAccumulator, _withdrawAccumulator);
}
}
/// @notice Retrieves the oldest observation
/// @param _nextIndex The next index of the Reserve observations
function _getOldestObservation(uint24 _nextIndex)
internal
view
returns (uint24 index, ObservationLib.Observation memory observation)
{
index = _nextIndex;
observation = reserveAccumulators[index];
// If the TWAB is not initialized we go to the beginning of the TWAB circular buffer at index 0
if (observation.timestamp == 0) {
index = 0;
observation = reserveAccumulators[0];
}
}
/// @notice Retrieves the newest observation
/// @param _nextIndex The next index of the Reserve observations
function _getNewestObservation(uint24 _nextIndex)
internal
view
returns (uint24 index, ObservationLib.Observation memory observation)
{
index = uint24(RingBufferLib.newestIndex(_nextIndex, MAX_CARDINALITY));
observation = reserveAccumulators[index];
}
}