EIP.tools

EIP.tools

⚠️ DraftStandards Track: ERC

ERC-8238: Coercion-Resistant Vault

A smart contract wallet with spending limits, timelocks, and multisig to protect against physical coercion attacks

Authors
Created2026-04-01
Discussion Linkhttps://ethereum-magicians.org/t/erc-8238-coercion-resistant-vault/28130
Requires

Markdown

https://raw.githubusercontent.com/ethereum/ERCs/re...
Pull Request#1703PR open

EIP-GPT summary

Contents
AbstractMotivationSpecificationDefinitionsInterface — Core (ETH)Interface — ERC-20 Token ExtensionInterface — DeFi Execution ExtensionConstantsBehavior RequirementsHot Balance and Spending Limits — ETHHot Balance and Spending Limits — ERC-20 TokensCold Vault WithdrawalsMultisig BypassGuardian Management InvariantsEmergency PauseToken AllowancesDeFi ExecutionWhitelist ManagementConfiguration SecurityERC-165 SupportERC-4337 Account AbstractionERC-4337 Behavior RequirementsRationaleWhy rate-limited hot balance instead of a fixed hot pool?Why independent spending limits per token?Why shared timelock across all assets?Why allow both timelock AND multisig?Why must configuration changes also be timelocked?Why are epoch duration decreases timelocked?Why can guardians cancel withdrawals?Why an emergency pause mechanism?Why a cap on pending withdrawal requests?Why guard against guardian removal below threshold?Why not use a duress PIN / panic wallet instead?Why allow DeFi execution via whitelisted targets?Why a dedicated approveToken() instead of using execute()?Why is DeFi execution not subject to spending limits?Why require ERC-4337 instead of just direct calls?Why is first-time token configuration immediate?Backwards CompatibilityReference ImplementationSecurity ConsiderationsAttacker holds victim for extended periodsAttacker drains multiple token typesAttacker targets guardiansSocial engineering of guardiansGriefing via cancellationGriefing via emergency pauseFront-running and MEVSmart contract riskERC-20 token risksMalicious whitelisted contractToken allowances via approveToken()Re-entrancy via execute()EntryPoint trust modelSignature replay across chainsConfiguration lock-outCopyright

Abstract

This ERC defines a standard interface for coercion-resistant vault contracts that partition a user's balance into two tiers: a "hot" balance available for immediate spending up to a configurable limit, and a "cold" vault requiring either a timelock delay or multisig approval to unlock. The standard supports both native ETH and ERC-20 tokens, with independent spending limits per asset. The design mirrors the delayed-opening safes used by banks and cash-in-transit companies, where even employees with physical access cannot immediately access the full contents, rendering physical coercion attacks economically unviable.

Motivation

Physical attacks against cryptocurrency holders—commonly known as "5wrenchattacks"—haverisendramatically.Accordingtopubliclymaintaineddatabasesofknownphysicalattacksagainstcryptoholders,reportedincidentsincreasedby169casesworldwideresultinginover5 wrench attacks"—have risen dramatically. According to publicly maintained databases of known physical attacks against crypto holders, reported incidents increased by 169% in 2025, with over 70 confirmed cases worldwide resulting in over 40 million stolen.

Unlike traditional financial systems where banks impose withdrawal limits, wire transfer delays, and fraud departments that can reverse transactions, self-custodial cryptocurrency wallets grant immediate, irreversible access to the entire balance to anyone who controls the private key. This creates a uniquely dangerous situation: attackers can coerce a victim into transferring their entire net worth in minutes, with no possibility of reversal.

Current mitigation strategies have significant limitations:

  • Decoy/duress wallets: Rely on the attacker not knowing about hidden funds. Attackers who research their targets (increasingly common) may not be satisfied with a small decoy balance and may escalate violence.
  • Pure multisig: Effective but introduces friction for everyday spending and requires always-available cosigners.
  • Pure timelock: Prevents all immediate spending, making the wallet impractical for daily use.

