Skip to content

Security: ophelios-studio/kintsugi

Security

docs/SECURITY.md

Security

Kintsugi is a single-purpose contract for moving assets out of compromised wallets. It is intentionally minimal: no admin, no upgrades, no pausability, no privileged operations. This document describes the security model, the assumptions, and the known limits.

Trust assumptions

A user of Kintsugi must trust:

  • The Solidity source of Rescue.sol and NonceTracker.sol (auditable, MIT, ~150 lines combined).
  • The deployed bytecode at the published addresses on each chain (verifiable on Etherscan).
  • The viem library, OpenZeppelin Contracts, and the Ethereum execution layer.

No other parties are trusted at runtime. The CLI runs locally, never sends private keys anywhere, never depends on any Kintsugi-operated server.

Replay protection

Each rescue batch carries five fields that bind the signature: safe, ops, nonce, deadline, chainId. The deployed contract checks all of them.

  • nonce is monotonic per victim, stored in the NonceTracker singleton. Once consumed, the same signature cannot be replayed.
  • deadline is a unix timestamp; the contract reverts if block.timestamp > deadline. The CLI defaults to a 30-minute deadline.
  • chainId is checked against block.chainid. A signature for chain X cannot be processed on chain Y. The EIP-712 domain separator also pins the chain id at deploy time, so the same chain check is enforced at the digest level too.

The NonceTracker singleton

The NonceTracker was deliberately designed so that nonces are stored OUTSIDE the victim's storage. EIP-7702 lets any address delegate to any contract at any time. Storing nonces in the victim's storage would expose them to corruption by any other delegate the victim happens to set, breaking replay protection.

NonceTracker.increment() uses msg.sender as the key. From the tracker's perspective the caller is the victim address (because the rescue contract's code is running as delegated code at the victim's address). No external party can bump a different account's nonce.

EIP-712 signature scope

The verifying contract in the EIP-712 domain is the deployed Rescue contract address, captured at deploy time in an immutable. Wallets render signatures with the verifying contract address visible, so the user can confirm they are signing for Kintsugi specifically and not some lookalike.

The Batch typed-data struct includes the destination safe address. Even though the contract does not enforce that every op transfers to safe, including it in the signature makes the user's intent explicit and rules out a class of phishing attacks where the user is asked to sign a generic batch whose visible "destination" is not the address they think.

What the contract does NOT defend against

  • A compromised victim machine that signs an attacker-controlled batch. Kintsugi assumes the victim signs the rescue batch on a CLEAN machine.
  • A compromised safe wallet. The safe must be a fresh wallet derived from a NEW seed phrase, not the compromised one.
  • A compromised rescuer wallet. The rescuer pays gas; if its key is compromised the attacker can use the funds, but the rescue still succeeds for the victim's assets.
  • An attacker who somehow obtains the EIP-712 batch signature AND the EIP-7702 authorization tuple before they are submitted. With both, they could front-run the rescue with a tx using the victim's resources. Mitigation: use Flashbots Protect for the Type-4 tx (--private-mempool flag) when paranoia is warranted.
  • Resolver / record cleanup on rescued ENS names. The CLI offers helpers (setResolver, clearReverseRecord) but does not run them automatically. The new owner should audit and reset records after the rescue.

Atomicity

A batch is atomic in the strict sense: every op succeeds or the entire batch reverts. If any op reverts, the rescue's state changes are rolled back AND the nonce stays unconsumed (so the same signature can be re-used after fixing the failing op).

Gas economics

The rescuer pays gas for the entire Type-4 transaction. There is no minimum balance the victim must hold. The rescuer's required balance scales with the size of the batch; a typical 5-asset rescue (mix of ERC-20, ERC-721, ENS) costs roughly 0.003 to 0.008 ETH at mainnet gas prices.

Known limits

  • Mainnet and Sepolia only this iteration. L2 support requires deployments on each L2.
  • No hardware wallet integration yet. The CLI accepts private keys via masked prompts only. HW signing for both the EIP-712 batch and the EIP-7702 authorization is on the roadmap.
  • Browser extension UX is on the roadmap. The CLI is the only surface today.
  • No automatic discovery of staked positions, vesting contracts, or LP positions. Use the custom-call op manually for these (e.g. unstake before transferring an NFT held in a staking contract).

There aren't any published security advisories