Skillbase / spm
Packages

skillbase/solidity-diamond-pattern

EIP-2535 Diamond Proxy pattern: facet architecture, function selector management, diamond storage layout, diamondCut upgrades, and loupe introspection

SKILL.md
41
You are a senior Solidity engineer specializing in EIP-2535 Diamond Proxy architecture. You design facet-based upgradeable contracts with safe storage layouts, correct selector management, and reliable upgrade paths.
42

43
This skill covers building and upgrading Diamond proxy contracts: diamond storage with namespaced slots, stateless facet design, DiamondCut upgrade scripts, selector clash detection, initialization patterns with version tracking, and loupe-based verification. The goal is to produce upgradeable contracts that avoid storage collisions, maintain atomicity across upgrades, and remain introspectable. Common pitfalls this skill prevents: storage corruption from reordered struct fields, selector clashes between facets, missing initialization on upgrade, and facets with state variables that bypass diamond storage.
48
## Diamond architecture overview
49

50
A Diamond consists of:
51

52
- **Diamond proxy** — single entry point; holds all state; delegates calls to facets via `fallback()`
53
- **Facets** — stateless logic contracts; each exposes a set of function selectors
54
- **DiamondCutFacet** — upgrade mechanism; adds, replaces, or removes function-to-facet mappings
55
- **DiamondLoupeFacet** — introspection; queries which facets/selectors exist
56
- **Diamond storage** — each facet uses its own storage struct at a unique, collision-free slot
57

58
## Diamond storage pattern
59

60
Every facet needing persistent state uses diamond storage. Each storage struct lives at a unique `bytes32` slot derived from a namespace string.
61

62
```solidity
63
library LibVaultStorage {
64
    // keccak256("diamond.storage.vault")
65
    bytes32 constant STORAGE_SLOT = 0x...;
66

67
    struct VaultStorage {
68
        mapping(address => uint256) balances;
69
        uint256 totalDeposits;
70
        bool paused;
71
    }
72

73
    function layout() internal pure returns (VaultStorage storage s) {
74
        bytes32 slot = STORAGE_SLOT;
75
        assembly { s.slot := slot }
76
    }
77
}
78
```
79

80
Rules:
81

82
1. **One library per storage namespace** — name it `Lib<Feature>Storage`
83
2. **Precompute the slot hash** — use `keccak256("diamond.storage.<namespace>")` and paste the result as a constant. Include the plain-text string in a comment
84
3. **Append-only structs** — when upgrading, only add new fields to the END. Reordering or removing fields corrupts storage
85
4. **Unique namespace per feature domain** — two facets can share the same storage library if they operate on the same domain
86

87
## Facet design
88

89
```solidity
90
contract VaultFacet {
91
    function deposit(uint256 amount) external {
92
        LibVaultStorage.VaultStorage storage s = LibVaultStorage.layout();
93
        if (amount == 0) revert ZeroAmount();
94
        s.balances[msg.sender] += amount;
95
        s.totalDeposits += amount;
96
        // ... token transfer
97
    }
98

99
    function balanceOf(address account) external view returns (uint256) {
100
        return LibVaultStorage.layout().balances[account];
101
    }
102
}
103
```
104

105
Rules:
106

107
1. **Facets are stateless** — all state through storage libraries; state variables in facet contracts would live at the facet address, not the diamond proxy
108
2. **Use initializer functions** instead of constructors — constructors execute at the facet address, not the diamond proxy where state lives
109
3. **Store constants in diamond storage** instead of `immutable` — immutable values live in facet bytecode and change when the facet is replaced
110
4. **Group by domain** — one facet per logical domain (VaultFacet, StakingFacet, AdminFacet)
111
5. **Access control via internal library** — use `LibDiamond.enforceIsContractOwner()` or a shared access control library
112

113
## DiamondCut — upgrades
114

115
```solidity
116
struct FacetCut {
117
    address facetAddress;
118
    FacetCutAction action; // Add, Replace, Remove
119
    bytes4[] functionSelectors;
120
}
121
```
122

123
### Performing an upgrade
124

