Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Minimum Viable TWAP #736

Merged
merged 26 commits into from
Jan 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
22c58d3
draft impl
dapp-whisperer Dec 11, 2023
1ff046d
add logging
dapp-whisperer Dec 11, 2023
6e0f87d
twap=spot, existing EToFoundry
dapp-whisperer Dec 11, 2023
88a93b9
more sync twap fixes
dapp-whisperer Dec 12, 2023
3f9e440
fix failing tests and add event in _setValue()
rayeaster Dec 12, 2023
e7c3f30
add simulation for twap observe
rayeaster Dec 12, 2023
ead8985
add twap accumulator foundry test
rayeaster Dec 13, 2023
ef9cdf4
restore skew for weighted observe value
rayeaster Dec 13, 2023
e4f7961
fix failing hardhat tests after restore skew weighted observe()
rayeaster Dec 13, 2023
c2d82ba
fix failed CdpManagerTest
rayeaster Dec 13, 2023
e3c58ae
remove test only
rayeaster Dec 13, 2023
23002b2
relax the tolerance for comparing redemption fee
rayeaster Dec 13, 2023
6fcdb82
add foudnry test to show manipulation attack mitigated by twap
rayeaster Dec 14, 2023
c81fa1e
fix: `getRealValue` is redundant
GalloDaSballo Dec 20, 2023
7f00214
fix: comment `setValue`
GalloDaSballo Dec 20, 2023
7085607
fix: early return on 0
GalloDaSballo Dec 20, 2023
d6ef2dd
chore: comment
GalloDaSballo Dec 20, 2023
c7b7f12
chore: prettier
GalloDaSballo Dec 20, 2023
0d95dd1
rename twap packed data for better clarity
rayeaster Dec 21, 2023
afbb362
chore: readability of names
GalloDaSballo Dec 21, 2023
50032ac
chore: natspec
GalloDaSballo Dec 21, 2023
0a7c1e3
fix: interface
GalloDaSballo Dec 21, 2023
9b8a19c
fix: compilation / renaming
GalloDaSballo Dec 21, 2023
1a6787c
Merge branch 'release-0.6' into feat/minimal-twap
Jan 3, 2024
c38ed96
fix failing foundry test by sync twap
rayeaster Jan 4, 2024
e160ef1
Merge remote-tracking branch 'ebtc/release-0.6' into feat/minimal-twap
rayeaster Jan 4, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 18 additions & 2 deletions packages/contracts/contracts/ActivePool.sol
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// SPDX-License-Identifier: MIT

pragma solidity 0.8.17;

Check warning on line 3 in packages/contracts/contracts/ActivePool.sol

View workflow job for this annotation

GitHub Actions / lint

Compiler version 0.8.17 does not satisfy the a semver requirement

import "./Interfaces/IActivePool.sol";
import "./Interfaces/ICollSurplusPool.sol";
Expand All @@ -11,6 +11,7 @@
import "./Dependencies/ReentrancyGuard.sol";
import "./Dependencies/AuthNoOwner.sol";
import "./Dependencies/BaseMath.sol";
import "./Dependencies/TwapWeightedObserver.sol";

