diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index ed64309..8adf798 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -8,7 +8,6 @@ on: pull_request: env: - FOUNDRY_PROFILE: ci ETH_RPC_URL: ${{ secrets.ETH_RPC_URL }} jobs: diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 7f231e2..0e580b1 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -7,9 +7,6 @@ on: - develop pull_request: -env: - FOUNDRY_PROFILE: ci - jobs: test: strategy: diff --git a/script/DeploySavingsAf.s.sol b/script/DeploySavingsAf.s.sol index 402b434..a460e5c 100644 --- a/script/DeploySavingsAf.s.sol +++ b/script/DeploySavingsAf.s.sol @@ -15,12 +15,8 @@ import "forge-std/Script.sol"; contract DeploySavingsAf is Script { - // @todo -- set oracles! - address[] public strategies; - uint256 private collateralChainlinkPriceOracleHeartbeat = 86_400; // 24 hours // @todo -- set correct heartbeat! - string private constant NAME = "Savings USDaf"; string private constant SYMBOL = "sUSDaf"; @@ -43,8 +39,6 @@ contract DeploySavingsAf is Script { address _ysyBOLDStrategy = STRATEGY_FACTORY.newStrategy( address(0x3414bd84dfF0900a9046a987f4dF2e0eF08Fa1ce), // ysyBOLD Address Registry ASSET, - address(0), - collateralChainlinkPriceOracleHeartbeat, "USDaf ysyBOLD Stability Pool" ); strategies.push(_ysyBOLDStrategy); @@ -52,8 +46,6 @@ contract DeploySavingsAf is Script { address _scrvUSDStrategy = STRATEGY_FACTORY.newStrategy( address(0x0C7B6C6a60ae2016199d393695667c1482719C82), // scrvUSD Address Registry ASSET, - address(0), - collateralChainlinkPriceOracleHeartbeat, "USDaf scrvUSD Stability Pool" ); strategies.push(_scrvUSDStrategy); @@ -61,8 +53,6 @@ contract DeploySavingsAf is Script { address _sUSDSStrategy = STRATEGY_FACTORY.newStrategy( address(0x330A0fDfc1818Be022FEDCE96A041293E16dc6d1), // sUSDS Address Registry ASSET, - address(0), - collateralChainlinkPriceOracleHeartbeat, "USDaf sUSDS Stability Pool" ); strategies.push(_sUSDSStrategy); @@ -70,8 +60,6 @@ contract DeploySavingsAf is Script { address _sfrxUSDStrategy = STRATEGY_FACTORY.newStrategy( address(0x0ad1C302203F0fbB6Ca34641BDFeF0Bf4182377c), // sfrxUSD Address Registry ASSET, - address(0), - collateralChainlinkPriceOracleHeartbeat, "USDaf sfrxUSD Stability Pool" ); strategies.push(_sfrxUSDStrategy); @@ -79,8 +67,6 @@ contract DeploySavingsAf is Script { address _tBTCStrategy = STRATEGY_FACTORY.newStrategy( address(0xbd9f75471990041A3e7C22872c814A273485E999), // tBTC Address Registry ASSET, - address(0), - collateralChainlinkPriceOracleHeartbeat, "USDaf tBTC Stability Pool" ); strategies.push(_tBTCStrategy); @@ -88,8 +74,6 @@ contract DeploySavingsAf is Script { address _WBTC18Strategy = STRATEGY_FACTORY.newStrategy( address(0x2C5A85a3fd181857D02baff169D1e1cB220ead6d), // WBTC18 Address Registry ASSET, - address(0), - collateralChainlinkPriceOracleHeartbeat, "USDaf WBTC18 Stability Pool" ); strategies.push(_WBTC18Strategy); diff --git a/script/DeployStrategies.s.sol b/script/DeployStrategies.s.sol index 2862012..45a144f 100644 --- a/script/DeployStrategies.s.sol +++ b/script/DeployStrategies.s.sol @@ -10,10 +10,6 @@ import "forge-std/Script.sol"; contract DeployStrategies is Script { - // @todo -- set oracles! - - uint256 public collateralChainlinkPriceOracleHeartbeat = 86_400; // 24 hours // @todo -- set correct heartbeat! - address public constant ASSET = 0x6440f144b7e50D6a8439336510312d2F54beB01D; // BOLD StrategyFactory public constant STRATEGY_FACTORY = StrategyFactory(0x73dfCc4fB90E6e252E5D41f6588534a8043dBa58); @@ -22,31 +18,16 @@ contract DeployStrategies is Script { vm.startBroadcast(vm.envUint("DEPLOYER_PRIVATE_KEY")); address _wethAddressesRegistry = address(0x20F7C9ad66983F6523a0881d0f82406541417526); // WETH Address Registry - address _wethStrategy = STRATEGY_FACTORY.newStrategy( - _wethAddressesRegistry, - ASSET, - address(0), - collateralChainlinkPriceOracleHeartbeat, - "Liquity V2 WETH Stability Pool" - ); + address _wethStrategy = + STRATEGY_FACTORY.newStrategy(_wethAddressesRegistry, ASSET, "Liquity V2 WETH Stability Pool"); address _wstethAddressesRegistry = address(0x8d733F7ea7c23Cbea7C613B6eBd845d46d3aAc54); // wstETH Address Registry - address _wstethStrategy = STRATEGY_FACTORY.newStrategy( - _wstethAddressesRegistry, - ASSET, - address(0), - collateralChainlinkPriceOracleHeartbeat, - "Liquity V2 wstETH Stability Pool" - ); + address _wstethStrategy = + STRATEGY_FACTORY.newStrategy(_wstethAddressesRegistry, ASSET, "Liquity V2 wstETH Stability Pool"); address _rethAddressesRegistry = address(0x6106046F031a22713697e04C08B330dDaf3e8789); // rETH Address Registry - address _rethStrategy = STRATEGY_FACTORY.newStrategy( - _rethAddressesRegistry, - ASSET, - address(0), - collateralChainlinkPriceOracleHeartbeat, - "Liquity V2 rETH Stability Pool" - ); + address _rethStrategy = + STRATEGY_FACTORY.newStrategy(_rethAddressesRegistry, ASSET, "Liquity V2 rETH Stability Pool"); vm.stopBroadcast(); diff --git a/src/Strategy.sol b/src/Strategy.sol index b4d2994..9373aa0 100644 --- a/src/Strategy.sol +++ b/src/Strategy.sol @@ -5,7 +5,6 @@ import {BaseHealthCheck, BaseStrategy, ERC20} from "@periphery/Bases/HealthCheck import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; -import {AggregatorV3Interface} from "./interfaces/AggregatorV3Interface.sol"; import {IPriceFeed} from "./interfaces/IPriceFeed.sol"; import {IStabilityPool} from "./interfaces/IStabilityPool.sol"; import {IAuction} from "./interfaces/IAuction.sol"; @@ -49,15 +48,9 @@ contract LiquityV2SPStrategy is BaseHealthCheck { /// @notice WAD constant uint256 private constant WAD = 1e18; - /// @notice Multiplier to scale 8-decimal Chainlink price to 18 decimals - uint256 private constant CHAINLINK_TO_WAD = 1e10; - /// @notice Auction starting price buffer percentage increase when the oracle is down uint256 public constant ORACLE_DOWN_BUFFER_PCT_MULTIPLIER = 200; // 200x - /// @notice Auction starting price buffer percentage increase when the auction price is too low - uint256 public constant AUCTION_PRICE_TOO_LOW_BUFFER_PCT_MULTIPLIER = 50; // 50x - /// @notice Minimum buffer percentage for the auction starting price uint256 public constant MIN_BUFFER_PERCENTAGE = WAD + 15e16; // 15% @@ -71,10 +64,6 @@ contract LiquityV2SPStrategy is BaseHealthCheck { /// considered worth depositing during harvests uint256 public constant MIN_DUST_THRESHOLD = 1e15; - /// @notice Heartbeat of the `CHAINLINK_ORACLE` - /// @dev We will skip the `_isAuctionPriceTooLow` check if the oracle is stale - uint256 public immutable CHAINLINK_ORACLE_HEARTBEAT; - /// @notice Asset dust threshold. We will not bother depositing amounts below this value on harvests uint256 public constant ASSET_DUST_THRESHOLD = MIN_DUST_THRESHOLD; @@ -85,11 +74,6 @@ contract LiquityV2SPStrategy is BaseHealthCheck { /// @dev Assumes the price feed is using 18 decimals IPriceFeed public immutable COLL_PRICE_ORACLE; - /// @notice Direct reference to the underlying Chainlink price feed used by `COLL_PRICE_ORACLE` - /// @dev This feed provides a read-only price for the collateral asset without - /// performing Liquity's internal validity or heartbeat checks - AggregatorV3Interface public immutable CHAINLINK_ORACLE; - /// @notice Stability Pool contract IStabilityPool public immutable SP; @@ -103,30 +87,23 @@ contract LiquityV2SPStrategy is BaseHealthCheck { /// @param _addressesRegistry Address of the AddressesRegistry /// @param _asset Address of the strategy's underlying asset /// @param _auctionFactory Address of the AuctionFactory - /// @param _oracle Address of the COLL/USD _read_ price oracle - /// @param _oracleHeartbeat Heartbeat of the `_oracle` /// @param _name Name of the strategy constructor( address _addressesRegistry, address _asset, address _auctionFactory, - address _oracle, - uint256 _oracleHeartbeat, string memory _name ) BaseHealthCheck(_asset, _name) { COLL_PRICE_ORACLE = IAddressesRegistry(_addressesRegistry).priceFeed(); (, bool _isOracleDown) = COLL_PRICE_ORACLE.fetchPrice(); require(!_isOracleDown && COLL_PRICE_ORACLE.priceSource() == IPriceFeed.PriceSource.primary, "!oracle"); - CHAINLINK_ORACLE = AggregatorV3Interface(_oracle); - require(CHAINLINK_ORACLE.decimals() == 8, "!decimals"); - CHAINLINK_ORACLE_HEARTBEAT = _oracleHeartbeat; - SP = IAddressesRegistry(_addressesRegistry).stabilityPool(); require(SP.boldToken() == _asset, "!sp"); COLL = ERC20(SP.collToken()); AUCTION = IAuctionFactory(_auctionFactory).createNewAuction(_asset); + AUCTION.setGovernanceOnlyKick(true); AUCTION.enable(address(COLL)); minAuctionPriceBps = 9_500; // 95% @@ -274,6 +251,9 @@ contract LiquityV2SPStrategy is BaseHealthCheck { /// @inheritdoc BaseStrategy function _harvestAndReport() internal override returns (uint256 _totalAssets) { if (!TokenizedStrategy.isShutdown()) { + uint256 _toClaim = SP.getDepositorYieldGain(address(this)); + if (_toClaim > ASSET_DUST_THRESHOLD) claim(); + uint256 _toDeploy = asset.balanceOf(address(this)); if (_toDeploy > ASSET_DUST_THRESHOLD) _deployFunds(_toDeploy); } @@ -294,12 +274,6 @@ contract LiquityV2SPStrategy is BaseHealthCheck { ) internal override { if (isCollateralGainToClaim()) claim(); - // If auction price is below our acceptable threshold, we will start a new one with an increased starting price - // such that there's a larger delay before we reach our minimum acceptable price again - bool _isPriceTooLow = false; - if (_isAuctionPriceTooLow()) _isPriceTooLow = true; - - // If there's an active auction, sweep if needed, and settle if (AUCTION.isActive(address(COLL))) { if (AUCTION.available(address(COLL)) > 0) AUCTION.sweep(address(COLL)); AUCTION.settle(address(COLL)); @@ -312,12 +286,11 @@ contract LiquityV2SPStrategy is BaseHealthCheck { uint256 _bufferPercentage = bufferPercentage; if (_isOracleDown || COLL_PRICE_ORACLE.priceSource() != IPriceFeed.PriceSource.primary) { _bufferPercentage *= ORACLE_DOWN_BUFFER_PCT_MULTIPLIER; - } else if (_isPriceTooLow) { - _bufferPercentage *= AUCTION_PRICE_TOO_LOW_BUFFER_PCT_MULTIPLIER; } // slither-disable-next-line divide-before-multiply - AUCTION.setStartingPrice(_available * _price / WAD * _bufferPercentage / WAD / WAD); // Reverts if there's an active auction + AUCTION.setStartingPrice(_available * _price / WAD * _bufferPercentage / WAD / WAD); + AUCTION.setMinimumPrice(_price * minAuctionPriceBps / MAX_BPS); COLL.safeTransfer(address(AUCTION), _toAuction); AUCTION.kick(address(COLL)); @@ -329,15 +302,9 @@ contract LiquityV2SPStrategy is BaseHealthCheck { // =============================================================== /// @inheritdoc BaseStrategy - /// @dev Tend is triggered if either: - /// - we need to stop an unhealthy auction, OR - /// - basefee <= maxGasPriceToTend and we have gains or collateral to auction function _tendTrigger() internal view override returns (bool) { if (TokenizedStrategy.totalAssets() == 0) return false; - // Check if active auction price is below our acceptable threshold - if (_isAuctionPriceTooLow()) return true; - // If active auction, wait if (AUCTION.available(address(COLL)) > dustThreshold) return false; @@ -349,47 +316,4 @@ contract LiquityV2SPStrategy is BaseHealthCheck { return block.basefee <= maxGasPriceToTend && (isCollateralGainToClaim() || _available > dustThreshold); } - /// @notice Used to trigger emergency stopping of unhealthy auctions - /// @dev Returns true if there is an active auction and its price is - /// below our minimum acceptable price - function _isAuctionPriceTooLow() internal view returns (bool) { - uint256 _minAuctionPriceBps = minAuctionPriceBps; - - // If zero, the check is disabled - if (_minAuctionPriceBps == 0) return false; - - // If no active auction, return false - if (AUCTION.available(address(COLL)) <= dustThreshold) return false; - - // Get the current market price of the collateral from Chainlink - (, int256 _answer,, uint256 _updatedAt,) = CHAINLINK_ORACLE.latestRoundData(); - - // If the oracle is stale, return false - if (_isStale(_answer, _updatedAt)) return false; - - // Scale the answer to 18 decimals - uint256 _marketPrice = uint256(_answer) * CHAINLINK_TO_WAD; - - // Our minimum acceptable price (some % below market price) - uint256 _minPrice = _marketPrice * _minAuctionPriceBps / MAX_BPS; - - // Price per unit of collateral required by the auction - uint256 _auctionPrice = AUCTION.price(address(COLL)); - - // Return true if auction price is below our minimum acceptable price - return _auctionPrice < _minPrice; - } - - /// @notice Check if the Chainlink oracle is stale - /// @param _answer Latest answer from the oracle - /// @param _updatedAt Timestamp of the latest answer from the oracle - /// @return True if the oracle is stale - function _isStale( - int256 _answer, - uint256 _updatedAt - ) internal view virtual returns (bool) { - bool _stale = _updatedAt + CHAINLINK_ORACLE_HEARTBEAT <= block.timestamp; - return _stale || _answer <= 0; - } - } diff --git a/src/StrategyFactory.sol b/src/StrategyFactory.sol index cc4c8bf..29ef4fa 100644 --- a/src/StrategyFactory.sol +++ b/src/StrategyFactory.sol @@ -40,21 +40,16 @@ contract StrategyFactory { * @notice Deploy a new Strategy. * @param _addressesRegistry The address of the AddressesRegistry. * @param _asset The underlying asset for the strategy to use. - * @param _oracle Address of the collateral's COLL/USD _read_ price oracle. - * @param _oracleHeartbeat Heartbeat of the `_oracle` * @return . The address of the new strategy. */ function newStrategy( address _addressesRegistry, address _asset, - address _oracle, - uint256 _oracleHeartbeat, string calldata _name ) external virtual onlyManagement returns (address) { // tokenized strategies available setters. - IStrategyInterface _newStrategy = IStrategyInterface( - address(new Strategy(_addressesRegistry, _asset, auctionFactory, _oracle, _oracleHeartbeat, _name)) - ); + IStrategyInterface _newStrategy = + IStrategyInterface(address(new Strategy(_addressesRegistry, _asset, auctionFactory, _name))); _newStrategy.setPerformanceFeeRecipient(performanceFeeRecipient); diff --git a/src/interfaces/IAuction.sol b/src/interfaces/IAuction.sol index 0b7b00f..966065f 100644 --- a/src/interfaces/IAuction.sol +++ b/src/interfaces/IAuction.sol @@ -13,10 +13,17 @@ interface IAuction { function price( address _from ) external view returns (uint256); + function minimumPrice() external view returns (uint256); function startingPrice() external view returns (uint256); function kick( address _token ) external returns (uint256); + function setGovernanceOnlyKick( + bool _governanceOnlyKick + ) external; + function setMinimumPrice( + uint256 _minimumPrice + ) external; function setStartingPrice( uint256 _startingPrice ) external; diff --git a/src/interfaces/IPriceFeed.sol b/src/interfaces/IPriceFeed.sol index a05e4ed..2ea23e1 100644 --- a/src/interfaces/IPriceFeed.sol +++ b/src/interfaces/IPriceFeed.sol @@ -1,8 +1,6 @@ // SPDX-License-Identifier: AGPL-3.0 pragma solidity 0.8.23; -import {AggregatorV3Interface} from "./AggregatorV3Interface.sol"; - interface IPriceFeed { enum PriceSource { diff --git a/src/interfaces/IStrategyInterface.sol b/src/interfaces/IStrategyInterface.sol index 729284e..f0278a9 100644 --- a/src/interfaces/IStrategyInterface.sol +++ b/src/interfaces/IStrategyInterface.sol @@ -17,13 +17,11 @@ interface IStrategyInterface is IBaseHealthCheck { address _address ) external view returns (bool); function ORACLE_DOWN_BUFFER_PCT_MULTIPLIER() external view returns (uint256); - function AUCTION_PRICE_TOO_LOW_BUFFER_PCT_MULTIPLIER() external view returns (uint256); function MIN_BUFFER_PERCENTAGE() external view returns (uint256); function MIN_MAX_GAS_PRICE_TO_TEND() external view returns (uint256); function MIN_DUST_THRESHOLD() external view returns (uint256); function COLL() external view returns (address); function COLL_PRICE_ORACLE() external view returns (address); - function CHAINLINK_ORACLE() external view returns (address); function SP() external view returns (address); function AUCTION() external view returns (address); diff --git a/src/interfaces/IWSTETH.sol b/src/interfaces/IWSTETH.sol deleted file mode 100644 index 78c376a..0000000 --- a/src/interfaces/IWSTETH.sol +++ /dev/null @@ -1,9 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0 -pragma solidity 0.8.23; - -interface IWSTETH { - - function stEthPerToken() external view returns (uint256); - function decimals() external view returns (uint8); - -} diff --git a/src/periphery/Oracles/BaseOracle.sol b/src/periphery/Oracles/BaseOracle.sol deleted file mode 100644 index 3174552..0000000 --- a/src/periphery/Oracles/BaseOracle.sol +++ /dev/null @@ -1,63 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0 -pragma solidity 0.8.23; - -import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; - -import {AggregatorV3Interface} from "../../interfaces/AggregatorV3Interface.sol"; - -contract BaseOracle { - - // =============================================================== - // Storage - // =============================================================== - - string public description; - - // =============================================================== - // Constants - // =============================================================== - - int256 internal constant _1E8 = 1e8; - int256 internal constant _1E10 = 1e10; - uint256 internal constant _48_HOURS = 172800; - uint256 internal constant _24_HOURS = 86400; - - // =============================================================== - // Constructor - // =============================================================== - - constructor( - string memory _description - ) { - description = _description; - } - - // =============================================================== - // View functions - // =============================================================== - - function decimals() public pure virtual returns (uint8) { - return 8; - } - - function latestRoundData() - external - view - virtual - returns (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound) - {} - - // =============================================================== - // Internal functions - // =============================================================== - - function _isStale( - int256 answer, - uint256 updatedAt, - uint256 heartbeat - ) internal view virtual returns (bool) { - bool stale = updatedAt + heartbeat <= block.timestamp; - return stale || answer <= 0; - } - -} diff --git a/src/periphery/Oracles/rETHOracle.sol b/src/periphery/Oracles/rETHOracle.sol deleted file mode 100644 index fab0e6b..0000000 --- a/src/periphery/Oracles/rETHOracle.sol +++ /dev/null @@ -1,64 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0 -pragma solidity 0.8.23; - -import {AggregatorV3Interface, BaseOracle, Math} from "./BaseOracle.sol"; - -contract rETHOracle is BaseOracle { - - // =============================================================== - // Constants - // =============================================================== - - /// @notice rETH/ETH Chainlink oracle heartbeat - /// @dev 86400 seconds according to chainlink docs, we double it - uint256 public constant RETH_ETH_HEARTBEAT = _48_HOURS; - - /// @notice ETH/USD Chainlink oracle heartbeat - /// @dev 3600 seconds according to chainlink docs, we use 24 hours - uint256 public constant ETH_USD_HEARTBEAT = _24_HOURS; - - /// @notice rETH/ETH Chainlink oracle - AggregatorV3Interface public constant RETH_ETH_ORACLE = - AggregatorV3Interface(0x536218f9E9Eb48863970252233c8F271f554C2d0); - - /// @notice ETH/USD Chainlink oracle - AggregatorV3Interface public constant ETH_USD_ORACLE = - AggregatorV3Interface(0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419); - - // =============================================================== - // Constructor - // =============================================================== - - constructor() BaseOracle("rETH / USD") { - require(RETH_ETH_ORACLE.decimals() == 18, "!RETH_ETH_ORACLE"); - require(ETH_USD_ORACLE.decimals() == 8, "!ETH_USD_ORACLE"); - } - - // =============================================================== - // View functions - // =============================================================== - - function latestRoundData() - external - view - override - returns (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound) - { - (, int256 rEthEthPrice,, uint256 rEthEthUpdatedAt,) = RETH_ETH_ORACLE.latestRoundData(); - (, int256 ethUsdPrice,, uint256 ethUsdUpdatedAt,) = ETH_USD_ORACLE.latestRoundData(); - - // If any of the oracles are stale, return 0 - if (_isStale(rEthEthPrice, rEthEthUpdatedAt, RETH_ETH_HEARTBEAT)) return (0, 0, 0, 0, 0); - if (_isStale(ethUsdPrice, ethUsdUpdatedAt, ETH_USD_HEARTBEAT)) return (0, 0, 0, 0, 0); - - // Scale price to 8 decimals - // slither-disable-next-line divide-before-multiply - rEthEthPrice = rEthEthPrice / _1E10; // 18 -> 8 - - // Calculate rETH/USD price with 8 decimals - int256 rEthUsdPrice = ethUsdPrice * rEthEthPrice / _1E8; - - return (0, rEthUsdPrice, 0, Math.min(rEthEthUpdatedAt, ethUsdUpdatedAt), 0); - } - -} diff --git a/src/periphery/Oracles/wstETHOracle.sol b/src/periphery/Oracles/wstETHOracle.sol deleted file mode 100644 index 9b0e822..0000000 --- a/src/periphery/Oracles/wstETHOracle.sol +++ /dev/null @@ -1,66 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0 -pragma solidity 0.8.23; - -import {IWSTETH} from "../../interfaces/IWSTETH.sol"; - -import {AggregatorV3Interface, BaseOracle} from "./BaseOracle.sol"; - -contract wstETHOracle is BaseOracle { - - // =============================================================== - // Constants - // =============================================================== - - /// @notice stETH/USD Chainlink oracle heartbeat - /// @dev 3600 seconds according to chainlink docs, we use 24 hours - uint256 public constant STETH_USD_HEARTBEAT = _24_HOURS; - - /// @notice Used to get the stETH/wstETH rate - /// @dev 18 decimals - IWSTETH public constant WSTETH = IWSTETH(0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0); - - /// @notice stETH/USD Chainlink oracle - AggregatorV3Interface public constant STETH_USD_ORACLE = - AggregatorV3Interface(0xCfE54B5cD566aB89272946F602D76Ea879CAb4a8); - - // =============================================================== - // Constructor - // =============================================================== - - constructor() BaseOracle("wstETH / USD") { - require(WSTETH.decimals() == 18, "!WSTETH"); - require(STETH_USD_ORACLE.decimals() == 8, "!STETH_USD_ORACLE"); - } - - // =============================================================== - // View functions - // =============================================================== - - function latestRoundData() - external - view - override - returns (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound) - { - (, int256 stEthUsdPrice,, uint256 stEthUsdUpdatedAt,) = STETH_USD_ORACLE.latestRoundData(); - - // If the oracle is stale, return 0 - if (_isStale(stEthUsdPrice, stEthUsdUpdatedAt, STETH_USD_HEARTBEAT)) return (0, 0, 0, 0, 0); - - // Get stETH/wstETH price - int256 stEthwstEthPrice = int256(WSTETH.stEthPerToken()); - - // If rate is 0, return 0 - if (stEthwstEthPrice == 0) return (0, 0, 0, 0, 0); - - // Scale price to 8 decimals - // slither-disable-next-line divide-before-multiply - stEthwstEthPrice = stEthwstEthPrice / _1E10; // 18 -> 8 - - // Calculate wstETH/USD price with 8 decimals - int256 wstEthUsdPrice = stEthUsdPrice * stEthwstEthPrice / _1E8; - - return (0, wstEthUsdPrice, 0, stEthUsdUpdatedAt, 0); - } - -} diff --git a/src/test/Operation.t.sol b/src/test/Operation.t.sol index 2c8b4ae..727bc65 100644 --- a/src/test/Operation.t.sol +++ b/src/test/Operation.t.sol @@ -32,7 +32,6 @@ contract OperationTest is Setup { assertEq(strategy.COLL(), tokenAddrs["WETH"]); assertEq(strategy.SP(), stabilityPool); assertEq(strategy.COLL_PRICE_ORACLE(), collateralPriceOracle); - assertEq(strategy.CHAINLINK_ORACLE(), collateralChainlinkPriceOracle); } function test_operation( @@ -333,7 +332,7 @@ contract OperationTest is Setup { function test_tendTrigger_priceTooLow( uint256 _amount ) public { - vm.assume(_amount > strategy.dustThreshold() && _amount < maxFuzzAmount); + _amount = bound(_amount, minFuzzAmount, maxFuzzAmount); // Deposit into strategy mintAndDepositIntoStrategy(strategy, user, _amount); @@ -344,12 +343,14 @@ contract OperationTest is Setup { (bool trigger,) = strategy.tendTrigger(); assertTrue(trigger); + uint256 _expectedMinPrice = ethPrice() * 1e10 * strategy.minAuctionPriceBps() / MAX_BPS; + // Kick it vm.prank(keeper); strategy.tend(); - // Get the starting price before - uint256 startingPriceBefore = IAuction(strategy.AUCTION()).startingPrice(); + // Make sure min price is ok + assertEq(IAuction(strategy.AUCTION()).minimumPrice(), _expectedMinPrice); // Skip enough time such that price is too low skip(1 hours); @@ -364,20 +365,13 @@ contract OperationTest is Setup { (trigger,) = strategy.tendTrigger(); assertTrue(trigger); - // Make sure auction is active - assertTrue(IAuction(strategy.AUCTION()).isActive(address(strategy.COLL()))); + // Make sure auction is not active + assertFalse(IAuction(strategy.AUCTION()).isActive(address(strategy.COLL()))); - // Tend to unwind and block auctions + // Tend to re-kick vm.prank(keeper); strategy.tend(); - // Make sure starting price is higher - assertApproxEqAbs( - IAuction(strategy.AUCTION()).startingPrice(), - startingPriceBefore * strategy.AUCTION_PRICE_TOO_LOW_BUFFER_PCT_MULTIPLIER(), - 50 - ); - // We shouldnt kick again (trigger,) = strategy.tendTrigger(); assertFalse(trigger); @@ -386,68 +380,17 @@ contract OperationTest is Setup { assertTrue(IAuction(strategy.AUCTION()).isActive(address(strategy.COLL()))); } - function test_tendTrigger_priceTooLow_butCLOracleBad( - uint256 _amount, - int256 _clPrice + function test_tendTrigger_priceTooLow_checkDisabled( + uint256 _amount ) public { - vm.assume(_amount > strategy.dustThreshold() && _amount < maxFuzzAmount); - vm.assume(_clPrice <= 0); + _amount = bound(_amount, minFuzzAmount, maxFuzzAmount); // Deposit into strategy mintAndDepositIntoStrategy(strategy, user, _amount); - // Make sure there's something to kick - airdrop(ERC20(strategy.COLL()), address(strategy), _amount); - - (bool trigger,) = strategy.tendTrigger(); - assertTrue(trigger); - - // Kick it - vm.prank(keeper); - strategy.tend(); - - // Get the starting price before - uint256 startingPriceBefore = IAuction(strategy.AUCTION()).startingPrice(); - - // Skip enough time such that price is too low - skip(1 hours); - - // Make sure auction price is lower than our min price - assertTrue( - IAuction(strategy.AUCTION()).price(address(strategy.COLL())) - < ethPrice() * 1e10 * strategy.minAuctionPriceBps() / MAX_BPS - ); - - // We need to start a new auction now - (trigger,) = strategy.tendTrigger(); - assertTrue(trigger); - - AggregatorV3Interface oracle = AggregatorV3Interface(strategy.CHAINLINK_ORACLE()); - (, int256 _answer,,,) = oracle.latestRoundData(); - - // Make sure Chainlink always returns 0 - vm.mockCall( - address(oracle), - abi.encodeWithSelector(AggregatorV3Interface.latestRoundData.selector), - abi.encode(0, _clPrice, 0, 0, 0) - ); - - // We no longer do the auction price check, so no need to tend again - (trigger,) = strategy.tendTrigger(); - assertFalse(trigger); - } - - // NOTE: this test may fail depending on the `collateralChainlinkPriceOracleHeartbeat` value - // bc of conditions around oracle staleness and auction time - function test_tendTrigger_priceTooLow_butCLOracleStale( - uint256 _amount, - int256 _clPrice - ) public { - vm.assume(_amount > strategy.dustThreshold() && _amount < maxFuzzAmount); - vm.assume(_clPrice <= 0); - - // Deposit into strategy - mintAndDepositIntoStrategy(strategy, user, _amount); + // Disable the auction price check + vm.prank(management); + strategy.setMinAuctionPriceBps(0); // Make sure there's something to kick airdrop(ERC20(strategy.COLL()), address(strategy), _amount); @@ -459,57 +402,13 @@ contract OperationTest is Setup { vm.prank(keeper); strategy.tend(); - // Skip enough time such that price is too low - skip(1 hours); - - // Make sure auction price is lower than our min price - assertTrue( - IAuction(strategy.AUCTION()).price(address(strategy.COLL())) - < ethPrice() * 1e10 * strategy.minAuctionPriceBps() / MAX_BPS - ); - - // We need to start a new auction now - (trigger,) = strategy.tendTrigger(); - assertTrue(trigger); - - // Break the heartbeat by skipping time - skip(1 hours); - - // We no longer do the auction price check, so no need to tend again - (trigger,) = strategy.tendTrigger(); - assertFalse(trigger); - } - - function test_tendTrigger_priceTooLow_checkDisabled( - uint256 _amount - ) public { - vm.assume(_amount > strategy.dustThreshold() && _amount < maxFuzzAmount); - - test_tendTrigger_priceTooLow(_amount); - - // Disable the auction price check - vm.prank(management); - strategy.setMinAuctionPriceBps(0); - - // TKS will not kick again - (bool trigger,) = strategy.tendTrigger(); - assertFalse(trigger); + // Make sure min price is ok + assertEq(IAuction(strategy.AUCTION()).minimumPrice(), 0); - // Get the starting price before - uint256 startingPriceBefore = IAuction(strategy.AUCTION()).startingPrice(); + // Skip enough time such that price _would_ be too low + skip(10 hours); - // But we can manually force kick with a low starting price - vm.prank(keeper); - strategy.tend(); - - // Make sure starting price is lower - assertApproxEqAbs( - IAuction(strategy.AUCTION()).startingPrice(), - startingPriceBefore / strategy.AUCTION_PRICE_TOO_LOW_BUFFER_PCT_MULTIPLIER(), - 50 - ); - - // Make sure active auction + // Make sure auction is still active assertTrue(IAuction(strategy.AUCTION()).isActive(address(strategy.COLL()))); } @@ -603,7 +502,7 @@ contract OperationTest is Setup { function test_tendActiveAuction( uint256 _amount ) public { - vm.assume(_amount > strategy.dustThreshold() && _amount < maxFuzzAmount); + _amount = bound(_amount, minFuzzAmount, maxFuzzAmount); address coll = strategy.COLL(); IAuction auction = IAuction(strategy.AUCTION()); @@ -636,8 +535,8 @@ contract OperationTest is Setup { uint256 _amount, uint256 _maxAuctionAmount ) public { - vm.assume(_amount > strategy.dustThreshold() && _amount < maxFuzzAmount); - vm.assume(_maxAuctionAmount > strategy.dustThreshold() && _maxAuctionAmount < _amount); + _amount = bound(_amount, minFuzzAmount, maxFuzzAmount); + _maxAuctionAmount = bound(_maxAuctionAmount, minFuzzAmount, _amount); vm.prank(management); strategy.setMaxAuctionAmount(_maxAuctionAmount); @@ -653,6 +552,7 @@ contract OperationTest is Setup { // Kick it vm.prank(keeper); strategy.tend(); + // revert("Asd"); assertTrue(auction.isActive(coll)); assertEq(auction.available(coll), _maxAuctionAmount); @@ -683,7 +583,7 @@ contract OperationTest is Setup { function test_tendSettleAuction( uint256 _amount ) public { - vm.assume(_amount > strategy.dustThreshold() && _amount < maxFuzzAmount); + vm.assume(_amount > minFuzzAmount && _amount < maxFuzzAmount); address coll = strategy.COLL(); IAuction auction = IAuction(strategy.AUCTION()); @@ -716,7 +616,7 @@ contract OperationTest is Setup { function test_tendSettleAuctionAndKickNewOne( uint256 _amount ) public { - vm.assume(_amount > strategy.dustThreshold() && _amount < maxFuzzAmount); + vm.assume(_amount > minFuzzAmount && _amount < maxFuzzAmount); address coll = strategy.COLL(); IAuction auction = IAuction(strategy.AUCTION()); @@ -753,8 +653,8 @@ contract OperationTest is Setup { uint256 _amount, uint256 _amountTooLow ) public { - vm.assume(_amount > strategy.dustThreshold() && _amount < maxFuzzAmount); - vm.assume(_amountTooLow <= strategy.dustThreshold()); + _amount = bound(_amount, minFuzzAmount, maxFuzzAmount); + _amountTooLow = bound(_amountTooLow, 0, strategy.dustThreshold()); address coll = strategy.COLL(); IAuction auction = IAuction(strategy.AUCTION()); @@ -849,31 +749,33 @@ contract OperationTest is Setup { assertLt(IAuction(strategy.AUCTION()).price(address(strategy.COLL())), ethPrice() * 1e10 * 20 / 100); } - function test_ongoingAuctionAfterPriceTooLow_canOverride( - uint256 _amount - ) public { - vm.assume(_amount > strategy.dustThreshold() && _amount < maxFuzzAmount); + // function test_ongoingAuctionAfterPriceTooLow_canOverride( + // uint256 _amount + // ) public { + // vm.assume(_amount > strategy.dustThreshold() && _amount < maxFuzzAmount); - test_tendTrigger_priceTooLow(_amount); + // test_tendTrigger_priceTooLow(_amount); - // We can override the current delayed auction with a fast one - vm.prank(keeper); - strategy.tend(); + // // We can override the current delayed auction with a fast one + // vm.prank(keeper); + // strategy.tend(); - // Check auction starting price - (uint256 _price,) = IPriceFeed(strategy.COLL_PRICE_ORACLE()).fetchPrice(); - uint256 _toAuctionPrice = _amount * _price / 1e18; - uint256 _expectedStartingPrice = _toAuctionPrice * 115 / 100 / 1e18; - assertEq(IAuction(strategy.AUCTION()).startingPrice(), _expectedStartingPrice); + // // Check auction starting price + // (uint256 _price,) = IPriceFeed(strategy.COLL_PRICE_ORACLE()).fetchPrice(); + // uint256 _toAuctionPrice = _amount * _price / 1e18; + // uint256 _expectedStartingPrice = _toAuctionPrice * 115 / 100 / 1e18; + // assertEq(IAuction(strategy.AUCTION()).startingPrice(), _expectedStartingPrice); - // Check auction price - uint256 _expectedPrice = _expectedStartingPrice * 1e36 / _amount; - assertApproxEq(IAuction(strategy.AUCTION()).price(strategy.COLL()), _expectedPrice, 1); - } + // // Check auction price + // uint256 _expectedPrice = _expectedStartingPrice * 1e36 / _amount; + // assertApproxEq(IAuction(strategy.AUCTION()).price(strategy.COLL()), _expectedPrice, 1); + // } - function test_kickAuction_permissionlessKick( + function test_kickAuction_permissioned( address _address ) public { + vm.assume(_address != address(strategy)); + address coll = strategy.COLL(); IAuction auction = IAuction(strategy.AUCTION()); @@ -881,6 +783,7 @@ contract OperationTest is Setup { airdrop(ERC20(coll), address(auction), 1 ether); vm.prank(_address); + vm.expectRevert("!governance"); auction.kick(coll); } diff --git a/src/test/PriceOracle.t.sol b/src/test/PriceOracle.t.sol deleted file mode 100644 index b8e252a..0000000 --- a/src/test/PriceOracle.t.sol +++ /dev/null @@ -1,28 +0,0 @@ -pragma solidity ^0.8.18; - -import "forge-std/console2.sol"; -import {Setup} from "./utils/Setup.sol"; - -import {wstETHOracle} from "../periphery/Oracles/wstETHOracle.sol"; -// import {rETHOracle} from "../periphery/Oracles/rETHOracle.sol"; - -contract PriceOracleTest is Setup { - - wstETHOracle public oracle; - // rETHOracle public oracle; - - function setUp() public override { - super.setUp(); - - oracle = new wstETHOracle(); - // oracle = new rETHOracle(); - } - - function test_priceOracleSanityCheck() public { - (, int256 answer,, uint256 updatedAt,) = oracle.latestRoundData(); - console2.log("price", uint256(answer)); - assertGt(answer, 0); - assertGt(updatedAt, 0); - } - -} diff --git a/src/test/Zapper.t.sol b/src/test/Zapper.t.sol index e1f63d1..382a173 100644 --- a/src/test/Zapper.t.sol +++ b/src/test/Zapper.t.sol @@ -123,8 +123,8 @@ contract ZapperTest is Setup { uint256 _shares, uint256 _assets ) external { - vm.assume(_shares > minFuzzAmount && _shares < maxFuzzAmount); - vm.assume(_assets > minFuzzAmount && _assets < maxFuzzAmount); + _shares = bound(_shares, minFuzzAmount, maxFuzzAmount); + _assets = bound(_assets, minFuzzAmount, maxFuzzAmount); assertEq(oracle.convertToAssets(_shares), zapper.previewRedeem(_shares)); assertEq(oracle.convertToShares(_assets), zapper.previewDeposit(_assets)); diff --git a/src/test/utils/Setup.sol b/src/test/utils/Setup.sol index ae019c5..6fdebb0 100644 --- a/src/test/utils/Setup.sol +++ b/src/test/utils/Setup.sol @@ -52,8 +52,7 @@ contract Setup is ExtendedTest, IEvents { address public stabilityPool = address(0x5721cbbd64fc7Ae3Ef44A0A3F9a790A9264Cf9BF); // WETH Stability Pool address public collateralPriceOracle = address(0xCC5F8102eb670c89a4a3c567C13851260303c24F); // Liquity WETH Price Oracle address public collateralChainlinkPriceOracle = address(0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419); // Chainlink ETH/USD - uint256 public collateralChainlinkPriceOracleHeartbeat = 3600 * 2; // Chainlink docs say 3600 seconds, we double it - address public auctionFactory = address(0xbC587a495420aBB71Bbd40A0e291B64e80117526); // Newest Auction Factory + address public auctionFactory = address(0x6c2A7584aB3f3B46f93bEb3645205CE71F123B44); // Newest Auction Factory // Address of the real deployed Factory address public factory; @@ -62,15 +61,15 @@ contract Setup is ExtendedTest, IEvents { uint256 public decimals; uint256 public MAX_BPS = 10_000; - // Fuzz from $0.1 of 1e18 stable coins up to 10 billion of a 1e18 coin + // Fuzz from $100 of 1e18 stable coins up to 10 billion of a 1e18 coin uint256 public maxFuzzAmount = 10_000_000_000 ether; - uint256 public minFuzzAmount = 0.1 ether; + uint256 public minFuzzAmount = 10_000 ether; // Default profit max unlock time is set for 10 days uint256 public profitMaxUnlockTime = 10 days; function setUp() public virtual { - uint256 _blockNumber = 23_499_885; // Caching for faster tests + uint256 _blockNumber = 24_125_659; // Caching for faster tests vm.selectFork(vm.createFork(vm.envString("ETH_RPC_URL"), _blockNumber)); _setTokenAddrs(); @@ -102,15 +101,7 @@ contract Setup is ExtendedTest, IEvents { // we save the strategy as a IStrategyInterface to give it the needed interface vm.prank(management); IStrategyInterface _strategy = IStrategyInterface( - address( - strategyFactory.newStrategy( - addressesRegistry, - address(asset), - collateralChainlinkPriceOracle, - collateralChainlinkPriceOracleHeartbeat, - "Tokenized Strategy" - ) - ) + address(strategyFactory.newStrategy(addressesRegistry, address(asset), "Tokenized Strategy")) ); vm.prank(management); @@ -186,7 +177,6 @@ contract Setup is ExtendedTest, IEvents { IStabilityPool _stabilityPool = IStabilityPool(stabilityPool); vm.prank(_stabilityPool.activePool()); _stabilityPool.triggerBoldRewards(_amount); - strategy.claim(); } function simulateCollateralGain() public {