概念
通用可升级代理标准(Universal Upgradeable Proxy Standard),简写UUPS, 该标准与所有合约普遍兼容,并且不会在代理合约和业务逻辑合约之间造成不兼容。这是通过利用代理合约中唯一的存储位置来存储逻辑合约的地址来实现的。兼容性检查可确保成功升级。升级可以无限次执行,也可以由自定义逻辑确定。此外,还提供了一种从多个构造函数中进行选择的方法,该方法不会妨碍验证字节码的能力。
调用流程
delegatecall()
- 合约A中的函数允许外部合约B(委托)修改A的存储- 代理合约——存储数据,但通过
delegatecall()
的方式使用外部合约B的逻辑的合约A。 - 逻辑合约- 合约B ,包含代理合约A使用的逻辑
- 可代理合约——逻辑合约B中继承,提供升级功能代理合约
代理合约函数
fallback
所提议的回退函数遵循其他代理合约实现中看到的常见模式。但是,逻辑合约的地址不是强制使用变量,而是存储在定义的存储位置keccak256("PROXIABLE")
。这消除了代理合约和逻辑合约中变量之间发生冲突的可能性,从而提供了与任何逻辑合约的“通用”兼容性。
function() external payable {
assembly { // solium-disable-line
let contractLogic := sload(0xc5f16f0fcc639fa48a6947836d9850f504798523bf8c9a3a87d5876cf622bcf7)
calldatacopy(0x0, 0x0, calldatasize)
let success := delegatecall(sub(gas, 10000), contractLogic, 0x0, calldatasize, 0, 0)
let retSz := returndatasize
returndatacopy(0, 0, retSz)
switch success
case 0 {
revert(0, retSz)
}
default {
return(0, retSz)
}
}
}
constructor
所提出的构造函数接受任意数量和任意类型的参数,因此与任何逻辑合约构造函数兼容。
此外,代理合约构造函数的任意性提供了从逻辑合约源代码中可用的一个或多个构造函数中进行选择的能力(例如,,,constructor1
……constructor2
等)。请注意,如果逻辑合约中包含多个构造函数,则应包含检查以禁止在初始化后再次调用构造函数。
值得注意的是,支持多个构造函数的附加功能不会妨碍代理合约字节码的验证,因为可以首先使用代理合约 ABI,然后使用逻辑合约 ABI 来解码初始化 tx 调用数据(输入)。
下面的合约展示了代理合约的拟议实施方案。
contract Proxy {
// Code position in storage is keccak256("PROXIABLE") = "0xc5f16f0fcc639fa48a6947836d9850f504798523bf8c9a3a87d5876cf622bcf7"
constructor(bytes memory constructData, address contractLogic) public {
// save the code address
assembly { // solium-disable-line
sstore(0xc5f16f0fcc639fa48a6947836d9850f504798523bf8c9a3a87d5876cf622bcf7, contractLogic)
}
(bool success, bytes memory _ ) = contractLogic.delegatecall(constructData); // solium-disable-line
require(success, "Construction failed");
}
function() external payable {
assembly { // solium-disable-line
let contractLogic := sload(0xc5f16f0fcc639fa48a6947836d9850f504798523bf8c9a3a87d5876cf622bcf7)
calldatacopy(0x0, 0x0, calldatasize)
let success := delegatecall(sub(gas, 10000), contractLogic, 0x0, calldatasize, 0, 0)
let retSz := returndatasize
returndatacopy(0, 0, retSz)
switch success
case 0 {
revert(0, retSz)
}
default {
return(0, retSz)
}
}
}
}
可代理合约函数
代理合约包含在逻辑合约中,提供执行升级所需的功能。兼容性检查proxiable
可防止升级期间发生无法修复的更新。
警告:
updateCodeAddress
并且proxiable
必须存在于逻辑合约中。未包含这些内容可能会阻止升级,并可能导致代理合约完全无法使用。
proxiable
兼容性检查以确保新的逻辑合约实现了通用可升级代理标准。请注意,为了支持未来的实现,比较bytes32
可能会发生变化,例如keccak256("PROXIABLE-ERC1822-v1")
。
updateCodeAddress
将逻辑合约的地址存储keccak256("PROXIABLE")
在代理合约中。
下面的合约展示了可代理合约的拟议实施方案。
<span style="color:#212529"><span style="background-color:#ffffff"><span style="background-color:#eeeeff"><span style="background-color:#eeeeff"><code>contract Proxiable {
<span style="color:#999988"><em>// Code position in storage is keccak256("PROXIABLE") = "0xc5f16f0fcc639fa48a6947836d9850f504798523bf8c9a3a87d5876cf622bcf7"</em></span>
<strong>function</strong> updateCodeAddress(address newAddress) internal {
require(
bytes32(<span style="color:#009999">0xc5f16f0fcc639fa48a6947836d9850f504798523bf8c9a3a87d5876cf622bcf7</span>) <strong>==</strong> Proxiable(newAddress).proxiableUUID(),
"<span style="color:#dd1144">Not compatible</span>"
);
assembly { <span style="color:#999988"><em>// solium-disable-line</em></span>
sstore(<span style="color:#009999">0xc5f16f0fcc639fa48a6947836d9850f504798523bf8c9a3a87d5876cf622bcf7</span>, newAddress)
}
}
<strong>function</strong> proxiableUUID() <strong>public</strong> pure returns (bytes32) {
<strong>return</strong> <span style="color:#009999">0xc5f16f0fcc639fa48a6947836d9850f504798523bf8c9a3a87d5876cf622bcf7</span>;
}
}</code></span></span></span></span>
使用代理时的陷阱
使用代理合约时,所有逻辑合约都应采用以下常见的最佳实践。
将变量与逻辑分离
在设计新的逻辑合约时,应仔细考虑,以防止升级后与代理合约的现有存储不兼容。具体来说,不应修改新合约中变量的实例化顺序,并且应在上一个逻辑合约的所有现有变量之后添加任何新变量
为了促进这一做法,我们建议使用一个包含所有变量的“基础”合约,并在后续逻辑合约中继承该合约。这种做法大大降低了意外重新排序变量或在存储中覆盖变量的可能性。
限制危险功能
可代理合约中的兼容性检查是一种安全机制,用于防止升级到未实现通用可升级代理标准的逻辑合约。但是,正如 parity 钱包黑客攻击中发生的那样,仍然有可能对逻辑合约本身造成无法弥补的损害。
为了防止逻辑合约遭到破坏,我们建议将任何可能造成破坏的函数的权限限制为onlyOwner
,并在部署到空地址(例如 address(1))后立即放弃逻辑合约的所有权。 潜在破坏性函数包括本机函数(例如SELFDESTRUCT
)以及代码可能来自外部的函数(例如 、CALLCODE
和 ) 。在下面的ERC20代币 delegatecall()
示例中,LibraryLock
合约用于防止逻辑合约遭到破坏。
在下面示例中,我们展示了标准所有权示例,并将其限制updateCodeAddress
为仅限所有者。
contract Owned is Proxiable {
// ensures no one can manipulate this contract once it is deployed
address public owner = address(1);
function constructor1() public{
// ensures this can be called only once per *proxy* contract deployed
require(owner == address(0));
owner = msg.sender;
}
function updateCode(address newCode) onlyOwner public {
updateCodeAddress(newCode);
}
modifier onlyOwner() {
require(msg.sender == owner, "Only owner is allowed to perform this action");
_;
}
}
实现代码
ERC-20 代币
代理合约
pragma solidity ^0.5.1;
contract Proxy {
// Code position in storage is keccak256("PROXIABLE") = "0xc5f16f0fcc639fa48a6947836d9850f504798523bf8c9a3a87d5876cf622bcf7"
constructor(bytes memory constructData, address contractLogic) public {
// save the code address
assembly { // solium-disable-line
sstore(0xc5f16f0fcc639fa48a6947836d9850f504798523bf8c9a3a87d5876cf622bcf7, contractLogic)
}
(bool success, bytes memory _ ) = contractLogic.delegatecall(constructData); // solium-disable-line
require(success, "Construction failed");
}
function() external payable {
assembly { // solium-disable-line
let contractLogic := sload(0xc5f16f0fcc639fa48a6947836d9850f504798523bf8c9a3a87d5876cf622bcf7)
calldatacopy(0x0, 0x0, calldatasize)
let success := delegatecall(sub(gas, 10000), contractLogic, 0x0, calldatasize, 0, 0)
let retSz := returndatasize
returndatacopy(0, 0, retSz)
switch success
case 0 {
revert(0, retSz)
}
default {
return(0, retSz)
}
}
}
}
代币逻辑合约
contract Proxiable {
// Code position in storage is keccak256("PROXIABLE") = "0xc5f16f0fcc639fa48a6947836d9850f504798523bf8c9a3a87d5876cf622bcf7"
function updateCodeAddress(address newAddress) internal {
require(
bytes32(0xc5f16f0fcc639fa48a6947836d9850f504798523bf8c9a3a87d5876cf622bcf7) == Proxiable(newAddress).proxiableUUID(),
"Not compatible"
);
assembly { // solium-disable-line
sstore(0xc5f16f0fcc639fa48a6947836d9850f504798523bf8c9a3a87d5876cf622bcf7, newAddress)
}
}
function proxiableUUID() public pure returns (bytes32) {
return 0xc5f16f0fcc639fa48a6947836d9850f504798523bf8c9a3a87d5876cf622bcf7;
}
}
contract Owned {
address owner;
function setOwner(address _owner) internal {
owner = _owner;
}
modifier onlyOwner() {
require(msg.sender == owner, "Only owner is allowed to perform this action");
_;
}
}
contract LibraryLockDataLayout {
bool public initialized = false;
}
contract LibraryLock is LibraryLockDataLayout {
// Ensures no one can manipulate the Logic Contract once it is deployed.
// PARITY WALLET HACK PREVENTION
modifier delegatedOnly() {
require(initialized == true, "The library is locked. No direct 'call' is allowed");
_;
}
function initialize() internal {
initialized = true;
}
}
contract ERC20DataLayout is LibraryLockDataLayout {
uint256 public totalSupply;
mapping(address=>uint256) public tokens;
}
contract ERC20 {
// ...
function transfer(address to, uint256 amount) public {
require(tokens[msg.sender] >= amount, "Not enough funds for transfer");
tokens[to] += amount;
tokens[msg.sender] -= amount;
}
}
contract MyToken is ERC20DataLayout, ERC20, Owned, Proxiable, LibraryLock {
function constructor1(uint256 _initialSupply) public {
totalSupply = _initialSupply;
tokens[msg.sender] = _initialSupply;
initialize();
setOwner(msg.sender);
}
function updateCode(address newCode) public onlyOwner delegatedOnly {
updateCodeAddress(newCode);
}
function transfer(address to, uint256 amount) public delegatedOnly {
ERC20.transfer(to, amount);
}
}