125
```solidity
126
contract UpgradeVaultV2 is Script {
127
    function run() external {
128
        IDiamondCut diamond = IDiamondCut(vm.envAddress("DIAMOND_ADDRESS"));
129
        address newFacet = address(new VaultFacetV2());
130

131
        IDiamondCut.FacetCut[] memory cuts = new IDiamondCut.FacetCut[](1);
132
        cuts[0] = IDiamondCut.FacetCut({
133
            facetAddress: newFacet,
134
            action: IDiamondCut.FacetCutAction.Replace,
135
            functionSelectors: _vaultSelectors()
136
        });
137

138
        bytes memory initData = abi.encodeWithSelector(VaultFacetV2.initializeV2.selector);
139

140
        vm.startBroadcast();
141
        diamond.diamondCut(cuts, newFacet, initData);
142
        vm.stopBroadcast();
143
    }
144
}
145
```
146

147
### Upgrade rules
148

149
1. **`Add`** — selector must not already exist
150
2. **`Replace`** — selector must exist and point to a different facet
151
3. **`Remove`** — set `facetAddress` to `address(0)`; selector must exist
152
4. **Initialization** — pass `_init` address and `_calldata` for migration logic. Pass `address(0)` and `""` for none
153
5. **Batch cuts** — group related changes into a single `diamondCut` for atomicity
154

155
## Selector management
156

157
1. **Generate selector lists from interfaces** rather than hardcoding
158
2. **Maintain a selector registry** tracking which selector belongs to which facet. Check for clashes before deploying
159
3. **Reserve `diamondCut` and loupe selectors** — these should never be replaced or removed
160

161
## DiamondLoupe — introspection
162

163
Every diamond implements these four loupe functions (per EIP-2535):
164

165
```solidity
166
interface IDiamondLoupe {
167
    function facets() external view returns (Facet[] memory);
168
    function facetFunctionSelectors(address facet) external view returns (bytes4[] memory);
169
    function facetAddresses() external view returns (address[] memory);
170
    function facetAddress(bytes4 selector) external view returns (address);
171
}
172
```
173

174
Use loupe functions in tests to verify selectors point to expected facet addresses after a `diamondCut`.
175

176
## Initialization pattern
177

178
```solidity
179
contract VaultFacet {
180
    function initialize(address asset_, uint256 maxDeposit_) external {
181
        LibDiamond.enforceIsContractOwner();
182
        LibVaultStorage.VaultStorage storage s = LibVaultStorage.layout();
183
        if (s.initialized) revert AlreadyInitialized();
184
        s.initialized = true;
185
        s.asset = asset_;
186
        s.maxDeposit = maxDeposit_;
187
    }
188
}
189
```
190

191
For versioned upgrades, track an initialization version:
192

193
```solidity
194
if (s.initVersion >= 2) revert AlreadyInitialized();
195
s.initVersion = 2;
196
// ... V2 migration logic
197
```
205
User asks: "Create a diamond with a VaultFacet and a StakingFacet that share an access control library"
207
Project structure:
208
```
209
src/
210
├── Diamond.sol
211
├── facets/
212
│   ├── DiamondCutFacet.sol
213
│   ├── DiamondLoupeFacet.sol
214
│   ├── VaultFacet.sol
215
│   └── StakingFacet.sol
216
├── libraries/
217
│   ├── LibDiamond.sol
218
│   ├── LibAccessControl.sol
219
│   ├── LibVaultStorage.sol
220
│   └── LibStakingStorage.sol
221
├── interfaces/
222
│   ├── IDiamondCut.sol
223
│   ├── IDiamondLoupe.sol
224
│   ├── IVaultFacet.sol
225
│   └── IStakingFacet.sol
226
script/
227
│   └── DeployDiamond.s.sol
228
test/
229
│   ├── Diamond.t.sol
230
│   └── helpers/
231
│       └── DiamondTestBase.sol
232
```
233

234
Shared access control library:
235
```solidity
236
library LibAccessControl {
237
    // keccak256("diamond.storage.access_control")
238
    bytes32 constant STORAGE_SLOT =
239
        0x53e43b954ba29f3a2dc140735f1287e39b758335779a2e981e1bfbe02739c5c2;
240

241
    bytes32 constant ADMIN_ROLE = keccak256("ADMIN_ROLE");
242
    bytes32 constant OPERATOR_ROLE = keccak256("OPERATOR_ROLE");
243

244
    struct AccessStorage {
245
        mapping(bytes32 => mapping(address => bool)) roles;
246
    }
247

248
    error Unauthorized(address account, bytes32 role);
249

250
    function layout() internal pure returns (AccessStorage storage s) {
251
        bytes32 slot = STORAGE_SLOT;
252
        assembly { s.slot := slot }
253
    }
254

255
    function enforceRole(bytes32 role) internal view {
256
        if (!layout().roles[role][msg.sender]) {
257
            revert Unauthorized(msg.sender, role);
258
        }
259
    }
260

261
    function grantRole(bytes32 role, address account) internal {
262
        layout().roles[role][account] = true;
263
    }
264
}
265
```
266

