Skip to content

Repository files navigation

Soroban Smart Contracts — AgentVerseStellar

Smart Contracts CI

Dos contratos Soroban desplegados en Testnet, conectados entre sí:

  • MyToken (my-token): token fungible SEP-0041 con mint gobernado por owner, sell con burn, pausable.
  • PromptMarketplace (prompt-marketplace): marketplace donde admins registran prompts, usuarios compran (quemando tokens), y admins pueden re-mintear.

Deploy en Testnet

Contrato ID Wasm Hash Tamaño
MyToken CCHAUOEVX6TQD56VFZY2GI3N3HNF5W6QSRRKAGKDXV6S53T4BKD5PYQD 6613593d... 9.7 KB
PromptMarketplace CA6RRLV4IBLKRRLPUDCVXZFDKRE77YHBNJFSXEFFLXV6EUAVVVS6HJUQ f31af684... 5.6 KB

⚠️ Estas instancias son anteriores a set_marketplace. Siguen exponiendo mint_forwarded / sell_forwarded sin caller confiable. No las uses para medir economía ni como referencia del modelo de auth actual. Hay que redeployar token + marketplace y bindear con set_marketplace una sola vez.

Arquitectura actual: el marketplace almacena el ID del token en __constructor. El token almacena una única dirección de marketplace Wasm con set_marketplace, ejecutable una sola vez por el owner; una cuenta (G...) o el propio token se rechazan. buy_prompt y buy_private_prompt llaman a my_token::sell_forwarded vía env.invoke_contract; remint llama a my_token::mint_forwarded. Antes de cada sub-invocación el marketplace llama authorize_as_current_contract, y el token rechaza cualquier caller que no sea el marketplace enlazado.


Tests

Unit tests (58)

# Todos los tests (58)
SOROBAN_SDK_BUILD_SYSTEM_SUPPORTS_SPEC_SHAKING_V2=1 cargo test --workspace --target aarch64-apple-darwin

# Por paquete
SOROBAN_SDK_BUILD_SYSTEM_SUPPORTS_SPEC_SHAKING_V2=1 cargo test -p my-token --target aarch64-apple-darwin
SOROBAN_SDK_BUILD_SYSTEM_SUPPORTS_SPEC_SHAKING_V2=1 cargo test -p prompt-marketplace --target aarch64-apple-darwin

En Linux/CI usa --target x86_64-unknown-linux-gnu, y en Windows usa --target x86_64-pc-windows-msvc, en vez de aarch64-apple-darwin. El target por defecto del workspace (.cargo/config.toml) es wasm32v1-none, que no soporta cargo test — siempre hay que pasar --target explícito para correr tests.

Autorización cross-contract en Soroban v25

Soroban v25's mock auth no puede satisfacer un SEGUNDO require_auth() para la MISMA address dentro de un mismo árbol de invocación: si la invocación raíz llama buyer.require_auth() y luego una sub-invocación (marketplace → token vía invoke_contract) también llama require_auth() para buyer, el host rechaza con Error(Auth, ExistingValue) — mock auth no tiene forma de representar "esta address ya autorizó más arriba en el árbol".

Mitigación adoptada en este código: sell_forwarded y mint_forwarded (contracts/tokens/src/contract.rs) solo mutan balances si el marketplace Wasm enlazado con set_marketplace autoriza la llamada. El marketplace hace esa autorización con authorize_as_current_contract justo antes de invocar al token. Una llamada directa externa, una llamada sin marketplace configurado, una llamada desde otro marketplace, o un binding a una cuenta (G...) falla antes de cambiar balance o supply.

scripts/integration-test.sh ejercita el mismo flujo de punta a punta contra testnet real con firmas genuinas (Soroban CLI), que es el único lugar donde un require_auth() anidado genuino (si se reintrodujera por error) sería detectado.

Integration test (testnet)

Prueba el flujo completo contra testnet real: mint → register → buy (cross-contract) → verify → remint → verify.

