以太坊已经部署的合约是无法修改的, 如果出现漏洞会出现被盗的风险, Openzeppelin给出了一种可以变相修改及升级合约的方式-代理模式. 我们在部署自己逻辑合约的时候同时部署一份代理合约, 所有用户消息通过代理合约重新定向到已部署的逻辑合约. 如果需要升级已部署的逻辑合约, 只要部署最新版的逻辑合约, 然后修改代理合约中引用逻辑合约的合约地址即可.

Openzeppelin提供了三种代理模式
Inherited Storage
Eternal Storage
Unstructured Storage
所有三种模式都依赖于低级委托调用. Solidity虽然提供了delegatecall函数, 但是它只返回true/false是否调用成功, 并且不允许你管理返回的数据.
在深入代理模式前, 首先要理解下面两个重要的概念:
当调用了合约不支持的函数时, 回调函数会被调用. 你可以编写自定义回调函数来处理这种情况. 代理合约使用自定义回调函数将调用重定向到其他合约实现.
每当合约A将调用委托给合约B时, 它会在合约A的上下文中执行合约B的代码. 这意味着 msg.value 和 msg.sender的值会被保留, 并且每次存储修改都会影响合约A的存储.
Zeppelin 的代理合约由所有代理模式共享, 出于这个特殊原因, 它实现了自己的委托调用函数, 该函数返回导致调用逻辑合约的值. 如果您打算使用 Zeppelin 的代理合约代码, 您应该详细了解您将使用的代码. 让我们确切地探索它是如何工作的, 并了解它用于实现这一目标的汇编操作码. (请随意参考 Solidity 的 Assembly 文档以获取更多信息)
[assembly {
let ptr := mload(0x40)
calldatacopy(ptr, 0, calldatasize)
let result := delegatecall(gas, _impl, ptr, calldatasize, 0, 0)
let size := returndatasize
returndatacopy(ptr, 0, size)
switch result
case 0 { revert(ptr, size) }
default { return(ptr, size) }
}
为了将调用委托给另一个 Solidity 合约函数, 我们必须将代理接收到的 msg.data 传递给它. 由于 msg.data 是字节类型, 一种动态数据结构, 它具有不同的大小, 存储在 msg.data 的第一个字大小(32 个字节)中. 如果我们只想提取实际数据, 我们需要跳过第一个字的大小, 并从 msg.data 的 0x20(32 字节)开始. 但是, 我们将利用两个操作码来代替执行此操作. 我们将使用 calldatasize 来获取 msg.data 的大小, 并使用 calldatacopy 将其复制到我们的 ptr 变量中.
注意我们是如何初始化我们的 ptr 变量的. 在Solidity中, 位于 0x40 位置的内存槽是特殊的, 因为它包含了下一个可用的自由内存指针的值. 每次你直接保存一个变量到内存, 你应该通过检查 0x40 的值来咨询你应该把它保存到哪里. 现在我们知道了允许我们保存变量的位置, 我们可以使用calldatacopy将大小为 calldatasize 的calldata从调用数据的0开始复制到 ptr 的位置.
let ptr := mload(0x40)
calldatacopy(ptr, 0, calldatasize)
让我们看看汇编块中使用 delegatecall 的代码:
let result := delegatecall(gas, _impl, ptr, calldatasize, 0, 0)
Parameters
gas我们传入执行函数所需的气体_impl我们正在调用的逻辑合约的地址ptr数据开始位置的内存指针calldatasize我们传递的数据的大小。0表示从调用逻辑合约返回的数据输出. 这是未使用的, 因为我们还不知道输出数据的大小, 因此无法将其分配给变量. 稍后我们仍然可以使用returndata操作码访问此信息0表示大小。这是未使用的, 因为我们没有机会创建临时变量来存储数据, 因为在调用另一个合约之前我们不知道它的大小. 我们可以通过稍后调用returndatasize操作码使用替代方法获取此值
如下代码使用 returndatasize 操作码获取返回数据的大小
let size := returndatasize
我们使用返回数据的大小将返回数据的内容复制到我们的 ptr 变量中,并使用辅助操作码函数 returndatacopy
returndatacopy(ptr, 0, size)
最后, switch 语句要么返回返回的数据, 要么在出现问题时抛出异常.

太好了, 我们现在有一种方法可以从逻辑合约中检索适当的结果值.
现在我们了解了代理合约的工作原理, 让我们看看 Zeppelin 提出的三种模式: 使用继承存储、非结构化存储和永恒存储的可升级性.
这三种方法有不同的方法来解决相同的技术难题: 如何确保逻辑合约不会覆盖代理中使用的状态变量以实现可升级性.
任何代理架构模式的主要问题是如何处理存储分配. 请记住, 由于我们将一个合约用于存储而另一个用于逻辑, 因此它们中的任何一个都有可能覆盖已经使用的存储槽。这意味着如果代理合约有一个状态变量来跟踪某个存储槽的最新逻辑合约地址, 而逻辑合约不知道它, 那么逻辑合约可以在同一个槽中存储一些其他数据, 从而覆盖代理的关键信息. Zeppelin 的三种方法提供了不同的方式来构建您的系统, 使您的合约可以通过代理模式进行升级.

