A trustless, non-custodial inheritance protocol for Ethereum assets.
Aeternum Core lets users store ETH in a self-sovereign vault, send and receive funds like a normal wallet, and configure a backup address with an inactivity timer. If the user goes silent beyond their chosen period — lost keys, death, incapacitation — the protocol automatically transfers their ETH to the backup address. No custodians. No admin backdoors. Just code.
- Documentation
- Architecture
- Repository Structure
- Trust Model
- How It Works
- Immutable Variables
- Dependencies
- Quickstart
- Development
- Post-Deployment
- Security
- Audit
- License
Aeternum Core is a single-contract architecture. Each registered wallet has its own isolated vault within the contract — balances are tracked individually, never pooled. Recovery is triggered via a permissionless triggerRecovery(wallet) function: any external address may call it once a wallet's inactivity period has elapsed, and the contract independently re-validates all safety conditions regardless of who calls it.
In practice, recovery is automated by the Aeternum Labs keeper bot, with Gelato and Chainlink CRE planned as additional independent keepers in later phases. None of this requires a contract change — every keeper, present or future, is just another permissionless caller of triggerRecovery. For the full keeper architecture — off-chain discovery, on-chain validation, batched execution, and the multi-chain rollout — see the Keeper Network documentation.
AeternumVault
├── User Vault (per address)
│ ├── balance ← ETH held in escrow
│ ├── backupAddress ← recovery destination
│ ├── inactivityPeriod ← seconds before recovery triggers
│ └── lastActivity ← timestamp of last on-chain interaction
│
└── Keeper Interface (permissionless)
├── triggerRecovery(wallet) ← permissionless recovery entry point
├── isRecoveryDue(wallet) ← free view; used by the Aeternum Labs bot to
│ re-validate DB candidates before submission
└── getTriggerableVaultsBatch(start, size) ← permissionless batch view for querying due
wallets directly from chain state; not used
by the Aeternum Labs bot's live scan path,
which relies on its own indexed database
instead. Available to any other keeper,
researcher, or tool that wants to query due
wallets without running its own indexer.
aeternum-core/
├── src/
│ ├── AeternumVault.sol ← Core contract
│ └── interfaces/
│ └── IAeternumVault.sol ← Full interface (events, errors, structs, functions)
│
├── test/
│ ├── unit/
│ │ └── AeternumVault.t.sol ← Unit, fuzz, and invariant tests
│ ├── invariant/
│ │ └── AeternumVaultEchidna.sol ← Echidna property-based fuzzing suite
│ └── mocks/
│ ├── ReentrantAttacker.sol ← Reentrancy security test helper
│ ├── RejectingReceiver.sol ← Failed recovery simulation helper
│ └── RejectingCallerMock.sol ← Transfer failure simulation (withdrawAll/cancel)
│
├── script/
│ ├── Deploy.s.sol ← Deployment script with post-deploy checks
│ └── HelperConfig.s.sol ← Network-aware configuration resolver
│
├── audits/
│ ├── 2026-07-07_Aeternum-core_audit_rev3.pdf ← Security audit — Revision 3 (current, keeper architecture)
│ ├── 2026-06-03_Aeternum-core_audit_rev2.pdf ← Security audit — Revision 2 (fee-free model)
│ └── 2026-05-04_Aeternum-core_audit_rev1.pdf ← Security audit — Revision 1 (original pre-audit)
│
├── lib/
│ ├── forge-std ← Foundry standard library
│ └── openzeppelin-contracts ← OpenZeppelin contracts
│
├── echidna.config.yml ← Echidna fuzzer configuration
├── foundry.lock ← Foundry dependency lockfile
├── foundry.toml ← Foundry configuration
├── .solhint.json ← Solhint linting rules
├── .env.example ← Environment variable template
├── README.md ← Project overview and documentation
├── CONTRIBUTING.md ← Contribution guidelines
├── LICENSE.md ← License (BUSL 1.1)
└── SECURITY.md ← Security policy and vulnerability disclosure
| Actor | Can Do | Cannot Do |
|---|---|---|
| User | Register, deposit, send, withdrawAll, ping, update config, cancel | Access other users' funds |
| Any external caller (Aeternum Labs' keeper bot, a future Gelato/CRE keeper, the beneficiary, or any other address) | Call triggerRecovery(wallet) once inactivity conditions are met; call getTriggerableVaultsBatch (view) |
Alter configs, redirect funds, or trigger recovery before the inactivity period elapses |
| No one | — | Pause recovery, upgrade the contract, or access user funds |
1. Register
The user calls register() with a backup address and inactivity period. ETH deposited at registration goes directly into their vault.
2. Use the vault
The vault behaves like a normal wallet. Users can deposit() ETH, send() ETH to any address, withdrawAll() back to themselves, and update their recovery configuration at any time. Every interaction resets the inactivity timer — proving liveness to the contract.
3. Stay active
If a user wants to prove liveness without moving funds, they call ping() — a single cheap storage write that resets the timer.
4. Recovery triggers
When a wallet's inactivity period has elapsed and its balance is non-zero, any external caller may call triggerRecovery(wallet) to transfer the escrowed ETH to the registered backup address. The contract validates all conditions independently of the caller — the caller supplies only a wallet address and has no influence over the outcome. In practice, this is automated by the Aeternum Labs keeper bot in the current phase; see Keeper Network for how it discovers, validates, and submits due wallets.
5. Failed recovery
If a backup address cannot receive ETH (e.g. a contract that rejects transfers), the failure is counted. After MAX_RECOVERY_ATTEMPTS consecutive failures, the vault is deregistered and the balance remains fully accessible — the user can still send() or withdrawAll() at any time, and can re-register with a new backup address.
6. Cancel anytime
Users can call cancelRecovery() at any time to withdraw their full balance and deregister from monitoring in a single transaction.
| Immutable Variable | Value | Description |
|---|---|---|
MIN_INACTIVITY_PERIOD |
180 days (5 minutes for testnet) | Minimum inactivity period users are allowed to configure |
MAX_INACTIVITY_PERIOD |
3650 days | Maximum allowed inactivity period to prevent permanent fund lockup |
MAX_RECOVERY_ATTEMPTS |
3 | Consecutive failed recovery attempts before a vault is permanently abandoned |
- Foundry — Development framework
curl -L https://foundry.paradigm.xyz | bash
foundryup- OpenZeppelin Contracts — ReentrancyGuard
forge install OpenZeppelin/openzeppelin-contracts --no-commit- Slither — Static analysis
pip install slither-analyzer --break-system-packages- Echidna — Property-based fuzz testing
docker pull ghcr.io/crytic/echidna/echidna- Solhint — Linting
npm install -g solhint- lcov — Coverage reports
# Ubuntu
sudo apt install lcov
# macOS
brew install lcovgit clone https://github.com/Aeternumlabs/aeternum-core.git
cd aeternum-coreforge installcp .env.example .envforge build# Full test suite
forge test -vv
# With gas report
forge test --gas-report
# Specific test
forge test --match-test test_triggerRecovery_executesRecovery -vvvv
# Fuzz tests only
forge test --match-test testFuzz -vv
# Invariant tests only
forge test --match-test invariant -vvforge coverage# Slither
slither src/AeternumVault.sol \
--solc-remaps "@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/"
# Solhint
solhint src/AeternumVault.sol# Create Echidna alias
alias echidna='docker run --rm -v $(pwd):/src -w /src ghcr.io/crytic/echidna/echidna echidna'
source ~/.bashrc
# Run property-based fuzzing
echidna test/invariant/AeternumVaultEchidna.sol \
--contract AeternumVaultEchidna \
--config echidna.config.yml# Import your private key into Foundry's local keystore
cast wallet import private-key --interactive
# Dry run — Sepolia
forge script script/Deploy.s.sol \
--rpc-url $SEPOLIA_RPC_URL \
-vvvv
# Live deploy + Etherscan verification — Sepolia
forge script script/Deploy.s.sol \
--account private-key \
--rpc-url $SEPOLIA_RPC_URL \
--etherscan-api-key $ETHERSCAN_API_KEY \
--broadcast \
--verify \
-vvvv
# Dry run — Mainnet (always run before broadcasting)
forge script script/Deploy.s.sol \
--rpc-url $MAINNET_RPC_URL \
-vvvv
# Live deploy + Etherscan verification — Mainnet
forge script script/Deploy.s.sol \
--account private-key \
--rpc-url $MAINNET_RPC_URL \
--etherscan-api-key $ETHERSCAN_API_KEY \
--broadcast \
--verify \
-vvvvAfter deploying the contract:
- Copy the deployed contract address into
.envasCONTRACT_ADDRESS. - Set
NEXT_PUBLIC_SEPOLIA_CONTRACT_ADDRESSin the frontend.env. - Update the Ponder indexer start block and contract address in
ponder.config.ts. - Start the keeper bot, pointing it at the deployed contract address and the running Ponder instance. The bot begins monitoring immediately — no on-chain registration required.
- Verify end-to-end: register a test vault with the minimum inactivity period, wait for expiry, and confirm the keeper bot submits
triggerRecoveryand the ETH reaches the backup address.
- Checks-Effects-Interactions (CEI) enforced on all ETH-transferring paths
- ReentrancyGuard applied as a secondary defence layer on all state-changing functions
- No admin backdoors — the contract contains no owner or privileged roles capable of pausing recovery, redirecting funds, or accessing user balances
- Permissionless entry point safety —
triggerRecovery(wallet)delegates entirely to_executeRecovery, which re-validates all conditions from storage before acting. The caller supplies only a wallet address — they cannot redirect funds, force early recovery, or cause double-spend regardless of who they are - O(1) registry removal — swap-and-pop with 1-indexed mappings prevents array corruption across any sequence of removals, whether from separate transactions, multiple
triggerRecoverycalls batched into a single transaction, or several keepers acting independently - Failed recovery handling — after
MAX_RECOVERY_ATTEMPTSconsecutive failures, the vault is permanently abandoned and the balance remains self-claimable viawithdrawAll()orsend() - Direct ETH transfer rejection —
receive()explicitly reverts, preventing accidental ETH loss
block.timestampis used for inactivity comparisons. Validator manipulation is bounded to ~12 seconds — negligible for inactivity periods measured in days and months.- The external ETH transfer in
_executeRecoveryis acknowledged (Slither: reentrancy-eth). Exploitation is prevented bynonReentrantontriggerRecovery, balance zeroing before the call (CEI), and permanent abandonment afterMAX_RECOVERY_ATTEMPTSconsecutive failures — eliminating the retry loop entirely.
- Revision 3 — 7th July 2026 — current. Reflects the migration from Chainlink Automation to the permissionless keeper architecture.
- Revision 2 — 3rd June 2026 — fee-free model migration.
- Revision 1 — 4th May 2026 — original pre-audit.
Pending — targeting external audit engagement prior to mainnet deployment.
Details to be announced soon.
AeternumVault V1 is source-available and licensed under the Business Source License 1.1 (BUSL-1.1). The protocol will automatically transition to GNU General Public License, version 3.0 or later, the earlier of (a) four years from the date of the first production deployment of the Licensed Work on the Ethereum Mainnet, or (b) January 1, 2031.