This document explains the Diamond Proxy Pattern (EIP-2535) implementation for EcoFundMe, including the integration with your LibAppStorage.
┌─────────────────────────────────────────────────────────────┐
│ Diamond Proxy │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ Fallback Function │ │
│ │ Routes calls to appropriate facets based on selector │ │
│ └───────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
│
┌────────────────────┼────────────────────┐
│ │ │
▼ ▼ ▼
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ CampaignFacet│ │ VotingFacet │ │IdentityFacet │
│ │ │ │ │ │
│ • create │ │ • submit │ │ • verify │
│ • donate │ │ Results │ │ KYB │
│ • milestone │ │ • approve │ │ • check │
└──────────────┘ └──────────────┘ └──────────────┘
│ │ │
└────────────────────┼────────────────────┘
│
▼
┌──────────────────┐
│ LibAppStorage │
│ │
│ Shared Storage │
│ at Fixed Slot │
└──────────────────┘
src/
├── Diamond.sol # Main proxy contract
├── DiamondInit.sol # Initialization contract
│
├── facets/
│ ├── DiamondCutFacet.sol # Facet management
│ ├── DiamondLoupeFacet.sol # Introspection
│ ├── OwnershipFacet.sol # Ownership management
│ ├── CampaignFacet.sol # Campaign logic (TODO)
│ ├── VotingFacet.sol # DAO governance (TODO)
│ └── IdentityFacet.sol # ONCHAINID integration (TODO)
│
├── libraries/
│ ├── LibDiamond.sol # Diamond storage & operations
│ └── LibAppStorage.sol # Your AppStorage (already created)
│
└── interfaces/
├── IDiamondCut.sol # Diamond cut interface
├── IDiamondLoupe.sol # Introspection interface
└── IERC173.sol # Ownership interface
The Diamond uses TWO separate storage positions:
// LibDiamond.sol - Diamond management storage
bytes32 constant DIAMOND_STORAGE_POSITION = keccak256("diamond.standard.diamond.storage");
// LibAppStorage.sol - Your application storage
bytes32 internal constant APP_STORAGE_POSITION = keccak256("ecofundme.app.storage.v1");Why separate?
LibDiamondstorage: Manages facet addresses and function selectorsLibAppStorage: Your business logic data (campaigns, milestones, etc.)- They never collide because they use different storage slots
Every facet accesses AppStorage the same way:
// In any facet
import {LibAppStorage} from "../libraries/LibAppStorage.sol";
contract CampaignFacet {
function createCampaign(...) external {
// Get AppStorage reference
LibAppStorage.AppStorage storage s = LibAppStorage.diamondStorage();
// Use it
s.campaignCount++;
s.campaigns[s.campaignCount] = ...;
}
}// User calls: diamond.createCampaign(...)
1. Call hits Diamond fallback()
2. Diamond looks up msg.sig (createCampaign selector) in LibDiamond storage
3. Finds CampaignFacet address
4. Delegatecalls to CampaignFacet.createCampaign()
5. CampaignFacet executes in Diamond's storage context
6. Accesses LibAppStorage at fixed slot positionDiamondCutFacet diamondCutFacet = new DiamondCutFacet();
DiamondLoupeFacet diamondLoupeFacet = new DiamondLoupeFacet();
OwnershipFacet ownershipFacet = new OwnershipFacet();
DiamondInit diamondInit = new DiamondInit();Diamond diamond = new Diamond(owner, address(diamondCutFacet));IDiamondCut.FacetCut[] memory cut = new IDiamondCut.FacetCut[](2);
// Add DiamondLoupeFacet
bytes4[] memory loupeSelectors = new bytes4[](5);
loupeSelectors[0] = DiamondLoupeFacet.facets.selector;
loupeSelectors[1] = DiamondLoupeFacet.facetFunctionSelectors.selector;
// ... more selectors
cut[0] = IDiamondCut.FacetCut({
facetAddress: address(diamondLoupeFacet),
action: IDiamondCut.FacetCutAction.Add,
functionSelectors: loupeSelectors
});
// Add OwnershipFacet
// ... similar patternDiamondInit.InitParams memory initParams = DiamondInit.InitParams({
admin: owner,
linkToken: linkTokenAddress,
automationRegistrar: registrarAddress,
automationRegistry: registryAddress,
identityFactory: identityFactoryAddress,
defaultUpkeepFunding: 5 ether,
defaultUpkeepGasLimit: 500_000
});
bytes memory initCalldata = abi.encodeWithSelector(
DiamondInit.init.selector,
initParams
);IDiamondCut(address(diamond)).diamondCut(
cut,
address(diamondInit),
initCalldata
);# Run tests
forge test
# Run specific test
forge test --match-test test_DiamondDeployment
# Run with verbosity
forge test -vvv
# Run with gas reporting
forge test --gas-report// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import {LibAppStorage} from "../libraries/LibAppStorage.sol";
contract CampaignFacet {
/// @notice Create a new campaign
function createCampaign(
string calldata title,
uint256 goalAmount,
uint64 duration,
address erc20Token
) external returns (uint256 campaignId) {
LibAppStorage.AppStorage storage s = LibAppStorage.diamondStorage();
// Your logic here
campaignId = LibAppStorage.allocateCampaignId(s, msg.sender);
LibAppStorage.CampaignData storage campaign = s.campaigns[campaignId];
campaign.title = title;
campaign.goalAmount = goalAmount;
campaign.duration = duration;
campaign.erc20Token = erc20Token;
campaign.deadline = uint64(block.timestamp) + duration;
campaign.state = LibAppStorage.CampaignState.Active;
return campaignId;
}
// More functions...
}CampaignFacet campaignFacet = new CampaignFacet();IDiamondCut.FacetCut[] memory cut = new IDiamondCut.FacetCut[](1);
bytes4[] memory selectors = new bytes4[](1);
selectors[0] = CampaignFacet.createCampaign.selector;
// Add more selectors for other functions
cut[0] = IDiamondCut.FacetCut({
facetAddress: address(campaignFacet),
action: IDiamondCut.FacetCutAction.Add,
functionSelectors: selectors
});
IDiamondCut(address(diamond)).diamondCut(cut, address(0), "");// Deploy new version
CampaignFacetV2 newFacet = new CampaignFacetV2();
// Prepare cut
IDiamondCut.FacetCut[] memory cut = new IDiamondCut.FacetCut[](1);
bytes4[] memory selectors = new bytes4[](1);
selectors[0] = CampaignFacetV2.createCampaign.selector;
cut[0] = IDiamondCut.FacetCut({
facetAddress: address(newFacet),
action: IDiamondCut.FacetCutAction.Replace, // REPLACE, not Add
functionSelectors: selectors
});
IDiamondCut(address(diamond)).diamondCut(cut, address(0), "");IDiamondCut.FacetCut[] memory cut = new IDiamondCut.FacetCut[](1);
bytes4[] memory selectors = new bytes4[](1);
selectors[0] = CampaignFacet.deprecatedFunction.selector;
cut[0] = IDiamondCut.FacetCut({
facetAddress: address(0), // address(0) for removal
action: IDiamondCut.FacetCutAction.Remove,
functionSelectors: selectors
});
IDiamondCut(address(diamond)).diamondCut(cut, address(0), "");- NEVER reorder fields in LibAppStorage.AppStorage
- ALWAYS add new fields at the end of structs
- Use assembly for storage access (already done in LibAppStorage.diamondStorage())
- Mappings inside structs are safe (you're already doing this correctly)
- NO storage variables in facets - Use LibAppStorage only
- Import LibAppStorage in every facet that needs storage
- Use
externalfor public functions (gas optimization) - Add events to facets, not storage library
// Check for selector collisions before adding
bytes4 selector = CampaignFacet.createCampaign.selector;
address existingFacet = IDiamondLoupe(diamond).facetAddress(selector);
require(existingFacet == address(0), "Selector already exists");# Deploy to localhost
forge script script/DeployDiamond.s.sol --rpc-url http://localhost:8545 --broadcast
# Deploy to Sepolia
forge script script/DeployDiamond.s.sol \
--rpc-url $SEPOLIA_RPC_URL \
--private-key $DEPLOYER_PRIVATE_KEY \
--broadcast \
--verify
# Verify on Etherscan
forge verify-contract <DIAMOND_ADDRESS> \
src/Diamond.sol:Diamond \
--chain-id 11155111 \
--etherscan-api-key $ETHERSCAN_API_KEY \
--constructor-args $(cast abi-encode "constructor(address,address)" "OWNER" "DIAMOND_CUT_FACET")
# Get function selector
cast sig "createCampaign(string,uint256,uint64,address)"
# Check facet functions
cast call $DIAMOND_ADDRESS "facetFunctionSelectors(address)(bytes4[])" $FACET_ADDRESS --rpc-url $RPC- Use
calldatafor arrays/strings in external functions - Pack structs efficiently (already done in LibAppStorage)
- Use
uint32/uint64instead ofuint256where possible (you're doing this!) - Batch operations when possible
- Use events for off-chain data instead of storing everything
- DiamondCut is protected by owner-only modifier ✅
- All facets use LibAppStorage for storage ✅
- No storage variables declared in facets ✅
- AppStorage uses append-only layout ✅
- Critical functions have access control (add in facets)
- Reentrancy guards where needed (add in facets)
- Input validation on all external functions (add in facets)
- Events emitted for important state changes (add in facets)
- ✅ Diamond infrastructure deployed
- ✅ LibAppStorage integrated
- ⏳ Create CampaignFacet
- ⏳ Create VotingFacet
- ⏳ Create IdentityFacet
- ⏳ Create VerificationFacet
- ⏳ Add comprehensive tests
- ⏳ Security audit