diff --git a/src/feeds/ERC4626Feed.sol b/src/feeds/ERC4626Feed.sol new file mode 100644 index 00000000..b1842f05 --- /dev/null +++ b/src/feeds/ERC4626Feed.sol @@ -0,0 +1,111 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.4; + +import {IChainlinkCurveFeed} from "src/interfaces/IChainlinkCurveFeed.sol"; +import {IERC4626} from "lib/openzeppelin-contracts/contracts/interfaces/IERC4626.sol"; +import {IERC20Metadata} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; +import {FixedPointMathLib} from "solmate/utils/FixedPointMathLib.sol"; + +/// @title ERC4626Feed +/// @notice This contract is a generalized contract for an ERC4626 vault which has a feed in a Normalized Asset to USD price +/// @dev It will convert the normalized asset to USD price to the Asset to USD price using the vault's rate +/// @dev This is contract is meant to be used in combination with ChainlinkCurveFeed or ChainlinkCurve2CoinsFeed contracts. +/// @dev Underlying asset decimals must be 18 or lower. + +contract ERC4626Feed { + using FixedPointMathLib for uint256; + error DecimalsMismatch(); + + // ChainlinkCurve feed for the normalized asset to USD price + IChainlinkCurveFeed public immutable feed; + // ERC4626 vault asset + IERC4626 public immutable vault; + // Asset scale + uint256 public immutable assetScale; + // Scaling factor + uint256 public constant SCALE = 1e18; + // Description of the feed + string public description; + + constructor(address _vault, address _feed) { + feed = IChainlinkCurveFeed(_feed); + vault = IERC4626(_vault); + assetScale = 10 ** IERC20Metadata(vault.asset()).decimals(); + + if ( + feed.decimals() != 18 || + vault.decimals() != 18 || + assetScale > SCALE + ) revert DecimalsMismatch(); + + description = string( + abi.encodePacked( + feed.description(), + " using ", + vault.symbol(), + " vault rate" + ) + ); + } + + /** + * @return roundId The round ID from the feed + * @return assetToUsdPrice The latest asset price in USD + * @return startedAt The timestamp when the latest round of feed started + * @return updatedAt The timestamp when the latest round of 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 normalizedAssetToUsdPrice, + uint256 startedAt, + uint256 updatedAt, + uint80 answeredInRound + ) = feed.latestRoundData(); + + uint256 assetToUnderlyingRate; + + try vault.previewRedeem(SCALE) returns (uint256 rate) { + assetToUnderlyingRate = rate.divWadDown(assetScale); + } catch { + assetToUnderlyingRate = vault.convertToAssets(SCALE).divWadDown( + assetScale + ); + } + + // Multiply Normalized Asset/USD price by asset/underlying rate to get Asset/USD price + int256 assetToUsdPrice = int256( + (uint256(normalizedAssetToUsdPrice) * assetToUnderlyingRate) / SCALE + ); + + return ( + roundId, + assetToUsdPrice, + startedAt, + updatedAt, + answeredInRound + ); + } + + /** + @notice Retrieves the latest asset price + @return price The latest asset price + */ + function latestAnswer() external view returns (int256) { + (, int256 price, , , ) = latestRoundData(); + return price; + } + + /** + * @notice Retrieves number of decimals for the price feed + * @return decimals The number of decimals for the price feed + */ + function decimals() public pure returns (uint8) { + return 18; + } +} diff --git a/src/interfaces/IChainlinkCurveFeed.sol b/src/interfaces/IChainlinkCurveFeed.sol new file mode 100644 index 00000000..1b44f91b --- /dev/null +++ b/src/interfaces/IChainlinkCurveFeed.sol @@ -0,0 +1,31 @@ +pragma solidity ^0.8.13; + +import {IChainlinkBasePriceFeed} from "src/interfaces/IChainlinkFeed.sol"; +import {ICurvePool} from "src/interfaces/ICurvePool.sol"; + +interface IChainlinkCurveFeed { + function decimals() external view returns (uint8 decimals); + + function latestRoundData() + external + view + returns ( + uint80 roundId, + int256 crvUsdPrice, + uint256 startedAt, + uint256 updatedAt, + uint80 answeredInRound + ); + + function latestAnswer() external view returns (int256 price); + + function description() external view returns (string memory description); + + function assetToUsd() external view returns (IChainlinkBasePriceFeed feed); + + function curvePool() external view returns (ICurvePool pool); + + function targetIndex() external view returns (uint256 index); + + function assetOrTargetK() external view returns (uint256 k); +} diff --git a/src/interfaces/ICurvePool.sol b/src/interfaces/ICurvePool.sol index cce8cf5b..3bbe0e00 100644 --- a/src/interfaces/ICurvePool.sol +++ b/src/interfaces/ICurvePool.sol @@ -75,4 +75,6 @@ interface ICurvePool { function symbol() external view returns (string memory); function lp_token() external view returns (address); + + function decimals() external view returns (uint256); } diff --git a/test/feedForkTests/SdeUSDFeedFork.t.sol b/test/feedForkTests/SdeUSDFeedFork.t.sol new file mode 100644 index 00000000..94a11690 --- /dev/null +++ b/test/feedForkTests/SdeUSDFeedFork.t.sol @@ -0,0 +1,192 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.19; + +import "forge-std/Test.sol"; +import "src/feeds/ERC4626Feed.sol"; +import "src/interfaces/IChainlinkFeed.sol"; +import {ChainlinkCurveFeed, ICurvePool} from "src/feeds/ChainlinkCurveFeed.sol"; +import "forge-std/console.sol"; +import {ERC20 as ERC20Mock} from "test/mocks/ERC20.sol"; +import {ERC4626, ERC20} from "@openzeppelin/contracts/token/ERC20/extensions/ERC4626.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +contract Mock4626 is ERC4626 { + constructor(IERC20 _asset) ERC20("MOCK", "MOCK") ERC4626(_asset) {} + + function _decimalsOffset() internal view override returns (uint8) { + return 12; + } +} + +contract SdeUSDFeedForkTest is Test { + ChainlinkCurveFeed curveFeed; + ERC4626Feed feed; + address curvePool = address(0x82202CAEC5E6d85014eADC68D4912F3C90093e7C); + uint256 k = 0; + uint256 targetIndex = 1; + address sdeUSD = address(0x5C5b196aBE0d54485975D1Ec29617D42D9198326); + address dolaFeed = address(0x6255981e2a1EBeA600aFC506185590eD383517be); + + ERC20Mock mock6Decimals; + Mock4626 mock6Vault; + ERC4626Feed feed6Decimals; + + function setUp() public { + string memory url = vm.rpcUrl("mainnet"); + vm.createSelectFork(url); + curveFeed = new ChainlinkCurveFeed(dolaFeed, curvePool, k, targetIndex); + feed = new ERC4626Feed(sdeUSD, address(curveFeed)); + } + + function test_6Decimals_underlying_asset_Feed_returns_18() public { + mock6Decimals = new ERC20Mock("Mock6", "M6", 6); + mock6Vault = new Mock4626(IERC20(address(mock6Decimals))); + assertEq(mock6Vault.decimals(), 18); + + feed6Decimals = new ERC4626Feed( + address(mock6Vault), + address(curveFeed) + ); + uint256 amount = 10000e6; + assertEq(feed6Decimals.decimals(), 18); + assertEq(feed6Decimals.assetScale(), 1e6); + + mock6Decimals.mint(address(this), amount); + mock6Decimals.approve(address(mock6Vault), amount); + mock6Vault.deposit(amount, address(this)); + + assertEq(mock6Vault.convertToAssets(1e18), 1e6); + assertEq(mock6Vault.previewRedeem(1e18), 1e6); + assertEq(mock6Vault.convertToShares(1e6), 1e18); + assertEq(mock6Vault.previewDeposit(1e6), 1e18); + + uint256 sdeUSDNormalizedToDola = ICurvePool(curvePool).price_oracle( + curveFeed.assetOrTargetK() + ); + + int256 dolaToUsdPrice = curveFeed.assetToUsd().latestAnswer(); + int256 sdeUSDNormalizedToUsdPrice = int256( + (sdeUSDNormalizedToDola * uint(dolaToUsdPrice)) / 1e18 + ); + + uint256 sdeUSDToDeUSDRate = mock6Vault.previewRedeem(1e18); + + uint256 price = (uint(sdeUSDNormalizedToUsdPrice) * sdeUSDToDeUSDRate) / + 1e6; + assertEq(feed6Decimals.latestAnswer(), int256(price)); + } + + function test_decimals() public { + assertEq(feed.decimals(), 18); + assertEq(feed.assetScale(), 1e18); + } + + function test_description() public { + assertEq(feed.description(), "sdeUSD / USD using sdeUSD vault rate"); + } + + function test_latestRoundData() public { + ( + uint80 roundId, + int256 sdeUSDToUsdPrice, + uint256 startedAt, + uint256 updatedAt, + uint80 answeredInRound + ) = feed.latestRoundData(); + assertEq(feed.latestAnswer(), _calculateSdeUSDPrice()); + ( + uint80 roundId2, + , + uint256 startedAt2, + uint256 updatedAt2, + uint80 answeredInRound2 + ) = IChainlinkBasePriceFeed(dolaFeed).latestRoundData(); + // Data are + assertEq(roundId, roundId2); + assertEq(sdeUSDToUsdPrice, _calculateSdeUSDPrice()); + assertEq(startedAt, startedAt2); + assertEq(updatedAt, updatedAt2); + assertEq(answeredInRound, answeredInRound2); + } + + function test_sdeUSD_upward_depeg() public { + int256 answer = feed.latestAnswer(); + assertEq(feed.latestAnswer(), _calculateSdeUSDPrice()); + uint256 mockRate = 2e18; + _mockVaultRate(sdeUSD, mockRate); + assertEq(feed.latestAnswer(), _calculateSdeUSDPrice()); + assertGt(feed.latestAnswer(), answer); + } + + function test_sdeUSD_downward_depeg() public { + int256 answer = feed.latestAnswer(); + assertEq(feed.latestAnswer(), _calculateSdeUSDPrice()); + uint256 mockRate = 0.5e18; + _mockVaultRate(sdeUSD, mockRate); + assertEq(feed.latestAnswer(), _calculateSdeUSDPrice()); + assertLt(feed.latestAnswer(), answer); + } + + function test_previewRedeemRevert_useConvertToAssets() public { + // Mock preview redeem rate + _mockVaultRate(sdeUSD, 2e18); + // Answer is equal to preview redeem rate but not equal to convert to assets + assertEq(feed.latestAnswer(), _calculateSdeUSDPrice()); + assertGt(feed.latestAnswer(), _calculateSdeUSDPriceConvertToAssets()); + // Mock preview redeem revert to use convert to assets + _mockPreviewRevert(sdeUSD); + assertEq(feed.latestAnswer(), _calculateSdeUSDPriceConvertToAssets()); + } + + function _calculateSdeUSDPrice() internal view returns (int256) { + uint256 sdeUSDNormalizedToDola = ICurvePool(curvePool).price_oracle( + curveFeed.assetOrTargetK() + ); + + int256 dolaToUsdPrice = curveFeed.assetToUsd().latestAnswer(); + int256 sdeUSDNormalizedToUsdPrice = int256( + (sdeUSDNormalizedToDola * uint(dolaToUsdPrice)) / 1e18 + ); + + uint256 sdeUSDToDeUSDRate = IERC4626(sdeUSD).previewRedeem(1e18); + return + (sdeUSDNormalizedToUsdPrice * int(sdeUSDToDeUSDRate)) / + int256(1e18); + } + + function _calculateSdeUSDPriceConvertToAssets() + internal + view + returns (int256) + { + uint256 sdeUSDNormalizedToDola = ICurvePool(curvePool).price_oracle( + curveFeed.assetOrTargetK() + ); + + int256 dolaToUsdPrice = curveFeed.assetToUsd().latestAnswer(); + int256 sdeUSDNormalizedToUsdPrice = int256( + (sdeUSDNormalizedToDola * uint(dolaToUsdPrice)) / 1e18 + ); + + uint256 sdeUSDToDeUSDRate = IERC4626(sdeUSD).convertToAssets(1e18); + return + (sdeUSDNormalizedToUsdPrice * int(sdeUSDToDeUSDRate)) / + int256(1e18); + } + + function _mockVaultRate(address vault, uint256 mockRate) internal { + vm.mockCall( + vault, + abi.encodeWithSelector(IERC4626.previewRedeem.selector, 1e18), + abi.encode(mockRate) + ); + } + + function _mockPreviewRevert(address vault) internal { + vm.mockCallRevert( + vault, + abi.encodeWithSelector(IERC4626.previewRedeem.selector, 1e18), + "mock revert" + ); + } +} diff --git a/test/marketForkTests/SdeUSDMarketForkTest.t.sol b/test/marketForkTests/SdeUSDMarketForkTest.t.sol new file mode 100644 index 00000000..7b335e62 --- /dev/null +++ b/test/marketForkTests/SdeUSDMarketForkTest.t.sol @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import "forge-std/Test.sol"; +import "./MarketBaseForkTest.sol"; +import "src/feeds/ERC4626Feed.sol"; +import "src/Market.sol"; +import {ChainlinkCurveFeed} from "src/feeds/ChainlinkCurveFeed.sol"; + +contract SdeUSDMarketForkTest is MarketBaseForkTest { + address curvePool = address(0x82202CAEC5E6d85014eADC68D4912F3C90093e7C); + uint256 k = 0; + uint256 targetIndex = 1; + address sdeUSD = address(0x5C5b196aBE0d54485975D1Ec29617D42D9198326); + address dolaFeed = address(0x6255981e2a1EBeA600aFC506185590eD383517be); + + function setUp() public virtual { + //This will fail if there's no mainnet variable in foundry.toml + string memory url = vm.rpcUrl("mainnet"); + vm.createSelectFork(url, 21880783); + address curveFeed = address( + new ChainlinkCurveFeed(dolaFeed, curvePool, k, targetIndex) + ); + address feedAddr = address(new ERC4626Feed(sdeUSD, curveFeed)); + + address marketAddr = address( + new Market( + gov, + lender, + pauseGuardian, + simpleERC20EscrowAddr, + IDolaBorrowingRights(dbrAddr), + IERC20(sdeUSD), + IOracle(oracleAddr), + 5000, + 1000, + 1000, + false + ) + ); + _advancedInit(marketAddr, feedAddr, false); + } +} diff --git a/test/util/aleTests/ALEBaseSimpleForkTest.t.sol b/test/util/aleTests/ALEBaseSimpleForkTest.t.sol new file mode 100644 index 00000000..e3d5d865 --- /dev/null +++ b/test/util/aleTests/ALEBaseSimpleForkTest.t.sol @@ -0,0 +1,1161 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import "forge-std/Test.sol"; +import "test/marketForkTests/SdeUSDMarketForkTest.t.sol"; +import {ALE} from "src/util/ALE.sol"; + +contract MockExchangeProxy { + IOracle oracle; + IERC20 dola; + + constructor(address _oracle, address _dola) { + oracle = IOracle(_oracle); + dola = IERC20(_dola); + } + + function swapDolaIn( + IERC20 collateral, + uint256 dolaAmount + ) external returns (bool success, bytes memory ret) { + dola.transferFrom(msg.sender, address(this), dolaAmount); + uint256 collateralAmount = (dolaAmount * 1e18) / + oracle.viewPrice(address(collateral), 0); + collateral.transfer(msg.sender, collateralAmount); + success = true; + } + + function swapDolaOut( + IERC20 collateral, + uint256 collateralAmount + ) external returns (bool success, bytes memory ret) { + collateral.transferFrom(msg.sender, address(this), collateralAmount); + uint256 dolaAmount = (collateralAmount * + oracle.viewPrice(address(collateral), 0)) / 1e18; + dola.transfer(msg.sender, dolaAmount); + success = true; + } +} + +interface IFlashMinter { + function setMaxFlashLimit(uint256 limit) external; +} + +abstract contract ALEBaseSimpleForkTest is MarketForkTest { + bytes exceededLimit = "Exceeded credit limit"; + bytes repaymentGtThanDebt = "Repayment greater than debt"; + + error NothingToDeposit(); + + MockExchangeProxy exchangeProxy; + ALE ale; + address triDBR = 0xC7DE47b9Ca2Fc753D6a2F167D8b3e19c6D18b19a; + IFlashMinter flash; + + function getMaxLeverageBorrowAmount( + uint256 collateralAmount, + uint256 iterations + ) internal view returns (uint256) { + uint256 maxDolaAmount = getMaxBorrowAmount(collateralAmount); + uint256 totalDola = maxDolaAmount; + for (uint i = 0; i < iterations; i++) { + uint256 dolaAmount = getMaxBorrowAmount( + convertDolaToCollat(maxDolaAmount) + ); + maxDolaAmount = dolaAmount; + totalDola += dolaAmount; + } + return totalDola; + } + + function test_depositAndLeveragePosition_buyDBR(uint256 amount) public { + vm.assume(amount < 50000 ether); + vm.assume(amount > 0.000001 ether); + // We are going to deposit and leverage the position + // uint amount = 13606; + address userPk = vm.addr(1); + deal(address(market.collateral()), userPk, amount); + + uint maxBorrowAmount = getMaxBorrowAmount(amount) / 10; // we want to borrow only 10% of the max amount to exchange + + // recharge mocked proxy for swap, we need to swap DOLA to collateral + + deal( + address(market.collateral()), + address(exchangeProxy), + convertDolaToCollat(maxBorrowAmount) + ); + + vm.startPrank(userPk, userPk); + + // Calculate the amount of DOLA needed to borrow to buy the DBR needed to cover for the borrowing period + (uint256 dolaForDBR, uint256 dbrAmount) = ale + .approximateDolaAndDbrNeeded(maxBorrowAmount, 365 days, 8); + + // Sign Message for borrow on behalf + 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)" + ), + address(ale), + userPk, + maxBorrowAmount + dolaForDBR, + 0, + block.timestamp + ) + ) + ) + ); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(1, hash); + + ALE.Permit memory permit = ALE.Permit(block.timestamp, v, r, s); + + ALE.DBRHelper memory dbrData = ALE.DBRHelper( + dolaForDBR, + (dbrAmount * 98) / 100, + 0 + ); // DBR buy + + bytes memory swapData = abi.encodeWithSelector( + MockExchangeProxy.swapDolaIn.selector, + collateral, + maxBorrowAmount + ); + + assertEq(dbr.balanceOf(userPk), 0); + + collateral.approve(address(ale), amount); + + // We set amount as initial deposit + ale.depositAndLeveragePosition( + amount, + maxBorrowAmount, + address(market), + address(exchangeProxy), + swapData, + permit, + bytes(""), + dbrData, + false + ); + + // Balance in escrow is equal to the collateral deposited + the extra collateral swapped from the leverage + assertEq( + collateral.balanceOf(address(market.predictEscrow(userPk))), + amount + convertDolaToCollat(maxBorrowAmount) + ); + assertEq(DOLA.balanceOf(userPk), 0); + + assertGt(dbr.balanceOf(userPk), (dbrAmount * 98) / 100); + } + + function test_fail_depositAndLeveragePosition_buyDBR_with_ZERO_deposit() + public + { + // We are going to deposit and leverage the position + uint amount = 1 ether; + address userPk = vm.addr(1); + deal(address(market.collateral()), userPk, amount); + + uint maxBorrowAmount = getMaxBorrowAmount(amount) / 10; // we want to borrow only 10% of the max amount to exchange + + // recharge mocked proxy for swap, we need to swap DOLA to collateral + deal( + address(market.collateral()), + address(exchangeProxy), + convertDolaToCollat(maxBorrowAmount) + ); + + vm.startPrank(userPk, userPk); + + // Calculate the amount of DOLA needed to borrow to buy the DBR needed to cover for the borrowing period + (uint256 dolaForDBR, uint256 dbrAmount) = ale + .approximateDolaAndDbrNeeded(maxBorrowAmount, 365 days, 8); + + // Sign Message for borrow on behalf + 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)" + ), + address(ale), + userPk, + maxBorrowAmount + dolaForDBR, + 0, + block.timestamp + ) + ) + ) + ); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(1, hash); + + ALE.Permit memory permit = ALE.Permit(block.timestamp, v, r, s); + + ALE.DBRHelper memory dbrData = ALE.DBRHelper( + dolaForDBR, + (dbrAmount * 98) / 100, // DBR buy, + 0 // Dola to borrow and withdraw after leverage + ); + + bytes memory swapData = abi.encodeWithSelector( + MockExchangeProxy.swapDolaIn.selector, + collateral, + maxBorrowAmount + ); + + assertEq(dbr.balanceOf(userPk), 0); + + collateral.approve(address(ale), amount); + + // We try to set 0 as initial deposit, reverts + vm.expectRevert(NothingToDeposit.selector); + ale.depositAndLeveragePosition( + 0, + maxBorrowAmount, + address(market), + address(exchangeProxy), + swapData, + permit, + bytes(""), + dbrData, + false + ); + } + + function test_leveragePosition_buyDBR_withdrawDOLA() public { + // We are going to deposit some CRV, then leverage the position + uint amount = 1000 ether; + uint dolaToWithdraw = 100 ether; + + address userPk = vm.addr(1); + deal(address(market.collateral()), userPk, amount); + + uint maxBorrowAmount = getMaxBorrowAmount(amount); + + // recharge mocked proxy for swap, we need to swap DOLA to collateral + deal( + address(market.collateral()), + address(exchangeProxy), + convertDolaToCollat(maxBorrowAmount + dolaToWithdraw) + ); + + vm.startPrank(userPk, userPk); + // Initial CRV deposit + deposit(amount); + + // Calculate the amount of DOLA needed to borrow to buy the DBR needed to cover for the borrowing period + (uint256 dolaForDBR, uint256 dbrAmount) = ale + .approximateDolaAndDbrNeeded(maxBorrowAmount, 365 days, 8); + + // Sign Message for borrow on behalf + 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)" + ), + address(ale), + userPk, + maxBorrowAmount + dolaForDBR + dolaToWithdraw, + 0, + block.timestamp + ) + ) + ) + ); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(1, hash); + + ALE.Permit memory permit = ALE.Permit(block.timestamp, v, r, s); + + ALE.DBRHelper memory dbrData = ALE.DBRHelper( + dolaForDBR, + (dbrAmount * 98) / 100, // DBR buy + dolaToWithdraw // Dola to borrow and withdraw after leverage + ); + + bytes memory swapData = abi.encodeWithSelector( + MockExchangeProxy.swapDolaIn.selector, + collateral, + maxBorrowAmount + ); + + assertEq(dbr.balanceOf(userPk), 0); + + ale.leveragePosition( + maxBorrowAmount, + address(market), + address(exchangeProxy), + swapData, + permit, + bytes(""), + dbrData + ); + + // Balance in escrow is equal to the collateral deposited + the extra collateral swapped from the leverage + assertEq( + collateral.balanceOf(address(market.predictEscrow(userPk))), + amount + convertDolaToCollat(maxBorrowAmount) + ); + assertEq(DOLA.balanceOf(userPk), dolaToWithdraw); + + assertGt(dbr.balanceOf(userPk), (dbrAmount * 98) / 100); + } + + function test_leveragePosition_buyDBR() public { + // We are going to deposit some CRV, then leverage the position + uint amount = 1 ether; + address userPk = vm.addr(1); + deal(address(market.collateral()), userPk, amount); + + uint maxBorrowAmount = getMaxBorrowAmount(amount); + + // recharge mocked proxy for swap, we need to swap DOLA to collateral + deal( + address(market.collateral()), + address(exchangeProxy), + convertDolaToCollat(maxBorrowAmount) + ); + + vm.startPrank(userPk, userPk); + // Initial CRV deposit + deposit(amount); + + // Calculate the amount of DOLA needed to borrow to buy the DBR needed to cover for the borrowing period + (uint256 dolaForDBR, uint256 dbrAmount) = ale + .approximateDolaAndDbrNeeded(maxBorrowAmount, 365 days, 8); + + // Sign Message for borrow on behalf + 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)" + ), + address(ale), + userPk, + maxBorrowAmount + dolaForDBR, + 0, + block.timestamp + ) + ) + ) + ); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(1, hash); + + ALE.Permit memory permit = ALE.Permit(block.timestamp, v, r, s); + + ALE.DBRHelper memory dbrData = ALE.DBRHelper( + dolaForDBR, + (dbrAmount * 98) / 100, // DBR buy + 0 // Dola to borrow and withdraw after leverage + ); + + bytes memory swapData = abi.encodeWithSelector( + MockExchangeProxy.swapDolaIn.selector, + collateral, + maxBorrowAmount + ); + + assertEq(dbr.balanceOf(userPk), 0); + + ale.leveragePosition( + maxBorrowAmount, + address(market), + address(exchangeProxy), + swapData, + permit, + bytes(""), + dbrData + ); + + // Balance in escrow is equal to the collateral deposited + the extra collateral swapped from the leverage + assertEq( + collateral.balanceOf(address(market.predictEscrow(userPk))), + amount + convertDolaToCollat(maxBorrowAmount) + ); + assertEq(DOLA.balanceOf(userPk), 0); + + assertGt(dbr.balanceOf(userPk), (dbrAmount * 98) / 100); + } + + function test_deleveragePosition_sellDBR() public { + uint amount = 1 ether; + address userPk = vm.addr(1); + deal(address(market.collateral()), userPk, amount); + gibDBR(userPk, amount); + + // Max Amount borrowable is the one available from collateral amount + + // the extra borrow amount from the max borrow amount swapped and re-deposited as collateral + uint borrowAmount = getMaxBorrowAmount(amount) / 2; + + // recharge mocked proxy for swap, we need to swap collateral to DOLA + vm.startPrank(gov); + DOLA.mint(address(exchangeProxy), convertCollatToDola(amount / 10)); + vm.stopPrank(); + + vm.startPrank(userPk, userPk); + // CRV deposit and DOLA borrow + deposit(amount); + market.borrow(borrowAmount); + + assertEq( + collateral.balanceOf(address(market.predictEscrow(userPk))), + amount + ); + assertEq(DOLA.balanceOf(userPk), borrowAmount); + + // We are going to withdraw only 1/10 of the collateral to deleverage + uint256 amountToWithdraw = collateral.balanceOf( + address(market.predictEscrow(userPk)) + ) / 10; + + 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)" + ), + address(ale), + userPk, + amountToWithdraw, + 0, + block.timestamp + ) + ) + ) + ); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(1, hash); + + ALE.Permit memory permit = ALE.Permit(block.timestamp, v, r, s); + + ALE.DBRHelper memory dbrData = ALE.DBRHelper( + dbr.balanceOf(userPk), + 0, + 0 + ); // Sell DBR + + bytes memory swapData = abi.encodeWithSelector( + MockExchangeProxy.swapDolaOut.selector, + collateral, + amountToWithdraw + ); + + dbr.approve(address(ale), dbr.balanceOf(userPk)); + + ale.deleveragePosition( + convertCollatToDola(amountToWithdraw), + address(market), + amountToWithdraw, + address(exchangeProxy), + swapData, + permit, + bytes(""), + dbrData + ); + + // Some collateral has been withdrawn + assertEq( + collateral.balanceOf(address(market.predictEscrow(userPk))), + amount - amountToWithdraw + ); + + // User still has dola and actually he has more bc he sold his DBRs + assertGt(DOLA.balanceOf(userPk), borrowAmount); + + assertEq(dbr.balanceOf(userPk), 0); + } + + function test_deleveragePosition_withdrawALL_sellDBR() public { + uint amount = 1 ether; + address userPk = vm.addr(1); + deal(address(market.collateral()), userPk, amount); + gibDBR(userPk, amount); + + // Max Amount borrowable is the one available from collateral amount + + // the extra borrow amount from the max borrow amount swapped and re-deposited as collateral + uint borrowAmount = getMaxBorrowAmount(amount) / 2; + + // recharge mocked proxy for swap, we need to swap collateral to DOLA + vm.startPrank(gov); + DOLA.mint(address(exchangeProxy), convertCollatToDola(amount)); + vm.stopPrank(); + + vm.startPrank(userPk, userPk); + // CRV deposit and DOLA borrow + deposit(amount); + market.borrow(borrowAmount); + + assertEq( + collateral.balanceOf(address(market.predictEscrow(userPk))), + amount + ); + assertEq(DOLA.balanceOf(userPk), borrowAmount); + + // We are going to withdraw ALL the collateral to deleverage + uint256 amountToWithdraw = collateral.balanceOf( + address(market.predictEscrow(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)" + ), + address(ale), + userPk, + amountToWithdraw, + 0, + block.timestamp + ) + ) + ) + ); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(1, hash); + + ALE.Permit memory permit = ALE.Permit(block.timestamp, v, r, s); + + ALE.DBRHelper memory dbrData = ALE.DBRHelper( + dbr.balanceOf(userPk), + 0, + 0 + ); // Sell DBR + + bytes memory swapData = abi.encodeWithSelector( + MockExchangeProxy.swapDolaOut.selector, + collateral, + amountToWithdraw / 2 + ); + + dbr.approve(address(ale), dbr.balanceOf(userPk)); + + assertEq(collateral.balanceOf(userPk), 0); + + ale.deleveragePosition( + borrowAmount, + address(market), + amountToWithdraw, + address(exchangeProxy), + swapData, + permit, + bytes(""), + dbrData + ); + + // No collateral left in the escrow + assertEq( + collateral.balanceOf(address(market.predictEscrow(userPk))), + 0 + ); + + // User still has dola and actually he has more bc he sold his DBRs + assertGt(DOLA.balanceOf(userPk), borrowAmount); + + assertEq(dbr.balanceOf(userPk), 0); + + assertEq(collateral.balanceOf(userPk), amountToWithdraw / 2); + } + + function test_max_leveragePosition() public { + // We are going to deposit some CRV, then fully leverage the position + + uint amount = 1 ether; + address userPk = vm.addr(1); + deal(address(market.collateral()), userPk, amount); + gibDBR(userPk, amount); + + // Max Amount borrowable is the one available from collateral amount + + // the extra borrow amount from the max borrow amount swapped and re-deposited as collateral + uint maxBorrowAmount = getMaxLeverageBorrowAmount(amount, 100); + + // recharge mocked proxy for swap, we need to swap DOLA to collateral + deal( + address(market.collateral()), + address(exchangeProxy), + convertDolaToCollat(maxBorrowAmount) + ); + + vm.startPrank(userPk, userPk); + // Initial CRV deposit + deposit(amount); + + // We are going to leverage the max amount we can borrow + 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)" + ), + address(ale), + userPk, + maxBorrowAmount, + 0, + block.timestamp + ) + ) + ) + ); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(1, hash); + + ALE.Permit memory permit = ALE.Permit(block.timestamp, v, r, s); + + ALE.DBRHelper memory dbrData; // NO DBR + + bytes memory swapData = abi.encodeWithSelector( + MockExchangeProxy.swapDolaIn.selector, + collateral, + maxBorrowAmount + ); + + ale.leveragePosition( + maxBorrowAmount, + address(market), + address(exchangeProxy), + swapData, + permit, + bytes(""), + dbrData + ); + + // Balance in escrow is equal to the collateral deposited + the extra collateral swapped from the leverage + assertEq( + collateral.balanceOf(address(market.predictEscrow(userPk))), + amount + convertDolaToCollat(maxBorrowAmount) + ); + assertEq(DOLA.balanceOf(userPk), 0); + } + + function test_max_deleveragePosition(uint amount) public { + // We are going to deposit some CRV, then fully leverage the position + vm.assume(amount < 40000 ether); + vm.assume(amount > 0.00000001 ether); + + address userPk = vm.addr(1); + deal(address(market.collateral()), userPk, amount); + gibDBR(userPk, amount); + + // Max Amount borrowable is the one available from collateral amount + + // the extra borrow amount from the max borrow amount swapped and re-deposited as collateral + uint maxBorrowAmount = getMaxBorrowAmount(amount); + + // recharge mocked proxy for swap, we need to swap collateral to DOLA + vm.startPrank(gov); + DOLA.mint(address(exchangeProxy), convertCollatToDola(amount)); + vm.stopPrank(); + + vm.startPrank(userPk, userPk); + // Initial CRV deposit + deposit(amount); + market.borrow(maxBorrowAmount); + + assertEq( + collateral.balanceOf(address(market.predictEscrow(userPk))), + amount + ); + assertEq(DOLA.balanceOf(userPk), maxBorrowAmount); + + // We are going to deleverage and withdraw ALL collateral + uint256 amountToWithdraw = collateral.balanceOf( + address(market.predictEscrow(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)" + ), + address(ale), + userPk, + amountToWithdraw, + 0, + block.timestamp + ) + ) + ) + ); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(1, hash); + + ALE.Permit memory permit = ALE.Permit(block.timestamp, v, r, s); + + ALE.DBRHelper memory dbrData; // NO DBR + + bytes memory swapData = abi.encodeWithSelector( + MockExchangeProxy.swapDolaOut.selector, + collateral, + amountToWithdraw + ); + + ale.deleveragePosition( + maxBorrowAmount, + address(market), + amountToWithdraw, + address(exchangeProxy), + swapData, + permit, + bytes(""), + dbrData + ); + + // No collateral in the escrow + assertEq( + collateral.balanceOf(address(market.predictEscrow(userPk))), + 0 + ); + // All collateral is swapped to DOLA and sent to the user + assertEq(DOLA.balanceOf(userPk), convertCollatToDola(amount)); + } + + function test_max_leverageAndDeleveragePosition(uint256 amount) public { + // We are going to deposit some CRV, then fully leverage the position + // and then fully deleverage it (withdrawing ALL the collateral) + vm.assume(amount < 40000 ether); + vm.assume(amount > 0.00000001 ether); + + address userPk = vm.addr(1); + deal(address(market.collateral()), userPk, amount); + gibDBR(userPk, amount); + + // Max Amount borrowable is the one available from collateral amount + + // the extra borrow amount from the max borrow amount swapped and re-deposited as collateral + uint maxBorrowAmount = getMaxLeverageBorrowAmount(amount, 100); + + // recharge proxy for swap, we need to swap DOLA to collateral + deal( + address(market.collateral()), + address(exchangeProxy), + convertDolaToCollat(maxBorrowAmount) + ); + // we also need to mint DOLA into the swap mock bc we will swap ALL the collateral, not only the one added from the leverage + vm.startPrank(gov); + DOLA.mint(address(exchangeProxy), convertCollatToDola(amount)); + vm.stopPrank(); + + vm.startPrank(userPk, userPk); + // Initial CRV deposit + deposit(amount); + + // We are going to leverage the max amount we can borrow + 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)" + ), + address(ale), + userPk, + maxBorrowAmount, + 0, + block.timestamp + ) + ) + ) + ); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(1, hash); + + ALE.Permit memory permit = ALE.Permit(block.timestamp, v, r, s); + + ALE.DBRHelper memory dbrData; // NO DBR + + bytes memory swapData = abi.encodeWithSelector( + MockExchangeProxy.swapDolaIn.selector, + collateral, + maxBorrowAmount + ); + + ale.leveragePosition( + maxBorrowAmount, + address(market), + address(exchangeProxy), + swapData, + permit, + bytes(""), + dbrData + ); + + // We now deleverage and withdraw ALL the collateral (which will be swapped for DOLA) + uint256 amountToWithdraw = collateral.balanceOf( + address(market.predictEscrow(userPk)) + ); + + hash = keccak256( + abi.encodePacked( + "\x19\x01", + market.DOMAIN_SEPARATOR(), + keccak256( + abi.encode( + keccak256( + "WithdrawOnBehalf(address caller,address from,uint256 amount,uint256 nonce,uint256 deadline)" + ), + address(ale), + userPk, + amountToWithdraw, + 1, + block.timestamp + ) + ) + ) + ); + (v, r, s) = vm.sign(1, hash); + + permit = ALE.Permit(block.timestamp, v, r, s); + + swapData = abi.encodeWithSelector( + MockExchangeProxy.swapDolaOut.selector, + collateral, + amountToWithdraw + ); + + ale.deleveragePosition( + maxBorrowAmount, + address(market), + amountToWithdraw, + address(exchangeProxy), + swapData, + permit, + bytes(""), + dbrData + ); + + // We have fully deleveraged the position (no collateral left in the escrow) + // extra DOLA swapped is sent to the user (after burning) + assertEq( + collateral.balanceOf(address(market.predictEscrow(userPk))), + 0 + ); + assertEq( + DOLA.balanceOf(userPk), + convertCollatToDola(amountToWithdraw) - maxBorrowAmount + ); + } + + function test_deleveragePosition_if_collateral_no_debt() public { + uint amount = 1 ether; + address userPk = vm.addr(1); + deal(address(market.collateral()), userPk, amount); + gibDBR(userPk, amount); + + // recharge mocked proxy for swap, we need to swap collateral to DOLA + vm.startPrank(gov); + DOLA.mint(address(exchangeProxy), convertCollatToDola(amount)); + vm.stopPrank(); + + vm.startPrank(userPk, userPk); + deposit(amount); + + uint256 amountToWithdraw = amount; + + 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)" + ), + address(ale), + userPk, + amountToWithdraw, + 0, + block.timestamp + ) + ) + ) + ); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(1, hash); + + ALE.Permit memory permit = ALE.Permit(block.timestamp, v, r, s); + + ALE.DBRHelper memory dbrData; // NO DBR + + bytes memory swapData = abi.encodeWithSelector( + MockExchangeProxy.swapDolaOut.selector, + collateral, + amountToWithdraw + ); + + // vm.expectRevert(repaymentGtThanDebt); + // WE can deleverage even if we have no debt, will be swapped to DOLA and sent to the user + ale.deleveragePosition( + 0, + address(market), + amountToWithdraw, + address(exchangeProxy), + swapData, + permit, + bytes(""), + dbrData + ); + + assertEq( + collateral.balanceOf(address(market.predictEscrow(userPk))), + 0 + ); + assertEq(DOLA.balanceOf(userPk), convertCollatToDola(amount)); + } + + function test_fail_leveragePosition_if_no_collateral() public { + // We are going to deposit some CRV, then leverage the position + uint amount = 1 ether; + address userPk = vm.addr(1); + deal(address(market.collateral()), userPk, amount); + gibDBR(userPk, amount); + + uint maxBorrowAmount = getMaxBorrowAmount(amount); + + // recharge mocked proxy for swap, we need to swap DOLA to collateral + deal( + address(market.collateral()), + address(exchangeProxy), + convertDolaToCollat(maxBorrowAmount) + ); + + vm.startPrank(userPk, userPk); + + // Sign Message for borrow on behalf + 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)" + ), + address(ale), + userPk, + maxBorrowAmount, + 0, + block.timestamp + ) + ) + ) + ); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(1, hash); + + ALE.Permit memory permit = ALE.Permit(block.timestamp, v, r, s); + + ALE.DBRHelper memory dbrData; // NO DBR + + bytes memory swapData = abi.encodeWithSelector( + MockExchangeProxy.swapDolaIn.selector, + collateral, + maxBorrowAmount + ); + + vm.expectRevert(exceededLimit); + ale.leveragePosition( + maxBorrowAmount, + address(market), + address(exchangeProxy), + swapData, + permit, + bytes(""), + dbrData + ); + } + + function test_fail_deleveragePosition_if_no_collateral() public { + uint amount = 1 ether; + address userPk = vm.addr(1); + deal(address(market.collateral()), userPk, amount); + gibDBR(userPk, amount); + + // recharge mocked proxy for swap, we need to swap collateral to DOLA + vm.startPrank(gov); + DOLA.mint(address(exchangeProxy), convertCollatToDola(amount)); + vm.stopPrank(); + + vm.startPrank(userPk, userPk); + + uint256 amountToWithdraw = amount; + + 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)" + ), + address(ale), + userPk, + amountToWithdraw, + 0, + block.timestamp + ) + ) + ) + ); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(1, hash); + + ALE.Permit memory permit = ALE.Permit(block.timestamp, v, r, s); + + ALE.DBRHelper memory dbrData; // NO DBR + + bytes memory swapData = abi.encodeWithSelector( + MockExchangeProxy.swapDolaOut.selector, + collateral, + amountToWithdraw + ); + + // Cannot make a repayment without debt + vm.expectRevert(repaymentGtThanDebt); + ale.deleveragePosition( + 1 ether, + address(market), + amountToWithdraw, + address(exchangeProxy), + swapData, + permit, + bytes(""), + dbrData + ); + } + + function test_fail_max_leveragePosition_buyDBR() public { + // We are going to deposit some CRV, then fully leverage the position + + uint amount = 1 ether; + address userPk = vm.addr(1); + deal(address(market.collateral()), userPk, amount); + gibDBR(userPk, amount); + + // Max Amount borrowable is the one available from collateral amount + + // all redeposited amount as collateral + uint maxBorrowAmount = getMaxLeverageBorrowAmount(amount, 100); + + // recharge mocked proxy for swap, we need to swap DOLA to collateral + deal( + address(market.collateral()), + address(exchangeProxy), + convertDolaToCollat(maxBorrowAmount) + ); + + vm.startPrank(userPk, userPk); + // Initial CRV deposit + deposit(amount); + + // Calculate the amount of DOLA needed to buy the DBR to cover for the borrowing period + (uint256 dolaForDBR, uint256 dbrAmount) = ale + .approximateDolaAndDbrNeeded(maxBorrowAmount, 365 days, 8); + + // We are going to leverage the max amount we can borrow + the amount needed to buy the DBR + 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)" + ), + address(ale), + userPk, + maxBorrowAmount + dolaForDBR, + 0, + block.timestamp + ) + ) + ) + ); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(1, hash); + + ALE.Permit memory permit = ALE.Permit(block.timestamp, v, r, s); + + ALE.DBRHelper memory dbrData = ALE.DBRHelper( + dolaForDBR, + (dbrAmount * 99) / 100, + 0 + ); // buy DBR + + bytes memory swapData = abi.encodeWithSelector( + MockExchangeProxy.swapDolaIn.selector, + collateral, + maxBorrowAmount + ); + + // Cannot MAX leverage a position and buying DBR at the same time + vm.expectRevert(exceededLimit); + ale.leveragePosition( + maxBorrowAmount, + address(market), + address(exchangeProxy), + swapData, + permit, + bytes(""), + dbrData + ); + } + + function test_fail_setMarket_NoMarket() public { + address fakeMarket = address(0x69); + + vm.expectRevert( + abi.encodeWithSelector(ALE.NoMarket.selector, fakeMarket) + ); + ale.setMarket(fakeMarket, address(0), address(0), true); + } + + function test_fail_setMarket_Wrong_BuySellToken_Without_Helper() public { + ale.updateMarketHelper(address(market), address(0)); + + address fakeBuySellToken = address(0x69); + + vm.expectRevert( + abi.encodeWithSelector( + ALE.MarketSetupFailed.selector, + address(market), + fakeBuySellToken, + address(collateral), + address(0) + ) + ); + ale.setMarket(address(market), fakeBuySellToken, address(0), true); + + vm.expectRevert(); + ale.setMarket(address(market), address(0), address(0), true); + } + + function test_fail_updateMarketHelper_NoMarket() public { + address wrongMarket = address(0x69); + address newHelper = address(0x70); + + vm.expectRevert( + abi.encodeWithSelector(ALE.MarketNotSet.selector, wrongMarket) + ); + ale.updateMarketHelper(wrongMarket, newHelper); + } +} diff --git a/test/util/aleTests/ALESdeUSDForkTest.t.sol b/test/util/aleTests/ALESdeUSDForkTest.t.sol new file mode 100644 index 00000000..86055559 --- /dev/null +++ b/test/util/aleTests/ALESdeUSDForkTest.t.sol @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import "forge-std/Test.sol"; +import "test/marketForkTests/SdeUSDMarketForkTest.t.sol"; +import "test/util/aleTests/ALEBaseSimpleForkTest.t.sol"; +import "src/DBR.sol"; +import "test/mocks/ERC20.sol"; + +contract ALESdeUSDForkTest is SdeUSDMarketForkTest, ALEBaseSimpleForkTest { + function setUp() public override { + super.setUp(); + + exchangeProxy = new MockExchangeProxy( + address(market.oracle()), + address(DOLA) + ); + + ale = new ALE(address(exchangeProxy), triDBR); + // ALE setup + vm.startPrank(gov); + DOLA.addMinter(address(ale)); + borrowController.allow(address(ale)); + vm.stopPrank(); + + ale.setMarket( + address(market), + address(market.collateral()), + address(0), + true + ); + } +}