diff --git a/evm/src/etherfi/EtherfiAdapter.sol b/evm/src/etherfi/EtherfiAdapter.sol index ad5d78d..0ba73d9 100644 --- a/evm/src/etherfi/EtherfiAdapter.sol +++ b/evm/src/etherfi/EtherfiAdapter.sol @@ -7,13 +7,15 @@ import {SafeERC20} from "openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol"; /// @title Etherfi Adapter -/// @dev This contract supports the following swaps: ETH->eETH, wETH<->eETH, +/// @dev This contract supports the following swaps: ETH->eETH, weETH<->eETH, /// ETH->weETH contract EtherfiAdapter is ISwapAdapter { using SafeERC20 for IERC20; - IWeEth weEth; - IeEth eEth; + uint256 constant PRECISE_UNIT = 10 ** 18; + + IWeEth immutable weEth; + IeEth immutable eEth; ILiquidityPool public liquidityPool; constructor(address _weEth) { @@ -58,10 +60,28 @@ contract EtherfiAdapter is ISwapAdapter { _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 + ); + } _prices[i] = getPriceAt( - sellTokenAddress, buyTokenAddress, _specifiedAmounts[i] + sellTokenAddress, + buyTokenAddress, + _specifiedAmounts[i], + totalPooledEther, + eEthTotalShares ); } } @@ -100,15 +120,13 @@ contract EtherfiAdapter is ISwapAdapter { } } trade.gasUsed = gasBefore - gasleft(); - if (side == OrderSide.Sell) { - trade.price = getPriceAt( - sellTokenAddress, buyTokenAddress, specifiedAmount - ); - } else { - trade.price = getPriceAt( - sellTokenAddress, buyTokenAddress, trade.calculatedAmount - ); - } + trade.price = getPriceAt( + sellTokenAddress, + buyTokenAddress, + PRECISE_UNIT, + liquidityPool.getTotalPooledEther(), + eEth.totalShares() + ); } /// @inheritdoc ISwapAdapter @@ -121,11 +139,14 @@ contract EtherfiAdapter is ISwapAdapter { { 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)) { + /// @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 { + } else { limits[0] = IERC20(address(eEth)).totalSupply() * 90 / 100; } limits[1] = limits[0]; @@ -177,18 +198,15 @@ contract EtherfiAdapter is ISwapAdapter { if (side == OrderSide.Buy) { uint256 amountIn = getAmountIn(address(0), address(eEth), amount); liquidityPool.deposit{value: amountIn}(); - IERC20(address(eEth)).safeTransfer( - address(msg.sender), amount - ); + IERC20(address(eEth)).safeTransfer(address(msg.sender), amount); return amountIn; } else { - uint256 balBefore = IERC20(address(eEth)).balanceOf(address(msg.sender)); - liquidityPool.deposit{value: amount}(); - uint256 receivedAmount = IERC20(address(eEth)).balanceOf(address(msg.sender)) - balBefore; - IERC20(address(eEth)).transfer( - msg.sender, receivedAmount - ); - return receivedAmount; + 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; } } @@ -232,21 +250,26 @@ contract EtherfiAdapter is ISwapAdapter { returns (uint256) { if (side == OrderSide.Buy) { - uint256 amountIn = getAmountIn(address(eEth), address(weEth), amount); - IERC20(address(eEth)).safeTransferFrom(msg.sender, address(this), amount); - IERC20(address(eEth)).safeIncreaseAllowance(address(weEth), amountIn); + 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 balBefore = eEth.shares(address(this)); uint256 receivedAmount = weEth.wrap(amountIn); - uint256 realSpentEeth = balBefore - eEth.shares(address(this)); IERC20(address(weEth)).safeTransfer( address(msg.sender), receivedAmount ); - return realSpentEeth; + return amountIn; } else { - IERC20(address(eEth)).safeTransferFrom(msg.sender, address(this), amount); + IERC20(address(eEth)).safeTransferFrom( + msg.sender, address(this), amount + ); IERC20(address(eEth)).safeIncreaseAllowance(address(weEth), amount); uint256 receivedAmount = weEth.wrap(amount); @@ -264,51 +287,111 @@ contract EtherfiAdapter is ISwapAdapter { returns (uint256) { if (side == OrderSide.Buy) { - uint256 amountIn = getAmountIn(address(weEth), address(eEth), amount); - IERC20(address(weEth)).safeTransferFrom(msg.sender, address(this), amountIn); + 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); - IERC20(address(eEth)).safeTransfer( - address(msg.sender), receivedAmount + IERC20(address(weEth)).safeTransferFrom( + msg.sender, address(this), amount ); - return receivedAmount; + 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 - function _sharesForDepositAmount(uint256 _depositAmount) internal view returns (uint256) { - uint256 totalPooledEther = liquidityPool.getTotalPooledEther() - _depositAmount; - if (totalPooledEther == 0) { + /// @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 * eEth.totalShares()) / totalPooledEther; + 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 - function getPriceAt(address sellToken, address buyToken, uint256 amount) - internal - view - returns (Fraction memory) - { + /// @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), amount); + return Fraction( + _sharesForDepositAmount( + amount, totalPooledEther, eEthTotalShares + ), + amount + ); } else { - uint256 eEthOut = _sharesForDepositAmount(amount); - return Fraction(liquidityPool.sharesForAmount(eEthOut), amount); + uint256 eEthOut = _sharesForDepositAmount( + amount, totalPooledEther, eEthTotalShares + ); + return Fraction( + _getWeETHByeEth( + eEthOut, + totalPooledEther + amount, + eEthTotalShares + eEthOut + ), + amount + ); } } else if (sellToken == address(eEth)) { - return Fraction(liquidityPool.sharesForAmount(amount), amount); + return Fraction( + _getWeETHByeEth(amount, totalPooledEther, eEthTotalShares), + amount + ); } else { - return Fraction(liquidityPool.amountForShare(amount), amount); + return Fraction( + _getEethByWeEth(amount, totalPooledEther, eEthTotalShares), + amount + ); } } @@ -328,7 +411,7 @@ contract EtherfiAdapter is ISwapAdapter { } } else if (sellToken == address(eEth)) { // eEth-weEth - return liquidityPool.amountForShare(amountOut); + return weEth.getEETHByWeETH(amountOut); } else { // weEth-eEth return weEth.getWeETHByeETH(amountOut); @@ -336,22 +419,6 @@ contract EtherfiAdapter is ISwapAdapter { } } -interface IWithdrawRequestNFT { - function requestWithdraw( - uint96 amountOfEEth, - uint96 shareOfEEth, - address recipient, - uint256 fee - ) external payable returns (uint256); - - function getClaimableAmount(uint256 tokenId) - external - view - returns (uint256); - - function claimWithdraw(uint256 tokenId) external; -} - interface ILiquidityPool { function numPendingDeposits() external view returns (uint32); function totalValueOutOfLp() external view returns (uint128); @@ -378,7 +445,6 @@ interface ILiquidityPool { function requestWithdraw(address recipient, uint256 amount) external returns (uint256); - function withdrawRequestNFT() external view returns (IWithdrawRequestNFT); } interface IeEth { diff --git a/evm/test/EtherfiAdapter.t.sol b/evm/test/EtherfiAdapter.t.sol index b7db414..8d0c563 100644 --- a/evm/test/EtherfiAdapter.t.sol +++ b/evm/test/EtherfiAdapter.t.sol @@ -30,23 +30,29 @@ contract EtherfiAdapterTest is Test, ISwapAdapterTypes { function testPriceFuzzEtherfi(uint256 amount0, uint256 amount1) public { bytes32 pair = bytes32(0); - uint256[] memory limits = adapter.getLimits(pair, IERC20(address(weEth)), IERC20(address(eEth))); + 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); - + + 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 { + function testSwapFuzzEtherfiEethWeEth(uint256 specifiedAmount, bool isBuy) + public + { OrderSide side = isBuy ? OrderSide.Buy : OrderSide.Sell; IERC20 eEth_ = IERC20(address(eEth)); @@ -57,17 +63,23 @@ contract EtherfiAdapterTest is Test, ISwapAdapterTypes { 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) + /// @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]); + 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) + /// @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); + adapter.swap( + pair, IERC20(address(0)), eEth_, OrderSide.Buy, specifiedAmount + ); eEth_.approve(address(adapter), specifiedAmount); } @@ -81,24 +93,32 @@ contract EtherfiAdapterTest is Test, ISwapAdapterTypes { if (trade.calculatedAmount > 0) { if (side == OrderSide.Buy) { assertGe( - specifiedAmount, - weEth_.balanceOf(address(this)) - weEth_balance + 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 + /// @dev Transfer function contains rounding errors because of + /// rewards in weETH contract, therefore we assume a +/-2 + /// tolerance assertLe( - specifiedAmount - 2, - weEth_.balanceOf(address(this)) - weEth_balance + weEth_.balanceOf(address(this)) - weEth_balance, + specifiedAmount ); assertLe( - trade.calculatedAmount - 2, - eEth_balance - eEth_.balanceOf(address(this)) + 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 + /// @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)) @@ -111,7 +131,9 @@ contract EtherfiAdapterTest is Test, ISwapAdapterTypes { } } - function testSwapFuzzEtherfiWeEthEeth(uint256 specifiedAmount, bool isBuy) public { + function testSwapFuzzEtherfiWeEthEeth(uint256 specifiedAmount, bool isBuy) + public + { OrderSide side = isBuy ? OrderSide.Buy : OrderSide.Sell; IERC20 eEth_ = IERC20(address(eEth)); @@ -123,17 +145,23 @@ contract EtherfiAdapterTest is Test, ISwapAdapterTypes { 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) + /// @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]); + 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) + /// @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); + adapter.swap( + pair, IERC20(address(0)), weEth_, OrderSide.Buy, specifiedAmount + ); weEth_.approve(address(adapter), specifiedAmount); } @@ -141,7 +169,8 @@ contract EtherfiAdapterTest is Test, ISwapAdapterTypes { 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 + /// @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; @@ -154,7 +183,9 @@ contract EtherfiAdapterTest is Test, ISwapAdapterTypes { 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 + /// @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 @@ -180,7 +211,9 @@ contract EtherfiAdapterTest is Test, ISwapAdapterTypes { } } - function testSwapFuzzEtherfiEthEeth(uint256 specifiedAmount, bool isBuy) public { + function testSwapFuzzEtherfiEthEeth(uint256 specifiedAmount, bool isBuy) + public + { OrderSide side = isBuy ? OrderSide.Buy : OrderSide.Sell; IERC20 eth_ = IERC20(address(0)); @@ -210,7 +243,9 @@ contract EtherfiAdapterTest is Test, ISwapAdapterTypes { 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 + /// @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 @@ -221,8 +256,7 @@ contract EtherfiAdapterTest is Test, ISwapAdapterTypes { ); } else { assertEq( - specifiedAmount, - eth_balance - address(adapter).balance + specifiedAmount, eth_balance - address(adapter).balance ); assertEq( trade.calculatedAmount, @@ -232,7 +266,9 @@ contract EtherfiAdapterTest is Test, ISwapAdapterTypes { } } - function testSwapFuzzEtherfiEthWeEth(uint256 specifiedAmount, bool isBuy) public { + function testSwapFuzzEtherfiEthWeEth(uint256 specifiedAmount, bool isBuy) + public + { OrderSide side = isBuy ? OrderSide.Buy : OrderSide.Sell; IERC20 eth_ = IERC20(address(0)); @@ -262,7 +298,9 @@ contract EtherfiAdapterTest is Test, ISwapAdapterTypes { 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 + /// @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 @@ -273,8 +311,7 @@ contract EtherfiAdapterTest is Test, ISwapAdapterTypes { ); } else { assertEq( - specifiedAmount, - eth_balance - address(adapter).balance + specifiedAmount, eth_balance - address(adapter).balance ); assertEq( trade.calculatedAmount, @@ -295,60 +332,64 @@ contract EtherfiAdapterTest is Test, ISwapAdapterTypes { function executeIncreasingSwapsEtherfi(OrderSide side) internal { bytes32 pair = bytes32(0); - uint256 amountConstant_ = 10**18; - + 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; + 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]); + + 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 + 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) - ); - + 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))); - + uint256[] memory limits = adapter.getLimits( + pair, IERC20(address(eEth)), IERC20(address(weEth)) + ); + assertEq(limits.length, 2); } }