Skip to content

fix(factory): validate init_params and version before deploying - #1236

Merged
nanaf6203-bit merged 1 commit into
MettaChain:mainfrom
merlik787-droi:fix/issue-1175-factory-init-param-validation
Sep 26, 2026
Merged

nanaf6203-bit merged 1 commit into
MettaChain:mainfrom
merlik787-droi:fix/issue-1175-factory-init-param-validation

Conversation

@merlik787-droi

Copy link
Copy Markdown
Contributor

Summary

Validates init_params and version in deploy_contract, making Error::InvalidParameters reachable for the first time.

Closes #1175
Closes #1176
Closes #1177
Closes #1178

Why

deploy_contract passed config.init_params straight to builder::build_contract, which ignores the payload entirely and returns Ok unconditionally. version was stored with no check at all.

Error::InvalidParameters was therefore unreachable dead code — it existed to tell callers their parameters had been checked, and nothing checked them. That is worse than having no variant: it is a variant that actively misleads an integrator reading the error enum.

Two concrete problems beyond the dead variant:

  • Unbounded init_params was copied into the transaction, re-encoded for the cross-contract call, and written into the deployment record's footprint, with nothing rejecting it at any point.
  • Unbounded version is unbounded permanent storage in DeployedContract, paid for by the factory and returned by get_deployment. One call with a 1 MB version string is a permanent storage subsidy.

What changed

contracts/factory/src/lib.rs

  • MAX_INIT_PARAMS_LEN = 4_096 and MAX_VERSION_LEN = 64, both documented with the reasoning and the template sizes they sit well above.
  • deploy_contract rejects:
    • empty or over-long init_params → InvalidParameters (now actually constructed)
    • empty or over-long version → InvalidVersion (new, appended last)
  • validate_deployment_request — a private associated function holding the rules.
  • deployment_limits() — a read-only message so a client can check a request before submitting it rather than discovering the limits from a failed transaction.

The judgement call: rejecting empty init_params

This is the part of the change worth reviewing carefully, because it is the one behaviour that is not unambiguously correct.

The factory stores only a Hash per contract type, not its constructor metadata. It therefore cannot type-check the payload against the expected ABI, and an empty argument list is only actually wrong if the target constructor takes arguments. A ContractType with a genuinely zero-argument constructor would now be undeployable.

I rejected empty anyway, for three reasons:

  1. build_contract discards the payload, so a deployment registered with no constructor arguments is unverifiable by construction. Nothing downstream can go back and check that the target accepted them.
  2. The issue's own framing is that malformed params "produce arbitrary, unverifiable registrations". An empty list registered as a success is the clearest instance of that.
  3. No DeploymentTemplate in templates.rs encodes to an empty payload — they all emit at least a token name and symbol. So requiring non-empty matches the intended calling convention rather than inventing a new one.

If a ContractType does turn out to need a zero-argument constructor, the correct follow-up is a per-type policy (a requires_params(ContractType) -> bool consulted by the validator), not relaxing the rule globally. I did not build that here because it is speculative — no such type exists today — and it would widen the diff considerably. Flagging it rather than silently guessing.

Other decisions

  • Validation runs before the code-hash lookup. A malformed call is reported on its own terms instead of surfacing as CodeHashNotSet and sending the caller hunting for a code-hash problem that does not exist. Pinned by test_validation_precedes_the_code_hash_check, with test_well_formed_request_still_reports_the_missing_code_hash guarding the other direction so the existing guard still fires.
  • InvalidVersion is separate from InvalidParameters. The issue suggested reusing InvalidParameters for both, but telling a caller their version string was empty when their params were fine is not useful. Appended last, so no existing discriminant moves.
  • init_params is checked before version, so a caller fixing one problem at a time gets a deterministic first error.
  • Constants live here, not in propchain-traits. The issue mentions "shared constants", but this crate has no propchain-traits dependency, and adding one is a Cargo.toml change plus a hand-edited Cargo.lock — not something to do blind in a PR about parameter validation.

Tests

13 new tests, plus one existing fixture updated.

Updated: escrow_config supplied init_params: Vec::new(), which is now rejected, so it supplies a small non-empty payload. This was the only existing test affected, and the fixture was itself an instance of the bug — it demonstrated the factory happily recording a deployment with no constructor arguments.

