Skip to content

Commit 6b6ccc2

Browse files
committed
docs(soroban): add wasm size guidance for agents
1 parent b5bc579 commit 6b6ccc2

1 file changed

Lines changed: 329 additions & 0 deletions

File tree

contract/vault/soroban/AGENTS.md

Lines changed: 329 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,329 @@
1+
# Soroban Vault Agent Guide
2+
3+
This file contains Soroban-specific guidance for future agents working in `contract/vault/soroban`.
4+
5+
## Scope
6+
7+
This directory is the Soroban executor layer for the shared vault kernel plus the closely related
8+
Soroban-side companion contracts:
9+
10+
- `templar-soroban-runtime` in `contract/vault/soroban`
11+
- `templar-soroban-governance` in `contract/vault/soroban/governance`
12+
- `templar-soroban-share-token` in `contract/vault/soroban/share-token`
13+
- `templar-soroban-blend-adapter` in `contract/vault/soroban/blend-adapter`
14+
- shared ABI/types in `contract/vault/soroban/shared-types`
15+
- the Soroban ERC-4626 proxy in `contract/proxy-4626-soroban`
16+
17+
Read these first before making non-trivial changes:
18+
19+
- `contract/vault/soroban/README.md`
20+
- `contract/vault/soroban/STRIDE.md`
21+
- `contract/vault/soroban/SIZE_BUDGET.md`
22+
- `contract/vault/soroban/src/contract/entrypoints.rs`
23+
- `contract/vault/soroban/src/effects/mod.rs`
24+
- `contract/vault/soroban/src/storage/mod.rs`
25+
- `contract/vault/soroban/governance/src/lib.rs`
26+
- `contract/vault/soroban/share-token/src/lib.rs`
27+
- `contract/vault/soroban/blend-adapter/src/lib.rs`
28+
- `contract/vault/soroban/shared-types/src/lib.rs`
29+
30+
## Why This Area Is High Risk
31+
32+
- The runtime is the Soroban execution boundary for the canonical vault kernel state machine.
33+
- It owns custody, storage serialization, address mapping, auth enforcement, and effect execution.
34+
- Soroban artifact size is a shipping constraint, not a cleanup task.
35+
- Governance is split across contracts: timelock/orchestration lives in the governance contract,
36+
but the runtime still applies the canonical state changes.
37+
38+
## Canonical Invariants
39+
40+
Preserve these unless the architecture is being changed deliberately and the change is documented,
41+
measured, and re-verified:
42+
43+
- The runtime is the sole canonical owner of custody, `total_assets`, `total_shares`, fee anchor,
44+
payout settlement, and accepted external-asset accounting state.
45+
- Governance proposal submission, timelock maturity, approval/revocation, and abdication live in
46+
the governance contract, but vault-bound mutations are still applied by the runtime via
47+
`execute_governance(env, caller, payload)`.
48+
- The share token only allows vault-authorized `mint()` and `burn()`. User transfers still require
49+
`from.require_auth()`.
50+
- Read-only preview and getter surfaces must stay non-authoritative.
51+
- Serialized `VaultState` remains a practical resource boundary because Soroban persists a single
52+
`StateBlob`. Pending withdrawals are the main long-lived growth vector.
53+
- `extend_ttl()` is permissionless and is part of the operational liveness model.
54+
55+
## Binary Size Gate
56+
57+
The Soroban runtime deploy artifact must stay at or below `128 KiB` (`131072` bytes).
58+
59+
Use these commands:
60+
61+
- `just -f contract/vault/soroban/justfile build`
62+
- `just -f contract/vault/soroban/justfile size-budget-check`
63+
- `just -f contract/vault/soroban/justfile wasm-analyze 250 120`
64+
- `just -f contract/vault/soroban/justfile wasm-analyze-print all 120`
65+
66+
Important details:
67+
68+
- The size gate checks `target/wasm32-unknown-unknown/release-soroban/templar_soroban_runtime.deploy.wasm`.
69+
- `build` also emits `templar_soroban_runtime.optimized.wasm`; the deploy artifact is the
70+
contractspec-stripped one.
71+
- `scripts/strip_contractspec.py` typically saves about `7 KiB` by removing `contractspecv0`.
72+
- The current recovered evidence from `2026-04-23` recorded `137127` bytes, which is `6055`
73+
bytes over budget.
74+
75+
Common growth pitfalls:
76+
77+
- Duplicate normalization logic at runtime boundaries
78+
- Extra allocation-heavy helper layers in hot or state-heavy paths
79+
- Generic-heavy shared helpers and command surfaces that increase monomorphization
80+
- Public ABI/event/spec changes that pull in more generated metadata
81+
82+
When the size gate fails:
83+
84+
1. Rebuild with `just -f contract/vault/soroban/justfile build`.
85+
2. Run `just -f contract/vault/soroban/justfile size-budget-check`.
86+
3. Run `just -f contract/vault/soroban/justfile wasm-analyze 250 120`.
87+
4. Prefer deleting duplicated logic or narrowing compile surface before introducing new helper
88+
layers or architectural churn.
89+
5. Treat low-delta slimming as the default path. Escalate to a topology split only if measured
90+
evidence still leaves the runtime above `131072` bytes.
91+
92+
## Release WASM Inspection Workflow
93+
94+
Do not stop at `size-budget-check`. When the artifact grows, inspect the built release artifacts
95+
directly and keep the commands/results in the task notes or PR description.
96+
97+
Release artifacts to inspect after `just -f contract/vault/soroban/justfile build`:
98+
99+
- `target/wasm32-unknown-unknown/release-soroban/templar_soroban_runtime.wasm`
100+
- `target/wasm32-unknown-unknown/release-soroban/templar_soroban_runtime.optimized.wasm`
101+
- `target/wasm32-unknown-unknown/release-soroban/templar_soroban_runtime.deploy.wasm`
102+
103+
Recommended workflow:
104+
105+
1. Confirm exact byte sizes.
106+
2. Inspect section layout with `wasm-objdump`.
107+
3. Use `twiggy` to find the biggest retained items and dominator chains.
108+
4. If needed, dump WAT and inspect suspicious symbols, long match arms, repeated helpers, or
109+
unexpectedly large data/custom sections.
110+
5. Compare before/after outputs when evaluating a refactor. Do not rely on intuition.
111+
112+
Commands:
113+
114+
- `stat -c '%s %n' target/wasm32-unknown-unknown/release-soroban/templar_soroban_runtime.wasm target/wasm32-unknown-unknown/release-soroban/templar_soroban_runtime.optimized.wasm target/wasm32-unknown-unknown/release-soroban/templar_soroban_runtime.deploy.wasm`
115+
- `wasm-objdump -h target/wasm32-unknown-unknown/release-soroban/templar_soroban_runtime.deploy.wasm`
116+
- `wasm-objdump -x target/wasm32-unknown-unknown/release-soroban/templar_soroban_runtime.deploy.wasm > /tmp/templar_soroban_runtime.deploy.objdump.txt`
117+
- `twiggy top target/wasm32-unknown-unknown/release-soroban/templar_soroban_runtime.wasm -n 80`
118+
- `twiggy dominators target/wasm32-unknown-unknown/release-soroban/templar_soroban_runtime.wasm`
119+
- `twiggy monos target/wasm32-unknown-unknown/release-soroban/templar_soroban_runtime.wasm`
120+
- `wasm2wat target/wasm32-unknown-unknown/release-soroban/templar_soroban_runtime.deploy.wasm -o /tmp/templar_soroban_runtime.deploy.wat`
121+
122+
What to look for:
123+
124+
- Large `code` or `data` sections in `wasm-objdump -h`
125+
- Unexpected custom sections that survived stripping
126+
- Large monomorphized functions in `twiggy monos`
127+
- Dominator chains caused by generic-heavy helpers, large enum dispatch, or duplicated conversion
128+
paths
129+
- ABI/spec/event-related growth after adding command variants, event payload fields, or public
130+
methods
131+
- Helper layers that look small in Rust source but retain large downstream call trees in `twiggy`
132+
133+
Interpretation guidance:
134+
135+
- If `deploy.wasm` grew but `optimized.wasm` did not, check stripping/output-path issues first.
136+
- If both `optimized.wasm` and `deploy.wasm` grew similarly, inspect retained code shape with
137+
`twiggy`.
138+
- If section growth is concentrated in custom/data sections, inspect serialization payloads,
139+
event/spec metadata, and embedded strings before changing control flow.
140+
- If code growth is concentrated in a few dominators, attack those first. Small deletions there
141+
can collapse a large retained subtree.
142+
- Prefer comparing `twiggy top`, `twiggy dominators`, and `wasm-objdump -h` before and after a
143+
patch. Size debugging should be evidence-driven.
144+
145+
## Codegen Reduction Tips
146+
147+
Primary reference:
148+
149+
- Rust and WebAssembly code-size guide: <https://rustwasm.github.io/docs/book/reference/code-size.html>
150+
151+
This workspace already uses strong size-oriented release settings in the root `Cargo.toml`:
152+
153+
- `codegen-units = 1`
154+
- `opt-level = "z"`
155+
- `lto = "fat"`
156+
- `panic = "abort"`
157+
- `strip = "symbols"`
158+
- Soroban release profile with `overflow-checks = false`
159+
160+
Do not casually undo those. Also remember that Cargo only reads profile settings from the
161+
workspace-root `Cargo.toml`, not from member crates.
162+
163+
When the runtime bloats, check these levers before reaching for a bigger refactor:
164+
165+
- Measure `opt-level = "s"` versus `opt-level = "z"` for `release-soroban`. Rust’s official docs
166+
note that `"s"` can sometimes produce a smaller binary than `"z"`. Do not assume `"z"` wins.
167+
- Keep release builds non-incremental and size-oriented. Incremental compilation trades away some
168+
optimizations and is not the right mode for artifact measurement.
169+
- Confirm debug info and symbol/name sections are actually gone before changing source. Cargo’s
170+
profile docs and the Rust/Wasm guide both note that debug info and the wasm `names` section can
171+
be major size contributors. In this repo, `debug = false`, `strip = "symbols"`, and the
172+
optimizer/strip pipeline should already remove them, so unexpected growth here usually means a
173+
build-path regression.
174+
- Check for duplicate crate versions with `cargo tree -d`. Cargo’s docs note that avoiding multiple
175+
versions of the same package can help executable size as well as build time.
176+
- Inspect feature flow with `cargo tree -e features` and `cargo tree -e features -i <crate>`.
177+
Cargo feature unification can silently turn on defaults or widen a dependency’s surface far
178+
beyond what this crate asked for directly.
179+
- Prune dependency features aggressively. In size-critical crates, prefer `default-features = false`
180+
when viable, and do not enable convenience features in the runtime unless they are required in
181+
shipping WASM.
182+
- Treat every new public method, event field, command variant, and shared-type variant as a size
183+
decision. Public ABI growth often pulls both more code and more spec metadata into the artifact.
184+
- Reduce monomorphization pressure. Be suspicious of generic-heavy helper layers, duplicated
185+
conversion helpers, broad enums, and wrappers that instantiate the same logic across many type
186+
combinations.
187+
- Audit inlining decisions explicitly. `#[inline]` and especially `#[inline(always)]` can improve
188+
speed while making wasm larger by cloning helper bodies into many call sites. In size-critical
189+
code, do not add inline attributes casually, and re-check existing ones when a function starts
190+
dominating `twiggy` output.
191+
- For hot generic helpers used with many concrete types, consider whether a trait-object or
192+
non-generic boundary would be smaller. The Rust and WebAssembly guidance explicitly calls out
193+
generic monomorphization as a common source of wasm bloat. Do not change dispatch style blindly;
194+
measure the tradeoff.
195+
- Dynamic dispatch is a size lever, not a default style rule. If the same generic helper is being
196+
instantiated across many concrete types, a trait object like `&dyn Trait` can collapse the code
197+
to one emitted implementation. The tradeoff is lost specialization and indirect-call overhead.
198+
Use it only where the call frequency and optimization loss are acceptable.
199+
- Consider `#[inline(never)]` on cold, reused helpers if `twiggy` or LLVM IR suggests they are
200+
being inlined into many call sites and inflating the code section. Do this surgically and only
201+
after measurement; forcing no-inline on hot code can hurt performance and occasionally even block
202+
other size wins.
203+
- Avoid string formatting in release-critical runtime paths when static strings or compact error
204+
codes are sufficient. The Rust and WebAssembly guidance calls out `format!`, `to_string`, and
205+
related formatting machinery as a common source of code growth.
206+
- Avoid panicking paths in shipping runtime code when a normal error return will do. Panics and
207+
panic formatting can retain surprising amounts of code. If an invariant truly cannot fail, keep
208+
the path minimal and document why.
209+
- Reuse the existing kernel abort helpers in
210+
[contract/vault/kernel/src/abort.rs](/data/projects/contracts/contract/vault/kernel/src/abort.rs)
211+
when you need a documented impossible-path trap on wasm32. In particular, prefer the existing
212+
`abort!`, `unwrap_abort!`, and `unwrap_abort_result!` macros over introducing fresh panic-heavy
213+
helpers. These already compile to `core::arch::wasm32::unreachable()` on wasm32 while preserving
214+
a normal panic in non-wasm test builds.
215+
- Watch for implicit panics, not just `panic!()`: indexing, division, and `unwrap()` can all pull
216+
in panic machinery. Prefer `.get()`, checked arithmetic, and explicit error handling where that
217+
preserves the intended semantics.
218+
- Prefer one shared implementation over many near-identical helpers, but only if the shared path
219+
does not introduce a larger generic surface. Measure both shapes if unsure.
220+
- Be careful with derives and serialization surface area. Extra derived impls and broad serde/postcard
221+
reachability can retain code that looks cheap at the source level.
222+
- If `twiggy` shows allocator symbols such as `dlmalloc`, `__rust_alloc`, or `__rust_realloc` high
223+
in the retained-size list, treat allocation reduction as a first-class size tactic. The Rust and
224+
WebAssembly guide notes that the default wasm allocator is roughly ten kilobytes. Avoiding
225+
allocation entirely is best; switching allocators is only worth considering after compatibility
226+
and runtime-cost review.
227+
- Re-check whether the growth is in the Rust-generated code or in custom/data/spec sections before
228+
editing logic. If the bytes are not in `code`, logic changes may be the wrong fix.
229+
- If the optimizer path changes, compare the raw release artifact against the optimizer output.
230+
The Rust and Wasm guidance notes that post-processing with `wasm-opt -Os` or `-Oz` can save
231+
additional size, and our Soroban build already relies on the Stellar optimizer path for this
232+
reason.
233+
- `wasm-opt` is still worth considering when the Soroban optimizer path changes or when you need an
234+
independent measurement. Binaryen’s docs describe `wasm-opt` as the standard post-link optimizer
235+
for making wasm smaller and faster. It is not currently installed in this workspace, so do not
236+
add it as a required step without updating the tooling docs.
237+
- `wasm-snip` is a last-resort tool, not a default optimization pass. The Rust and WebAssembly
238+
guide suggests it mainly for code such as panic infrastructure that provably cannot execute at
239+
runtime. For contracts, do not use it unless the removed path is demonstrably unreachable and
240+
the result is re-optimized and re-tested.
241+
- If `twiggy` is not enough to explain a large function, inspect LLVM IR for the release wasm build:
242+
`cargo rustc --profile release-soroban --target wasm32-unknown-unknown -p templar-soroban-runtime -- --emit llvm-ir`
243+
Then inspect the generated `.ll` file in `target/wasm32-unknown-unknown/release-soroban/deps/`.
244+
The Rust and WebAssembly guide recommends this when you need to see what got inlined into a
245+
retained function.
246+
247+
Practical order of attack:
248+
249+
1. Measure section growth with `wasm-objdump -h`.
250+
2. Attribute retained code with `twiggy top`, `dominators`, and `monos`.
251+
3. Check `cargo tree -d` and `cargo tree -e features -i <crate>` before assuming the bloat is in
252+
your own code.
253+
4. Trim features, duplicate versions, and ABI/spec surface before changing architecture.
254+
5. Compare `"s"` vs `"z"` if code shape changed materially.
255+
6. Check for accidental inlining via `#[inline]`, `#[inline(always)]`, or large cold helpers being
256+
cloned into many sites.
257+
7. Use LLVM IR inspection if a retained giant still lacks a clear source-level cause.
258+
8. Escalate to a larger split only after the measured low-delta levers are exhausted.
259+
260+
## Security Notes
261+
262+
- `initialize()` is highly sensitive because front-running it would seize governance/curator
263+
control. Keep deployment and initialization assumptions explicit.
264+
- Review auth on every privileged Soroban entrypoint. Do not rely on outer routing alone.
265+
- Soroban transactions are atomic, but adapter correctness, state ordering, and accepted external
266+
asset snapshots still matter for accounting safety.
267+
- Changes to postcard serialization, versioning, migration, or storage keys are security-relevant.
268+
- The kernel-to-Soroban address mapping is critical for effect routing. Treat changes there as
269+
high impact.
270+
- `withdraw()` and `redeem()` include an idle-only atomic path that bypasses the queued withdrawal
271+
lifecycle. Do not change queue semantics without checking both paths.
272+
- Governance abdication is irreversible. Any change to governance action kind mapping or timelock
273+
policy needs a high-suspicion review.
274+
- `RemoveMarket`, skim recipient changes, and share-token authority changes can all become
275+
authority drift or asset-loss bugs if altered casually.
276+
277+
## Working Norms
278+
279+
- Prefer small, measurable patches over broad Soroban refactors.
280+
- If you touch anything that can affect size, measure the artifact after the change.
281+
- Preserve the existing contract split: runtime, governance, share-token, and blend adapter each
282+
have distinct authority boundaries.
283+
- Avoid broad shared-type expansion. Shared ABI crates can silently increase runtime size.
284+
- Be explicit about wire formats, storage formats, and event payloads.
285+
- Keep tests out of runtime implementation modules. Do not add inline `#[cfg(test)] mod tests`
286+
blocks to files such as `src/contract/entrypoints.rs`; put runtime tests in `src/tests.rs` or
287+
integration tests under `tests/`.
288+
- If behavior changes around governance bridging, withdrawal lifecycle, or share-token auth,
289+
update the docs in this directory in the same change.
290+
291+
## Verification
292+
293+
Minimum runtime verification:
294+
295+
- `cargo test -p templar-soroban-runtime -- --nocapture`
296+
- `cargo test -p templar-soroban-runtime --test integration_tests -- --nocapture`
297+
- `cargo test -p templar-soroban-runtime --test property_tests -- --nocapture`
298+
299+
Size verification:
300+
301+
- `just -f contract/vault/soroban/justfile build`
302+
- `just -f contract/vault/soroban/justfile size-budget-check`
303+
304+
When relevant, also run:
305+
306+
- `cargo test -p templar-soroban-governance -- --nocapture`
307+
- `cargo test -p templar-soroban-share-token -- --nocapture`
308+
- `cargo test -p templar-soroban-blend-adapter -- --nocapture`
309+
- `just -f contract/vault/soroban/justfile check-4626-proxy`
310+
311+
Use `wasm-analyze` when:
312+
313+
- the deploy artifact grows unexpectedly
314+
- you add a new shared type, command variant, event shape, or generic helper
315+
- you move code across runtime/governance/share-token/adapter boundaries
316+
317+
Use direct `twiggy` and `wasm-objdump` on the release artifacts when:
318+
319+
- a change regresses `size-budget-check`
320+
- you need to know whether growth came from code, data, or custom sections
321+
- you need to attribute bloat to a specific retained function tree rather than a crate-level guess
322+
323+
If an important verification step cannot run, say so explicitly.
324+
325+
## Documentation Maintenance
326+
327+
- Keep `README.md`, `STRIDE.md`, `SIZE_BUDGET.md`, and this file aligned.
328+
- Update this file when Soroban build paths, size rules, authority boundaries, or verification
329+
commands change.

0 commit comments

Comments
 (0)