|
| 1 | +# EulerSwap Developer Guide |
| 2 | + |
| 3 | +## Code structure |
| 4 | + |
| 5 | +EulerSwap is split into the following main contracts: |
| 6 | + |
| 7 | +* `EulerSwap`: Contract that is installed as an EVC operator by liquidity providers, and is also invoked by swappers in order to execute a swap. |
| 8 | + * `UniswapHook`: This is an internal contract used by `EulerSwap` that contains the functions required to function as a Uniswap4 hook. |
| 9 | +* `EulerSwapFactory`: The factory contract for creating `EulerSwap` instances. |
| 10 | +* `EulerSwapRegistry`: The registry serves as a directory for advertising and discovering active `EulerSwap` instances. |
| 11 | +* `EulerSwapPeriphery`: Simple wrapper contract for quoting and performing swaps, while handling approvals, slippage, etc. |
| 12 | + |
| 13 | +The above contracts depend on libraries: |
| 14 | + |
| 15 | +* `CtxLib`: Allows access to the `EulerSwap` context: Structured storage and the instance parameters |
| 16 | +* `FundsLib`: Moving tokens: approvals and transfers in/out |
| 17 | +* `CurveLib`: Mathematical routines for calculating the EulerSwap curve |
| 18 | +* `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. |
| 19 | +* `SwapLib`: Core logic for executing swaps. |
| 20 | + |
| 21 | + |
| 22 | +## Operational flow |
| 23 | + |
| 24 | +The following steps outline how an EulerSwap operator is created and configured: |
| 25 | + |
| 26 | +1. Deposit initial liquidity into one or both of the underlying credit vaults to enable swaps. |
| 27 | +1. Choose the desired pool parameters (`IEulerSwap.StaticParams` and `IEulerSwap.DynamicParams` structs) and initial state (`IEulerSwap.InitialState`). |
| 28 | +1. Create the EulerSwap instance: |
| 29 | + 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 (required for Uniswap4 support). |
| 30 | + 1. Install the above address as an [EVC operator](https://evc.wtf/docs/whitepaper/#operators), ensuring that any previous `EulerSwap` operators are uninstalled. |
| 31 | + 1. Invoke `deployPool()` on the EulerSwapFactory. |
| 32 | +1. Optional: Call the `registerPool()` method on the EulerSwapRegistry with the above instance address. |
| 33 | + |
| 34 | + |
| 35 | + |
| 36 | + |
| 37 | +## Pool Parameters |
| 38 | + |
| 39 | +When creating an EulerSwap instance, the pool is parameterised by two different classes of parameters: |
| 40 | + |
| 41 | +* Static Parameters: These are immutable parameters that cannot be changed through the pool's lifetime. To save gas, these are passed as trailing calldata to EulerSwap instances. |
| 42 | +* Dynamic Parameters: These parameters can be modified by the pool owner, and are kept in storage. |
| 43 | + |
| 44 | +In addition, the initial state of the the reserves are provided. |
| 45 | + |
| 46 | +### Static Parameters |
| 47 | + |
| 48 | +* `supplyVault0` and `supplyVault1`: Addresses of vaults that should be used to store balances. Swaps will first attempt to withdraw from the corresponding input vault before doing any borrowing. |
| 49 | +* `borrowVault0` and `borrowVault1`: Addresses of vaults that should be borrowed from once the corresponding supply vault is exhausted. These can be the same addresses as the supply vaults. If `address(0)` is provided, then any operation that causes the pool to attempt a borrow will fail. |
| 50 | +* `eulerAccount`: The owner/holder of the liquidity. This address must install the EulerSwap instance as an [EVC operator](https://evc.wtf/docs/whitepaper/#operators). |
| 51 | +* `feeRecipient`: The address that receives swapping fees. Use `address(0)` for them to accrue to the `eulerAccount`. |
| 52 | +* `protocolFeeRecipient` and `protocolFee`: These control the protocol fee settings. They should be read from the EulerSwapFactory prior to creating an instance. |
| 53 | + |
| 54 | +### Dynamic Parameters |
| 55 | + |
| 56 | +* `equilibriumReserve0` and `equilibriumReserve1`: At equilibrium, how much "virtual reserve" of each asset should the pool consider it has. This is not necessarily the same as how much actual liquidity is available, since extra liquidity may be borrowable. Like all reserve values, these are in units of the underlying asset. |
| 57 | +* `minReserve0` and `minReserve1`: These are the minimum values that the reserves are allowed to be reduced to. Use `0` for both if you want to support full-range liquidity. Otherwise, a non-zero value can be chosen so that the actual underlying liquidity is depleted when this level of reserve is reached. This allows an instance to provide all of its liquidity over a restricted price range. |
| 58 | +* `priceX` and `priceY`: These form the numerator and denominator of a fraction that represents the price of the assets at the equilibrium point. This fraction must also reflect any decimal differences between the two assets. |
| 59 | +* `concentrationX` and `concentrationY`: These control how concentrated the swap curve is on each side of the equilibirum point. The more concentrated, the smaller the price impact is for a given size trade. These are 18-scale decimal numbers between 0 and 1. A concentration of 0 means constant-product, and a concentration of 1 means constant-sum. |
| 60 | +* `fee0` and `fee1`: The fee to be applied to the input of asset0 and asset1 respectively. These are 18-scale decimal numbers. The special value of `1e18` means that swaps in this direction are rejected. These can be overridden by the [getFee hook](#get-fee-hook). |
| 61 | +* `expiration`: A timestamp after which swaps can no longer be performed on this pool. This is useful for pools that implement limit orders. |
| 62 | +* `swapHookedOperations` and `swapHook`: See [Hooks](#hooks). |
| 63 | + |
| 64 | +### Initial State |
| 65 | + |
| 66 | +This allows the `reserve0` and `reserve1` state variables to be set to arbitrary values. In most cases these can just be set to be the same as `equilibriumReserve0` and `equilibriumReserve1`. However, if you wish for the pool to start at point on the swapping curve different from the equilibrium point, different values can be selected. |
| 67 | + |
| 68 | +Note that in the initial configuration, these values are verified to represent a point exactly on the curve. If they are either above or below the curve, the pool deployment will fail. |
| 69 | + |
| 70 | +### Reconfiguration |
| 71 | + |
| 72 | +The dynamic parameters and the initial state can be changed at any time via the `reconfigure()` method. This method can be invoked by the following entities: |
| 73 | + |
| 74 | +* The `eulerAccount` address from the static parameters. |
| 75 | +* Any EVC operator that the `eulerAccount` has designated to perform actions on its behalf. |
| 76 | +* A *manager* address that the `eulerAccount` has delegated by calling `setManager`. This is useful in order to give an address `reconfigure()` support without allowing it full EVC operator access. |
| 77 | +* The `swapHook` address (allowing the [afterSwap hook](#after-swap-hook) to reconfigure). |
| 78 | + |
| 79 | +When reconfiguring, the provided initial state reserves are not verified to be precisely on the curve. Although they may not be below the curve, they may be up and to the right, representing excess value exists in the pool that is not claimed by the EulerSwap instance. Pools should be careful to not leak value in this case, since any excess tokens can be claimed by the next swapper, even with 0 input tokens. Setting the reserve values to the same as the equilibrium reserves will never leak value in this way. |
| 80 | + |
| 81 | + |
| 82 | + |
| 83 | +## Factory |
| 84 | + |
| 85 | +The `EulerSwapFactory` is a permissionless contract for creating `EulerSwap` instances. Given the pool parameters and initial state, it does some basic validation, creates an instance, and invokes `activate()` on the instance, which does some additional validation and sets up its storage. |
| 86 | + |
| 87 | +Note that the factory allows any types of vaults to be used by EulerSwap operators. Care should be taken when interacting with EulerSwap instances for this reason, since not all vault types have been designed to work correctly with `EulerSwap`. In order to limit this, swappers can choose to only use instances that have been added to the [registry](#Registry), which validates vaults according to a [perspective](#valid-vault-perspectives). |
| 88 | + |
| 89 | +### Metaproxies |
| 90 | + |
| 91 | +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. |
| 92 | + |
| 93 | +When an `EulerSwap` instance is created, the `IEulerSwap.StaticParams` 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 static parameters to be accessed cheaply when servicing a swap, compared to if they had to be read from storage. |
| 94 | + |
| 95 | + |
| 96 | + |
| 97 | +## Registry |
| 98 | + |
| 99 | +The `EulerSwapRegistry` contract is an optional directory that pools can be added to. Only the creator of a pool can add it. By adding a pool to a registry, you are advertising it to solvers and aggregators. Pools in a registry are discoverable by trading pair. Although some solvers may be able to find and use pools that have not been added to any registry, others rely on a more organised and searchable directory of pools. |
| 100 | + |
| 101 | +When adding a pool to a registry, you may be required to post a *validity bond*. This is a bond denominated in native token, the minimum value of which is set by a special registry curator. If you remove your pool from the registry, the bond will be returned to you. However, if at any time your pool is quoting swaps that cannot actually be filled, you may forfeit the bond. |
| 102 | + |
| 103 | +There are two mechanisms for the bond to be seized: |
| 104 | + |
| 105 | +* The registry curator may manually unregister your pool. At their discretion, they may either return the bond to the pool creator, or seize it to discourage invalid/spam registrations. |
| 106 | +* Any user may *challenge* a pool by providing a quote that cannot be filled. If the challenge is successful, the pool is unregistered and the challenger receives the bond. |
| 107 | + |
| 108 | +You can read the minimum required validity bond for a registry by calling the `minimumValidityBond()` method. If this value is 0, then no value is required to be sent with `registerPool()`, and pools cannot be challenged (although the curator may still manually remove them). |
| 109 | + |
| 110 | +### Valid Vault Perspectives |
| 111 | + |
| 112 | +The registry contract verifies that the contract instances it registers were created by the `EulerSwapFactory`. During registration, it queries the instance to determine which underlying vaults it is using, and then verifies these are acceptable by calling an `isVerified()` method on a *perspective* contract. Typically this will be a simple contract that checks that the instance was created by the Euler Vault Kit factory, however the curator may install a new perspective contract to allow additional vault-types. |
| 113 | + |
| 114 | +### Challenges |
| 115 | + |
| 116 | +In order to challenge a pool to retrieve the validity bond, a challenger invokes the `challengePool` method. As arguments, the challenger provides the parameters required to perform a swap on the pool: The input/output tokens, an amount, and whether the swap is exact input or exact output. The registry then performs the following: |
| 117 | + |
| 118 | +* A quote is retrieved for this swap. |
| 119 | + * If this fails, the challenge is rejected. |
| 120 | +* The swap is actually performed by taking the input tokens from the challenger. The challenger must've given appropriate token approval to the registry. In all cases, the funds will be returned to the challenger, meaning they can be sourced with a flash loan. |
| 121 | + * If this swap succeeds, the entire transaction is reverted (including the swap) and the challenge is rejected. |
| 122 | + * If the swap failed for any reason other than `E_AccountLiquidity()` or `HookError()` then the challenge is rejected. This check is necessary because some vaults can fail for other expected reasons, such as unpopulated pull oracles. |
| 123 | +* At this point, the challenge has succeeded. The validity bond is sent to the `recipient` address provided by the challenger, and the pool is unregistered. |
| 124 | + |
| 125 | + |
| 126 | + |
| 127 | +## Hooks |
| 128 | + |
| 129 | +Custom behaviour can be added to an EulerSwap instance via the hook mechanism. There are two hooks, one that runs before the swap is performed (and during quoting), and one that runs after a swap has been performed. |
| 130 | + |
| 131 | +Pool operators who want to ensure their pool remains in a registry must ensure that the hooks they install do not revert. If they do revert, they may be challenged and removed. To prevent a swap temporarily, the `getFee` hook can return `1e18` (see below). |
| 132 | + |
| 133 | +The `swapHookedOperations` is a bitmask that controls which of the two hooks should be invoked. The `IEulerSwapHookTarget.sol` file contains 3 constants that should be bitwise OR'ed together to select which hooks should be invoked: |
| 134 | + |
| 135 | +* `EULER_SWAP_HOOK_BEFORE_SWAP` |
| 136 | +* `EULER_SWAP_HOOK_GET_FEE` |
| 137 | +* `EULER_SWAP_HOOK_AFTER_SWAP` |
| 138 | + |
| 139 | +### Before Swap Hook |
| 140 | + |
| 141 | +This hook is invoked before the swap actually starts. No tokens will have yet been taken or sent. |
| 142 | + |
| 143 | +The hook is invoked with a regular `call`, meaning that it may perform state-changing operations. However, it is not allowed to call back into the EulerSwap instance, because it holds a reentrancy lock. |
| 144 | + |
| 145 | +Note that hooks which modify storage should verify the `msg.sender` is actually the expected EulerSwap pool instance, otherwise anyone could invoke the hook methods at any time and potentially cause unexpected behaviour. Alternatively, hooks may use `msg.sender` as a mapping key for their storage, so any third-party callers would be unable to touch the storage used for the EulerSwap instance(s). |
| 146 | + |
| 147 | +### Get Fee Hook |
| 148 | + |
| 149 | +This hook is invoked in two cases: |
| 150 | + |
| 151 | +* When a quote is being calculated. Since quotes are performed by view methods, the getFee hook must not modify any storage in this case. To indicate this, the hook receives a `readOnly` boolean parameter. |
| 152 | +* When a swap is about to be performed. `readOnly` will be false in this case, allowing storage to be modified. |
| 153 | + |
| 154 | +In either case, the getFee hook should return the fee that will be required for the swap. This is a fraction scaled by 18 decimals. For example, a fee of 10% would be `0.1e18`. In addition, there are two special additional values supported: |
| 155 | + |
| 156 | +* `1e18`: This indicates the swap is rejected. |
| 157 | +* `type(uint64).max`: This indicates that the default fee configured in the dynamic parameters should be used instead. |
| 158 | + |
| 159 | +The same warning about verifying `msg.sender` in the beforeSwap hook also applies. |
| 160 | + |
| 161 | +### After Swap Hook |
| 162 | + |
| 163 | +This hook is invoked after a swap has been performed, so it can always modify storage. It is invoked at the very end of a swapping operation, so it sees the final effects of a swap on the pool's reserves, and the underlying vaults. |
| 164 | + |
| 165 | +If the after swap hook reverts, then the entire swap will be aborted. This can be used to perform post-swap invariant checks. For example, it could verify that borrow interest being paid is not too high. Note however that doing so may cause complications for aggregators/solvers since they cannot necessarily rely on the quotes issued by your pool to actually be executable. For this reason, pools that revert may be challenged and removed from registry, if validity bonds are posted. |
| 166 | + |
| 167 | +While invoking the after swap hook, the EulerSwap instance's reentrancy lock is unlocked. This allows the hook to call `reconfigure()` on the instance if desired. |
| 168 | + |
| 169 | +The same warning about verifying `msg.sender` in the beforeSwap hook also applies. |
0 commit comments