-
Notifications
You must be signed in to change notification settings - Fork 13
/
PoolKeeper.sol
254 lines (225 loc) · 10.3 KB
/
PoolKeeper.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
//SPDX-License-Identifier: CC-BY-NC-ND-4.0
pragma solidity 0.8.7;
import "../interfaces/IPoolKeeper.sol";
import "../interfaces/IOracleWrapper.sol";
import "../interfaces/IPoolFactory.sol";
import "../interfaces/ILeveragedPool.sol";
import "../interfaces/IERC20DecimalsWrapper.sol";
import "../interfaces/IERC20DecimalsWrapper.sol";
import "./PoolSwapLibrary.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/proxy/Clones.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "abdk-libraries-solidity/ABDKMathQuad.sol";
import "@chainlink/contracts/src/v0.8/interfaces/AggregatorV2V3Interface.sol";
/// @title The manager contract for multiple markets and the pools in them
contract PoolKeeper is IPoolKeeper, Ownable {
/* Constants */
uint256 public constant BASE_TIP = 5; // 5% base tip
uint256 public constant TIP_DELTA_PER_BLOCK = 5; // 5% increase per block
uint256 public constant BLOCK_TIME = 13; /* in seconds */
uint256 public constant MAX_DECIMALS = 18;
uint256 public constant MAX_TIP = 100; /* maximum keeper tip */
// #### Global variables
/**
* @notice Format: Pool address => last executionPrice
*/
mapping(address => int256) public executionPrice;
IPoolFactory public factory;
bytes16 constant fixedPoint = 0x403abc16d674ec800000000000000000; // 1 ether
uint256 public gasPrice = 10 gwei;
// #### Functions
constructor(address _factory) {
require(_factory != address(0), "Factory cannot be 0 address");
factory = IPoolFactory(_factory);
}
/**
* @notice When a pool is created, this function is called by the factory to initiate price trackings
* @param _poolAddress The address of the newly-created pools
*/
function newPool(address _poolAddress) external override onlyFactory {
address oracleWrapper = ILeveragedPool(_poolAddress).oracleWrapper();
int256 firstPrice = IOracleWrapper(oracleWrapper).getPrice();
require(firstPrice > 0, "First price is non-positive");
int256 startingPrice = ABDKMathQuad.toInt(ABDKMathQuad.mul(ABDKMathQuad.fromInt(firstPrice), fixedPoint));
emit PoolAdded(_poolAddress, firstPrice);
executionPrice[_poolAddress] = startingPrice;
}
// Keeper network
/**
* @notice Check if upkeep is required
* @param _pool The address of the pool to upkeep
* @return upkeepNeeded Whether or not upkeep is needed for this single pool
*/
function checkUpkeepSinglePool(address _pool) public view override returns (bool) {
if (!factory.isValidPool(_pool)) {
return false;
}
// The update interval has passed
return ILeveragedPool(_pool).intervalPassed();
}
/**
* @notice Checks multiple pools if any of them need updating
* @param _pools The array of pools to check
* @return upkeepNeeded Whether or not at least one pool needs upkeeping
*/
function checkUpkeepMultiplePools(address[] calldata _pools) external view override returns (bool) {
for (uint256 i = 0; i < _pools.length; i++) {
if (checkUpkeepSinglePool(_pools[i])) {
// One has been found that requires upkeeping
return true;
}
}
return false;
}
/**
* @notice Called by keepers to perform an update on a single pool
* @param _pool The pool code to perform the update for
*/
function performUpkeepSinglePool(address _pool) public override {
uint256 startGas = gasleft();
// validate the pool, check that the interval time has passed
if (!checkUpkeepSinglePool(_pool)) {
return;
}
ILeveragedPool pool = ILeveragedPool(_pool);
(int256 latestPrice, bytes memory data, uint256 savedPreviousUpdatedTimestamp, uint256 updateInterval) = pool
.getUpkeepInformation();
// Start a new round
// Get price in WAD format
int256 lastExecutionPrice = executionPrice[_pool];
executionPrice[_pool] = latestPrice;
// This allows us to still batch multiple calls to executePriceChange, even if some are invalid
// Without reverting the entire transaction
try pool.poolUpkeep(lastExecutionPrice, latestPrice) {
// If poolUpkeep is successful, refund the keeper for their gas costs
uint256 gasSpent = startGas - gasleft();
payKeeper(_pool, gasPrice, gasSpent, savedPreviousUpdatedTimestamp, updateInterval);
emit UpkeepSuccessful(_pool, data, lastExecutionPrice, latestPrice);
} catch Error(string memory reason) {
// If poolUpkeep fails for any other reason, emit event
emit PoolUpkeepError(_pool, reason);
}
}
/**
* @notice Called by keepers to perform an update on multiple pools
* @param pools pool codes to perform the update for
*/
function performUpkeepMultiplePools(address[] calldata pools) external override {
for (uint256 i = 0; i < pools.length; i++) {
performUpkeepSinglePool(pools[i]);
}
}
/**
* @notice Pay keeper for upkeep
* @param _pool Address of the given pool
* @param _gasPrice Price of a single gas unit (in ETH (wei))
* @param _gasSpent Number of gas units spent
* @param _savedPreviousUpdatedTimestamp Last timestamp when the pool's price execution happened
* @param _updateInterval Pool interval of the given pool
*/
function payKeeper(
address _pool,
uint256 _gasPrice,
uint256 _gasSpent,
uint256 _savedPreviousUpdatedTimestamp,
uint256 _updateInterval
) internal {
uint256 reward = keeperReward(_pool, _gasPrice, _gasSpent, _savedPreviousUpdatedTimestamp, _updateInterval);
if (ILeveragedPool(_pool).payKeeperFromBalances(msg.sender, reward)) {
emit KeeperPaid(_pool, msg.sender, reward);
} else {
// Usually occurs if pool just started and does not have any funds
emit KeeperPaymentError(_pool, msg.sender, reward);
}
}
/**
* @notice Payment keeper receives for performing upkeep on a given pool
* @param _pool Address of the given pool
* @param _gasPrice Price of a single gas unit (in ETH (wei))
* @param _gasSpent Number of gas units spent
* @param _savedPreviousUpdatedTimestamp Last timestamp when the pool's price execution happened
* @param _poolInterval Pool interval of the given pool
* @return Number of settlement tokens to give to the keeper for work performed
*/
function keeperReward(
address _pool,
uint256 _gasPrice,
uint256 _gasSpent,
uint256 _savedPreviousUpdatedTimestamp,
uint256 _poolInterval
) public view returns (uint256) {
// keeper gas cost in wei. WAD formatted
uint256 _keeperGas = keeperGas(_pool, _gasPrice, _gasSpent);
// tip percent in wad units
bytes16 _tipPercent = ABDKMathQuad.fromUInt(keeperTip(_savedPreviousUpdatedTimestamp, _poolInterval));
// amount of settlement tokens to give to the keeper
_tipPercent = ABDKMathQuad.div(_tipPercent, ABDKMathQuad.fromUInt(100));
int256 wadRewardValue = ABDKMathQuad.toInt(
ABDKMathQuad.add(
ABDKMathQuad.fromUInt(_keeperGas),
ABDKMathQuad.div((ABDKMathQuad.mul(ABDKMathQuad.fromUInt(_keeperGas), _tipPercent)), fixedPoint)
)
);
uint256 decimals = IERC20DecimalsWrapper(ILeveragedPool(_pool).quoteToken()).decimals();
uint256 deWadifiedReward = PoolSwapLibrary.fromWad(uint256(wadRewardValue), decimals);
// _keeperGas + _keeperGas * percentTip
return deWadifiedReward;
}
/**
* @notice Compensation a keeper will receive for their gas expenditure
* @param _pool Address of the given pool
* @param _gasPrice Price of a single gas unit (in ETH (wei))
* @param _gasSpent Number of gas units spent
* @return Keeper's gas compensation
*/
function keeperGas(
address _pool,
uint256 _gasPrice,
uint256 _gasSpent
) public view returns (uint256) {
int256 settlementTokenPrice = IOracleWrapper(ILeveragedPool(_pool).settlementEthOracle()).getPrice();
if (settlementTokenPrice <= 0) {
return 0;
} else {
/* safe due to explicit bounds check above */
/* (wei * Settlement / ETH) / fixed point (10^18) = amount in settlement */
bytes16 _weiSpent = ABDKMathQuad.fromUInt(_gasPrice * _gasSpent);
bytes16 _settlementTokenPrice = ABDKMathQuad.fromUInt(uint256(settlementTokenPrice));
return
ABDKMathQuad.toUInt(ABDKMathQuad.div(ABDKMathQuad.mul(_weiSpent, _settlementTokenPrice), fixedPoint));
}
}
/**
* @notice Tip a keeper will receive for successfully updating the specified pool
* @param _savedPreviousUpdatedTimestamp Last timestamp when the pool's price execution happened
* @param _poolInterval Pool interval of the given pool
* @return Percent of the `keeperGas` cost to add to payment, as a percent
*/
function keeperTip(uint256 _savedPreviousUpdatedTimestamp, uint256 _poolInterval) public view returns (uint256) {
/* the number of blocks that have elapsed since the given pool's updateInterval passed */
uint256 elapsedBlocksNumerator = (block.timestamp - (_savedPreviousUpdatedTimestamp + _poolInterval));
uint256 keeperTip = BASE_TIP + (TIP_DELTA_PER_BLOCK * elapsedBlocksNumerator) / BLOCK_TIME;
// In case of network outages or otherwise, we want to cap the tip so that the keeper cost isn't unbounded
if (keeperTip > MAX_TIP) {
return MAX_TIP;
} else {
return keeperTip;
}
}
function setFactory(address _factory) external override onlyOwner {
factory = IPoolFactory(_factory);
}
/**
* @notice Sets the gas price to be used in compensating keepers for successful upkeep
* @param _price Price (in ETH) per unit gas
* @dev Only owner
*/
function setGasPrice(uint256 _price) external onlyOwner {
gasPrice = _price;
}
modifier onlyFactory() {
require(msg.sender == address(factory), "Caller not factory");
_;
}
}