diff --git a/src/pages/protocol/tips/tip-1008.mdx b/src/pages/protocol/tips/tip-1008.mdx new file mode 100644 index 0000000..27fb25c --- /dev/null +++ b/src/pages/protocol/tips/tip-1008.mdx @@ -0,0 +1,245 @@ +--- +id: TIP-1008 +title: Account Ownership Transfer +description: Mechanism for Tempo accounts to transfer ownership to a new address or relinquish root key control entirely. +authors: Georgios Konstantopoulos, Tanishk Goyal, jxom +status: Draft +related: AccountKeychain, TIP-1004, Tempo Transaction Spec +--- + +# TIP-1008: Account Ownership Transfer + +## Abstract + +TIP-1008 introduces the ability for Tempo accounts to transfer ownership to a new address or permanently disable root key control by transferring to `address(0)`. This enables multisig governance (where the root key should not have unilateral control) and prepares for post-quantum cryptography migration (where accounts can add a PQ-secure key and disable the classical root key). + +To ensure security, this TIP also modifies signature verification at the protocol level: `ecrecover` and other signature verification precompiles will check if an account has transferred ownership, preventing the original root key from signing valid messages (including EIP-2612 permits, Permit2, and any EIP-712 signed messages). + +## Motivation + +### Multisig Governance + +Currently, multisig wallets on Tempo still have a root key that could theoretically bypass the multisig. For true multisig security, the root key must be completely disabled, forcing all operations through the multisig logic. + +### Post-Quantum Cryptography + +As quantum computing advances, accounts will need to migrate from ECDSA (secp256k1/P256) to post-quantum signature schemes. This requires: +1. Adding a PQ-secure access key +2. Disabling the classical root key to prevent quantum attacks + +### Signature Verification Attack Surface + +Without protocol-level enforcement, a "disabled" root key could still sign valid messages for: +- EIP-2612 `permit()` on TIP-20 tokens +- Permit2 signatures +- Any contract using `ecrecover` directly +- EIP-712 typed data signatures + +This TIP ensures that ownership transfer/relinquishment is enforced at the signature verification level, not just the account level. + +--- + +# Specification + +## Storage Changes + +### AccountKeychain Precompile + +Add a new storage mapping to track account ownership: + +```solidity +// owner[account] -> owner address +// - address(0) means the root key (derived from account address) is the owner (default) +// - any other address means ownership has been transferred to that address +// - account address means ownership is relinquished (root key disabled, no new owner) +mapping(address account => address owner) public accountOwner; +``` + +**Ownership States:** + +| `accountOwner[account]` | Meaning | +|------------------------|---------| +| `address(0)` | Default: root key (= account address) is owner | +| `account` (self) | Relinquished: root key disabled, no external owner | +| Other address | Transferred: new address is the owner | + +## New Precompile Functions + +### `transferOwnership` + +```solidity +/// @notice Transfer account ownership to a new address or relinquish control +/// @param newOwner The new owner address. Use address(0) to relinquish control. +/// @dev Can only be called by the current owner (root key or transferred owner). +/// - If newOwner is address(0), sets accountOwner[account] = account (relinquished) +/// - Otherwise, sets accountOwner[account] = newOwner +/// Once relinquished (set to self), ownership CANNOT be reclaimed. +/// Emits OwnershipTransferred event. +function transferOwnership(address newOwner) external; +``` + +### `getOwner` + +```solidity +/// @notice Get the current owner of an account +/// @param account The account to query +/// @return owner The owner address: +/// - address(0) if root key is owner (default) +/// - account address if relinquished +/// - other address if transferred +function getOwner(address account) external view returns (address owner); +``` + +### `isRootKeyActive` + +```solidity +/// @notice Check if the root key is still active for an account +/// @param account The account to query +/// @return active True if root key can sign for this account, false if ownership transferred/relinquished +/// @dev Returns true only if accountOwner[account] == address(0) +function isRootKeyActive(address account) external view returns (bool active); +``` + +## New Events + +```solidity +/// @notice Emitted when account ownership is transferred or relinquished +/// @param account The account whose ownership changed +/// @param previousOwner The previous owner (address(0) for root key) +/// @param newOwner The new owner (account address if relinquished, address(0) means root key) +event OwnershipTransferred( + address indexed account, + address indexed previousOwner, + address indexed newOwner +); +``` + +## New Errors + +```solidity +/// @notice Caller is not the current owner of the account +error NotOwner(); + +/// @notice Cannot transfer ownership - account ownership has been relinquished +error OwnershipRelinquished(); + +/// @notice Cannot transfer ownership to the account's own address directly +/// @dev Use address(0) to relinquish ownership instead +error InvalidNewOwner(); +``` + +## Ownership Transfer Behavior + +### Authorization + +Only the current owner can call `transferOwnership`: + +1. **Default state** (`accountOwner[account] == address(0)`): The root key (transaction signed by account's private key) can transfer +2. **Transferred state** (`accountOwner[account] == someAddress`): Only `someAddress` can call (as msg.sender or via their root key) +3. **Relinquished state** (`accountOwner[account] == account`): NO ONE can transfer - ownership is permanently disabled + +### Transfer Logic + +```solidity +function transferOwnership(address newOwner) external { + address account = msg.sender; + address currentOwner = accountOwner[account]; + + // Check authorization + // If currentOwner is address(0), root key is owner - checked via transactionKey + // If currentOwner is account itself, ownership is relinquished + if (currentOwner == account) { + revert OwnershipRelinquished(); + } + + // For non-default ownership, the caller must be the current owner + if (currentOwner != address(0)) { + if (msg.sender != currentOwner) { + revert NotOwner(); + } + } else { + // Default state: root key must authorize + if (transactionKey != address(0)) { + revert NotOwner(); + } + } + + // Cannot set newOwner to account address directly - use address(0) for relinquish + if (newOwner == account) { + revert InvalidNewOwner(); + } + + address effectiveNewOwner = newOwner; + if (newOwner == address(0)) { + // Relinquish: store account address to mark as permanently disabled + effectiveNewOwner = account; + } + + emit OwnershipTransferred(account, currentOwner, effectiveNewOwner); + accountOwner[account] = effectiveNewOwner; +} +``` + +## Signature Verification Modifications + +### ecrecover Interception + +The protocol MUST intercept `ecrecover` (precompile at `0x01`) to check ownership status: + +``` +ecrecover_modified(hash, v, r, s): + 1. recovered_address = ecrecover_original(hash, v, r, s) + 2. if recovered_address == address(0): + return address(0) // Invalid signature + 3. owner_status = AccountKeychain.accountOwner[recovered_address] + 4. if owner_status != address(0): + // Ownership transferred or relinquished - root key invalid + return address(0) + 5. return recovered_address +``` + +### P256 and WebAuthn Verification + +The same ownership check MUST be applied to P256 (`0x100`) and WebAuthn signature verification precompiles. + +## Gas Costs + +| Operation | Gas Cost | +|-----------|----------| +| `transferOwnership` | 25,000 (storage write) | +| `getOwner` | 2,600 (cold) / 100 (warm) | +| `isRootKeyActive` | 2,600 (cold) / 100 (warm) | +| `ecrecover` ownership check | +2,600 (cold) / +100 (warm) per call | + +--- + +# Invariants + +## Ownership State Machine + +``` +┌─────────────────┐ +│ Default State │ +│ owner = 0x0 │ +│ (root key) │ +└────────┬────────┘ + │ transferOwnership(addr) + ▼ +┌─────────────────┐ transferOwnership(0x0) ┌─────────────────┐ +│ Transferred │ ─────────────────────────────► │ Relinquished │ +│ owner = addr │ │ owner = self │ +└────────┬────────┘ └─────────────────┘ + │ transferOwnership(newAddr) │ + ▼ │ +┌─────────────────┐ │ +│ Transferred │ │ +│ owner = newAddr │ ───────────────────────────────────────┘ +└─────────────────┘ transferOwnership(0x0) +``` + +## Core Invariants + +1. **Ownership monotonicity**: Once ownership is relinquished, it can NEVER be reclaimed +2. **Root key invalidation**: After ownership transfer, `ecrecover` returning the account address MUST fail ownership checks +3. **Access key preservation**: Existing access keys remain valid after ownership transfer +4. **Signature verification consistency**: All signature verification paths MUST enforce ownership checks