/**
* @title The Active Pool holds the collateral and EBTC debt (only accounting but not EBTC tokens) for all active cdps.
Expand All @@ -19,7 +20,14 @@
* @notice (destination may vary depending on the liquidation conditions).
* @dev ActivePool also allows ERC3156 compatible flashloan of stETH token
*/
contract ActivePool is IActivePool, ERC3156FlashLender, ReentrancyGuard, BaseMath, AuthNoOwner {
contract ActivePool is
IActivePool,
ERC3156FlashLender,
ReentrancyGuard,
BaseMath,
AuthNoOwner,
TwapWeightedObserver
{
using SafeERC20 for IERC20;
string public constant NAME = "ActivePool";

Expand All @@ -36,20 +44,20 @@
// --- Contract setters ---

/// @notice Constructor for the ActivePool contract
/// @dev Initializes the contract with the borrowerOperationsAddress, cdpManagerAddress, collateral token address, collSurplusAddress, and feeRecipientAddress

Check warning on line 47 in packages/contracts/contracts/ActivePool.sol

View workflow job for this annotation

GitHub Actions / lint

Line length must be no more than 120 but current length is 162
/// @param _borrowerOperationsAddress The address of the Borrower Operations contract
/// @param _cdpManagerAddress The address of the Cdp Manager contract
/// @param _collTokenAddress The address of the collateral token
/// @param _collSurplusAddress The address of the collateral surplus pool
/// @param _feeRecipientAddress The address of the fee recipient

constructor(

Check warning on line 54 in packages/contracts/contracts/ActivePool.sol

View workflow job for this annotation

GitHub Actions / lint

Explicitly mark visibility in function (Set ignoreConstructors to true if using solidity >=0.7.0)
address _borrowerOperationsAddress,
address _cdpManagerAddress,
address _collTokenAddress,
address _collSurplusAddress,
address _feeRecipientAddress
) {
) TwapWeightedObserver(0) {
borrowerOperationsAddress = _borrowerOperationsAddress;
cdpManagerAddress = _cdpManagerAddress;
collateral = ICollateralToken(_collTokenAddress);
Expand All @@ -63,12 +71,14 @@
}

emit FeeRecipientAddressChanged(_feeRecipientAddress);

require(systemDebt == 0, "ActivePool: systemDebt should be 0 for TWAP initialization");

Check warning on line 75 in packages/contracts/contracts/ActivePool.sol

View workflow job for this annotation

GitHub Actions / lint

Error message for require is too long
}

// --- Getters for public variables. Required by IPool interface ---

/// @notice Amount of stETH collateral shares in the contract
/// @dev Not necessarily equal to the the contract's raw systemCollShares balance - tokens can be forcibly sent to contracts

Check warning on line 81 in packages/contracts/contracts/ActivePool.sol

View workflow job for this annotation

GitHub Actions / lint

Line length must be no more than 120 but current length is 128
/// @return uint256 The amount of systemCollShares allocated to the pool

function getSystemCollShares() external view override returns (uint256) {
Expand All @@ -76,7 +86,7 @@
}

/// @notice Returns the systemDebt state variable
/// @dev The amount of EBTC debt in the pool. Like systemCollShares, this is not necessarily equal to the contract's EBTC token balance - tokens can be forcibly sent to contracts

Check warning on line 89 in packages/contracts/contracts/ActivePool.sol

View workflow job for this annotation

GitHub Actions / lint

Line length must be no more than 120 but current length is 182
/// @return uint256 The amount of EBTC debt in the pool

function getSystemDebt() external view override returns (uint256) {
Expand Down Expand Up @@ -116,11 +126,11 @@
}

/// @notice Sends stETH to a specified account, drawing from both core shares and liquidator rewards shares
/// @notice Liquidator reward shares are not tracked via internal accounting in the active pool and are assumed to be present in expected amount as part of the intended behavior of BorowerOperations and CdpManager

Check warning on line 129 in packages/contracts/contracts/ActivePool.sol

View workflow job for this annotation

GitHub Actions / lint

Line length must be no more than 120 but current length is 217
/// @dev Liquidator reward shares are added when a cdp is opened, and removed when it is closed
/// @dev closeCdp() or liqudations result in the actor (borrower or liquidator respectively) receiving the liquidator reward shares

Check warning on line 131 in packages/contracts/contracts/ActivePool.sol

View workflow job for this annotation

GitHub Actions / lint

Line length must be no more than 120 but current length is 135
/// @dev Redemptions result in the shares being sent to the coll surplus pool for claiming by the CDP owner
/// @dev Note that funds in the coll surplus pool, just like liquidator reward shares, are not tracked as part of the system CR or coll of a CDP.

Check warning on line 133 in packages/contracts/contracts/ActivePool.sol

View workflow job for this annotation

GitHub Actions / lint

Line length must be no more than 120 but current length is 149
/// @dev Requires that the caller is either BorrowerOperations or CdpManager
/// @param _account The address of the account to send systemCollShares and the liquidator reward to
/// @param _shares The amount of systemCollShares to send
Expand All @@ -134,7 +144,7 @@
_requireCallerIsBOorCdpM();

uint256 cachedSystemCollShares = systemCollShares;
require(cachedSystemCollShares >= _shares, "ActivePool: Insufficient collateral shares");

Check warning on line 147 in packages/contracts/contracts/ActivePool.sol

View workflow job for this annotation

GitHub Actions / lint

Error message for require is too long
uint256 totalShares = _shares + _liquidatorRewardShares;
unchecked {
// Safe per the check above
Expand Down Expand Up @@ -196,6 +206,9 @@

uint256 cachedSystemDebt = systemDebt + _amount;

_setValue(uint128(cachedSystemDebt)); // @audit update TWAP global spot value and accumulator variable along with a timestamp
update(); // @audit update TWAP Observer accumulator and weighted average

systemDebt = cachedSystemDebt;
emit ActivePoolEBTCDebtUpdated(cachedSystemDebt);
}
Expand All @@ -209,6 +222,9 @@

uint256 cachedSystemDebt = systemDebt - _amount;

_setValue(uint128(cachedSystemDebt)); // @audit update TWAP global spot value and accumulator variable along with a timestamp
update(); // @audit update TWAP Observer accumulator and weighted average

systemDebt = cachedSystemDebt;
emit ActivePoolEBTCDebtUpdated(cachedSystemDebt);
}
Expand Down
3 changes: 2 additions & 1 deletion packages/contracts/contracts/CdpManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -348,6 +348,7 @@ contract CdpManager is CdpManagerStorage, ICdpManager, Proxy {
totals.tcrAtStart = tcrAtStart;
totals.systemCollSharesAtStart = systemCollSharesAtStart;
totals.systemDebtAtStart = systemDebtAtStart;
totals.twapSystemDebtAtStart = EbtcMath._min(activePool.observe(), systemDebtAtStart); // @audit Return the smaller value of the two, bias towards a larger redemption scaling fee
}

