Skip to content
HomeAboutServicesGuidesContact

Guide

Access control and upgrade paths

Where the largest single losses sit, and the class least likely to be a bug. Most of it is the system working as designed, for the wrong person.

In short

  • Privileged access bypasses every other protection at once, which is why this class produces the largest single losses rather than the most frequent ones.
  • An upgrade does not change a rule. It replaces every rule, because the implementation is where all of them live.
  • Most of this class is not a defect. It is a design with a key attached, and the question is who holds the key and how fast they could act.
  • The method is enumeration: every state-changing function against who may call it, every role against what it can reach. There is no clever version of this.

Why this class is the expensive one

Other classes defeat a protection. This one acquires it.

A reentrancy drains a function. A price manipulation distorts one calculation. A privileged account does whatever the protocol permits its most trusted role to do, which is usually everything, and does it without defeating anything at all.

That is why the losses here are the largest even though the incidents are not the most numerous. The ceiling on most classes is whatever that specific flaw reaches. The ceiling on this one is the whole system.

In plain terms: ask what your most privileged role could do on its worst day. That number is your actual exposure, and it is unrelated to how good the rest of the code is.

The simple half

Some of this class really is defects, and they are found by looking rather than by thinking hard.

A missing modifier: a function that should be restricted is not. A wrong modifier: it checks a role, but not the one that matters. These survive because the function was added after the pattern was established, and nobody re-ran the enumeration.

An unprotected initialiser is the deployment-shaped version. An upgradeable contract is deployed, and until it is configured, anyone can call the function that sets the owner. On a public chain the gap between those two transactions is a race, and code that assumes it wins the race is code that loses it.

Our own review found exactly that: an initialiser callable by anyone after deployment, reproduced on a fork by a bot that claimed the vault in the same block the team deployed it.

In plain terms: deploy and initialise in one transaction, and add a guard that makes the second call impossible rather than merely unlikely.

Upgrades replace every rule at once

An upgradeable system separates the address users interact with from the code that runs. The proxy holds the state and forwards calls; the implementation holds the logic. Upgrading means pointing the proxy at different logic.

The consequence is often understated. An upgrade does not modify a rule. It replaces the entire set of them, including the ones protecting the funds, in a single transaction. Every audit finding, every invariant, every access check applies to the implementation that was reviewed and to no other.

So the security of an upgradeable protocol is not the security of its current code. It is the security of whatever can change that code, which is a different question with a different answer.

Three specific failures live here. An implementation contract left uninitialised can be initialised by anyone and, where the logic permits a self-destructing or delegating call, taken over in ways that affect the proxy. A storage layout changed carelessly between versions causes the new logic to read the old variables at the wrong slots, so a balance mapping becomes an owner address. And a proxy admin that is also a user-facing role creates function selector collisions that route calls to the wrong place.

Delegatecall and borrowed code

A normal call runs another contract code in that contract own context. A delegatecall runs another contract code against your storage, your balance and your identity.

That is the mechanism proxies are built on, and used deliberately it is fine. The risk appears when the target is not fully controlled: a library address that can be changed, a module registered by a role, a destination derived from user input. Code reached that way can write any storage slot it likes, including the slot recording who the owner is.

In plain terms: delegatecall into an address you do not control is equivalent to handing over the contract. There is no partial version of that trust.

The key behind the role

A capability matrix says what each role can do. It does not say who holds the role, and that is where the assumption actually sits.

A multisig is stronger than a single key only to the extent its signers are independent. Three signatures from three laptops in one office is a two-of-three in theory and close to a one-of-one in practice. The same applies to a governance contract whose voting power is concentrated enough that one holder decides outcomes alone.

A retired key is a claim until it is checked. An address whose powers were renounced should be listed with that status so a reader can verify it on chain rather than take it on trust.

And off-chain access belongs in the same register. An account that can propose an upgrade, trigger a pause or publish a price is a privileged role even though it never appears in a contract, and it is usually protected by whatever multi-factor policy the team happens to run.

What a timelock actually buys

A timelock does not prevent a hostile change. It delays one, publicly, which is a different and smaller promise than it is usually read as.

The value is entirely in the window: time for anyone watching to notice an upgrade has been queued and act before it executes. That only helps if the delay is long enough for a reaction and somebody is actually watching. A forty-eight hour delay with no monitoring and no alerting is a forty-eight hour delay nobody will use.

It also has to be unavoidable. A timelock the admin can bypass through a second path, or shorten by changing its own parameter without delay, is a timelock in name.

In plain terms: a timelock converts a silent change into a noisy one. Everything after that depends on the noise reaching someone who can act.

How a reviewer maps it

By enumeration, and there is no shortcut version.

Every state-changing function is listed against who may call it, including the functions added last and the ones inherited from a base contract. Every role is listed against what it can reach, directly and through any contract it controls. Then the two lists are compared against what the team believes is true, and the differences are the findings.

The second pass is the upgrade path. Which addresses can change code or parameters, through what mechanism, with what delay, and whether any route exists that skips the delay.

The third is the question the matrix is built to answer: can any role move user funds directly. That one is categorical rather than a matter of degree, and in a well-designed system the answer is no for every role including the owner.

What to ask of your own system

Six questions. The first three are answerable from the code; the last three are not, which is the point.

Which functions change state, and which role gates each one?

What can change the code or the parameters, and is there any path that avoids the timelock?

Can any role move user balances directly?

Who physically holds each key, and are those people and devices actually independent?

If an upgrade were queued right now by someone hostile, who would notice, through what alert, and how long would they have?

Which off-chain accounts can trigger an on-chain privileged action, and what protects them?

Questions

Is an upgradeable contract less secure than an immutable one?

It is a different security model rather than a worse one. Immutability removes the upgrade risk and removes the ability to fix anything, so a defect found later is permanent. Upgradeability keeps the fix available and adds the question of who may use it. Both are defensible; what is not defensible is being upgradeable without deciding who controls it.

We use a well-known multisig. Is access control covered?

The mechanism is sound and the question moves to the signers. Independence is what the threshold assumes, and a quorum of people who share an office, a device model or a key-generation ceremony is weaker than its number suggests.

Does an audit cover who holds the keys?

A technical review covers what each role can do in code. Who holds the role, whether those parties are independent and how the keys are stored is usually a separate engagement, which is why access-control due diligence exists as its own piece of work.

Can monitoring help here?

Yes, more than for most classes. A privileged call is an on-chain event and a queued upgrade is visible before it executes. That visibility is what makes a timelock worth having, and it only exists if something is configured to watch for it.

The work behind this

Related reading