This ERC proposes a standard that combines the best of all approaches: immediate access to a limited "hot" balance for daily spending, with the bulk of funds locked behind timelocks and/or multisig requirements. The key insight is that even under physical coercion, the victim can truthfully state: "I can only send you X amount right now. The rest is locked and I physically cannot access it faster."

This approach:

  1. Removes the incentive for physical attacks by limiting the immediately extractable value.
  2. Provides verifiable constraints: The victim is not lying or hiding funds—the constraint is verifiable on-chain. Unlike duress wallets, there is no theater involved.
  3. Allows cancellation: Pending withdrawals from the cold vault can be cancelled during the timelock period, giving the victim time to act after the attacker leaves.
  4. Maintains usability: Daily spending from the hot balance works without friction.

Specification

The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in RFC 2119 and RFC 8174.

Definitions

  • Owner: The primary key holder of the vault, capable of spending from the hot balance and initiating cold vault withdrawals.
  • Guardian: A cosigner in the multisig scheme who can approve cold vault withdrawals or cancel pending withdrawals.
  • Hot balance: The portion of funds available for immediate transfer by the owner, subject to a rate limit. Tracked independently for ETH and each configured ERC-20 token.
  • Cold vault: The portion of funds requiring a timelock delay or multisig approval before transfer.
  • Spending limit: The maximum amount the owner can transfer from the hot balance within a configurable time window (epoch). Each asset (ETH and each ERC-20 token) has its own independent spending limit and epoch duration.
  • Timelock period: The minimum delay between initiating a cold vault withdrawal and the funds becoming transferable. Shared across all assets.
  • Withdrawal request: A pending transfer from the cold vault, subject to the timelock period and cancellable by the owner or any guardian. Each request specifies which asset (ETH or token) is being withdrawn.
  • Pause state: A temporary state invokable by any single guardian that blocks all value-moving operations (hot spends, DeFi execution, new withdrawal requests) for a bounded duration, allowing response to suspicious activity.

Interface — Core (ETH)

Every compliant contract MUST implement the following interface for native ETH:

// SPDX-License-Identifier: CC0-1.0
pragma solidity ^0.8.20;

interface ICoercionResistantVault {

    event Deposited(address indexed sender, uint256 amount);
    event HotSpend(address indexed to, uint256 amount);
    event SpendingLimitChanged(uint256 newLimit, uint256 newEpochDuration);
    event WithdrawalRequested(uint256 indexed requestId, address indexed token, address indexed to, uint256 amount, uint256 unlockTime);
    event WithdrawalExecuted(uint256 indexed requestId);
    event WithdrawalCancelled(uint256 indexed requestId, address cancelledBy);
    event WithdrawalApprovedByMultisig(uint256 indexed requestId);
    event TimelockChanged(uint256 newDuration);
    event GuardianChanged(address indexed guardian, bool added);
    event VaultPaused(address indexed by, uint256 until);
    event VaultUnpaused(address indexed by);

    function totalBalance() external view returns (uint256);
    function hotBalance() external view returns (uint256);
    function coldBalance() external view returns (uint256);
    function spendingLimit() external view returns (uint256);
    function epochDuration() external view returns (uint256);
    function spentInCurrentEpoch() external view returns (uint256);
    function remainingHotBudget() external view returns (uint256);
    function timelockDuration() external view returns (uint256);
    function multisigThreshold() external view returns (uint256);
    function isGuardian(address account) external view returns (bool);
    function pausedUntil() external view returns (uint256);
    function isPaused() external view returns (bool);
    function pendingWithdrawalCount() external view returns (uint256);
    function getWithdrawalRequest(uint256 requestId) external view returns (address token, address to, uint256 amount, uint256 unlockTime, bool executed, bool cancelled, uint256 approvalCount);

    function hotSpend(address payable to, uint256 amount) external;
    function requestWithdrawal(address payable to, uint256 amount) external returns (uint256 requestId);
    function executeWithdrawal(uint256 requestId) external;
    function cancelWithdrawal(uint256 requestId) external;
    function approveWithdrawal(uint256 requestId) external;
    function pause() external;
    function unpause() external;
    function setSpendingLimit(uint256 newLimit, uint256 newEpochDuration) external;
    function setTimelockDuration(uint256 newDuration) external;
    function setGuardian(address guardian, bool active) external;
    function setMultisigThreshold(uint256 threshold) external;
}

