diff --git a/.github/workflows/publish_npm.yml b/.github/workflows/publish_npm.yml index 776a31dd..fc79dcd2 100644 --- a/.github/workflows/publish_npm.yml +++ b/.github/workflows/publish_npm.yml @@ -40,5 +40,5 @@ jobs: - name: "publish" run: npm publish --access public - env: - NODE_AUTH_TOKEN: ${{secrets.NPM_AUTH_TOKEN}} \ No newline at end of file + env: + NODE_AUTH_TOKEN: ${{secrets.NPM_AUTH_TOKEN}} diff --git a/.gitignore b/.gitignore index 69507089..0a25aa23 100644 --- a/.gitignore +++ b/.gitignore @@ -15,6 +15,7 @@ *.log .DS_Store .pnp.* +.tool-versions yarn-debug.log* yarn-error.log* diff --git a/contracts/BreakerBox.sol b/contracts/BreakerBox.sol index a7205e9d..fbede8bd 100644 --- a/contracts/BreakerBox.sol +++ b/contracts/BreakerBox.sol @@ -314,7 +314,7 @@ contract BreakerBox is IBreakerBox, Initializable, Ownable { if (info.tradingMode != 0) { IBreaker breaker = IBreaker(tradingModeBreaker[info.tradingMode]); - uint256 cooldown = breaker.getCooldown(); + uint256 cooldown = breaker.getCooldown(rateFeedID); // If the cooldown == 0, then a manual reset is required. if (((cooldown > 0) && (cooldown + info.lastUpdatedTime) <= block.timestamp)) { diff --git a/contracts/MedianDeltaBreaker.sol b/contracts/MedianDeltaBreaker.sol index dd764300..c3c723fb 100644 --- a/contracts/MedianDeltaBreaker.sol +++ b/contracts/MedianDeltaBreaker.sol @@ -2,6 +2,8 @@ pragma solidity ^0.5.13; import { IBreaker } from "./interfaces/IBreaker.sol"; +import { WithCooldown } from "./common/breakers/WithCooldown.sol"; +import { WithThreshold } from "./common/breakers/WithThreshold.sol"; import { ISortedOracles } from "./interfaces/ISortedOracles.sol"; @@ -16,67 +18,63 @@ import { FixidityLib } from "./common/FixidityLib.sol"; * more than a configured relative threshold from the previous one. If this * breaker is triggered for a rate feed it should be set to no trading mode. */ -contract MedianDeltaBreaker is IBreaker, Ownable { +contract MedianDeltaBreaker is IBreaker, WithCooldown, WithThreshold, Ownable { using SafeMath for uint256; using FixidityLib for FixidityLib.Fraction; /* ==================== State Variables ==================== */ - - // The amount of time that must pass before the breaker can be reset for a rate feed. - // Should be set to 0 to force a manual reset. - uint256 public cooldownTime; - - // The default allowed threshold for the median rate change as a Fixidity fraction. - FixidityLib.Fraction public defaultRateChangeThreshold; - - // Maps rate feed to a threshold. - mapping(address => FixidityLib.Fraction) public rateChangeThreshold; - // Address of the Mento SortedOracles contract ISortedOracles public sortedOracles; - // Emitted when the default rate threshold is updated. - event DefaultRateChangeThresholdUpdated(uint256 defaultRateChangeThreshold); - - // Emitted when the rate threshold is updated. - event RateChangeThresholdUpdated(address rateFeedID, uint256 rateChangeThreshold); + // The previous median recorded for a ratefeed. + mapping(address => uint256) public previousMedianRates; /* ==================== Constructor ==================== */ constructor( - uint256 _cooldownTime, + uint256 _defaultCooldownTime, uint256 _defaultRateChangeThreshold, + ISortedOracles _sortedOracles, address[] memory rateFeedIDs, uint256[] memory rateChangeThresholds, - ISortedOracles _sortedOracles + uint256[] memory cooldownTimes ) public { _transferOwnership(msg.sender); - setCooldownTime(_cooldownTime); - setDefaultRateChangeThreshold(_defaultRateChangeThreshold); setSortedOracles(_sortedOracles); - setRateChangeThresholds(rateFeedIDs, rateChangeThresholds); + + _setDefaultCooldownTime(_defaultCooldownTime); + _setDefaultRateChangeThreshold(_defaultRateChangeThreshold); + _setRateChangeThresholds(rateFeedIDs, rateChangeThresholds); + _setCooldownTimes(rateFeedIDs, cooldownTimes); } /* ==================== Restricted Functions ==================== */ /** - * @notice Sets the cooldownTime to the specified value. - * @param _cooldownTime The new cooldownTime value. + * @notice Sets the cooldown time to the specified value for a rate feed. + * @param rateFeedIDs the targeted rate feed. + * @param cooldownTimes The new cooldownTime value. + * @dev Should be set to 0 to force a manual reset. + */ + function setCooldownTime(address[] calldata rateFeedIDs, uint256[] calldata cooldownTimes) external onlyOwner { + _setCooldownTimes(rateFeedIDs, cooldownTimes); + } + + /** + * @notice Sets the cooldownTime to the specified value for a rate feed. + * @param cooldownTime The new cooldownTime value. * @dev Should be set to 0 to force a manual reset. */ - function setCooldownTime(uint256 _cooldownTime) public onlyOwner { - cooldownTime = _cooldownTime; - emit CooldownTimeUpdated(_cooldownTime); + function setDefaultCooldownTime(uint256 cooldownTime) external onlyOwner { + _setDefaultCooldownTime(cooldownTime); } /** * @notice Sets rateChangeThreshold. * @param _defaultRateChangeThreshold The new rateChangeThreshold value. */ - function setDefaultRateChangeThreshold(uint256 _defaultRateChangeThreshold) public onlyOwner { - defaultRateChangeThreshold = FixidityLib.wrap(_defaultRateChangeThreshold); - require(defaultRateChangeThreshold.lt(FixidityLib.fixed1()), "rate change threshold must be less than 1"); - emit DefaultRateChangeThresholdUpdated(_defaultRateChangeThreshold); + function setDefaultRateChangeThreshold(uint256 _defaultRateChangeThreshold) external onlyOwner { + _setDefaultRateChangeThreshold(_defaultRateChangeThreshold); } /** @@ -84,23 +82,11 @@ contract MedianDeltaBreaker is IBreaker, Ownable { * @param rateFeedIDs Collection of the addresses rate feeds. * @param rateChangeThresholds Collection of the rate thresholds. */ - function setRateChangeThresholds(address[] memory rateFeedIDs, uint256[] memory rateChangeThresholds) - public + function setRateChangeThresholds(address[] calldata rateFeedIDs, uint256[] calldata rateChangeThresholds) + external onlyOwner { - require( - rateFeedIDs.length == rateChangeThresholds.length, - "rate feeds and rate change thresholds have to be the same length" - ); - for (uint256 i = 0; i < rateFeedIDs.length; i++) { - if (rateFeedIDs[i] != address(0) && rateChangeThresholds[i] != 0) { - FixidityLib.Fraction memory _rateChangeThreshold = FixidityLib.wrap(rateChangeThresholds[i]); - require(sortedOracles.getOracles(rateFeedIDs[i]).length > 0, "rate feed ID does not exist as it has 0 oracles"); - require(_rateChangeThreshold.lt(FixidityLib.fixed1()), "rate change threshold must be less than 1"); - rateChangeThreshold[rateFeedIDs[i]] = _rateChangeThreshold; - emit RateChangeThresholdUpdated(rateFeedIDs[i], rateChangeThresholds[i]); - } - } + _setRateChangeThresholds(rateFeedIDs, rateChangeThresholds); } /** @@ -115,14 +101,6 @@ contract MedianDeltaBreaker is IBreaker, Ownable { /* ==================== View Functions ==================== */ - /** - * @notice Gets the cooldown time for the breaker. - * @return Returns the time in seconds. - */ - function getCooldown() external view returns (uint256) { - return cooldownTime; - } - /** * @notice Check if the current median report rate for a rate feed change, relative * to the last median report, is greater than the configured threshold. @@ -131,17 +109,18 @@ contract MedianDeltaBreaker is IBreaker, Ownable { * @return triggerBreaker A bool indicating whether or not this breaker * should be tripped for the rate feed. */ - function shouldTrigger(address rateFeedID) public view returns (bool triggerBreaker) { - uint256 previousMedian = sortedOracles.previousMedianRate(rateFeedID); + function shouldTrigger(address rateFeedID) public returns (bool triggerBreaker) { + (uint256 currentMedian, ) = sortedOracles.medianRate(rateFeedID); + + uint256 previousMedian = previousMedianRates[rateFeedID]; + previousMedianRates[rateFeedID] = currentMedian; + if (previousMedian == 0) { - // Previous median will be 0 if this rate feed is new and has not had at least two median updates yet. + // Previous median will be 0 the first time rate is checked. return false; } - (uint256 currentMedian, ) = sortedOracles.medianRate(rateFeedID); - - // Check if current median is within allowed threshold of last median - triggerBreaker = !isWithinThreshold(previousMedian, currentMedian, rateFeedID); + return exceedsThreshold(previousMedian, currentMedian, rateFeedID); } /** @@ -150,38 +129,7 @@ contract MedianDeltaBreaker is IBreaker, Ownable { * @return resetBreaker A bool indicating whether or not * this breaker can be reset for the given rate feed. */ - function shouldReset(address rateFeedID) external view returns (bool resetBreaker) { + function shouldReset(address rateFeedID) external returns (bool resetBreaker) { return !shouldTrigger(rateFeedID); } - - /** - * @notice Checks if the specified current median rate is within the allowed threshold. - * @param prevRate The previous median rate. - * @param currentRate The current median rate. - * @param rateFeedID The specific rate ID to check threshold for. - * @return Returns a bool indicating whether or not the current rate - * is within the allowed threshold. - */ - function isWithinThreshold( - uint256 prevRate, - uint256 currentRate, - address rateFeedID - ) public view returns (bool) { - uint256 allowedThreshold = defaultRateChangeThreshold.unwrap(); - - uint256 rateSpecificThreshold = rateChangeThreshold[rateFeedID].unwrap(); - - // checks if a given rate feed id has a threshold set and reassignes it - if (rateSpecificThreshold != 0) allowedThreshold = rateSpecificThreshold; - - uint256 fixed1 = FixidityLib.fixed1().unwrap(); - - uint256 maxPercent = uint256(fixed1).add(allowedThreshold); - uint256 maxValue = (prevRate.mul(maxPercent)).div(10**24); - - uint256 minPercent = uint256(fixed1).sub(allowedThreshold); - uint256 minValue = (prevRate.mul(minPercent)).div(10**24); - - return (currentRate >= minValue && currentRate <= maxValue); - } } diff --git a/contracts/SortedOracles.sol b/contracts/SortedOracles.sol index fd684cb2..65310b31 100644 --- a/contracts/SortedOracles.sol +++ b/contracts/SortedOracles.sol @@ -38,7 +38,6 @@ contract SortedOracles is ISortedOracles, ICeloVersionedContract, Ownable, Initi mapping(address => uint256) public tokenReportExpirySeconds; IBreakerBox public breakerBox; - mapping(address => uint256) public previousMedianRate; event OracleAdded(address indexed token, address indexed oracleAddress); event OracleRemoved(address indexed token, address indexed oracleAddress); @@ -245,7 +244,6 @@ contract SortedOracles is ISortedOracles, ICeloVersionedContract, Ownable, Initi emit OracleReported(token, msg.sender, now, value); uint256 newMedian = rates[token].getMedianValue(); if (newMedian != originalMedian) { - previousMedianRate[token] = originalMedian; emit MedianUpdated(token, newMedian); } @@ -375,7 +373,6 @@ contract SortedOracles is ISortedOracles, ICeloVersionedContract, Ownable, Initi emit OracleReportRemoved(token, oracle); uint256 newMedian = rates[token].getMedianValue(); if (newMedian != originalMedian) { - previousMedianRate[token] = newMedian; emit MedianUpdated(token, newMedian); } } diff --git a/contracts/ValueDeltaBreaker.sol b/contracts/ValueDeltaBreaker.sol new file mode 100644 index 00000000..bd447c98 --- /dev/null +++ b/contracts/ValueDeltaBreaker.sol @@ -0,0 +1,153 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.5.13; + +import { IBreaker } from "./interfaces/IBreaker.sol"; +import { WithCooldown } from "./common/breakers/WithCooldown.sol"; +import { WithThreshold } from "./common/breakers/WithThreshold.sol"; + +import { ISortedOracles } from "./interfaces/ISortedOracles.sol"; + +import { Ownable } from "openzeppelin-solidity/contracts/ownership/Ownable.sol"; + +import { SafeMath } from "openzeppelin-solidity/contracts/math/SafeMath.sol"; +import { FixidityLib } from "./common/FixidityLib.sol"; + +/** + * @title Value Delta Breaker + * @notice Breaker contract that will trigger when the current oracle median rate change + * relative to a reference value is greater than a calculated threshold. If this + * breaker is triggered for a rate feed it should be set to no trading mode. + */ +contract ValueDeltaBreaker is IBreaker, WithCooldown, WithThreshold, Ownable { + using SafeMath for uint256; + using FixidityLib for FixidityLib.Fraction; + + /* ==================== State Variables ==================== */ + + // Address of the Mento SortedOracles contract + ISortedOracles public sortedOracles; + + // The reference value to check against + mapping(address => uint256) public referenceValues; + + /* ==================== Events ==================== */ + + // Emitted when the reference value is updated + event ReferenceValueUpdated(address rateFeedID, uint256 referenceValue); + + /* ==================== Constructor ==================== */ + + constructor( + uint256 _defaultCooldownTime, + uint256 _defaultRateChangeThreshold, + ISortedOracles _sortedOracles, + address[] memory rateFeedIDs, + uint256[] memory rateChangeThresholds, + uint256[] memory cooldownTimes + ) public { + _transferOwnership(msg.sender); + setSortedOracles(_sortedOracles); + + _setDefaultCooldownTime(_defaultCooldownTime); + _setDefaultRateChangeThreshold(_defaultRateChangeThreshold); + _setRateChangeThresholds(rateFeedIDs, rateChangeThresholds); + _setCooldownTimes(rateFeedIDs, cooldownTimes); + } + + /* ==================== Restricted Functions ==================== */ + + /** + * @notice Sets the cooldown time to the specified value for a rate feed. + * @param rateFeedIDs the targeted rate feed. + * @param cooldownTimes The new cooldownTime value. + * @dev Should be set to 0 to force a manual reset. + */ + function setCooldownTimes(address[] calldata rateFeedIDs, uint256[] calldata cooldownTimes) external onlyOwner { + _setCooldownTimes(rateFeedIDs, cooldownTimes); + } + + /** + * @notice Sets the cooldownTime to the specified value for a rate feed. + * @param cooldownTime The new cooldownTime value. + * @dev Should be set to 0 to force a manual reset. + */ + function setDefaultCooldownTime(uint256 cooldownTime) external onlyOwner { + _setDefaultCooldownTime(cooldownTime); + } + + /** + * @notice Sets rateChangeThreshold. + * @param _defaultRateChangeThreshold The new rateChangeThreshold value. + */ + function setDefaultRateChangeThreshold(uint256 _defaultRateChangeThreshold) external onlyOwner { + _setDefaultRateChangeThreshold(_defaultRateChangeThreshold); + } + + /** + * @notice Configures rate feed to rate shreshold pairs. + * @param rateFeedIDs Collection of the addresses rate feeds. + * @param rateChangeThresholds Collection of the rate thresholds. + */ + function setRateChangeThresholds(address[] calldata rateFeedIDs, uint256[] calldata rateChangeThresholds) + external + onlyOwner + { + _setRateChangeThresholds(rateFeedIDs, rateChangeThresholds); + } + + /** + * @notice Configures rate feed to reference value pairs. + * @param rateFeedIDs Collection of the addresses rate feeds. + * @param _referenceValues Collection of referance values. + */ + function setReferenceValues(address[] calldata rateFeedIDs, uint256[] calldata _referenceValues) external onlyOwner { + require(rateFeedIDs.length == _referenceValues.length, "array length missmatch"); + for (uint256 i = 0; i < rateFeedIDs.length; i++) { + require(rateFeedIDs[i] != address(0), "rate feed invalid"); + referenceValues[rateFeedIDs[i]] = _referenceValues[i]; + emit ReferenceValueUpdated(rateFeedIDs[i], _referenceValues[i]); + } + } + + /** + * @notice Sets the address of the sortedOracles contract. + * @param _sortedOracles The new address of the sorted oracles contract. + */ + function setSortedOracles(ISortedOracles _sortedOracles) public onlyOwner { + require(address(_sortedOracles) != address(0), "SortedOracles address must be set"); + sortedOracles = _sortedOracles; + emit SortedOraclesUpdated(address(_sortedOracles)); + } + + /* ==================== Public Functions ==================== */ + + /** + * @notice Check if the current median report rate change, for a rate feed, relative + * to the last median report is greater than a calculated threshold. + * If the change is greater than the threshold the breaker will trip. + * @param rateFeedID The rate feed to be checked. + * @return triggerBreaker A bool indicating whether or not this breaker + * should be tripped for the rate feed. + */ + function shouldTrigger(address rateFeedID) public returns (bool triggerBreaker) { + (uint256 currentMedian, ) = sortedOracles.medianRate(rateFeedID); + uint256 referenceValue = referenceValues[rateFeedID]; + + if (referenceValue == 0) { + // Never trigger if reference value is not set + return false; + } + + return exceedsThreshold(referenceValue, currentMedian, rateFeedID); + } + + /** + * @notice Checks whether or not the conditions have been met + * for the specifed rate feed to be reset. + * @return resetBreaker A bool indicating whether or not + * this breaker can be reset for the given rate feed. + */ + function shouldReset(address rateFeedID) external returns (bool resetBreaker) { + return !shouldTrigger(rateFeedID); + } +} diff --git a/contracts/common/breakers/WithCooldown.sol b/contracts/common/breakers/WithCooldown.sol new file mode 100644 index 00000000..72eb5d5b --- /dev/null +++ b/contracts/common/breakers/WithCooldown.sol @@ -0,0 +1,72 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.5.13; + +/** + * @title Breaker With Cooldown + * @notice Utility portion of a Breaker contract which deals with the + * cooldown component. + */ +contract WithCooldown { + /* ==================== Events ==================== */ + /** + * @notice Emitted after the cooldownTime has been updated. + * @param newCooldownTime The new cooldownTime of the breaker. + */ + event DefaultCooldownTimeUpdated(uint256 newCooldownTime); + + /** + * @notice Emitted after the cooldownTime has been updated. + * @param rateFeedID The rateFeedID targeted. + * @param newCooldownTime The new cooldownTime of the breaker. + */ + event RateFeedCooldownTimeUpdated(address rateFeedID, uint256 newCooldownTime); + + /* ==================== State Variables ==================== */ + + // The amount of time that must pass before the breaker can be reset for a rate feed. + // Should be set to 0 to force a manual reset. + uint256 public defaultCooldownTime; + mapping(address => uint256) public rateFeedCooldownTime; + + /* ==================== View Functions ==================== */ + + /** + * @notice Get the cooldown time for a rateFeedID + * @param rateFeedID the targeted rate feed. + * @return the rate specific or default cooldown + */ + function getCooldown(address rateFeedID) public view returns (uint256) { + uint256 _rateFeedCooldownTime = rateFeedCooldownTime[rateFeedID]; + if (_rateFeedCooldownTime == 0) { + return defaultCooldownTime; + } + return _rateFeedCooldownTime; + } + + /* ==================== Internal Functions ==================== */ + + /** + * @notice Sets the cooldown time to the specified value for a rate feed. + * @param rateFeedIDs the targeted rate feed. + * @param cooldownTimes The new cooldownTime value. + * @dev Should be set to 0 to force a manual reset. + */ + function _setCooldownTimes(address[] memory rateFeedIDs, uint256[] memory cooldownTimes) internal { + require(rateFeedIDs.length == cooldownTimes.length, "array length missmatch"); + for (uint256 i = 0; i < rateFeedIDs.length; i++) { + require(rateFeedIDs[i] != address(0), "rate feed invalid"); + rateFeedCooldownTime[rateFeedIDs[i]] = cooldownTimes[i]; + emit RateFeedCooldownTimeUpdated(rateFeedIDs[i], cooldownTimes[i]); + } + } + + /** + * @notice Sets the cooldownTime to the specified value for a rate feed. + * @param cooldownTime The new cooldownTime value. + * @dev Should be set to 0 to force a manual reset. + */ + function _setDefaultCooldownTime(uint256 cooldownTime) internal { + defaultCooldownTime = cooldownTime; + emit DefaultCooldownTimeUpdated(cooldownTime); + } +} diff --git a/contracts/common/breakers/WithThreshold.sol b/contracts/common/breakers/WithThreshold.sol new file mode 100644 index 00000000..ade068a3 --- /dev/null +++ b/contracts/common/breakers/WithThreshold.sol @@ -0,0 +1,94 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.5.13; + +import { FixidityLib } from "../FixidityLib.sol"; +import { SafeMath } from "openzeppelin-solidity/contracts/math/SafeMath.sol"; + +/** + * @title Breaker With Thershold + * @notice Utility portion of a Breaker contract which deals with + * managing a threshold percentage and checking two values + * . against it. + */ +contract WithThreshold { + using FixidityLib for FixidityLib.Fraction; + using SafeMath for uint256; + + /* ==================== Events ==================== */ + + // Emitted when the default rate threshold is updated. + event DefaultRateChangeThresholdUpdated(uint256 defaultRateChangeThreshold); + + // Emitted when the rate threshold is updated. + event RateChangeThresholdUpdated(address rateFeedID, uint256 rateChangeThreshold); + + /* ==================== State Variables ==================== */ + + // The default allowed threshold for the median rate change as a Fixidity fraction. + FixidityLib.Fraction public defaultRateChangeThreshold; + + // Maps rate feed to a threshold. + mapping(address => FixidityLib.Fraction) public rateChangeThreshold; + + /* ==================== View Functions ==================== */ + + /** + * @notice Checks if a value is in a certain theshold of a given reference value. + * @dev The reference value can be the previous median (MedianDeltaBreaker) or + * a static value (ValueDeltaBreaker), while the currentValue is usually + * the median after the most recent report. + * @param referenceValue The reference value to check against. + * @param currentValue The current value which is checked against the reference. + * @param rateFeedID The specific rate ID to check threshold for. + * @return Returns a bool indicating whether or not the current rate + * is within the allowed threshold. + */ + function exceedsThreshold( + uint256 referenceValue, + uint256 currentValue, + address rateFeedID + ) public view returns (bool) { + uint256 allowedThreshold = defaultRateChangeThreshold.unwrap(); + uint256 rateSpecificThreshold = rateChangeThreshold[rateFeedID].unwrap(); + // checks if a given rate feed id has a threshold set and reassignes it + if (rateSpecificThreshold != 0) allowedThreshold = rateSpecificThreshold; + + uint256 fixed1 = FixidityLib.fixed1().unwrap(); + + uint256 maxPercent = uint256(fixed1).add(allowedThreshold); + uint256 maxValue = (referenceValue.mul(maxPercent)).div(10**24); + + uint256 minPercent = uint256(fixed1).sub(allowedThreshold); + uint256 minValue = (referenceValue.mul(minPercent)).div(10**24); + + return (currentValue < minValue || currentValue > maxValue); + } + + /* ==================== Internal Functions ==================== */ + + /** + * @notice Sets rateChangeThreshold. + * @param _defaultRateChangeThreshold The new rateChangeThreshold value. + */ + function _setDefaultRateChangeThreshold(uint256 _defaultRateChangeThreshold) internal { + defaultRateChangeThreshold = FixidityLib.wrap(_defaultRateChangeThreshold); + require(defaultRateChangeThreshold.lt(FixidityLib.fixed1()), "value must be less than 1"); + emit DefaultRateChangeThresholdUpdated(_defaultRateChangeThreshold); + } + + /** + * @notice Configures rate feed to rate shreshold pairs. + * @param rateFeedIDs Collection of the addresses rate feeds. + * @param rateChangeThresholds Collection of the rate thresholds. + */ + function _setRateChangeThresholds(address[] memory rateFeedIDs, uint256[] memory rateChangeThresholds) internal { + require(rateFeedIDs.length == rateChangeThresholds.length, "array length missmatch"); + for (uint256 i = 0; i < rateFeedIDs.length; i++) { + require(rateFeedIDs[i] != address(0), "rate feed invalid"); + FixidityLib.Fraction memory _rateChangeThreshold = FixidityLib.wrap(rateChangeThresholds[i]); + require(_rateChangeThreshold.lt(FixidityLib.fixed1()), "value must be less than 1"); + rateChangeThreshold[rateFeedIDs[i]] = _rateChangeThreshold; + emit RateChangeThresholdUpdated(rateFeedIDs[i], rateChangeThresholds[i]); + } + } +} diff --git a/contracts/interfaces/IBreaker.sol b/contracts/interfaces/IBreaker.sol index a5ef59d4..19b63f30 100644 --- a/contracts/interfaces/IBreaker.sol +++ b/contracts/interfaces/IBreaker.sol @@ -6,12 +6,6 @@ pragma solidity ^0.5.13; * @notice Defines the basic interface for a Breaker */ interface IBreaker { - /** - * @notice Emitted after the cooldownTime has been updated. - * @param newCooldownTime The new cooldownTime of the breaker. - */ - event CooldownTimeUpdated(uint256 newCooldownTime); - /** * @notice Emitted when the sortedOracles address is updated. * @param newSortedOracles The address of the new sortedOracles. @@ -20,10 +14,11 @@ interface IBreaker { /** * @notice Retrieve the cooldown time for the breaker. + * @param rateFeedID The rate feed to get the cooldown for * @return cooldown The amount of time that must pass before the breaker can reset. * @dev when cooldown is 0 auto reset will not be attempted. */ - function getCooldown() external view returns (uint256 cooldown); + function getCooldown(address rateFeedID) external view returns (uint256 cooldown); /** * @notice Check if the criteria have been met, by a specified rateFeedID, to trigger the breaker. @@ -31,7 +26,7 @@ interface IBreaker { * @return triggerBreaker A boolean indicating whether or not the breaker * should be triggered for the given rate feed. */ - function shouldTrigger(address rateFeedID) external view returns (bool triggerBreaker); + function shouldTrigger(address rateFeedID) external returns (bool triggerBreaker); /** * @notice Check if the criteria to automatically reset the breaker have been met. @@ -41,5 +36,5 @@ interface IBreaker { * @dev Allows the definition of additional critera to check before reset. * If no additional criteria is needed set to !shouldTrigger(); */ - function shouldReset(address rateFeedID) external view returns (bool resetBreaker); + function shouldReset(address rateFeedID) external returns (bool resetBreaker); } diff --git a/contracts/interfaces/IBreakerBox.sol b/contracts/interfaces/IBreakerBox.sol index e85b782b..e206af8a 100644 --- a/contracts/interfaces/IBreakerBox.sol +++ b/contracts/interfaces/IBreakerBox.sol @@ -83,7 +83,7 @@ interface IBreakerBox { */ event SortedOraclesUpdated(address indexed newSortedOracles); - /** + /** * @notice Emitted when the breaker is enabled or disabled for a rate feed. * @param breaker The address of the breaker. * @param rateFeedID The address of the rate feed. diff --git a/contracts/interfaces/ISortedOracles.sol b/contracts/interfaces/ISortedOracles.sol index 165084a8..3f2d5954 100644 --- a/contracts/interfaces/ISortedOracles.sol +++ b/contracts/interfaces/ISortedOracles.sol @@ -30,8 +30,6 @@ interface ISortedOracles { function medianTimestamp(address) external view returns (uint256); - function previousMedianRate(address) external view returns (uint256); - function getOracles(address) external view returns (address[] memory); function getTimestamps(address token) diff --git a/lib/celo-foundry b/lib/celo-foundry index 41191a39..aba86156 160000 --- a/lib/celo-foundry +++ b/lib/celo-foundry @@ -1 +1 @@ -Subproject commit 41191a396f52b2bddade50b159a307c52bc9486f +Subproject commit aba8615646acc8c3f3cc83cf13eab1795600c388 diff --git a/test/MedianDeltaBreaker.t.sol b/test/MedianDeltaBreaker.t.sol index b5b91e48..ba3e18cc 100644 --- a/test/MedianDeltaBreaker.t.sol +++ b/test/MedianDeltaBreaker.t.sol @@ -25,15 +25,17 @@ contract MedianDeltaBreakerTest is Test, WithRegistry { MockSortedOracles sortedOracles; MedianDeltaBreaker breaker; - uint256 threshold = 0.15 * 10**24; // 15% - uint256 coolDownTime = 5 minutes; + uint256 defaultThreshold = 0.15 * 10**24; // 15% + uint256 defaultCooldownTime = 5 minutes; address[] rateFeedIDs = new address[](1); uint256[] rateChangeThresholds = new uint256[](1); + uint256[] cooldownTimes = new uint256[](1); - event BreakerTriggered(address indexed rateFeedID1); - event BreakerReset(address indexed rateFeedID1); - event CooldownTimeUpdated(uint256 newCooldownTime); + event BreakerTriggered(address indexed rateFeedID); + event BreakerReset(address indexed rateFeedID); + event DefaultCooldownTimeUpdated(uint256 newCooldownTime); + event CooldownTimeUpdated(address indexed rateFeedID, uint256 newCooldownTime); event DefaultRateChangeThresholdUpdated(uint256 newMinRateChangeThreshold); event SortedOraclesUpdated(address newSortedOracles); event RateChangeThresholdUpdated(address rateFeedID1, uint256 rateChangeThreshold); @@ -47,6 +49,7 @@ contract MedianDeltaBreakerTest is Test, WithRegistry { rateFeedIDs[0] = rateFeedID2; rateChangeThresholds[0] = 0.9 * 10**24; + cooldownTimes[0] = 10 minutes; changePrank(deployer); sortedOracles = new MockSortedOracles(); @@ -56,25 +59,12 @@ contract MedianDeltaBreakerTest is Test, WithRegistry { sortedOracles.addOracle(rateFeedID3, actor("oracleClient1")); breaker = new MedianDeltaBreaker( - coolDownTime, - threshold, + defaultCooldownTime, + defaultThreshold, + ISortedOracles(address(sortedOracles)), rateFeedIDs, rateChangeThresholds, - ISortedOracles(address(sortedOracles)) - ); - } - - function setupSortedOracles(uint256 currentMedianRate, uint256 previousMedianRate) public { - vm.mockCall( - address(sortedOracles), - abi.encodeWithSelector(sortedOracles.previousMedianRate.selector), - abi.encode(previousMedianRate) - ); - - vm.mockCall( - address(sortedOracles), - abi.encodeWithSelector(sortedOracles.medianRate.selector), - abi.encode(currentMedianRate, 1) + cooldownTimes ); } } @@ -86,12 +76,12 @@ contract MedianDeltaBreakerTest_constructorAndSetters is MedianDeltaBreakerTest assertEq(breaker.owner(), deployer); } - function test_constructor_shouldSetCooldownTime() public { - assertEq(breaker.cooldownTime(), coolDownTime); + function test_constructor_shouldSetDefaultCooldownTime() public { + assertEq(breaker.defaultCooldownTime(), defaultCooldownTime); } - function test_constructor_shouldSetRateChangeThreshold() public { - assertEq(breaker.defaultRateChangeThreshold(), threshold); + function test_constructor_shouldSetDefaultRateChangeThreshold() public { + assertEq(breaker.defaultRateChangeThreshold(), defaultThreshold); } function test_constructor_shouldSetSortedOracles() public { @@ -102,22 +92,23 @@ contract MedianDeltaBreakerTest_constructorAndSetters is MedianDeltaBreakerTest assertEq(breaker.rateChangeThreshold(rateFeedIDs[0]), rateChangeThresholds[0]); } + function test_constructor_shouldSetCooldownTimes() public { + assertEq(breaker.getCooldown(rateFeedIDs[0]), cooldownTimes[0]); + } + /* ---------- Setters ---------- */ - function test_setCooldownTime_whenCallerIsNotOwner_shouldRevert() public { + function test_setDefaultCooldownTime_whenCallerIsNotOwner_shouldRevert() public { vm.expectRevert("Ownable: caller is not the owner"); changePrank(notDeployer); - breaker.setCooldownTime(2 minutes); + breaker.setDefaultCooldownTime(2 minutes); } - function test_setCooldownTime_whenCallerIsOwner_shouldUpdateAndEmit() public { + function test_setDefaultCooldownTime_whenCallerIsOwner_shouldUpdateAndEmit() public { uint256 testCooldown = 39 minutes; vm.expectEmit(false, false, false, true); - emit CooldownTimeUpdated(testCooldown); - - breaker.setCooldownTime(testCooldown); - - assertEq(breaker.cooldownTime(), testCooldown); + emit DefaultCooldownTimeUpdated(testCooldown); + breaker.setDefaultCooldownTime(testCooldown); } function test_setRateChangeThreshold_whenCallerIsNotOwner_shouldRevert() public { @@ -128,7 +119,7 @@ contract MedianDeltaBreakerTest_constructorAndSetters is MedianDeltaBreakerTest } function test_setRateChangeThreshold_whenValueGreaterThanOne_shouldRevert() public { - vm.expectRevert("rate change threshold must be less than 1"); + vm.expectRevert("value must be less than 1"); breaker.setDefaultRateChangeThreshold(1 * 10**24); } @@ -173,19 +164,13 @@ contract MedianDeltaBreakerTest_constructorAndSetters is MedianDeltaBreakerTest address[] memory rateFeedIDs2 = new address[](2); rateFeedIDs2[0] = actor("randomRateFeed"); rateFeedIDs2[1] = actor("randomRateFeed2"); - vm.expectRevert("rate feeds and rate change thresholds have to be the same length"); + vm.expectRevert("array length missmatch"); breaker.setRateChangeThresholds(rateFeedIDs2, rateChangeThresholds); } function test_setRateChangeThreshold_whenThresholdIsMoreThan0_shouldRevert() public { rateChangeThresholds[0] = 1 * 10**24; - vm.expectRevert("rate change threshold must be less than 1"); - breaker.setRateChangeThresholds(rateFeedIDs, rateChangeThresholds); - } - - function test_setRateChangeThreshold_whenRateFeedIdDoesNotExist_shouldRevert() public { - rateFeedIDs[0] = actor("randomRateFeed"); - vm.expectRevert("rate feed ID does not exist as it has 0 oracles"); + vm.expectRevert("value must be less than 1"); breaker.setRateChangeThresholds(rateFeedIDs, rateChangeThresholds); } @@ -197,8 +182,12 @@ contract MedianDeltaBreakerTest_constructorAndSetters is MedianDeltaBreakerTest } /* ---------- Getters ---------- */ - function test_getCooldown_shouldReturnCooldown() public { - assertEq(breaker.getCooldown(), coolDownTime); + function test_getCooldown_withDefault_shouldReturnDefaultCooldown() public { + assertEq(breaker.getCooldown(rateFeedID1), defaultCooldownTime); + } + + function test_getCooldown_withoutdefault_shouldReturnSpecificCooldown() public { + assertEq(breaker.getCooldown(rateFeedIDs[0]), cooldownTimes[0]); } } @@ -206,20 +195,19 @@ contract MedianDeltaBreakerTest_shouldTrigger is MedianDeltaBreakerTest { function updateMedianByPercent(uint256 medianChangeScaleFactor, address _rateFeedID) public { uint256 previousMedianRate = 0.98 * 10**24; uint256 currentMedianRate = (previousMedianRate * medianChangeScaleFactor) / 10**24; - setupSortedOracles(currentMedianRate, previousMedianRate); + stdstore.target(address(breaker)).sig(0x7dffc308).with_key(_rateFeedID).checked_write(previousMedianRate); - vm.expectCall( + vm.mockCall( address(sortedOracles), - abi.encodeWithSelector(sortedOracles.previousMedianRate.selector, _rateFeedID) + abi.encodeWithSelector(sortedOracles.medianRate.selector), + abi.encode(currentMedianRate, 1) ); vm.expectCall(address(sortedOracles), abi.encodeWithSelector(sortedOracles.medianRate.selector, _rateFeedID)); } - function test_shouldTrigger_whithDefaultThreshold_shouldTrigger() public { + function test_shouldTrigger_withDefaultThreshold_shouldTrigger() public { assertEq(breaker.rateChangeThreshold(rateFeedID1), 0); - updateMedianByPercent(0.7 * 10**24, rateFeedID1); - assertTrue(breaker.shouldTrigger(rateFeedID1)); } @@ -244,7 +232,6 @@ contract MedianDeltaBreakerTest_shouldTrigger is MedianDeltaBreakerTest { function test_shouldTrigger_whenThresholdIsSmallerThanMedian_ShouldTrigger() public { updateMedianByPercent(1.1 * 10**24, rateFeedID3); - rateChangeThresholds[0] = 0.01 * 10**24; rateFeedIDs[0] = rateFeedID3; breaker.setRateChangeThresholds(rateFeedIDs, rateChangeThresholds); diff --git a/test/SortedOracles.t.sol b/test/SortedOracles.t.sol index 4c0e1f8d..5b1070ce 100644 --- a/test/SortedOracles.t.sol +++ b/test/SortedOracles.t.sol @@ -563,60 +563,4 @@ contract SortedOracles_report is SortedOraclesTest { sortedOracles.report(token, 9999, address(0), address(0)); } - - function test_report_whenMedianChanges_shouldUpdatePreviousMedian() public { - sortedOracles.setBreakerBox(mockBreakerBox); - - // Initially we have no rates, so no prevMedian or currentMedian - uint256 prevMedianBefore = sortedOracles.previousMedianRate(token); - (uint256 currentMedianBefore, ) = sortedOracles.medianRate(token); - assertTrue((prevMedianBefore == 0) && (currentMedianBefore == 0)); - - sortedOracles.addOracle(token, oracle); - sortedOracles.addOracle(token, oracleB); - - changePrank(oracle); - vm.expectEmit(true, false, false, false); - // Actual value doesn't matter, just that it was changed - emit MedianUpdated(token, 0); - sortedOracles.report(token, 9999, address(0), address(0)); - - // Now we have a report, current median is set but prev median should still be 0 - (uint256 currentMedianAfterFirstReport, ) = sortedOracles.medianRate(token); - uint256 prevMedianAfterFirstReport = sortedOracles.previousMedianRate(token); - assertEq(prevMedianAfterFirstReport, 0); - - changePrank(oracleB); - vm.expectEmit(true, false, false, false); - // Actual value doesn't matter, just that it was changed - emit MedianUpdated(token, 0); - sortedOracles.report(token, 23012, oracle, address(0)); - - // Now we have another median changing report, prev median should be the current median before this update - uint256 prevMedianAfter = sortedOracles.previousMedianRate(token); - assertEq(prevMedianAfter, currentMedianAfterFirstReport); - } - - function test_report_whenMedianDoesNotChange_shouldNotUpdatePreviousMedian() public { - test_report_whenMedianChanges_shouldUpdatePreviousMedian(); //¯\_(ツ)_/¯ - - // Get the current median & prev median - (uint256 currentMedianBefore, ) = sortedOracles.medianRate(token); - uint256 prevMedianBefore = sortedOracles.previousMedianRate(token); - - // Submit a report using the current median, so we don't get a change - changePrank(owner); - sortedOracles.addOracle(token, oracleC); - changePrank(oracleC); - sortedOracles.report(token, currentMedianBefore, oracle, address(0)); - - // Check median values are unchanged - (uint256 currentMedianAfter, ) = sortedOracles.medianRate(token); - assertEq(currentMedianBefore, currentMedianAfter); - - uint256 prevMedianAfter = sortedOracles.previousMedianRate(token); - - // Check prev median is unchanged - assertEq(prevMedianAfter, prevMedianBefore); - } } diff --git a/test/ValueDeltaBreaker.t.sol b/test/ValueDeltaBreaker.t.sol new file mode 100644 index 00000000..1b205eb6 --- /dev/null +++ b/test/ValueDeltaBreaker.t.sol @@ -0,0 +1,261 @@ +// SPDX-License-Identifier: UNLICENSED +// solhint-disable func-name-mixedcase, var-name-mixedcase, state-visibility +// solhint-disable const-name-snakecase, max-states-count, contract-name-camelcase +pragma solidity ^0.5.13; +pragma experimental ABIEncoderV2; + +import { Test, console2 as console } from "celo-foundry/Test.sol"; +import { ISortedOracles } from "contracts/interfaces/ISortedOracles.sol"; + +import { WithRegistry } from "./utils/WithRegistry.sol"; +import { SafeMath } from "openzeppelin-solidity/contracts/math/SafeMath.sol"; + +import { SortedLinkedListWithMedian } from "contracts/common/linkedlists/SortedLinkedListWithMedian.sol"; +import { FixidityLib } from "contracts/common/FixidityLib.sol"; + +import { ValueDeltaBreaker } from "contracts/ValueDeltaBreaker.sol"; +import { MockSortedOracles } from "./mocks/MockSortedOracles.sol"; + +contract ValueDeltaBreakerTest is Test, WithRegistry { + using FixidityLib for FixidityLib.Fraction; + + address deployer; + address notDeployer; + + address rateFeedID1; + address rateFeedID2; + address rateFeedID3; + MockSortedOracles sortedOracles; + ValueDeltaBreaker breaker; + + uint256 defaultThreshold = 0.15 * 10**24; // 15% + uint256 defaultCooldownTime = 5 minutes; + + address[] rateFeedIDs = new address[](1); + uint256[] rateChangeThresholds = new uint256[](1); + uint256[] cooldownTimes = new uint256[](1); + + event BreakerTriggered(address indexed rateFeedID); + event BreakerReset(address indexed rateFeedID); + event DefaultCooldownTimeUpdated(uint256 newCooldownTime); + event CooldownTimeUpdated(address indexed rateFeedID, uint256 newCooldownTime); + event DefaultRateChangeThresholdUpdated(uint256 newMinRateChangeThreshold); + event SortedOraclesUpdated(address newSortedOracles); + event RateChangeThresholdUpdated(address rateFeedID1, uint256 rateChangeThreshold); + + function setUp() public { + deployer = actor("deployer"); + notDeployer = actor("notDeployer"); + rateFeedID1 = actor("rateFeedID1"); + rateFeedID2 = actor("rateFeedID2"); + rateFeedID3 = actor("rateFeedID3"); + + rateFeedIDs[0] = rateFeedID2; + rateChangeThresholds[0] = 0.9 * 10**24; + cooldownTimes[0] = 10 minutes; + + changePrank(deployer); + sortedOracles = new MockSortedOracles(); + + sortedOracles.addOracle(rateFeedID1, actor("OracleClient")); + sortedOracles.addOracle(rateFeedID2, actor("oracleClient")); + sortedOracles.addOracle(rateFeedID3, actor("oracleClient1")); + + breaker = new ValueDeltaBreaker( + defaultCooldownTime, + defaultThreshold, + ISortedOracles(address(sortedOracles)), + rateFeedIDs, + rateChangeThresholds, + cooldownTimes + ); + } +} + +contract ValueDeltaBreakerTest_constructorAndSetters is ValueDeltaBreakerTest { + /* ---------- Constructor ---------- */ + + function test_constructor_shouldSetOwner() public { + assertEq(breaker.owner(), deployer); + } + + function test_constructor_shouldSetDefaultCooldownTime() public { + assertEq(breaker.defaultCooldownTime(), defaultCooldownTime); + } + + function test_constructor_shouldSetDefaultRateChangeThreshold() public { + assertEq(breaker.defaultRateChangeThreshold(), defaultThreshold); + } + + function test_constructor_shouldSetSortedOracles() public { + assertEq(address(breaker.sortedOracles()), address(sortedOracles)); + } + + function test_constructor_shouldSetRateChangeThresholds() public { + assertEq(breaker.rateChangeThreshold(rateFeedIDs[0]), rateChangeThresholds[0]); + } + + function test_constructor_shouldSetCooldownTimes() public { + assertEq(breaker.getCooldown(rateFeedIDs[0]), cooldownTimes[0]); + } + + /* ---------- Setters ---------- */ + + function test_setDefaultCooldownTime_whenCallerIsNotOwner_shouldRevert() public { + vm.expectRevert("Ownable: caller is not the owner"); + changePrank(notDeployer); + breaker.setDefaultCooldownTime(2 minutes); + } + + function test_setDefaultCooldownTime_whenCallerIsOwner_shouldUpdateAndEmit() public { + uint256 testCooldown = 39 minutes; + vm.expectEmit(false, false, false, true); + emit DefaultCooldownTimeUpdated(testCooldown); + + breaker.setDefaultCooldownTime(testCooldown); + + assertEq(breaker.defaultCooldownTime(), testCooldown); + } + + function test_setRateChangeThreshold_whenCallerIsNotOwner_shouldRevert() public { + vm.expectRevert("Ownable: caller is not the owner"); + changePrank(notDeployer); + + breaker.setDefaultRateChangeThreshold(123456); + } + + function test_setDefaultRateChangeThreshold_whenValueGreaterThanOne_shouldRevert() public { + vm.expectRevert("value must be less than 1"); + breaker.setDefaultRateChangeThreshold(1 * 10**24); + } + + function test_setDefaultRateChangeThreshold_whenCallerIsOwner_shouldUpdateAndEmit() public { + uint256 testThreshold = 0.1 * 10**24; + vm.expectEmit(false, false, false, true); + emit DefaultRateChangeThresholdUpdated(testThreshold); + + breaker.setDefaultRateChangeThreshold(testThreshold); + + assertEq(breaker.defaultRateChangeThreshold(), testThreshold); + } + + function test_setSortedOracles_whenSenderIsNotOwner_shouldRevert() public { + changePrank(notDeployer); + vm.expectRevert("Ownable: caller is not the owner"); + breaker.setSortedOracles(ISortedOracles(address(0))); + } + + function test_setSortedOracles_whenAddressIsZero_shouldRevert() public { + vm.expectRevert("SortedOracles address must be set"); + breaker.setSortedOracles(ISortedOracles(address(0))); + } + + function test_setSortedOracles_whenSenderIsOwner_shouldUpdateAndEmit() public { + address newSortedOracles = actor("newSortedOracles"); + vm.expectEmit(true, true, true, true); + emit SortedOraclesUpdated(newSortedOracles); + + breaker.setSortedOracles(ISortedOracles(newSortedOracles)); + + assertEq(address(breaker.sortedOracles()), newSortedOracles); + } + + function test_setRateChangeThresholds_whenSenderIsNotOwner_shouldRevert() public { + changePrank(notDeployer); + vm.expectRevert("Ownable: caller is not the owner"); + breaker.setRateChangeThresholds(rateFeedIDs, rateChangeThresholds); + } + + function test_setRateChangeThresholds_whenValuesAreDifferentLengths_shouldRevert() public { + address[] memory rateFeedIDs2 = new address[](2); + rateFeedIDs2[0] = actor("randomRateFeed"); + rateFeedIDs2[1] = actor("randomRateFeed2"); + vm.expectRevert("array length missmatch"); + breaker.setRateChangeThresholds(rateFeedIDs2, rateChangeThresholds); + } + + function test_setRateChangeThresholds_whenThresholdIsMoreThan0_shouldRevert() public { + rateChangeThresholds[0] = 1 * 10**24; + vm.expectRevert("value must be less than 1"); + breaker.setRateChangeThresholds(rateFeedIDs, rateChangeThresholds); + } + + function test_setRateChangeThresholds_whenSenderIsOwner_shouldUpdateAndEmit() public { + vm.expectEmit(true, true, true, true); + emit RateChangeThresholdUpdated(rateFeedIDs[0], rateChangeThresholds[0]); + breaker.setRateChangeThresholds(rateFeedIDs, rateChangeThresholds); + assertEq(breaker.rateChangeThreshold(rateFeedIDs[0]), rateChangeThresholds[0]); + } + + /* ---------- Getters ---------- */ + function test_getCooldown_withDefault_shouldReturnDefaultCooldown() public { + assertEq(breaker.getCooldown(rateFeedID1), defaultCooldownTime); + } + + function test_getCooldown_withoutdefault_shouldReturnSpecificCooldown() public { + assertEq(breaker.getCooldown(rateFeedIDs[0]), cooldownTimes[0]); + } +} + +contract ValueDeltaBreakerTest_shouldTrigger is ValueDeltaBreakerTest { + function setUp() public { + super.setUp(); + + address[] memory _rateFeedIDs = new address[](3); + uint256[] memory _referenceValues = new uint256[](3); + _rateFeedIDs[0] = rateFeedID1; + _rateFeedIDs[1] = rateFeedID2; + _rateFeedIDs[2] = rateFeedID3; + _referenceValues[0] = FixidityLib.fixed1().unwrap(); + _referenceValues[1] = FixidityLib.fixed1().unwrap(); + _referenceValues[2] = FixidityLib.fixed1().unwrap(); + + breaker.setReferenceValues(_rateFeedIDs, _referenceValues); + } + + function updateMedianByPercent(uint256 medianChangeScaleFactor, address _rateFeedID) public { + uint256 previousMedianRate = 10**24; + uint256 currentMedianRate = (previousMedianRate * medianChangeScaleFactor) / 10**24; + vm.mockCall( + address(sortedOracles), + abi.encodeWithSelector(sortedOracles.medianRate.selector), + abi.encode(currentMedianRate, 1) + ); + vm.expectCall(address(sortedOracles), abi.encodeWithSelector(sortedOracles.medianRate.selector, _rateFeedID)); + } + + function test_shouldTrigger_withDefaultThreshold_shouldTrigger() public { + assertEq(breaker.rateChangeThreshold(rateFeedID1), 0); + updateMedianByPercent(0.7 * 10**24, rateFeedID1); + assertTrue(breaker.shouldTrigger(rateFeedID1)); + } + + function test_shouldTrigger_whenThresholdIsLargerThanMedian_shouldNotTrigger() public { + updateMedianByPercent(0.7 * 10**24, rateFeedID1); + + rateChangeThresholds[0] = 0.8 * 10**24; + rateFeedIDs[0] = rateFeedID1; + breaker.setRateChangeThresholds(rateFeedIDs, rateChangeThresholds); + assertEq(breaker.rateChangeThreshold(rateFeedID1), rateChangeThresholds[0]); + + assertFalse(breaker.shouldTrigger(rateFeedID1)); + } + + function test_shouldTrigger_whithDefaultThreshold_ShouldNotTrigger() public { + assertEq(breaker.rateChangeThreshold(rateFeedID3), 0); + + updateMedianByPercent(1.1 * 10**24, rateFeedID3); + + assertFalse(breaker.shouldTrigger(rateFeedID3)); + } + + function test_shouldTrigger_whenThresholdIsSmallerThanMedian_ShouldTrigger() public { + updateMedianByPercent(1.1 * 10**24, rateFeedID3); + rateChangeThresholds[0] = 0.01 * 10**24; + rateFeedIDs[0] = rateFeedID3; + breaker.setRateChangeThresholds(rateFeedIDs, rateChangeThresholds); + assertEq(breaker.rateChangeThreshold(rateFeedID3), rateChangeThresholds[0]); + + assertTrue(breaker.shouldTrigger(rateFeedID3)); + } +} diff --git a/test/common/breakers/WithCooldown.t.sol b/test/common/breakers/WithCooldown.t.sol new file mode 100644 index 00000000..463bc6be --- /dev/null +++ b/test/common/breakers/WithCooldown.t.sol @@ -0,0 +1,77 @@ +// SPDX-License-Identifier: UNLICENSED +// solhint-disable func-name-mixedcase, var-name-mixedcase, state-visibility +// solhint-disable const-name-snakecase, max-states-count, contract-name-camelcase +pragma solidity ^0.5.13; + +import { Test, console2 as console } from "celo-foundry/Test.sol"; +import { WithCooldown } from "contracts/common/breakers/WithCooldown.sol"; + +contract WithCooldownTest is WithCooldown, Test { + function test_setDefaultCooldownTime() public { + uint256 testCooldown = 39 minutes; + vm.expectEmit(true, true, true, true); + emit DefaultCooldownTimeUpdated(testCooldown); + _setDefaultCooldownTime(testCooldown); + assertEq(defaultCooldownTime, testCooldown); + } + + function test_setCooldownTimes_withZeroAddress_reverts() public { + address[] memory rateFeedIDs = new address[](1); + uint256[] memory cooldownTimes = new uint256[](1); + + vm.expectRevert("rate feed invalid"); + _setCooldownTimes(rateFeedIDs, cooldownTimes); + } + + function test_setCooldownTimes_withMismatchingArrays_reverts() public { + address[] memory rateFeedIDs = new address[](2); + uint256[] memory cooldownTimes = new uint256[](1); + + vm.expectRevert("array length missmatch"); + _setCooldownTimes(rateFeedIDs, cooldownTimes); + } + + function test_setCoolDownTimes_emitsEvents() public { + address[] memory rateFeedIDs = new address[](2); + uint256[] memory cooldownTimes = new uint256[](2); + + rateFeedIDs[0] = address(1111); + rateFeedIDs[1] = address(2222); + cooldownTimes[0] = 1; + cooldownTimes[1] = 2; + + vm.expectEmit(true, true, true, true); + emit RateFeedCooldownTimeUpdated(rateFeedIDs[0], cooldownTimes[0]); + vm.expectEmit(true, true, true, true); + emit RateFeedCooldownTimeUpdated(rateFeedIDs[1], cooldownTimes[1]); + + _setCooldownTimes(rateFeedIDs, cooldownTimes); + + assertEq(rateFeedCooldownTime[rateFeedIDs[0]], cooldownTimes[0]); + assertEq(rateFeedCooldownTime[rateFeedIDs[1]], cooldownTimes[1]); + } + + function test_getCooldown_whenNoRateFeedSpecific_usesDefault() public { + uint256 testCooldown = 39 minutes; + address rateFeedID = address(1111); + _setDefaultCooldownTime(testCooldown); + + assertEq(getCooldown(rateFeedID), testCooldown); + } + + function test_getCooldown_whenRateFeedSpecific_usesRateSpecific() public { + uint256 testCooldown = 39 minutes; + uint256 defaultCooldown = 10 minutes; + address rateFeedID = address(1111); + + address[] memory rateFeedIDs = new address[](1); + rateFeedIDs[0] = rateFeedID; + uint256[] memory cooldownTimes = new uint256[](1); + cooldownTimes[0] = testCooldown; + + _setCooldownTimes(rateFeedIDs, cooldownTimes); + _setDefaultCooldownTime(defaultCooldown); + + assertEq(getCooldown(rateFeedID), testCooldown); + } +} \ No newline at end of file diff --git a/test/common/breakers/WithThreshold.t.sol b/test/common/breakers/WithThreshold.t.sol new file mode 100644 index 00000000..23d132c1 --- /dev/null +++ b/test/common/breakers/WithThreshold.t.sol @@ -0,0 +1,97 @@ +// SPDX-License-Identifier: UNLICENSED +// solhint-disable func-name-mixedcase, var-name-mixedcase, state-visibility +// solhint-disable const-name-snakecase, max-states-count, contract-name-camelcase +pragma solidity ^0.5.13; + +import { Test, console2 as console } from "celo-foundry/Test.sol"; +import { WithThreshold } from "contracts/common/breakers/WithThreshold.sol"; + +contract WithThresholdTest is WithThreshold, Test { + function test_setDefaultRateChangeThreshold() public { + uint256 testThreshold = 1e20; + vm.expectEmit(true, true, true, true); + emit DefaultRateChangeThresholdUpdated(testThreshold); + _setDefaultRateChangeThreshold(testThreshold); + assertEq(defaultRateChangeThreshold.unwrap(), testThreshold); + } + + function test_setRateChangeThresholds_withZeroAddress_reverts() public { + address[] memory rateFeedIDs = new address[](1); + uint256[] memory thresholds = new uint256[](1); + + vm.expectRevert("rate feed invalid"); + _setRateChangeThresholds(rateFeedIDs, thresholds); + } + + function test_setRateChangeThresholds_withMismatchingArrays_reverts() public { + address[] memory rateFeedIDs = new address[](2); + uint256[] memory thresholds = new uint256[](1); + + vm.expectRevert("array length missmatch"); + _setRateChangeThresholds(rateFeedIDs, thresholds); + } + + function test_setRateChangeThresholds_emitsEvents() public { + address[] memory rateFeedIDs = new address[](2); + uint256[] memory thresholds = new uint256[](2); + + rateFeedIDs[0] = address(1111); + rateFeedIDs[1] = address(2222); + thresholds[0] = 1e20; + thresholds[1] = 2e20; + + vm.expectEmit(true, true, true, true); + emit RateChangeThresholdUpdated(rateFeedIDs[0], thresholds[0]); + vm.expectEmit(true, true, true, true); + emit RateChangeThresholdUpdated(rateFeedIDs[1], thresholds[1]); + + _setRateChangeThresholds(rateFeedIDs, thresholds); + + assertEq(rateChangeThreshold[rateFeedIDs[0]].unwrap(), thresholds[0]); + assertEq(rateChangeThreshold[rateFeedIDs[1]].unwrap(), thresholds[1]); + } +} + +contract WithThresholdTest_exceedsThreshold is WithThresholdTest { + uint256 constant _1PC = 0.01 * 1e24; // 1% + uint256 constant _10PC = 0.1 * 1e24; // 10% + uint256 constant _20PC = 0.2 * 1e24; // 20% + + address rateFeedID0 = actor("rateFeedID0-10%"); + address rateFeedID1 = actor("rateFeedID2-1%"); + address rateFeedID2 = actor("rateFeedID3-default-20%"); + + function setUp() public { + uint256[] memory ts = new uint256[](2); + ts[0] = _10PC; + ts[1] = _1PC; + address[] memory rateFeedIDs = new address[](2); + rateFeedIDs[0] = rateFeedID0; + rateFeedIDs[1] = rateFeedID1; + + _setDefaultRateChangeThreshold(_20PC); + _setRateChangeThresholds(rateFeedIDs, ts); + } + + function test_exceedsThreshold_withDefault_whenWithin_isFalse() public { + assertEq(exceedsThreshold(1e24, 1.1 * 1e24, rateFeedID2), false); + assertEq(exceedsThreshold(1e24, 0.9 * 1e24, rateFeedID2), false); + } + + function test_exceedsThreshold_withDefault_whenNotWithin_isTrue() public { + assertEq(exceedsThreshold(1e24, 1.3 * 1e24, rateFeedID2), true); + assertEq(exceedsThreshold(1e24, 0.7 * 1e24, rateFeedID2), true); + } + + function test_exceedsThreshold_withOverride_whenWithin_isTrue() public { + assertEq(exceedsThreshold(1e24, 1.1 * 1e24, rateFeedID1), true); + assertEq(exceedsThreshold(1e24, 0.9 * 1e24, rateFeedID1), true); + assertEq(exceedsThreshold(1e24, 1.11 * 1e24, rateFeedID0), true); + assertEq(exceedsThreshold(1e24, 0.89 * 1e24, rateFeedID0), true); + } + + function test_exceedsThreshold_withOverride_whenNotWithin_isFalse() public { + assertEq(exceedsThreshold(1e24, 1.01 * 1e24, rateFeedID1), false); + assertEq(exceedsThreshold(1e24, 1.01 * 1e24, rateFeedID0), false); + } +} \ No newline at end of file diff --git a/test/mocks/MockBreaker.sol b/test/mocks/MockBreaker.sol index 011bc2ad..09cc33b4 100644 --- a/test/mocks/MockBreaker.sol +++ b/test/mocks/MockBreaker.sol @@ -17,7 +17,7 @@ contract MockBreaker { reset = _reset; } - function getCooldown() external view returns (uint256) { + function getCooldown(address) external view returns (uint256) { return cooldown; } diff --git a/test/mocks/MockReserve.sol b/test/mocks/MockReserve.sol index 569c1112..18875c4f 100644 --- a/test/mocks/MockReserve.sol +++ b/test/mocks/MockReserve.sol @@ -40,9 +40,11 @@ contract MockReserve { return true; } - function transferExchangeCollateralAsset( address tokenAddress, + function transferExchangeCollateralAsset( + address tokenAddress, address payable to, - uint256 amount) external returns (bool) { + uint256 amount + ) external returns (bool) { require(IERC20(tokenAddress).transfer(to, amount), "asset transfer failed"); return true; } diff --git a/test/utils/IntegrationSetup.sol b/test/utils/IntegrationSetup.sol index 8def0ff4..4a4b1e33 100644 --- a/test/utils/IntegrationSetup.sol +++ b/test/utils/IntegrationSetup.sol @@ -241,6 +241,7 @@ contract IntegrationSetup is Test, WithRegistry { // todo change these to correct values uint256[] memory rateChangeThresholds = new uint256[](5); + uint256[] memory cooldownTimes = new uint256[](5); rateChangeThresholds[0] = 0.15 * 10**24; rateChangeThresholds[1] = 0.14 * 10**24; @@ -252,11 +253,12 @@ contract IntegrationSetup is Test, WithRegistry { uint256 coolDownTime = 5 minutes; medianDeltaBreaker = new MedianDeltaBreaker( - coolDownTime, - threshold, + coolDownTime, + threshold, + ISortedOracles(address(sortedOracles)), rateFeedIDs, rateChangeThresholds, - ISortedOracles(address(sortedOracles)) + cooldownTimes ); breakerBox.addBreaker(address(medianDeltaBreaker), 1); diff --git a/tsconfig.json b/tsconfig.json index a1fe9c6c..503de332 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -8,4 +8,4 @@ }, "include": ["src"], "exclude": ["node_modules"] -} \ No newline at end of file +}