Concrete — Complete GuideIndependent guide, unofficial

01

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 & helpers

The 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

  • UUPS-upgradeable; CREATE2-deploys vaults as ERC-1967 proxies.
  • Keeps a registry of approved implementations and migration paths (setMigratable).
  • Vault deployment is permissionless (create()), but only from approved, unblocked implementations.
  • The factory is the immutable admin of every vault proxy; upgrade authority cannot move off it short of upgrading the factory. The vault's Ownable owner is transferable (transferOwnership) and gates factory.upgrade(...).
  • Periphery Factory deploys strategies/position helpers and supports deregisterStrategy (hands proxy-admin of a strategy to a new owner) - so strategy custody can go to a partner while vault custody stays with Concrete.

3. Vault implementations

ImplementationBehaviour
Standard (Atomic)Baseline ERC-4626 + strategy allocation; single-tx withdraw.
Async (Queued Withdrawal)Epoch-based withdrawal queue; toggled by the Vault Manager.
PredepositLayerZero cross-chain claim flow for pre-staked assets. Claim path bypasses hooks.
Bridged Standard / Bridged AsyncAdds 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

  • Depositing mints ERC-20 shares; redeeming burns them.
  • The vault keeps a cached snapshot of total assets, refreshed by the withYieldAccrual modifier before any economic op (deposit, mint, withdraw, redeem, allocate, configure, fee-recipient setters).
  • Conversions inside guarded ops use the cached snapshot, not live balanceOf → defends against donation / inflation attacks.
  • `totalAssets()` ≠ the cache: it calls _previewAccrueYieldAndFees() and re-reads each active strategy's totalAllocatedValue() live. The cache is exposed as cachedTotalAssets().
  • Pure book-keeping ops (strategy registry, hooks, deallocation order, pause) skip the modifier.

5. Strategies

  • One vault ↔ one strategy instance, same underlying asset, implementing IStrategyTemplate.
  • The Allocator (ALLOCATOR) calls vault.allocate(...) with per-strategy instructions. Routing/sizing decisions are off-chain; the vault only enforces postconditions (idle balance covers locked assets and, on async vaults, past-epoch unclaimed assets). No policy is decided on-chain.
  • Looping strategy composes lender + flash + swap modules (~two dozen functions across three interfaces), so new venues integrate without changing the strategy contract.
  • Trust in reported values: the vault treats what a strategy reports as truth - no on-vault delta threshold, oracle cross-check or safety flag. Rails live strategy-side:
  • On-chain accounting → value computed from chain state.
  • Asynchronous accounting → operator pushes a signed value within accountingValidityPeriod; late push ⇒ value function reverts ⇒ totalAssets() reverts ⇒ deposits, withdrawals, epoch processing halt until the strategy admin calls unpauseAndAdjustTotalAssets or the Strategy Manager toggles the strategy inactive.

5b. Hooks

Optional modules at fixed lifecycle points: before/after deposit, mint, withdraw, redeem, transfer. A vault stores one hook target + a flag bitmap.

HookPurpose
UserDepositCapHookPer-user deposit cap (one per vault).
WhitelistUserDepositHookOnly approved addresses may deposit.
DepositLockHookTime-locks minted shares; locked shares can't transfer/withdraw/redeem until each lock expires.
DepositLockWithFeeHookAdds early unlock for a time-decaying fee in shares.
HookContainerFans 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:

AxisHolderPowers
Vault OwnerOwnable ownerGates factory.upgrade(...) (outside role system)
RolesROLE_ADMIN grants: VAULT_MANAGER (state), STRATEGY_MANAGER (add/remove strategies), HOOK_MANAGER, ALLOCATOR, PAUSER, WITHDRAWAL_MANAGER, PRIORITY_WITHDRAWAL_EXECUTORDay-to-day ops; DEFAULT_ADMIN_ROLE intentionally unassigned
Factory OwnerProtocolFee-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).

8. Operational pitfalls (checklist for curators & integrators)

  • [ ] A fresh vault is not production-ready: only ROLE_ADMIN and VAULT_MANAGER are granted (to initialVaultManager). Grant the rest explicitly.
  • [ ] Paused vaults cannot complete an upgrade (_upgrade is whenNotPaused) - plan emergency response.
  • [ ] Deallocation order is load-bearing. A strategy missing from the order still counts in totalAssets() but can't be drained for user withdrawals.
  • [ ] Size accountingValidityPeriod for operator availability.
  • [ ] Whitelist hook + predeposit/bridged flows need separate gating.
  • [ ] Hook misconfiguration = vault-wide DoS risk.
  • [ ] Withdrawal caps/cooldowns interact - test combined behaviour.