// 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, wETH<->eETH, /// ETH->weETH contract EtherfiAdapter is ISwapAdapter { using SafeERC20 for IERC20; IWeEth weEth; IeEth eEth; ILiquidityPool public liquidityPool; constructor(address _weEth) { weEth = IWeEth(_weEth); eEth = weEth.eETH(); liquidityPool = eEth.liquidityPool(); } /// @dev Check if tokens in input are supported by this adapter modifier checkInputTokens(address sellToken, address buyToken) { if (sellToken == buyToken) { revert Unavailable("This pool only supports eETH, weEth and ETH"); } if ( sellToken != address(weEth) && sellToken != address(eEth) && sellToken != address(0) ) { revert Unavailable("This pool only supports eETH, weEth and ETH"); } if (buyToken != address(weEth) && buyToken != address(eEth)) { revert Unavailable("This pool only supports eETH, weEth and ETH"); } _; } /// @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); for (uint256 i = 0; i < _specifiedAmounts.length; i++) { _prices[i] = getPriceAt( sellTokenAddress, buyTokenAddress, _specifiedAmounts[i] ); } } /// @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(); if (side == OrderSide.Sell) { trade.price = getPriceAt( sellTokenAddress, buyTokenAddress, specifiedAmount ); } else { trade.price = getPriceAt( sellTokenAddress, buyTokenAddress, trade.calculatedAmount ); } } /// @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 only limit on Etherfi is applied on deposits(eth->eETH), and is type(uint128).max /// but we use the same amount for the others to underestimate limits[0] = IERC20(address(eEth)).totalSupply(); 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 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; } } /// @notice Swap ETH for weEth /// @param amount amountIn or amountOut depending on side function swapEthForWeEth(uint256 amount, OrderSide side) internal returns (uint256) { if (side == OrderSide.Buy) { IERC20(address(eEth)).safeTransferFrom(msg.sender, address(this), amount); uint256 amountIn = getAmountIn(address(0), address(weEth), amount); IERC20(address(eEth)).approve(address(weEth), amountIn); uint256 receivedAmountEeth = liquidityPool.deposit{value: amountIn}(); uint256 receivedAmount = weEth.wrap(receivedAmountEeth); IERC20(address(weEth)).safeTransfer( address(msg.sender), receivedAmount ); return amountIn; } else { IERC20(address(eEth)).safeTransferFrom(msg.sender, address(this), amount); IERC20(address(eEth)).approve(address(weEth), amount); uint256 receivedAmountEeth = liquidityPool.deposit{value: amount}(); 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), amount); IERC20(address(eEth)).approve(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; } else { IERC20(address(eEth)).safeTransferFrom(msg.sender, address(this), amount); IERC20(address(eEth)).approve(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); IERC20(address(eEth)).safeTransfer( address(msg.sender), receivedAmount ); return receivedAmount; } } /// @dev copy of '_sharesForDepositAmount' internal function in LiquidityPool function _sharesForDepositAmount(uint256 _depositAmount) internal view returns (uint256) { uint256 totalPooledEther = liquidityPool.getTotalPooledEther() - _depositAmount; if (totalPooledEther == 0) { return _depositAmount; } return (_depositAmount * eEth.totalShares()) / totalPooledEther; } /// @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) { if (sellToken == address(0)) { if (buyToken == address(eEth)) { return Fraction(_sharesForDepositAmount(amount), amount); } else { uint256 eEthOut = _sharesForDepositAmount(amount); return Fraction(liquidityPool.sharesForAmount(eEthOut), amount); } } else if (sellToken == address(eEth)) { return Fraction(liquidityPool.sharesForAmount(amount), amount); } else { return Fraction(liquidityPool.amountForShare(amount), 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 liquidityPool.amountForShare(amountOut); } else { // weEth-eEth return weEth.getWeETHByeETH(amountOut); } } } 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); 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); function withdrawRequestNFT() external view returns (IWithdrawRequestNFT); } 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); }