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.
| Contrato | ID | Wasm Hash | Tamaño |
|---|---|---|---|
| MyToken | CCHAUOEVX6TQD56VFZY2GI3N3HNF5W6QSRRKAGKDXV6S53T4BKD5PYQD |
6613593d... |
9.7 KB |
| PromptMarketplace | CA6RRLV4IBLKRRLPUDCVXZFDKRE77YHBNJFSXEFFLXV6EUAVVVS6HJUQ |
f31af684... |
5.6 KB |
⚠️ Estas instancias son anteriores aset_marketplace. Siguen exponiendomint_forwarded/sell_forwardedsin caller confiable. No las uses para medir economía ni como referencia del modelo de auth actual. Hay que redeployar token + marketplace y bindear conset_marketplaceuna 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.
# 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-darwinEn Linux/CI usa
--target x86_64-unknown-linux-gnu, y en Windows usa--target x86_64-pc-windows-msvc, en vez deaarch64-apple-darwin. El target por defecto del workspace (.cargo/config.toml) eswasm32v1-none, que no soportacargo test— siempre hay que pasar--targetexplícito para correr tests.
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.
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| 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 |
| 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 |
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-marketplaceRequiere 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).
# 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"
⚠️ ADVERTENCIA DE SEGURIDAD — LEER ANTES DE EJECUTARMainnet implica valor real y transacciones irreversibles. Antes de deployar:
- No hardcodees ni hagas
sourcede claves privadas. Usa un secrets manager (1Password CLI, AWS Secrets Manager, HashiCorp Vault, GitHub encrypted secrets, etc.) para inyectarMAINNET_DEPLOYER_SOURCEyMAINNET_ADMIN_SOURCEen runtime.- 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.
- Preferí cuentas multisig o hardware wallets para la cuenta admin (
MAINNET_ADMIN_ADDR). El deployer y el admin pueden ser distintas cuentas.- Revisa los parámetros de constructor. Un error en
owner/admino en eltokendel marketplace puede dejar los contratos incontrolables.- Tené XLM suficiente en la cuenta deployer para cubrir el rent/storage de ambos contratos en mainnet.
- 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
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.
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.shTambién podés usar el target de Makefile:
MAINNET_DEPLOYER_SOURCE="deployer" \
MAINNET_ADMIN_SOURCE="admin" \
MAINNET_ADMIN_ADDR="G..." \
make deploy-mainnetVariables 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:
- Construye ambos contratos con
cargo build --target wasm32v1-none --release. - Calcula el hash SHA-256 de cada WASM.
- Deploya
MyTokenyPromptMarketplacedesdeMAINNET_DEPLOYER_SOURCE. - Inicializa el token con el admin, nombre, símbolo y decimales configurados.
- Inicializa el marketplace con el admin y el
contract_iddel token recién deployado. - 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_iddel token deployado. - Hash WASM on-chain (fetcheado) coincide con el artefacto local.
- Metadata del token (
- Escribe
deploy-artifacts/mainnet-deploy-summary-<timestamp>.json.
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.shO vía Makefile:
MAINNET_TOKEN_ID="C..." \
MAINNET_MARKETPLACE_ID="C..." \
MAINNET_ADMIN_ADDR="G..." \
make verify-mainnetVerifica:
- 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.
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.
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í.
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"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
- 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)
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) llamarequire_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_forwardedymint_forwardedexigenmarketplace.require_auth()antes de mutar balances. - Para operaciones directas (sin marketplace), el token expone
sell(conrequire_auth) ymint(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.