diff --git a/src/feeds/DynamicFeeCurveFeed.sol b/src/feeds/DynamicFeeCurveFeed.sol new file mode 100644 index 00000000..641b7b71 --- /dev/null +++ b/src/feeds/DynamicFeeCurveFeed.sol @@ -0,0 +1,126 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {ICurvePool} from "src/interfaces/ICurvePool.sol"; +import {IChainlinkBasePriceFeed} from "src/interfaces/IChainlinkFeed.sol"; +import {IERC20} from "src/interfaces/IERC20.sol"; + +// Combined Chainlink and Curve price_oracle taking dynamic fees into consideration, allows for additional fallback to be set via ChainlinkBasePriceFeed +contract DynamicFeeCurveFeed { + /// @dev Chainlink base price feed implementation for the pairedToken to USD + IChainlinkBasePriceFeed public immutable pairedTokenToUsd; + /// @dev Curve 2-pool + ICurvePool public immutable curvePool; + /// @dev FiRM asset index in Curve pool `coins` array + uint256 public immutable assetIndex; + /// @dev Description of the feed + string public description; + /// @dev Price decimals of this feed + uint public constant decimals = 18; + /// @dev Max fee initially set to 2% + int public maxFee = 2e8; + + address public gov; + + address public pendingGov; + + constructor( + address _pairedTokenToUsd, + address _curvePool, + address _asset, + address _gov + ) { + gov = _gov; + pairedTokenToUsd = IChainlinkBasePriceFeed(_pairedTokenToUsd); + require( + pairedTokenToUsd.decimals() == 18, + "ChainlinkCurveFeed: DECIMALS_MISMATCH" + ); + curvePool = ICurvePool(_curvePool); + uint _index; + if(ICurvePool(_curvePool).coins(0) == _asset) + _index = 0; + else if(ICurvePool(_curvePool).coins(1) == _asset) + _index = 1; + else + revert("CurveFeed: ASSET NOT IN TWO POOL"); + assetIndex = _index; + + string memory coin = IERC20(_asset).symbol(); + description = string(abi.encodePacked(coin, " / USD")); + } + + event NewMaxFee(int maxFee); + event NewGov(address newGov); + event NewPendingGov(address newPendingGov); + + /** + * @notice Retrieves the latest round data for the pairedToken token price feed + * @return roundId The round ID of the Chainlink price feed for the feed with the lowest updatedAt feed + * @return usdPrice The latest FiRM asset price in USD + * @return startedAt The timestamp when the latest round of Chainlink price feed started of the lowest last updatedAt feed + * @return updatedAt The lowest timestamp when either of the latest round of Chainlink price feed was updated + * @return answeredInRound The round ID in which the answer was computed of the lowest updatedAt feed + */ + function latestRoundData() + public + view + returns ( + uint80 roundId, + int256 usdPrice, + uint256 startedAt, + uint256 updatedAt, + uint80 answeredInRound + ) + { + int256 pairedTokenToUsdPrice; + ( + roundId, + pairedTokenToUsdPrice, + startedAt, + updatedAt, + answeredInRound + ) = pairedTokenToUsd.latestRoundData(); + + int256 fee = int256(curvePool.fee()); + if(fee > maxFee) fee = maxFee; + //crv oracle price is either asset/pairedToken or pairedToken/asset depending on asset index + int256 crvOraclePrice = int256(curvePool.price_oracle()); + //Depending on assetIndex we either divide or multiply by crv oracle price + usdPrice = assetIndex == 0 ? + pairedTokenToUsdPrice * 1e18 / crvOraclePrice : + crvOraclePrice * pairedTokenToUsdPrice / 1e18; + //Reduce the price by the dynamic fee amount + usdPrice -= usdPrice * fee / 1e10; + } + + /** + * @notice Returns the latest price only + * @dev Unlike chainlink oracles, the latestAnswer will always be the same as in the latestRoundData + * @return int256 Returns the last finalized price of the chainlink oracle + */ + function latestAnswer() external view returns (int256) { + (, int256 latestPrice, , , ) = latestRoundData(); + return latestPrice; + } + + function setMaxFee(int _maxFee) external { + require(msg.sender == gov, "ONLY GOV"); + require(_maxFee >= 0 && _maxFee <= 1e10, "CurveFeed: maxFee > 100% or negative"); + maxFee = _maxFee; + emit NewMaxFee(_maxFee); + } + + function setPendingGov(address _gov) external { + require(msg.sender == gov, "ONLY GOV"); + pendingGov = _gov; + emit NewPendingGov(_gov); + } + + function acceptGov() external { + require(msg.sender == pendingGov, "ONLY PENDING GOV"); + gov = pendingGov; + pendingGov = address(0); + emit NewGov(gov); + } +} diff --git a/src/feeds/InvPriceFeed.sol b/src/feeds/InvPriceFeed.sol deleted file mode 100644 index e9c5739d..00000000 --- a/src/feeds/InvPriceFeed.sol +++ /dev/null @@ -1,186 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity ^0.8.4; - -interface IChainlinkFeed { - function aggregator() external view returns (address aggregator); - - function decimals() external view returns (uint8); - - function latestRoundData() - external - view - returns ( - uint80 roundId, - int256 invDollarPrice, - uint256 startedAt, - uint256 updatedAt, - uint80 answeredInRound - ); -} - -interface IAggregator { - function maxAnswer() external view returns (int192); - - function minAnswer() external view returns (int192); -} - -interface ICurvePool { - function price_oracle(uint256 k) external view returns (uint256); -} - -contract InvPriceFeed { - error OnlyGov(); - - ICurvePool public constant tricryptoETH = - ICurvePool(0x7F86Bf177Dd4F3494b841a37e810A34dD56c829B); - - ICurvePool public constant tricryptoINV = - ICurvePool(0x5426178799ee0a0181A89b4f57eFddfAb49941Ec); - - IChainlinkFeed public constant usdcToUsd = - IChainlinkFeed(0x8fFfFfd4AfB6115b954Bd326cbe7B4BA576818f6); - - IChainlinkFeed public constant ethToUsd = - IChainlinkFeed(0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419); - - uint256 public constant ethK = 1; - uint256 public constant invK = 1; - - uint256 public ethHeartbeat = 1 hours; - address public gov = 0x926dF14a23BE491164dCF93f4c468A50ef659D5B; - - modifier onlyGov() { - if (msg.sender != gov) revert OnlyGov(); - _; - } - - /** - * @notice Retrieves the latest round data for the INV token price feed - * @dev This function calculates the INV price in USD by combining the USDC to USD price from a Chainlink oracle - * and the INV to USDC ratio from the tricrypto pool. - * If USDC/USD price is out of bounds, it will fallback to ETH/USD price oracle and USDC to ETH ratio from the tricrypto pool - * @return roundId The round ID of the Chainlink price feed - * @return usdcUsdPrice The latest USDC price in USD computed from the INV/USDC and USDC/USD feeds - * @return startedAt The timestamp when the latest round of Chainlink price feed started - * @return updatedAt The timestamp when the latest round of Chainlink price feed was updated - * @return answeredInRound The round ID in which the answer was computed - */ - function latestRoundData() - public - view - returns (uint80, int256, uint256, uint256, uint80) - { - ( - uint80 roundId, - int256 usdcUsdPrice, - uint startedAt, - uint updatedAt, - uint80 answeredInRound - ) = usdcToUsd.latestRoundData(); - - if (isPriceOutOfBounds(usdcUsdPrice, usdcToUsd)) { - ( - roundId, - usdcUsdPrice, - startedAt, - updatedAt, - answeredInRound - ) = usdcToUsdFallbackOracle(); - } - int256 invUsdcPrice = int256(tricryptoINV.price_oracle(invK)); - - int256 invDollarPrice = (invUsdcPrice * usdcUsdPrice) / - int(10 ** (decimals() - 10)); - - return (roundId, invDollarPrice, startedAt, updatedAt, answeredInRound); - } - - /** - @notice Retrieves the latest price for the INV token - @return price The latest price for the INV token - */ - function latestAnswer() external view returns (int256) { - (, int256 price, , , ) = latestRoundData(); - return price; - } - - /** - * @notice Retrieves number of decimals for the INV price feed - * @return decimals The number of decimals for the INV price feed - */ - function decimals() public pure returns (uint8) { - return 18; - } - - /** - * @notice Checks if a given price is out of the boundaries defined in the Chainlink aggregator. - * @param price The price to be checked. - * @param feed The Chainlink feed to retrieve the boundary information from. - * @return bool Returns `true` if the price is out of bounds, otherwise `false`. - */ - function isPriceOutOfBounds( - int price, - IChainlinkFeed feed - ) public view returns (bool) { - IAggregator aggregator = IAggregator(feed.aggregator()); - int192 max = aggregator.maxAnswer(); - int192 min = aggregator.minAnswer(); - return (max <= price || min >= price); - } - - /** - * @notice Fetches the ETH to USD price and the ETH to USDC to get USDC/USD price, adjusts the decimals to match Chainlink oracles. - * @dev The function assumes that the `price_oracle` returns the price with 18 decimals, and it adjusts to 8 decimals for compatibility with Chainlink oracles. - * @return roundId The round ID of the ETH/USD Chainlink price feed - * @return usdcToUsdPrice The latest USDC price in USD computed from the ETH/USD and ETH/USDC feeds - * @return startedAt The timestamp when the latest round of ETH/USD Chainlink price feed started - * @return updatedAt The timestamp when the latest round of ETH/USD Chainlink price feed was updated - * @return answeredInRound The round ID of the ETH/USD Chainlink price feed in which the answer was computed - */ - function usdcToUsdFallbackOracle() - public - view - returns (uint80, int256, uint256, uint256, uint80) - { - int crvEthToUsdc = int(tricryptoETH.price_oracle(ethK)); - - ( - uint80 roundId, - int256 ethToUsdPrice, - uint256 startedAt, - uint256 updatedAt, - uint80 answeredInRound - ) = ethToUsd.latestRoundData(); - - int256 usdcToUsdPrice = (ethToUsdPrice * 10 ** 18) / crvEthToUsdc; - - if ( - isPriceOutOfBounds(ethToUsdPrice, ethToUsd) || - block.timestamp - updatedAt > ethHeartbeat - ) { - // Will force stale price on borrow controller - updatedAt = 0; - } - - return (roundId, usdcToUsdPrice, startedAt, updatedAt, answeredInRound); - } - - /** - * @notice Sets a new ETH heartbeat - * @dev Can only be called by the current gov address - * @param newHeartbeat The new ETH heartbeat - */ - function setEthHeartbeat(uint256 newHeartbeat) external onlyGov { - ethHeartbeat = newHeartbeat; - } - - /** - * @notice Sets a new gov address - * @dev Can only be called by the current gov address - * @param newGov The new gov address - */ - function setGov(address newGov) external onlyGov { - gov = newGov; - } -} diff --git a/src/interfaces/ICurvePool.sol b/src/interfaces/ICurvePool.sol index 3bbe0e00..7e8d36b7 100644 --- a/src/interfaces/ICurvePool.sol +++ b/src/interfaces/ICurvePool.sol @@ -77,4 +77,8 @@ interface ICurvePool { function lp_token() external view returns (address); function decimals() external view returns (uint256); + + function fee() external view returns(uint); + + function out_fee() external view returns(uint); } diff --git a/test/feedForkTests/InvDynamicFeeCurvePriceFeed.t.sol b/test/feedForkTests/InvDynamicFeeCurvePriceFeed.t.sol new file mode 100644 index 00000000..9f933df5 --- /dev/null +++ b/test/feedForkTests/InvDynamicFeeCurvePriceFeed.t.sol @@ -0,0 +1,92 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.19; + +import "forge-std/Test.sol"; +import "src/feeds/DynamicFeeCurveFeed.sol"; +import "forge-std/console.sol"; + +contract InvDynamicFeeCurveFeedTest is Test { + DynamicFeeCurveFeed feed; + address invWethPool = 0xDcD90D866Ff9636e5a04768825d05d27b3Fb19eC; + address baseWethToUsdFeed = 0x22390B88C53D1631f673b8Dcd91860267137b2c8; + address inv = address(0x41D5D79431A913C4aE7d69a668ecdfE5fF9DFB68); + address gov = address(0x926dF14a23BE491164dCF93f4c468A50ef659D5B); + IChainlinkBasePriceFeed oldInvFeed = IChainlinkBasePriceFeed(0x54F1E4EB93c5b5F4C12776c96e08a49A9928FE84); + address newGov = address(0xA); + function setUp() public { + string memory url = vm.rpcUrl("mainnet"); + vm.createSelectFork(url, 23776456); + feed = new DynamicFeeCurveFeed(baseWethToUsdFeed, invWethPool, inv, gov); + } + + function test_decimals() public { + assertEq(feed.decimals(), 18); + } + + function test_description() public { + string memory expected = "INV / USD"; + assertEq(feed.description(), expected); + } + + function test_latestRoundData() public { + ( + uint80 roundId, + int256 invUsdPrice, + uint startedAt, + uint updatedAt, + uint80 answeredInRound + ) = feed.latestRoundData(); + + ( + uint80 clRoundId, + , + uint clStartedAt, + uint clUpdatedAt, + uint80 clAnsweredInRound + ) = IChainlinkBasePriceFeed(feed.pairedTokenToUsd()).latestRoundData(); + + (,int256 estInvUsdPrice,,,) = oldInvFeed.latestRoundData(); + + assertEq(roundId, clRoundId); + assertApproxEqRel(invUsdPrice, estInvUsdPrice, 2e18, "Price feeds diverge too much"); + assertEq(startedAt, clStartedAt); + assertEq(updatedAt, clUpdatedAt); + assertEq(answeredInRound, clAnsweredInRound); + console.log(uint(invUsdPrice)); + } + + function testMaxFee() external { + vm.expectRevert("ONLY GOV"); + feed.setMaxFee(0); + + vm.prank(gov); + vm.expectRevert("CurveFeed: maxFee > 100%"); + feed.setMaxFee(1e10 + 1); + + vm.prank(gov); + feed.setMaxFee(0); + + (,int256 invUsdPrice,,,) = feed.latestRoundData(); + vm.prank(gov); + feed.setMaxFee(2e8); + (,int256 invUsdPriceWithFee,,,) = feed.latestRoundData(); + assertLt(invUsdPriceWithFee, invUsdPrice); + } + + function testGovChange() external { + vm.expectRevert("ONLY GOV"); + feed.setPendingGov(newGov); + + vm.prank(gov); + feed.setPendingGov(newGov); + assertEq(feed.pendingGov(), newGov); + + vm.expectRevert("ONLY PENDING GOV"); + feed.acceptGov(); + + vm.prank(newGov); + feed.acceptGov(); + assertEq(feed.gov(), newGov); + assertEq(feed.pendingGov(), address(0)); + } +} diff --git a/test/feedForkTests/InvFeedFork.t.sol b/test/feedForkTests/InvFeedFork.t.sol deleted file mode 100644 index a8fcedff..00000000 --- a/test/feedForkTests/InvFeedFork.t.sol +++ /dev/null @@ -1,338 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.19; - -import "forge-std/Test.sol"; -import "src/feeds/InvPriceFeed.sol"; -import "forge-std/console.sol"; -import {IAggregator} from "src/feeds/InvPriceFeed.sol"; - -contract InvFeedFork is Test { - InvPriceFeed feed; - - function setUp() public { - string memory url = vm.rpcUrl("mainnet"); - vm.createSelectFork(url); - feed = new InvPriceFeed(); - } - - function test_decimals() public { - assertEq(feed.decimals(), 18); - } - - function test_latestRoundData() public { - ( - uint80 clRoundId, - int256 clUsdcToUsdPrice, - uint clStartedAt, - uint clUpdatedAt, - uint80 clAnsweredInRound - ) = feed.usdcToUsd().latestRoundData(); - ( - uint80 roundId, - int256 invUsdPrice, - uint startedAt, - uint updatedAt, - uint80 answeredInRound - ) = feed.latestRoundData(); - assertEq(roundId, clRoundId); - assertEq(startedAt, clStartedAt); - assertEq(updatedAt, clUpdatedAt); - assertEq(answeredInRound, clAnsweredInRound); - - uint256 invUSDCPrice = feed.tricryptoINV().price_oracle(1); - uint256 estimatedInvUSDPrice = (invUSDCPrice * - uint256(clUsdcToUsdPrice) * - 10 ** 10) / 10 ** 18; - - assertEq(uint256(invUsdPrice), estimatedInvUSDPrice); - assertEq(uint256(invUsdPrice), uint(feed.latestAnswer())); - } - - function testWillReturnFallbackWhenOutOfMaxBounds() public { - ( - uint80 clRoundId, - , - uint clStartedAt, - uint clUpdatedAt, - uint80 clAnsweredInRound - ) = feed.ethToUsd().latestRoundData(); - - _mockChainlinkPrice( - feed.usdcToUsd(), - IAggregator(feed.usdcToUsd().aggregator()).maxAnswer() - ); - ( - uint80 roundId, - int256 invUsdPrice, - uint startedAt, - uint updatedAt, - uint80 answeredInRound - ) = feed.latestRoundData(); - - assertEq(clRoundId, roundId); - assertEq(clStartedAt, startedAt); - assertEq(clUpdatedAt, updatedAt); - assertEq(clAnsweredInRound, answeredInRound); - - uint256 invUSDCPrice = feed.tricryptoINV().price_oracle(1); - (, int256 usdcFallback, , , ) = feed.usdcToUsdFallbackOracle(); - - uint256 estimatedInvUSDPrice = (invUSDCPrice * - uint256(usdcFallback) * - 10 ** 10) / 10 ** 18; - - assertEq(uint256(invUsdPrice), estimatedInvUSDPrice); - assertEq(uint256(invUsdPrice), uint(feed.latestAnswer())); - } - - function testWillReturnFallbackWhenOutOfMinBounds() public { - ( - uint80 clRoundId, - , - uint clStartedAt, - uint clUpdatedAt, - uint80 clAnsweredInRound - ) = feed.ethToUsd().latestRoundData(); - - _mockChainlinkPrice( - feed.usdcToUsd(), - IAggregator(feed.usdcToUsd().aggregator()).minAnswer() - ); - - ( - uint80 roundId, - int256 invUsdPrice, - uint startedAt, - uint updatedAt, - uint80 answeredInRound - ) = feed.latestRoundData(); - - assertEq(clRoundId, roundId); - assertEq(clStartedAt, startedAt); - assertEq(clUpdatedAt, updatedAt); - assertEq(clAnsweredInRound, answeredInRound); - - uint256 invUSDCPrice = feed.tricryptoINV().price_oracle(1); - (, int256 usdcFallback, , , ) = feed.usdcToUsdFallbackOracle(); - - uint256 estimatedInvUSDPrice = (invUSDCPrice * - uint256(usdcFallback) * - 10 ** 10) / 10 ** 18; - - assertEq(uint256(invUsdPrice), estimatedInvUSDPrice); - assertEq(uint256(invUsdPrice), uint(feed.latestAnswer())); - } - - function test_StaleETH_WillReturnFallbackWhenOutOfMinBoundsUSDC() public { - ( - uint80 clRoundId, - , - uint clStartedAt, - , - uint80 clAnsweredInRound - ) = feed.ethToUsd().latestRoundData(); - - _mockChainlinkPrice( - feed.usdcToUsd(), - IAggregator(feed.usdcToUsd().aggregator()).minAnswer() - ); - _mockChainlinkUpdatedAt(feed.ethToUsd(), -1 * int(feed.ethHeartbeat())); - - ( - uint80 roundId, - int256 invUsdPrice, - uint startedAt, - uint updatedAt, - uint80 answeredInRound - ) = feed.latestRoundData(); - - assertEq(clRoundId, roundId); - assertEq(clStartedAt, startedAt); - assertEq(0, updatedAt); // if stale price return updateAt == 0 - assertEq(clAnsweredInRound, answeredInRound); - - uint256 invUSDCPrice = feed.tricryptoINV().price_oracle(1); - (, int256 usdcFallback, , , ) = feed.usdcToUsdFallbackOracle(); - - uint256 estimatedInvUSDPrice = (invUSDCPrice * - uint256(usdcFallback) * - 10 ** 10) / 10 ** 18; - - assertEq(uint256(invUsdPrice), estimatedInvUSDPrice); - assertEq(uint256(invUsdPrice), uint(feed.latestAnswer())); - } - - function test_OutOfMinBoundsETH_WillReturn_STALE_fallbackWhenOutOfMinBoundsUSDC() - public - { - ( - uint80 clRoundId, - , - uint clStartedAt, - , - uint80 clAnsweredInRound - ) = feed.ethToUsd().latestRoundData(); - - _mockChainlinkPrice( - feed.usdcToUsd(), - IAggregator(feed.usdcToUsd().aggregator()).minAnswer() - ); - _mockChainlinkPrice( - feed.ethToUsd(), - IAggregator(feed.ethToUsd().aggregator()).minAnswer() - ); - - ( - uint80 roundId, - int256 invUsdPrice, - uint startedAt, - uint updatedAt, - uint80 answeredInRound - ) = feed.latestRoundData(); - - assertEq(clRoundId, roundId); - assertEq(clStartedAt, startedAt); - assertEq(0, updatedAt); // if out of bounds return updateAt == 0 - assertEq(clAnsweredInRound, answeredInRound); - - uint256 invUSDCPrice = feed.tricryptoINV().price_oracle(1); - (, int256 usdcFallback, , , ) = feed.usdcToUsdFallbackOracle(); - - uint256 estimatedInvUSDPrice = (invUSDCPrice * - uint256(usdcFallback) * - 10 ** 10) / 10 ** 18; - - assertEq(uint256(invUsdPrice), estimatedInvUSDPrice); - assertEq(uint256(invUsdPrice), uint(feed.latestAnswer())); - } - - function test_OutOfMaxBoundsETH_WillReturn_STALE_fallbackWhenOutOfMinBoundsUSDC() - public - { - ( - uint80 clRoundId, - , - uint clStartedAt, - , - uint80 clAnsweredInRound - ) = feed.ethToUsd().latestRoundData(); - - _mockChainlinkPrice( - feed.usdcToUsd(), - IAggregator(feed.usdcToUsd().aggregator()).minAnswer() - ); - _mockChainlinkPrice(feed.ethToUsd(), type(int192).max); // won't revert even if maxAnswer is the maximum int192 value but will return Stale price - - ( - uint80 roundId, - int256 invUsdPrice, - uint startedAt, - uint updatedAt, - uint80 answeredInRound - ) = feed.latestRoundData(); - - assertEq(clRoundId, roundId); - assertEq(clStartedAt, startedAt); - assertEq(0, updatedAt); // if out of bounds ETH return updateAt == 0 - assertEq(clAnsweredInRound, answeredInRound); - - uint256 invUSDCPrice = feed.tricryptoINV().price_oracle(1); - (, int256 usdcFallback, , , ) = feed.usdcToUsdFallbackOracle(); - - uint256 estimatedInvUSDPrice = (invUSDCPrice * uint256(usdcFallback)) / - 10 ** 8; - - assertEq(uint256(invUsdPrice), estimatedInvUSDPrice); - assertEq(uint256(invUsdPrice), uint(feed.latestAnswer())); - } - - function test_compare_oracle() public { - (, int256 invUsdPrice, , , ) = feed.latestRoundData(); - assertEq(uint256(invUsdPrice), uint(feed.latestAnswer())); - - _mockChainlinkPrice( - feed.usdcToUsd(), - IAggregator(feed.usdcToUsd().aggregator()).minAnswer() - ); - - (, int256 invUsdPriceFallback, , , ) = feed.latestRoundData(); - assertEq(uint256(invUsdPriceFallback), uint(feed.latestAnswer())); - assertApproxEqAbs( - uint256(invUsdPrice), - uint256(invUsdPriceFallback), - 0.5 ether - ); // 0.5 dollar - } - - function test_setEthHeartbeat() public { - assertEq(feed.ethHeartbeat(), 3600); - - vm.expectRevert(InvPriceFeed.OnlyGov.selector); - feed.setEthHeartbeat(100); - assertEq(feed.ethHeartbeat(), 3600); - - vm.prank(feed.gov()); - feed.setEthHeartbeat(100); - assertEq(feed.ethHeartbeat(), 100); - } - - function test_setGov() public { - assertEq(feed.gov(), 0x926dF14a23BE491164dCF93f4c468A50ef659D5B); - - vm.expectRevert(InvPriceFeed.OnlyGov.selector); - feed.setGov(address(this)); - assertEq(feed.gov(), 0x926dF14a23BE491164dCF93f4c468A50ef659D5B); - - vm.prank(feed.gov()); - feed.setGov(address(this)); - assertEq(feed.gov(), address(this)); - } - - function _mockChainlinkPrice( - IChainlinkFeed clFeed, - int mockPrice - ) internal { - ( - uint80 roundId, - , - uint startedAt, - uint updatedAt, - uint80 answeredInRound - ) = clFeed.latestRoundData(); - vm.mockCall( - address(clFeed), - abi.encodeWithSelector(IChainlinkFeed.latestRoundData.selector), - abi.encode( - roundId, - mockPrice, - startedAt, - updatedAt, - answeredInRound - ) - ); - } - - function _mockChainlinkUpdatedAt( - IChainlinkFeed clFeed, - int updatedAtDelta - ) internal { - ( - uint80 roundId, - int price, - uint startedAt, - uint updatedAt, - uint80 answeredInRound - ) = clFeed.latestRoundData(); - vm.mockCall( - address(clFeed), - abi.encodeWithSelector(IChainlinkFeed.latestRoundData.selector), - abi.encode( - roundId, - price, - startedAt, - uint(int(updatedAt) + updatedAtDelta), - answeredInRound - ) - ); - } -} diff --git a/test/feedForkTests/InvFeedV2.t.sol b/test/feedForkTests/InvFeedV2.t.sol deleted file mode 100644 index 770396bc..00000000 --- a/test/feedForkTests/InvFeedV2.t.sol +++ /dev/null @@ -1,88 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.19; - -import "forge-std/Test.sol"; -import "src/feeds/ChainlinkCurve2CoinsFeed.sol"; -import {ChainlinkBasePriceFeed} from "src/feeds/ChainlinkBasePriceFeed.sol"; -import {ConfigAddr} from "test/ConfigAddr.sol"; -import "forge-std/console.sol"; - -contract InvFeedV2Test is Test, ConfigAddr { - ChainlinkCurve2CoinsFeed feed; - ChainlinkBasePriceFeed ethUsdWrapper; - address invEth = address(0x6bD88c57523bF138A19b263E8ebC8661c836B171); - address ethUsdClFeed = address(0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419); - uint256 invIndex = 1; //targetIndex - uint256 ethUsdHeartbeat = 1 hours; - - function setUp() public { - string memory url = vm.rpcUrl("mainnet"); - vm.createSelectFork(url); - ethUsdWrapper = new ChainlinkBasePriceFeed( - gov, - ethUsdClFeed, - address(0), - ethUsdHeartbeat - ); - feed = new ChainlinkCurve2CoinsFeed( - address(ethUsdWrapper), - invEth, - invIndex - ); - } - - function test_deployment() public { - assertEq(address(feed.assetToUsd()), address(ethUsdWrapper)); - assertEq(address(feed.curvePool()), invEth); - assertEq(feed.targetIndex(), invIndex); - assertEq(feed.decimals(), 18); - assertEq(address(ethUsdWrapper.assetToUsd()), ethUsdClFeed); - assertEq(ethUsdWrapper.assetToUsdHeartbeat(), ethUsdHeartbeat); - assertEq(ethUsdWrapper.decimals(), 18); - } - function test_description() public { - string memory expected = "INV / USD"; - string memory actual = feed.description(); - assertEq(expected, actual); - - assertEq(ethUsdWrapper.description(), "ETH / USD"); - } - - function test_latestRoundData() public { - ( - uint80 roundId, - int256 invUsdPrice, - uint256 startedAt, - uint256 updatedAt, - uint80 answeredInRound - ) = feed.latestRoundData(); - - ( - uint80 roundIdCl, - int256 ethUsdPrice, - uint256 startedAtCl, - uint256 updatedAtCl, - uint80 answeredInRoundCl - ) = ethUsdWrapper.latestRoundData(); - - uint256 invEthPrice = ICurvePool(invEth).price_oracle(); - assertEq(roundId, roundIdCl); - assertEq( - uint(invUsdPrice), - ((uint(invEthPrice) * uint(ethUsdPrice)) / 1e18) - ); - assertEq(startedAt, startedAtCl); - assertEq(updatedAt, updatedAtCl); - assertEq(answeredInRound, answeredInRoundCl); - } - - function test_latestAnswer() public { - int256 invUsdPrice = feed.latestAnswer(); - int256 ethUsdPrice = ethUsdWrapper.latestAnswer(); - uint256 invEthPrice = ICurvePool(invEth).price_oracle(); - assertEq( - uint(invUsdPrice), - (uint(invEthPrice) * uint(ethUsdPrice)) / 1e18 - ); - } -} diff --git a/test/marketForkTests/InvMarketForkBaseTest.t.sol b/test/marketForkTests/InvMarketForkBaseTest.t.sol index d868e785..7ecbc069 100644 --- a/test/marketForkTests/InvMarketForkBaseTest.t.sol +++ b/test/marketForkTests/InvMarketForkBaseTest.t.sol @@ -17,7 +17,7 @@ contract InvMarketBaseForkTest is MarketBaseForkTest { 0xdcd2D918511Ba39F2872EB731BB88681AE184244 ); address marketAddr = 0xb516247596Ca36bf32876199FBdCaD6B3322330B; - address feedAddr = 0xC54Ca0a605D5DA34baC77f43efb55519fC53E78e; + address feedAddr = 0x5E38e4f3cea5f9333984Fc7B527Fbf2B8bb3bd51; _advancedInit(marketAddr, feedAddr, true); vm.startPrank(gov); dbr.addMinter(address(distributor)); diff --git a/test/marketForkTests/InvMarketForkTest.t.sol b/test/marketForkTests/InvMarketForkTest.t.sol deleted file mode 100644 index 0c3da587..00000000 --- a/test/marketForkTests/InvMarketForkTest.t.sol +++ /dev/null @@ -1,907 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.13; - -import "forge-std/Test.sol"; -import "./MarketForkTest.sol"; -import {InvPriceFeed, IAggregator} from "src/feeds/InvPriceFeed.sol"; -import {console} from "forge-std/console.sol"; -import {BorrowController} from "src/BorrowController.sol"; -import "src/DBR.sol"; -import {Fed} from "src/Fed.sol"; -import {Market} from "src/Market.sol"; -import "src/Oracle.sol"; -import {DbrDistributor, IDBR} from "src/DbrDistributor.sol"; -import {INVEscrow, IXINV, IDbrDistributor} from "src/escrows/INVEscrow.sol"; -import "test/mocks/ERC20.sol"; -import {BorrowContract} from "test/mocks/BorrowContract.sol"; - -interface IBorrowControllerLatest { - function borrowAllowed( - address msgSender, - address borrower, - uint amount - ) external returns (bool); - - function onRepay(uint amount) external; - - function setStalenessThreshold( - address market, - uint stalenessThreshold - ) external; - - function operator() external view returns (address); - - function isBelowMinDebt( - address market, - address borrower, - uint amount - ) external view returns (bool); - - function isPriceStale(address market) external view returns (bool); - - function dailyLimits(address market) external view returns (uint); -} - -contract InvMarketForkTest is MarketForkTest { - bytes onlyGovUnpause = "Only governance can unpause"; - bytes onlyPauseGuardianOrGov = - "Only pause guardian or governance can pause"; - address lender = 0x2b34548b865ad66A2B046cb82e59eE43F75B90fd; - IERC20 INV = IERC20(0x41D5D79431A913C4aE7d69a668ecdfE5fF9DFB68); - IXINV xINV = IXINV(0x1637e4e9941D55703a7A5E7807d6aDA3f7DCD61B); - DbrDistributor distributor; - InvPriceFeed newFeed; - BorrowContract borrowContract; - - function setUp() public { - //This will fail if there's no mainnet variable in foundry.toml - string memory url = vm.rpcUrl("mainnet"); - vm.createSelectFork(url, 18336060); - distributor = DbrDistributor( - 0xdcd2D918511Ba39F2872EB731BB88681AE184244 - ); - newFeed = new InvPriceFeed(); - vm.prank(gov); - newFeed.setEthHeartbeat(1 hours); - init(0xb516247596Ca36bf32876199FBdCaD6B3322330B, address(newFeed)); - vm.startPrank(gov); - dbr.addMinter(address(distributor)); - - distributor.setRewardRateConstraints( - 126839167935058000, - 317097919837646000 - ); - distributor.setRewardRateConstraints(0, 317097919837646000); - vm.stopPrank(); - - borrowContract = new BorrowContract( - address(market), - payable(address(collateral)) - ); - } - - function testDeposit() public { - gibCollateral(user, testAmount); - - vm.startPrank(user, user); - deposit(testAmount); - assertLe( - market.escrows(user).balance(), - testAmount, - "Escrow balance is less than or equal deposit amount due to rounding errors" - ); - assertGt( - market.escrows(user).balance() + 10, - testAmount, - "Escrow balance is greater than deposit amount when adjusted slightly up" - ); - } - - function testDeposit_CanClaimDbr_AfterTimePassed() public { - gibCollateral(user, testAmount); - - vm.startPrank(user, user); - deposit(testAmount); - assertLe( - market.escrows(user).balance(), - testAmount, - "Escrow balance is less than or equal deposit amount due to rounding errors" - ); - assertGt( - market.escrows(user).balance() + 10, - testAmount, - "Escrow balance is greater than deposit amount when adjusted slightly up" - ); - vm.warp(block.timestamp + 3600); - uint dbrBeforeClaim = dbr.balanceOf(user); - INVEscrow(address(market.escrows(user))).claimDBR(); - uint dbrAfterClaim = dbr.balanceOf(user); - assertGt(dbrAfterClaim, dbrBeforeClaim, "No DBR issued"); - } - - function testDeposit_succeed_depositForOtherUser() public { - gibCollateral(user, testAmount); - - vm.startPrank(user, user); - collateral.approve(address(market), testAmount); - market.deposit(user2, testAmount); - assertLe( - market.escrows(user2).balance(), - testAmount, - "Escrow balance is less than or equal deposit amount due to rounding errors" - ); - assertGt( - market.escrows(user2).balance() + 10, - testAmount, - "Escrow balance is greater than deposit amount when adjusted slightly up" - ); - } - - function testBorrow_Fails_if_price_stale_usdcToUsd_chainlink() public { - _setNewBorrowController(); - - testAmount = 300 ether; - gibCollateral(user, testAmount); - gibDBR(user, testAmount); - vm.startPrank(user, user); - deposit(testAmount); - - _mockChainlinkUpdatedAt( - IChainlinkFeed(address(newFeed.usdcToUsd())), - -86461 - ); // price stale for usdcToUsd - - assertTrue( - IBorrowControllerLatest(0x44B7895989Bc7886423F06DeAa844D413384b0d6) - .isPriceStale(address(market)) - ); - assertFalse( - IBorrowControllerLatest(0x44B7895989Bc7886423F06DeAa844D413384b0d6) - .isBelowMinDebt(address(market), user, 1300 ether) - ); // Minimum debt is 1250 DOLA - - vm.expectRevert(bytes("Denied by borrow controller")); - market.borrow(1300 ether); - } - - function testBorrow_Fails_if_price_stale_ethToUsd_when_usdcToUsd_MIN_out_of_bounds() - public - { - _setNewBorrowController(); - - testAmount = 300 ether; - gibCollateral(user, testAmount); - gibDBR(user, testAmount); - vm.startPrank(user, user); - deposit(testAmount); - - _mockChainlinkPrice( - IChainlinkFeed(address(newFeed.usdcToUsd())), - IAggregator(newFeed.usdcToUsd().aggregator()).minAnswer() - 1 - ); // min out of bounds - _mockChainlinkUpdatedAt( - IChainlinkFeed(address(newFeed.ethToUsd())), - -3601 - ); // staleness for ethToUsd - - assertTrue( - IBorrowControllerLatest(0x44B7895989Bc7886423F06DeAa844D413384b0d6) - .isPriceStale(address(market)) - ); - assertFalse( - IBorrowControllerLatest(0x44B7895989Bc7886423F06DeAa844D413384b0d6) - .isBelowMinDebt(address(market), user, 1300 ether) - ); // Minimum debt is 1250 DOLA - - _mockChainlinkPrice( - IChainlinkFeed(address(newFeed.usdcToUsd())), - IAggregator(newFeed.usdcToUsd().aggregator()).minAnswer() - 1 - ); // min out of bounds - _mockChainlinkUpdatedAt( - IChainlinkFeed(address(newFeed.ethToUsd())), - -3601 - ); // staleness for ethToUsd - - vm.expectRevert("Denied by borrow controller"); - market.borrow(1300 ether); - } - - function testBorrow_Fails_if_price_stale_ethToUsd_when_usdcToUsd_MAX_out_of_bounds() - public - { - _setNewBorrowController(); - - testAmount = 300 ether; - gibCollateral(user, testAmount); - gibDBR(user, testAmount); - vm.startPrank(user, user); - deposit(testAmount); - - _mockChainlinkPrice( - IChainlinkFeed(address(newFeed.usdcToUsd())), - IAggregator(newFeed.usdcToUsd().aggregator()).maxAnswer() + 1 - ); // Max out of bounds - _mockChainlinkUpdatedAt( - IChainlinkFeed(address(newFeed.ethToUsd())), - -3601 - ); // staleness for ethToUsd - - assertTrue( - IBorrowControllerLatest(0x44B7895989Bc7886423F06DeAa844D413384b0d6) - .isPriceStale(address(market)) - ); - assertFalse( - IBorrowControllerLatest(0x44B7895989Bc7886423F06DeAa844D413384b0d6) - .isBelowMinDebt(address(market), user, 1300 ether) - ); // Minimum debt is 1250 DOLA - - _mockChainlinkPrice( - IChainlinkFeed(address(newFeed.usdcToUsd())), - IAggregator(newFeed.usdcToUsd().aggregator()).maxAnswer() + 1 - ); // Max out of bounds - _mockChainlinkUpdatedAt( - IChainlinkFeed(address(newFeed.ethToUsd())), - -3601 - ); // staleness for ethToUsd - - vm.expectRevert("Denied by borrow controller"); - market.borrow(1300 ether); - } - - function testBorrow_Fails_if_ethToUsd_MIN_out_of_bounds_when_usdcToUsd_MAX_out_of_bounds() - public - { - _setNewBorrowController(); - - testAmount = 300 ether; - gibCollateral(user, testAmount); - gibDBR(user, testAmount); - vm.startPrank(user, user); - deposit(testAmount); - - _mockChainlinkPrice( - IChainlinkFeed(address(newFeed.usdcToUsd())), - IAggregator(newFeed.usdcToUsd().aggregator()).maxAnswer() + 1 - ); // Max out of bounds - _mockChainlinkPrice( - IChainlinkFeed(address(newFeed.ethToUsd())), - IAggregator(newFeed.ethToUsd().aggregator()).minAnswer() - 1 - ); // Min out of bounds for ethToUsd - - assertTrue( - IBorrowControllerLatest(0x44B7895989Bc7886423F06DeAa844D413384b0d6) - .isPriceStale(address(market)) - ); - assertFalse( - IBorrowControllerLatest(0x44B7895989Bc7886423F06DeAa844D413384b0d6) - .isBelowMinDebt(address(market), user, 1300 ether) - ); // Minimum debt is 1250 DOLA - - _mockChainlinkPrice( - IChainlinkFeed(address(newFeed.usdcToUsd())), - IAggregator(newFeed.usdcToUsd().aggregator()).maxAnswer() + 1 - ); // Max out of bounds - _mockChainlinkPrice( - IChainlinkFeed(address(newFeed.ethToUsd())), - IAggregator(newFeed.ethToUsd().aggregator()).minAnswer() - 1 - ); // Min out of bounds for ethToUsd - - vm.expectRevert("Denied by borrow controller"); - market.borrow(1300 ether); - } - - function testBorrow_Fails_if_ethToUsd_MAX_out_of_bounds_when_usdcToUsd_MAX_out_of_bounds() - public - { - _setNewBorrowController(); - - testAmount = 300 ether; - gibCollateral(user, testAmount); - gibDBR(user, testAmount); - vm.startPrank(user, user); - deposit(testAmount); - - _mockChainlinkPrice( - IChainlinkFeed(address(newFeed.usdcToUsd())), - IAggregator(newFeed.usdcToUsd().aggregator()).maxAnswer() + 1 - ); // Max out of bounds - _mockChainlinkPrice( - IChainlinkFeed(address(newFeed.ethToUsd())), - type(int192).max - ); // Max out of bounds - - assertTrue( - IBorrowControllerLatest(0x44B7895989Bc7886423F06DeAa844D413384b0d6) - .isPriceStale(address(market)) - ); - assertFalse( - IBorrowControllerLatest(0x44B7895989Bc7886423F06DeAa844D413384b0d6) - .isBelowMinDebt(address(market), user, 1300 ether) - ); // Minimum debt is 1250 DOLA - - vm.expectRevert("Denied by borrow controller"); - market.borrow(1300 ether); - } - - function testBorrow_if_price_NOT_stale_ethToUsd_when_usdcToUsd_MAX_out_of_bounds() - public - { - _setNewBorrowController(); - - testAmount = 300 ether; - gibCollateral(user, testAmount); - gibDBR(user, testAmount); - vm.startPrank(user, user); - deposit(testAmount); - - _mockChainlinkPrice( - IChainlinkFeed(address(newFeed.usdcToUsd())), - IAggregator(newFeed.usdcToUsd().aggregator()).maxAnswer() + 1 - ); // Max out of bounds - - assertFalse( - IBorrowControllerLatest(0x44B7895989Bc7886423F06DeAa844D413384b0d6) - .isPriceStale(address(market)) - ); - assertFalse( - IBorrowControllerLatest(0x44B7895989Bc7886423F06DeAa844D413384b0d6) - .isBelowMinDebt(address(market), user, 1300 ether) - ); // Minimum debt is 1250 DOLA - (, , , uint updatedAt, ) = newFeed.usdcToUsdFallbackOracle(); - (, int price, , uint updated, ) = newFeed.ethToUsd().latestRoundData(); - emit log_int(price); - emit log_uint(updated); - emit log_uint(block.timestamp - updated); - - assertGt(updatedAt, 0, "Price out of bounds on fallback"); - - market.borrow(1300 ether); - assertEq(DOLA.balanceOf(user), 1300 ether); - } - - function testBorrow_if_price_NOT_stale_ethToUsd_when_usdcToUsd_MIN_out_of_bounds() - public - { - _setNewBorrowController(); - - testAmount = 300 ether; - gibCollateral(user, testAmount); - gibDBR(user, testAmount); - vm.startPrank(user, user); - deposit(testAmount); - - _mockChainlinkPrice( - IChainlinkFeed(address(newFeed.usdcToUsd())), - IAggregator(newFeed.usdcToUsd().aggregator()).minAnswer() - 1 - ); // min out of bounds - - assertFalse( - IBorrowControllerLatest(0x44B7895989Bc7886423F06DeAa844D413384b0d6) - .isPriceStale(address(market)) - ); - assertFalse( - IBorrowControllerLatest(0x44B7895989Bc7886423F06DeAa844D413384b0d6) - .isBelowMinDebt(address(market), user, 1300 ether) - ); // Minimum debt is 1250 DOLA - (, , , uint updatedAt, ) = newFeed.usdcToUsdFallbackOracle(); - assertGt(updatedAt, 0, "Price out of bounds on fallback"); - - market.borrow(1300 ether); - assertEq(DOLA.balanceOf(user), 1300 ether); - } - - function testDepositAndBorrow_Fails() public { - gibCollateral(user, testAmount); - gibDBR(user, testAmount); - vm.prank(gov); - market.pauseBorrows(true); - - vm.startPrank(user, user); - - uint borrowAmount = 1; - collateral.approve(address(market), testAmount); - vm.expectRevert(); - market.depositAndBorrow(testAmount, borrowAmount); - } - - function testBorrowOnBehalf_Fails() public { - address userPk = vm.addr(1); - gibCollateral(userPk, testAmount); - gibDBR(userPk, testAmount); - - vm.startPrank(userPk, userPk); - uint maxBorrowAmount = 1; - bytes32 hash = keccak256( - abi.encodePacked( - "\x19\x01", - market.DOMAIN_SEPARATOR(), - keccak256( - abi.encode( - keccak256( - "BorrowOnBehalf(address caller,address from,uint256 amount,uint256 nonce,uint256 deadline)" - ), - user2, - userPk, - maxBorrowAmount, - 0, - block.timestamp - ) - ) - ) - ); - (uint8 v, bytes32 r, bytes32 s) = vm.sign(1, hash); - - deposit(testAmount); - vm.stopPrank(); - - vm.prank(gov); - market.pauseBorrows(true); - - vm.startPrank(user2, user2); - vm.expectRevert(); - market.borrowOnBehalf( - userPk, - maxBorrowAmount, - block.timestamp, - v, - r, - s - ); - } - - function testRepay_Fails_WhenAmountGtThanDebt() public { - gibCollateral(user, testAmount); - gibDBR(user, testAmount); - gibDOLA(user, 500e18); - - vm.startPrank(user, user); - - deposit(testAmount); - - vm.expectRevert("Repayment greater than debt"); - market.repay(user, 1); - } - - function testGetWithdrawalLimit_Returns_CollateralBalance() public { - gibCollateral(user, testAmount); - gibDBR(user, testAmount); - - vm.startPrank(user, user); - deposit(testAmount); - - uint collateralBalance = market.escrows(user).balance(); - assertLe( - collateralBalance, - testAmount, - "Is less than or equal deposit amount due to rounding errors" - ); - assertGt( - collateralBalance + 10, - testAmount, - "Is greater than deposit amount when adjusted slightly up" - ); - assertEq( - market.getWithdrawalLimit(user), - collateralBalance, - "Should return collateralBalance when user's escrow balance > 0 & debts = 0" - ); - } - - function testGetWithdrawalLimit_ReturnsHigherBalance_WhenTimePassed() - public - { - gibCollateral(user, testAmount); - gibDBR(user, testAmount); - - vm.startPrank(user, user); - deposit(testAmount); - - uint collateralBalanceBefore = market.escrows(user).balance(); - vm.roll(block.number + 1); - uint collateralBalanceAfter = market.escrows(user).balance(); - assertGt( - collateralBalanceAfter, - collateralBalanceBefore, - "Collateral balance didn't increase" - ); - assertGt( - collateralBalanceAfter, - testAmount, - "Collateral balance not greater than testAmount" - ); - assertEq( - market.getWithdrawalLimit(user), - collateralBalanceAfter, - "Should return collateralBalance when user's escrow balance > 0 & debts = 0" - ); - } - - function testGetWithdrawalLimit_Returns_0_WhenEscrowBalanceIs0() public { - gibCollateral(user, testAmount); - gibDBR(user, testAmount); - - vm.startPrank(user, user); - deposit(testAmount); - - uint collateralBalance = market.escrows(user).balance(); - assertLe( - collateralBalance, - testAmount, - "Is less than or equal deposit amount due to rounding errors" - ); - assertGt( - collateralBalance + 10, - testAmount, - "Is greater than deposit amount when adjusted slightly up" - ); - - market.withdraw(market.getWithdrawalLimit(user)); - assertLt( - market.getWithdrawalLimit(user), - 10, - "Should return dust when user's escrow balance is emptied" - ); - } - - function testPauseBorrows() public { - vm.startPrank(gov); - - market.pauseBorrows(true); - assertEq(market.borrowPaused(), true, "Market wasn't paused"); - market.pauseBorrows(false); - assertEq(market.borrowPaused(), false, "Market wasn't unpaused"); - - vm.stopPrank(); - vm.startPrank(pauseGuardian); - market.pauseBorrows(true); - assertEq(market.borrowPaused(), true, "Market wasn't paused"); - vm.expectRevert(onlyGovUnpause); - market.pauseBorrows(false); - vm.stopPrank(); - - vm.startPrank(user, user); - vm.expectRevert(onlyPauseGuardianOrGov); - market.pauseBorrows(true); - - vm.expectRevert(onlyGovUnpause); - market.pauseBorrows(false); - } - - function testWithdraw() public { - gibCollateral(user, testAmount); - gibDBR(user, testAmount); - vm.startPrank(user, user); - - deposit(testAmount); - - assertEq(collateral.balanceOf(user), 0, "failed to deposit collateral"); - - market.withdraw(market.getWithdrawalLimit(user)); - - assertLt( - market.predictEscrow(user).balance(), - testAmount, - "failed to withdraw collateral" - ); - assertLe( - collateral.balanceOf(user), - testAmount, - "Is less than or equal deposit amount due to rounding errors" - ); - assertGt( - collateral.balanceOf(user) + 10, - testAmount, - "Is greater than deposit amount when adjusted slightly up" - ); - } - - function testWithdraw_When_TimePassed() public { - gibCollateral(user, testAmount); - gibDBR(user, testAmount); - vm.startPrank(user, user); - - deposit(testAmount); - - assertEq(collateral.balanceOf(user), 0, "failed to deposit collateral"); - - vm.roll(block.number + 1); - market.withdraw(testAmount); - - assertGt(market.predictEscrow(user).balance(), 0, "Escrow is empty"); - assertEq( - collateral.balanceOf(user), - testAmount, - "failed to withdraw collateral" - ); - } - - function testWithdrawOnBehalf() public { - address userPk = vm.addr(1); - gibCollateral(userPk, testAmount); - gibDBR(userPk, testAmount); - - vm.startPrank(userPk); - deposit(testAmount); - bytes32 hash = keccak256( - abi.encodePacked( - "\x19\x01", - market.DOMAIN_SEPARATOR(), - keccak256( - abi.encode( - keccak256( - "WithdrawOnBehalf(address caller,address from,uint256 amount,uint256 nonce,uint256 deadline)" - ), - user2, - userPk, - market.getWithdrawalLimit(userPk), - 0, - block.timestamp - ) - ) - ) - ); - (uint8 v, bytes32 r, bytes32 s) = vm.sign(1, hash); - vm.stopPrank(); - - vm.startPrank(user2); - market.withdrawOnBehalf( - userPk, - market.getWithdrawalLimit(userPk), - block.timestamp, - v, - r, - s - ); - - assertLe( - collateral.balanceOf(user2), - testAmount, - "Is less than or equal deposit amount due to rounding errors" - ); - assertGt( - collateral.balanceOf(user2) + 10, - testAmount, - "Is greater than deposit amount when adjusted slightly up" - ); - } - - function testWithdrawOnBehalf_When_InvalidateNonceCalledPrior() public { - address userPk = vm.addr(1); - gibCollateral(userPk, testAmount); - gibDBR(userPk, testAmount); - - vm.startPrank(userPk); - bytes32 hash = keccak256( - abi.encodePacked( - "\x19\x01", - market.DOMAIN_SEPARATOR(), - keccak256( - abi.encode( - keccak256( - "WithdrawOnBehalf(address caller,address from,uint256 amount,uint256 nonce,uint256 deadline)" - ), - user2, - userPk, - testAmount, - 0, - block.timestamp - ) - ) - ) - ); - (uint8 v, bytes32 r, bytes32 s) = vm.sign(1, hash); - - deposit(testAmount); - market.invalidateNonce(); - vm.stopPrank(); - - vm.startPrank(user2); - vm.expectRevert("INVALID_SIGNER"); - market.withdrawOnBehalf(userPk, testAmount, block.timestamp, v, r, s); - } - - function testWithdrawOnBehalf_When_DeadlineHasPassed() public { - address userPk = vm.addr(1); - gibCollateral(userPk, testAmount); - gibDBR(userPk, testAmount); - - uint timestamp = block.timestamp; - - vm.startPrank(userPk); - bytes32 hash = keccak256( - abi.encodePacked( - "\x19\x01", - market.DOMAIN_SEPARATOR(), - keccak256( - abi.encode( - keccak256( - "WithdrawOnBehalf(address caller,address from,uint256 amount,uint256 nonce,uint256 deadline)" - ), - user2, - userPk, - testAmount, - 0, - timestamp - ) - ) - ) - ); - (uint8 v, bytes32 r, bytes32 s) = vm.sign(1, hash); - - deposit(testAmount); - market.invalidateNonce(); - vm.stopPrank(); - - vm.startPrank(user2); - vm.warp(block.timestamp + 1); - vm.expectRevert("DEADLINE_EXPIRED"); - market.withdrawOnBehalf(userPk, testAmount, timestamp, v, r, s); - } - - //Access Control Tests - - function test_accessControl_setOracle() public { - vm.startPrank(gov); - market.setOracle(IOracle(address(0))); - vm.stopPrank(); - - vm.expectRevert(onlyGov); - market.setOracle(IOracle(address(0))); - } - - function test_accessControl_setBorrowController() public { - vm.startPrank(gov); - market.setBorrowController(IBorrowController(address(0))); - vm.stopPrank(); - - vm.expectRevert(onlyGov); - market.setBorrowController(IBorrowController(address(0))); - } - - function test_accessControl_setGov() public { - vm.startPrank(gov); - market.setGov(address(0)); - vm.stopPrank(); - - vm.expectRevert(onlyGov); - market.setGov(address(0)); - } - - function test_accessControl_setLender() public { - vm.startPrank(gov); - market.setLender(address(0)); - vm.stopPrank(); - - vm.expectRevert(onlyGov); - market.setLender(address(0)); - } - - function test_accessControl_setPauseGuardian() public { - vm.startPrank(gov); - market.setPauseGuardian(address(0)); - vm.stopPrank(); - - vm.expectRevert(onlyGov); - market.setPauseGuardian(address(0)); - } - - function test_accessControl_setCollateralFactorBps() public { - vm.startPrank(gov); - market.setCollateralFactorBps(100); - - vm.expectRevert("Invalid collateral factor"); - market.setCollateralFactorBps(10001); - vm.stopPrank(); - - vm.expectRevert(onlyGov); - market.setCollateralFactorBps(100); - } - - function test_accessControl_setReplenismentIncentiveBps() public { - vm.startPrank(gov); - market.setReplenismentIncentiveBps(100); - - vm.expectRevert("Invalid replenishment incentive"); - market.setReplenismentIncentiveBps(10001); - vm.stopPrank(); - - vm.expectRevert(onlyGov); - market.setReplenismentIncentiveBps(100); - } - - function test_accessControl_setLiquidationIncentiveBps() public { - vm.startPrank(gov); - market.setLiquidationIncentiveBps(100); - - vm.expectRevert("Invalid liquidation incentive"); - market.setLiquidationIncentiveBps(0); - vm.stopPrank(); - - vm.expectRevert(onlyGov); - market.setLiquidationIncentiveBps(100); - } - - function test_accessControl_setLiquidationFactorBps() public { - vm.startPrank(gov); - market.setLiquidationFactorBps(100); - - vm.expectRevert("Invalid liquidation factor"); - market.setLiquidationFactorBps(0); - vm.stopPrank(); - - vm.expectRevert(onlyGov); - market.setLiquidationFactorBps(100); - } - - function test_accessControl_setLiquidationFeeBps() public { - vm.startPrank(gov); - market.setLiquidationFeeBps(100); - - vm.expectRevert("Invalid liquidation fee"); - market.setLiquidationFeeBps(10001); - vm.stopPrank(); - - vm.expectRevert(onlyGov); - market.setLiquidationFeeBps(100); - } - - function _mockChainlinkPrice( - IChainlinkFeed clFeed, - int mockPrice - ) internal { - ( - uint80 roundId, - , - uint startedAt, - uint updatedAt, - uint80 answeredInRound - ) = clFeed.latestRoundData(); - vm.mockCall( - address(clFeed), - abi.encodeWithSelector(IChainlinkFeed.latestRoundData.selector), - abi.encode( - roundId, - mockPrice, - startedAt, - updatedAt, - answeredInRound - ) - ); - } - - function _mockChainlinkUpdatedAt( - IChainlinkFeed clFeed, - int updatedAtDelta - ) internal { - ( - uint80 roundId, - int price, - uint startedAt, - uint updatedAt, - uint80 answeredInRound - ) = clFeed.latestRoundData(); - vm.mockCall( - address(clFeed), - abi.encodeWithSelector(IChainlinkFeed.latestRoundData.selector), - abi.encode( - roundId, - price, - startedAt, - uint(int(updatedAt) + updatedAtDelta), - answeredInRound - ) - ); - } - - function _setNewBorrowController() internal { - vm.startPrank(gov); - market.setBorrowController( - IBorrowController(0x44B7895989Bc7886423F06DeAa844D413384b0d6) - ); - BorrowController(address(market.borrowController())).setDailyLimit( - address(market), - 30000 ether - ); // at the current block there's there's only 75 Dola to borrow - vm.stopPrank(); - } -}