Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: DAI-like permit and live tests for zappers #124

Merged
merged 6 commits into from
May 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions .github/workflows/pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ on:
env:
HUSKY: 0
CI: true
ATTACH_ADDRESS_PROVIDER: "0x9ea7b04Da02a5373317D745c1571c84aaD03321D"

jobs:
checks:
Expand Down Expand Up @@ -37,10 +38,10 @@ jobs:
run: forge install

- name: Run forge unit tests
run: forge test --match-test test_U
run: forge test --match-test test_U -vv

- name: Run forge integration tests
run: forge test --match-test _live_ --fork-url ${{ secrets.MAINNET_TESTS_FORK }} --chain-id 1337
run: forge test --match-test _live_ --fork-url ${{ secrets.MAINNET_TESTS_FORK }} --chain-id 1337 -vv

- name: Perform checks
run: |
Expand Down
27 changes: 27 additions & 0 deletions contracts/integrations/external/IERC20PermitAllowed.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// SPDX-License-Identifier: GPL-2.0-or-later
pragma solidity >=0.5.0;

/// @title Interface for permit
/// @notice Interface used by DAI/CHAI for permit
interface IERC20PermitAllowed {
/// @notice Approve the spender to spend some tokens via the holder signature
/// @dev This is the permit interface used by DAI and CHAI
/// @param holder The address of the token holder, the token owner
/// @param spender The address of the token spender
/// @param nonce The holder's nonce, increases at each call to permit
/// @param expiry The timestamp at which the permit is no longer valid
/// @param allowed Boolean that sets approval amount, true for type(uint256).max and false for 0
/// @param v Must produce valid secp256k1 signature from the holder along with `r` and `s`
/// @param r Must produce valid secp256k1 signature from the holder along with `v` and `s`
/// @param s Must produce valid secp256k1 signature from the holder along with `r` and `v`
function permit(
address holder,
address spender,
uint256 nonce,
uint256 expiry,
bool allowed,
uint8 v,
bytes32 r,
bytes32 s
) external;
}
21 changes: 21 additions & 0 deletions contracts/interfaces/zappers/IERC20ZapperDeposits.sol
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,16 @@ interface IERC20ZapperDeposits {
external
returns (uint256 tokenOutAmount);

function depositWithPermitAllowed(
uint256 tokenInAmount,
address receiver,
uint256 nonce,
uint256 expiry,
uint8 v,
bytes32 r,
bytes32 s
) external returns (uint256 tokenOutAmount);

function depositWithReferral(uint256 tokenInAmount, address receiver, uint256 referralCode)
external
returns (uint256 tokenOutAmount);
Expand All @@ -23,4 +33,15 @@ interface IERC20ZapperDeposits {
bytes32 r,
bytes32 s
) external returns (uint256 tokenOutAmount);

function depositWithReferralAndPermitAllowed(
uint256 tokenInAmount,
address receiver,
uint256 referralCode,
uint256 nonce,
uint256 expiry,
uint8 v,
bytes32 r,
bytes32 s
) external returns (uint256 tokenOutAmount);
}
10 changes: 10 additions & 0 deletions contracts/interfaces/zappers/IZapper.sol
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,14 @@ interface IZapper {
function redeemWithPermit(uint256 tokenOutAmount, address receiver, uint256 deadline, uint8 v, bytes32 r, bytes32 s)
external
returns (uint256 tokenInAmount);

function redeemWithPermitAllowed(
uint256 tokenOutAmount,
address receiver,
uint256 nonce,
uint256 expiry,
uint8 v,
bytes32 r,
bytes32 s
) external returns (uint256 tokenInAmount);
}
20 changes: 20 additions & 0 deletions contracts/interfaces/zappers/IZapperRegister.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// SPDX-License-Identifier: MIT
// Gearbox Protocol. Generalized leverage for DeFi protocols
// (c) Gearbox Foundation, 2023.
pragma solidity ^0.8.17;

import {IVersion} from "@gearbox-protocol/core-v2/contracts/interfaces/IVersion.sol";

interface IZapperRegisterEvents {
event AddZapper(address);

event RemoveZapper(address);
}

interface IZapperRegister is IVersion, IZapperRegisterEvents {
function zappers(address pool) external view returns (address[] memory);

function addZapper(address zapper) external;

function removeZapper(address zapper) external;
}
80 changes: 0 additions & 80 deletions contracts/test/live/adapters/AdapterTestHelper.sol

This file was deleted.

160 changes: 160 additions & 0 deletions contracts/test/live/zappers/AllZappers.live.t.sol
Original file line number Diff line number Diff line change
@@ -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;
}
}
Loading
Loading