建立及使用加密資產
本文為此系列文章的第四部分:
- AZTEC 協定簡介
- 發佈 AZTEC 至 Ganache
- 證明、簽收、及檢視金鑰管理
- 建立及使用加密資產 (英文原文)
我們的 範例 dApp 實作了一個運行在以太坊上的加密借貸服務。它有以下功能:
- 借方能新增一個借款需求,其中款項將被加密。
- 貸方能發出檢視款項的請求。
- 借方同意貸方檢視權限後,貸方能支付款項,此款項也會被加密。合約將證明支付的加密款項等於需求的加密款項。
- 借方能支付利息至合約,利息及新餘額將被加密。
- 貸方能從合約領取已實現的利息。合約將證明領取的加密款項以及領取後的加密餘額都是正確的。
- 貸方能在借方未支付足夠利息或時限後未償還本金時將該合約標示為違約。合約將證明本身已達到違約的條件。
- 借方能支付所剩利息及償還本金。合約將證明支付的加密款項是正確的。
為打造以上功能,我們需要兩種加密資產及以下證明:Mint、Join Split、Bilateral Swap、Dividend、Private Range。
零知識借貸資產
由於貸款的實際金額需被加密,它必須以票據的形式儲存在鏈上。我們的零知識借貸資產合約 (以下簡稱借貸合約) 將繼承 EIP 1724 ZkAssetMintable.sol
合約,成為一個可加密的資產。
pragma solidity >= 0.5.0 <0.7.0;
import "@aztec/protocol/contracts/ERC1724/ZkAssetMintable.sol";
import "@aztec/protocol/contracts/libs/NoteUtils.sol";
import "@aztec/protocol/contracts/interfaces/IZkAsset.sol";contract Loan is ZkAssetMintable {
using NoteUtils for bytes; constructor(
address _aceAddress,
) public ZkAssetMintable(_aceAddress, address(0), 1, true, false)
{
}
}
一開始,借貸合約的登記所裡並沒有任何票據。於是必須透過 Mint 證明來新增等同借貸款項的票據。
第 1 步 — 建構 Mint 證明
使用 aztec.js
來新建證明:
const {
proofData,
} = aztec.proof.mint.encodeMintTransaction({
newTotalMinted: newTotalNote,
oldTotalMinted: oldTotalNote,
adjustedNotes: [loanNotionalNote],
senderAddress: loanDappContract.address,
});
第 2 步
以上證明將在借貸合約所屬的票據登記所裡新增一個票據。但只有中心的擁有人可以呼叫合約的 confidentialMint
方法。在我們的例子裡,擁有人是執行建構方法的合約 — LoanDapp。所以我們須從該合約裡執行此步驟:
Loan(loanId).confidentialMint(MINT_PROOF, bytes(_proofData));
零知識代幣資產
借貸服務中很多步驟都要雙方支付或提領特定金額,且所有值都需被加密。於是我們需建立另一個繼承 EIP 1724 的零知識資產合約來代表實際被交易的 ERC20 代幣 (DAI、CUSD等)。
此零知識代幣資產合約 (以下簡稱代幣合約) 的設定跟前面借貸合約不同,它必須宣告所對應的 ERC20 代幣合約位址且票據供給的總量是不能調整的。
pragma solidity >= 0.5.0 <0.7.0;import "@aztec/protocol/contracts/ERC1724/ZkAsset.sol";contract ZKERC20 is ZkAsset {constructor(
address _aceAddress,
address _erc20Address
) public ZkAsset(_aceAddress, address(_erc20Address), 1, false, true) {}
}
要在代幣合約裡新增一個有價值的票據只能透過 ERC20 轉帳至 ACE。票據的價值為轉帳金額乘上比例因數 (scaling factor)。透過此方式建立的票據將被 ACE 所擁有。
值得注意的是,透過觀察 ERC20 轉出的金額及同時產生的票據,在這裡產生的票據有可能被猜出實際價值。基於此,我們建議將一筆轉帳交易的結果拆解為多個票據來避免此風險。
如果要達到完全的保密,可建立一個沒連結公開 ERC20 代幣合約的資產合約,並交由可信的第三方來代為新增票據。例如在傳統銀行轉帳後由銀行建立為該使用者擁有的票據。 Carbon Money 正在開發此項服務。
為簡化範例,我們並不考慮此風險並使用公開的 ERC20 合約。
第 1 步
允許 ACE 合約代替使用者消費票據。
await settlementToken.approve(aceContract.address, value);
第 2 步 — 建構證明
const {
proofData,
expectedOutput
} = aztec.proof.joinSplit.encodeJoinSplitTransaction({
inputNotes: [],
outputNotes: [Note1, Note2], // 總和將等於 value
senderAddress: account.address,
inputNoteOwners: [],
publicOwner: account.address,
kPublic: -value,
validatorAddress: joinSplitContract.address,
});
這個 Join Split 證明的變化形不需輸入票據 inputNotes
,但需要被轉換的 ERC20 代幣的值 kPublic
。負值代表代幣被消耗而轉為票據 (如將票據兌換回代幣則為正值)。這個證明驗證輸出票據 outputNotes
的實際和將等於公開值 kPublic
。
這個證明也需要票據擁有人的位址 publicOwner
,以及發送這個證明至 ACE 的寄送者位址 senderAddress
。
第 3 步 — 允許 ACE 消費代幣
任何會造成資產轉移的證明都須先得到該資產擁有人的許可。如此一來 ACE 才得以轉換證明裡使用到的公開代幣,並在處理 ERC20 上多加一層保護機制。
await ACE.publicApprove(
zkAsset.address,
hashProof,
kPublic,
{
from: accounts[0],
},
);
第 4 步 — 代轉交易
當使用者呼叫借貸合約,合約代為執行 ACE 的 validateProof
時,該合約的位址必須被定義為證明裡寄送人的位址。如此將避免惡意的第三方擷取資訊來做其他用途。
(bytes memory _proofOutputs) = ACE.validateProof(
JOIN_SPLIT_PROOF,
address(this),
_proofData
);
第 5 步 — 處理轉換指示
成功被驗證的證明將會回傳一個陣列。dApp 可以用返回值裡的指示來改變票據狀態。
(bytes memory _proofOutputs) = ACE.validateProof(
JOIN_SPLIT_PROOF,
address(this),
_proof2
);settlementToken.confidentialTransferFrom(
JOIN_SPLIT_PROOF,
_proofOutputs.get(0)
);
支付款項
當建立了借貸合約跟代幣合約,並初始化各自的票據登記所後,貸款人就可以出借款項。下圖表示當下兩個合約的票據:
在這邊我們使用 Bilateral Swap 證明來交換資產。借方希望能收到等值於貸款項的代幣票據。貸方則期待收到代表 100% 貸款合約所有權的票據。在這個例子裡就是圖左邊的票據交換右邊等值的票據。擁有所有權票據就可以領取借方支付的利息及還款。未來如果貸方想要交易這個貸款,可將所有權票據拆成不同價值的多個票據並變更擁有人。
第 1 步 — 允許借貸合約消費票據
ACE 驗證證明後,將更新已得到擁有人許可變更的票據於登記所裡的狀態。在 Bilateral Swap 證明裡,如果一方沒允許合約變更票據狀態,交易將只完成一半,使得一方得不到所要求的票據。於是 dApp 開發者在將任何證明遞給 AZTEC 系統前都需要先檢查票據的權限設定是否有效。因為 ACE 只會驗證加密值套用於等式中是否正確,而不會檢查處置票據狀態的邏輯是否合理。在範例中,dApp 須檢查兩方提供的票據都已被授權給合約使用。
為了讓交易順利進行,借方和貸方需要允許借貸合約 loanAddress
分配他們的票據:
const settlementSignature = signNote(
zkSettlementAsset.address,
settlementNoteHash,
loanAddress,
lender.privateKey,
);await zkSettlementAsset.confidentialApprove(
settlementNoteHash,
loanAddress,
true,
settlementSignature,
{
from: lender.address,
},
);
第 2 步 — 建構證明
const {
proofData,
} = aztec.proof.bilateralSwap.encodeBilateralSwapTransaction({
inputNotes: [takerBid, takerAsk],
outputNotes: [makerAsk, makerBid],
senderAddress: loanId,
});
這個證明需使用四個票據,並符合以下陳述:
- takerBid 等於 makerAsk
- takerAsk 等於 makerBid
第 3 步 — 證明並更新狀態
如先前提到,代為傳送證明給 ACE 的合約位址必須被定義為證明的寄送人,以防止第三方擷取該證明來做其他用途。
一經驗證,證明的返回值即可用來更新票據狀態。以下步驟將在代幣合約的票據登記所裡銷毀 takerBid
並新增 makerAsk
,然後在借貸合約的票據登記所裡銷毀 makerBid
並新增 takerAsk
。
(bytes memory _proofOutputs) = ACE.validateProof(
JOIN_SPLIT_PROOF,
address(this),
_proofData
);
(bytes memory _loanProofOutputs) = _proofOutputs.get(0);
(bytes memory _settlementProofOutputs) = _proofOutputs.get(1);settlementZkAsset.confidentialTransferFrom(
BILATERAL_SWAP_PROOF,
_settlementProofOutputs
);loanZkAsset.confidentialTransferFrom(
BILATERAL_SWAP_PROOF,
_loanProofOutputs
);
就這樣!這個貸款已成立且所有款項都是加密的。
利息流
AZTEC 票據可以被智能合約擁有,使得打造複雜的金融服務變得可行。範例中,貸方可以在任一時間點提領已生效的利息。而當帳戶餘額不夠時,貸方則可將貸款標示為違約,智能合約將轉移抵押品給貸方。
為了讓借方可以不用在每次貸方提領利息時都出面,合約將代為驗證提領的利息金額是否有效,並用此證明結果的返回值將等值的票據轉給貸方。這個過程需要 Dividend 與 Join Split 兩個證明。Dividend 驗證票據的加密值是另一個票據加密值乘以一個比例後再加上一個餘數票據的值 Residual
(以處理無法被除盡的情況)。
Note1 * a = Note2 * b + Residual
讓 Note2
代表提領的金額,那麼建構這個證明的人將會希望所挑選的 a
和 b
將使得餘數接近零。於是可以用以下公式來表示 Note1
Note1 = Note2 * (b / a)
要套用以上公式,我們需要找到一個比例乘以另一個已知的票據來代表提領的利息票據 AccruedInterest
。在我們的範例中,我們使用貸款的所有權票據作為已知的票據。
於是可以整理出以下公式:
由於智能合約裡已經定義了 ElapsedTime
、InterestRate
和 InterestPeriod
。於是只有在 AccruedInterest
是有效的情況下才能滿足公式 (1)。
如果 Dividend 證明通過,我們可以信任累計利息票據的值是正確的。如果在 Join Split 證明裡使用這個票據,則可以從 CurrentInterestBalance
拆解出 AccruedInterest
和餘值。
貸方可以在任何時間點執行這個證明流程,提領出到當下的秒數所累積的利息。而區塊鏈也能正確驗證加密金額是否有效。
程序化管理 — 去仲介化
傳統過程中,當借方未定時付利息或是在期限後未償還款項,貸方須經由法律程序來徵收借方的抵押品。但在區塊鏈上,一旦合約證明了借方已違約,就可以將當初借方所登記的抵押品自動轉到貸方名下,不需律師、法庭等第三方的仲裁。
要驗證違約,需要先前提到的 Dividend 證明來驗證該時間點的利息,以及 Private Range 證明來驗證所欠的利息是否多於可提領的帳戶餘額。
最終成果
完整的原始碼可以從 github 專案 上取得。
感謝閱讀本系列文章!