這算是開篇吧。

關於區塊鏈,看過太多解讀,有時候都沒有心思做什麼爭論。我只想對入這行的產品們說,咱們將因為區塊鏈而獲得更多的操控感,從業務體系、到組織管理,再到經濟體系,一切都將攤開在我們的面前。相應的,你除了互聯網的東西外,趕緊補點代碼,趕緊補點博弈論,趕緊補點組織行為學,你將受益匪淺。

為什麼寫GUSD

沒什麼特殊的,只是趕巧了,他正好趕到我視野當中了。而現在來看,這件事情,確實有可能在幣圈興起點風浪。

這是一個穩定幣StableCoin

GUSD所涉及的不是一個新的概念,就是所謂的穩定幣(StableCoin),一般這類幣會跟某國法幣綁定在一起,維持穩定的兌換比率,就跟當年美元跟黃金錨定一樣。

在GUSD之前,早就有一種叫USDT的穩定幣了(你還能從我之前的文章中,找到一篇USDT的分析文章)。USDT目前的發行量非常大,算是最知名的、錨定美元的穩定幣。但是由於其賬目一直處於不透明狀態,而眼看著盤子又越做越大,自然引發了很多口水。於是,各種替代性的穩定幣,就橫空出世。

而替代品當中,一直鮮有突出者,但這次的GUSD,卻感覺來勢洶洶。最主要的原因是,他將存款託管在US Bank那裡,賬目可以每月做第三方審計,其第一期的審計公司是全美排名前50的一家大型審計諮詢公司---BPM。

Gemini以非常強勢的聲調宣告,老子一切都按規矩來,你們放大眼睛監督。你要知道,USDT最受詬病的一點就是,沒有傳統銀行為他提供存款託管服務,也沒有做過真正合格的第三方審計。

這也就難怪,為什麼有人喊出「GUSD就是電子美鈔」。如果真如官方所宣揚的那樣,「combines the creditworthiness and price stability of the U.S. dollar with blockchain technology」,美元進出有傳統銀行把關,GUSD代幣流轉跨國無阻,那麼這款電子美鈔,將具備攪動全球金融版圖的能力,實現其宣揚「Send and Receive U.S. dollars like Email」。

當然 我最感興趣的 還是他構建的智能合約

上面扯的那些,對我們來講太遠了。我此刻比較感興趣的是,Genimi Dollar是基於以太坊發行的一款代幣。

沒辦法,我太愛以太坊的智能合約了。智能合約屬於那種看上去簡單,但具備革命性力量的東西,在我看來屬於串聯傳統行業和區塊鏈領域的中間樞紐。如果你想了解你到底能在區塊鏈領域裡面做點什麼不一樣的,強烈建議從智能合約這個東西入手。

結果,果然不虛此行。

在深入研讀GUSD代碼和白皮書的過程中,項目方向我們展示了一個「如果搭建一個安全的、可監控的貨幣體系」,而這些思路本身,無論對於技術開發,還是對於產品設計,都有著非常深刻的啟發意義。

GUSD簡介

這是著名的幣圈雙子星(當年跟扎克伯格打官司的那對雙胞胎,也是當前最知名的比特幣富翁)推出的一個錨定美元的穩定型加密貨幣(StableCoin),對標另一種穩定幣USDT。

USDT的發行方Tether是加密貨幣交易所Bitfinex的關聯企業,聲稱與美元保持1:1的兌換關係。在各國(特別是我國)紛紛關閉發幣兌換通道以後,USDT的存在為廣大的幣民提供了進出加密貨幣交易所的通道,以及穩定的計價標的。

GUSD的發行方是Gemini,也是一個知名的加密貨幣交易所,哈哈,是不是也很巧合。你要說多關注下新聞,你會發現幾乎所有大的交易所,都想發行自己穩定幣,這裡面隱藏的利益,實在是太誘人了。

該幣種的發行過程,就是在美元存款賬戶和智能合約發行的代幣之間,所以一比一的映射。

關鍵信息

  • 官網:gemini.com/dollar/
  • 發行人Gemini Trust Company, LLC (一家紐約的信託公司)
  • 存款銀行:美國銀行,並且滿足了FDIC "Pass-through"存款保險的要求和使用限制
  • 會計審計:每月由公開註冊的會計師事務所,對存款賬戶進行審計,並公布審計結果
  • 安全審計:兼容ERC20標準,發行於以太坊上;發行所用的智能合約經由Tail of Bits, inc.審計;報告在此(gemini-dollar-trailofbits-audit.pdf)
  • 智能合約:etherscan的ERC20追蹤地址;GUSD的代碼地址在此;
  • 白皮書:地址

