El usdcx-v1 contrato implementa el protocolo USDC xReserve para mover USDC entre Stacks y cadenas externas. Sirve como el punto de entrada principal para acuñar y quemar USDCx basado en intenciones de depósito emitidas por Circle.
Este contrato maneja:
Parsear y validar las cargas de intención de depósito
Recuperar y verificar firmas de atestadores de Circle
Aplicar protección contra replays basada en nonces
Acuñar USDCx a través del usdcx contrato token
Quemar USDCx para iniciar retiros
Gestionar claves de atestadores de Circle
Manejar conversiones de destinatarios específicas de Stacks
Aplicar lógica de tarifas para acuñaciones patrocinadas
Gestionar umbrales mínimos de retiro
Funciona en conjunto con el usdcx contrato token principal, que hace cumplir los roles del protocolo (mint, gobernanza, etc.). Todas las acuñaciones/quemas se realizan a través de protocol-mint y protocol-burn.
Acuñación de USDCx
El punto de entrada para acuñar USDCx es a través de la mint función. El llamador proporciona una intención de depósito serializada, junto con una firma, ambos recibidos fuera de cadena como parte del proceso de puente. La intención de depósito se parsea según la especificación xReserve.
Hay algunos elementos específicos de Stacks:
El remote-token de la intención de depósito DEBE ser los bytes serializados por consenso del principal .usdcx (donde la dirección del desplegador depende de la red), con 0x rellenado a la izquierda.
El remote-domain para Stacks es siempre 10003.
Porque Stacks solo soporta u128 enteros, las funciones de deserialización lanzan un error si cualquier entero de 64 bytes en la intención de depósito es mayor que u128::max. Esto está permitido según la especificación xReserve.
Verificación de atestaciones
El .usdcx-v1 el contrato mantiene un circle-attestors mapa para llevar registro de las claves públicas de atestadores válidos. Cuando se proporciona una intención de depósito, debe estar firmada por una clave pública en este mapa.
Quemado de USDCx
Para retirar USDCx a otra cadena, los usuarios llaman a burn. La cantidad especificada de USDCx se quema de su cuenta en Stacks. Se emite un print evento, que se usa para desencadenar una atestación de quema fuera de cadena.
El .usdcx-v1 el contrato almacena una variable de cantidad mínima. Los usuarios deben retirar al menos esta cantidad, o la quema falla. Las cuentas con el rol 0x04 pueden actualizar esta variable.
;; USDCx v1
;;
;; Este contrato implementa el protocolo USDC xReserve para puentear USDC entre
;; Stacks y otras cadenas.
;;
;; Este contrato es el punto de entrada principal para acuñar y quemar USDCx.
;; Ocurrió un error al recuperar la clave pública de la firma de la intención de depósito.
;; clave pública.
(define-constant ERR_UNABLE_TO_RECOVER_PK (err u100))
;; La longitud de la intención de depósito es inválida.
(define-constant ERR_INVALID_DEPOSIT_BYTE_LENGTH (err u101))
;; La cantidad de la intención de depósito es mayor que u128::max.
(define-constant ERR_INVALID_DEPOSIT_AMOUNT_TOO_HIGH (err u102))
;; La tarifa máxima de la intención de depósito es mayor que u128::max.
(define-constant ERR_INVALID_DEPOSIT_MAX_FEE_TOO_HIGH (err u103))
;; Los bytes mágicos de la intención de depósito son inválidos.
(define-constant ERR_INVALID_DEPOSIT_INTENT_MAGIC (err u104))
;; La longitud de los datos hook de la intención de depósito es inválida.
(define-constant ERR_INVALID_DEPOSIT_HOOK_DATA_LENGTH (err u105))
;; La firma de la intención de depósito es inválida.
(define-constant ERR_INVALID_DEPOSIT_SIGNATURE (err u106))
;; La versión de la intención de depósito es inválida.
(define-constant ERR_INVALID_DEPOSIT_VERSION (err u107))
;; Después de descontar las tarifas, la cantidad de USDCx a acuñar es cero.
(define-constant ERR_INVALID_DEPOSIT_AMOUNT_ZERO (err u108))
;; La cantidad de la tarifa de la acuñación es mayor que la tarifa máxima de la intención de depósito.
(define-constant ERR_INVALID_DEPOSIT_FEE_AMOUNT_TOO_HIGH (err u109))
;; El dominio remoto de la intención de depósito es inválido.
(define-constant ERR_INVALID_DEPOSIT_REMOTE_DOMAIN (err u110))
;; El token remoto de la intención de depósito es inválido.
(define-constant ERR_INVALID_DEPOSIT_REMOTE_TOKEN (err u111))
;; El destinatario remoto de la intención de depósito es inválido.
(define-constant ERR_INVALID_DEPOSIT_REMOTE_RECIPIENT (err u112))
;; Este nonce ya ha sido usado en un depósito diferente
(define-constant ERR_INVALID_DEPOSIT_NONCE (err u113))
;; La tarifa máxima es mayor o igual que la cantidad.
(define-constant ERR_INVALID_DEPOSIT_MAX_FEE_GTE_AMOUNT (err u114))
;; La longitud del destinatario remoto de la intención de depósito es inválida.
(define-constant ERR_INVALID_DEPOSIT_REMOTE_RECIPIENT_LENGTH (err u115))
;; La cantidad de retiro es menor que la cantidad mínima de retiro.
(define-constant ERR_INVALID_WITHDRAWAL_AMOUNT_TOO_LOW (err u116))
;; El dominio nativo no es el valor soportado (actualmente solo 0)
(define-constant ERR_INVALID_NATIVE_DOMAIN (err u117))
;; Bytes mágicos para la codificación de depósitos
(define-constant DEPOSIT_INTENT_MAGIC 0x5a2e0acd)
;; Versión soportada para parsear intenciones de depósito
(define-constant DEPOSIT_INTENT_VERSION u1)
;; Dominio nativo soportado para retiros
(define-constant ETHEREUM_NATIVE_DOMAIN u0)
;; `domain` permitido para depósitos
(define-constant DOMAIN u10003)
;; Mapa de nonces usados
(define-map used-nonces
(buff 32)
bool
)
;; Mapa de claves públicas de atestadores de Circle
(define-map circle-attestors
(buff 33)
bool
)
;; Cantidad mínima requerida para retirar USDCx
(define-data-var min-withdrawal-amount uint u0)
;; Función auxiliar para parsear una intención de depósito desde bytes crudos.
;; Esta función se encarga de parsear la intención de depósito de acuerdo con la especificación de Circle.
;; La lógica específica de Stacks (como convertir el destinatario remoto a un principal) se maneja en otras funciones.
;;
;; Para validación completa, incluyendo parsear el destinatario remoto y prevenir la reutilización de nonces, use
;; `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
))
),
})
)
)
)
;; Recuperar la clave pública del atestador desde una intención de depósito y una firma.
;; La recuperación se hace hasheando la intención de depósito (vía `keccak256`)
;; y luego usando la función `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)
)
)
;; Añadir o quitar un atestador de Circle.
;;
;; Solo puede ser llamado por un llamador con el rol de gobernanza.
(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)
)
)
;; Recuperar y verificar una firma de intención de depósito.
;;
;; Primero se recupera la clave pública (vía `recover-deposit-intent-pk`).
;; Luego, la clave pública se verifica contra el mapa `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)
)
)
)
;; Convertir 32 bytes a un principal estándar. Esto se serializa como
;; 1 byte de versión, más 20 bytes de hash. Esto luego se rellena a la izquierda
;; con 11 bytes de 0x00.
;;
;; Para soportar contratos como destinatarios, `hook-data` puede contener un nombre de contrato.
;; Para usar esta funcionalidad, `hook-data` DEBE ser un buffer serializado por consenso
;; del tipo { contract-name: (string-ascii 40) }.
;;
;; Si `hook-data` no se puede deserializar, esta función recurre
;; a usar un principal estándar.
(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)))
;; Evitar un error en tiempo de ejecución de la VM cuando `hook-data` está vacío:
(hook-contract-name (if (is-eq (len hook-data) u0)
none
(from-consensus-buff? { contract-name: (string-ascii 40) } hook-data)
))
)
;; Debe tener 0x00 como relleno
(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
))
)
)
;; Versión codificada de 32 bytes de la dirección del contrato `.usdcx`.
;; Esto debe usarse en las intenciones de depósito como el campo `remote-token`.
(define-read-only (get-valid-remote-token)
(concat 0x00000000
(unwrap-panic (as-max-len? (unwrap-panic (to-consensus-buff? .usdcx)) u28))
)
)
;; Función auxiliar para parsear y validar una intención de depósito.
;;
;; Además del parseo básico (realizado vía `parse-deposit-intent`), esta función
;; también valida ciertos campos específicos de Stacks, como el
;; token remoto, dominio remoto, destinatario remoto y versión.
;;
;; Adicionalmente, esta función valida los campos `amount` y `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)))
;; Este nonce ya ha sido usado en un depósito diferente
ERR_INVALID_DEPOSIT_NONCE
)
(ok (merge parsed-intent { remote-recipient: remote-recipient }))
)
)
;; Acuñar USDCx usando una intención de depósito.
;; Este es el punto de entrada principal para acuñar USDCx.
;;
;; Además de la validación realizada por `parse-and-validate-deposit-intent`, y
;; `verify-deposit-intent-signature`, esta función también valida la `fee-amount`
;; proporcionada por el llamador para asegurar que no sean posibles acuñaciones de cantidad cero.
;;
;; Si `fee-amount` es distinto de cero (y menor que la `max-fee` del depósito),
;; esta función acuñará `fee-amount` de USDCx al llamador. Esto permite
;; que cuentas distintas del destinatario del depósito cubran la tarifa STX necesaria para acuñar.
(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
)
;; acuñar al destinatario
(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)
)
)
;; Establecer la cantidad mínima de retiro.
;;
;; Solo puede ser llamado por un llamador con el rol personalizado `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)
)
;; Quemar USDCx con el propósito de retirar USDCx del protocolo.
;;
;; Esta función quema USDCx de la cuenta del llamador y emite un evento `burn`.
;;
;; La cantidad debe ser mayor o igual que la cantidad mínima de retiro.
;;
;; `native-domain` debe ser un valor soportado (actualmente solo `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)
)
)