# Requiere contratos recién desplegados y bindeados. Los IDs de la tabla de
# Testnet de arriba NO sirven: no implementan get_marketplace.
TOKEN="<MY_TOKEN_ID>" \
MKT="<MARKETPLACE_ID>" \
bash scripts/integration-test.sh

Token: 21 tests

Test Qué cubre
test_token_mint_and_balance Mint vía storage API + balance + total_supply
test_token_metadata name, symbol, decimals
test_mint_multiple_same_user Mint acumulativo a la misma address
test_mint_to_different_users Mint a múltiples usuarios, supply tracking
test_mint_overflow_panics i128::MAX + 1 debe panic
test_zero_balance_default Balance por defecto es 0
test_set_marketplace_stores_address Owner configura el marketplace confiable
test_get_marketplace_fails_before_binding Leer marketplace falla si aun no fue configurado
test_set_marketplace_requires_owner No-owner no puede configurar el marketplace; el binding no se escribe
test_set_marketplace_cannot_retarget El marketplace no puede retargetearse silenciosamente
test_set_marketplace_rejects_account_address Una cuenta (G...) no puede ser el marketplace
test_set_marketplace_rejects_token_self El token no puede bindearse a sí mismo
test_sell_forwarded_fails_without_marketplace_binding Burn forwarded falla si no hay marketplace configurado
test_sell_forwarded_fails_from_direct_external_call Llamada directa externa no puede quemar tokens
test_sell_forwarded_rejects_holder_auth_without_marketplace_caller Auth del holder no sustituye al caller marketplace
test_sell_forwarded_updates_balance Happy path: marketplace auth quema tokens y emite SellEvent
test_mint_forwarded_fails_without_marketplace_binding Mint forwarded falla si no hay marketplace configurado
test_mint_forwarded_fails_from_direct_external_call Llamada directa externa no puede mintear tokens
test_mint_forwarded_rejects_owner_auth_without_marketplace_caller Auth del owner no sustituye al caller marketplace
test_mint_forwarded_updates_balance Happy path: marketplace auth mintea tokens y emite MintEvent
test_mint_forwarded_rejects_non_positive_amount Amount 0 paniquea en el camino forwarded

Marketplace: 36 tests

Test Qué cubre
test_register_and_query_prompt Happy path: register → get_price / get_owner
test_duplicate_registration_panics Mismo ID no se puede registrar dos veces
test_update_price Admin cambia precio
test_remove_prompt Admin elimina prompt (idempotente)
test_multiple_prompts_independent Prompts distintos no interfieren
test_get_price_unregistered_panics Consultar precio de prompt inexistente
test_non_admin_cannot_register No-admin no puede registrar
test_non_admin_cannot_update_price No-admin no puede cambiar precio
test_non_admin_cannot_remove No-admin no puede eliminar
test_register_zero_price_panics Precio 0 es inválido
test_update_price_zero_panics Actualizar a precio 0 es inválido
test_register_max_price i128::MAX funciona como precio
test_update_unregistered_prompt_panics Actualizar precio de prompt inexistente
test_register_after_remove Re-registrar mismo ID post-eliminación
test_has_access_unregistered has_access sin compra devuelve false
test_token_mint_and_balance Mint vía storage en contexto del token
test_buy_prompt_cross_contract E2E: register → buy_prompt (cross-contract real vía invoke_contract) → balance quemado
test_has_access_after_buy has_access es false antes de comprar y true después de buy_prompt
test_buy_prompt_emits_event buy_prompt emite PromptPurchased con buyer/prompt_id/price correctos
test_remint_cross_contract E2E: remint (cross-contract real vía invoke_contract) → balance minteado
test_remint_emits_event remint emite TokensReminted con admin/to/amount correctos
test_unbound_marketplace_cannot_burn_forwarded_tokens Un marketplace no enlazado no puede quemar balances por forwarded burn
test_unbound_marketplace_cannot_mint_forwarded_tokens Un marketplace no enlazado no puede mintear por forwarded mint