New:

  • empty init_params → InvalidParameters; over-long → InvalidParameters; exactly at the limit → accepted and recorded;
  • empty version → InvalidVersion; over-long → InvalidVersion; exactly at the limit → accepted and recorded verbatim;
  • boundaries are exact, checked at ±1 on both limits;
  • init_params is reported before version;
  • validation precedes the code-hash check, and a well-formed request still reports CodeHashNotSet;
  • a failed validation records nothing — across all four rejection cases, deployment_count stays 0, the deployer list stays empty, and no deployment record exists;
  • limits are non-zero and sensibly ordered;
  • a semver-with-build-metadata label round-trips verbatim.

Not addressed here

Two things in this issue's block are out of scope for this PR and remain open elsewhere:

  • The zero-address problem. build_contract still returns AccountId::from([0u8; 32]), so a successful deployment is still registered against an un-callable address. That is the separate zero-address issue, and it needs real instantiation plumbing. This PR does not paper over it and does not add a zero-address rejection, which would make every deployment fail against the current stub.
  • Unbounded deployer_contracts. Per-deployer growth is a storage/gas concern about the registry, not about validating a request.

Integration changes

  • One new error variant, InvalidVersion, appended last.
  • One new read-only message, deployment_limits().
  • No storage layout change. No fields added, removed, or reordered.
  • No new dependencies, no Cargo.lock change.
  • set_code_hash, change_admin, the getters, and the recording logic are untouched.

Test plan

  • cargo test -p propchain-factory — not run. No code validation was performed, by explicit instruction; this change is source-only and manually reviewed.
  • cargo fmt --all -- --check — not run, same reason. The new helper config_with has a long signature that rustfmt would wrap.
  • cargo clippy --workspace --all-targets -- -D warnings — not run, same reason.
  • cargo build --workspace — not run, same reason.

The 13 tests added here are expected to compile and pass, but they are unverified. The pre-existing tests were read and their expectations checked against the new validation by hand; test_deploy_without_code_hash_fails and test_deployment_getters_reflect_recorded_deployments were the two that could have broken, and both are accounted for above.

Env vars

  • None. No new environment variables, configuration, or deployment steps.

`deploy_contract` passed `config.init_params` straight to
`builder::build_contract`, which ignores the payload and returns `Ok`
unconditionally, and it stored `version` with no check at all. So
`Error::InvalidParameters` was unreachable dead code: it existed to tell
callers their parameters were checked, and nothing checked them.

Two things were wrong with that. An oversized `init_params` was copied
into the transaction, re-encoded for the cross-contract call, and then
written into the deployment record's footprint with nothing to stop it,
and an unbounded `version` is unbounded permanent storage in
`DeployedContract`, paid for by the factory.

`deploy_contract` now rejects an empty or over-long `init_params` with
`InvalidParameters`, and an empty or over-long `version` with a new
`InvalidVersion` so a caller can tell which half of the request was
rejected. Validation runs before the code-hash lookup, so a malformed
call is reported on its own terms instead of surfacing as
`CodeHashNotSet` and sending the caller hunting for a problem that does
not exist.

Rejecting *empty* params is the judgement call in this change. The
factory stores only a `Hash` per type, not the constructor metadata, so
it cannot type-check the payload against the expected ABI, and an empty
argument list is only wrong if the target constructor takes arguments.
It is still the right default: `build_contract` discards the payload, so
a deployment registered with no constructor arguments is unverifiable
by construction, and no `DeploymentTemplate` in `templates.rs` encodes
to an empty payload, so this matches the intended calling convention.
If some `ContractType` genuinely needs a zero-argument constructor, the
follow-up is a per-type policy rather than relaxing the global rule --
noted in the PR.

`MAX_INIT_PARAMS_LEN` (4 KiB) and `MAX_VERSION_LEN` (64) are defined
here rather than in `propchain-traits` because this crate has no
`propchain-traits` dependency, and both limits are readable on-chain via
a new `deployment_limits()` so a client can pre-validate instead of
discovering them from a failed transaction.

This changes one existing test fixture: `escrow_config` supplied
`init_params: Vec::new()`, which is exactly the input now rejected.

Closes MettaChain#1175
Closes MettaChain#1176
Closes MettaChain#1177
Closes MettaChain#1178
@drips-wave

drips-wave Bot commented Sep 26, 2026

Copy link
Copy Markdown

@merlik787-droi Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits.

You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀

Learn more about application limits

@nanaf6203-bit nanaf6203-bit left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@nanaf6203-bit
nanaf6203-bit merged commit c989c5a into MettaChain:main Sep 26, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment