3Jane 7d TimelockController - 18/09/2026 22:06 - HIGH
- Protocol: 3JANE
- Contract: 3Jane 7d TimelockController —
0x3D3C41419Ab401cd25055E8f9421D7D96d887885 - Chain: Mainnet (chain id 1)
- Risk: HIGH
Summary
Upgrades two core 3Jane proxies — USD3 and sUSD3 — to new implementations via ProxyAdmin upgradeAndCall with empty calldata. USD3 moves to a new verified implementation; sUSD3 likewise. Adds eight functions on each: ring-fence conduit management, ring-fence release, supply-cap exemptions, market wind-down clearing, and internal loss/wrapUSDC/deposit-deployment logic. Removes initializer, reinitialize, restartStrategy, minCommitmentTime, and the entire depositor whitelist set (setWhitelistEnabled, setWhitelist, setDepositorWhitelist). Empty calldata means no post-upgrade hook fires — no reinitialize or migration is executed in this batch, leaving any state transition to a later call. Both new implementations are verified on Etherscan, so bytecode is inspectable. New loss and wind-down primitives on a stablecoin wrapper pair are a material behavioral expansion of the protocol's control surface. Storage uses EIP-7201 namespaced slots, so positional layout was not validated — namespaced collisions are not ruled out. Routing through the 7d TimelockController plus two separate ProxyAdmins adds operational surface.
Analysis
What the batch actually does
Two upgradeAndCall(address,address,bytes) invocations are routed via the two ProxyAdmin contracts listed as targets:
0x41C838664a9C64905537fF410333B9f5964cC596(ProxyAdmin) and0xecda55c32966B00592Ed3922E386063e1Bc752c2(ProxyAdmin).
The batch body contains only the two calls, so the batch structure itself carries no extra economy of action beyond the atomicity guarantee. Each call is an owner-only proxy upgrade — ProxyAdmin.upgradeAndCall is onlyOwner, so both ProxyAdmins must be owned by the executing 3Jane 7d TimelockController for the batch to succeed. That dependency chain (timelock → ProxyAdmin → proxy) is two hops longer than a direct admin call and is worth keeping in mind when reading the rest of this report.
Call 1 repoints the USD3 transparent proxy at 0x056B269Eb1f75477a8666ae8C7fE01b64dD55eCc from its current implementation 0xB606fB370Eaaad03d71B49aE5E42AA4aEC7458D9 to the new implementation 0xd1F1c3F485063712873285BF4ef25ab068f13893.
Call 2 does the same for the sUSD3 proxy at 0xf689555121e529Ff0463e191F9Bd9d1E496164a7, moving it from 0x529cbf11fFbC272D63858ca40A2C7F2695712073 to 0x6093d95f6C102163D19F5681E7D84997060BBAed.
Both new implementations are verified on Etherscan, so the bytecode that will start serving USD3 and sUSD3 is inspectable rather than opaque. Both independent simulations returned SUCCESS and emitted the expected Upgraded event with the corresponding new implementation address. The simulations are per-call and independent; they do not prove the batch executes atomically, and the reported gas figure of 0 is a simulation artifact rather than a real cost estimate.
The empty calldata argument
The third argument to both upgradeAndCall calls is a zero-length byte string, and that value is identical across both calls. Per the ProxyAdmin source, UPGRADE_INTERFACE_VERSION is "5.0.0", and under that interface version the third argument must be the empty byte string when no function should be called — the contract explicitly documents that this makes it impossible to invoke the proxy's receive function during an upgrade. In other words, empty data here is the canonical "upgrade only, no post-upgrade hook" form, not a truncated or accidentally dropped payload.
The practical consequence: no reinitialize, no migration routine, no post-upgrade configuration call fires at the moment of implementation swap. Whatever state transitions the new code needs — initializing any newly introduced storage, seeding ring-fence configuration, clearing wind-down flags — must be performed by a separate, later transaction. Until that happens, the new code is live against old state. Notably, the new implementations remove initialize, reinitialize, and restartStrategy, so the usual post-upgrade re-entry points do not exist on the new bytecode. If newly added namespaced storage variables require initialization and no setter path covers them, they will sit at their zero/false defaults, and there is no reinitialize to fix that afterwards. This is the single most important operational consequence of the empty calldata.
Added surface
Each new implementation adds eight functions, and the additions are essentially identical across the pair:
setSupplyCapExempt(address,bool)—onlyManagement. Grants or revokes an address's exemption from supply-cap accounting. This is a control over who can push supply beyond configured caps.setRingFenceConduit(address,bool)—onlyManagement. Configures an address as a ring-fence conduit, i.e. an entity permitted to interact with ring-fenced funds. Whitelisting a conduit is a permission grant, not proof of how that address will use the permission.releaseRingFence(uint256)—onlyManagement. Releases a ring-fenced amount. Theuint256is an ambiguous unit: no Token Flows section is present, no movement signature identifies it as a token quantity, and no single related-token entry normalizes it. Given_wrapUSDCsits alongside it, a USDC-denominated (6-decimal) reading is plausible, but plausible is not verified. Treat the amount as an unresolved raw integer until the getter or a follow-up transaction disambiguates it.clearMarketWindDown(Id)— external, and critically listed without anonlyManagementmodifier, unlike every other new state-changing setter in the diff. A publicly reachable function that clears a market's wind-down flag is a materially different risk profile from the rest of this batch: it implies wind-down status can be lifted by any caller. This is the item that most deserves verification against the actual verified source before execution.marketInWindDown(Id) external view— read helper exposing wind-down status._pendingLoss() internal view,_wrapUSDC(uint256,bool) private,_deployDepositedFunds() private— internal machinery._wrapUSDCand_deployDepositedFundstogether describe a flow where idle deposits are wrapped and deployed into whatever venue the protocol uses. Inserting_pendingLossinto that path suggests loss accounting now gates or informs deployment decisions — a behavioural change in how depositor funds are moved, though no amounts are observable in this transaction.
The Id type used by the wind-down functions is not resolved by any provided section, so its encoding is not identified here. It is not a keccak256 role or parameter pre-image that the supplied context maps.
Removed surface
USD3 loses seven functions; sUSD3 loses six. Both drop initialize, reinitialize (reinitializer(2)), restartStrategy (reinitializer(3)), minCommitmentTime, setWhitelistEnabled, and setWhitelist. USD3 additionally drops setDepositorWhitelist, which sUSD3 never had.
The whitelist removal deserves a flag in both directions. Removing setWhitelistEnabled, setWhitelist, and setDepositorWhitelist removes an access-gating control surface: either the gating is being retired as obsolete, or the protocol loses a compliance/eligibility lever that existed before. Either reading is a change in who can hold or deposit, and the calldata alone does not say which. The removal of minCommitmentTime likewise removes a minimum-lock getter — consistent with a deposit-timing regime being dropped, but again, this is inference from the diff, not a verified statement of intent.
The fact that USD3 and sUSD3 do not have byte-identical diffs (7 removals vs 6) is a notable observation: despite being a wrapper pair that otherwise shares the same eight additions, their codebases have diverged. That makes them non-fungible from an audit standpoint — verifying one does not fully verify the other.
Storage layout
Both upgrades use EIP-7201 namespaced storage, and the positional layout check was skipped for both. ERC-7201 namespacing makes accidental overlap with non-namespaced slots less likely, but the diff tooling used here did not validate that new namespaced structures avoid collisions with existing ones. The removal of whitelist functions does not necessarily remove their namespaced storage, and the newly added ring-fence and wind-down structures may introduce namespaces that did not previously exist. Namespaced collisions are therefore not ruled out by the available checks. This is a residual technical risk that cannot be cleared from the material provided, and it is a further reason to prefer a lower-risk alternative if one exists (e.g. staged upgrades with observation windows).
Asset and token flow
There is none in this transaction. Both calls carry empty calldata and no value, and the only state change at execution time is the implementation pointer inside each proxy's ERC-1967 implementation slot. No USD3 or sUSD3 supply is minted or burned, no balances move, and no approvals are set. The asset-flow risk is entirely prospective: the added ring-fence-release, conduit-whitelisting, supply-cap-exemption, and deposit-deployment primitives are new ways funds can be moved or permissions granted, and they will be exercisable the moment the implementation swaps in. The absence of token movement in this batch is not evidence that the new control surface is inert — it means the levers are being installed and pulled later.
Risk rationale
This is a HIGH-severity change, consistent with the anchor for upgradeAndCall routed through a ProxyAdmin, and adjusted upward within that band by specific details rather than held at the baseline:
- It swaps live implementation code on two core protocol contracts. USD3 and sUSD3 are the assets users hold; their implementation pointer determines every future holder interaction. Any behavioural regression in the new bytecode applies immediately and protocol-wide.
- The new code is verified, which materially reduces — but does not eliminate — the risk. Verified bytecode can be read; it cannot be proven safe from an Etherscan verification alone.
clearMarketWindDown(Id)appears without an access modifier in the diff, while its siblings areonlyManagement. If that reading is accurate against the deployed source, an unpermissioned state-changing wind-down override is being introduced on a stablecoin pair. This alone justifies keeping the verdict at HIGH rather than letting verification pull it down.- No post-upgrade hook fires. Any initialized state the new code needs is left at defaults with
reinitializeremoved from the new bytecode. If a later configuration transaction is required and does not land correctly, the protocol runs new code against uninitialized state for an indeterminate period. - Storage compatibility was not validated. Namespaced layout means positional checks were skipped rather than passed.
- The pair diverges (different removal counts), so one implementation's review does not cover the other.
- Removal of the whitelist control set is a change in who can be gated, and the direction of that change is not determinable from calldata.
Partially offsetting factors: both new implementations are verified; both simulations succeeded and emitted the expected Upgraded event; the ProxyAdmin interface version is 5.0.0 and empty calldata is its intended no-hook form, so the empty bytes are not evidence of a dropped payload; and the route through the 3Jane 7d TimelockController provides a seven-day observation window before execution, giving depositors and integrators time to review the new bytecode and exit if they disagree with it. There is no Stated Intent section contradicting the calldata here, so no proposer-description mismatch is being flagged; the concern is entirely in what the calldata does on its own.
Net: a deliberate, verified, timelocked implementation upgrade of two core assets, carrying a materially expanded control surface (ring fences, conduit whitelisting, supply-cap exemptions, wind-down clearing, deposit deployment) and an unvalidated storage layout. The verification and the timelock keep it out of CRITICAL; the unmodifiered clearMarketWindDown, the missing post-upgrade initialization path, the unverified storage compatibility, and the removal of access-gating functions keep it at HIGH. Before execution, confirm the access control on clearMarketWindDown in the verified source, confirm which storage namespaces are new and how they get initialized given reinitialize is gone, and confirm whether the whitelist removal is a deliberate retirement or an unintended loss of an eligibility control.
Call Flow
From: 0x3D3C41419Ab401cd25055E8f9421D7D96d887885
-
upgradeAndCall(address,address,bytes)on0x41C838664a9C64905537fF410333B9f5964cC596(ProxyAdmin)address proxy:0x056B269Eb1f75477a8666ae8C7fE01b64dD55eCc(USD3)address implementation:0xd1F1c3F485063712873285BF4ef25ab068f13893(USD3)bytes data:0x- Independent simulation: SUCCESS (does not prove the batch succeeds atomically)
-
upgradeAndCall(address,address,bytes)on0xecda55c32966B00592Ed3922E386063e1Bc752c2(ProxyAdmin)address proxy:0xf689555121e529Ff0463e191F9Bd9d1E496164a7(sUSD3)address implementation:0x6093d95f6C102163D19F5681E7D84997060BBAed(sUSD3)bytes data:0x- Independent simulation: SUCCESS (does not prove the batch succeeds atomically)
Reference
| Address | Label | Role | Description |
|---|---|---|---|
0x3D3C41419Ab401cd25055E8f9421D7D96d887885 |
3Jane 7d TimelockController | Executor | Executor: Execution authority for the governance transaction |
0x41C838664a9C64905537fF410333B9f5964cC596 |
ProxyAdmin | Call target | Call target: Receives upgradeAndCall(address,address,bytes) |
0x056B269Eb1f75477a8666ae8C7fE01b64dD55eCc |
USD3 | Calldata argument | Calldata argument: Passed as proxy to upgradeAndCall(address,address,bytes) |
0xd1F1c3F485063712873285BF4ef25ab068f13893 |
USD3 | Calldata argument | Calldata argument: Passed as implementation to upgradeAndCall(address,address,bytes) |
0xecda55c32966B00592Ed3922E386063e1Bc752c2 |
ProxyAdmin | Call target | Call target: Receives upgradeAndCall(address,address,bytes) |
0xf689555121e529Ff0463e191F9Bd9d1E496164a7 |
sUSD3 | Calldata argument | Calldata argument: Passed as proxy to upgradeAndCall(address,address,bytes) |
0x6093d95f6C102163D19F5681E7D84997060BBAed |
sUSD3 | Calldata argument | Calldata argument: Passed as implementation to upgradeAndCall(address,address,bytes) |