;; USDCx v1
;;
;; 该合约实现了用于在
;; Stacks 与其他链之间桥接 USDC 的 USDC xReserve 协议。
;;
;; 该合约是铸造和销毁 USDCx 的主要入口点。
;; 在恢复存款意图签名的
;; 公钥时发生错误。
(define-constant ERR_UNABLE_TO_RECOVER_PK (err u100))
;; 存款意图的长度无效。
(define-constant ERR_INVALID_DEPOSIT_BYTE_LENGTH (err u101))
;; 存款意图的金额大于 u128::max。
(define-constant ERR_INVALID_DEPOSIT_AMOUNT_TOO_HIGH (err u102))
;; 存款意图的最大费用大于 u128::max。
(define-constant ERR_INVALID_DEPOSIT_MAX_FEE_TOO_HIGH (err u103))
;; 存款意图的魔术字节无效。
(define-constant ERR_INVALID_DEPOSIT_INTENT_MAGIC (err u104))
;; 存款意图的钩子数据长度无效。
(define-constant ERR_INVALID_DEPOSIT_HOOK_DATA_LENGTH (err u105))
;; 存款意图的签名无效。
(define-constant ERR_INVALID_DEPOSIT_SIGNATURE (err u106))
;; 存款意图的版本无效。
(define-constant ERR_INVALID_DEPOSIT_VERSION (err u107))
;; 扣除费用后,要铸造的 USDCx 数量为零。
(define-constant ERR_INVALID_DEPOSIT_AMOUNT_ZERO (err u108))
;; 铸造的费用金额大于存款意图的最大费用。
(define-constant ERR_INVALID_DEPOSIT_FEE_AMOUNT_TOO_HIGH (err u109))
;; 存款意图的远程域无效。
(define-constant ERR_INVALID_DEPOSIT_REMOTE_DOMAIN (err u110))
;; 存款意图的远程代币无效。
(define-constant ERR_INVALID_DEPOSIT_REMOTE_TOKEN (err u111))
;; 存款意图的远程接收方无效。
(define-constant ERR_INVALID_DEPOSIT_REMOTE_RECIPIENT (err u112))
;; 该随机数(nonce)已在不同的存款中使用
(define-constant ERR_INVALID_DEPOSIT_NONCE (err u113))
;; 最大费用大于或等于金额。
(define-constant ERR_INVALID_DEPOSIT_MAX_FEE_GTE_AMOUNT (err u114))
;; 存款意图的远程接收方长度无效。
(define-constant ERR_INVALID_DEPOSIT_REMOTE_RECIPIENT_LENGTH (err u115))
;; 提现金额低于最低提现金额。
(define-constant ERR_INVALID_WITHDRAWAL_AMOUNT_TOO_LOW (err u116))
;; 原生域不是受支持的值(当前仅为 0)
(define-constant ERR_INVALID_NATIVE_DOMAIN (err u117))
;; 存款编码的魔术字节
(define-constant DEPOSIT_INTENT_MAGIC 0x5a2e0acd)
;; 解析存款意图支持的版本
(define-constant DEPOSIT_INTENT_VERSION u1)
;; 支持的提现原生域
(define-constant ETHEREUM_NATIVE_DOMAIN u0)
;; 存款允许的 `domain`
(define-constant DOMAIN u10003)
;; 已使用随机数的映射
(define-map used-nonces
(buff 32)
bool
)
;; Circle 见证者公钥的映射
(define-map circle-attestors
(buff 33)
bool
)
;; 提现 USDCx 所需的最小金额
(define-data-var min-withdrawal-amount uint u0)
;; 帮助函数:从原始字节解析存款意图。
;; 此函数负责根据 Circle 规范解析存款意图。
;; Stacks 特有的逻辑(例如将远程接收方转换为 principal)由其他函数处理。
;;
;; 对于完整验证,包括解析远程接收方和防止随机数重用,请使用
;; `parse-and-validate-deposit-intent`。
(define-read-only (parse-deposit-intent (deposit-intent (buff 320)))
(begin
(asserts! (>= (len deposit-intent) u240) ERR_INVALID_DEPOSIT_BYTE_LENGTH)
(let (
(magic (unwrap-panic (as-max-len? (unwrap-panic (slice? deposit-intent u0 u4)) u4)))
(version (buff-to-uint-be (unwrap-panic (as-max-len? (unwrap-panic (slice? deposit-intent u4 u8)) u4))))
(amount-left-bytes (unwrap-panic (as-max-len? (unwrap-panic (slice? deposit-intent u8 u24)) u16)))
(amount (buff-to-uint-be (unwrap-panic (as-max-len? (unwrap-panic (slice? deposit-intent u24 u40)) u16))))
(remote-domain (buff-to-uint-be (unwrap-panic (as-max-len? (unwrap-panic (slice? deposit-intent u40 u44)) u4))))
(remote-token (unwrap-panic (as-max-len? (unwrap-panic (slice? deposit-intent u44 u76)) u32)))
(remote-recipient (unwrap-panic (as-max-len? (unwrap-panic (slice? deposit-intent u76 u108)) u32)))
(local-token (unwrap-panic (as-max-len? (unwrap-panic (slice? deposit-intent u108 u140)) u32)))
(local-depositor (unwrap-panic (as-max-len? (unwrap-panic (slice? deposit-intent u140 u172)) u32)))
(max-fee-left-bytes (unwrap-panic (as-max-len? (unwrap-panic (slice? deposit-intent u172 u188)) u16)))
(max-fee (buff-to-uint-be (unwrap-panic (as-max-len? (unwrap-panic (slice? deposit-intent u188 u204)) u16))))
(nonce (unwrap-panic (as-max-len? (unwrap-panic (slice? deposit-intent u204 u236)) u32)))
(hook-data-len (buff-to-uint-be (unwrap-panic (as-max-len? (unwrap-panic (slice? deposit-intent u236 u240)) u4))))
)
(asserts! (is-eq magic DEPOSIT_INTENT_MAGIC)
ERR_INVALID_DEPOSIT_INTENT_MAGIC
)
(asserts! (is-eq amount-left-bytes 0x00000000000000000000000000000000)
ERR_INVALID_DEPOSIT_AMOUNT_TOO_HIGH
)
(asserts! (is-eq max-fee-left-bytes 0x00000000000000000000000000000000)
ERR_INVALID_DEPOSIT_MAX_FEE_TOO_HIGH
)
(asserts! (is-eq (len deposit-intent) (+ u240 hook-data-len))
ERR_INVALID_DEPOSIT_HOOK_DATA_LENGTH
)
(ok {
magic: magic,
version: version,
amount: amount,
remote-domain: remote-domain,
remote-token: remote-token,
remote-recipient: remote-recipient,
local-token: local-token,
local-depositor: local-depositor,
max-fee: max-fee,
nonce: nonce,
hook-data: (if (is-eq hook-data-len u0)
0x
(unwrap-panic (as-max-len?
(unwrap-panic (slice? deposit-intent u240 (+ u240 hook-data-len)))
u80
))
),
})
)
)
)
;; 从存款意图和签名中恢复见证者的公钥。
;; 恢复通过对存款意图进行哈希(使用 `keccak256`)来完成,
;; 然后使用 `secp256k1-recover?` 函数。
(define-read-only (recover-deposit-intent-pk
(deposit-intent (buff 320))
(signature (buff 65))
)
(let (
(hash (keccak256 deposit-intent))
(recovered-pk (unwrap! (secp256k1-recover? hash signature) ERR_UNABLE_TO_RECOVER_PK))
)
(ok recovered-pk)
)
)
;; 添加或移除 Circle 见证者。
;;
;; 只能由具有治理角色的调用者调用。
(define-public (add-or-remove-circle-attestor
(public-key (buff 33))
(enabled bool)
)
(begin
;; #[filter(public-key, enabled)]
(try! (contract-call? .usdcx validate-protocol-caller 0x00 contract-caller))
(map-set circle-attestors public-key enabled)
(ok true)
)
)
;; 恢复并验证存款意图签名。
;;
;; 首先恢复公钥(通过 `recover-deposit-intent-pk`)。
;; 然后,将公钥与 `circle-attestors` 映射进行检查。
(define-read-only (verify-deposit-intent-signature
(deposit-intent (buff 320))
(signature (buff 65))
)
(begin
;; #[filter(deposit-intent, signature)]
(let ((recovered-pk (try! (recover-deposit-intent-pk deposit-intent signature))))
(asserts! (default-to false (map-get? circle-attestors recovered-pk))
ERR_INVALID_DEPOSIT_SIGNATURE
)
(ok recovered-pk)
)
)
)
;; 将 32 字节转换为标准 principal。此序列化形式为
;; 1 个版本字节,加上 20 个哈希字节。然后在左侧填充
;; 11 个 0x00 字节。
;;
;; 为了支持合约作为接收方,`hook-data` 可以包含合约名称。
;; 要使用此功能,`hook-data` 必须是类型为 { contract-name: (string-ascii 40) } 的共识序列化缓冲区。
;; 如果无法将 `hook-data` 反序列化,则此函数回退
;;
;; 为使用标准 principal。
(define-read-only (get-remote-recipient
(remote-recipient-bytes (buff 32))
(hook-data (buff 80))
(valid-len (asserts! (is-eq (len remote-recipient-bytes) u32)
)
(let (
ERR_INVALID_DEPOSIT_REMOTE_RECIPIENT_LENGTH
(version-byte (unwrap-panic (element-at? remote-recipient-bytes u11)))
))
(hash-bytes (unwrap-panic (as-max-len? (unwrap-panic (slice? remote-recipient-bytes u12 u32)) u20)))
;; 当 `hook-data` 为空时避免虚拟机运行时错误:
(hook-contract-name (if (is-eq (len hook-data) u0)
none
(from-consensus-buff? { contract-name: (string-ascii 40) } hook-data)
;; 必须有 0x00 作为填充
))
)
(asserts!
(is-eq
(unwrap-panic (as-max-len? (unwrap-panic (slice? remote-recipient-bytes u0 u11)) u11))
0x0000000000000000000000
ERR_INVALID_DEPOSIT_REMOTE_RECIPIENT
)
(ok (unwrap!
)
(match hook-contract-name
contract-name-tup (principal-construct? version-byte hash-bytes
(get contract-name contract-name-tup)
(principal-construct? version-byte hash-bytes)
)
;; `.usdcx` 合约地址的 32 字节编码版本。
)
(ok (unwrap!
))
)
)
;; 这必须在存款意图中用作 `remote-token` 字段。
(define-read-only (get-valid-remote-token)
(concat 0x00000000
(unwrap-panic (as-max-len? (unwrap-panic (to-consensus-buff? .usdcx)) u28))
;; 帮助函数:解析并验证存款意图。
)
)
;; 除了通过 `parse-deposit-intent` 完成的基本解析之外,此函数
;;
;; 还验证某些 Stacks 特有的字段,例如
;; 远程代币、远程域、远程接收方和版本。
;; 此外,此函数验证 `amount` 和 `max-fee` 字段。
;;
(define-read-only (parse-and-validate-deposit-intent (deposit-intent (buff 320)))
(parsed-intent (try! (parse-deposit-intent deposit-intent)))
(let (
(remote-recipient (try! (get-remote-recipient (get remote-recipient parsed-intent)
(get hook-data parsed-intent)
(amount (get amount parsed-intent))
)))
(asserts! (is-eq (get remote-token parsed-intent) (get-valid-remote-token))
)
ERR_INVALID_DEPOSIT_REMOTE_TOKEN
(asserts! (> amount u0) ERR_INVALID_DEPOSIT_AMOUNT_ZERO)
)
(asserts! (is-eq (get remote-domain parsed-intent) DOMAIN)
ERR_INVALID_DEPOSIT_REMOTE_DOMAIN
(asserts! (is-eq (get version parsed-intent) DEPOSIT_INTENT_VERSION)
)
ERR_INVALID_DEPOSIT_VERSION
(asserts! (>= amount (get max-fee parsed-intent))
)
ERR_INVALID_DEPOSIT_MAX_FEE_GTE_AMOUNT
(asserts! (is-none (map-get? used-nonces (get nonce parsed-intent)))
)
ERR_INVALID_DEPOSIT_NONCE
;; 该随机数(nonce)已在不同的存款中使用
(ok (merge parsed-intent { remote-recipient: remote-recipient }))
)
;; 使用存款意图铸造 USDCx。
)
)
;; 这是铸造 USDCx 的主要入口点。
;; 除了 `parse-and-validate-deposit-intent` 和
;;
;; `verify-deposit-intent-signature` 执行的验证之外,此函数还验证调用者提供的 `fee-amount`,
;; 以确保无法进行零金额铸造。
;; 如果 `fee-amount` 非零(且小于存款的 `max-fee`),
;;
;; 此函数将向调用者铸造等额的 USDCx 作为手续费。这样可以
;; 允许除存款接收方之外的账户支付铸造所需的 STX 费用。
(define-public (mint
(fee-amount uint)
(deposit-intent (buff 320))
(signature (buff 65))
(parsed-intent (try! (parse-and-validate-deposit-intent deposit-intent)))
)
(let (
(recovered-pk (try! (verify-deposit-intent-signature deposit-intent signature)))
(mint-amount (- (get amount parsed-intent) fee-amount))
(asserts! (>= (get max-fee parsed-intent) fee-amount)
)
ERR_INVALID_DEPOSIT_FEE_AMOUNT_TOO_HIGH
;; 铸造给接收方
)
(if (is-eq mint-amount u0)
true
(try! (contract-call? .usdcx protocol-mint mint-amount
(get remote-recipient parsed-intent)
(if (is-eq fee-amount u0)
))
)
(try! (contract-call? .usdcx protocol-mint fee-amount tx-sender))
(try! (contract-call? .usdcx protocol-mint mint-amount
(map-set used-nonces (get nonce parsed-intent) true)
)
(print {
topic: "mint",
parsed-intent: parsed-intent,
attestor-pk: recovered-pk,
mint-amount: mint-amount,
fee-amount: fee-amount,
;; 设置最低提现金额。
})
(ok true)
)
)
;; 只能由具有自定义角色 `0x04` 的调用者调用。
;;
(define-public (set-min-withdrawal-amount (new-min-withdrawal-amount uint))
(try! (contract-call? .usdcx validate-protocol-caller 0x04 contract-caller))
(begin
(var-set min-withdrawal-amount new-min-withdrawal-amount)
(define-read-only (get-min-withdrawal-amount)
(ok true)
)
)
(var-get min-withdrawal-amount)
;; 为了从协议中提现 USDCx 而销毁 USDCx。
)
;; 此函数从调用者账户中销毁 USDCx 并发出 `burn` 事件。
;;
;; 金额必须大于或等于最低提现金额。
;;
;; `native-domain` 必须是受支持的值(当前仅为 `ETHEREUM_NATIVE_DOMAIN` (u0))。
;;
(define-public (burn
(amount uint)
(native-domain uint)
(native-recipient (buff 32))
(asserts! (>= amount (var-get min-withdrawal-amount))
)
(begin
ERR_INVALID_WITHDRAWAL_AMOUNT_TOO_LOW
(asserts! (is-eq native-domain ETHEREUM_NATIVE_DOMAIN)
)
ERR_INVALID_NATIVE_DOMAIN
(try! (contract-call? .usdcx protocol-burn amount tx-sender))
)
topic: "burn",
topic: "mint",
native-domain: native-domain,
native-recipient: native-recipient,
sender: tx-sender,
USDCx-v1 合约摘要
amount: amount,
})
(ok true)
)
)