Interface — ERC-20 Token Extension

Compliant contracts that support ERC-20 tokens MUST additionally implement the following interface:

interface ICoercionResistantVaultTokens {

    event TokenDeposited(address indexed token, address indexed sender, uint256 amount);
    event TokenHotSpend(address indexed token, address indexed to, uint256 amount);
    event TokenSpendingLimitChanged(address indexed token, uint256 newLimit, uint256 newEpochDuration);

    function totalTokenBalance(address token) external view returns (uint256);
    function remainingTokenHotBudget(address token) external view returns (uint256);
    function tokenHotBalance(address token) external view returns (uint256);
    function tokenColdBalance(address token) external view returns (uint256);
    function tokenSpendingLimit(address token) external view returns (uint256 limit, uint256 epoch);
    function tokenSpentInCurrentEpoch(address token) external view returns (uint256);

    function depositToken(address token, uint256 amount) external;
    function hotSpendToken(address token, address to, uint256 amount) external;
    function requestTokenWithdrawal(address token, address payable to, uint256 amount) external returns (uint256 requestId);
    function setTokenSpendingLimit(address token, uint256 newLimit, uint256 newEpochDuration) external;
}

Interface — DeFi Execution Extension

Compliant contracts that support interaction with external DeFi protocols MUST additionally implement the following interface:

interface ICoercionResistantVaultExecutor {

    event Executed(address indexed target, uint256 value, bytes data, bytes result);
    event BatchExecuted(uint256 count);
    event TokenApproval(address indexed token, address indexed spender, uint256 amount);
    event TargetWhitelisted(address indexed target, bool allowed);
    event WhitelistChangeScheduled(address indexed target, uint256 effectiveTime);
    event WhitelistChangeCancelled(address indexed target);

    function isWhitelisted(address target) external view returns (bool);
    function approveToken(address token, address spender, uint256 amount) external;
    function execute(address target, uint256 value, bytes calldata data) external returns (bytes memory result);
    function executeBatch(address[] calldata targets, uint256[] calldata values, bytes[] calldata datas) external returns (bytes[] memory results);
    function setWhitelistedTarget(address target, bool allowed) external;
    function executeWhitelistChange(address target) external;
    function cancelWhitelistChange(address target) external;
}

Constants

Compliant implementations MUST define the following constants:

  • MAX_PENDING_WITHDRAWALS: the maximum number of concurrent unresolved withdrawal requests. RECOMMENDED value: 32. Prevents unbounded storage growth and DoS via request spam if the owner's key is compromised.
  • MAX_PAUSE_DURATION: the maximum duration of a single guardian-initiated pause. RECOMMENDED value: 24 hours. After this period the pause auto-expires unless extended via multisig.

Behavior Requirements

Hot Balance and Spending Limits — ETH

  1. The vault MUST track a spendingLimit and an epochDuration for native ETH.
  2. The vault MUST track spentInCurrentEpoch and reset it when the current epoch expires.
  3. hotSpend() MUST revert if amount > remainingHotBudget().
  4. remainingHotBudget() MUST return min(spendingLimit - spentInCurrentEpoch, address(this).balance).
  5. The hot balance is NOT a separate pool—it is a rate-limited view of the total balance. The cold balance is calculated as totalBalance - remainingHotBudget().
  6. Reducing epochDuration while keeping the same spendingLimit effectively increases the spending rate and MUST be subject to the same timelock delay as a limit increase.

Hot Balance and Spending Limits — ERC-20 Tokens

  1. Each ERC-20 token MUST have independent spending limit, epoch duration, and epoch tracking state.
  2. A token with no configured spending limit (epoch duration = 0) MUST have a hot budget of zero—all tokens are in the cold vault by default until configured.
  3. hotSpendToken() MUST revert if the token has no spending limit configured.
  4. hotSpendToken() MUST revert if amount > remainingTokenHotBudget(token).
  5. Token epoch resets MUST be independent from the ETH epoch.
  6. Per-token epoch duration decreases MUST follow the same timelock rule as ETH.

