Architecture deep dive - Concrete Earn V2
Level: Advanced · Source: official developer docs. Concrete's contract source is in a private repository (partners can request access after signing an NDA), so this page describes documented behaviour only.
1. Four layers
┌─────────────────────────────┐
│ Factory (UUPS, CREATE2) │ deploys vaults, curates implementations,
└──────────────┬──────────────┘ owns upgrade paths, sets fee recipients
│ create()
┌──────────────▼──────────────┐
users ───────►│ Vault (ERC-4626, ERC-1967) │ custody, shares, accounting, access control
└───┬─────────────────────┬───┘
pre/post hooks │ │ allocate / deallocate / report value
┌─────────────▼────┐ ┌────────▼────────────────────────────┐
│ Hook (≤1 target) │ │ Strategies (IStrategyTemplate) │
│ HookContainer ×6 │ │ idle · lending · looping · multisig │
└──────────────────┘ └─────────────────────────────────────┘
Periphery Factory deploys strategies & helpersThe vault does not seek yield itself. Product variants live in the modules a vault references, so launching a vault is composing modules, not forking a contract.
2. Factory
3. Vault implementations
| Implementation | Behaviour |
|---|---|
| Standard (Atomic) | Baseline ERC-4626 + strategy allocation; single-tx withdraw. |
| Async (Queued Withdrawal) | Epoch-based withdrawal queue; toggled by the Vault Manager. |
| Predeposit | LayerZero cross-chain claim flow for pre-staked assets. Claim path bypasses hooks. |
| Bridged Standard / Bridged Async | Adds one-shot unbackedMint (needs totalSupply()==0 and maxDepositLimit==0) for migrations. |
Looping strategies and fee splitting are not vault implementations: looping is a strategy plugged into a Standard vault; fee splitting lives in downstream contracts the vault mints to.
Vault-level limits: max total deposit cap, min/max per deposit and withdrawal, optional per-user cap.
4. Vault internals: custody, shares, accounting
5. Strategies
5b. Hooks
Optional modules at fixed lifecycle points: before/after deposit, mint, withdraw, redeem, transfer. A vault stores one hook target + a flag bitmap.
| Hook | Purpose |
|---|---|
UserDepositCapHook | Per-user deposit cap (one per vault). |
WhitelistUserDepositHook | Only approved addresses may deposit. |
DepositLockHook | Time-locks minted shares; locked shares can't transfer/withdraw/redeem until each lock expires. |
DepositLockWithFeeHook | Adds early unlock for a time-decaying fee in shares. |
HookContainer | Fans one vault call out to up to six hooks. |
Gotchas: a revert in any hook reverts the whole operation (a buggy hook can freeze the vault until fixed; only HOOK_MANAGER can change hooks); hooks don't gate cross-chain claim paths or unbackedMint.
6. Authority model
Authority is split across three axes:
| Axis | Holder | Powers |
|---|---|---|
| Vault Owner | Ownable owner | Gates factory.upgrade(...) (outside role system) |
| Roles | ROLE_ADMIN grants: VAULT_MANAGER (state), STRATEGY_MANAGER (add/remove strategies), HOOK_MANAGER, ALLOCATOR, PAUSER, WITHDRAWAL_MANAGER, PRIORITY_WITHDRAWAL_EXECUTOR | Day-to-day ops; DEFAULT_ADMIN_ROLE intentionally unassigned |
| Factory Owner | Protocol | Fee-recipient config on every vault (bypasses vault roles) |
Operational keys can be split between parties (curator ≠ allocator ≠ pauser). The Concrete docs label governance roles as low-frequency/high-impact (held by Vault Admin) and Allocator/Withdrawal Manager as high-frequency/low-impact automated services.
7. Fee distribution
Fees are minted as shares to managementFeeRecipient and performanceFeeRecipient (set by the factory owner). Each typically points to a `TwoWayFeeSplitter` (periphery) that splits between mainRecipient and secondaryRecipient using feeFractionOfSecondaryRecipient in bps out of 10,000 (0 → all main, 10000 → all secondary). Anyone may call distributeFees(vault).