Euler.sol
), the contracts are organised into modules, which live in contracts/modules/
.contracts/Constants.sol
for 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.transfer
method is invoked, a Transfer
event 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.docs/proxy-protocol.md
.contracts/Euler.sol
is 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.data
it receives from the proxy:msg.sender
, which corresponds to the address of the proxy.msgSender
passed in from the (trusted) proxy, which corresponds to the original msg.sender
that invoked the proxy.solc
directly when communicating with the proxies, while still allowing the functions to extract the proxy addresses and original msg.sender
s 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).upgradeAdmin
address 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.balanceOf
to 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.balanceOf
are in internal bookkeeping units and don't really have any meaning to external users. There is of course a balanceOfUnderlying
method (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).BaseLogic
which provides common lending logic related functionality. This contract inherits from BaseModule
, which inherits from Base
, which inherits from Storage
.contracts/Storage.sol
contains 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.js
has 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 test/storage.js
test.increaseObservationCardinalityNext()
on the uniswap pool to increase the size of the uniswap oracle's ring buffer to a minimum size. By default this size is 144, because this is on-average sufficient to satisfy a TWAP window of 30 minutes, assuming 12.5 second block times.twapWindow
parameter. 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 144 blocks old).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 onDeferredLiquidityCheck()
function on 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.uint
and 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.accruedInterest
since the last operation is computed. In Compound, this is added to totalBorrows
, and accruedInterest * reserveFactor
is added to totalReserves
, resulting in new value for the assets:totalReserves
is 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.totalSupply
is 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:totalBorrows
in 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 reserveFactor
proportion of the interest away from EToken holders to the reserves.newTotalSupply
using Compound's value:newExchangeRate
: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.1
and 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 1
and there will therefore be a liquidation opportunity.0.999
then 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 exec
module. 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_PERIOD
seconds 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).balanceOf
method 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.md
for 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.