# 合约设计文档大纲 # ***术语表*** | 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. 逃离模式下,用户可以使用资产树的节点哈希信息提交默克尔证明,直接从一层自主提现。