Cold Vault Withdrawals

  1. requestWithdrawal() MUST create a pending request with unlockTime = block.timestamp + timelockDuration.
  2. requestWithdrawal() and requestTokenWithdrawal() MUST revert if pendingWithdrawalCount() >= MAX_PENDING_WITHDRAWALS. This prevents unbounded storage growth and denial-of-service via request spam.
  3. requestTokenWithdrawal() MUST store the token address in the withdrawal request.
  4. executeWithdrawal() MUST handle both ETH and token requests based on the stored token address (address(0) = ETH).
  5. executeWithdrawal() MUST revert if block.timestamp < unlockTime.
  6. executeWithdrawal() MUST revert if the request has been cancelled.
  7. cancelWithdrawal() MUST be callable by the owner OR any registered guardian.
  8. cancelWithdrawal() MUST work at any time before execution, including after the timelock expires, and including while the vault is paused (safety action).
  9. Executed or cancelled withdrawal requests MUST decrement pendingWithdrawalCount.

Multisig Bypass

  1. approveWithdrawal() MUST be callable only by registered guardians.
  2. Each guardian MUST only be able to approve each request once.
  3. When approvalCount >= multisigThreshold, the withdrawal MUST become immediately executable (timelock bypassed).
  4. multisigThreshold MUST be at least 2 when guardians are configured.

Guardian Management Invariants

  1. Removing a guardian MUST revert if it would leave guardianList.length < multisigThreshold. The owner MUST explicitly reduce the threshold first via setMultisigThreshold() before removing guardians that would violate this invariant. This prevents the vault from entering an unreachable state where multisig bypass is impossible.

Emergency Pause

  1. pause() MUST be callable by any single registered guardian.
  2. While paused, the following operations MUST revert: hotSpend, hotSpendToken, requestWithdrawal, requestTokenWithdrawal, execute, executeBatch, approveToken.
  3. While paused, the following operations MUST remain callable: cancelWithdrawal, approveWithdrawal, unpause, any view functions, deposits, and configuration reads. These are safety or read-only actions.
  4. A pause MUST auto-expire after MAX_PAUSE_DURATION from activation. After expiry, operations resume without any explicit unpause() call.
  5. unpause() before auto-expiry MUST require multisig approval (>= multisigThreshold guardian signatures). The owner alone cannot unpause.
  6. A single guardian MAY extend an active pause by calling pause() again, resetting the auto-expiry timer. This allows sustained freezing when suspicious activity continues.

Token Allowances

  1. approveToken() MUST revert if spender is not in the whitelist.
  2. approveToken() is the ONLY mechanism to grant ERC-20 allowances from the vault. ERC-20 token contracts MUST NOT be added to the whitelist — doing so would allow execute() to call transfer() or approve() directly, bypassing spending limits and the whitelisted-spender requirement.
  3. approveToken() with amount = 0 MUST revoke the allowance. Implementations SHOULD encourage revoking allowances after use.

DeFi Execution

  1. execute() MUST revert if target is not in the whitelist.
  2. execute() MUST revert if target is address(0) or the vault itself.
  3. executeBatch() MUST revert atomically if any individual call fails.
  4. executeBatch() MUST revert if array lengths do not match.
  5. ETH value sent via execute() is NOT subject to the hot spending limit. DeFi operations (swaps, liquidity provision) are value-preserving exchanges where the vault retains custody of the resulting assets. The whitelist is the security boundary, not the spending limit.
  6. hotSpend() and hotSpendToken() remain the only paths for direct asset transfers to non-whitelisted addresses.

Whitelist Management

  1. Adding a target to the whitelist MUST be subject to a timelock delay equal to at least the current timelockDuration.
  2. Removing a target from the whitelist MAY take effect immediately (this makes the vault more restrictive, not less).
  3. The whitelist MUST NOT allow adding the vault's own address as a target (re-entrancy vector).
  4. Cancellation of pending whitelist changes MUST be callable by the owner or any guardian.

