Euler.sol), the contracts are organised into modules, which live in
contracts/Constants.solfor the registry of module IDs. There are 3 categories of modules:
contracts/Proxy.sol. This is a very simple contract that forwards its requests to the main Euler contract address, along with the original msg.sender. The call is done with a normal
call(), so the execution takes place within the Euler contract's storage context, not the proxy's.
transfermethod is invoked, a
Transferevent must be logged from the EToken proxy's address, not the main Euler address.
balanceOf()methods of the ETokens and DTokens (which necessarily have the same selector) will collide.
contracts/Euler.solis the only other code that cannot be upgraded. It is the implementation of the main Euler contract. Essentially its only job is to be a placeholder address for the Euler storage, and to
delegatecall()to the appropriate modules.
contracts/Euler.sol:dispatch()appends some extra data onto the end of the
msg.datait receives from the proxy:
msg.sender, which corresponds to the address of the proxy.
msgSenderpassed in from the (trusted) proxy, which corresponds to the original
msg.senderthat invoked the proxy.
solcdirectly when communicating with the proxies, while still allowing the functions to extract the proxy addresses and original
msg.senders as seen by the proxies.
delegatecall(), the proxy address is typically available to module code as
msg.sender, so why is it necessary to pass this in to the modules? It's because batch requests allow users to invoke methods without going through the proxies (see below).
upgradeAdminaddress is the only address that can upgrade modules. However, the installer module itself is also upgradeable, so this logic can be restricted as we move towards greater levels of decentralisation.
balanceOfto increase. Rather, that fixed balance entitles you to reclaim more and more of the underlying asset as time progresses. Although the AAVE model is conceptually nicer, experience has shown that increasing balance tokens causes a lot of pain to integrators. In particular, if you transfer X ETokens into a pool contract and later withdraw that same X, you have not earned any interest and the pool has some left over dust ETokens that typically aren't allocated to anyone.
balanceOfare in internal bookkeeping units and don't really have any meaning to external users. There is of course a
balanceOfUnderlyingmethod (named the same as Compound's method, which may become a defacto standard) that returns the amount in terms of the underlying and does increase block to block.
approveDebt()to grant another account permission to send you some amount of DTokens.
approveDebt()name was used instead of the ERC-20
approve()due to concerns that some contracts might unintentionally allow themselves to receive "negative value" tokens.
INTERNAL_DEBT_PRECISION. This is because DTokens are tracked at a greater precision versus ETokens (27 decimals versus 18) so that interest compounding is more accurate. However, to present a common external decimals amount, this internal precision is hidden from external users of the contract. Note that these decimal place amounts remain the same even if the underlying token uses fewer decimal places than 18 (see the Decimals Normalisation section below).
BaseLogicwhich provides common lending logic related functionality. This contract inherits from
BaseModule, which inherits from
Base, which inherits from
contracts/Storage.solcontains the storage layout that is used by all modules. It is important that this match, since all modules are called with
delegatecall()from the Euler contract context. Furthermore, it is important that upgrades preserve the storage ordering and offsets. The test
test/storage.jshas the beginning of an implementation to take the Soldity compiler's storage layout output and verify that it is consistent across upgrades. After we deploy our first version, we will "freeze" the storage layout and encode this in the
increaseObservationCardinalityNext()on the uniswap pool to increase the size of the uniswap oracle's ring buffer to a minimum size (by default 10). It will try to retrieve prices averaged over the per-instrument
twapWindowparameter. If it cannot be serviced because the oldest value in the ring buffer is too recent, it will use the oldest price available (which we have ensured is at least 10 blocks old). In this case, it will pay the gas cost to increase the ring buffer by 1. The intent is to apply this feedback to tokens that have too short a ring-buffer, and dynamically lengthen it until such time as this is no longer the case.
twapWindow, the RiskManager may apply an extra factor to decrease the token's collateral/borrow factor for the purposes of borrowing/withdrawing (but not for liquidations). This is still TBD.
contracts/BaseLogic.sol:checkLiquidity(), which calls the internal RiskManager module's
requireLiquidity()which will revert the transaction if the account is insufficiently collateralised.
deferLiquidityCheck()function in the Exec module. This function disables all liquidity checking for a specified account, and then re-enters the caller by calling the
msg.sender. While this callback is executing,
checkLiquidity()will not bother checking the liquidity for the specified account. After the function returns, the liquidity will then be checked.
FlashLoan, which complies with the ERC-3156 standard. The adaptor internally uses liquidity deferral to borrow tokens and additionally requires that the loan is paid back in full within the transaction.
uintand XORed (exclusive ORed) with the ethereum address.
contracts/modules/Exec.sol:batchDispatch()function allows a group of Euler interactions to be executed within a single blockchain transaction. This is a "partial" solution since users cannot execute arbitrary logic in between the interactions, but is nonetheless sufficient for a wide variety of use-cases.
depositAndEnter()function (which would imply a combinatorial explosion of method combinations), batch transactions can be employed.
accruedInterestsince the last operation is computed. In Compound, this is added to
accruedInterest * reserveFactoris added to
totalReserves, resulting in new value for the assets:
totalReservesis in units of the underlying, so it does not accrue interest, however governance could vote to withdraw these reserves and re-deposit them in exchange for CTokens, which would.
totalSupplyis the sum of the balances of all CToken holders, the exchange rate between these CToken balances and the underlying token is:
newAssets, Euler increases
totalSupply. So the new value for assets is computed as though no reserve fees were being deducted:
totalBorrowsin the same way as Compound. But then, instead of adding the collected fee to
totalReserves(causing it to be deducted from
newAssetsCompound), a special number of new ETokens are minted and credited to the reserves, which increases
totalSupply. This number of newly minted ETokens is selected so as to inflate the supply just enough to divert a
reserveFactorproportion of the interest away from EToken holders to the reserves.
newTotalSupplyusing Compound's value:
newTotalSupply - totalSupply.
increaseBorrow()(and not in
checkLiquidity()), pTokens cannot even be flash borrowed.
liquidate()method of the Liquidation module can be invoked by anyone (except by the violating account itself, to avoid aliasing bugs). The account invoking this method is called the "liquidator". This method does two things:
1. For example, if an account's health score has fallen to
0.98, then the discount received is
1 - 0.98 = 0.02, or 2%.
1.1and then a large swap is performed on a borrowed asset which increases its current price on Uniswap significantly. Immediately after this swap (ie, throughout the rest of the block the swap was included in) then the TWAP of the asset is unchanged (since no time has passed). This means that the account's health score is also unchanged.
1. Assuming that the TWAP hasn't yet caught up with the current price, then in any subsequent block the health score will be below
1and there will therefore be a liquidation opportunity.
0.999then the discount would be a mere 0.1%. This level of discount is most likely not enough to make a liquidation worth-while. First of all, because the prices used to calculate the equivalent values of assets are TWAPs, they don't yet take into account the current (non-averaged) price of the underlying asset. Secondly, the discount must compensate the liquidator for any execution slippage, gas costs, and other operational overhead.
liquidate(). This means that liquidations are permission-less which is desirable for various reasons, not least of which because liquidations cannot be censored.
trackAverageLiquidity()function in the
execmodule. This will cause most operations such as depositing and withdrawing to consume more gas, but will make the account eligible for extra discount privileges, if it participates in liquidations.
AVERAGE_LIQUIDITY_PERIODseconds have elapsed will your full liquidity value be reflected.
exec.getUpdatedAverageLiquidity(), so long as your application can accept the limitations described above.
interestAccumulator()that retrieves the current interest accumulator for an asset. Because the accumulators are updated lazily as described above, rather than just returning the stored value, this method computes the updated accumulator given the most recent block's timestamp (sometimes called a "counterfactual" value).
balanceOfmethod can return different results with no intervening operations. In this case, the total pool available to owners of ETokens of these underlyings will be affected, but the protocol itself will not be (assuming such tokens have collateral factors of 0, which is the default).
docs/attacks.mdfor some more notes on the threat modelling.
balanceOf, we treat that result as though it were 0 (which a malicious token could also do of course). This way liquidity checks will at least succeed, allowing non-malicious collaterals to be liquidated.