Skip to content
Protocol contracts not yet configured for this build
Enter app

Security

Multisig and timelock

Some properties cannot be enforced in a contract. These are operational requirements, and they are stated here because the alternative is pretending the code covers them.

Requirement 1 — admin roles on a multisig

DEFAULT_ADMIN_ROLE on all four AccessControl contracts, and owner on the registry, must be held by a multisig behind a timelock. Never a hot deployer wallet.

What a compromised admin could otherwise do:

  • Pause staking indefinitely.
  • Widen maxPriceImpactBps to 10,000, removing the buyback price bound.
  • Grant KEEPER_ROLE to itself and drain the buyback budget through bad executions.
  • Grant GOVERNOR_ROLE on the reserve to itself and, after the timelock, withdraw.

What it still could not do: change the split, take staked principal, change the registered token, or mint.

Requirement 2 — governor and guardian must differ

solidity
bytes32 public constant GOVERNOR_ROLE = keccak256("GOVERNOR_ROLE");bytes32 public constant GUARDIAN_ROLE = keccak256("GUARDIAN_ROLE");

The guardian's veto is the only check on a reserve withdrawal during its 2-day window. If one entity holds both, the check is decorative.

The contract cannot enforce this, because it cannot know that two addresses are controlled by the same person.

Requirement 3 — keeper on a dedicated hot wallet

The keeper key will be used frequently and automatically, so treat it as compromised-by-default. It should hold only gas and nothing else. Its powers are already bounded — see keeper constraints.

Requirement 4 — verify the transfer on chain

After granting roles to the multisig, the deployer must renounce, and this must be verified, not assumed:

bash
cast call <contract> 'hasRole(bytes32,address)(bool)' \  0x0000000000000000000000000000000000000000000000000000000000000000 <deployer> \  --rpc-url https://rpc.mainnet.chain.robinhood.com

Every contract must return false for the deployer, for every role.

The registry uses Ownable2Step, so ownership transfer requires the multisig to call acceptOwnership() — a mistyped address cannot orphan the contract.

Requirement 5 — publish the holders

Users cannot evaluate trust assumptions they cannot see. Role holders belong in contract roles and in the deployment record once assigned.

Suggested configuration

RoleHolderThreshold
Registry ownerMultisig3-of-5
DEFAULT_ADMIN_ROLESame multisig3-of-5
PAUSER_ROLEMultisig, or a faster incident multisig2-of-5
EMERGENCY_ROLEMultisig3-of-5
KEEPER_ROLEDedicated hot wallet
GOVERNOR_ROLEMultisig3-of-5
GUARDIAN_ROLEA different set of signers2-of-3

A faster threshold on PAUSER_ROLE is defensible: pausing is a safe action that traps nothing, so optimising for speed there costs little.

Until this is done

Plouto should not be described as production-ready, and this documentation does not describe it that way. See known limitations.