;; USDCx v1
;;
;; 此合约实现了用于在
;; Stacks 与其他链之间桥接 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))
;; 存款意图的 hook 数据长度无效。
(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)
;; 已使用 nonce 的映射
(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)由其他函数处理。
;;
;; 若要进行完整验证,包括解析远程接收者与防止 nonce 重放,请使用
;; `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))
)
(let (
(valid-len (asserts! (is-eq (len remote-recipient-bytes) u32)
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)
)
ERR_INVALID_DEPOSIT_REMOTE_RECIPIENT
))
)
)
;; 32 字节编码版本的 `.usdcx` 合约地址。
;; 这必须在存款意图中用作 `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)))
(let (
(parsed-intent (try! (parse-deposit-intent deposit-intent)))
(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)))
;; 此 nonce 已在不同的存款中使用过
ERR_INVALID_DEPOSIT_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`),
;; 此函数将向调用者铸造 `fee-amount` 数量的 USDCx。这允许
;; 除存款接收者之外的账户支付铸造所需的 STX 费用。
(define-public (mint
(deposit-intent (buff 320))
(signature (buff 65))
(fee-amount uint)
)
(let (
(parsed-intent (try! (parse-and-validate-deposit-intent deposit-intent)))
(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)
true
(try! (contract-call? .usdcx protocol-mint fee-amount tx-sender))
)
(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))
(begin
(try! (contract-call? .usdcx validate-protocol-caller 0x04 contract-caller))
(var-set min-withdrawal-amount new-min-withdrawal-amount)
(ok true)
)
)
(define-read-only (get-min-withdrawal-amount)
(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))
)
(begin
(asserts! (>= amount (var-get min-withdrawal-amount))
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))
(print {
topic: "burn",
native-domain: native-domain,
native-recipient: native-recipient,
sender: tx-sender,
amount: amount,
})
(ok true)
)
)