fix(dojo-utils): return real address on already-deployed path#3404
fix(dojo-utils): return real address on already-deployed path#3404
Conversation
`Deployer::deploy_via_udc` computes the deterministic UDC-derived contract address, checks `is_deployed` at that address, and when the contract is already there returns `Ok((Felt::ZERO, Noop))`. The real address is computed then dropped. Callers relying on the return value (including everything that expects idempotent deploys) see zero. The `deploy_via_udc_getcall` signature reinforces this — it returns `Option<(Felt, Call)>` where `None` means "already deployed", and the address computed along the way is lost. Change: - `deploy_via_udc_getcall` now returns `(Felt, Option<Call>)`. The address is always surfaced; the Option<Call> encodes whether a deploy call is needed. - `deploy_via_udc` unwraps that to `(address, TransactionResult::Noop)` on the already-deployed path, with the correct address. The one external caller in `sozo-ops::migrate` was pattern-matching `Some((_, call))` / `None` and discarding the address — it adapts to the new shape trivially (`let (_, call) = ...await?; deploy_call = call`), in fact becoming shorter and clearer. Breaking change for any direct consumer of `deploy_via_udc_getcall`, but the return type change is mechanical to fix at each site. Reproduction before this change: run `saya-ops core-contract deploy --salt X` twice against the same L2. First run succeeds, second run returns contract_address="0x0" in its JSON output, which propagates into any downstream orchestration and causes subsequent txns targeting the deployed contract to fail with "Requested contract address 0x0 is not deployed." Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Warning Rate limit exceeded
Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 39 minutes and 29 seconds. ⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (1)
Ohayo sensei! 🙏 Let me break down these changes for you. WalkthroughThe Changes
Estimated code review effort🎯 2 (Simple) | ⏱️ ~10 minutes 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
🧹 Nitpick comments (2)
crates/dojo/utils/src/tx/deployer.rs (2)
51-67: Avoid building UDC calldata before the no-op check, sensei.Since repeated deployments are the path being fixed here, defer the calldata allocation until after
is_deployedreturnsfalse.♻️ Proposed allocation cleanup
- let udc_calldata = [ - vec![class_hash, salt, deployer_address, Felt::from(constructor_calldata.len())], - constructor_calldata.to_vec(), - ] - .concat(); - let contract_address = get_contract_address(salt, class_hash, constructor_calldata, deployer_address); if is_deployed(contract_address, &self.account.provider()).await? { return Ok((contract_address, None)); } + let mut udc_calldata = Vec::with_capacity(4 + constructor_calldata.len()); + udc_calldata.extend_from_slice(&[ + class_hash, + salt, + deployer_address, + Felt::from(constructor_calldata.len()), + ]); + udc_calldata.extend_from_slice(constructor_calldata); + Ok(( contract_address, Some(Call { calldata: udc_calldata, selector: UDC_DEPLOY_SELECTOR, to: UDC_ADDRESS }), ))🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@crates/dojo/utils/src/tx/deployer.rs` around lines 51 - 67, The code currently builds udc_calldata before checking whether the contract is already deployed, causing unnecessary allocation on no-op paths; change the flow in the function that calls get_contract_address, is_deployed, and returns Option<Call> so that you compute contract_address via get_contract_address first, call is_deployed(contract_address, &self.account.provider()).await? and return Ok((contract_address, None)) if deployed, and only after that allocate udc_calldata and construct Some(Call { calldata: udc_calldata, selector: UDC_DEPLOY_SELECTOR, to: UDC_ADDRESS }); this defers the vector allocation until it is actually needed.
78-83: Ohayo sensei — this fixes the no-op address bug.
deploy_via_udcnow preserves the computedcontract_addresswhen returningTransactionResult::Noop; please add/keep a regression test for this exact path so it does not regress again.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@crates/dojo/utils/src/tx/deployer.rs` around lines 78 - 83, deploy_via_udc currently discards the computed contract_address when it returns TransactionResult::Noop; ensure the function (and the call site using deploy_via_udc_getcall) always returns the computed contract_address alongside TransactionResult::Noop (i.e., keep the contract_address value when call is None), and add or retain a regression test that exercises the path where deploy_via_udc/ deploy_via_udc_getcall yields None for call so that the preserved contract_address is asserted in the result.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@crates/dojo/utils/src/tx/deployer.rs`:
- Around line 51-67: The code currently builds udc_calldata before checking
whether the contract is already deployed, causing unnecessary allocation on
no-op paths; change the flow in the function that calls get_contract_address,
is_deployed, and returns Option<Call> so that you compute contract_address via
get_contract_address first, call is_deployed(contract_address,
&self.account.provider()).await? and return Ok((contract_address, None)) if
deployed, and only after that allocate udc_calldata and construct Some(Call {
calldata: udc_calldata, selector: UDC_DEPLOY_SELECTOR, to: UDC_ADDRESS }); this
defers the vector allocation until it is actually needed.
- Around line 78-83: deploy_via_udc currently discards the computed
contract_address when it returns TransactionResult::Noop; ensure the function
(and the call site using deploy_via_udc_getcall) always returns the computed
contract_address alongside TransactionResult::Noop (i.e., keep the
contract_address value when call is None), and add or retain a regression test
that exercises the path where deploy_via_udc/ deploy_via_udc_getcall yields None
for call so that the preserved contract_address is asserted in the result.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: d3b4f062-8b0d-43f7-98c8-d6536c497fb9
📒 Files selected for processing (2)
crates/dojo/utils/src/tx/deployer.rscrates/sozo/ops/src/migrate/mod.rs
Spins up a katana_runner, deploys the predeclared account class at a fixed salt, then verifies both `deploy_via_udc_getcall` and `deploy_via_udc` return the real contract address on the already-deployed path. Before this change the tests would fail with address=Felt::ZERO on the second call; after, both paths return the deterministic UDC-derived address and deploy_via_udc returns TransactionResult::Noop. Uses the same harness pattern as the existing waiter tests (\`#[katana_runner::test]\` + \`RunnerCtx\`) so no new dev-deps needed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
CI's fmt check uses the pinned nightly-2024-08-28 rustfmt which collapses the chained-await blocks and the multi-line assert_eq! in the new test onto single lines. Purely cosmetic. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Claude finished @kariy's task —— View job Analyzing PR changes and assessing documentation impact... Tasks
AnalysisThis PR fixes a bug in the internal Key findings:
Conclusion: No documentation updates are needed because:
|
Saya merge commit f109098 advances dojo-utils past dojoengine/dojo#3404, which fixes `Deployer::deploy_via_udc` to return the real contract address on the already-deployed path (it was returning Felt::ZERO). Downstream impact on this bundle: `deploy-contracts` is now idempotent across `docker compose down && up` cycles when the user's L2 keeps state. Previously the second deploy-contracts run wrote `0x0` into /shared/addresses.env via the jq parse and setup-program panicked. Bundle details: - docker/Dockerfile.saya: SAYA_REV bumped d63a549 → f109098. - .agents/skills/run-tee-stack/troubleshooting.md: expand the saya- version-mismatch section to flag the 0x0-on-redeploy symptom too. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Problem
Deployer::deploy_via_udcis non-idempotent: if a contract is already deployed at the deterministic UDC-derived address, the second invocation returnsOk((Felt::ZERO, TransactionResult::Noop))instead ofOk((real_address, Noop)).The real address IS computed — right before we decide it's already deployed — then dropped:
The outer
deploy_via_udcthen does:Impact
Any caller that relies on the return value to know what got deployed breaks on re-runs. Concrete case: we hit this in
saya-ops core-contract deploy --salt X. First run: works. Second run with the same salt: returnscontract_address: "0x0"in both text and--output jsonoutput. Downstream orchestration (our docker-compose bootstrap,tests/saya-tee/when re-run) writes0x0into state files and the next step panics onRequested contract address 0x0 is not deployed.Fix
Change
deploy_via_udc_getcallto return(Felt, Option<Call>)instead ofOption<(Felt, Call)>. The address is always surfaced; theOption<Call>encodes "does a call need to be made":deploy_via_udcunpacks that cleanly:Breaking change?
deploy_via_udc_getcallsignature changed, so yes — for any direct consumer. There's one in-tree caller (sozo-ops::migrate) which was pattern-matchingSome((_, call))/Noneand discarding the address; it adapts tolet (_, call) = ... ; deploy_call = calland actually gets shorter.deploy_via_udcreturn type is unchanged ((Felt, TransactionResult)). The only behavior change is "the Felt is now correct when TransactionResult::Noop."Test plan
cargo check -p dojo-utils -p sozo-opscleansaya-ops core-contract deploy --salt 0x7eetwice against the same katana L2 dev chain. Before fix: second run returns0x0. After fix: returns the real0x37189b18....Downstream
saya-opshas a local workaround PR (fix(ops): recover real address on dojo-utils already-deployed path saya#64) that recomputes the address on the zero-address path. Once this merges and saya'sdojo-utilsgit dep advances to a commit including this fix, that workaround becomes a no-op and can be removed.🤖 Generated with Claude Code
Summary by CodeRabbit