Smart Contract Architecture
Design patterns, inheritance hierarchy, and technical implementation of KhipuVault smart contracts.
Smart Contract Architecture
KhipuVault's smart contracts are built with security, upgradability, and gas efficiency in mind. This guide explains the contract architecture, design patterns, and how contracts interact with each other.
Contract Hierarchy
┌─────────────────────────────────────────────┐
│ BasePoolV3 (Abstract) │
│ ├─ Ownable (OpenZeppelin) │
│ ├─ ReentrancyGuard (OpenZeppelin) │
│ ├─ UUPSUpgradeable (OpenZeppelin) │
│ └─ Core pool logic │
└──────────────┬──────────────────────────────┘
│
┌───────┴───────┬──────────────┬─────────────┐
│ │ │ │
┌──────▼──────┐ ┌──────▼──────┐ ┌────▼────┐ ┌──────▼──────┐
│ Individual │ │ Cooperative │ │ Lottery │ │ Rotating │
│ PoolV3 │ │ PoolV3 │ │ PoolV3 │ │ Pool │
└─────────────┘ └─────────────┘ └─────────┘ └─────────────┘
│ │ │ │
└───────┬───────┴──────────────┴─────────────┘
│
┌──────▼──────────────┐
│ MezoIntegrationV3 │
│ (Yield Strategy) │
└──────┬──────────────┘
│
┌──────▼──────────────┐
│ YieldAggregatorV3 │
│ (Multiple │
│ Strategies) │
└─────────────────────┘
│
┌──────▼──────────────┐
│ Mezo Protocol │
│ (External) │
│ - Stability Pool │
│ - Trove Manager │
│ - Price Feed │
└─────────────────────┘Core Contracts
BasePoolV3
Purpose: Abstract base contract providing core pool functionality.
Key Features:
- MUSD token deposits and withdrawals
- Balance tracking per user
- Yield calculation and distribution
- Reentrancy protection
- Upgradeable via UUPS proxy
Storage Layout:
contract BasePoolV3 {
IERC20 public musdToken; // MUSD token address
mapping(address => uint256) public balances; // User balances
uint256 public totalDeposits; // Total pool deposits
uint256 public totalYield; // Accumulated yield
mapping(address => uint256) public lastYieldClaim; // Yield tracking
}Critical Functions:
deposit(uint256 amount)- Add funds to poolwithdraw(uint256 amount)- Remove funds from poolcalculateYield(address user)- Calculate user's yieldclaimYield()- Withdraw earned yield_authorizeUpgrade()- Upgrade authorization (owner only)
IndividualPoolV3
Address: 0xdfBEd2D3efBD2071fD407bF169b5e5533eA90393
Purpose: Personal savings pools with full user control.
Additional Features:
- Solo ownership (1 user per pool)
- Flexible deposits/withdrawals
- No governance required
- Yield auto-compounding option
Unique Storage:
contract IndividualPoolV3 is BasePoolV3 {
address public owner; // Pool owner
bool public autoCompound; // Auto-compound yield
uint256 public minimumDeposit; // Min deposit (10 MUSD)
uint256 public createdAt; // Creation timestamp
}CooperativePoolV3
Address: 0x323FcA9b377fe29B8fc95dDbD9Fe54cea1655F88
Purpose: Multi-user pools with democratic governance.
Additional Features:
- Multiple members
- Voting on withdrawals (if enabled)
- Shared yield distribution
- Member permissions
Unique Storage:
contract CooperativePoolV3 is BasePoolV3 {
address[] public members; // Pool members
mapping(address => bool) public isMember;
bool public requiresVoting; // Governance flag
uint256 public votingThreshold; // % required to pass
struct Proposal {
address target;
uint256 amount;
uint256 votesFor;
uint256 votesAgainst;
bool executed;
}
mapping(uint256 => Proposal) public proposals;
}LotteryPoolV3
Purpose: No-loss lottery (Prize Pool).
Additional Features:
- Random winner selection
- Prize distribution from yield
- Ticket-based entries
- Chainlink VRF (future) for randomness
Unique Storage:
contract LotteryPoolV3 is BasePoolV3 {
uint256 public drawInterval; // Time between draws
uint256 public lastDrawTime; // Last draw timestamp
address public lastWinner; // Previous winner
uint256 public lastPrize; // Previous prize amount
mapping(address => uint256) public tickets; // User entries
}RotatingPool (ROSCA)
Purpose: Traditional rotating savings and credit association.
Additional Features:
- Fixed rotation order
- Predefined payout schedule
- No yields (social credit)
- Locked until user's turn
Unique Storage:
contract RotatingPool is BasePoolV3 {
address[] public rotationOrder; // Payout order
uint256 public currentRound; // Current rotation
mapping(address => bool) public hasClaimed; // Claim tracking
uint256 public contributionAmount; // Fixed contribution
uint256 public roundInterval; // Days between rounds
}Integration Contracts
MezoIntegrationV3
Address: 0x043def502e4A1b867Fd58Df0Ead080B8062cE1c6
Purpose: Interface with Mezo protocol for yield generation.
Key Functions:
interface IMezoIntegration {
function depositToStabilityPool(uint256 amount) external;
function withdrawFromStabilityPool(uint256 amount) external;
function claimRewards() external returns (uint256);
function calculateAPY() external view returns (uint256);
}How It Works:
- Pools deposit MUSD to MezoIntegration
- MezoIntegration deposits to Mezo Stability Pool
- Stability Pool generates yield
- MezoIntegration claims and distributes back to pools
YieldAggregatorV3
Address: 0x3D28A5eF59Cf3ab8E2E11c0A8031373D46370BE6
Purpose: Route deposits to best-performing yield strategies.
Strategies:
- Mezo Stability Pool (primary)
- Future: Lending protocols
- Future: Liquidity mining
Key Functions:
interface IYieldAggregator {
function deposit(uint256 amount) external;
function withdraw(uint256 amount) external;
function harvest() external returns (uint256);
function getAPY() external view returns (uint256);
function rebalance() external; // Owner only
}Design Patterns
1. UUPS Upgradeable Pattern
All pool contracts use OpenZeppelin's UUPS (Universal Upgradeable Proxy Standard):
import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
contract IndividualPoolV3 is UUPSUpgradeable {
function _authorizeUpgrade(address newImplementation)
internal
override
onlyOwner
{}
}Benefits:
- Fix bugs without losing state
- Add features without migration
- Gas-efficient (minimal proxy overhead)
- Owner-controlled upgrades
Safety:
- Storage layout must be preserved
- Use storage gaps for future variables
- Test upgrades on testnet first
2. Reentrancy Protection
All state-changing functions use OpenZeppelin's ReentrancyGuard:
function withdraw(uint256 amount) external nonReentrant {
require(balances[msg.sender] >= amount, "Insufficient balance");
balances[msg.sender] -= amount;
totalDeposits -= amount;
musdToken.safeTransfer(msg.sender, amount);
emit Withdrawal(msg.sender, amount);
}Pattern: Checks → Effects → Interactions (CEI)
3. Custom Errors (Gas Optimization)
Replace require strings with custom errors:
// Before (expensive)
require(amount > 0, "Amount must be greater than zero");
// After (cheaper)
error InvalidAmount();
if (amount == 0) revert InvalidAmount();Savings: ~50 gas per error
4. Events for Indexing
Comprehensive events for off-chain indexing:
event Deposit(
address indexed user,
uint256 amount,
uint256 newBalance,
uint256 timestamp
);
event Withdrawal(
address indexed user,
uint256 amount,
uint256 newBalance,
uint256 timestamp
);
event YieldClaimed(
address indexed user,
uint256 yieldAmount,
uint256 timestamp
);Best Practices:
- Index frequently-queried fields (user addresses)
- Include timestamps for analytics
- Emit after state changes confirm
5. Access Control
Use OpenZeppelin Ownable for admin functions:
import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
contract IndividualPoolV3 is OwnableUpgradeable {
function setMinimumDeposit(uint256 newMin) external onlyOwner {
minimumDeposit = newMin;
emit MinimumDepositUpdated(newMin);
}
}For multi-role systems (future):
import "@openzeppelin/contracts/access/AccessControl.sol";
contract CooperativePoolV3 is AccessControl {
bytes32 public constant ADMIN_ROLE = keccak256("ADMIN_ROLE");
bytes32 public constant MEMBER_ROLE = keccak256("MEMBER_ROLE");
}Security Features
1. Input Validation
function deposit(uint256 amount) external {
if (amount == 0) revert InvalidAmount();
if (amount < minimumDeposit) revert BelowMinimum();
if (balances[msg.sender] + amount > type(uint256).max) revert Overflow();
// ... rest of logic
}2. Safe Math (Solidity 0.8+)
Built-in overflow/underflow protection:
// Automatically reverts on overflow
balances[msg.sender] += amount;
// Use unchecked for gas savings when safe
unchecked {
totalDeposits += amount; // Known to be safe
}3. Pull Over Push (Withdrawal Pattern)
Users withdraw their funds (pull) instead of contract sending (push):
// Good (pull)
function withdraw(uint256 amount) external {
require(balances[msg.sender] >= amount);
balances[msg.sender] -= amount;
musdToken.safeTransfer(msg.sender, amount);
}
// Bad (push)
function distributeYield() external {
for (uint i = 0; i < users.length; i++) {
musdToken.safeTransfer(users[i], yield[i]); // Can fail
}
}4. Pause Mechanism (Emergency)
import "@openzeppelin/contracts/security/Pausable.sol";
contract IndividualPoolV3 is Pausable {
function deposit(uint256 amount) external whenNotPaused {
// Normal logic
}
function pause() external onlyOwner {
_pause();
}
}Gas Optimizations
1. Storage Packing
// Packed in single slot (save gas)
struct PoolInfo {
uint128 balance; // 128 bits
uint64 createdAt; // 64 bits
uint64 lastUpdate; // 64 bits
}
// Total: 256 bits = 1 storage slot2. Memory vs Storage
// Load to memory once
function calculateTotal() external view returns (uint256) {
uint256 _totalDeposits = totalDeposits; // Memory copy
uint256 _totalYield = totalYield; // Memory copy
return _totalDeposits + _totalYield;
}3. Short-Circuit Evaluation
// Most likely to fail first
if (amount == 0 || balances[msg.sender] < amount || paused()) {
revert();
}Testing Strategy
Unit Tests (Foundry)
// test/IndividualPoolV3.t.sol
contract IndividualPoolV3Test is Test {
function testDeposit() public {
pool.deposit(100 ether);
assertEq(pool.balances(user), 100 ether);
}
function testDepositRevertsOnZero() public {
vm.expectRevert(InvalidAmount.selector);
pool.deposit(0);
}
}Integration Tests
function testDepositWithYield() public {
// 1. Deposit
pool.deposit(100 ether);
// 2. Generate yield
vm.warp(block.timestamp + 365 days);
mezoIntegration.harvest();
// 3. Check yield
uint256 yield = pool.calculateYield(user);
assertGt(yield, 0);
}Invariant Tests
function invariant_totalDepositsSumOfBalances() public {
uint256 sum = 0;
for (uint i = 0; i < users.length; i++) {
sum += pool.balances(users[i]);
}
assertEq(sum, pool.totalDeposits());
}Deployment Process
-
Compile Contracts
cd packages/contracts forge build -
Run Tests
forge test --gas-report -
Deploy to Testnet
forge script script/DeployPools.s.sol --rpc-url mezo-testnet --broadcast -
Verify on Explorer
forge verify-contract <address> <contract> --chain mezo-testnet
Next Steps
IndividualPool Reference
Complete IndividualPool documentation
CooperativePool Reference
Complete CooperativePool documentation
API Design
Backend architecture patterns
Questions? Join Discord #smart-contracts or email dev@khipuvault.com