Build WASM

SOROBAN_SDK_BUILD_SYSTEM_SUPPORTS_SPEC_SHAKING_V2=1 stellar contract build --package my-token
SOROBAN_SDK_BUILD_SYSTEM_SUPPORTS_SPEC_SHAKING_V2=1 stellar contract build --package prompt-marketplace

Requiere el target wasm32v1-none y la env var SOROBAN_SDK_BUILD_SYSTEM_SUPPORTS_SPEC_SHAKING_V2=1 para Spec Shaking v2. Alternativamente, para mainnet se usa cargo build --target wasm32v1-none --release (ver sección Deploy en Mainnet).


Deploy

# Token
stellar contract deploy \
  --wasm target/wasm32v1-none/release/my_token.wasm \
  --source default \
  --network testnet \
  --alias my_token \
  -- \
  --owner "$(stellar keys address default)" \
  --name "PromptToken" \
  --symbol "PRMPT" \
  --decimals 7

# Marketplace (usar el ID del token recién deployado)
stellar contract deploy \
  --wasm target/wasm32v1-none/release/prompt_marketplace.wasm \
  --source default \
  --network testnet \
  --alias prompt_marketplace \
  -- \
  --admin "$(stellar keys address default)" \
  --token "$TOKEN_ID"

# Bind token -> marketplace once (IDs reales del deploy de arriba, no los de la tabla)
stellar contract invoke \
  --id "$TOKEN_ID" \
  --source default \
  --network testnet \
  --send=yes \
  -- \
  set_marketplace \
  --marketplace "$MARKETPLACE_ID"

Deploy en Mainnet

⚠️ ADVERTENCIA DE SEGURIDAD — LEER ANTES DE EJECUTAR

Mainnet implica valor real y transacciones irreversibles. Antes de deployar:

  1. No hardcodees ni hagas source de claves privadas. Usa un secrets manager (1Password CLI, AWS Secrets Manager, HashiCorp Vault, GitHub encrypted secrets, etc.) para inyectar MAINNET_DEPLOYER_SOURCE y MAINNET_ADMIN_SOURCE en runtime.
  2. Verifica el código y el WASM antes de deployar. Recompila desde una fuente confiable, calcula el hash SHA-256 y comparalo con el artefacto que vas a subir.
  3. Preferí cuentas multisig o hardware wallets para la cuenta admin (MAINNET_ADMIN_ADDR). El deployer y el admin pueden ser distintas cuentas.
  4. Revisa los parámetros de constructor. Un error en owner/admin o en el token del marketplace puede dejar los contratos incontrolables.
  5. Tené XLM suficiente en la cuenta deployer para cubrir el rent/storage de ambos contratos en mainnet.

Prerrequisitos

  • Stellar CLI configurado para mainnet (stellar network add mainnet ... o variables de entorno equivalentes).
  • Acceso a un RPC endpoint de mainnet (Stellar public RPC, Blockdaemon, etc.).
  • Cuenta con XLM real para pagar fees y rent.
  • Cuenta admin separada (recomendado) con su par de claves seguras.
  • Target de compilación instalado: rustup target add wasm32v1-none.
  • Variable de entorno para Spec Shaking V2:
    export SOROBAN_SDK_BUILD_SYSTEM_SUPPORTS_SPEC_SHAKING_V2=1

Migración de instancias existentes

Las instancias desplegadas antes de set_marketplace no deben usarse para valor real. Para migrar, despliega un nuevo MyToken, despliega un nuevo PromptMarketplace apuntando a ese token, ejecuta set_marketplace una sola vez en el token con el ID del marketplace nuevo y verifica get_marketplace. Los balances y compras existentes no se reinterpretan automáticamente; cualquier migración de balances debe ser una operación explícita, revisada y auditable.

Scripts de mainnet

scripts/deploy-mainnet.sh

Build release, calcula hashes, deploya, inicializa y valida ambos contratos. Emite un resumen JSON en deploy-artifacts/.

