Hi everyone,
I’d like to start an initial discussion for a new ERC proposal tentatively titled “Mandated Execution for Tokenized Vaults”.
Process note: ERC proposals are submitted to ethereum/ERCs (not ethereum/EIPs). This EthMagicians thread is the recommended first step and will be linked from the draft via discussions-to.
Links
-
Working draft (spec): mandated-vault-factory/docs/erc-xxxx-spec.md at master · tabilabs/mandated-vault-factory · GitHub
-
Reference implementation (Foundry):
-
Tests:
Summary (TL;DR)
This ERC defines a minimal, interoperable interface for delegated strategy execution on ERC-4626 tokenized vaults, enabling external executors (agents/solvers) to submit on-chain executions without custody of the authority’s private key, while the vault enforces hard risk constraints on-chain.
Core idea:
-
The vault exposes a standardized
execute(…)entrypoint. -
An Authority signs an EIP-712 Mandate defining:
an adapter allowlist commitment (Merkle root over (adapter address, adapter codehash)),
a max single-execution drawdown bound,
a max epoch cumulative drawdown bound (high-water mark),
optional binding to an intended action batch (payloadDigest),
hash-committed extensions (extensionsHash).
-
The Executor submits
execute(mandate, actions, signature, adapterProofs, extensions). -
The vault executes adapter calls via
CALLonly and then enforces a circuit breaker by comparing pre/posttotalAssets()(ERC-4626).
Minimal Interface (Excerpt)
struct Action {
address adapter;
uint256 value; // MUST be 0 in Core v1
bytes data;
}
struct Mandate {
address executor;
uint256 nonce;
uint48 deadline;
uint64 authorityEpoch;
uint16 maxDrawdownBps;
uint16 maxCumulativeDrawdownBps;
bytes32 allowedAdaptersRoot;
bytes32 payloadDigest;
bytes32 extensionsHash;
}
function execute(
Mandate calldata mandate,
Action calldata actions,
bytes calldata signature,
bytes32 calldata adapterProofs,
bytes calldata extensions
) external returns (uint256 preAssets, uint256 postAssets);
Design Goals / Non-Goals
Goals:
-
A minimal “thin waist” interface for vault-native delegated execution, with strong on-chain risk bounds.
-
Support both EOAs (ECDSA) and contract authorities (ERC-1271).
-
Keep the core compatible with ERC-4626 vault accounting semantics.
Non-goals (at least for Core v1):
-
Standardizing a full intent system, solver marketplace, or off-chain discovery layer.
-
Supporting arbitrary ETH value forwarding in Core (push to extensions if needed).
Motivation
DeFi is increasingly moving from “users manually call contracts” toward “agents/solvers execute strategies.” ERC-4626 standardizes pooled custody and accounting, but it does not standardize a trust-minimized way for a vault to grant bounded execution power to third-party executors.
Existing standards cover adjacent layers (delegation plumbing, intent expression, smart account execution), but there is no minimal “thin waist” interface for vault-native, risk-constrained delegated execution that:
-
keeps authority in an off-chain signature (EIP-712 / ERC-1271),
-
enforces an allowlist of callable strategy components (adapters),
-
enforces post-execution risk bounds at the vault level (via ERC-4626 accounting).
Specification Overview (Core)
Core concepts:
- Action: a low-level call to an adapter:
{ adapter, value, data }.
Core forbids native value transfer: action.value MUST be 0.
Calls are executed via CALL only (no DELEGATECALL).
- Mandate: EIP-712 signed authorization with nonce/deadline replay protection.
Supports EOA and ERC-1271 contract authorities.
Includes authorityEpoch to domain-separate signatures across authority rotations.
-
Adapter allowlist: Merkle root over
(adapter, extcodehash(adapter))so allowlisting pins runtime bytecode. -
Circuit breaker:
Single-execution loss bound: compare preAssets = totalAssets() vs postAssets.
Cumulative loss bound: compare epochAssets (high-water mark) vs postAssets.
epochAssets updates to the maximum observed totalAssets() within the epoch.
Authority can resetEpoch() to start a fresh epoch.
- Extensions:
Hash-committed via extensionsHash = keccak256(extensions).
Canonical encoding: extensions = abi.encode(Extension) with strictly ascending unique id.
Unknown required extensions revert; unknown optional extensions may be ignored.
Example extension: SelectorAllowlist@v1 to constrain (adapter, selector) pairs per action.
Security Model & Caveats
-
totalAssets()manipulability: this ERC’s circuit breaker assumestotalAssets()is resistant to atomic manipulation by the executor within the same transaction. Vaults whose valuation can be flash-manipulated (e.g., spot-price oracle usage) should not rely solely on this mechanism and should add additional safeguards (e.g., TWAP, oracle extension). -
Upgradeable proxy adapters: codehash pinning does not generally pin proxy implementations; proxy-style adapters are unsafe unless additional pinning is added.
-
Reentrancy / vault busy: reference implementation uses a shared mutex to prevent reentering
execute, and to prevent share mint/burn entrypoints (deposit/mint/withdraw/redeem) duringexecute. -
Unbounded open mandates are forbidden: if
executor == address(0)thenpayloadDigestmust be nonzero (bind to the intendedactions). -
Audit status: The reference implementation is unaudited and provided for discussion purposes only. It has not undergone formal third-party security review. Do not use in production without independent audit.
Anticipated Questions (FAQ)
Q1: Can totalAssets() be manipulated within the same transaction, defeating the circuit breaker?
Yes, the circuit breaker is only as strong as the vault’s totalAssets() implementation. Vaults that derive valuation from spot prices (e.g., AMM reserves) are vulnerable to flash-loan manipulation within the same transaction. This is an explicit caveat in the spec (Security Model §1). The recommended mitigation is to use TWAP or oracle-based valuation, which can be enforced via extensions (e.g., an OracleValuation@v1 extension). The circuit breaker provides a meaningful bound for vaults with manipulation-resistant accounting (e.g., lending protocol aTokens, staking wrappers).
Q2: Doesn’t the codehash pinning in the adapter allowlist fail for upgradeable proxy adapters?
Correct. For ERC-1967 or similar proxies, extcodehash returns the proxy’s bytecode hash, not the implementation’s. A proxy upgrade changes behavior without changing the allowlisted codehash, so previously valid Merkle proofs remain valid post-upgrade. This is documented in this draft and in the reference implementation repo’s SECURITY.md. The recommended operational mitigation is: (1) prefer immutable adapters for long-lived mandates, (2) revoke and re-sign allowlist roots when proxy implementations change. A future extension could add implementation-level pinning (e.g., ImplementationPin@v1), but this is explicitly out of scope for Core.
Q3: Why should this be an ERC rather than an application-level convention?
Standardization at the ERC level enables a shared interface for the entire executor/agent ecosystem:
-
SDK composability: agent frameworks and solver networks can integrate with any compliant vault without per-vault adapters.
-
Audit reuse: a single audited reference implementation reduces per-deployment audit costs.
-
Tooling: block explorers, monitoring dashboards, and risk analysis tools can parse
MandateExecutedevents uniformly across all vaults. -
Interoperability: depositors can evaluate vault risk constraints (drawdown bounds, adapter allowlists) through a common on-chain interface, regardless of the vault operator.
Without standardization, every vault reinvents execution delegation with incompatible interfaces, fragmenting the agent layer.
Q4: Why does Core forbid action.value != 0? Isn’t that too restrictive?
Core sets action.value == 0 as the baseline for three reasons:
-
Accounting clarity: ERC-4626
totalAssets()tracks ERC-20 balances. Native ETH flows create accounting blind spots that are hard to reason about in the circuit breaker. -
Attack surface reduction: allowing arbitrary ETH forwarding via adapters opens re-entrancy and gas-griefing vectors that complicate the security model.
-
Extension path: if a vault needs ETH flows, it can implement an extension (e.g.,
NativeValueForwarding@v1) with explicit opt-in. This keeps Core minimal while supporting the use case.
Feedback Requested (Open Questions)
- Merkle leaf encoding safety: current draft uses
leaf = keccak256(abi.encode(adapter, codeHash))(64-byte preimage). OpenZeppelin warns that 64-byte leaves can be reinterpreted as internal nodes in some constructions. Should Core:
add explicit domain separation / prefixing for leaves, or
switch to abi.encodePacked with a prefix, or
version the allowlist leaf scheme (e.g., AdapterAllowlist@v2)?
-
Native ETH value transfer: is keeping Core as
value == 0the right baseline, pushing ETH flows into explicit extensions? -
Revert data size / gas griefing: should implementations cap revert-data copying (at the cost of losing full diagnostic bytes) or keep full data as in the reference implementation?
-
Input size limits: should some limits (actions/proof depth/extensions bytes) be required vs recommended?
-
Additional standardized extensions: should we standardize common extensions (oracle/TWAP valuation, slippage rules, adapter capability introspection) or keep Core minimal and let ecosystems define them?
Next Steps (ERCs Submission Workflow)
-
Create EthMagicians discussion thread (this post).
-
Publish/confirm a public reference implementation repo (spec + code + tests) so reviewers can actually read the draft.
-
Fill remaining frontmatter in the draft (
author,discussions-to, etc.). -
Fork
ethereum/ERCs. -
Add
erc-XXXX.mdconverted from the draft. -
Open a PR; editors assign an official number.
-
Track status progression: Draft → Review → Last Call → Final.
Thanks in advance for feedback.
— tabilabs (tabilabs) (lancy@tabilabs.org)