代理协议
代理是不可升级的桩合约,有两个工作:
将外部用户的方法调用转发到主Euler合约
接收来自主Euler合约的方法调用并按照指示记录事件
尽管代理本身是不可升级的,但它们与 Euler 的模块系统集成在一起,于是就可以升级了。
以下协议都使用自定义组装例程,而不是solidity ABI 编码器/解码器。虽然我们不会掉以轻心,但将其保持在纯solidity中的测量开销太高了。为了弥补这种可能令人遗憾的汇编使用,本文档详细解释了协议。
代理 -> 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
:
对于在回退中收到的调用数据,代理在 dispatch()
(0xe9c4a3ac
) 前添加 4 字节选择器,并附加其对 msg.sender
的视图:
This data is then passed to the Euler contract with a CALL
(not DELEGATECALL
).
然后将此数据通过CALL
(而不是DELEGATECALL
)传递给Euler合约。
Euler -> 模块
在 dispatch()
方法中,Euler合约查找 msg.sender
的 its 视图,该视图对应于代理地址。
然后在trustedSenders
映射中查找假定的代理地址,该映射必须存在,否则调用将被复原。通过在moduleId
字段中有一个非零条目来确定它是否存在(模块必须具有非零 ID)。
可以将代理地址添加到 trustedSenders
的唯一方法是Euler合约本身创建它(使用 contracts/Base.sol
中的 _createProxy
函数)。
在单独代理模块的情况下,trustedSenders
中的相同slot也将包含模块实现的地址。如果不是(即多代理模块),则必须通过在 moduleLookup
映射中的附加查找来寻找模块实现。这是因为在升级期间,单独代理模块只需要更新对应的一点,而多代理模块则需要更新 trustedSenders
中的每个相应条目。
此时我们知道消息来自合法代理,因此可以假设最后 20 个字节对应于调用代理的实际msg.sender
。检查调用数据的长度。它至少应该是 4 + 4 + 20
字节长,对应于:
4 个字节用于
dispatch()
选择器。用于调用代理的选择器的 4 个字节(模块中不支持非标准 ABI 调用和回退方法)。
msg.sender
结尾的 20 个字节。
Euler 合约将接收到的 calldata 并剥离 dispatch()
选择器,然后附加 msg.sender
的 its 视图(汇编中的caller()
),它对应于代理的地址。这导致以下结果:
然后,这些数据通过 DELEGATECALL
发送到模块实现,因此模块实现的代码在主Euler合约的存储环境文中执行。
模块实现将使用solidity ABI 解码器解压原始调用数据,忽略末尾的40 个字节。
模块不被允许访问 msg.sender
。相反,他们应该使用 contracts/BaseModule.sol
中的 unpackTrailingParamMsgSender()
助手,它将从尾随调用数据中检索消息发送者。
当模块需要访问代理地址时,有一个复合帮助器 unpackTrailingParams()
返回两个尾随参数。 msg.sender
仍然不允许用于此,因为模块可以通过批处理调用,而不是通过代理。
模块 -> 代理
当一个模块直接发出一个日志(或solidity级别的“事件”)时,它将从主Euler合约的地址发生。这对于许多日志来说都无碍,但在某些情况下,比如模块正实施 ERC-20 标准时,就不太行了。在这些情况下,有必要从代理本身的地址发出日志。
为了做到这一点,Euler合约(特别是其中一个模块)对代理地址执行CALL
。
当代理看到Euler合约(它的创建者)对其回退的调用时,它知道不要重新进入Euler合约。相反,它将此调用解释为发出日志消息的指令。这是调用数据的格式:
The proxy unpacks this message and executes the appropriate log instruction, log0
, log1
, etc, depending on the number of topics.
代理解包此消息并根据主题的数量执行适当的日志指令log0
、log1
等。
Last updated