代碼設計

Gemini設計了一種可升級的架構,以便於實現幾個目的:修復隱患;擴展功能;提升運轉效率;對交易保留暫停、禁止和回滾的能力;

GUSD合約解析

這個框架中,最主要的是3個合約,「Proxy」,「Impl」,「Store」。智能合約Proxy是Gemini Dollar的對外界面,是GUSD在以太坊上唯一地址,永久有效;他為GUSD持有者提供了互動通道,為他們提供了轉賬、查詢餘額等方面的能力;

然而,在Proxy合約裡面,並沒有直接包含「動作執行」代碼,也沒有存儲相關數據;實際上,他將諸如「轉賬」、「發行」等能力,委託給了另一個智能合約「Impl」。

而『Impl」也沒有直接存儲GUSD相關的賬本數據(記錄誰有多少Token),類似的,他將這個賬本委託給了「Store」。關係圖如下:

合約監護人

為了能對一些高風險的動作施加控制,項目方設計了一個離線的審批機制。在這個項目中,每一個智能合約,都設置了監護人,在執行某些關鍵的動作時,一定要由監護人發起。

監護人可能是一個智能合約,或者是一個keyset(在線的也好,離線的也罷),這個監護人可能也需要從上游監護人那裡獲取許可,並以此類推,從而實現多層審批制。

比如,一個智能合約從另一個合約那裡獲取執行許可,而另一個智能合約最終從一個keyset那裡獲取許可,如果此處這個keyset是離線的,那麼我們實際上就構建了一個離線的審批許可制度。

在GUSD的實踐中,許可審批流程如下:

合約升級

合約升級是一件非常有風險的事情,這時候,上面講的多層審批制就派的上用場了(離線keyset的多層審批制)。升級過程如下:

  1. 創建新的「Impl」實例 - Impl(2);
  2. 由監護人(Custodian)向「Proxy」發出「升級」指示,命令對方,從今往後,當接到用戶的交互請求而需要「執行特定動作」時,只能將「執行過程」委託給「步驟1當中創建Impl(2)」;
  3. 監護人Custodian同時向「Store」也發出升級指令,命令對方不要再聽信舊Impl的任何「更改賬簿」的指令,轉而只聽從Impl(2)的指令,將其作為唯一可信的賬本更新信號來源;

所有的升級動作,都是在監護人Custodian的監護下執行的。

當然,監護人Custodian自身也可以被更改。比如,如果需要更新離線的keyset,那麼可以讓「OLd Custodian」向「Proxy」發出指令,要求其以後聽從「New Custodian」的指令,而「New Custodian」的上級監護人是「New Offline Keyset」。

印發GUSD

印發GUSD也是一個高風險的動作。需要同時結合線上審批的靈活性和線下審批的安全性。為此,項目方設計了一種混合的審批許可制,施加的對象是Impl,正是該智能合約控制著GUSD的發行。

該智能合約的監護體系中,既包含線上機制,又包含線下機制。為了實現這種特殊的設計,項目方將另一個叫做「PrintLimiter」的智能合約引入了Impl的審批鏈條中(但是,我在代碼中並沒有發現,白皮書上卻寫了,我估計,審批鏈條並沒有完全按照設計稿來)。

在上述結構中,PrintLimiter是線上的監護人,規定了Impl印發新幣的數量上限。而提升這個數量上限,則需要獲得離線keynet的許可。

合約安全性

GUSD體系設計了以下安全措施:

  1. 離線keys:當涉及到高風險操作時,我們需要該Keys來完成授權,其被存儲於Gemini專有的冷存儲器中;
  2. Key生成器:Keys are generated, stored, and managed onboard hardware security modules(HSMs,注1). We only use HSMs, each a 「signer,」 that have achieved a rating of FIPS PUB 140-2 Level 3 or higher(注2);
  3. 多重簽名:高風險操作,需要至少兩名簽字人提供數字簽名;
  4. 時間鎖:即便是獲得了授權,這些動作在執行前還是要等待一段時間,以提供一些反應時間;
  5. 撤銷許可權:待執行的動作可以被撤回,允許人們發現並撤銷某些惡意操作;

注1:硬體安全模塊(英語:hardware security module,縮寫HSM)是一種用於保護和管理強認證系統所使用的密鑰,並同時提供相關密碼學操作的計算機硬體設備。硬體安全模塊一般通過擴展卡或外部設備的形式直接連接到電腦或網路伺服器。

