Skip to content
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
245 changes: 245 additions & 0 deletions src/pages/protocol/tips/tip-1008.mdx
Original file line number Diff line number Diff line change
@@ -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