diff --git a/.github/workflows/evm.yml b/.github/workflows/evm.yml index bdd69a4..e3d0ddd 100644 --- a/.github/workflows/evm.yml +++ b/.github/workflows/evm.yml @@ -1,7 +1,7 @@ -name: test evm +name: test & check evm on: - push: + pull_request: paths: - "evm/**" @@ -15,6 +15,9 @@ jobs: name: Foundry project runs-on: ubuntu-latest + defaults: + run: + working-directory: evm steps: - uses: actions/checkout@v3 with: @@ -27,20 +30,18 @@ jobs: - name: Run Forge build run: | - cd evm forge --version forge build --sizes id: build - name: Run Forge format check run: | - forge --version - forge fmt --check - id: format + forge --version + forge fmt --check + id: format - name: Run Forge tests run: | - cd evm forge test -vvv id: test env: diff --git a/evm/lib/forge-std b/evm/lib/forge-std index f73c73d..e4aef94 160000 --- a/evm/lib/forge-std +++ b/evm/lib/forge-std @@ -1 +1 @@ -Subproject commit f73c73d2018eb6a111f35e4dae7b4f27401e9421 +Subproject commit e4aef94c1768803a16fe19f7ce8b65defd027cfd diff --git a/evm/lib/openzeppelin-contracts b/evm/lib/openzeppelin-contracts index 932fddf..11dc5e3 160000 --- a/evm/lib/openzeppelin-contracts +++ b/evm/lib/openzeppelin-contracts @@ -1 +1 @@ -Subproject commit 932fddf69a699a9a80fd2396fd1a2ab91cdda123 +Subproject commit 11dc5e3809ebe07d5405fe524385cbe4f890a08b diff --git a/evm/src/angle/AngleAdapter.sol b/evm/src/angle/AngleAdapter.sol new file mode 100644 index 0000000..9aaa50d --- /dev/null +++ b/evm/src/angle/AngleAdapter.sol @@ -0,0 +1,710 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +pragma experimental ABIEncoderV2; +pragma solidity ^0.8.13; + +import {ISwapAdapter} from "src/interfaces/ISwapAdapter.sol"; +import {IERC20} from "openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; +import {IERC20Metadata} from + "openzeppelin-contracts/contracts/token/ERC20/extensions/IERC20Metadata.sol"; +import {SafeERC20} from + "openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol"; + +/// @dev custom reserve limit factor to prevent revert errors in OrderSide.Buy +uint256 constant RESERVE_LIMIT_FACTOR = 10; +uint256 constant STANDARD_TOKEN_DECIMALS = 10 ** 18; + +/// @title AngleAdapter +/// @dev Information about prices: When swapping collateral to agEUR, the trade +/// price will not decrease(amountOut). Instead, when swapping agEUR to +/// collateral, it will, because agEUR is minted, and this mechanism is used to +/// stabilize the agEUR price. +contract AngleAdapter is ISwapAdapter { + using SafeERC20 for IERC20; + + ITransmuter immutable transmuter; + + constructor(ITransmuter _transmuter) { + transmuter = _transmuter; + } + + /// @inheritdoc ISwapAdapter + /** + * @dev It is not possible to reproduce the swap in a view mode (like + * Bancor, Uniswap v2, etc..) as the swap produce a change of storage in + * the Angle protocol, that impacts the price post trade. Due to the + * architecture of Angle, it's not possible to calculate the storage + * modifications of Angle inside the adapter. + */ + function price(bytes32, address, address, uint256[] memory) + external + pure + override + returns (Fraction[] memory) + { + revert NotImplemented("AngleAdapter.price"); + } + + /// @inheritdoc ISwapAdapter + /** + * @dev The price post trade is indipendent by the amount, since is the + * price with minimal fees with 0 slippage. In Angle there is no price with + * 0 slippage, so we use the PRECISE_UNIT (10^18, that is a small value) as + * input amount to have a slippage ---> 0. + */ + function swap( + bytes32, + address sellToken, + address buyToken, + OrderSide side, + uint256 specifiedAmount + ) external returns (Trade memory trade) { + if (specifiedAmount == 0) { + return trade; + } + + uint256 gasBefore = gasleft(); + if (side == OrderSide.Sell) { + trade.calculatedAmount = sell(sellToken, buyToken, specifiedAmount); + } else { + trade.calculatedAmount = buy(sellToken, buyToken, specifiedAmount); + } + trade.gasUsed = gasBefore - gasleft(); + uint8 decimals = side == OrderSide.Sell + ? IERC20Metadata(sellToken).decimals() + : IERC20Metadata(buyToken).decimals(); + trade.price = getPriceAt(sellToken, buyToken, side, decimals); + } + + /// @inheritdoc ISwapAdapter + /// @dev Mint may have no limits, but we underestimate them to make sure, + /// with the same amount of sellToken. We use the quoteIn (incl. fee), + /// because calculating fee requires a part of the implementation of the + /// Angle Diamond Storage, and therefore redundant functions and excessive + /// contract size, with an high complexity. In addition, we underestimate to + /// RESERVE_LIMIT_FACTOR to ensure swaps with OrderSide.Buy won't fail + /// anyway. + function getLimits(bytes32, address sellToken, address buyToken) + external + view + override + returns (uint256[] memory limits) + { + limits = new uint256[](2); + address transmuterAddress = address(transmuter); + + if (buyToken == transmuter.agToken()) { + // mint(buy agToken) + Collateral memory collatInfo = + transmuter.getCollateralInfo(sellToken); + if (collatInfo.isManaged > 0) { + limits[0] = + LibManager.maxAvailable(collatInfo.managerData.config); + } else { + limits[0] = IERC20(sellToken).balanceOf(transmuterAddress); + } + limits[1] = transmuter.quoteIn(limits[0], sellToken, buyToken); + limits[1] = limits[1] / RESERVE_LIMIT_FACTOR; + limits[0] = limits[0] / RESERVE_LIMIT_FACTOR; + } else { + // burn(sell agToken) + Collateral memory collatInfo = + transmuter.getCollateralInfo(buyToken); + if (collatInfo.isManaged > 0) { + limits[1] = + LibManager.maxAvailable(collatInfo.managerData.config); + } else { + limits[1] = IERC20(buyToken).balanceOf(transmuterAddress); + } + limits[0] = transmuter.quoteIn(limits[1], buyToken, sellToken); + limits[1] = limits[1] / RESERVE_LIMIT_FACTOR; + limits[0] = limits[0] / RESERVE_LIMIT_FACTOR; + } + } + + /// @inheritdoc ISwapAdapter + function getCapabilities(bytes32, address, address) + external + pure + override + returns (Capability[] memory capabilities) + { + capabilities = new Capability[](2); + capabilities[0] = Capability.SellOrder; + capabilities[1] = Capability.BuyOrder; + } + + /// @inheritdoc ISwapAdapter + /// @dev Since Angle has no pool IDs but supports 3 tokens(agToken and the + /// collaterals), we return all the available collaterals and the + /// agToken(agEUR) + function getTokens(bytes32) + external + view + override + returns (address[] memory tokens) + { + address[] memory collateralsAddresses = transmuter.getCollateralList(); + tokens = new address[](collateralsAddresses.length + 1); + for (uint256 i = 0; i < collateralsAddresses.length; i++) { + tokens[i] = address(collateralsAddresses[i]); + } + tokens[collateralsAddresses.length] = transmuter.agToken(); + } + + function getPoolIds(uint256, uint256) + external + pure + override + returns (bytes32[] memory) + { + revert NotImplemented("AngleAdapter.getPoolIds"); + } + + /// @notice Calculates pool prices for specified amounts + /// @param tokenIn The token being sold + /// @param tokenOut The token being bought + /// @param side Order side + /// @param decimals Decimals of the sell token + /// @return The price as a fraction corresponding to the provided amount. + function getPriceAt( + address tokenIn, + address tokenOut, + OrderSide side, + uint8 decimals + ) internal view returns (Fraction memory) { + uint256 amountOut; + uint256 amountIn; + if (side == OrderSide.Sell) { + amountIn = 10 ** decimals; + amountOut = transmuter.quoteIn(amountIn, tokenIn, tokenOut); + } else { + amountOut = 10 ** decimals; + amountIn = transmuter.quoteOut(amountOut, tokenIn, tokenOut); + } + return Fraction(amountOut, amountIn); + } + + /// @notice Executes a sell order on the contract. + /// @param sellToken The token being sold. + /// @param buyToken The token being bought. + /// @param amount The amount to be traded. + /// @return calculatedAmount The amount of tokens received. + function sell(address sellToken, address buyToken, uint256 amount) + internal + returns (uint256 calculatedAmount) + { + IERC20(sellToken).safeTransferFrom(msg.sender, address(this), amount); + IERC20(sellToken).approve(address(transmuter), amount); + calculatedAmount = transmuter.swapExactInput( + amount, 0, sellToken, buyToken, msg.sender, 0 + ); + } + + /// @notice Executes a buy order on the contract. + /// @param sellToken The token being sold. + /// @param buyToken The token being bought. + /// @param amountOut The amount of buyToken to receive. + /// @return calculatedAmount The amount of tokens received. + function buy(address sellToken, address buyToken, uint256 amountOut) + internal + returns (uint256 calculatedAmount) + { + calculatedAmount = transmuter.quoteOut(amountOut, sellToken, buyToken); + + IERC20(sellToken).safeTransferFrom( + msg.sender, address(this), calculatedAmount + ); + IERC20(sellToken).approve(address(transmuter), calculatedAmount); + transmuter.swapExactOutput( + amountOut, type(uint256).max, sellToken, buyToken, msg.sender, 0 + ); + } +} + +interface IAgToken is IERC20 { + /*////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + MINTER ROLE ONLY FUNCTIONS + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////*/ + + /// @notice Lets a whitelisted contract mint agTokens + /// @param account Address to mint to + /// @param amount Amount to mint + function mint(address account, uint256 amount) external; + + /// @notice Burns `amount` tokens from a `burner` address after being asked + /// to by `sender` + /// @param amount Amount of tokens to burn + /// @param burner Address to burn from + /// @param sender Address which requested the burn from `burner` + /// @dev This method is to be called by a contract with the minter right + /// after being requested + /// to do so by a `sender` address willing to burn tokens from another + /// `burner` address + /// @dev The method checks the allowance between the `sender` and the + /// `burner` + function burnFrom(uint256 amount, address burner, address sender) + external; + + /// @notice Burns `amount` tokens from a `burner` address + /// @param amount Amount of tokens to burn + /// @param burner Address to burn from + /// @dev This method is to be called by a contract with a minter right on + /// the AgToken after being + /// requested to do so by an address willing to burn tokens from its address + function burnSelf(uint256 amount, address burner) external; + + /*////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + TREASURY ONLY FUNCTIONS + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////*/ + + /// @notice Adds a minter in the contract + /// @param minter Minter address to add + /// @dev Zero address checks are performed directly in the `Treasury` + /// contract + function addMinter(address minter) external; + + /// @notice Removes a minter from the contract + /// @param minter Minter address to remove + /// @dev This function can also be called by a minter wishing to revoke + /// itself + function removeMinter(address minter) external; + + /// @notice Sets a new treasury contract + /// @param _treasury New treasury address + function setTreasury(address _treasury) external; + + /*////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + EXTERNAL FUNCTIONS + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////*/ + + /// @notice Checks whether an address has the right to mint agTokens + /// @param minter Address for which the minting right should be checked + /// @return Whether the address has the right to mint agTokens or not + function isMinter(address minter) external view returns (bool); + + /// @notice Amount of decimals of the stablecoin + function decimals() external view returns (uint8); +} + +enum ManagerType { + EXTERNAL +} + +enum WhitelistType { + BACKED +} + +struct ManagerStorage { + IERC20[] subCollaterals; // Subtokens handled by the manager or strategies + bytes config; // Additional configuration data +} + +struct Collateral { + uint8 isManaged; // If the collateral is managed through external strategies + uint8 isMintLive; // If minting from this asset is unpaused + uint8 isBurnLive; // If burning to this asset is unpaused + uint8 decimals; // IERC20Metadata(collateral).decimals() + uint8 onlyWhitelisted; // If only whitelisted addresses can burn or redeem + // for this token + uint216 normalizedStables; // Normalized amount of stablecoins issued from + // this collateral + uint64[] xFeeMint; // Increasing exposures in [0,BASE_9[ + int64[] yFeeMint; // Mint fees at the exposures specified in `xFeeMint` + uint64[] xFeeBurn; // Decreasing exposures in ]0,BASE_9] + int64[] yFeeBurn; // Burn fees at the exposures specified in `xFeeBurn` + bytes oracleConfig; // Data about the oracle used for the collateral + bytes whitelistData; // For whitelisted collateral, data used to verify + // whitelists + ManagerStorage managerData; // For managed collateral, data used to handle + // the strategies +} + +struct TransmuterStorage { + IAgToken agToken; // agToken handled by the system + uint8 isRedemptionLive; // If redemption is unpaused + uint8 statusReentrant; // If call is reentrant or not + uint128 normalizedStables; // Normalized amount of stablecoins issued by the + // system + uint128 normalizer; // To reconcile `normalizedStables` values with the + // actual amount + address[] collateralList; // List of collateral assets supported by the + // system + uint64[] xRedemptionCurve; // Increasing collateral ratios > 0 + int64[] yRedemptionCurve; // Value of the redemption fees at + // `xRedemptionCurve` + mapping(address => Collateral) collaterals; // Maps a collateral asset to + // its parameters + mapping(address => uint256) isTrusted; // If an address is trusted to update + // the normalizer value + mapping(address => uint256) isSellerTrusted; // If an address is trusted to + // sell accruing reward tokens + mapping(WhitelistType => mapping(address => uint256)) isWhitelistedForType; +} + +interface IManager { + /// @notice Returns the amount of collateral managed by the Manager + /// @return balances Balances of all the subCollaterals handled by the + /// manager + /// @dev MUST NOT revert + function totalAssets() + external + view + returns (uint256[] memory balances, uint256 totalValue); + + /// @notice Hook to invest `amount` of `collateral` + /// @dev MUST revert if the manager cannot accept these funds + /// @dev MUST have received the funds beforehand + function invest(uint256 amount) external; + + /// @notice Sends `amount` of `collateral` to the `to` address + /// @dev Called when `agToken` are burnt and during redemptions + // @dev MUST revert if there are not funds enough available + /// @dev MUST be callable only by the transmuter + function release(address asset, address to, uint256 amount) external; + + /// @notice Gives the maximum amount of collateral immediately available for + /// a transfer + /// @dev Useful for integrators using `quoteIn` and `quoteOut` + function maxAvailable() external view returns (uint256); +} + +/// @title LibManager +/// @author Angle Labs, Inc. +/// @dev Managed collateral assets may be handled through external smart +/// contracts or directly through this library +/// @dev There is no implementation at this point for a managed collateral +/// handled through this library, and +/// a new specific `ManagerType` would need to be added in this case +library LibManager { + /// @notice Checks to which address managed funds must be transferred + function transferRecipient(bytes memory config) + internal + view + returns (address recipient) + { + (ManagerType managerType, bytes memory data) = + parseManagerConfig(config); + recipient = address(this); + if (managerType == ManagerType.EXTERNAL) { + return abi.decode(data, (address)); + } + } + + /// @notice Performs a transfer of `token` for a collateral that is managed + /// to a `to` address + /// @dev `token` may not be the actual collateral itself, as some + /// collaterals have subcollaterals associated + /// with it + /// @dev Eventually pulls funds from strategies + function release( + address token, + address to, + uint256 amount, + bytes memory config + ) internal { + (ManagerType managerType, bytes memory data) = + parseManagerConfig(config); + if (managerType == ManagerType.EXTERNAL) { + abi.decode(data, (IManager)).release(token, to, amount); + } + } + + /// @notice Gets the balances of all the tokens controlled through + /// `managerData` + /// @return balances An array of size `subCollaterals` with current balances + /// of all subCollaterals + /// including the one corresponding to the `managerData` given + /// @return totalValue The value of all the `subCollaterals` in `collateral` + /// @dev `subCollaterals` must always have as first token (index 0) the + /// collateral itself + function totalAssets(bytes memory config) + internal + view + returns (uint256[] memory balances, uint256 totalValue) + { + (ManagerType managerType, bytes memory data) = + parseManagerConfig(config); + if (managerType == ManagerType.EXTERNAL) { + return abi.decode(data, (IManager)).totalAssets(); + } + } + + /// @notice Calls a hook if needed after new funds have been transfered to a + /// manager + function invest(uint256 amount, bytes memory config) internal { + (ManagerType managerType, bytes memory data) = + parseManagerConfig(config); + if (managerType == ManagerType.EXTERNAL) { + abi.decode(data, (IManager)).invest(amount); + } + } + + /// @notice Returns available underlying tokens, for instance if liquidity + /// is fully used and + /// not withdrawable the function will return 0 + function maxAvailable(bytes memory config) + internal + view + returns (uint256 available) + { + (ManagerType managerType, bytes memory data) = + parseManagerConfig(config); + if (managerType == ManagerType.EXTERNAL) { + return abi.decode(data, (IManager)).maxAvailable(); + } + } + + /// @notice Decodes the `managerData` associated to a collateral + function parseManagerConfig(bytes memory config) + internal + pure + returns (ManagerType managerType, bytes memory data) + { + (managerType, data) = abi.decode(config, (ManagerType, bytes)); + } +} + +interface ITransmuter { + function implementation() external view returns (address); + + function setDummyImplementation(address _implementation) external; + + function facetAddress(bytes4 _functionSelector) + external + view + returns (address facetAddress_); + + function facetAddresses() + external + view + returns (address[] memory facetAddresses_); + + function facetFunctionSelectors(address _facet) + external + view + returns (bytes4[] memory _facetFunctionSelectors); + + function accessControlManager() external view returns (address); + + function agToken() external view returns (address); + + function getCollateralBurnFees(address collateral) + external + view + returns (uint64[] memory xFeeBurn, int64[] memory yFeeBurn); + + function getCollateralDecimals(address collateral) + external + view + returns (uint8); + + function getCollateralInfo(address collateral) + external + view + returns (Collateral memory); + + function getCollateralList() external view returns (address[] memory); + + function getCollateralMintFees(address collateral) + external + view + returns (uint64[] memory xFeeMint, int64[] memory yFeeMint); + + function getCollateralRatio() + external + view + returns (uint64 collatRatio, uint256 stablecoinsIssued); + + function getCollateralWhitelistData(address collateral) + external + view + returns (bytes memory); + + function getIssuedByCollateral(address collateral) + external + view + returns (uint256 stablecoinsFromCollateral, uint256 stablecoinsIssued); + + function getManagerData(address collateral) + external + view + returns (bool, address[] memory, bytes memory); + + function getOracle(address collateral) + external + view + returns ( + uint8 oracleType, + uint8 targetType, + bytes memory oracleData, + bytes memory targetData + ); + + function getOracleValues(address collateral) + external + view + returns ( + uint256 mint, + uint256 burn, + uint256 ratio, + uint256 minRatio, + uint256 redemption + ); + + function getRedemptionFees() + external + view + returns ( + uint64[] memory xRedemptionCurve, + int64[] memory yRedemptionCurve + ); + + function getTotalIssued() external view returns (uint256); + + function isPaused(address collateral, uint8 action) + external + view + returns (bool); + + function isTrusted(address sender) external view returns (bool); + + function isTrustedSeller(address sender) external view returns (bool); + + function isValidSelector(bytes4 selector) external view returns (bool); + + function isWhitelistedCollateral(address collateral) + external + view + returns (bool); + + function isWhitelistedForCollateral(address collateral, address sender) + external + returns (bool); + + function isWhitelistedForType(uint8 whitelistType, address sender) + external + view + returns (bool); + + function sellRewards(uint256 minAmountOut, bytes memory payload) + external + returns (uint256 amountOut); + + function addCollateral(address collateral) external; + + function adjustStablecoins( + address collateral, + uint128 amount, + bool increase + ) external; + + function changeAllowance(address token, address spender, uint256 amount) + external; + + function recoverERC20( + address collateral, + address token, + address to, + uint256 amount + ) external; + + function revokeCollateral(address collateral) external; + + function setAccessControlManager(address _newAccessControlManager) + external; + + function setOracle(address collateral, bytes memory oracleConfig) + external; + + function setWhitelistStatus( + address collateral, + uint8 whitelistStatus, + bytes memory whitelistData + ) external; + + function toggleTrusted(address sender, uint8 t) external; + + function setFees( + address collateral, + uint64[] memory xFee, + int64[] memory yFee, + bool mint + ) external; + + function setRedemptionCurveParams(uint64[] memory xFee, int64[] memory yFee) + external; + + function togglePause(address collateral, uint8 pausedType) external; + + function toggleWhitelist(uint8 whitelistType, address who) external; + + function quoteIn(uint256 amountIn, address tokenIn, address tokenOut) + external + view + returns (uint256 amountOut); + + function quoteOut(uint256 amountOut, address tokenIn, address tokenOut) + external + view + returns (uint256 amountIn); + + function swapExactInput( + uint256 amountIn, + uint256 amountOutMin, + address tokenIn, + address tokenOut, + address to, + uint256 deadline + ) external returns (uint256 amountOut); + + function swapExactInputWithPermit( + uint256 amountIn, + uint256 amountOutMin, + address tokenIn, + address to, + uint256 deadline, + bytes memory permitData + ) external returns (uint256 amountOut); + + function swapExactOutput( + uint256 amountOut, + uint256 amountInMax, + address tokenIn, + address tokenOut, + address to, + uint256 deadline + ) external returns (uint256 amountIn); + + function swapExactOutputWithPermit( + uint256 amountOut, + uint256 amountInMax, + address tokenIn, + address to, + uint256 deadline, + bytes memory permitData + ) external returns (uint256 amountIn); + + function quoteRedemptionCurve(uint256 amount) + external + view + returns (address[] memory tokens, uint256[] memory amounts); + + function redeem( + uint256 amount, + address receiver, + uint256 deadline, + uint256[] memory minAmountOuts + ) external returns (address[] memory tokens, uint256[] memory amounts); + + function redeemWithForfeit( + uint256 amount, + address receiver, + uint256 deadline, + uint256[] memory minAmountOuts, + address[] memory forfeitTokens + ) external returns (address[] memory tokens, uint256[] memory amounts); + + function updateNormalizer(uint256 amount, bool increase) + external + returns (uint256); +} diff --git a/evm/src/angle/manifest.yaml b/evm/src/angle/manifest.yaml new file mode 100644 index 0000000..ef376f7 --- /dev/null +++ b/evm/src/angle/manifest.yaml @@ -0,0 +1,35 @@ +# 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 + +# The file containing the adapter contract +contract: AngleAdapter.sol + +# Deployment instances used to generate chain specific bytecode. +instances: + - chain: + name: mainnet + id: 0 + arguments: + - "0x00253582b2a3FE112feEC532221d9708c64cEFAb" + +# 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/src/balancer-v2/BalancerV2SwapAdapter.sol b/evm/src/balancer-v2/BalancerV2SwapAdapter.sol index e373649..e93702b 100644 --- a/evm/src/balancer-v2/BalancerV2SwapAdapter.sol +++ b/evm/src/balancer-v2/BalancerV2SwapAdapter.sol @@ -1,7 +1,11 @@ // SPDX-License-Identifier: AGPL-3.0-or-later pragma solidity ^0.8.13; -import {IERC20, ISwapAdapter} from "src/interfaces/ISwapAdapter.sol"; +import {ISwapAdapter} from "src/interfaces/ISwapAdapter.sol"; +import { + IERC20, + SafeERC20 +} from "openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol"; // Maximum Swap In/Out Ratio - 0.3 // https://balancer.gitbook.io/balancer/core-concepts/protocol/limitations#v2-limits @@ -9,6 +13,8 @@ uint256 constant RESERVE_LIMIT_FACTOR = 4; uint256 constant SWAP_DEADLINE_SEC = 1000; contract BalancerV2SwapAdapter is ISwapAdapter { + using SafeERC20 for IERC20; + IVault immutable vault; constructor(address payable vault_) { @@ -27,8 +33,8 @@ contract BalancerV2SwapAdapter is ISwapAdapter { /// as a Fraction struct. function priceSingle( bytes32 poolId, - IERC20 sellToken, - IERC20 buyToken, + address sellToken, + address buyToken, uint256 sellAmount ) public returns (Fraction memory calculatedPrice) { IVault.BatchSwapStep[] memory swapSteps = new IVault.BatchSwapStep[](1); @@ -40,8 +46,8 @@ contract BalancerV2SwapAdapter is ISwapAdapter { userData: "" }); address[] memory assets = new address[](2); - assets[0] = address(sellToken); - assets[1] = address(buyToken); + assets[0] = sellToken; + assets[1] = buyToken; IVault.FundManagement memory funds = IVault.FundManagement({ sender: msg.sender, fromInternalBalance: false, @@ -63,8 +69,8 @@ contract BalancerV2SwapAdapter is ISwapAdapter { function getSellAmount( bytes32 poolId, - IERC20 sellToken, - IERC20 buyToken, + address sellToken, + address buyToken, uint256 buyAmount ) public returns (uint256 sellAmount) { IVault.BatchSwapStep[] memory swapSteps = new IVault.BatchSwapStep[](1); @@ -76,8 +82,8 @@ contract BalancerV2SwapAdapter is ISwapAdapter { userData: "" }); address[] memory assets = new address[](2); - assets[0] = address(sellToken); - assets[1] = address(buyToken); + assets[0] = sellToken; + assets[1] = buyToken; IVault.FundManagement memory funds = IVault.FundManagement({ sender: msg.sender, fromInternalBalance: false, @@ -94,31 +100,23 @@ contract BalancerV2SwapAdapter is ISwapAdapter { sellAmount = uint256(assetDeltas[0]); } - function priceBatch( + function price( bytes32 poolId, - IERC20 sellToken, - IERC20 buyToken, + address sellToken, + address buyToken, uint256[] memory specifiedAmounts ) external returns (Fraction[] memory calculatedPrices) { + calculatedPrices = new Fraction[](specifiedAmounts.length); for (uint256 i = 0; i < specifiedAmounts.length; i++) { calculatedPrices[i] = priceSingle(poolId, sellToken, buyToken, specifiedAmounts[i]); } } - function price(bytes32, IERC20, IERC20, uint256[] memory) - external - pure - override - returns (Fraction[] memory) - { - revert NotImplemented("BalancerV2SwapAdapter.price"); - } - function swap( bytes32 poolId, - IERC20 sellToken, - IERC20 buyToken, + address sellToken, + address buyToken, OrderSide side, uint256 specifiedAmount ) external override returns (Trade memory trade) { @@ -136,16 +134,18 @@ contract BalancerV2SwapAdapter is ISwapAdapter { limit = type(uint256).max; } - sellToken.transferFrom(msg.sender, address(this), sellAmount); - sellToken.approve(address(vault), sellAmount); + IERC20(sellToken).safeTransferFrom( + msg.sender, address(this), sellAmount + ); + IERC20(sellToken).safeIncreaseAllowance(address(vault), sellAmount); uint256 gasBefore = gasleft(); trade.calculatedAmount = vault.swap( IVault.SingleSwap({ poolId: poolId, kind: kind, - assetIn: address(sellToken), - assetOut: address(buyToken), + assetIn: sellToken, + assetOut: buyToken, amount: specifiedAmount, userData: "" }), @@ -162,14 +162,14 @@ contract BalancerV2SwapAdapter is ISwapAdapter { trade.price = priceSingle(poolId, sellToken, buyToken, specifiedAmount); } - function getLimits(bytes32 poolId, IERC20 sellToken, IERC20 buyToken) + function getLimits(bytes32 poolId, address sellToken, address buyToken) external view override returns (uint256[] memory limits) { limits = new uint256[](2); - (IERC20[] memory tokens, uint256[] memory balances,) = + (address[] memory tokens, uint256[] memory balances,) = vault.getPoolTokens(poolId); for (uint256 i = 0; i < tokens.length; i++) { @@ -182,22 +182,23 @@ contract BalancerV2SwapAdapter is ISwapAdapter { } } - function getCapabilities(bytes32, IERC20, IERC20) + function getCapabilities(bytes32, address, address) external pure override returns (Capability[] memory capabilities) { - capabilities = new Capability[](2); + capabilities = new Capability[](3); capabilities[0] = Capability.SellOrder; capabilities[1] = Capability.BuyOrder; + capabilities[2] = Capability.PriceFunction; } function getTokens(bytes32 poolId) external view override - returns (IERC20[] memory tokens) + returns (address[] memory tokens) { (tokens,,) = vault.getPoolTokens(poolId); } @@ -472,7 +473,7 @@ interface IVault { external view returns ( - IERC20[] memory tokens, + address[] memory tokens, uint256[] memory balances, uint256 lastChangeBlock ); diff --git a/evm/src/etherfi/EtherfiAdapter.sol b/evm/src/etherfi/EtherfiAdapter.sol new file mode 100644 index 0000000..a032a2e --- /dev/null +++ b/evm/src/etherfi/EtherfiAdapter.sol @@ -0,0 +1,480 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +pragma experimental ABIEncoderV2; +pragma solidity ^0.8.13; + +import {ISwapAdapter} from "src/interfaces/ISwapAdapter.sol"; +import {IERC20} from "openzeppelin-contracts/contracts/token/ERC20/IERC20.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, + address sellToken, + address buyToken, + uint256[] memory specifiedAmounts + ) + external + view + override + checkInputTokens(sellToken, buyToken) + returns (Fraction[] memory prices) + { + prices = new Fraction[](specifiedAmounts.length); + uint256 totalPooledEther = liquidityPool.getTotalPooledEther(); + uint256 eEthTotalShares = eEth.totalShares(); + + for (uint256 i = 0; i < specifiedAmounts.length; i++) { + if (sellToken == address(0)) { + uint256 sharesForDepositAmount = _sharesForDepositAmount( + specifiedAmounts[i], totalPooledEther, eEthTotalShares + ); + prices[i] = getPriceAt( + sellToken, + buyToken, + specifiedAmounts[i], + totalPooledEther + specifiedAmounts[i], + eEthTotalShares + sharesForDepositAmount + ); + } else { + prices[i] = getPriceAt( + sellToken, + buyToken, + specifiedAmounts[i], + totalPooledEther, + eEthTotalShares + ); + } + } + } + + /// @inheritdoc ISwapAdapter + function swap( + bytes32, + address sellToken, + address buyToken, + OrderSide side, + uint256 specifiedAmount + ) + external + override + checkInputTokens(sellToken, buyToken) + returns (Trade memory trade) + { + if (specifiedAmount == 0) { + return trade; + } + uint256 gasBefore = gasleft(); + if (sellToken == address(0)) { + if (buyToken == address(eEth)) { + trade.calculatedAmount = swapEthForEeth(specifiedAmount, side); + } else { + trade.calculatedAmount = swapEthForWeEth(specifiedAmount, side); + } + } else { + if (sellToken == 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( + sellToken, + buyToken, + PRECISE_UNIT, + liquidityPool.getTotalPooledEther(), + eEth.totalShares() + ); + } + + /// @inheritdoc ISwapAdapter + function getLimits(bytes32, address sellToken, address buyToken) + external + view + override + checkInputTokens(sellToken, 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 (sellToken == address(weEth) || 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, address, address) + 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 (address[] memory tokens) + { + tokens = new address[](3); + tokens[0] = address(0); + tokens[1] = address(eEth); + tokens[2] = 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)).safeTransfer(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)).safeTransfer(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/src/integral/IntegralSwapAdapter.sol b/evm/src/integral/IntegralSwapAdapter.sol index b79f1be..36a0f11 100644 --- a/evm/src/integral/IntegralSwapAdapter.sol +++ b/evm/src/integral/IntegralSwapAdapter.sol @@ -1,10 +1,13 @@ // SPDX-License-Identifier: AGPL-3.0-or-later pragma solidity ^0.8.13; -import {IERC20, ISwapAdapter} from "src/interfaces/ISwapAdapter.sol"; -import {ERC20} from "openzeppelin-contracts/contracts/token/ERC20/ERC20.sol"; -import {SafeERC20} from - "openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol"; +import {ISwapAdapter} from "src/interfaces/ISwapAdapter.sol"; +import {IERC20Metadata} from + "openzeppelin-contracts/contracts/token/ERC20/extensions/IERC20Metadata.sol"; +import { + IERC20, + SafeERC20 +} from "openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol"; /// @dev Integral submitted deadline of 3600 seconds (1 hour) to Paraswap, but /// it is not strictly necessary to be this long @@ -38,24 +41,23 @@ contract IntegralSwapAdapter is ISwapAdapter { /// values to make sure the return value is the expected from caller. function price( bytes32, - IERC20 _sellToken, - IERC20 _buyToken, + address _sellToken, + address _buyToken, uint256[] memory _specifiedAmounts ) external view override returns (Fraction[] memory _prices) { _prices = new Fraction[](_specifiedAmounts.length); - Fraction memory price = - getPriceAt(address(_sellToken), address(_buyToken)); + Fraction memory uniformPrice = getPriceAt(_sellToken, _buyToken); for (uint256 i = 0; i < _specifiedAmounts.length; i++) { - _prices[i] = price; + _prices[i] = uniformPrice; } } /// @inheritdoc ISwapAdapter function swap( bytes32, - IERC20 sellToken, - IERC20 buyToken, + address sellToken, + address buyToken, OrderSide side, uint256 specifiedAmount ) external override returns (Trade memory trade) { @@ -72,18 +74,18 @@ contract IntegralSwapAdapter is ISwapAdapter { trade.calculatedAmount = buy(sellToken, buyToken, specifiedAmount); } trade.gasUsed = gasBefore - gasleft(); - trade.price = getPriceAt(address(sellToken), address(buyToken)); + trade.price = getPriceAt(sellToken, buyToken); } /// @inheritdoc ISwapAdapter - function getLimits(bytes32, IERC20 sellToken, IERC20 buyToken) + function getLimits(bytes32, address sellToken, address buyToken) external view override returns (uint256[] memory limits) { (,,, uint256 limitMax0,, uint256 limitMax1) = - relayer.getPoolState(address(sellToken), address(buyToken)); + relayer.getPoolState(sellToken, buyToken); limits = new uint256[](2); limits[0] = limitMax0; @@ -98,7 +100,7 @@ contract IntegralSwapAdapter is ISwapAdapter { } /// @inheritdoc ISwapAdapter - function getCapabilities(bytes32, IERC20, IERC20) + function getCapabilities(bytes32, address, address) external pure override @@ -116,12 +118,12 @@ contract IntegralSwapAdapter is ISwapAdapter { external view override - returns (IERC20[] memory tokens) + returns (address[] memory tokens) { - tokens = new IERC20[](2); + tokens = new address[](2); ITwapPair pair = ITwapPair(address(bytes20(poolId))); - tokens[0] = IERC20(pair.token0()); - tokens[1] = IERC20(pair.token1()); + tokens[0] = pair.token0(); + tokens[1] = pair.token1(); } /// @inheritdoc ISwapAdapter @@ -147,23 +149,22 @@ contract IntegralSwapAdapter is ISwapAdapter { /// @param buyToken The address of the token being bought. /// @param amount The amount to be traded. /// @return uint256 The amount of tokens received. - function sell(IERC20 sellToken, IERC20 buyToken, uint256 amount) + function sell(address sellToken, address buyToken, uint256 amount) internal returns (uint256) { - uint256 amountOut = - relayer.quoteSell(address(sellToken), address(buyToken), amount); + uint256 amountOut = relayer.quoteSell(sellToken, buyToken, amount); if (amountOut == 0) { revert Unavailable("AmountOut is zero!"); } - sellToken.safeTransferFrom(msg.sender, address(this), amount); - sellToken.safeIncreaseAllowance(address(relayer), amount); + IERC20(sellToken).safeTransferFrom(msg.sender, address(this), amount); + IERC20(sellToken).safeIncreaseAllowance(address(relayer), amount); relayer.sell( ITwapRelayer.SellParams({ - tokenIn: address(sellToken), - tokenOut: address(buyToken), + tokenIn: sellToken, + tokenOut: buyToken, wrapUnwrap: false, to: msg.sender, submitDeadline: uint32(block.timestamp + SWAP_DEADLINE_SEC), @@ -180,24 +181,22 @@ contract IntegralSwapAdapter is ISwapAdapter { /// @param buyToken The address of the token being bought. /// @param amountBought The amount of buyToken tokens to buy. /// @return uint256 The amount of tokens received. - function buy(IERC20 sellToken, IERC20 buyToken, uint256 amountBought) + function buy(address sellToken, address buyToken, uint256 amountBought) internal returns (uint256) { - uint256 amountIn = relayer.quoteBuy( - address(sellToken), address(buyToken), amountBought - ); + uint256 amountIn = relayer.quoteBuy(sellToken, buyToken, amountBought); if (amountIn == 0) { revert Unavailable("AmountIn is zero!"); } - sellToken.safeTransferFrom(msg.sender, address(this), amountIn); - sellToken.safeIncreaseAllowance(address(relayer), amountIn); + IERC20(sellToken).safeTransferFrom(msg.sender, address(this), amountIn); + IERC20(sellToken).safeIncreaseAllowance(address(relayer), amountIn); relayer.buy( ITwapRelayer.BuyParams({ - tokenIn: address(sellToken), - tokenOut: address(buyToken), + tokenIn: sellToken, + tokenOut: buyToken, wrapUnwrap: false, to: msg.sender, submitDeadline: uint32(block.timestamp + SWAP_DEADLINE_SEC), @@ -217,20 +216,18 @@ contract IntegralSwapAdapter is ISwapAdapter { view returns (Fraction memory) { - uint256 priceWithoutFee = relayer.getPriceByTokenAddresses( - address(sellToken), address(buyToken) - ); + uint256 priceWithoutFee = + relayer.getPriceByTokenAddresses(sellToken, buyToken); ITwapFactory factory = ITwapFactory(relayer.factory()); - address pairAddress = - factory.getPair(address(sellToken), address(buyToken)); + address pairAddress = factory.getPair(sellToken, buyToken); // get swapFee formatted; swapFee is a constant uint256 swapFeeFormatted = (STANDARD_TOKEN_DECIMALS - relayer.swapFee(pairAddress)); // get token decimals - uint256 sellTokenDecimals = 10 ** ERC20(sellToken).decimals(); - uint256 buyTokenDecimals = 10 ** ERC20(buyToken).decimals(); + uint256 sellTokenDecimals = 10 ** IERC20Metadata(sellToken).decimals(); + uint256 buyTokenDecimals = 10 ** IERC20Metadata(buyToken).decimals(); /** * @dev diff --git a/evm/src/interfaces/ISwapAdapter.sol b/evm/src/interfaces/ISwapAdapter.sol index 178f2c6..ab3bacc 100644 --- a/evm/src/interfaces/ISwapAdapter.sol +++ b/evm/src/interfaces/ISwapAdapter.sol @@ -1,7 +1,6 @@ // SPDX-License-Identifier: AGPL-3.0-or-later pragma solidity ^0.8.13; -import {IERC20} from "openzeppelin-contracts/contracts/interfaces/IERC20.sol"; import {ISwapAdapterTypes} from "src/interfaces/ISwapAdapterTypes.sol"; /// @title ISwapAdapter @@ -35,10 +34,10 @@ interface ISwapAdapter is ISwapAdapterTypes { /// provided amounts. function price( bytes32 poolId, - IERC20 sellToken, - IERC20 buyToken, + address sellToken, + address buyToken, uint256[] memory specifiedAmounts - ) external view returns (Fraction[] memory prices); + ) external returns (Fraction[] memory prices); /** * @notice Simulates swapping tokens on a given pool. @@ -59,8 +58,8 @@ interface ISwapAdapter is ISwapAdapterTypes { */ function swap( bytes32 poolId, - IERC20 sellToken, - IERC20 buyToken, + address sellToken, + address buyToken, OrderSide side, uint256 specifiedAmount ) external returns (Trade memory trade); @@ -76,25 +75,27 @@ interface ISwapAdapter is ISwapAdapterTypes { /// @param sellToken The token being sold. /// @param buyToken The token being bought. /// @return limits An array of limits. - function getLimits(bytes32 poolId, IERC20 sellToken, IERC20 buyToken) + function getLimits(bytes32 poolId, address sellToken, address buyToken) external returns (uint256[] memory limits); /// @notice Retrieves the capabilities of the selected pool. /// @param poolId The ID of the trading pool. /// @return capabilities An array of Capability. - function getCapabilities(bytes32 poolId, IERC20 sellToken, IERC20 buyToken) - external - returns (Capability[] memory capabilities); + function getCapabilities( + bytes32 poolId, + address sellToken, + address buyToken + ) external returns (Capability[] memory capabilities); /// @notice Retrieves the tokens in the selected pool. /// @dev Mainly used for testing as this is redundant with the required /// substreams implementation. /// @param poolId The ID of the trading pool. - /// @return tokens An array of IERC20 contracts. + /// @return tokens An array of address contracts. function getTokens(bytes32 poolId) external - returns (IERC20[] memory tokens); + returns (address[] memory tokens); /// @notice Retrieves a range of pool IDs. /// @dev Mainly used for testing. It is alright to not return all available diff --git a/evm/src/interfaces/ISwapAdapterTypes.sol b/evm/src/interfaces/ISwapAdapterTypes.sol index 13993a2..85e12f4 100644 --- a/evm/src/interfaces/ISwapAdapterTypes.sol +++ b/evm/src/interfaces/ISwapAdapterTypes.sol @@ -1,8 +1,6 @@ // SPDX-License-Identifier: AGPL-3.0-or-later pragma solidity ^0.8.13; -import {IERC20} from "openzeppelin-contracts/contracts/interfaces/IERC20.sol"; - interface ISwapAdapterTypes { /// @dev The OrderSide enum represents possible sides of a trade: Sell or /// Buy. E.g. if OrderSide is Sell, the sell amount is interpreted to be diff --git a/evm/src/template/TemplateSwapAdapter.sol b/evm/src/template/TemplateSwapAdapter.sol index e7f1dcc..b64d64c 100644 --- a/evm/src/template/TemplateSwapAdapter.sol +++ b/evm/src/template/TemplateSwapAdapter.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: AGPL-3.0-or-later pragma solidity ^0.8.13; -import {IERC20, ISwapAdapter} from "src/interfaces/ISwapAdapter.sol"; +import {ISwapAdapter} from "src/interfaces/ISwapAdapter.sol"; /// @title TemplateSwapAdapter /// @dev This is a template for a swap adapter. @@ -10,8 +10,8 @@ import {IERC20, ISwapAdapter} from "src/interfaces/ISwapAdapter.sol"; contract TemplateSwapAdapter is ISwapAdapter { function price( bytes32 _poolId, - IERC20 _sellToken, - IERC20 _buyToken, + address _sellToken, + address _buyToken, uint256[] memory _specifiedAmounts ) external view override returns (Fraction[] memory _prices) { revert NotImplemented("TemplateSwapAdapter.price"); @@ -19,31 +19,32 @@ contract TemplateSwapAdapter is ISwapAdapter { function swap( bytes32 poolId, - IERC20 sellToken, - IERC20 buyToken, + address sellToken, + address buyToken, OrderSide side, uint256 specifiedAmount ) external returns (Trade memory trade) { revert NotImplemented("TemplateSwapAdapter.swap"); } - function getLimits(bytes32 poolId, IERC20 sellToken, IERC20 buyToken) + function getLimits(bytes32 poolId, address sellToken, address buyToken) external returns (uint256[] memory limits) { revert NotImplemented("TemplateSwapAdapter.getLimits"); } - function getCapabilities(bytes32 poolId, IERC20 sellToken, IERC20 buyToken) - external - returns (Capability[] memory capabilities) - { + function getCapabilities( + bytes32 poolId, + address sellToken, + address buyToken + ) external returns (Capability[] memory capabilities) { revert NotImplemented("TemplateSwapAdapter.getCapabilities"); } function getTokens(bytes32 poolId) external - returns (IERC20[] memory tokens) + returns (address[] memory tokens) { revert NotImplemented("TemplateSwapAdapter.getTokens"); } diff --git a/evm/src/template/manifest.yaml b/evm/src/template/manifest.yaml index 069da33..c15890d 100644 --- a/evm/src/template/manifest.yaml +++ b/evm/src/template/manifest.yaml @@ -5,8 +5,9 @@ author: # Protocol Constants constants: + # The expected average gas cost of a swap protocol_gas: 30000 - # minimum capabilities we can expect, individual pools may extend these + # Minimum capabilities we can expect, individual pools may extend these capabilities: - SellSide - BuySide diff --git a/evm/src/uniswap-v2/UniswapV2SwapAdapter.sol b/evm/src/uniswap-v2/UniswapV2SwapAdapter.sol index 8704677..679a6b4 100644 --- a/evm/src/uniswap-v2/UniswapV2SwapAdapter.sol +++ b/evm/src/uniswap-v2/UniswapV2SwapAdapter.sol @@ -1,12 +1,18 @@ // SPDX-License-Identifier: AGPL-3.0-or-later pragma solidity ^0.8.13; -import {IERC20, ISwapAdapter} from "src/interfaces/ISwapAdapter.sol"; +import {ISwapAdapter} from "src/interfaces/ISwapAdapter.sol"; +import { + IERC20, + SafeERC20 +} from "openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol"; // Uniswap handles arbirary amounts, but we limit the amount to 10x just in case uint256 constant RESERVE_LIMIT_FACTOR = 10; contract UniswapV2SwapAdapter is ISwapAdapter { + using SafeERC20 for IERC20; + IUniswapV2Factory immutable factory; constructor(address factory_) { @@ -16,8 +22,8 @@ contract UniswapV2SwapAdapter is ISwapAdapter { /// @inheritdoc ISwapAdapter function price( bytes32 poolId, - IERC20 sellToken, - IERC20 buyToken, + address sellToken, + address buyToken, uint256[] memory specifiedAmounts ) external view override returns (Fraction[] memory prices) { prices = new Fraction[](specifiedAmounts.length); @@ -60,8 +66,8 @@ contract UniswapV2SwapAdapter is ISwapAdapter { /// @inheritdoc ISwapAdapter function swap( bytes32 poolId, - IERC20 sellToken, - IERC20 buyToken, + address sellToken, + address buyToken, OrderSide side, uint256 specifiedAmount ) external override returns (Trade memory trade) { @@ -104,7 +110,7 @@ contract UniswapV2SwapAdapter is ISwapAdapter { /// @return calculatedAmount The amount of tokens received. function sell( IUniswapV2Pair pair, - IERC20 sellToken, + address sellToken, bool zero2one, uint112 reserveIn, uint112 reserveOut, @@ -113,8 +119,7 @@ contract UniswapV2SwapAdapter is ISwapAdapter { address swapper = msg.sender; uint256 amountOut = getAmountOut(amount, reserveIn, reserveOut); - // TODO: use safeTransferFrom - sellToken.transferFrom(swapper, address(pair), amount); + IERC20(sellToken).safeTransferFrom(swapper, address(pair), amount); if (zero2one) { pair.swap(0, amountOut, swapper, ""); } else { @@ -156,7 +161,7 @@ contract UniswapV2SwapAdapter is ISwapAdapter { /// @return calculatedAmount The amount of tokens sold. function buy( IUniswapV2Pair pair, - IERC20 sellToken, + address sellToken, bool zero2one, uint112 reserveIn, uint112 reserveOut, @@ -168,8 +173,8 @@ contract UniswapV2SwapAdapter is ISwapAdapter { if (amount == 0) { return 0; } - // TODO: use safeTransferFrom - sellToken.transferFrom(swapper, address(pair), amount); + + IERC20(sellToken).safeTransferFrom(swapper, address(pair), amount); if (zero2one) { pair.swap(0, amountOut, swapper, ""); } else { @@ -203,7 +208,7 @@ contract UniswapV2SwapAdapter is ISwapAdapter { } /// @inheritdoc ISwapAdapter - function getLimits(bytes32 poolId, IERC20 sellToken, IERC20 buyToken) + function getLimits(bytes32 poolId, address sellToken, address buyToken) external view override @@ -222,7 +227,7 @@ contract UniswapV2SwapAdapter is ISwapAdapter { } /// @inheritdoc ISwapAdapter - function getCapabilities(bytes32, IERC20, IERC20) + function getCapabilities(bytes32, address, address) external pure override @@ -239,12 +244,12 @@ contract UniswapV2SwapAdapter is ISwapAdapter { external view override - returns (IERC20[] memory tokens) + returns (address[] memory tokens) { - tokens = new IERC20[](2); + tokens = new address[](2); IUniswapV2Pair pair = IUniswapV2Pair(address(bytes20(poolId))); - tokens[0] = IERC20(pair.token0()); - tokens[1] = IERC20(pair.token1()); + tokens[0] = address(pair.token0()); + tokens[1] = address(pair.token1()); } /// @inheritdoc ISwapAdapter diff --git a/evm/test/AngleAdapter.t.sol b/evm/test/AngleAdapter.t.sol new file mode 100644 index 0000000..b8e50c6 --- /dev/null +++ b/evm/test/AngleAdapter.t.sol @@ -0,0 +1,200 @@ +// 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/angle/AngleAdapter.sol"; +import "src/interfaces/ISwapAdapterTypes.sol"; +import "src/libraries/FractionMath.sol"; +import "forge-std/console.sol"; + +contract AngleAdapterTest is Test, ISwapAdapterTypes { + using FractionMath for Fraction; + + AngleAdapter adapter; + IERC20 agEUR; + IERC20 constant EURC = IERC20(0x1aBaEA1f7C830bD89Acc67eC4af516284b1bC33c); + ITransmuter constant transmuter = + ITransmuter(0x00253582b2a3FE112feEC532221d9708c64cEFAb); + + uint256 constant TEST_ITERATIONS = 100; + + function setUp() public { + uint256 forkBlock = 18921770; + vm.createSelectFork(vm.rpcUrl("mainnet"), forkBlock); + adapter = new AngleAdapter(transmuter); + agEUR = IERC20(transmuter.agToken()); + + vm.label(address(adapter), "AngleAdapter"); + vm.label(address(agEUR), "agEUR"); + vm.label(address(EURC), "EURC"); + } + + function testSwapFuzzAngleMint(uint256 specifiedAmount, bool isBuy) + public + { + OrderSide side = isBuy ? OrderSide.Buy : OrderSide.Sell; + + bytes32 pair = bytes32(0); + uint256[] memory limits = + adapter.getLimits(pair, address(EURC), address(agEUR)); + + if (side == OrderSide.Buy) { + vm.assume(specifiedAmount < limits[1] && specifiedAmount > 0); + + deal(address(EURC), address(this), type(uint256).max); + EURC.approve(address(adapter), type(uint256).max); + } else { + vm.assume(specifiedAmount < limits[0] && specifiedAmount > 0); + + deal(address(EURC), address(this), specifiedAmount); + EURC.approve(address(adapter), specifiedAmount); + } + + uint256 eurc_balance = EURC.balanceOf(address(this)); + uint256 agEUR_balance = agEUR.balanceOf(address(this)); + + Trade memory trade = adapter.swap( + pair, address(EURC), address(agEUR), side, specifiedAmount + ); + + if (trade.calculatedAmount > 0) { + if (side == OrderSide.Buy) { + assertEq( + specifiedAmount, + agEUR.balanceOf(address(this)) - agEUR_balance + ); + assertEq( + trade.calculatedAmount, + eurc_balance - EURC.balanceOf(address(this)) + ); + } else { + assertEq( + specifiedAmount, + eurc_balance - EURC.balanceOf(address(this)) + ); + assertEq( + trade.calculatedAmount, + agEUR.balanceOf(address(this)) - agEUR_balance + ); + } + } + } + + function testSwapFuzzAngleRedeem(uint256 specifiedAmount, bool isBuy) + public + { + OrderSide side = isBuy ? OrderSide.Buy : OrderSide.Sell; + + bytes32 pair = bytes32(0); + uint256[] memory limits = + adapter.getLimits(pair, address(agEUR), address(EURC)); + + if (side == OrderSide.Buy) { + vm.assume(specifiedAmount < limits[1] && specifiedAmount > 0); + + deal(address(agEUR), address(this), type(uint256).max); + agEUR.approve(address(adapter), type(uint256).max); + } else { + vm.assume(specifiedAmount < limits[0] && specifiedAmount > 0); + + deal(address(agEUR), address(this), specifiedAmount); + agEUR.approve(address(adapter), specifiedAmount); + } + + uint256 eurc_balance = EURC.balanceOf(address(this)); + uint256 agEUR_balance = agEUR.balanceOf(address(this)); + + Trade memory trade = adapter.swap( + pair, address(agEUR), address(EURC), side, specifiedAmount + ); + + if (trade.calculatedAmount > 0) { + if (side == OrderSide.Buy) { + assertEq( + specifiedAmount, + EURC.balanceOf(address(this)) - eurc_balance + ); + assertEq( + trade.calculatedAmount, + agEUR_balance - agEUR.balanceOf(address(this)) + ); + } else { + assertEq( + specifiedAmount, + agEUR_balance - agEUR.balanceOf(address(this)) + ); + assertEq( + trade.calculatedAmount, + EURC.balanceOf(address(this)) - eurc_balance + ); + } + } + } + + function testSwapSellIncreasingAngle() public { + executeIncreasingSwapsAngle(OrderSide.Sell); + } + + function executeIncreasingSwapsAngle(OrderSide side) internal { + bytes32 pair = bytes32(0); + + uint256[] memory amounts = new uint256[](TEST_ITERATIONS); + for (uint256 i = 0; i < TEST_ITERATIONS; i++) { + amounts[i] = side == OrderSide.Sell + ? (100 * i * 10 ** 18) + : (100 * i * 10 ** 6); + } + + Trade[] memory trades = new Trade[](TEST_ITERATIONS); + uint256 beforeSwap; + for (uint256 i = 1; i < TEST_ITERATIONS; i++) { + beforeSwap = vm.snapshot(); + + if (side == OrderSide.Sell) { + deal(address(agEUR), address(this), amounts[i]); + agEUR.approve(address(adapter), amounts[i]); + } else { + deal(address(agEUR), address(this), type(uint256).max); + agEUR.approve(address(adapter), type(uint256).max); + } + trades[i] = adapter.swap( + pair, address(agEUR), address(EURC), side, amounts[i] + ); + vm.revertTo(beforeSwap); + } + + for (uint256 i = 1; i < TEST_ITERATIONS - 1; i++) { + assertLe(trades[i].calculatedAmount, trades[i + 1].calculatedAmount); + assertFalse( + trades[i].price.compareFractions(trades[i + 1].price) == -1 + ); + } + } + + function testSwapBuyIncreasingAngle() public { + executeIncreasingSwapsAngle(OrderSide.Buy); + } + + function testGetCapabilitiesAngle(bytes32 pair, address t0, address t1) + public + { + Capability[] memory res = adapter.getCapabilities(pair, t0, t1); + + assertEq(res.length, 2); + } + + function testGetTokensAngle() public { + address[] memory tokens = adapter.getTokens(bytes32(0)); + + assertGe(tokens.length, 2); + } + + function testGetLimitsAngle() public { + bytes32 pair = bytes32(0); + uint256[] memory limits = + adapter.getLimits(pair, address(agEUR), address(EURC)); + + assertEq(limits.length, 2); + } +} diff --git a/evm/test/BalancerV2SwapAdapter.t.sol b/evm/test/BalancerV2SwapAdapter.t.sol index fcb6328..20f4f8d 100644 --- a/evm/test/BalancerV2SwapAdapter.t.sol +++ b/evm/test/BalancerV2SwapAdapter.t.sol @@ -17,8 +17,8 @@ contract BalancerV2SwapAdapterTest is Test, ISwapAdapterTypes { IVault(payable(0xBA12222222228d8Ba445958a75a0704d566BF2C8)); BalancerV2SwapAdapter adapter; - IERC20 constant WETH = IERC20(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2); - IERC20 constant BAL = IERC20(0xba100000625a3754423978a60c9317c58a424e3D); + address constant WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; + address constant BAL = 0xba100000625a3754423978a60c9317c58a424e3D; address constant B_80BAL_20WETH = 0x5c6Ee304399DBdB9C8Ef030aB642B10820DB8F56; bytes32 constant B_80BAL_20WETH_POOL_ID = 0x5c6ee304399dbdb9c8ef030ab642b10820db8f56000200000000000000000014; @@ -34,20 +34,22 @@ contract BalancerV2SwapAdapterTest is Test, ISwapAdapterTypes { vm.label(address(balancerV2Vault), "IVault"); vm.label(address(adapter), "BalancerV2SwapAdapter"); vm.label(address(WETH), "WETH"); - vm.label(address(BAL), "BAL"); + vm.label(BAL, "BAL"); vm.label(address(B_80BAL_20WETH), "B_80BAL_20WETH"); } function testPrice() public { uint256[] memory amounts = new uint256[](2); - amounts[0] = 100; - amounts[1] = 200; - vm.expectRevert( - abi.encodeWithSelector( - NotImplemented.selector, "BalancerV2SwapAdapter.price" - ) - ); - adapter.price(B_80BAL_20WETH_POOL_ID, BAL, WETH, amounts); + amounts[0] = 1e18; + amounts[1] = 2e18; + + Fraction[] memory prices = + adapter.price(B_80BAL_20WETH_POOL_ID, BAL, WETH, amounts); + + for (uint256 i = 0; i < prices.length; i++) { + assertGt(prices[i].numerator, 0); + assertGt(prices[i].denominator, 0); + } } function testPriceSingleFuzz() public { @@ -97,17 +99,17 @@ contract BalancerV2SwapAdapterTest is Test, ISwapAdapterTypes { // TODO calculate the amountIn by using price function as in // testPriceDecreasing - deal(address(BAL), address(this), type(uint256).max); - BAL.approve(address(adapter), type(uint256).max); + deal(BAL, address(this), type(uint256).max); + IERC20(BAL).approve(address(adapter), type(uint256).max); } else { vm.assume(specifiedAmount < limits[0]); - deal(address(BAL), address(this), specifiedAmount); - BAL.approve(address(adapter), specifiedAmount); + deal(BAL, address(this), specifiedAmount); + IERC20(BAL).approve(address(adapter), specifiedAmount); } - uint256 bal_balance = BAL.balanceOf(address(this)); - uint256 weth_balance = WETH.balanceOf(address(this)); + uint256 bal_balance = IERC20(BAL).balanceOf(address(this)); + uint256 weth_balance = IERC20(WETH).balanceOf(address(this)); Trade memory trade = adapter.swap( B_80BAL_20WETH_POOL_ID, BAL, WETH, side, specifiedAmount @@ -117,19 +119,20 @@ contract BalancerV2SwapAdapterTest is Test, ISwapAdapterTypes { if (side == OrderSide.Buy) { assertEq( specifiedAmount, - WETH.balanceOf(address(this)) - weth_balance + IERC20(WETH).balanceOf(address(this)) - weth_balance ); assertEq( trade.calculatedAmount, - bal_balance - BAL.balanceOf(address(this)) + bal_balance - IERC20(BAL).balanceOf(address(this)) ); } else { assertEq( - specifiedAmount, bal_balance - BAL.balanceOf(address(this)) + specifiedAmount, + bal_balance - IERC20(BAL).balanceOf(address(this)) ); assertEq( trade.calculatedAmount, - WETH.balanceOf(address(this)) - weth_balance + IERC20(WETH).balanceOf(address(this)) - weth_balance ); } } @@ -144,8 +147,8 @@ contract BalancerV2SwapAdapterTest is Test, ISwapAdapterTypes { uint256 beforeSwap = vm.snapshot(); - deal(address(BAL), address(this), amounts[i]); - BAL.approve(address(adapter), amounts[i]); + deal(BAL, address(this), amounts[i]); + IERC20(BAL).approve(address(adapter), amounts[i]); trades[i] = adapter.swap( B_80BAL_20WETH_POOL_ID, BAL, WETH, OrderSide.Sell, amounts[i] ); @@ -175,8 +178,8 @@ contract BalancerV2SwapAdapterTest is Test, ISwapAdapterTypes { uint256 amountIn = (amounts[i] * price.denominator / price.numerator) * 2; - deal(address(BAL), address(this), amountIn); - BAL.approve(address(adapter), amountIn); + deal(BAL, address(this), amountIn); + IERC20(BAL).approve(address(adapter), amountIn); trades[i] = adapter.swap( B_80BAL_20WETH_POOL_ID, BAL, WETH, OrderSide.Buy, amounts[i] ); @@ -203,19 +206,19 @@ contract BalancerV2SwapAdapterTest is Test, ISwapAdapterTypes { function testGetCapabilitiesFuzz(bytes32 pool, address t0, address t1) public { - Capability[] memory res = - adapter.getCapabilities(pool, IERC20(t0), IERC20(t1)); + Capability[] memory res = adapter.getCapabilities(pool, t0, t1); - assertEq(res.length, 2); + assertEq(res.length, 3); assertEq(uint256(res[0]), uint256(Capability.SellOrder)); assertEq(uint256(res[1]), uint256(Capability.BuyOrder)); + assertEq(uint256(res[2]), uint256(Capability.PriceFunction)); } function testGetTokens() public { - IERC20[] memory tokens = adapter.getTokens(B_80BAL_20WETH_POOL_ID); + address[] memory tokens = adapter.getTokens(B_80BAL_20WETH_POOL_ID); - assertEq(address(tokens[0]), address(BAL)); - assertEq(address(tokens[1]), address(WETH)); + assertEq(tokens[0], BAL); + assertEq(tokens[1], address(WETH)); } function testGetPoolIds() public { diff --git a/evm/test/EtherfiAdapter.t.sol b/evm/test/EtherfiAdapter.t.sol new file mode 100644 index 0000000..366561a --- /dev/null +++ b/evm/test/EtherfiAdapter.t.sol @@ -0,0 +1,410 @@ +// 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, address(address(weEth)), address(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, address(address(weEth)), address(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, address(eEth_), address(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, + address(address(0)), + address(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, + address(address(0)), + address(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, address(eEth_), address(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, address(weEth_), address(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, + address(address(0)), + address(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, + address(address(0)), + address(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, address(weEth_), address(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; + + address eth_ = address(0); + IERC20 eEth_ = IERC20(address(eEth)); + bytes32 pair = bytes32(0); + uint256[] memory limits = adapter.getLimits(pair, eth_, address(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_, address(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; + + address eth_ = address(0); + IERC20 weEth_ = IERC20(address(weEth)); + bytes32 pair = bytes32(0); + uint256[] memory limits = adapter.getLimits(pair, eth_, address(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_, address(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, + address(address(weEth)), + address(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].gasUsed, trades[i + 1].gasUsed); + } + } + + function testGetCapabilitiesEtherfi(bytes32 pair, address t0, address t1) + public + { + Capability[] memory res = + adapter.getCapabilities(pair, address(t0), address(t1)); + + assertEq(res.length, 3); + } + + function testGetTokensEtherfi() public { + bytes32 pair = bytes32(0); + address[] memory tokens = adapter.getTokens(pair); + + assertEq(tokens.length, 3); + } + + function testGetLimitsEtherfi() public { + bytes32 pair = bytes32(0); + uint256[] memory limits = + adapter.getLimits(pair, address(eEth), address(weEth)); + + assertEq(limits.length, 2); + } +} diff --git a/evm/test/IntegralSwapAdapter.t.sol b/evm/test/IntegralSwapAdapter.t.sol index dcba8fb..8c3e129 100644 --- a/evm/test/IntegralSwapAdapter.t.sol +++ b/evm/test/IntegralSwapAdapter.t.sol @@ -12,8 +12,8 @@ contract IntegralSwapAdapterTest is Test, ISwapAdapterTypes { IntegralSwapAdapter adapter; ITwapRelayer relayer; - IERC20 constant WETH = IERC20(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2); - IERC20 constant USDC = IERC20(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48); + address constant WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; + address constant USDC = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48; address constant USDC_WETH_PAIR = 0x2fe16Dd18bba26e457B7dD2080d5674312b026a2; address constant relayerAddress = 0xd17b3c9784510E33cD5B87b490E79253BcD81e2E; @@ -26,7 +26,7 @@ contract IntegralSwapAdapterTest is Test, ISwapAdapterTypes { relayer = ITwapRelayer(relayerAddress); vm.label(address(WETH), "WETH"); - vm.label(address(USDC), "USDC"); + vm.label(USDC, "USDC"); vm.label(address(USDC_WETH_PAIR), "USDC_WETH_PAIR"); } @@ -66,8 +66,8 @@ contract IntegralSwapAdapterTest is Test, ISwapAdapterTypes { limitsMin = getMinLimits(USDC, WETH); vm.assume(specifiedAmount > limitsMin[1] * 115 / 100); - deal(address(USDC), address(this), type(uint256).max); - USDC.approve(address(adapter), type(uint256).max); + deal(USDC, address(this), type(uint256).max); + IERC20(USDC).approve(address(adapter), type(uint256).max); } else { limits = adapter.getLimits(pair, USDC, WETH); vm.assume(specifiedAmount < limits[0]); @@ -75,12 +75,12 @@ contract IntegralSwapAdapterTest is Test, ISwapAdapterTypes { limitsMin = getMinLimits(USDC, WETH); vm.assume(specifiedAmount > limitsMin[0] * 115 / 100); - deal(address(USDC), address(this), type(uint256).max); - USDC.approve(address(adapter), specifiedAmount); + deal(USDC, address(this), type(uint256).max); + IERC20(USDC).approve(address(adapter), specifiedAmount); } - uint256 usdc_balance_before = USDC.balanceOf(address(this)); - uint256 weth_balance_before = WETH.balanceOf(address(this)); + uint256 usdc_balance_before = IERC20(USDC).balanceOf(address(this)); + uint256 weth_balance_before = IERC20(WETH).balanceOf(address(this)); Trade memory trade = adapter.swap(pair, USDC, WETH, side, specifiedAmount); @@ -89,22 +89,22 @@ contract IntegralSwapAdapterTest is Test, ISwapAdapterTypes { if (side == OrderSide.Buy) { assertEq( specifiedAmount, - WETH.balanceOf(address(this)) - weth_balance_before + IERC20(WETH).balanceOf(address(this)) - weth_balance_before ); assertEq( trade.calculatedAmount, - usdc_balance_before - USDC.balanceOf(address(this)) + usdc_balance_before - IERC20(USDC).balanceOf(address(this)) ); } else { assertEq( specifiedAmount, - usdc_balance_before - USDC.balanceOf(address(this)) + usdc_balance_before - IERC20(USDC).balanceOf(address(this)) ); assertEq( trade.calculatedAmount, - WETH.balanceOf(address(this)) - weth_balance_before + IERC20(WETH).balanceOf(address(this)) - weth_balance_before ); } } @@ -135,8 +135,8 @@ contract IntegralSwapAdapterTest is Test, ISwapAdapterTypes { for (uint256 i = 1; i < TEST_ITERATIONS; i++) { beforeSwap = vm.snapshot(); - deal(address(USDC), address(this), amounts[i]); - USDC.approve(address(adapter), amounts[i]); + deal(USDC, address(this), amounts[i]); + IERC20(USDC).approve(address(adapter), amounts[i]); trades[i] = adapter.swap(pair, USDC, WETH, side, amounts[i]); vm.revertTo(beforeSwap); @@ -152,15 +152,14 @@ contract IntegralSwapAdapterTest is Test, ISwapAdapterTypes { function testGetCapabilitiesIntegral(bytes32 pair, address t0, address t1) public { - Capability[] memory res = - adapter.getCapabilities(pair, IERC20(t0), IERC20(t1)); + Capability[] memory res = adapter.getCapabilities(pair, t0, t1); assertEq(res.length, 4); } function testGetTokensIntegral() public { bytes32 pair = bytes32(bytes20(USDC_WETH_PAIR)); - IERC20[] memory tokens = adapter.getTokens(pair); + address[] memory tokens = adapter.getTokens(pair); assertEq(tokens.length, 2); } @@ -172,7 +171,7 @@ contract IntegralSwapAdapterTest is Test, ISwapAdapterTypes { assertEq(limits.length, 2); } - function getMinLimits(IERC20 sellToken, IERC20 buyToken) + function getMinLimits(address sellToken, address buyToken) public view returns (uint256[] memory limits) diff --git a/evm/test/UniswapV2SwapAdapter.t.sol b/evm/test/UniswapV2SwapAdapter.t.sol index b5a43fe..b275c82 100644 --- a/evm/test/UniswapV2SwapAdapter.t.sol +++ b/evm/test/UniswapV2SwapAdapter.t.sol @@ -11,8 +11,8 @@ contract UniswapV2PairFunctionTest is Test, ISwapAdapterTypes { using FractionMath for Fraction; UniswapV2SwapAdapter adapter; - IERC20 constant WETH = IERC20(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2); - IERC20 constant USDC = IERC20(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48); + address constant WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; + address constant USDC = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48; address constant USDC_WETH_PAIR = 0xB4e16d0168e52d35CaCD2c6185b44281Ec28C9Dc; uint256 constant TEST_ITERATIONS = 100; @@ -24,9 +24,9 @@ contract UniswapV2PairFunctionTest is Test, ISwapAdapterTypes { new UniswapV2SwapAdapter(0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f); vm.label(address(adapter), "UniswapV2SwapAdapter"); - vm.label(address(WETH), "WETH"); - vm.label(address(USDC), "USDC"); - vm.label(address(USDC_WETH_PAIR), "USDC_WETH_PAIR"); + vm.label(WETH, "WETH"); + vm.label(USDC, "USDC"); + vm.label(USDC_WETH_PAIR, "USDC_WETH_PAIR"); } function testPriceFuzz(uint256 amount0, uint256 amount1) public { @@ -75,17 +75,17 @@ contract UniswapV2PairFunctionTest is Test, ISwapAdapterTypes { // TODO calculate the amountIn by using price function as in // BalancerV2 testPriceDecreasing - deal(address(USDC), address(this), type(uint256).max); - USDC.approve(address(adapter), type(uint256).max); + deal(USDC, address(this), type(uint256).max); + IERC20(USDC).approve(address(adapter), type(uint256).max); } else { vm.assume(specifiedAmount < limits[0]); - deal(address(USDC), address(this), specifiedAmount); - USDC.approve(address(adapter), specifiedAmount); + deal(USDC, address(this), specifiedAmount); + IERC20(USDC).approve(address(adapter), specifiedAmount); } - uint256 usdc_balance = USDC.balanceOf(address(this)); - uint256 weth_balance = WETH.balanceOf(address(this)); + uint256 usdc_balance = IERC20(USDC).balanceOf(address(this)); + uint256 weth_balance = IERC20(WETH).balanceOf(address(this)); Trade memory trade = adapter.swap(pair, USDC, WETH, side, specifiedAmount); @@ -94,20 +94,20 @@ contract UniswapV2PairFunctionTest is Test, ISwapAdapterTypes { if (side == OrderSide.Buy) { assertEq( specifiedAmount, - WETH.balanceOf(address(this)) - weth_balance + IERC20(WETH).balanceOf(address(this)) - weth_balance ); assertEq( trade.calculatedAmount, - usdc_balance - USDC.balanceOf(address(this)) + usdc_balance - IERC20(USDC).balanceOf(address(this)) ); } else { assertEq( specifiedAmount, - usdc_balance - USDC.balanceOf(address(this)) + usdc_balance - IERC20(USDC).balanceOf(address(this)) ); assertEq( trade.calculatedAmount, - WETH.balanceOf(address(this)) - weth_balance + IERC20(WETH).balanceOf(address(this)) - weth_balance ); } } @@ -130,8 +130,8 @@ contract UniswapV2PairFunctionTest is Test, ISwapAdapterTypes { for (uint256 i = 0; i < TEST_ITERATIONS; i++) { beforeSwap = vm.snapshot(); - deal(address(USDC), address(this), amounts[i]); - USDC.approve(address(adapter), amounts[i]); + deal(USDC, address(this), amounts[i]); + IERC20(USDC).approve(address(adapter), amounts[i]); trades[i] = adapter.swap(pair, USDC, WETH, side, amounts[i]); vm.revertTo(beforeSwap); @@ -149,8 +149,7 @@ contract UniswapV2PairFunctionTest is Test, ISwapAdapterTypes { } function testGetCapabilities(bytes32 pair, address t0, address t1) public { - Capability[] memory res = - adapter.getCapabilities(pair, IERC20(t0), IERC20(t1)); + Capability[] memory res = adapter.getCapabilities(pair, t0, t1); assertEq(res.length, 3); }