Skip to content

feat(core): first-class upgradeable contracts (UUPS/Transparent proxies) — #155 - #169

Merged
robercano merged 1 commit into
mainfrom
feat/issue-155-upgradeable-proxies
Jul 29, 2026
Merged

feat(core): first-class upgradeable contracts (UUPS/Transparent proxies) — #155#169
robercano merged 1 commit into
mainfrom
feat/issue-155-upgradeable-proxies

Conversation

@robercano-ghbot

Copy link
Copy Markdown
Collaborator

Summary

Adds first-class upgradeable contracts (UUPS / Transparent proxies + an upgrade flow) to @redeploy/core's declarative spec. Closes #155.

Declare upgradeable on a ContractEntry and the compiler deploys the implementation + proxy (+ ProxyAdmin where applicable) using Hardhat Ignition primitives; downstream refs resolve to the proxy address.

What changed (all within packages/core)

  • Spec types (src/spec/types.ts): ContractEntry.upgradeable?: { kind: "uups" | "transparent"; initializer?: { function; args? }; proxyAdminOwner? }. Fully backward compatible.
  • Schema (src/spec/schema.ts): zod validation of the new shape (initializer args reuse contractArgSchema).
  • Validation (src/spec/validate.ts): new cross-field rules — MISSING_PROXY_ADMIN_OWNER (transparent requires an explicit owner — never silently defaulted), UNUSED_PROXY_ADMIN_OWNER (rejected on uups), INVALID_IDENTIFIER (initializer function must be a Solidity identifier); refs in initializer.args / proxyAdminOwner participate in ordering + cycle checks like constructor args.
  • Compiler (src/compile/compile.ts): emits the Ignition future chain — impl → optional m.encodeFunctionCallERC1967Proxy (uups) / TransparentUpgradeableProxy (transparent) → m.contractAt handle (stored under entry.id, so refs resolve to the proxy). Proxy artifact names overridable via CompileOptions.proxyArtifacts. Adds compileUpgrade() for the v1 upgrade flow (fresh future id required; explicit proxy-admin address for transparent — never guessed).
  • Simulate (src/simulate/simulate.ts): PlannedStep.upgradeable marker; dependsOn folds in initializer/proxyAdminOwner refs. Non-upgradeable output unchanged.
  • Journal/snapshot data model (src/deploy/proxyHistory.ts, new): typed proxy implementation-history model + pure derive/append helpers so a future @redeploy/reader change can expose implementation history. Not wired into reader (separate module).
  • Docs (packages/core/README.md, new): usage, uups vs transparent, refs-resolve-to-proxy, proxy-admin ownership, and the storage-layout caveat.

Out of scope (per the issue)

  • Storage-layout safety checks — explicitly out of scope for v1; documented in code + README that teams must run their own (e.g. OZ upgrades validation).
  • No changes to packages/reader, packages/verify, or apps/studio (their proxy follow-ups are separate).

Gates (independently verified on branch head d247ecb)

  • build / lint / typecheck / test: pass — packages/core 400/400 tests
  • coverage: packages/core 93.29% stmts / 91.69% branch / 98.46% funcs (threshold 80)

Review

Four adversarial lenses (correctness, tests, security, performance) all APPROVED (consensus). Security confirmed proxy-admin ownership is a hard validation error (never silently defaulted), initializer flows only through m.encodeFunctionCall, Map/Set prototype-pollution posture preserved, and artifact-name overrides are path-traversal-guarded.

Non-blocking follow-ups (noted by reviewers, not addressed here)

  • Reserve derived future-id suffixes (_impl / _proxy / _initData) against the spec id namespace in validateSpec (today a colliding hand-written id fails loudly at buildModule, not at validation).
  • Optional extra tests: transparent downstream-consumer ref resolution; upgradeable entry with constructor args; transparent-upgrade-with-initializer arg position; after + upgradeable.
  • Overloaded initializer signatures aren't disambiguable (bare identifier only) — acceptable v1 limitation.

🤖 Generated with Claude Code

…xies) — #155

Adds a declarative `upgradeable` field to `ContractEntry` (spec/types.ts,
spec/schema.ts) supporting UUPS and Transparent proxy patterns, with
cross-field validation (spec/validate.ts): initializer/proxyAdminOwner refs
participate in ordering/cycle checks exactly like constructor args,
initializer.function is validated as a plain Solidity identifier
(INVALID_IDENTIFIER), and proxyAdminOwner is required for transparent
proxies / rejected for uups (MISSING_PROXY_ADMIN_OWNER /
UNUSED_PROXY_ADMIN_OWNER) — no unsafe defaults.

compile/compile.ts composes the proxy by hand from Ignition primitives
(ignition-core 0.15.15 has no m.proxy): impl deployment, an optional
m.encodeFunctionCall initializer, the ERC1967Proxy/TransparentUpgradeableProxy
future (artifact names overridable via CompileOptions.proxyArtifacts), and a
final m.contractAt handle stored under the entry's own id so every
downstream ref (and the compiled result) resolves to the PROXY, not the
implementation. Also adds compileUpgrade() — a standalone v1 upgrade-flow
compiler (UUPS: upgradeToAndCall; Transparent: ProxyAdmin.upgradeAndCall) —
and deploy/proxyHistory.ts, a typed proxy-implementation-history data model
with pure derive/append helpers (not wired into reader — future work).

simulate.ts surfaces an `upgradeable` marker on PlannedStep and includes
initializer/proxyAdminOwner refs in dependsOn, without changing output for
non-upgradeable entries.

Storage-layout safety is explicitly out of scope (v1) — documented in
spec/types.ts, compile.ts, and the new packages/core/README.md.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@robercano
robercano merged commit bee5a6f into main Jul 29, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[wave 2] core: first-class upgradeable contracts (UUPS/Transparent proxies + upgrade steps)

2 participants