MAINNET_DEPLOYER_SOURCE="deployer" \
MAINNET_ADMIN_SOURCE="admin" \
MAINNET_ADMIN_ADDR="G..." \
bash scripts/deploy-mainnet.sh

También podés usar el target de Makefile:

MAINNET_DEPLOYER_SOURCE="deployer" \
MAINNET_ADMIN_SOURCE="admin" \
MAINNET_ADMIN_ADDR="G..." \
make deploy-mainnet

Variables opcionales:

Variable Descripción Default
MAINNET_TOKEN_NAME Nombre del token AgentVerse Token
MAINNET_TOKEN_SYMBOL Símbolo del token AVT
MAINNET_TOKEN_DECIMALS Decimales 7
MAINNET_DEPLOY_OUT_DIR Carpeta de artefactos ./deploy-artifacts

El script:

  1. Construye ambos contratos con cargo build --target wasm32v1-none --release.
  2. Calcula el hash SHA-256 de cada WASM.
  3. Deploya MyToken y PromptMarketplace desde MAINNET_DEPLOYER_SOURCE.
  4. Inicializa el token con el admin, nombre, símbolo y decimales configurados.
  5. Inicializa el marketplace con el admin y el contract_id del token recién deployado.
  6. Valida post-deploy:
    • Metadata del token (name, symbol, decimals).
    • Supply inicial igual a 0.
    • Owner del token igual a MAINNET_ADMIN_ADDR.
    • Admin del marketplace igual a MAINNET_ADMIN_ADDR.
    • Token del marketplace igual al contract_id del token deployado.
    • Hash WASM on-chain (fetcheado) coincide con el artefacto local.
  7. Escribe deploy-artifacts/mainnet-deploy-summary-<timestamp>.json.

scripts/verify-mainnet.sh

Verifica un par de contratos ya deployados sin re-deployar. Útil para CI o validaciones periódicas.

MAINNET_TOKEN_ID="C..." \
MAINNET_MARKETPLACE_ID="C..." \
MAINNET_ADMIN_ADDR="G..." \
bash scripts/verify-mainnet.sh

O vía Makefile:

MAINNET_TOKEN_ID="C..." \
MAINNET_MARKETPLACE_ID="C..." \
MAINNET_ADMIN_ADDR="G..." \
make verify-mainnet

Verifica:

  • Hash WASM on-chain vs. artefacto local.
  • Admin/owner de ambos contratos.
  • Link token ↔ marketplace.
  • Supply inicial (0).
  • Metadata del token.

Escribe deploy-artifacts/mainnet-verify-report-<timestamp>.json.

Multisig / DAuthorization (roadmap v2)

La v1 actual soporta cuentas multisig configuradas en Stellar CLI (el CLI pedirá/co-firmará las transacciones). Para una v2 se documentará como mejora:

  • Flujo de firmas separadas: el deployer crea la transacción, múltiples signers la firman off-line, y alguien la publica.
  • DAuthorization: delegar privilegios admin a un módulo de gobernanza on-chain.

Mainnet Release Gate

Antes de ejecutar deploy-mainnet.sh con fondos reales, este release está sujeto al gate de #26. El checklist maestro, con estado de cada criterio de aceptación, vive en docs/security/MAINNET_RELEASE_CHECKLIST.md.

Documentos y scripts del paquete de release:

Entregable Qué cubre
docs/security/MAINNET_RELEASE_CHECKLIST.md Checklist maestro: mapea cada bullet de la solución propuesta y cada criterio de aceptación del #26 a su estado real.
docs/security/MAINNET_CUSTODY_POLICY.md Separación deployer/admin, política de multisig/hardware wallet, rotación ante compromiso.
docs/security/AUDIT_PROCESS.md Proceso de revisión de seguridad independiente, severidades, SLA, plantilla de sign-off (vacía hasta que ocurra una revisión real).
docs/operations/MONITORING_AND_INCIDENT_RESPONSE.md Señales de monitoreo, niveles de incidente, criterios de pausa, rollback/migración, dueños nombrados.
scripts/testnet-dry-run.sh (make testnet-dry-run) Deploy fresco en Testnet + escenarios adversariales, presupuesto de recursos, drill de pausa/recuperación y reconciliación.
scripts/canary-mainnet.sh (make canary-mainnet) Compra canary capada y con confirmación explícita contra Mainnet ya deployado y re-verificado; valida settlement, entrega y reconciliación.

