Security Considerations
What to watch for when writing or reviewing a facet: access control, reentrancy, storage safety, and the upgrade path.
Most of the security model in BLOK Capital comes from a handful of patterns repeated across every facet. If you're writing a new one, or reviewing someone else's, these are the things that actually matter.
Access control#
Every mutating function needs an explicit check on who's allowed to call it. The two patterns you'll see throughout the base facets are an owner check (onlyGardenOwner, transferable via OwnershipFacet) and a self-call check.
The self-call pattern shows up specifically for DEX and swap functions once a Garden is connected to an Index: Facet.sol restricts those calls to msg.sender == address(this), meaning only the Garden itself, mid-rebalance, can trigger them. No external account or contract can call in and front-run a rebalance while it's in progress. If you're adding a function that moves funds, decide early which of these two checks it needs, and don't skip the check just because a function looks read-only at first glance.
Reentrancy#
State-changing entry points that touch external contracts (swaps, transfers, upgrades) use nonReentrant or an equivalent custom guard. The Index facet, for example, tracks a rebalancing boolean in its own storage specifically so a rebalance can't be re-entered mid-flight. If your facet calls out to an external protocol (a DEX, a lending market), assume that call can re-enter you, and guard accordingly. Follow checks-effects-interactions: validate first, update your own state, then make the external call last.
Storage safety#
Every facet's state lives at a storage slot derived from keccak256 of its storage library's name (masked per EIP-7201), not at a fixed offset. This is what lets many facets share one Garden's storage without colliding, but it also means the library name is the storage key: rename a deployed storage library and every Garden using it loses access to that data permanently, it doesn't move with the rename.
Two rules follow from that:
- Always derive the slot through
LibStorageSlot.deriveStorageSlot(), never inline the formula yourself. - Never rename a storage library once it's deployed. If a rename is genuinely necessary, it takes a migration facet that reads the old slot and writes the new one, run against every deployed Garden, not a find-and-replace.
You don't have to police this by memory. forge test runs three guards on every suite: one catches two different files reusing the same library name (a silent collision), one proves that detector actually works, and one fails if a library listed in storage-registry.json (the source of truth for every module's deployed storage libraries) no longer exists in the codebase. If you add a new facet with its own storage, give the library a name that's unique across the whole repo, and if it's a new module, register it and run the registry-sync script so storage-registry.json stays current.
The upgrade path is closed by default#
diamondCut is blocked on every Garden. The only way a facet reaches a Garden is by being registered in the Facet Registry and explicitly allowed for that Garden's type, and the only way an existing Garden picks up a change is through the Upgrade facet's hash-verified sync with the registry. If you're proposing a new facet, expect it to go through registry approval, not a direct cut. This is deliberate: it means a compromised owner key can't unilaterally install arbitrary logic into a Garden, and the four base facets (ownership, cut, upgrade, loupe) can never be swapped out at all, even by governance.
Rebalancing-specific guardrails#
If your work touches the Index rebalancing path, know that it already assumes hostile conditions:
- Flash-loan protection: the intent and the actual rebalance must land in different blocks, so no single-transaction manipulation can bracket both.
- Value-loss guardrail: a rebalance reverts if total portfolio value drops more than 0.5% versus before the swaps.
- Per-asset tolerance: post-swap balances have to land within 2% of their target allocation.
Don't loosen any of these thresholds without understanding why they're there. They exist to bound what an off-chain relayer, or a compromised one, can do to a Garden's holdings. A separate Rebalancer contract, not the Garden itself, is what triggers this across every Garden of a given Index type at once, and it's permissionless once a cooldown has passed, so treat "anyone can call this" as the default assumption, not an edge case.
The protocol-wide kill switch#
Above the level of any single Garden, Protocol Status can put the whole protocol into one of three states: active, upgrades-disabled, or inactive. State changes aren't authorized by a simple owner key, they're authorized by a security council tracked through ENS domains. The Garden Factory checks this before deploying anything new, and refuses while the protocol is inactive. If you're integrating something that assumes the protocol is always available, it isn't; design for the inactive case.
This is a multi-chain codebase#
Facets are organized per chain (arbitrumOne, avalanche, ethereum today), and which integrations exist depends on which one you're looking at. Don't assume a pattern, a guardrail, or an integration you've seen on one chain exists on another, check the facet folder for that specific chain.
Where to go for detail#
This page is the checklist. For how each of these is actually implemented, see the individual facet references under Blok-C-V1-Core, and the architecture-level explanation in Smart Contracts.
Last updated:
Edit this page (opens in a new tab)