// SPDX-License-Identifier: MIT pragma solidity ^0.8.24; /* * ============================================================================ * NOTICE * This is a REFERENCE IMPLEMENTATION. It has NOT been audited. * Do not delegate an address that holds anything of value to this code. * The production contract address and its code hash will be announced only * by @DecessorETH (https://x.com/DecessorETH). Any other address is not ours. * Experimental software: bugs are possible. Nothing here is financial advice. * ============================================================================ * * Decessor: an address with a successor. * * An externally owned account (EOA) points its code at this contract with an * EIP-7702 authorization. It keeps its address and its private key. From then * on, this code runs in the context of the EOA: address(this) is the EOA, the * storage below lives in the EOA's own storage, and the EOA's balance is the * balance that passes on. * * configure(successors[], silence) -> owner only ; silence >= 30 days * execute(to, value, data) / pulse() -> owner only ; lastSeen = now * claim() -> successor[i] only ; require now > lastSeen + silence + i * CASCADE_DELAY ; graceEnd = now + 30 days * cancel() -> owner only ; graceEnd = 0 (any owner transaction also cancels) * take() -> claimant only ; require graceEnd != 0 && now > graceEnd ; control -> successor ; fee -> treasury * * "Owner" means the EOA calling its own code (msg.sender == address(this)), * which is what happens when the key holder sends a transaction to their own * address. Every owner call goes through _seen(), which resets the clock and * cancels any pending claim. * * js/sim.js mirrors this file function by function (same names, same * require() order, same revert strings, same constants) and is unit tested in * tests/sim.test.js. Keep the two in sync. */ contract Decessor { /* ------------------------------------------------------------- constants */ /// @notice Time a claimant must wait after claim() before take(). The owner can cancel throughout. uint64 public constant GRACE = 30 days; /// @notice Extra wait for each later successor: successor i may claim after silence + i * CASCADE_DELAY. uint64 public constant CASCADE_DELAY = 90 days; /// @notice Shortest silence an owner may configure. uint64 public constant MIN_SILENCE = 30 days; /// @notice Longest silence an owner may configure (guards against locking successors out for centuries). uint64 public constant MAX_SILENCE = 3650 days; /// @notice Maximum length of the succession cascade. uint256 public constant MAX_SUCCESSORS = 3; /// @notice Fee on the ETH that passes at take(), in basis points: 50 = 0.5%. Nothing is charged before. uint256 public constant FEE_BPS = 50; uint256 public constant BPS = 10_000; /// @notice Receives the fee at take(). Immutable values live in the runtime bytecode, /// so they are readable from a delegated EOA (unlike this contract's own storage). address public immutable TREASURY; enum State { ACTIVE, CLAIMABLE, IN_GRACE, PASSED } /* --------------------------------------------------------------- storage */ /// @dev Namespaced storage (ERC-7201). A delegated EOA may have used other code before, or may /// later, so nothing is kept at low slots that another delegate could also be writing to. /// @custom:storage-location erc7201:decessor.storage.v1 struct Layout { address[] successors; // slot S : length ; keccak256(S) + i : successors[i] uint64 silence; // slot S + 1 : bytes 0..7 uint64 lastSeen; // slot S + 1 : bytes 8..15 uint64 graceEnd; // slot S + 1 : bytes 16..23 (0 = no pending claim) uint8 claimant; // slot S + 1 : byte 24 (index into successors, valid while graceEnd != 0) address controller; // slot S + 2 : the successor who took control (0 = not passed) } /// @dev keccak256(abi.encode(uint256(keccak256("decessor.storage.v1")) - 1)) & ~bytes32(uint256(0xff)) bytes32 private constant STORAGE_SLOT = 0xfa8125a9fac28d1f6f4b05fe454526c3d5323babf90eb8663d81994abd87e200; function _layout() private pure returns (Layout storage s) { assembly { s.slot := STORAGE_SLOT } } /* ---------------------------------------------------------------- events */ event Configured(address[] successors, uint64 silence); event Seen(uint64 lastSeen); event Claimed(address indexed successor, uint256 index, uint64 graceEnd); event Cancelled(address indexed claimant); event Passed(address indexed successor, uint256 fee); event Executed(address indexed to, uint256 value, bytes data); /* ----------------------------------------------------------- constructor */ /// @dev The constructor runs once, for the implementation contract. Delegating EOAs never run it, /// which is fine: they only read the immutable TREASURY and their own namespaced storage. constructor(address treasury) { require(treasury != address(0), "Decessor: zero treasury"); TREASURY = treasury; } /* ------------------------------------------------------------- modifiers */ /// @dev The EOA itself, before any take(). Under EIP-7702 a transaction the key holder sends to /// their own address arrives with msg.sender == address(this). Nobody else can produce that. function _onlyOwner() private view returns (Layout storage s) { s = _layout(); require(msg.sender == address(this), "Decessor: not owner"); require(s.controller == address(0), "Decessor: passed"); } /// @dev Every owner transaction: reset the clock, cancel any pending claim. function _seen(Layout storage s) private { s.lastSeen = uint64(block.timestamp); if (s.graceEnd != 0) { emit Cancelled(s.successors[s.claimant]); s.graceEnd = 0; s.claimant = 0; } emit Seen(uint64(block.timestamp)); } /* ----------------------------------------------------------------- owner */ /// @notice Name successors in order and set the silence period. Replaces any previous configuration. /// @param successors 1 to MAX_SUCCESSORS distinct addresses; successors[0] may claim first. /// @param silence seconds without an owner transaction before successors[0] may claim. function configure(address[] calldata successors, uint64 silence) external { Layout storage s = _onlyOwner(); uint256 n = successors.length; require(n > 0 && n <= MAX_SUCCESSORS, "Decessor: bad successor count"); require(silence >= MIN_SILENCE, "Decessor: silence too short"); require(silence <= MAX_SILENCE, "Decessor: silence too long"); for (uint256 i; i < n; ++i) { address a = successors[i]; require(a != address(0), "Decessor: zero successor"); require(a != address(this), "Decessor: self successor"); for (uint256 j; j < i; ++j) { require(successors[j] != a, "Decessor: duplicate successor"); } } _seen(s); s.successors = successors; s.silence = silence; emit Configured(successors, silence); } /// @notice Reset the clock without moving anything. function pulse() external { Layout storage s = _onlyOwner(); _seen(s); } /// @notice Make a call from the address. Before take(): owner only, and it resets the clock. /// After take(): the controller (the successor) acts for the address, and the clock is over. function execute(address to, uint256 value, bytes calldata data) external returns (bytes memory result) { Layout storage s = _layout(); if (s.controller != address(0)) { require(msg.sender == s.controller, "Decessor: not controller"); } else { s = _onlyOwner(); _seen(s); } emit Executed(to, value, data); bool ok; (ok, result) = to.call{value: value}(data); if (!ok) { // Bubble up the callee's revert data unchanged. assembly { revert(add(result, 0x20), mload(result)) } } } /// @notice Cancel a pending claim. Any owner transaction does the same; this one says so explicitly. /// Available until take() has actually happened, even after the grace period has run out. function cancel() external { Layout storage s = _onlyOwner(); require(s.graceEnd != 0, "Decessor: no claim"); _seen(s); } /* ------------------------------------------------------------ successors */ /// @notice Start the grace period. successors[i] may claim once the owner has been silent for /// silence + i * CASCADE_DELAY. Earlier successors' windows stay open. function claim() external { Layout storage s = _layout(); require(s.controller == address(0), "Decessor: passed"); uint256 n = s.successors.length; require(n != 0, "Decessor: not configured"); uint256 i = _indexOf(s, msg.sender); require(i < n, "Decessor: not successor"); require(s.graceEnd == 0, "Decessor: claim pending"); require(block.timestamp > uint256(s.lastSeen) + s.silence + i * CASCADE_DELAY, "Decessor: silence not over"); s.claimant = uint8(i); s.graceEnd = uint64(block.timestamp + GRACE); emit Claimed(msg.sender, i, s.graceEnd); } /// @notice After grace, the claimant takes control of the address. The fee is charged here and /// only here: FEE_BPS of the ETH held by the address, sent to TREASURY. function take() external { Layout storage s = _layout(); require(s.controller == address(0), "Decessor: passed"); require(s.graceEnd != 0, "Decessor: no claim"); address heir = s.successors[s.claimant]; require(msg.sender == heir, "Decessor: not claimant"); require(block.timestamp > s.graceEnd, "Decessor: grace not over"); // Effects before the interaction. s.controller = heir; s.graceEnd = 0; uint256 fee = (address(this).balance * FEE_BPS) / BPS; emit Passed(heir, fee); if (fee != 0) { (bool ok,) = TREASURY.call{value: fee}(""); require(ok, "Decessor: fee transfer failed"); } } /* ----------------------------------------------------------------- views */ function state() public view returns (State) { Layout storage s = _layout(); if (s.controller != address(0)) return State.PASSED; if (s.graceEnd != 0) return State.IN_GRACE; if (s.successors.length != 0 && block.timestamp > uint256(s.lastSeen) + s.silence) return State.CLAIMABLE; return State.ACTIVE; } /// @notice Who controls the address through this code: the EOA itself, then the successor after take(). function owner() external view returns (address) { address c = _layout().controller; return c != address(0) ? c : address(this); } function getSuccessors() external view returns (address[] memory) { return _layout().successors; } function config() external view returns (uint64 silence, uint64 lastSeen, uint64 graceEnd, uint8 claimant, address controller) { Layout storage s = _layout(); return (s.silence, s.lastSeen, s.graceEnd, s.claimant, s.controller); } function _indexOf(Layout storage s, address who) private view returns (uint256 i) { uint256 n = s.successors.length; for (; i < n; ++i) { if (s.successors[i] == who) return i; } // returns n when not found } /* ------------------------------------------------- receiving assets */ // A delegated EOA has code, so plain ETH transfers and safe token transfers now call into it. // Without these, the address could no longer receive ETH, ERC-721 or ERC-1155 tokens. // Receiving never touches the clock: only the owner's own transactions count. receive() external payable {} function onERC721Received(address, address, uint256, bytes calldata) external pure returns (bytes4) { return this.onERC721Received.selector; } function onERC1155Received(address, address, uint256, uint256, bytes calldata) external pure returns (bytes4) { return this.onERC1155Received.selector; } function onERC1155BatchReceived(address, address, uint256[] calldata, uint256[] calldata, bytes calldata) external pure returns (bytes4) { return this.onERC1155BatchReceived.selector; } function supportsInterface(bytes4 interfaceId) external pure returns (bool) { return interfaceId == 0x01ffc9a7 // ERC-165 || interfaceId == 0x150b7a02 // ERC-721 receiver || interfaceId == 0x4e2312e0; // ERC-1155 receiver } }