From 94644ac735b22ba7805ff426656c99f2c747ec92 Mon Sep 17 00:00:00 2001 From: domenicodev Date: Mon, 22 Jan 2024 12:08:59 +0100 Subject: [PATCH 01/50] feat: Initial setup and price() function implementation --- evm/src/angle/AngleAdapter.sol | 282 +++++++++++++++++++++++++++++++++ evm/src/angle/manifest.yaml | 36 +++++ 2 files changed, 318 insertions(+) create mode 100644 evm/src/angle/AngleAdapter.sol create mode 100644 evm/src/angle/manifest.yaml diff --git a/evm/src/angle/AngleAdapter.sol b/evm/src/angle/AngleAdapter.sol new file mode 100644 index 0000000..b2363a7 --- /dev/null +++ b/evm/src/angle/AngleAdapter.sol @@ -0,0 +1,282 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +pragma experimental ABIEncoderV2; +pragma solidity ^0.8.13; + +import {IERC20, ISwapAdapter} from "src/interfaces/ISwapAdapter.sol"; + +/// @title AngleAdapter +contract AngleAdapter is ISwapAdapter { + + ITransmuter transmuter; + + constructor(ITransmuter _transmuter) { + transmuter = _transmuter; + } + + function price( + bytes32, + IERC20 _sellToken, + IERC20 _buyToken, + uint256[] memory _specifiedAmounts + ) external view override 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(_specifiedAmounts[i], sellTokenAddress, buyTokenAddress); + } + } + + function swap( + bytes32 poolId, + IERC20 sellToken, + IERC20 buyToken, + OrderSide side, + uint256 specifiedAmount + ) external returns (Trade memory trade) { + revert NotImplemented("TemplateSwapAdapter.swap"); + } + + function getLimits(bytes32, IERC20 sellToken, IERC20 buyToken) + external + returns (uint256[] memory limits) + { + revert NotImplemented("TemplateSwapAdapter.getLimits"); + } + + function getCapabilities(bytes32 poolId, IERC20 sellToken, IERC20 buyToken) + external + returns (Capability[] memory capabilities) + { + revert NotImplemented("TemplateSwapAdapter.getCapabilities"); + } + + function getTokens(bytes32 poolId) + external + returns (IERC20[] memory tokens) + { + revert NotImplemented("TemplateSwapAdapter.getTokens"); + } + + function getPoolIds(uint256 offset, uint256 limit) + external + returns (bytes32[] memory ids) + { + revert NotImplemented("TemplateSwapAdapter.getPoolIds"); + } + + /// @notice Calculates pool prices for specified amounts + /// @param amountIn The amount of the token being sold + /// @param tokenIn The token being sold + /// @param tokenOut The token being bought + /// @return The price as a fraction corresponding to the provided amount. + function getPriceAt(uint256 amountIn, address tokenIn, address tokenOut) + internal + view + returns (Fraction memory) + { + uint256 amountOut = transmuter.quoteIn(amountIn, tokenIn, tokenOut); + return Fraction( + amountOut, + amountIn + ); + } +} + +abstract contract ITransmuter { + struct Tuple6871229 { + address facetAddress; + uint8 action; + bytes4[] functionSelectors; + } + + struct Tuple1236461 { + address facetAddress; + bytes4[] functionSelectors; + } + + struct Tuple3550792 { + uint8 isManaged; + uint8 isMintLive; + uint8 isBurnLive; + uint8 decimals; + uint8 onlyWhitelisted; + uint216 normalizedStables; + uint64[] xFeeMint; + int64[] yFeeMint; + uint64[] xFeeBurn; + int64[] yFeeBurn; + bytes oracleConfig; + bytes whitelistData; + Tuple5479340 managerData; + } + + struct Tuple5479340 { + address[] subCollaterals; + bytes config; + } + + function diamondCut(Tuple6871229[] memory _diamondCut, address _init, bytes memory _calldata) external {} + + 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 facets() external view returns (Tuple1236461[] memory facets_) {} + + 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 (Tuple3550792 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 setCollateralManager(address collateral, Tuple5479340 memory managerData) 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..3298c49 --- /dev/null +++ b/evm/src/angle/manifest.yaml @@ -0,0 +1,36 @@ +# information about the author helps us reach out in case of issues. +author: + name: YourCompany + email: developer@yourcompany.xyz + +# 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: TemplateSwapAdapter.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 From ba289d5bd1f41d28db15d837ebca5f8299d55bbb Mon Sep 17 00:00:00 2001 From: domenicodev Date: Tue, 23 Jan 2024 16:54:38 +0100 Subject: [PATCH 02/50] feat: Added imports in a different file as too long, implemented getLimits and getTokens --- evm/src/angle/AngleAdapter.sol | 90 ++++++++-------- evm/src/angle/AngleUtils.sol | 187 +++++++++++++++++++++++++++++++++ 2 files changed, 235 insertions(+), 42 deletions(-) create mode 100644 evm/src/angle/AngleUtils.sol diff --git a/evm/src/angle/AngleAdapter.sol b/evm/src/angle/AngleAdapter.sol index b2363a7..062e27c 100644 --- a/evm/src/angle/AngleAdapter.sol +++ b/evm/src/angle/AngleAdapter.sol @@ -2,7 +2,8 @@ pragma experimental ABIEncoderV2; pragma solidity ^0.8.13; -import {IERC20, ISwapAdapter} from "src/interfaces/ISwapAdapter.sol"; +/// @dev Wrapped imports (incl. ISwapAdapter and IERC20) are included in utils +import "./AngleUtils.sol"; /// @title AngleAdapter contract AngleAdapter is ISwapAdapter { @@ -13,6 +14,7 @@ contract AngleAdapter is ISwapAdapter { transmuter = _transmuter; } + /// @inheritdoc ISwapAdapter function price( bytes32, IERC20 _sellToken, @@ -38,11 +40,42 @@ contract AngleAdapter is ISwapAdapter { revert NotImplemented("TemplateSwapAdapter.swap"); } + /// @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. function getLimits(bytes32, IERC20 sellToken, IERC20 buyToken) external + view + override returns (uint256[] memory limits) { - revert NotImplemented("TemplateSwapAdapter.getLimits"); + limits = new uint256[](2); + address sellTokenAddress = address(sellToken); + address buyTokenAddress = address(buyToken); + + if(buyTokenAddress == transmuter.agToken()) { // mint(buy agToken) + Collateral memory collatInfo = transmuter.getCollateralInfo(sellTokenAddress); + if(collatInfo.isManaged > 0) { + limits[0] = LibManager.maxAvailable(collatInfo.managerData.config); + } + else { + limits[0] = sellToken.balanceOf(address(transmuter)); + } + limits[1] = transmuter.quoteIn(limits[0], sellTokenAddress, buyTokenAddress); + } + else { // burn(sell agToken) + Collateral memory collatInfo = transmuter.getCollateralInfo(buyTokenAddress); + uint256 collatLimit; + if(collatInfo.isManaged > 0) { + collatLimit = LibManager.maxAvailable(collatInfo.managerData.config); + } + else { + collatLimit = buyToken.balanceOf(address(transmuter)); + } + limits[0] = transmuter.quoteIn(collatLimit, buyTokenAddress, sellTokenAddress); + limits[1] = collatLimit; + } } function getCapabilities(bytes32 poolId, IERC20 sellToken, IERC20 buyToken) @@ -52,11 +85,21 @@ contract AngleAdapter is ISwapAdapter { revert NotImplemented("TemplateSwapAdapter.getCapabilities"); } - function getTokens(bytes32 poolId) + /// @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 (IERC20[] memory tokens) { - revert NotImplemented("TemplateSwapAdapter.getTokens"); + address[] memory collateralsAddresses = transmuter.getCollateralList(); + tokens = new IERC20[](collateralsAddresses.length + 1); + for(uint256 i = 0; i < collateralsAddresses.length; i++) { + tokens[i] = IERC20(collateralsAddresses[i]); + } + tokens[collateralsAddresses.length] = IERC20(transmuter.agToken()); } function getPoolIds(uint256 offset, uint256 limit) @@ -85,39 +128,6 @@ contract AngleAdapter is ISwapAdapter { } abstract contract ITransmuter { - struct Tuple6871229 { - address facetAddress; - uint8 action; - bytes4[] functionSelectors; - } - - struct Tuple1236461 { - address facetAddress; - bytes4[] functionSelectors; - } - - struct Tuple3550792 { - uint8 isManaged; - uint8 isMintLive; - uint8 isBurnLive; - uint8 decimals; - uint8 onlyWhitelisted; - uint216 normalizedStables; - uint64[] xFeeMint; - int64[] yFeeMint; - uint64[] xFeeBurn; - int64[] yFeeBurn; - bytes oracleConfig; - bytes whitelistData; - Tuple5479340 managerData; - } - - struct Tuple5479340 { - address[] subCollaterals; - bytes config; - } - - function diamondCut(Tuple6871229[] memory _diamondCut, address _init, bytes memory _calldata) external {} function implementation() external view returns (address) {} @@ -129,8 +139,6 @@ abstract contract ITransmuter { function facetFunctionSelectors(address _facet) external view returns (bytes4[] memory _facetFunctionSelectors) {} - function facets() external view returns (Tuple1236461[] memory facets_) {} - function accessControlManager() external view returns (address) {} function agToken() external view returns (address) {} @@ -141,7 +149,7 @@ abstract contract ITransmuter { function getCollateralDecimals(address collateral) external view returns (uint8) {} - function getCollateralInfo(address collateral) external view returns (Tuple3550792 memory) {} + function getCollateralInfo(address collateral) external view returns (Collateral memory) {} function getCollateralList() external view returns (address[] memory) {} @@ -203,8 +211,6 @@ abstract contract ITransmuter { function setAccessControlManager(address _newAccessControlManager) external {} - function setCollateralManager(address collateral, Tuple5479340 memory managerData) external {} - function setOracle(address collateral, bytes memory oracleConfig) external {} function setWhitelistStatus(address collateral, uint8 whitelistStatus, bytes memory whitelistData) external {} diff --git a/evm/src/angle/AngleUtils.sol b/evm/src/angle/AngleUtils.sol new file mode 100644 index 0000000..95ca64c --- /dev/null +++ b/evm/src/angle/AngleUtils.sol @@ -0,0 +1,187 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.13; + +/** + * @dev Collection of wrapped interfaces, items and libraries for Angle + * we use them here to maintain integrity and readability of contracts + * as there are many different imports and items + */ + +import {IERC20, ISwapAdapter} from "src/interfaces/ISwapAdapter.sol"; + + +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)); + } +} From 339b0b17b9da9e01dc1baff0fe928848e97d4ad6 Mon Sep 17 00:00:00 2001 From: domenicodev Date: Tue, 23 Jan 2024 16:55:40 +0100 Subject: [PATCH 03/50] fix: Fixed manifest.yaml --- evm/src/angle/manifest.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/evm/src/angle/manifest.yaml b/evm/src/angle/manifest.yaml index 3298c49..88a2f39 100644 --- a/evm/src/angle/manifest.yaml +++ b/evm/src/angle/manifest.yaml @@ -1,7 +1,7 @@ # information about the author helps us reach out in case of issues. author: - name: YourCompany - email: developer@yourcompany.xyz + name: Propellerheads.xyz + email: alan@propellerheads.xyz # Protocol Constants constants: @@ -13,7 +13,7 @@ constants: - PriceFunction # The file containing the adapter contract -contract: TemplateSwapAdapter.sol +contract: AngleAdapter.sol # Deployment instances used to generate chain specific bytecode. instances: From 7c1e102f73b5bef8c3ff4b33b87bcc477f56473c Mon Sep 17 00:00:00 2001 From: domenicodev Date: Tue, 23 Jan 2024 16:58:29 +0100 Subject: [PATCH 04/50] fix: Fixed getPoolIds not implemented text, improved gas in getLimits --- evm/src/angle/AngleAdapter.sol | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/evm/src/angle/AngleAdapter.sol b/evm/src/angle/AngleAdapter.sol index 062e27c..d15c01c 100644 --- a/evm/src/angle/AngleAdapter.sol +++ b/evm/src/angle/AngleAdapter.sol @@ -53,6 +53,7 @@ contract AngleAdapter is ISwapAdapter { limits = new uint256[](2); address sellTokenAddress = address(sellToken); address buyTokenAddress = address(buyToken); + address transmuterAddress = address(transmuter); if(buyTokenAddress == transmuter.agToken()) { // mint(buy agToken) Collateral memory collatInfo = transmuter.getCollateralInfo(sellTokenAddress); @@ -60,7 +61,7 @@ contract AngleAdapter is ISwapAdapter { limits[0] = LibManager.maxAvailable(collatInfo.managerData.config); } else { - limits[0] = sellToken.balanceOf(address(transmuter)); + limits[0] = sellToken.balanceOf(transmuterAddress); } limits[1] = transmuter.quoteIn(limits[0], sellTokenAddress, buyTokenAddress); } @@ -71,7 +72,7 @@ contract AngleAdapter is ISwapAdapter { collatLimit = LibManager.maxAvailable(collatInfo.managerData.config); } else { - collatLimit = buyToken.balanceOf(address(transmuter)); + collatLimit = buyToken.balanceOf(transmuterAddress); } limits[0] = transmuter.quoteIn(collatLimit, buyTokenAddress, sellTokenAddress); limits[1] = collatLimit; @@ -102,11 +103,13 @@ contract AngleAdapter is ISwapAdapter { tokens[collateralsAddresses.length] = IERC20(transmuter.agToken()); } - function getPoolIds(uint256 offset, uint256 limit) + function getPoolIds(uint256, uint256) external - returns (bytes32[] memory ids) + pure + override + returns (bytes32[] memory) { - revert NotImplemented("TemplateSwapAdapter.getPoolIds"); + revert NotImplemented("AngleAdapter.getPoolIds"); } /// @notice Calculates pool prices for specified amounts From a4693f5300d425975621f6a0199949b19e1b9047 Mon Sep 17 00:00:00 2001 From: domenicodev Date: Tue, 23 Jan 2024 18:43:57 +0100 Subject: [PATCH 05/50] feat: Implemented swap() --- evm/src/angle/AngleAdapter.sol | 62 +++++++++++++++++++++++++++++++++- 1 file changed, 61 insertions(+), 1 deletion(-) diff --git a/evm/src/angle/AngleAdapter.sol b/evm/src/angle/AngleAdapter.sol index d15c01c..36f9a62 100644 --- a/evm/src/angle/AngleAdapter.sol +++ b/evm/src/angle/AngleAdapter.sol @@ -30,6 +30,7 @@ contract AngleAdapter is ISwapAdapter { } } + /// @inheritdoc ISwapAdapter function swap( bytes32 poolId, IERC20 sellToken, @@ -37,13 +38,28 @@ contract AngleAdapter is ISwapAdapter { OrderSide side, uint256 specifiedAmount ) external returns (Trade memory trade) { - revert NotImplemented("TemplateSwapAdapter.swap"); + if (specifiedAmount == 0) { + return trade; + } + + uint256 gasBefore = gasleft(); + if (side == OrderSide.Sell) { + trade.calculatedAmount = + sell(sellToken, buyToken, specifiedAmount); + trade.price = getPriceAt(specifiedAmount, address(sellToken), address(buyToken)); + } else { + trade.calculatedAmount = + buy(sellToken, buyToken, specifiedAmount); + trade.price = getPriceAt(specifiedAmount, address(buyToken), address(sellToken)); + } + trade.gasUsed = gasBefore - gasleft(); } /// @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 * 0.9 to ensure swaps with OrderSide.Buy won't fail anyway. function getLimits(bytes32, IERC20 sellToken, IERC20 buyToken) external view @@ -63,6 +79,7 @@ contract AngleAdapter is ISwapAdapter { else { limits[0] = sellToken.balanceOf(transmuterAddress); } + limits[0] = limits[0] * 90 / 100; limits[1] = transmuter.quoteIn(limits[0], sellTokenAddress, buyTokenAddress); } else { // burn(sell agToken) @@ -74,6 +91,7 @@ contract AngleAdapter is ISwapAdapter { else { collatLimit = buyToken.balanceOf(transmuterAddress); } + collatLimit = collatLimit * 90 / 100; limits[0] = transmuter.quoteIn(collatLimit, buyTokenAddress, sellTokenAddress); limits[1] = collatLimit; } @@ -128,6 +146,48 @@ contract AngleAdapter is ISwapAdapter { 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( + IERC20 sellToken, + IERC20 buyToken, + uint256 amount + ) internal returns (uint256 calculatedAmount) { + address sellTokenAddress = address(sellToken); + address buyTokenAddress = address(buyToken); + uint256 amountOut = transmuter.quoteIn(amount, sellTokenAddress, buyTokenAddress); + + // TODO: use safeTransferFrom + sellToken.transferFrom(msg.sender, address(this), amount); + sellToken.approve(address(transmuter), amount); + transmuter.swapExactInput(amount, 0, sellTokenAddress, buyTokenAddress, msg.sender, 0); + return amountOut; + } + + /// @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( + IERC20 sellToken, + IERC20 buyToken, + uint256 amountOut + ) internal returns (uint256 calculatedAmount) { + address sellTokenAddress = address(sellToken); + address buyTokenAddress = address(buyToken); + uint256 amountIn = transmuter.quoteOut(amountOut, sellTokenAddress, buyTokenAddress); + + // TODO: use safeTransferFrom + sellToken.transferFrom(msg.sender, address(this), amountIn); + sellToken.approve(address(transmuter), amountIn); + transmuter.swapExactOutput(amountOut, type(uint256).max, sellTokenAddress, buyTokenAddress, msg.sender, 0); + return amountIn; + } } abstract contract ITransmuter { From 16237824a58a2ebc8401fd21455c73d561116acf Mon Sep 17 00:00:00 2001 From: domenicodev Date: Tue, 23 Jan 2024 19:06:02 +0100 Subject: [PATCH 06/50] fix: Fixed divisions in getLimits, added RESERVE_LIMIT_FACTOR as constant --- evm/src/angle/AngleAdapter.sol | 11 +++++++---- evm/src/angle/AngleUtils.sol | 2 ++ 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/evm/src/angle/AngleAdapter.sol b/evm/src/angle/AngleAdapter.sol index 36f9a62..63c6ef3 100644 --- a/evm/src/angle/AngleAdapter.sol +++ b/evm/src/angle/AngleAdapter.sol @@ -4,6 +4,7 @@ pragma solidity ^0.8.13; /// @dev Wrapped imports (incl. ISwapAdapter and IERC20) are included in utils import "./AngleUtils.sol"; +import "forge-std/Test.sol"; /// @title AngleAdapter contract AngleAdapter is ISwapAdapter { @@ -59,7 +60,7 @@ contract AngleAdapter is 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 * 0.9 to ensure swaps with OrderSide.Buy won't fail anyway. + /// In addition, we underestimate to / RESERVE_LIMIT_FACTOR to ensure swaps with OrderSide.Buy won't fail anyway. function getLimits(bytes32, IERC20 sellToken, IERC20 buyToken) external view @@ -79,8 +80,9 @@ contract AngleAdapter is ISwapAdapter { else { limits[0] = sellToken.balanceOf(transmuterAddress); } - limits[0] = limits[0] * 90 / 100; limits[1] = transmuter.quoteIn(limits[0], sellTokenAddress, buyTokenAddress); + limits[1] = limits[1] / RESERVE_LIMIT_FACTOR; + limits[0] = limits[0] / RESERVE_LIMIT_FACTOR; } else { // burn(sell agToken) Collateral memory collatInfo = transmuter.getCollateralInfo(buyTokenAddress); @@ -91,9 +93,9 @@ contract AngleAdapter is ISwapAdapter { else { collatLimit = buyToken.balanceOf(transmuterAddress); } - collatLimit = collatLimit * 90 / 100; limits[0] = transmuter.quoteIn(collatLimit, buyTokenAddress, sellTokenAddress); - limits[1] = collatLimit; + limits[1] = collatLimit / RESERVE_LIMIT_FACTOR; + limits[0] = limits[0] / RESERVE_LIMIT_FACTOR; } } @@ -140,6 +142,7 @@ contract AngleAdapter is ISwapAdapter { view returns (Fraction memory) { + console.log("log", amountIn, tokenIn, tokenOut); uint256 amountOut = transmuter.quoteIn(amountIn, tokenIn, tokenOut); return Fraction( amountOut, diff --git a/evm/src/angle/AngleUtils.sol b/evm/src/angle/AngleUtils.sol index 95ca64c..7a9d1e6 100644 --- a/evm/src/angle/AngleUtils.sol +++ b/evm/src/angle/AngleUtils.sol @@ -9,6 +9,8 @@ pragma solidity ^0.8.13; import {IERC20, ISwapAdapter} from "src/interfaces/ISwapAdapter.sol"; +/// @dev custom reserve limit factor to prevent revert errors in OrderSide.Buy +uint256 constant RESERVE_LIMIT_FACTOR = 5; interface IAgToken is IERC20 { /*////////////////////////////////////////////////////////////////////////////////////////////////////////////////// From b589940dc9ad7ec4f8649f0ae5bd53eed95f74c9 Mon Sep 17 00:00:00 2001 From: domenicodev Date: Tue, 23 Jan 2024 19:07:32 +0100 Subject: [PATCH 07/50] fix: Removed console logs --- evm/src/angle/AngleAdapter.sol | 2 -- 1 file changed, 2 deletions(-) diff --git a/evm/src/angle/AngleAdapter.sol b/evm/src/angle/AngleAdapter.sol index 63c6ef3..81f5ee3 100644 --- a/evm/src/angle/AngleAdapter.sol +++ b/evm/src/angle/AngleAdapter.sol @@ -4,7 +4,6 @@ pragma solidity ^0.8.13; /// @dev Wrapped imports (incl. ISwapAdapter and IERC20) are included in utils import "./AngleUtils.sol"; -import "forge-std/Test.sol"; /// @title AngleAdapter contract AngleAdapter is ISwapAdapter { @@ -142,7 +141,6 @@ contract AngleAdapter is ISwapAdapter { view returns (Fraction memory) { - console.log("log", amountIn, tokenIn, tokenOut); uint256 amountOut = transmuter.quoteIn(amountIn, tokenIn, tokenOut); return Fraction( amountOut, From 42ac2b1b60189708142b8c5ea3d7c2e7c96b077c Mon Sep 17 00:00:00 2001 From: domenicodev Date: Wed, 24 Jan 2024 11:21:35 +0100 Subject: [PATCH 08/50] feat: Added top comment explaining prices --- evm/src/angle/AngleAdapter.sol | 3 +++ 1 file changed, 3 insertions(+) diff --git a/evm/src/angle/AngleAdapter.sol b/evm/src/angle/AngleAdapter.sol index 81f5ee3..26bf0bb 100644 --- a/evm/src/angle/AngleAdapter.sol +++ b/evm/src/angle/AngleAdapter.sol @@ -6,6 +6,9 @@ pragma solidity ^0.8.13; import "./AngleUtils.sol"; /// @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 { ITransmuter transmuter; From 69eeec4385a6f8e3bf4581bd242e51489129ea1a Mon Sep 17 00:00:00 2001 From: domenicodev Date: Wed, 24 Jan 2024 11:52:47 +0100 Subject: [PATCH 09/50] fix: Fixed getPriceAt to obtain the correct prices --- evm/src/angle/AngleAdapter.sol | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/evm/src/angle/AngleAdapter.sol b/evm/src/angle/AngleAdapter.sol index 26bf0bb..45833e6 100644 --- a/evm/src/angle/AngleAdapter.sol +++ b/evm/src/angle/AngleAdapter.sol @@ -29,7 +29,7 @@ contract AngleAdapter is ISwapAdapter { address buyTokenAddress = address(_buyToken); for (uint256 i = 0; i < _specifiedAmounts.length; i++) { - _prices[i] = getPriceAt(_specifiedAmounts[i], sellTokenAddress, buyTokenAddress); + _prices[i] = getPriceAt(_specifiedAmounts[i], sellTokenAddress, buyTokenAddress, OrderSide.Sell); } } @@ -49,12 +49,11 @@ contract AngleAdapter is ISwapAdapter { if (side == OrderSide.Sell) { trade.calculatedAmount = sell(sellToken, buyToken, specifiedAmount); - trade.price = getPriceAt(specifiedAmount, address(sellToken), address(buyToken)); } else { trade.calculatedAmount = buy(sellToken, buyToken, specifiedAmount); - trade.price = getPriceAt(specifiedAmount, address(buyToken), address(sellToken)); } + trade.price = getPriceAt(specifiedAmount, address(sellToken), address(buyToken), side); trade.gasUsed = gasBefore - gasleft(); } @@ -135,16 +134,26 @@ contract AngleAdapter is ISwapAdapter { } /// @notice Calculates pool prices for specified amounts - /// @param amountIn The amount of the token being sold + /// @param amount The amount of the token being sold(if side == Sell) or bought(if side == Buy) /// @param tokenIn The token being sold /// @param tokenOut The token being bought + /// @param side Order side /// @return The price as a fraction corresponding to the provided amount. - function getPriceAt(uint256 amountIn, address tokenIn, address tokenOut) + function getPriceAt(uint256 amount, address tokenIn, address tokenOut, OrderSide side) internal view returns (Fraction memory) { - uint256 amountOut = transmuter.quoteIn(amountIn, tokenIn, tokenOut); + uint256 amountOut; + uint256 amountIn; + if(side == OrderSide.Sell) { + amountIn = amount; + amountOut = transmuter.quoteIn(amountIn, tokenIn, tokenOut); + } + else { + amountOut = amount; + amountIn = transmuter.quoteOut(amount, tokenIn, tokenOut); + } return Fraction( amountOut, amountIn From 61bb75985cf21e1d8eb6289a172ce3a9b8b73b47 Mon Sep 17 00:00:00 2001 From: domenicodev Date: Wed, 24 Jan 2024 11:53:56 +0100 Subject: [PATCH 10/50] feat: Implemented getCapabilities --- evm/src/angle/AngleAdapter.sol | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/evm/src/angle/AngleAdapter.sol b/evm/src/angle/AngleAdapter.sol index 45833e6..55b5d2f 100644 --- a/evm/src/angle/AngleAdapter.sol +++ b/evm/src/angle/AngleAdapter.sol @@ -35,7 +35,7 @@ contract AngleAdapter is ISwapAdapter { /// @inheritdoc ISwapAdapter function swap( - bytes32 poolId, + bytes32, IERC20 sellToken, IERC20 buyToken, OrderSide side, @@ -100,11 +100,17 @@ contract AngleAdapter is ISwapAdapter { } } - function getCapabilities(bytes32 poolId, IERC20 sellToken, IERC20 buyToken) + /// @inheritdoc ISwapAdapter + function getCapabilities(bytes32, IERC20, IERC20) external + pure + override returns (Capability[] memory capabilities) { - revert NotImplemented("TemplateSwapAdapter.getCapabilities"); + capabilities = new Capability[](3); + capabilities[0] = Capability.SellOrder; + capabilities[1] = Capability.BuyOrder; + capabilities[2] = Capability.PriceFunction; } /// @inheritdoc ISwapAdapter From 8ef2f75faec68fb350c36c064987334b7285988f Mon Sep 17 00:00:00 2001 From: domenicodev Date: Wed, 24 Jan 2024 17:29:13 +0100 Subject: [PATCH 11/50] fix: Increased RESERVE_LIMIT_FACTOR to prevent reverts --- evm/src/angle/AngleUtils.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/evm/src/angle/AngleUtils.sol b/evm/src/angle/AngleUtils.sol index 7a9d1e6..a346784 100644 --- a/evm/src/angle/AngleUtils.sol +++ b/evm/src/angle/AngleUtils.sol @@ -10,7 +10,7 @@ pragma solidity ^0.8.13; import {IERC20, ISwapAdapter} from "src/interfaces/ISwapAdapter.sol"; /// @dev custom reserve limit factor to prevent revert errors in OrderSide.Buy -uint256 constant RESERVE_LIMIT_FACTOR = 5; +uint256 constant RESERVE_LIMIT_FACTOR = 10; interface IAgToken is IERC20 { /*////////////////////////////////////////////////////////////////////////////////////////////////////////////////// From 96915bd158e1ba9f1c8cbcdef3c25f8f2593920c Mon Sep 17 00:00:00 2001 From: domenicodev Date: Wed, 24 Jan 2024 17:33:10 +0100 Subject: [PATCH 12/50] feat: Moved imports to main adapter file --- evm/src/angle/AngleAdapter.sol | 182 ++++++++++++++++++++++++++- evm/src/angle/AngleUtils.sol | 189 ---------------------------- evm/test/AngleAdapter.t.sol | 223 +++++++++++++++++++++++++++++++++ 3 files changed, 403 insertions(+), 191 deletions(-) delete mode 100644 evm/src/angle/AngleUtils.sol create mode 100644 evm/test/AngleAdapter.t.sol diff --git a/evm/src/angle/AngleAdapter.sol b/evm/src/angle/AngleAdapter.sol index 55b5d2f..96aeab8 100644 --- a/evm/src/angle/AngleAdapter.sol +++ b/evm/src/angle/AngleAdapter.sol @@ -2,8 +2,10 @@ pragma experimental ABIEncoderV2; pragma solidity ^0.8.13; -/// @dev Wrapped imports (incl. ISwapAdapter and IERC20) are included in utils -import "./AngleUtils.sol"; +import {IERC20, ISwapAdapter} from "src/interfaces/ISwapAdapter.sol"; + +/// @dev custom reserve limit factor to prevent revert errors in OrderSide.Buy +uint256 constant RESERVE_LIMIT_FACTOR = 10; /// @title AngleAdapter /// @dev Information about prices: When swapping collateral to agEUR, the trade price will not decrease(amountOut); @@ -209,6 +211,182 @@ contract AngleAdapter is ISwapAdapter { } } +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)); + } +} + abstract contract ITransmuter { function implementation() external view returns (address) {} diff --git a/evm/src/angle/AngleUtils.sol b/evm/src/angle/AngleUtils.sol deleted file mode 100644 index a346784..0000000 --- a/evm/src/angle/AngleUtils.sol +++ /dev/null @@ -1,189 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.13; - -/** - * @dev Collection of wrapped interfaces, items and libraries for Angle - * we use them here to maintain integrity and readability of contracts - * as there are many different imports and items - */ - -import {IERC20, ISwapAdapter} from "src/interfaces/ISwapAdapter.sol"; - -/// @dev custom reserve limit factor to prevent revert errors in OrderSide.Buy -uint256 constant RESERVE_LIMIT_FACTOR = 10; - -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)); - } -} diff --git a/evm/test/AngleAdapter.t.sol b/evm/test/AngleAdapter.t.sol new file mode 100644 index 0000000..fe0c7c8 --- /dev/null +++ b/evm/test/AngleAdapter.t.sol @@ -0,0 +1,223 @@ +// 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"; + +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 testPriceFuzzAngle(uint256 amount0, uint256 amount1) public { + bytes32 pair = bytes32(0); + uint256[] memory limits = adapter.getLimits(pair, EURC, agEUR); + vm.assume(amount0 < limits[0] && amount0 > 0); + vm.assume(amount1 < limits[0] && amount1 > 0); + + uint256[] memory amounts = new uint256[](2); + amounts[0] = amount0; + amounts[1] = amount1; + + Fraction[] memory prices = adapter.price(pair, EURC, agEUR, amounts); + + for (uint256 i = 0; i < prices.length; i++) { + assertGt(prices[i].numerator, 0); + assertGt(prices[i].denominator, 0); + } + } + + + function testPriceDecreasingAngle() public { + bytes32 pair = bytes32(0); + uint256[] memory amounts = new uint256[](TEST_ITERATIONS); + + for (uint256 i = 0; i < TEST_ITERATIONS; i++) { + amounts[i] = 1000 * (i+1) * 10 ** 18; + } + + Fraction[] memory prices = adapter.price(pair, agEUR, EURC, amounts); + + for (uint256 i = 0; i < TEST_ITERATIONS - 1; i++) { + assertEq(prices[i].compareFractions(prices[i + 1]), 1); + assertGt(prices[i].denominator, 0); + assertGt(prices[i + 1].denominator, 0); + } + } + + function testSwapFuzzAngleMint(uint256 specifiedAmount, bool isBuy) public { + OrderSide side = isBuy ? OrderSide.Buy : OrderSide.Sell; + + bytes32 pair = bytes32(0); + uint256[] memory limits = adapter.getLimits(pair, EURC, 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, EURC, 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, agEUR, EURC); + console.log(limits[0], limits[1]); + + 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, agEUR, 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, agEUR, EURC, side, amounts[i]); + vm.revertTo(beforeSwap); + } + + for (uint256 i = 1; i < TEST_ITERATIONS - 1; i++) { + assertLe(trades[i].calculatedAmount, trades[i + 1].calculatedAmount); + assertEq(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, IERC20(t0), IERC20(t1)); + + assertEq(res.length, 3); + } + + function testGetTokensAngle() public { + IERC20[] memory tokens = adapter.getTokens(bytes32(0)); + + assertGe(tokens.length, 2); + } + + function testGetLimits() public { + bytes32 pair = bytes32(0); + uint256[] memory limits = adapter.getLimits(pair, agEUR, EURC); + + assertEq(limits.length, 2); + } +} From 174c4e158afd4c475c2e4126f7f6db50784a1698 Mon Sep 17 00:00:00 2001 From: domenicodev Date: Wed, 24 Jan 2024 17:33:47 +0100 Subject: [PATCH 13/50] Revert "feat: Moved imports to main adapter file" This reverts commit 96915bd158e1ba9f1c8cbcdef3c25f8f2593920c. --- evm/src/angle/AngleAdapter.sol | 182 +-------------------------- evm/src/angle/AngleUtils.sol | 189 ++++++++++++++++++++++++++++ evm/test/AngleAdapter.t.sol | 223 --------------------------------- 3 files changed, 191 insertions(+), 403 deletions(-) create mode 100644 evm/src/angle/AngleUtils.sol delete mode 100644 evm/test/AngleAdapter.t.sol diff --git a/evm/src/angle/AngleAdapter.sol b/evm/src/angle/AngleAdapter.sol index 96aeab8..55b5d2f 100644 --- a/evm/src/angle/AngleAdapter.sol +++ b/evm/src/angle/AngleAdapter.sol @@ -2,10 +2,8 @@ pragma experimental ABIEncoderV2; pragma solidity ^0.8.13; -import {IERC20, ISwapAdapter} from "src/interfaces/ISwapAdapter.sol"; - -/// @dev custom reserve limit factor to prevent revert errors in OrderSide.Buy -uint256 constant RESERVE_LIMIT_FACTOR = 10; +/// @dev Wrapped imports (incl. ISwapAdapter and IERC20) are included in utils +import "./AngleUtils.sol"; /// @title AngleAdapter /// @dev Information about prices: When swapping collateral to agEUR, the trade price will not decrease(amountOut); @@ -211,182 +209,6 @@ contract AngleAdapter is ISwapAdapter { } } -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)); - } -} - abstract contract ITransmuter { function implementation() external view returns (address) {} diff --git a/evm/src/angle/AngleUtils.sol b/evm/src/angle/AngleUtils.sol new file mode 100644 index 0000000..a346784 --- /dev/null +++ b/evm/src/angle/AngleUtils.sol @@ -0,0 +1,189 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.13; + +/** + * @dev Collection of wrapped interfaces, items and libraries for Angle + * we use them here to maintain integrity and readability of contracts + * as there are many different imports and items + */ + +import {IERC20, ISwapAdapter} from "src/interfaces/ISwapAdapter.sol"; + +/// @dev custom reserve limit factor to prevent revert errors in OrderSide.Buy +uint256 constant RESERVE_LIMIT_FACTOR = 10; + +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)); + } +} diff --git a/evm/test/AngleAdapter.t.sol b/evm/test/AngleAdapter.t.sol deleted file mode 100644 index fe0c7c8..0000000 --- a/evm/test/AngleAdapter.t.sol +++ /dev/null @@ -1,223 +0,0 @@ -// 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"; - -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 testPriceFuzzAngle(uint256 amount0, uint256 amount1) public { - bytes32 pair = bytes32(0); - uint256[] memory limits = adapter.getLimits(pair, EURC, agEUR); - vm.assume(amount0 < limits[0] && amount0 > 0); - vm.assume(amount1 < limits[0] && amount1 > 0); - - uint256[] memory amounts = new uint256[](2); - amounts[0] = amount0; - amounts[1] = amount1; - - Fraction[] memory prices = adapter.price(pair, EURC, agEUR, amounts); - - for (uint256 i = 0; i < prices.length; i++) { - assertGt(prices[i].numerator, 0); - assertGt(prices[i].denominator, 0); - } - } - - - function testPriceDecreasingAngle() public { - bytes32 pair = bytes32(0); - uint256[] memory amounts = new uint256[](TEST_ITERATIONS); - - for (uint256 i = 0; i < TEST_ITERATIONS; i++) { - amounts[i] = 1000 * (i+1) * 10 ** 18; - } - - Fraction[] memory prices = adapter.price(pair, agEUR, EURC, amounts); - - for (uint256 i = 0; i < TEST_ITERATIONS - 1; i++) { - assertEq(prices[i].compareFractions(prices[i + 1]), 1); - assertGt(prices[i].denominator, 0); - assertGt(prices[i + 1].denominator, 0); - } - } - - function testSwapFuzzAngleMint(uint256 specifiedAmount, bool isBuy) public { - OrderSide side = isBuy ? OrderSide.Buy : OrderSide.Sell; - - bytes32 pair = bytes32(0); - uint256[] memory limits = adapter.getLimits(pair, EURC, 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, EURC, 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, agEUR, EURC); - console.log(limits[0], limits[1]); - - 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, agEUR, 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, agEUR, EURC, side, amounts[i]); - vm.revertTo(beforeSwap); - } - - for (uint256 i = 1; i < TEST_ITERATIONS - 1; i++) { - assertLe(trades[i].calculatedAmount, trades[i + 1].calculatedAmount); - assertEq(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, IERC20(t0), IERC20(t1)); - - assertEq(res.length, 3); - } - - function testGetTokensAngle() public { - IERC20[] memory tokens = adapter.getTokens(bytes32(0)); - - assertGe(tokens.length, 2); - } - - function testGetLimits() public { - bytes32 pair = bytes32(0); - uint256[] memory limits = adapter.getLimits(pair, agEUR, EURC); - - assertEq(limits.length, 2); - } -} From a4f0f93e3771a4498cd4c96215feec9ef9b5906c Mon Sep 17 00:00:00 2001 From: domenicodev Date: Wed, 24 Jan 2024 17:36:08 +0100 Subject: [PATCH 14/50] feat: Moved solidity imports and declarations to main adapter file --- evm/src/angle/AngleAdapter.sol | 182 ++++++++++++++++++++++++++++++- evm/src/angle/AngleUtils.sol | 189 --------------------------------- 2 files changed, 180 insertions(+), 191 deletions(-) delete mode 100644 evm/src/angle/AngleUtils.sol diff --git a/evm/src/angle/AngleAdapter.sol b/evm/src/angle/AngleAdapter.sol index 55b5d2f..96aeab8 100644 --- a/evm/src/angle/AngleAdapter.sol +++ b/evm/src/angle/AngleAdapter.sol @@ -2,8 +2,10 @@ pragma experimental ABIEncoderV2; pragma solidity ^0.8.13; -/// @dev Wrapped imports (incl. ISwapAdapter and IERC20) are included in utils -import "./AngleUtils.sol"; +import {IERC20, ISwapAdapter} from "src/interfaces/ISwapAdapter.sol"; + +/// @dev custom reserve limit factor to prevent revert errors in OrderSide.Buy +uint256 constant RESERVE_LIMIT_FACTOR = 10; /// @title AngleAdapter /// @dev Information about prices: When swapping collateral to agEUR, the trade price will not decrease(amountOut); @@ -209,6 +211,182 @@ contract AngleAdapter is ISwapAdapter { } } +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)); + } +} + abstract contract ITransmuter { function implementation() external view returns (address) {} diff --git a/evm/src/angle/AngleUtils.sol b/evm/src/angle/AngleUtils.sol deleted file mode 100644 index a346784..0000000 --- a/evm/src/angle/AngleUtils.sol +++ /dev/null @@ -1,189 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.13; - -/** - * @dev Collection of wrapped interfaces, items and libraries for Angle - * we use them here to maintain integrity and readability of contracts - * as there are many different imports and items - */ - -import {IERC20, ISwapAdapter} from "src/interfaces/ISwapAdapter.sol"; - -/// @dev custom reserve limit factor to prevent revert errors in OrderSide.Buy -uint256 constant RESERVE_LIMIT_FACTOR = 10; - -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)); - } -} From f18dce5c04b22be117bea628d99c338adcd0e06d Mon Sep 17 00:00:00 2001 From: domenicodev Date: Thu, 25 Jan 2024 11:50:02 +0100 Subject: [PATCH 15/50] feat: Created AngleAdapter tests --- evm/test/AngleAdapter.t.sol | 223 ++++++++++++++++++++++++++++++++++++ 1 file changed, 223 insertions(+) create mode 100644 evm/test/AngleAdapter.t.sol diff --git a/evm/test/AngleAdapter.t.sol b/evm/test/AngleAdapter.t.sol new file mode 100644 index 0000000..433174b --- /dev/null +++ b/evm/test/AngleAdapter.t.sol @@ -0,0 +1,223 @@ +// 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"; + +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 testPriceFuzzAngle(uint256 amount0, uint256 amount1) public { + bytes32 pair = bytes32(0); + uint256[] memory limits = adapter.getLimits(pair, EURC, agEUR); + vm.assume(amount0 < limits[0] && amount0 > 0); + vm.assume(amount1 < limits[0] && amount1 > 0); + + uint256[] memory amounts = new uint256[](2); + amounts[0] = amount0; + amounts[1] = amount1; + + Fraction[] memory prices = adapter.price(pair, EURC, agEUR, amounts); + + for (uint256 i = 0; i < prices.length; i++) { + assertGt(prices[i].numerator, 0); + assertGt(prices[i].denominator, 0); + } + } + + + function testPriceDecreasingAngle() public { + bytes32 pair = bytes32(0); + uint256[] memory amounts = new uint256[](TEST_ITERATIONS); + + for (uint256 i = 0; i < TEST_ITERATIONS; i++) { + amounts[i] = 1000 * (i+1) * 10 ** 18; + } + + Fraction[] memory prices = adapter.price(pair, agEUR, EURC, amounts); + + for (uint256 i = 0; i < TEST_ITERATIONS - 1; i++) { + assertEq(prices[i].compareFractions(prices[i + 1]), 1); + assertGt(prices[i].denominator, 0); + assertGt(prices[i + 1].denominator, 0); + } + } + + function testSwapFuzzAngleMint(uint256 specifiedAmount, bool isBuy) public { + OrderSide side = isBuy ? OrderSide.Buy : OrderSide.Sell; + + bytes32 pair = bytes32(0); + uint256[] memory limits = adapter.getLimits(pair, EURC, 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, EURC, 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, agEUR, EURC); + console.log(limits[0], limits[1]); + + 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, agEUR, 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, agEUR, EURC, side, amounts[i]); + vm.revertTo(beforeSwap); + } + + for (uint256 i = 1; i < TEST_ITERATIONS - 1; i++) { + assertLe(trades[i].calculatedAmount, trades[i + 1].calculatedAmount); + assertEq(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, IERC20(t0), IERC20(t1)); + + assertEq(res.length, 3); + } + + function testGetTokensAngle() public { + IERC20[] memory tokens = adapter.getTokens(bytes32(0)); + + assertGe(tokens.length, 2); + } + + function testGetLimitsAngle() public { + bytes32 pair = bytes32(0); + uint256[] memory limits = adapter.getLimits(pair, agEUR, EURC); + + assertEq(limits.length, 2); + } +} From 76bc1f58ac84f6060e1a876703cedc58ff789ab5 Mon Sep 17 00:00:00 2001 From: domenicodev Date: Tue, 27 Feb 2024 11:40:56 +0100 Subject: [PATCH 16/50] feat: Initial Setup --- evm/src/etherfi/EtherfiAdapter.sol | 122 +++++++++++++++++++++++++++++ evm/src/etherfi/manifest.yaml | 36 +++++++++ 2 files changed, 158 insertions(+) create mode 100644 evm/src/etherfi/EtherfiAdapter.sol create mode 100644 evm/src/etherfi/manifest.yaml diff --git a/evm/src/etherfi/EtherfiAdapter.sol b/evm/src/etherfi/EtherfiAdapter.sol new file mode 100644 index 0000000..d2c6c4f --- /dev/null +++ b/evm/src/etherfi/EtherfiAdapter.sol @@ -0,0 +1,122 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +pragma experimental ABIEncoderV2; +pragma solidity ^0.8.13; + +import {IERC20, ISwapAdapter} from "src/interfaces/ISwapAdapter.sol"; + +/// @title Etherfi Adapter +/// @dev This contract supports the following swaps: eETH<->ETH, wETH<->eETH, wETH<->ETH +contract EtherfiAdapter is ISwapAdapter { + + IWeEth wEeth; + IeEth eEth; + ILiquidityPool 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 && sellToken != address(0)) { + revert Unavailable("This pool only supports eETH, weEth and ETH"); + } + if(buyToken != address(wEeth) && buyToken != address(eEth) && buyToken != address(0)) { + revert Unavailable("This pool only supports eETH, weEth and ETH"); + } + _; + } + + function price( + bytes32 _poolId, + IERC20 _sellToken, + IERC20 _buyToken, + uint256[] memory _specifiedAmounts + ) external view override returns (Fraction[] memory _prices) { + revert NotImplemented("TemplateSwapAdapter.price"); + } + + function swap( + bytes32 poolId, + IERC20 sellToken, + IERC20 buyToken, + OrderSide side, + uint256 specifiedAmount + ) external returns (Trade memory trade) { + revert NotImplemented("TemplateSwapAdapter.swap"); + } + + function getLimits(bytes32 poolId, IERC20 sellToken, IERC20 buyToken) + external + returns (uint256[] memory limits) + { + revert NotImplemented("TemplateSwapAdapter.getLimits"); + } + + function getCapabilities(bytes32 poolId, IERC20 sellToken, IERC20 buyToken) + external + returns (Capability[] memory capabilities) + { + revert NotImplemented("TemplateSwapAdapter.getCapabilities"); + } + + function getTokens(bytes32 poolId) + external + returns (IERC20[] memory tokens) + { + revert NotImplemented("TemplateSwapAdapter.getTokens"); + } + + function getPoolIds(uint256 offset, uint256 limit) + external + returns (bytes32[] memory ids) + { + revert NotImplemented("TemplateSwapAdapter.getPoolIds"); + } +} + +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); + +} + +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..c6cb262 --- /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: Propellerheads.xyz + email: alan@propellerheads.xyz + +# 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 From de4c816570a4d0e61a7aa018825efb5ba69cd2d2 Mon Sep 17 00:00:00 2001 From: domenicodev Date: Tue, 27 Feb 2024 11:42:21 +0100 Subject: [PATCH 17/50] feat: Implemented getTokens --- evm/src/etherfi/EtherfiAdapter.sol | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/evm/src/etherfi/EtherfiAdapter.sol b/evm/src/etherfi/EtherfiAdapter.sol index d2c6c4f..a926976 100644 --- a/evm/src/etherfi/EtherfiAdapter.sol +++ b/evm/src/etherfi/EtherfiAdapter.sol @@ -65,11 +65,17 @@ contract EtherfiAdapter is ISwapAdapter { revert NotImplemented("TemplateSwapAdapter.getCapabilities"); } - function getTokens(bytes32 poolId) + /// @inheritdoc ISwapAdapter + function getTokens(bytes32) external + view + override returns (IERC20[] memory tokens) { - revert NotImplemented("TemplateSwapAdapter.getTokens"); + tokens = new IERC20[](3); + tokens[0] = IERC20(address(0)); + tokens[1] = IERC20(address(eEth)); + tokens[2] = IERC20(address(wEeth)); } function getPoolIds(uint256 offset, uint256 limit) From d4e9e213ae12a67704c9424a5ec236049e0907d8 Mon Sep 17 00:00:00 2001 From: domenicodev Date: Tue, 27 Feb 2024 11:43:26 +0100 Subject: [PATCH 18/50] feat: Implemented getPoolIds --- evm/src/etherfi/EtherfiAdapter.sol | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/evm/src/etherfi/EtherfiAdapter.sol b/evm/src/etherfi/EtherfiAdapter.sol index a926976..32c7067 100644 --- a/evm/src/etherfi/EtherfiAdapter.sol +++ b/evm/src/etherfi/EtherfiAdapter.sol @@ -78,11 +78,13 @@ contract EtherfiAdapter is ISwapAdapter { tokens[2] = IERC20(address(wEeth)); } - function getPoolIds(uint256 offset, uint256 limit) + /// @inheritdoc ISwapAdapter + function getPoolIds(uint256, uint256) external returns (bytes32[] memory ids) { - revert NotImplemented("TemplateSwapAdapter.getPoolIds"); + ids[] = new bytes32[](1); + ids[0] = bytes20(address(liquidityPool)); } } From 61d4c4eb9f29b980d380eba15d6662864d71cfd6 Mon Sep 17 00:00:00 2001 From: domenicodev Date: Tue, 27 Feb 2024 12:11:04 +0100 Subject: [PATCH 19/50] feat: Implemented helper functions and missing constants --- evm/src/etherfi/EtherfiAdapter.sol | 64 +++++++++++++++++++++++++++++- 1 file changed, 62 insertions(+), 2 deletions(-) diff --git a/evm/src/etherfi/EtherfiAdapter.sol b/evm/src/etherfi/EtherfiAdapter.sol index 32c7067..33ccc1b 100644 --- a/evm/src/etherfi/EtherfiAdapter.sol +++ b/evm/src/etherfi/EtherfiAdapter.sol @@ -8,14 +8,19 @@ import {IERC20, ISwapAdapter} from "src/interfaces/ISwapAdapter.sol"; /// @dev This contract supports the following swaps: eETH<->ETH, wETH<->eETH, wETH<->ETH contract EtherfiAdapter is ISwapAdapter { + uint16 mintFee; // fee = 0.001 ETH * 'mintFee' + uint16 burnFee; // fee = 0.001 ETH * 'burnFee' + IWeEth wEeth; IeEth eEth; ILiquidityPool liquidityPool; + IMembershipManager membershipManager; constructor(address _wEeth) { wEeth = IWeEth(_wEeth); eEth = wEeth.eETH(); liquidityPool = eEth.liquidityPool(); + membershipManager = liquidityPool.membershipManager(); } /// @dev Check if tokens in input are supported by this adapter @@ -32,6 +37,9 @@ contract EtherfiAdapter is ISwapAdapter { _; } + /// @dev enable receive as this contract supports ETH + receive() external payable {} + function price( bytes32 _poolId, IERC20 _sellToken, @@ -51,11 +59,42 @@ contract EtherfiAdapter is ISwapAdapter { revert NotImplemented("TemplateSwapAdapter.swap"); } - function getLimits(bytes32 poolId, IERC20 sellToken, IERC20 buyToken) + function getLimits(bytes32, IERC20 sellToken, IERC20 buyToken) external + view + override + checkInputTokens(address(sellToken), address(buyToken)) returns (uint256[] memory limits) { - revert NotImplemented("TemplateSwapAdapter.getLimits"); + address sellTokenAddress = address(sellToken); + address buyTokenAddress = address(buyToken); + limits = new uint256[](2); + + if(sellTokenAddress == address(0)) { + if(buyTokenAddress == address(eEth)) { + + } + else { // ETH-weETH + + } + } + else if(sellTokenAddress == address(wEeth)) { + if(buyTokenAddress == address(0)) { + + } + else { // wEeth-ETH + + } + } + else if(sellTokenAddress == address(eEth)) { + if(buyTokenAddress == address(0)) { + + } + else { // eEth-wEeth + + } + } + } function getCapabilities(bytes32 poolId, IERC20 sellToken, IERC20 buyToken) @@ -86,6 +125,25 @@ contract EtherfiAdapter is ISwapAdapter { ids[] = new bytes32[](1); ids[0] = bytes20(address(liquidityPool)); } + + /// @notice Swap ETH for eETH using MembershipManager + /// @param payedAmount result of getETHRequiredToMintEeth(), the amount of ETH to pay(incl. fee) + /// @param receivedAmount eETH received + function swapEthForEeth(uint256 payedAmount, uint256 receivedAmount) internal { + return membershipManager.wrapEth(_amount, 0); + } + + /// @notice Get ETH required to mint `_amount` eETH + function getETHRequiredToMintEeth(uint256 _amount) internal returns (uint256) { + uint256 feeAmount = uint256(mintFee) * 0.001 ether; + return _amount + feeAmount; + } +} + +interface IMembershipManager { + + function wrapEth(uint256 _amount, uint256 _amountForPoints) external payable returns (uint256); + } interface ILiquidityPool { @@ -103,6 +161,8 @@ interface ILiquidityPool { function deposit(address _referral) external payable returns (uint256); function deposit(address _user, address _referral) external payable returns (uint256); + function membershipManager() external view returns (IMembershipManager); + } interface IeEth { From 0f3fe80b00bcae4c100241025bcd12e002e5eabd Mon Sep 17 00:00:00 2001 From: domenicodev Date: Wed, 28 Feb 2024 12:07:57 +0100 Subject: [PATCH 20/50] feat: Implemented internal swap functions and getAmountIn --- evm/src/etherfi/EtherfiAdapter.sol | 108 ++++++++++++++++++++--------- 1 file changed, 77 insertions(+), 31 deletions(-) diff --git a/evm/src/etherfi/EtherfiAdapter.sol b/evm/src/etherfi/EtherfiAdapter.sol index 33ccc1b..d2ac197 100644 --- a/evm/src/etherfi/EtherfiAdapter.sol +++ b/evm/src/etherfi/EtherfiAdapter.sol @@ -3,24 +3,21 @@ 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: eETH<->ETH, wETH<->eETH, wETH<->ETH +/// @dev This contract supports the following swaps: ETH->eETH, wETH<->eETH, ETH->weETH contract EtherfiAdapter is ISwapAdapter { + using SafeERC20 for IERC20; - uint16 mintFee; // fee = 0.001 ETH * 'mintFee' - uint16 burnFee; // fee = 0.001 ETH * 'burnFee' - - IWeEth wEeth; + IWeEth weEth; IeEth eEth; - ILiquidityPool liquidityPool; - IMembershipManager membershipManager; + ILiquidityPool public liquidityPool; - constructor(address _wEeth) { - wEeth = IWeEth(_wEeth); - eEth = wEeth.eETH(); + constructor(address _weEth) { + weEth = IWeEth(_weEth); + eEth = weEth.eETH(); liquidityPool = eEth.liquidityPool(); - membershipManager = liquidityPool.membershipManager(); } /// @dev Check if tokens in input are supported by this adapter @@ -28,10 +25,10 @@ contract EtherfiAdapter is ISwapAdapter { if(sellToken == buyToken) { revert Unavailable("This pool only supports eETH, weEth and ETH"); } - if(sellToken != address(wEeth) && sellToken != address(eEth) && sellToken && sellToken != address(0)) { + 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) && buyToken != address(0)) { + if(buyToken != address(weEth) && buyToken != address(eEth)) { revert Unavailable("This pool only supports eETH, weEth and ETH"); } _; @@ -74,15 +71,15 @@ contract EtherfiAdapter is ISwapAdapter { if(buyTokenAddress == address(eEth)) { } - else { // ETH-weETH + else { // ETH-weEth } } - else if(sellTokenAddress == address(wEeth)) { + else if(sellTokenAddress == address(weEth)) { if(buyTokenAddress == address(0)) { } - else { // wEeth-ETH + else { // weEth-ETH } } @@ -90,7 +87,7 @@ contract EtherfiAdapter is ISwapAdapter { if(buyTokenAddress == address(0)) { } - else { // eEth-wEeth + else { // eEth-weEth } } @@ -114,7 +111,7 @@ contract EtherfiAdapter is ISwapAdapter { tokens = new IERC20[](3); tokens[0] = IERC20(address(0)); tokens[1] = IERC20(address(eEth)); - tokens[2] = IERC20(address(wEeth)); + tokens[2] = IERC20(address(weEth)); } /// @inheritdoc ISwapAdapter @@ -122,27 +119,75 @@ contract EtherfiAdapter is ISwapAdapter { external returns (bytes32[] memory ids) { - ids[] = new bytes32[](1); + ids = new bytes32[](1); ids[0] = bytes20(address(liquidityPool)); } - /// @notice Swap ETH for eETH using MembershipManager - /// @param payedAmount result of getETHRequiredToMintEeth(), the amount of ETH to pay(incl. fee) - /// @param receivedAmount eETH received - function swapEthForEeth(uint256 payedAmount, uint256 receivedAmount) internal { - return membershipManager.wrapEth(_amount, 0); + /// @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); + uint256 receivedAmount = liquidityPool.deposit{value: amountIn}(); + IERC20(address(eEth)).safeTransfer(address(msg.sender), receivedAmount); + return amountIn; + } + else { + uint256 receivedAmount = liquidityPool.deposit{value: amount}(); + IERC20(address(eEth)).safeTransfer(address(msg.sender), receivedAmount); + return receivedAmount; + } } - /// @notice Get ETH required to mint `_amount` eETH - function getETHRequiredToMintEeth(uint256 _amount) internal returns (uint256) { - uint256 feeAmount = uint256(mintFee) * 0.001 ether; - return _amount + feeAmount; + /// @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) { + 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)).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 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 IMembershipManager { +interface IWithdrawRequestNFT { - function wrapEth(uint256 _amount, uint256 _amountForPoints) external payable returns (uint256); + 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; } @@ -161,7 +206,8 @@ interface ILiquidityPool { function deposit(address _referral) external payable returns (uint256); function deposit(address _user, address _referral) external payable returns (uint256); - function membershipManager() external view returns (IMembershipManager); + function requestWithdraw(address recipient, uint256 amount) external returns (uint256); + function withdrawRequestNFT() external view returns (IWithdrawRequestNFT); } From 6cd57ebaf4c5cc4f49619b549c3655cf19ad52c2 Mon Sep 17 00:00:00 2001 From: domenicodev Date: Wed, 28 Feb 2024 12:38:59 +0100 Subject: [PATCH 21/50] feat: Implemented price --- evm/src/etherfi/EtherfiAdapter.sol | 261 ++++++++++++++++++++--------- 1 file changed, 183 insertions(+), 78 deletions(-) diff --git a/evm/src/etherfi/EtherfiAdapter.sol b/evm/src/etherfi/EtherfiAdapter.sol index d2ac197..4a15437 100644 --- a/evm/src/etherfi/EtherfiAdapter.sol +++ b/evm/src/etherfi/EtherfiAdapter.sol @@ -3,10 +3,12 @@ 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"; +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 +/// @dev This contract supports the following swaps: ETH->eETH, wETH<->eETH, +/// ETH->weETH contract EtherfiAdapter is ISwapAdapter { using SafeERC20 for IERC20; @@ -22,13 +24,16 @@ contract EtherfiAdapter is ISwapAdapter { /// @dev Check if tokens in input are supported by this adapter modifier checkInputTokens(address sellToken, address buyToken) { - if(sellToken == buyToken) { + if (sellToken == buyToken) { revert Unavailable("This pool only supports eETH, weEth and ETH"); } - if(sellToken != address(weEth) && sellToken != address(eEth) && sellToken != address(0)) { + 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)) { + if (buyToken != address(weEth) && buyToken != address(eEth)) { revert Unavailable("This pool only supports eETH, weEth and ETH"); } _; @@ -37,23 +42,66 @@ contract EtherfiAdapter is ISwapAdapter { /// @dev enable receive as this contract supports ETH receive() external payable {} + /// @inheritdoc ISwapAdapter function price( - bytes32 _poolId, + bytes32, IERC20 _sellToken, IERC20 _buyToken, uint256[] memory _specifiedAmounts - ) external view override returns (Fraction[] memory _prices) { - revert NotImplemented("TemplateSwapAdapter.price"); + ) + 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]); + } } function swap( - bytes32 poolId, + bytes32, IERC20 sellToken, IERC20 buyToken, OrderSide side, uint256 specifiedAmount - ) external returns (Trade memory trade) { - revert NotImplemented("TemplateSwapAdapter.swap"); + ) + 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 = swapEthForWeEth(specifiedAmount, + // side); + // } else { + // trade.calculatedAmount = swapWeEthForEeth(specifiedAmount, + // side); + // } + // } + // trade.gasUsed = gasBefore - gasleft(); + // // trade.price = getPriceAt(); } function getLimits(bytes32, IERC20 sellToken, IERC20 buyToken) @@ -66,32 +114,17 @@ contract EtherfiAdapter is ISwapAdapter { address sellTokenAddress = address(sellToken); address buyTokenAddress = address(buyToken); limits = new uint256[](2); - - if(sellTokenAddress == address(0)) { - if(buyTokenAddress == address(eEth)) { + if (sellTokenAddress == address(0)) { + if (buyTokenAddress == address(eEth)) {} else { // ETH-weEth } - else { // ETH-weEth - + } else if (sellTokenAddress == address(weEth)) { + if (buyTokenAddress == address(0)) {} else { // weEth-ETH + } + } else if (sellTokenAddress == address(eEth)) { + if (buyTokenAddress == address(0)) {} else { // eEth-weEth } } - else if(sellTokenAddress == address(weEth)) { - if(buyTokenAddress == address(0)) { - - } - else { // weEth-ETH - - } - } - else if(sellTokenAddress == address(eEth)) { - if(buyTokenAddress == address(0)) { - - } - else { // eEth-weEth - - } - } - } function getCapabilities(bytes32 poolId, IERC20 sellToken, IERC20 buyToken) @@ -125,112 +158,184 @@ contract EtherfiAdapter is ISwapAdapter { /// @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) { + function swapEthForEeth(uint256 amount, OrderSide side) + internal + returns (uint256) + { + if (side == OrderSide.Buy) { uint256 amountIn = getAmountIn(address(0), address(eEth), amount); uint256 receivedAmount = liquidityPool.deposit{value: amountIn}(); - IERC20(address(eEth)).safeTransfer(address(msg.sender), receivedAmount); + IERC20(address(eEth)).safeTransfer( + address(msg.sender), receivedAmount + ); return amountIn; - } - else { + } else { uint256 receivedAmount = liquidityPool.deposit{value: amount}(); - IERC20(address(eEth)).safeTransfer(address(msg.sender), receivedAmount); + IERC20(address(eEth)).safeTransfer( + address(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) { + function swapEthForWeEth(uint256 amount, OrderSide side) + internal + returns (uint256) + { + if (side == OrderSide.Buy) { uint256 amountIn = getAmountIn(address(0), address(weEth), amount); IERC20(address(eEth)).approve(address(weEth), amountIn); - uint256 receivedAmountEeth = liquidityPool.deposit{value: amountIn}(); + uint256 receivedAmountEeth = + liquidityPool.deposit{value: amountIn}(); uint256 receivedAmount = weEth.wrap(receivedAmountEeth); - IERC20(address(weEth)).safeTransfer(address(msg.sender), receivedAmount); + IERC20(address(weEth)).safeTransfer( + address(msg.sender), receivedAmount + ); return amountIn; - } - else { + } else { 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); + IERC20(address(weEth)).safeTransfer( + address(msg.sender), receivedAmount + ); return receivedAmount; } } - /// @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); + /// @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) {} else {} + } + + /// @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) {} else {} + } + + /// @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(liquidityPool.sharesForAmount(amount), amount); + } else { + uint256 eEthOut = liquidityPool.sharesForAmount(amount); + return Fraction(liquidityPool.sharesForAmount(eEthOut), amount); } - 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); + } 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 weEth.getEETHByWeETH(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 requestWithdraw(uint96 amountOfEEth, uint96 shareOfEEth, address recipient, uint256 fee) - external payable returns (uint256); - - function getClaimableAmount(uint256 tokenId) external view 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 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 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 deposit(address _user, address _referral) + external + payable + returns (uint256); - function requestWithdraw(address recipient, uint256 amount) external 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 getWeETHByeETH(uint256 _eETHAmount) + external + view + returns (uint256); - function getEETHByWeETH(uint256 _weETHAmount) 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); - } From 08917023e06a37c6f71ca6c71d5fae8b60a01d19 Mon Sep 17 00:00:00 2001 From: domenicodev Date: Wed, 28 Feb 2024 16:31:28 +0100 Subject: [PATCH 22/50] feat: Implemented getLimits --- evm/src/etherfi/EtherfiAdapter.sol | 73 +++++++++++++++--------------- 1 file changed, 37 insertions(+), 36 deletions(-) diff --git a/evm/src/etherfi/EtherfiAdapter.sol b/evm/src/etherfi/EtherfiAdapter.sol index 4a15437..ee9ba0b 100644 --- a/evm/src/etherfi/EtherfiAdapter.sol +++ b/evm/src/etherfi/EtherfiAdapter.sol @@ -60,7 +60,9 @@ contract EtherfiAdapter is ISwapAdapter { address buyTokenAddress = address(_buyToken); for (uint256 i = 0; i < _specifiedAmounts.length; i++) { - _prices[i] = getPriceAt(sellTokenAddress, buyTokenAddress, _specifiedAmounts[i]); + _prices[i] = getPriceAt( + sellTokenAddress, buyTokenAddress, _specifiedAmounts[i] + ); } } @@ -76,34 +78,39 @@ contract EtherfiAdapter is ISwapAdapter { checkInputTokens(address(sellToken), address(buyToken)) returns (Trade memory trade) { - // if (specifiedAmount == 0) { - // return 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 = swapEthForWeEth(specifiedAmount, - // side); - // } else { - // trade.calculatedAmount = swapWeEthForEeth(specifiedAmount, - // side); - // } - // } - // trade.gasUsed = gasBefore - gasleft(); - // // trade.price = getPriceAt(); + 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 = swapEthForWeEth(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 @@ -115,16 +122,10 @@ contract EtherfiAdapter is ISwapAdapter { address buyTokenAddress = address(buyToken); limits = new uint256[](2); - if (sellTokenAddress == address(0)) { - if (buyTokenAddress == address(eEth)) {} else { // ETH-weEth - } - } else if (sellTokenAddress == address(weEth)) { - if (buyTokenAddress == address(0)) {} else { // weEth-ETH - } - } else if (sellTokenAddress == address(eEth)) { - if (buyTokenAddress == address(0)) {} else { // eEth-weEth - } - } + /// @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] = type(uint128).max; + limits[1] = limits[0]; } function getCapabilities(bytes32 poolId, IERC20 sellToken, IERC20 buyToken) From 1064fbf7eb0fb80463bcac9565a39a359722cb72 Mon Sep 17 00:00:00 2001 From: domenicodev Date: Wed, 28 Feb 2024 16:32:37 +0100 Subject: [PATCH 23/50] feat: Implemented swap --- evm/src/etherfi/EtherfiAdapter.sol | 11 +++++++-- evm/test/EtherfiAdapter.t.sol | 37 ++++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+), 2 deletions(-) create mode 100644 evm/test/EtherfiAdapter.t.sol diff --git a/evm/src/etherfi/EtherfiAdapter.sol b/evm/src/etherfi/EtherfiAdapter.sol index ee9ba0b..1ac19de 100644 --- a/evm/src/etherfi/EtherfiAdapter.sol +++ b/evm/src/etherfi/EtherfiAdapter.sol @@ -66,6 +66,7 @@ contract EtherfiAdapter is ISwapAdapter { } } + /// @inheritdoc ISwapAdapter function swap( bytes32, IERC20 sellToken, @@ -128,11 +129,17 @@ contract EtherfiAdapter is ISwapAdapter { limits[1] = limits[0]; } - function getCapabilities(bytes32 poolId, IERC20 sellToken, IERC20 buyToken) + /// @inheritdoc ISwapAdapter + function getCapabilities(bytes32, IERC20, IERC20) external + pure + override returns (Capability[] memory capabilities) { - revert NotImplemented("TemplateSwapAdapter.getCapabilities"); + capabilities = new Capability[](3); + capabilities[0] = Capability.SellOrder; + capabilities[1] = Capability.BuyOrder; + capabilities[2] = Capability.PriceFunction; } /// @inheritdoc ISwapAdapter diff --git a/evm/test/EtherfiAdapter.t.sol b/evm/test/EtherfiAdapter.t.sol new file mode 100644 index 0000000..2834ef6 --- /dev/null +++ b/evm/test/EtherfiAdapter.t.sol @@ -0,0 +1,37 @@ +// 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 { + EtherfiAdapter adapter; + IWeEth wEeth = IWeEth(0xCd5fE23C85820F7B72D0926FC9b05b43E359b7ee); + IeEth eEth; + + 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 testMe() public { + // // uint256 requiredETH = adapter.getETHRequiredToMintEeth(1 ether); + // deal(address(adapter), 100 ether); + // // adapter.swapEthForEeth(1 ether, OrderSide.Buy); + // IERC20(address(eEth)).approve(address(adapter), type(uint256).max); + + // console.log(IERC20(address(eEth)).balanceOf(address(this))); + // console.log(adapter.swapEthForWeEth(1 ether, OrderSide.Buy)); + // console.log(IERC20(address(eEth)).balanceOf(address(this))); + } +} From 24682409c46a32115347455e5b8b94089af70176 Mon Sep 17 00:00:00 2001 From: domenicodev Date: Thu, 29 Feb 2024 18:08:42 +0100 Subject: [PATCH 24/50] fix: Fixed swap function and initial tests --- evm/src/etherfi/EtherfiAdapter.sol | 85 ++++++++++-- evm/test/EtherfiAdapter.t.sol | 213 +++++++++++++++++++++++++++-- 2 files changed, 272 insertions(+), 26 deletions(-) diff --git a/evm/src/etherfi/EtherfiAdapter.sol b/evm/src/etherfi/EtherfiAdapter.sol index 1ac19de..da3f8f8 100644 --- a/evm/src/etherfi/EtherfiAdapter.sol +++ b/evm/src/etherfi/EtherfiAdapter.sol @@ -94,7 +94,7 @@ contract EtherfiAdapter is ISwapAdapter { } } else { if (sellTokenAddress == address(eEth)) { - trade.calculatedAmount = swapEthForWeEth(specifiedAmount, side); + trade.calculatedAmount = swapEethForWeEth(specifiedAmount, side); } else { trade.calculatedAmount = swapWeEthForEeth(specifiedAmount, side); } @@ -119,13 +119,11 @@ contract EtherfiAdapter is ISwapAdapter { checkInputTokens(address(sellToken), address(buyToken)) returns (uint256[] memory limits) { - address sellTokenAddress = address(sellToken); - address buyTokenAddress = address(buyToken); 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] = type(uint128).max; + limits[0] = IERC20(address(eEth)).totalSupply(); limits[1] = limits[0]; } @@ -158,6 +156,8 @@ contract EtherfiAdapter is ISwapAdapter { /// @inheritdoc ISwapAdapter function getPoolIds(uint256, uint256) external + view + override returns (bytes32[] memory ids) { ids = new bytes32[](1); @@ -172,15 +172,17 @@ contract EtherfiAdapter is ISwapAdapter { { if (side == OrderSide.Buy) { uint256 amountIn = getAmountIn(address(0), address(eEth), amount); - uint256 receivedAmount = liquidityPool.deposit{value: amountIn}(); + liquidityPool.deposit{value: amountIn}(); IERC20(address(eEth)).safeTransfer( - address(msg.sender), receivedAmount + address(msg.sender), amount ); return amountIn; } else { - uint256 receivedAmount = liquidityPool.deposit{value: amount}(); - IERC20(address(eEth)).safeTransfer( - address(msg.sender), receivedAmount + 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; } @@ -193,22 +195,30 @@ contract EtherfiAdapter is ISwapAdapter { 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; } } @@ -219,7 +229,30 @@ contract EtherfiAdapter is ISwapAdapter { internal returns (uint256) { - if (side == OrderSide.Buy) {} else {} + 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 @@ -228,7 +261,31 @@ contract EtherfiAdapter is ISwapAdapter { internal returns (uint256) { - if (side == OrderSide.Buy) {} else {} + 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 @@ -241,9 +298,9 @@ contract EtherfiAdapter is ISwapAdapter { { if (sellToken == address(0)) { if (buyToken == address(eEth)) { - return Fraction(liquidityPool.sharesForAmount(amount), amount); + return Fraction(_sharesForDepositAmount(amount), amount); } else { - uint256 eEthOut = liquidityPool.sharesForAmount(amount); + uint256 eEthOut = _sharesForDepositAmount(amount); return Fraction(liquidityPool.sharesForAmount(eEthOut), amount); } } else if (sellToken == address(eEth)) { @@ -269,7 +326,7 @@ contract EtherfiAdapter is ISwapAdapter { } } else if (sellToken == address(eEth)) { // eEth-weEth - return weEth.getEETHByWeETH(amountOut); + return liquidityPool.amountForShare(amountOut); } else { // weEth-eEth return weEth.getWeETHByeETH(amountOut); diff --git a/evm/test/EtherfiAdapter.t.sol b/evm/test/EtherfiAdapter.t.sol index 2834ef6..0bef988 100644 --- a/evm/test/EtherfiAdapter.t.sol +++ b/evm/test/EtherfiAdapter.t.sol @@ -9,29 +9,218 @@ import "src/etherfi/EtherfiAdapter.sol"; contract EtherfiAdapterTest is Test, ISwapAdapterTypes { EtherfiAdapter adapter; - IWeEth wEeth = IWeEth(0xCd5fE23C85820F7B72D0926FC9b05b43E359b7ee); + IWeEth weEth = IWeEth(0xCd5fE23C85820F7B72D0926FC9b05b43E359b7ee); IeEth eEth; function setUp() public { uint256 forkBlock = 19218495; vm.createSelectFork(vm.rpcUrl("mainnet"), forkBlock); - adapter = new EtherfiAdapter(address(wEeth)); - eEth = wEeth.eETH(); + adapter = new EtherfiAdapter(address(weEth)); + eEth = weEth.eETH(); - vm.label(address(wEeth), "WeETH"); + vm.label(address(weEth), "WeETH"); vm.label(address(eEth), "eETH"); } receive() external payable {} - function testMe() public { - // // uint256 requiredETH = adapter.getETHRequiredToMintEeth(1 ether); - // deal(address(adapter), 100 ether); - // // adapter.swapEthForEeth(1 ether, OrderSide.Buy); - // IERC20(address(eEth)).approve(address(adapter), type(uint256).max); + function testPriceFuzzEtherfi(uint256 amount0, uint256 amount1) public { + bytes32 pair = bytes32(0); + uint256[] memory limits = adapter.getLimits(pair, IERC20(address(weEth)), IERC20(address(eEth))); + vm.assume(amount0 < limits[0] && amount0 > 0); + vm.assume(amount1 < limits[1] && amount1 > 0); + + uint256[] memory amounts = new uint256[](2); + amounts[0] = amount0; + amounts[1] = amount1; + + Fraction[] memory prices = adapter.price(pair, IERC20(address(weEth)), IERC20(address(eEth)), amounts); + + for (uint256 i = 0; i < prices.length; i++) { + assertGt(prices[i].numerator, 0); + assertGt(prices[i].denominator, 0); + } + } - // console.log(IERC20(address(eEth)).balanceOf(address(this))); - // console.log(adapter.swapEthForWeEth(1 ether, OrderSide.Buy)); - // console.log(IERC20(address(eEth)).balanceOf(address(this))); + function testSwapFuzzEtherfiEethWeEth(uint256 specifiedAmount, bool isBuy) public { + OrderSide side = isBuy ? OrderSide.Buy : OrderSide.Sell; + + IERC20 eEth_ = IERC20(address(eEth)); + IERC20 weEth_ = IERC20(address(weEth)); + bytes32 pair = bytes32(0); + uint256[] memory limits = adapter.getLimits(pair, eEth_, weEth_); + + if (side == OrderSide.Buy) { + vm.assume(specifiedAmount < limits[1] && specifiedAmount > 100); + + /// @dev workaround for eETH "deal", as standard ERC20 does not work(balance is shares) + deal(address(adapter), type(uint256).max); + adapter.swap(pair, IERC20(address(0)), eEth_, OrderSide.Buy, limits[0]); + + eEth_.approve(address(adapter), type(uint256).max); + } else { + vm.assume(specifiedAmount < limits[0] && specifiedAmount > 100); + + /// @dev workaround for eETH "deal", as standard ERC20 does not work(balance is shares) + deal(address(adapter), type(uint128).max); + adapter.swap(pair, IERC20(address(0)), eEth_, OrderSide.Buy, specifiedAmount); + + eEth_.approve(address(adapter), specifiedAmount); + } + + uint256 eEth_balance = eEth_.balanceOf(address(this)); + uint256 weEth_balance = weEth_.balanceOf(address(this)); + + Trade memory trade = + adapter.swap(pair, eEth_, weEth_, side, specifiedAmount); + + if (trade.calculatedAmount > 0) { + if (side == OrderSide.Buy) { + assertGe( + specifiedAmount, + weEth_.balanceOf(address(this)) - weEth_balance + ); + /// @dev Transfer function contains rounding errors because of rewards in weETH contract, therefore we assume a +/-2 tolerance + assertLe( + specifiedAmount - 2, + weEth_.balanceOf(address(this)) - weEth_balance + ); + assertLe( + trade.calculatedAmount - 2, + eEth_balance - eEth_.balanceOf(address(this)) + ); + } 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 testSwapFuzzEtherfiWeEthEth(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, weEth_, eEth_); + + if (side == OrderSide.Buy) { + vm.assume(specifiedAmount < limits[1] && specifiedAmount > 100); + + /// @dev workaround for eETH "deal", as standard ERC20 does not work(balance is shares) + deal(address(adapter), type(uint256).max); + adapter.swap(pair, IERC20(address(0)), weEth_, OrderSide.Buy, limits[0]); + + weEth_.approve(address(adapter), type(uint256).max); + } else { + vm.assume(specifiedAmount < limits[0] && specifiedAmount > 100); + + /// @dev workaround for eETH "deal", as standard ERC20 does not work(balance is shares) + deal(address(adapter), type(uint128).max); + adapter.swap(pair, IERC20(address(0)), weEth_, OrderSide.Buy, specifiedAmount); + + weEth_.approve(address(adapter), specifiedAmount); + } + + uint256 eEth_balance = eEth_.balanceOf(address(this)); + uint256 weEth_balance = weEth_.balanceOf(address(this)); + + Trade memory trade = + adapter.swap(pair, weEth_, 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 weETH contract, therefore we assume a +/-2 tolerance + assertLe( + specifiedAmount - 2, + eEth_.balanceOf(address(this)) - eEth_balance + ); + assertLe( + trade.calculatedAmount - 2, + weEth_balance - weEth_.balanceOf(address(this)) + ); + } else { + assertGe( + specifiedAmount, + weEth_balance - weEth_.balanceOf(address(this)) + ); + /// @dev Transfer function contains rounding errors because of rewards in eETH contract, therefore we assume a +/-2 tolerance + assertLe( + specifiedAmount - 2, + weEth_balance - weEth_.balanceOf(address(this)) + ); + assertEq( + trade.calculatedAmount, + eEth_.balanceOf(address(this)) - eEth_balance + ); + } + } + } + + function testSwapFuzzEtherfiEthEeth(uint256 specifiedAmount, bool isBuy) public { + OrderSide side = isBuy ? OrderSide.Buy : OrderSide.Sell; + + IERC20 eth_ = IERC20(address(0)); + IERC20 eEth_ = IERC20(address(eEth)); + bytes32 pair = bytes32(0); + uint256[] memory limits = adapter.getLimits(pair, eth_, eEth_); + + if (side == OrderSide.Buy) { + vm.assume(specifiedAmount < limits[1] && specifiedAmount > 10); + + deal(address(adapter), 100**18); + } else { + vm.assume(specifiedAmount < limits[0] && specifiedAmount > 10); + + deal(address(adapter), specifiedAmount); + } + + uint256 eth_balance = address(adapter).balance; + uint256 eEth_balance = eEth_.balanceOf(address(this)); + + Trade memory trade = + adapter.swap(pair, eth_, eEth_, side, specifiedAmount); + + if (trade.calculatedAmount > 0) { + if (side == OrderSide.Buy) { + assertGe( + specifiedAmount, + eEth_.balanceOf(address(this)) - eEth_balance + ); + /// @dev Transfer function contains rounding errors because of rewards in eETH contract, therefore we assume a +/-2 tolerance + assertLe( + specifiedAmount - 2, + eEth_.balanceOf(address(this)) - eEth_balance + ); + assertEq( + trade.calculatedAmount, + eth_balance - address(adapter).balance + ); + } else { + assertEq( + specifiedAmount, + eth_balance - address(adapter).balance + ); + assertEq( + trade.calculatedAmount, + eEth_.balanceOf(address(this)) - eEth_balance + ); + } + } } } From 57dc954f0caa4567dc29ea52edc302275c9f330b Mon Sep 17 00:00:00 2001 From: domenicodev Date: Fri, 1 Mar 2024 16:23:56 +0100 Subject: [PATCH 25/50] fix: Fixed getLimits and improved swap --- evm/src/etherfi/EtherfiAdapter.sol | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/evm/src/etherfi/EtherfiAdapter.sol b/evm/src/etherfi/EtherfiAdapter.sol index da3f8f8..5db8764 100644 --- a/evm/src/etherfi/EtherfiAdapter.sol +++ b/evm/src/etherfi/EtherfiAdapter.sol @@ -121,9 +121,13 @@ contract EtherfiAdapter is ISwapAdapter { { 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(); + /// @dev Limits are underestimated to 90% of totalSupply as both weEth and eEth have no limits but revert in some cases + if(address(sellToken) == address(weEth) || address(buyToken) == address(weEth)) { + limits[0] = IERC20(address(weEth)).totalSupply() * 90 / 100; + } + else { + limits[0] = IERC20(address(eEth)).totalSupply() * 90 / 100; + } limits[1] = limits[0]; } @@ -194,13 +198,13 @@ contract EtherfiAdapter is ISwapAdapter { internal returns (uint256) { + IERC20 eEth_ = IERC20(address(eEth)); 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}(); + eEth_.approve(address(weEth), receivedAmountEeth); uint256 receivedAmount = weEth.wrap(receivedAmountEeth); IERC20(address(weEth)).safeTransfer( @@ -209,10 +213,8 @@ contract EtherfiAdapter is ISwapAdapter { 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}(); + eEth_.approve(address(weEth), receivedAmountEeth); uint256 receivedAmount = weEth.wrap(receivedAmountEeth); IERC20(address(weEth)).safeTransfer( From 9deddb52412e939b650bc3882390a70674fb55e3 Mon Sep 17 00:00:00 2001 From: domenicodev Date: Fri, 1 Mar 2024 19:02:00 +0100 Subject: [PATCH 26/50] feat: Finalized tests --- evm/test/EtherfiAdapter.t.sol | 153 ++++++++++++++++++++++++++++++---- 1 file changed, 139 insertions(+), 14 deletions(-) diff --git a/evm/test/EtherfiAdapter.t.sol b/evm/test/EtherfiAdapter.t.sol index 0bef988..1527d5c 100644 --- a/evm/test/EtherfiAdapter.t.sol +++ b/evm/test/EtherfiAdapter.t.sol @@ -107,11 +107,12 @@ contract EtherfiAdapterTest is Test, ISwapAdapterTypes { } } - function testSwapFuzzEtherfiWeEthEth(uint256 specifiedAmount, bool isBuy) public { + function testSwapFuzzEtherfiWeEthEeth(uint256 specifiedAmount, bool isBuy) public { OrderSide side = isBuy ? OrderSide.Buy : OrderSide.Sell; IERC20 eEth_ = IERC20(address(eEth)); IERC20 weEth_ = IERC20(address(weEth)); + uint256 weEth_bal_before = weEth_.balanceOf(address(this)); bytes32 pair = bytes32(0); uint256[] memory limits = adapter.getLimits(pair, weEth_, eEth_); @@ -136,18 +137,22 @@ contract EtherfiAdapterTest is Test, ISwapAdapterTypes { uint256 eEth_balance = eEth_.balanceOf(address(this)); uint256 weEth_balance = weEth_.balanceOf(address(this)); + /// @dev as of rounding errors in Etherfi, specifiedAmount might lose small digits for small numbers + /// therefore we use weEth_balance - weEth_bal_before as specifiedAmount + uint256 realAmountWeEth_ = weEth_balance - weEth_bal_before; + Trade memory trade = - adapter.swap(pair, weEth_, eEth_, side, specifiedAmount); + adapter.swap(pair, weEth_, eEth_, side, realAmountWeEth_); if (trade.calculatedAmount > 0) { if (side == OrderSide.Buy) { assertGe( - specifiedAmount, + 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( - specifiedAmount - 2, + realAmountWeEth_ - 2, eEth_.balanceOf(address(this)) - eEth_balance ); assertLe( @@ -155,16 +160,15 @@ contract EtherfiAdapterTest is Test, ISwapAdapterTypes { weEth_balance - weEth_.balanceOf(address(this)) ); } else { - assertGe( - specifiedAmount, - weEth_balance - weEth_.balanceOf(address(this)) - ); - /// @dev Transfer function contains rounding errors because of rewards in eETH contract, therefore we assume a +/-2 tolerance - assertLe( - specifiedAmount - 2, - weEth_balance - weEth_.balanceOf(address(this)) - ); 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 ); @@ -183,7 +187,7 @@ contract EtherfiAdapterTest is Test, ISwapAdapterTypes { if (side == OrderSide.Buy) { vm.assume(specifiedAmount < limits[1] && specifiedAmount > 10); - deal(address(adapter), 100**18); + deal(address(adapter), eEth_.totalSupply()); } else { vm.assume(specifiedAmount < limits[0] && specifiedAmount > 10); @@ -223,4 +227,125 @@ contract EtherfiAdapterTest is Test, ISwapAdapterTypes { } } } + + function testSwapFuzzEtherfiEthWeEth(uint256 specifiedAmount, bool isBuy) public { + OrderSide side = isBuy ? OrderSide.Buy : OrderSide.Sell; + + IERC20 eth_ = IERC20(address(0)); + IERC20 weEth_ = IERC20(address(weEth)); + bytes32 pair = bytes32(0); + uint256[] memory limits = adapter.getLimits(pair, eth_, weEth_); + + if (side == OrderSide.Buy) { + vm.assume(specifiedAmount < limits[1] && specifiedAmount > 10); + + deal(address(adapter), weEth_.totalSupply()); + } else { + vm.assume(specifiedAmount < limits[0] && specifiedAmount > 10); + + deal(address(adapter), specifiedAmount); + } + + uint256 eth_balance = address(adapter).balance; + uint256 weEth_balance = weEth_.balanceOf(address(this)); + + Trade memory trade = + adapter.swap(pair, eth_, weEth_, side, specifiedAmount); + + if (trade.calculatedAmount > 0) { + if (side == OrderSide.Buy) { + assertGe( + specifiedAmount, + weEth_.balanceOf(address(this)) - weEth_balance + ); + /// @dev Transfer function contains rounding errors because of rewards in eETH contract, therefore we assume a +/-2 tolerance + assertLe( + specifiedAmount - 2, + weEth_.balanceOf(address(this)) - weEth_balance + ); + assertEq( + trade.calculatedAmount, + eth_balance - address(adapter).balance + ); + } else { + assertEq( + specifiedAmount, + eth_balance - address(adapter).balance + ); + assertEq( + trade.calculatedAmount, + weEth_.balanceOf(address(this)) - weEth_balance + ); + } + } + } + + function testSwapSellIncreasingEtherfi() public { + executeIncreasingSwapsEtherfi(OrderSide.Sell); + } + + function testSwapBuyIncreasingEtherfi() public { + executeIncreasingSwapsEtherfi(OrderSide.Buy); + } + + function executeIncreasingSwapsIntegral(OrderSide side) internal { + bytes32 pair = bytes32(0); + + uint256 amountConstant_ = side == 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(USDC), address(this), amounts[i]); + USDC.approve(address(adapter), amounts[i]); + + trades[i] = adapter.swap(pair, USDC, WETH, 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); + assertEq(trades[i].price.compareFractions(trades[i + 1].price), 0); + } + } + + function testGetCapabilitiesEtherfi( + bytes32 pair, + address t0, + address t1 + ) public { + Capability[] memory res = adapter.getCapabilities( + pair, + IERC20(t0), + IERC20(t1) + ); + + assertEq(res.length, 3); + } + + function testGetTokensEtherfi() public { + bytes32 pair = bytes32(0); + IERC20[] memory tokens = adapter.getTokens(pair); + + assertEq(tokens.length, 3); + } + + function testGetLimitsEtherfi() public { + bytes32 pair = bytes32(0); + uint256[] memory limits = adapter.getLimits(pair, IERC20(address(eEth)), IERC20(address(weEth))); + + assertEq(limits.length, 2); + } } From 3fabb488055b424a69d9fd0760cd4db413cd6150 Mon Sep 17 00:00:00 2001 From: domenicodev Date: Mon, 4 Mar 2024 10:10:23 +0100 Subject: [PATCH 27/50] feat: Finalized tests --- evm/test/EtherfiAdapter.t.sol | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/evm/test/EtherfiAdapter.t.sol b/evm/test/EtherfiAdapter.t.sol index 1527d5c..b7db414 100644 --- a/evm/test/EtherfiAdapter.t.sol +++ b/evm/test/EtherfiAdapter.t.sol @@ -8,10 +8,14 @@ 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); @@ -288,15 +292,15 @@ contract EtherfiAdapterTest is Test, ISwapAdapterTypes { executeIncreasingSwapsEtherfi(OrderSide.Buy); } - function executeIncreasingSwapsIntegral(OrderSide side) internal { + function executeIncreasingSwapsEtherfi(OrderSide side) internal { bytes32 pair = bytes32(0); - uint256 amountConstant_ = side == 10**18; + uint256 amountConstant_ = 10**18; uint256[] memory amounts = new uint256[](TEST_ITERATIONS); amounts[0] = amountConstant_; for (uint256 i = 1; i < TEST_ITERATIONS; i++) { - amounts[i] = amountConstant_ * i; + amounts[i] = amountConstant_ + i; } Trade[] memory trades = new Trade[](TEST_ITERATIONS); @@ -304,10 +308,10 @@ contract EtherfiAdapterTest 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(address(weEth), address(this), amounts[i]); + IERC20(address(weEth)).approve(address(adapter), amounts[i]); - trades[i] = adapter.swap(pair, USDC, WETH, side, amounts[i]); + trades[i] = adapter.swap(pair, IERC20(address(weEth)), IERC20(address(eEth)), side, amounts[i]); vm.revertTo(beforeSwap); } @@ -317,7 +321,6 @@ contract EtherfiAdapterTest is Test, ISwapAdapterTypes { trades[i + 1].calculatedAmount ); assertLe(trades[i].gasUsed, trades[i + 1].gasUsed); - assertEq(trades[i].price.compareFractions(trades[i + 1].price), 0); } } From 45f8aa1268f0e1510594744e42ba4c77291ae832 Mon Sep 17 00:00:00 2001 From: domenicodev Date: Mon, 4 Mar 2024 10:11:15 +0100 Subject: [PATCH 28/50] chore: Changed approve to safeIncreaseAllowance --- evm/src/etherfi/EtherfiAdapter.sol | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/evm/src/etherfi/EtherfiAdapter.sol b/evm/src/etherfi/EtherfiAdapter.sol index 5db8764..ad5d78d 100644 --- a/evm/src/etherfi/EtherfiAdapter.sol +++ b/evm/src/etherfi/EtherfiAdapter.sol @@ -204,7 +204,7 @@ contract EtherfiAdapter is ISwapAdapter { uint256 receivedAmountEeth = liquidityPool.deposit{value: amountIn}(); - eEth_.approve(address(weEth), receivedAmountEeth); + eEth_.safeIncreaseAllowance(address(weEth), receivedAmountEeth); uint256 receivedAmount = weEth.wrap(receivedAmountEeth); IERC20(address(weEth)).safeTransfer( @@ -214,7 +214,7 @@ contract EtherfiAdapter is ISwapAdapter { return amountIn; } else { uint256 receivedAmountEeth = liquidityPool.deposit{value: amount}(); - eEth_.approve(address(weEth), receivedAmountEeth); + eEth_.safeIncreaseAllowance(address(weEth), receivedAmountEeth); uint256 receivedAmount = weEth.wrap(receivedAmountEeth); IERC20(address(weEth)).safeTransfer( @@ -234,7 +234,7 @@ contract EtherfiAdapter is ISwapAdapter { 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); + IERC20(address(eEth)).safeIncreaseAllowance(address(weEth), amountIn); uint256 balBefore = eEth.shares(address(this)); uint256 receivedAmount = weEth.wrap(amountIn); @@ -247,7 +247,7 @@ contract EtherfiAdapter is ISwapAdapter { return realSpentEeth; } else { IERC20(address(eEth)).safeTransferFrom(msg.sender, address(this), amount); - IERC20(address(eEth)).approve(address(weEth), amount); + IERC20(address(eEth)).safeIncreaseAllowance(address(weEth), amount); uint256 receivedAmount = weEth.wrap(amount); IERC20(address(weEth)).safeTransfer( From 2a6093a9947b3872d42109598b00cab5b7880ef6 Mon Sep 17 00:00:00 2001 From: domenicodev Date: Wed, 6 Mar 2024 18:17:56 +0100 Subject: [PATCH 29/50] fix: Review fixes and improvements --- evm/src/etherfi/EtherfiAdapter.sol | 214 +++++++++++++++++++---------- evm/test/EtherfiAdapter.t.sol | 161 ++++++++++++++-------- 2 files changed, 241 insertions(+), 134 deletions(-) diff --git a/evm/src/etherfi/EtherfiAdapter.sol b/evm/src/etherfi/EtherfiAdapter.sol index ad5d78d..0ba73d9 100644 --- a/evm/src/etherfi/EtherfiAdapter.sol +++ b/evm/src/etherfi/EtherfiAdapter.sol @@ -7,13 +7,15 @@ import {SafeERC20} from "openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol"; /// @title Etherfi Adapter -/// @dev This contract supports the following swaps: ETH->eETH, wETH<->eETH, +/// @dev This contract supports the following swaps: ETH->eETH, weETH<->eETH, /// ETH->weETH contract EtherfiAdapter is ISwapAdapter { using SafeERC20 for IERC20; - IWeEth weEth; - IeEth eEth; + uint256 constant PRECISE_UNIT = 10 ** 18; + + IWeEth immutable weEth; + IeEth immutable eEth; ILiquidityPool public liquidityPool; constructor(address _weEth) { @@ -58,10 +60,28 @@ contract EtherfiAdapter is ISwapAdapter { _prices = new Fraction[](_specifiedAmounts.length); address sellTokenAddress = address(_sellToken); address buyTokenAddress = address(_buyToken); + uint256 totalPooledEther = liquidityPool.getTotalPooledEther(); + uint256 eEthTotalShares = eEth.totalShares(); for (uint256 i = 0; i < _specifiedAmounts.length; i++) { + if (sellTokenAddress == address(0)) { + uint256 sharesForDepositAmount = _sharesForDepositAmount( + _specifiedAmounts[i], totalPooledEther, eEthTotalShares + ); + _prices[i] = getPriceAt( + sellTokenAddress, + buyTokenAddress, + _specifiedAmounts[i], + totalPooledEther + _specifiedAmounts[i], + eEthTotalShares + sharesForDepositAmount + ); + } _prices[i] = getPriceAt( - sellTokenAddress, buyTokenAddress, _specifiedAmounts[i] + sellTokenAddress, + buyTokenAddress, + _specifiedAmounts[i], + totalPooledEther, + eEthTotalShares ); } } @@ -100,15 +120,13 @@ contract EtherfiAdapter is ISwapAdapter { } } trade.gasUsed = gasBefore - gasleft(); - if (side == OrderSide.Sell) { - trade.price = getPriceAt( - sellTokenAddress, buyTokenAddress, specifiedAmount - ); - } else { - trade.price = getPriceAt( - sellTokenAddress, buyTokenAddress, trade.calculatedAmount - ); - } + trade.price = getPriceAt( + sellTokenAddress, + buyTokenAddress, + PRECISE_UNIT, + liquidityPool.getTotalPooledEther(), + eEth.totalShares() + ); } /// @inheritdoc ISwapAdapter @@ -121,11 +139,14 @@ contract EtherfiAdapter is ISwapAdapter { { limits = new uint256[](2); - /// @dev Limits are underestimated to 90% of totalSupply as both weEth and eEth have no limits but revert in some cases - if(address(sellToken) == address(weEth) || address(buyToken) == address(weEth)) { + /// @dev Limits are underestimated to 90% of totalSupply as both weEth + /// and eEth have no limits but revert in some cases + if ( + address(sellToken) == address(weEth) + || address(buyToken) == address(weEth) + ) { limits[0] = IERC20(address(weEth)).totalSupply() * 90 / 100; - } - else { + } else { limits[0] = IERC20(address(eEth)).totalSupply() * 90 / 100; } limits[1] = limits[0]; @@ -177,18 +198,15 @@ contract EtherfiAdapter is ISwapAdapter { if (side == OrderSide.Buy) { uint256 amountIn = getAmountIn(address(0), address(eEth), amount); liquidityPool.deposit{value: amountIn}(); - IERC20(address(eEth)).safeTransfer( - address(msg.sender), amount - ); + IERC20(address(eEth)).safeTransfer(address(msg.sender), amount); return amountIn; } else { - uint256 balBefore = IERC20(address(eEth)).balanceOf(address(msg.sender)); - liquidityPool.deposit{value: amount}(); - uint256 receivedAmount = IERC20(address(eEth)).balanceOf(address(msg.sender)) - balBefore; - IERC20(address(eEth)).transfer( - msg.sender, receivedAmount - ); - return receivedAmount; + uint256 receivedAmount = liquidityPool.deposit{value: amount}(); + uint256 balBeforeUser = + IERC20(address(eEth)).balanceOf(address(msg.sender)); + IERC20(address(eEth)).transfer(msg.sender, receivedAmount); + return IERC20(address(eEth)).balanceOf(address(msg.sender)) + - balBeforeUser; } } @@ -232,21 +250,26 @@ contract EtherfiAdapter is ISwapAdapter { returns (uint256) { if (side == OrderSide.Buy) { - uint256 amountIn = getAmountIn(address(eEth), address(weEth), amount); - IERC20(address(eEth)).safeTransferFrom(msg.sender, address(this), amount); - IERC20(address(eEth)).safeIncreaseAllowance(address(weEth), amountIn); + uint256 amountIn = + getAmountIn(address(eEth), address(weEth), amount); + IERC20(address(eEth)).safeTransferFrom( + msg.sender, address(this), amountIn + ); + IERC20(address(eEth)).safeIncreaseAllowance( + address(weEth), amountIn + ); - uint256 balBefore = eEth.shares(address(this)); uint256 receivedAmount = weEth.wrap(amountIn); - uint256 realSpentEeth = balBefore - eEth.shares(address(this)); IERC20(address(weEth)).safeTransfer( address(msg.sender), receivedAmount ); - return realSpentEeth; + return amountIn; } else { - IERC20(address(eEth)).safeTransferFrom(msg.sender, address(this), amount); + IERC20(address(eEth)).safeTransferFrom( + msg.sender, address(this), amount + ); IERC20(address(eEth)).safeIncreaseAllowance(address(weEth), amount); uint256 receivedAmount = weEth.wrap(amount); @@ -264,51 +287,111 @@ contract EtherfiAdapter is ISwapAdapter { returns (uint256) { if (side == OrderSide.Buy) { - uint256 amountIn = getAmountIn(address(weEth), address(eEth), amount); - IERC20(address(weEth)).safeTransferFrom(msg.sender, address(this), amountIn); + uint256 amountIn = + getAmountIn(address(weEth), address(eEth), amount); + IERC20(address(weEth)).safeTransferFrom( + msg.sender, address(this), amountIn + ); uint256 receivedAmount = weEth.unwrap(amountIn); IERC20(address(eEth)).safeTransfer( address(msg.sender), receivedAmount ); return amountIn; } else { - IERC20(address(weEth)).safeTransferFrom(msg.sender, address(this), amount); - uint256 receivedAmount = weEth.unwrap(amount); - IERC20(address(eEth)).safeTransfer( - address(msg.sender), receivedAmount + IERC20(address(weEth)).safeTransferFrom( + msg.sender, address(this), amount ); - return receivedAmount; + uint256 receivedAmount = weEth.unwrap(amount); + uint256 balBeforeUser = + IERC20(address(eEth)).balanceOf(address(msg.sender)); + IERC20(address(eEth)).transfer(msg.sender, receivedAmount); + return IERC20(address(eEth)).balanceOf(address(msg.sender)) + - balBeforeUser; } } - /// @dev copy of '_sharesForDepositAmount' internal function in LiquidityPool - function _sharesForDepositAmount(uint256 _depositAmount) internal view returns (uint256) { - uint256 totalPooledEther = liquidityPool.getTotalPooledEther() - _depositAmount; - if (totalPooledEther == 0) { + /// @dev copy of '_sharesForDepositAmount' internal function in + /// LiquidityPool, without ether subtraction + function _sharesForDepositAmount( + uint256 _depositAmount, + uint256 _totalPooledEther, + uint256 _eEthTotalShares + ) internal pure returns (uint256) { + if (_totalPooledEther == 0) { return _depositAmount; } - return (_depositAmount * eEth.totalShares()) / totalPooledEther; + return (_depositAmount * _eEthTotalShares) / _totalPooledEther; + } + + /// @dev copy of 'getWeETHByeEth' function in weETH, dynamic + function _getWeETHByeEth( + uint256 _depositAmount, + uint256 _totalPooledEther, + uint256 _eEthTotalShares + ) internal pure returns (uint256) { + if (_totalPooledEther == 0) { + return 0; + } + return (_depositAmount * _eEthTotalShares) / _totalPooledEther; + } + + /// @dev copy of 'getEethByWeEth' function in weETH, dynamic + function _getEethByWeEth( + uint256 _depositAmount, + uint256 _totalPooledEther, + uint256 _eEthTotalShares + ) internal pure returns (uint256) { + if (_eEthTotalShares == 0) { + return 0; + } + return (_depositAmount * _totalPooledEther) / _eEthTotalShares; } /// @notice Get swap price /// @param sellToken token to sell /// @param buyToken token to buy - function getPriceAt(address sellToken, address buyToken, uint256 amount) - internal - view - returns (Fraction memory) - { + /// @param totalPooledEther total pooled ether after or before trade if + /// required + /// @param eEthTotalShares total shares of eETH after or before trade if + /// required + function getPriceAt( + address sellToken, + address buyToken, + uint256 amount, + uint256 totalPooledEther, + uint256 eEthTotalShares + ) internal view returns (Fraction memory) { if (sellToken == address(0)) { if (buyToken == address(eEth)) { - return Fraction(_sharesForDepositAmount(amount), amount); + return Fraction( + _sharesForDepositAmount( + amount, totalPooledEther, eEthTotalShares + ), + amount + ); } else { - uint256 eEthOut = _sharesForDepositAmount(amount); - return Fraction(liquidityPool.sharesForAmount(eEthOut), amount); + uint256 eEthOut = _sharesForDepositAmount( + amount, totalPooledEther, eEthTotalShares + ); + return Fraction( + _getWeETHByeEth( + eEthOut, + totalPooledEther + amount, + eEthTotalShares + eEthOut + ), + amount + ); } } else if (sellToken == address(eEth)) { - return Fraction(liquidityPool.sharesForAmount(amount), amount); + return Fraction( + _getWeETHByeEth(amount, totalPooledEther, eEthTotalShares), + amount + ); } else { - return Fraction(liquidityPool.amountForShare(amount), amount); + return Fraction( + _getEethByWeEth(amount, totalPooledEther, eEthTotalShares), + amount + ); } } @@ -328,7 +411,7 @@ contract EtherfiAdapter is ISwapAdapter { } } else if (sellToken == address(eEth)) { // eEth-weEth - return liquidityPool.amountForShare(amountOut); + return weEth.getEETHByWeETH(amountOut); } else { // weEth-eEth return weEth.getWeETHByeETH(amountOut); @@ -336,22 +419,6 @@ contract EtherfiAdapter is ISwapAdapter { } } -interface IWithdrawRequestNFT { - function requestWithdraw( - uint96 amountOfEEth, - uint96 shareOfEEth, - address recipient, - uint256 fee - ) external payable returns (uint256); - - function getClaimableAmount(uint256 tokenId) - external - view - returns (uint256); - - function claimWithdraw(uint256 tokenId) external; -} - interface ILiquidityPool { function numPendingDeposits() external view returns (uint32); function totalValueOutOfLp() external view returns (uint128); @@ -378,7 +445,6 @@ interface ILiquidityPool { function requestWithdraw(address recipient, uint256 amount) external returns (uint256); - function withdrawRequestNFT() external view returns (IWithdrawRequestNFT); } interface IeEth { diff --git a/evm/test/EtherfiAdapter.t.sol b/evm/test/EtherfiAdapter.t.sol index b7db414..8d0c563 100644 --- a/evm/test/EtherfiAdapter.t.sol +++ b/evm/test/EtherfiAdapter.t.sol @@ -30,23 +30,29 @@ contract EtherfiAdapterTest is Test, ISwapAdapterTypes { function testPriceFuzzEtherfi(uint256 amount0, uint256 amount1) public { bytes32 pair = bytes32(0); - uint256[] memory limits = adapter.getLimits(pair, IERC20(address(weEth)), IERC20(address(eEth))); + uint256[] memory limits = adapter.getLimits( + pair, IERC20(address(weEth)), IERC20(address(eEth)) + ); vm.assume(amount0 < limits[0] && amount0 > 0); vm.assume(amount1 < limits[1] && amount1 > 0); - + uint256[] memory amounts = new uint256[](2); amounts[0] = amount0; amounts[1] = amount1; - - Fraction[] memory prices = adapter.price(pair, IERC20(address(weEth)), IERC20(address(eEth)), amounts); - + + Fraction[] memory prices = adapter.price( + pair, IERC20(address(weEth)), IERC20(address(eEth)), amounts + ); + for (uint256 i = 0; i < prices.length; i++) { assertGt(prices[i].numerator, 0); assertGt(prices[i].denominator, 0); } } - function testSwapFuzzEtherfiEethWeEth(uint256 specifiedAmount, bool isBuy) public { + function testSwapFuzzEtherfiEethWeEth(uint256 specifiedAmount, bool isBuy) + public + { OrderSide side = isBuy ? OrderSide.Buy : OrderSide.Sell; IERC20 eEth_ = IERC20(address(eEth)); @@ -57,17 +63,23 @@ contract EtherfiAdapterTest is Test, ISwapAdapterTypes { if (side == OrderSide.Buy) { vm.assume(specifiedAmount < limits[1] && specifiedAmount > 100); - /// @dev workaround for eETH "deal", as standard ERC20 does not work(balance is shares) + /// @dev workaround for eETH "deal", as standard ERC20 does not + /// work(balance is shares) deal(address(adapter), type(uint256).max); - adapter.swap(pair, IERC20(address(0)), eEth_, OrderSide.Buy, limits[0]); + adapter.swap( + pair, IERC20(address(0)), eEth_, OrderSide.Buy, limits[0] + ); eEth_.approve(address(adapter), type(uint256).max); } else { vm.assume(specifiedAmount < limits[0] && specifiedAmount > 100); - /// @dev workaround for eETH "deal", as standard ERC20 does not work(balance is shares) + /// @dev workaround for eETH "deal", as standard ERC20 does not + /// work(balance is shares) deal(address(adapter), type(uint128).max); - adapter.swap(pair, IERC20(address(0)), eEth_, OrderSide.Buy, specifiedAmount); + adapter.swap( + pair, IERC20(address(0)), eEth_, OrderSide.Buy, specifiedAmount + ); eEth_.approve(address(adapter), specifiedAmount); } @@ -81,24 +93,32 @@ contract EtherfiAdapterTest is Test, ISwapAdapterTypes { if (trade.calculatedAmount > 0) { if (side == OrderSide.Buy) { assertGe( - specifiedAmount, - weEth_.balanceOf(address(this)) - weEth_balance + weEth_.balanceOf(address(this)) - weEth_balance, + specifiedAmount - 2 ); - /// @dev Transfer function contains rounding errors because of rewards in weETH contract, therefore we assume a +/-2 tolerance + /// @dev Transfer function contains rounding errors because of + /// rewards in weETH contract, therefore we assume a +/-2 + /// tolerance assertLe( - specifiedAmount - 2, - weEth_.balanceOf(address(this)) - weEth_balance + weEth_.balanceOf(address(this)) - weEth_balance, + specifiedAmount ); assertLe( - trade.calculatedAmount - 2, - eEth_balance - eEth_.balanceOf(address(this)) + eEth_balance - eEth_.balanceOf(address(this)), + trade.calculatedAmount + 2 + ); + assertGe( + eEth_balance - eEth_.balanceOf(address(this)), + trade.calculatedAmount - 1 ); } else { assertGe( specifiedAmount, eEth_balance - eEth_.balanceOf(address(this)) ); - /// @dev Transfer function contains rounding errors because of rewards in eETH contract, therefore we assume a +/-2 tolerance + /// @dev Transfer function contains rounding errors because of + /// rewards in eETH contract, therefore we assume a +/-2 + /// tolerance assertLe( specifiedAmount - 2, eEth_balance - eEth_.balanceOf(address(this)) @@ -111,7 +131,9 @@ contract EtherfiAdapterTest is Test, ISwapAdapterTypes { } } - function testSwapFuzzEtherfiWeEthEeth(uint256 specifiedAmount, bool isBuy) public { + function testSwapFuzzEtherfiWeEthEeth(uint256 specifiedAmount, bool isBuy) + public + { OrderSide side = isBuy ? OrderSide.Buy : OrderSide.Sell; IERC20 eEth_ = IERC20(address(eEth)); @@ -123,17 +145,23 @@ contract EtherfiAdapterTest is Test, ISwapAdapterTypes { if (side == OrderSide.Buy) { vm.assume(specifiedAmount < limits[1] && specifiedAmount > 100); - /// @dev workaround for eETH "deal", as standard ERC20 does not work(balance is shares) + /// @dev workaround for eETH "deal", as standard ERC20 does not + /// work(balance is shares) deal(address(adapter), type(uint256).max); - adapter.swap(pair, IERC20(address(0)), weEth_, OrderSide.Buy, limits[0]); + adapter.swap( + pair, IERC20(address(0)), weEth_, OrderSide.Buy, limits[0] + ); weEth_.approve(address(adapter), type(uint256).max); } else { vm.assume(specifiedAmount < limits[0] && specifiedAmount > 100); - /// @dev workaround for eETH "deal", as standard ERC20 does not work(balance is shares) + /// @dev workaround for eETH "deal", as standard ERC20 does not + /// work(balance is shares) deal(address(adapter), type(uint128).max); - adapter.swap(pair, IERC20(address(0)), weEth_, OrderSide.Buy, specifiedAmount); + adapter.swap( + pair, IERC20(address(0)), weEth_, OrderSide.Buy, specifiedAmount + ); weEth_.approve(address(adapter), specifiedAmount); } @@ -141,7 +169,8 @@ contract EtherfiAdapterTest is Test, ISwapAdapterTypes { uint256 eEth_balance = eEth_.balanceOf(address(this)); uint256 weEth_balance = weEth_.balanceOf(address(this)); - /// @dev as of rounding errors in Etherfi, specifiedAmount might lose small digits for small numbers + /// @dev as of rounding errors in Etherfi, specifiedAmount might lose + /// small digits for small numbers /// therefore we use weEth_balance - weEth_bal_before as specifiedAmount uint256 realAmountWeEth_ = weEth_balance - weEth_bal_before; @@ -154,7 +183,9 @@ contract EtherfiAdapterTest is Test, ISwapAdapterTypes { realAmountWeEth_, eEth_.balanceOf(address(this)) - eEth_balance ); - /// @dev Transfer function contains rounding errors because of rewards in weETH contract, therefore we assume a +/-2 tolerance + /// @dev Transfer function contains rounding errors because of + /// rewards in weETH contract, therefore we assume a +/-2 + /// tolerance assertLe( realAmountWeEth_ - 2, eEth_.balanceOf(address(this)) - eEth_balance @@ -180,7 +211,9 @@ contract EtherfiAdapterTest is Test, ISwapAdapterTypes { } } - function testSwapFuzzEtherfiEthEeth(uint256 specifiedAmount, bool isBuy) public { + function testSwapFuzzEtherfiEthEeth(uint256 specifiedAmount, bool isBuy) + public + { OrderSide side = isBuy ? OrderSide.Buy : OrderSide.Sell; IERC20 eth_ = IERC20(address(0)); @@ -210,7 +243,9 @@ contract EtherfiAdapterTest is Test, ISwapAdapterTypes { specifiedAmount, eEth_.balanceOf(address(this)) - eEth_balance ); - /// @dev Transfer function contains rounding errors because of rewards in eETH contract, therefore we assume a +/-2 tolerance + /// @dev Transfer function contains rounding errors because of + /// rewards in eETH contract, therefore we assume a +/-2 + /// tolerance assertLe( specifiedAmount - 2, eEth_.balanceOf(address(this)) - eEth_balance @@ -221,8 +256,7 @@ contract EtherfiAdapterTest is Test, ISwapAdapterTypes { ); } else { assertEq( - specifiedAmount, - eth_balance - address(adapter).balance + specifiedAmount, eth_balance - address(adapter).balance ); assertEq( trade.calculatedAmount, @@ -232,7 +266,9 @@ contract EtherfiAdapterTest is Test, ISwapAdapterTypes { } } - function testSwapFuzzEtherfiEthWeEth(uint256 specifiedAmount, bool isBuy) public { + function testSwapFuzzEtherfiEthWeEth(uint256 specifiedAmount, bool isBuy) + public + { OrderSide side = isBuy ? OrderSide.Buy : OrderSide.Sell; IERC20 eth_ = IERC20(address(0)); @@ -262,7 +298,9 @@ contract EtherfiAdapterTest is Test, ISwapAdapterTypes { specifiedAmount, weEth_.balanceOf(address(this)) - weEth_balance ); - /// @dev Transfer function contains rounding errors because of rewards in eETH contract, therefore we assume a +/-2 tolerance + /// @dev Transfer function contains rounding errors because of + /// rewards in eETH contract, therefore we assume a +/-2 + /// tolerance assertLe( specifiedAmount - 2, weEth_.balanceOf(address(this)) - weEth_balance @@ -273,8 +311,7 @@ contract EtherfiAdapterTest is Test, ISwapAdapterTypes { ); } else { assertEq( - specifiedAmount, - eth_balance - address(adapter).balance + specifiedAmount, eth_balance - address(adapter).balance ); assertEq( trade.calculatedAmount, @@ -295,60 +332,64 @@ contract EtherfiAdapterTest is Test, ISwapAdapterTypes { function executeIncreasingSwapsEtherfi(OrderSide side) internal { bytes32 pair = bytes32(0); - uint256 amountConstant_ = 10**18; - + uint256 amountConstant_ = 10 ** 18; + uint256[] memory amounts = new uint256[](TEST_ITERATIONS); amounts[0] = amountConstant_; for (uint256 i = 1; i < TEST_ITERATIONS; i++) { - amounts[i] = amountConstant_ + i; + amounts[i] = amountConstant_ * i; } - + Trade[] memory trades = new Trade[](TEST_ITERATIONS); uint256 beforeSwap; for (uint256 i = 1; i < TEST_ITERATIONS; i++) { beforeSwap = vm.snapshot(); - + deal(address(weEth), address(this), amounts[i]); IERC20(address(weEth)).approve(address(adapter), amounts[i]); - - trades[i] = adapter.swap(pair, IERC20(address(weEth)), IERC20(address(eEth)), side, amounts[i]); + + trades[i] = adapter.swap( + pair, + IERC20(address(weEth)), + IERC20(address(eEth)), + side, + amounts[i] + ); vm.revertTo(beforeSwap); } - + for (uint256 i = 1; i < TEST_ITERATIONS - 1; i++) { - assertLe( - trades[i].calculatedAmount, - trades[i + 1].calculatedAmount + assertLe(trades[i].calculatedAmount, trades[i + 1].calculatedAmount); + console.log( + "Prices", trades[i].price.numerator, trades[i].price.denominator ); + console.log("Amounts", trades[i].calculatedAmount, amounts[i]); assertLe(trades[i].gasUsed, trades[i + 1].gasUsed); } } - function testGetCapabilitiesEtherfi( - bytes32 pair, - address t0, - address t1 - ) public { - Capability[] memory res = adapter.getCapabilities( - pair, - IERC20(t0), - IERC20(t1) - ); - + function testGetCapabilitiesEtherfi(bytes32 pair, address t0, address t1) + public + { + Capability[] memory res = + adapter.getCapabilities(pair, IERC20(t0), IERC20(t1)); + assertEq(res.length, 3); } - + function testGetTokensEtherfi() public { bytes32 pair = bytes32(0); IERC20[] memory tokens = adapter.getTokens(pair); - + assertEq(tokens.length, 3); } - + function testGetLimitsEtherfi() public { bytes32 pair = bytes32(0); - uint256[] memory limits = adapter.getLimits(pair, IERC20(address(eEth)), IERC20(address(weEth))); - + uint256[] memory limits = adapter.getLimits( + pair, IERC20(address(eEth)), IERC20(address(weEth)) + ); + assertEq(limits.length, 2); } } From 511ca59f5942af342bfa9f8825b77cd0f61cbd25 Mon Sep 17 00:00:00 2001 From: Ale personal Date: Sat, 16 Mar 2024 19:45:15 +0100 Subject: [PATCH 30/50] feat: Improve adapter and tests --- evm/src/angle/AngleAdapter.sol | 575 ++++++++++++++++++++++----------- evm/test/AngleAdapter.t.sol | 73 ++--- 2 files changed, 404 insertions(+), 244 deletions(-) diff --git a/evm/src/angle/AngleAdapter.sol b/evm/src/angle/AngleAdapter.sol index 96aeab8..521b688 100644 --- a/evm/src/angle/AngleAdapter.sol +++ b/evm/src/angle/AngleAdapter.sol @@ -3,36 +3,34 @@ pragma experimental ABIEncoderV2; pragma solidity ^0.8.13; import {IERC20, ISwapAdapter} from "src/interfaces/ISwapAdapter.sol"; +import {IERC20Metadata} from + "openzeppelin-contracts/contracts/token/ERC20/extensions/IERC20Metadata.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 +/// @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 { - - ITransmuter transmuter; + ITransmuter immutable transmuter; constructor(ITransmuter _transmuter) { transmuter = _transmuter; } /// @inheritdoc ISwapAdapter - function price( - bytes32, - IERC20 _sellToken, - IERC20 _buyToken, - uint256[] memory _specifiedAmounts - ) external view override 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(_specifiedAmounts[i], sellTokenAddress, buyTokenAddress, OrderSide.Sell); - } + function price(bytes32, IERC20, IERC20, uint256[] memory) + external + pure + override + returns (Fraction[] memory) + { + revert NotImplemented("AngleAdapter.price"); } /// @inheritdoc ISwapAdapter @@ -49,21 +47,27 @@ contract AngleAdapter is ISwapAdapter { uint256 gasBefore = gasleft(); if (side == OrderSide.Sell) { - trade.calculatedAmount = - sell(sellToken, buyToken, specifiedAmount); + trade.calculatedAmount = sell(sellToken, buyToken, specifiedAmount); } else { - trade.calculatedAmount = - buy(sellToken, buyToken, specifiedAmount); + trade.calculatedAmount = buy(sellToken, buyToken, specifiedAmount); } - trade.price = getPriceAt(specifiedAmount, address(sellToken), address(buyToken), side); trade.gasUsed = gasBefore - gasleft(); + uint8 decimals = side == OrderSide.Sell + ? IERC20Metadata(address(sellToken)).decimals() + : IERC20Metadata(address(buyToken)).decimals(); + trade.price = + getPriceAt(address(sellToken), address(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. + /// @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, IERC20 sellToken, IERC20 buyToken) external view @@ -75,29 +79,33 @@ contract AngleAdapter is ISwapAdapter { address buyTokenAddress = address(buyToken); address transmuterAddress = address(transmuter); - if(buyTokenAddress == transmuter.agToken()) { // mint(buy agToken) - Collateral memory collatInfo = transmuter.getCollateralInfo(sellTokenAddress); - if(collatInfo.isManaged > 0) { - limits[0] = LibManager.maxAvailable(collatInfo.managerData.config); - } - else { + if (buyTokenAddress == transmuter.agToken()) { + // mint(buy agToken) + Collateral memory collatInfo = + transmuter.getCollateralInfo(sellTokenAddress); + if (collatInfo.isManaged > 0) { + limits[0] = + LibManager.maxAvailable(collatInfo.managerData.config); + } else { limits[0] = sellToken.balanceOf(transmuterAddress); } - limits[1] = transmuter.quoteIn(limits[0], sellTokenAddress, buyTokenAddress); + limits[1] = + transmuter.quoteIn(limits[0], sellTokenAddress, buyTokenAddress); limits[1] = limits[1] / RESERVE_LIMIT_FACTOR; limits[0] = limits[0] / RESERVE_LIMIT_FACTOR; - } - else { // burn(sell agToken) - Collateral memory collatInfo = transmuter.getCollateralInfo(buyTokenAddress); - uint256 collatLimit; - if(collatInfo.isManaged > 0) { - collatLimit = LibManager.maxAvailable(collatInfo.managerData.config); + } else { + // burn(sell agToken) + Collateral memory collatInfo = + transmuter.getCollateralInfo(buyTokenAddress); + if (collatInfo.isManaged > 0) { + limits[1] = + LibManager.maxAvailable(collatInfo.managerData.config); + } else { + limits[1] = buyToken.balanceOf(transmuterAddress); } - else { - collatLimit = buyToken.balanceOf(transmuterAddress); - } - limits[0] = transmuter.quoteIn(collatLimit, buyTokenAddress, sellTokenAddress); - limits[1] = collatLimit / RESERVE_LIMIT_FACTOR; + limits[0] = + transmuter.quoteIn(limits[1], buyTokenAddress, sellTokenAddress); + limits[1] = limits[1] / RESERVE_LIMIT_FACTOR; limits[0] = limits[0] / RESERVE_LIMIT_FACTOR; } } @@ -112,11 +120,11 @@ contract AngleAdapter is ISwapAdapter { capabilities = new Capability[](3); capabilities[0] = Capability.SellOrder; capabilities[1] = Capability.BuyOrder; - capabilities[2] = Capability.PriceFunction; } /// @inheritdoc ISwapAdapter - /// @dev Since Angle has no pool IDs but supports 3 tokens(agToken and the collaterals), + /// @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 @@ -126,7 +134,7 @@ contract AngleAdapter is ISwapAdapter { { address[] memory collateralsAddresses = transmuter.getCollateralList(); tokens = new IERC20[](collateralsAddresses.length + 1); - for(uint256 i = 0; i < collateralsAddresses.length; i++) { + for (uint256 i = 0; i < collateralsAddresses.length; i++) { tokens[i] = IERC20(collateralsAddresses[i]); } tokens[collateralsAddresses.length] = IERC20(transmuter.agToken()); @@ -142,30 +150,27 @@ contract AngleAdapter is ISwapAdapter { } /// @notice Calculates pool prices for specified amounts - /// @param amount The amount of the token being sold(if side == Sell) or bought(if side == Buy) /// @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(uint256 amount, address tokenIn, address tokenOut, OrderSide side) - internal - view - returns (Fraction memory) - { + function getPriceAt( + address tokenIn, + address tokenOut, + OrderSide side, + uint8 decimals + ) internal view returns (Fraction memory) { uint256 amountOut; uint256 amountIn; - if(side == OrderSide.Sell) { - amountIn = amount; + if (side == OrderSide.Sell) { + amountIn = 10 ** decimals; amountOut = transmuter.quoteIn(amountIn, tokenIn, tokenOut); + } else { + amountOut = 10 ** decimals; + amountIn = transmuter.quoteOut(amountOut, tokenIn, tokenOut); } - else { - amountOut = amount; - amountIn = transmuter.quoteOut(amount, tokenIn, tokenOut); - } - return Fraction( - amountOut, - amountIn - ); + return Fraction(amountOut, amountIn); } /// @notice Executes a sell order on the contract. @@ -173,20 +178,20 @@ contract AngleAdapter is ISwapAdapter { /// @param buyToken The token being bought. /// @param amount The amount to be traded. /// @return calculatedAmount The amount of tokens received. - function sell( - IERC20 sellToken, - IERC20 buyToken, - uint256 amount - ) internal returns (uint256 calculatedAmount) { + function sell(IERC20 sellToken, IERC20 buyToken, uint256 amount) + internal + returns (uint256 calculatedAmount) + { address sellTokenAddress = address(sellToken); address buyTokenAddress = address(buyToken); - uint256 amountOut = transmuter.quoteIn(amount, sellTokenAddress, buyTokenAddress); + calculatedAmount = + transmuter.quoteIn(amount, sellTokenAddress, buyTokenAddress); - // TODO: use safeTransferFrom sellToken.transferFrom(msg.sender, address(this), amount); sellToken.approve(address(transmuter), amount); - transmuter.swapExactInput(amount, 0, sellTokenAddress, buyTokenAddress, msg.sender, 0); - return amountOut; + transmuter.swapExactInput( + amount, 0, sellTokenAddress, buyTokenAddress, msg.sender, 0 + ); } /// @notice Executes a buy order on the contract. @@ -194,26 +199,31 @@ contract AngleAdapter is ISwapAdapter { /// @param buyToken The token being bought. /// @param amountOut The amount of buyToken to receive. /// @return calculatedAmount The amount of tokens received. - function buy( - IERC20 sellToken, - IERC20 buyToken, - uint256 amountOut - ) internal returns (uint256 calculatedAmount) { + function buy(IERC20 sellToken, IERC20 buyToken, uint256 amountOut) + internal + returns (uint256 calculatedAmount) + { address sellTokenAddress = address(sellToken); address buyTokenAddress = address(buyToken); - uint256 amountIn = transmuter.quoteOut(amountOut, sellTokenAddress, buyTokenAddress); + calculatedAmount = + transmuter.quoteOut(amountOut, sellTokenAddress, buyTokenAddress); - // TODO: use safeTransferFrom - sellToken.transferFrom(msg.sender, address(this), amountIn); - sellToken.approve(address(transmuter), amountIn); - transmuter.swapExactOutput(amountOut, type(uint256).max, sellTokenAddress, buyTokenAddress, msg.sender, 0); - return amountIn; + sellToken.transferFrom(msg.sender, address(this), calculatedAmount); + sellToken.approve(address(transmuter), calculatedAmount); + transmuter.swapExactOutput( + amountOut, + type(uint256).max, + sellTokenAddress, + buyTokenAddress, + msg.sender, + 0 + ); } } interface IAgToken is IERC20 { /*////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - MINTER ROLE ONLY FUNCTIONS + MINTER ROLE ONLY FUNCTIONS //////////////////////////////////////////////////////////////////////////////////////////////////////////////////*/ /// @notice Lets a whitelisted contract mint agTokens @@ -221,34 +231,42 @@ interface IAgToken is IERC20 { /// @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` + /// @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; + /// @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 + /// @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 + 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 + /// @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 + /// @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 @@ -256,7 +274,7 @@ interface IAgToken is IERC20 { function setTreasury(address _treasury) external; /*////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - EXTERNAL FUNCTIONS + EXTERNAL FUNCTIONS //////////////////////////////////////////////////////////////////////////////////////////////////////////////////*/ /// @notice Checks whether an address has the right to mint agTokens @@ -277,46 +295,61 @@ enum WhitelistType { } struct ManagerStorage { - IERC20[] subCollaterals; // Subtokens handled by the manager or strategies - bytes config; // Additional configuration data + 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 + 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 + 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 + /// @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); + 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 @@ -329,121 +362,224 @@ interface IManager { /// @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 + /// @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 +/// @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); + 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)); + 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 + /// @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); + 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 + /// @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(); + /// @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 + /// @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); + (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 + /// @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(); + 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) { + function parseManagerConfig(bytes memory config) + internal + pure + returns (ManagerType managerType, bytes memory data) + { (managerType, data) = abi.decode(config, (ManagerType, bytes)); } } abstract contract ITransmuter { - function implementation() external view returns (address) {} function setDummyImplementation(address _implementation) external {} - function facetAddress(bytes4 _functionSelector) external view returns (address facetAddress_) {} + function facetAddress(bytes4 _functionSelector) + external + view + returns (address facetAddress_) + {} - function facetAddresses() external view returns (address[] memory facetAddresses_) {} + function facetAddresses() + external + view + returns (address[] memory facetAddresses_) + {} - function facetFunctionSelectors(address _facet) external view returns (bytes4[] memory _facetFunctionSelectors) {} + 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 getCollateralBurnFees(address collateral) + external + view + returns (uint64[] memory xFeeBurn, int64[] memory yFeeBurn) + {} - function getCollateralDecimals(address collateral) external view returns (uint8) {} + function getCollateralDecimals(address collateral) + external + view + returns (uint8) + {} - function getCollateralInfo(address collateral) external view returns (Collateral memory) {} + 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 getCollateralMintFees(address collateral) + external + view + returns (uint64[] memory xFeeMint, int64[] memory yFeeMint) + {} - function getCollateralRatio() external view returns (uint64 collatRatio, uint256 stablecoinsIssued) {} + function getCollateralRatio() + external + view + returns (uint64 collatRatio, uint256 stablecoinsIssued) + {} - function getCollateralWhitelistData(address collateral) external view returns (bytes memory) {} + function getCollateralWhitelistData(address collateral) + external + view + returns (bytes memory) + {} - function getIssuedByCollateral( - address collateral - ) external view returns (uint256 stablecoinsFromCollateral, uint256 stablecoinsIssued) {} + function getIssuedByCollateral(address collateral) + external + view + returns (uint256 stablecoinsFromCollateral, uint256 stablecoinsIssued) + {} - function getManagerData(address collateral) external view returns (bool, address[] memory, bytes memory) {} + 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 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 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) + returns ( + uint64[] memory xRedemptionCurve, + int64[] memory yRedemptionCurve + ) {} function getTotalIssued() external view returns (uint256) {} - function isPaused(address collateral, uint8 action) external view returns (bool) {} + function isPaused(address collateral, uint8 action) + external + view + returns (bool) + {} function isTrusted(address sender) external view returns (bool) {} @@ -451,43 +587,91 @@ abstract contract ITransmuter { function isValidSelector(bytes4 selector) external view returns (bool) {} - function isWhitelistedCollateral(address collateral) external view returns (bool) {} + function isWhitelistedCollateral(address collateral) + external + view + returns (bool) + {} - function isWhitelistedForCollateral(address collateral, address sender) external returns (bool) {} + function isWhitelistedForCollateral(address collateral, address sender) + external + returns (bool) + {} - function isWhitelistedForType(uint8 whitelistType, address sender) external view returns (bool) {} + function isWhitelistedForType(uint8 whitelistType, address sender) + external + view + returns (bool) + {} - function sellRewards(uint256 minAmountOut, bytes memory payload) external returns (uint256 amountOut) {} + 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 adjustStablecoins( + address collateral, + uint128 amount, + bool increase + ) external {} - function changeAllowance(address token, address spender, uint256 amount) external {} + function changeAllowance(address token, address spender, uint256 amount) + external + {} - function recoverERC20(address collateral, address token, address to, 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 setAccessControlManager(address _newAccessControlManager) + external + {} - function setOracle(address collateral, bytes memory oracleConfig) external {} + function setOracle(address collateral, bytes memory oracleConfig) + external + {} - function setWhitelistStatus(address collateral, uint8 whitelistStatus, bytes memory whitelistData) 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 setFees( + address collateral, + uint64[] memory xFee, + int64[] memory yFee, + bool mint + ) external {} - function setRedemptionCurveParams(uint64[] memory xFee, int64[] memory yFee) 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 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 quoteOut(uint256 amountOut, address tokenIn, address tokenOut) + external + view + returns (uint256 amountIn) + {} function swapExactInput( uint256 amountIn, @@ -525,9 +709,11 @@ abstract contract ITransmuter { bytes memory permitData ) external returns (uint256 amountIn) {} - function quoteRedemptionCurve( - uint256 amount - ) external view returns (address[] memory tokens, uint256[] memory amounts) {} + function quoteRedemptionCurve(uint256 amount) + external + view + returns (address[] memory tokens, uint256[] memory amounts) + {} function redeem( uint256 amount, @@ -544,5 +730,8 @@ abstract contract ITransmuter { address[] memory forfeitTokens ) external returns (address[] memory tokens, uint256[] memory amounts) {} - function updateNormalizer(uint256 amount, bool increase) external returns (uint256) {} + function updateNormalizer(uint256 amount, bool increase) + external + returns (uint256) + {} } diff --git a/evm/test/AngleAdapter.t.sol b/evm/test/AngleAdapter.t.sol index 433174b..c8e23f1 100644 --- a/evm/test/AngleAdapter.t.sol +++ b/evm/test/AngleAdapter.t.sol @@ -6,6 +6,7 @@ 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; @@ -13,15 +14,15 @@ contract AngleAdapterTest is Test, ISwapAdapterTypes { AngleAdapter adapter; IERC20 agEUR; IERC20 constant EURC = IERC20(0x1aBaEA1f7C830bD89Acc67eC4af516284b1bC33c); - ITransmuter constant transmuter = ITransmuter(0x00253582b2a3FE112feEC532221d9708c64cEFAb); + 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); + adapter = new AngleAdapter(transmuter); agEUR = IERC20(transmuter.agToken()); vm.label(address(adapter), "AngleAdapter"); @@ -29,43 +30,9 @@ contract AngleAdapterTest is Test, ISwapAdapterTypes { vm.label(address(EURC), "EURC"); } - function testPriceFuzzAngle(uint256 amount0, uint256 amount1) public { - bytes32 pair = bytes32(0); - uint256[] memory limits = adapter.getLimits(pair, EURC, agEUR); - vm.assume(amount0 < limits[0] && amount0 > 0); - vm.assume(amount1 < limits[0] && amount1 > 0); - - uint256[] memory amounts = new uint256[](2); - amounts[0] = amount0; - amounts[1] = amount1; - - Fraction[] memory prices = adapter.price(pair, EURC, agEUR, amounts); - - for (uint256 i = 0; i < prices.length; i++) { - assertGt(prices[i].numerator, 0); - assertGt(prices[i].denominator, 0); - } - } - - - function testPriceDecreasingAngle() public { - bytes32 pair = bytes32(0); - uint256[] memory amounts = new uint256[](TEST_ITERATIONS); - - for (uint256 i = 0; i < TEST_ITERATIONS; i++) { - amounts[i] = 1000 * (i+1) * 10 ** 18; - } - - Fraction[] memory prices = adapter.price(pair, agEUR, EURC, amounts); - - for (uint256 i = 0; i < TEST_ITERATIONS - 1; i++) { - assertEq(prices[i].compareFractions(prices[i + 1]), 1); - assertGt(prices[i].denominator, 0); - assertGt(prices[i + 1].denominator, 0); - } - } - - function testSwapFuzzAngleMint(uint256 specifiedAmount, bool isBuy) public { + function testSwapFuzzAngleMint(uint256 specifiedAmount, bool isBuy) + public + { OrderSide side = isBuy ? OrderSide.Buy : OrderSide.Sell; bytes32 pair = bytes32(0); @@ -112,12 +79,13 @@ contract AngleAdapterTest is Test, ISwapAdapterTypes { } } - function testSwapFuzzAngleRedeem(uint256 specifiedAmount, bool isBuy) public { + function testSwapFuzzAngleRedeem(uint256 specifiedAmount, bool isBuy) + public + { OrderSide side = isBuy ? OrderSide.Buy : OrderSide.Sell; bytes32 pair = bytes32(0); uint256[] memory limits = adapter.getLimits(pair, agEUR, EURC); - console.log(limits[0], limits[1]); if (side == OrderSide.Buy) { vm.assume(specifiedAmount < limits[1] && specifiedAmount > 0); @@ -169,8 +137,9 @@ contract AngleAdapterTest is Test, ISwapAdapterTypes { 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); + amounts[i] = side == OrderSide.Sell + ? (100 * i * 10 ** 18) + : (100 * i * 10 ** 6); } Trade[] memory trades = new Trade[](TEST_ITERATIONS); @@ -178,22 +147,22 @@ contract AngleAdapterTest is Test, ISwapAdapterTypes { for (uint256 i = 1; i < TEST_ITERATIONS; i++) { beforeSwap = vm.snapshot(); - if(side == OrderSide.Sell) { + if (side == OrderSide.Sell) { deal(address(agEUR), address(this), amounts[i]); agEUR.approve(address(adapter), amounts[i]); - } - else { + } else { deal(address(agEUR), address(this), type(uint256).max); - agEUR.approve(address(adapter), type(uint256).max); + agEUR.approve(address(adapter), type(uint256).max); } - trades[i] = adapter.swap(pair, agEUR, EURC, side, amounts[i]); vm.revertTo(beforeSwap); } for (uint256 i = 1; i < TEST_ITERATIONS - 1; i++) { assertLe(trades[i].calculatedAmount, trades[i + 1].calculatedAmount); - assertEq(trades[i].price.compareFractions(trades[i + 1].price), 1); + assertFalse( + trades[i].price.compareFractions(trades[i + 1].price) == -1 + ); } } @@ -201,7 +170,9 @@ contract AngleAdapterTest is Test, ISwapAdapterTypes { executeIncreasingSwapsAngle(OrderSide.Buy); } - function testGetCapabilitiesAngle(bytes32 pair, address t0, address t1) public { + function testGetCapabilitiesAngle(bytes32 pair, address t0, address t1) + public + { Capability[] memory res = adapter.getCapabilities(pair, IERC20(t0), IERC20(t1)); From 66e25d1a098d29a3c048c68ff2432c5368d75ff7 Mon Sep 17 00:00:00 2001 From: pistomat Date: Wed, 27 Mar 2024 10:59:45 +0100 Subject: [PATCH 31/50] fix the format check CI --- .github/workflows/evm.yml | 15 ++++++++------- evm/src/template/manifest.yaml | 3 ++- 2 files changed, 10 insertions(+), 8 deletions(-) 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/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 From dfa731a93d9267a224fee6adf055521a322eaf07 Mon Sep 17 00:00:00 2001 From: pistomat Date: Wed, 27 Mar 2024 13:14:24 +0100 Subject: [PATCH 32/50] Remove IERC20 from ISwapAdapter and use SafeERC20 for IERC20 --- evm/src/balancer-v2/BalancerV2SwapAdapter.sol | 54 +++++++------ evm/src/integral/IntegralSwapAdapter.sol | 79 +++++++++---------- evm/src/interfaces/ISwapAdapter.sol | 23 +++--- evm/src/interfaces/ISwapAdapterTypes.sol | 2 - evm/src/template/TemplateSwapAdapter.sol | 23 +++--- evm/src/uniswap-v2/UniswapV2SwapAdapter.sol | 39 +++++---- evm/test/BalancerV2SwapAdapter.t.sol | 44 +++++------ evm/test/IntegralSwapAdapter.t.sol | 37 +++++---- evm/test/UniswapV2SwapAdapter.t.sol | 37 +++++---- 9 files changed, 173 insertions(+), 165 deletions(-) diff --git a/evm/src/balancer-v2/BalancerV2SwapAdapter.sol b/evm/src/balancer-v2/BalancerV2SwapAdapter.sol index e373649..13b2f2d 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, @@ -96,8 +102,8 @@ contract BalancerV2SwapAdapter is ISwapAdapter { function priceBatch( bytes32 poolId, - IERC20 sellToken, - IERC20 buyToken, + address sellToken, + address buyToken, uint256[] memory specifiedAmounts ) external returns (Fraction[] memory calculatedPrices) { for (uint256 i = 0; i < specifiedAmounts.length; i++) { @@ -106,7 +112,7 @@ contract BalancerV2SwapAdapter is ISwapAdapter { } } - function price(bytes32, IERC20, IERC20, uint256[] memory) + function price(bytes32, address, address, uint256[] memory) external pure override @@ -117,8 +123,8 @@ contract BalancerV2SwapAdapter is ISwapAdapter { function swap( bytes32 poolId, - IERC20 sellToken, - IERC20 buyToken, + address sellToken, + address buyToken, OrderSide side, uint256 specifiedAmount ) external override returns (Trade memory trade) { @@ -136,16 +142,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 +170,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,7 +190,7 @@ contract BalancerV2SwapAdapter is ISwapAdapter { } } - function getCapabilities(bytes32, IERC20, IERC20) + function getCapabilities(bytes32, address, address) external pure override @@ -197,7 +205,7 @@ contract BalancerV2SwapAdapter is ISwapAdapter { external view override - returns (IERC20[] memory tokens) + returns (address[] memory tokens) { (tokens,,) = vault.getPoolTokens(poolId); } @@ -472,7 +480,7 @@ interface IVault { external view returns ( - IERC20[] memory tokens, + address[] memory tokens, uint256[] memory balances, uint256 lastChangeBlock ); 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..50fed77 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,8 +34,8 @@ 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); @@ -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/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/BalancerV2SwapAdapter.t.sol b/evm/test/BalancerV2SwapAdapter.t.sol index fcb6328..06ccfe0 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,7 +34,7 @@ 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"); } @@ -97,17 +97,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 +117,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 +145,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 +176,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,8 +204,7 @@ 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(uint256(res[0]), uint256(Capability.SellOrder)); @@ -212,10 +212,10 @@ contract BalancerV2SwapAdapterTest is Test, ISwapAdapterTypes { } 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/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); } From 045fa1cc0d045a7a1fbd8a89643c21759c316ecd Mon Sep 17 00:00:00 2001 From: Ale personal Date: Wed, 27 Mar 2024 14:59:07 +0100 Subject: [PATCH 33/50] improve: Change ITransmuter from abstract contract to interface --- evm/src/angle/AngleAdapter.sol | 131 +++++++++++++-------------------- 1 file changed, 52 insertions(+), 79 deletions(-) diff --git a/evm/src/angle/AngleAdapter.sol b/evm/src/angle/AngleAdapter.sol index 521b688..508abd6 100644 --- a/evm/src/angle/AngleAdapter.sol +++ b/evm/src/angle/AngleAdapter.sol @@ -464,82 +464,71 @@ library LibManager { } } -abstract contract ITransmuter { - function implementation() external view returns (address) {} +interface ITransmuter { + function implementation() external view returns (address); - function setDummyImplementation(address _implementation) external {} + function setDummyImplementation(address _implementation) external; function facetAddress(bytes4 _functionSelector) external view - returns (address facetAddress_) - {} + returns (address facetAddress_); function facetAddresses() external view - returns (address[] memory facetAddresses_) - {} + returns (address[] memory facetAddresses_); function facetFunctionSelectors(address _facet) external view - returns (bytes4[] memory _facetFunctionSelectors) - {} + returns (bytes4[] memory _facetFunctionSelectors); - function accessControlManager() external view returns (address) {} + function accessControlManager() external view returns (address); - function agToken() external view returns (address) {} + function agToken() external view returns (address); function getCollateralBurnFees(address collateral) external view - returns (uint64[] memory xFeeBurn, int64[] memory yFeeBurn) - {} + returns (uint64[] memory xFeeBurn, int64[] memory yFeeBurn); function getCollateralDecimals(address collateral) external view - returns (uint8) - {} + returns (uint8); function getCollateralInfo(address collateral) external view - returns (Collateral memory) - {} + returns (Collateral memory); - function getCollateralList() external view returns (address[] memory) {} + function getCollateralList() external view returns (address[] memory); function getCollateralMintFees(address collateral) external view - returns (uint64[] memory xFeeMint, int64[] memory yFeeMint) - {} + returns (uint64[] memory xFeeMint, int64[] memory yFeeMint); function getCollateralRatio() external view - returns (uint64 collatRatio, uint256 stablecoinsIssued) - {} + returns (uint64 collatRatio, uint256 stablecoinsIssued); function getCollateralWhitelistData(address collateral) external view - returns (bytes memory) - {} + returns (bytes memory); function getIssuedByCollateral(address collateral) external view - returns (uint256 stablecoinsFromCollateral, uint256 stablecoinsIssued) - {} + returns (uint256 stablecoinsFromCollateral, uint256 stablecoinsIssued); function getManagerData(address collateral) external view - returns (bool, address[] memory, bytes memory) - {} + returns (bool, address[] memory, bytes memory); function getOracle(address collateral) external @@ -549,8 +538,7 @@ abstract contract ITransmuter { uint8 targetType, bytes memory oracleData, bytes memory targetData - ) - {} + ); function getOracleValues(address collateral) external @@ -561,8 +549,7 @@ abstract contract ITransmuter { uint256 ratio, uint256 minRatio, uint256 redemption - ) - {} + ); function getRedemptionFees() external @@ -570,108 +557,96 @@ abstract contract ITransmuter { returns ( uint64[] memory xRedemptionCurve, int64[] memory yRedemptionCurve - ) - {} + ); - function getTotalIssued() external view returns (uint256) {} + function getTotalIssued() external view returns (uint256); function isPaused(address collateral, uint8 action) external view - returns (bool) - {} + returns (bool); - function isTrusted(address sender) external view returns (bool) {} + function isTrusted(address sender) external view returns (bool); - function isTrustedSeller(address sender) external view returns (bool) {} + function isTrustedSeller(address sender) external view returns (bool); - function isValidSelector(bytes4 selector) external view returns (bool) {} + function isValidSelector(bytes4 selector) external view returns (bool); function isWhitelistedCollateral(address collateral) external view - returns (bool) - {} + returns (bool); function isWhitelistedForCollateral(address collateral, address sender) external - returns (bool) - {} + returns (bool); function isWhitelistedForType(uint8 whitelistType, address sender) external view - returns (bool) - {} + returns (bool); function sellRewards(uint256 minAmountOut, bytes memory payload) external - returns (uint256 amountOut) - {} + returns (uint256 amountOut); - function addCollateral(address collateral) external {} + function addCollateral(address collateral) external; function adjustStablecoins( address collateral, uint128 amount, bool increase - ) external {} + ) external; function changeAllowance(address token, address spender, uint256 amount) - external - {} + external; function recoverERC20( address collateral, address token, address to, uint256 amount - ) external {} + ) external; - function revokeCollateral(address collateral) external {} + function revokeCollateral(address collateral) external; function setAccessControlManager(address _newAccessControlManager) - external - {} + external; function setOracle(address collateral, bytes memory oracleConfig) - external - {} + external; function setWhitelistStatus( address collateral, uint8 whitelistStatus, bytes memory whitelistData - ) external {} + ) external; - function toggleTrusted(address sender, uint8 t) external {} + function toggleTrusted(address sender, uint8 t) external; function setFees( address collateral, uint64[] memory xFee, int64[] memory yFee, bool mint - ) external {} + ) external; function setRedemptionCurveParams(uint64[] memory xFee, int64[] memory yFee) - external - {} + external; - function togglePause(address collateral, uint8 pausedType) external {} + function togglePause(address collateral, uint8 pausedType) external; - function toggleWhitelist(uint8 whitelistType, address who) external {} + function toggleWhitelist(uint8 whitelistType, address who) external; function quoteIn(uint256 amountIn, address tokenIn, address tokenOut) external view - returns (uint256 amountOut) - {} + returns (uint256 amountOut); function quoteOut(uint256 amountOut, address tokenIn, address tokenOut) external view - returns (uint256 amountIn) - {} + returns (uint256 amountIn); function swapExactInput( uint256 amountIn, @@ -680,7 +655,7 @@ abstract contract ITransmuter { address tokenOut, address to, uint256 deadline - ) external returns (uint256 amountOut) {} + ) external returns (uint256 amountOut); function swapExactInputWithPermit( uint256 amountIn, @@ -689,7 +664,7 @@ abstract contract ITransmuter { address to, uint256 deadline, bytes memory permitData - ) external returns (uint256 amountOut) {} + ) external returns (uint256 amountOut); function swapExactOutput( uint256 amountOut, @@ -698,7 +673,7 @@ abstract contract ITransmuter { address tokenOut, address to, uint256 deadline - ) external returns (uint256 amountIn) {} + ) external returns (uint256 amountIn); function swapExactOutputWithPermit( uint256 amountOut, @@ -707,20 +682,19 @@ abstract contract ITransmuter { address to, uint256 deadline, bytes memory permitData - ) external returns (uint256 amountIn) {} + ) external returns (uint256 amountIn); function quoteRedemptionCurve(uint256 amount) external view - returns (address[] memory tokens, uint256[] memory amounts) - {} + 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) {} + ) external returns (address[] memory tokens, uint256[] memory amounts); function redeemWithForfeit( uint256 amount, @@ -728,10 +702,9 @@ abstract contract ITransmuter { uint256 deadline, uint256[] memory minAmountOuts, address[] memory forfeitTokens - ) external returns (address[] memory tokens, uint256[] memory amounts) {} + ) external returns (address[] memory tokens, uint256[] memory amounts); function updateNormalizer(uint256 amount, bool increase) external - returns (uint256) - {} + returns (uint256); } From 77843d0695f0bdea4769ced33465f17fcdd59615 Mon Sep 17 00:00:00 2001 From: Ale personal Date: Wed, 27 Mar 2024 15:08:24 +0100 Subject: [PATCH 34/50] fix: Update manifest of Angle --- evm/src/angle/manifest.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/evm/src/angle/manifest.yaml b/evm/src/angle/manifest.yaml index 88a2f39..a6780c4 100644 --- a/evm/src/angle/manifest.yaml +++ b/evm/src/angle/manifest.yaml @@ -10,7 +10,6 @@ constants: capabilities: - SellSide - BuySide - - PriceFunction # The file containing the adapter contract contract: AngleAdapter.sol From 2d21f988dd70d787049ad71c79ef24137ff5905a Mon Sep 17 00:00:00 2001 From: Alessandro Bergamaschi <51761861+Aleione@users.noreply.github.com> Date: Wed, 27 Mar 2024 15:09:58 +0100 Subject: [PATCH 35/50] Update evm/src/angle/AngleAdapter.sol Co-authored-by: pistomat --- evm/src/angle/AngleAdapter.sol | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/evm/src/angle/AngleAdapter.sol b/evm/src/angle/AngleAdapter.sol index 508abd6..0365d13 100644 --- a/evm/src/angle/AngleAdapter.sol +++ b/evm/src/angle/AngleAdapter.sol @@ -12,9 +12,8 @@ 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 +/// 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 { ITransmuter immutable transmuter; From 81106d75f55225ea021a45ed8cf0994d10ba30fa Mon Sep 17 00:00:00 2001 From: Ale personal Date: Wed, 27 Mar 2024 15:24:15 +0100 Subject: [PATCH 36/50] improve: Optimize sell function of Angle adapter --- evm/src/angle/AngleAdapter.sol | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/evm/src/angle/AngleAdapter.sol b/evm/src/angle/AngleAdapter.sol index 0365d13..83e97ca 100644 --- a/evm/src/angle/AngleAdapter.sol +++ b/evm/src/angle/AngleAdapter.sol @@ -183,12 +183,10 @@ contract AngleAdapter is ISwapAdapter { { address sellTokenAddress = address(sellToken); address buyTokenAddress = address(buyToken); - calculatedAmount = - transmuter.quoteIn(amount, sellTokenAddress, buyTokenAddress); sellToken.transferFrom(msg.sender, address(this), amount); sellToken.approve(address(transmuter), amount); - transmuter.swapExactInput( + calculatedAmount = transmuter.swapExactInput( amount, 0, sellTokenAddress, buyTokenAddress, msg.sender, 0 ); } From 115c7c26ccbc739d64247e70786e66f615f1d72f Mon Sep 17 00:00:00 2001 From: Alessandro Bergamaschi <51761861+Aleione@users.noreply.github.com> Date: Wed, 27 Mar 2024 15:28:34 +0100 Subject: [PATCH 37/50] Update evm/src/angle/AngleAdapter.sol Co-authored-by: pistomat --- evm/src/angle/AngleAdapter.sol | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/evm/src/angle/AngleAdapter.sol b/evm/src/angle/AngleAdapter.sol index 83e97ca..ecd4b8c 100644 --- a/evm/src/angle/AngleAdapter.sol +++ b/evm/src/angle/AngleAdapter.sol @@ -59,14 +59,13 @@ contract AngleAdapter is ISwapAdapter { } /// @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. + /// @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, IERC20 sellToken, IERC20 buyToken) external view From 113afd04fb416c89dfd7e4077b174161ecd13ff1 Mon Sep 17 00:00:00 2001 From: Alessandro Bergamaschi <51761861+Aleione@users.noreply.github.com> Date: Wed, 27 Mar 2024 15:28:47 +0100 Subject: [PATCH 38/50] Update evm/src/angle/AngleAdapter.sol Co-authored-by: pistomat --- evm/src/angle/AngleAdapter.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/evm/src/angle/AngleAdapter.sol b/evm/src/angle/AngleAdapter.sol index ecd4b8c..2b53436 100644 --- a/evm/src/angle/AngleAdapter.sol +++ b/evm/src/angle/AngleAdapter.sol @@ -122,8 +122,8 @@ contract AngleAdapter is ISwapAdapter { /// @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) + /// collaterals), we return all the available collaterals and the + /// agToken(agEUR) function getTokens(bytes32) external view From 17954c3ffecb8540de0a604b743806af53c67914 Mon Sep 17 00:00:00 2001 From: Ale personal Date: Wed, 27 Mar 2024 15:33:40 +0100 Subject: [PATCH 39/50] fix: Change output number of Angle capabilities from 3 to 2 --- evm/src/angle/AngleAdapter.sol | 2 +- evm/test/AngleAdapter.t.sol | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/evm/src/angle/AngleAdapter.sol b/evm/src/angle/AngleAdapter.sol index 2b53436..19723ac 100644 --- a/evm/src/angle/AngleAdapter.sol +++ b/evm/src/angle/AngleAdapter.sol @@ -115,7 +115,7 @@ contract AngleAdapter is ISwapAdapter { override returns (Capability[] memory capabilities) { - capabilities = new Capability[](3); + capabilities = new Capability[](2); capabilities[0] = Capability.SellOrder; capabilities[1] = Capability.BuyOrder; } diff --git a/evm/test/AngleAdapter.t.sol b/evm/test/AngleAdapter.t.sol index c8e23f1..33da838 100644 --- a/evm/test/AngleAdapter.t.sol +++ b/evm/test/AngleAdapter.t.sol @@ -176,7 +176,7 @@ contract AngleAdapterTest is Test, ISwapAdapterTypes { Capability[] memory res = adapter.getCapabilities(pair, IERC20(t0), IERC20(t1)); - assertEq(res.length, 3); + assertEq(res.length, 2); } function testGetTokensAngle() public { From f2afca9b282864407c8b8079591b412489cf8ae9 Mon Sep 17 00:00:00 2001 From: Ale personal Date: Wed, 27 Mar 2024 16:58:24 +0100 Subject: [PATCH 40/50] fix: Update manifest of Angle --- evm/src/angle/manifest.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/evm/src/angle/manifest.yaml b/evm/src/angle/manifest.yaml index a6780c4..ef376f7 100644 --- a/evm/src/angle/manifest.yaml +++ b/evm/src/angle/manifest.yaml @@ -1,7 +1,7 @@ # information about the author helps us reach out in case of issues. author: - name: Propellerheads.xyz - email: alan@propellerheads.xyz + name: shadowycoders.dev + email: hello@shadowycreators.com # Protocol Constants constants: From 5ce93956ff7078e0210c3b53ace258f8218dd73b Mon Sep 17 00:00:00 2001 From: Ale personal Date: Wed, 27 Mar 2024 17:09:15 +0100 Subject: [PATCH 41/50] improve: Add docstring to the swap and price functions of Angle --- evm/src/angle/AngleAdapter.sol | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/evm/src/angle/AngleAdapter.sol b/evm/src/angle/AngleAdapter.sol index 19723ac..44842b4 100644 --- a/evm/src/angle/AngleAdapter.sol +++ b/evm/src/angle/AngleAdapter.sol @@ -23,6 +23,13 @@ contract AngleAdapter is ISwapAdapter { } /// @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, IERC20, IERC20, uint256[] memory) external pure @@ -33,6 +40,12 @@ contract AngleAdapter is ISwapAdapter { } /// @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, IERC20 sellToken, From 21a2ecac2890c95d90013f6eeb82cd2981307cbc Mon Sep 17 00:00:00 2001 From: domenicodev Date: Fri, 5 Apr 2024 17:53:50 +0200 Subject: [PATCH 42/50] fix: Review Fixes --- evm/src/etherfi/EtherfiAdapter.sol | 37 ++++++++++++++++++++---------- 1 file changed, 25 insertions(+), 12 deletions(-) diff --git a/evm/src/etherfi/EtherfiAdapter.sol b/evm/src/etherfi/EtherfiAdapter.sol index 0ba73d9..e4a5c16 100644 --- a/evm/src/etherfi/EtherfiAdapter.sol +++ b/evm/src/etherfi/EtherfiAdapter.sol @@ -16,7 +16,7 @@ contract EtherfiAdapter is ISwapAdapter { IWeEth immutable weEth; IeEth immutable eEth; - ILiquidityPool public liquidityPool; + ILiquidityPool immutable liquidityPool; constructor(address _weEth) { weEth = IWeEth(_weEth); @@ -24,19 +24,26 @@ contract EtherfiAdapter is ISwapAdapter { liquidityPool = eEth.liquidityPool(); } - /// @dev Check if tokens in input are supported by this adapter + /// @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 eETH, weEth and ETH"); + 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 eETH, weEth and ETH"); + 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 eETH, weEth and ETH"); + revert Unavailable( + "This pool only supports ETH->eETH, weETH<->eETH and ETH->weETH swaps" + ); } _; } @@ -75,14 +82,15 @@ contract EtherfiAdapter is ISwapAdapter { totalPooledEther + _specifiedAmounts[i], eEthTotalShares + sharesForDepositAmount ); + } else { + _prices[i] = getPriceAt( + sellTokenAddress, + buyTokenAddress, + _specifiedAmounts[i], + totalPooledEther, + eEthTotalShares + ); } - _prices[i] = getPriceAt( - sellTokenAddress, - buyTokenAddress, - _specifiedAmounts[i], - totalPooledEther, - eEthTotalShares - ); } } @@ -120,6 +128,11 @@ contract EtherfiAdapter is ISwapAdapter { } } trade.gasUsed = gasBefore - gasleft(); + + /// @dev as the price is constant for all the traded amounts and depends + /// only on the totalPooledEther and totalShares, we can use a standard + /// amount(PRECISE_UNIT) to render a well-formatted price without + /// precisions loss trade.price = getPriceAt( sellTokenAddress, buyTokenAddress, From 0ae7fd2ed0c65b6d1060cf025e9f5c903f816700 Mon Sep 17 00:00:00 2001 From: domenicodev Date: Fri, 5 Apr 2024 17:56:43 +0200 Subject: [PATCH 43/50] updated manifest --- evm/src/etherfi/manifest.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/evm/src/etherfi/manifest.yaml b/evm/src/etherfi/manifest.yaml index c6cb262..0f655e4 100644 --- a/evm/src/etherfi/manifest.yaml +++ b/evm/src/etherfi/manifest.yaml @@ -1,7 +1,7 @@ # information about the author helps us reach out in case of issues. author: - name: Propellerheads.xyz - email: alan@propellerheads.xyz + name: shadowycoders.dev + email: hello@shadowycreators.com # Protocol Constants constants: From 1738ca7bd25d2d20f6de3e0211e92d6caf783ff6 Mon Sep 17 00:00:00 2001 From: pistomat Date: Sat, 13 Apr 2024 11:11:20 +0000 Subject: [PATCH 44/50] Fix merge issues --- evm/src/angle/AngleAdapter.sol | 72 +++++++++++++------------ evm/src/etherfi/EtherfiAdapter.sol | 84 ++++++++++++++---------------- evm/test/AngleAdapter.t.sol | 16 +++--- evm/test/EtherfiAdapter.t.sol | 42 +++++++-------- 4 files changed, 104 insertions(+), 110 deletions(-) diff --git a/evm/src/angle/AngleAdapter.sol b/evm/src/angle/AngleAdapter.sol index 44842b4..a1c41ac 100644 --- a/evm/src/angle/AngleAdapter.sol +++ b/evm/src/angle/AngleAdapter.sol @@ -2,9 +2,12 @@ pragma experimental ABIEncoderV2; pragma solidity ^0.8.13; -import {IERC20, ISwapAdapter} from "src/interfaces/ISwapAdapter.sol"; +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; @@ -16,6 +19,8 @@ uint256 constant STANDARD_TOKEN_DECIMALS = 10 ** 18; /// 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) { @@ -30,7 +35,7 @@ contract AngleAdapter is ISwapAdapter { * architecture of Angle, it's not possible to calculate the storage * modifications of Angle inside the adapter. */ - function price(bytes32, IERC20, IERC20, uint256[] memory) + function price(bytes32, address, address, uint256[] memory) external pure override @@ -48,8 +53,8 @@ contract AngleAdapter is ISwapAdapter { */ function swap( bytes32, - IERC20 sellToken, - IERC20 buyToken, + address sellToken, + address buyToken, OrderSide side, uint256 specifiedAmount ) external returns (Trade memory trade) { @@ -65,10 +70,10 @@ contract AngleAdapter is ISwapAdapter { } trade.gasUsed = gasBefore - gasleft(); uint8 decimals = side == OrderSide.Sell - ? IERC20Metadata(address(sellToken)).decimals() - : IERC20Metadata(address(buyToken)).decimals(); + ? IERC20Metadata(sellToken).decimals() + : IERC20Metadata(buyToken).decimals(); trade.price = - getPriceAt(address(sellToken), address(buyToken), side, decimals); + getPriceAt(sellToken, buyToken, side, decimals); } /// @inheritdoc ISwapAdapter @@ -79,50 +84,48 @@ contract AngleAdapter is ISwapAdapter { /// 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, IERC20 sellToken, IERC20 buyToken) + function getLimits(bytes32, address sellToken, address buyToken) external view override returns (uint256[] memory limits) { limits = new uint256[](2); - address sellTokenAddress = address(sellToken); - address buyTokenAddress = address(buyToken); address transmuterAddress = address(transmuter); - if (buyTokenAddress == transmuter.agToken()) { + if (buyToken == transmuter.agToken()) { // mint(buy agToken) Collateral memory collatInfo = - transmuter.getCollateralInfo(sellTokenAddress); + transmuter.getCollateralInfo(sellToken); if (collatInfo.isManaged > 0) { limits[0] = LibManager.maxAvailable(collatInfo.managerData.config); } else { - limits[0] = sellToken.balanceOf(transmuterAddress); + limits[0] = IERC20(sellToken).balanceOf(transmuterAddress); } limits[1] = - transmuter.quoteIn(limits[0], sellTokenAddress, buyTokenAddress); + 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(buyTokenAddress); + transmuter.getCollateralInfo(buyToken); if (collatInfo.isManaged > 0) { limits[1] = LibManager.maxAvailable(collatInfo.managerData.config); } else { - limits[1] = buyToken.balanceOf(transmuterAddress); + limits[1] = IERC20(buyToken).balanceOf(transmuterAddress); } limits[0] = - transmuter.quoteIn(limits[1], buyTokenAddress, sellTokenAddress); + 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, IERC20, IERC20) + function getCapabilities(bytes32, address, address) external pure override @@ -141,14 +144,14 @@ contract AngleAdapter is ISwapAdapter { external view override - returns (IERC20[] memory tokens) + returns (address[] memory tokens) { address[] memory collateralsAddresses = transmuter.getCollateralList(); - tokens = new IERC20[](collateralsAddresses.length + 1); + tokens = new address[](collateralsAddresses.length + 1); for (uint256 i = 0; i < collateralsAddresses.length; i++) { - tokens[i] = IERC20(collateralsAddresses[i]); + tokens[i] = address(collateralsAddresses[i]); } - tokens[collateralsAddresses.length] = IERC20(transmuter.agToken()); + tokens[collateralsAddresses.length] = transmuter.agToken(); } function getPoolIds(uint256, uint256) @@ -189,17 +192,14 @@ contract AngleAdapter is ISwapAdapter { /// @param buyToken The token being bought. /// @param amount The amount to be traded. /// @return calculatedAmount The amount of tokens received. - function sell(IERC20 sellToken, IERC20 buyToken, uint256 amount) + function sell(address sellToken, address buyToken, uint256 amount) internal returns (uint256 calculatedAmount) { - address sellTokenAddress = address(sellToken); - address buyTokenAddress = address(buyToken); - - sellToken.transferFrom(msg.sender, address(this), amount); - sellToken.approve(address(transmuter), amount); + IERC20(sellToken).safeTransferFrom(msg.sender, address(this), amount); + IERC20(sellToken).approve(address(transmuter), amount); calculatedAmount = transmuter.swapExactInput( - amount, 0, sellTokenAddress, buyTokenAddress, msg.sender, 0 + amount, 0, sellToken, buyToken, msg.sender, 0 ); } @@ -208,22 +208,20 @@ contract AngleAdapter is ISwapAdapter { /// @param buyToken The token being bought. /// @param amountOut The amount of buyToken to receive. /// @return calculatedAmount The amount of tokens received. - function buy(IERC20 sellToken, IERC20 buyToken, uint256 amountOut) + function buy(address sellToken, address buyToken, uint256 amountOut) internal returns (uint256 calculatedAmount) { - address sellTokenAddress = address(sellToken); - address buyTokenAddress = address(buyToken); calculatedAmount = - transmuter.quoteOut(amountOut, sellTokenAddress, buyTokenAddress); + transmuter.quoteOut(amountOut, sellToken, buyToken); - sellToken.transferFrom(msg.sender, address(this), calculatedAmount); - sellToken.approve(address(transmuter), calculatedAmount); + IERC20(sellToken).safeTransferFrom(msg.sender, address(this), calculatedAmount); + IERC20(sellToken).approve(address(transmuter), calculatedAmount); transmuter.swapExactOutput( amountOut, type(uint256).max, - sellTokenAddress, - buyTokenAddress, + sellToken, + buyToken, msg.sender, 0 ); diff --git a/evm/src/etherfi/EtherfiAdapter.sol b/evm/src/etherfi/EtherfiAdapter.sol index e4a5c16..7b720d0 100644 --- a/evm/src/etherfi/EtherfiAdapter.sol +++ b/evm/src/etherfi/EtherfiAdapter.sol @@ -2,7 +2,8 @@ pragma experimental ABIEncoderV2; pragma solidity ^0.8.13; -import {IERC20, ISwapAdapter} from "src/interfaces/ISwapAdapter.sol"; +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"; @@ -54,39 +55,37 @@ contract EtherfiAdapter is ISwapAdapter { /// @inheritdoc ISwapAdapter function price( bytes32, - IERC20 _sellToken, - IERC20 _buyToken, - uint256[] memory _specifiedAmounts + address sellToken, + address buyToken, + uint256[] memory specifiedAmounts ) external view override - checkInputTokens(address(_sellToken), address(_buyToken)) - returns (Fraction[] memory _prices) + checkInputTokens(sellToken, buyToken) + returns (Fraction[] memory prices) { - _prices = new Fraction[](_specifiedAmounts.length); - address sellTokenAddress = address(_sellToken); - address buyTokenAddress = address(_buyToken); + prices = new Fraction[](specifiedAmounts.length); uint256 totalPooledEther = liquidityPool.getTotalPooledEther(); uint256 eEthTotalShares = eEth.totalShares(); - for (uint256 i = 0; i < _specifiedAmounts.length; i++) { - if (sellTokenAddress == address(0)) { + for (uint256 i = 0; i < specifiedAmounts.length; i++) { + if (sellToken == address(0)) { uint256 sharesForDepositAmount = _sharesForDepositAmount( - _specifiedAmounts[i], totalPooledEther, eEthTotalShares + specifiedAmounts[i], totalPooledEther, eEthTotalShares ); - _prices[i] = getPriceAt( - sellTokenAddress, - buyTokenAddress, - _specifiedAmounts[i], - totalPooledEther + _specifiedAmounts[i], + prices[i] = getPriceAt( + sellToken, + buyToken, + specifiedAmounts[i], + totalPooledEther + specifiedAmounts[i], eEthTotalShares + sharesForDepositAmount ); } else { - _prices[i] = getPriceAt( - sellTokenAddress, - buyTokenAddress, - _specifiedAmounts[i], + prices[i] = getPriceAt( + sellToken, + buyToken, + specifiedAmounts[i], totalPooledEther, eEthTotalShares ); @@ -97,31 +96,28 @@ contract EtherfiAdapter is ISwapAdapter { /// @inheritdoc ISwapAdapter function swap( bytes32, - IERC20 sellToken, - IERC20 buyToken, + address sellToken, + address buyToken, OrderSide side, uint256 specifiedAmount ) external override - checkInputTokens(address(sellToken), address(buyToken)) + checkInputTokens(sellToken, 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)) { + if (sellToken == address(0)) { + if (buyToken == address(eEth)) { trade.calculatedAmount = swapEthForEeth(specifiedAmount, side); } else { trade.calculatedAmount = swapEthForWeEth(specifiedAmount, side); } } else { - if (sellTokenAddress == address(eEth)) { + if (sellToken == address(eEth)) { trade.calculatedAmount = swapEethForWeEth(specifiedAmount, side); } else { trade.calculatedAmount = swapWeEthForEeth(specifiedAmount, side); @@ -134,8 +130,8 @@ contract EtherfiAdapter is ISwapAdapter { /// amount(PRECISE_UNIT) to render a well-formatted price without /// precisions loss trade.price = getPriceAt( - sellTokenAddress, - buyTokenAddress, + sellToken, + buyToken, PRECISE_UNIT, liquidityPool.getTotalPooledEther(), eEth.totalShares() @@ -143,11 +139,11 @@ contract EtherfiAdapter is ISwapAdapter { } /// @inheritdoc ISwapAdapter - function getLimits(bytes32, IERC20 sellToken, IERC20 buyToken) + function getLimits(bytes32, address sellToken, address buyToken) external view override - checkInputTokens(address(sellToken), address(buyToken)) + checkInputTokens(sellToken, buyToken) returns (uint256[] memory limits) { limits = new uint256[](2); @@ -155,8 +151,8 @@ contract EtherfiAdapter is ISwapAdapter { /// @dev Limits are underestimated to 90% of totalSupply as both weEth /// and eEth have no limits but revert in some cases if ( - address(sellToken) == address(weEth) - || address(buyToken) == address(weEth) + sellToken == address(weEth) + || buyToken == address(weEth) ) { limits[0] = IERC20(address(weEth)).totalSupply() * 90 / 100; } else { @@ -166,7 +162,7 @@ contract EtherfiAdapter is ISwapAdapter { } /// @inheritdoc ISwapAdapter - function getCapabilities(bytes32, IERC20, IERC20) + function getCapabilities(bytes32, address, address) external pure override @@ -183,12 +179,12 @@ contract EtherfiAdapter is ISwapAdapter { external view override - returns (IERC20[] memory tokens) + returns (address[] memory tokens) { - tokens = new IERC20[](3); - tokens[0] = IERC20(address(0)); - tokens[1] = IERC20(address(eEth)); - tokens[2] = IERC20(address(weEth)); + tokens = new address[](3); + tokens[0] = address(0); + tokens[1] = address(eEth); + tokens[2] = address(weEth); } /// @inheritdoc ISwapAdapter @@ -217,7 +213,7 @@ contract EtherfiAdapter is ISwapAdapter { uint256 receivedAmount = liquidityPool.deposit{value: amount}(); uint256 balBeforeUser = IERC20(address(eEth)).balanceOf(address(msg.sender)); - IERC20(address(eEth)).transfer(msg.sender, receivedAmount); + IERC20(address(eEth)).safeTransfer(msg.sender, receivedAmount); return IERC20(address(eEth)).balanceOf(address(msg.sender)) - balBeforeUser; } @@ -317,7 +313,7 @@ contract EtherfiAdapter is ISwapAdapter { uint256 receivedAmount = weEth.unwrap(amount); uint256 balBeforeUser = IERC20(address(eEth)).balanceOf(address(msg.sender)); - IERC20(address(eEth)).transfer(msg.sender, receivedAmount); + IERC20(address(eEth)).safeTransfer(msg.sender, receivedAmount); return IERC20(address(eEth)).balanceOf(address(msg.sender)) - balBeforeUser; } diff --git a/evm/test/AngleAdapter.t.sol b/evm/test/AngleAdapter.t.sol index 33da838..b78dcbc 100644 --- a/evm/test/AngleAdapter.t.sol +++ b/evm/test/AngleAdapter.t.sol @@ -36,7 +36,7 @@ contract AngleAdapterTest is Test, ISwapAdapterTypes { OrderSide side = isBuy ? OrderSide.Buy : OrderSide.Sell; bytes32 pair = bytes32(0); - uint256[] memory limits = adapter.getLimits(pair, EURC, agEUR); + uint256[] memory limits = adapter.getLimits(pair, address(EURC), address(agEUR)); if (side == OrderSide.Buy) { vm.assume(specifiedAmount < limits[1] && specifiedAmount > 0); @@ -54,7 +54,7 @@ contract AngleAdapterTest is Test, ISwapAdapterTypes { uint256 agEUR_balance = agEUR.balanceOf(address(this)); Trade memory trade = - adapter.swap(pair, EURC, agEUR, side, specifiedAmount); + adapter.swap(pair, address(EURC), address(agEUR), side, specifiedAmount); if (trade.calculatedAmount > 0) { if (side == OrderSide.Buy) { @@ -85,7 +85,7 @@ contract AngleAdapterTest is Test, ISwapAdapterTypes { OrderSide side = isBuy ? OrderSide.Buy : OrderSide.Sell; bytes32 pair = bytes32(0); - uint256[] memory limits = adapter.getLimits(pair, agEUR, EURC); + uint256[] memory limits = adapter.getLimits(pair, address(agEUR), address(EURC)); if (side == OrderSide.Buy) { vm.assume(specifiedAmount < limits[1] && specifiedAmount > 0); @@ -103,7 +103,7 @@ contract AngleAdapterTest is Test, ISwapAdapterTypes { uint256 agEUR_balance = agEUR.balanceOf(address(this)); Trade memory trade = - adapter.swap(pair, agEUR, EURC, side, specifiedAmount); + adapter.swap(pair, address(agEUR), address(EURC), side, specifiedAmount); if (trade.calculatedAmount > 0) { if (side == OrderSide.Buy) { @@ -154,7 +154,7 @@ contract AngleAdapterTest is Test, ISwapAdapterTypes { deal(address(agEUR), address(this), type(uint256).max); agEUR.approve(address(adapter), type(uint256).max); } - trades[i] = adapter.swap(pair, agEUR, EURC, side, amounts[i]); + trades[i] = adapter.swap(pair, address(agEUR), address(EURC), side, amounts[i]); vm.revertTo(beforeSwap); } @@ -174,20 +174,20 @@ contract AngleAdapterTest is Test, ISwapAdapterTypes { public { Capability[] memory res = - adapter.getCapabilities(pair, IERC20(t0), IERC20(t1)); + adapter.getCapabilities(pair, t0, t1); assertEq(res.length, 2); } function testGetTokensAngle() public { - IERC20[] memory tokens = adapter.getTokens(bytes32(0)); + address[] memory tokens = adapter.getTokens(bytes32(0)); assertGe(tokens.length, 2); } function testGetLimitsAngle() public { bytes32 pair = bytes32(0); - uint256[] memory limits = adapter.getLimits(pair, agEUR, EURC); + uint256[] memory limits = adapter.getLimits(pair, address(agEUR), address(EURC)); assertEq(limits.length, 2); } diff --git a/evm/test/EtherfiAdapter.t.sol b/evm/test/EtherfiAdapter.t.sol index 8d0c563..8840c66 100644 --- a/evm/test/EtherfiAdapter.t.sol +++ b/evm/test/EtherfiAdapter.t.sol @@ -31,7 +31,7 @@ contract EtherfiAdapterTest is Test, ISwapAdapterTypes { function testPriceFuzzEtherfi(uint256 amount0, uint256 amount1) public { bytes32 pair = bytes32(0); uint256[] memory limits = adapter.getLimits( - pair, IERC20(address(weEth)), IERC20(address(eEth)) + pair, address(address(weEth)), address(address(eEth)) ); vm.assume(amount0 < limits[0] && amount0 > 0); vm.assume(amount1 < limits[1] && amount1 > 0); @@ -41,7 +41,7 @@ contract EtherfiAdapterTest is Test, ISwapAdapterTypes { amounts[1] = amount1; Fraction[] memory prices = adapter.price( - pair, IERC20(address(weEth)), IERC20(address(eEth)), amounts + pair, address(address(weEth)), address(address(eEth)), amounts ); for (uint256 i = 0; i < prices.length; i++) { @@ -58,7 +58,7 @@ contract EtherfiAdapterTest is Test, ISwapAdapterTypes { IERC20 eEth_ = IERC20(address(eEth)); IERC20 weEth_ = IERC20(address(weEth)); bytes32 pair = bytes32(0); - uint256[] memory limits = adapter.getLimits(pair, eEth_, weEth_); + uint256[] memory limits = adapter.getLimits(pair, address(eEth_), address(weEth_)); if (side == OrderSide.Buy) { vm.assume(specifiedAmount < limits[1] && specifiedAmount > 100); @@ -67,7 +67,7 @@ contract EtherfiAdapterTest is Test, ISwapAdapterTypes { /// work(balance is shares) deal(address(adapter), type(uint256).max); adapter.swap( - pair, IERC20(address(0)), eEth_, OrderSide.Buy, limits[0] + pair, address(address(0)), address(eEth_), OrderSide.Buy, limits[0] ); eEth_.approve(address(adapter), type(uint256).max); @@ -78,7 +78,7 @@ contract EtherfiAdapterTest is Test, ISwapAdapterTypes { /// work(balance is shares) deal(address(adapter), type(uint128).max); adapter.swap( - pair, IERC20(address(0)), eEth_, OrderSide.Buy, specifiedAmount + pair, address(address(0)), address(eEth_), OrderSide.Buy, specifiedAmount ); eEth_.approve(address(adapter), specifiedAmount); @@ -88,7 +88,7 @@ contract EtherfiAdapterTest is Test, ISwapAdapterTypes { uint256 weEth_balance = weEth_.balanceOf(address(this)); Trade memory trade = - adapter.swap(pair, eEth_, weEth_, side, specifiedAmount); + adapter.swap(pair, address(eEth_), address(weEth_), side, specifiedAmount); if (trade.calculatedAmount > 0) { if (side == OrderSide.Buy) { @@ -140,7 +140,7 @@ contract EtherfiAdapterTest is Test, ISwapAdapterTypes { IERC20 weEth_ = IERC20(address(weEth)); uint256 weEth_bal_before = weEth_.balanceOf(address(this)); bytes32 pair = bytes32(0); - uint256[] memory limits = adapter.getLimits(pair, weEth_, eEth_); + uint256[] memory limits = adapter.getLimits(pair, address(weEth_), address(eEth_)); if (side == OrderSide.Buy) { vm.assume(specifiedAmount < limits[1] && specifiedAmount > 100); @@ -149,7 +149,7 @@ contract EtherfiAdapterTest is Test, ISwapAdapterTypes { /// work(balance is shares) deal(address(adapter), type(uint256).max); adapter.swap( - pair, IERC20(address(0)), weEth_, OrderSide.Buy, limits[0] + pair, address(address(0)), address(weEth_), OrderSide.Buy, limits[0] ); weEth_.approve(address(adapter), type(uint256).max); @@ -160,7 +160,7 @@ contract EtherfiAdapterTest is Test, ISwapAdapterTypes { /// work(balance is shares) deal(address(adapter), type(uint128).max); adapter.swap( - pair, IERC20(address(0)), weEth_, OrderSide.Buy, specifiedAmount + pair, address(address(0)), address(weEth_), OrderSide.Buy, specifiedAmount ); weEth_.approve(address(adapter), specifiedAmount); @@ -175,7 +175,7 @@ contract EtherfiAdapterTest is Test, ISwapAdapterTypes { uint256 realAmountWeEth_ = weEth_balance - weEth_bal_before; Trade memory trade = - adapter.swap(pair, weEth_, eEth_, side, realAmountWeEth_); + adapter.swap(pair, address(weEth_), address(eEth_), side, realAmountWeEth_); if (trade.calculatedAmount > 0) { if (side == OrderSide.Buy) { @@ -216,10 +216,10 @@ contract EtherfiAdapterTest is Test, ISwapAdapterTypes { { OrderSide side = isBuy ? OrderSide.Buy : OrderSide.Sell; - IERC20 eth_ = IERC20(address(0)); + address eth_ = address(0); IERC20 eEth_ = IERC20(address(eEth)); bytes32 pair = bytes32(0); - uint256[] memory limits = adapter.getLimits(pair, eth_, eEth_); + uint256[] memory limits = adapter.getLimits(pair, eth_, address(eEth_)); if (side == OrderSide.Buy) { vm.assume(specifiedAmount < limits[1] && specifiedAmount > 10); @@ -235,7 +235,7 @@ contract EtherfiAdapterTest is Test, ISwapAdapterTypes { uint256 eEth_balance = eEth_.balanceOf(address(this)); Trade memory trade = - adapter.swap(pair, eth_, eEth_, side, specifiedAmount); + adapter.swap(pair, eth_, address(eEth_), side, specifiedAmount); if (trade.calculatedAmount > 0) { if (side == OrderSide.Buy) { @@ -271,10 +271,10 @@ contract EtherfiAdapterTest is Test, ISwapAdapterTypes { { OrderSide side = isBuy ? OrderSide.Buy : OrderSide.Sell; - IERC20 eth_ = IERC20(address(0)); + address eth_ = address(0); IERC20 weEth_ = IERC20(address(weEth)); bytes32 pair = bytes32(0); - uint256[] memory limits = adapter.getLimits(pair, eth_, weEth_); + uint256[] memory limits = adapter.getLimits(pair, eth_, address(weEth_)); if (side == OrderSide.Buy) { vm.assume(specifiedAmount < limits[1] && specifiedAmount > 10); @@ -290,7 +290,7 @@ contract EtherfiAdapterTest is Test, ISwapAdapterTypes { uint256 weEth_balance = weEth_.balanceOf(address(this)); Trade memory trade = - adapter.swap(pair, eth_, weEth_, side, specifiedAmount); + adapter.swap(pair, eth_, address(weEth_), side, specifiedAmount); if (trade.calculatedAmount > 0) { if (side == OrderSide.Buy) { @@ -350,8 +350,8 @@ contract EtherfiAdapterTest is Test, ISwapAdapterTypes { trades[i] = adapter.swap( pair, - IERC20(address(weEth)), - IERC20(address(eEth)), + address(address(weEth)), + address(address(eEth)), side, amounts[i] ); @@ -372,14 +372,14 @@ contract EtherfiAdapterTest is Test, ISwapAdapterTypes { public { Capability[] memory res = - adapter.getCapabilities(pair, IERC20(t0), IERC20(t1)); + adapter.getCapabilities(pair, address(t0), address(t1)); assertEq(res.length, 3); } function testGetTokensEtherfi() public { bytes32 pair = bytes32(0); - IERC20[] memory tokens = adapter.getTokens(pair); + address[] memory tokens = adapter.getTokens(pair); assertEq(tokens.length, 3); } @@ -387,7 +387,7 @@ contract EtherfiAdapterTest is Test, ISwapAdapterTypes { function testGetLimitsEtherfi() public { bytes32 pair = bytes32(0); uint256[] memory limits = adapter.getLimits( - pair, IERC20(address(eEth)), IERC20(address(weEth)) + pair, address(eEth), address(weEth) ); assertEq(limits.length, 2); From 54e4c9f92eb5dafd4f19af640d7812e241f57201 Mon Sep 17 00:00:00 2001 From: pistomat Date: Sat, 13 Apr 2024 11:11:34 +0000 Subject: [PATCH 45/50] chore: forge fmt --- evm/src/angle/AngleAdapter.sol | 23 ++++++--------- evm/src/etherfi/EtherfiAdapter.sol | 5 +--- evm/test/AngleAdapter.t.sol | 26 ++++++++++------- evm/test/EtherfiAdapter.t.sol | 45 +++++++++++++++++++++--------- 4 files changed, 57 insertions(+), 42 deletions(-) diff --git a/evm/src/angle/AngleAdapter.sol b/evm/src/angle/AngleAdapter.sol index a1c41ac..9aaa50d 100644 --- a/evm/src/angle/AngleAdapter.sol +++ b/evm/src/angle/AngleAdapter.sol @@ -72,8 +72,7 @@ contract AngleAdapter is ISwapAdapter { uint8 decimals = side == OrderSide.Sell ? IERC20Metadata(sellToken).decimals() : IERC20Metadata(buyToken).decimals(); - trade.price = - getPriceAt(sellToken, buyToken, side, decimals); + trade.price = getPriceAt(sellToken, buyToken, side, decimals); } /// @inheritdoc ISwapAdapter @@ -103,8 +102,7 @@ contract AngleAdapter is ISwapAdapter { } else { limits[0] = IERC20(sellToken).balanceOf(transmuterAddress); } - limits[1] = - transmuter.quoteIn(limits[0], sellToken, buyToken); + limits[1] = transmuter.quoteIn(limits[0], sellToken, buyToken); limits[1] = limits[1] / RESERVE_LIMIT_FACTOR; limits[0] = limits[0] / RESERVE_LIMIT_FACTOR; } else { @@ -117,8 +115,7 @@ contract AngleAdapter is ISwapAdapter { } else { limits[1] = IERC20(buyToken).balanceOf(transmuterAddress); } - limits[0] = - transmuter.quoteIn(limits[1], buyToken, sellToken); + limits[0] = transmuter.quoteIn(limits[1], buyToken, sellToken); limits[1] = limits[1] / RESERVE_LIMIT_FACTOR; limits[0] = limits[0] / RESERVE_LIMIT_FACTOR; } @@ -212,18 +209,14 @@ contract AngleAdapter is ISwapAdapter { internal returns (uint256 calculatedAmount) { - calculatedAmount = - transmuter.quoteOut(amountOut, sellToken, buyToken); + calculatedAmount = transmuter.quoteOut(amountOut, sellToken, buyToken); - IERC20(sellToken).safeTransferFrom(msg.sender, address(this), calculatedAmount); + 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 + amountOut, type(uint256).max, sellToken, buyToken, msg.sender, 0 ); } } diff --git a/evm/src/etherfi/EtherfiAdapter.sol b/evm/src/etherfi/EtherfiAdapter.sol index 7b720d0..a032a2e 100644 --- a/evm/src/etherfi/EtherfiAdapter.sol +++ b/evm/src/etherfi/EtherfiAdapter.sol @@ -150,10 +150,7 @@ contract EtherfiAdapter is ISwapAdapter { /// @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) - ) { + if (sellToken == address(weEth) || buyToken == address(weEth)) { limits[0] = IERC20(address(weEth)).totalSupply() * 90 / 100; } else { limits[0] = IERC20(address(eEth)).totalSupply() * 90 / 100; diff --git a/evm/test/AngleAdapter.t.sol b/evm/test/AngleAdapter.t.sol index b78dcbc..b8e50c6 100644 --- a/evm/test/AngleAdapter.t.sol +++ b/evm/test/AngleAdapter.t.sol @@ -36,7 +36,8 @@ contract AngleAdapterTest is Test, ISwapAdapterTypes { OrderSide side = isBuy ? OrderSide.Buy : OrderSide.Sell; bytes32 pair = bytes32(0); - uint256[] memory limits = adapter.getLimits(pair, address(EURC), address(agEUR)); + uint256[] memory limits = + adapter.getLimits(pair, address(EURC), address(agEUR)); if (side == OrderSide.Buy) { vm.assume(specifiedAmount < limits[1] && specifiedAmount > 0); @@ -53,8 +54,9 @@ contract AngleAdapterTest is Test, ISwapAdapterTypes { 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); + Trade memory trade = adapter.swap( + pair, address(EURC), address(agEUR), side, specifiedAmount + ); if (trade.calculatedAmount > 0) { if (side == OrderSide.Buy) { @@ -85,7 +87,8 @@ contract AngleAdapterTest is Test, ISwapAdapterTypes { OrderSide side = isBuy ? OrderSide.Buy : OrderSide.Sell; bytes32 pair = bytes32(0); - uint256[] memory limits = adapter.getLimits(pair, address(agEUR), address(EURC)); + uint256[] memory limits = + adapter.getLimits(pair, address(agEUR), address(EURC)); if (side == OrderSide.Buy) { vm.assume(specifiedAmount < limits[1] && specifiedAmount > 0); @@ -102,8 +105,9 @@ contract AngleAdapterTest is Test, ISwapAdapterTypes { 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); + Trade memory trade = adapter.swap( + pair, address(agEUR), address(EURC), side, specifiedAmount + ); if (trade.calculatedAmount > 0) { if (side == OrderSide.Buy) { @@ -154,7 +158,9 @@ contract AngleAdapterTest is Test, ISwapAdapterTypes { 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]); + trades[i] = adapter.swap( + pair, address(agEUR), address(EURC), side, amounts[i] + ); vm.revertTo(beforeSwap); } @@ -173,8 +179,7 @@ contract AngleAdapterTest is Test, ISwapAdapterTypes { function testGetCapabilitiesAngle(bytes32 pair, address t0, address t1) public { - Capability[] memory res = - adapter.getCapabilities(pair, t0, t1); + Capability[] memory res = adapter.getCapabilities(pair, t0, t1); assertEq(res.length, 2); } @@ -187,7 +192,8 @@ contract AngleAdapterTest is Test, ISwapAdapterTypes { function testGetLimitsAngle() public { bytes32 pair = bytes32(0); - uint256[] memory limits = adapter.getLimits(pair, address(agEUR), address(EURC)); + uint256[] memory limits = + adapter.getLimits(pair, address(agEUR), address(EURC)); assertEq(limits.length, 2); } diff --git a/evm/test/EtherfiAdapter.t.sol b/evm/test/EtherfiAdapter.t.sol index 8840c66..c96fa28 100644 --- a/evm/test/EtherfiAdapter.t.sol +++ b/evm/test/EtherfiAdapter.t.sol @@ -58,7 +58,8 @@ contract EtherfiAdapterTest is Test, ISwapAdapterTypes { IERC20 eEth_ = IERC20(address(eEth)); IERC20 weEth_ = IERC20(address(weEth)); bytes32 pair = bytes32(0); - uint256[] memory limits = adapter.getLimits(pair, address(eEth_), address(weEth_)); + uint256[] memory limits = + adapter.getLimits(pair, address(eEth_), address(weEth_)); if (side == OrderSide.Buy) { vm.assume(specifiedAmount < limits[1] && specifiedAmount > 100); @@ -67,7 +68,11 @@ contract EtherfiAdapterTest is Test, ISwapAdapterTypes { /// work(balance is shares) deal(address(adapter), type(uint256).max); adapter.swap( - pair, address(address(0)), address(eEth_), OrderSide.Buy, limits[0] + pair, + address(address(0)), + address(eEth_), + OrderSide.Buy, + limits[0] ); eEth_.approve(address(adapter), type(uint256).max); @@ -78,7 +83,11 @@ contract EtherfiAdapterTest is Test, ISwapAdapterTypes { /// work(balance is shares) deal(address(adapter), type(uint128).max); adapter.swap( - pair, address(address(0)), address(eEth_), OrderSide.Buy, specifiedAmount + pair, + address(address(0)), + address(eEth_), + OrderSide.Buy, + specifiedAmount ); eEth_.approve(address(adapter), specifiedAmount); @@ -87,8 +96,9 @@ contract EtherfiAdapterTest is Test, ISwapAdapterTypes { 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); + Trade memory trade = adapter.swap( + pair, address(eEth_), address(weEth_), side, specifiedAmount + ); if (trade.calculatedAmount > 0) { if (side == OrderSide.Buy) { @@ -140,7 +150,8 @@ contract EtherfiAdapterTest is Test, ISwapAdapterTypes { 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_)); + uint256[] memory limits = + adapter.getLimits(pair, address(weEth_), address(eEth_)); if (side == OrderSide.Buy) { vm.assume(specifiedAmount < limits[1] && specifiedAmount > 100); @@ -149,7 +160,11 @@ contract EtherfiAdapterTest is Test, ISwapAdapterTypes { /// work(balance is shares) deal(address(adapter), type(uint256).max); adapter.swap( - pair, address(address(0)), address(weEth_), OrderSide.Buy, limits[0] + pair, + address(address(0)), + address(weEth_), + OrderSide.Buy, + limits[0] ); weEth_.approve(address(adapter), type(uint256).max); @@ -160,7 +175,11 @@ contract EtherfiAdapterTest is Test, ISwapAdapterTypes { /// work(balance is shares) deal(address(adapter), type(uint128).max); adapter.swap( - pair, address(address(0)), address(weEth_), OrderSide.Buy, specifiedAmount + pair, + address(address(0)), + address(weEth_), + OrderSide.Buy, + specifiedAmount ); weEth_.approve(address(adapter), specifiedAmount); @@ -174,8 +193,9 @@ contract EtherfiAdapterTest is Test, ISwapAdapterTypes { /// 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_); + Trade memory trade = adapter.swap( + pair, address(weEth_), address(eEth_), side, realAmountWeEth_ + ); if (trade.calculatedAmount > 0) { if (side == OrderSide.Buy) { @@ -386,9 +406,8 @@ contract EtherfiAdapterTest is Test, ISwapAdapterTypes { function testGetLimitsEtherfi() public { bytes32 pair = bytes32(0); - uint256[] memory limits = adapter.getLimits( - pair, address(eEth), address(weEth) - ); + uint256[] memory limits = + adapter.getLimits(pair, address(eEth), address(weEth)); assertEq(limits.length, 2); } From 1b2e183a851b0a4af7f33ca330ad6d5dd9d8cf28 Mon Sep 17 00:00:00 2001 From: pistomat Date: Sat, 13 Apr 2024 11:17:03 +0000 Subject: [PATCH 46/50] Remove test console logs --- evm/test/EtherfiAdapter.t.sol | 4 ---- 1 file changed, 4 deletions(-) diff --git a/evm/test/EtherfiAdapter.t.sol b/evm/test/EtherfiAdapter.t.sol index c96fa28..366561a 100644 --- a/evm/test/EtherfiAdapter.t.sol +++ b/evm/test/EtherfiAdapter.t.sol @@ -380,10 +380,6 @@ contract EtherfiAdapterTest is Test, ISwapAdapterTypes { for (uint256 i = 1; i < TEST_ITERATIONS - 1; i++) { assertLe(trades[i].calculatedAmount, trades[i + 1].calculatedAmount); - console.log( - "Prices", trades[i].price.numerator, trades[i].price.denominator - ); - console.log("Amounts", trades[i].calculatedAmount, amounts[i]); assertLe(trades[i].gasUsed, trades[i + 1].gasUsed); } } From 98857be30501c10be8939b6f025ce4f5b9dd3fa5 Mon Sep 17 00:00:00 2001 From: Louise Poole Date: Mon, 15 Apr 2024 16:18:48 +0100 Subject: [PATCH 47/50] Fix balancer price and capabilities --- evm/lib/forge-std | 2 +- evm/lib/openzeppelin-contracts | 2 +- evm/src/balancer-v2/BalancerV2SwapAdapter.sol | 12 ++---------- evm/src/interfaces/ISwapAdapter.sol | 2 +- 4 files changed, 5 insertions(+), 13 deletions(-) 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/balancer-v2/BalancerV2SwapAdapter.sol b/evm/src/balancer-v2/BalancerV2SwapAdapter.sol index 13b2f2d..2b5f149 100644 --- a/evm/src/balancer-v2/BalancerV2SwapAdapter.sol +++ b/evm/src/balancer-v2/BalancerV2SwapAdapter.sol @@ -100,7 +100,7 @@ contract BalancerV2SwapAdapter is ISwapAdapter { sellAmount = uint256(assetDeltas[0]); } - function priceBatch( + function price( bytes32 poolId, address sellToken, address buyToken, @@ -112,15 +112,6 @@ contract BalancerV2SwapAdapter is ISwapAdapter { } } - function price(bytes32, address, address, uint256[] memory) - external - pure - override - returns (Fraction[] memory) - { - revert NotImplemented("BalancerV2SwapAdapter.price"); - } - function swap( bytes32 poolId, address sellToken, @@ -199,6 +190,7 @@ contract BalancerV2SwapAdapter is ISwapAdapter { capabilities = new Capability[](2); capabilities[0] = Capability.SellOrder; capabilities[1] = Capability.BuyOrder; + capabilities[2] = Capability.PriceFunction; } function getTokens(bytes32 poolId) diff --git a/evm/src/interfaces/ISwapAdapter.sol b/evm/src/interfaces/ISwapAdapter.sol index 50fed77..ab3bacc 100644 --- a/evm/src/interfaces/ISwapAdapter.sol +++ b/evm/src/interfaces/ISwapAdapter.sol @@ -37,7 +37,7 @@ interface ISwapAdapter is ISwapAdapterTypes { 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. From 306293764cb9aee25189ef26b2571d580865a10f Mon Sep 17 00:00:00 2001 From: Louise Poole Date: Mon, 15 Apr 2024 16:56:19 +0100 Subject: [PATCH 48/50] Fix tests --- evm/src/balancer-v2/BalancerV2SwapAdapter.sol | 2 +- evm/test/BalancerV2SwapAdapter.t.sol | 20 ++++++++++--------- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/evm/src/balancer-v2/BalancerV2SwapAdapter.sol b/evm/src/balancer-v2/BalancerV2SwapAdapter.sol index 2b5f149..040aba9 100644 --- a/evm/src/balancer-v2/BalancerV2SwapAdapter.sol +++ b/evm/src/balancer-v2/BalancerV2SwapAdapter.sol @@ -187,7 +187,7 @@ contract BalancerV2SwapAdapter is ISwapAdapter { 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; diff --git a/evm/test/BalancerV2SwapAdapter.t.sol b/evm/test/BalancerV2SwapAdapter.t.sol index 06ccfe0..a336af8 100644 --- a/evm/test/BalancerV2SwapAdapter.t.sol +++ b/evm/test/BalancerV2SwapAdapter.t.sol @@ -40,14 +40,15 @@ contract BalancerV2SwapAdapterTest is Test, ISwapAdapterTypes { 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] = amount0; + amounts[1] = amount1; + + 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 { @@ -206,9 +207,10 @@ contract BalancerV2SwapAdapterTest is Test, ISwapAdapterTypes { { 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 { From 45dc7aa45778c9301be90f2859ea79a724d0df79 Mon Sep 17 00:00:00 2001 From: Louise Poole Date: Mon, 15 Apr 2024 17:41:16 +0100 Subject: [PATCH 49/50] Fix prices function --- evm/src/balancer-v2/BalancerV2SwapAdapter.sol | 1 + evm/test/BalancerV2SwapAdapter.t.sol | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/evm/src/balancer-v2/BalancerV2SwapAdapter.sol b/evm/src/balancer-v2/BalancerV2SwapAdapter.sol index 040aba9..e93702b 100644 --- a/evm/src/balancer-v2/BalancerV2SwapAdapter.sol +++ b/evm/src/balancer-v2/BalancerV2SwapAdapter.sol @@ -106,6 +106,7 @@ contract BalancerV2SwapAdapter is ISwapAdapter { 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]); diff --git a/evm/test/BalancerV2SwapAdapter.t.sol b/evm/test/BalancerV2SwapAdapter.t.sol index a336af8..3be35a5 100644 --- a/evm/test/BalancerV2SwapAdapter.t.sol +++ b/evm/test/BalancerV2SwapAdapter.t.sol @@ -40,8 +40,8 @@ contract BalancerV2SwapAdapterTest is Test, ISwapAdapterTypes { function testPrice() public { uint256[] memory amounts = new uint256[](2); - amounts[0] = amount0; - amounts[1] = amount1; + amounts[0] = 1e18; + amounts[1] = 2e18; Fraction[] memory prices = adapter.price(B_80BAL_20WETH_POOL_ID, BAL, WETH, amounts); From cf7516df254e7498c0e712463fd16b5c3fe8787a Mon Sep 17 00:00:00 2001 From: Louise Poole Date: Mon, 15 Apr 2024 17:42:30 +0100 Subject: [PATCH 50/50] Format --- evm/test/BalancerV2SwapAdapter.t.sol | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/evm/test/BalancerV2SwapAdapter.t.sol b/evm/test/BalancerV2SwapAdapter.t.sol index 3be35a5..20f4f8d 100644 --- a/evm/test/BalancerV2SwapAdapter.t.sol +++ b/evm/test/BalancerV2SwapAdapter.t.sol @@ -43,7 +43,8 @@ contract BalancerV2SwapAdapterTest is Test, ISwapAdapterTypes { amounts[0] = 1e18; amounts[1] = 2e18; - Fraction[] memory prices = adapter.price(B_80BAL_20WETH_POOL_ID, BAL, WETH, amounts); + 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);