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): https://github.com/tabilabs/mandated-vault-factory/blob/master/docs/erc-xxxx-spec.md
- Reference implementation (Foundry): mandated-vault-factory/src/MandatedVaultClone.sol at master · tabilabs/mandated-vault-factory · GitHub
- Tests: mandated-vault-factory/test at master · tabilabs/mandated-vault-factory · GitHub
### 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 **`CALL` only** and then enforces a **circuit breaker** by comparing pre/post `totalAssets()` (ERC-4626).
### Minimal Interface (Excerpt)
```solidity
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 assumes `totalAssets()` 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`) during `execute`.
- **Unbounded open mandates are forbidden:** if `executor == address(0)` then `payloadDigest` must be nonzero (bind to the intended `actions`).
- **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 `MandateExecuted` events 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:
1. **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.
2. **Attack surface reduction**: allowing arbitrary ETH forwarding via adapters opens re-entrancy and gas-griefing vectors that complicate the security model.
3. **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)
1. **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`)?
2. **Native ETH value transfer:** is keeping Core as `value == 0` the right baseline, pushing ETH flows into explicit extensions?
3. **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?
4. **Input size limits:** should some limits (actions/proof depth/extensions bytes) be required vs recommended?
5. **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)
1. Create EthMagicians discussion thread (this post).
2. Publish/confirm a public reference implementation repo (spec + code + tests) so reviewers can actually read the draft.
3. Fill remaining frontmatter in the draft (`author`, `discussions-to`, etc.).
4. Fork `ethereum/ERCs`.
5. Add `erc-XXXX.md` converted from the draft.
6. Open a PR; editors assign an official number.
7. Track status progression: Draft → Review → Last Call → Final.
Thanks in advance for feedback.
— tabilabs (tabilabs) (`lancy@tabilabs.org`)