注2:National Institute for Standards and Technology, 「Digital Signature Standard (DSS),」 In Federal Information Processing Standards Publication 186-4, csrc.nist.gov/publicati, July 2013.

代碼解讀

我是個做產品,但是為了搞清楚這裡面的關節,炸著膽子學起了Solidity(以太坊的智能合約語言)。我猜,以後做產品的,多少都要懂點這個語言。所以,放棄抵抗吧。

我已經將注釋儘可能的的簡化,並翻譯成中文,如果有問題,歡迎討論。微信lixiaoqing0709。而且,很遺憾的是,知乎還有字數限制,所以,我只能將關鍵代碼ERC20Impl.sol貼上來。有需要的,直接聯繫我要吧。

/** @title 兼容ERC20標準的代幣合約,從架構上來講,本合約是一個居間的媒介合約,
* 一邊連接著實現了ERC20標準介面的ERC20Proxy合約,另一邊連接著存儲著代幣賬本的ERC20Store合約。
* ERC20Proxy將具體執行動作的授權給了本合約來實現,而ERC20Strore則接收本合約的信息,記錄動作執行
* 所引發的賬本變化;
*
* @dev 本合約除了執行ERC20標準的規定動作之外,還做了一些擴展,列示如下:
* 1. 更改代表的總供應量;
* 2. 批量轉賬;
* 3. 更改授信額度;(a賬戶可以將一定代幣授信給b賬戶,供其支配)
* 4. 行使各賬戶的轉賬委託,控制轉賬行為(sweeping)
*
* @author Gemini Trust Company, LLC
*/
contract ERC20Impl is CustodianUpgradeable {

// 記錄「增加代幣供應(即印錢)」申請的自定義結構體.
struct PendingPrint {
address receiver; // 申請人,也是新增代幣的接收人;用戶將Dollar存入Gemini,自然會要求獲得對應額度的GUSD
uint256 value; // 申請印發的代幣數量
}
// 成員變數
ERC20Proxy public erc20Proxy; // 關聯實現了ERC20標準的介面界面
ERC20Store public erc20Store; // 關聯本代幣的賬簿
address public sweeper; // 被委託執行sweeping動作的唯一被委託人
bytes32 public sweepMsg; // 當用戶將轉賬能力授權給sweeper時,需要使用ECDSA方法對一段消息進行簽名,這段消息就是sweepMsg
mapping (address => bool) public sweptSet; // 記錄address的sweeping授權情況;如果授權了,則返回True;
mapping (bytes32 => PendingPrint) public pendingPrintMap; // 記錄「印錢」的申請歷史,每個申請對應一個lock id
/// @dev 構造函數,初始化本合約;custodian是合約監護人;sweeper是執行sweeping的執行人
function ERC20Impl(address _erc20Proxy,address _erc20Store,address _custodian,address _sweeper)
CustodianUpgradeable(_custodian)
public
{
require(_sweeper != 0);
erc20Proxy = ERC20Proxy(_erc20Proxy);
erc20Store = ERC20Store(_erc20Store); // 即對外結構proxy和數據存儲,放在不同的合約中
sweeper = _sweeper;
sweepMsg = keccak256(address(this), "sweep"); //將授權需要簽署的消息,初始化
}
modifier onlyProxy {
require(msg.sender == address(erc20Proxy)); // 保證只接受從proxy界面傳遞過來的動作指令
_;
}
modifier onlySweeper {
require(msg.sender == sweeper); // 保證只有sweeping的指定執行人才能調用
_;
}

// @notice ERC20標準中規定的`approve`(授信)函數;只有erc20proxy合約能夠調用他
function approveWithSender(address _sender,address _spender,uint256 _value)
public
onlyProxy
returns (bool success)
{
require(_spender != address(0));
erc20Store.setAllowance(_sender, _spender, _value); // 指示後台store賬簿,記錄授信
erc20Proxy.emitApproval(_sender, _spender, _value); // 告知前台proxy界面,授信成功
return true;
}
/// @notice 增加授信的核心代碼;只有前台proxy能夠調用次函數;
function increaseApprovalWithSender(address _sender,address _spender,uint256 _addedValue)
public
onlyProxy
returns (bool success)
{
require(_spender != address(0));
uint256 currentAllowance = erc20Store.allowed(_sender, _spender); // 從後台Store那裡獲取當前的授信額度
uint256 newAllowance = currentAllowance + _addedValue; // 獲取增加授信後,授信的總額度
require(newAllowance >= currentAllowance); // 防止溢出
erc20Store.setAllowance(_sender, _spender, newAllowance); // 指示後台Store賬簿,更新授信數量
erc20Proxy.emitApproval(_sender, _spender, newAllowance); // 告知其他proxy界面,增加授信成功
return true;
}
/// @notice 降低授信額度的核心代碼;只有前台Proxy能夠調用此函數
function decreaseApprovalWithSender(address _sender,address _spender,uint256 _subtractedValue)
public
onlyProxy
returns (bool success)
{
require(_spender != address(0));
uint256 currentAllowance = erc20Store.allowed(_sender, _spender);
uint256 newAllowance = currentAllowance - _subtractedValue;
require(newAllowance <= currentAllowance);
erc20Store.setAllowance(_sender, _spender, newAllowance);
erc20Proxy.emitApproval(_sender, _spender, newAllowance);
return true;
}
/** @notice 申請印發新幣,並將新幣存入指定賬號;每次申請,都對對應一個新生成的lock id
*任何人都可以申請,但是最終確認需要調用confirmPrint來確認
*/
function requestPrint(address _receiver, uint256 _value) public returns (bytes32 lockId) {
require(_receiver != address(0));
lockId = generateLockId();
pendingPrintMap[lockId] = PendingPrint({
receiver: _receiver,
value: _value
});
emit PrintingLocked(lockId, _receiver, _value);
}
/// @notice 確認某個「印發新幣」的申請;只有監護人Custodian才能執行此動作
function confirmPrint(bytes32 _lockId) public onlyCustodian {
PendingPrint storage print = pendingPrintMap[_lockId];
address receiver = print.receiver;
require (receiver != address(0));
uint256 value = print.value;
delete pendingPrintMap[_lockId]; // 既然都已經確認了,就不算是申請了;從申請表中移除
uint256 supply = erc20Store.totalSupply(); // 從後台Store那裡獲取當前的代幣供應量
uint256 newSupply = supply + value; // 印發新幣後,總供應量是多少
if (newSupply >= supply) {
erc20Store.setTotalSupply(newSupply); // 指示後台Store賬簿,更新總發行量
erc20Store.addBalance(receiver, value); // 指示後台Store賬簿,將印發的新幣,存儲指定賬戶
emit PrintingConfirmed(_lockId, receiver, value); // 激發事件,通告印發成功
erc20Proxy.emitTransfer(address(0), receiver, value); // 告知proxy前台,印發並轉賬成功
}
}
/// @notice 將sender賬戶裡面的代幣,銷毀
function burn(uint256 _value) public returns (bool success) {
uint256 balanceOfSender = erc20Store.balances(msg.sender); // 從後台store那裡獲取sender賬戶的有餘額
require(_value <= balanceOfSender); // 不能超額銷毀
erc20Store.setBalance(msg.sender, balanceOfSender - _value); // 指示後台Store執行銷毀
erc20Store.setTotalSupply(erc20Store.totalSupply() - _value); // 既然銷毀了,那麼總發行量也要更新
erc20Proxy.emitTransfer(msg.sender, address(0), _value); // 通知proxy前台,銷毀完成
return true;
}
/** @notice 允許函數調用人(sender)將自己賬戶裡面的錢,一次性的向一批賬戶,完成轉賬;
* 毫無疑問,這樣省gas費
*/
function batchTransfer(address[] _tos, uint256[] _values) public returns (bool success) {
require(_tos.length == _values.length);
uint256 numTransfers = _tos.length;
uint256 senderBalance = erc20Store.balances(msg.sender); // 獲取sender的代幣餘額
for (uint256 i = 0; i < numTransfers; i++) {
address to = _tos[i];
require(to != address(0));
uint256 v = _values[i];
require(senderBalance >= v);
if (msg.sender != to) {
senderBalance -= v;
erc20Store.addBalance(to, v);
}
erc20Proxy.emitTransfer(msg.sender, to, v);
}
erc20Store.setBalance(msg.sender, senderBalance); // 完成後,告知後台store,更新sender的有餘額
return true;
}
/** @notice 將一批賬號的轉賬許可權,委託給sweeper賬號;當sweeper賬號獲得這個授權後,
* 他就可以將這些賬號中的餘額轉給任意賬號;
*
* @dev 單個賬號進行委託授權時,需要調用ECDSA方法簽署sweepMsg;傳遞給本本方法後,
* sweeper需要relay簽名,獲取用戶地址,從而獲得委託授權;
*
* @param _vs v 是ECDSA簽名中的產生的「V」元素 - recovery byte components
* @param _rs r 是ECDSA簽名中產生的「R」元素
* @param _ss s 是ECDSA簽名中產生的「S」元素
* @param _to 承接餘額的目的地賬戶地址
*/
function enableSweep(uint8[] _vs, bytes32[] _rs, bytes32[] _ss, address _to) public onlySweeper {
require(_to != address(0));
require((_vs.length == _rs.length) && (_vs.length == _ss.length));
uint256 numSignatures = _vs.length;
uint256 sweptBalance = 0;
for (uint256 i=0; i<numSignatures; ++i) {
// 用戶地址用ECDSA方法對數據進行簽名,得到v-r-s三組字元;
// 收到簽名的人,可以用ecrecover(消息哈希值,v,r,s)獲取當初簽名的用戶地址
address from = ecrecover(sweepMsg, _vs[i], _rs[i], _ss[i]);
if (from != address(0)) {
sweptSet[from] = true; // 將授權記錄在案;一朝授權,無須反覆授權,sweeper可以反覆轉賬
uint256 fromBalance = erc20Store.balances(from); // 從後台Store那裡獲取賬戶餘額
if (fromBalance > 0) {
sweptBalance += fromBalance;
erc20Store.setBalance(from, 0); // 將代幣從委託賬戶里轉出
erc20Proxy.emitTransfer(from, _to, fromBalance);
}
}
}
if (sweptBalance > 0) {
erc20Store.addBalance(_to, sweptBalance); // 將餘額全部轉入目的地賬戶
}
}
/** @notice 對於已經授權過的賬戶,sweeper可以反覆將手伸進來,把錢拿走;
* 因為授權信息已經在第一次就記錄到sweptSet[]裡面了,所以無須再次授權
*/
function replaySweep(address[] _froms, address _to) public onlySweeper {
require(_to != address(0));
uint256 lenFroms = _froms.length;
uint256 sweptBalance = 0;
for (uint256 i=0; i<lenFroms; ++i) {
address from = _froms[i];
if (sweptSet[from]) {
uint256 fromBalance = erc20Store.balances(from);
if (fromBalance > 0) {
sweptBalance += fromBalance;
erc20Store.setBalance(from, 0);
erc20Proxy.emitTransfer(from, _to, fromBalance);
}
}
}
if (sweptBalance > 0) {
erc20Store.addBalance(_to, sweptBalance);
}
}
/// @notice 類似ERC20標註介面中的TransferFrom;屬於擴展功能
function transferFromWithSender(address _sender,address _from,address _to,uint256 _value)
public
onlyProxy
returns (bool success)
{
require(_to != address(0));
uint256 balanceOfFrom = erc20Store.balances(_from);
require(_value <= balanceOfFrom);
uint256 senderAllowance = erc20Store.allowed(_from, _sender);
require(_value <= senderAllowance);
erc20Store.setBalance(_from, balanceOfFrom - _value);
erc20Store.addBalance(_to, _value);
erc20Store.setAllowance(_from, _sender, senderAllowance - _value);
erc20Proxy.emitTransfer(_from, _to, _value);
return true;
}
/// @notice 類似ERC20標註介面中的Transfer; 屬於擴展功能
function transferWithSender(address _sender,address _to,uint256 _value)
public
onlyProxy
returns (bool success)
{
require(_to != address(0));
uint256 balanceOfSender = erc20Store.balances(_sender);
require(_value <= balanceOfSender);
erc20Store.setBalance(_sender, balanceOfSender - _value);
erc20Store.addBalance(_to, _value);
erc20Proxy.emitTransfer(_sender, _to, _value);
return true;
}
/// @notice 實現ERC20標準介面中的totalSupply
function totalSupply() public view returns (uint256) {
return erc20Store.totalSupply();
}
/// @notice 實現ERC20標準介面中的balanceOf
function balanceOf(address _owner) public view returns (uint256 balance) {
return erc20Store.balances(_owner);
}
/// @notice 實現ERC20標準介面中的allowance
function allowance(address _owner, address _spender) public view returns (uint256 remaining) {
return erc20Store.allowed(_owner, _spender);
}
/// 印發新幣申請成功時,會激發此事件
event PrintingLocked(bytes32 _lockId, address _receiver, uint256 _value);
/// 印發新幣的申請被確認後,會激發此事件
event PrintingConfirmed(bytes32 _lockId, address _receiver, uint256 _value);
}

推薦閱讀:

相关文章