owned this note
owned this note
Published
Linked with GitHub
# 合约设计文档大纲
# ***术语表***
| Terms | Description |
| :--------:|:-------------------|
|资产树|资产树是保存了全部DeGate账户和资产中重要数据的默克尔树。|
|资产私钥|资产私钥指的是资产公私钥对中的私钥。|
|资产公钥|资产公钥指的是资产公私钥对中的公钥。|
|区块提交|区块提交是Operator通过Postman向DeGate智能合约提交zkBlock的零知识证明的过程。|
|出块节点|出块节点负责完成DeGate链下交易的ZK-Rollup流程,包括交易排序、打包、出块、生产零知识证明、数据上链。|
|Calldata|Calldata是调用智能合约方法时填写到data字段的内容。DeGate链下账户和资产数据都通过calldata传给智能合约。|
|电路|电路是描述需要进行零知识证明事件的系统模块,以zkBlock作为输入,输出相应的零知识证明。|
|DeGate账户|DeGate账户用来在DeGate协议内管理资产、下单、充值、提现和转账。|
|充值|充值功能允许将代币或ETH从钱包地址转移到DeGate账户。|
|高级充值|用户调用DeGate智能合约的Deposit方法进行充值。|
|标准充值|用户直接发送资产到DeGate智能合约地址进行充值。|
|完整树|完整树是保存了全部DeGate账户、资产和订单数据的默克尔树。|
|逃离模式|逃亡模式允许用户不通过任何第三方,自主从DeGate账户取回资产。当逃亡模式被启动,DeGate智能合约将拒绝接收新的zkBlock数据, 只支持用户取回资产到钱包地址。|
|强制提现|强制提现是DeGate智能合约功能,允许用户发起提现请求,要求DeGate节点在规定的时间范围内执行请求。|
|矿工费|矿工费是在以太坊网络上执行操作所需的计算工作量成本。目前DeGate协议中可以使用 ETH、USDC 和 USDT 来支付矿工费。|
|链下交易|DeGate节点处理部分用户请求时,会产生链下交易。这些交易最终会以ZK-Rollup发到链上。|
|链上撤单|链上撤单旨在增强DeGate协议的无需信任。对于还在有效期内的已取消订单,可以发起链上取消的请求,使该订单不会再被执行。|
|Operator|Operator是出块节点的核心角色,负责交易排序、打包和出块,并调用其他模块完成ZK-Rollup流程。|
|Postman|Postman负责调用DeGate智能合约,将calldata传到智能合约,完成数据上链。|
|付费入账|付费入账是一种仅在用户用完免费充值额度(由DeGate协议补贴)时才会发生的动作,此时用户需要支付矿工费来完成充值请求。|
|关停模式|DeGate可以主动激活关停模式,将所有资产退回给用户。|
|手续费|每次成交时都会收取手续费,由Taker全额支付。 一个订单如果产生多次成交,会收取多笔手续费。 手续费是DeGate协议的主要收入来源。|
|转账|转账功能实现了2个DeGate账户之间的资产转账。|
|提现|提现功能允许将资产从用户的DeGate账户转移回钱包地址。|
|ZKP-Worker|ZKP-Worker根据Operator传入的数据,通过电路来生成零知识证明。|
# 文档目的
介绍合约的功能设计和具体实现。
# 合约功能概述
DeGate合约负责处理出块节点提交上链的zkBlock区块,并使用***zk-SNARKs***零知识证明来验证zkBlock数据的正确性。zkBlock验证通过后,合约会进行状态的变更。
普通用户通过充值/提现/代币注册等接口,和合约进行交互。
## zkBlock的处理和验证
Postman账户调用合约的submitBlocks接口提交zkBlock, 合约需要处理和验证如下信息
### 验证完整树与资产树
DeGate的系统中,维护了用户完整信息的完整树和资产信息的资产树。zkBlock的header保存了父区块和当前区块的两棵树的根。***合约通过验证父区块的根和合约中当前保存的根是一致的,从而可以更新到当前区块的根。***
资产树的另一个作用是保证系统的trustless,在逃离模式下, 用户可以提交默克尔证明来和资产树对比验证,直接从一层提现。
### 验证时间戳
zkBlock的header中包含了一个电路时间戳,这个时间戳作为电路的输入,并参与到电路中交易的验证(交易是否超时),电路无法验证这个时间戳的正确性。DeGate系统要求Operator将电路时间戳作为zkBlock区块的一部分,在合约中进行验证。
合约会对比zkBlock上链时一层的时间戳(TimestampL1)和zkBlock中的电路时间戳,进行校验,确保电路时间戳在有效范围内。
电路时间戳有效范围 (TIMESTAMP_HALF_WINDOW_SIZE_IN_SECONDS 配置为7days):
[TimestampL1 - TIMESTAMP_HALF_WINDOW_SIZE_IN_SECONDS, TimestampL1 + TIMESTAMP_HALF_WINDOW_SIZE_IN_SECONDS]
### 验证和处理部分交易
zkBlock包含所有的用户交易,其中一部分交易需要在合约中处理,比如充值(Depoist),提现(Withdraw)和账户更新(AccountUpdate)。这部分交易也被称为conditional交易。
这部分交易数据的正确性,由电路零知识证明和合约共同来确保。
合约会按照一定的顺序处理这些交易,具体参见实现章节。如果任何交易处理失败,则整个zkBlock提交失败。
### 零知识证明key
零知识证明key,包括证明key(proving key)和验证key(verifying key)。这两种key,分别用来生成proof和验证proof。DeGate是如何生成零知识证明key的,参考Trusted-Setup的说明文档。
在出块节点侧,使用电路程序和证明key来生成zkBlock对应的proof。
在合约侧,合约根据zkBlock的size信息,选择对应的验证key来验证proof的正确性。
不同的zkBlock的size,会对应不同的零知识证明key。DeGate合约在部署时,会配置一组不同size对应的验证key。size的选择,参考 电路设计文档-zkBlock大小
验证key通过硬编码的方式注册到合约中。
每一对零知识证明key(证明key/验证key)唯一对应一个电路逻辑,更换key即更换电路逻辑。为了确保系统完全地trustless,代码逻辑不允许更改,所以不允许更新和添加验证key。
### 验证零知识证明
zkBlock包含零知识证明,合约使用硬编码的验证key,来验证证明的正确性。
证明proof由出块节点生成,并作为zkBlock的一部分提交上链。
DeGate采用***zk-SNARKs***协议的Groth16算法来实现零知识证明,Groth16采用ALT_BN128曲线。 以太坊在EIP196和EIP197中添加了这条曲线上计算的预编译指令, ecAdd/ecMul/ecPairing, 可以便宜高效的执行验证。
验证的输入参数,包括验证key、证明proof和PublicInputDataHash, 其中PublicInputDataHash是zkBlock数据的哈希。验证过程中, 根据输入的参数,构建一组pair。执行配对检查函数ecPairing,确认pair正确。
在DeGate合约的实现中,使用了***ethsnarks***的Verifier合约,进行零知识证明验证。
## 充值功能
用户可以使用如下方式进行代币充值。
### 标准充值
支持白名单币种。
通过直接转账代币到DeGate合约的方式进行充值,也被称为标准充值。相比合约接口充值,用户不需要发送授权交易。
合约侧为了验证这类充值,合约中未确认所有权的代币余额需要大于充值金额。出块节点的Operator账户有权使用这部分余额,充值给任意账户。在合约中,这部分余额可以通过getUnconfirmedBalance接口查询。
### 高级充值
支持任意币种。
通过调用DeGate合约的deposit接口,进行充值。如果存入ERC20代币,用户需要先发送交易到ERC20代币合约,授权一定额度给DeGate合约。
出块节点需要在 MAX_AGE_DEPOSIT_UNTIL_WITHDRAWABLE(15 days)内, 处理用户的充值。否则,用户可以调用withdrawFromDepositRequest 直接从合约中提取充值金额。
### 充值手续费
正常情况下,充值均免费。如果短时间内充值交易的笔数过多,则对充值进行收费。
免费可用的充值交易笔数,可以通过getFreeDepositRemained 接口查询。(默认配置,免费可用5000笔,上限5000笔,同时每个区块恢复2笔)
对于收费的合约充值,用户需要在发起调用deposit的交易里转入一定的ETH(默认配置0.01ETH),才会被处理,否则直接拒绝。
### ***用户充值流程***
用户链上发起一笔充值交易,交易入块并通过区块确认后,DeGate节点会把充值数量计入账户可用余额,用户就能立即使用这笔资产。同时,Operator会发起一笔确认充值的链下交易,经过打包、出块、生成证明流程、更新默克尔树资产一系列操作后,最终rollup到链上,完成充值。
充值流程和方法对比,详情参考DeGate产品功能文档 https://docs.degate.com/v/product_zh/main-features/deposit
## 提现功能
用户可以通过如下方式进行提现。
### 普通提现
用户通过发送签名到DeGate服务端的方式,进行普通提现,而不需要调用合约接口。
这部分提现,在出块节点的Postman账户提交区块上链时,进行处理。
### 强制提现
强制提现支持用户强制出块节点允许其提取对应账户下的余额,调用forceWithdraw合约接口。
出块节点必须在规定时间MAX_AGE_FORCED_REQUEST_UNTIL_WITHDRAW_MODE(15 days)内上链处理。否则一旦超时,任何人可以调用合约notifyForcedRequestTooOld接口,使得DeGate进入逃离模式。
强制提现需要用户支付一定的ETH手续费(默认配置0.01ETH,最大0.25ETH)。
强制提现要求用户提供账户地址,代币地址,和账户对应的AccountID。数据由用户提供,因此存在输入参数不匹配的可能。由于合约并不记录用户的账户与余额等信息,需要由出块节点的Postman账户将数据和证明提交到链上,并由合约删除错误的强制提现记录。
为了防止恶意用户发送过多强制提现交易攻击DeGate系统, 合约中设置了最大可用笔数MAX_OPEN_FORCED_REQUESTS(1000000笔), 定义了排队处理中的最大强制提现笔数。
***如果强制提现交易过多,DeGate系统将来不及处理,被迫进入逃离模式。1000000笔的设定,是评估DeGate处理能力后的合理设置。
针对恶意用户消耗过多笔数,导致普通用户无法使用的情况,DeGate系统会根据实际情况,调整合理的强制提现费用来增加攻击者成本,使得攻击在经济上不可行。***
### 逃离模式提现
进入逃离模式后,DeGate交易所的基本功能都会停止,出块节点也不能再提交区块。
用户只能够基于最后一个区块的状态提取资金。需要调用合约的`withdrawFromMerkleTree`接口,提交默克尔证明进行提现,一次只能提取一个币种的所有余额。
默克尔证明包含了用户某个币种的资产树的枝杈树信息,合约通过比较计算出的资产树和最新状态的资产树,来验证交易正确。
### ***用户提现流程***
用户可随时发出提现链下请求,从DeGate合约中取回资产。用户签名发起提现请求,节点验证签名通过后,先从该资产的可用余额中锁定需要提现的部分数量,然后交给Operator进行rollup操作。当包含了提现请求的rollup交易入块后,用户会收到提现的资产。假如节点拒接处理提现申请,用户可通过强制提现和逃离模式来取回资产。
提现流程和方法对比,详情参考DeGate产品功能文档 https://docs.degate.com/v/product_zh/main-features/withdrawal
## 代币注册
在DeGate中使用代币之前,需要进行注册。任何人都可以调用registerToken接口,进行代币注册。
合约会确保代币只能注册一次,ETH和DG代币在合约部署时注册。
在代币充值时,如果代币没有注册,合约会进行自动注册。
ID为0到31的代币由DeGate交易所预留,只能由交易所admin进行注册。这些币种称之为BindToken,聚合交易时费用更低。
## ***协议费用***
使用DeGate时,根据不同场景的操作,用户可能需要支付不同的费用。
操作请求与费用的说明,参考DeGate产品功能文档 https://docs.degate.com/v/product_zh/concepts/protocol-fees
# 合约的实现
## 合约实现概述
合约的实现包括如下几个模块
* Exchange合约,处理zkBlock和充值提现请求等。
* Deposit合约,托管用户资产,提供代币的转入转出功能。
* Loopring合约,Exchange合约参数配置。
* BlockVerifier合约,电路零知识证明key注册和proof验证功能。
## Exchange合约
### zkBlock数据定义
出块节点Postman账户通过调用 submitBlocks 接口提交zkBlock上链。
```
function submitBlocks(Block[] zkBlocks)
```
#### 1. zkBlock的数据定义如下
```
struct Block
{
uint8 blockType;
uint16 blockSize;
uint8 blockVersion;
bytes data;
uint256[8] proof;
bool storeBlockInfoOnchain; // not used
bytes auxiliaryData;
bytes offchainData; // not used
}
```
* blockType,zkBlock类型,目前恒定为0,仅支持一种类型。
* blockSize,zkBlock包含的交易数量,数量范围是 [320, 170, 85, 42, 20, 10, 5]
* blockVersion,zkBlock的版本号,默认为0。
* data, 是zkBlock的具体数据,也被称为PublicInputData,data执行sha256哈希后生成的PublicInputDataHash作为input输入给电路,来生成证明proof。data由BlockHeader和交易的PublicInputData数据两部分组成。
* proof, zkBlock的零知识证明proof。
* auxiliaryData, 合约需要处理的交易的另一部分数据。这部分数据不会作为input输入给电路。每个conditional交易会有一个对应的AuxiliaryData,具体定义参考各交易的Auxdata数据部分。
#### 2. zkBlock的data中的BlockHeader定义
BlockHeader序列化为167 bytes,定义如下
```
struct BlockHeader
{
address exchange;
bytes32 merkleRootBefore;
bytes32 merkleRootAfter;
bytes32 merkleAssetRootBefore;
bytes32 merkleAssetRootAfter;
uint32 timestamp;
uint8 protocolFeeBips;
uint32 numConditionalTransactions;
uint32 operatorAccountID;
uint16 depositSize;
uint16 accountUpdateSize;
uint16 withdrawSize;
}
```
* exchange, exchange合约地址。
* merkleRootBefore, zkBlock出块前,完整树根哈希
* merkleRootafter, zkBlock出块后,完整树根哈希
* merkleAssetRootBefore, zkBlock出块前,资产树根哈希
* merkleAssetRootAfter, zkBlock出块后,资产树根哈希
* timestamp, 电路输入的时间戳
* protocolFeeBips, 现货交易最大手续费,所有现货交易的手续费收费比例均不能大于此费率. 费率的单位为万分之一,比如18的话,就是万分之18.
* numConditionalTransactions, 合约需要处理的交易数量, 即 zkBlock中充值交易的数量 + zkBlock中账户更新交易的数量 + zkBlock中提现交易的数量。
* operatorAccountID, 出块账号的AccountID
* depositSize, zkBlock中充值交易的数量
* accountUpdateSize, zkBlock中账户更新交易的数量
* withdrawSize, zkBlock中提现交易的数量
#### 3. zkBlock的data中的PublicInputData定义
交易的PublicInputData数据, 是合约需要处理的交易(Conditional交易)的序列化汇总。需要按照约定的数量和顺序,进行解析处理。
* BlockHeader中的depositSize/accountUpdateSize/withdrawSize 分别定义了这3种交易的数量
* 交易按照类型排列,Deposit/AccountUpdate排在最前面,Withdraw排在最后。按照顺序和交易的数量信息,合约可以推导出交易的类型。
[Deposit transactions][AccountUpdate transactions][…others…][Withdraw transactions]
* 每个交易的PublicInputData为83 bytes,具体定义参考各交易的PublicInputData部分。
### Conditional交易说明
需要在合约中处理的交易,比如充值(Depoist),提现(Withdraw)和账户更新(AccountUpdate)。这部分交易被称为conditional交易。
每个交易的数据,包括PublicInputData和Auxdata两部分。
#### PublicInputData数据
每个交易的PublicInputData数据固定为83 bytes,不足部分用0 bytes填充。
为了节省gas,PublicInputData切分为80 bytes(part1)和3 bytes(part2)两部分。在Block的data中,所有交易的part1部分聚合之后放到前面,part2部分聚合之后放到后面。
因此,读取交易的PublicInputData数据,需要分别读取part1和part2,再拼接到一起,合计83 bytes。
#### AuxData数据
每个交易的Auxdata数据为AuxiliaryData对象。所有交易的Auxdata数据存储到AuxiliaryData[] 数组中,序列化为bytes后,存到Block中的auxiliaryData。
```
struct AuxiliaryData
{
bytes data;
}
```
读取交易的Auxdata数据,即按照顺序从数组中读取。
Deposit交易不需要AuxData,为了统一处理,Deposit交易的AuxData的data字段为空数据"0x"。
合约中,会验证AuxiliaryData[] 数组长度和Conditional交易数量一致。
### Deposit交易定义和处理
Deposit交易用来处理用户的充值请求。
Deposit交易的PublicInputData定义
```
- Type: 1 byte (0 for Advanced Deposit, 1 for Standard Deposit)
- Owner: 20 bytes
- Account ID: 4 bytes
- Token ID: 4 bytes
- Amount: 31 bytes
```
* Type,***高级充值 Type =0,标准充值 Type =1***
* Owner,用户的一层账户地址
* Account ID,用户账户在DeGate系统中的账户ID
* Token ID,充值代币在DeGate系统中的代币ID
* Amount 充值数量, uint248类型。
合约根据交易的PublicInputData定义decode出交易的具体字段,并根据Type 分别处理合约充值和转账充值。
合约充值处理:
1. 未处理的充值请求pendingDeposit 存在
2. 未处理的amount数量大于或等于充值的amount数量。
3. 如果充值请求没有扣除所有的pending余额,则pendingDeposit 需要更新余额并保留。否则的话,则会删除 pendingDeposit请求。
4. 未处理的充值的数量, 可以通过 getPendingDepositAmount 接口查询。
转账充值处理:
1. 未确认所有权的代币余额unconfirmedBalance 需要大于充值的amount数量。
2. 直接转账充值,合约中无法记录发起充值的用户,因此合约中只需确认数量合法。
3. 未确认所有权的代币余额可以通过getUnconfirmedBalance接口查询。
### AccountUpdate交易定义和处理
AccountUpdate允许用户添加更新资产公钥。
AccountUpdate交易的PublicInputData定义
```
- Type: 1 byte (type = 1 for a conditional transaction)
- Account owner: 20 bytes
- Account ID: 4 bytes (for contract verification)
- Fee token ID: 4 bytes
- Fee amount: 2 bytes (16 bits, 11 bits for the mantissa part and 5 for the exponent part)
- Public key: 32 bytes
- Nonce: 4 bytes
- Account ID: 4 bytes (appointed accountID by DeGate, used for crawling blocks to restore the Asset Merkle Tree)
```
* Type, 只支持ECDSA签名类型的AccountUpdate交易,Type字段必须为1。
* Account owner,用户的一层账户地址
* Account ID, 用户账户在DeGate系统中的账户ID
* Fee token ID, Fee代币在DeGate系统中的代币ID
* Fee amount,Fee数量
* Public key,资产公钥,EdDSA格式
* Nonce, 防重放字段,nonce每次使用后递增。
* Account ID,预分配的账户ID,可以用来恢复资产树。
Auxdata定义
```
struct AccountUpdateAuxiliaryData
{
bytes signature;
uint96 maxFee;
uint32 validUntil;
}
```
* signature 是用户的ECDSA签名, 签名使用EIP712方式。签名由用户使用一层账户的私钥签署。
签名内容:
```
AccountUpdate {
owner (address)
accountID (uint32)
feeTokenID (uint32)
maxFee (uint96)
publicKeyX (uint256)
validUntil (uint32)
nonce (uint32)
}
```
* maxFee,最大支付的fee,用户签名的一部分。
* validUntil,交易的有效期,用户签名的一部分。
合约根据交易的PublicInputData定义和AuxData定义,decode出交易的具体字段。
合约处理流程:
1. 验证交易的有效期validUntil,电路时间戳需要小于validUntil。
2. 实际支付fee必须小于等于maxFee。
3. 验证用户签名signature正确。
### Withdraw交易定义和处理
Withdraw交易允许用户将资产从L2提现到一层。
Withdraw交易的PublicInputData定义
```
- Type: 1 bytes (type > 0 for conditional withdrawals, type == 2 for a valid forced withdrawal, type == 3 when invalid)
- Owner: 20 bytes
- Account ID: 4 bytes
- Token ID: 4 bytes
- Fee token ID: 4 bytes
- Fee amount: 2 bytes (16 bits, 11 bits for the mantissa part and 5 for the exponent part)
- StorageID: 4 bytes
- OnchainDataHash: 20 bytes
```
* Type, withdraw包括4种type类型,合约负责处理1/2/3类型,具体在处理中描述。
* Owner,用户指定的一层收款地址
* Account ID, 发起提现的用户的DeGate账户ID
* Token ID,提现代币在DeGate系统中的代币ID
* Fee token ID, Fee代币在DeGate系统中的代币ID
* Fee amount,Fee数量
* StorageID,做为签名一部分防重放,并且记录提现已经完成
* OnchainDataHash,是minGas,to和amount3个字段的hash。 由于83bytes的限制,所以这3个字段没有放到publicInput中(而是以hash的方式),最终通过auxData的方式去传递给合约。合约端要去验证这3个字段的hash与OnchainDataHash相等。
Auxdata定义
```
struct WithdrawalAuxiliaryData
{
bool storeRecipient;
uint gasLimit;
bytes signature;
uint248 minGas;
address to;
uint96 maxFee;
uint32 validUntil;
uint248 amount;
}
```
* storeRecipient, 链上保存提现记录。
* gasLimit,withdraw提币支付的gas上限。
* signature, 用户的ECDSA签名。
* minGas,withdraw提币支付的gas最低值。
* to, withdraw收币地址
* maxFee,最大支付的fee,用户签名的一部分。实际支付fee必须小于等于maxFee。
* validUntil,交易的有效期,用户签名的一部分。
* amount,withdraw提币数量。
合约根据交易的PublicInputData定义和AuxData定义,decode出交易的具体字段。
合约处理流程:
1. 验证onchainDataHash正确.
```
bytes20 onchainDataHash = bytes20(
abi.encodePacked(
minGas,
to,
amount
).fastSHA256()
);
```
2. 验证withdraw交易类型
- type=0:EdDSA签名模式。合约不验证。
- type=1:ECDSA签名模式。合约验证:
- 验证交易的有效期validUntil,电路时间戳需要小于validUntil。
- fee 小于或者等于 maxFee
- 验证用户签名signature正确
- type=2:有效的强制提现。合约验证:
- withdraw的from和to必须相等。***from是发起强制提现交易到链上的地址,to是Auxdata中的to地址。强制提现交易只能提现给from地址,因此from和to地址需要一致。***
- fee 必须是0
- forcedWithdrawal请求存在,并且请求的owner和from地址一致
- forcedWithdrawal请求不存在,必须是关停模式,operator替用户强制提现
- type=3:无效的强制提现,***无效是指用户输入了错误的账户ID进行强制提现***。
- withdraw的from和to必须相等。***from是发起强制提现交易到链上的地址,to是Auxdata中的to地址。强制提现交易只能提现给from地址,因此from和to地址需要一致。***
- fee 必须是0
- forcedWithdrawal的请求存在,并且请求的owner和from地址不一致
- amount必须是0。不处理提币,只清理合约中的forcedWithdrawal无效请求
3. 处理提币
- 合约验证auxData中gasLimit需要大于或者等于minGas。
- 调用deposit合约的提币接口,尝试向用户地址转币。
- 允许提币接口执行失败,而withdraw交易成功,不影响submitBlocks的交易上链。如果提币失败的话,则将提币请求记录到amountWithdrawable表中。用户可以调用withdrawFromApprovedWithdrawals直接从一层提币。
withdrawFromApprovedWithdrawals提币接口,允许用户手动提币,处理withdraw交易中提币失败的情况。
```
function withdrawFromApprovedWithdrawals(address[] owners, address[] tokens)
```
合约处理流程:
1. 任何人可以调用,并传递一组用户地址owners 和一组代币地址tokens做为输入参数
2. 合约按照参数,依次处理,检查amountWithdrawable表中存在owner和token的提币记录
3. 根据amountWithdrawable表中记录的余额,向用户地址转币
4. 删除amountWithdrawable表中的提币记录
### 交易最大手续费的校验和更新
不同的交易对,存在不同的交易手续费,而电路并不约束手续费的具体数值,这部分校验由合约执行。
在出块节点Postman账户提交zkBlock时,会验证和更新交易最大手续费。
- 校验机制
每个提交的zkBlock的BlockHeader中,protocolFeeBips就是现货交易最大手续费。电路中,会约束该zkBlock中的所有现货交易的手续费均不大于此费率。
合约中,会比较BlockHeader中的protocolFeeBips和合约中设置的费率一致,否则上链失败。
- 延时更新
合约中设置的费率采用延时更新的机制进行修改,延时为MIN_AGE_PROTOCOL_FEES_UNTIL_UPDATED(7天)。
延时更新, 需要admin调用Loopring合约updateProtocolFeeSettings接口设置需要改的数值。
延时期限过后,在提交zkBlock时会调用validateAndSyncProtocolFees进行更新。
### zkBlock上链接口实现
Postman通过调用 submitBlocks 接口提交zkBlock上链。
```
function submitBlocks(Block[] zkBlocks)
```
合约处理流程:
1. 验证zkBlock中完整树与资产树root正确
2. 验证zkBlock中电路时间戳合法
3. 处理Conditional交易
4. 交易最大手续费的校验和更新
5. 调用BlockVerifier合约的verifyProofs接口,验证零知识证明
### 合约充值接口实现
合约充值接口,允许用户发起合约充值请求
```
function deposit(address from,address to,address tokenAddress,uint248 amount, bytes extraData)
```
1. 检查token地址,如果没有注册,调用registerToken进行注册。
2. 检查是否需要对充值进行收费。 如果需要,从转入的eth中扣除手续费depositFeeETH。如果eth不足,则充值交易失败。
3. 调用Deposit合约的deposit接口执行充值转币流程
4. 记录用户的充值请求到 pendingDeposits列表, 用户的相同币种的多笔充值会合并到一条充值请求记录。
用户的充值,需要等待DeGate构建Deposit交易并提交上链后,才能够完成。
如果Operator一直没有处理,则用户可以调用withdrawFromDepositRequest 直接从合约中提取充值金额。
withdrawFromDepositRequest提币接口
```
function withdrawFromDepositRequest(address owner, address token)
```
合约处理流程:
1. 任何人可以调用,传递用户地址owner和代币地址token作为参数
2. 检查pendingDeposits列表中存在充值请求记录
3. 检查充值请求超时没有处理(大于 MAX_AGE_DEPOSIT_UNTIL_WITHDRAWABLE), 或者进入了逃离模式
4. 根据pendingDeposits表中记录的余额,向用户地址转币
5. 删除充值请求记录
### 强制提现接口实现
强制提现接口,允许用户发起强制提现请求
```
function forceWithdraw(address from, address token, uint32 accountID)
```
1. 检查强制提现存在可用的笔数
2. 检查转入的eth足够支付强制提现手续费
3. 记录用户的强制提现请求到pendingForcedWithdrawals列表
用户的强制提现,需要等待DeGate构建Withdraw交易并提交上链后,才能够完成。
如果Operator一直没有处理,则用户可以触发交易所进入逃离模式。
### 逃离模式实现
#### 逃离模式的触发接口
```
function notifyForcedRequestTooOld(uint32 accountID, address token)
```
1. 检查accountID和token对应的强制提现请求存在
2. 检查该请求已经超时,MAX_AGE_FORCED_REQUEST_UNTIL_WITHDRAW_MODE(15 days)
3. 设置逃离模式启动时间为当前一层的时间戳。
4. 可以调用isInWithdrawalMode查询,当前是否是逃离模式 (withdrawalModeStartTime > 0)
#### 逃离模式的提现接口
```
function withdrawFromMerkleTree(MerkleProof merkleProof)
```
1. 检查必须是逃离模式
2. 每个用户的每个币种,只允许处理一次提现
3. 验证默克尔证明正确
4. 处理提币,提现的币种会全额提现,并标记该币种已经处理。
5. withdrawFromMerkleTree的开销,约为300w gas。
#### 默克尔证明MerkleProof的验证
MerkleProof的数据定义
```
struct AccountLeaf
{
uint32 accountID;
address owner;
uint pubKeyX;
uint pubKeyY;
uint32 nonce;
}
struct BalanceLeaf
{
uint32 tokenID;
uint248 balance;
}
struct MerkleProof
{
AccountLeaf accountLeaf;
BalanceLeaf balanceLeaf;
uint[48] accountMerkleProof;
uint[48] balanceMerkleProof;
}
```
- accountLeaf,资产树的账户叶子节点
- balanceLeaf,余额子树的代币叶子节点
- accountMerkleProof, 从叶子节点到 资产树的根节点的proof。这是一个4叉树,一共是3*16层 = 48个。account根节点,即资产树的根。
- balanceMerkleProof, 从叶子节点到余额子树的根节点的proof。这是一个4叉树, 一共是3*16层 = 48个。balance根节点,是账户叶子节点的一部分,参与账户节点的计算。
MerkleProof的验证
```
function verifyAccountBalance(uint merkleRoot, MerkleProof merkleProof)
```
1. 计算代币叶子节点哈希, hash(balance)
2. 将balanceMerkleProof按照顺序分为每组3个,一共16组(层)。从最底层,使用计算出的叶子节点和每组的3个节点,计算出上一层的根节点。依次递归,从而计算出余额子树的根节点 balancesRoot
3. 计算账户叶子节点哈希, hash(owner, pubKeyX, pubKeyY, nonce, balancesRoot)
4. 将accountMerkleProof按照顺序分为每组3个,一共16组(层)。从最底层,使用计算出的叶子节点和每组的3个节点,计算出上一层的根节点。依次递归,从而计算出资产树的根节点 accountsRoot
5. 验证计算出的accountsRoot和合约中存储的资产树的根是一致的。
### 关停模式实现
交易所admin可以选择在任何时候,调用shutdown 进入关停模式。
即使在shutdown模式,用户仍可以通过普通提现的方式来取走他的资金, Operator也可以正常出块。
进入shutdown模式后,如果用户没有进行withdraw操作,Operator有权限替代用户发起forceWithdraw,并将资金提取到用户的一层地址。
在这个模式,Operator只会处理withdraw交易,而不会再处理其他类型交易。
关停模式的触发接口
```
function shutdown()
```
1. 如果交易所是逃离模式,则无法进入关停模式
2. 设置关停模式启动时间为当前一层的时间戳。
3. 可以调用isShutdown 查询,当前是否是关停模式((shutdownModeStartTime > 0)
## Deposit合约
Deposit合约是存储用户代币的合约,Exchange合约调用Deposit合约进行充值和提现。Deposit合约,只提供接口来转账ETH和ERC20,确保用户资金安全。
### 充提接口
```
function deposit(address from, address token, uint248 amount)
```
合约处理流程:
1. 只能由Exchange合约调用
2. 充值ETH,检查转入数额大于等于amount,多的部分退款。
3. 充值ERC20,需要调用TransferFrom将代币从用户账户转入到Deposit合约,因此用户需要预先授权。
4. 转账充值, 是直接转账到Deposit合约,而不会调用到deposit接口。
```
function withdraw(address to, address token, uint amount)
```
合约处理流程:
1. 只能由Exchange合约调用
2. 将代币转账给to地址。
### 非标准代币
余额会自动通胀/通缩的rebase类型币种(比如ampl),DeGate不支持。
余额在转账时变化的burn类型币种(比如cult DAO),DeGate 默认情况不支持, 但可以通过添加白名单的方式来支持。
白名单接口
```
function setCheckBalance(address token, bool checkBalance)
```
合约处理流程:
1. 添加token到白名单列表
2. 充值时,需要计算合约实际的余额变化,来确定充值金额。
## Loopring合约
Loopring合约, 用来配置强制提现手续费和交易最大手续费。这是沿用的路印合约模块,用来管理多个Exchanges和参数配置。在DeGate中只会有一个Exchange合约。
强制提现手续费接口
```
function updateSettings (uint forcedWithdrawalFee)
```
1. 检查手续费不能大于MAX_FORCED_WITHDRAWAL_FEE (0.25 ETH)
交易最大手续费接口
```
function updateProtocolFeeSettings(uint8 protocolFeeBips)
```
1. 交易最大手续费延时(7 days)更新。
## BlockVerifier合约
BlockVerifier合约,负责电路零知识证明key注册和proof验证功能。
### 电路验证key的注册
电路验证key通过硬编码的方式,直接将一组keys写入到 VerificationKeys.sol 合约。
这组keys,对应的zkBlock的size为 [320, 170, 85, 42, 20, 10, 5]
合约部署后,DeGate中不提供接口新增和修改验证key。
### 零知识证明proof验证
验证接口verifyProofs
```
function verifyProofs(uint8 blockType, uint16 blockSize, uint8 blockVersion, uint[] publicInputs, uint[] proofs)
```
* blockType, zkBlock类型,目前恒定为0,仅支持一种类型。
* blockSize, zkBlock包含的交易数量,对应了电路size。
* blockVersion,zkBlock的版本号,默认为0。
* publicInputs, zkBlock的data数据的哈希
* proofs, zkBlock的零知识证明proof
验证流程
1. 根据blockType/blockSize/blockVersion,选择验证key
2. 根据zkBlock数量,调用第三方library的verify接口验证proof
- 单个proof,调用[Verifier合约](https://github.com/HarryR/ethsnarks/blob/master/contracts/Verifier.sol)验证。
* 批量proof,调用[BatchVerifier合约](https://github.com/matter-labs-archive/Groth16BatchVerifier/blob/master/BatchedSnarkVerifier/contracts/BatchVerifier.sol)验证。
3. 验证的gas开销
* 单个proof,由于批量验证有固定的开销,使用Verifier合约单个验证更高效。
* 多个proof, 使用批量验证更为高效。
* 单个proof的证明消耗 大约18.7w gas,批量验证的开销 与proof的数量n成正比, 大约 4.6w*n + 15.3w gas。
## 合约的Trustless说明
用户可以无需信任的和DeGate合约交互,合约的Trustless由以下几点保证
1. 不允许更新合约代码: DeGate合约没有使用proxy更新模式,部署后不允许更新合约代码。
2. 不允许更新电路代码: BlockVerifier合约中,不允许新增和修改电路验证key。
3. 用户可以使用强制提现,来约束Operator处理自己的提现交易或者促使交易所进入逃离模式。
4. 用户可以通过爬块获取所有的calldata 并基于这些calldata可以恢复出完整的资产树
5. 逃离模式下,用户可以使用资产树的节点哈希信息提交默克尔证明,直接从一层自主提现。