;; 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.
;; Se produjo un error al recuperar la clave pública de una firma de intención
;; de depósito.
(define-constant ERR_UNABLE_TO_RECOVER_PK (err u100))
;; La longitud de la intención de depósito no es vá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 comisión 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 no son válidos.
(define-constant ERR_INVALID_DEPOSIT_INTENT_MAGIC (err u104))
;; La longitud de los datos de hook de la intención de depósito no es válida.
(define-constant ERR_INVALID_DEPOSIT_HOOK_DATA_LENGTH (err u105))
;; La firma de la intención de depósito no es válida.
(define-constant ERR_INVALID_DEPOSIT_SIGNATURE (err u106))
;; La versión de la intención de depósito no es válida.
(define-constant ERR_INVALID_DEPOSIT_VERSION (err u107))
;; Después de contabilizar las comisiones, la cantidad de USDCx a acuñar es cero.
(define-constant ERR_INVALID_DEPOSIT_AMOUNT_ZERO (err u108))
;; La cantidad de comisión de la acuñación es mayor que la comisión 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 no es válido.
(define-constant ERR_INVALID_DEPOSIT_REMOTE_DOMAIN (err u110))
;; El token remoto de la intención de depósito no es válido.
(define-constant ERR_INVALID_DEPOSIT_REMOTE_TOKEN (err u111))
;; El destinatario remoto de la intención de depósito no es válido.
(define-constant ERR_INVALID_DEPOSIT_REMOTE_RECIPIENT (err u112))
;; Este nonce ya se ha usado en un depósito diferente
(define-constant ERR_INVALID_DEPOSIT_NONCE (err u113))
;; La comisión 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 no es válida.
(define-constant ERR_INVALID_DEPOSIT_REMOTE_RECIPIENT_LENGTH (err u115))
;; La cantidad de retirada es menor que la cantidad mínima de retirada.
(define-constant ERR_INVALID_WITHDRAWAL_AMOUNT_TOO_LOW (err u116))
;; El dominio nativo no es el valor admitido (actualmente solo 0)
(define-constant ERR_INVALID_NATIVE_DOMAIN (err u117))
;; Bytes mágicos para la codificación del depósito
(define-constant DEPOSIT_INTENT_MAGIC 0x5a2e0acd)
;; Versión admitida para analizar intenciones de depósito
(define-constant DEPOSIT_INTENT_VERSION u1)
;; Dominio nativo admitido para retiradas
(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 analizar una intención de depósito a partir de bytes sin procesar.
;; Esta función se encarga de analizar la intención de depósito según la especificación de Circle.
;; La lógica específica de Stacks (como convertir el destinatario remoto en un principal) la manejan otras funciones.
;;
;; Para una validación completa, incluido el análisis del destinatario remoto y evitar la reutilización de nonces, usa
;; `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 a partir de una intención de depósito y una firma.
;; La recuperación se realiza mediante el hash de la intención de depósito (con `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 eliminar un atestador de Circle.
;;
;; Solo puede ser llamada 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 (mediante `recover-deposit-intent-pk`).
;; Luego, la clave pública se comprueba 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 en un principal estándar. Esto se serializa como
;; 1 byte de versión, más 20 bytes de hash. Luego se rellena a la izquierda
;; con 11 bytes de 0x00.
;;
;; Para admitir 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
;; al uso de 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)))
;; Evita un error de 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 en 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 analizar y validar una intención de depósito.
;;
;; Además del análisis básico (realizado mediante `parse-deposit-intent`), esta función
;; también valida ciertos campos específicos de Stacks, como el
;; token remoto, el dominio remoto, el destinatario remoto y la versión.
;;
;; Además, 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 se ha 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 el `fee-amount`
;; proporcionado por el llamador para garantizar que no sean posibles acuñaciones por cantidad cero.
;;
;; Si `fee-amount` no es cero (y es menor que la `max-fee` del depósito),
;; esta función acuñará `fee-amount` de USDCx para el llamador. Esto permite
;; que cuentas distintas del destinatario del depósito cubran la comisión 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 retirada.
;;
;; Solo puede ser llamada 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 retirarlo 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 retirada.
;;
;; `native-domain` debe ser un valor admitido (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)
)
)