diff --git a/contracts/test/live/adapters/AdapterTestHelper.sol b/contracts/test/live/adapters/AdapterTestHelper.sol deleted file mode 100644 index b7682c47..00000000 --- a/contracts/test/live/adapters/AdapterTestHelper.sol +++ /dev/null @@ -1,80 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -// Gearbox Protocol. Generalized leverage for DeFi protocols -// (c) Gearbox Foundation, 2023. -pragma solidity ^0.8.17; - -import { - ICreditManagerV3, - ICreditManagerV3Events -} from "@gearbox-protocol/core-v3/contracts/interfaces/ICreditManagerV3.sol"; -import {ICreditFacadeV3Events} from "@gearbox-protocol/core-v3/contracts/interfaces/ICreditFacadeV3.sol"; - -// TEST -import "../../lib/constants.sol"; - -// SUITES -import {TokensTestSuite} from "@gearbox-protocol/core-v3/contracts/test/suites/TokensTestSuite.sol"; -import {Tokens} from "@gearbox-protocol/sdk-gov/contracts/Tokens.sol"; - -// import {BalanceHelper} from "../../helpers/BalanceHelper.sol"; -// import {CreditFacadeTestHelper} from "../../helpers/CreditFacadeTestHelper.sol"; -import {IntegrationTestHelper} from "@gearbox-protocol/core-v3/contracts/test/helpers/IntegrationTestHelper.sol"; -// import {CreditConfig} from "../../config/CreditConfig.sol"; - -contract AdapterTestHelper is Test, ICreditManagerV3Events, ICreditFacadeV3Events, IntegrationTestHelper { - function _setUp() internal { - _setUp(Tokens.DAI); - } - - function _setUp(Tokens t) internal { - require(t == Tokens.DAI || t == Tokens.WETH || t == Tokens.STETH, "Unsupported token"); - - tokenTestSuite = new TokensTestSuite(); - tokenTestSuite.topUpWETH{value: 100 * WAD}(); - - // CreditConfig creditConfig = new CreditConfig(tokenTestSuite, t); - - // cft = new CreditFacadeV3TestSuite(creditConfig); - - // underlying = cft.underlying(); - - // CreditManagerV3 = cft.CreditManagerV3(); - // creditFacade = cft.creditFacade(); - // CreditConfiguratorV3 = cft.CreditConfiguratorV3(); - } - - function _getUniswapDeadline() internal view returns (uint256) { - return block.timestamp + 1; - } - - function expectMulticallStackCalls( - address creditAccount, - address borrower, - address targetContract, - bytes memory callData, - address tokenIn, - bool allowTokenIn - ) internal { - vm.expectEmit(true, true, false, false); - emit StartMultiCall(creditAccount, borrower); - - if (allowTokenIn) { - vm.expectCall( - address(creditManager), - abi.encodeCall(ICreditManagerV3.approveCreditAccount, (tokenIn, type(uint256).max)) - ); - } - - vm.expectCall(address(creditManager), abi.encodeCall(ICreditManagerV3.execute, (callData))); - - vm.expectEmit(true, false, false, false); - emit Execute(creditAccount, targetContract); - - if (allowTokenIn) { - vm.expectCall(address(creditManager), abi.encodeCall(ICreditManagerV3.approveCreditAccount, (tokenIn, 1))); - } - - vm.expectEmit(false, false, false, false); - emit FinishMultiCall(); - } -} diff --git a/contracts/test/live/zappers/AllZappers.live.t.sol b/contracts/test/live/zappers/AllZappers.live.t.sol new file mode 100644 index 00000000..774b2761 --- /dev/null +++ b/contracts/test/live/zappers/AllZappers.live.t.sol @@ -0,0 +1,160 @@ +// SPDX-License-Identifier: UNLICENSED +// Gearbox Protocol. Generalized leverage for DeFi protocols +// (c) Gearbox Foundation, 2024. +pragma solidity ^0.8.17; + +import {SafeERC20} from "@1inch/solidity-utils/contracts/libraries/SafeERC20.sol"; +import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; + +import {IZapper} from "../../../interfaces/zappers/IZapper.sol"; +import {ETH_ADDRESS, IETHZapperDeposits} from "../../../interfaces/zappers/IETHZapperDeposits.sol"; +import {IERC20ZapperDeposits} from "../../../interfaces/zappers/IERC20ZapperDeposits.sol"; + +import {ZapperLiveTestHelper} from "../../suites/ZapperLiveTestHelper.sol"; + +/// @notice Generic test for all deployed zappers. +/// @dev Deposits and redeems might revert for various natural reasons not necessarily related to zapper's correctness, +/// which can't be handled in the general case and must be dealt with in specialized tests. This test simply wraps +/// all reverts to avoid unnecessary false negatives, and only ensures that *non-reverting* zappers work properly. +contract AllZappersLiveTest is ZapperLiveTestHelper { + using SafeERC20 for ERC20; + + address user = makeAddr("user"); + address receiver = makeAddr("receiver"); + + function test_live_all_zappers() public attachOrLiveZapperTest { + emit log_string(""); + emit log_named_address("Pool", address(pool)); + address[] memory zappers = zapperRegister.zappers(address(pool)); + for (uint256 i; i < zappers.length; ++i) { + emit log_string(""); + emit log_named_address("Zapper", zappers[i]); + + address tokenIn = IZapper(zappers[i]).tokenIn(); + address tokenOut = IZapper(zappers[i]).tokenOut(); + emit log_named_address("Input token", tokenIn); + emit log_named_address("Output token", tokenOut); + + uint256 snapshot = vm.snapshot(); + _test_deposit(zappers[i], tokenIn, tokenOut); + vm.revertTo(snapshot); + _test_redeem(zappers[i], tokenIn, tokenOut); + vm.revertTo(snapshot); + } + } + + function _test_deposit(address zapper, address tokenIn, address tokenOut) internal { + uint256 tokenInDecimals = (tokenIn == ETH_ADDRESS ? 18 : ERC20(tokenIn).decimals()); + uint256 tokenOutDecimals = ERC20(tokenOut).decimals(); + + uint256 tokenInAmount = 10 ** tokenInDecimals; + try IZapper(zapper).previewDeposit(tokenInAmount) returns (uint256 previewAmountOut) { + assertGt(previewAmountOut, 0, "Deposit preview returns 0"); + uint256 tokenOutBalanceBefore = ERC20(tokenOut).balanceOf(receiver); + + (uint256 tokenOutAmount, bool success, bytes memory reason) = tokenIn == ETH_ADDRESS + ? _depositETH(zapper, tokenInAmount) + : _depositERC20(zapper, tokenIn, tokenInAmount); + if (!success) { + emit log_string(string.concat("Deposit failed, reason: ", vm.toString(reason))); + return; + } + + assertGe(tokenOutAmount, previewAmountOut, "previewDeposit overestimates"); + uint256 tokenOutBalanceAfter = ERC20(tokenOut).balanceOf(receiver); + assertEq(tokenOutBalanceAfter, tokenOutBalanceBefore + tokenOutAmount, "Incorrect amount received"); + + emit log_named_decimal_uint("Deposited", tokenInAmount, tokenInDecimals); + emit log_named_decimal_uint("Received", tokenOutAmount, tokenOutDecimals); + } catch (bytes memory reason) { + if (_isNotImplementedException(reason)) { + emit log_string("Deposit is not supported"); + } else { + emit log_string(string.concat("Deposit preview failed, reason: ", vm.toString(reason))); + } + } + } + + function _test_redeem(address zapper, address tokenIn, address tokenOut) internal { + uint256 tokenInDecimals = tokenIn == ETH_ADDRESS ? 18 : ERC20(tokenIn).decimals(); + uint256 tokenOutDecimals = ERC20(tokenOut).decimals(); + + uint256 tokenOutAmount = 10 ** tokenOutDecimals; + try IZapper(zapper).previewRedeem(tokenOutAmount) returns (uint256 previewAmountIn) { + assertGt(previewAmountIn, 0, "Redeem preview returns 0"); + uint256 tokenInBalanceBefore = + tokenIn == ETH_ADDRESS ? address(receiver).balance : ERC20(tokenIn).balanceOf(receiver); + + (uint256 tokenInAmount, bool success, bytes memory reason) = _redeem(zapper, tokenOut, tokenOutAmount); + if (!success) { + emit log_string(string.concat("Redeem failed, reason: ", vm.toString(reason))); + return; + } + + assertGe(tokenInAmount, previewAmountIn, "Redeem preview overestimates"); + uint256 tokenInBalanceAfter = + tokenIn == ETH_ADDRESS ? address(receiver).balance : ERC20(tokenIn).balanceOf(receiver); + assertEq(tokenInBalanceAfter, tokenInBalanceBefore + tokenInAmount, "Incorrect amount received"); + + emit log_named_decimal_uint("Redeemed", tokenOutAmount, tokenOutDecimals); + emit log_named_decimal_uint("Received", tokenInAmount, tokenInDecimals); + } catch (bytes memory reason) { + if (_isNotImplementedException(reason)) { + emit log_string("Redeem is not supported"); + } else { + emit log_string(string.concat("Redeem preview failed, reason: ", vm.toString(reason))); + } + } + } + + function _depositETH(address zapper, uint256 tokenInAmount) + internal + returns (uint256 tokenOutAmount, bool success, bytes memory revertReason) + { + deal(user, tokenInAmount); + vm.prank(user); + try IETHZapperDeposits(zapper).deposit{value: tokenInAmount}(receiver) returns (uint256 value) { + tokenOutAmount = value; + success = true; + } catch (bytes memory reason) { + revertReason = reason; + } + } + + function _depositERC20(address zapper, address tokenIn, uint256 tokenInAmount) + internal + returns (uint256 tokenOutAmount, bool success, bytes memory revertReason) + { + deal(tokenIn, user, tokenInAmount); + vm.startPrank(user); + ERC20(tokenIn).forceApprove(zapper, tokenInAmount); + try IERC20ZapperDeposits(zapper).deposit(tokenInAmount, receiver) returns (uint256 value) { + tokenOutAmount = value; + success = true; + } catch (bytes memory reason) { + revertReason = reason; + } + vm.stopPrank(); + } + + function _redeem(address zapper, address tokenOut, uint256 tokenOutAmount) + internal + returns (uint256 tokenInAmount, bool success, bytes memory revertReason) + { + deal(tokenOut, user, tokenOutAmount); + vm.startPrank(user); + ERC20(tokenOut).forceApprove(zapper, tokenOutAmount); + try IZapper(zapper).redeem(tokenOutAmount, receiver) returns (uint256 value) { + tokenInAmount = value; + success = true; + } catch (bytes memory reason) { + revertReason = reason; + } + vm.stopPrank(); + } + + function _isNotImplementedException(bytes memory reason) internal pure returns (bool) { + // bytes4(keccak256(bytes("NotImplementedException()"))) + return reason.length == 4 && bytes4(reason) == 0x24e46f70; + } +} diff --git a/contracts/test/suites/ZapperLiveTestHelper.sol b/contracts/test/suites/ZapperLiveTestHelper.sol new file mode 100644 index 00000000..4214685c --- /dev/null +++ b/contracts/test/suites/ZapperLiveTestHelper.sol @@ -0,0 +1,162 @@ +// SPDX-License-Identifier: UNLICENSED +// Gearbox Protocol. Generalized leverage for DeFi protocols +// (c) Gearbox Foundation, 2024. +pragma solidity ^0.8.17; + +import {IERC20Permit} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Permit.sol"; + +import {PoolV3} from "@gearbox-protocol/core-v3/contracts/pool/PoolV3.sol"; +import {Tokens} from "@gearbox-protocol/sdk-gov/contracts/Tokens.sol"; + +import {IZapper} from "../../interfaces/zappers/IZapper.sol"; +import {IZapperRegister} from "../../interfaces/zappers/IZapperRegister.sol"; + +import {DTokenDepositZapper} from "../../zappers/DTokenDepositZapper.sol"; +import {DTokenFarmingZapper} from "../../zappers/DTokenFarmingZapper.sol"; +import {UnderlyingDepositZapper} from "../../zappers/UnderlyingDepositZapper.sol"; +import {UnderlyingFarmingZapper} from "../../zappers/UnderlyingFarmingZapper.sol"; +import {WETHDepositZapper} from "../../zappers/WETHDepositZapper.sol"; +import {WETHFarmingZapper} from "../../zappers/WETHFarmingZapper.sol"; +import {WstETHDepositZapper} from "../../zappers/WstETHDepositZapper.sol"; +import {WstETHFarmingZapper} from "../../zappers/WstETHFarmingZapper.sol"; +import {ZapperRegister} from "../../zappers/ZapperRegister.sol"; + +import {LiveTestHelper} from "./LiveTestHelper.sol"; + +contract ZapperLiveTestHelper is LiveTestHelper { + IZapperRegister zapperRegister; + mapping(address => address) farmingPools; + mapping(address => address) legacyPools; + + modifier attachOrLiveZapperTest() { + // Setting `--chain-id` to 1337 or 31337 will make `NetworkDetector` try to deduce chain id + // by calling known contracts on various networks, and set `chainId` to the deduced value. + // If it fails, the value stays equal to the passed one, and the test can be skipped. + if (chainId == 1337 || chainId == 31337) return; + + // Either `ATTACH_ADDRESS_PROVIDER` or `LIVE_TEST_CONFIG` must be specified. + // The former allows to test zappers on existing pools, while the latter allows to create an arbitrary one. + address attachedAddressProvider = vm.envOr("ATTACH_ADDRESS_PROVIDER", address(0)); + if (attachedAddressProvider != address(0)) { + _attachCore(); + _attachState(); + + // By default, attach tests are run for already deployed zappers. + // To test the ones that are not deployed yet, set `REDEPLOY_ZAPPERS` to `true`. + bool redeployZappers = vm.envOr("REDEPLOY_ZAPPERS", false); + if (redeployZappers) { + zapperRegister = new ZapperRegister(address(addressProvider)); + } else { + uint256 version = vm.envOr("ATTACH_ZAPPER_REGISTER_VERSION", uint256(300)); + zapperRegister = IZapperRegister(addressProvider.getAddressOrRevert("ZAPPER_REGISTER", version)); + } + + // If `ATTACH_POOL` is specified, the tests are executed only for this pool's zappers. + // Otherwise, they are run for all v3 pools. + address attachedPool = vm.envOr("ATTACH_POOL", address(0)); + if (attachedPool != address(0)) { + _attachPool(attachedPool); + if (redeployZappers) _deployZappers(address(pool)); + _; + } else { + address[] memory pools = cr.getPools(); + for (uint256 i; i < pools.length; ++i) { + if (PoolV3(pools[i]).version() >= 3_00 && !PoolV3(pools[i]).paused()) { + _attachPool(pools[i]); + if (redeployZappers) _deployZappers(pools[i]); + _; + } + } + } + } else { + // Deploy the system from scratch using given config. + _setupCore(); + _attachState(); + zapperRegister = new ZapperRegister(address(addressProvider)); + _deployPool(getDeployConfig(vm.envString("LIVE_TEST_CONFIG"))); + _deployZappers(address(pool)); + _; + } + } + + function _getZapper(address pool, address tokenIn, address tokenOut) internal view returns (address) { + // TODO: Assumes that (tokenIn, tokenOut) uniquely identify a zapper, which is not necessarily true. + address[] memory zappers = zapperRegister.zappers(pool); + for (uint256 i; i < zappers.length; ++i) { + if (IZapper(zappers[i]).tokenIn() == tokenIn && IZapper(zappers[i]).tokenOut() == tokenOut) { + return zappers[i]; + } + } + return address(0); + } + + function _deployZappers(address pool) internal { + address underlying = PoolV3(pool).underlyingToken(); + address farmingPool = farmingPools[pool]; + + // Underlying zapper + try IERC20Permit(underlying).DOMAIN_SEPARATOR() returns (bytes32) { + zapperRegister.addZapper(address(new UnderlyingDepositZapper(pool))); + } catch {} + if (farmingPool != address(0)) { + zapperRegister.addZapper(address(new UnderlyingFarmingZapper(pool, farmingPool))); + } + + // dToken zapper + address legacyPool = legacyPools[underlying]; + if (legacyPool != address(0)) { + zapperRegister.addZapper(address(new DTokenDepositZapper(pool, legacyPool))); + if (farmingPool != address(0)) { + zapperRegister.addZapper(address(new DTokenFarmingZapper(pool, legacyPool, farmingPool))); + } + } + + // WETH zapper + if (underlying == tokenTestSuite.addressOf(Tokens.WETH)) { + zapperRegister.addZapper(address(new WETHDepositZapper(pool))); + if (farmingPool != address(0)) { + zapperRegister.addZapper(address(new WETHFarmingZapper(pool, farmingPool))); + } + } + + // wstETH zapper + if ( + underlying == tokenTestSuite.addressOf(Tokens.wstETH) + && tokenTestSuite.addressOf(Tokens.STETH) != address(0) + ) { + zapperRegister.addZapper(address(new WstETHDepositZapper(pool))); + if (farmingPool != address(0)) { + zapperRegister.addZapper(address(new WstETHFarmingZapper(pool, farmingPool))); + } + } + } + + function _attachState() internal { + // TODO: Would be nice to have this information stored in sdk-gov instead. + // Not exactly clear how to test in case farming pool is not deployed yet. + farmingPools[tokenTestSuite.addressOf(Tokens.dWETHV3)] = tokenTestSuite.addressOf(Tokens.sdWETHV3); + farmingPools[tokenTestSuite.addressOf(Tokens.dWBTCV3)] = tokenTestSuite.addressOf(Tokens.sdWBTCV3); + farmingPools[tokenTestSuite.addressOf(Tokens.dUSDCV3)] = tokenTestSuite.addressOf(Tokens.sdUSDCV3); + farmingPools[tokenTestSuite.addressOf(Tokens.dUSDTV3)] = tokenTestSuite.addressOf(Tokens.sdUSDTV3); + farmingPools[tokenTestSuite.addressOf(Tokens.dDAIV3)] = tokenTestSuite.addressOf(Tokens.sdDAIV3); + farmingPools[tokenTestSuite.addressOf(Tokens.dGHOV3)] = tokenTestSuite.addressOf(Tokens.sdGHOV3); + + legacyPools[tokenTestSuite.addressOf(Tokens.DAI)] = _getLegacyPool(Tokens.DAI); + legacyPools[tokenTestSuite.addressOf(Tokens.WETH)] = _getLegacyPool(Tokens.WETH); + legacyPools[tokenTestSuite.addressOf(Tokens.WBTC)] = _getLegacyPool(Tokens.WBTC); + legacyPools[tokenTestSuite.addressOf(Tokens.USDC)] = _getLegacyPool(Tokens.USDC); + legacyPools[tokenTestSuite.addressOf(Tokens.FRAX)] = _getLegacyPool(Tokens.FRAX); + legacyPools[tokenTestSuite.addressOf(Tokens.wstETH)] = _getLegacyPool(Tokens.wstETH); + } + + function _getLegacyPool(Tokens t) internal view returns (address) { + address token = tokenTestSuite.addressOf(t); + if (token == address(0)) return address(0); + (bool success, bytes memory result) = token.staticcall(abi.encodeWithSignature("owner()")); + if (!success) { + (success, result) = token.staticcall(abi.encodeWithSignature("poolService()")); + if (!success) return address(0); + } + return abi.decode(result, (address)); + } +}