diff --git a/.github/workflows/foundry-test.yml b/.github/workflows/foundry-test.yml index ee4f93c36..814dfb841 100644 --- a/.github/workflows/foundry-test.yml +++ b/.github/workflows/foundry-test.yml @@ -44,6 +44,6 @@ jobs: - name: Foundry Test working-directory: ./packages/contracts/ - run: forge test -vvv ${{ github.base_ref == 'main' && '--fuzz-runs 200' || '' }} + run: forge test -vv ${{ github.base_ref == 'main' && '--fuzz-runs 200' || '' }} env: - MAINNET_RPC_URL: ${{ secrets.MAINNET_RPC_URL }} \ No newline at end of file + MAINNET_RPC_URL: ${{ secrets.MAINNET_RPC_URL }} diff --git a/packages/contracts/contracts/ChronicleAdapter.sol b/packages/contracts/contracts/ChronicleAdapter.sol new file mode 100644 index 000000000..954356580 --- /dev/null +++ b/packages/contracts/contracts/ChronicleAdapter.sol @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.17; + +import {IPriceFetcher} from "./Interfaces/IOracleCaller.sol"; +import {IChronicle} from "./Interfaces/IChronicle.sol"; + +/// @notice Chronicle oracle adapter for EbtcFeed +/// @notice https://etherscan.io/address/0x02238bb0085395ae52cd4755456891fc2fd5934d +contract ChronicleAdapter is IPriceFetcher { + uint256 public constant MAX_STALENESS = 24 hours; + uint256 public constant ADAPTER_PRECISION = 1e18; + + address public immutable BTC_STETH_FEED; + uint256 public immutable FEED_PRECISION; + + constructor(address _btcStEthFeed) { + BTC_STETH_FEED = _btcStEthFeed; + + uint256 feedDecimals = IChronicle(BTC_STETH_FEED).decimals(); + require(feedDecimals > 0 && feedDecimals <= 18); + + FEED_PRECISION = 10 ** feedDecimals; + } + + function fetchPrice() external returns (uint256) { + (uint256 price, uint256 age) = IChronicle(BTC_STETH_FEED).readWithAge(); + uint256 staleness = block.timestamp - age; + if (staleness > MAX_STALENESS) revert("ChronicleAdapter: stale price"); + + return (price * ADAPTER_PRECISION) / FEED_PRECISION; + } +} diff --git a/packages/contracts/contracts/Interfaces/IChronicle.sol b/packages/contracts/contracts/Interfaces/IChronicle.sol new file mode 100644 index 000000000..dca6a6e79 --- /dev/null +++ b/packages/contracts/contracts/Interfaces/IChronicle.sol @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.17; + +/// @title IChronicle +/// @author chronicleprotocol (https://github.com/chronicleprotocol/chronicle-std/blob/ea9afe78a1d33245afcdbcc3f530ee9cbd7cde28/src/IChronicle.sol) +/// @notice Partial interface for Chronicle Protocol's oracle products. +interface IChronicle { + /// @notice Returns the oracle's current value and its age. + /// @dev Reverts if no value set. + /// @return value The oracle's current value. + /// @return age The value's age. + function readWithAge() external view returns (uint256 value, uint256 age); + + /// @notice Returns the oracle's decimals. + /// @return The decimals of the oracle. + function decimals() external view returns (uint8); +} diff --git a/packages/contracts/contracts/LeverageMacroBase.sol b/packages/contracts/contracts/LeverageMacroBase.sol index 4581718fe..fb864f68a 100644 --- a/packages/contracts/contracts/LeverageMacroBase.sol +++ b/packages/contracts/contracts/LeverageMacroBase.sol @@ -279,11 +279,11 @@ abstract contract LeverageMacroBase { // Early return return; } else if (check.operator == Operator.gte) { - require(check.value >= valueToCheck, "!LeverageMacroReference: gte post check"); + require(valueToCheck >= check.value, "!LeverageMacroBase: gte post check"); } else if (check.operator == Operator.lte) { - require(check.value <= valueToCheck, "!LeverageMacroReference: let post check"); + require(valueToCheck <= check.value, "!LeverageMacroBase: lte post check"); } else if (check.operator == Operator.equal) { - require(check.value == valueToCheck, "!LeverageMacroReference: equal post check"); + require(check.value == valueToCheck, "!LeverageMacroBase: equal post check"); } else { revert("Operator not found"); } diff --git a/packages/contracts/contracts/TestContracts/CollateralTokenTester.sol b/packages/contracts/contracts/TestContracts/CollateralTokenTester.sol index 40544e5c7..32b11278b 100644 --- a/packages/contracts/contracts/TestContracts/CollateralTokenTester.sol +++ b/packages/contracts/contracts/TestContracts/CollateralTokenTester.sol @@ -40,6 +40,8 @@ contract CollateralTokenTester is ICollateralToken, ICollateralTokenOracle, Owna uint256 private slotsPerEpoch = 32; uint256 private secondsPerSlot = 12; + bool public submitShouldRevert; + receive() external payable { deposit(); } @@ -53,6 +55,18 @@ contract CollateralTokenTester is ICollateralToken, ICollateralTokenOracle, Owna emit Deposit(msg.sender, msg.value, _share); } + function submit(address _referral) public payable returns (uint256) { + if (submitShouldRevert) { + revert(); + } + + deposit(); + } + + function setSubmitShouldRevert(bool _submitShouldRevert) public { + submitShouldRevert = _submitShouldRevert; + } + /// @dev Deposit collateral without ether for testing purposes function forceDeposit(uint256 ethToDeposit) external { if (!isUncappedMinter[msg.sender]) { diff --git a/packages/contracts/contracts/TestContracts/MockAggregator.sol b/packages/contracts/contracts/TestContracts/MockAggregator.sol index 946d2a677..c0eb81dfb 100644 --- a/packages/contracts/contracts/TestContracts/MockAggregator.sol +++ b/packages/contracts/contracts/TestContracts/MockAggregator.sol @@ -131,6 +131,10 @@ contract MockAggregator is AggregatorV3Interface { return (prevRoundId, prevPrice, 0, updateTime, 0); } + function readWithAge() external view returns (uint256 value, uint256 age) { + return (uint256(price), updateTime); + } + function description() external pure override returns (string memory) { return ""; } diff --git a/packages/contracts/foundry_test/CDPManager.redemptions.t.sol b/packages/contracts/foundry_test/CDPManager.redemptions.t.sol index 831daa59c..63dcd690c 100644 --- a/packages/contracts/foundry_test/CDPManager.redemptions.t.sol +++ b/packages/contracts/foundry_test/CDPManager.redemptions.t.sol @@ -87,7 +87,7 @@ contract CDPManagerRedemptionsTest is eBTCBaseInvariants { } function testMultipleRedemption(uint256 _cdpNumber, uint256 _collAmt) public { - _cdpNumber = bound(_cdpNumber, 2, 1000); + _cdpNumber = bound(_cdpNumber, 2, 500); _collAmt = bound(_collAmt, 22e17 + 1, 10000e18); uint256 _price = priceFeedMock.getPrice(); diff --git a/packages/contracts/foundry_test/ChronicleAdapter.t.sol b/packages/contracts/foundry_test/ChronicleAdapter.t.sol new file mode 100644 index 000000000..da145f2ab --- /dev/null +++ b/packages/contracts/foundry_test/ChronicleAdapter.t.sol @@ -0,0 +1,44 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.17; + +import "forge-std/Test.sol"; +import {MockAggregator} from "../contracts/TestContracts/MockAggregator.sol"; +import {ChronicleAdapter} from "../contracts/ChronicleAdapter.sol"; + +contract ChainlinkAdapterTest is Test { + MockAggregator internal stEthBtcAggregator; + ChronicleAdapter internal chronicleAdapter; + + constructor() {} + + function testSuccessPrecision8() public { + stEthBtcAggregator = new MockAggregator(8); + chronicleAdapter = new ChronicleAdapter(address(stEthBtcAggregator)); + + stEthBtcAggregator.setUpdateTime(block.timestamp); + stEthBtcAggregator.setPrice(5341495); + + assertEq(chronicleAdapter.fetchPrice(), 53414950000000000); + } + + function testSuccessPrecision18() public { + stEthBtcAggregator = new MockAggregator(18); + chronicleAdapter = new ChronicleAdapter(address(stEthBtcAggregator)); + + stEthBtcAggregator.setUpdateTime(block.timestamp); + stEthBtcAggregator.setPrice(53414952714851023); + + assertEq(chronicleAdapter.fetchPrice(), 53414952714851023); + } + + function testFailureFreshness() public { + stEthBtcAggregator = new MockAggregator(18); + chronicleAdapter = new ChronicleAdapter(address(stEthBtcAggregator)); + + stEthBtcAggregator.setUpdateTime(block.timestamp - 24 hours - 1); + stEthBtcAggregator.setPrice(100e18); + + vm.expectRevert("ChronicleAdapter: stale price"); + chronicleAdapter.fetchPrice(); + } +} diff --git a/packages/contracts/foundry_test/SimplifiedDiamondLikeLeverage.t.sol b/packages/contracts/foundry_test/SimplifiedDiamondLikeLeverage.t.sol index 355a77c3c..7e7c2526d 100644 --- a/packages/contracts/foundry_test/SimplifiedDiamondLikeLeverage.t.sol +++ b/packages/contracts/foundry_test/SimplifiedDiamondLikeLeverage.t.sol @@ -453,7 +453,8 @@ contract SimplifiedDiamondLikeLeverageTests is eBTCBaseInvariants { uint256 debt = 1e18; uint256 margin = 5 ether; uint256 flAmount = _debtToCollateral(debt); - uint256 totalCollateral = ((flAmount + margin) * 9995) / 1e4; + // remove 3 basis points from flAmount to account for flash loan fee + uint256 totalCollateral = ((flAmount * 9997) / 10000) + margin; LeverageMacroBase.OpenCdpForOperation memory cdp; @@ -477,6 +478,15 @@ contract SimplifiedDiamondLikeLeverageTests is eBTCBaseInvariants { address(eBTCToken), address(collateral), debt + ), + _postCheckParams: _getPostCheckParams( + bytes32(0), + debt, + // expected collateral should exclude LIQUIDATOR_REWARD and be converted to shares + collateral.getSharesByPooledEth( + totalCollateral - borrowerOperations.LIQUIDATOR_REWARD() + ), + ICdpManagerData.Status.active ) }); } @@ -505,7 +515,8 @@ contract SimplifiedDiamondLikeLeverageTests is eBTCBaseInvariants { LeverageMacroBase.OpenCdpForOperation memory _cdp, uint256 _flAmount, uint256 _stEthBalance, - bytes memory _exchangeData + bytes memory _exchangeData, + LeverageMacroBase.PostCheckParams memory _postCheckParams ) internal { LeverageMacroBase.LeverageMacroOperation memory op; @@ -521,12 +532,7 @@ contract SimplifiedDiamondLikeLeverageTests is eBTCBaseInvariants { _flAmount, op, LeverageMacroBase.PostOperationCheck.openCdp, - _getPostCheckParams( - bytes32(0), - _cdp.eBTCToMint, - _cdp.stETHToDeposit, - ICdpManagerData.Status.active - ) + _postCheckParams ); }