AeternumAeternum
Back to App

Security Design

Reentrancy protection, permissionless execution safety, the CEI pattern, and the immutable contract guarantee.


Non-Custodial by Design

The foundation of Aeternum's security model is non-custody: the protocol never owns your funds. The contract holds them in escrow on your behalf and executes only what you have configured. There is no admin key that can redirect funds, no pause mechanism that can freeze withdrawals, and no upgrade path that can rewrite the rules after you have deposited.

This is not a promise from a team — it is a property of the code. AeternumVault has no Ownable inheritance, no onlyOwner modifier on any function, and no proxy pattern. The deployed bytecode is what runs, permanently.


Reentrancy Protection: Two Independent Layers

A reentrancy attack occurs when a malicious contract calls back into an ETH-sending function before the first call completes — exploiting the window between "send ETH" and "update state" to steal funds multiple times. AeternumVault uses two independent defenses.

Layer 1 — Checks-Effects-Interactions (CEI)

Every function that transfers ETH follows a strict ordering: update all state before making the external call. By the time ETH leaves the contract, the caller's balance is already recorded as zero. Even if a malicious backup address attempted a reentrancy, there would be nothing left to steal:

function _executeRecovery(address wallet) internal {
    // ... checks (re-validate eligibility) ...
 
    uint256 amount = config.balance;
 
    // --- Effects (ALL before external call)
    config.balance = 0;               // zeroed before ETH moves
    config.isActive = false;          // removed from active state
    config.failedRecoveryAttempts = newAttempts;
    _removeFromRegistry(wallet);      // removed from registry
 
    // --- Interaction (ETH transfer happens last)
    (bool success,) = backupAddress.call{value: amount}("");
 
    if (!success) {
        // Restore balance only if transfer failed — safe because nonReentrant
        // locks all reentrant calls and balance was zeroed above
        config.balance = amount;
        // ... failure handling ...
    }
}

Layer 2 — ReentrancyGuard

Every function that sends ETH is protected by OpenZeppelin's ReentrancyGuard:

