From 1944b29aa320ad998ecc7995b40ee2ab4307f37f Mon Sep 17 00:00:00 2001 From: Ammar Arif Date: Wed, 22 Apr 2026 12:43:16 -0500 Subject: [PATCH] fix(ops): recover real address on dojo-utils already-deployed path MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `dojo-utils::Deployer::deploy_via_udc` computes the deterministic UDC-derived contract address, checks whether a contract is already deployed there via `is_deployed`, and returns early with `Ok((Felt::ZERO, TransactionResult::Noop))` when it is. The real address is computed and then dropped — callers see zero. That's a real problem for us: `saya-ops core-contract deploy --salt X` is meant to be idempotent, but the second invocation with the same salt emits `contract_address: "0x0"` in both text and JSON output. Consumers (including our docker-compose init container) then write 0x0 into the bootstrap manifest and the next step (setup-program) panics on a nonexistent contract. Workaround in saya-ops: when we see `(Felt::ZERO, Noop)`, recompute the address ourselves via `starknet::core::utils::get_contract_address` using the same inputs dojo-utils used. That's exactly what dojo-utils computed and discarded. Verified against a local katana L2 that had already been deployed against with salt 0x7ee from a prior run: Before fix: contract_address: "0x0" After fix: contract_address: "0x37189b1807f1358074b70b3dc8ab79167bbf72cff1296286052f6dfe31c8f15" This is a local defense; the proper fix is in dojo-utils (return `(real_address, Noop)` instead of `(ZERO, Noop)`). Filing that separately against dojoengine/dojo. Co-Authored-By: Claude Opus 4.7 (1M context) --- bin/ops/src/core_contract/utils.rs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/bin/ops/src/core_contract/utils.rs b/bin/ops/src/core_contract/utils.rs index 6aeb77c..4e94287 100644 --- a/bin/ops/src/core_contract/utils.rs +++ b/bin/ops/src/core_contract/utils.rs @@ -12,6 +12,7 @@ use log::{debug, trace, warn}; use starknet::accounts::{Account, SingleOwnerAccount}; use starknet::core::crypto::compute_hash_on_elements; use starknet::core::types::{contract::SierraClass, Call, Felt, FlattenedSierraClass}; +use starknet::core::utils::get_contract_address; use starknet::macros::{selector, short_string}; use starknet::providers::jsonrpc::HttpTransport; use starknet::providers::JsonRpcClient; @@ -139,6 +140,18 @@ pub async fn deploy_contract( .await { Ok((contract_address, transaction_result)) => { + // dojo-utils's `deploy_via_udc` returns `(Felt::ZERO, Noop)` when + // the contract is already deployed at the deterministic UDC-derived + // address — it computes the real address internally then drops it + // before returning. Recompute locally so downstream callers (and + // --output json consumers) see the actual address, not zero. + let contract_address = if contract_address == Felt::ZERO + && matches!(transaction_result, TransactionResult::Noop) + { + get_contract_address(salt, class_hash, constructor_calldata, Felt::ZERO) + } else { + contract_address + }; match &transaction_result { TransactionResult::Noop => { debug!(contract:% = contract_name; "Contract already deployed.");