Configuration Security

  1. Increases to spendingLimit (ETH) MUST be subject to a timelock delay equal to at least the current timelockDuration. This prevents an attacker from forcing the victim to raise the limit and then drain immediately.
  2. Increases to token spending limits MUST follow the same timelock rule. First-time configuration (when no limit exists) MAY take effect immediately, as it only enables a hot budget where none existed before.
  3. Decreases to timelockDuration MUST be subject to a delay equal to the current timelockDuration.
  4. Changes to the guardian set MUST be subject to a timelock delay.
  5. Decreases to spending limits (ETH or token) and increases to timelockDuration MAY take effect immediately (these changes make the vault more secure, not less).
  6. The timelockDuration is shared across all assets—there is no per-token timelock.

ERC-165 Support

Compliant contracts MUST implement ERC-165 and return true for the interface IDs of ICoercionResistantVault and, if token support is implemented, ICoercionResistantVaultTokens. If DeFi execution support is implemented, the contract MUST also return true for ICoercionResistantVaultExecutor.

ERC-4337 Account Abstraction

Compliant contracts MUST implement the ERC-4337 IAccount interface (v0.7), allowing the vault to be used as a first-class smart account with any dApp that supports UserOperations (Uniswap, Aave, etc. via compatible wallets like MetaMask).

interface IAccount {
    function validateUserOp(
        PackedUserOperation calldata userOp,
        bytes32 userOpHash,
        uint256 missingAccountFunds
    ) external returns (uint256 validationData);
}

ERC-4337 Behavior Requirements

  1. validateUserOp() MUST only be callable by the canonical EntryPoint.
  2. validateUserOp() MUST verify that the signature is a valid ECDSA signature from the vault owner over the EIP-191 prefixed userOpHash.
  3. validateUserOp() MUST reject malleable signatures (high s values per EIP-2).
  4. validateUserOp() MUST pay missingAccountFunds to the EntryPoint when > 0.
  5. All onlyOwner functions (hotSpend, execute, approveToken, configuration, etc.) MUST accept calls from the EntryPoint address in addition to the owner, because the EntryPoint calls the account with userOp.callData after successful validation.
  6. Implementations MUST provide a mechanism to deposit ETH to the EntryPoint to cover gas costs for UserOperations.
  7. The vault's security model (spending limits, timelocks, whitelist, pause state) MUST apply identically regardless of whether calls arrive directly from the owner or via the EntryPoint.

Rationale

Why rate-limited hot balance instead of a fixed hot pool?

A fixed hot pool requires the user to manually "refill" it, adding friction. A rate-limited approach means the user always has access to their daily budget without any action, while the bulk of funds remains inaccessible on short notice. The rate limit also means that even if an attacker keeps the victim coerced for extended periods, the maximum extractable value grows linearly and slowly.

Why independent spending limits per token?

Different assets have different values and use patterns. A user might want a generous USDC spending limit for daily expenses (e.g., 500 USDC/day) while keeping a very tight limit on WETH or WBTC. Independent limits allow granular control per asset. This also means that adding a new token to the vault doesn't automatically make it spendable—tokens start with zero hot budget by default.

Why shared timelock across all assets?

The timelock is a security parameter, not an asset-management parameter. Having per-token timelocks would mean the attacker could search for the token with the shortest timelock to extract value. A single shared timelock simplifies the security model and ensures consistent protection regardless of which assets the vault holds.

Why allow both timelock AND multisig?

Different users have different threat models and social graphs:

  • A user with trusted family members or business partners may prefer a 2-of-3 multisig for quick access when legitimately needed.
  • A user without suitable cosigners may prefer a longer timelock (72h+) as the sole protection mechanism.
  • Many users will use both: multisig for planned large transactions, timelock as the fallback for solo operations.

Why must configuration changes also be timelocked?

Without this, an attacker could force the victim to: (1) raise the spending limit to infinity, (2) immediately drain the entire balance. The timelock on configuration changes closes this attack vector. This applies equally to ETH and token spending limits.

