[ERC-XXXX] Mandated Execution for Tokenized Vaults (ERC-4626): risk-constrained delegated strategy execution

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`)