Skip to content

Commit f2c7c19

Browse files
authored
Merge pull request #152 from fei-protocol/feat/v2-TribeRewardsController
FeiTimedMinter + PCVEquityMinter
2 parents 28342c8 + 922eed2 commit f2c7c19

16 files changed

+1377
-11
lines changed

contracts/mock/MockCollateralizationOracle.sol

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ contract MockCollateralizationOracle is MockOracle {
99

1010
uint256 public pcvValue = 5e20;
1111

12-
constructor(uint256 exchangeRate)
12+
constructor(uint256 exchangeRate)
1313
MockOracle(exchangeRate)
1414
{
1515
}
@@ -26,4 +26,8 @@ contract MockCollateralizationOracle is MockOracle {
2626
function pcvEquityValue() public view returns (int256) {
2727
return int256(pcvValue) - int256(userCirculatingFei);
2828
}
29-
}
29+
30+
function pcvStats() public view returns(uint256, uint256, int256, bool) {
31+
return (pcvValue, userCirculatingFei, pcvEquityValue(), valid);
32+
}
33+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
// SPDX-License-Identifier: GPL-3.0-or-later
2+
pragma solidity ^0.8.4;
3+
4+
import "./MockOracle.sol";
5+
import "./MockCoreRef.sol";
6+
7+
contract MockOracleCoreRef is MockOracle, MockCoreRef {
8+
constructor(address core, uint256 usdPerEth) MockCoreRef(core) MockOracle(usdPerEth) {}
9+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
// SPDX-License-Identifier: GPL-3.0-or-later
2+
pragma solidity ^0.8.4;
3+
4+
import "../refs/CoreRef.sol";
5+
import "../pcv/IPCVDepositV2.sol";
6+
7+
contract MockPCVDepositV2 is IPCVDepositV2, CoreRef {
8+
9+
address public override balanceReportedIn;
10+
11+
uint256 private resistantBalance;
12+
uint256 private resistantProtocolOwnedFei;
13+
14+
constructor(
15+
address _core,
16+
address _token,
17+
uint256 _resistantBalance,
18+
uint256 _resistantProtocolOwnedFei
19+
) CoreRef(_core) {
20+
balanceReportedIn = _token;
21+
resistantBalance = _resistantBalance;
22+
resistantProtocolOwnedFei = _resistantProtocolOwnedFei;
23+
}
24+
25+
function set(uint256 _resistantBalance, uint256 _resistantProtocolOwnedFei) public {
26+
resistantBalance = _resistantBalance;
27+
resistantProtocolOwnedFei = _resistantProtocolOwnedFei;
28+
}
29+
30+
// gets the resistant token balance and protocol owned fei of this deposit
31+
function resistantBalanceAndFei() external view override returns (uint256, uint256) {
32+
return (resistantBalance, resistantProtocolOwnedFei);
33+
}
34+
35+
// IPCVDeposit V1
36+
function deposit() external override {}
37+
function withdraw(address to, uint256 amount) external override {}
38+
function withdrawERC20(address token, address to, uint256 amount) external override {}
39+
function withdrawETH(address payable to, uint256 amount) external override {}
40+
function balance() external override view returns (uint256) {
41+
return resistantBalance;
42+
}
43+
}
Lines changed: 289 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,289 @@
1+
// SPDX-License-Identifier: GPL-3.0-or-later
2+
pragma solidity ^0.8.4;
3+
4+
import "./IOracle.sol";
5+
import "./ICollateralizationOracle.sol";
6+
import "../refs/CoreRef.sol";
7+
import "../pcv/IPCVDepositV2.sol";
8+
import "@openzeppelin/contracts/utils/structs/EnumerableSet.sol";
9+
10+
interface IPausable {
11+
function paused() external view returns (bool);
12+
}
13+
14+
/// @title Fei Protocol's Collateralization Oracle
15+
/// @author eswak
16+
/// @notice Reads a list of PCVDeposit that report their amount of collateral
17+
/// and the amount of protocol-owned FEI they manage, to deduce the
18+
/// protocol-wide collateralization ratio.
19+
contract CollateralizationOracle is ICollateralizationOracle, CoreRef {
20+
using Decimal for Decimal.D256;
21+
using EnumerableSet for EnumerableSet.AddressSet;
22+
23+
// ----------- Events -----------
24+
25+
event DepositAdd(address from, address indexed deposit, address indexed token);
26+
event DepositRemove(address from, address indexed deposit);
27+
event OracleUpdate(address from, address indexed token, address indexed oldOracle, address indexed newOracle);
28+
29+
// ----------- Properties -----------
30+
31+
/// @notice List of PCVDeposits to exclude from calculations
32+
mapping(address => bool) public excludedDeposits;
33+
34+
/// @notice Map of oracles to use to get USD values of assets held in
35+
/// PCV deposits. This map is used to get the oracle address from
36+
/// and ERC20 address.
37+
mapping(address => address) public tokenToOracle;
38+
/// @notice Map from token address to an array of deposit addresses. It is
39+
// used to iterate on all deposits while making oracle requests
40+
// only once.
41+
mapping(address => EnumerableSet.AddressSet) private tokenToDeposits;
42+
/// @notice Map from deposit address to token address. It is used like an
43+
/// indexed version of the pcvDeposits array, to check existence
44+
/// of a PCVdeposit in the current config.
45+
mapping(address => address) public depositToToken;
46+
/// @notice Array of all tokens held in the PCV. Used for iteration on tokens
47+
/// and oracles.
48+
EnumerableSet.AddressSet private tokensInPcv;
49+
50+
// ----------- Constructor -----------
51+
52+
/// @notice CollateralizationOracle constructor
53+
/// @param _core Fei Core for reference
54+
constructor(
55+
address _core
56+
) CoreRef(_core) {}
57+
58+
// ----------- Convenience getters -----------
59+
60+
/// @notice returns true if a token is held in the pcv
61+
function isTokenInPcv(address token) external view returns(bool) {
62+
return tokensInPcv.contains(token);
63+
}
64+
65+
/// @notice returns an array of the addresses of tokens held in the pcv.
66+
function getTokensInPcv() external view returns(address[] memory) {
67+
uint256 _length = tokensInPcv.length();
68+
address[] memory tokens = new address[](_length);
69+
for (uint256 i = 0; i < _length; i++) {
70+
tokens[i] = tokensInPcv.at(i);
71+
}
72+
return tokens;
73+
}
74+
75+
/// @notice returns token at index i of the array of PCV tokens
76+
function getTokenInPcv(uint256 i) external view returns(address) {
77+
return tokensInPcv.at(i);
78+
}
79+
80+
/// @notice returns an array of the deposits holding a given token.
81+
function getDepositsForToken(address _token) external view returns(address[] memory) {
82+
uint256 _length = tokenToDeposits[_token].length();
83+
address[] memory deposits = new address[](_length);
84+
for (uint256 i = 0; i < _length; i++) {
85+
deposits[i] = tokenToDeposits[_token].at(i);
86+
}
87+
return deposits;
88+
}
89+
90+
/// @notice returns the address of deposit at index i of token _token
91+
function getDepositForToken(address token, uint256 i) external view returns(address) {
92+
return tokenToDeposits[token].at(i);
93+
}
94+
95+
// ----------- State-changing methods -----------
96+
97+
/// @notice Guardians can exclude & re-include some PCVDeposits from the list,
98+
/// because a faulty deposit or a paused oracle that prevents reading
99+
/// from certain deposits could be problematic.
100+
/// @param _deposit : the deposit to exclude or re-enable.
101+
/// @param _excluded : the new exclusion flag for this deposit.
102+
function setDepositExclusion(address _deposit, bool _excluded) external onlyGuardianOrGovernor {
103+
excludedDeposits[_deposit] = _excluded;
104+
}
105+
106+
/// @notice Add a PCVDeposit to the list of deposits inspected by the
107+
/// collateralization ratio oracle.
108+
/// note : this function reverts if the deposit is already in the list.
109+
/// note : this function reverts if the deposit's token has no oracle.
110+
/// @param _deposit : the PCVDeposit to add to the list.
111+
function addDeposit(address _deposit) public onlyGovernor {
112+
// if the PCVDeposit is already listed, revert.
113+
require(depositToToken[_deposit] == address(0), "CollateralizationOracle: deposit duplicate");
114+
115+
// get the token in which the deposit reports its token
116+
address _token = IPCVDepositV2(_deposit).balanceReportedIn();
117+
118+
// revert if there is no oracle of this deposit's token
119+
require(tokenToOracle[_token] != address(0), "CollateralizationOracle: no oracle");
120+
121+
// update maps & arrays for faster access
122+
depositToToken[_deposit] = _token;
123+
tokenToDeposits[_token].add(_deposit);
124+
tokensInPcv.add(_token);
125+
126+
// emit event
127+
emit DepositAdd(msg.sender, _deposit, _token);
128+
}
129+
130+
/// @notice Remove a PCVDeposit from the list of deposits inspected by
131+
/// the collateralization ratio oracle.
132+
/// note : this function reverts if the input deposit is not found.
133+
/// @param _deposit : the PCVDeposit address to remove from the list.
134+
function removeDeposit(address _deposit) public onlyGovernor {
135+
// get the token in which the deposit reports its token
136+
address _token = depositToToken[_deposit];
137+
138+
// revert if the deposit is not found
139+
require(_token != address(0), "CollateralizationOracle: deposit not found");
140+
141+
// update maps & arrays for faster access
142+
// deposits array for the deposit's token
143+
depositToToken[_deposit] = address(0);
144+
tokenToDeposits[_token].remove(_deposit);
145+
146+
// if it was the last deposit to have this token, remove this token from
147+
// the arrays also
148+
if (tokenToDeposits[_token].length() == 0) {
149+
tokensInPcv.remove(_token);
150+
}
151+
152+
// emit event
153+
emit DepositRemove(msg.sender, _deposit);
154+
}
155+
156+
/// @notice Swap a PCVDeposit with a new one, for instance when a new version
157+
/// of a deposit (holding the same token) is deployed.
158+
/// @param _oldDeposit : the PCVDeposit to remove from the list.
159+
/// @param _newDeposit : the PCVDeposit to add to the list.
160+
function swapDeposit(address _oldDeposit, address _newDeposit) external onlyGovernor {
161+
removeDeposit(_oldDeposit);
162+
addDeposit(_newDeposit);
163+
}
164+
165+
/// @notice Set the price feed oracle (in USD) for a given asset.
166+
/// @param _token : the asset to add price oracle for
167+
/// @param _newOracle : price feed oracle for the given asset
168+
function setOracle(address _token, address _newOracle) external onlyGovernor {
169+
require(_token != address(0), "CollateralizationOracle: token must be != 0x0");
170+
require(_newOracle != address(0), "CollateralizationOracle: oracle must be != 0x0");
171+
172+
// add oracle to the map(ERC20Address) => OracleAddress
173+
address _oldOracle = tokenToOracle[_token];
174+
tokenToOracle[_token] = _newOracle;
175+
176+
// emit event
177+
emit OracleUpdate(msg.sender, _token, _oldOracle, _newOracle);
178+
}
179+
180+
// ----------- IOracle override methods -----------
181+
/// @notice update all oracles required for this oracle to work that are not
182+
/// paused themselves.
183+
function update() external override whenNotPaused {
184+
for (uint256 i = 0; i < tokensInPcv.length(); i++) {
185+
address _oracle = tokenToOracle[tokensInPcv.at(i)];
186+
if (!IPausable(_oracle).paused()) {
187+
IOracle(_oracle).update();
188+
}
189+
}
190+
}
191+
192+
// @notice returns true if any of the oracles required for this oracle to
193+
// work are outdated.
194+
function isOutdated() external override view returns (bool) {
195+
bool _outdated = false;
196+
for (uint256 i = 0; i < tokensInPcv.length() && !_outdated; i++) {
197+
address _oracle = tokenToOracle[tokensInPcv.at(i)];
198+
if (!IPausable(_oracle).paused()) {
199+
_outdated = _outdated || IOracle(_oracle).isOutdated();
200+
}
201+
}
202+
return _outdated;
203+
}
204+
205+
/// @notice Get the current collateralization ratio of the protocol.
206+
/// @return collateralRatio the current collateral ratio of the protocol.
207+
/// @return validityStatus the current oracle validity status (false if any
208+
/// of the oracles for tokens held in the PCV are invalid, or if
209+
/// this contract is paused).
210+
function read() public override view returns (Decimal.D256 memory collateralRatio, bool validityStatus) {
211+
// fetch PCV stats
212+
(
213+
uint256 _protocolControlledValue,
214+
uint256 _userCirculatingFei,
215+
, // we don't need protocol equity
216+
bool _valid
217+
) = pcvStats();
218+
219+
// The protocol collateralization ratio is defined as the total USD
220+
// value of assets held in the PCV, minus the circulating FEI.
221+
collateralRatio = Decimal.ratio(_protocolControlledValue, _userCirculatingFei);
222+
validityStatus = _valid;
223+
}
224+
225+
// ----------- ICollateralizationOracle override methods -----------
226+
227+
/// @notice returns the Protocol-Controlled Value, User-circulating FEI, and
228+
/// Protocol Equity.
229+
/// @return protocolControlledValue : the total USD value of all assets held
230+
/// by the protocol.
231+
/// @return userCirculatingFei : the number of FEI not owned by the protocol.
232+
/// @return protocolEquity : the difference between PCV and user circulating FEI.
233+
/// If there are more circulating FEI than $ in the PCV, equity is 0.
234+
/// @return validityStatus : the current oracle validity status (false if any
235+
/// of the oracles for tokens held in the PCV are invalid, or if
236+
/// this contract is paused).
237+
function pcvStats() public override view returns (
238+
uint256 protocolControlledValue,
239+
uint256 userCirculatingFei,
240+
int256 protocolEquity,
241+
bool validityStatus
242+
) {
243+
uint256 _protocolControlledFei = 0;
244+
validityStatus = !paused();
245+
246+
// For each token...
247+
for (uint256 i = 0; i < tokensInPcv.length(); i++) {
248+
address _token = tokensInPcv.at(i);
249+
uint256 _totalTokenBalance = 0;
250+
251+
// For each deposit...
252+
for (uint256 j = 0; j < tokenToDeposits[_token].length(); j++) {
253+
address _deposit = tokenToDeposits[_token].at(j);
254+
255+
// ignore deposits that are excluded by the Guardian
256+
if (!excludedDeposits[_deposit]) {
257+
// read the deposit, and increment token balance/protocol fei
258+
(uint256 _depositBalance, uint256 _depositFei) = IPCVDepositV2(_deposit).resistantBalanceAndFei();
259+
_totalTokenBalance += _depositBalance;
260+
_protocolControlledFei += _depositFei;
261+
}
262+
}
263+
264+
// If the protocol holds non-zero balance of tokens, fetch the oracle price to
265+
// increment PCV by _totalTokenBalance * oracle price USD.
266+
if (_totalTokenBalance != 0) {
267+
(Decimal.D256 memory _oraclePrice, bool _oracleValid) = IOracle(tokenToOracle[_token]).read();
268+
if (!_oracleValid) {
269+
validityStatus = false;
270+
}
271+
protocolControlledValue += _oraclePrice.mul(_totalTokenBalance).asUint256();
272+
}
273+
}
274+
275+
userCirculatingFei = fei().totalSupply() - _protocolControlledFei;
276+
protocolEquity = int256(protocolControlledValue) - int256(userCirculatingFei);
277+
}
278+
279+
/// @notice returns true if the protocol is overcollateralized. Overcollateralization
280+
/// is defined as the protocol having more assets in its PCV (Protocol
281+
/// Controlled Value) than the circulating (user-owned) FEI, i.e.
282+
/// a positive Protocol Equity.
283+
/// Note: the validity status is ignored in this function.
284+
function isOvercollateralized() external override view whenNotPaused returns (bool) {
285+
(,, int256 _protocolEquity, bool _valid) = pcvStats();
286+
require(_valid, "CollateralizationOracle: reading is invalid");
287+
return _protocolEquity > 0;
288+
}
289+
}

contracts/oracle/ICollateralizationOracle.sol

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,15 @@ interface ICollateralizationOracle is IOracle {
99

1010
// ----------- Getters -----------
1111

12-
function isOvercollateralized() external view returns (bool);
13-
14-
function userCirculatingFei() external view returns (uint256);
12+
// returns the PCV value, User-circulating FEI, and Protocol Equity, as well
13+
// as a validity status.
14+
function pcvStats() external view returns (
15+
uint256 protocolControlledValue,
16+
uint256 userCirculatingFei,
17+
int256 protocolEquity,
18+
bool validityStatus
19+
);
1520

16-
function pcvValue() external view returns (uint256);
17-
18-
function pcvEquityValue() external view returns (uint256);
21+
// true if Protocol Equity > 0
22+
function isOvercollateralized() external view returns (bool);
1923
}

0 commit comments

Comments
 (0)