Skip to main content

Diamond 101 (1)

· 6 min read

本文為 Diamond 101 系列的第一篇文章,將解釋什麼是可升級合約、常見的實作以及不同實作之間的設計。

Proxy 的組成

可升級合約顧名思義就是可以更新邏輯的合約,實作大多基於 Proxy。Proxy 是一種 Solidity 的 design pattern,將資料與邏輯分開處理,只要能替換掉邏輯,就能升級。

Delegatecall

首先需要先來補充一點背景知識。目前所有的 Proxy 都是 delegatecall 的應用。簡單說明 delegatecall 就是以別的合約的函式來操作發出 delegatecall 的合約的 storage。而一般的合約調用則是以自己的函式操作自己的 storage。舉例來說,當 A 合約對 B 合約調用 delegatecall 時,B 合約的函式會被調用,但是對 storage 的寫入都會執行 A 合約上。附上常見的 delegatecall 實作

Delegatecall 也衍生出不少有趣的 EIP,例如 EIP-1167,以最小的成本複製一個合約。

fallback 與 receive

有關合約調用的交易 (transaction) 的 data 欄位不會空著,前面 4 個 bytes 為 function selector,用來讓 EVM 知道要執行合約中的哪段 bytecode。要是沒有 function selector 或是 function selector 不在合約中怎麼辦?這時則會看合約有沒有實作 fallback 或是 receive,兩者觸發的條件如下:

  • fallback:
    • 合約中沒有對應的 function selector
    • 沒有 receive 時
  • receive:
    • 交易的 data 為空 (不管 value 為多少)

Proxy 合約中不會紀錄邏輯合約 (logic contract) 每一個 function selector,就會利用 fallback 的特性處理所有的合約調用,將所有的合約調用都 delegatecall 至邏輯合約 (logic contract)。可以來看一下 Openzeppelin 提供的實作,就是 delegatecall 跟 fallback, receive 的組合拳。

Transparent Proxy

架構圖如上,來看一下 Openzeppelin 的介紹

The way we deal with this problem is via the transparent proxy pattern. The goal of a transparent proxy is to be indistinguishable by a user from the actual logic contract.

這是何意呢?主要是要讓使用者跟邏輯合約 (logic contract) 緊緊連結在一起,不過這樣寫也很讓人難懂。簡單來說就是使用者所發起的合約調用都要以 delegatecall 調用邏輯合約 (logic contract),而 admin 所發起的合約調用永遠不會到邏輯合約。

示意圖如下

msg.senderowner()upgradeTo()transfer()
adminreturns proxy.ownerupgrades proxyreverts
userreturns ERC20 ownerrevertssends ERC20 transfer

Openzeppelin 採用 EIP-1967 作為實作。實作上,Proxy 裡面的每個管理 proxy 的 function 都會加上身份驗證的流程,只讓 admin 可以調用。但是為何還要一個而 ProxyAdmin 呢?ProxyAdmin 主要用途是為了執行查詢的用途還有讓 owner 解放。

舉個例子,admin() 應該要是個 view function 但是因為 fallback 的關係不能標示 view,需要利用另外一個合約再包裝一層才能標示 view。此外 admin 也可能同時是 user,把權限交給 ProxyAdmin 同時有更好的 function interface 可以調用,也可以讓 admin 解開限制。

Proxy 中 admin function 的舉例

modifier ifAdmin() {
if (msg.sender == _getAdmin()) {
_;
} else {
_fallback();
}
}

function admin() external ifAdmin returns (address admin_) {
admin_ = _getAdmin();
}

ProxyAdmin 中對 admin function 的封裝

staticcall 一起使用,才能標示為 view

function getProxyAdmin(TransparentUpgradeableProxy proxy) public view virtual returns (address) {
(bool success, bytes memory returndata) = address(proxy).staticcall(hex"f851a440");
require(success);
return abi.decode(returndata, (address));
}

UUPS

UUPS 的規格和介面是基於 EIP-1822,不過 Openzeppelin 也是和 EIP-1967 搭配作為實作。Transparent Proxy 最大的差異在做 upgrade 的函式同樣也放在邏輯合約中,讓 Proxy 只留 fallback 和 receive。升級合約也需要透過 delegatecall 調用邏輯合約。合約架構更為精簡,不用額外的身份驗證或是額外的 Admin 合約。

Diamond

Diamond 怎麼升級跟 Transparent Proxy 跟 UUPS 截然不同。Transparent Proxy 跟 UUPS 每次更新都需要將整份合約更新,除了不需要更新的部分也連帶更新了,而且因為合約大小有上限 (EIP-170) 也不能在單個合約一直增加新的邏輯。所以有人提出了 EIP-2535,主要是建立一張表來註冊不同的 function selector。有趣的地方在於可以註冊不同合約的 function selector。也就是說 delegatecall 可以調用的合約不只限於一個。除了彈性變大之外,也可以只新增或是修改其中一個 function selector。

Reference