Proxy Protocol
Proxies are non-upgradeable stub contracts that have two jobs:
Forward method calls from external users to the main Euler contract
Receive method calls from the main Euler contract and log events as instructed
Although proxies themselves are non-upgradeable, they integrate with Euler's module system, which does allow for upgrades.
The following protocols all use custom assembly routines instead of the solidity ABI encoder/decoder. While we don't take this lightly, the measured overhead of keeping this in pure solidity was too high. In order to make up for this otherwise regrettable use of assembly, this document explains the protocols in detail.
Proxy -> Euler
To the calldata received in a fallback, the proxy prepends the 4-byte selector for dispatch()
(0xe9c4a3ac
), and appends its view of msg.sender
:
This data is then passed to the Euler contract with a CALL
(not DELEGATECALL
).
Euler -> module
In the dispatch()
method, the Euler contract looks up its view of msg.sender
, which corresponds to the proxy address.
The presumed proxy address is then looked up in the trustedSenders
mapping, which must exist otherwise the call is reverted. It is determined to exist by having a non-zero entry in the moduleId
field (modules must have non-zero IDs).
The only way a proxy address can be added to trustedSenders
is if the Euler contract itself creates it (using the _createProxy
function in contracts/Base.sol
).
In the case of a single-proxy module, the same storage slot in trustedSenders
will also contain an address for the module's implementation. If not (ie multi-proxy modules), then the module implementation must be looked up with an additional lookup in the moduleLookup
mapping. This is because during an upgrade, single-proxy modules just have to update this one spot, whereas multi-proxy modules would otherwise need to update every corresponding entry in trustedSenders
.
At this point we know the message is originating from a legitimate proxy, so the last 20 bytes can be assumed to correspond to an actual msg.sender
who invoked a proxy. The length of the calldata is checked. It should be at least 4 + 4 + 20
bytes long, which corresponds to:
4 bytes for the
dispatch()
selector.4 bytes for selector used to call the proxy (non-standard ABI invocations and fallback methods are not supported in modules).
20 bytes for the trailing
msg.sender
.
The Euler contract then takes the received calldata and strips off the dispatch()
selector, and then appends its view of msg.sender
(caller()
in assembly), which corresponds to the proxy's address. This results in the following:
This data is then sent to the module implementation with DELEGATECALL
, so the module implementation code is executing within the storage context of the main Euler contract.
The module implementation will unpack the original calldata using the solidity ABI decoder, ignoring the trailing 40 bytes.
Modules are not allowed to access msg.sender
. Instead, they should use the unpackTrailingParamMsgSender()
helper in contracts/BaseModule.sol
which will retrieve the message sender from the trailing calldata.
When modules need to access the proxy address, there is a composite helper unpackTrailingParams()
that returns both trailing params. msg.sender
is still not allowed to be used for this, since modules can be invoked via a batch dispatch, instead of via the proxy.
module -> Proxy
When a module directly emits a log (or "event" at the solidity level) it will happen from the main Euler contract's address. This is fine for many logs, but not in certain cases like when a module is implementing the ERC-20 standard. In these cases it is necessary to emit the log from the address of the proxy itself.
In order to do this, the Euler contract (specifically one of the modules) does a CALL
to the proxy address.
When the proxy sees a call to its fallback from the Euler contract (its creator), it knows not to re-enter the Euler contract. Instead, it interprets this call as an instruction to issue a log message. This is the format of the calldata:
The proxy unpacks this message and executes the appropriate log instruction, log0
, log1
, etc, depending on the number of topics.
Last updated