diff --git a/CHANGELOG.md b/CHANGELOG.md index 757102c..8f7c8be 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## 4.10.0 +### Prudentia +#### Controllers +- Add flag to enable or disable compute ahead logic: If enabled, the controller's computeRate function will calculate the rate on-the-fly with clamping. Otherwise, it will return the last stored rate. +- Add IonicRateController: A RateController that computes rates for Ionic tokens, accruing interest on the underlying tokens before pushing new rates. + ## v4.9.1 ### Prudentia #### Controllers diff --git a/README.md b/README.md index 85ba33c..0875e3a 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # Adrastia Periphery [![standard-readme compliant](https://img.shields.io/badge/readme%20style-standard-brightgreen.svg?style=flat-square)](https://github.com/RichardLitt/standard-readme) -![4015 out of 4015 tests passing](https://img.shields.io/badge/tests-4015/4015%20passing-brightgreen.svg?style=flat-square) +![4038 out of 4038 tests passing](https://img.shields.io/badge/tests-4038/4038%20passing-brightgreen.svg?style=flat-square) ![test-coverage 100%](https://img.shields.io/badge/test%20coverage-100%25-brightgreen.svg?style=flat-square) Adrastia Periphery is a set of Solidity smart contracts that complement the [Adrastia Core](https://github.com/adrastia-oracle/adrastia-core) smart contracts. diff --git a/contracts/rates/ManagedRateController.sol b/contracts/rates/ManagedRateController.sol index b73b2a3..2f1f6e5 100644 --- a/contracts/rates/ManagedRateController.sol +++ b/contracts/rates/ManagedRateController.sol @@ -32,15 +32,18 @@ contract ManagedRateController is RateController, AccessControlEnumerable { /** * @notice Constructs the ManagedRateController contract. + * @param computeAhead_ True if the rates returned by computeRate should be computed on-the-fly with clamping; + * false if the returned rates should be the same as the last pushed rates (from the buffer). * @param period_ The period for the rate controller. * @param initialBufferCardinality_ The initial buffer cardinality for the rate controller. * @param updatersMustBeEoa_ A flag indicating if updaters must be externally owned accounts. */ constructor( + bool computeAhead_, uint32 period_, uint8 initialBufferCardinality_, bool updatersMustBeEoa_ - ) RateController(period_, initialBufferCardinality_, updatersMustBeEoa_) { + ) RateController(computeAhead_, period_, initialBufferCardinality_, updatersMustBeEoa_) { initializeRoles(); } diff --git a/contracts/rates/RateController.sol b/contracts/rates/RateController.sol index fcdb5d0..7e4cd59 100644 --- a/contracts/rates/RateController.sol +++ b/contracts/rates/RateController.sol @@ -40,6 +40,10 @@ abstract contract RateController is ERC165, HistoricalRates, IRateComputer, IUpd /// @dev This is a security feature to prevent malicious contracts from updating rates. bool public immutable updatersMustBeEoa; + /// @notice True if the rates returned by computeRate should be computed on-the-fly with clamping; false if the + /// returned rates should be the same as the last pushed rates (from the buffer). + bool public immutable computeAhead; + /// @notice Maps a token to its rate configuration. mapping(address => RateConfig) internal rateConfigs; @@ -82,14 +86,18 @@ abstract contract RateController is ERC165, HistoricalRates, IRateComputer, IUpd error PauseStatusUnchanged(address token, bool paused); /// @notice Creates a new rate controller. + /// @param computeAhead_ True if the rates returned by computeRate should be computed on-the-fly with clamping; + /// false if the returned rates should be the same as the last pushed rates (from the buffer). /// @param period_ The period of the rate controller, in seconds. This is the frequency at which rates are updated. /// @param initialBufferCardinality_ The initial capacity of the rate buffer. /// @param updatersMustBeEoa_ True if all rate updaters must be EOA accounts; false otherwise. constructor( + bool computeAhead_, uint32 period_, uint8 initialBufferCardinality_, bool updatersMustBeEoa_ ) HistoricalRates(initialBufferCardinality_) { + computeAhead = computeAhead_; period = period_; updatersMustBeEoa = updatersMustBeEoa_; } @@ -213,11 +221,24 @@ abstract contract RateController is ERC165, HistoricalRates, IRateComputer, IUpd } } - /// @inheritdoc IRateComputer + /// @notice Computes the rate for a token. If computeAhead is true, the rate is computed on-the-fly with clamping; + /// otherwise, the rate is the same as the last pushed rate (from the buffer). + /// @param token The address of the token to compute the rate for. + /// @return rate The rate for the token. function computeRate(address token) external view virtual override returns (uint64) { - (, uint64 newRate) = computeRateAndClamp(token); + if (computeAhead) { + (, uint64 newRate) = computeRateAndClamp(token); - return newRate; + return newRate; + } else { + BufferMetadata storage meta = rateBufferMetadata[token]; + if (meta.size == 0) { + // We've never computed a rate, so revert. + revert InsufficientData(token, 0, 1); + } + + return getLatestRate(token).current; + } } /// @inheritdoc IPeriodic @@ -375,7 +396,7 @@ abstract contract RateController is ERC165, HistoricalRates, IRateComputer, IUpd return rateBuffers[token][meta.end]; } - /// @notice Computes the rate for the given token. + /// @notice Computes the target rate for the given token (without clamping). /// @dev This function calculates the rate for the specified token by summing its base rate /// and the weighted rates of its components. The component rates are computed using the `computeRate` /// function of each component and multiplied by the corresponding weight, then divided by 10,000. diff --git a/contracts/rates/controllers/CapController.sol b/contracts/rates/controllers/CapController.sol index 0c78f11..31735d5 100644 --- a/contracts/rates/controllers/CapController.sol +++ b/contracts/rates/controllers/CapController.sol @@ -26,15 +26,18 @@ abstract contract CapController is RateController { /** * @notice Constructs the CapController contract. + * @param computeAhead_ True if the rates returned by computeRate should be computed on-the-fly with clamping; + * false if the returned rates should be the same as the last pushed rates (from the buffer). * @param period_ The period of the rate controller. * @param initialBufferCardinality_ The initial cardinality of the rate buffers. * @param updatersMustBeEoa_ Whether or not the updaters must be EOA. */ constructor( + bool computeAhead_, uint32 period_, uint8 initialBufferCardinality_, bool updatersMustBeEoa_ - ) RateController(period_, initialBufferCardinality_, updatersMustBeEoa_) {} + ) RateController(computeAhead_, period_, initialBufferCardinality_, updatersMustBeEoa_) {} /** * @notice Sets the change threshold for the specified token. When the rate changes by more than the threshold, an diff --git a/contracts/rates/controllers/ManagedCapController.sol b/contracts/rates/controllers/ManagedCapController.sol index af108cc..8e765ec 100644 --- a/contracts/rates/controllers/ManagedCapController.sol +++ b/contracts/rates/controllers/ManagedCapController.sol @@ -32,15 +32,18 @@ contract ManagedCapController is CapController, AccessControlEnumerable { /** * @notice Constructs the ManagedCapController contract. + * @param computeAhead_ True if the rates returned by computeRate should be computed on-the-fly with clamping; + * false if the returned rates should be the same as the last pushed rates (from the buffer). * @param period_ The period for the rate controller. * @param initialBufferCardinality_ The initial buffer cardinality for the rate controller. * @param updatersMustBeEoa_ A flag indicating if updaters must be externally owned accounts. */ constructor( + bool computeAhead_, uint32 period_, uint8 initialBufferCardinality_, bool updatersMustBeEoa_ - ) CapController(period_, initialBufferCardinality_, updatersMustBeEoa_) { + ) CapController(computeAhead_, period_, initialBufferCardinality_, updatersMustBeEoa_) { initializeRoles(); } diff --git a/contracts/rates/controllers/ManagedPidController.sol b/contracts/rates/controllers/ManagedPidController.sol index 27eb1fc..0225491 100644 --- a/contracts/rates/controllers/ManagedPidController.sol +++ b/contracts/rates/controllers/ManagedPidController.sol @@ -30,15 +30,18 @@ contract ManagedPidController is PidController, AccessControlEnumerable { /// @notice Constructs the ManagedPidController. /// @param inputAndErrorOracle_ Oracle to provide input and error values. + /// @param computeAhead_ True if the rates returned by computeRate should be computed on-the-fly with clamping; + /// false if the returned rates should be the same as the last pushed rates (from the buffer). /// @param period_ The period for the rate controller. /// @param initialBufferCardinality_ Initial size of the buffer for rate storage. /// @param updatersMustBeEoa_ Flag to determine if updaters must be externally owned accounts. constructor( ILiquidityOracle inputAndErrorOracle_, + bool computeAhead_, uint32 period_, uint8 initialBufferCardinality_, bool updatersMustBeEoa_ - ) PidController(inputAndErrorOracle_, period_, initialBufferCardinality_, updatersMustBeEoa_) { + ) PidController(inputAndErrorOracle_, computeAhead_, period_, initialBufferCardinality_, updatersMustBeEoa_) { initializeRoles(); } diff --git a/contracts/rates/controllers/PidController.sol b/contracts/rates/controllers/PidController.sol index c8ed8d7..99b1d67 100644 --- a/contracts/rates/controllers/PidController.sol +++ b/contracts/rates/controllers/PidController.sol @@ -91,15 +91,18 @@ abstract contract PidController is RateController { /// @notice Constructs the PidController. /// @param inputAndErrorOracle_ Default oracle to provide input and error values. + /// @param computeAhead_ True if the rates returned by computeRate should be computed on-the-fly with clamping; + /// false if the returned rates should be the same as the last pushed rates (from the buffer). /// @param period_ The period for the rate controller. /// @param initialBufferCardinality_ Initial size of the buffer for rate storage. /// @param updatersMustBeEoa_ Flag to determine if updaters must be externally owned accounts. constructor( ILiquidityOracle inputAndErrorOracle_, + bool computeAhead_, uint32 period_, uint8 initialBufferCardinality_, bool updatersMustBeEoa_ - ) RateController(period_, initialBufferCardinality_, updatersMustBeEoa_) { + ) RateController(computeAhead_, period_, initialBufferCardinality_, updatersMustBeEoa_) { if (period_ == 0) revert InvalidPeriod(period_); validateInputAndErrorOracle(inputAndErrorOracle_, true); @@ -165,18 +168,6 @@ abstract contract PidController is RateController { } } - /// @inheritdoc RateController - /// @dev Returns the current rate (latest stored) for the token, reverting if the rate has never been computed. - function computeRate(address token) external view virtual override returns (uint64) { - BufferMetadata storage meta = rateBufferMetadata[token]; - if (meta.size == 0) { - // We've never computed a rate, so revert. - revert InsufficientData(token, 0, 1); - } - - return getLatestRate(token).current; - } - /// @inheritdoc RateController /// @dev Updates are not needed if the PID config is uninitialized. function needsUpdate(bytes memory data) public view virtual override returns (bool b) { @@ -409,8 +400,11 @@ abstract contract PidController is RateController { // Compute output int256 output = pTerm + pidState.iTerm - dTerm; + // Store last values to be used in the next iteration computation pidState.lastInput = input; pidState.lastError = err; + + // Set target to the output rate (before clamping) and clamp to the range [0, 2^64) (to fit inside uint64). if (output < int256(0)) { target = 0; } else if (output >= int256(uint256(type(uint64).max))) { @@ -418,6 +412,9 @@ abstract contract PidController is RateController { } else { target = uint64(uint256(output)); } + + // Clamp the output. Note that clampChange is false here but this parameter is ignored as we indicate this + // is the output rate, signaling to use the rate controller's main clamping function which has change clamping. output = clampBigSignedRate(token, output, true, false, 0); // Clamping the output returns a value in the range [0, 2^64), so we can safely cast it to uint64. current = uint64(uint256(output)); @@ -430,6 +427,10 @@ abstract contract PidController is RateController { return target; } + function computeRateAndClamp(address token) internal view virtual override returns (uint64 target, uint64 newRate) { + (target, newRate, ) = computeNextPidRate(token); + } + /// @inheritdoc RateController function updateAndCompute(address token) internal virtual override returns (uint64 target, uint64 current) { PidState memory newPidState; diff --git a/contracts/rates/controllers/proto/aave/AaveRateController.sol b/contracts/rates/controllers/proto/aave/AaveRateController.sol index 240903d..02acc4a 100644 --- a/contracts/rates/controllers/proto/aave/AaveRateController.sol +++ b/contracts/rates/controllers/proto/aave/AaveRateController.sol @@ -29,7 +29,7 @@ contract AaveRateController is RateController { uint32 period_, uint8 initialBufferCardinality_, bool updatersMustBeEoa_ - ) RateController(period_, initialBufferCardinality_, updatersMustBeEoa_) { + ) RateController(false, period_, initialBufferCardinality_, updatersMustBeEoa_) { aclManager = aclManager_; } diff --git a/contracts/rates/controllers/proto/ionic/IonicPidController.sol b/contracts/rates/controllers/proto/ionic/IonicPidController.sol index 3fd6c23..006bf2c 100644 --- a/contracts/rates/controllers/proto/ionic/IonicPidController.sol +++ b/contracts/rates/controllers/proto/ionic/IonicPidController.sol @@ -15,10 +15,19 @@ contract IonicPidController is ManagedPidController { constructor( IComptroller comptroller_, ILiquidityOracle inputAndErrorOracle_, + bool computeAhead_, uint32 period_, uint8 initialBufferCardinality_, bool updatersMustBeEoa_ - ) ManagedPidController(inputAndErrorOracle_, period_, initialBufferCardinality_, updatersMustBeEoa_) { + ) + ManagedPidController( + inputAndErrorOracle_, + computeAhead_, + period_, + initialBufferCardinality_, + updatersMustBeEoa_ + ) + { comptroller = comptroller_; } diff --git a/contracts/rates/controllers/proto/ionic/IonicRateController.sol b/contracts/rates/controllers/proto/ionic/IonicRateController.sol new file mode 100644 index 0000000..b6c84a2 --- /dev/null +++ b/contracts/rates/controllers/proto/ionic/IonicRateController.sol @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity =0.8.13; + +import "../../../ManagedRateController.sol"; +import "../../../../vendor/ionic/IComptroller.sol"; +import "../../../../vendor/ionic/ICToken.sol"; + +contract IonicRateController is ManagedRateController { + IComptroller public immutable comptroller; + + error CTokenNotFound(address token); + + error FailedToAccrueInterest(address token, address cToken, uint256 errorCode); + + constructor( + IComptroller comptroller_, + bool computeAhead_, + uint32 period_, + uint8 initialBufferCardinality_, + bool updatersMustBeEoa_ + ) ManagedRateController(computeAhead_, period_, initialBufferCardinality_, updatersMustBeEoa_) { + comptroller = comptroller_; + } + + /// @dev Overridden to accrue interest for the prior rate before pushing the new rate. + function push(address token, RateLibrary.Rate memory rate) internal virtual override { + // Try and accrue interest if we have a prior rate + if (rateBufferMetadata[token].size > 0) { + address cToken = comptroller.cTokensByUnderlying(token); + if (cToken == address(0)) { + // Note that this check is not applied for the first rate to allow for the initial rate to be set + // before the cToken is added to the comptroller. + revert CTokenNotFound(token); + } + + // Accrue interest for the prior rate before pushing the new rate + uint256 accrueCode = ICToken(cToken).accrueInterest(); + if (accrueCode != 0) { + revert FailedToAccrueInterest(token, cToken, accrueCode); + } + } + + super.push(token, rate); + } +} diff --git a/contracts/rates/controllers/proto/truefi/TrueFiAlocPidController.sol b/contracts/rates/controllers/proto/truefi/TrueFiAlocPidController.sol index c8f9123..a34e4bb 100644 --- a/contracts/rates/controllers/proto/truefi/TrueFiAlocPidController.sol +++ b/contracts/rates/controllers/proto/truefi/TrueFiAlocPidController.sol @@ -7,10 +7,19 @@ import "../../../../vendor/truefi/IAutomatedLineOfCredit.sol"; contract TrueFiAlocPidController is ManagedPidController { constructor( ILiquidityOracle inputAndErrorOracle_, + bool computeAhead_, uint32 period_, uint8 initialBufferCardinality_, bool updatersMustBeEoa_ - ) ManagedPidController(inputAndErrorOracle_, period_, initialBufferCardinality_, updatersMustBeEoa_) {} + ) + ManagedPidController( + inputAndErrorOracle_, + computeAhead_, + period_, + initialBufferCardinality_, + updatersMustBeEoa_ + ) + {} /// @dev Overridden to accrue interest for the prior rate before pushing the new rate. function push(address alocAddress, RateLibrary.Rate memory rate) internal virtual override { diff --git a/contracts/test/rates/RateControllerStub.sol b/contracts/test/rates/RateControllerStub.sol index 63e7f00..fa1e6cf 100644 --- a/contracts/test/rates/RateControllerStub.sol +++ b/contracts/test/rates/RateControllerStub.sol @@ -21,10 +21,11 @@ contract RateControllerStub is ManagedRateController { mapping(address => OnPauseCall) public onPauseCalls; constructor( + bool computeAhead_, uint32 period_, uint8 initialBufferCardinality_, bool updatersMustBeEoa_ - ) ManagedRateController(period_, initialBufferCardinality_, updatersMustBeEoa_) {} + ) ManagedRateController(computeAhead_, period_, initialBufferCardinality_, updatersMustBeEoa_) {} function stubPush(address token, uint64 target, uint64 current, uint32 timestamp) public { RateLibrary.Rate memory rate; diff --git a/contracts/test/rates/controllers/CapControllerStub.sol b/contracts/test/rates/controllers/CapControllerStub.sol index 083545e..4876340 100644 --- a/contracts/test/rates/controllers/CapControllerStub.sol +++ b/contracts/test/rates/controllers/CapControllerStub.sol @@ -14,10 +14,11 @@ contract CapControllerStub is ManagedCapController { Config public config; constructor( + bool computeAhead_, uint32 period_, uint8 initialBufferCardinality_, bool updatersMustBeEoa_ - ) ManagedCapController(period_, initialBufferCardinality_, updatersMustBeEoa_) {} + ) ManagedCapController(computeAhead_, period_, initialBufferCardinality_, updatersMustBeEoa_) {} function overrideNeedsUpdate(bool overridden, bool needsUpdate_) public { config.needsUpdateOverridden = overridden; diff --git a/contracts/test/rates/controllers/PidControllerStub.sol b/contracts/test/rates/controllers/PidControllerStub.sol index 9ffa0ca..a5432f2 100644 --- a/contracts/test/rates/controllers/PidControllerStub.sol +++ b/contracts/test/rates/controllers/PidControllerStub.sol @@ -22,10 +22,11 @@ contract PidControllerStub is ManagedPidController, InputAndErrorAccumulatorStub mapping(address => OnPauseCall) public onPauseCalls; constructor( + bool computeAhead_, uint32 period_, uint8 initialBufferCardinality_, bool updatersMustBeEoa_ - ) ManagedPidController(this, period_, initialBufferCardinality_, updatersMustBeEoa_) {} + ) ManagedPidController(this, computeAhead_, period_, initialBufferCardinality_, updatersMustBeEoa_) {} function canUpdate( bytes memory data diff --git a/hardhat.config.js b/hardhat.config.js index d4c0d27..3d41141 100644 --- a/hardhat.config.js +++ b/hardhat.config.js @@ -5,6 +5,7 @@ require("hardhat-gas-reporter"); require("hardhat-tracer"); require("@atixlabs/hardhat-time-n-mine"); require("@nomiclabs/hardhat-etherscan"); +require("hardhat-contract-sizer"); const SOLC_8 = { version: "0.8.13", @@ -34,6 +35,7 @@ module.exports = { order: "fifo", }, }, + allowUnlimitedContractSize: true, }, polygon: { chainId: 137, @@ -89,4 +91,8 @@ module.exports = { }, ], }, + contractSizer: { + runOnCompile: true, + except: ["test"], + }, }; diff --git a/package.json b/package.json index bef256c..51c42ba 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@adrastia-oracle/adrastia-periphery", - "version": "4.9.1", + "version": "4.10.0", "main": "index.js", "author": "TRILEZ SOFTWARE INC.", "license": "BUSL-1.1", @@ -45,6 +45,7 @@ "ethereum-waffle": "^4.0.10", "ethers": "^5.7.2", "hardhat": "2.14.1", + "hardhat-contract-sizer": "^2.10.0", "hardhat-gas-reporter": "^1.0.9", "hardhat-tracer": "^2.3.4", "prettier": "^2.8.8", diff --git a/test/rates/computers/historical-rates-computer.js b/test/rates/computers/historical-rates-computer.js index c09facb..9e1cc78 100644 --- a/test/rates/computers/historical-rates-computer.js +++ b/test/rates/computers/historical-rates-computer.js @@ -7,6 +7,7 @@ const { AddressZero } = ethers.constants; const DEFAULT_PERIOD = 100; const DEFAULT_INITIAL_BUFFER_CARDINALITY = 10; const DEFAULT_UPDATERS_MUST_BE_EAO = false; +const DEFAULT_COMPUTE_AHEAD = true; const USDC = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"; @@ -20,6 +21,7 @@ describe("HistoricalRatesComputer#constructor", function () { it("Sets the default config a rate provider is provided", async function () { const rateControllerFactory = await ethers.getContractFactory("RateControllerStub"); const rateController = await rateControllerFactory.deploy( + DEFAULT_COMPUTE_AHEAD, DEFAULT_PERIOD, DEFAULT_INITIAL_BUFFER_CARDINALITY, DEFAULT_UPDATERS_MUST_BE_EAO @@ -49,6 +51,7 @@ describe("HistoricalRatesComputer#constructor", function () { it("Sets the default config a rate provider is provided, with an alternative index and high availability", async function () { const rateControllerFactory = await ethers.getContractFactory("RateControllerStub"); const rateController = await rateControllerFactory.deploy( + DEFAULT_COMPUTE_AHEAD, DEFAULT_PERIOD, DEFAULT_INITIAL_BUFFER_CARDINALITY, DEFAULT_UPDATERS_MUST_BE_EAO @@ -111,6 +114,7 @@ describe("HistoricalRatesComputer#computeRate", function () { const rateControllerFactory = await ethers.getContractFactory("RateControllerStub"); rateController = await rateControllerFactory.deploy( + DEFAULT_COMPUTE_AHEAD, DEFAULT_PERIOD, DEFAULT_INITIAL_BUFFER_CARDINALITY, DEFAULT_UPDATERS_MUST_BE_EAO @@ -268,11 +272,13 @@ describe("HistoricalRatesComputer#setConfig", function () { const rateControllerFactory = await ethers.getContractFactory("RateControllerStub"); rateController = await rateControllerFactory.deploy( + DEFAULT_COMPUTE_AHEAD, DEFAULT_PERIOD, DEFAULT_INITIAL_BUFFER_CARDINALITY, DEFAULT_UPDATERS_MUST_BE_EAO ); secondRateController = await rateControllerFactory.deploy( + DEFAULT_COMPUTE_AHEAD, DEFAULT_PERIOD, DEFAULT_INITIAL_BUFFER_CARDINALITY, DEFAULT_UPDATERS_MUST_BE_EAO @@ -509,6 +515,7 @@ describe("HistoricalRatesComputer#isUsingDefaultConfig", function () { const rateControllerFactory = await ethers.getContractFactory("RateControllerStub"); rateController = await rateControllerFactory.deploy( + DEFAULT_COMPUTE_AHEAD, DEFAULT_PERIOD, DEFAULT_INITIAL_BUFFER_CARDINALITY, DEFAULT_UPDATERS_MUST_BE_EAO @@ -626,6 +633,7 @@ describe("HistoricalRatesComputer#revertToDefaultConfig", function () { const rateControllerFactory = await ethers.getContractFactory("RateControllerStub"); rateController = await rateControllerFactory.deploy( + DEFAULT_COMPUTE_AHEAD, DEFAULT_PERIOD, DEFAULT_INITIAL_BUFFER_CARDINALITY, DEFAULT_UPDATERS_MUST_BE_EAO @@ -703,6 +711,7 @@ describe("HistoricalRatesComputer#getConfig", function () { const rateControllerFactory = await ethers.getContractFactory("RateControllerStub"); rateController = await rateControllerFactory.deploy( + DEFAULT_COMPUTE_AHEAD, DEFAULT_PERIOD, DEFAULT_INITIAL_BUFFER_CARDINALITY, DEFAULT_UPDATERS_MUST_BE_EAO @@ -822,6 +831,7 @@ describe("HistoricalRatesComputer#computeRateIndex", function () { const rateControllerFactory = await ethers.getContractFactory("RateControllerStub"); rateController = await rateControllerFactory.deploy( + DEFAULT_COMPUTE_AHEAD, DEFAULT_PERIOD, DEFAULT_INITIAL_BUFFER_CARDINALITY, DEFAULT_UPDATERS_MUST_BE_EAO @@ -932,6 +942,7 @@ describe("HistoricalRatesComputer - IHistoricalRates implementation", function ( const [signer] = await ethers.getSigners(); rateController = await rateControllerStubFactory.deploy( + DEFAULT_COMPUTE_AHEAD, DEFAULT_PERIOD, DEFAULT_INITIAL_BUFFER_CARDINALITY, DEFAULT_UPDATERS_MUST_BE_EAO diff --git a/test/rates/computers/managed-historical-rates-computer.js b/test/rates/computers/managed-historical-rates-computer.js index 6dcd541..882c232 100644 --- a/test/rates/computers/managed-historical-rates-computer.js +++ b/test/rates/computers/managed-historical-rates-computer.js @@ -7,6 +7,7 @@ const { AddressZero } = ethers.constants; const DEFAULT_PERIOD = 100; const DEFAULT_INITIAL_BUFFER_CARDINALITY = 10; const DEFAULT_UPDATERS_MUST_BE_EAO = false; +const DEFAULT_COMPUTE_AHEAD = true; describe("ManagedHistoricalRatesComputer#setConfig", function () { var computerFactory; @@ -29,6 +30,7 @@ describe("ManagedHistoricalRatesComputer#setConfig", function () { const rateControllerFactory = await ethers.getContractFactory("RateControllerStub"); rateController = await rateControllerFactory.deploy( + DEFAULT_COMPUTE_AHEAD, DEFAULT_PERIOD, DEFAULT_INITIAL_BUFFER_CARDINALITY, DEFAULT_UPDATERS_MUST_BE_EAO diff --git a/test/rates/controllers/cap-controller.js b/test/rates/controllers/cap-controller.js index a62699f..f0f327a 100644 --- a/test/rates/controllers/cap-controller.js +++ b/test/rates/controllers/cap-controller.js @@ -16,6 +16,7 @@ const USDC = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"; const PERIOD = 100; const INITIAL_BUFFER_CARDINALITY = 2; const UPDATERS_MUST_BE_EOA = false; +const COMPUTE_AHEAD = true; const TWO_PERCENT_CHANGE = ethers.utils.parseUnits("0.02", 8); @@ -62,12 +63,14 @@ describe("CapController#constructor", function () { [1, 2], // period [1, 2], // initialBufferCardinality [false, true], // updatersMustBeEoa + [false, true], // computeAhead ]; for (const test of combos(testCombinations)) { const period = test[0]; const initialBufferCardinality = test[1]; const updatersMustBeEoa = test[2]; + const computeAhead = test[3]; it( "Should deploy with period=" + @@ -75,10 +78,18 @@ describe("CapController#constructor", function () { ", initialBufferCardinality=" + initialBufferCardinality + ", updatersMustBeEoa=" + - updatersMustBeEoa, + updatersMustBeEoa + + ", computeAhead=" + + computeAhead, async function () { - const rateController = await factory.deploy(period, initialBufferCardinality, updatersMustBeEoa); - + const rateController = await factory.deploy( + computeAhead, + period, + initialBufferCardinality, + updatersMustBeEoa + ); + + expect(await rateController.computeAhead()).to.equal(computeAhead); expect(await rateController.period()).to.equal(period); expect(await rateController.getRatesCapacity(GRT)).to.equal(initialBufferCardinality); expect(await rateController.updatersMustBeEoa()).to.equal(updatersMustBeEoa); @@ -95,7 +106,12 @@ describe("CapController#setRatesCapacity", function () { beforeEach(async () => { const controllerFactory = await ethers.getContractFactory("ManagedCapController"); - controller = await controllerFactory.deploy(PERIOD, INITIAL_BUFFER_CARDINALITY, UPDATERS_MUST_BE_EOA); + controller = await controllerFactory.deploy( + COMPUTE_AHEAD, + PERIOD, + INITIAL_BUFFER_CARDINALITY, + UPDATERS_MUST_BE_EOA + ); // Get our signer address const [signer] = await ethers.getSigners(); @@ -128,7 +144,12 @@ describe("CapController#setChangeThreshold", function () { beforeEach(async () => { const controllerFactory = await ethers.getContractFactory("ManagedCapController"); - controller = await controllerFactory.deploy(PERIOD, INITIAL_BUFFER_CARDINALITY, UPDATERS_MUST_BE_EOA); + controller = await controllerFactory.deploy( + COMPUTE_AHEAD, + PERIOD, + INITIAL_BUFFER_CARDINALITY, + UPDATERS_MUST_BE_EOA + ); // Get our signer address const [signer] = await ethers.getSigners(); @@ -198,7 +219,12 @@ describe("CapController#willAnythingChange", function () { beforeEach(async () => { const controllerFactory = await ethers.getContractFactory("CapControllerStub"); - controller = await controllerFactory.deploy(PERIOD, INITIAL_BUFFER_CARDINALITY, UPDATERS_MUST_BE_EOA); + controller = await controllerFactory.deploy( + COMPUTE_AHEAD, + PERIOD, + INITIAL_BUFFER_CARDINALITY, + UPDATERS_MUST_BE_EOA + ); // Get our signer address const [signer] = await ethers.getSigners(); @@ -382,7 +408,12 @@ describe("CapController#changeThresholdSurpassed", function () { beforeEach(async () => { const controllerFactory = await ethers.getContractFactory("CapControllerStub"); - controller = await controllerFactory.deploy(PERIOD, INITIAL_BUFFER_CARDINALITY, UPDATERS_MUST_BE_EOA); + controller = await controllerFactory.deploy( + COMPUTE_AHEAD, + PERIOD, + INITIAL_BUFFER_CARDINALITY, + UPDATERS_MUST_BE_EOA + ); }); it("Returns true when the change is enormously large", async function () { @@ -398,7 +429,12 @@ describe("CapController#update", function () { async function deploy(updatersMustBeEoa) { const controllerFactory = await ethers.getContractFactory("CapControllerStub"); - controller = await controllerFactory.deploy(PERIOD, INITIAL_BUFFER_CARDINALITY, updatersMustBeEoa); + controller = await controllerFactory.deploy( + COMPUTE_AHEAD, + PERIOD, + INITIAL_BUFFER_CARDINALITY, + updatersMustBeEoa + ); // Get our signer address const [signer] = await ethers.getSigners(); @@ -466,7 +502,12 @@ describe("CapController#manuallyPushRate", function () { beforeEach(async function () { const controllerFactory = await ethers.getContractFactory("CapControllerStub"); - controller = await controllerFactory.deploy(PERIOD, INITIAL_BUFFER_CARDINALITY, UPDATERS_MUST_BE_EOA); + controller = await controllerFactory.deploy( + COMPUTE_AHEAD, + PERIOD, + INITIAL_BUFFER_CARDINALITY, + UPDATERS_MUST_BE_EOA + ); // Get our signer address const [signer] = await ethers.getSigners(); @@ -560,7 +601,12 @@ describe("CapController#setUpdatesPaused", function () { beforeEach(async () => { const controllerFactory = await ethers.getContractFactory("CapControllerStub"); - controller = await controllerFactory.deploy(PERIOD, INITIAL_BUFFER_CARDINALITY, UPDATERS_MUST_BE_EOA); + controller = await controllerFactory.deploy( + COMPUTE_AHEAD, + PERIOD, + INITIAL_BUFFER_CARDINALITY, + UPDATERS_MUST_BE_EOA + ); // Get our signer address const [signer] = await ethers.getSigners(); @@ -626,7 +672,12 @@ describe("CapController#canUpdate", function () { async function deploy(updatersMustBeEOA) { const controllerFactory = await ethers.getContractFactory("CapControllerStub"); - controller = await controllerFactory.deploy(PERIOD, INITIAL_BUFFER_CARDINALITY, updatersMustBeEOA); + controller = await controllerFactory.deploy( + COMPUTE_AHEAD, + PERIOD, + INITIAL_BUFFER_CARDINALITY, + updatersMustBeEOA + ); // Get our signer address const [signer] = await ethers.getSigners(); @@ -708,7 +759,12 @@ describe("CapController#setConfig", function () { beforeEach(async () => { const controllerFactory = await ethers.getContractFactory("CapControllerStub"); - controller = await controllerFactory.deploy(PERIOD, INITIAL_BUFFER_CARDINALITY, UPDATERS_MUST_BE_EOA); + controller = await controllerFactory.deploy( + COMPUTE_AHEAD, + PERIOD, + INITIAL_BUFFER_CARDINALITY, + UPDATERS_MUST_BE_EOA + ); // Get our signer address const [signer] = await ethers.getSigners(); @@ -766,7 +822,12 @@ describe("CapController#supportsInterface", function () { beforeEach(async () => { const controllerFactory = await ethers.getContractFactory("ManagedCapController"); - controller = await controllerFactory.deploy(PERIOD, INITIAL_BUFFER_CARDINALITY, UPDATERS_MUST_BE_EOA); + controller = await controllerFactory.deploy( + COMPUTE_AHEAD, + PERIOD, + INITIAL_BUFFER_CARDINALITY, + UPDATERS_MUST_BE_EOA + ); const interfaceIdsFactory = await ethers.getContractFactory("InterfaceIds"); interfaceIds = await interfaceIdsFactory.deploy(); diff --git a/test/rates/controllers/ionic/ionic-pid-controller.js b/test/rates/controllers/ionic/ionic-pid-controller.js index 64cfd00..3cf24a3 100644 --- a/test/rates/controllers/ionic/ionic-pid-controller.js +++ b/test/rates/controllers/ionic/ionic-pid-controller.js @@ -65,7 +65,14 @@ describe("IonicPidController", function () { await oracle.deployed(); const controllerFactory = await ethers.getContractFactory("IonicPidController"); - controller = await controllerFactory.deploy(comptroller.address, oracle.address, DEFAULT_PERIOD, 1, false); + controller = await controllerFactory.deploy( + comptroller.address, + oracle.address, + false, + DEFAULT_PERIOD, + 1, + false + ); // Grant roles const [signer] = await ethers.getSigners(); diff --git a/test/rates/controllers/ionic/ionic-rate-controller.js b/test/rates/controllers/ionic/ionic-rate-controller.js new file mode 100644 index 0000000..47c03dc --- /dev/null +++ b/test/rates/controllers/ionic/ionic-rate-controller.js @@ -0,0 +1,132 @@ +const { expect } = require("chai"); +const { BigNumber } = require("ethers"); +const { ethers, timeAndMine } = require("hardhat"); + +const AddressZero = ethers.constants.AddressZero; + +const USDC = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"; +const GRT = "0xc944E90C64B2c07662A292be6244BDf05Cda44a7"; + +const ORACLE_UPDATER_MANAGER_ROLE = ethers.utils.keccak256(ethers.utils.toUtf8Bytes("UPDATER_ADMIN_ROLE")); +const ORACLE_UPDATER_ROLE = ethers.utils.keccak256(ethers.utils.toUtf8Bytes("ORACLE_UPDATER_ROLE")); +const RATE_ADMIN_ROLE = ethers.utils.keccak256(ethers.utils.toUtf8Bytes("RATE_ADMIN_ROLE")); +const UPDATE_PAUSE_ADMIN_ROLE = ethers.utils.keccak256(ethers.utils.toUtf8Bytes("UPDATE_PAUSE_ADMIN_ROLE")); + +// In this example, 1e18 = 100% +const DEFAULT_CONFIG = { + max: ethers.utils.parseUnits("1.0", 18), // 100% + min: ethers.utils.parseUnits("0.0", 18), // 0% + maxIncrease: ethers.utils.parseUnits("0.02", 18), // 2% + maxDecrease: ethers.utils.parseUnits("0.01", 18), // 1% + maxPercentIncrease: 10000, // 100% + maxPercentDecrease: 10000, // 100% + base: ethers.utils.parseUnits("0.6", 18), // 60% + componentWeights: [], + components: [], +}; + +const DEFAULT_PERIOD = 100; + +describe("IonicRateController", function () { + describe("IonicRateController#update", function () { + var controller; + var token; + var cToken; + var comptroller; + + beforeEach(async function () { + token = USDC; + + const cTokenFactory = await ethers.getContractFactory("IonicCTokenStub"); + cToken = await cTokenFactory.deploy(token); + await cToken.deployed(); + + const comptrollerFactory = await ethers.getContractFactory("IonicStub"); + comptroller = await comptrollerFactory.deploy(); + + await comptroller.stubSetCToken(token, cToken.address); + + const controllerFactory = await ethers.getContractFactory("IonicRateController"); + controller = await controllerFactory.deploy(comptroller.address, false, DEFAULT_PERIOD, 1, false); + + // Grant roles + const [signer] = await ethers.getSigners(); + await controller.grantRole(ORACLE_UPDATER_MANAGER_ROLE, signer.address); + await controller.grantRole(ORACLE_UPDATER_ROLE, signer.address); + await controller.grantRole(RATE_ADMIN_ROLE, signer.address); + await controller.grantRole(UPDATE_PAUSE_ADMIN_ROLE, signer.address); + + // Set configs + await controller.setConfig(token, DEFAULT_CONFIG); + }); + + it("Doesn't call accrueInterest if the buffer is empty", async function () { + const updateData = ethers.utils.defaultAbiCoder.encode(["address"], [token]); + await expect(controller.update(updateData)).to.not.emit(cToken, "InterestAccrued"); + }); + + it("Calls accrueInterest if the buffer is not empty", async function () { + const startingRate = ethers.utils.parseUnits("0.2", 18); + await controller.manuallyPushRate(token, startingRate, startingRate, 1); + + const period = await controller.period(); + // Advance the period + await timeAndMine.increaseTime(period.toNumber()); + + const updateData = ethers.utils.defaultAbiCoder.encode(["address"], [token]); + await expect(controller.update(updateData)).to.emit(cToken, "InterestAccrued"); + }); + + it("Reverts if accrueInterest is not successful and the buffer is not empty", async function () { + const startingRate = ethers.utils.parseUnits("0.2", 18); + await controller.manuallyPushRate(token, startingRate, startingRate, 1); + + // Set the interest rate to be unavailable + await cToken.stubSetAccrueInterestReturnCode(1); + + const period = await controller.period(); + // Advance the period + await timeAndMine.increaseTime(period.toNumber()); + + const updateData = ethers.utils.defaultAbiCoder.encode(["address"], [token]); + await expect(controller.update(updateData)).to.be.revertedWith("FailedToAccrueInterest"); + }); + + it("Reverts if it can't find the cToken", async function () { + // Configure GRT + await controller.setConfig(GRT, DEFAULT_CONFIG); + + // Push an initial rate (should succeed) + const startingRate = ethers.utils.parseUnits("0.2", 18); + await controller.manuallyPushRate(GRT, startingRate, startingRate, 1); + + const period = await controller.period(); + // Advance the period + await timeAndMine.increaseTime(period.toNumber()); + + const updateData = ethers.utils.defaultAbiCoder.encode(["address"], [GRT]); + await expect(controller.update(updateData)).to.be.revertedWith("CTokenNotFound"); + }); + + it("Calls accrueInterest before pushing the new rate", async function () { + const startingRate = ethers.utils.parseUnits("0.2", 18); + await controller.manuallyPushRate(token, startingRate, startingRate, 1); + + // Set a new base rate of 90% + const newBaseRate = ethers.utils.parseUnits("0.9", 18); + await controller.setConfig(token, { ...DEFAULT_CONFIG, base: newBaseRate }); + + const period = await controller.period(); + // Advance the period + await timeAndMine.increaseTime(period.toNumber()); + + const updateData = ethers.utils.defaultAbiCoder.encode(["address"], [token]); + await expect(controller.update(updateData)).to.emit(cToken, "InterestAccrued").withArgs(startingRate); + + const newRate = await controller.computeRate(token); + + // The new rate should be different from the starting rate + expect(newRate).to.not.eq(startingRate); + }); + }); +}); diff --git a/test/rates/controllers/truefi/truefi-pid-controller.js b/test/rates/controllers/truefi/truefi-pid-controller.js index c480f50..f06fcb7 100644 --- a/test/rates/controllers/truefi/truefi-pid-controller.js +++ b/test/rates/controllers/truefi/truefi-pid-controller.js @@ -53,7 +53,7 @@ describe("TrueFiAlocPidController", function () { await oracle.deployed(); const controllerFactory = await ethers.getContractFactory("TrueFiAlocPidController"); - controller = await controllerFactory.deploy(oracle.address, DEFAULT_PERIOD, 1, false); + controller = await controllerFactory.deploy(oracle.address, false, DEFAULT_PERIOD, 1, false); // Grant roles const [signer] = await ethers.getSigners(); diff --git a/test/rates/rate-controller.js b/test/rates/rate-controller.js index a6b9d58..c777db9 100644 --- a/test/rates/rate-controller.js +++ b/test/rates/rate-controller.js @@ -16,6 +16,7 @@ const USDC = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"; const PERIOD = 100; const INITIAL_BUFFER_CARDINALITY = 2; const UPDATERS_MUST_BE_EOA = false; +const COMPUTE_AHEAD = true; const MAX_RATE = BigNumber.from(2).pow(64).sub(1); const MIN_RATE = BigNumber.from(0); @@ -77,8 +78,9 @@ async function deployStandardController(overrides, contractName = "RateControlle const period = overrides?.period ?? PERIOD; const initialBufferCardinality = overrides?.initialBufferCardinality ?? INITIAL_BUFFER_CARDINALITY; const updaterMustBeEoa = overrides?.updaterMustBeEoa ?? UPDATERS_MUST_BE_EOA; + const computeAhead = overrides?.computeAhead ?? COMPUTE_AHEAD; - controller = await controllerFactory.deploy(period, initialBufferCardinality, updaterMustBeEoa); + controller = await controllerFactory.deploy(computeAhead, period, initialBufferCardinality, updaterMustBeEoa); return { controller: controller, @@ -91,8 +93,9 @@ async function deployPidController(overrides, contractName = "PidControllerStub" const period = overrides?.period ?? PERIOD; const initialBufferCardinality = overrides?.initialBufferCardinality ?? INITIAL_BUFFER_CARDINALITY; const updaterMustBeEoa = overrides?.updaterMustBeEoa ?? UPDATERS_MUST_BE_EOA; + const computeAhead = overrides?.computeAhead ?? false; - controller = await controllerFactory.deploy(period, initialBufferCardinality, updaterMustBeEoa); + controller = await controllerFactory.deploy(computeAhead, period, initialBufferCardinality, updaterMustBeEoa); return { controller: controller, @@ -112,17 +115,29 @@ describe("PidController#constructor", function () { }); it("Correctly sets the input and error oracle address", async function () { - const controller = await factory.deploy(oracle.address, 1, 1, false); + const controller = await factory.deploy(oracle.address, false, 1, 1, false); expect(await controller.getInputAndErrorOracle(GRT)).to.equal(oracle.address); }); + it("Deploys without compute ahead", async function () { + const controller = await factory.deploy(oracle.address, false, 1, 1, false); + + expect(await controller.computeAhead()).to.equal(false); + }); + + it("Deploys with compute ahead", async function () { + const controller = await factory.deploy(oracle.address, true, 1, 1, false); + + expect(await controller.computeAhead()).to.equal(true); + }); + it("Reverts if the period is zero", async function () { - await expect(factory.deploy(oracle.address, 0, 1, false)).to.be.revertedWith("InvalidPeriod"); + await expect(factory.deploy(oracle.address, false, 0, 1, false)).to.be.revertedWith("InvalidPeriod"); }); it("Reverts if the oracle address is zero", async function () { - await expect(factory.deploy(AddressZero, 1, 1, false)).to.be.revertedWith("InvalidInputAndErrorOracle"); + await expect(factory.deploy(AddressZero, false, 1, 1, false)).to.be.revertedWith("InvalidInputAndErrorOracle"); }); }); @@ -137,7 +152,7 @@ describe("PidController#setPidConfig", function () { oracle = await oracleFactory.deploy(); await oracle.deployed(); - controller = await factory.deploy(oracle.address, 1, 1, false); + controller = await factory.deploy(oracle.address, false, 1, 1, false); await controller.deployed(); }); @@ -353,7 +368,7 @@ describe("PidController#setDefaultInputAndErrorOracle", function () { newOracle = await oracleFactory.deploy(); await newOracle.deployed(); - controller = await factory.deploy(oracle.address, 1, 1, false); + controller = await factory.deploy(oracle.address, false, 1, 1, false); await controller.deployed(); }); @@ -425,7 +440,7 @@ describe("PidController#onPaused", function () { oracle = await oracleFactory.deploy(); await oracle.deployed(); - controller = await factory.deploy(oracle.address, 1, 1, false); + controller = await factory.deploy(oracle.address, false, 1, 1, false); await controller.deployed(); }); @@ -478,11 +493,61 @@ describe("PidController#onPaused", function () { }); function describeStandardControllerComputeRateTests(contractName, deployFunc) { - describe(contractName + "#computeRate", function () { + describe(contractName + "#computeRate (computeAhead = false)", function () { var controller; beforeEach(async () => { - const deployment = await deployFunc(); + const deployment = await deployFunc({ computeAhead: false }); + controller = deployment.controller; + + // Get our signer address + const [signer] = await ethers.getSigners(); + + // Grant all roles to the signer + await controller.grantRole(ORACLE_UPDATER_MANAGER_ROLE, signer.address); + await controller.grantRole(ORACLE_UPDATER_ROLE, signer.address); + await controller.grantRole(RATE_ADMIN_ROLE, signer.address); + await controller.grantRole(UPDATE_PAUSE_ADMIN_ROLE, signer.address); + + // Set config for GRT + await controller.setConfig(GRT, DEFAULT_CONFIG); + }); + + it("Reverts if we've never updated the rate", async function () { + await expect(controller.computeRate(GRT)).to.be.revertedWith("InsufficientData"); + }); + + it("Returns the latest current rate (n=1)", async function () { + const updateData = ethers.utils.defaultAbiCoder.encode(["address"], [GRT]); + await controller.update(updateData); + + // Change the base so that the next rate should change + await controller.setConfig(GRT, { + ...DEFAULT_CONFIG, + base: DEFAULT_CONFIG.base.div(2), + }); + + // Get the historical rate + const historicalRate = await controller.getRateAt(GRT, 0); + + const rate = await controller.computeRate(GRT); + + expect(rate).to.equal(historicalRate.current); + + // Sanity check that the next rate is not the same + const period = await controller.period(); + await timeAndMine.increaseTime(period.toNumber()); + await controller.update(updateData); + const nextRate = await controller.computeRate(GRT); + expect(nextRate).to.not.equal(rate); + }); + }); + + describe(contractName + "#computeRate (computeAhead = true)", function () { + var controller; + + beforeEach(async () => { + const deployment = await deployFunc({ computeAhead: true }); controller = deployment.controller; // Get our signer address @@ -929,11 +994,11 @@ function describeStandardControllerComputeRateTests(contractName, deployFunc) { } function describePidControllerComputeRateTests(contractName, deployFunc) { - describe(contractName + "#computeRate", function () { + describe(contractName + "#computeRate (computeAhead = false)", function () { var controller; beforeEach(async () => { - const deployment = await deployFunc(); + const deployment = await deployFunc({ computeAhead: false }); controller = deployment.controller; // Get our signer address @@ -997,6 +1062,84 @@ function describePidControllerComputeRateTests(contractName, deployFunc) { expect(rate).to.equal(historicalRate.current); }); }); + + describe(contractName + "#computeRate (computeAhead = true)", function () { + var controller; + + beforeEach(async () => { + const deployment = await deployFunc({ computeAhead: true }); + controller = deployment.controller; + + // Get our signer address + const [signer] = await ethers.getSigners(); + + // Grant all roles to the signer + await controller.grantRole(ORACLE_UPDATER_MANAGER_ROLE, signer.address); + await controller.grantRole(ORACLE_UPDATER_ROLE, signer.address); + await controller.grantRole(RATE_ADMIN_ROLE, signer.address); + await controller.grantRole(UPDATE_PAUSE_ADMIN_ROLE, signer.address); + + // Set config for GRT + await controller.setConfig(GRT, DEFAULT_CONFIG); + + // Set PID config for GRT + await controller.setPidConfig(GRT, DEFAULT_PID_CONFIG); + + await controller.setTarget(GRT, ethers.utils.parseUnits("0.9", 8)); + await controller.setInput(GRT, ethers.utils.parseUnits("1.0", 8)); + }); + + it("Doesn't revert if we've never computed a rate before", async function () { + await expect(controller.computeRate(GRT)).to.not.be.reverted; + }); + + it("Doesn't return the last stored rate", async function () { + const updateData = ethers.utils.defaultAbiCoder.encode(["address"], [GRT]); + await controller.update(updateData); + + // Get the historical rate + const historicalRate = await controller.getRateAt(GRT, 0); + + const rate = await controller.computeRate(GRT); + + expect(rate).to.not.equal(historicalRate.current); + }); + + it("Applies clamping based on the last stored rate", async function () { + // Manually push a rate + const rate = ethers.utils.parseUnits("0.50", 8); + await controller.stubPush(GRT, rate, rate, 1); + + // Set the config s.t. the rate only decreases by 0.01% + const config = { + ...DEFAULT_CONFIG, + maxDecrease: ethers.utils.parseUnits("0.0001", 8), + maxPercentDecrease: MAX_PERCENT_DECREASE, + }; + await controller.setConfig(GRT, config); + + // Set the input to 100% and the target to 1% so the rate decreases + await controller.setInput(GRT, ethers.utils.parseUnits("1.0", 8)); + await controller.setTarget(GRT, ethers.utils.parseUnits("0.01", 8)); + + const expectedRate = rate.sub(config.maxDecrease); + + const reportedRate = await controller.computeRate(GRT); + + expect(reportedRate).to.equal(expectedRate); + + // Sanity check that the rate is clamped + await controller.setConfig(GRT, { + ...config, + maxDecrease: MAX_RATE, + maxPercentDecrease: MAX_PERCENT_DECREASE, + }); + + const unclampedRate = await controller.computeRate(GRT); + + expect(unclampedRate).to.be.lt(reportedRate); + }); + }); } function describePidControllerNeedsUpdateTests(deployFunc, getController) { @@ -4173,31 +4316,43 @@ function describeTests( period: 1, initialBufferCardinality: 1, updaterMustBeEoa: false, + computeAhead: false, }, { period: 2, initialBufferCardinality: 1, updaterMustBeEoa: false, + computeAhead: false, }, { period: 1, initialBufferCardinality: 2, updaterMustBeEoa: false, + computeAhead: false, }, { period: 1, initialBufferCardinality: 1, updaterMustBeEoa: true, + computeAhead: false, }, { period: 2, initialBufferCardinality: 1, updaterMustBeEoa: true, + computeAhead: false, }, { period: 1, initialBufferCardinality: 2, updaterMustBeEoa: true, + computeAhead: false, + }, + { + period: 1, + initialBufferCardinality: 1, + updaterMustBeEoa: false, + computeAhead: true, }, ]; @@ -4207,12 +4362,14 @@ function describeTests( test.period + ", initialBufferCardinality " + test.initialBufferCardinality + - ", and updaterMustBeEoa " + - test.updaterMustBeEoa, + ", updaterMustBeEoa " + + test.updaterMustBeEoa + + ", and computeAhead ", async function () { const deployment = await deployFunc(test); const controller = deployment.controller; + expect(await controller.computeAhead()).to.equal(test.computeAhead); expect(await controller.period()).to.equal(test.period); expect(await controller.getRatesCapacity(GRT)).to.equal(test.initialBufferCardinality); expect(await controller.updatersMustBeEoa()).to.equal(test.updaterMustBeEoa); diff --git a/yarn.lock b/yarn.lock index 2b13111..a04a685 100644 --- a/yarn.lock +++ b/yarn.lock @@ -84,6 +84,11 @@ "@chainsafe/persistent-merkle-tree" "^0.4.2" case "^1.6.3" +"@colors/colors@1.5.0": + version "1.5.0" + resolved "https://registry.yarnpkg.com/@colors/colors/-/colors-1.5.0.tgz#bb504579c1cae923e6576a4f5da43d25f97bdbd9" + integrity sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ== + "@ethereum-waffle/chai@4.0.10": version "4.0.10" resolved "https://registry.yarnpkg.com/@ethereum-waffle/chai/-/chai-4.0.10.tgz#6f600a40b6fdaed331eba42b8625ff23f3a0e59a" @@ -1808,7 +1813,7 @@ chalk@^2.0.0, chalk@^2.4.2: escape-string-regexp "^1.0.5" supports-color "^5.3.0" -chalk@^4.1.0, chalk@^4.1.2: +chalk@^4.0.0, chalk@^4.1.0, chalk@^4.1.2: version "4.1.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== @@ -1895,6 +1900,15 @@ cli-table3@^0.5.0: optionalDependencies: colors "^1.1.2" +cli-table3@^0.6.0: + version "0.6.5" + resolved "https://registry.yarnpkg.com/cli-table3/-/cli-table3-0.6.5.tgz#013b91351762739c16a9567c21a04632e449bf2f" + integrity sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ== + dependencies: + string-width "^4.2.0" + optionalDependencies: + "@colors/colors" "1.5.0" + cliui@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/cliui/-/cliui-5.0.0.tgz#deefcfdb2e800784aa34f46fa08e06851c7bbbc5" @@ -3074,6 +3088,15 @@ har-validator@~5.1.3: ajv "^6.12.3" har-schema "^2.0.0" +hardhat-contract-sizer@^2.10.0: + version "2.10.0" + resolved "https://registry.yarnpkg.com/hardhat-contract-sizer/-/hardhat-contract-sizer-2.10.0.tgz#72646f43bfe50e9a5702c9720c9bc3e77d93a2c9" + integrity sha512-QiinUgBD5MqJZJh1hl1jc9dNnpJg7eE/w4/4GEnrcmZJJTDbVFNe3+/3Ep24XqISSkYxRz36czcPHKHd/a0dwA== + dependencies: + chalk "^4.0.0" + cli-table3 "^0.6.0" + strip-ansi "^6.0.0" + hardhat-gas-reporter@^1.0.9: version "1.0.9" resolved "https://registry.yarnpkg.com/hardhat-gas-reporter/-/hardhat-gas-reporter-1.0.9.tgz#9a2afb354bc3b6346aab55b1c02ca556d0e16450"