diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..7af4ee0 --- /dev/null +++ b/.env.example @@ -0,0 +1,11 @@ +# Private key to interact with contracts using scripts inside /script +WALLET_PRIVATE_KEY=0xa26.. +# RPC +MAINNET_RPC_URL="https://" +ARBITRUM_RPC_URL="https://" +BASE_RPC_URL="https://" + +# Explorer API Key +ETHERSCAN_MAINNET_API_KEY=adada +ETHERSCAN_ARBITRUM_API_KEY=adada +ETHERSCAN_BASE_API_KEY=adada \ No newline at end of file diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 762a296..27666da 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -16,7 +16,7 @@ jobs: name: Foundry project runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v3 with: submodules: recursive @@ -36,7 +36,7 @@ jobs: - name: Run Forge build run: | - forge build --sizes + forge build --force --skip test --sizes id: build - name: Run Forge tests diff --git a/.gitignore b/.gitignore index 85198aa..29fa0d2 100644 --- a/.gitignore +++ b/.gitignore @@ -6,9 +6,15 @@ out/ !/broadcast /broadcast/*/31337/ /broadcast/**/dry-run/ - -# Docs -docs/ +broadcast/ +script/json/out/ # Dotenv file .env + +# Coverage file +lcov.info + +# MAC files +.DS_Store +__MACOSX \ No newline at end of file diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..86c64a9 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,15 @@ +[submodule "lib/forge-std"] + path = lib/forge-std + url = https://github.com/foundry-rs/forge-std +[submodule "lib/openzeppelin-contracts"] + path = lib/openzeppelin-contracts + url = https://github.com/OpenZeppelin/openzeppelin-contracts +[submodule "lib/euler-vault-kit"] + path = lib/euler-vault-kit + url = https://github.com/euler-xyz/euler-vault-kit +[submodule "lib/ethereum-vault-connector"] + path = lib/ethereum-vault-connector + url = https://github.com/euler-xyz/ethereum-vault-connector +[submodule "lib/v4-periphery"] + path = lib/v4-periphery + url = https://github.com/uniswap/v4-periphery diff --git a/README.md b/README.md index 9265b45..0c6d28f 100644 --- a/README.md +++ b/README.md @@ -1,66 +1,89 @@ -## Foundry +# EulerSwap -**Foundry is a blazing fast, portable and modular toolkit for Ethereum application development written in Rust.** +EulerSwap is an automated market maker (AMM) that integrates with Euler [credit vaults](https://docs.euler.finance/euler-vault-kit-white-paper/) to provide deeper liquidity for swaps. When a user initiates a swap, a smart contract called an EulerSwap operator borrows the required output token using the input token as collateral. This model enables up to 40x the liquidity depth of traditional AMMs by making idle assets in Euler more efficient. Unlike traditional AMMs, which often fragment liquidity across multiple pools, EulerSwap further increases capital efficiency by allowing a single, cross-collateralised credit vault to support multiple asset pairs at once. At its core, EulerSwap uses a flexible AMM curve to optimise swap pricing, ensuring deep liquidity while maintaining market balance. By combining just-in-time liquidity, shared liquidity across pools, and customisable AMM mechanics, EulerSwap reduces inefficiencies in liquidity provision, offering deeper markets, lower costs, and greater control for liquidity providers. -Foundry consists of: +For more information, refer to the [white paper](./docs/whitepaper/EulerSwap_White_Paper.pdf). -- **Forge**: Ethereum testing framework (like Truffle, Hardhat and DappTools). -- **Cast**: Swiss army knife for interacting with EVM smart contracts, sending transactions and getting chain data. -- **Anvil**: Local Ethereum node, akin to Ganache, Hardhat Network. -- **Chisel**: Fast, utilitarian, and verbose solidity REPL. +## Usage -## Documentation +EulerSwap comes with a comprehensive set of tests written in Solidity, which can be executed using Foundry. -https://book.getfoundry.sh/ +To install Foundry: -## Usage +```sh +curl -L https://foundry.paradigm.xyz | bash +``` -### Build +This will download foundryup. To start Foundry, run: -```shell -$ forge build +```sh +foundryup ``` -### Test +To clone the repo: -```shell -$ forge test +```sh +git clone https://github.com/euler-xyz/euler-swap.git && cd euler-swap ``` -### Format +## Testing -```shell -$ forge fmt -``` +### in `default` mode -### Gas Snapshots +To run the tests in a `default` mode: -```shell -$ forge snapshot +```sh +forge test ``` -### Anvil +### in `coverage` mode -```shell -$ anvil +```sh +forge coverage ``` -### Deploy +## Smart Contracts Documentation -```shell -$ forge script script/Counter.s.sol:CounterScript --rpc-url --private-key +```sh +forge doc --serve --port 4000 ``` -### Cast +## Private Deployment -```shell -$ cast -``` +- EulerSwapFactory: 0x04C54FF83e4BC428FD1eDA2f41cdBd583A2e9cF8 +- EulerSwapPeriphery: 0x64A8410D7D2ecF3Aaf32b6C3932e4586f3C42ecE -### Help +------ -```shell -$ forge --help -$ anvil --help -$ cast --help -``` +- EulerSwapFactory: 0xF75548aF02f1928CbE9015985D4Fcbf96d728544 +- EulerSwapPeriphery: 0x813D74E832b3d9E9451d8f0E871E877edf2a5A5f +- USDT-USDT pool: 0x2bFED8dBEb8e6226a15300AC77eE9130E52410fE + +------ with Uniswap hook contracts (latest) + +- EulerSwap implementation: 0x0B8CD42911551882638f4C762A66570e1fAc624f +- EulerSwapFactory: 0x52177559e6430396b9A7E2176Ef33b4e4052D125 +- EulerSwapPeriphery: 0x9F27Bc363DB128cdC349CA54671E6Fbe2bE194D0 +- USDT-USDT pool: 0x13f627635CD96a2A75c2efBDba979172cAb2E888 + +## Safety + +This software is experimental and is provided "as is" and "as available". + +No warranties are provided and no liability will be accepted for any loss incurred through the use of this codebase. + +Always include thorough tests when using EulerSwap to ensure it interacts correctly with your code. + +## Known limitations + +Refer to the [white paper](./docs/whitepaper/EulerSwap_White_Paper.pdf) for a list of known limitations and security considerations. + +## Contributing + +The code is currently in an experimental phase. Feedback or ideas for improving EulerSwap are appreciated. Contributions are welcome from anyone interested in conducting security research, writing more tests including formal verification, improving readability and documentation, optimizing, simplifying, or developing integrations. + +## License + +(c) 2024-2025 Euler Labs Ltd. + +All rights reserved. diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..d38e22b --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,25 @@ +# Euler Security Policy + +## Vulnerability Disclosure and Bug Bounty + +Security is a top priority at Euler, and we engage in regular security reviews and have an active bug bounty program to ensure the integrity of our systems. + +To report a vulnerability, **please submit it through our bug bounty program**: +[Euler Bug Bounty](https://euler.finance/bug-bounty) + +**Reports sent via email will not be accepted.** Email should only be used for general security inquiries. + +## Security Team Contact Details + +For security-related questions or inquiries (not vulnerability reports), you can contact us via: +- **Email**: [security@euler.xyz](mailto:security@euler.xyz) +- **PGP Encryption**: [Euler Public Key](https://euler.finance/.well-known/public-key.asc) + +## Previous Security Reviews + +Euler undergoes regular security audits. You can find details of previous security reviews here: +[Euler Security Reviews](https://docs.euler.finance/security/security-reviews) + +## Preferred Languages + +We accept security-related inquiries in **English (en)** diff --git a/TODO b/TODO new file mode 100644 index 0000000..f87274b --- /dev/null +++ b/TODO @@ -0,0 +1,19 @@ +? tighten up getLimits bounds + +TESTING + +* when exchange rate in vaults != 1 +* better coverage of swaps done via hooks + +IDEAS + +* Currently we have only been supporting stable-stable pairs + * What extra considerations would there be for floating pairs? +* Automatically re-invest fees? There are a few options: + * Don't do anything: Re-deploying probably isn't a huge deal + * Increase the reserves by the fee amount + * Increase the reserves by the extra amount of possible leverage supported by the new fee + * Apply fees to a super-concentrated middle section of the curve (needs R&D) +* Could current reserves be calculated dynamically based on balances/debts/debt limits? + * I guess you would lose a chunk of interest to arbitrage + * Donation attacks? diff --git a/audits/eulerswap-audit-report.pdf b/audits/eulerswap-audit-report.pdf new file mode 100644 index 0000000..486f1f7 Binary files /dev/null and b/audits/eulerswap-audit-report.pdf differ diff --git a/docs/architecture.md b/docs/architecture.md new file mode 100644 index 0000000..8aefb20 --- /dev/null +++ b/docs/architecture.md @@ -0,0 +1,116 @@ +# EulerSwap architecture + +## Overview + +EulerSwap is an automated market maker (AMM) that integrates with Euler credit vaults to provide deeper liquidity for swaps. + +Each EulerSwap instance is a lightweight smart contract that functions as an [EVC operator](https://evc.wtf/docs/whitepaper/#operators) while implementing a highly customizable AMM curve to determine swap output amounts. + +When a user initiates a swap, the EulerSwap operator borrows the required output token using the input token as collateral. The operator’s internal AMM curve governs the exchange rate, ensuring deep liquidity over short timeframes while maintaining a balance between collateral and debt over the long term. + +Swapping can be performed by invoking the EulerSwap instance, either through a Uniswap2-compatible `swap()` function or as a [Uniswap4 hook](https://docs.uniswap.org/contracts/v4/concepts/hooks). + +## Code structure + +EulerSwap is split into the following main contracts: + +* `EulerSwap`: Contract that is installed as an EVC operator by liquidity providers, and is also invoked by swappers in order to execute a swap. + * `UniswapHook`: The functions required so that the EulerSwap instance can function as a Uniswap4 hook. +* `EulerSwapFactory`: Factory contract for creating `EulerSwap` instances and for querying existing instances. +* `EulerSwapPeriphery`: This is a wrapper contract for quoting and performing swaps, while handling approvals, slippage, etc. + +The above contracts depend on libraries: + +* `CtxLib`: Allows access to the `EulerSwap` context: Structured storage and the instance parameters +* `FundsLib`: Moving tokens: approvals and transfers in/out +* `CurveLib`: Mathematical routines for calculating the EulerSwap curve +* `QuoteLib`: Computing quotes. This involves invoking the logic from `CurveLib`, as well as taking into account other limitations such as vault utilisation, supply caps, etc. + +And some utilities: + +* `MetaProxyDeployer`: Deploys EIP-3448-style proxies. +* `ProtocolFee`: The factory stores protocol fee parameters that will affect subsequently created `EulerSwap` instances. These can be changed by an owner. + +## Operational flow + +The following steps outline how an EulerSwap operator is created and configured: + +1. Deposit initial liquidity into one or both of the underlying credit vaults to enable swaps. +1. Choose the desired pool parameters (`IEulerSwap.Params` struct). The `protocolFee` and `protocolFeeRecipient` must be read from the factory. +1. [Mine](https://docs.uniswap.org/contracts/v4/guides/hooks/hook-deployment#hook-miner) a salt such that the predicted address of the `EulerSwap` instance will be deployed with the correct flags. +1. Install the above address as an EVC operator, ensuring that any previous `EulerSwap` operators are uninstalled. +1. Invoke `deployPool()` on the EulerSwap factory. + +## Metaproxies + +Each `EulerSwap` instance is a lightweight proxy, roughly modelled after [EIP-3448](https://eips.ethereum.org/EIPS/eip-3448). The only difference is that EIP-3448 appends the length of the metadata, whereas we don't, since it is a fixed size. + +When an `EulerSwap` instance is created, the `IEulerSwap.Params` struct is ABI encoded and provided as the proxy metadata. This is provided to the implementation contract as trailing calldata via `delegatecall`. This allows the parameters to be accessed cheaply when servicing a swap, compared to if they had to be read from storage. + +## Curve Parameters + +Traditional AMMs hold dedicated reserves of each of the supported tokens, which inherently limit the sizes of swaps that can be serviced. For example, if an AMM has 100 units of a token available, there is no possible price that can convince it to send more than 100 units. + +Since EulerSwap does not have dedicated reserves, its swapping limits must be defined in another way. This is accomplished by having the EulerSwap operator define an abstract curve. The domain of this curve defines the swap limits, which can be considered the virtual reserves. + +The abstract curve is centred on an *equilibrium point*. This is parameterised by two equilibrium reserves values. These specify the magnitude of the virtual reserves, and are effectively hard limits on the supported swap sizes. They are often equal, but do not necessarily have to be (for instance, if the two vaults have asymmetric LTVs). + +At the equilibrium point, the marginal swap price is defined by the ratio of two parameters `priceX` and `priceY`. Generally operators will choose the price ratio at equilibrium to be the asset's pegged price, or the wider market price. The prices should also compensate for a difference in token decimals, if any. + +Finally, the curve is parameterised by two **concentration factors** between `0` and `1`. Each corresponds to the portion of the curve to the left or right of the equilibrium point. These factors control the shape of each side of the curve (to the left of the equilibrium point, and to the right). These parameters change the curve shape according to a blend of constant product and constant sum. The closer to `0` the more the curve resembles a constant product, and the closer to `1`, constant sum. + +In most cases (except with concentration factor of `1`), virtual reserves can never be fully depleted. The limits can only be approached asymptotically. + +Generally it is expected that arbitrage will favour returning the reserves to the equilbrium point. The price and the convex constant-product-like curve shape encourages this. If the price at equilibrium is accurate then the equilbrium point always represents the point of minimum NAV for the operator, and this point is arbitrage-free. + +## Initial State + +The curve as parameterised above is an abstract geometric shape. In order to actually make use of it, you must install it on an account that already has some existing conditions. For example, it may already have a borrow, or it may have unequal deposits in the two vaults. + +To be as flexible as possible, EulerSwap allows you to specify the **current reserves** when you are instantiating a pool. + +If the current state of the account is where you wish the equilbrium point to be, then you should make the current reserves the same as the equilibrium reserves. Otherwise, the current reserves can be offset (take from one side and give to the other) to specify a new equilibrium point that swapping activity should take you to. + +Note that there may be a race condition when removing one swap operator and installing another. In between when you've calculated the current reserves and when you've actually created and installed the new operator, a swap may occur that modifies the account state. To avoid this, a wrapper contract should be used that calculates the current reserves. Or, more simply, just verifies that the account state was as observed by the operator and otherwise reverts. + + +## Fees + +Swapping fees are charged by requiring the swapper to pay slightly more of the input token than is required by the curve parameters. This extra amount is simply directly deposited into the vaults on behalf of the EulerSwap account. This means that it has the effect of increasing the account's NAV, but does not change the shape of the curve itself. The curve is always static, per EulerSwap instance. + +When an EulerSwap instance is created, a **protocol fee** parameter may be installed by the factory. This portion of the collected fees are routed to a protocol fee recipient. An administrator of the factory can change the the protocol fee and recipient for future created EulerSwap instances, although previously created instances will not be updated retroactively. + + +## Reserve desynchronisation + +The EulerSwap contract tracks the current reserves in storage. After a swap, the amount of received tokens is added to the current reserves, and the amount of sent tokens subtracted. Since the reserves are not allowed to go negative, this implies a hard limit on the swap sizes. + +While these reserves track the state of the world as influenced by swaps, they can get out-of-sync with the actual account for various reasons: + +* Interest can be accrued, either increasing or decreasing the account's NAV. +* Swap fees are not tracked, and instead increase the account's NAV. +* The account could be liquidated. +* The account owner could manually add or remove funds, repay loans, etc. + +In order to correct any desynchronisation, the EulerSwap operator should be uninstalled and a new, updated one installed instead. + + +## getLimits + +Although the virtual reserves specify a hard limit for swaps, there may be other implicit limits that are even lower: + +* The vaults have high utilisation and cannot service large borrows or withdrawals +* The vaults have supply and/or borrow caps +* The operator may have been uninstalled + +There is a function `getLimits` that can take these into account. This function itself is an upper-bound and the values it returns may not be swappable either, in particular if the curve shape does not allow it. However, it makes a best effort and this function can be used to rapidly exclude pools that are definitely unable to service a given size swap. + + +## Swapper Security + +When swapping with an EulerSwap instance, users should always make sure that they received the desired amount of output tokens in one of two ways: + +* Actually checking your output token balances before and after and making sure they increased by an amount to satisfy slippage. +* Ensure that the EulerSwap code is a trusted instance that will send the specified output amount or revert if not possible. This can be done by making sure an instance was created by a trusted factory. + +In particular, note that the periphery does not perform either of these checks, so if you use the periphery for swapping, you should ensure that you only interact with EulerSwap instances created by a known-good factory. diff --git a/docs/audits/EulerSwapHook_Audit_Scope.md b/docs/audits/EulerSwapHook_Audit_Scope.md new file mode 100644 index 0000000..adbc546 --- /dev/null +++ b/docs/audits/EulerSwapHook_Audit_Scope.md @@ -0,0 +1,61 @@ +# EulerSwapHook Audit + +Through a new partnership between Euler Labs and Uniswap Foundation, the teams intend to expose EulerSwap's core logic and mechanisms via a Uniswap v4 Hook interface. + +This is primarily done by inheriting `UniswapHook.sol:UniswapHook`, i.e. `EulerSwap is UniswapHook, ...`, and implementing a "custom curve" via `beforeSwap`. The implementation will allow integrators, interfaces, and aggregators, to trade on EulerSwap as-if it is any other Uniswap v4 Pool + +```solidity +// assuming the EulerSwapHook was instantiated via EulerSwapFactory +PoolKey memory poolKey = PoolKey({ + currency0: currency0, + currency1: currency1, + fee: fee, + tickSpacing: 1, + hooks: IHooks(address(eulerSwapHook)) +}); + +minimalRouter.swap(poolKey, zeroForOne, amountIn, 0); +``` + + +## Audit Scope + +The scope of audit involves a re-audit of EulerSwap, primarily `src/`: + +``` +├── src +│ ├── CtxLib.sol +│ ├── CurveLib.sol +│ ├── EulerSwap.sol +│ ├── EulerSwapFactory.sol +│ ├── EulerSwapPeriphery.sol +│ ├── FundsLib.sol +│ ├── MetaProxyDeployer.sol +│ ├── QuoteLib.sol +│ ├── UniswapHook.sol +``` + +> The interfaces are out of scope + +## Notable Changes since the prior audit: + +* Introduction of Uniswap v4 Hook logic +* Addition of a protocol fee +* Refactoring EulerSwap instances to delegate call into an implementation contract +* Replaced binary-search quoting, with a closed formula `fInverse()` + +## Known Caveats + +### Prepaid Inputs + +Due to technical requirements, EulerSwapHook must take the input token from PoolManager and deposit it into Euler Vaults. It will appear that EulerSwapHook can only support input sizes of `IERC20.balanceOf(PoolManager)`. However swap routers can pre-emptively send input tokens (from user wallet to PoolManager) prior to calling `poolManager.swap` to get around this limitation. + +An example `test/utils/MinimalRouter.sol` is provided as an example. + +### Invalidated Salts + +Uniswap v4 Hooks encode their behaviors within the address, requiring deployers to mine salts for a particular address pattern. Because constructor arguments influence the precomputed address during the salt-finding process, governance may accidentally invalidate a discovered salt by updating the protocol fee. + +The EulerSwapFactory passes a protocol fee and protocol fee recipient to a EulerSwap instance (hook). If governance were modify either values between salt-discovery and EulerSwap deployment, the deployment would fail. + +This scenario is unlikely to happen as we do not expect protocol fee parameters to change; as well, governance can pre-emptively warn deployers of the parameter change. diff --git a/docs/boundary-analysis.md b/docs/boundary-analysis.md new file mode 100644 index 0000000..45a27ed --- /dev/null +++ b/docs/boundary-analysis.md @@ -0,0 +1,138 @@ +# Boundary analysis + +## Introduction + +The EulerSwap automated market maker (AMM) curve is governed by two key functions: f() and fInverse(). These functions are critical to maintaining protocol invariants and ensuring accurate swap calculations within the AMM. This document provides a detailed boundary analysis of both functions, assessing their Solidity implementations against the equations in the white paper. It ensures that appropriate safety measures are in place to avoid overflow, underflow, and precision loss, and that unchecked operations are thoroughly justified. + +## Implementation of function `f()` + +The `f()` function is part of the EulerSwap core, defined in `EulerSwap.sol`, and corresponds to equation (2) in the EulerSwap white paper. The `f()` function is a parameterisable curve in the `EulerSwap` contract that defines the permissible boundary for points in EulerSwap AMMs. The curve allows points on or above and to the right of the curve while restricting others. Its primary purpose is to act as an invariant validator by checking if a hypothetical state `(x, y)` within the AMM is valid. It also calculates swap output amounts for given inputs, though some swap scenarios require `fInverse()`. + +### Derivation + +This derivation shows how to implement the `f()` function in Solidity, starting from the theoretical model described in the EulerSwap white paper. The initial equation from the EulerSwap white paper is: + +\[ +y_0 + \left(\frac{p_x}{p_y}\right) (x_0 - x) \left(c + (1 - c) \frac{x_0}{x}\right) +\] + +Multiply the second term by \(\frac{x}{x}\) and scale `c` by \(1e18\): + +\[ +y_0 + \left(\frac{p_x}{p_y}\right) (x_0 - x) \frac{(c \cdot x) + (1e18 - c) \cdot x_0}{x \cdot 1e18} +\] + +Reorder division by \(p_y\) to prepare for Solidity implementation: + +\[ +y_0 + p_x \cdot (x_0 - x) \cdot \frac{(c \cdot x) + (1e18 - c) \cdot x_0}{x \cdot 1e18} \cdot \frac{1}{p_y} +\] + +To avoid intermediate overflow, use `Math.mulDiv` in Solidity, which combines multiplication and division safely: + +\[ +y_0 + \frac{\text{Math.mulDiv}(p_x \cdot (x_0 - x), c \cdot x + (1e18 - c) \cdot x_0, x \cdot 1e18)}{p_y} +\] + +Applying ceiling rounding with `Math.Rounding.Ceil` ensures accuracy: + +\[ +y_0 + \left(\text{Math.mulDiv}(p_x \cdot (x_0 - x), c \cdot x + (1e18 - c) \cdot x_0, x \cdot 1e18, \text{Math.Rounding.Ceil}) + (p_y - 1)\right) / p_y +\] + +Adding `(p_y - 1)` ensures proper ceiling rounding by making sure the result is rounded up when the numerator is not perfectly divisible by `p_y`. + +### Boundary analysis + +#### Pre-conditions + +- \(x \leq x_0\) +- \(1e18 \leq p_x, p_y \leq 1e36\) (60 to 120 bits) +- \(1 \leq x_0, y_0 \leq 2^{112} - 1 \approx 5.19e33\) (0 to 112 bits) +- \(1 < c \leq 1e18\) (0 to 60 bits) + +#### Step-by-step + +The arguments to `mulDiv` are safe from overflow: + +- **Arg 1:** `px * (x0 - x)` ≤ `1e36 * (2**112 - 1)` ≈ 232 bits +- **Arg 2:** `c * x + (1e18 - c) * x0` ≤ `1e18 * (2**112 - 1) * 2` ≈ 173 bits +- **Arg 3:** `x * 1e18` ≤ `1e18 * (2**112 - 1)` ≈ 172 bits + +If `mulDiv` or the addition with `y0` overflows, the result would exceed `type(uint112).max`. When `mulDiv` overflows, its result would be > `2**256 - 1`. Dividing by `py` (`1e36` max) gives ~`2**136`, which exceeds the `2**112 - 1` limit, meaning these results are invalid as they cannot be satisfied by any swapper. + +#### Unchecked math considerations + +The arguments to `mulDiv` are protected from overflow as demonstrated above. The `mulDiv` output is further limited to `2**248 - 1` to prevent overflow in subsequent operations: + +```solidity +unchecked { + uint256 v = Math.mulDiv(px * (x0 - x), c * x + (1e18 - c) * x0, x * 1e18, Math.Rounding.Ceil); + require(v <= type(uint248).max, Overflow()); + return y0 + (v + (py - 1)) / py; +} +``` + +This does not introduce additional failure cases. Even values between `2**248 - 1` and `2**256 - 1` would not reduce to `2**112 - 1`, aligning with the boundary analysis. + +## Implementation of function `fInverse()` + +The `fInverse()` function, defined in `EulerSwapPeriphery.sol`, is part of the periphery because it is not required as an invariant. Instead, its sole purpose is to facilitate specific swap input and output calculations that cannot be managed by `f()`. This function maps to equation (22) in the Appendix of the EulerSwap white paper. + +### Boundary analysis + +#### Pre-conditions + +- \(y > y_0\) +- \(1e18 \leq p_x, p_y \leq 1e36\) (60 to 120 bits) +- \(1 \leq x_0, y_0 \leq 2^{112} - 1 \approx 5.19e33\) (0 to 112 bits) +- \(1 < c \leq 1e18\) (0 to 60 bits) + +#### Step-by-step + +1. **A component (`A = 2 * c`)** + + - Since `c <= 1e18`, `A = 2 * c <= 2e18`, well within `uint256` capacity (max `2**256 - 1`). + +2. **B component calculation** + + - `B = int256((px * (y - y0) + py - 1) / py) - int256((x0 * (2 * c - 1e18) + 1e18 - 1) / 1e18)` + - The first term is bounded by `(px * (y - y0)) / py`, where `px, py <= 1e36` and `(y - y0) <= 2**112 - 1`. + - The second term scales `x0` with `(2 * c - 1e18)`, keeping the result well within the `int256` bounds due to controlled arithmetic and the limits on `c` and `x0`. + +3. **Absolute value and B² computation** + + - `absB = B < 0 ? uint256(-B) : uint256(B)` + - `squaredB = Math.mulDiv(absB, absB, 1e18, Math.Rounding.Ceil)` + - As `absB` is derived from `B`, and `B` is bounded, `squaredB` remains within a safe range. + +4. **4AC Component (`AC4 = AC4a * AC4b / 1e18`)** + + - `AC4a = Math.mulDiv(4 * c, (1e18 - c), 1e18, Math.Rounding.Ceil)` + - `4 * c * (1e18 - c)` has a maximum of `1e18 * 1e18 = 1e36`, divided by `1e18`, the result ≤ `1e18`. + - `AC4b = Math.mulDiv(x0, x0, 1e18, Math.Rounding.Ceil)` + - The maximum value of `x0 * x0` is `(2**112 - 1)² ≈ 2**224`, safely within the `uint256` range. + +5. **Discriminant calculation** + + - `discriminant = (squaredB + AC4) * 1e18` + - Since both `squaredB` and `AC4` are bounded by `uint256`, multiplying by `1e18` does not cause overflow. + +6. **Square root computation and adjustment** + + - `uint256 sqrt = Math.sqrt(discriminant)` + - The square root of a `uint256` value is always within `uint128`, making this operation safe. + - Adjustment step `sqrt = (sqrt * sqrt < discriminant) ? sqrt + 1 : sqrt` maintains precision without overflow. + +7. **Final computation of `x`** + - `Math.mulDiv(uint256(int256(sqrt) - B), 1e18, A, Math.Rounding.Ceil)` + - The subtraction and multiplication are controlled by previous bounds, ensuring no overflow. + - Division by `A` is safe as `A` is non-zero and small (`≤ 2e18`). + +#### Unchecked math considerations + +As above, the use of unchecked arithmetic is safe because all inputs are bounded by pre-conditions. + +## Conclusion + +The `f()` and `fInverse()` functions of EulerSwap are implemented with rigorous safety measures, using `Math.mulDiv` for safe arithmetic and applying ceiling rounding to maintain precision. Boundary analysis shows that all potential overflow scenarios are precluded by pre-condition checks and bounded operations, justifying the use of unchecked math in the Solidity implementation. diff --git a/docs/interfaces.md b/docs/interfaces.md new file mode 100644 index 0000000..f06898a --- /dev/null +++ b/docs/interfaces.md @@ -0,0 +1,113 @@ +# EulerSwap interfaces + +## **IEulerSwap interface** + +### **Overview** + +The `IEulerSwap` interface defines the core functionality for executing token swaps, activating the contract, and verifying the swapping curve invariant. + +### **Functions** + +#### `swap(uint256 amount0Out, uint256 amount1Out, address to, bytes calldata data) external;` + +- **description**: Optimistically sends the requested amounts of tokens to the `to` address, invokes `uniswapV2Call` callback on `to` (if `data` was provided), and then verifies that a sufficient amount of tokens were transferred to satisfy the swapping curve invariant. + +#### `activate() external;` + +- **description**: Approves the vaults to access the EulerSwap instance's tokens, and enables vaults as collateral. Can be invoked by anybody, and is harmless if invoked again. Calling this function is optional: EulerSwap can be activated on the first swap. + +#### `verify(uint256 newReserve0, uint256 newReserve1) external view returns (bool);` + +- **description**: Function that defines the shape of the swapping curve. Returns true if the specified reserve amounts would be acceptable (i.e., above and to-the-right of the swapping curve). + +### **Accessors** + +#### `curve() external view returns (bytes32);` + +- **description**: Returns the identifier of the swapping curve. + +#### `vault0() external view returns (address);` + +- **description**: Returns the address of vault 0. + +#### `vault1() external view returns (address);` + +- **description**: Returns the address of vault 1. + +#### `asset0() external view returns (address);` + +- **description**: Returns the address of asset 0. + +#### `asset1() external view returns (address);` + +- **description**: Returns the address of asset 1. + +#### `eulerAccount() external view returns (address);` + +- **description**: Returns the address of the account managing EulerSwap. + +#### `equilibriumReserve0() external view returns (uint112);` + +- **description**: Returns the equilibrium reserve amount of asset 0. + +#### `equilibriumReserve1() external view returns (uint112);` + +- **description**: Returns the equilibrium reserve amount of asset 1. + +#### `feeMultiplier() external view returns (uint256);` + +- **description**: Returns the fee multiplier applied to transactions. + +#### `getReserves() external view returns (uint112 reserve0, uint112 reserve1, uint32 status);` + +- **description**: Retrieves the current reserve amounts and contract status. + +### **Curve accessors** + +#### `priceX() external view returns (uint256);` + +- **description**: Returns the marginal price of asset X in terms of asset Y at the equilibrium point. + +#### `priceY() external view returns (uint256);` + +- **description**: Returns the marginal price of asset Y in terms of asset X at the equilibrium point. + +#### `concentrationX() external view returns (uint256);` + +- **description**: Returns the liquidity concentration of asset X. + +#### `concentrationY() external view returns (uint256);` + +- **description**: Returns the liquidity concentration of asset Y. + +--- + +## **IEulerSwapPeriphery interface** + +### **Overview** + +The `IEulerSwapPeriphery` interface provides auxiliary functions for quoting token swap amounts before execution. + +### **Functions** + +#### `quoteExactInput(address eulerSwap, address tokenIn, address tokenOut, uint256 amountIn) external view returns (uint256);` + +- **description**: Calculates how much `tokenOut` can be received for `amountIn` of `tokenIn`. + +#### `quoteExactOutput(address eulerSwap, address tokenIn, address tokenOut, uint256 amountOut) external view returns (uint256);` + +- **description**: Calculates how much `tokenIn` is required to receive `amountOut` of `tokenOut`. + +--- + +## **IUniswapV2Callee interface** + +### **Overview** + +The `IUniswapV2Callee` interface defines the callback function used for executing swaps on EulerSwap. + +### **Functions** + +#### `uniswapV2Call(address sender, uint256 amount0, uint256 amount1, bytes calldata data) external;` + +- **description**: Callback function invoked by EulerSwap during a swap operation, allowing the contract to perform additional logic. diff --git a/docs/whitepaper/EulerSwap_White_Paper.pdf b/docs/whitepaper/EulerSwap_White_Paper.pdf new file mode 100644 index 0000000..a7d4fd9 Binary files /dev/null and b/docs/whitepaper/EulerSwap_White_Paper.pdf differ diff --git a/docs/whitepaper/curve.png b/docs/whitepaper/curve.png new file mode 100644 index 0000000..59176ba Binary files /dev/null and b/docs/whitepaper/curve.png differ diff --git a/docs/whitepaper/main.tex b/docs/whitepaper/main.tex new file mode 100644 index 0000000..1ac4e24 --- /dev/null +++ b/docs/whitepaper/main.tex @@ -0,0 +1,390 @@ +\documentclass{article} + +% Encoding and Geometry +\usepackage[utf8]{inputenc} +\usepackage[a4paper, margin=1in]{geometry} % Standard 1-inch margins for better layout +\usepackage{parskip} + +% Math Packages +\usepackage{amsmath, amsfonts, mathtools} + +% Graphics and Figures +\usepackage{graphicx} +\usepackage{caption} +\usepackage{subcaption} +\usepackage{multirow} +\usepackage{tikz} +\usetikzlibrary{positioning} % Ensures compatibility with Overleaf + +% Hyperlinks +\usepackage[colorlinks=true, linkcolor=blue, urlcolor=blue]{hyperref} % Load last to avoid conflicts + +% Floating Objects Placement +\usepackage[section]{placeins} + +% Bibliography +\usepackage[ + backend=biber, + style=alphabetic, + sorting=ynt +]{biblatex} +\addbibresource{references.bib} + +% Title Information +\title{EulerSwap White Paper} +\author{Euler Labs} +\date{February 2025} + +\begin{document} + +\maketitle + +\begin{abstract} +EulerSwap is an automated market maker (AMM) that integrates with Euler credit vaults to provide deeper liquidity for swaps. When a user initiates a swap, a smart contract called an EulerSwap operator borrows the required output token using the input token as collateral. This model enables up to 40x the liquidity depth of traditional AMMs by making idle assets in Euler more efficient. Unlike traditional AMMs, which often fragment liquidity across multiple pools, EulerSwap further increases capital efficiency by allowing a single, cross-collateralised credit vault to support multiple asset pairs at once. At its core, EulerSwap uses a flexible AMM curve to optimise swap pricing, ensuring deep liquidity while maintaining market balance. By combining just-in-time liquidity, shared liquidity across pools, and customisable AMM mechanics, EulerSwap reduces inefficiencies in liquidity provision, offering deeper markets, lower costs, and greater control for liquidity providers. +\end{abstract} + +\section{Introduction} + +EulerSwap is an automated market maker (AMM) that leverages Euler credit vaults for the provision of just-in-time liquidity as a way to provide deeper liquidity for swaps. EulerSwap differs from traditional AMMs in that it does not depend on many small liquidity providers (LPs) pooling their liquidity inside a single contract. Instead, each EulerSwap instance has a single LP -- an account holder on Euler -- whose deposits function as both liquidity for swaps and as margin collateral for building leveraged positions inside Euler. + +When a user initiates a swap, EulerSwap dynamically borrows the required `out token' using the `in token' and the LP's initial margin collateral as backing. This borrowing approach ensures liquidity is provided just-in-time, only when needed, maximising capital efficiency. By borrowing liquidity to service swaps in this way, EulerSwap can turn \$1M initial liquidity into the equivalent of a \$40m deep liquidity pool on a traditional AMM (see Example below). + +The way this works under the hood is by taking advantage of the `operator' functionality of the Ethereum Vault Connector (EVC), which provides a way for Euler credit vault users to delegate authority to someone else to manage their account balances. An EulerSwap operator is a smart contract that manages an LP's account on their behalf using the logic encoded in a custom-built AMM smart contract. Specifically, an EulerSwap operator is able to rebalance the collateral and debt inside an LPs account in order to service swaps and earn swap fees. + +Unlike conventional AMMs, which usually require separate liquidity pools for each asset pair, EulerSwap further increases capital efficiency by enabling the use of a single, cross-collateralised credit vault to support multiple asset pairs at once. This liquidity-sharing model ensures that idle liquidity can be used efficiently across multiple trading pairs simultaneously. This design resembles Curve's 3pool concept, but extends it to any number of asset pairings. Unlike traditional AMMs that silo liquidity into separate pools, EulerSwap's cross-collateralisation allows a single USDC credit vault to back multiple trading pairs simultaneously, maximising liquidity utilisation. A single USDC credit vault could be used to support swaps with USDT, USDE, DAI, and many other pairs all in the same block. + +EulerSwap introduces a flexible AMM curve (illustrated in Figure \ref{fig:fig1}) that supports deep liquidity for short-term swaps while maintaining a neutral net position over longer periods. A highly customisable curve allows for bespoke pricing strategies, asymmetric liquidity deposits, and single-sided liquidity concentration—all fully controlled by the LP. LPs can modify their EulerSwap parameters at any time by simply swapping out the operator smart contract in their Euler account for another one. + +For end-users, EulerSwap offers a seamless, Uniswap-style experience, while behind the scenes, it harnesses advanced features such as dynamic liquidity borrowing, custom pricing curves, and shared liquidity provisioning. Liquidity providers, including professional market makers, token issuers, and DAOs, gain unprecedented capital efficiency. Meanwhile, swappers—ranging from leverage traders and aggregators to MEV bots—benefit from deeper liquidity and optimised trade execution. + +\section{Example} + +Suppose that Euler allows borrowing of USDT with USDC as collateral at a loan-to-value (LTV) ratio of 0.95, and vice versa. This means that for every \$1 of USDC or USDT collateral, a user can borrow up to \$0.95 of the other asset. + +Now suppose you have an account on Euler with 1M USDC deposited as initial liquidity. Using maximum leverage your account could hypothetically support deposits of 20M USDC and debts of 19M USDT. Alternatively, if you swapped your 1 million USDC to 1 million USDT, it could support the opposite. + +To enable swaps and earn additional yield, you install an EulerSwap operator on your account to facilitate swaps. Now let’s say another user wants to swap 10M USDC for USDT. The steps are as follows: + +\begin{enumerate} + \item The swapper sends 10M USDC to your EulerSwap operator as the swap input amount. + \item The operator deposits the 10M USDC as collateral in Euler. + \item The operator then borrows approximately 10M USDT against the account's collateral, which includes your original deposit, alongside the swap input. + \item The operator sends the borrowed USDT to the user as the swap output. +\end{enumerate} + +\quad +Importantly, this isn’t a 1:1 swap because: a) the EulerSwap operator charges a fee for facilitating the swap; and b) the exact swap output is determined by an AMM curve inside the operator, chosen by you, which factors in increasingly large price impact as the swap input amount increases. After the swap, your Euler account now holds 11M USDC deposits and 10M USDT debt. Later, when a swap occurs in the reverse direction, the incoming `in token' repays the outstanding loan, and any excess collateral is returned as the `out token.' + +The AMM curve inside the operator helps to ensure that imbalances in collateral and debt on the account encourage swaps that bring your account back to neutrality. The AMM is designed to help ensure that positions on your account do not remain open for extended periods, reducing your account's exposure to borrowing costs. Over time, your account will incur costs due to small interest rate differentials, whilst generating significant swap fees through utilisation of idle liquidity in Euler's credit vaults. The whole process is depicted in Fig. \ref{fig:EulerSwap_liquidity}. + +\bigskip +\begin{figure}[h] + \centering + \begin{tikzpicture}[ + node distance=1cm, + every node/.style={draw, text width=2.5cm, align=center, rounded corners} + ] + % Nodes + \node (user1) {User Sends 10M USDC}; + \node (EulerSwap) [right=of user1] {EulerSwap Swap Operator}; + \node (deposit) [below=of EulerSwap] {Deposits 10M USDC as Collateral}; + \node (borrow) [below=of deposit] {Borrows ~10M USDT}; + \node (user2) [left=of borrow] {User Receives 10M USDT}; + + % Arrows + \draw[->] (user1) -- (EulerSwap); + \draw[->] (EulerSwap) -- (deposit); + \draw[->] (deposit) -- (borrow); + \draw[->] (borrow) -- (user2); + \end{tikzpicture} + \caption{\textbf{Swap flow in EulerSwap AMM}. EulerSwap’s just-in-time liquidity borrowing. The swap operator dynamically increases liquidity by borrowing the ``out token" against the ``in token."} + \label{fig:EulerSwap_liquidity} +\end{figure} + +\section{Virtual reserves and debt limits} + +Since EulerSwap AMMs do not hold the assets used to service swaps at all times, they do swap calculations based on `virtual' reserves and debt limits, rather than on `real' reserves. + +Each EulerSwap LP can configure independent virtual reserve levels. These reserves define the maximum debt exposure an AMM will take on. For instance, if a user deposits \$1,000 in collateral and sets virtual reserves at \$5,000 per vault, the AMM effectively supports up to \$10,000 in combined swap depth, with a loan-to-value (LTV) ratio of 83.3\%. + +Note that the effective LTV must always remain below the borrowing LTV of the credit vault to prevent liquidation. Additionally, different AMM curves influence whether the maximum virtual reserves are achievable. + +\section{Curve} + +The space of possible reserves in an EulerSwap AMM is determined by how much debt a swap operator is allowed to hold. The EulerSwap curve passes through an equilibrium point $(x_0, y_0)$, at which the marginal price is defined by: + +\begin{equation} +\frac{dy}{dx} \Big|_{(x_0, y_0)} = -\frac{p_x}{p_y}. +\end{equation} + +Unlike most AMM curves, which are usually defined by a single convex function, EulerSwap uses a piecewise-defined curve, with different functions providing trading behaviour either side of the equilibrium point: + +\begin{equation} + \label{eq:fx-main} + f(x) = + \begin{dcases} + f_1(x), + & 0 < x \leq x_0 \\ + f_2(x), + & x_0 < x + \end{dcases}. +\end{equation} + +In the domain $0 < x \leq x_0$, the curve is defined by + +\begin{equation} + \label{eq:fx1-main} + f_1(x) + = + y_{0}+\frac{p_{x}}{p_{y}}\left(x_{0}-x\right)\left(c_{x}+\left(1-c_{x}\right)\left(\frac{x_{0}}{x}\right)\right). +\end{equation} + +In the domain $x_0 < x$, the curve is defined by + +\begin{equation} + \label{eq:fx2-main} + f_2(x) + = + \frac{ + \sqrt{ + \left( \frac{p_x}{p_y} (x - x_0) + y_0 (1 - 2c_y) \right)^2 + + 4c_y (1 - c_y) y_0^2 + } + - \left( \frac{p_x}{p_y} (x - x_0) + y_0 (1 - 2c_y) \right) + }{2c_y}. +\end{equation} + + The $c_x, c_y$ parameters here are liquidity concentration parameters that control how liquidity is distributed along the curve, with values closer to 1 concentrating liquidity around equilibrium and values closer to 0 distributing it across a wider price range. This flexibility enables EulerSwap to be used for entirely new use cases or to simulate the behaviour of atypical AMM protocols, such as the MakerDAO \href{https://mips.makerdao.com/mips/details/MIP29}{Peg Stability Module} (PSM). A full derivation is given in the Appendix \ref{sec:appendix}. + + \begin{figure}[h] % 'h' places it here, 't' for top, 'b' for bottom, 'p' for separate page + \centering % Centers the image + \includegraphics[width=0.5\textwidth]{curve.png} % Adjust width as needed + \caption{\textbf{EulerSwap AMM curve.} The EulerSwap curve (red line) consists of two sides with separate reserve values $x_0, y_0$ and liquidity concentration parameters $c_x, c_y$, allowing liquidity to be distributed asymmetrically. This means liquidity can be more or less dense or concentrated on one side of the AMM relative to the other. The exchange rate at equilibrium is determined by the pricing parameters $p_x, p_y$ and is fully flexible. You can interact with the curve \href{https://www.desmos.com/calculator/gzwmvbs1dk}{here} on Desmos to compare its behaviour with traditional constant-sum and constant-product curves (black lines).} + \label{fig:fig1} % Useful for referencing the figure in text +\end{figure} + +\section{Conclusion} + +EulerSwap enhances AMMs by leveraging just-in-time liquidity to expand the depth of liquidity available to swappers. By integrating Euler’s credit vaults, it enables LPs to earn swap fees on top of their ordinary deposits while offering a customisable AMM curve that supports concentrated, distributed, and asymmetric liquidity structures. These features make EulerSwap adaptable to a wide range of trading strategies. However, this efficiency is not without some trade-offs. Swap operators incur interest costs on borrowed assets, which can erode profitability if not offset by swap fees, interest on collateral, or token incentives. Additionally, EulerSwap inherits Euler’s risk parameters, making it most effective for correlated asset pairs with high loan-to-value (LTV) ratios, while volatile assets pose higher liquidation risks if positions become imbalanced. Despite these trade-offs, EulerSwap represents a breakthrough in AMM design by repurposing idle liquidity in lending protocols, reducing capital inefficiencies, and optimising swap execution. By catering to professional market makers, DAOs, and algorithmic traders, it positions itself as a powerful tool for on-chain liquidity optimisation. Future research could explore optimal strategies for setting pricing and liquidity concentration parameters dynamically based on market conditions. EulerSwap's design opens new possibilities for AMM efficiency, setting a foundation for further DeFi innovation. + +\section*{Acknowledgments} + +During a security review of EulerSwap, Chris Michel noted similarities between some of its underlying concepts and BlackHoleSwap, a project prototype he had reviewed years earlier. While EulerSwap was designed independently and without influence from that project, the resemblance is significant enough to warrant its recognition here as prior art. + +\newpage +\section{Appendix} +\label{sec:appendix} + +\subsection{Curve derivation} +\label{sec:curve-derivation} + +We begin with an Automated Market Maker (AMM) holding initial liquidity reserves of two assets, $X$ and $Y$, denoted as $x_0$ and $y_0$, respectively. In the absence of trading, the AMM remains at equilibrium at the point $(x_0, y_0)$. + +Our goal is to derive a curve for a constant-function trading market maker (CFMM) that supports swaps between the two assets with the following properties: + +\begin{itemize} + \item Passes through the equilibrium point $(x_0, y_0)$. + \item Maintains an exchange rate, given by the slope of the AMM curve, of $-p_x / p_y$ at $(x_0, y_0)$. + \item Allows liquidity concentration to be adjusted via parameters $c_x$ and $c_y$, which control the liquidity available for swaps to the left and right of the equilibrium point. +\end{itemize} + +To develop such a function, we first introduce two fundamental AMM curve models. + +\subsubsection{Constant-sum and constant-product AMM curves} + +The canonical constant-sum (CSMM) and constant-product (CPMM) market making curves are given by: + +\begin{align} + x + y &= x_0 + y_0, \\ + xy &= x_0 y_0. +\end{align} + +The CSMM is simply a line, whilst the CPMM is a hyperbola. These curves can be thought of as two extremes when it comes to the distribution of liquidity. The CSMM concentrates liquidity at a single exchange rate, whilst the CPMM distributes liquidity across a wide range of different exchange rates. By default, these curves intersect at the equilbirium point $(x_0, y_0)$, where their slopes are: + +\begin{align} + \frac{dy}{dx} &= -1, \\ + \frac{dy}{dx} &= -\frac{y}{x}. +\end{align} + +Since real-world markets often operate at variable exchange rates at equilibrium, we introduce custom pricing parameters $p_x$ and $p_y$ to allow flexibility in defining the slope at the equilibrium point: + +\begin{align} + \label{eq:weighted-line} + p_x x + p_y y &= p_x x_0 + p_y y_0, \\ + \label{eq:exponential-form} + x^{p_y y_0} y^{p_x x_0} &= x_0^{p_y y_0} y_0^{p_x x_0}. +\end{align} + +The CSMM is now a line with a slope parameterised by the ratio of the pricing parameters, whilst the CPMM is now a \textit{weighted} hyperbola. Taking the derivatives of these equations with respect to $x$, we obtain: + +\begin{align} + \frac{dy}{dx} &= -\frac{p_x}{p_y}, \\ + \frac{dy}{dx} &= -\frac{p_x}{p_y} \frac{x_0 y}{y_0 x}. +\end{align} + +These results confirm that at equilibrium $(x_0, y_0)$, the slope of both functions is: + +\[ +\frac{dy}{dx} \Big|_{(x_0, y_0)} = -\frac{p_x}{p_y}. +\] + +Whilst these curves sufficiently generalise the constant-sum and constant-product curves to an arbitrary price at equilibrium, in practice, the modified CPMM equation \eqref{eq:exponential-form} has limited practical application as a CFMM because it involves an exponential form that is computationally intensive and impractical for on-chain calculations. We therefore seek a simpler equation that preserves the spirit of the trading behaviour of the generalised CPMM; that is, some kind of herpbola that passes through the equilibrium point $(x_0, y_0)$ and maintains an exchange rate of $-p_x / p_y$ at that point. To resolve this, we introduce the concept of artificial reserves. + +\subsubsection{Introducing artificial reserves to create a new, simpler curve} + +Note that in the interval $0 < x < x_0$ swaps should only increase liquidity beyond $y_0$ and deplete $x_0$ liquidity. That is, our trading function in this interval need not depend on the initial amount of $y_0$ liquidity. This suggests that we can split the domain of the AMM curves into two, and replace the real reserve $y_0$ in the interval $0 < x < x_0$ with an carefully chosen artificial reserve $y_v$ designed to eliminate the exponential form in the weighted hyperbola. + +Re-arranging equations \eqref{eq:weighted-line} and \eqref{eq:exponential-form} into explicit functions of $y$, we obtain +\begin{align} + \label{eq:weighted-line-explicit} + y &= y_0 + \frac{p_x}{ p_y} (x_0 - x), \\ + \label{eq:exponential-form-explicit} + y &= y_0 \left( \frac{x_0}{x} \right)^{\frac{p_y y_0}{p_x x_0}}. +\end{align} + +In this view, it is easy to see that a substitution of $y_0 \rightarrow y_v$, given by + +\[ +y_v = x_0 \frac{p_x}{p_y}. +\] + +will eliminate the exponential form in equation \eqref{eq:exponential-form-explicit}. We then have equations + +\begin{align} + y &= \frac{p_x}{p_y} (2x_0 - x), \\ + y &= \frac{p_x}{p_y} \frac{x_0^2}{x}. +\end{align} + +Note that simply shifting the curve up or down does not impact the slope or shapes of the curves and therefore has no impact on trading behaviour. Since these curves no longer pass through \( (x_0, y_0) \), we therefore correct them by adding back the difference \( y_0 - p_x / p_y x_0 \). This leads to: + +\begin{align} + y &= y_0 + \frac{p_x}{p_y} (x_0 - x), \\ + y &= y_0 + \frac{p_x}{p_y} (x_0 - x) \left( \frac{x_0}{x} \right). +\end{align} + +Taking the derivatives of these equations with respect to $x$, we obtain: + +\begin{align} + \frac{dy}{dx} &= -\frac{p_x}{p_y}, \\ + \frac{dy}{dx} &= -\frac{p_x}{p_y} \left( \frac{x_0}{x} + \frac{x_0(x_0 - x)}{x^2} \right). +\end{align} + +These results confirm that at equilibrium $(x_0, y_0)$, the slope of both functions is: + +\[ +\frac{dy}{dx} \Big|_{(x_0, y_0)} = -\frac{p_x}{p_y}. +\] + +\subsubsection{Unifying into a single curve in the region $0 < x < x_0$} + +To create a single unified curve, we introduce a liquidity concentration parameter \( c_x \in [0, 1] \), which determines the curve’s behaviour: + +\begin{itemize} + \item When \( c_x = 1 \), the AMM functions as a constant-sum AMM. + \item When \( c_x = 0 \), the AMM behaves as a constant-product-like AMM. + \item Intermediate values of \( c_x \) create a hybrid trading function with liquidity that is more or less concentrated around the equilibrium point. +\end{itemize} + +This gives the final equation: + +\begin{equation} + \label{eq:EulerSwap-1} + y = y_0 + \frac{p_x}{p_y} (x_0 - x) \left( c_x + (1 - c_x) \left(\frac{x_0}{x}\right) \right). +\end{equation} + +This is equivalent to the function \( f_1(x) \) given by equation \eqref{eq:fx1-main} in the main text. + +\subsubsection{Extending the curve to the $x > x_0$ region} + +It is important to remember that equation \eqref{eq:EulerSwap-1} only works as a trading function in the interval $0 < x < x_0$. For \( x > x_0 \), we construct a reflected curve that also passes through \( (x_0, y_0) \) and maintains the required derivative: + +\begin{equation} + \label{eq:EulerSwap-3-inverse} + x = x_0 + \frac{p_y}{p_x} (y_0 - y) \left( c_y + (1 - c_y) \left(\frac{y_0}{y}\right) \right). +\end{equation} + +Since this equation defines \( x \) in terms of \( y \), we invert it by solving for \( y \), leading to the quadratic equation: + +\begin{equation} + y = c_y y^2 + \left( \frac{p_x}{p_y} (x - x_0) - y_0(2c_y - 1) \right)y - (1 - c_y) y_0^2. +\end{equation} + +Taking only the positive real root, we obtain: + +\begin{equation} + \label{eq:EulerSwap-2} + y = \frac{ + \sqrt{ + \left( \frac{p_x}{p_y} (x - x_0) + y_0 (1 - 2c_y) \right)^2 + + 4c_y (1 - c_y) y_0^2 + } + - \left( \frac{p_x}{p_y} (x - x_0) + y_0 (1 - 2c_y) \right) + }{2c_y}. +\end{equation} + +This corresponds to the function \( f_2(x) \) given by equation \eqref{eq:fx2-main} in the main text. + +\subsection{Invariant derivation} +\label{sec:invariant-derivation} + +In traditional AMM protocols, the curve is typically defined as an implicit function of $y$. For example, the classic Uniswap AMM follows a constant-product equation: + +\begin{equation} + xy = x_0 y_0 +\end{equation} + +where $x_0$ and $y_0$ are the initial liquidity reserves. This equation defines an invariant condition, ensuring that any valid swap must satisfy: + +\begin{equation} + xy \geq x_0 y_0. +\end{equation} + +This condition guarantees that after any trade, the product of the new reserves remains at least as large as the initial product, ensuring that swaps cannot drain liquidity from the pool. + +Rearranging this condition, we can express the invariant as a lower bound for $y$: + +\begin{equation} + y \geq \frac{x_0 y_0}{x}. +\end{equation} + +This means that for any valid trade, the resulting reserve state must lie on or above the AMM curve. + +\subsubsection{Extending the invariant to EulerSwap} + +For EulerSwap, we apply a similar principle. Given any reserve state $(x, y)$, we check whether it satisfies an equivalent invariant condition. + +From equation \eqref{eq:EulerSwap-1}, for values where \( 0 < x < x_0 \), the AMM curve constraint requires: + +\begin{equation} + \label{eq:invariant-x1} + y \geq y_{0}+\frac{p_{x}}{p_{y}}\left(x_{0}-x\right)\left(c_{x}+\left(1-c_{x}\right)\left(\frac{x_{0}}{x}\right)\right). +\end{equation} + +For values where \( x > x_0 \), we could derive a similar condition for $y$ from equation \eqref{eq:EulerSwap-2}. However, a simpler and computationally cheaper approach is to use the inverse equation \eqref{eq:EulerSwap-3-inverse}, which defines the lower bound for $x$ instead: + +\begin{equation} + \label{eq:invariant-x2} + x \geq x_{0}+\frac{p_{y}}{p_{x}}\left(y_{0}-y\right)\left(c_{y}+\left(1-c_{y}\right)\left(\frac{y_{0}}{y}\right)\right). +\end{equation} + +These conditions together define the valid liquidity states in EulerSwap, ensuring that the AMM remains balanced while allowing for greater flexibility in liquidity provisioning. + +\section{Disclaimer} + +This paper is for general information purposes only. It does not constitute investment +advice or a recommendation or solicitation to buy or sell any investment and should not +be used in the evaluation of the merits of making any investment decision. It should not +be relied upon for accounting, legal or tax advice or investment recommendations. This +paper reflects current opinions of the authors and is not made on behalf of Euler Labs or its +affiliates and does not necessarily reflect the opinions of Euler Labs, its affiliates or individuals +associated with Euler Labs. The opinions reflected herein are subject to change without being +updated. + +\end{document} + + + +Section 5.1.2 starts off with the principle that the trading function should not depend on y0, and applies it to formula (8), on the assumption (correct me if I'm wrong) that whatever y_v we choose will not change the trading behaviour. However this is not true for formula (8), because y0 is not just a vertical offset, but is embedded in the exponents. +What stays true is just the fact that, whatever y_v we choose, the curve will pass through the point (x0, y_v) with slope -px/py, so we might as well choose an "easy" one that simplifies the calculations: the driving reason for imposing px x0 = py yv seems rather to be that this renders the exponents in formula (8) equal, thus effectively reverting it to an unweighted hyperbola. Then, once we add back the vertical offset, the trading behaviour of formula (14) indeed no longer depends on y0, but just because we have now "made" y0 just a vertical offset, shifting formula (12). +In other words, we have formula (8) which defines a parametric curve y = f(x; x*,y*,p*) that always passes through the point (x*,y*) with slope -p*. We want a function that passes through (x0,y0) with slope -p0. We could use y = f(x; x0,y0,p0) but this is computationally intensive. Instead, we choose y = f(x; x0,x0p0,p0) + (y0 - x0p0) because this still satisfies the requirements, but the particular choice of y* makes f() less computationally intensive. +In fact, there is a mistake earlier on in the appendix: the slope of the plain hyperbola x y = x0 y0 at the point (x0, y0) is not -1 but -y0/x0. The choice of y_v = x0 px/py makes that slope equal to -px/py. So overall it would seem like choosing a plain hyperbola from the start, with appropriate parameters and offset, would do the trick. Have I interpreted correctly the intentions of your derivation process? \ No newline at end of file diff --git a/foundry-docs/.gitignore b/foundry-docs/.gitignore new file mode 100644 index 0000000..4e42a1b --- /dev/null +++ b/foundry-docs/.gitignore @@ -0,0 +1 @@ +book/ \ No newline at end of file diff --git a/foundry-docs/book.css b/foundry-docs/book.css new file mode 100644 index 0000000..b5ce903 --- /dev/null +++ b/foundry-docs/book.css @@ -0,0 +1,13 @@ +table { + margin: 0 auto; + border-collapse: collapse; + width: 100%; +} + +table td:first-child { + width: 15%; +} + +table td:nth-child(2) { + width: 25%; +} \ No newline at end of file diff --git a/foundry-docs/book.toml b/foundry-docs/book.toml new file mode 100644 index 0000000..62a23bf --- /dev/null +++ b/foundry-docs/book.toml @@ -0,0 +1,12 @@ +[book] +src = "src" +title = "Euler Swap Contracts Documentation" + +[output.html] +no-section-label = true +additional-js = ["solidity.min.js"] +additional-css = ["book.css"] +git-repository-url = "https://github.com/euler-xyz/euler-maglev" + +[output.html.fold] +enable = true diff --git a/foundry-docs/solidity.min.js b/foundry-docs/solidity.min.js new file mode 100644 index 0000000..1924932 --- /dev/null +++ b/foundry-docs/solidity.min.js @@ -0,0 +1,74 @@ +hljs.registerLanguage("solidity",(()=>{"use strict";function e(){try{return!0 +}catch(e){return!1}} +var a=/-?(\b0[xX]([a-fA-F0-9]_?)*[a-fA-F0-9]|(\b[1-9](_?\d)*(\.((\d_?)*\d)?)?|\.\d(_?\d)*)([eE][-+]?\d(_?\d)*)?|\b0)(?!\w|\$)/ +;e()&&(a=a.source.replace(/\\b/g,"(?{ +var a=r(e),o=l(e),c=/[A-Za-z_$][A-Za-z_$0-9.]*/,d=e.inherit(e.TITLE_MODE,{ +begin:/[A-Za-z$_][0-9A-Za-z$_]*/,lexemes:c,keywords:n}),u={className:"params", +begin:/\(/,end:/\)/,excludeBegin:!0,excludeEnd:!0,lexemes:c,keywords:n, +contains:[e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,a,o,s]},_={ +className:"operator",begin:/:=|->/};return{keywords:n,lexemes:c, +contains:[a,o,i,t,e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,s,_,{ +className:"function",lexemes:c,beginKeywords:"function",end:"{",excludeEnd:!0, +contains:[d,u,e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,_]}]}}, +solAposStringMode:r,solQuoteStringMode:l,HEX_APOS_STRING_MODE:i, +HEX_QUOTE_STRING_MODE:t,SOL_NUMBER:s,isNegativeLookbehindAvailable:e} +;const{baseAssembly:c,solAposStringMode:d,solQuoteStringMode:u,HEX_APOS_STRING_MODE:_,HEX_QUOTE_STRING_MODE:m,SOL_NUMBER:b,isNegativeLookbehindAvailable:E}=o +;return e=>{for(var a=d(e),s=u(e),n=[],i=0;i<32;i++)n[i]=i+1 +;var t=n.map((e=>8*e)),r=[];for(i=0;i<=80;i++)r[i]=i +;var l=n.map((e=>"bytes"+e)).join(" ")+" ",o=t.map((e=>"uint"+e)).join(" ")+" ",g=t.map((e=>"int"+e)).join(" ")+" ",M=[].concat.apply([],t.map((e=>r.map((a=>e+"x"+a))))),p={ +keyword:"var bool string int uint "+g+o+"byte bytes "+l+"fixed ufixed "+M.map((e=>"fixed"+e)).join(" ")+" "+M.map((e=>"ufixed"+e)).join(" ")+" enum struct mapping address new delete if else for while continue break return throw emit try catch revert unchecked _ function modifier event constructor fallback receive error virtual override constant immutable anonymous indexed storage memory calldata external public internal payable pure view private returns import from as using pragma contract interface library is abstract type assembly", +literal:"true false wei gwei szabo finney ether seconds minutes hours days weeks years", +built_in:"self this super selfdestruct suicide now msg block tx abi blockhash gasleft assert require Error Panic sha3 sha256 keccak256 ripemd160 ecrecover addmod mulmod log0 log1 log2 log3 log4" +},O={className:"operator",begin:/[+\-!~*\/%<>&^|=]/ +},C=/[A-Za-z_$][A-Za-z_$0-9]*/,N={className:"params",begin:/\(/,end:/\)/, +excludeBegin:!0,excludeEnd:!0,lexemes:C,keywords:p, +contains:[e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,a,s,b,"self"]},f={ +begin:/\.\s*/,end:/[^A-Za-z0-9$_\.]/,excludeBegin:!0,excludeEnd:!0,keywords:{ +built_in:"gas value selector address length push pop send transfer call callcode delegatecall staticcall balance code codehash wrap unwrap name creationCode runtimeCode interfaceId min max" +},relevance:2},y=e.inherit(e.TITLE_MODE,{begin:/[A-Za-z$_][0-9A-Za-z$_]*/, +lexemes:C,keywords:p}),w={className:"built_in", +begin:(E()?"(?`|`address`|The address of the EVC contract.| + + +### activate + +Approves the vaults to access the EulerSwap instance's tokens, and enables +vaults as collateral. Can be invoked by anybody, and is harmless if invoked again. +Calling this function is optional: EulerSwap can be activated on the first swap. + + +```solidity +function activate() public; +``` + +### verify + +Function that defines the shape of the swapping curve. Returns true iff +the specified reserve amounts would be acceptable (ie it is above and to-the-right +of the swapping curve). + + +```solidity +function verify(uint256 newReserve0, uint256 newReserve1) public view returns (bool); +``` + +### withdrawAssets + + +```solidity +function withdrawAssets(address vault, uint256 amount, address to) internal; +``` + +### depositAssets + + +```solidity +function depositAssets(address vault, uint256 amount) internal returns (uint256); +``` + +### myDebt + + +```solidity +function myDebt(address vault) internal view returns (uint256); +``` + +### myBalance + + +```solidity +function myBalance(address vault) internal view returns (uint256); +``` + +### offsetReserve + + +```solidity +function offsetReserve(uint112 reserve, address vault) internal view returns (uint112); +``` + +### f + +*EulerSwap curve definition +Pre-conditions: x <= x0, 1 <= {px,py} <= 1e36, {x0,y0} <= type(uint112).max, c <= 1e18* + + +```solidity +function f(uint256 x, uint256 px, uint256 py, uint256 x0, uint256 y0, uint256 c) internal pure returns (uint256); +``` + +## Events +### EulerSwapCreated + +```solidity +event EulerSwapCreated(address indexed asset0, address indexed asset1); +``` + +### Swap + +```solidity +event Swap( + address indexed sender, + uint256 amount0In, + uint256 amount1In, + uint256 amount0Out, + uint256 amount1Out, + uint112 reserve0, + uint112 reserve1, + address indexed to +); +``` + +## Errors +### Locked + +```solidity +error Locked(); +``` + +### Overflow + +```solidity +error Overflow(); +``` + +### BadParam + +```solidity +error BadParam(); +``` + +### DifferentEVC + +```solidity +error DifferentEVC(); +``` + +### AssetsOutOfOrderOrEqual + +```solidity +error AssetsOutOfOrderOrEqual(); +``` + +### CurveViolation + +```solidity +error CurveViolation(); +``` + +### DepositFailure + +```solidity +error DepositFailure(bytes reason); +``` + diff --git a/foundry-docs/src/src/EulerSwapFactory.sol/contract.EulerSwapFactory.md b/foundry-docs/src/src/EulerSwapFactory.sol/contract.EulerSwapFactory.md new file mode 100644 index 0000000..8b45d63 --- /dev/null +++ b/foundry-docs/src/src/EulerSwapFactory.sol/contract.EulerSwapFactory.md @@ -0,0 +1,168 @@ +# EulerSwapFactory +[Git Source](https://github.com/euler-xyz/euler-maglev/blob/d6fc4adb9f1050f1348bfff5db3603f2482ba705/src/EulerSwapFactory.sol) + +**Inherits:** +[IEulerSwapFactory](/src/interfaces/IEulerSwapFactory.sol/interface.IEulerSwapFactory.md), EVCUtil + +**Author:** +Euler Labs (https://www.eulerlabs.com/) + +**Note:** +security-contact: security@euler.xyz + + +## State Variables +### allPools +*An array to store all pools addresses.* + + +```solidity +address[] public allPools; +``` + + +### eulerAccountToPool +*Mapping between euler account and deployed pool that is currently set as operator* + + +```solidity +mapping(address eulerAccount => address operator) public eulerAccountToPool; +``` + + +## Functions +### constructor + + +```solidity +constructor(address evc) EVCUtil(evc); +``` + +### deployPool + +Deploy a new EulerSwap pool with the given parameters + +*The pool address is deterministically generated using CREATE2 with a salt derived from +the euler account address and provided salt parameter. This allows the pool address to be +predicted before deployment.* + + +```solidity +function deployPool(IEulerSwap.Params memory params, IEulerSwap.CurveParams memory curveParams, bytes32 salt) + external + returns (address); +``` +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`params`|`IEulerSwap.Params`|Core pool parameters including vaults, account, and fee settings| +|`curveParams`|`IEulerSwap.CurveParams`|Parameters defining the curve shape including prices and concentrations| +|`salt`|`bytes32`|Unique value to generate deterministic pool address| + +**Returns** + +|Name|Type|Description| +|----|----|-----------| +|``|`address`|Address of the newly deployed pool| + + +### allPoolsLength + +Get the length of `allPools` array. + + +```solidity +function allPoolsLength() external view returns (uint256); +``` +**Returns** + +|Name|Type|Description| +|----|----|-----------| +|``|`uint256`|`allPools` length.| + + +### getAllPoolsListSlice + +Get a slice of the deployed pools array. + + +```solidity +function getAllPoolsListSlice(uint256 _start, uint256 _end) external view returns (address[] memory); +``` +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`_start`|`uint256`|Start index of the slice.| +|`_end`|`uint256`|End index of the slice.| + +**Returns** + +|Name|Type|Description| +|----|----|-----------| +|``|`address[]`|An array containing the slice of the deployed pools.| + + +### checkEulerAccountOperators + +Validates operator authorization for euler account. First checks if the account has an existing operator +and ensures it is deauthorized. Then verifies the new pool is authorized as an operator. Finally, updates the +mapping to track the new pool as the account's operator. + + +```solidity +function checkEulerAccountOperators(address eulerAccount, address newPool) internal; +``` +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`eulerAccount`|`address`|The address of the euler account.| +|`newPool`|`address`|The address of the new pool.| + + +## Events +### PoolDeployed + +```solidity +event PoolDeployed( + address indexed asset0, + address indexed asset1, + address vault0, + address vault1, + uint256 indexed feeMultiplier, + address eulerAccount, + uint256 priceX, + uint256 priceY, + uint256 concentrationX, + uint256 concentrationY, + address pool +); +``` + +## Errors +### InvalidQuery + +```solidity +error InvalidQuery(); +``` + +### Unauthorized + +```solidity +error Unauthorized(); +``` + +### OldOperatorStillInstalled + +```solidity +error OldOperatorStillInstalled(); +``` + +### OperatorNotInstalled + +```solidity +error OperatorNotInstalled(); +``` + diff --git a/foundry-docs/src/src/EulerSwapPeriphery.sol/contract.EulerSwapPeriphery.md b/foundry-docs/src/src/EulerSwapPeriphery.sol/contract.EulerSwapPeriphery.md new file mode 100644 index 0000000..a262f00 --- /dev/null +++ b/foundry-docs/src/src/EulerSwapPeriphery.sol/contract.EulerSwapPeriphery.md @@ -0,0 +1,219 @@ +# EulerSwapPeriphery +[Git Source](https://github.com/euler-xyz/euler-maglev/blob/d6fc4adb9f1050f1348bfff5db3603f2482ba705/src/EulerSwapPeriphery.sol) + +**Inherits:** +[IEulerSwapPeriphery](/src/interfaces/IEulerSwapPeriphery.sol/interface.IEulerSwapPeriphery.md) + + +## Functions +### swapExactIn + +Swap `amountIn` of `tokenIn` for `tokenOut`, with at least `amountOutMin` received. + + +```solidity +function swapExactIn(address eulerSwap, address tokenIn, address tokenOut, uint256 amountIn, uint256 amountOutMin) + external; +``` + +### swapExactOut + +Swap `amountOut` of `tokenOut` for `tokenIn`, with at most `amountInMax` paid. + + +```solidity +function swapExactOut(address eulerSwap, address tokenIn, address tokenOut, uint256 amountOut, uint256 amountInMax) + external; +``` + +### quoteExactInput + +How much `tokenOut` can I get for `amountIn` of `tokenIn`? + + +```solidity +function quoteExactInput(address eulerSwap, address tokenIn, address tokenOut, uint256 amountIn) + external + view + returns (uint256); +``` + +### quoteExactOutput + +How much `tokenIn` do I need to get `amountOut` of `tokenOut`? + + +```solidity +function quoteExactOutput(address eulerSwap, address tokenIn, address tokenOut, uint256 amountOut) + external + view + returns (uint256); +``` + +### swap + +*Internal function to execute a token swap through EulerSwap* + + +```solidity +function swap(address eulerSwap, address tokenIn, address tokenOut, uint256 amountIn, uint256 amountOut) internal; +``` +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`eulerSwap`|`address`|The EulerSwap contract address to execute the swap through| +|`tokenIn`|`address`|The address of the input token being swapped| +|`tokenOut`|`address`|The address of the output token being received| +|`amountIn`|`uint256`|The amount of input tokens to swap| +|`amountOut`|`uint256`|The amount of output tokens to receive| + + +### computeQuote + +*Computes the quote for a swap by applying fees and validating state conditions* + +*Validates: +- EulerSwap operator is installed +- Token pair is supported +- Sufficient reserves exist +- Sufficient cash is available* + + +```solidity +function computeQuote(IEulerSwap eulerSwap, address tokenIn, address tokenOut, uint256 amount, bool exactIn) + internal + view + returns (uint256); +``` +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`eulerSwap`|`IEulerSwap`|The EulerSwap contract to quote from| +|`tokenIn`|`address`|The input token address| +|`tokenOut`|`address`|The output token address| +|`amount`|`uint256`|The amount to quote (input amount if exactIn=true, output amount if exactIn=false)| +|`exactIn`|`bool`|True if quoting for exact input amount, false if quoting for exact output amount| + +**Returns** + +|Name|Type|Description| +|----|----|-----------| +|``|`uint256`|The quoted amount (output amount if exactIn=true, input amount if exactIn=false)| + + +### binarySearch + +Binary searches for the output amount along a swap curve given input parameters + +*General-purpose routine for binary searching swapping curves. +Although some curves may have more efficient closed-form solutions, +this works with any monotonic curve.* + + +```solidity +function binarySearch( + IEulerSwap eulerSwap, + uint112 reserve0, + uint112 reserve1, + uint256 amount, + bool exactIn, + bool asset0IsInput +) internal view returns (uint256 output); +``` +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`eulerSwap`|`IEulerSwap`|The EulerSwap contract to search the curve for| +|`reserve0`|`uint112`|Current reserve of asset0 in the pool| +|`reserve1`|`uint112`|Current reserve of asset1 in the pool| +|`amount`|`uint256`|The input or output amount depending on exactIn| +|`exactIn`|`bool`|True if amount is input amount, false if amount is output amount| +|`asset0IsInput`|`bool`|True if asset0 is being input, false if asset1 is being input| + +**Returns** + +|Name|Type|Description| +|----|----|-----------| +|`output`|`uint256`|The calculated output amount from the binary search| + + +### fInverse + +Computes the inverse of the `f()` function for the EulerSwap liquidity curve. + +*Solves for `x` given `y` using the quadratic formula derived from the liquidity curve: +x = (-b + sqrt(b^2 + 4ac)) / 2a +Utilises mulDiv to avoid overflow and ensures precision with upward rounding.* + +**Notes:** +- precision: Uses rounding up to maintain precision in all calculations. + +- safety: FullMath handles potential overflow in the b^2 computation. + +- requirement: Input `y` must be strictly greater than `y0`; otherwise, the function will revert. + + +```solidity +function fInverse(uint256 y, uint256 px, uint256 py, uint256 x0, uint256 y0, uint256 c) + external + pure + returns (uint256); +``` +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`y`|`uint256`|The y-coordinate input value (must be greater than `y0`).| +|`px`|`uint256`|Price factor for the x-axis (scaled by 1e18, between 1e18 and 1e36).| +|`py`|`uint256`|Price factor for the y-axis (scaled by 1e18, between 1e18 and 1e36).| +|`x0`|`uint256`|Reference x-value on the liquidity curve (≤ 2^112 - 1).| +|`y0`|`uint256`|Reference y-value on the liquidity curve (≤ 2^112 - 1).| +|`c`|`uint256`|Curve parameter shaping liquidity concentration (scaled by 1e18, between 0 and 1e18).| + +**Returns** + +|Name|Type|Description| +|----|----|-----------| +|``|`uint256`|x The computed x-coordinate on the liquidity curve.| + + +## Errors +### UnsupportedPair + +```solidity +error UnsupportedPair(); +``` + +### OperatorNotInstalled + +```solidity +error OperatorNotInstalled(); +``` + +### InsufficientReserves + +```solidity +error InsufficientReserves(); +``` + +### InsufficientCash + +```solidity +error InsufficientCash(); +``` + +### AmountOutLessThanMin + +```solidity +error AmountOutLessThanMin(); +``` + +### AmountInMoreThanMax + +```solidity +error AmountInMoreThanMax(); +``` + diff --git a/foundry-docs/src/src/README.md b/foundry-docs/src/src/README.md new file mode 100644 index 0000000..48c4e58 --- /dev/null +++ b/foundry-docs/src/src/README.md @@ -0,0 +1,7 @@ + + +# Contents +- [interfaces](/src/interfaces) +- [EulerSwap](EulerSwap.sol/contract.EulerSwap.md) +- [EulerSwapFactory](EulerSwapFactory.sol/contract.EulerSwapFactory.md) +- [EulerSwapPeriphery](EulerSwapPeriphery.sol/contract.EulerSwapPeriphery.md) diff --git a/foundry-docs/src/src/interfaces/IEulerSwap.sol/interface.IEulerSwap.md b/foundry-docs/src/src/interfaces/IEulerSwap.sol/interface.IEulerSwap.md new file mode 100644 index 0000000..cda0611 --- /dev/null +++ b/foundry-docs/src/src/interfaces/IEulerSwap.sol/interface.IEulerSwap.md @@ -0,0 +1,177 @@ +# IEulerSwap +[Git Source](https://github.com/euler-xyz/euler-maglev/blob/d6fc4adb9f1050f1348bfff5db3603f2482ba705/src/interfaces/IEulerSwap.sol) + + +## Functions +### swap + +Optimistically sends the requested amounts of tokens to the `to` +address, invokes `uniswapV2Call` callback on `to` (if `data` was provided), +and then verifies that a sufficient amount of tokens were transferred to +satisfy the swapping curve invariant. + + +```solidity +function swap(uint256 amount0Out, uint256 amount1Out, address to, bytes calldata data) external; +``` + +### activate + +Approves the vaults to access the EulerSwap instance's tokens, and enables +vaults as collateral. Can be invoked by anybody, and is harmless if invoked again. +Calling this function is optional: EulerSwap can be activated on the first swap. + + +```solidity +function activate() external; +``` + +### verify + +Function that defines the shape of the swapping curve. Returns true iff +the specified reserve amounts would be acceptable (ie it is above and to-the-right +of the swapping curve). + + +```solidity +function verify(uint256 newReserve0, uint256 newReserve1) external view returns (bool); +``` + +### EVC + +Returns the address of the Ethereum Vault Connector (EVC) used by this contract. + + +```solidity +function EVC() external view returns (address); +``` +**Returns** + +|Name|Type|Description| +|----|----|-----------| +|``|`address`|The address of the EVC contract.| + + +### curve + + +```solidity +function curve() external view returns (bytes32); +``` + +### vault0 + + +```solidity +function vault0() external view returns (address); +``` + +### vault1 + + +```solidity +function vault1() external view returns (address); +``` + +### asset0 + + +```solidity +function asset0() external view returns (address); +``` + +### asset1 + + +```solidity +function asset1() external view returns (address); +``` + +### eulerAccount + + +```solidity +function eulerAccount() external view returns (address); +``` + +### initialReserve0 + + +```solidity +function initialReserve0() external view returns (uint112); +``` + +### initialReserve1 + + +```solidity +function initialReserve1() external view returns (uint112); +``` + +### feeMultiplier + + +```solidity +function feeMultiplier() external view returns (uint256); +``` + +### getReserves + + +```solidity +function getReserves() external view returns (uint112 reserve0, uint112 reserve1, uint32 status); +``` + +### priceX + + +```solidity +function priceX() external view returns (uint256); +``` + +### priceY + + +```solidity +function priceY() external view returns (uint256); +``` + +### concentrationX + + +```solidity +function concentrationX() external view returns (uint256); +``` + +### concentrationY + + +```solidity +function concentrationY() external view returns (uint256); +``` + +## Structs +### Params + +```solidity +struct Params { + address vault0; + address vault1; + address eulerAccount; + uint112 debtLimit0; + uint112 debtLimit1; + uint256 fee; +} +``` + +### CurveParams + +```solidity +struct CurveParams { + uint256 priceX; + uint256 priceY; + uint256 concentrationX; + uint256 concentrationY; +} +``` + diff --git a/foundry-docs/src/src/interfaces/IEulerSwapFactory.sol/interface.IEulerSwapFactory.md b/foundry-docs/src/src/interfaces/IEulerSwapFactory.sol/interface.IEulerSwapFactory.md new file mode 100644 index 0000000..3743ef0 --- /dev/null +++ b/foundry-docs/src/src/interfaces/IEulerSwapFactory.sol/interface.IEulerSwapFactory.md @@ -0,0 +1,35 @@ +# IEulerSwapFactory +[Git Source](https://github.com/euler-xyz/euler-maglev/blob/d6fc4adb9f1050f1348bfff5db3603f2482ba705/src/interfaces/IEulerSwapFactory.sol) + + +## Functions +### deployPool + + +```solidity +function deployPool(IEulerSwap.Params memory params, IEulerSwap.CurveParams memory curveParams, bytes32 salt) + external + returns (address); +``` + +### allPools + + +```solidity +function allPools(uint256 index) external view returns (address); +``` + +### allPoolsLength + + +```solidity +function allPoolsLength() external view returns (uint256); +``` + +### getAllPoolsListSlice + + +```solidity +function getAllPoolsListSlice(uint256 start, uint256 end) external view returns (address[] memory); +``` + diff --git a/foundry-docs/src/src/interfaces/IEulerSwapPeriphery.sol/interface.IEulerSwapPeriphery.md b/foundry-docs/src/src/interfaces/IEulerSwapPeriphery.sol/interface.IEulerSwapPeriphery.md new file mode 100644 index 0000000..898fa72 --- /dev/null +++ b/foundry-docs/src/src/interfaces/IEulerSwapPeriphery.sol/interface.IEulerSwapPeriphery.md @@ -0,0 +1,49 @@ +# IEulerSwapPeriphery +[Git Source](https://github.com/euler-xyz/euler-maglev/blob/d6fc4adb9f1050f1348bfff5db3603f2482ba705/src/interfaces/IEulerSwapPeriphery.sol) + + +## Functions +### swapExactIn + +Swap `amountIn` of `tokenIn` for `tokenOut`, with at least `amountOutMin` received. + + +```solidity +function swapExactIn(address eulerSwap, address tokenIn, address tokenOut, uint256 amountIn, uint256 amountOutMin) + external; +``` + +### swapExactOut + +Swap `amountOut` of `tokenOut` for `tokenIn`, with at most `amountInMax` paid. + + +```solidity +function swapExactOut(address eulerSwap, address tokenIn, address tokenOut, uint256 amountOut, uint256 amountInMax) + external; +``` + +### quoteExactInput + +How much `tokenOut` can I get for `amountIn` of `tokenIn`? + + +```solidity +function quoteExactInput(address eulerSwap, address tokenIn, address tokenOut, uint256 amountIn) + external + view + returns (uint256); +``` + +### quoteExactOutput + +How much `tokenIn` do I need to get `amountOut` of `tokenOut`? + + +```solidity +function quoteExactOutput(address eulerSwap, address tokenIn, address tokenOut, uint256 amountOut) + external + view + returns (uint256); +``` + diff --git a/foundry-docs/src/src/interfaces/IUniswapV2Callee.sol/interface.IUniswapV2Callee.md b/foundry-docs/src/src/interfaces/IUniswapV2Callee.sol/interface.IUniswapV2Callee.md new file mode 100644 index 0000000..811ac44 --- /dev/null +++ b/foundry-docs/src/src/interfaces/IUniswapV2Callee.sol/interface.IUniswapV2Callee.md @@ -0,0 +1,12 @@ +# IUniswapV2Callee +[Git Source](https://github.com/euler-xyz/euler-maglev/blob/d6fc4adb9f1050f1348bfff5db3603f2482ba705/src/interfaces/IUniswapV2Callee.sol) + + +## Functions +### uniswapV2Call + + +```solidity +function uniswapV2Call(address sender, uint256 amount0, uint256 amount1, bytes calldata data) external; +``` + diff --git a/foundry-docs/src/src/interfaces/README.md b/foundry-docs/src/src/interfaces/README.md new file mode 100644 index 0000000..fdfceb3 --- /dev/null +++ b/foundry-docs/src/src/interfaces/README.md @@ -0,0 +1,7 @@ + + +# Contents +- [IEulerSwap](IEulerSwap.sol/interface.IEulerSwap.md) +- [IEulerSwapFactory](IEulerSwapFactory.sol/interface.IEulerSwapFactory.md) +- [IEulerSwapPeriphery](IEulerSwapPeriphery.sol/interface.IEulerSwapPeriphery.md) +- [IUniswapV2Callee](IUniswapV2Callee.sol/interface.IUniswapV2Callee.md) diff --git a/foundry.toml b/foundry.toml index 25b918f..c46a452 100644 --- a/foundry.toml +++ b/foundry.toml @@ -2,5 +2,22 @@ src = "src" out = "out" libs = ["lib"] +solc = "0.8.27" +optimizer = true +optimizer_runs = 10000 +gas_reports = ["*"] +fs_permissions = [{ access = "read-write", path = "./"}] + +[rpc_endpoints] +mainnet = "${MAINNET_RPC_URL}" +base = "${BASE_RPC_URL}" + +[etherscan] +mainnet = { key = "${ETHERSCAN_MAINNET_API_KEY}" } +base = { key = "${ETHERSCAN_BASE_API_KEY}" } + +[doc] +out = "foundry-docs/" +title = "Euler Swap Contracts Documentation" # See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options diff --git a/lib/ethereum-vault-connector b/lib/ethereum-vault-connector new file mode 160000 index 0000000..4538f07 --- /dev/null +++ b/lib/ethereum-vault-connector @@ -0,0 +1 @@ +Subproject commit 4538f07342d116c6ade2af6d2aeac5398bfb578d diff --git a/lib/euler-vault-kit b/lib/euler-vault-kit new file mode 160000 index 0000000..83481a4 --- /dev/null +++ b/lib/euler-vault-kit @@ -0,0 +1 @@ +Subproject commit 83481a4e95d4d2ee50c95843841be952bbf3f48d diff --git a/lib/forge-std b/lib/forge-std new file mode 160000 index 0000000..1eea5ba --- /dev/null +++ b/lib/forge-std @@ -0,0 +1 @@ +Subproject commit 1eea5bae12ae557d589f9f0f0edae2faa47cb262 diff --git a/lib/openzeppelin-contracts b/lib/openzeppelin-contracts new file mode 160000 index 0000000..acd4ff7 --- /dev/null +++ b/lib/openzeppelin-contracts @@ -0,0 +1 @@ +Subproject commit acd4ff74de833399287ed6b31b4debf6b2b35527 diff --git a/lib/v4-periphery b/lib/v4-periphery new file mode 160000 index 0000000..9628c36 --- /dev/null +++ b/lib/v4-periphery @@ -0,0 +1 @@ +Subproject commit 9628c36b4f5083d19606e63224e4041fe748edae diff --git a/remappings.txt b/remappings.txt new file mode 100644 index 0000000..8773f81 --- /dev/null +++ b/remappings.txt @@ -0,0 +1,7 @@ +openzeppelin-contracts/=lib/openzeppelin-contracts/contracts/ +evc/=lib/ethereum-vault-connector/src/ +evk/=lib/euler-vault-kit/src/ +ethereum-vault-connector/=lib/ethereum-vault-connector/src/ +evk-test/=lib/euler-vault-kit/test/ +permit2/=lib/euler-vault-kit/lib/permit2/ +@uniswap/v4-core/=lib/v4-periphery/lib/v4-core/ \ No newline at end of file diff --git a/script/Counter.s.sol b/script/Counter.s.sol deleted file mode 100644 index cdc1fe9..0000000 --- a/script/Counter.s.sol +++ /dev/null @@ -1,19 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.13; - -import {Script, console} from "forge-std/Script.sol"; -import {Counter} from "../src/Counter.sol"; - -contract CounterScript is Script { - Counter public counter; - - function setUp() public {} - - function run() public { - vm.startBroadcast(); - - counter = new Counter(); - - vm.stopBroadcast(); - } -} diff --git a/script/DeployPool.s.sol b/script/DeployPool.s.sol new file mode 100644 index 0000000..ff60726 --- /dev/null +++ b/script/DeployPool.s.sol @@ -0,0 +1,83 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.0; + +import {ScriptUtil} from "./ScriptUtil.s.sol"; +import {IEulerSwapFactory, IEulerSwap, EulerSwapFactory} from "../src/EulerSwapFactory.sol"; +import {IEVC, IEulerSwap} from "../src/EulerSwap.sol"; +import {HookMiner} from "../test/utils/HookMiner.sol"; +import {Hooks} from "@uniswap/v4-core/src/libraries/Hooks.sol"; +import {MetaProxyDeployer} from "../src/utils/MetaProxyDeployer.sol"; + +/// @title Script to deploy new pool. +contract DeployPool is ScriptUtil { + function run() public { + // load wallet + uint256 eulerAccountKey = vm.envUint("WALLET_PRIVATE_KEY"); + address eulerAccount = vm.rememberKey(eulerAccountKey); + + // load JSON file + string memory inputScriptFileName = "DeployPool_input.json"; + string memory json = _getJsonFile(inputScriptFileName); + + EulerSwapFactory factory = EulerSwapFactory(vm.parseJsonAddress(json, ".factory")); + IEulerSwap.Params memory poolParams = IEulerSwap.Params({ + vault0: vm.parseJsonAddress(json, ".vault0"), + vault1: vm.parseJsonAddress(json, ".vault1"), + eulerAccount: eulerAccount, + equilibriumReserve0: uint112(vm.parseJsonUint(json, ".equilibriumReserve0")), + equilibriumReserve1: uint112(vm.parseJsonUint(json, ".equilibriumReserve1")), + priceX: vm.parseJsonUint(json, ".priceX"), + priceY: vm.parseJsonUint(json, ".priceY"), + concentrationX: vm.parseJsonUint(json, ".concentrationX"), + concentrationY: vm.parseJsonUint(json, ".concentrationY"), + fee: vm.parseJsonUint(json, ".fee"), + protocolFee: vm.parseJsonUint(json, ".protocolFee"), + protocolFeeRecipient: vm.parseJsonAddress(json, ".protocolFeeRecipient") + }); + IEulerSwap.InitialState memory initialState = IEulerSwap.InitialState({ + currReserve0: uint112(vm.parseJsonUint(json, ".currReserve0")), + currReserve1: uint112(vm.parseJsonUint(json, ".currReserve1")) + }); + address eulerSwapImpl = vm.parseJsonAddress(json, ".eulerSwapImplementation"); + + bytes memory creationCode = MetaProxyDeployer.creationCodeMetaProxy(eulerSwapImpl, abi.encode(poolParams)); + (address predictedPoolAddress, bytes32 salt) = HookMiner.find( + address(address(factory)), + eulerAccount, + uint160( + Hooks.BEFORE_INITIALIZE_FLAG | Hooks.BEFORE_SWAP_FLAG | Hooks.BEFORE_SWAP_RETURNS_DELTA_FLAG + | Hooks.BEFORE_ADD_LIQUIDITY_FLAG + ), + creationCode + ); + + IEVC evc = IEVC(factory.EVC()); + IEVC.BatchItem[] memory items = new IEVC.BatchItem[](2); + + items[0] = IEVC.BatchItem({ + onBehalfOfAccount: address(0), + targetContract: address(evc), + value: 0, + data: abi.encodeCall(evc.setAccountOperator, (eulerAccount, predictedPoolAddress, true)) + }); + items[1] = IEVC.BatchItem({ + onBehalfOfAccount: eulerAccount, + targetContract: address(factory), + value: 0, + data: abi.encodeCall(EulerSwapFactory.deployPool, (poolParams, initialState, salt)) + }); + + vm.startBroadcast(eulerAccount); + evc.batch(items); + vm.stopBroadcast(); + + address pool = factory.poolByEulerAccount(eulerAccount); + + string memory outputScriptFileName = "DeployPool_output.json"; + + string memory object; + object = vm.serializeAddress("factory", "deployedPool", pool); + + vm.writeJson(object, string.concat(vm.projectRoot(), "/script/json/out/", outputScriptFileName)); + } +} diff --git a/script/DeployProtocol.s.sol b/script/DeployProtocol.s.sol new file mode 100644 index 0000000..1ffb1fc --- /dev/null +++ b/script/DeployProtocol.s.sol @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.0; + +import {ScriptUtil} from "./ScriptUtil.s.sol"; +import {EulerSwapFactory} from "../src/EulerSwapFactory.sol"; +import {EulerSwapPeriphery} from "../src/EulerSwapPeriphery.sol"; +import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; +import {EulerSwap} from "../src/EulerSwap.sol"; + +/// @title Script to deploy EulerSwapFactory & EulerSwapPeriphery. +contract DeployProtocol is ScriptUtil { + function run() public { + // load wallet + uint256 deployerKey = vm.envUint("WALLET_PRIVATE_KEY"); + address deployerAddress = vm.rememberKey(deployerKey); + + // load JSON file + string memory inputScriptFileName = "DeployProtocol_input.json"; + string memory json = _getJsonFile(inputScriptFileName); + + address evc = vm.parseJsonAddress(json, ".evc"); + address poolManager = vm.parseJsonAddress(json, ".poolManager"); + address evkFactory = vm.parseJsonAddress(json, ".evkFactory"); + address feeOwner = vm.parseJsonAddress(json, ".feeOwner"); + + vm.startBroadcast(deployerAddress); + + address eulerSwapImpl = address(new EulerSwap(evc, poolManager)); + new EulerSwapFactory(evc, evkFactory, eulerSwapImpl, feeOwner); + new EulerSwapPeriphery(); + vm.stopBroadcast(); + } +} diff --git a/script/README.md b/script/README.md new file mode 100644 index 0000000..eee6f22 --- /dev/null +++ b/script/README.md @@ -0,0 +1,24 @@ +# Forge scripts + +Every script takes inputs via a `ScriptName_input.json` file inside the json directory. + +Before running the scripts, please make sure to fill the `.env` file following the `.env.example`. The main env variables for the script to succefully run, are `WALLET_PRIVATE_KEY` and the `NETWORK_RPC_URL`. + +After filling the `.env` file, make sure to run: `source .env` in your terminal. + +## Deploy protocol + +- Fill the `DeployProtocol_input.json` file with the needed inputs. +- Run `forge script ./script/DeployProtocol.s.sol --rpc-url network_name --broadcast --slow` + +## Deploy new pool + +- Fill the `DeployPool_input.json` file with the needed inputs. +- In pool deployment, the `eulerAccount` address is the deployer address, so we derive the address from the attached private key in the `.env` file. +- Run `forge script ./script/DeployPool.s.sol --rpc-url network_name --broadcast --slow` + +## Exact in swap + +- Fill the `SwapExactIn_input.json` file with the needed inputs. +- Run `forge script ./script/SwapExactIn.s.sol --rpc-url network_name --broadcast --slow` + diff --git a/script/ScriptUtil.s.sol b/script/ScriptUtil.s.sol new file mode 100644 index 0000000..45caffb --- /dev/null +++ b/script/ScriptUtil.s.sol @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.0; + +import "forge-std/Script.sol"; + +contract ScriptUtil is Script { + function _getJsonFile(string memory _jsonFile) internal view returns (string memory) { + return vm.readFile(_getJsonFilePath(_jsonFile)); + } + + function _getJsonFilePath(string memory _jsonFile) private view returns (string memory) { + string memory root = vm.projectRoot(); + return string.concat(root, "/script/json/", _jsonFile); + } +} diff --git a/script/SwapExactIn.s.sol b/script/SwapExactIn.s.sol new file mode 100644 index 0000000..f2349c7 --- /dev/null +++ b/script/SwapExactIn.s.sol @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.0; + +import {SafeERC20, IERC20} from "openzeppelin-contracts/token/ERC20/utils/SafeERC20.sol"; + +import {EulerSwap} from "../src/EulerSwap.sol"; +import {EulerSwapPeriphery} from "../src/EulerSwapPeriphery.sol"; + +import {ScriptUtil} from "./ScriptUtil.s.sol"; + +contract SwapExactIn is ScriptUtil { + using SafeERC20 for IERC20; + + function run() public { + // load wallet + uint256 swapperKey = vm.envUint("WALLET_PRIVATE_KEY"); + address swapperAddress = vm.rememberKey(swapperKey); + + // load JSON file + string memory inputScriptFileName = "SwapExactIn_input.json"; + string memory json = _getJsonFile(inputScriptFileName); + + EulerSwapPeriphery periphery = EulerSwapPeriphery(vm.parseJsonAddress(json, ".periphery")); + EulerSwap pool = EulerSwap(vm.parseJsonAddress(json, ".pool")); + address tokenIn = vm.parseJsonAddress(json, ".tokenIn"); + address tokenOut = vm.parseJsonAddress(json, ".tokenOut"); + uint256 amountIn = vm.parseJsonUint(json, ".amountIn"); + + uint256 amountOutMin = periphery.quoteExactInput(address(pool), tokenIn, tokenOut, amountIn); + + vm.startBroadcast(swapperAddress); + + IERC20(tokenIn).forceApprove(address(periphery), amountIn); + + periphery.swapExactIn(address(pool), tokenIn, tokenOut, amountIn, swapperAddress, amountOutMin, 0); + + vm.stopBroadcast(); + } +} diff --git a/script/json/DeployPool_input.json b/script/json/DeployPool_input.json new file mode 100644 index 0000000..3fd506b --- /dev/null +++ b/script/json/DeployPool_input.json @@ -0,0 +1,17 @@ +{ + "factory": "0x52177559e6430396b9A7E2176Ef33b4e4052D125", + "eulerSwapImplementation": "0x0B8CD42911551882638f4C762A66570e1fAc624f", + "vault0": "0xa66957e58b60d6b92b850c8773a9ff9b0ba96a65", + "vault1": "0x4212e01c7c8e1c21dea6030c74ae2084f5337bd1", + "equilibriumReserve0": 2000e6, + "equilibriumReserve1": 2000e6, + "priceX": 1e18, + "priceY": 1e18, + "concentrationX": 0.97e18, + "concentrationY": 0.97e18, + "fee": 0, + "protocolFee": 0, + "protocolFeeRecipient": "0x0000000000000000000000000000000000000000", + "currReserve0": 2000e6, + "currReserve1": 2000e6 +} \ No newline at end of file diff --git a/script/json/DeployProtocol_input.json b/script/json/DeployProtocol_input.json new file mode 100644 index 0000000..8209a02 --- /dev/null +++ b/script/json/DeployProtocol_input.json @@ -0,0 +1,6 @@ +{ + "evc": "0x0C9a3dd6b8F28529d72d7f9cE918D493519EE383", + "poolManager": "0x000000000004444c5dc75cB358380D2e3dE08A90", + "evkFactory": "0x29a56a1b8214D9Cf7c5561811750D5cBDb45CC8e", + "feeOwner": "0x603765f9e9B8E3CBACbdf7A6e963B7EAD77AE86f" +} diff --git a/script/json/SwapExactIn_input.json b/script/json/SwapExactIn_input.json new file mode 100644 index 0000000..63ebd4b --- /dev/null +++ b/script/json/SwapExactIn_input.json @@ -0,0 +1,7 @@ +{ + "periphery": "0x9F27Bc363DB128cdC349CA54671E6Fbe2bE194D0", + "pool": "0x13f627635CD96a2A75c2efBDba979172cAb2E888", + "tokenIn": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "tokenOut": "0xdAC17F958D2ee523a2206206994597C13D831ec7", + "amountIn": 4e6 +} \ No newline at end of file diff --git a/src/Counter.sol b/src/Counter.sol deleted file mode 100644 index aded799..0000000 --- a/src/Counter.sol +++ /dev/null @@ -1,14 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.13; - -contract Counter { - uint256 public number; - - function setNumber(uint256 newNumber) public { - number = newNumber; - } - - function increment() public { - number++; - } -} diff --git a/src/EulerSwap.sol b/src/EulerSwap.sol new file mode 100644 index 0000000..af3534b --- /dev/null +++ b/src/EulerSwap.sol @@ -0,0 +1,184 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.27; + +import {IUniswapV2Callee} from "./interfaces/IUniswapV2Callee.sol"; + +import {EVCUtil} from "evc/utils/EVCUtil.sol"; +import {IEVC} from "evc/interfaces/IEthereumVaultConnector.sol"; +import {IEVault} from "evk/EVault/IEVault.sol"; + +import {IEulerSwap} from "./interfaces/IEulerSwap.sol"; +import {UniswapHook} from "./UniswapHook.sol"; +import {CtxLib} from "./libraries/CtxLib.sol"; +import {FundsLib} from "./libraries/FundsLib.sol"; +import {CurveLib} from "./libraries/CurveLib.sol"; +import {QuoteLib} from "./libraries/QuoteLib.sol"; + +contract EulerSwap is IEulerSwap, EVCUtil, UniswapHook { + bytes32 public constant curve = bytes32("EulerSwap v1"); + + event EulerSwapActivated(address indexed asset0, address indexed asset1); + event Swap( + address indexed sender, + uint256 amount0In, + uint256 amount1In, + uint256 amount0Out, + uint256 amount1Out, + uint112 reserve0, + uint112 reserve1, + address indexed to + ); + + error Locked(); + error AlreadyActivated(); + error BadParam(); + error AmountTooBig(); + error AssetsOutOfOrderOrEqual(); + + constructor(address evc_, address poolManager_) EVCUtil(evc_) UniswapHook(evc_, poolManager_) { + CtxLib.Storage storage s = CtxLib.getStorage(); + + s.status = 2; // can only be used via delegatecall proxy + } + + modifier nonReentrant() { + CtxLib.Storage storage s = CtxLib.getStorage(); + + require(s.status == 1, Locked()); + s.status = 2; + _; + s.status = 1; + } + + modifier nonReentrantView() { + CtxLib.Storage storage s = CtxLib.getStorage(); + require(s.status != 2, Locked()); + + _; + } + + /// @inheritdoc IEulerSwap + function activate(InitialState calldata initialState) external { + CtxLib.Storage storage s = CtxLib.getStorage(); + Params memory p = CtxLib.getParams(); + + require(s.status == 0, AlreadyActivated()); + s.status = 1; + + // Parameter validation + + require(p.fee < 1e18, BadParam()); + require(p.priceX > 0 && p.priceY > 0, BadParam()); + require(p.priceX <= 1e36 && p.priceY <= 1e36, BadParam()); + require(p.concentrationX <= 1e18 && p.concentrationY <= 1e18, BadParam()); + + { + address asset0Addr = IEVault(p.vault0).asset(); + address asset1Addr = IEVault(p.vault1).asset(); + require(asset0Addr < asset1Addr, AssetsOutOfOrderOrEqual()); + emit EulerSwapActivated(asset0Addr, asset1Addr); + } + + // Initial state + + s.reserve0 = initialState.currReserve0; + s.reserve1 = initialState.currReserve1; + + require(CurveLib.verify(p, s.reserve0, s.reserve1), CurveLib.CurveViolation()); + if (s.reserve0 != 0) require(!CurveLib.verify(p, s.reserve0 - 1, s.reserve1), CurveLib.CurveViolation()); + if (s.reserve1 != 0) require(!CurveLib.verify(p, s.reserve0, s.reserve1 - 1), CurveLib.CurveViolation()); + + // Configure external contracts + + FundsLib.approveVault(p.vault0); + FundsLib.approveVault(p.vault1); + + IEVC(evc).enableCollateral(p.eulerAccount, p.vault0); + IEVC(evc).enableCollateral(p.eulerAccount, p.vault1); + + // Uniswap hooks + + if (address(poolManager) != address(0)) activateHook(p); + } + + /// @inheritdoc IEulerSwap + function getParams() external pure returns (Params memory) { + return CtxLib.getParams(); + } + + /// @inheritdoc IEulerSwap + function getAssets() external view returns (address asset0, address asset1) { + Params memory p = CtxLib.getParams(); + + asset0 = IEVault(p.vault0).asset(); + asset1 = IEVault(p.vault1).asset(); + } + + /// @inheritdoc IEulerSwap + function getReserves() external view nonReentrantView returns (uint112, uint112, uint32) { + CtxLib.Storage storage s = CtxLib.getStorage(); + + return (s.reserve0, s.reserve1, s.status); + } + + /// @inheritdoc IEulerSwap + function computeQuote(address tokenIn, address tokenOut, uint256 amount, bool exactIn) + external + view + nonReentrantView + returns (uint256) + { + Params memory p = CtxLib.getParams(); + + return QuoteLib.computeQuote(address(evc), p, QuoteLib.checkTokens(p, tokenIn, tokenOut), amount, exactIn); + } + + /// @inheritdoc IEulerSwap + function getLimits(address tokenIn, address tokenOut) external view nonReentrantView returns (uint256, uint256) { + Params memory p = CtxLib.getParams(); + + if (!evc.isAccountOperatorAuthorized(p.eulerAccount, address(this))) return (0, 0); + + return QuoteLib.calcLimits(p, QuoteLib.checkTokens(p, tokenIn, tokenOut)); + } + + /// @inheritdoc IEulerSwap + function swap(uint256 amount0Out, uint256 amount1Out, address to, bytes calldata data) + external + callThroughEVC + nonReentrant + { + require(amount0Out <= type(uint112).max && amount1Out <= type(uint112).max, AmountTooBig()); + + CtxLib.Storage storage s = CtxLib.getStorage(); + Params memory p = CtxLib.getParams(); + + // Optimistically send tokens + + if (amount0Out > 0) FundsLib.withdrawAssets(address(evc), p, p.vault0, amount0Out, to); + if (amount1Out > 0) FundsLib.withdrawAssets(address(evc), p, p.vault1, amount1Out, to); + + // Invoke callback + + if (data.length > 0) IUniswapV2Callee(to).uniswapV2Call(_msgSender(), amount0Out, amount1Out, data); + + // Deposit all available funds, adjust received amounts downward to collect fees + + uint256 amount0In = FundsLib.depositAssets(address(evc), p, p.vault0); + uint256 amount1In = FundsLib.depositAssets(address(evc), p, p.vault1); + + // Verify curve invariant is satisfied + + { + uint256 newReserve0 = s.reserve0 + amount0In - amount0Out; + uint256 newReserve1 = s.reserve1 + amount1In - amount1Out; + + require(CurveLib.verify(p, newReserve0, newReserve1), CurveLib.CurveViolation()); + + s.reserve0 = uint112(newReserve0); + s.reserve1 = uint112(newReserve1); + } + + emit Swap(_msgSender(), amount0In, amount1In, amount0Out, amount1Out, s.reserve0, s.reserve1, to); + } +} diff --git a/src/EulerSwapFactory.sol b/src/EulerSwapFactory.sol new file mode 100644 index 0000000..00cfbb5 --- /dev/null +++ b/src/EulerSwapFactory.sol @@ -0,0 +1,211 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.27; + +import {EnumerableSet} from "openzeppelin-contracts/utils/structs/EnumerableSet.sol"; + +import {IEulerSwapFactory, IEulerSwap} from "./interfaces/IEulerSwapFactory.sol"; +import {EVCUtil} from "ethereum-vault-connector/utils/EVCUtil.sol"; +import {GenericFactory} from "evk/GenericFactory/GenericFactory.sol"; + +import {EulerSwap} from "./EulerSwap.sol"; +import {ProtocolFee} from "./utils/ProtocolFee.sol"; +import {MetaProxyDeployer} from "./utils/MetaProxyDeployer.sol"; + +/// @title EulerSwapFactory contract +/// @custom:security-contact security@euler.xyz +/// @author Euler Labs (https://www.eulerlabs.com/) +contract EulerSwapFactory is IEulerSwapFactory, EVCUtil, ProtocolFee { + using EnumerableSet for EnumerableSet.AddressSet; + + /// @dev Vaults must be deployed by this factory + address public immutable evkFactory; + /// @dev The EulerSwap code instance that will be proxied to + address public immutable eulerSwapImpl; + + /// @dev Mapping from euler account to pool, if installed + mapping(address eulerAccount => address) internal installedPools; + /// @dev Set of all pool addresses + EnumerableSet.AddressSet internal allPools; + /// @dev Mapping from sorted pair of underlyings to set of pools + mapping(address asset0 => mapping(address asset1 => EnumerableSet.AddressSet)) internal poolMap; + + event PoolDeployed(address indexed asset0, address indexed asset1, address indexed eulerAccount, address pool); + event PoolConfig(address indexed pool, IEulerSwap.Params params, IEulerSwap.InitialState initialState); + event PoolUninstalled(address indexed asset0, address indexed asset1, address indexed eulerAccount, address pool); + + error InvalidQuery(); + error Unauthorized(); + error OldOperatorStillInstalled(); + error OperatorNotInstalled(); + error InvalidVaultImplementation(); + error SliceOutOfBounds(); + error InvalidProtocolFee(); + + constructor(address evc, address evkFactory_, address eulerSwapImpl_, address feeOwner_) + EVCUtil(evc) + ProtocolFee(feeOwner_) + { + evkFactory = evkFactory_; + eulerSwapImpl = eulerSwapImpl_; + } + + /// @inheritdoc IEulerSwapFactory + function deployPool(IEulerSwap.Params memory params, IEulerSwap.InitialState memory initialState, bytes32 salt) + external + returns (address) + { + require(_msgSender() == params.eulerAccount, Unauthorized()); + require( + GenericFactory(evkFactory).isProxy(params.vault0) && GenericFactory(evkFactory).isProxy(params.vault1), + InvalidVaultImplementation() + ); + require( + params.protocolFee == protocolFee && params.protocolFeeRecipient == protocolFeeRecipient, + InvalidProtocolFee() + ); + + uninstall(params.eulerAccount); + + EulerSwap pool = EulerSwap(MetaProxyDeployer.deployMetaProxy(eulerSwapImpl, abi.encode(params), salt)); + + updateEulerAccountState(params.eulerAccount, address(pool)); + + pool.activate(initialState); + + (address asset0, address asset1) = pool.getAssets(); + emit PoolDeployed(asset0, asset1, params.eulerAccount, address(pool)); + emit PoolConfig(address(pool), params, initialState); + + return address(pool); + } + + /// @inheritdoc IEulerSwapFactory + function uninstallPool() external { + uninstall(_msgSender()); + } + + /// @inheritdoc IEulerSwapFactory + function computePoolAddress(IEulerSwap.Params memory poolParams, bytes32 salt) external view returns (address) { + return address( + uint160( + uint256( + keccak256( + abi.encodePacked( + bytes1(0xff), + address(this), + salt, + keccak256(MetaProxyDeployer.creationCodeMetaProxy(eulerSwapImpl, abi.encode(poolParams))) + ) + ) + ) + ) + ); + } + + /// @inheritdoc IEulerSwapFactory + function poolByEulerAccount(address eulerAccount) external view returns (address) { + return installedPools[eulerAccount]; + } + + /// @inheritdoc IEulerSwapFactory + function poolsLength() external view returns (uint256) { + return allPools.length(); + } + + /// @inheritdoc IEulerSwapFactory + function poolsSlice(uint256 start, uint256 end) external view returns (address[] memory) { + return _getSlice(allPools, start, end); + } + + /// @inheritdoc IEulerSwapFactory + function pools() external view returns (address[] memory) { + return allPools.values(); + } + + /// @inheritdoc IEulerSwapFactory + function poolsByPairLength(address asset0, address asset1) external view returns (uint256) { + return poolMap[asset0][asset1].length(); + } + + /// @inheritdoc IEulerSwapFactory + function poolsByPairSlice(address asset0, address asset1, uint256 start, uint256 end) + external + view + returns (address[] memory) + { + return _getSlice(poolMap[asset0][asset1], start, end); + } + + /// @inheritdoc IEulerSwapFactory + function poolsByPair(address asset0, address asset1) external view returns (address[] memory) { + return poolMap[asset0][asset1].values(); + } + + /// @notice Validates operator authorization for euler account and update the relevant EulerAccountState. + /// @param eulerAccount The address of the euler account. + /// @param newOperator The address of the new pool. + function updateEulerAccountState(address eulerAccount, address newOperator) internal { + require(evc.isAccountOperatorAuthorized(eulerAccount, newOperator), OperatorNotInstalled()); + + (address asset0, address asset1) = _getAssets(newOperator); + + installedPools[eulerAccount] = newOperator; + + allPools.add(newOperator); + poolMap[asset0][asset1].add(newOperator); + } + + /// @notice Uninstalls the pool associated with the given Euler account + /// @dev This function removes the pool from the factory's tracking and emits a PoolUninstalled event + /// @dev The function checks if the operator is still installed and reverts if it is + /// @dev If no pool exists for the account, the function returns without any action + /// @param eulerAccount The address of the Euler account whose pool should be uninstalled + function uninstall(address eulerAccount) internal { + address pool = installedPools[eulerAccount]; + + if (pool == address(0)) return; + + require(!evc.isAccountOperatorAuthorized(eulerAccount, pool), OldOperatorStillInstalled()); + + (address asset0, address asset1) = _getAssets(pool); + + allPools.remove(pool); + poolMap[asset0][asset1].remove(pool); + + delete installedPools[eulerAccount]; + + emit PoolUninstalled(asset0, asset1, eulerAccount, pool); + } + + /// @notice Retrieves the asset addresses for a given pool + /// @dev Calls the pool contract to get its asset0 and asset1 addresses + /// @param pool The address of the pool to query + /// @return The addresses of asset0 and asset1 in the pool + function _getAssets(address pool) internal view returns (address, address) { + return IEulerSwap(pool).getAssets(); + } + + /// @notice Returns a slice of an array of addresses + /// @dev Creates a new memory array containing elements from start to end index + /// If end is type(uint256).max, it will return all elements from start to the end of the array + /// @param arr The storage array to slice + /// @param start The starting index of the slice (inclusive) + /// @param end The ending index of the slice (exclusive) + /// @return A new memory array containing the requested slice of addresses + function _getSlice(EnumerableSet.AddressSet storage arr, uint256 start, uint256 end) + internal + view + returns (address[] memory) + { + uint256 length = arr.length(); + if (end == type(uint256).max) end = length; + if (end < start || end > length) revert SliceOutOfBounds(); + + address[] memory slice = new address[](end - start); + for (uint256 i; i < end - start; ++i) { + slice[i] = arr.at(start + i); + } + + return slice; + } +} diff --git a/src/EulerSwapPeriphery.sol b/src/EulerSwapPeriphery.sol new file mode 100644 index 0000000..66d94f3 --- /dev/null +++ b/src/EulerSwapPeriphery.sol @@ -0,0 +1,99 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.27; + +import {SafeERC20, IERC20} from "openzeppelin-contracts/token/ERC20/utils/SafeERC20.sol"; + +import {IEVC} from "evc/interfaces/IEthereumVaultConnector.sol"; +import {IEVault} from "evk/EVault/IEVault.sol"; +import {IEulerSwapPeriphery} from "./interfaces/IEulerSwapPeriphery.sol"; +import {IEulerSwap} from "./interfaces/IEulerSwap.sol"; + +contract EulerSwapPeriphery is IEulerSwapPeriphery { + using SafeERC20 for IERC20; + + error AmountOutLessThanMin(); + error AmountInMoreThanMax(); + error DeadlineExpired(); + + /// @inheritdoc IEulerSwapPeriphery + function swapExactIn( + address eulerSwap, + address tokenIn, + address tokenOut, + uint256 amountIn, + address receiver, + uint256 amountOutMin, + uint256 deadline + ) external { + require(deadline == 0 || deadline >= block.timestamp, DeadlineExpired()); + + uint256 amountOut = IEulerSwap(eulerSwap).computeQuote(tokenIn, tokenOut, amountIn, true); + + require(amountOut >= amountOutMin, AmountOutLessThanMin()); + + swap(IEulerSwap(eulerSwap), tokenIn, tokenOut, amountIn, amountOut, receiver); + } + + /// @inheritdoc IEulerSwapPeriphery + function swapExactOut( + address eulerSwap, + address tokenIn, + address tokenOut, + uint256 amountOut, + address receiver, + uint256 amountInMax, + uint256 deadline + ) external { + require(deadline == 0 || deadline >= block.timestamp, DeadlineExpired()); + + uint256 amountIn = IEulerSwap(eulerSwap).computeQuote(tokenIn, tokenOut, amountOut, false); + + require(amountIn <= amountInMax, AmountInMoreThanMax()); + + swap(IEulerSwap(eulerSwap), tokenIn, tokenOut, amountIn, amountOut, receiver); + } + + /// @inheritdoc IEulerSwapPeriphery + function quoteExactInput(address eulerSwap, address tokenIn, address tokenOut, uint256 amountIn) + external + view + returns (uint256) + { + return IEulerSwap(eulerSwap).computeQuote(tokenIn, tokenOut, amountIn, true); + } + + /// @inheritdoc IEulerSwapPeriphery + function quoteExactOutput(address eulerSwap, address tokenIn, address tokenOut, uint256 amountOut) + external + view + returns (uint256) + { + return IEulerSwap(eulerSwap).computeQuote(tokenIn, tokenOut, amountOut, false); + } + + /// @inheritdoc IEulerSwapPeriphery + function getLimits(address eulerSwap, address tokenIn, address tokenOut) external view returns (uint256, uint256) { + return IEulerSwap(eulerSwap).getLimits(tokenIn, tokenOut); + } + + /// @dev Internal function to execute a token swap through EulerSwap + /// @param eulerSwap The EulerSwap contract address to execute the swap through + /// @param tokenIn The address of the input token being swapped + /// @param tokenOut The address of the output token being received + /// @param amountIn The amount of input tokens to swap + /// @param amountOut The amount of output tokens to receive + /// @param receiver The address that should receive the swap output + function swap( + IEulerSwap eulerSwap, + address tokenIn, + address tokenOut, + uint256 amountIn, + uint256 amountOut, + address receiver + ) internal { + IERC20(tokenIn).safeTransferFrom(msg.sender, address(eulerSwap), amountIn); + + bool isAsset0In = tokenIn < tokenOut; + (isAsset0In) ? eulerSwap.swap(0, amountOut, receiver, "") : eulerSwap.swap(amountOut, 0, receiver, ""); + } +} diff --git a/src/UniswapHook.sol b/src/UniswapHook.sol new file mode 100644 index 0000000..f5ab544 --- /dev/null +++ b/src/UniswapHook.sol @@ -0,0 +1,165 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.27; + +import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; +import {BaseHook} from "v4-periphery/src/utils/BaseHook.sol"; +import {PoolKey} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; +import {Currency} from "@uniswap/v4-core/src/types/Currency.sol"; +import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol"; +import {Hooks} from "@uniswap/v4-core/src/libraries/Hooks.sol"; +import {SafeCast} from "@uniswap/v4-core/src/libraries/SafeCast.sol"; +import { + BeforeSwapDelta, toBeforeSwapDelta, BeforeSwapDeltaLibrary +} from "@uniswap/v4-core/src/types/BeforeSwapDelta.sol"; + +import {IEVault} from "evk/EVault/IEVault.sol"; + +import {IEulerSwap} from "./interfaces/IEulerSwap.sol"; +import {CtxLib} from "./libraries/CtxLib.sol"; +import {QuoteLib} from "./libraries/QuoteLib.sol"; +import {CurveLib} from "./libraries/CurveLib.sol"; +import {FundsLib} from "./libraries/FundsLib.sol"; + +contract UniswapHook is BaseHook { + using SafeCast for uint256; + + address private immutable evc; + + PoolKey internal _poolKey; + + error AlreadyInitialized(); + error NativeConcentratedLiquidityUnsupported(); + + constructor(address evc_, address _poolManager) BaseHook(IPoolManager(_poolManager)) { + evc = evc_; + } + + function activateHook(IEulerSwap.Params memory p) internal { + Hooks.validateHookPermissions(this, getHookPermissions()); + + address asset0Addr = IEVault(p.vault0).asset(); + address asset1Addr = IEVault(p.vault1).asset(); + + // convert fee in WAD to pips. 0.003e18 / 1e12 = 3000 = 0.30% + uint24 fee = uint24(p.fee / 1e12); + + _poolKey = PoolKey({ + currency0: Currency.wrap(asset0Addr), + currency1: Currency.wrap(asset1Addr), + fee: fee, + tickSpacing: 1, // hard-coded tick spacing, as its unused + hooks: IHooks(address(this)) + }); + + // create the pool on v4, using starting price as sqrtPrice(1/1) * Q96 + poolManager.initialize(_poolKey, 79228162514264337593543950336); + } + + /// @dev Helper function to return the poolKey as its struct type + function poolKey() external view returns (PoolKey memory) { + return _poolKey; + } + + /// @dev Prevent hook address validation in constructor, which is not needed + /// because hook instances are proxies. Instead, the address is validated + /// in activateHook(). + function validateHookAddress(BaseHook _this) internal pure override {} + + function _beforeSwap(address, PoolKey calldata key, IPoolManager.SwapParams calldata params, bytes calldata) + internal + override + returns (bytes4, BeforeSwapDelta, uint24) + { + IEulerSwap.Params memory p = CtxLib.getParams(); + + uint256 amountInWithoutFee; + uint256 amountOut; + BeforeSwapDelta returnDelta; + + { + uint256 amountIn; + bool isExactInput = params.amountSpecified < 0; + if (isExactInput) { + amountIn = uint256(-params.amountSpecified); + amountOut = QuoteLib.computeQuote(evc, p, params.zeroForOne, uint256(-params.amountSpecified), true); + } else { + amountIn = QuoteLib.computeQuote(evc, p, params.zeroForOne, uint256(params.amountSpecified), false); + amountOut = uint256(params.amountSpecified); + } + + // return the delta to the PoolManager, so it can process the accounting + // exact input: + // specifiedDelta = positive, to offset the input token taken by the hook (negative delta) + // unspecifiedDelta = negative, to offset the credit of the output token paid by the hook (positive delta) + // exact output: + // specifiedDelta = negative, to offset the output token paid by the hook (positive delta) + // unspecifiedDelta = positive, to offset the input token taken by the hook (negative delta) + returnDelta = isExactInput + ? toBeforeSwapDelta(amountIn.toInt128(), -(amountOut.toInt128())) + : toBeforeSwapDelta(-(amountOut.toInt128()), amountIn.toInt128()); + + // take the input token, from the PoolManager to the Euler vault + // the debt will be paid by the swapper via the swap router + poolManager.take(params.zeroForOne ? key.currency0 : key.currency1, address(this), amountIn); + amountInWithoutFee = FundsLib.depositAssets(evc, p, params.zeroForOne ? p.vault0 : p.vault1); + + // pay the output token, to the PoolManager from an Euler vault + // the credit will be forwarded to the swap router, which then forwards it to the swapper + poolManager.sync(params.zeroForOne ? key.currency1 : key.currency0); + FundsLib.withdrawAssets(evc, p, params.zeroForOne ? p.vault1 : p.vault0, amountOut, address(poolManager)); + poolManager.settle(); + } + + { + CtxLib.Storage storage s = CtxLib.getStorage(); + + uint256 newReserve0 = params.zeroForOne ? (s.reserve0 + amountInWithoutFee) : (s.reserve0 - amountOut); + uint256 newReserve1 = !params.zeroForOne ? (s.reserve1 + amountInWithoutFee) : (s.reserve1 - amountOut); + + require(newReserve0 <= type(uint112).max && newReserve1 <= type(uint112).max, CurveLib.Overflow()); + require(CurveLib.verify(p, newReserve0, newReserve1), CurveLib.CurveViolation()); + + s.reserve0 = uint112(newReserve0); + s.reserve1 = uint112(newReserve1); + } + + return (BaseHook.beforeSwap.selector, returnDelta, 0); + } + + /// @dev Each deployed hook only services one pair and prevent subsequent initializations + function _beforeInitialize(address, PoolKey calldata, uint160) internal view override returns (bytes4) { + // when the hook is deployed for the first time, the internal _poolKey is empty + // upon activation, the internal _poolKey is initialized and set + // once the hook contract is activated, do not allow subsequent initializations + require(_poolKey.tickSpacing == 0, AlreadyInitialized()); + return BaseHook.beforeInitialize.selector; + } + + function _beforeAddLiquidity(address, PoolKey calldata, IPoolManager.ModifyLiquidityParams calldata, bytes calldata) + internal + pure + override + returns (bytes4) + { + revert NativeConcentratedLiquidityUnsupported(); + } + + function getHookPermissions() public pure override returns (Hooks.Permissions memory) { + return Hooks.Permissions({ + beforeInitialize: true, + afterInitialize: false, + beforeAddLiquidity: true, + afterAddLiquidity: false, + beforeRemoveLiquidity: false, + afterRemoveLiquidity: false, + beforeSwap: true, + afterSwap: false, + beforeDonate: false, + afterDonate: false, + beforeSwapReturnDelta: true, + afterSwapReturnDelta: false, + afterAddLiquidityReturnDelta: false, + afterRemoveLiquidityReturnDelta: false + }); + } +} diff --git a/src/interfaces/IEulerSwap.sol b/src/interfaces/IEulerSwap.sol new file mode 100644 index 0000000..c94558a --- /dev/null +++ b/src/interfaces/IEulerSwap.sol @@ -0,0 +1,66 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity >=0.8.0; + +interface IEulerSwap { + /// @dev Immutable pool parameters. Passed to the instance via proxy trailing data. + struct Params { + // Entities + address vault0; + address vault1; + address eulerAccount; + // Curve + uint112 equilibriumReserve0; + uint112 equilibriumReserve1; + uint256 priceX; + uint256 priceY; + uint256 concentrationX; + uint256 concentrationY; + // Fees + uint256 fee; + uint256 protocolFee; + address protocolFeeRecipient; + } + + /// @dev Starting configuration of pool storage. + struct InitialState { + uint112 currReserve0; + uint112 currReserve1; + } + + /// @notice Performs initial activation setup, such as approving vaults to access the + /// EulerSwap instance's tokens, enabling vaults as collateral, setting up Uniswap + /// hooks, etc. This should only be invoked by the factory. + function activate(InitialState calldata initialState) external; + + /// @notice Retrieves the pool's immutable parameters. + function getParams() external view returns (Params memory); + + /// @notice Retrieves the underlying assets supported by this pool. + function getAssets() external view returns (address asset0, address asset1); + + /// @notice Retrieves the current reserves from storage, along with the pool's lock status. + /// @return reserve0 The amount of asset0 in the pool + /// @return reserve1 The amount of asset1 in the pool + /// @return status The status of the pool (0 = unactivated, 1 = unlocked, 2 = locked) + function getReserves() external view returns (uint112 reserve0, uint112 reserve1, uint32 status); + + /// @notice Generates a quote for how much a given size swap will cost. + /// @param tokenIn The input token that the swapper SENDS + /// @param tokenOut The output token that the swapper GETS + /// @param amount The quantity of input or output tokens, for exact input and exact output swaps respectively + /// @param exactIn True if this is an exact input swap, false if exact output + /// @return The quoted quantity of output or input tokens, for exact input and exact output swaps respectively + function computeQuote(address tokenIn, address tokenOut, uint256 amount, bool exactIn) + external + view + returns (uint256); + + /// @notice Upper-bounds on the amounts of each token that this pool can currently support swaps for. + function getLimits(address tokenIn, address tokenOut) external view returns (uint256, uint256); + + /// @notice Optimistically sends the requested amounts of tokens to the `to` + /// address, invokes `uniswapV2Call` callback on `to` (if `data` was provided), + /// and then verifies that a sufficient amount of tokens were transferred to + /// satisfy the swapping curve invariant. + function swap(uint256 amount0Out, uint256 amount1Out, address to, bytes calldata data) external; +} diff --git a/src/interfaces/IEulerSwapFactory.sol b/src/interfaces/IEulerSwapFactory.sol new file mode 100644 index 0000000..bdd4c34 --- /dev/null +++ b/src/interfaces/IEulerSwapFactory.sol @@ -0,0 +1,82 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity >=0.8.0; + +import {IEulerSwap} from "./IEulerSwap.sol"; + +interface IEulerSwapFactory { + /// @notice Deploy a new EulerSwap pool with the given parameters + /// @dev The pool address is deterministically generated using CREATE2 with a salt derived from + /// the euler account address and provided salt parameter. This allows the pool address to be + /// predicted before deployment. + /// @param params Core pool parameters including vaults, account, fees, and curve shape + /// @param initialState Initial state of the pool + /// @param salt Unique value to generate deterministic pool address + /// @return Address of the newly deployed pool + function deployPool(IEulerSwap.Params memory params, IEulerSwap.InitialState memory initialState, bytes32 salt) + external + returns (address); + + /// @notice Uninstalls the pool associated with the Euler account + /// @dev This function removes the pool from the factory's tracking and emits a PoolUninstalled event + /// @dev The function can only be called by the Euler account that owns the pool + /// @dev If no pool is installed for the caller, the function returns without any action + function uninstallPool() external; + + /// @notice Compute the address of a new EulerSwap pool with the given parameters + /// @dev The pool address is deterministically generated using CREATE2 with a salt derived from + /// the euler account address and provided salt parameter. This allows the pool address to be + /// predicted before deployment. + /// @param poolParams Core pool parameters including vaults, account, and fee settings + /// @param salt Unique value to generate deterministic pool address + /// @return Address of the newly deployed pool + function computePoolAddress(IEulerSwap.Params memory poolParams, bytes32 salt) external view returns (address); + + /// @notice Returns a slice of all deployed pools + /// @dev Returns a subset of the pools array from start to end index + /// @param start The starting index of the slice (inclusive) + /// @param end The ending index of the slice (exclusive) + /// @return An array containing the requested slice of pool addresses + function poolsSlice(uint256 start, uint256 end) external view returns (address[] memory); + + /// @notice Returns all deployed pools + /// @dev Returns the complete array of all pool addresses + /// @return An array containing all pool addresses + function pools() external view returns (address[] memory); + + /// @notice Returns the number of pools for a specific asset pair + /// @dev Returns the length of the pool array for the given asset pair + /// @param asset0 The address of the first asset + /// @param asset1 The address of the second asset + /// @return The number of pools for the specified asset pair + function poolsByPairLength(address asset0, address asset1) external view returns (uint256); + + /// @notice Returns a slice of pools for a specific asset pair + /// @dev Returns a subset of the pools array for the given asset pair from start to end index + /// @param asset0 The address of the first asset + /// @param asset1 The address of the second asset + /// @param start The starting index of the slice (inclusive) + /// @param end The ending index of the slice (exclusive) + /// @return An array containing the requested slice of pool addresses for the asset pair + function poolsByPairSlice(address asset0, address asset1, uint256 start, uint256 end) + external + view + returns (address[] memory); + + /// @notice Returns all pools for a specific asset pair + /// @dev Returns the complete array of pool addresses for the given asset pair + /// @param asset0 The address of the first asset + /// @param asset1 The address of the second asset + /// @return An array containing all pool addresses for the specified asset pair + function poolsByPair(address asset0, address asset1) external view returns (address[] memory); + + /// @notice Returns the pool address associated with a specific holder + /// @dev Returns the pool address from the EulerAccountState mapping for the given holder + /// @param who The address of the holder to query + /// @return The address of the pool associated with the holder + function poolByEulerAccount(address who) external view returns (address); + + /// @notice Returns the total number of deployed pools + /// @dev Returns the length of the allPools array + /// @return The total number of pools deployed through the factory + function poolsLength() external view returns (uint256); +} diff --git a/src/interfaces/IEulerSwapPeriphery.sol b/src/interfaces/IEulerSwapPeriphery.sol new file mode 100644 index 0000000..bc9337a --- /dev/null +++ b/src/interfaces/IEulerSwapPeriphery.sol @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity >=0.8.0; + +interface IEulerSwapPeriphery { + /// @notice Swap `amountIn` of `tokenIn` for `tokenOut`, with at least `amountOutMin` received. + /// Output tokens are sent to `receiver`. The swap will fail after `deadline` (unless `deadline` is 0). + /// IMPORTANT: `eulerSwap` must be a trusted contract, for example created by a trusted factory. + function swapExactIn( + address eulerSwap, + address tokenIn, + address tokenOut, + uint256 amountIn, + address receiver, + uint256 amountOutMin, + uint256 deadline + ) external; + + /// @notice Swap `amountOut` of `tokenOut` for `tokenIn`, with at most `amountInMax` paid. + /// Output tokens are sent to `receiver`. The swap will fail after `deadline` (unless `deadline` is 0). + /// IMPORTANT: `eulerSwap` must be a trusted contract, for example created by a trusted factory. + function swapExactOut( + address eulerSwap, + address tokenIn, + address tokenOut, + uint256 amountOut, + address receiver, + uint256 amountInMax, + uint256 deadline + ) external; + + /// @notice How much `tokenOut` can I get for `amountIn` of `tokenIn`? + function quoteExactInput(address eulerSwap, address tokenIn, address tokenOut, uint256 amountIn) + external + view + returns (uint256); + + /// @notice How much `tokenIn` do I need to get `amountOut` of `tokenOut`? + function quoteExactOutput(address eulerSwap, address tokenIn, address tokenOut, uint256 amountOut) + external + view + returns (uint256); + + /// @notice Upper-bound on the max amount that can be sold of tokenIn and bought of tokenOut + function getLimits(address eulerSwap, address tokenIn, address tokenOut) external view returns (uint256, uint256); +} diff --git a/src/interfaces/IUniswapV2Callee.sol b/src/interfaces/IUniswapV2Callee.sol new file mode 100644 index 0000000..4c99ebb --- /dev/null +++ b/src/interfaces/IUniswapV2Callee.sol @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity >=0.8.0; + +interface IUniswapV2Callee { + function uniswapV2Call(address sender, uint256 amount0, uint256 amount1, bytes calldata data) external; +} diff --git a/src/libraries/CtxLib.sol b/src/libraries/CtxLib.sol new file mode 100644 index 0000000..ed11e77 --- /dev/null +++ b/src/libraries/CtxLib.sol @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.27; + +import {IEulerSwap} from "../interfaces/IEulerSwap.sol"; + +library CtxLib { + struct Storage { + uint112 reserve0; + uint112 reserve1; + uint32 status; // 0 = unactivated, 1 = unlocked, 2 = locked + } + + // keccak256("eulerSwap.storage") + bytes32 internal constant CtxStorageLocation = 0xae890085f98619e96ae34ba28d74baa4a4f79785b58fd4afcd3dc0338b79df91; + + function getStorage() internal pure returns (Storage storage s) { + assembly { + s.slot := CtxStorageLocation + } + } + + error InsufficientCalldata(); + + /// @dev Unpacks encoded Params from trailing calldata. Loosely based on + /// the implementation from EIP-3448 (except length is hard-coded). + /// 384 is the size of the Params struct after ABI encoding. + function getParams() internal pure returns (IEulerSwap.Params memory p) { + require(msg.data.length >= 384, InsufficientCalldata()); + unchecked { + return abi.decode(msg.data[msg.data.length - 384:], (IEulerSwap.Params)); + } + } +} diff --git a/src/libraries/CurveLib.sol b/src/libraries/CurveLib.sol new file mode 100644 index 0000000..f818d50 --- /dev/null +++ b/src/libraries/CurveLib.sol @@ -0,0 +1,117 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.27; + +import {Math} from "openzeppelin-contracts/utils/math/Math.sol"; + +import {IEulerSwap} from "../interfaces/IEulerSwap.sol"; + +library CurveLib { + error Overflow(); + error CurveViolation(); + + /// @notice Returns true if the specified reserve amounts would be acceptable, false otherwise. + /// Acceptable points are on, or above and to-the-right of the swapping curve. + function verify(IEulerSwap.Params memory p, uint256 newReserve0, uint256 newReserve1) + internal + pure + returns (bool) + { + if (newReserve0 > type(uint112).max || newReserve1 > type(uint112).max) return false; + + if (newReserve0 >= p.equilibriumReserve0) { + if (newReserve1 >= p.equilibriumReserve1) return true; + return newReserve0 + >= f(newReserve1, p.priceY, p.priceX, p.equilibriumReserve1, p.equilibriumReserve0, p.concentrationY); + } else { + if (newReserve1 < p.equilibriumReserve1) return false; + return newReserve1 + >= f(newReserve0, p.priceX, p.priceY, p.equilibriumReserve0, p.equilibriumReserve1, p.concentrationX); + } + } + + /// @dev EulerSwap curve definition + /// Pre-conditions: 0 < x <= x0, 1 <= {px,py} <= 1e36, {x0,y0} <= type(uint112).max, c <= 1e18 + function f(uint256 x, uint256 px, uint256 py, uint256 x0, uint256 y0, uint256 c) internal pure returns (uint256) { + unchecked { + uint256 v = Math.mulDiv(px * (x0 - x), c * x + (1e18 - c) * x0, x * 1e18, Math.Rounding.Ceil); + require(v <= type(uint248).max, Overflow()); + return y0 + (v + (py - 1)) / py; + } + } + + /// @dev EulerSwap inverse function definition + /// Pre-conditions: 0 < x <= x0, 1 <= {px,py} <= 1e36, {x0,y0} <= type(uint112).max, c <= 1e18 + function fInverse(uint256 y, uint256 px, uint256 py, uint256 x0, uint256 y0, uint256 c) + internal + pure + returns (uint256) + { + // components of quadratic equation + int256 B; + uint256 C; + uint256 fourAC; + + unchecked { + int256 term1 = int256(Math.mulDiv(py * 1e18, y - y0, px, Math.Rounding.Ceil)); // scale: 1e36 + int256 term2 = (2 * int256(c) - int256(1e18)) * int256(x0); // scale: 1e36 + B = (term1 - term2) / int256(1e18); // scale: 1e18 + C = Math.mulDiv(1e18 - c, x0 * x0, 1e18, Math.Rounding.Ceil); // scale: 1e36 + fourAC = Math.mulDiv(4 * c, C, 1e18, Math.Rounding.Ceil); // scale: 1e36 + } + + uint256 absB = uint256(B >= 0 ? B : -B); + uint256 squaredB; + uint256 discriminant; + uint256 sqrt; + if (absB < 1e36) { + // B^2 can be calculated directly at 1e18 scale without overflowing + unchecked { + squaredB = absB * absB; // scale: 1e36 + discriminant = squaredB + fourAC; // scale: 1e36 + sqrt = Math.sqrt(discriminant); // scale: 1e18 + sqrt = (sqrt * sqrt < discriminant) ? sqrt + 1 : sqrt; + } + } else { + // B^2 cannot be calculated directly at 1e18 scale without overflowing + uint256 scale = computeScale(absB); // calculate the scaling factor such that B^2 can be calculated without overflowing + squaredB = Math.mulDiv(absB / scale, absB, scale, Math.Rounding.Ceil); + discriminant = squaredB + fourAC / (scale * scale); + sqrt = Math.sqrt(discriminant); + sqrt = (sqrt * sqrt < discriminant) ? sqrt + 1 : sqrt; + sqrt = sqrt * scale; + } + + uint256 x; + if (B <= 0) { + // use the regular quadratic formula solution (-b + sqrt(b^2 - 4ac)) / 2a + x = Math.mulDiv(absB + sqrt, 1e18, 2 * c, Math.Rounding.Ceil) + 1; + } else { + // use the "citardauq" quadratic formula solution 2c / (-b + sqrt(b^2 - 4ac)) + x = (2 * C + (absB + sqrt - 1)) / (absB + sqrt) + 1; + } + + if (x >= x0) { + return x0; + } else { + return x; + } + } + + /// @dev Utility to derive optimal scale for computations in fInverse + function computeScale(uint256 x) internal pure returns (uint256 scale) { + // calculate number of bits in x + uint256 bits = 0; + while (x > 0) { + x >>= 1; + bits++; + } + + // 2^excessBits is how much we need to scale down to prevent overflow when squaring x + if (bits > 128) { + uint256 excessBits = bits - 128; + scale = 1 << excessBits; + } else { + scale = 1; + } + } +} diff --git a/src/libraries/FundsLib.sol b/src/libraries/FundsLib.sol new file mode 100644 index 0000000..91f9073 --- /dev/null +++ b/src/libraries/FundsLib.sol @@ -0,0 +1,115 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.27; + +import {SafeERC20, IERC20} from "openzeppelin-contracts/token/ERC20/utils/SafeERC20.sol"; +import {IAllowanceTransfer} from "permit2/src/interfaces/IAllowanceTransfer.sol"; + +import {IEVC} from "evc/interfaces/IEthereumVaultConnector.sol"; +import {IEVault, IBorrowing, IERC4626, IRiskManager} from "evk/EVault/IEVault.sol"; +import {Errors as EVKErrors} from "evk/EVault/shared/Errors.sol"; + +import {IEulerSwap} from "../interfaces/IEulerSwap.sol"; + +library FundsLib { + using SafeERC20 for IERC20; + + error DepositFailure(bytes reason); + + /// @notice Approves tokens for a given vault, supporting both standard approvals and permit2 + /// @param vault The address of the vault to approve the token for + function approveVault(address vault) internal { + address asset = IEVault(vault).asset(); + address permit2 = IEVault(vault).permit2Address(); + if (permit2 == address(0)) { + IERC20(asset).forceApprove(vault, type(uint256).max); + } else { + IERC20(asset).forceApprove(permit2, type(uint256).max); + IAllowanceTransfer(permit2).approve(asset, vault, type(uint160).max, type(uint48).max); + } + } + + /// @notice Withdraws assets from a vault, first using available balance and then borrowing if needed + /// @param evc EVC instance + /// @param p EulerSwap parameters + /// @param vault The address of the vault to withdraw from + /// @param amount The total amount of assets to withdraw + /// @param to The address that will receive the withdrawn assets + /// @dev This function first checks if there's an existing balance in the vault. + /// @dev If there is, it withdraws the minimum of the requested amount and available balance. + /// @dev If more assets are needed after withdrawal, it enables the controller and borrows the remaining amount. + function withdrawAssets(address evc, IEulerSwap.Params memory p, address vault, uint256 amount, address to) + internal + { + uint256 balance; + { + uint256 shares = IEVault(vault).balanceOf(p.eulerAccount); + balance = shares == 0 ? 0 : IEVault(vault).convertToAssets(shares); + } + + if (balance > 0) { + uint256 avail = amount < balance ? amount : balance; + IEVC(evc).call(vault, p.eulerAccount, 0, abi.encodeCall(IERC4626.withdraw, (avail, to, p.eulerAccount))); + amount -= avail; + } + + if (amount > 0) { + IEVC(evc).enableController(p.eulerAccount, vault); + IEVC(evc).call(vault, p.eulerAccount, 0, abi.encodeCall(IBorrowing.borrow, (amount, to))); + } + } + + /// @notice Deposits assets into a vault and automatically repays any outstanding debt + /// @param evc EVC instance + /// @param p EulerSwap parameters + /// @param vault The address of the vault to deposit into + /// @return The amount of assets successfully deposited + /// @dev This function attempts to deposit assets into the specified vault. + /// @dev If the deposit fails with E_ZeroShares error, it safely returns 0 (this happens with very small amounts). + /// @dev After successful deposit, if the user has any outstanding controller-enabled debt, it attempts to repay it. + /// @dev If all debt is repaid, the controller is automatically disabled to reduce gas costs in future operations. + function depositAssets(address evc, IEulerSwap.Params memory p, address vault) internal returns (uint256) { + address asset = IEVault(vault).asset(); + + uint256 amount = IERC20(asset).balanceOf(address(this)); + if (amount == 0) return 0; + + uint256 feeAmount = amount * p.fee / 1e18; + + { + uint256 protocolFeeAmount = feeAmount * p.protocolFee / 1e18; + + if (protocolFeeAmount != 0) { + IERC20(asset).safeTransfer(p.protocolFeeRecipient, protocolFeeAmount); + amount -= protocolFeeAmount; + feeAmount -= protocolFeeAmount; + } + } + + uint256 deposited; + + if (IEVC(evc).isControllerEnabled(p.eulerAccount, vault)) { + uint256 debt = IEVault(vault).debtOf(p.eulerAccount); + uint256 repaid = IEVault(vault).repay(amount > debt ? debt : amount, p.eulerAccount); + + amount -= repaid; + debt -= repaid; + deposited += repaid; + + if (debt == 0) { + IEVC(evc).call(vault, p.eulerAccount, 0, abi.encodeCall(IRiskManager.disableController, ())); + } + } + + if (amount > 0) { + try IEVault(vault).deposit(amount, p.eulerAccount) {} + catch (bytes memory reason) { + require(bytes4(reason) == EVKErrors.E_ZeroShares.selector, DepositFailure(reason)); + amount = 0; + } + + deposited += amount; + } + + return deposited > feeAmount ? deposited - feeAmount : 0; + } +} diff --git a/src/libraries/QuoteLib.sol b/src/libraries/QuoteLib.sol new file mode 100644 index 0000000..465d4a4 --- /dev/null +++ b/src/libraries/QuoteLib.sol @@ -0,0 +1,222 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.27; + +import {IEVC} from "evc/interfaces/IEthereumVaultConnector.sol"; +import {IEVault} from "evk/EVault/IEVault.sol"; +import {IEulerSwap} from "../interfaces/IEulerSwap.sol"; +import {CtxLib} from "./CtxLib.sol"; +import {CurveLib} from "./CurveLib.sol"; + +library QuoteLib { + error UnsupportedPair(); + error OperatorNotInstalled(); + error SwapLimitExceeded(); + + /// @dev Computes the quote for a swap by applying fees and validating state conditions + /// @param evc EVC instance + /// @param p The EulerSwap params + /// @param asset0IsInput Swap direction + /// @param amount The amount to quote (input amount if exactIn=true, output amount if exactIn=false) + /// @param exactIn True if quoting for exact input amount, false if quoting for exact output amount + /// @return The quoted amount (output amount if exactIn=true, input amount if exactIn=false) + /// @dev Validates: + /// - EulerSwap operator is installed + /// - Token pair is supported + /// - Sufficient reserves exist + /// - Sufficient cash is available + function computeQuote(address evc, IEulerSwap.Params memory p, bool asset0IsInput, uint256 amount, bool exactIn) + internal + view + returns (uint256) + { + if (amount == 0) return 0; + + require(IEVC(evc).isAccountOperatorAuthorized(p.eulerAccount, address(this)), OperatorNotInstalled()); + require(amount <= type(uint112).max, SwapLimitExceeded()); + + uint256 fee = p.fee; + + // exactIn: decrease effective amountIn + if (exactIn) amount = amount - (amount * fee / 1e18); + + (uint256 inLimit, uint256 outLimit) = calcLimits(p, asset0IsInput); + + uint256 quote = findCurvePoint(p, amount, exactIn, asset0IsInput); + + if (exactIn) { + // if `exactIn`, `quote` is the amount of assets to buy from the AMM + require(amount <= inLimit && quote <= outLimit, SwapLimitExceeded()); + } else { + // if `!exactIn`, `amount` is the amount of assets to buy from the AMM + require(amount <= outLimit && quote <= inLimit, SwapLimitExceeded()); + } + + // exactOut: inflate required amountIn + if (!exactIn) quote = (quote * 1e18) / (1e18 - fee); + + return quote; + } + + /// @notice Calculates the maximum input and output amounts for a swap based on protocol constraints + /// @dev Determines limits by checking multiple factors: + /// 1. Supply caps and existing debt for the input token + /// 2. Available reserves in the EulerSwap for the output token + /// 3. Available cash and borrow caps for the output token + /// 4. Account balances in the respective vaults + /// @param p The EulerSwap params + /// @param asset0IsInput Boolean indicating whether asset0 (true) or asset1 (false) is the input token + /// @return uint256 Maximum amount of input token that can be deposited + /// @return uint256 Maximum amount of output token that can be withdrawn + function calcLimits(IEulerSwap.Params memory p, bool asset0IsInput) internal view returns (uint256, uint256) { + CtxLib.Storage storage s = CtxLib.getStorage(); + + uint256 inLimit = type(uint112).max; + uint256 outLimit = type(uint112).max; + + address eulerAccount = p.eulerAccount; + (IEVault vault0, IEVault vault1) = (IEVault(p.vault0), IEVault(p.vault1)); + // Supply caps on input + { + IEVault vault = (asset0IsInput ? vault0 : vault1); + uint256 maxDeposit = vault.debtOf(eulerAccount) + vault.maxDeposit(eulerAccount); + if (maxDeposit < inLimit) inLimit = maxDeposit; + } + + // Remaining reserves of output + { + uint112 reserveLimit = asset0IsInput ? s.reserve1 : s.reserve0; + if (reserveLimit < outLimit) outLimit = reserveLimit; + } + + // Remaining cash and borrow caps in output + { + IEVault vault = (asset0IsInput ? vault1 : vault0); + + uint256 cash = vault.cash(); + if (cash < outLimit) outLimit = cash; + + (, uint16 borrowCap) = vault.caps(); + uint256 maxWithdraw = decodeCap(uint256(borrowCap)); + maxWithdraw = vault.totalBorrows() > maxWithdraw ? 0 : maxWithdraw - vault.totalBorrows(); + if (maxWithdraw > cash) maxWithdraw = cash; + maxWithdraw += vault.convertToAssets(vault.balanceOf(eulerAccount)); + if (maxWithdraw < outLimit) outLimit = maxWithdraw; + } + + return (inLimit, outLimit); + } + + /// @notice Decodes a compact-format cap value to its actual numerical value + /// @dev The cap uses a compact-format where: + /// - If amountCap == 0, there's no cap (returns max uint256) + /// - Otherwise, the lower 6 bits represent the exponent (10^exp) + /// - The upper bits (>> 6) represent the mantissa + /// - The formula is: (10^exponent * mantissa) / 100 + /// @param amountCap The compact-format cap value to decode + /// @return The actual numerical cap value (type(uint256).max if uncapped) + /// @custom:security Uses unchecked math for gas optimization as calculations cannot overflow: + /// maximum possible value 10^(2^6-1) * (2^10-1) ≈ 1.023e+66 < 2^256 + function decodeCap(uint256 amountCap) internal pure returns (uint256) { + if (amountCap == 0) return type(uint256).max; + + unchecked { + // Cannot overflow because this is less than 2**256: + // 10**(2**6 - 1) * (2**10 - 1) = 1.023e+66 + return 10 ** (amountCap & 63) * (amountCap >> 6) / 100; + } + } + + /// @notice Verifies that the given tokens are supported by the EulerSwap pool and determines swap direction + /// @dev Returns a boolean indicating whether the input token is asset0 (true) or asset1 (false) + /// @param p The EulerSwap params + /// @param tokenIn The input token address for the swap + /// @param tokenOut The output token address for the swap + /// @return asset0IsInput True if tokenIn is asset0 and tokenOut is asset1, false if reversed + /// @custom:error UnsupportedPair Thrown if the token pair is not supported by the EulerSwap pool + function checkTokens(IEulerSwap.Params memory p, address tokenIn, address tokenOut) + internal + view + returns (bool asset0IsInput) + { + address asset0 = IEVault(p.vault0).asset(); + address asset1 = IEVault(p.vault1).asset(); + + if (tokenIn == asset0 && tokenOut == asset1) asset0IsInput = true; + else if (tokenIn == asset1 && tokenOut == asset0) asset0IsInput = false; + else revert UnsupportedPair(); + } + + function findCurvePoint(IEulerSwap.Params memory p, uint256 amount, bool exactIn, bool asset0IsInput) + internal + view + returns (uint256 output) + { + CtxLib.Storage storage s = CtxLib.getStorage(); + + uint256 px = p.priceX; + uint256 py = p.priceY; + uint256 x0 = p.equilibriumReserve0; + uint256 y0 = p.equilibriumReserve1; + uint256 cx = p.concentrationX; + uint256 cy = p.concentrationY; + uint112 reserve0 = s.reserve0; + uint112 reserve1 = s.reserve1; + + uint256 xNew; + uint256 yNew; + + if (exactIn) { + // exact in + if (asset0IsInput) { + // swap X in and Y out + xNew = reserve0 + amount; + if (xNew < x0) { + // remain on f() + yNew = CurveLib.f(xNew, px, py, x0, y0, cx); + } else { + // move to g() + yNew = CurveLib.fInverse(xNew, py, px, y0, x0, cy); + } + output = reserve1 > yNew ? reserve1 - yNew : 0; + } else { + // swap Y in and X out + yNew = reserve1 + amount; + if (yNew < y0) { + // remain on g() + xNew = CurveLib.f(yNew, py, px, y0, x0, cy); + } else { + // move to f() + xNew = CurveLib.fInverse(yNew, px, py, x0, y0, cx); + } + output = reserve0 > xNew ? reserve0 - xNew : 0; + } + } else { + // exact out + if (asset0IsInput) { + // swap Y out and X in + require(reserve1 > amount, SwapLimitExceeded()); + yNew = reserve1 - amount; + if (yNew < y0) { + // remain on g() + xNew = CurveLib.f(yNew, py, px, y0, x0, cy); + } else { + // move to f() + xNew = CurveLib.fInverse(yNew, px, py, x0, y0, cx); + } + output = xNew > reserve0 ? xNew - reserve0 : 0; + } else { + // swap X out and Y in + require(reserve0 > amount, SwapLimitExceeded()); + xNew = reserve0 - amount; + if (xNew < x0) { + // remain on f() + yNew = CurveLib.f(xNew, px, py, x0, y0, cx); + } else { + // move to g() + yNew = CurveLib.fInverse(xNew, py, px, y0, x0, cy); + } + output = yNew > reserve1 ? yNew - reserve1 : 0; + } + } + } +} diff --git a/src/utils/MetaProxyDeployer.sol b/src/utils/MetaProxyDeployer.sol new file mode 100644 index 0000000..e906302 --- /dev/null +++ b/src/utils/MetaProxyDeployer.sol @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +pragma solidity ^0.8.0; + +/// @title MetaProxyDeployer +/// @custom:security-contact security@euler.xyz +/// @author Euler Labs (https://www.eulerlabs.com/) +/// @notice Contract for deploying minimal proxies with metadata, based on EIP-3448. +/// @dev The metadata of the proxies does not include the data length as defined by EIP-3448, saving gas at a cost of +/// supporting variable size data. +/// @dev This was adapted from the Euler Vault Kit's implementation to use CREATE2 +library MetaProxyDeployer { + error E_DeploymentFailed(); + + // Meta proxy bytecode from EIP-3488 https://eips.ethereum.org/EIPS/eip-3448 + bytes constant BYTECODE_HEAD = hex"600b380380600b3d393df3363d3d373d3d3d3d60368038038091363936013d73"; + bytes constant BYTECODE_TAIL = hex"5af43d3d93803e603457fd5bf3"; + + /// @dev Computes the creation code + function creationCodeMetaProxy(address targetContract, bytes memory metadata) + internal + pure + returns (bytes memory) + { + return abi.encodePacked(BYTECODE_HEAD, targetContract, BYTECODE_TAIL, metadata); + } + + /// @dev Creates a proxy for `targetContract` with metadata from `metadata`. + /// @return addr A non-zero address if successful. + function deployMetaProxy(address targetContract, bytes memory metadata, bytes32 salt) + internal + returns (address addr) + { + bytes memory code = creationCodeMetaProxy(targetContract, metadata); + + assembly ("memory-safe") { + addr := create2(0, add(code, 32), mload(code), salt) + } + + if (addr == address(0)) revert E_DeploymentFailed(); + } +} diff --git a/src/utils/ProtocolFee.sol b/src/utils/ProtocolFee.sol new file mode 100644 index 0000000..8f3dd03 --- /dev/null +++ b/src/utils/ProtocolFee.sol @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.27; + +import {Owned} from "solmate/src/auth/Owned.sol"; + +abstract contract ProtocolFee is Owned { + uint256 public protocolFee; + address public protocolFeeRecipient; + + error InvalidFee(); + + constructor(address _feeOwner) Owned(_feeOwner) {} + + /// @notice Set the protocol fee, expressed as a percentage of LP fee + /// @param newFee The new protocol fee, in WAD units (0.10e18 = 10%) + function setProtocolFee(uint256 newFee) external onlyOwner { + require(newFee < 1e18, InvalidFee()); + protocolFee = newFee; + } + + function setProtocolFeeRecipient(address newRecipient) external onlyOwner { + protocolFeeRecipient = newRecipient; + } +} diff --git a/test/AltDecimals.t.sol b/test/AltDecimals.t.sol new file mode 100644 index 0000000..ea9ed8a --- /dev/null +++ b/test/AltDecimals.t.sol @@ -0,0 +1,92 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.24; + +import {IEVault, EulerSwapTestBase, EulerSwap, TestERC20} from "./EulerSwapTestBase.t.sol"; + +contract AltDecimals is EulerSwapTestBase { + EulerSwap public eulerSwap; + + function setUp() public virtual override { + super.setUp(); + } + + function test_alt_decimals_6_18_in() public { + eulerSwap = createEulerSwap(50e6, 60e18, 0, 1e18, 1e6, 0.9e18, 0.9e18); + skimAll(eulerSwap, true); + + uint256 amount = 1e6; + uint256 q = periphery.quoteExactInput(address(eulerSwap), address(assetTST), address(assetTST2), amount); + assertApproxEqAbs(q, 1e18, 0.01e18); + + assetTST.mint(address(this), amount); + assetTST.transfer(address(eulerSwap), amount); + + { + uint256 qPlus = q + MAX_QUOTE_ERROR + 1; + vm.expectRevert(); + eulerSwap.swap(0, qPlus, address(this), ""); + } + + eulerSwap.swap(0, q, address(this), ""); + } + + function test_alt_decimals_6_18_out() public { + eulerSwap = createEulerSwap(50e6, 60e18, 0, 1e18, 1e6, 0.9e18, 0.9e18); + skimAll(eulerSwap, true); + + uint256 amount = 1e18; + uint256 q = periphery.quoteExactOutput(address(eulerSwap), address(assetTST), address(assetTST2), amount); + assertApproxEqAbs(q, 1e6, 0.01e6); + + assetTST.mint(address(this), q); + assetTST.transfer(address(eulerSwap), q); + + { + uint256 amountPlus = amount + 0.0000001e18; + vm.expectRevert(); + eulerSwap.swap(0, amountPlus, address(this), ""); + } + + eulerSwap.swap(0, amount, address(this), ""); + } + + function test_alt_decimals_18_6_in() public { + eulerSwap = createEulerSwap(60e18, 50e6, 0, 1e6, 1e18, 0.9e18, 0.9e18); + skimAll(eulerSwap, true); + + uint256 amount = 1e18; + uint256 q = periphery.quoteExactInput(address(eulerSwap), address(assetTST), address(assetTST2), amount); + assertApproxEqAbs(q, 1e6, 0.01e6); + + assetTST.mint(address(this), amount); + assetTST.transfer(address(eulerSwap), amount); + + { + uint256 qPlus = q + MAX_QUOTE_ERROR + 1; + vm.expectRevert(); + eulerSwap.swap(0, qPlus, address(this), ""); + } + + eulerSwap.swap(0, q, address(this), ""); + } + + function test_alt_decimals_18_6_out() public { + eulerSwap = createEulerSwap(60e18, 50e6, 0, 1e6, 1e18, 0.9e18, 0.9e18); + skimAll(eulerSwap, false); + + uint256 amount = 1e6; + uint256 q = periphery.quoteExactOutput(address(eulerSwap), address(assetTST), address(assetTST2), amount); + assertApproxEqAbs(q, 1e18, 0.01e18); + + assetTST.mint(address(this), q); + assetTST.transfer(address(eulerSwap), q); + + { + uint256 amountPlus = amount + 1; + vm.expectRevert(); + eulerSwap.swap(0, amountPlus, address(this), ""); + } + + eulerSwap.swap(0, amount, address(this), ""); + } +} diff --git a/test/Basic.t.sol b/test/Basic.t.sol new file mode 100644 index 0000000..945993c --- /dev/null +++ b/test/Basic.t.sol @@ -0,0 +1,209 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.24; + +import {IEVault, IEulerSwap, EulerSwapTestBase, EulerSwap, TestERC20} from "./EulerSwapTestBase.t.sol"; +import {QuoteLib} from "../src/libraries/QuoteLib.sol"; + +contract Basic is EulerSwapTestBase { + EulerSwap public eulerSwap; + + function setUp() public virtual override { + super.setUp(); + + eulerSwap = createEulerSwap(60e18, 60e18, 0, 1e18, 1e18, 0.4e18, 0.85e18); + } + + function test_basicSwap_exactIn() public monotonicHolderNAV { + uint256 amountIn = 1e18; + uint256 amountOut = + periphery.quoteExactInput(address(eulerSwap), address(assetTST), address(assetTST2), amountIn); + assertApproxEqAbs(amountOut, 0.9974e18, 0.0001e18); + + assetTST.mint(address(this), amountIn); + + assetTST.transfer(address(eulerSwap), amountIn); + eulerSwap.swap(0, amountOut, address(this), ""); + + assertEq(assetTST2.balanceOf(address(this)), amountOut); + } + + function test_basicSwap_exactOut() public monotonicHolderNAV { + uint256 amountOut = 1e18; + uint256 amountIn = + periphery.quoteExactOutput(address(eulerSwap), address(assetTST), address(assetTST2), amountOut); + assertApproxEqAbs(amountIn, 1.0025e18, 0.0001e18); + + assetTST.mint(address(this), amountIn); + + assetTST.transfer(address(eulerSwap), amountIn); + eulerSwap.swap(0, amountOut, address(this), ""); + + assertEq(assetTST2.balanceOf(address(this)), amountOut); + } + + function test_badTokenAddrs() public { + vm.expectRevert(QuoteLib.UnsupportedPair.selector); + periphery.quoteExactInput(address(eulerSwap), address(assetTST), address(1234), 0); + + vm.expectRevert(QuoteLib.UnsupportedPair.selector); + periphery.quoteExactInput(address(eulerSwap), address(1234), address(assetTST), 0); + } + + function test_altPrice() public { + uint256 price = 0.5e18; + uint256 px = price; + uint256 py = 1e18; + oracle.setPrice(address(eTST), unitOfAccount, 0.5e18); + oracle.setPrice(address(assetTST), unitOfAccount, 0.5e18); + + int256 origNAV = getHolderNAV(); + + eulerSwap = createEulerSwap(60e18, 60e18, 0, px, py, 0.4e18, 0.85e18); + + uint256 amountIn = 1e18; + uint256 amountOut = + periphery.quoteExactInput(address(eulerSwap), address(assetTST), address(assetTST2), amountIn); + + assetTST.mint(address(this), amountIn); + + assetTST.transfer(address(eulerSwap), amountIn); + eulerSwap.swap(0, amountOut, address(this), ""); + assertEq(assetTST2.balanceOf(address(this)), amountOut); + + assertGe(getHolderNAV(), origNAV); + } + + function test_pathIndependent(uint256 amount, bool dir) public monotonicHolderNAV { + amount = bound(amount, 0.1e18, 25e18); + + TestERC20 t1; + TestERC20 t2; + if (dir) (t1, t2) = (assetTST, assetTST2); + else (t1, t2) = (assetTST2, assetTST); + + t1.mint(address(this), amount); + + uint256 q = periphery.quoteExactInput(address(eulerSwap), address(t1), address(t2), amount); + + t1.transfer(address(eulerSwap), amount); + if (dir) eulerSwap.swap(0, q, address(this), ""); + else eulerSwap.swap(q, 0, address(this), ""); + assertEq(t2.balanceOf(address(this)), q); + + t2.transfer(address(eulerSwap), q); + if (dir) eulerSwap.swap(amount, 0, address(this), ""); + else eulerSwap.swap(0, amount, address(this), ""); + + uint256 q2 = periphery.quoteExactInput(address(eulerSwap), address(t1), address(t2), amount); + assertEq(q, q2); + } + + function test_fuzzParams(uint256 amount, uint256 amount2, uint256 price, uint256 cx, uint256 cy, bool dir) public { + amount = bound(amount, 0.1e18, 25e18); + amount2 = bound(amount2, 0.1e18, 25e18); + price = bound(price, 0.1e18, 10e18); + cx = bound(cx, 0.01e18, 0.99e18); + cy = bound(cy, 0.01e18, 0.99e18); + + { + uint256 px = price; + uint256 py = 1e18; + oracle.setPrice(address(eTST), unitOfAccount, price); + oracle.setPrice(address(assetTST), unitOfAccount, price); + + eulerSwap = createEulerSwap(60e18, 60e18, 0, px, py, cx, cy); + } + + int256 origNAV = getHolderNAV(); + + TestERC20 t1; + TestERC20 t2; + if (dir) (t1, t2) = (assetTST, assetTST2); + else (t1, t2) = (assetTST2, assetTST); + + t1.mint(address(this), amount); + uint256 q = periphery.quoteExactInput(address(eulerSwap), address(t1), address(t2), amount); + { + uint256 qRev = periphery.quoteExactOutput(address(eulerSwap), address(t1), address(t2), q); + assertApproxEqAbs(amount, qRev, 200 + (MAX_QUOTE_ERROR + 1) * 2); // max 100:1 price differential, 2 swaps + } + + t1.transfer(address(eulerSwap), amount); + if (dir) eulerSwap.swap(0, q, address(this), ""); + else eulerSwap.swap(q, 0, address(this), ""); + assertEq(t2.balanceOf(address(this)), q); + + t2.mint(address(this), amount2); + uint256 q2 = periphery.quoteExactInput(address(eulerSwap), address(t2), address(t1), amount2); + { + uint256 qRev = periphery.quoteExactOutput(address(eulerSwap), address(t2), address(t1), q2); + assertApproxEqAbs(amount2, qRev, 200 + (MAX_QUOTE_ERROR + 1) * 2); + } + + t2.transfer(address(eulerSwap), amount2); + if (dir) eulerSwap.swap(q2, 0, address(this), ""); + else eulerSwap.swap(0, q2, address(this), ""); + assertEq(t1.balanceOf(address(this)), q2); + + assertGe(getHolderNAV(), origNAV); + } + + function test_fuzzAll(uint256 cx, uint256 cy, uint256 fee, uint256[8] calldata amounts, bool[8] calldata dirs) + public + { + cx = bound(cx, 0.01e18, 0.99e18); + cy = bound(cy, 0.01e18, 0.99e18); + fee = bound(fee, 0, 0.1e18); + + eulerSwap = createEulerSwap(60e18, 60e18, fee, 1e18, 1e18, cx, cy); + + int256 origNAV = getHolderNAV(); + + for (uint256 i = 0; i < 8; i++) { + uint256 amount = bound(amounts[i], 0.1e18, 5e18); + bool dir = dirs[i]; + + TestERC20 t1; + TestERC20 t2; + if (dir) (t1, t2) = (assetTST, assetTST2); + else (t1, t2) = (assetTST2, assetTST); + + t1.mint(address(this), amount); + uint256 q = periphery.quoteExactInput(address(eulerSwap), address(t1), address(t2), amount); + + // Try to swap out 1 extra + + t1.transfer(address(eulerSwap), amount); + + { + uint256 qPlus = q + MAX_QUOTE_ERROR + 1; + vm.expectRevert(); + if (dir) eulerSwap.swap(0, qPlus, address(this), ""); + else eulerSwap.swap(qPlus, 0, address(this), ""); + } + + // Confirm actual quote works + + uint256 prevBal = t2.balanceOf(address(this)); + if (dir) eulerSwap.swap(0, q, address(this), ""); + else eulerSwap.swap(q, 0, address(this), ""); + assertEq(t2.balanceOf(address(this)), q + prevBal); + + assertGe(getHolderNAV(), origNAV); + } + } + + /* + // Make `f()` function public to run this test + function test_fFuncOverflow(uint256 xt, uint256 px, uint256 py, uint256 x0, uint256 y0, uint256 c) public view { + x0 = bound(x0, 1, type(uint112).max); + y0 = bound(y0, 0, type(uint112).max); + xt = bound(xt, 1 + x0 / 1e3, x0); // thousand-fold price movement + px = bound(px, 1, 1e36); + py = bound(py, 1, 1e36); + c = bound(c, 1, 1e18); + + eulerSwap.f(xt, px, py, x0, y0, c); + } + */ +} diff --git a/test/Counter.t.sol b/test/Counter.t.sol deleted file mode 100644 index 54b724f..0000000 --- a/test/Counter.t.sol +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.13; - -import {Test, console} from "forge-std/Test.sol"; -import {Counter} from "../src/Counter.sol"; - -contract CounterTest is Test { - Counter public counter; - - function setUp() public { - counter = new Counter(); - counter.setNumber(0); - } - - function test_Increment() public { - counter.increment(); - assertEq(counter.number(), 1); - } - - function testFuzz_SetNumber(uint256 x) public { - counter.setNumber(x); - assertEq(counter.number(), x); - } -} diff --git a/test/Ctx.t.sol b/test/Ctx.t.sol new file mode 100644 index 0000000..5bcb3af --- /dev/null +++ b/test/Ctx.t.sol @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.24; + +import {EulerSwapTestBase, EulerSwap, IEulerSwap} from "./EulerSwapTestBase.t.sol"; +import {CtxLib} from "../src/libraries/CtxLib.sol"; + +contract CtxTest is EulerSwapTestBase { + function setUp() public virtual override { + super.setUp(); + } + + function test_staticCtxStorage() public pure { + assertEq(CtxLib.CtxStorageLocation, keccak256("eulerSwap.storage")); + } + + function test_staticParamSize() public view { + IEulerSwap.Params memory params = getEulerSwapParams(1e18, 1e18, 1e18, 1e18, 0.4e18, 0.85e18, 0, 0, address(0)); + assertEq(abi.encode(params).length, 384); + } + + function test_insufficientCalldata() public { + // Proxy appends 384 bytes of calldata, so you can't call directly without this + + vm.expectRevert(CtxLib.InsufficientCalldata.selector); + EulerSwap(eulerSwapImpl).getParams(); + } + + function test_callImplementationDirectly() public { + // Underlying implementation is locked: must call via a proxy + + bool success; + + vm.expectRevert(EulerSwap.AlreadyActivated.selector); + (success,) = eulerSwapImpl.call( + padCalldata( + abi.encodeCall(EulerSwap.activate, (IEulerSwap.InitialState({currReserve0: 1e18, currReserve1: 1e18}))) + ) + ); + + vm.expectRevert(EulerSwap.Locked.selector); + (success,) = eulerSwapImpl.call(padCalldata(abi.encodeCall(EulerSwap.getReserves, ()))); + } + + function padCalldata(bytes memory inp) internal pure returns (bytes memory) { + IEulerSwap.Params memory params; + return abi.encodePacked(inp, abi.encode(params)); + } +} diff --git a/test/CurveLib.t.sol b/test/CurveLib.t.sol new file mode 100644 index 0000000..baaf864 --- /dev/null +++ b/test/CurveLib.t.sol @@ -0,0 +1,117 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.24; + +import "forge-std/Test.sol"; +import "forge-std/console.sol"; +import {EulerSwapTestBase, EulerSwap, TestERC20} from "./EulerSwapTestBase.t.sol"; +import {IEulerSwap} from "../src/interfaces/IEulerSwap.sol"; +import {CurveLib} from "../src/libraries/CurveLib.sol"; + +contract CurveLibTest is EulerSwapTestBase { + EulerSwap public eulerSwap; + + function setUp() public virtual override { + super.setUp(); + } + + function testGas_fInverse() public pure { + // Set representative values within valid bounds + uint256 px = 1e18; + uint256 py = 1e18; + uint256 x0 = 1e14; + uint256 y0 = 1e14; + uint256 c = 1e18; + + // Use CurveLib.f to get a valid y + uint256 x = 1e12; + uint256 y = CurveLib.f(x, px, py, x0, y0, c); + + // Measure gas of fInverse + CurveLib.fInverse(y, px, py, x0, y0, c); + } + + function test_fuzzfInverse(uint256 x, uint256 px, uint256 py, uint256 x0, uint256 y0, uint256 cx, uint256 cy) + public + pure + { + // Params + px = 1e18; + py = bound(py, 1, 1e36); + x0 = bound(x0, 1, 1e28); + y0 = bound(y0, 0, 1e28); + cx = bound(cx, 1, 1e18); + cy = bound(cy, 1, 1e18); + console.log("px", px); + console.log("py", py); + console.log("x0", x0); + console.log("y0", y0); + console.log("cx", cx); + console.log("cy", cy); + + IEulerSwap.Params memory p = IEulerSwap.Params({ + vault0: address(0), + vault1: address(0), + eulerAccount: address(0), + equilibriumReserve0: uint112(x0), + equilibriumReserve1: uint112(y0), + priceX: px, + priceY: py, + concentrationX: cx, + concentrationY: cy, + fee: 0, + protocolFee: 0, + protocolFeeRecipient: address(0) + }); + + x = bound(x, 1, x0); + + uint256 y = CurveLib.f(x, px, py, x0, y0, cx); + console.log("y ", y); + uint256 xCalc = CurveLib.fInverse(y, px, py, x0, y0, cx); + console.log("xCalc", xCalc); + uint256 yCalc = CurveLib.f(xCalc, px, py, x0, y0, cx); + uint256 xBin = binarySearch(p, y, 1, x0); + uint256 yBin = CurveLib.f(xBin, px, py, x0, y0, cx); + console.log("x ", x); + console.log("xCalc", xCalc); + console.log("xBin ", xBin); + console.log("y ", y); + console.log("yCalc", yCalc); + console.log("yBin ", yBin); + + if (x < type(uint112).max && y < type(uint112).max) { + assert(CurveLib.verify(p, xCalc, y)); + console.log("Invariant passed"); + assert(xCalc - xBin <= 3 || y - yCalc <= 3); // suspect this is 2 wei error in fInverse() + 1 wei error in f() + console.log("Margin error passed"); + } + } + + /// @dev Less efficient method to compute fInverse. Useful for differential fuzzing. + function binarySearch(IEulerSwap.Params memory p, uint256 newReserve1, uint256 xMin, uint256 xMax) + internal + pure + returns (uint256) + { + if (xMin < 1) { + xMin = 1; + } + while (xMin < xMax) { + uint256 xMid = (xMin + xMax) / 2; + uint256 fxMid = + CurveLib.f(xMid, p.priceX, p.priceY, p.equilibriumReserve0, p.equilibriumReserve1, p.concentrationX); + if (newReserve1 >= fxMid) { + xMax = xMid; + } else { + xMin = xMid + 1; + } + } + if ( + newReserve1 + < CurveLib.f(xMin, p.priceX, p.priceY, p.equilibriumReserve0, p.equilibriumReserve1, p.concentrationX) + ) { + xMin += 1; + } + return xMin; + } +} diff --git a/test/DepositFailures.t.sol b/test/DepositFailures.t.sol new file mode 100644 index 0000000..5cee89f --- /dev/null +++ b/test/DepositFailures.t.sol @@ -0,0 +1,111 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.24; + +import {IEVault, IEulerSwap, EulerSwapTestBase, EulerSwap, TestERC20} from "./EulerSwapTestBase.t.sol"; +import {IRMTestFixed} from "evk-test/mocks/IRMTestFixed.sol"; +import {Errors as EVKErrors} from "evk/EVault/shared/Errors.sol"; +import {FundsLib} from "../src/libraries/FundsLib.sol"; +import "evk/EVault/shared/Constants.sol" as EVKConstants; + +contract DepositFailuresTest is EulerSwapTestBase { + EulerSwap public eulerSwap; + address public griefer = makeAddr("griefer"); + + function setUp() public virtual override { + super.setUp(); + + eulerSwap = createEulerSwap(60e18, 60e18, 0, 1e18, 1e18, 0.4e18, 0.85e18); + } + + function test_griefing() public monotonicHolderNAV { + // Make a borrow to push exchange rate > 1 + + eTST2.setInterestRateModel(address(new IRMTestFixed())); + + mintAndDeposit(griefer, eTST, 100e18); + + vm.prank(griefer); + evc.enableCollateral(griefer, address(eTST)); + vm.prank(griefer); + evc.enableController(griefer, address(eTST2)); + + vm.prank(griefer); + eTST2.borrow(1e18, griefer); + skip(1); + + // Do a swap + + uint256 amountIn = 1e18; + uint256 amountOut = + periphery.quoteExactInput(address(eulerSwap), address(assetTST), address(assetTST2), amountIn); + assertApproxEqAbs(amountOut, 0.9974e18, 0.0001e18); + + // Honest deposit + assetTST.mint(address(this), amountIn); + assetTST.transfer(address(eulerSwap), amountIn); + + // Griefer front-runs with 1 wei deposit, which rounds down to 0 shares + assetTST2.mint(address(this), 1); + assetTST2.transfer(address(eulerSwap), 1); + + // Naive deposit() would fail with E_ZeroShares + eulerSwap.swap(0, amountOut, address(this), ""); + + assertEq(assetTST2.balanceOf(address(this)), amountOut); + + assertEq(assetTST2.balanceOf(address(eulerSwap)), 1); // griefing transfer was untouched + } + + function test_depositFailure() public monotonicHolderNAV { + // Do a swap + + uint256 amountIn = 1e18; + uint256 amountOut = + periphery.quoteExactInput(address(eulerSwap), address(assetTST), address(assetTST2), amountIn); + assertApproxEqAbs(amountOut, 0.9974e18, 0.0001e18); + + // Honest deposit + assetTST.mint(address(this), amountIn); + assetTST.transfer(address(eulerSwap), amountIn); + + // Griefer front-runs with 1 wei deposit, which rounds down to 0 shares + assetTST2.mint(address(this), 1); + assetTST2.transfer(address(eulerSwap), 1); + + // Force deposits to fail + eTST2.setHookConfig(address(0), EVKConstants.OP_DEPOSIT); + + vm.expectRevert( + abi.encodeWithSelector( + FundsLib.DepositFailure.selector, abi.encodeWithSelector(EVKErrors.E_OperationDisabled.selector) + ) + ); + eulerSwap.swap(0, amountOut, address(this), ""); + + assertEq(assetTST2.balanceOf(address(this)), 0); + + assertEq(assetTST2.balanceOf(address(eulerSwap)), 1); // griefing transfer was untouched + } + + function test_manualEnableController() public monotonicHolderNAV { + vm.prank(holder); + evc.enableController(holder, address(eTST)); + + uint256 amountIn = 50e18; + uint256 amountOut = + periphery.quoteExactInput(address(eulerSwap), address(assetTST), address(assetTST2), amountIn); + + assetTST.mint(address(this), amountIn); + assetTST.transfer(address(eulerSwap), amountIn); + eulerSwap.swap(0, amountOut, address(this), ""); + + // Swap the other way to measure gas impact + + amountIn = 100e18; + amountOut = periphery.quoteExactInput(address(eulerSwap), address(assetTST2), address(assetTST), amountIn); + + assetTST2.mint(address(this), amountIn); + assetTST2.transfer(address(eulerSwap), amountIn); + eulerSwap.swap(amountOut, 0, address(this), ""); + } +} diff --git a/test/EulerSwapTestBase.t.sol b/test/EulerSwapTestBase.t.sol new file mode 100644 index 0000000..5c149f4 --- /dev/null +++ b/test/EulerSwapTestBase.t.sol @@ -0,0 +1,304 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.24; + +import {Test, console} from "forge-std/Test.sol"; +import {EVaultTestBase, TestERC20, IRMTestDefault} from "evk-test/unit/evault/EVaultTestBase.t.sol"; +import {IEVault} from "evk/EVault/IEVault.sol"; +import {IEulerSwap, IEVC, EulerSwap} from "../src/EulerSwap.sol"; +import {EulerSwapFactory} from "../src/EulerSwapFactory.sol"; +import {EulerSwapPeriphery} from "../src/EulerSwapPeriphery.sol"; +import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; +import {HookMiner} from "./utils/HookMiner.sol"; +import {Hooks} from "@uniswap/v4-core/src/libraries/Hooks.sol"; +import {MetaProxyDeployer} from "../src/utils/MetaProxyDeployer.sol"; + +contract EulerSwapTestBase is EVaultTestBase { + uint256 public constant MAX_QUOTE_ERROR = 2; + + address public depositor = makeAddr("depositor"); + address public holder = makeAddr("holder"); + address public recipient = makeAddr("recipient"); + address public anyone = makeAddr("anyone"); + + TestERC20 assetTST3; + IEVault public eTST3; + + address public eulerSwapImpl; + EulerSwapFactory public eulerSwapFactory; + EulerSwapPeriphery public periphery; + + uint256 currSalt = 0; + address installedOperator; + + modifier monotonicHolderNAV() { + int256 orig = getHolderNAV(); + _; + assertGe(getHolderNAV(), orig); + } + + function deployEulerSwap(address poolManager_) public { + eulerSwapImpl = address(new EulerSwap(address(evc), poolManager_)); + eulerSwapFactory = new EulerSwapFactory(address(evc), address(factory), eulerSwapImpl, address(this)); + periphery = new EulerSwapPeriphery(); + } + + function removeInstalledOperator() public { + if (installedOperator == address(0)) return; + + vm.prank(holder); + evc.setAccountOperator(holder, installedOperator, false); + + installedOperator = address(0); + } + + function setUp() public virtual override { + super.setUp(); + + deployEulerSwap(address(0)); // Default is no poolManager + + // deploy more vaults + assetTST3 = new TestERC20("Test Token 3", "TST3", 18, false); + eTST3 = IEVault( + factory.createProxy(address(0), true, abi.encodePacked(address(assetTST3), address(oracle), unitOfAccount)) + ); + eTST3.setHookConfig(address(0), 0); + eTST3.setInterestRateModel(address(new IRMTestDefault())); + eTST3.setMaxLiquidationDiscount(0.2e4); + eTST3.setFeeReceiver(feeReceiver); + + // Vault config + + eTST.setLTV(address(eTST2), 0.9e4, 0.9e4, 0); + eTST2.setLTV(address(eTST), 0.9e4, 0.9e4, 0); + eTST.setLTV(address(eTST3), 0.9e4, 0.9e4, 0); + + // Pricing + + oracle.setPrice(address(assetTST), unitOfAccount, 1e18); + oracle.setPrice(address(assetTST2), unitOfAccount, 1e18); + oracle.setPrice(address(assetTST3), unitOfAccount, 1e18); + oracle.setPrice(address(eTST), unitOfAccount, 1e18); + oracle.setPrice(address(eTST2), unitOfAccount, 1e18); + oracle.setPrice(address(eTST3), unitOfAccount, 1e18); + + oracle.setPrice(address(assetTST), address(assetTST2), 1e18); + oracle.setPrice(address(assetTST2), address(assetTST), 1e18); + oracle.setPrice(address(assetTST), address(assetTST3), 1e18); + oracle.setPrice(address(assetTST3), address(assetTST), 1e18); + + // Funding + + mintAndDeposit(depositor, eTST, 100e18); + mintAndDeposit(depositor, eTST2, 100e18); + mintAndDeposit(depositor, eTST3, 100e18); + + mintAndDeposit(holder, eTST, 10e18); + mintAndDeposit(holder, eTST2, 10e18); + mintAndDeposit(holder, eTST3, 10e18); + } + + function skimAll(EulerSwap ml, bool order) public { + if (order) { + runSkimAll(ml, true); + runSkimAll(ml, false); + } else { + runSkimAll(ml, false); + runSkimAll(ml, true); + } + } + + function getHolderNAV() internal view returns (int256) { + uint256 balance0 = eTST.convertToAssets(eTST.balanceOf(holder)); + uint256 debt0 = eTST.debtOf(holder); + uint256 balance1 = eTST2.convertToAssets(eTST2.balanceOf(holder)); + uint256 debt1 = eTST2.debtOf(holder); + + uint256 balValue = oracle.getQuote(balance0, address(assetTST), unitOfAccount) + + oracle.getQuote(balance1, address(assetTST2), unitOfAccount); + uint256 debtValue = oracle.getQuote(debt0, address(assetTST), unitOfAccount) + + oracle.getQuote(debt1, address(assetTST2), unitOfAccount); + + return int256(balValue) - int256(debtValue); + } + + function createEulerSwap( + uint112 reserve0, + uint112 reserve1, + uint256 fee, + uint256 px, + uint256 py, + uint256 cx, + uint256 cy + ) internal returns (EulerSwap) { + return createEulerSwapFull(reserve0, reserve1, fee, px, py, cx, cy, 0, address(0)); + } + + function createEulerSwapFull( + uint112 reserve0, + uint112 reserve1, + uint256 fee, + uint256 px, + uint256 py, + uint256 cx, + uint256 cy, + uint256 protocolFee, + address protocolFeeRecipient + ) internal returns (EulerSwap) { + removeInstalledOperator(); + + IEulerSwap.Params memory params = + getEulerSwapParams(reserve0, reserve1, px, py, cx, cy, fee, protocolFee, protocolFeeRecipient); + IEulerSwap.InitialState memory initialState = + IEulerSwap.InitialState({currReserve0: reserve0, currReserve1: reserve1}); + + bytes32 salt = bytes32(currSalt++); + + address predictedAddr = eulerSwapFactory.computePoolAddress(params, salt); + + vm.prank(holder); + evc.setAccountOperator(holder, predictedAddr, true); + installedOperator = predictedAddr; + + vm.prank(holder); + EulerSwap eulerSwap = EulerSwap(eulerSwapFactory.deployPool(params, initialState, salt)); + + return eulerSwap; + } + + function createEulerSwapHook( + uint112 reserve0, + uint112 reserve1, + uint256 fee, + uint256 px, + uint256 py, + uint256 cx, + uint256 cy + ) internal returns (EulerSwap) { + return createEulerSwapHookFull(reserve0, reserve1, fee, px, py, cx, cy, 0, address(0)); + } + + function createEulerSwapHookFull( + uint112 reserve0, + uint112 reserve1, + uint256 fee, + uint256 px, + uint256 py, + uint256 cx, + uint256 cy, + uint256 protocolFee, + address protocolFeeRecipient + ) internal returns (EulerSwap) { + removeInstalledOperator(); + + IEulerSwap.Params memory params = + getEulerSwapParams(reserve0, reserve1, px, py, cx, cy, fee, protocolFee, protocolFeeRecipient); + IEulerSwap.InitialState memory initialState = + IEulerSwap.InitialState({currReserve0: reserve0, currReserve1: reserve1}); + + bytes memory creationCode = MetaProxyDeployer.creationCodeMetaProxy(eulerSwapImpl, abi.encode(params)); + (address predictedAddr, bytes32 salt) = HookMiner.find( + address(eulerSwapFactory), + holder, + uint160( + Hooks.BEFORE_INITIALIZE_FLAG | Hooks.BEFORE_SWAP_FLAG | Hooks.BEFORE_SWAP_RETURNS_DELTA_FLAG + | Hooks.BEFORE_ADD_LIQUIDITY_FLAG + ), + creationCode + ); + + vm.prank(holder); + evc.setAccountOperator(holder, predictedAddr, true); + installedOperator = predictedAddr; + + vm.prank(holder); + EulerSwap eulerSwap = EulerSwap(eulerSwapFactory.deployPool(params, initialState, salt)); + + return eulerSwap; + } + + function mintAndDeposit(address who, IEVault vault, uint256 amount) internal { + TestERC20 tok = TestERC20(vault.asset()); + tok.mint(who, amount); + + vm.prank(who); + tok.approve(address(vault), type(uint256).max); + + vm.prank(who); + vault.deposit(amount, who); + } + + function runSkimAll(EulerSwap ml, bool dir) internal returns (uint256) { + uint256 skimmed = 0; + uint256 val = 1; + + // Phase 1: Keep doubling skim amount until it fails + + while (true) { + (uint256 amount0, uint256 amount1) = dir ? (val, uint256(0)) : (uint256(0), val); + + try ml.swap(amount0, amount1, address(0xDEAD), "") { + skimmed += val; + val *= 2; + } catch { + break; + } + } + + // Phase 2: Keep halving skim amount until 1 wei skim fails + + while (true) { + if (val > 1) val /= 2; + + (uint256 amount0, uint256 amount1) = dir ? (val, uint256(0)) : (uint256(0), val); + + try ml.swap(amount0, amount1, address(0xDEAD), "") { + skimmed += val; + } catch { + if (val == 1) break; + } + } + + return skimmed; + } + + function getEulerSwapParams( + uint112 reserve0, + uint112 reserve1, + uint256 px, + uint256 py, + uint256 cx, + uint256 cy, + uint256 fee, + uint256 protocolFee, + address protocolFeeRecipient + ) internal view returns (EulerSwap.Params memory) { + return IEulerSwap.Params({ + vault0: address(eTST), + vault1: address(eTST2), + eulerAccount: holder, + equilibriumReserve0: reserve0, + equilibriumReserve1: reserve1, + priceX: px, + priceY: py, + concentrationX: cx, + concentrationY: cy, + fee: fee, + protocolFee: protocolFee, + protocolFeeRecipient: protocolFeeRecipient + }); + } + + function logState(address ml) internal view { + (uint112 reserve0, uint112 reserve1,) = EulerSwap(ml).getReserves(); + + console.log("--------------------"); + console.log("Account States:"); + console.log("HOLDER"); + console.log(" eTST Vault assets: ", eTST.convertToAssets(eTST.balanceOf(holder))); + console.log(" eTST Vault debt: ", eTST.debtOf(holder)); + console.log(" eTST2 Vault assets: ", eTST2.convertToAssets(eTST2.balanceOf(holder))); + console.log(" eTST2 Vault debt: ", eTST2.debtOf(holder)); + console.log(" reserve0: ", reserve0); + console.log(" reserve1: ", reserve1); + } +} diff --git a/test/FactoryTest.t.sol b/test/FactoryTest.t.sol new file mode 100644 index 0000000..01ab94f --- /dev/null +++ b/test/FactoryTest.t.sol @@ -0,0 +1,324 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.24; + +import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; +import {PoolManagerDeployer} from "./utils/PoolManagerDeployer.sol"; +import {Hooks} from "@uniswap/v4-core/src/libraries/Hooks.sol"; +import {HookMiner} from "./utils/HookMiner.sol"; +import {EulerSwapTestBase, IEulerSwap, IEVC, EulerSwap} from "./EulerSwapTestBase.t.sol"; +import {EulerSwapFactory, IEulerSwapFactory} from "../src/EulerSwapFactory.sol"; +import {EulerSwap} from "../src/EulerSwap.sol"; +import {MetaProxyDeployer} from "../src/utils/MetaProxyDeployer.sol"; + +contract FactoryTest is EulerSwapTestBase { + IPoolManager public poolManager; + + function setUp() public virtual override { + super.setUp(); + + poolManager = PoolManagerDeployer.deploy(address(this)); + + deployEulerSwap(address(poolManager)); + + assertEq(eulerSwapFactory.EVC(), address(evc)); + } + + function getBasicParams() + internal + view + returns (IEulerSwap.Params memory poolParams, IEulerSwap.InitialState memory initialState) + { + poolParams = getEulerSwapParams(1e18, 1e18, 1e18, 1e18, 0.4e18, 0.85e18, 0, 0, address(0)); + initialState = IEulerSwap.InitialState({currReserve0: 1e18, currReserve1: 1e18}); + } + + function mineSalt(IEulerSwap.Params memory poolParams) internal view returns (address hookAddress, bytes32 salt) { + uint160 flags = uint160( + Hooks.BEFORE_INITIALIZE_FLAG | Hooks.BEFORE_SWAP_FLAG | Hooks.BEFORE_SWAP_RETURNS_DELTA_FLAG + | Hooks.BEFORE_ADD_LIQUIDITY_FLAG + ); + bytes memory creationCode = MetaProxyDeployer.creationCodeMetaProxy(eulerSwapImpl, abi.encode(poolParams)); + (hookAddress, salt) = HookMiner.find(address(eulerSwapFactory), holder, flags, creationCode); + } + + function mineBadSalt(IEulerSwap.Params memory poolParams) + internal + view + returns (address hookAddress, bytes32 salt) + { + // missing BEFORE_ADD_LIQUIDITY_FLAG + uint160 flags = + uint160(Hooks.BEFORE_INITIALIZE_FLAG | Hooks.BEFORE_SWAP_FLAG | Hooks.BEFORE_SWAP_RETURNS_DELTA_FLAG); + bytes memory creationCode = MetaProxyDeployer.creationCodeMetaProxy(eulerSwapImpl, abi.encode(poolParams)); + (hookAddress, salt) = HookMiner.find(address(eulerSwapFactory), holder, flags, creationCode); + } + + function testDifferingAddressesSameSalt() public view { + (IEulerSwap.Params memory poolParams,) = getBasicParams(); + + address a1 = eulerSwapFactory.computePoolAddress(poolParams, bytes32(0)); + + poolParams.eulerAccount = address(123); + + address a2 = eulerSwapFactory.computePoolAddress(poolParams, bytes32(0)); + + assert(a1 != a2); + } + + function testDeployPool() public { + uint256 allPoolsLengthBefore = eulerSwapFactory.poolsLength(); + + // test when new pool not set as operator + + (IEulerSwap.Params memory poolParams, IEulerSwap.InitialState memory initialState) = getBasicParams(); + + (address hookAddress, bytes32 salt) = mineSalt(poolParams); + + address predictedAddress = eulerSwapFactory.computePoolAddress(poolParams, salt); + assertEq(hookAddress, predictedAddress); + + IEVC.BatchItem[] memory items = new IEVC.BatchItem[](1); + + items[0] = IEVC.BatchItem({ + onBehalfOfAccount: holder, + targetContract: address(eulerSwapFactory), + value: 0, + data: abi.encodeCall(EulerSwapFactory.deployPool, (poolParams, initialState, salt)) + }); + + vm.prank(holder); + vm.expectRevert(EulerSwapFactory.OperatorNotInstalled.selector); + evc.batch(items); + + // success test + + items = new IEVC.BatchItem[](2); + + items[0] = IEVC.BatchItem({ + onBehalfOfAccount: address(0), + targetContract: address(evc), + value: 0, + data: abi.encodeCall(evc.setAccountOperator, (holder, predictedAddress, true)) + }); + items[1] = IEVC.BatchItem({ + onBehalfOfAccount: holder, + targetContract: address(eulerSwapFactory), + value: 0, + data: abi.encodeCall(EulerSwapFactory.deployPool, (poolParams, initialState, salt)) + }); + + vm.prank(holder); + evc.batch(items); + + address eulerSwap = eulerSwapFactory.poolByEulerAccount(holder); + + assertEq(address(EulerSwap(eulerSwap).poolManager()), address(poolManager)); + + uint256 allPoolsLengthAfter = eulerSwapFactory.poolsLength(); + assertEq(allPoolsLengthAfter - allPoolsLengthBefore, 1); + + address[] memory poolsList = eulerSwapFactory.pools(); + assertEq(poolsList.length, 1); + assertEq(poolsList[0], eulerSwap); + assertEq(poolsList[0], address(eulerSwap)); + + // revert when attempting to deploy a new pool (with a different salt) + poolParams.fee = 1; + (address newHookAddress, bytes32 newSalt) = mineSalt(poolParams); + assertNotEq(newHookAddress, hookAddress); + assertNotEq(newSalt, salt); + + items = new IEVC.BatchItem[](1); + items[0] = IEVC.BatchItem({ + onBehalfOfAccount: holder, + targetContract: address(eulerSwapFactory), + value: 0, + data: abi.encodeCall(EulerSwapFactory.deployPool, (poolParams, initialState, newSalt)) + }); + + vm.prank(holder); + vm.expectRevert(EulerSwapFactory.OldOperatorStillInstalled.selector); + evc.batch(items); + } + + function testBadSalt() public { + (IEulerSwap.Params memory poolParams, IEulerSwap.InitialState memory initialState) = getBasicParams(); + (address hookAddress, bytes32 salt) = mineBadSalt(poolParams); + + vm.prank(holder); + evc.setAccountOperator(holder, hookAddress, true); + + vm.expectRevert(abi.encodeWithSelector(Hooks.HookAddressNotValid.selector, hookAddress)); + vm.prank(holder); + eulerSwapFactory.deployPool(poolParams, initialState, salt); + } + + function testInvalidPoolsSliceOutOfBounds() public { + vm.expectRevert(EulerSwapFactory.SliceOutOfBounds.selector); + eulerSwapFactory.poolsSlice(1, 0); + } + + function testDeployWithInvalidVaultImplementation() public { + bytes32 salt = bytes32(uint256(1234)); + (IEulerSwap.Params memory poolParams, IEulerSwap.InitialState memory initialState) = getBasicParams(); + + // Create a fake vault that's not deployed by the factory + address fakeVault = address(0x1234); + poolParams.vault0 = fakeVault; + poolParams.vault1 = address(eTST2); + + vm.prank(holder); + vm.expectRevert(EulerSwapFactory.InvalidVaultImplementation.selector); + eulerSwapFactory.deployPool(poolParams, initialState, salt); + } + + function testDeployWithUnauthorizedCaller() public { + bytes32 salt = bytes32(uint256(1234)); + (IEulerSwap.Params memory poolParams, IEulerSwap.InitialState memory initialState) = getBasicParams(); + + // Call from a different address than the euler account + vm.prank(address(0x1234)); + vm.expectRevert(EulerSwapFactory.Unauthorized.selector); + eulerSwapFactory.deployPool(poolParams, initialState, salt); + } + + function testDeployWithAssetsOutOfOrderOrEqual() public { + (IEulerSwap.Params memory poolParams, IEulerSwap.InitialState memory initialState) = getBasicParams(); + (poolParams.vault0, poolParams.vault1) = (poolParams.vault1, poolParams.vault0); + + (address hookAddress, bytes32 salt) = mineSalt(poolParams); + + vm.prank(holder); + evc.setAccountOperator(holder, hookAddress, true); + + vm.prank(holder); + vm.expectRevert(EulerSwap.AssetsOutOfOrderOrEqual.selector); + eulerSwapFactory.deployPool(poolParams, initialState, salt); + } + + function testDeployWithBadFee() public { + (IEulerSwap.Params memory poolParams, IEulerSwap.InitialState memory initialState) = getBasicParams(); + poolParams.fee = 1e18; + + (address hookAddress, bytes32 salt) = mineSalt(poolParams); + + vm.prank(holder); + evc.setAccountOperator(holder, hookAddress, true); + + vm.prank(holder); + vm.expectRevert(EulerSwap.BadParam.selector); + eulerSwapFactory.deployPool(poolParams, initialState, salt); + } + + function testPoolsByPair() public { + // First deploy a pool + (IEulerSwap.Params memory poolParams, IEulerSwap.InitialState memory initialState) = getBasicParams(); + (address hookAddress, bytes32 salt) = mineSalt(poolParams); + + IEVC.BatchItem[] memory items = new IEVC.BatchItem[](2); + items[0] = IEVC.BatchItem({ + onBehalfOfAccount: address(0), + targetContract: address(evc), + value: 0, + data: abi.encodeCall(evc.setAccountOperator, (holder, hookAddress, true)) + }); + items[1] = IEVC.BatchItem({ + onBehalfOfAccount: holder, + targetContract: address(eulerSwapFactory), + value: 0, + data: abi.encodeCall(EulerSwapFactory.deployPool, (poolParams, initialState, salt)) + }); + + vm.prank(holder); + evc.batch(items); + + // Get the deployed pool and its assets + address pool = eulerSwapFactory.poolByEulerAccount(holder); + (address asset0, address asset1) = EulerSwap(pool).getAssets(); + + // Test poolsByPairLength + assertEq(eulerSwapFactory.poolsByPairLength(asset0, asset1), 1); + + // Test poolsByPairSlice + address[] memory slice = eulerSwapFactory.poolsByPairSlice(asset0, asset1, 0, 1); + assertEq(slice.length, 1); + assertEq(slice[0], hookAddress); + + // Test poolsByPair + address[] memory pools = eulerSwapFactory.poolsByPair(asset0, asset1); + assertEq(pools.length, 1); + assertEq(pools[0], hookAddress); + } + + address alice = makeAddr("alice"); + address bob = makeAddr("bob"); + + function test_multipleUninstalls() public { + (IEulerSwap.Params memory params, IEulerSwap.InitialState memory initialState) = getBasicParams(); + + // Deploy pool for Alice + params.eulerAccount = holder = alice; + (address alicePool, bytes32 aliceSalt) = mineSalt(params); + + vm.startPrank(alice); + evc.setAccountOperator(alice, alicePool, true); + eulerSwapFactory.deployPool(params, initialState, aliceSalt); + + // Deploy pool for Bob + params.eulerAccount = holder = bob; + (address bobPool, bytes32 bobSalt) = mineSalt(params); + + vm.startPrank(bob); + evc.setAccountOperator(bob, bobPool, true); + eulerSwapFactory.deployPool(params, initialState, bobSalt); + + { + address[] memory ps = eulerSwapFactory.pools(); + assertEq(ps.length, 2); + assertEq(ps[0], alicePool); + assertEq(ps[1], bobPool); + } + + { + (address asset0, address asset1) = EulerSwap(alicePool).getAssets(); + address[] memory ps = eulerSwapFactory.poolsByPair(asset0, asset1); + assertEq(ps.length, 2); + assertEq(ps[0], alicePool); + assertEq(ps[1], bobPool); + } + + // Uninstall pool for Alice + vm.startPrank(alice); + evc.setAccountOperator(alice, alicePool, false); + eulerSwapFactory.uninstallPool(); + + { + address[] memory ps = eulerSwapFactory.pools(); + assertEq(ps.length, 1); + assertEq(ps[0], bobPool); + } + + { + (address asset0, address asset1) = EulerSwap(alicePool).getAssets(); + address[] memory ps = eulerSwapFactory.poolsByPair(asset0, asset1); + assertEq(ps.length, 1); + assertEq(ps[0], bobPool); + } + + // Uninstalling pool for Bob reverts due to an OOB access of the allPools array + vm.startPrank(bob); + evc.setAccountOperator(bob, bobPool, false); + eulerSwapFactory.uninstallPool(); + + { + address[] memory ps = eulerSwapFactory.pools(); + assertEq(ps.length, 0); + } + + { + (address asset0, address asset1) = EulerSwap(alicePool).getAssets(); + address[] memory ps = eulerSwapFactory.poolsByPair(asset0, asset1); + assertEq(ps.length, 0); + } + } +} diff --git a/test/Fees.t.sol b/test/Fees.t.sol new file mode 100644 index 0000000..61ee202 --- /dev/null +++ b/test/Fees.t.sol @@ -0,0 +1,119 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.24; + +import {Test, console} from "forge-std/Test.sol"; +import {IEVault, IEulerSwap, EulerSwapTestBase, EulerSwap, TestERC20} from "./EulerSwapTestBase.t.sol"; +import {CurveLib} from "../src/libraries/CurveLib.sol"; + +contract FeesTest is EulerSwapTestBase { + EulerSwap public eulerSwap; + + function setUp() public virtual override { + super.setUp(); + + eulerSwap = createEulerSwap(60e18, 60e18, 0, 1e18, 1e18, 0.9e18, 0.9e18); + } + + function test_fees_exactIn() public monotonicHolderNAV { + int256 origNav = getHolderNAV(); + uint256 fee = 0.05e18; + + // No fees + + uint256 amountInNoFees = 1e18; + uint256 amountOutNoFees = + periphery.quoteExactInput(address(eulerSwap), address(assetTST), address(assetTST2), amountInNoFees); + assertApproxEqAbs(amountOutNoFees, 0.9983e18, 0.0001e18); + + // With fees: Increase input amount so that corresponding output amount matches + + eulerSwap = createEulerSwap(60e18, 60e18, fee, 1e18, 1e18, 0.9e18, 0.9e18); + + uint256 amountIn = amountInNoFees * 1e18 / (1e18 - fee); + uint256 amountOut = + periphery.quoteExactInput(address(eulerSwap), address(assetTST), address(assetTST2), amountIn); + assertApproxEqAbs(amountOut, amountOutNoFees, 1); // Same except for possible rounding down by 1 + + // Actually execute swap + + assetTST.mint(address(this), amountIn); + assetTST.transfer(address(eulerSwap), amountIn); + + // Pulling out one extra reverts... + + vm.expectRevert(CurveLib.CurveViolation.selector); + eulerSwap.swap(0, amountOut + MAX_QUOTE_ERROR + 1, address(this), ""); + + // Just right: + + eulerSwap.swap(0, amountOut, address(this), ""); + + // Swapper received their quoted amount: + + assertEq(assetTST2.balanceOf(address(this)), amountOut); + + // eulerSwap instance is empty: + + assertEq(assetTST.balanceOf(address(eulerSwap)), 0); + assertEq(assetTST2.balanceOf(address(eulerSwap)), 0); + + // Holder's NAV increased by fee amount, plus slightly extra because we are not at curve equilibrium point + + uint256 protocolFeesCollected = assetTST.balanceOf(address(0)); + + assertGt(getHolderNAV() + int256(protocolFeesCollected), origNav + int256(amountIn - amountInNoFees)); + assertEq(eTST.balanceOf(address(holder)), 10e18 + amountIn - protocolFeesCollected); + assertEq(eTST2.balanceOf(address(holder)), 10e18 - amountOut); + } + + function test_fees_exactOut() public monotonicHolderNAV { + int256 origNav = getHolderNAV(); + uint256 fee = 0.05e18; + + // No fees + + uint256 amountOut = 1e18; + uint256 amountInNoFees = + periphery.quoteExactOutput(address(eulerSwap), address(assetTST), address(assetTST2), amountOut); + assertApproxEqAbs(amountInNoFees, 1.0017e18, 0.0001e18); + + // With fees: Increase input amount so output amount stays same + + eulerSwap = createEulerSwap(60e18, 60e18, fee, 1e18, 1e18, 0.9e18, 0.9e18); + + uint256 amountIn = + periphery.quoteExactOutput(address(eulerSwap), address(assetTST), address(assetTST2), amountOut); + assertApproxEqAbs(amountIn, amountInNoFees * 1e18 / (1e18 - fee), 1); // Same except for possible rounding up by 1 + + // Actually execute swap + + assetTST.mint(address(this), amountIn); + assetTST.transfer(address(eulerSwap), amountIn); + + // Pulling out one extra reverts... + + vm.expectRevert(CurveLib.CurveViolation.selector); + eulerSwap.swap(0, amountOut + MAX_QUOTE_ERROR + 1, address(this), ""); + + // Just right: + + eulerSwap.swap(0, amountOut, address(this), ""); + + // Swapper received their quoted amount: + + assertEq(assetTST2.balanceOf(address(this)), amountOut); + + // eulerSwap instance is empty: + + assertEq(assetTST.balanceOf(address(eulerSwap)), 0); + assertEq(assetTST2.balanceOf(address(eulerSwap)), 0); + + // Holder's NAV increased by fee amount, plus slightly extra because we are not at curve equilibrium point + + uint256 protocolFeesCollected = assetTST.balanceOf(address(0)); + + assertGt(getHolderNAV() + int256(protocolFeesCollected), origNav + int256(amountIn - amountInNoFees)); + assertEq(eTST.balanceOf(address(holder)), 10e18 + amountIn - protocolFeesCollected); + assertEq(eTST2.balanceOf(address(holder)), 10e18 - amountOut); + } +} diff --git a/test/HookFees.t.sol b/test/HookFees.t.sol new file mode 100644 index 0000000..e8aa47f --- /dev/null +++ b/test/HookFees.t.sol @@ -0,0 +1,177 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.24; + +import {EulerSwapTestBase, EulerSwap, EulerSwapPeriphery, IEulerSwap} from "./EulerSwapTestBase.t.sol"; +import {TestERC20} from "evk-test/unit/evault/EVaultTestBase.t.sol"; +import {EulerSwap} from "../src/EulerSwap.sol"; + +import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; +import {IPoolManager, PoolManagerDeployer} from "./utils/PoolManagerDeployer.sol"; +import {PoolSwapTest} from "@uniswap/v4-core/src/test/PoolSwapTest.sol"; +import {MinimalRouter} from "./utils/MinimalRouter.sol"; +import {Currency, CurrencyLibrary} from "@uniswap/v4-core/src/types/Currency.sol"; +import {StateLibrary} from "@uniswap/v4-core/src/libraries/StateLibrary.sol"; +import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol"; +import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; + +contract HookFeesTest is EulerSwapTestBase { + using StateLibrary for IPoolManager; + + address protocolFeeRecipient = makeAddr("protocolFeeRecipient"); + + EulerSwap public eulerSwap; + + IPoolManager public poolManager; + PoolSwapTest public swapRouter; + MinimalRouter public minimalRouter; + + PoolSwapTest.TestSettings public settings = PoolSwapTest.TestSettings({takeClaims: false, settleUsingBurn: false}); + + function setUp() public virtual override { + super.setUp(); + + poolManager = PoolManagerDeployer.deploy(address(this)); + swapRouter = new PoolSwapTest(poolManager); + minimalRouter = new MinimalRouter(poolManager); + + deployEulerSwap(address(poolManager)); + + // set swap fee to 10 bips + eulerSwap = createEulerSwapHook(60e18, 60e18, 0.001e18, 1e18, 1e18, 0.4e18, 0.85e18); + + // confirm pool was created + assertFalse(eulerSwap.poolKey().currency1 == CurrencyLibrary.ADDRESS_ZERO); + (uint160 sqrtPriceX96,,,) = poolManager.getSlot0(eulerSwap.poolKey().toId()); + assertNotEq(sqrtPriceX96, 0); + } + + function test_SwapExactIn_withLpFee() public { + int256 origNav = getHolderNAV(); + (uint112 r0, uint112 r1,) = eulerSwap.getReserves(); + + uint256 amountIn = 1e18; + uint256 amountInWithoutFee = amountIn - (amountIn * eulerSwap.getParams().fee / 1e18); + uint256 amountOut = + periphery.quoteExactInput(address(eulerSwap), address(assetTST), address(assetTST2), amountIn); + + assetTST.mint(anyone, amountIn); + + vm.startPrank(anyone); + assetTST.approve(address(minimalRouter), amountIn); + + bool zeroForOne = address(assetTST) < address(assetTST2); + BalanceDelta result = minimalRouter.swap(eulerSwap.poolKey(), zeroForOne, amountIn, 0, ""); + vm.stopPrank(); + + assertEq(assetTST.balanceOf(anyone), 0); + assertEq(assetTST2.balanceOf(anyone), amountOut); + + assertEq(zeroForOne ? uint256(-int256(result.amount0())) : uint256(-int256(result.amount1())), amountIn); + assertEq(zeroForOne ? uint256(int256(result.amount1())) : uint256(int256(result.amount0())), amountOut); + + // assert fees were not added to the reserves + (uint112 r0New, uint112 r1New,) = eulerSwap.getReserves(); + if (zeroForOne) { + assertEq(r0New, r0 + amountInWithoutFee); + assertEq(r1New, r1 - amountOut); + } else { + // oneForZero, so the curve received asset1 + assertEq(r0New, r0 - amountOut); + assertEq(r1New, r1 + amountInWithoutFee); + } + + assertGt(getHolderNAV(), origNav + int256(amountIn - amountInWithoutFee)); + } + + function test_SwapExactOut_withLpFee() public { + int256 origNav = getHolderNAV(); + (uint112 r0, uint112 r1,) = eulerSwap.getReserves(); + + uint256 amountOut = 1e18; + uint256 amountIn = + periphery.quoteExactOutput(address(eulerSwap), address(assetTST), address(assetTST2), amountOut); + + // inverse of the fee math in Periphery + uint256 amountInWithoutFee = amountIn * (1e18 - eulerSwap.getParams().fee) / 1e18; + + assetTST.mint(anyone, amountIn); + + vm.startPrank(anyone); + assetTST.approve(address(minimalRouter), amountIn); + + bool zeroForOne = address(assetTST) < address(assetTST2); + BalanceDelta result = minimalRouter.swap(eulerSwap.poolKey(), zeroForOne, amountIn, amountOut, ""); + vm.stopPrank(); + + assertEq(assetTST.balanceOf(anyone), 0); + assertEq(assetTST2.balanceOf(anyone), amountOut); + + assertEq(zeroForOne ? uint256(-int256(result.amount0())) : uint256(-int256(result.amount1())), amountIn); + assertEq(zeroForOne ? uint256(int256(result.amount1())) : uint256(int256(result.amount0())), amountOut); + + // assert fees were not added to the reserves + (uint112 r0New, uint112 r1New,) = eulerSwap.getReserves(); + if (zeroForOne) { + assertEq(r0New, r0 + amountInWithoutFee + 1); // 1 wei of imprecision + assertEq(r1New, r1 - amountOut); + } else { + // oneForZero, so the curve received asset1 + assertEq(r0New, r0 - amountOut); + assertEq(r1New, r1 + amountInWithoutFee); + } + + assertGt(getHolderNAV(), origNav + int256(amountIn - amountInWithoutFee)); + } + + function test_protocolFee() public { + // set protocol fee to 10% of the LP fee + uint256 protocolFee = 0.1e18; + + eulerSwapFactory.setProtocolFee(protocolFee); + eulerSwapFactory.setProtocolFeeRecipient(protocolFeeRecipient); + + // set swap fee to 10 bips and activate the pool + eulerSwap = createEulerSwapHookFull( + 60e18, 60e18, 0.001e18, 1e18, 1e18, 0.4e18, 0.85e18, protocolFee, protocolFeeRecipient + ); + + int256 origNav = getHolderNAV(); + (uint112 r0, uint112 r1,) = eulerSwap.getReserves(); + + uint256 amountIn = 1e18; + uint256 amountInWithoutFee = amountIn - (amountIn * eulerSwap.getParams().fee / 1e18); + uint256 amountOut = + periphery.quoteExactInput(address(eulerSwap), address(assetTST), address(assetTST2), amountIn); + + assetTST.mint(anyone, amountIn); + + vm.startPrank(anyone); + assetTST.approve(address(minimalRouter), amountIn); + + bool zeroForOne = address(assetTST) < address(assetTST2); + BalanceDelta result = minimalRouter.swap(eulerSwap.poolKey(), zeroForOne, amountIn, 0, ""); + vm.stopPrank(); + + assertEq(assetTST.balanceOf(anyone), 0); + assertEq(assetTST2.balanceOf(anyone), amountOut); + + assertEq(zeroForOne ? uint256(-int256(result.amount0())) : uint256(-int256(result.amount1())), amountIn); + assertEq(zeroForOne ? uint256(int256(result.amount1())) : uint256(int256(result.amount0())), amountOut); + + // assert fees were not added to the reserves + (uint112 r0New, uint112 r1New,) = eulerSwap.getReserves(); + if (zeroForOne) { + assertEq(r0New, r0 + amountInWithoutFee); + assertEq(r1New, r1 - amountOut); + } else { + // oneForZero, so the curve received asset1 + assertEq(r0New, r0 - amountOut); + assertEq(r1New, r1 + amountInWithoutFee); + } + + uint256 protocolFeeCollected = assetTST.balanceOf(protocolFeeRecipient); + assertGt(protocolFeeCollected, 0); + + assertGt(getHolderNAV(), origNav + int256(amountIn - amountInWithoutFee) - int256(protocolFeeCollected)); + } +} diff --git a/test/HookSwaps.t.sol b/test/HookSwaps.t.sol new file mode 100644 index 0000000..ad28c4f --- /dev/null +++ b/test/HookSwaps.t.sol @@ -0,0 +1,204 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.24; + +import {EulerSwapTestBase, EulerSwap, EulerSwapPeriphery, IEulerSwap} from "./EulerSwapTestBase.t.sol"; +import {TestERC20} from "evk-test/unit/evault/EVaultTestBase.t.sol"; +import {EulerSwap} from "../src/EulerSwap.sol"; +import {UniswapHook} from "../src/UniswapHook.sol"; + +import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; +import {IPoolManager, PoolManagerDeployer} from "./utils/PoolManagerDeployer.sol"; +import {PoolSwapTest} from "@uniswap/v4-core/src/test/PoolSwapTest.sol"; +import {MinimalRouter} from "./utils/MinimalRouter.sol"; +import {PoolModifyLiquidityTest} from "@uniswap/v4-core/src/test/PoolModifyLiquidityTest.sol"; +import {Currency, CurrencyLibrary} from "@uniswap/v4-core/src/types/Currency.sol"; +import {StateLibrary} from "@uniswap/v4-core/src/libraries/StateLibrary.sol"; +import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol"; +import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; +import {CustomRevert} from "@uniswap/v4-core/src/libraries/CustomRevert.sol"; +import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol"; +import {Hooks} from "@uniswap/v4-core/src/libraries/Hooks.sol"; + +contract HookSwapsTest is EulerSwapTestBase { + using StateLibrary for IPoolManager; + + EulerSwap public eulerSwap; + + IPoolManager public poolManager; + PoolSwapTest public swapRouter; + MinimalRouter public minimalRouter; + PoolModifyLiquidityTest public liquidityManager; + + PoolSwapTest.TestSettings public settings = PoolSwapTest.TestSettings({takeClaims: false, settleUsingBurn: false}); + + function setUp() public virtual override { + super.setUp(); + + poolManager = PoolManagerDeployer.deploy(address(this)); + swapRouter = new PoolSwapTest(poolManager); + minimalRouter = new MinimalRouter(poolManager); + liquidityManager = new PoolModifyLiquidityTest(poolManager); + + deployEulerSwap(address(poolManager)); + + eulerSwap = createEulerSwapHook(60e18, 60e18, 0, 1e18, 1e18, 0.4e18, 0.85e18); + + // confirm pool was created + assertFalse(eulerSwap.poolKey().currency1 == CurrencyLibrary.ADDRESS_ZERO); + (uint160 sqrtPriceX96,,,) = poolManager.getSlot0(eulerSwap.poolKey().toId()); + assertNotEq(sqrtPriceX96, 0); + } + + function test_SwapExactIn() public { + uint256 amountIn = 1e18; + uint256 amountOut = + periphery.quoteExactInput(address(eulerSwap), address(assetTST), address(assetTST2), amountIn); + + assetTST.mint(anyone, amountIn); + + vm.startPrank(anyone); + assetTST.approve(address(minimalRouter), amountIn); + + bool zeroForOne = address(assetTST) < address(assetTST2); + BalanceDelta result = minimalRouter.swap(eulerSwap.poolKey(), zeroForOne, amountIn, 0, ""); + vm.stopPrank(); + + assertEq(assetTST.balanceOf(anyone), 0); + assertEq(assetTST2.balanceOf(anyone), amountOut); + + assertEq(zeroForOne ? uint256(-int256(result.amount0())) : uint256(-int256(result.amount1())), amountIn); + assertEq(zeroForOne ? uint256(int256(result.amount1())) : uint256(int256(result.amount0())), amountOut); + } + + /// @dev swapping with an amount that exceeds PoolManager's ERC20 token balance will revert + /// if the router does not pre-pay the input + function test_swapExactIn_revertWithoutTokenLiquidity() public { + uint256 amountIn = 1e18; // input amount exceeds PoolManager balance + + assetTST.mint(anyone, amountIn); + + vm.startPrank(anyone); + assetTST.approve(address(swapRouter), amountIn); + + bool zeroForOne = address(assetTST) < address(assetTST2); + PoolKey memory poolKey = eulerSwap.poolKey(); + vm.expectRevert(); + _swap(poolKey, zeroForOne, true, amountIn); + vm.stopPrank(); + } + + function test_SwapExactOut() public { + uint256 amountOut = 1e18; + uint256 amountIn = + periphery.quoteExactOutput(address(eulerSwap), address(assetTST), address(assetTST2), amountOut); + + assetTST.mint(anyone, amountIn); + + vm.startPrank(anyone); + assetTST.approve(address(minimalRouter), amountIn); + + bool zeroForOne = address(assetTST) < address(assetTST2); + BalanceDelta result = minimalRouter.swap(eulerSwap.poolKey(), zeroForOne, amountIn, amountOut, ""); + vm.stopPrank(); + + assertEq(assetTST.balanceOf(anyone), 0); + assertEq(assetTST2.balanceOf(anyone), amountOut); + + assertEq(zeroForOne ? uint256(-int256(result.amount0())) : uint256(-int256(result.amount1())), amountIn); + assertEq(zeroForOne ? uint256(int256(result.amount1())) : uint256(int256(result.amount0())), amountOut); + } + + /// @dev swapping with an amount that exceeds PoolManager's ERC20 token balance will revert + /// if the router does not pre-pay the input + function test_SwapExactOut_revertWithoutTokenLiquidity() public { + uint256 amountOut = 1e18; + uint256 amountIn = + periphery.quoteExactOutput(address(eulerSwap), address(assetTST), address(assetTST2), amountOut); + + assetTST.mint(anyone, amountIn); + + vm.startPrank(anyone); + assetTST.approve(address(swapRouter), amountIn); + bool zeroForOne = address(assetTST) < address(assetTST2); + PoolKey memory poolKey = eulerSwap.poolKey(); + vm.expectRevert(); + _swap(poolKey, zeroForOne, false, amountOut); + vm.stopPrank(); + } + + function test_hookPermissions() public view { + Hooks.Permissions memory perms = eulerSwap.getHookPermissions(); + + assertTrue(perms.beforeInitialize); + assertTrue(perms.beforeAddLiquidity); + assertTrue(perms.beforeSwap); + assertTrue(perms.beforeSwapReturnDelta); + + assertFalse(perms.afterInitialize); + assertFalse(perms.afterAddLiquidity); + assertFalse(perms.beforeRemoveLiquidity); + assertFalse(perms.afterRemoveLiquidity); + assertFalse(perms.afterSwap); + assertFalse(perms.beforeDonate); + assertFalse(perms.afterDonate); + assertFalse(perms.afterSwapReturnDelta); + assertFalse(perms.afterAddLiquidityReturnDelta); + assertFalse(perms.afterRemoveLiquidityReturnDelta); + } + + /// @dev adding liquidity as a concentrated liquidity position will revert + function test_revertAddConcentratedLiquidity() public { + assetTST.mint(anyone, 10000e18); + assetTST2.mint(anyone, 10000e18); + + vm.startPrank(anyone); + assetTST.approve(address(liquidityManager), 1e18); + assetTST2.approve(address(liquidityManager), 1e18); + + PoolKey memory poolKey = eulerSwap.poolKey(); + + // hook intentionally reverts to prevent v3-CLAMM positions + vm.expectRevert( + abi.encodeWithSelector( + CustomRevert.WrappedError.selector, + address(eulerSwap), + IHooks.beforeAddLiquidity.selector, + abi.encodeWithSelector(UniswapHook.NativeConcentratedLiquidityUnsupported.selector), + abi.encodeWithSelector(Hooks.HookCallFailed.selector) + ) + ); + liquidityManager.modifyLiquidity( + poolKey, + IPoolManager.ModifyLiquidityParams({tickLower: -10, tickUpper: 10, liquidityDelta: 1000, salt: bytes32(0)}), + "" + ); + vm.stopPrank(); + } + + /// @dev initializing a new pool on an existing eulerswap instance will revert + function test_revertSubsequentInitialize() public { + PoolKey memory newPoolKey = eulerSwap.poolKey(); + newPoolKey.currency0 = CurrencyLibrary.ADDRESS_ZERO; + + // hook intentionally reverts to prevent subsequent initializations + vm.expectRevert( + abi.encodeWithSelector( + CustomRevert.WrappedError.selector, + address(eulerSwap), + IHooks.beforeInitialize.selector, + abi.encodeWithSelector(UniswapHook.AlreadyInitialized.selector), + abi.encodeWithSelector(Hooks.HookCallFailed.selector) + ) + ); + poolManager.initialize(newPoolKey, 79228162514264337593543950336); + } + + function _swap(PoolKey memory key, bool zeroForOne, bool exactInput, uint256 amount) internal { + IPoolManager.SwapParams memory swapParams = IPoolManager.SwapParams({ + zeroForOne: zeroForOne, + amountSpecified: exactInput ? -int256(amount) : int256(amount), + sqrtPriceLimitX96: zeroForOne ? TickMath.MIN_SQRT_PRICE + 1 : TickMath.MAX_SQRT_PRICE - 1 + }); + swapRouter.swap(key, swapParams, settings, ""); + } +} diff --git a/test/Limits.t.sol b/test/Limits.t.sol new file mode 100644 index 0000000..2cef14e --- /dev/null +++ b/test/Limits.t.sol @@ -0,0 +1,150 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.24; + +import {EulerSwapTestBase, EulerSwap, EulerSwapPeriphery, IEulerSwap} from "./EulerSwapTestBase.t.sol"; +import {QuoteLib} from "../src/libraries/QuoteLib.sol"; + +contract LimitsTest is EulerSwapTestBase { + EulerSwap public eulerSwap; + + function setUp() public virtual override { + super.setUp(); + + eulerSwap = createEulerSwap(60e18, 60e18, 0, 1e18, 1e18, 0.9e18, 0.9e18); + } + + function test_basicLimits() public { + (uint256 inLimit, uint256 outLimit) = + periphery.getLimits(address(eulerSwap), address(assetTST), address(assetTST2)); + + assertEq(inLimit, type(uint112).max - 110e18); // max uint minus 110 (100 deposited by depositor, 10 by holder) + assertEq(outLimit, 60e18); + + // Exact output + + uint256 quote = periphery.quoteExactOutput(address(eulerSwap), address(assetTST), address(assetTST2), 50e18); + assertEq(quote, 75e18); + + quote = periphery.quoteExactOutput(address(eulerSwap), address(assetTST), address(assetTST2), 59.9999999e18); + assertApproxEqAbs(quote, 3.6e27, 0.1e27); + + vm.expectRevert(QuoteLib.SwapLimitExceeded.selector); + quote = periphery.quoteExactOutput(address(eulerSwap), address(assetTST), address(assetTST2), 60e18); + + vm.expectRevert(QuoteLib.SwapLimitExceeded.selector); + quote = periphery.quoteExactOutput(address(eulerSwap), address(assetTST), address(assetTST2), 60.000001e18); + + // Exact input + + vm.expectRevert(QuoteLib.SwapLimitExceeded.selector); + quote = periphery.quoteExactInput(address(eulerSwap), address(assetTST), address(assetTST2), type(uint112).max); + } + + function test_basicLimitsReverse() public view { + (uint256 inLimit, uint256 outLimit) = + periphery.getLimits(address(eulerSwap), address(assetTST2), address(assetTST)); + + assertEq(outLimit, 60e18); + assertEq(inLimit, type(uint112).max - 110e18); + } + + function test_supplyCapExceeded() public { + eTST.setCaps(uint16(2.72e2 << 6) | 18, 0); + + (uint256 inLimit, uint256 outLimit) = + periphery.getLimits(address(eulerSwap), address(assetTST), address(assetTST2)); + + assertEq(inLimit, 0); // cap exceeded + assertEq(outLimit, 60e18); + + vm.expectRevert(QuoteLib.SwapLimitExceeded.selector); + periphery.quoteExactInput(address(eulerSwap), address(assetTST), address(assetTST2), 1); + } + + function test_supplyCapExceededReverse() public { + eTST2.setCaps(uint16(2.72e2 << 6) | 18, 0); + + (uint256 inLimit, uint256 outLimit) = + periphery.getLimits(address(eulerSwap), address(assetTST2), address(assetTST)); + + assertEq(inLimit, 0); // cap exceeded + assertEq(outLimit, 60e18); + } + + function test_supplyCapExtra() public { + eTST.setCaps(uint16(2.72e2 << 6) | (18 + 2), 0); + + (uint256 inLimit, uint256 outLimit) = + periphery.getLimits(address(eulerSwap), address(assetTST), address(assetTST2)); + + assertEq(inLimit, 162e18); // 272 - 110 + assertEq(outLimit, 60e18); + + uint256 quote = + periphery.quoteExactInput(address(eulerSwap), address(assetTST), address(assetTST2), 161.9999e18); + assertApproxEqAbs(quote, 56.9e18, 0.1e18); + + vm.expectRevert(QuoteLib.SwapLimitExceeded.selector); + periphery.quoteExactInput(address(eulerSwap), address(assetTST), address(assetTST2), 162e18 + 1); + } + + function test_utilisation() public { + vm.prank(depositor); + eTST2.withdraw(95e18, address(depositor), address(depositor)); + + (uint256 inLimit, uint256 outLimit) = + periphery.getLimits(address(eulerSwap), address(assetTST), address(assetTST2)); + + assertEq(inLimit, type(uint112).max - 110e18); + assertEq(outLimit, 15e18); // 110 - 95 + } + + function test_borrowCap() public { + eTST2.setCaps(0, uint16(8.5e2 << 6) | 18); + + (uint256 inLimit, uint256 outLimit) = + periphery.getLimits(address(eulerSwap), address(assetTST), address(assetTST2)); + + assertEq(inLimit, type(uint112).max - 110e18); + assertEq(outLimit, 18.5e18); // 10 in balance, plus 8.5 borrow cap + } + + function test_amountTooBig() public monotonicHolderNAV { + vm.expectRevert(EulerSwap.AmountTooBig.selector); + eulerSwap.swap(type(uint256).max, 0, address(this), ""); + + vm.expectRevert(EulerSwap.AmountTooBig.selector); + eulerSwap.swap(0, type(uint256).max, address(this), ""); + } + + function test_quoteWhenAboveCurve() public { + // Donate 100 and 100 to the pool, raising the reserves above the curve + assetTST.mint(depositor, 100e18); + assetTST2.mint(depositor, 100e18); + vm.prank(depositor); + assetTST.transfer(address(eulerSwap), 10e18); + vm.prank(depositor); + assetTST2.transfer(address(eulerSwap), 10e18); + eulerSwap.swap(0, 0, address(this), ""); + + uint256 amount; + + // Exact output quotes: Costs nothing to perform this swap (in theory the quote could + // be negative, but this is not supported by the interface) + + amount = periphery.quoteExactOutput(address(eulerSwap), address(assetTST), address(assetTST2), 1e18); + assertEq(amount, 0); + + amount = periphery.quoteExactOutput(address(eulerSwap), address(assetTST2), address(assetTST), 1e18); + assertEq(amount, 0); + + // Exact input quotes: The additional extractable value is provided as swap output, even + // with tiny quotes such as 1 wei. + + amount = periphery.quoteExactInput(address(eulerSwap), address(assetTST), address(assetTST2), 1); + assertApproxEqAbs(amount, 19.8e18, 0.1e18); + + amount = periphery.quoteExactInput(address(eulerSwap), address(assetTST2), address(assetTST), 1); + assertApproxEqAbs(amount, 19.8e18, 0.1e18); + } +} diff --git a/test/Periphery.t.sol b/test/Periphery.t.sol new file mode 100644 index 0000000..75fa610 --- /dev/null +++ b/test/Periphery.t.sol @@ -0,0 +1,131 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.24; + +import {EulerSwapTestBase, EulerSwap, EulerSwapPeriphery, IEulerSwap} from "./EulerSwapTestBase.t.sol"; + +contract PeripheryTest is EulerSwapTestBase { + EulerSwap public eulerSwap; + + function setUp() public virtual override { + super.setUp(); + + eulerSwap = createEulerSwap(60e18, 60e18, 0, 1e18, 1e18, 0.4e18, 0.85e18); + } + + function test_SwapExactIn() public { + uint256 amountIn = 1e18; + uint256 amountOut = + periphery.quoteExactInput(address(eulerSwap), address(assetTST), address(assetTST2), amountIn); + + assetTST.mint(anyone, amountIn); + + vm.startPrank(anyone); + assetTST.approve(address(periphery), amountIn); + periphery.swapExactIn(address(eulerSwap), address(assetTST), address(assetTST2), amountIn, anyone, amountOut, 0); + vm.stopPrank(); + + assertEq(assetTST2.balanceOf(anyone), amountOut); + } + + function test_SwapExactIn_AmountOutLessThanMin() public { + uint256 amountIn = 1e18; + uint256 amountOut = + periphery.quoteExactInput(address(eulerSwap), address(assetTST), address(assetTST2), amountIn); + + assetTST.mint(anyone, amountIn); + + vm.startPrank(anyone); + assetTST.approve(address(periphery), amountIn); + vm.expectRevert(EulerSwapPeriphery.AmountOutLessThanMin.selector); + periphery.swapExactIn( + address(eulerSwap), address(assetTST), address(assetTST2), amountIn, anyone, amountOut + 1, 0 + ); + vm.stopPrank(); + } + + function test_SwapExactOut() public { + uint256 amountOut = 1e18; + uint256 amountIn = + periphery.quoteExactOutput(address(eulerSwap), address(assetTST), address(assetTST2), amountOut); + + assetTST.mint(anyone, amountIn); + + vm.startPrank(anyone); + assetTST.approve(address(periphery), amountIn); + periphery.swapExactOut( + address(eulerSwap), address(assetTST), address(assetTST2), amountOut, anyone, amountIn, 0 + ); + vm.stopPrank(); + + assertEq(assetTST2.balanceOf(anyone), amountOut); + } + + function test_SwapExactOut_AmountInMoreThanMax() public { + uint256 amountOut = 1e18; + uint256 amountIn = + periphery.quoteExactOutput(address(eulerSwap), address(assetTST), address(assetTST2), amountOut); + + assetTST.mint(anyone, amountIn); + + vm.startPrank(anyone); + assetTST.approve(address(periphery), amountIn); + vm.expectRevert(EulerSwapPeriphery.AmountInMoreThanMax.selector); + periphery.swapExactOut( + address(eulerSwap), address(assetTST), address(assetTST2), amountOut * 2, anyone, amountIn, 0 + ); + vm.stopPrank(); + } + + function test_SwapAltReceiver() public { + address altReceiver = address(1234); + + uint256 amountIn = 1e18; + uint256 amountOut = + periphery.quoteExactInput(address(eulerSwap), address(assetTST), address(assetTST2), amountIn); + + assetTST.mint(anyone, amountIn); + + vm.startPrank(anyone); + assetTST.approve(address(periphery), amountIn); + periphery.swapExactIn( + address(eulerSwap), address(assetTST), address(assetTST2), amountIn, altReceiver, amountOut, 0 + ); + vm.stopPrank(); + + assertEq(assetTST2.balanceOf(anyone), 0); + assertEq(assetTST2.balanceOf(altReceiver), amountOut); + } + + function test_SwapDeadline() public { + skip(1000); + + uint256 amountIn = 1e18; + uint256 amountOut = + periphery.quoteExactInput(address(eulerSwap), address(assetTST), address(assetTST2), amountIn); + + assetTST.mint(anyone, amountIn); + + vm.startPrank(anyone); + assetTST.approve(address(periphery), amountIn); + + vm.expectRevert(EulerSwapPeriphery.DeadlineExpired.selector); + periphery.swapExactIn( + address(eulerSwap), address(assetTST), address(assetTST2), amountIn, anyone, amountOut, block.timestamp - 1 + ); + + periphery.swapExactIn( + address(eulerSwap), address(assetTST), address(assetTST2), amountIn, anyone, amountOut, block.timestamp + 1 + ); + vm.stopPrank(); + + assertEq(assetTST2.balanceOf(anyone), amountOut); + } + + function test_SwapZeroAmounts() public view { + assertEq(periphery.quoteExactInput(address(eulerSwap), address(assetTST), address(assetTST2), 0), 0); + assertEq(periphery.quoteExactInput(address(eulerSwap), address(assetTST2), address(assetTST), 0), 0); + + assertEq(periphery.quoteExactOutput(address(eulerSwap), address(assetTST), address(assetTST2), 0), 0); + assertEq(periphery.quoteExactOutput(address(eulerSwap), address(assetTST2), address(assetTST), 0), 0); + } +} diff --git a/test/PreserveNav.t.sol b/test/PreserveNav.t.sol new file mode 100644 index 0000000..a20d312 --- /dev/null +++ b/test/PreserveNav.t.sol @@ -0,0 +1,94 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.24; + +import {EulerSwapTestBase, EulerSwap, TestERC20} from "./EulerSwapTestBase.t.sol"; + +contract PreserveNav is EulerSwapTestBase { + EulerSwap public eulerSwap; + + function setUp() public virtual override { + super.setUp(); + } + + function test_preserve_nav( + uint256 cx, + uint256 cy, + uint256 fee, + bool preSkimDir, + bool dir1, + uint256 amount1, + bool dir2, + uint256 amount2 + ) public { + cx = bound(cx, 0.1e18, 0.99e18); + cy = bound(cy, 0.1e18, 0.99e18); + fee = bound(fee, 0, 0.2e18); + amount1 = bound(amount1, 0.00001e18, 25e18); + amount2 = bound(amount2, 0.00001e18, 25e18); + + if (fee < 0.1e18) fee = 0; // half of the time use fee 0 + + else fee -= 0.1e18; + + eulerSwap = createEulerSwap(60e18, 60e18, fee, 1e18, 1e18, cx, cy); + + skimAll(eulerSwap, preSkimDir); + int256 nav1 = getHolderNAV(); + + { + TestERC20 t1; + TestERC20 t2; + if (dir1) (t1, t2) = (assetTST, assetTST2); + else (t1, t2) = (assetTST2, assetTST); + + uint256 q = periphery.quoteExactInput(address(eulerSwap), address(t1), address(t2), amount1); + { + uint256 qRev = periphery.quoteExactOutput(address(eulerSwap), address(t1), address(t2), q); + assertApproxEqAbs(amount1, qRev, (MAX_QUOTE_ERROR + 1) * 2); + } + + t1.mint(address(this), amount1); + t1.transfer(address(eulerSwap), amount1); + + { + uint256 qPlus = q + MAX_QUOTE_ERROR + 1; + vm.expectRevert(); + if (dir1) eulerSwap.swap(0, qPlus, address(this), ""); + else eulerSwap.swap(qPlus, 0, address(this), ""); + } + + if (dir1) eulerSwap.swap(0, q, address(this), ""); + else eulerSwap.swap(q, 0, address(this), ""); + } + + assertGe(getHolderNAV(), nav1); + + { + TestERC20 t1; + TestERC20 t2; + if (dir2) (t1, t2) = (assetTST, assetTST2); + else (t1, t2) = (assetTST2, assetTST); + + uint256 q = periphery.quoteExactInput(address(eulerSwap), address(t1), address(t2), amount2); + { + uint256 qRev = periphery.quoteExactOutput(address(eulerSwap), address(t1), address(t2), q); + assertApproxEqAbs(amount2, qRev, (MAX_QUOTE_ERROR + 1) * 2); + } + + t1.mint(address(this), amount2); + t1.transfer(address(eulerSwap), amount2); + + { + uint256 qPlus = q + MAX_QUOTE_ERROR + 1; + vm.expectRevert(); + if (dir2) eulerSwap.swap(0, qPlus, address(this), ""); + else eulerSwap.swap(qPlus, 0, address(this), ""); + } + + if (dir2) eulerSwap.swap(0, q, address(this), ""); + else eulerSwap.swap(q, 0, address(this), ""); + } + + assertGe(getHolderNAV(), nav1); + } +} diff --git a/test/UniswapV2Call.t.sol b/test/UniswapV2Call.t.sol new file mode 100644 index 0000000..1d90ed6 --- /dev/null +++ b/test/UniswapV2Call.t.sol @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.24; + +import {IUniswapV2Callee} from "../src/interfaces/IUniswapV2Callee.sol"; +import {IEVault, EulerSwapTestBase, EulerSwap, TestERC20} from "./EulerSwapTestBase.t.sol"; + +contract UniswapV2CallTest is EulerSwapTestBase { + EulerSwap public eulerSwap; + SwapCallbackTest swapCallback; + + function setUp() public virtual override { + super.setUp(); + + eulerSwap = createEulerSwap(60e18, 60e18, 0, 1e18, 1e18, 0.4e18, 0.85e18); + + swapCallback = new SwapCallbackTest(); + } + + function test_callback() public { + uint256 amountIn = 1e18; + uint256 amountOut = + periphery.quoteExactInput(address(eulerSwap), address(assetTST), address(assetTST2), amountIn); + assertApproxEqAbs(amountOut, 0.9974e18, 0.0001e18); + + assetTST.mint(address(this), amountIn); + assetTST.transfer(address(eulerSwap), amountIn); + + uint256 randomBalance = 3e18; + vm.prank(anyone); + swapCallback.executeSwap(eulerSwap, 0, amountOut, abi.encode(randomBalance)); + assertEq(assetTST2.balanceOf(address(swapCallback)), amountOut); + assertEq(swapCallback.callbackSender(), address(swapCallback)); + assertEq(swapCallback.callbackAmount0(), 0); + assertEq(swapCallback.callbackAmount1(), amountOut); + assertEq(swapCallback.randomBalance(), randomBalance); + } +} + +contract SwapCallbackTest is IUniswapV2Callee { + address public callbackSender; + uint256 public callbackAmount0; + uint256 public callbackAmount1; + uint256 public randomBalance; + + function executeSwap(EulerSwap eulerSwap, uint256 amountIn, uint256 amountOut, bytes calldata data) external { + eulerSwap.swap(amountIn, amountOut, address(this), data); + } + + function uniswapV2Call(address sender, uint256 amount0, uint256 amount1, bytes calldata data) external { + randomBalance = abi.decode(data, (uint256)); + + callbackSender = sender; + callbackAmount0 = amount0; + callbackAmount1 = amount1; + } + + function test_avoid_coverage() public pure { + return; + } +} diff --git a/test/utils/CurveExtrasLib.sol b/test/utils/CurveExtrasLib.sol new file mode 100644 index 0000000..9d3933b --- /dev/null +++ b/test/utils/CurveExtrasLib.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.27; + +import {Math} from "openzeppelin-contracts/utils/math/Math.sol"; + +library CurveExtrasLib { + /// @dev EulerSwap derivative helper function to find the price after a swap + /// Pre-conditions: 0 < x <= x0 <= type(uint112).max, 1 <= {px,py} <= 1e36, c <= 1e18 + function df_dx(uint256 x, uint256 px, uint256 py, uint256 x0, uint256 c) internal pure returns (int256) { + uint256 r = Math.mulDiv(x0 * x0 / x, 1e18, x, Math.Rounding.Ceil); + return -int256(px * (c + (1e18 - c) * r / 1e18) / py); + } +} diff --git a/test/utils/HookMiner.sol b/test/utils/HookMiner.sol new file mode 100644 index 0000000..1b28a84 --- /dev/null +++ b/test/utils/HookMiner.sol @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.21; + +import {Hooks} from "@uniswap/v4-core/src/libraries/Hooks.sol"; + +/// @title HookMiner +/// @notice a minimal library for mining hook addresses +library HookMiner { + // mask to slice out the bottom 14 bit of the address + uint160 constant FLAG_MASK = Hooks.ALL_HOOK_MASK; // 0000 ... 0000 0011 1111 1111 1111 + + // Maximum number of iterations to find a salt, avoid infinite loops or MemoryOOG + // (arbitrarily set) + uint256 constant MAX_LOOP = 160_444; + + /// @notice Find a salt that produces a hook address with the desired `flags` + /// @param deployer The address that will deploy the hook. Typically the EulerSwapFactory. + /// @param account The account that is invoking the factory. + /// @param flags The desired flags for the hook address. Example `uint160(Hooks.BEFORE_SWAP_FLAG | Hooks.AFTER_SWAP_FLAG | ...)` + /// @param creationCode The creation code of a hook contract. Example: `type(Counter).creationCode` + /// @return (hookAddress, salt) The hook deploys to `hookAddress` when using `salt` with the syntax: `new Hook{salt: salt}()` + function find(address deployer, address account, uint160 flags, bytes memory creationCode) + internal + view + returns (address, bytes32) + { + flags = flags & FLAG_MASK; // mask for only the bottom 14 bits + + address hookAddress; + for (uint256 salt; salt < MAX_LOOP; salt++) { + hookAddress = computeAddress(deployer, account, salt, creationCode); + + // if the hook's bottom 14 bits match the desired flags AND the address does not have bytecode, we found a match + if (uint160(hookAddress) & FLAG_MASK == flags && hookAddress.code.length == 0) { + return (hookAddress, bytes32(salt)); + } + } + revert("HookMiner: could not find salt"); + } + + /// @notice Precompute a contract address deployed via CREATE2 + /// @param deployer The address that will deploy the hook. Typically the EulerSwapFactory. + /// @param account The account that is invoking the factory. + /// @param salt The salt used to deploy the hook + /// @param creationCode The creation code of a hook contract. + function computeAddress(address deployer, address account, uint256 salt, bytes memory creationCode) + internal + pure + returns (address hookAddress) + { + return address( + uint160(uint256(keccak256(abi.encodePacked(bytes1(0xFF), deployer, salt, keccak256(creationCode))))) + ); + } +} diff --git a/test/utils/MinimalRouter.sol b/test/utils/MinimalRouter.sol new file mode 100644 index 0000000..32475df --- /dev/null +++ b/test/utils/MinimalRouter.sol @@ -0,0 +1,85 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.24; + +import {CurrencyLibrary, Currency} from "@uniswap/v4-core/src/types/Currency.sol"; +import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; +import {BalanceDelta, toBalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; +import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; +import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol"; +import {Hooks} from "@uniswap/v4-core/src/libraries/Hooks.sol"; +import {CurrencySettler} from "@uniswap/v4-core/test/utils/CurrencySettler.sol"; +import {SafeCallback} from "v4-periphery/src/base/SafeCallback.sol"; +import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol"; +import {TransientStateLibrary} from "@uniswap/v4-core/src/libraries/TransientStateLibrary.sol"; + +contract MinimalRouter is SafeCallback { + using TransientStateLibrary for IPoolManager; + using CurrencySettler for Currency; + + uint160 public constant MIN_PRICE_LIMIT = TickMath.MIN_SQRT_PRICE + 1; + uint160 public constant MAX_PRICE_LIMIT = TickMath.MAX_SQRT_PRICE - 1; + + constructor(IPoolManager _manager) SafeCallback(_manager) {} + + /// @dev an unsafe swap function that does not check for slippage + /// @param key The pool key + /// @param zeroForOne The direction of the swap + /// @param amountIn The amount of input token, should be provided (as an estimate) for exact output swaps + /// @param amountOut The amount of output token can be provided as 0, for exact input swaps + /// @param hookData The data to pass to the hook + function swap(PoolKey memory key, bool zeroForOne, uint256 amountIn, uint256 amountOut, bytes memory hookData) + external + payable + returns (BalanceDelta delta) + { + delta = abi.decode( + poolManager.unlock(abi.encode(msg.sender, key, zeroForOne, amountIn, amountOut, hookData)), (BalanceDelta) + ); + + uint256 ethBalance = address(this).balance; + if (ethBalance > 0) CurrencyLibrary.ADDRESS_ZERO.transfer(msg.sender, ethBalance); + } + + function _unlockCallback(bytes calldata data) internal override returns (bytes memory) { + ( + address sender, + PoolKey memory key, + bool zeroForOne, + uint256 amountIn, + uint256 amountOut, + bytes memory hookData + ) = abi.decode(data, (address, PoolKey, bool, uint256, uint256, bytes)); + + // send the input first to avoid PoolManager token balance issues + zeroForOne + ? key.currency0.settle(poolManager, sender, amountIn, false) + : key.currency1.settle(poolManager, sender, amountIn, false); + + // execute the swap + poolManager.swap( + key, + IPoolManager.SwapParams({ + zeroForOne: zeroForOne, + amountSpecified: amountOut != 0 ? int256(amountOut) : -int256(amountIn), + sqrtPriceLimitX96: zeroForOne ? MIN_PRICE_LIMIT : MAX_PRICE_LIMIT + }), + hookData + ); + + // observe deltas + int256 delta0 = poolManager.currencyDelta(address(this), key.currency0); + int256 delta1 = poolManager.currencyDelta(address(this), key.currency1); + + // take the output + if (delta0 > 0) key.currency0.take(poolManager, sender, uint256(delta0), false); + if (delta1 > 0) key.currency1.take(poolManager, sender, uint256(delta1), false); + + // account for prepaid input against the observed deltas + BalanceDelta returnDelta = toBalanceDelta(int128(delta0), int128(delta1)) + + toBalanceDelta( + zeroForOne ? -int128(int256(amountIn)) : int128(0), zeroForOne ? int128(0) : -int128(int256(amountIn)) + ); + + return abi.encode(returnDelta); + } +} diff --git a/test/utils/PoolManagerDeployer.sol b/test/utils/PoolManagerDeployer.sol new file mode 100644 index 0000000..04b53ad --- /dev/null +++ b/test/utils/PoolManagerDeployer.sol @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: MIT +pragma solidity >= 0.8.0; + +import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; + +// temporarily stolen from: https://github.com/Uniswap/briefcase/blob/main/src/deployers/v4-core/PoolManagerDeployer.sol +library PoolManagerDeployer { + function deploy(address initialOwner) internal returns (IPoolManager manager) { + bytes memory args = abi.encode(initialOwner); + bytes memory initcode_ = abi.encodePacked(initcode(), args); + + assembly { + manager := create2(0, add(initcode_, 32), mload(initcode_), hex"00") + } + } + + /** + * @dev autogenerated - run `./script/util/create_briefcase.sh` to generate current initcode + * + * @notice This initcode is generated from the following contract: + * - Source Contract: lib/v4-core/src/PoolManager.sol + */ + function initcode() internal pure returns (bytes memory) { + return + hex"60a03460a057601f615e8238819003918201601f19168301916001600160401b0383118484101760a45780849260209460405283398101031260a057516001600160a01b0381169081900360a0575f80546001600160a01b0319168217815560405191907f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e08180a330608052615dc990816100b98239608051816135260152f35b5f80fd5b634e487b7160e01b5f52604160045260245ffdfe60a0806040526004361015610012575f80fd5b5f3560e01c908162fdd58e14612cd55750806301ffc9a714612c16578063095bcdb614612b6c5780630b0d9c0914612ae057806311da60b414612a85578063156e29f6146129d55780631e2eaeaf1461299b578063234266d7146126fc5780632d7713891461265157806335fd631a146125dd5780633dd45adb14612579578063426a8493146124f557806348c894911461226a5780635275965114612152578063558a72971461207b578063598af9e714611fe35780635a6bcfda1461144f5780636276cbbe14610f965780637e87ce7d14610e5957806380f0b44c14610d875780638161b87414610c315780638da5cb5b14610be157806397e8cd4e14610b7e5780639bf6645f14610b31578063a584119414610a66578063b6363cf2146109d5578063dbd035ff1461097f578063f02de3b21461092e578063f135baaa146108f4578063f2fde38b14610848578063f3cd914c146104ff578063f5298aca146103345763fe99049a14610186575f80fd5b346103305760807ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc360112610330576101bd612d3f565b6101c5612d62565b90604435917f1b3d7edb2e9c0b0e7c525b20aaaef0f5940d2ed71663c7d39266ecafac72885961027973ffffffffffffffffffffffffffffffffffffffff80606435951693843314158061030d575b610287575b845f52600460205260405f20875f5260205260405f2061023a878254612fed565b90551693845f52600460205260405f20865f5260205260405f2061025f828254612ffa565b905560408051338152602081019290925290918291820190565b0390a4602060405160018152f35b845f52600560205260405f208233165f5260205260405f20875f5260205260405f2054867fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff82036102da575b5050610219565b6102e391612fed565b855f52600560205260405f208333165f5260205260405f20885f5260205260405f20555f866102d3565b50845f52600360205260405f208233165f5260205260ff60405f20541615610214565b5f80fd5b346103305761034236612d85565b7fc090fc4683624cfc3884e9d8de5eca132f2d0ec062aff75d43c0465d5ceeab235c156104d7577f1b3d7edb2e9c0b0e7c525b20aaaef0f5940d2ed71663c7d39266ecafac7288596103ed73ffffffffffffffffffffffffffffffffffffffff805f9516956103bb6103b3866130aa565b3390896130f0565b169233841415806104a0575b6103f2575b8385526004602052604085208686526020526040852061025f828254612fed565b0390a4005b83855260056020526040852073ffffffffffffffffffffffffffffffffffffffff33168652602052604085208686526020526040852054817fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8203610459575b50506103cc565b61046291612fed565b84865260056020526040862073ffffffffffffffffffffffffffffffffffffffff331687526020526040862087875260205260408620558681610452565b5083855260036020526040852073ffffffffffffffffffffffffffffffffffffffff3316865260205260ff604086205416156103c7565b7f54e3ca0d000000000000000000000000000000000000000000000000000000005f5260045ffd5b34610330576101207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126103305761053836612e81565b60607fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff5c360112610330576040519061056f82612df6565b60a4358015158103610330578252602082019060c435825260e4359073ffffffffffffffffffffffffffffffffffffffff8216820361033057604084019182526101043567ffffffffffffffff8111610330576105d0903690600401612f4d565b9290937fc090fc4683624cfc3884e9d8de5eca132f2d0ec062aff75d43c0465d5ceeab235c156104d75761060261350f565b51156108205760a0822092835f52600660205260405f209061062382613576565b60808401958482828a8a5173ffffffffffffffffffffffffffffffffffffffff169361064e94613b44565b90949195606088015160020b908b511515905173ffffffffffffffffffffffffffffffffffffffff1691604051986106858a612e12565b895260208901526040880152606087015262ffffff166080860152885115155f149862ffffff6107a2986106db61078f9860209d6108005773ffffffffffffffffffffffffffffffffffffffff8b511695614959565b9492968291926107d3575b505073ffffffffffffffffffffffffffffffffffffffff845116938e6fffffffffffffffffffffffffffffffff60408301511691015160020b90604051958860801d600f0b875288600f0b60208801526040870152606086015260808501521660a08301527f40e9cecb9f5f1f1c5b9c97dec2917b7ee92e57ba5563708daca94dd84ad7112f60c03393a38673ffffffffffffffffffffffffffffffffffffffff8a5116613d81565b809491946107aa575b5050823391613652565b604051908152f35b73ffffffffffffffffffffffffffffffffffffffff6107cc9251169083613652565b8480610798565b73ffffffffffffffffffffffffffffffffffffffff165f5260018f5260405f209081540190558e806106e6565b73ffffffffffffffffffffffffffffffffffffffff8e8c01511695614959565b7fbe8b8507000000000000000000000000000000000000000000000000000000005f5260045ffd5b346103305760207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc360112610330577fffffffffffffffffffffffff00000000000000000000000000000000000000006108a0612d3f565b73ffffffffffffffffffffffffffffffffffffffff5f54916108c58284163314613007565b1691829116175f55337f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e05f80a3005b346103305760207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc360112610330576004355c5f5260205ff35b34610330575f7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261033057602073ffffffffffffffffffffffffffffffffffffffff60025416604051908152f35b346103305761098d36612f7b565b6040519160408360208152836020820152019160051b8301916020806040850193925b83355481520191019084838210156109cc5750602080916109b0565b60408186030190f35b346103305760407ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261033057610a0c612d3f565b73ffffffffffffffffffffffffffffffffffffffff610a29612d62565b91165f52600360205273ffffffffffffffffffffffffffffffffffffffff60405f2091165f52602052602060ff60405f2054166040519015158152f35b346103305760207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261033057610a9d612d3f565b73ffffffffffffffffffffffffffffffffffffffff81169081610ae15750505f7f27e098c505d44ec3574004bca052aabf76bd35004c182099d8c575fb238593b95d005b610aea90613a92565b907f27e098c505d44ec3574004bca052aabf76bd35004c182099d8c575fb238593b95d7f1e0745a7db1623981f0b2a5d4232364c00787266eb75ad546f190e6cebe9bd955d005b3461033057610b3f36612f7b565b6040519160408360208152836020820152019160051b8301916020806040850193925b83355c81520191019084838210156109cc575060208091610b62565b346103305760207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126103305773ffffffffffffffffffffffffffffffffffffffff610bca612d3f565b165f526001602052602060405f2054604051908152f35b34610330575f7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261033057602073ffffffffffffffffffffffffffffffffffffffff5f5416604051908152f35b346103305760607ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261033057610c68612d3f565b610c70612d62565b60443573ffffffffffffffffffffffffffffffffffffffff600254163303610d5f5773ffffffffffffffffffffffffffffffffffffffff821680151580610d1f575b610cf7576020936107a29280610cef5750815f526001855260405f20549384925b5f526001865260405f20610ce8848254612fed565b90556131f8565b938492610cd3565b7fc79e5948000000000000000000000000000000000000000000000000000000005f5260045ffd5b508073ffffffffffffffffffffffffffffffffffffffff7f27e098c505d44ec3574004bca052aabf76bd35004c182099d8c575fb238593b95c1614610cb2565b7f48f5c3ed000000000000000000000000000000000000000000000000000000005f5260045ffd5b346103305760407ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261033057610dbe612d3f565b7fc090fc4683624cfc3884e9d8de5eca132f2d0ec062aff75d43c0465d5ceeab235c156104d757335f90815273ffffffffffffffffffffffffffffffffffffffff8216602052604090205c610e146024356130aa565b9081600f0b03610e3157610e2f9133915f03600f0b906130f0565b005b7fbda73abf000000000000000000000000000000000000000000000000000000005f5260045ffd5b346103305760c07ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261033057610e9136612e81565b610e99612e6f565b9073ffffffffffffffffffffffffffffffffffffffff600254163303610d5f57623e900062fff0008316106103e9610fff8416101615610f6557602060a07fe9c42593e71f84403b84352cd168d693e2c9fcd1fdbcc3feb21d92b43e6696f9922092835f526006825260405f20610f0f81613576565b805479ffffff00000000000000000000000000000000000000000000008360b81b16907fffffffffffff000000ffffffffffffffffffffffffffffffffffffffffffffff1617905562ffffff60405191168152a2005b62ffffff827fa7abe2f7000000000000000000000000000000000000000000000000000000005f521660045260245ffd5b346103305760c07ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261033057610fce36612e81565b60a4359073ffffffffffffffffffffffffffffffffffffffff821680830361033057610ff861350f565b6060820191825160020b617fff81136114245750825160020b600181126113f9575073ffffffffffffffffffffffffffffffffffffffff815116602082019073ffffffffffffffffffffffffffffffffffffffff825116808210156113c2575050608082019073ffffffffffffffffffffffffffffffffffffffff82511690604084019161108c62ffffff845116826139b7565b1561139757506110a162ffffff835116613a75565b96835173ffffffffffffffffffffffffffffffffffffffff8116908133036112e0575b505060a0852090815f52600660205260405f2090815473ffffffffffffffffffffffffffffffffffffffff166112b8576020997fdd466e674ea557f56295e2d0218a125ea4b4f0f6f3307b95f85e6110838d6438927cffffff000000000000000000000000000000000000000000000000000061114260a0946145fc565b9260d01b168a76ffffff000000000000000000000000000000000000000084861b161717905562ffffff73ffffffffffffffffffffffffffffffffffffffff808a5116965116965116995160020b73ffffffffffffffffffffffffffffffffffffffff885116906040519b8c528c8c015260408b01528860608b015260020b98896080820152a45173ffffffffffffffffffffffffffffffffffffffff8116908133036111f4575b8585604051908152f35b61100016611203575b806111ea565b6112af9261128d604051937f6fe7e6eb0000000000000000000000000000000000000000000000000000000088860152336024860152604485019073ffffffffffffffffffffffffffffffffffffffff6080809282815116855282602082015116602086015262ffffff6040820151166040860152606081015160020b6060860152015116910152565b60e48301528361010483015261010482526112aa61012483612e2e565b613f25565b508280806111fd565b7f7983c051000000000000000000000000000000000000000000000000000000005f5260045ffd5b612000166112ef575b806110c4565b61139090604051907fdc98354e00000000000000000000000000000000000000000000000000000000602083015233602483015261137a604483018973ffffffffffffffffffffffffffffffffffffffff6080809282815116855282602082015116602086015262ffffff6040820151166040860152606081015160020b6060860152015116910152565b8860e483015260e482526112aa61010483612e2e565b50886112e9565b7fe65af6a0000000000000000000000000000000000000000000000000000000005f5260045260245ffd5b60449250604051917f6e6c983000000000000000000000000000000000000000000000000000000000835260048301526024820152fd5b7fe9e90588000000000000000000000000000000000000000000000000000000005f5260045260245ffd5b7fb70024f8000000000000000000000000000000000000000000000000000000005f5260045260245ffd5b34610330576101407ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126103305761148836612e81565b60807fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff5c36011261033057604051906114bf82612dda565b60a4358060020b810361033057825260c4358060020b810361033057602083015260e43560408301526101043560608301526101243567ffffffffffffffff811161033057611512903690600401612f4d565b90927fc090fc4683624cfc3884e9d8de5eca132f2d0ec062aff75d43c0465d5ceeab235c156104d75761154361350f565b60a0832093845f52600660205260405f20608052611562608051613576565b608084015173ffffffffffffffffffffffffffffffffffffffff811690813303611ede575b5050815160020b92602083015160020b916115a56040850151613785565b93606087015160020b9760608201516040519960c08b018b811067ffffffffffffffff821117611eb157604052338b528860208c01528660408c015287600f0b60608c015260808b015260a08a01525f9185881215611e7a577ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff276188812611e4e57620d89e88613611e22576040519261163c84612dda565b5f84525f60208501525f60408501525f606085015287600f0b611b25575b600460805101978960020b5f528860205260405f20988860020b5f5260205260405f206080515460a01c60020b8b81125f14611acf575060028060018c0154600184015490039b015491015490039b5b60a073ffffffffffffffffffffffffffffffffffffffff825116910151906040519160268301528960068301528b600383015281525f603a600c83012091816040820152816020820152525f5260066080510160205260405f20976fffffffffffffffffffffffffffffffff8954169982600f0b155f14611a72578a15611a4a5761176f61176960409f9b61184e9c6118609e5b60018301956117616002611755848a548503615703565b95019283548503615703565b9655556130aa565b916130aa565b6fffffffffffffffffffffffffffffffff169060801b179a8b965f84600f0b126119dc575b5082600f0b611898575b5050506117c46117b58560801d8360801d01613785565b9185600f0b90600f0b01613785565b6fffffffffffffffffffffffffffffffff169060801b1791815160020b90602083015160020b8c8401516060850151918e5194855260208501528d84015260608301527ff208f4912782fd25c7f114ca3723a2d5dd6f3bcc3ac8db5af63baa85f711d5ec60803393a38873ffffffffffffffffffffffffffffffffffffffff60808201511661385b565b8094919461186c575b50833391613652565b82519182526020820152f35b6118929073ffffffffffffffffffffffffffffffffffffffff6080840151169083613652565b85611857565b60805154929350909173ffffffffffffffffffffffffffffffffffffffff81169060a01c60020b828112156118fe575050906118f2926118e76118dd6118ed94614158565b91600f0b92614158565b90614527565b613785565b60801b5b8b808061179e565b92809193125f146119a95761193d9161192a6118ed6118ed9361192488600f0b91614158565b87614527565b9361193886600f0b92614158565b6144ca565b6fffffffffffffffffffffffffffffffff169060801b17906fffffffffffffffffffffffffffffffff61197c60036080510192600f0b8284541661456e565b167fffffffffffffffffffffffffffffffff000000000000000000000000000000008254161790556118f6565b906118ed9250926119bf6118dd6119c595614158565b906144ca565b6fffffffffffffffffffffffffffffffff166118f6565b808f9151611a1e575b01516119f2575b8e611794565b611a198260805160049160020b5f52016020525f6002604082208281558260018201550155565b6119ec565b611a458360805160049160020b5f52016020525f6002604082208281558260018201550155565b6119e5565b7faefeb924000000000000000000000000000000000000000000000000000000005f5260045ffd5b61176f61176960409f9b61184e9c6118609e6fffffffffffffffffffffffffffffffff611aa289600f0b8361456e565b167fffffffffffffffffffffffffffffffff0000000000000000000000000000000084541617835561173e565b9099908913611af55760028060018c0154600184015490039b015491015490039b6116aa565b9860026001608051015460018c01549003600183015490039a81806080510154910154900391015490039b6116aa565b6004608051018960020b5f5280602052898960405f20611b7e81546fffffffffffffffffffffffffffffffff611b6181831695600f0b8661456e565b16931594858515141595611dee575b508d600f0b9060801d613d3a565b60801b82179055602087015285528760020b5f5260205260405f208054906fffffffffffffffffffffffffffffffff8216611bbc8b600f0b8261456e565b901592836fffffffffffffffffffffffffffffffff831615141593611dc1575b8b600f0b9060801d600f0b03916f7fffffffffffffffffffffffffffffff83137fffffffffffffffffffffffffffffffff80000000000000000000000000000000841217611d9457826fffffffffffffffffffffffffffffffff935060801b83831617905516606086015260408501525f88600f0b1215611ca1575b8351611c85575b60408401511561165a57611c8060808c015160020b8860056080510161410c565b61165a565b611c9c60808c015160020b8a60056080510161410c565b611c5f565b60808b015160020b6fffffffffffffffffffffffffffffffff600181602088015116925f817ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff276180712817ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff27618050390620d89e8050301810416809111611d68576fffffffffffffffffffffffffffffffff6060860151161115611c5857867fb8e3c385000000000000000000000000000000000000000000000000000000005f5260045260245ffd5b897fb8e3c385000000000000000000000000000000000000000000000000000000005f5260045260245ffd5b7f4e487b71000000000000000000000000000000000000000000000000000000005f52601160045260245ffd5b6080515460a01c60020b8b13611bdc57600160805101546001840155600260805101546002840155611bdc565b6080515460a01c60020b1215611e05575b8e611b70565b600160805101546001840155600260805101546002840155611dff565b857f1ad777f8000000000000000000000000000000000000000000000000000000005f5260045260245ffd5b877fd5e2f7ab000000000000000000000000000000000000000000000000000000005f5260045260245ffd5b60448887604051917fc4433ed500000000000000000000000000000000000000000000000000000000835260048301526024820152fd5b7f4e487b71000000000000000000000000000000000000000000000000000000005f52604160045260245ffd5b5f604085015113808091611fd6575b15611f6b5750506040517f259982e5000000000000000000000000000000000000000000000000000000006020820152611f62916112aa82611f368887898c33602487016136cb565b037fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe08101845283612e2e565b505b8580611587565b159081611fc8575b50611f7f575b50611f64565b6040517f21d0ee70000000000000000000000000000000000000000000000000000000006020820152611fc1916112aa82611f368887898c33602487016136cb565b5085611f79565b610200915016151587611f73565b5061080082161515611eed565b346103305760607ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126103305761201a612d3f565b73ffffffffffffffffffffffffffffffffffffffff612037612d62565b91165f52600560205273ffffffffffffffffffffffffffffffffffffffff60405f2091165f5260205260405f206044355f52602052602060405f2054604051908152f35b346103305760407ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc360112610330576120b2612d3f565b602435908115158092036103305773ffffffffffffffffffffffffffffffffffffffff90335f52600360205260405f208282165f5260205260405f207fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0081541660ff851617905560405192835216907fceb576d9f15e4e200fdb5096d64d5dfd667e16def20c1eefd14256d8e3faa26760203392a3602060405160018152f35b346103305760c07ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126103305761218a36612e81565b612192612e6f565b906280000062ffffff60408301511614801590612246575b61221e5760a0906121ba8361368e565b205f52600660205260405f20906121d082613576565b81547fffffff000000ffffffffffffffffffffffffffffffffffffffffffffffffffff1660d09190911b7cffffff000000000000000000000000000000000000000000000000000016179055005b7f30d21641000000000000000000000000000000000000000000000000000000005f5260045ffd5b5073ffffffffffffffffffffffffffffffffffffffff6080820151163314156121aa565b346103305760207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126103305760043567ffffffffffffffff8111610330576122b9903690600401612f4d565b7fc090fc4683624cfc3884e9d8de5eca132f2d0ec062aff75d43c0465d5ceeab235c6124cd57612345915f9160017fc090fc4683624cfc3884e9d8de5eca132f2d0ec062aff75d43c0465d5ceeab235d60405193849283927f91dd734600000000000000000000000000000000000000000000000000000000845260206004850152602484019161306c565b038183335af19081156124c2575f9161241a575b507f7d4b3164c6e45b97e7d87b7125a44c5828d005af88f9d751cfd78729c5d99a0b5c6123f25760406020915f7fc090fc4683624cfc3884e9d8de5eca132f2d0ec062aff75d43c0465d5ceeab235d7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0601f835194859381855280519182918282880152018686015e5f85828601015201168101030190f35b7f5212cba1000000000000000000000000000000000000000000000000000000005f5260045ffd5b90503d805f833e61242b8183612e2e565b8101906020818303126103305780519067ffffffffffffffff8211610330570181601f820112156103305780519067ffffffffffffffff8211611eb1576040519261249e60207fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0601f8601160185612e2e565b8284526020838301011161033057815f9260208093018386015e8301015281612359565b6040513d5f823e3d90fd5b7f5090d6c6000000000000000000000000000000000000000000000000000000005f5260045ffd5b346103305773ffffffffffffffffffffffffffffffffffffffff61251836612d85565b91929092335f52600560205260405f208282165f5260205260405f20845f526020528260405f205560405192835216907fb3fd5071835887567a0671151121894ddccc2842f1d10bedad13e0d17cace9a760203392a4602060405160018152f35b60207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc360112610330576125ab612d3f565b7fc090fc4683624cfc3884e9d8de5eca132f2d0ec062aff75d43c0465d5ceeab235c156104d7576107a260209161342d565b346103305760407ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc360112610330576024356004356040519160408360208152826020820152019060051b8301916001602060408501935b835481520191019084838210156109cc57506020600191612635565b346103305760207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126103305773ffffffffffffffffffffffffffffffffffffffff61269d612d3f565b6126ab825f54163314613007565b16807fffffffffffffffffffffffff000000000000000000000000000000000000000060025416176002557fb4bd8ef53df690b9943d3318996006dbb82a25f54719d8c8035b516a2a5b8acc5f80a2005b34610330576101007ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126103305761273536612e81565b60c4359060a43560e43567ffffffffffffffff81116103305761275c903690600401612f4d565b9190937fc090fc4683624cfc3884e9d8de5eca132f2d0ec062aff75d43c0465d5ceeab235c156104d75761278e61350f565b60a0842094855f52600660205260405f20946127a986613576565b60808101805173ffffffffffffffffffffffffffffffffffffffff811690813303612943575b50506fffffffffffffffffffffffffffffffff60038801541697881561291b576020986127fb876130aa565b5f03612806876130aa565b5f036fffffffffffffffffffffffffffffffff169060801b179887612907575b866128f2575b5050612839338985613652565b60405190868252858a8301527f29ef05caaff9404b7cb6d1c0e9bbae9eaa7ab2541feba1a9c4248594c08156cb60403393a3519273ffffffffffffffffffffffffffffffffffffffff841693843303612897575b8888604051908152f35b6010166128a5575b8061288d565b6128e6956112aa93611f36926040519788957fe1b4af69000000000000000000000000000000000000000000000000000000008d88015233602488016135bc565b5082808080808061289f565b600201908660801b048154019055898061282c565b60018101828960801b048154019055612826565b7fa74f97ab000000000000000000000000000000000000000000000000000000005f5260045ffd5b602016612951575b806127cf565b6040517fb6a8b0fa000000000000000000000000000000000000000000000000000000006020820152612994916112aa82611f368b898b8d8b33602488016135bc565b508861294b565b346103305760207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261033057600435545f5260205ff35b34610330576129e336612d85565b907fc090fc4683624cfc3884e9d8de5eca132f2d0ec062aff75d43c0465d5ceeab235c156104d7577f1b3d7edb2e9c0b0e7c525b20aaaef0f5940d2ed71663c7d39266ecafac7288596103ed73ffffffffffffffffffffffffffffffffffffffff805f941695612a62612a55876130aa565b8603600f0b3390896130f0565b16938484526004602052604084208685526020526040842061025f828254612ffa565b5f7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc360112610330577fc090fc4683624cfc3884e9d8de5eca132f2d0ec062aff75d43c0465d5ceeab235c156104d75760206107a23361342d565b346103305760607ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261033057612b17612d3f565b612b1f612d62565b604435907fc090fc4683624cfc3884e9d8de5eca132f2d0ec062aff75d43c0465d5ceeab235c156104d757610e2f92612b67612b5a846130aa565b5f03600f0b3390836130f0565b6131f8565b346103305773ffffffffffffffffffffffffffffffffffffffff612b8f36612d85565b91929092335f52600460205260405f20845f5260205260405f20612bb4848254612fed565b90551690815f52600460205260405f20835f5260205260405f20612bd9828254612ffa565b9055604080513380825260208201939093527f1b3d7edb2e9c0b0e7c525b20aaaef0f5940d2ed71663c7d39266ecafac7288599181908101610279565b346103305760207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc360112610330576004357fffffffff00000000000000000000000000000000000000000000000000000000811680910361033057807f01ffc9a70000000000000000000000000000000000000000000000000000000060209214908115612cab575b506040519015158152f35b7f0f632fb30000000000000000000000000000000000000000000000000000000091501482612ca0565b346103305760407ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126103305760209073ffffffffffffffffffffffffffffffffffffffff612d24612d3f565b165f526004825260405f206024355f52825260405f20548152f35b6004359073ffffffffffffffffffffffffffffffffffffffff8216820361033057565b6024359073ffffffffffffffffffffffffffffffffffffffff8216820361033057565b7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc60609101126103305760043573ffffffffffffffffffffffffffffffffffffffff8116810361033057906024359060443590565b6080810190811067ffffffffffffffff821117611eb157604052565b6060810190811067ffffffffffffffff821117611eb157604052565b60a0810190811067ffffffffffffffff821117611eb157604052565b90601f7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0910116810190811067ffffffffffffffff821117611eb157604052565b60a4359062ffffff8216820361033057565b7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc60a09101126103305760405190612eb882612e12565b8160043573ffffffffffffffffffffffffffffffffffffffff8116810361033057815260243573ffffffffffffffffffffffffffffffffffffffff8116810361033057602082015260443562ffffff811681036103305760408201526064358060020b81036103305760608201526084359073ffffffffffffffffffffffffffffffffffffffff821682036103305760800152565b9181601f840112156103305782359167ffffffffffffffff8311610330576020838186019501011161033057565b9060207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc8301126103305760043567ffffffffffffffff811161033057826023820112156103305780600401359267ffffffffffffffff84116103305760248460051b83010111610330576024019190565b91908203918211611d9457565b91908201809211611d9457565b1561300e57565b60646040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152600c60248201527f554e415554484f52495a454400000000000000000000000000000000000000006044820152fd5b601f82602094937fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe093818652868601375f8582860101520116010190565b6f800000000000000000000000000000008110156130c857600f0b90565b7f93dafdf1000000000000000000000000000000000000000000000000000000005f5260045ffd5b9190600f0b9182156131f357613126919073ffffffffffffffffffffffffffffffffffffffff8092165f521660205260405f2090565b613132815c9283613b29565b80915d6131a357507fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f7d4b3164c6e45b97e7d87b7125a44c5828d005af88f9d751cfd78729c5d99a0b5c017f7d4b3164c6e45b97e7d87b7125a44c5828d005af88f9d751cfd78729c5d99a0b5d5b565b156131aa57565b60017f7d4b3164c6e45b97e7d87b7125a44c5828d005af88f9d751cfd78729c5d99a0b5c017f7d4b3164c6e45b97e7d87b7125a44c5828d005af88f9d751cfd78729c5d99a0b5d565b505050565b90919073ffffffffffffffffffffffffffffffffffffffff811690816132ea5750505f80808093855af11561322a5750565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0601f3d011673ffffffffffffffffffffffffffffffffffffffff604051927f90bfb8650000000000000000000000000000000000000000000000000000000084521660048301525f6024830152608060448301528060a00160648301523d60848301523d5f60a484013e7ff4b3b1bc0000000000000000000000000000000000000000000000000000000060c4828401600460a4820152015260e40190fd5b60205f60448194968260409573ffffffffffffffffffffffffffffffffffffffff988751998a947fa9059cbb00000000000000000000000000000000000000000000000000000000865216600485015260248401525af13d15601f3d116001855114161716928281528260208201520152156133635750565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0601f3d0116604051917f90bfb86500000000000000000000000000000000000000000000000000000000835260048301527fa9059cbb000000000000000000000000000000000000000000000000000000006024830152608060448301528060a00160648301523d60848301523d5f60a484013e7ff27f64e40000000000000000000000000000000000000000000000000000000060c4828401600460a4820152015260e40190fd5b7f27e098c505d44ec3574004bca052aabf76bd35004c182099d8c575fb238593b95c919073ffffffffffffffffffffffffffffffffffffffff8316613482576131a19034935b61347c856130aa565b906130f0565b346134e7576131a1906134be7f1e0745a7db1623981f0b2a5d4232364c00787266eb75ad546f190e6cebe9bd955c6134b986613a92565b612fed565b935f7f27e098c505d44ec3574004bca052aabf76bd35004c182099d8c575fb238593b95d613473565b7fb0ec849e000000000000000000000000000000000000000000000000000000005f5260045ffd5b73ffffffffffffffffffffffffffffffffffffffff7f000000000000000000000000000000000000000000000000000000000000000016300361354e57565b7f0d89438e000000000000000000000000000000000000000000000000000000005f5260045ffd5b5473ffffffffffffffffffffffffffffffffffffffff161561359457565b7f486aa307000000000000000000000000000000000000000000000000000000005f5260045ffd5b91926136376101209473ffffffffffffffffffffffffffffffffffffffff61364f999794168552602085019073ffffffffffffffffffffffffffffffffffffffff6080809282815116855282602082015116602086015262ffffff6040820151166040860152606081015160020b6060860152015116910152565b60c083015260e082015281610100820152019161306c565b90565b9073ffffffffffffffffffffffffffffffffffffffff60206131a1949361368185848351168660801d906130f0565b01511690600f0b906130f0565b62ffffff16620f424081116136a05750565b7f14002113000000000000000000000000000000000000000000000000000000005f5260045260245ffd5b9061364f95936137486101609473ffffffffffffffffffffffffffffffffffffffff61377794168552602085019073ffffffffffffffffffffffffffffffffffffffff6080809282815116855282602082015116602086015262ffffff6040820151166040860152606081015160020b6060860152015116910152565b8051600290810b60c08501526020820151900b60e0840152604081015161010084015260600151610120830152565b81610140820152019161306c565b9081600f0b9182036130c857565b926138419061381261364f99979473ffffffffffffffffffffffffffffffffffffffff6101a09895168752602087019073ffffffffffffffffffffffffffffffffffffffff6080809282815116855282602082015116602086015262ffffff6040820151166040860152606081015160020b6060860152015116910152565b8051600290810b60c08701526020820151900b60e0860152604081015161010086015260600151610120850152565b61014083015261016082015281610180820152019161306c565b939590919296945f9673ffffffffffffffffffffffffffffffffffffffff861633146139ac57885f6040870151135f1461393b5761040087166138a2575b50505050505050565b61392e9799985092613927969594926138ef9261391b956040519788967f9f063efc0000000000000000000000000000000000000000000000000000000060208901523360248901613793565b037fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe08101835282612e2e565b6002821615159161459f565b80926145bf565b915f808080808080613899565b95949392919061010086166139535750505050505050565b61392e979950869850916138ef916139a09493613927986040519788967f6c2bbe7e0000000000000000000000000000000000000000000000000000000060208901523360248901613793565b6001821615159161459f565b505f96505050505050565b608081161580613a69575b613a3f57604081161580613a5d575b613a3f5761040081161580613a51575b613a3f5761010081161580613a45575b613a3f5773ffffffffffffffffffffffffffffffffffffffff8116613a1f575062ffffff1662800000141590565b613fff161590811591613a30575090565b62800000915062ffffff161490565b50505f90565b506001811615156139f1565b506002811615156139e1565b506004811615156139d1565b506008811615156139c2565b6280000062ffffff821614613a8d5761364f8161368e565b505f90565b73ffffffffffffffffffffffffffffffffffffffff1680613ab257504790565b6020602491604051928380927f70a082310000000000000000000000000000000000000000000000000000000082523060048301525afa9081156124c2575f91613afa575090565b90506020813d602011613b21575b81613b1560209383612e2e565b81010312610330575190565b3d9150613b08565b9190915f8382019384129112908015821691151617611d9457565b6020830151955f9586959194913373ffffffffffffffffffffffffffffffffffffffff851614613d2d5760808416613b7e575b5050505050565b613c66926138ef613c6092613c4c946040519586947f575e24b4000000000000000000000000000000000000000000000000000000006020870152336024870152613c16604487018c73ffffffffffffffffffffffffffffffffffffffff6080809282815116855282602082015116602086015262ffffff6040820151166040860152606081015160020b6060860152015116910152565b8051151560e487015260208101516101048701526040015173ffffffffffffffffffffffffffffffffffffffff16610124860152565b61014061014485015261016484019161306c565b82613f25565b916060835103613d05576040015162ffffff166280000014613cf9575b600816613c94575b80808080613b77565b604001519250608083901d600f0b8015613c8b57613cb5905f861295613b29565b9315613cf1575f84135b613cc9575f613c8b565b7ffa0b71d6000000000000000000000000000000000000000000000000000000005f5260045ffd5b5f8412613cbf565b60608201519350613c83565b7f1e048e1d000000000000000000000000000000000000000000000000000000005f5260045ffd5b505f965086955050505050565b90600f0b90600f0b01907fffffffffffffffffffffffffffffffff8000000000000000000000000000000082126f7fffffffffffffffffffffffffffffff831317611d9457565b9196959394929473ffffffffffffffffffffffffffffffffffffffff83163314613f18578460801d94600f0b938860408516613e40575b50505050505f9481600f0b15801590613e34575b613dd8575b5050509190565b613e0f9395505f60208201511290511515145f14613e17576fffffffffffffffffffffffffffffffff169060801b175b80936145bf565b5f8080613dd1565b906fffffffffffffffffffffffffffffffff169060801b17613e08565b5082600f0b1515613dcc565b613efc613f08946138ef6118ed95613f0e999895613ee1613c16966040519788967fb47b2fb1000000000000000000000000000000000000000000000000000000006020890152336024890152604488019073ffffffffffffffffffffffffffffffffffffffff6080809282815116855282602082015116602086015262ffffff6040820151166040860152606081015160020b6060860152015116910152565b8c61014485015261016061016485015261018484019161306c565b6004821615159161459f565b90613d3a565b5f80808088613db8565b5050505050909150905f90565b9190918251925f8060208301958682865af115613fc3575050604051917fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0603f3d011683016040523d83523d9060208401915f833e6020845110918215613f8f575b5050613d0557565b5190517fffffffff000000000000000000000000000000000000000000000000000000009182169116141590505f80613f87565b5183517fffffffff00000000000000000000000000000000000000000000000000000000811691600481106140d7575b50507fffffffff000000000000000000000000000000000000000000000000000000007fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0601f3d01169173ffffffffffffffffffffffffffffffffffffffff604051947f90bfb865000000000000000000000000000000000000000000000000000000008652166004850152166024830152608060448301528060a00160648301523d60848301523d5f60a484013e7fa9e35b2f0000000000000000000000000000000000000000000000000000000060c4828401600460a4820152015260e40190fd5b7fffffffff000000000000000000000000000000000000000000000000000000009250829060040360031b1b16168280613ff3565b919060020b9060020b9081810761413a5705908160081d5f52602052600160ff60405f2092161b8154189055565b601c906044926040519163d4d8f3e683526020830152604082015201fd5b60020b908160ff1d82810118620d89e8811161449e5763ffffffff9192600182167001fffcb933bd6fad37aa2d162d1a59400102700100000000000000000000000000000000189160028116614482575b60048116614466575b6008811661444a575b6010811661442e575b60208116614412575b604081166143f6575b608081166143da575b61010081166143be575b61020081166143a2575b6104008116614386575b610800811661436a575b611000811661434e575b6120008116614332575b6140008116614316575b61800081166142fa575b6201000081166142de575b6202000081166142c3575b6204000081166142a8575b620800001661428f575b5f12614268575b0160201c90565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff04614261565b6b048a170391f7dc42444e8fa290910260801c9061425a565b6d2216e584f5fa1ea926041bedfe9890920260801c91614250565b916e5d6af8dedb81196699c329225ee6040260801c91614245565b916f09aa508b5b7a84e1c677de54f3e99bc90260801c9161423a565b916f31be135f97d08fd981231505542fcfa60260801c9161422f565b916f70d869a156d2a1b890bb3df62baf32f70260801c91614225565b916fa9f746462d870fdf8a65dc1f90e061e50260801c9161421b565b916fd097f3bdfd2022b8845ad8f792aa58250260801c91614211565b916fe7159475a2c29b7443b29c7fa6e889d90260801c91614207565b916ff3392b0822b70005940c7a398e4b70f30260801c916141fd565b916ff987a7253ac413176f2b074cf7815e540260801c916141f3565b916ffcbe86c7900a88aedcffc83b479aa3a40260801c916141e9565b916ffe5dee046a99a2a811c461f1969c30530260801c916141df565b916fff2ea16466c96a3843ec78b326b528610260801c916141d6565b916fff973b41fa98c081472e6896dfb254c00260801c916141cd565b916fffcb9843d60f6159c9db58835c9266440260801c916141c4565b916fffe5caca7e10e4e61c3624eaa0941cd00260801c916141bb565b916ffff2e50f5f656932ef12357cf3c7fdcc0260801c916141b2565b916ffff97272373d413259a46990580e213a0260801c916141a9565b827f8b86327a000000000000000000000000000000000000000000000000000000005f5260045260245ffd5b905f83600f0b125f146144ff576144f5925f036fffffffffffffffffffffffffffffffff1691615a3d565b5f81126130c85790565b61451b926fffffffffffffffffffffffffffffffff16916159e2565b5f81126130c8575f0390565b905f83600f0b125f14614552576144f5925f036fffffffffffffffffffffffffffffffff1691615b34565b61451b926fffffffffffffffffffffffffffffffff1691615a7d565b906fffffffffffffffffffffffffffffffff90600f0b911601908160801c61459257565b6393dafdf15f526004601cfd5b906145a991613f25565b9015613a8d576040815103613d05576040015190565b6145e2906145d48360801d8260801d03613785565b92600f0b90600f0b03613785565b6fffffffffffffffffffffffffffffffff169060801b1790565b73fffd8963efd1fc6a506488495d951d516396168273ffffffffffffffffffffffffffffffffffffffff7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffefffd895d830116116148e05777ffffffffffffffffffffffffffffffffffffffff000000008160201b168060ff61467983615bdb565b1691608083106148d457507fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8182011c5b800280607f1c8160ff1c1c800280607f1c8160ff1c1c800280607f1c8160ff1c1c800280607f1c8160ff1c1c800280607f1c8160ff1c1c800280607f1c8160ff1c1c80029081607f1c8260ff1c1c80029283607f1c8460ff1c1c80029485607f1c8660ff1c1c80029687607f1c8860ff1c1c80029889607f1c8a60ff1c1c80029a8b607f1c8c60ff1c1c80029c8d80607f1c9060ff1c1c800260cd1c6604000000000000169d60cc1c6608000000000000169c60cb1c6610000000000000169b60ca1c6620000000000000169a60c91c6640000000000000169960c81c6680000000000000169860c71c670100000000000000169760c61c670200000000000000169660c51c670400000000000000169560c41c670800000000000000169460c31c671000000000000000169360c21c672000000000000000169260c11c674000000000000000169160c01c67800000000000000016907fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff800160401b1717171717171717171717171717693627a301d71055774c85027ffffffffffffffffffffffffffffffffffd709b7e5480fba5a50fed5e62ffc556810160801d60020b906fdb2df09e81959a81455e260799a0632f0160801d60020b918282145f146148915750905090565b73ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff6148c584614158565b16116148cf575090565b905090565b905081607f031b6146a9565b73ffffffffffffffffffffffffffffffffffffffff907f61487524000000000000000000000000000000000000000000000000000000005f521660045260245ffd5b811561492c570490565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52601260045260245ffd5b6040519290915f61496985612df6565b5f855260208501925f845260408601955f875280968654956040860151159586155f146156f557610fff8860b81c16945b8151925f948a73ffffffffffffffffffffffffffffffffffffffff16918288528b60a01c60020b90526fffffffffffffffffffffffffffffffff60038d0154169052608083015162400000811615155f146156e65762bfffff166149fd8161368e565b61ffff88166156cb575b8096620f424062ffffff8316101561569a575b8451156156845750508861562457606083019073ffffffffffffffffffffffffffffffffffffffff825116818110156155ed5750505173ffffffffffffffffffffffffffffffffffffffff166401000276a38111156155c257505b604051986101008a018a811067ffffffffffffffff821117611eb1576040525f8a525f60208b01525f60408b01525f60608b01525f60808b01525f60a08b01525f60c08b015288155f146155b45760018b0154949390945b60e08b01525b8015801561557a575b6154205788868d8c8e73ffffffffffffffffffffffffffffffffffffffff8351168252602083015160020b602089015160020b90815f8183071291050386155f14615275576fffffffffffffffffffffffffffffffff937ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff2761860409460019484600560ff60609716938260020b60081d890b5f5201602052875f207fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8460ff031c9054169283151593845f146152635790614bb760ff92615bdb565b90031660020b900360020b0260020b5b905b15158684015260020b8060208401521315615238575b620d89e8602082015160020b121561522a575b73ffffffffffffffffffffffffffffffffffffffff614c17602083015160020b614158565b16918291015273ffffffffffffffffffffffffffffffffffffffff8551169673ffffffffffffffffffffffffffffffffffffffff60608c0151169283911516818310189118021892015116928d73ffffffffffffffffffffffffffffffffffffffff8316821015915f87125f1461507f5762ffffff8516620f424003614c9f81895f03615785565b94841561506e57614cb1888483615a7d565b955b868110614fb257509660a093929173ffffffffffffffffffffffffffffffffffffffff98978891620f424062ffffff8316145f14614f9e575050865b955b15614f905791614d0092615a3d565b925b60c0820152015260808d0152168c525f8351135f14614f605760a08a0151905f82126130c8570392614d3d60808b015160c08c015190612ffa565b5f81126130c8578103908113600116611d9457935b61ffff8716614f18575b6fffffffffffffffffffffffffffffffff60408d01511680614efe575b5073ffffffffffffffffffffffffffffffffffffffff8c511673ffffffffffffffffffffffffffffffffffffffff60608c01511681145f14614ec2575060408a0151614e10575b88614e03577fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff60208b015160020b0160020b5b60020b60208d01525b9392614ad3565b60208a015160020b614df3565b88614e96576fffffffffffffffffffffffffffffffff614e7d8d8d8d600460e08201519260206002820154935b015160020b60020b5f520160205260405f2091600183019081549003905560028201908154900390555460801d908c15614e88575b60400151831661456e565b1660408d0152614dc0565b5f91909103600f0b90614e72565b6fffffffffffffffffffffffffffffffff614e7d8d8d8d6004600183015492602060e084015193614e3d565b73ffffffffffffffffffffffffffffffffffffffff8b51168103614ee7575b50614dfc565b614ef0906145fc565b60020b60208d01525f614ee1565b60c08b015160801b0460e08b01510160e08b01525f614d79565b9662ffffff861661ffff881603614f435760c08a0151905b8160c08c01510360c08c01520196614d5c565b620f424060808b015161ffff89169060c08d015101020490614f30565b60808a015160c08b015101905f82126130c857019260a08a01515f81126130c857614f8a91613b29565b93614d52565b614f9992615b34565b614d00565b62ffffff614fad921689615c68565b614cef565b9650505092505082918415811517615061578e60a09173ffffffffffffffffffffffffffffffffffffffff96845f14614ffc57614ff0878284615d07565b80978a015f0395614cf1565b87871161503a576150356150306150286fffffffffffffffffffffffffffffffff84168a60601b614922565b8a8516612ffa565b615d9b565b614ff0565b61503561503061505c6fffffffffffffffffffffffffffffffff84168a61588a565b615028565b634f2461b85f526004601cfd5b6150798882856159e2565b95614cb3565b9193509190831561521957615095858284615a3d565b915b8287106150f7579073ffffffffffffffffffffffffffffffffffffffff9560a09280965b156150e857916150ca92615a7d565b925b6150e362ffffff8d16620f42408190039086615c68565b614d02565b6150f1926159e2565b926150cc565b50915050838315821517615061578d83156151ef575073ffffffffffffffffffffffffffffffffffffffff851161519c578460601b6fffffffffffffffffffffffffffffffff851680820615159104015b73ffffffffffffffffffffffffffffffffffffffff8316928184111561518f578f939573ffffffffffffffffffffffffffffffffffffffff60a093819803165b80966150bb565b634323a5555f526004601cfd5b6fffffffffffffffffffffffffffffffff84166151c7816c0100000000000000000000000088615943565b90801561492c576c010000000000000000000000008709156151485760010180615148575f80fd5b9180856152148873ffffffffffffffffffffffffffffffffffffffff9860a095615c91565b615188565b615224858383615b34565b91615097565b620d89e86020820152614bf2565b7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff276186020820152614bdf565b5060020b900360020b0260020b614bc7565b60019194939650600592955001938460020b60081d60010b5f520160205260405f207fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff600160ff86161b0119905416908d8b831592831597885f146153c15750505050610330578f9160018f8f96907ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff276186060928f989560409660ff896fffffffffffffffffffffffffffffffff9a5f03166101e07f804040554300526644320000502061067405302602000010750620017611707760fc7fb6db6db6ddddddddd34d34d349249249210842108c6318c639ce739cffffffff840260f81c161b60f71c167e1f0d1e100c1d070f090b19131c1706010e11080a1a141802121b1503160405601f85851693831c63d76453e004161a17031660020b9060020b0160020b0260020b5b90614bc9565b90956fffffffffffffffffffffffffffffffff955060409450600193987ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff27618918960ff6060969b811681031660020b9060020b0160020b0260020b6153bb565b949891955099969298919598602088015160a01b76ffffff0000000000000000000000000000000000000000167fffffffffffffffffff000000000000000000000000000000000000000000000073ffffffffffffffffffffffffffffffffffffffff8a51169216171782556fffffffffffffffffffffffffffffffff6003830154166fffffffffffffffffffffffffffffffff604089015116809103615535575b5082156155265760e060029101519101555b825190155f82121461551057506154ee6154f69293613785565b925103613785565b6fffffffffffffffffffffffffffffffff169060801b1793565b6154f69250906155209103613785565b91613785565b60e060019101519101556154d4565b6fffffffffffffffffffffffffffffffff167fffffffffffffffffffffffffffffffff000000000000000000000000000000006003840154161760038301555f6154c2565b5073ffffffffffffffffffffffffffffffffffffffff8c511673ffffffffffffffffffffffffffffffffffffffff60608501511614614adc565b60028b015494939094614acd565b7f9e4d7cc7000000000000000000000000000000000000000000000000000000005f5260045260245ffd5b60449250604051917f7c9c6e8f00000000000000000000000000000000000000000000000000000000835260048301526024820152fd5b606083019073ffffffffffffffffffffffffffffffffffffffff825116818111156155ed5750505173ffffffffffffffffffffffffffffffffffffffff1673fffd8963efd1fc6a506488495d951d5263988d268110156155c25750614a75565b9a509a50509950505050505050505f925f929190565b5f85511315614a1a577f96206246000000000000000000000000000000000000000000000000000000005f5260045ffd5b62ffffff610fff89169116620f424081830204910103614a07565b508960d01c62ffffff166149fd565b610fff8860c41c169461499a565b90808202917fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff828209918380841093039280840393847001000000000000000000000000000000001115610330571461577c57700100000000000000000000000000000000910990828211900360801b910360801c1790565b50505060801c90565b818102907fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff83820990828083109203918083039283620f424011156103305714615804577fde8f6cefed634549b62c77574f722e1ac57e23f24d8fd5cb790fb65668c2613993620f4240910990828211900360fa1b910360061c170290565b5050620f424091500490565b90808202917fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff828209918380841093039280840393846c0100000000000000000000000011156103305714615881576c01000000000000000000000000910990828211900360a01b910360601c1790565b50505060601c90565b908160601b907fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff6c01000000000000000000000000840992828085109403938085039485841115610330571461593c576c0100000000000000000000000082910981805f03168092046002816003021880820260020302808202600203028082026002030280820260020302808202600203028091026002030293600183805f03040190848311900302920304170290565b5091500490565b91818302917fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8185099383808610950394808603958685111561033057146159da579082910981805f03168092046002816003021880820260020302808202600203028082026002030280820260020302808202600203028091026002030293600183805f03040190848311900302920304170290565b505091500490565b6fffffffffffffffffffffffffffffffff6c010000000000000000000000009173ffffffffffffffffffffffffffffffffffffffff80600195169116038060ff1d90810118931692615a348185615810565b93091515160190565b6fffffffffffffffffffffffffffffffff9073ffffffffffffffffffffffffffffffffffffffff8061364f9594169116038060ff1d908101189116615810565b9073ffffffffffffffffffffffffffffffffffffffff811673ffffffffffffffffffffffffffffffffffffffff831611615b2e575b73ffffffffffffffffffffffffffffffffffffffff8216928315615b22577bffffffffffffffffffffffffffffffff00000000000000000000000073ffffffffffffffffffffffffffffffffffffffff615b16948185169403169160601b16615c68565b90808206151591040190565b62bfc9215f526004601cfd5b90615ab2565b73ffffffffffffffffffffffffffffffffffffffff821673ffffffffffffffffffffffffffffffffffffffff821611615bd5575b73ffffffffffffffffffffffffffffffffffffffff8116918215615b225761364f937bffffffffffffffffffffffffffffffff00000000000000000000000073ffffffffffffffffffffffffffffffffffffffff615bd0948185169403169160601b16615943565b614922565b90615b68565b8015610330577f07060605060205000602030205040001060502050303040105050304000000006f8421084210842108cc6318c6db6d54be826fffffffffffffffffffffffffffffffff1060071b83811c67ffffffffffffffff1060061b1783811c63ffffffff1060051b1783811c61ffff1060041b1783811c60ff1060031b1792831c1c601f161a1790565b929190615c76828286615943565b93821561492c5709615c8457565b9060010190811561033057565b91908115615d02577bffffffffffffffffffffffffffffffff00000000000000000000000073ffffffffffffffffffffffffffffffffffffffff9160601b169216918282029183838311918404141615615cf55761364f9261503092820391615c68565b63f5c787f15f526004601cfd5b505090565b90918015615d955773ffffffffffffffffffffffffffffffffffffffff7bffffffffffffffffffffffffffffffff000000000000000000000000819460601b16921680820281615d578483614922565b14615d7d575b5090615d6c615d719284614922565b612ffa565b80820615159104011690565b8301838110615d5d579150615d9192615c68565b1690565b50905090565b9073ffffffffffffffffffffffffffffffffffffffff82169182036130c85756fea164736f6c634300081a000a"; + } +}