Skip to content

Security: den-labs/dengrow

Security

docs/SECURITY.md

Security Review - DenGrow Contracts

Fecha Revisión Inicial: 2026-02-04 Última Actualización: 2026-02-06 Contratos Revisados: plant-game-v1.clar, plant-nft.clar, plant-storage.clar, impact-registry.clar Estado: ✅ Testnet Deployed - Issues Críticos Resueltos


Resumen Ejecutivo

Severidad Cantidad Estado Descripción
🔴 CRÍTICO 1 RESUELTO update-owner ahora valida caller
🟡 MEDIO 2 RESUELTO Mint público, metadata API implementada
🟢 BAJO 2 ⚠️ Pendiente Trait de testnet, optimización gas

Status:SEGURO PARA TESTNET - Todos los issues críticos y medios fueron resueltos. Deployed: Testnet (ST23SRWT9A0CYMPW4Q32D0D7KT2YY07PQAVJY3NJZ)


TL;DR - Estado Actual (2026-02-06)

✅ Lo que está bien

  • Seguridad: Issue crítico de update-owner RESUELTO - solo plant-nft puede actualizar ownership
  • Funcionalidad: Mint público funcionando, metadata API implementada con traits
  • Testing: 103 tests passing con coverage completo incluyendo security
  • Deployment: 4 contratos deployed en testnet y funcionando correctamente
  • Arquitectura: Sistema upgradeable (storage + logic + nft + registry) funcionando

⚠️ Lo que falta (No crítico para testnet)

  • Issue #4: Optimización de gas (~10 μSTX por llamada)
  • Issue #5: Trait de testnet (funciona con mainnet trait, solo convención)

🚀 Bloqueadores para Mainnet

Ver docs/IMPACT_POLICY.md para 6 decisiones pendientes antes de mainnet:

  1. Partner de tree-planting
  2. Schedule de redemptions
  3. Funding inicial
  4. Post-graduation UX
  5. Proceso de redemption
  6. User rewards

Recomendación: Los contratos son seguros para testnet. Resolver IMPACT_POLICY antes de mainnet.


🔴 CRÍTICO - Issue #1: update-owner Sin Validación ✅ RESUELTO

Estado: ✅ ARREGLADO (2026-02-05)

Implementado en:

  • plant-game-v1.clar:163-169 (testnet deployed)
  • plant-game.clar:159-173 (legacy, también corregido)

Problema Original (2026-02-04)

(define-public (update-owner (token-id uint) (new-owner principal))
  (let
    (
      (plant-data (unwrap! (map-get? plants { token-id: token-id }) ERR-PLANT-NOT-FOUND))
    )
    ;; Update only the owner field, preserve all other state
    (ok (map-set plants
      { token-id: token-id }
      (merge plant-data { owner: new-owner })
    ))
  )
)

Vulnerabilidad: Cualquier principal puede cambiar el owner de cualquier planta llamando directamente a update-owner, sin necesidad de ser el dueño del NFT.

Impacto

  • Atacante puede robar ownership de plantas sin transferir el NFT
  • Usuario pierde control sobre su planta (no puede regar)
  • Atacante puede regar plantas ajenas

Exploit Example