Esta issue depende de #9 (política de procedencia de dependencias) — ese trabajo se resuelve en #9, no se duplica aquí.

Invocar

Los IDs de abajo son las instancias viejas de Testnet (sin set_marketplace). Sirven solo para lecturas de metadata, no para el flujo de compra actual.

# Leer nombre del token
stellar contract invoke --source default --network testnet \
  --id CCHAUOEVX6TQD56VFZY2GI3N3HNF5W6QSRRKAGKDXV6S53T4BKD5PYQD \
  -- name

# Registrar prompt
stellar contract invoke --source default --network testnet \
  --id CA6RRLV4IBLKRRLPUDCVXZFDKRE77YHBNJFSXEFFLXV6EUAVVVS6HJUQ \
  --send=yes \
  -- register_prompt --prompt_id "alpha" --price 500 \
  --owner "$(stellar keys address default)"

# Consultar precio
stellar contract invoke --source default --network testnet \
  --id CA6RRLV4IBLKRRLPUDCVXZFDKRE77YHBNJFSXEFFLXV6EUAVVVS6HJUQ \
  -- get_price --prompt_id "alpha"

Estructura del proyecto

contracts/
├── tokens/                      # MyToken (token fungible)
│   ├── Cargo.toml
│   └── src/
│       ├── lib.rs               # module exports, re-export público
│       ├── contract.rs          # #[contract] MyToken — API pública + traits
│       ├── events.rs            # #[contractevent] structs (MintEvent, SellEvent, etc.)
│       ├── core/
│       │   └── token.rs         # TokenManager — lógica de negocio
│       ├── storage/
│       │   └── types.rs         # DataKey, contracttype structs
│       └── tests.rs             # 21 tests
└── marketplace/                 # PromptMarketplace
    ├── Cargo.toml
    └── src/
        ├── lib.rs               # module exports
        ├── contract.rs          # #[contract] PromptMarketplace — API pública
        ├── storage/
        │   └── types.rs         # DataKey, Prompt struct, errors
        └── tests.rs             # 36 tests
Cargo.toml                       # workspace: tokens + marketplace

Stack

  • Soroban SDK: 25.3.0
  • OZ Stellar Contracts: v0.7.1 (fungible token, ownable, pausable)
  • Stellar CLI: 26.1.0
  • Target: wasm32v1-none (release), aarch64-apple-darwin (tests)

Nota sobre autenticación cross-contract

soroban-sdk v25 tiene una limitación: require_auth() para una misma dirección solo puede ejecutarse UNA VEZ por árbol de llamadas. Llamarlo en la root invocation y luego en una sub-invocación (via invoke_contract) falla con Error(Auth, ExistingValue).

Patrón adoptado:

  • La función raíz (ej. buy_prompt, buy_private_prompt, remint) llama require_auth() para buyer/admin.
  • El marketplace autoriza la llamada al token con authorize_as_current_contract.
  • El token guarda un único marketplace Wasm con set_marketplace; sell_forwarded y mint_forwarded exigen marketplace.require_auth() antes de mutar balances.
  • Para operaciones directas (sin marketplace), el token expone sell (con require_auth) y mint (con #[only_owner]).

Esto aplica también a TokenManager::sell que usa Base::update en vez de Base::burn para evitar el doble require_auth de Base::burn.

Ver Autorización cross-contract en Soroban v25 para cómo este patrón se prueba en los tests.

About

Tokenizer smart contracts

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages