diff --git a/evm/src/etherfi/EtherfiAdapter.sol b/evm/src/etherfi/EtherfiAdapter.sol new file mode 100644 index 0000000..e4a5c16 --- /dev/null +++ b/evm/src/etherfi/EtherfiAdapter.sol @@ -0,0 +1,487 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +pragma experimental ABIEncoderV2; +pragma solidity ^0.8.13; + +import {IERC20, ISwapAdapter} from "src/interfaces/ISwapAdapter.sol"; +import {SafeERC20} from + "openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol"; + +/// @title Etherfi Adapter +/// @dev This contract supports the following swaps: ETH->eETH, weETH<->eETH, +/// ETH->weETH +contract EtherfiAdapter is ISwapAdapter { + using SafeERC20 for IERC20; + + uint256 constant PRECISE_UNIT = 10 ** 18; + + IWeEth immutable weEth; + IeEth immutable eEth; + ILiquidityPool immutable liquidityPool; + + constructor(address _weEth) { + weEth = IWeEth(_weEth); + eEth = weEth.eETH(); + liquidityPool = eEth.liquidityPool(); + } + + /// @dev Check if swap between provided sellToken and buyToken are supported + /// by this adapter + modifier checkInputTokens(address sellToken, address buyToken) { + if (sellToken == buyToken) { + revert Unavailable( + "This pool only supports ETH->eETH, weETH<->eETH and ETH->weETH swaps" + ); + } + if ( + sellToken != address(weEth) && sellToken != address(eEth) + && sellToken != address(0) + ) { + revert Unavailable( + "This pool only supports ETH->eETH, weETH<->eETH and ETH->weETH swaps" + ); + } + if (buyToken != address(weEth) && buyToken != address(eEth)) { + revert Unavailable( + "This pool only supports ETH->eETH, weETH<->eETH and ETH->weETH swaps" + ); + } + _; + } + + /// @dev enable receive as this contract supports ETH + receive() external payable {} + + /// @inheritdoc ISwapAdapter + function price( + bytes32, + IERC20 _sellToken, + IERC20 _buyToken, + uint256[] memory _specifiedAmounts + ) + external + view + override + checkInputTokens(address(_sellToken), address(_buyToken)) + returns (Fraction[] memory _prices) + { + _prices = new Fraction[](_specifiedAmounts.length); + address sellTokenAddress = address(_sellToken); + address buyTokenAddress = address(_buyToken); + uint256 totalPooledEther = liquidityPool.getTotalPooledEther(); + uint256 eEthTotalShares = eEth.totalShares(); + + for (uint256 i = 0; i < _specifiedAmounts.length; i++) { + if (sellTokenAddress == address(0)) { + uint256 sharesForDepositAmount = _sharesForDepositAmount( + _specifiedAmounts[i], totalPooledEther, eEthTotalShares + ); + _prices[i] = getPriceAt( + sellTokenAddress, + buyTokenAddress, + _specifiedAmounts[i], + totalPooledEther + _specifiedAmounts[i], + eEthTotalShares + sharesForDepositAmount + ); + } else { + _prices[i] = getPriceAt( + sellTokenAddress, + buyTokenAddress, + _specifiedAmounts[i], + totalPooledEther, + eEthTotalShares + ); + } + } + } + + /// @inheritdoc ISwapAdapter + function swap( + bytes32, + IERC20 sellToken, + IERC20 buyToken, + OrderSide side, + uint256 specifiedAmount + ) + external + override + checkInputTokens(address(sellToken), address(buyToken)) + returns (Trade memory trade) + { + if (specifiedAmount == 0) { + return trade; + } + + address sellTokenAddress = address(sellToken); + address buyTokenAddress = address(buyToken); + uint256 gasBefore = gasleft(); + if (sellTokenAddress == address(0)) { + if (buyTokenAddress == address(eEth)) { + trade.calculatedAmount = swapEthForEeth(specifiedAmount, side); + } else { + trade.calculatedAmount = swapEthForWeEth(specifiedAmount, side); + } + } else { + if (sellTokenAddress == address(eEth)) { + trade.calculatedAmount = swapEethForWeEth(specifiedAmount, side); + } else { + trade.calculatedAmount = swapWeEthForEeth(specifiedAmount, side); + } + } + trade.gasUsed = gasBefore - gasleft(); + + /// @dev as the price is constant for all the traded amounts and depends + /// only on the totalPooledEther and totalShares, we can use a standard + /// amount(PRECISE_UNIT) to render a well-formatted price without + /// precisions loss + trade.price = getPriceAt( + sellTokenAddress, + buyTokenAddress, + PRECISE_UNIT, + liquidityPool.getTotalPooledEther(), + eEth.totalShares() + ); + } + + /// @inheritdoc ISwapAdapter + function getLimits(bytes32, IERC20 sellToken, IERC20 buyToken) + external + view + override + checkInputTokens(address(sellToken), address(buyToken)) + returns (uint256[] memory limits) + { + limits = new uint256[](2); + + /// @dev Limits are underestimated to 90% of totalSupply as both weEth + /// and eEth have no limits but revert in some cases + if ( + address(sellToken) == address(weEth) + || address(buyToken) == address(weEth) + ) { + limits[0] = IERC20(address(weEth)).totalSupply() * 90 / 100; + } else { + limits[0] = IERC20(address(eEth)).totalSupply() * 90 / 100; + } + limits[1] = limits[0]; + } + + /// @inheritdoc ISwapAdapter + function getCapabilities(bytes32, IERC20, IERC20) + external + pure + override + returns (Capability[] memory capabilities) + { + capabilities = new Capability[](3); + capabilities[0] = Capability.SellOrder; + capabilities[1] = Capability.BuyOrder; + capabilities[2] = Capability.PriceFunction; + } + + /// @inheritdoc ISwapAdapter + function getTokens(bytes32) + external + view + override + returns (IERC20[] memory tokens) + { + tokens = new IERC20[](3); + tokens[0] = IERC20(address(0)); + tokens[1] = IERC20(address(eEth)); + tokens[2] = IERC20(address(weEth)); + } + + /// @inheritdoc ISwapAdapter + function getPoolIds(uint256, uint256) + external + view + override + returns (bytes32[] memory ids) + { + ids = new bytes32[](1); + ids[0] = bytes20(address(liquidityPool)); + } + + /// @notice Swap ETH for eETH + /// @param amount amountIn or amountOut depending on side + function swapEthForEeth(uint256 amount, OrderSide side) + internal + returns (uint256) + { + if (side == OrderSide.Buy) { + uint256 amountIn = getAmountIn(address(0), address(eEth), amount); + liquidityPool.deposit{value: amountIn}(); + IERC20(address(eEth)).safeTransfer(address(msg.sender), amount); + return amountIn; + } else { + uint256 receivedAmount = liquidityPool.deposit{value: amount}(); + uint256 balBeforeUser = + IERC20(address(eEth)).balanceOf(address(msg.sender)); + IERC20(address(eEth)).transfer(msg.sender, receivedAmount); + return IERC20(address(eEth)).balanceOf(address(msg.sender)) + - balBeforeUser; + } + } + + /// @notice Swap ETH for weEth + /// @param amount amountIn or amountOut depending on side + function swapEthForWeEth(uint256 amount, OrderSide side) + internal + returns (uint256) + { + IERC20 eEth_ = IERC20(address(eEth)); + if (side == OrderSide.Buy) { + uint256 amountIn = getAmountIn(address(0), address(weEth), amount); + + uint256 receivedAmountEeth = + liquidityPool.deposit{value: amountIn}(); + eEth_.safeIncreaseAllowance(address(weEth), receivedAmountEeth); + uint256 receivedAmount = weEth.wrap(receivedAmountEeth); + + IERC20(address(weEth)).safeTransfer( + address(msg.sender), receivedAmount + ); + + return amountIn; + } else { + uint256 receivedAmountEeth = liquidityPool.deposit{value: amount}(); + eEth_.safeIncreaseAllowance(address(weEth), receivedAmountEeth); + uint256 receivedAmount = weEth.wrap(receivedAmountEeth); + + IERC20(address(weEth)).safeTransfer( + address(msg.sender), receivedAmount + ); + + return receivedAmount; + } + } + + /// @notice Swap eETH for weETH + /// @param amount amountIn or amountOut depending on side + function swapEethForWeEth(uint256 amount, OrderSide side) + internal + returns (uint256) + { + if (side == OrderSide.Buy) { + uint256 amountIn = + getAmountIn(address(eEth), address(weEth), amount); + IERC20(address(eEth)).safeTransferFrom( + msg.sender, address(this), amountIn + ); + IERC20(address(eEth)).safeIncreaseAllowance( + address(weEth), amountIn + ); + + uint256 receivedAmount = weEth.wrap(amountIn); + + IERC20(address(weEth)).safeTransfer( + address(msg.sender), receivedAmount + ); + + return amountIn; + } else { + IERC20(address(eEth)).safeTransferFrom( + msg.sender, address(this), amount + ); + IERC20(address(eEth)).safeIncreaseAllowance(address(weEth), amount); + uint256 receivedAmount = weEth.wrap(amount); + + IERC20(address(weEth)).safeTransfer( + address(msg.sender), receivedAmount + ); + return receivedAmount; + } + } + + /// @notice Swap weETH for eEth + /// @param amount amountIn or amountOut depending on side + function swapWeEthForEeth(uint256 amount, OrderSide side) + internal + returns (uint256) + { + if (side == OrderSide.Buy) { + uint256 amountIn = + getAmountIn(address(weEth), address(eEth), amount); + IERC20(address(weEth)).safeTransferFrom( + msg.sender, address(this), amountIn + ); + uint256 receivedAmount = weEth.unwrap(amountIn); + IERC20(address(eEth)).safeTransfer( + address(msg.sender), receivedAmount + ); + return amountIn; + } else { + IERC20(address(weEth)).safeTransferFrom( + msg.sender, address(this), amount + ); + uint256 receivedAmount = weEth.unwrap(amount); + uint256 balBeforeUser = + IERC20(address(eEth)).balanceOf(address(msg.sender)); + IERC20(address(eEth)).transfer(msg.sender, receivedAmount); + return IERC20(address(eEth)).balanceOf(address(msg.sender)) + - balBeforeUser; + } + } + + /// @dev copy of '_sharesForDepositAmount' internal function in + /// LiquidityPool, without ether subtraction + function _sharesForDepositAmount( + uint256 _depositAmount, + uint256 _totalPooledEther, + uint256 _eEthTotalShares + ) internal pure returns (uint256) { + if (_totalPooledEther == 0) { + return _depositAmount; + } + return (_depositAmount * _eEthTotalShares) / _totalPooledEther; + } + + /// @dev copy of 'getWeETHByeEth' function in weETH, dynamic + function _getWeETHByeEth( + uint256 _depositAmount, + uint256 _totalPooledEther, + uint256 _eEthTotalShares + ) internal pure returns (uint256) { + if (_totalPooledEther == 0) { + return 0; + } + return (_depositAmount * _eEthTotalShares) / _totalPooledEther; + } + + /// @dev copy of 'getEethByWeEth' function in weETH, dynamic + function _getEethByWeEth( + uint256 _depositAmount, + uint256 _totalPooledEther, + uint256 _eEthTotalShares + ) internal pure returns (uint256) { + if (_eEthTotalShares == 0) { + return 0; + } + return (_depositAmount * _totalPooledEther) / _eEthTotalShares; + } + + /// @notice Get swap price + /// @param sellToken token to sell + /// @param buyToken token to buy + /// @param totalPooledEther total pooled ether after or before trade if + /// required + /// @param eEthTotalShares total shares of eETH after or before trade if + /// required + function getPriceAt( + address sellToken, + address buyToken, + uint256 amount, + uint256 totalPooledEther, + uint256 eEthTotalShares + ) internal view returns (Fraction memory) { + if (sellToken == address(0)) { + if (buyToken == address(eEth)) { + return Fraction( + _sharesForDepositAmount( + amount, totalPooledEther, eEthTotalShares + ), + amount + ); + } else { + uint256 eEthOut = _sharesForDepositAmount( + amount, totalPooledEther, eEthTotalShares + ); + return Fraction( + _getWeETHByeEth( + eEthOut, + totalPooledEther + amount, + eEthTotalShares + eEthOut + ), + amount + ); + } + } else if (sellToken == address(eEth)) { + return Fraction( + _getWeETHByeEth(amount, totalPooledEther, eEthTotalShares), + amount + ); + } else { + return Fraction( + _getEethByWeEth(amount, totalPooledEther, eEthTotalShares), + amount + ); + } + } + + /// @notice Get amountIn for swap functions with OrderSide buy + function getAmountIn(address sellToken, address buyToken, uint256 amountOut) + internal + view + returns (uint256) + { + if (sellToken == address(0)) { + if (buyToken == address(eEth)) { + return liquidityPool.amountForShare(amountOut); + } else { + uint256 ethRequiredForEeth = + liquidityPool.amountForShare(amountOut); + return liquidityPool.amountForShare(ethRequiredForEeth); + } + } else if (sellToken == address(eEth)) { + // eEth-weEth + return weEth.getEETHByWeETH(amountOut); + } else { + // weEth-eEth + return weEth.getWeETHByeETH(amountOut); + } + } +} + +interface ILiquidityPool { + function numPendingDeposits() external view returns (uint32); + function totalValueOutOfLp() external view returns (uint128); + function totalValueInLp() external view returns (uint128); + function getTotalEtherClaimOf(address _user) + external + view + returns (uint256); + function getTotalPooledEther() external view returns (uint256); + function sharesForAmount(uint256 _amount) external view returns (uint256); + function sharesForWithdrawalAmount(uint256 _amount) + external + view + returns (uint256); + function amountForShare(uint256 _share) external view returns (uint256); + + function deposit() external payable returns (uint256); + function deposit(address _referral) external payable returns (uint256); + function deposit(address _user, address _referral) + external + payable + returns (uint256); + + function requestWithdraw(address recipient, uint256 amount) + external + returns (uint256); +} + +interface IeEth { + function liquidityPool() external view returns (ILiquidityPool); + + function totalShares() external view returns (uint256); + + function shares(address _user) external view returns (uint256); +} + +interface IWeEth { + function eETH() external view returns (IeEth); + + function getWeETHByeETH(uint256 _eETHAmount) + external + view + returns (uint256); + + function getEETHByWeETH(uint256 _weETHAmount) + external + view + returns (uint256); + + function wrap(uint256 _eETHAmount) external returns (uint256); + + function unwrap(uint256 _weETHAmount) external returns (uint256); +} diff --git a/evm/src/etherfi/manifest.yaml b/evm/src/etherfi/manifest.yaml new file mode 100644 index 0000000..0f655e4 --- /dev/null +++ b/evm/src/etherfi/manifest.yaml @@ -0,0 +1,36 @@ +# information about the author helps us reach out in case of issues. +author: + name: shadowycoders.dev + email: hello@shadowycreators.com + +# Protocol Constants +constants: + protocol_gas: 30000 + # minimum capabilities we can expect, individual pools may extend these + capabilities: + - SellSide + - BuySide + - PriceFunction + +# The file containing the adapter contract +contract: EtherfiAdapter.sol + +# Deployment instances used to generate chain specific bytecode. +instances: + - chain: + name: mainnet + id: 0 + arguments: + - "0xCd5fE23C85820F7B72D0926FC9b05b43E359b7ee" + +# Specify some automatic test cases in case getPoolIds and +# getTokens are not implemented. +tests: + instances: + - pool_id: "0xB4e16d0168e52d35CaCD2c6185b44281Ec28C9Dc" + sell_token: "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2" + buy_token: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" + block: 17000000 + chain: + id: 0 + name: mainnet diff --git a/evm/test/EtherfiAdapter.t.sol b/evm/test/EtherfiAdapter.t.sol new file mode 100644 index 0000000..8d0c563 --- /dev/null +++ b/evm/test/EtherfiAdapter.t.sol @@ -0,0 +1,395 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +pragma solidity ^0.8.13; + +import "forge-std/Test.sol"; +import "openzeppelin-contracts/contracts/interfaces/IERC20.sol"; +import "src/interfaces/ISwapAdapterTypes.sol"; +import "src/libraries/FractionMath.sol"; +import "src/etherfi/EtherfiAdapter.sol"; + +contract EtherfiAdapterTest is Test, ISwapAdapterTypes { + using FractionMath for Fraction; + + EtherfiAdapter adapter; + IWeEth weEth = IWeEth(0xCd5fE23C85820F7B72D0926FC9b05b43E359b7ee); + IeEth eEth; + + uint256 constant TEST_ITERATIONS = 100; + + function setUp() public { + uint256 forkBlock = 19218495; + vm.createSelectFork(vm.rpcUrl("mainnet"), forkBlock); + adapter = new EtherfiAdapter(address(weEth)); + eEth = weEth.eETH(); + + vm.label(address(weEth), "WeETH"); + vm.label(address(eEth), "eETH"); + } + + receive() external payable {} + + function testPriceFuzzEtherfi(uint256 amount0, uint256 amount1) public { + bytes32 pair = bytes32(0); + uint256[] memory limits = adapter.getLimits( + pair, IERC20(address(weEth)), IERC20(address(eEth)) + ); + vm.assume(amount0 < limits[0] && amount0 > 0); + vm.assume(amount1 < limits[1] && amount1 > 0); + + uint256[] memory amounts = new uint256[](2); + amounts[0] = amount0; + amounts[1] = amount1; + + Fraction[] memory prices = adapter.price( + pair, IERC20(address(weEth)), IERC20(address(eEth)), amounts + ); + + for (uint256 i = 0; i < prices.length; i++) { + assertGt(prices[i].numerator, 0); + assertGt(prices[i].denominator, 0); + } + } + + function testSwapFuzzEtherfiEethWeEth(uint256 specifiedAmount, bool isBuy) + public + { + OrderSide side = isBuy ? OrderSide.Buy : OrderSide.Sell; + + IERC20 eEth_ = IERC20(address(eEth)); + IERC20 weEth_ = IERC20(address(weEth)); + bytes32 pair = bytes32(0); + uint256[] memory limits = adapter.getLimits(pair, eEth_, weEth_); + + if (side == OrderSide.Buy) { + vm.assume(specifiedAmount < limits[1] && specifiedAmount > 100); + + /// @dev workaround for eETH "deal", as standard ERC20 does not + /// work(balance is shares) + deal(address(adapter), type(uint256).max); + adapter.swap( + pair, IERC20(address(0)), eEth_, OrderSide.Buy, limits[0] + ); + + eEth_.approve(address(adapter), type(uint256).max); + } else { + vm.assume(specifiedAmount < limits[0] && specifiedAmount > 100); + + /// @dev workaround for eETH "deal", as standard ERC20 does not + /// work(balance is shares) + deal(address(adapter), type(uint128).max); + adapter.swap( + pair, IERC20(address(0)), eEth_, OrderSide.Buy, specifiedAmount + ); + + eEth_.approve(address(adapter), specifiedAmount); + } + + uint256 eEth_balance = eEth_.balanceOf(address(this)); + uint256 weEth_balance = weEth_.balanceOf(address(this)); + + Trade memory trade = + adapter.swap(pair, eEth_, weEth_, side, specifiedAmount); + + if (trade.calculatedAmount > 0) { + if (side == OrderSide.Buy) { + assertGe( + weEth_.balanceOf(address(this)) - weEth_balance, + specifiedAmount - 2 + ); + /// @dev Transfer function contains rounding errors because of + /// rewards in weETH contract, therefore we assume a +/-2 + /// tolerance + assertLe( + weEth_.balanceOf(address(this)) - weEth_balance, + specifiedAmount + ); + assertLe( + eEth_balance - eEth_.balanceOf(address(this)), + trade.calculatedAmount + 2 + ); + assertGe( + eEth_balance - eEth_.balanceOf(address(this)), + trade.calculatedAmount - 1 + ); + } else { + assertGe( + specifiedAmount, + eEth_balance - eEth_.balanceOf(address(this)) + ); + /// @dev Transfer function contains rounding errors because of + /// rewards in eETH contract, therefore we assume a +/-2 + /// tolerance + assertLe( + specifiedAmount - 2, + eEth_balance - eEth_.balanceOf(address(this)) + ); + assertEq( + trade.calculatedAmount, + weEth_.balanceOf(address(this)) - weEth_balance + ); + } + } + } + + function testSwapFuzzEtherfiWeEthEeth(uint256 specifiedAmount, bool isBuy) + public + { + OrderSide side = isBuy ? OrderSide.Buy : OrderSide.Sell; + + IERC20 eEth_ = IERC20(address(eEth)); + IERC20 weEth_ = IERC20(address(weEth)); + uint256 weEth_bal_before = weEth_.balanceOf(address(this)); + bytes32 pair = bytes32(0); + uint256[] memory limits = adapter.getLimits(pair, weEth_, eEth_); + + if (side == OrderSide.Buy) { + vm.assume(specifiedAmount < limits[1] && specifiedAmount > 100); + + /// @dev workaround for eETH "deal", as standard ERC20 does not + /// work(balance is shares) + deal(address(adapter), type(uint256).max); + adapter.swap( + pair, IERC20(address(0)), weEth_, OrderSide.Buy, limits[0] + ); + + weEth_.approve(address(adapter), type(uint256).max); + } else { + vm.assume(specifiedAmount < limits[0] && specifiedAmount > 100); + + /// @dev workaround for eETH "deal", as standard ERC20 does not + /// work(balance is shares) + deal(address(adapter), type(uint128).max); + adapter.swap( + pair, IERC20(address(0)), weEth_, OrderSide.Buy, specifiedAmount + ); + + weEth_.approve(address(adapter), specifiedAmount); + } + + uint256 eEth_balance = eEth_.balanceOf(address(this)); + uint256 weEth_balance = weEth_.balanceOf(address(this)); + + /// @dev as of rounding errors in Etherfi, specifiedAmount might lose + /// small digits for small numbers + /// therefore we use weEth_balance - weEth_bal_before as specifiedAmount + uint256 realAmountWeEth_ = weEth_balance - weEth_bal_before; + + Trade memory trade = + adapter.swap(pair, weEth_, eEth_, side, realAmountWeEth_); + + if (trade.calculatedAmount > 0) { + if (side == OrderSide.Buy) { + assertGe( + realAmountWeEth_, + eEth_.balanceOf(address(this)) - eEth_balance + ); + /// @dev Transfer function contains rounding errors because of + /// rewards in weETH contract, therefore we assume a +/-2 + /// tolerance + assertLe( + realAmountWeEth_ - 2, + eEth_.balanceOf(address(this)) - eEth_balance + ); + assertLe( + trade.calculatedAmount - 2, + weEth_balance - weEth_.balanceOf(address(this)) + ); + } else { + assertEq( + realAmountWeEth_, + weEth_balance - weEth_.balanceOf(address(this)) + ); + assertLe( + trade.calculatedAmount - 2, + eEth_.balanceOf(address(this)) - eEth_balance + ); + assertGe( + trade.calculatedAmount, + eEth_.balanceOf(address(this)) - eEth_balance + ); + } + } + } + + function testSwapFuzzEtherfiEthEeth(uint256 specifiedAmount, bool isBuy) + public + { + OrderSide side = isBuy ? OrderSide.Buy : OrderSide.Sell; + + IERC20 eth_ = IERC20(address(0)); + IERC20 eEth_ = IERC20(address(eEth)); + bytes32 pair = bytes32(0); + uint256[] memory limits = adapter.getLimits(pair, eth_, eEth_); + + if (side == OrderSide.Buy) { + vm.assume(specifiedAmount < limits[1] && specifiedAmount > 10); + + deal(address(adapter), eEth_.totalSupply()); + } else { + vm.assume(specifiedAmount < limits[0] && specifiedAmount > 10); + + deal(address(adapter), specifiedAmount); + } + + uint256 eth_balance = address(adapter).balance; + uint256 eEth_balance = eEth_.balanceOf(address(this)); + + Trade memory trade = + adapter.swap(pair, eth_, eEth_, side, specifiedAmount); + + if (trade.calculatedAmount > 0) { + if (side == OrderSide.Buy) { + assertGe( + specifiedAmount, + eEth_.balanceOf(address(this)) - eEth_balance + ); + /// @dev Transfer function contains rounding errors because of + /// rewards in eETH contract, therefore we assume a +/-2 + /// tolerance + assertLe( + specifiedAmount - 2, + eEth_.balanceOf(address(this)) - eEth_balance + ); + assertEq( + trade.calculatedAmount, + eth_balance - address(adapter).balance + ); + } else { + assertEq( + specifiedAmount, eth_balance - address(adapter).balance + ); + assertEq( + trade.calculatedAmount, + eEth_.balanceOf(address(this)) - eEth_balance + ); + } + } + } + + function testSwapFuzzEtherfiEthWeEth(uint256 specifiedAmount, bool isBuy) + public + { + OrderSide side = isBuy ? OrderSide.Buy : OrderSide.Sell; + + IERC20 eth_ = IERC20(address(0)); + IERC20 weEth_ = IERC20(address(weEth)); + bytes32 pair = bytes32(0); + uint256[] memory limits = adapter.getLimits(pair, eth_, weEth_); + + if (side == OrderSide.Buy) { + vm.assume(specifiedAmount < limits[1] && specifiedAmount > 10); + + deal(address(adapter), weEth_.totalSupply()); + } else { + vm.assume(specifiedAmount < limits[0] && specifiedAmount > 10); + + deal(address(adapter), specifiedAmount); + } + + uint256 eth_balance = address(adapter).balance; + uint256 weEth_balance = weEth_.balanceOf(address(this)); + + Trade memory trade = + adapter.swap(pair, eth_, weEth_, side, specifiedAmount); + + if (trade.calculatedAmount > 0) { + if (side == OrderSide.Buy) { + assertGe( + specifiedAmount, + weEth_.balanceOf(address(this)) - weEth_balance + ); + /// @dev Transfer function contains rounding errors because of + /// rewards in eETH contract, therefore we assume a +/-2 + /// tolerance + assertLe( + specifiedAmount - 2, + weEth_.balanceOf(address(this)) - weEth_balance + ); + assertEq( + trade.calculatedAmount, + eth_balance - address(adapter).balance + ); + } else { + assertEq( + specifiedAmount, eth_balance - address(adapter).balance + ); + assertEq( + trade.calculatedAmount, + weEth_.balanceOf(address(this)) - weEth_balance + ); + } + } + } + + function testSwapSellIncreasingEtherfi() public { + executeIncreasingSwapsEtherfi(OrderSide.Sell); + } + + function testSwapBuyIncreasingEtherfi() public { + executeIncreasingSwapsEtherfi(OrderSide.Buy); + } + + function executeIncreasingSwapsEtherfi(OrderSide side) internal { + bytes32 pair = bytes32(0); + + uint256 amountConstant_ = 10 ** 18; + + uint256[] memory amounts = new uint256[](TEST_ITERATIONS); + amounts[0] = amountConstant_; + for (uint256 i = 1; i < TEST_ITERATIONS; i++) { + amounts[i] = amountConstant_ * i; + } + + Trade[] memory trades = new Trade[](TEST_ITERATIONS); + uint256 beforeSwap; + for (uint256 i = 1; i < TEST_ITERATIONS; i++) { + beforeSwap = vm.snapshot(); + + deal(address(weEth), address(this), amounts[i]); + IERC20(address(weEth)).approve(address(adapter), amounts[i]); + + trades[i] = adapter.swap( + pair, + IERC20(address(weEth)), + IERC20(address(eEth)), + side, + amounts[i] + ); + vm.revertTo(beforeSwap); + } + + for (uint256 i = 1; i < TEST_ITERATIONS - 1; i++) { + assertLe(trades[i].calculatedAmount, trades[i + 1].calculatedAmount); + console.log( + "Prices", trades[i].price.numerator, trades[i].price.denominator + ); + console.log("Amounts", trades[i].calculatedAmount, amounts[i]); + assertLe(trades[i].gasUsed, trades[i + 1].gasUsed); + } + } + + function testGetCapabilitiesEtherfi(bytes32 pair, address t0, address t1) + public + { + Capability[] memory res = + adapter.getCapabilities(pair, IERC20(t0), IERC20(t1)); + + assertEq(res.length, 3); + } + + function testGetTokensEtherfi() public { + bytes32 pair = bytes32(0); + IERC20[] memory tokens = adapter.getTokens(pair); + + assertEq(tokens.length, 3); + } + + function testGetLimitsEtherfi() public { + bytes32 pair = bytes32(0); + uint256[] memory limits = adapter.getLimits( + pair, IERC20(address(eEth)), IERC20(address(weEth)) + ); + + assertEq(limits.length, 2); + } +}