contract AeternumVault is IAeternumVault, ReentrancyGuard {

ReentrancyGuard maintains a locked/unlocked mutex in contract storage. Any attempt to invoke a guarded function while one is already executing immediately reverts — blocking reentrant calls at the function boundary, independently of how state is ordered inside the function. On the recovery path, this guard sits on triggerRecovery() — the sole external entry point into _executeRecovery().

The two layers address different threat surfaces. CEI eliminates the economic incentive: config.balance is zeroed before any ETH transfer, so a reentrant call finds nothing left to steal. ReentrancyGuard provides a hard execution boundary that holds even on the failure-restore path — where some state writes unavoidably occur after the external call because the contract can only determine a transfer has failed after attempting it. A third layer closes the loop: after MAX_RECOVERY_ATTEMPTS consecutive failures, the vault is permanently abandoned and removed from monitoring, eliminating the retry surface entirely.

Info

The internal security audit (AV-01) confirmed this architecture. Slither classifies the failure-restore path finding as reentrancy-benign (Low) rather than reentrancy-eth (Medium), because config.balance is provably zeroed before the call — making double-spend impossible regardless of reentrant behavior. No High or Medium severity findings remain open in the current revision.


Permissionless Execution — Why No Access Gate Is Needed

triggerRecovery() is the only function through which funds move without an explicit user instruction — which is exactly why it's worth asking what stops an arbitrary caller from doing something harmful with it, given that literally any address can call it.

The answer isn't a permission check on the caller. There isn't one:

function triggerRecovery(address wallet) external nonReentrant {
    _executeRecovery(wallet);
}

No modifier restricts msg.sender here at all — nonReentrant guards against reentrancy, not against who is calling. The safety property comes from somewhere else entirely: the function gives the caller no leverage to begin with. The caller supplies only a wallet address. Every subsequent decision — whether recovery executes, how much moves, and where it goes — is made exclusively from storage the vault owner wrote, re-validated from scratch inside _executeRecovery() on every single call:

if (!config.isActive) return;
if (config.balance == 0) return;
if (block.timestamp < config.lastActivity + config.inactivityPeriod) return;

A caller can trigger the check. A caller cannot influence its outcome. Removing the requirement for a single designated caller doesn't weaken this property in any way — it just removes a dependency on any one party remaining available to submit the transaction. See Keeper Network for how this plays out with multiple keepers calling concurrently.

This wasn't always the design

Earlier versions of this contract gated recovery execution behind a one-time-set forwarder address — a specific address, registered once at deployment, that was the only caller permitted to execute recovery. That model has been removed entirely (audit finding AV-04, resolved). It eliminated both the forwarder-registration bootstrap window it required and the single-point-of-failure risk of depending on one automation provider's infrastructure remaining online. The permissionless model above is not a simplification of the old one — it's a different, and more trustless, security property.


Direct ETH Rejection

The contract's receive() function is set to revert unconditionally:

receive() external payable {
    revert AeternumVault__DirectTransferNotAllowed();
}

If you accidentally send ETH directly to the contract address without going through deposit() or register(), the transaction fails immediately and your ETH is returned. This prevents funds from becoming permanently stranded in the contract with no vault association.


No Admin Key

The absence of admin functionality is worth stating explicitly because it is an unusual design choice. Many DeFi contracts include emergency pause mechanisms, fee collection functions, or upgrade paths "for safety". These introduce a class of risk: the admin key can be compromised, the multisig can be coerced, the governance vote can be manipulated.

AeternumVault has none of these. There is no privileged operation of any kind, anywhere in the contract — not even a one-time bootstrap call.

Common admin functionPresent in AeternumVault?
Pause / unpause vaultNo
Drain contract fundsNo
Change fee parametersNo
Upgrade contract logicNo
Override user recovery configNo
Gate recovery execution to a specific callerNo

Immutable Contract Guarantee

Each deployed version of AeternumVault is immutable and cannot be modified after deployment. There are no proxy upgrades, no admin-controlled logic changes, and no governance mechanisms that could alter how your vault operates once you have deposited.

The rules you agree to when registering — the inactivity bounds, the retry cap, and the recovery logic itself — are fixed for the lifetime of that deployed contract.

Aeternum Labs will continue developing new versions, deployed as entirely new contracts. You choose whether to stay on your current deployment or migrate to a newer one. You are never forced to upgrade.


Testing and Audit Status

AeternumVault has completed a comprehensive internal security assessment (Revision 3, 7th July 2026), superseding the prior assessment from June 2026 (Revision 2). The revised assessment reflects the current contract architecture — a permissionless keeper model, no subscription or treasury logic.

The full report is available here: Aeternum Internal Audit Report (Revision 3).

External audit required before mainnet

The internal assessment was conducted by the contract author. An independent external audit by a professional security firm is required before mainnet deployment. The contract should not be used for real-value ETH until that audit is complete and its findings have been addressed. Mainnet gas cost estimates have not yet been validated under production fork conditions, and the off-chain keeper bot infrastructure that submits recovery transactions has not been separately audited.

Test Coverage

Test typeDetailResult
Unit, fuzz, and invariant tests (combined)127 tests total — 100% line and function coverage on AeternumVault.sol127 passing, 0 failing
Fuzz tests6 functions × 256 runs — inactivity period bounds, deposit/send amounts, recovery timing, permissionless caller checks, pagination edge casesAll passing
Foundry invariant tests3 properties × 128,000+ calls — ETH balance solvency, registry active-status integrity, abandoned-wallet exclusionAll passing
Echidna property fuzzing12 properties × 50,127 randomised call sequencesAll passing
Static analysisSlither v0.11.4 — 0 High, 0 Medium findingsSee finding summary below
LintingSolhintNot re-run this revision — Revision 2 result (0 errors, 0 warnings) carried forward for reference only

Internal Audit Finding Summary

SeverityCountStatus
High0
Medium01 finding resolved in this revision — see AV-02
Low2Acknowledged — mitigated or acceptable
Informational5Acknowledged

The two open Low findings and all five Informational findings are covered in full in the linked report. The two most relevant to this page are detailed below.

AV-01 (Low) — Reentrancy-benign in _executeRecovery: On the failure-restore path, several state variables are written after the external ETH transfer call. These post-call writes are unavoidable — the contract can only determine a transfer has failed after making it. Slither classifies this as reentrancy-benign rather than reentrancy-eth because config.balance is provably zeroed before the call. Three independent layers prevent exploitation: nonReentrant on triggerRecovery, the pre-call balance zeroing, and permanent vault abandonment after MAX_RECOVERY_ATTEMPTS failures.

AV-02 (Medium, resolved) — External calls inside a batch recovery loop: In the prior forwarder-gated architecture, a single automation call could iterate internally over multiple wallets, containing an external ETH transfer inside that loop. This is resolved as a direct consequence of the current architecture: triggerRecovery() now processes exactly one wallet per call, with no loop inside the contract at all. Batch submission, where it happens, is handled externally by whichever keeper is submitting transactions — outside the contract, and outside this audit's scope — not by AeternumVault.sol itself. No function in the current contract contains an external call inside a loop.