_requireTCRisNotBelowMCR(totals.price, totals.tcrAtStart);
Expand Down Expand Up @@ -466,7 +467,7 @@ contract CdpManager is CdpManagerStorage, ICdpManager, Proxy {
_updateBaseRateFromRedemption(
totals.collSharesDrawn,
totals.price,
totals.systemDebtAtStart
totals.twapSystemDebtAtStart
);

// Calculate the ETH fee
Expand Down
139 changes: 139 additions & 0 deletions packages/contracts/contracts/Dependencies/TwapWeightedObserver.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
// SPDX-License Identifier: MIT
pragma solidity 0.8.17;
import {ITwapWeightedObserver} from "../Interfaces/ITwapWeightedObserver.sol";

/// @title TwapWeightedObserver
/// @notice Given a value, applies a time-weighted TWAP that smooths out changes over a 7 days period
/// @dev Used to get the lowest value of total supply to prevent underpaying redemptions
contract TwapWeightedObserver is ITwapWeightedObserver {
PackedData public data;
uint128 public valueToTrack;

constructor(uint128 initialValue) {
PackedData memory cachedData = PackedData({
observerCumuVal: initialValue,
accumulator: initialValue,
lastObserved: uint64(block.timestamp),
lastAccrued: uint64(block.timestamp),
lastObservedAverage: initialValue
});

valueToTrack = initialValue;
data = cachedData;
}

/// TWAP ///
event NewTrackValue(uint256 _oldValue, uint256 _newValue, uint256 _ts, uint256 _newAcc);

// Set to new value, sync accumulator to now with old value
// Changes in same block have no impact, as no time has expired
// Effectively we use the previous block value, and we magnify it by weight
function _setValue(uint128 newValue) internal {
uint128 _newAcc = _updateAcc(valueToTrack);

data.lastAccrued = uint64(block.timestamp);
emit NewTrackValue(valueToTrack, newValue, block.timestamp, _newAcc);
valueToTrack = newValue;
}

// Update the accumulator based on time passed
function _updateAcc(uint128 oldValue) internal returns (uint128) {
uint128 _newAcc = data.accumulator + oldValue * (timeToAccrue());
data.accumulator = _newAcc;
return _newAcc;
}

/// @notice Returns the time since the last update
/// @return Duration since last update
/// @dev Safe from overflow for tens of thousands of years
function timeToAccrue() public view returns (uint64) {
return uint64(block.timestamp) - data.lastAccrued;
}

/// @notice Returns the accumulator value, adjusted according to the current value and block timestamp
// Return the update value to now
function _syncToNow() internal view returns (uint128) {
return data.accumulator + (valueToTrack * (timeToAccrue()));
}

// == Getters == //

/// @notice Returns the accumulator value, adjusted according to the current value and block timestamp
function getLatestAccumulator() public view returns (uint128) {
return _syncToNow();
}

/// END TWAP ///

/// TWAP WEIGHTED OBSERVER ///

// Hardcoded TWAP Period of 7 days
uint256 public constant PERIOD = 7 days;

// Look at last
// Linear interpolate (or prob TWAP already does that for you)

/// @notice Returns the current value, adjusted according to the current value and block timestamp
function observe() external returns (uint256) {
// Here, we need to apply the new accumulator to skew the price in some way
// The weight of the skew should be proportional to the time passed
uint256 futureWeight = block.timestamp - data.lastObserved;

if (futureWeight == 0) {
return data.lastObservedAverage;
}

// A reference period is 7 days
// For each second passed after update
// Let's virtally sync TWAP
// With a weight, that is higher, the more time has passed
(uint128 virtualAvgValue, uint128 obsAcc) = _calcUpdatedAvg();

if (_checkUpdatePeriod()) {
_update(virtualAvgValue, obsAcc); // May as well update
// Return virtual
return virtualAvgValue;
}

uint256 weightedAvg = data.lastObservedAverage * (PERIOD - futureWeight);
uint256 weightedVirtual = virtualAvgValue * (futureWeight);

uint256 weightedMean = (weightedAvg + weightedVirtual) / PERIOD;

return weightedMean;
}

/// @dev Usual Accumulator Math, (newAcc - acc0) / (now - t0)
function _calcUpdatedAvg() internal view returns (uint128, uint128) {
uint128 latestAcc = getLatestAccumulator();
uint128 avgValue = (latestAcc - data.observerCumuVal) /
(uint64(block.timestamp) - data.lastObserved);
return (avgValue, latestAcc);
}

/// @dev Utility to update internal data
function _update(uint128 avgValue, uint128 obsAcc) internal {
data.lastObservedAverage = avgValue;
data.observerCumuVal = obsAcc;
data.lastObserved = uint64(block.timestamp);
}

/// @dev Should we update in observe?
function _checkUpdatePeriod() internal returns (bool) {
return block.timestamp >= (data.lastObserved + PERIOD);
}

/// @dev update time-weighted Observer
function update() public {
if (_checkUpdatePeriod()) {
(uint128 avgValue, uint128 latestAcc) = _calcUpdatedAvg();
_update(avgValue, latestAcc);
}
}

function getData() external view returns (PackedData memory) {
return data;
}

/// END TWAP WEIGHTED OBSERVER ///
}
3 changes: 2 additions & 1 deletion packages/contracts/contracts/Interfaces/IActivePool.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@
pragma solidity 0.8.17;

import "./IPool.sol";
import "./ITwapWeightedObserver.sol";

interface IActivePool is IPool {
interface IActivePool is IPool, ITwapWeightedObserver {
// --- Events ---
event ActivePoolEBTCDebtUpdated(uint256 _EBTCDebt);
event SystemCollSharesUpdated(uint256 _coll);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// SPDX-License Identifier: MIT
pragma solidity 0.8.17;

interface IBaseTwapWeightedObserver {
// NOTE: Packing manually is cheaper, but this is simpler to understand and follow
struct PackedData {
// Slot 0
// Seconds in a year: 3.154e+7
/// @dev Accumulator value recorded for TWAP Observer until last update
uint128 observerCumuVal; // 3.154e+7 * 80 * 100e27 = 2.5232e+38 | log_2(100e27 * 3.154e+7 * 80) = 127.568522171
/// @dev Accumulator for TWAP globally
uint128 accumulator; // 3.154e+7 * 80 * 100e27 = 2.5232e+38 | log_2(100e27 * 3.154e+7 * 80) = 127.568522171
// NOTE: We can further compress this slot but we will not be able to use only one (see u72 impl)
/// So what's the point of making the code more complex?

// Slot 1
/// @dev last update timestamp for TWAP Observer
uint64 lastObserved; // Thousands of Years, if we use relative time we can use u32 | Relative to deploy time (as immutable)
/// @dev last update timestamp for TWAP global track(spot) value
uint64 lastAccrued; // Thousands of years
// Expect eBTC debt to never surpass 100e27, which is 100 BILLION eBTC
// log_2(100e27) = 96.3359147517 | log_2(100e27 / 1e18) = 36.5412090438
// We could use a u64
/// @dev average value since last observe
uint128 lastObservedAverage;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,7 @@ interface ICdpManagerData is IRecoveryModeGracePeriod {
uint256 decayedBaseRate;
uint256 price;
uint256 systemDebtAtStart;
uint256 twapSystemDebtAtStart;
uint256 systemCollSharesAtStart;
uint256 tcrAtStart;
}
Expand Down
17 changes: 17 additions & 0 deletions packages/contracts/contracts/Interfaces/ITwapWeightedObserver.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// SPDX-License Identifier: MIT
pragma solidity 0.8.17;
import {IBaseTwapWeightedObserver} from "./IBaseTwapWeightedObserver.sol";

interface ITwapWeightedObserver is IBaseTwapWeightedObserver {
function PERIOD() external view returns (uint256);

function valueToTrack() external view returns (uint128);

function timeToAccrue() external view returns (uint64);

function getLatestAccumulator() external view returns (uint128);

function observe() external returns (uint256);

function update() external;
}
20 changes: 18 additions & 2 deletions packages/contracts/contracts/TestContracts/ActivePoolTester.sol
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,26 @@ contract ActivePoolTester is ActivePool {
bytes4 public constant FUNC_SIG_FL_FEE = 0x72c27b62; //setFeeBps(uint256)
bytes4 public constant FUNC_SIG_MAX_FL_FEE = 0x246d4569; //setMaxFeeBps(uint256)

function unprotectedIncreaseSystemDebt(uint256 _amount) external {
function unprotectedIncreaseSystemDebt(uint256 _amount) public {
systemDebt = systemDebt + _amount;
}

function unprotectedReceiveColl(uint256 _amount) external {
function unprotectedReceiveColl(uint256 _amount) public {
systemCollShares = systemCollShares + _amount;
}

function unprotectedIncreaseSystemDebtAndUpdate(uint256 _amount) external {
unprotectedIncreaseSystemDebt(_amount);
_setValue(uint128(systemDebt));
update();
}

function unprotectedReceiveCollAndUpdate(uint256 _amount) external {
unprotectedReceiveColl(_amount);
_setValue(uint128(systemDebt));
update();
}

function unprotectedallocateSystemCollSharesToFeeRecipient(uint256 _shares) external {
systemCollShares = systemCollShares - _shares;
feeRecipientCollShares = feeRecipientCollShares + _shares;
Expand All @@ -41,6 +53,10 @@ contract ActivePoolTester is ActivePool {
emit FeeRecipientClaimableCollSharesIncreased(feeRecipientCollShares, _shares);
}

function unprotectedSetTwapTrackVal(uint256 _val) public {
_setValue(uint128(_val));
}

// dummy test functions for sweepToken()
function balanceOf(address account) external pure returns (uint256) {
return 1234567890;
Expand Down
10 changes: 9 additions & 1 deletion packages/contracts/contracts/TestContracts/CDPManagerTester.sol
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,15 @@ contract CdpManagerTester is CdpManager {
uint256 _ETHDrawn,
uint256 _price
) external view returns (uint256) {
uint256 _totalEBTCSupply = _getSystemDebt();
return getUpdatedBaseRateFromRedemptionWithSystemDebt(_ETHDrawn, _price, _getSystemDebt());
}

function getUpdatedBaseRateFromRedemptionWithSystemDebt(
uint256 _ETHDrawn,
uint256 _price,
uint256 _systemDebt
) public view returns (uint256) {
uint256 _totalEBTCSupply = EbtcMath._min(_getSystemDebt(), _systemDebt);
uint256 decayedBaseRate = _calcDecayedBaseRate();
uint256 redeemedEBTCFraction = (collateral.getPooledEthByShares(_ETHDrawn) * _price) /
_totalEBTCSupply;
Expand Down
16 changes: 16 additions & 0 deletions packages/contracts/contracts/TestContracts/Pretty.sol
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,22 @@ library Pretty {
return _pretty(n, decimals);
}

function pretty(uint128 n) external pure returns (string memory) {
return _pretty(uint128(n), DEFAULT_DECIMALS);
}

function pretty(uint128 n, uint8 decimals) external pure returns (string memory) {
return _pretty(uint128(n), decimals);
}

function pretty(uint64 n) external pure returns (string memory) {
return _pretty(uint64(n), DEFAULT_DECIMALS);
}

function pretty(uint64 n, uint8 decimals) external pure returns (string memory) {
return _pretty(uint64(n), decimals);
}

function pretty(int256 n) external pure returns (string memory) {
return _prettyInt(n, DEFAULT_DECIMALS);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
pragma solidity 0.8.17;

import "@crytic/properties/contracts/util/PropertiesConstants.sol";
import "@crytic/properties/contracts/util/Hevm.sol";

import "../../Interfaces/ICdpManagerData.sol";
import "../../Dependencies/SafeMath.sol";
Expand Down Expand Up @@ -393,6 +394,11 @@ abstract contract TargetContractSetup is BaseStorageVariables, PropertiesConstan
simulator = new Simulator(actorsArray, cdpManager, sortedCdps, borrowerOperations);
}

function _syncSystemDebtTwapToSpotValue() internal {
hevm.warp(block.timestamp + activePool.PERIOD());
activePool.update();
}

function _openWhaleCdpAndTransferEBTC() internal {
bool success;
Actor actor = actors[USER3]; // USER3 is the whale CDP holder
Expand Down
Loading
Loading