Skip to main content

Staking and Certificate Control

An address has two credentials, and a validator that only governs the payment half leaves the other one open. The staking credential controls who collects rewards and who may register or deregister the credential itself, and both are authorized independently of spending. These two entries are what goes wrong when a protocol forgets that.

Insufficient Staking Control

From MLabs Common Plutus Vulnerabilities

Identifier: insufficient-staking-control

Property statement: All scripts explicitly account for staking credentials.

Test: A transaction successfully changes or incorrectly sets the staking credential of a UTxO locked by a validator of the protocol. Alternatively, a transaction sets an arbitrary staking credential for an output being locked by an external credential and holding value consumed from the protocol.

Impact:

  • Unpredictable addresses
  • Illegitimate staking rewards

Further explanation: When writing the logic for a Plutus script, it is easy to focus too much on the set of rules that must be enforced by a validator and start thinking of these rules as solely defining the Cardano addresses. This is, treating validator hashes and addresses interchangeably. An example of such behaviour is illustrated by the following excerpt:

vulnValidator __ ctx =
traceIfFalse "Must continue tokens" (txOutValue ownInput == contVal)
where
ownInput = txInInfoResolved $ findOwnInput ctx
ownValidatorHash = ownHash ctx
[(_, contVal)] = scriptOutputsAt ownValidatorHash (scriptContextTxInfo ctx)

The validator above tries to make sure that after consuming a UTxO locked by vulnValidator, an output holding the same value is locked back. However, it forgets about the staking credentials, so the output can actually be locked in a very large number of addresses.

This is because addresses are composed of credentials that control the spending of UTxOs and staking credentials that control the claiming of ADA staking rewards. Therefore, validation would succeed as long as the output is locked in an address which has ownHash as credential. However, there are as many such addresses as possible public keys or script hashes for a staking credential.

By exploiting this, anybody could send the funds to an address with a staking credential controlled by them. This would not grant them control over the funds, since they are still guarded by the validator's logic, but would grant them control over the staking rewards generated by all the ADA present in the locked output.

Apart from losing control over staking rewards, ignoring the staking credentials could have further consequences and result in a catastrophic outcome. This is because since the UTxO holding the funds can live in a big spectrum of addresses, it becomes more difficult to reason about the rules that control their spending.

For instance, to prevent a multiple satisfaction attack, a validator could have a rule ensuring that only one input coming from the address of the input being validated is present in the transaction. This works correctly assuming that all relevant funds are locked in the same address as the input being validated. However, as soon as part of the funds end up in an address with the same credential but different staking credential, the check could be by-passed and tokens could leak.

Finally, this not only applies when locking UTxOs in scripts but also when sending funds from a script to a public key. If the script controlling it only checks that funds are correctly sent to the public key but neglects the staking credentials, the value could be sent to a mangled address, where the public key is correct but an arbitrary staking key has been used. Although the legit owner of the funds would still have access to them, the right to claim staking rewards generated by the ADA in these funds would temporarily belong to an actor different than the legit owner of the funds.

This problem is prevented by explicitly checking the staking credentials, taking into account complete addresses instead of only the credentials controlling the spending of funds.

An address assembled this way, the correct payment credential paired with an attacker's staking credential, is often called a franken address (or mangled address), and it has caused real value loss in deployed protocols. A related anti-pattern is using the staking credential as an authorizer: gating an action, a whitelist, or an airdrop on the stake credential of an address rather than the payment credential. Because anyone can build an address that reuses a victim's stake credential under a payment key they control, authorizing by the stake credential can be spoofed. Authorize by the payment credential (which actually controls the funds), and when paying out to an address, check or reconstruct the complete address rather than a single credential.

Unconstrained Certificate Operations

Concerns the withdraw-zero pattern documented by Anastasia Labs design patterns and CIP-112.

Identifier: certificate-deregistration

Property statement: A staking script explicitly handles its certificate (registration and deregistration) operations, denying by default any it does not intend to allow.

Test: A transaction submitted by an unrelated party successfully deregisters the protocol's staking credential.

Impact:

  • Protocol liveness halted until re-registration
  • Repeatable griefing plus theft of the refunded key deposit

Further explanation: Many protocols centralize validation with the withdraw-zero pattern: instead of each input re-running the same expensive checks, the spend validators only require that a specific staking script executes in the transaction, and the real logic runs once in that staking script. A staking script executes when the transaction includes a withdrawal from its reward account, even a withdrawal of zero, which is why the pattern is cheap. It has a precondition, though: the stake credential must be registered, or the zero withdrawal fails phase-1 validation.

A staking script runs for more than withdrawals. Registering or deregistering its credential also invokes it, under its certifying (publish) purpose. If the script does not constrain that purpose, for example a catch-all fallback that returns success for any operation it did not explicitly consider, then anyone can submit a deregistration certificate for the credential and the script will approve it.

Deregistration does two things: it refunds the key deposit (2 ADA on mainnet) to whoever submitted the certificate, and it removes the credential. Every subsequent protocol transaction that relies on the withdraw-zero withdrawal now fails, because it withdraws from a reward account that no longer exists, until someone re-registers the credential and pays the 2 ADA deposit again. An attacker can repeat this, turning it into a cheap, repeatable denial of service with a small profit on each round.

The attack has a mirror image. If the unguarded operation is registration and delegation rather than deregistration, an attacker can register the credential and delegate it to a stake pool. Nothing halts on-chain, which is what makes it subtle: real rewards begin accruing to the reward account, and off-chain code that hardcodes a withdrawal amount of zero starts building invalid transactions, because the reward account's balance is no longer zero. Production teams have called this out as one of the least obvious ways to break a withdraw-zero deployment.

Aiken's default is protective here: a validator with no fallback handler rejects any purpose it does not explicitly handle, so an unhandled certificate operation is denied. The vulnerability appears when a developer adds a permissive else that succeeds, or writes a publish handler that does not guard which certificate is being posted. Prevent it by handling the certificate purpose explicitly and denying deregistration unless the protocol genuinely intends to allow it, and by not making liveness depend on a single credential that anyone can deregister. The same discipline covers the mirror attack: on a forwarding-only credential, deny registration and delegation as well, and have off-chain code read the actual reward balance rather than assume it is zero.