Why are epoch duration decreases timelocked?

A subtle attack vector: with a fixed spending limit, reducing the epoch duration increases the effective spending rate. For example, going from "1 ETH / 24h" to "1 ETH / 1h" means 24x more can be extracted per day. Without the timelock on duration decreases, an attacker could force the victim to compress epochs and drain rapidly. Treating duration decreases the same as limit increases closes this vector.

Why can guardians cancel withdrawals?

If an attacker forces a cold vault withdrawal and then leaves (planning to return after the timelock), a guardian can cancel the withdrawal during the delay period. This gives the victim and their trusted contacts a window to respond.

Why an emergency pause mechanism?

Cancellation alone is insufficient — it only stops pending cold vault withdrawals. It does not stop the owner's key from continuing to drain the hot balance or interact with DeFi via execute(). The emergency pause is a true "freeze everything now" button available to any single guardian, for cases where the owner has been kidnapped, the key has been compromised, or unusual activity patterns suggest an ongoing attack.

Single-guardian activation is intentional: waiting for multisig consensus during an active attack defeats the purpose. The 24-hour auto-expiry and multisig requirement for extension prevent a malicious or compromised guardian from freezing funds indefinitely.

Why a cap on pending withdrawal requests?

Without a limit, the owner (or an attacker with the owner's key) could call requestWithdrawal() thousands of times, bloating storage and potentially causing denial-of-service conditions. The cap is low enough to prevent abuse (32) but high enough for any legitimate multi-withdrawal workflow. Cancelled or executed requests free up slots, so the limit applies only to unresolved requests.

Why guard against guardian removal below threshold?

If removing a guardian would leave fewer guardians than the multisig threshold, the multisig bypass path becomes permanently unreachable. Requiring an explicit threshold reduction before such a removal forces the owner to acknowledge the change and maintains a consistent, reachable state.

Why not use a duress PIN / panic wallet instead?

Duress-based systems rely on the attacker not knowing about the mechanism. A verifiable on-chain constraint is superior because: the limitation is publicly auditable, the victim does not need to lie under extreme stress, and the mechanism works regardless of attacker sophistication.

Why allow DeFi execution via whitelisted targets?

A vault that can only send ETH/tokens to addresses is not practical for DeFi users. By allowing execute() calls to whitelisted contracts, the vault can act as a smart account while funds never leave custody. The whitelist is the security boundary — adding a new target requires waiting the full timelock.

Why a dedicated approveToken() instead of using execute()?

If allowances were granted via execute() calling approve() on the token contract, the token contract itself would need to be whitelisted. But a whitelisted token contract would also allow execute() to call transfer(), bypassing spending limits. approveToken(token, spender, amount) solves this by only requiring the spender to be whitelisted, not the token contract.

Why is DeFi execution not subject to spending limits?

DeFi operations like swaps and liquidity provision are value-preserving: the vault sends ETH/tokens to a protocol and receives other assets back. The whitelist serves as the security control instead. hotSpend() and hotSpendToken() remain the exclusive paths for direct transfers where spending limits are enforced.

Why require ERC-4337 instead of just direct calls?

With ERC-4337, the vault becomes a standard smart account that works with any dApp via compatible wallets. The vault's spending limits, timelocks, and whitelist apply identically regardless of the execution path. ERC-4337 is the transport layer; the vault's security model is the enforcement layer.

Why is first-time token configuration immediate?

When setTokenSpendingLimit() is called for a token with no prior limit, the change only enables a hot budget where none existed. This is necessary for usability: without it, every new token added to the vault would require waiting the full timelock period before any daily spending is possible. The risk is bounded because the spending limit itself is still capped.

Backwards Compatibility

This ERC introduces a new contract standard and does not modify existing standards. It is fully backwards compatible. Existing wallets can migrate funds to a compliant vault contract at any time.

Reference Implementation

A complete reference implementation supporting both native ETH and ERC-20 tokens, along with a comprehensive Foundry test suite (71 tests covering all behavior requirements) and a Sepolia fork integration test exercising the vault against live WETH9 and Uniswap V3 contracts, is available in the development repository (github.com/DeFiRe-business/eip-proposal-5wrench).

The implementation is deployed and verified on Sepolia at 0x7B36499A730341c7BdaA6A603c525d54416cD0Ab. A live interactive demo (defire-business.github.io/eip-proposal-5wrench/demo/) allows testing all vault flows including a real Uniswap V3 swap via MetaMask.

Security Considerations

Attacker holds victim for extended periods

If an attacker holds a victim for multiple epochs, they can extract spendingLimit per epoch. Implementations SHOULD allow users to set aggressive spending limits (e.g., 0.1 ETH per 24h) to minimize this risk. A 72-hour timelock combined with a low daily limit means even sustained coercion yields limited value.

Attacker drains multiple token types

An attacker aware of the vault's contents could attempt to extract the hot budget from every configured token simultaneously. The total extractable value is the sum of all per-token hot budgets. Users SHOULD consider the aggregate hot exposure across all assets when configuring limits.

Attacker targets guardians

If an attacker knows the guardian addresses, they might target guardians to force multisig approval. Implementations SHOULD recommend that guardian identities remain private. Guardians MAY be institutional custodians, hardware wallets in bank vaults, or time-delayed smart contracts themselves.

Social engineering of guardians

A sophisticated attacker might contact guardians with a plausible story to get their approval. Guardians SHOULD establish out-of-band verification protocols with the vault owner (e.g., code words, video calls, in-person verification).

Griefing via cancellation

A malicious guardian could repeatedly cancel legitimate withdrawal requests. The owner SHOULD be able to remove guardians, though this action is itself subject to a timelock.

Griefing via emergency pause

A malicious guardian could repeatedly pause the vault. The 24-hour auto-expiry bounds the damage of any single pause. The owner can initiate guardian removal after an abusive pause, though the removal itself is timelocked. Implementations MAY add a rate limit on pause activations by a single guardian.

Front-running and MEV

Withdrawal execution transactions are not time-sensitive in a way that creates significant MEV opportunities, as the unlock time provides a wide execution window.

Smart contract risk

As with any smart contract wallet, bugs in the implementation could lead to loss of funds. Implementations MUST undergo thorough auditing. The simplicity of the interface is intentional—fewer features mean a smaller attack surface.

ERC-20 token risks

Some ERC-20 tokens have non-standard behavior (fee-on-transfer, rebasing, pausable, blocklisted). Implementations SHOULD account for fee-on-transfer tokens by checking actual received amounts. Implementations SHOULD NOT assume that transfer(to, amount) always delivers exactly amount tokens.

Malicious whitelisted contract

If a whitelisted contract is compromised or upgraded to a malicious implementation, the attacker could drain the vault via execute(). Implementations SHOULD recommend whitelisting only immutable or well-audited contracts. Guardians can immediately remove a compromised target from the whitelist.

Token allowances via approveToken()

Token contracts themselves MUST NOT be whitelisted — doing so would allow execute() to call transfer() or approve() directly, bypassing spending limits. Implementations SHOULD recommend exact-amount approvals and revoking allowances after use.

Re-entrancy via execute()

The execute() function performs an external call, which could re-enter the vault. Implementations SHOULD use a reentrancy guard on execute() and executeBatch().

EntryPoint trust model

The vault trusts the EntryPoint to only call functions after successful signature validation. Implementations MUST use the canonical EntryPoint and MUST NOT allow changing it after deployment.

Signature replay across chains

The userOpHash provided by the EntryPoint includes the chain ID, preventing cross-chain replay.

Configuration lock-out

If the owner loses access to their key, the cold vault funds become permanently locked. Implementations SHOULD consider adding a social recovery mechanism compatible with the vault's security model.

Copyright and related rights waived via CC0.

EIP.tools

EIP.tools

Search, read, and map Ethereum improvement proposals, ERCs, RIPs, and CAIPs from one focused interface.

Farcaster
by @apoorveth