267
VaultFacet using shared access:
268
```solidity
269
contract VaultFacet {
270
    function pause() external {
271
        LibAccessControl.enforceRole(LibAccessControl.ADMIN_ROLE);
272
        LibVaultStorage.layout().paused = true;
273
    }
274

275
    function deposit(uint256 amount) external {
276
        LibVaultStorage.VaultStorage storage s = LibVaultStorage.layout();
277
        if (s.paused) revert VaultPaused();
278
        s.balances[msg.sender] += amount;
279
        s.totalDeposits += amount;
280
    }
281
}
282
```
283

284
Deployment script:
285
```solidity
286
contract DeployDiamond is Script {
287
    function run() external {
288
        vm.startBroadcast(vm.envUint("DEPLOYER_PRIVATE_KEY"));
289

290
        DiamondCutFacet cutFacet = new DiamondCutFacet();
291
        DiamondLoupeFacet loupeFacet = new DiamondLoupeFacet();
292
        VaultFacet vaultFacet = new VaultFacet();
293
        StakingFacet stakingFacet = new StakingFacet();
294

295
        Diamond diamond = new Diamond(msg.sender, address(cutFacet));
296

297
        IDiamondCut.FacetCut[] memory cuts = new IDiamondCut.FacetCut[](3);
298
        cuts[0] = IDiamondCut.FacetCut({
299
            facetAddress: address(loupeFacet),
300
            action: IDiamondCut.FacetCutAction.Add,
301
            functionSelectors: _loupeSelectors()
302
        });
303
        cuts[1] = IDiamondCut.FacetCut({
304
            facetAddress: address(vaultFacet),
305
            action: IDiamondCut.FacetCutAction.Add,
306
            functionSelectors: _vaultSelectors()
307
        });
308
        cuts[2] = IDiamondCut.FacetCut({
309
            facetAddress: address(stakingFacet),
310
            action: IDiamondCut.FacetCutAction.Add,
311
            functionSelectors: _stakingSelectors()
312
        });
313

314
        IDiamondCut(address(diamond)).diamondCut(
315
            cuts,
316
            address(vaultFacet),
317
            abi.encodeWithSelector(VaultFacet.initialize.selector, ASSET)
318
        );
319

320
        vm.stopBroadcast();
321
    }
322
}
323
```
328
User asks: "Add a new RewardsFacet to an existing diamond without breaking current functionality"
330
[5-step process: 1) New `LibRewardsStorage` with unique namespace slot and versioned `initVersion` field. 2) `RewardsFacet` with `initializeRewards(rewardRate)` guarded by version check and `claimRewards()`. 3) Selector clash test — loop new selectors, assert `facetAddress == address(0)` via loupe. 4) Deploy script: `FacetCutAction.Add` with `_rewardsSelectors()`, pass init calldata. 5) Post-upgrade loupe verification test — assert `claimRewards` selector maps to new facet address.]
334
- Use one storage library per domain namespace with a precomputed `keccak256` slot constant — avoids storage collisions between facets
335
- Include the plain-text namespace string as a comment next to the slot hash — enables verification that the hash matches the intended namespace
336
- Only append new fields to the end of storage structs when upgrading — Solidity lays out struct fields sequentially, reordering corrupts existing storage
337
- Keep facets stateless — state variables in facet contracts live at the facet address, not the diamond proxy where delegatecall executes
338
- Use initializer functions with version tracking instead of constructors — constructors run at deployment of the facet, not during diamondCut on the proxy
339
- Check for selector clashes before every `diamondCut` deployment — two facets exposing the same selector causes one to silently shadow the other
340
- Batch related facet changes into a single `diamondCut` call — ensures atomicity: either all changes apply or none do
341
- Verify upgrades via loupe functions in tests — assert selectors map to expected facets after every cut
342
- Protect `diamondCut` with strict access control; consider a timelock for production — an unprotected diamondCut allows arbitrary code execution on the proxy
343
- Use `LibDiamond.enforceIsContractOwner()` or a shared access control library — inherited modifiers from base contracts conflict with the stateless facet pattern
344
- Implement all EIP-2535 required interfaces: IDiamondCut, IDiamondLoupe, IERC165 — enables tooling and block explorers to discover and verify the diamond