;; Alice mintea planta #1
(contract-call? .plant-nft mint 'ST1...)  ;; token-id: u1, owner: Alice

;; Bob (atacante) llama directamente a update-owner
(contract-call? .plant-game update-owner u1 'ST2...) ;; Ahora Bob es owner en plant-game

;; Bob puede regar la planta de Alice
(contract-call? .plant-game water u1)  ;; ✅ Pasa porque plant-game.owner = Bob

;; Alice NO puede regar su propia planta
(contract-call? .plant-game water u1)  ;; ❌ Falla con ERR-NOT-OWNER

Fix Requerido

Opción A: Restringir a solo plant-nft contract (RECOMENDADO)

(define-public (update-owner (token-id uint) (new-owner principal))
  (let
    (
      (plant-data (unwrap! (map-get? plants { token-id: token-id }) ERR-PLANT-NOT-FOUND))
    )
    ;; AGREGAR: Solo el contrato plant-nft puede llamar esta función
    (asserts! (is-eq contract-caller .plant-nft) ERR-NOT-AUTHORIZED)

    ;; Update only the owner field, preserve all other state
    (ok (map-set plants
      { token-id: token-id }
      (merge plant-data { owner: new-owner })
    ))
  )
)

Opción B: Hacer función privada y crear wrapper interno

;; Cambiar a privada
(define-private (update-owner-internal (token-id uint) (new-owner principal))
  ...
)

;; Llamar desde transfer en plant-nft

Error code a agregar:

(define-constant ERR-NOT-AUTHORIZED (err u105))

✅ Fix Implementado (Opción A)

plant-game-v1.clar:163-169 (deployed en testnet):

(define-public (update-owner (token-id uint) (new-owner principal))
  (begin
    ;; Only the plant-nft contract can update ownership
    (asserts! (is-eq contract-caller .plant-nft) ERR-NOT-AUTHORIZED)
    ;; Delegate to storage
    (contract-call? .plant-storage update-plant-owner token-id new-owner)
  )
)

Verificación:

  • ✅ Validación contract-caller presente
  • ✅ Error ERR-NOT-AUTHORIZED implementado (línea 36)
  • ✅ Solo plant-nft puede actualizar ownership
  • ✅ 103 tests passing incluyen este escenario

🟡 MEDIO - Issue #2: Mint Permission Restrictivo ✅ RESUELTO

Estado: ✅ RESUELTO - Mint Público Implementado

plant-nft.clar:78-90 (deployed en testnet):

(define-public (mint (recipient principal))
  (let ((token-id (+ (var-get last-token-id) u1)))
    ;; Check collection limit
    (asserts! (< (var-get last-token-id) COLLECTION_LIMIT) ERR_SOLD_OUT)
    ;; Mint the NFT (NO owner restriction)
    (try! (nft-mint? plant-nft token-id recipient))
    ;; Initialize plant in storage
    (try! (contract-call? .plant-storage initialize-plant token-id recipient))
    ;; Update counter
    (var-set last-token-id token-id)
    (ok token-id)
  )
)

Verificación:

  • ✅ Sin restricción ERR_OWNER_ONLY
  • ✅ Cualquier usuario puede mintear
  • ✅ Perfecto para testnet público
  • ⚠️ Considerar agregar fee para mainnet

Problema Original (2026-02-04)

plant-nft.clar:59 (versión anterior)

(asserts! (is-eq tx-sender CONTRACT_OWNER) ERR_OWNER_ONLY)

Contexto: Solo el deployer podía mintear NFTs.

Impacto Original en Testnet

  • Usuarios no podían mintear sus propias plantas
  • Se requería minteo manual para cada usuario
  • No funcional para testing público

Opciones

Opción A: Remover restricción para Testnet (NO recomendado para Mainnet)

;; (asserts! (is-eq tx-sender CONTRACT_OWNER) ERR_OWNER_ONLY)

Opción B: Whitelist de minters

(define-map authorized-minters principal bool)

(define-public (add-minter (minter principal))
  (begin
    (asserts! (is-eq tx-sender CONTRACT_OWNER) ERR_OWNER_ONLY)
    (ok (map-set authorized-minters minter true))
  )
)

(define-public (mint (recipient principal))
  (let ((token-id (+ (var-get last-token-id) u1)))
    (asserts! (< (var-get last-token-id) COLLECTION_LIMIT) ERR_SOLD_OUT)
    ;; Check if caller is authorized
    (asserts!
      (or
        (is-eq tx-sender CONTRACT_OWNER)
        (default-to false (map-get? authorized-minters tx-sender))
      )
      ERR_OWNER_ONLY
    )
    ...
  )
)

Opción C: Mint público con fee (Mainnet-ready)

(define-constant MINT_FEE u1000000) ;; 1 STX

(define-public (mint (recipient principal))
  (let ((token-id (+ (var-get last-token-id) u1)))
    (asserts! (< (var-get last-token-id) COLLECTION_LIMIT) ERR_SOLD_OUT)

    ;; Charge fee (except for contract owner)
    (if (is-eq tx-sender CONTRACT_OWNER)
      true
      (try! (stx-transfer? MINT_FEE tx-sender CONTRACT_OWNER))
    )
    ...
  )
)

Recomendación para Testnet: Opción A (remover restricción) Recomendación para Mainnet: Opción C (mint con fee)


🟡 MEDIO - Issue #3: Metadata URI Placeholder ✅ RESUELTO

Estado: ✅ RESUELTO - Metadata API Implementada

plant-nft.clar:30 (deployed en testnet):

(define-data-var base-uri (string-ascii 80) "https://dengrow.vercel.app/api/metadata/{id}")

Implementación Completa:

  • ✅ API endpoint: apps/web/src/app/api/metadata/[tokenId]/route.ts
  • ✅ Trait system: 5 categorías (Pot, Background, Flower, Companion, Species)
  • ✅ Deterministic generation: Hash-based desde token-id
  • ✅ Dynamic images: apps/web/src/app/api/image/[tokenId]/route.ts
  • ✅ Stage-aware: 5 stages × 5 species = 25 variaciones
  • ✅ Admin function: set-base-uri implementado (líneas 97-101)

Verificación:

curl https://dengrow.vercel.app/api/metadata/1
# Returns proper SIP-009 metadata with traits and image URL

Problema Original (2026-02-04)

(define-data-var base-uri (string-ascii 80) "https://placedog.net/500/500?id={id}")

Contexto: Placeholder de perros en lugar de metadata real.

Impacto Original

  • NFTs aparecían con imágenes de perros en explorers
  • No había información de traits/stages
  • No cumplía expectativas de usuarios

🟢 BAJO - Issue #4: Gas Optimization en calculate-stage ⚠️ PENDIENTE

Estado: ⚠️ No Implementado (No Crítico)

Ubicación Actual

plant-game-v1.clar:44-58

Optimización Sugerida

;; Actual: Nested if (funciona, pero verbose)
(define-private (calculate-stage (growth-points uint))
  (if (<= growth-points u1)
    STAGE-SEED
    (if (<= growth-points u3)
      STAGE-SPROUT
      (if (<= growth-points u5)
        STAGE-PLANT
        (if (is-eq growth-points u6)
          STAGE-BLOOM
          STAGE-TREE
        )
      )
    )
  )
)

;; Optimizado: Eliminando el is-eq innecesario
(define-private (calculate-stage (growth-points uint))
  (if (<= growth-points u1)
    STAGE-SEED
    (if (<= growth-points u3)
      STAGE-SPROUT
      (if (<= growth-points u5)
        STAGE-PLANT
        (if (<= growth-points u6)
          STAGE-BLOOM
          STAGE-TREE  ;; Si growth > 6, siempre es Tree
        )
      )
    )
  )
)

Impacto: Mínimo, ahorra ~10 gas por llamada.


🟢 BAJO - Issue #5: SIP-009 Trait Comentado ⚠️ PENDIENTE

Estado: ⚠️ No Actualizado (Funciona, pero no sigue convención)

Ubicación Actual

plant-nft.clar:17

Estado Actual

(impl-trait 'SP2PABAF9FTAJYNFZH93XENAJ8FVY99RRM50D2JG9.nft-trait.nft-trait) ;; Mainnet trait

Contexto: Usa mainnet trait en lugar de testnet trait.

Impacto: Bajo - El contrato funciona correctamente en testnet, pero no sigue la convención de usar el trait específico de cada red.

Recomendación para Mainnet

Antes de mainnet deployment, verificar que se usa el trait correcto para cada red:

;; Para Mainnet:
(impl-trait 'SP2PABAF9FTAJYNFZH93XENAJ8FVY99RRM50D2JG9.nft-trait.nft-trait)

;; Para Testnet (opcional, si se redeploya):
(impl-trait 'STM6S3AESTK9NAYE3Z7RS00T11ER8JJCDNTKG711.nft-trait.nft-trait)

✅ Fortalezas de Seguridad

plant-game.clar

  1. Ownership verification en water()

    (asserts! (is-eq tx-sender (get owner plant-data)) ERR-NOT-OWNER)
  2. Cooldown enforcement correcto

    (asserts!
      (or
        (is-eq last-water u0)
        (>= current-block (+ last-water BLOCKS-PER-DAY))
      )
      ERR-COOLDOWN-ACTIVE
    )
  3. Tree finality enforcement

    (asserts! (< current-stage STAGE-TREE) ERR-ALREADY-TREE)
  4. No integer overflow en growth-points

    • Máximo teórico: 7 waters = u7
    • uint puede manejar hasta u340282366920938463463374607431768211455
  5. Prevención de re-inicialización

    (asserts! (is-none existing-plant) ERR-PLANT-ALREADY-EXISTS)

plant-nft.clar

  1. SIP-009 compliant

    • get-last-token-id
    • get-token-uri
    • get-owner
    • transfer
  2. Transfer ownership verification

    (asserts! (is-eq tx-sender sender) ERR_NOT_TOKEN_OWNER)
  3. Collection limit enforcement

    (asserts! (< (var-get last-token-id) COLLECTION_LIMIT) ERR_SOLD_OUT)
  4. Atomic mint + initialize

    • Si initialize-plant falla, todo el mint revierte

Test Coverage

103/103 tests passing ✅ (Actualizado 2026-02-06)

Coverage por área:

  • Initialization: 100%
  • Ownership: 100%
  • Cooldown: 100%
  • Stage progression: 100%
  • Read-only functions: 100%
  • Transfer integration: 100%
  • Edge cases: 100%
  • Impact Registry: 100% (nuevo en M4)
  • Upgradeable Architecture: 100% (nuevo en M1)
  • Authorization Chain: 100% (storage → game-v1 → nft)

Security test cases incluidos:

  • ✅ Direct call to update-owner (rechaza si no es plant-nft)
  • ✅ Contract-call vs tx-sender diferenciación
  • ✅ Graduation registration automático
  • ✅ Storage authorization checks

Comando:

pnpm --filter @dengrow/contracts test
# Output: 103 passed

Checklist Pre-Deployment

✅ TESTNET DEPLOYMENT (Completado 2026-02-05)

CRÍTICO

  • ✅ Fix update-owner con validación de caller
    • Implementado en plant-game-v1.clar:166
    • 103 tests passing

Recomendado para Testnet

  • ✅ Remover mint permission → Mint público implementado
  • ✅ Actualizar base-uri → https://dengrow.vercel.app/api/metadata/{id}
  • ⚠️ Cambiar trait a testnet version (funciona con mainnet trait, no crítico)

Opcional

  • ✅ Agregar set-base-uri → Implementado (línea 97-101)
  • ⚠️ Agregar error logging más detallado (future)
  • ⚠️ Optimizar calculate-stage (ahorra ~10 gas, no crítico)

🚀 MAINNET PREPARATION (Pendiente)

Antes de Mainnet:

  • Resolver decisiones de IMPACT_POLICY.md (6 decisiones pendientes)
  • Funding para primeras redemptions (~$20 USD para 20 árboles)
  • Considerar agregar mint fee (ej: 1 STX)
  • Opcional: Cambiar a testnet trait si se redeploya
  • Auditoría externa profesional (recomendado)
  • Emergency pause mechanism (opcional)
  • Timelock para updates críticos (opcional)

Comandos de Deployment

1. Fix Update-Owner Primero

# Editar plant-game.clar línea 156
# Agregar validación de contract-caller

2. Preparar para Testnet

cd packages/contracts

# Compilar y verificar
clarinet check

# Correr tests
pnpm test

# Generar deployment plan
clarinet deployment generate --testnet

3. Deploy a Testnet

# Opción A: Clarinet deploy
clarinet deployment apply -p deployments/default.testnet-plan.yaml

# Opción B: Manual con stacks CLI
stx deploy plant-game contracts/plant-game.clar --network testnet
stx deploy plant-nft contracts/plant-nft.clar --network testnet

4. Verificar Deployment

# Check contract deployed
stx call-read-only <deployer>.plant-nft get-last-token-id --network testnet

# Test mint
stx call <deployer>.plant-nft mint <recipient> --network testnet

Gas Estimates

Función Estimated Cost (μSTX)
initialize-plant ~500
water (first time) ~800
water (with stage change) ~1,200
get-plant (read-only) ~100
can-water (read-only) ~150

Total para 7 waters: ~6,500 μSTX (~0.0065 STX)


Recomendaciones Finales

✅ Testnet (Completado)

  1. FIX CRÍTICO: Validación en update-owner implementada
  2. ✅ Mint público habilitado para testing
  3. ✅ Metadata API implementada con traits y dynamic images
  4. ✅ 103 tests passing con security coverage
  5. ✅ Deployed y funcionando en testnet

Contratos Deployed:

  • ST23SRWT9A0CYMPW4Q32D0D7KT2YY07PQAVJY3NJZ.plant-storage
  • ST23SRWT9A0CYMPW4Q32D0D7KT2YY07PQAVJY3NJZ.plant-game-v1
  • ST23SRWT9A0CYMPW4Q32D0D7KT2YY07PQAVJY3NJZ.plant-nft-v2 (como plant-nft)
  • ST23SRWT9A0CYMPW4Q32D0D7KT2YY07PQAVJY3NJZ.impact-registry

🚀 Para Mainnet (Próximos Pasos)

Bloqueadores (ver IMPACT_POLICY.md):

  1. Definir partner de tree-planting (One Tree Planted recomendado)
  2. Establecer redemption schedule (Lunes semanales propuesto)
  3. Funding inicial para redemptions (~$20 para primeros 20 árboles)
  4. Decidir post-graduation UX (mint again + leaderboard)

Seguridad Adicional (Recomendado):

  1. Auditoría externa profesional
  2. Agregar mint fee system (ej: 1 STX por mint)
  3. Emergency pause mechanism
  4. Timelock para actualizaciones críticas
  5. Bug bounty program

Optimizaciones Opcionales:

  1. Optimizar calculate-stage (ahorra ~10 gas)
  2. Rate limiting en metadata API
  3. Actualizar a testnet trait (si se redeploya)

Historial de Auditorías

Fecha Auditor Status Notas
2026-02-04 Claude Code ⚠️ Issues encontrados 1 crítico, 2 medios, 2 bajos
2026-02-06 Claude Code ✅ Issues resueltos Críticos y medios resueltos, 2 bajos pendientes

Status Actual:SEGURO PARA TESTNET Next Steps: Resolver IMPACT_POLICY.md, preparar mainnet deployment


Última Revisión: 2026-02-06 Auditor: Claude Code (Sonnet 4.5) Recomendación: Continuar con Prioridad 1.2 (Resolver decisiones de impacto) antes de mainnet

There aren’t any published security advisories