Security Features
Overview
The ZEUR protocol implements multiple layers of security to protect user funds and ensure protocol stability. These security measures include smart contract security patterns, economic security mechanisms, operational security controls, and comprehensive monitoring systems.
Smart Contract Security
1. Reentrancy Protection
All external functions use OpenZeppelin's ReentrancyGuard:
contract Pool is ReentrancyGuardUpgradeable {
function supply(address asset, uint256 amount, address from)
external
payable
nonReentrant
{
// Reentrancy-safe implementation
// State changes before external calls
_updateUserBalance(from, asset, amount);
// External calls after state updates
IERC20(asset).safeTransferFrom(msg.sender, address(this), amount);
IVault(vault).lockCollateral(from, amount);
}
}
CEI Pattern (Checks-Effects-Interactions)
All functions follow the CEI pattern:
function withdraw(address asset, uint256 amount, address to) external nonReentrant {
// CHECKS
require(amount > 0, "Invalid amount");
require(getUserHealthFactor(msg.sender) >= HEALTH_FACTOR_BASE, "Insufficient health");
// EFFECTS
_updateUserBalance(msg.sender, asset, -amount);
colToken.burn(msg.sender, amount);
// INTERACTIONS
vault.unlockCollateral(to, amount);
}
2. Integer Overflow Protection
Using Solidity 0.8+ built-in overflow protection and SafeMath patterns:
// Automatic overflow protection in Solidity 0.8+
function calculateValue(uint256 amount, uint256 price) internal pure returns (uint256) {
return amount * price; // Reverts on overflow
}
// Additional validation for critical calculations
function calculateCollateralValue(uint256 amount, uint256 price) internal pure returns (uint256) {
require(amount <= type(uint128).max, "Amount too large");
require(price <= type(uint128).max, "Price too large");
return amount * price;
}
3. Access Control Security
Comprehensive role-based access control:
contract Pool is AccessManagedUpgradeable {
modifier onlyPoolAdmin() {
require(hasRole(POOL_ADMIN_ROLE, msg.sender), "Not pool admin");
_;
}
modifier onlyEmergency() {
require(
hasRole(EMERGENCY_ROLE, msg.sender) || hasRole(ADMIN_ROLE, msg.sender),
"Not authorized for emergency"
);
_;
}
}
4. Input Validation
Rigorous input validation on all external functions:
function supply(address asset, uint256 amount, address from) external payable {
require(asset != address(0), "Invalid asset");
require(amount > 0, "Amount must be positive");
require(from != address(0), "Invalid recipient");
require(collateralAssetList.contains(asset), "Asset not supported");
require(!collateralConfigs[asset].isPaused, "Asset paused");
// Additional validation
require(amount <= collateralConfigs[asset].supplyCap, "Exceeds supply cap");
}
Economic Security
1. Liquidation Mechanism
Robust liquidation system prevents bad debt:
function liquidate(address collateral, address debt, uint256 amount, address user) external {
// Verify liquidation conditions
UserAccountData memory userData = getUserAccountData(user);
require(userData.healthFactor < HEALTH_FACTOR_BASE, "Position healthy");
// Limit liquidation amount (max 50%)
uint256 userDebt = IERC20(debtToken).balanceOf(user);
uint256 maxLiquidation = userDebt / 2;
if (amount > maxLiquidation) {
amount = maxLiquidation;
}
// Execute liquidation with bonus
uint256 collateralSeized = calculateCollateralSeized(amount, collateral, debt);
_executeLiquidation(user, collateral, debt, amount, collateralSeized);
}
2. Supply and Borrow Caps
Limit protocol exposure to any single asset:
struct CollateralConfiguration {
uint256 supplyCap; // Maximum total supply
uint256 borrowCap; // Maximum borrowing against this asset
// ... other fields
}
function checkSupplyCap(address asset, uint256 amount) internal view {
uint256 totalSupply = IERC20(colToken).totalSupply();
require(totalSupply + amount <= supplyCap, "Supply cap exceeded");
}
3. Oracle Security
Multiple validation layers for price data:
function getAssetPrice(address asset) external view returns (uint256) {
AggregatorV3Interface priceFeed = priceFeeds[asset];
(uint80 roundId, int256 price, , uint256 updatedAt, uint80 answeredInRound) =
priceFeed.latestRoundData();
// Comprehensive validation
require(price > 0, "Invalid price");
require(updatedAt > 0, "Price not updated");
require(block.timestamp - updatedAt <= stalenessTolerance, "Stale price");
require(answeredInRound >= roundId, "Stale round");
// Optional: Price deviation check
validatePriceDeviation(uint256(price), lastValidPrice[asset]);
return uint256(price);
}
Circuit Breakers
1. Emergency Pause Mechanism
Protocol-wide emergency controls:
contract Pool is PausableUpgradeable {
mapping(address => bool) public assetPaused;
bool public protocolPaused;
modifier whenAssetNotPaused(address asset) {
require(!assetPaused[asset] && !protocolPaused, "Operations paused");
_;
}
function pauseAsset(address asset) external onlyEmergency {
assetPaused[asset] = true;
emit AssetPaused(asset, msg.sender);
}
function pauseProtocol() external onlyEmergency {
protocolPaused = true;
emit ProtocolPaused(msg.sender);
}
}
2. Price Deviation Circuit Breaker
Automatic protection against oracle manipulation:
uint256 public constant MAX_PRICE_DEVIATION = 1000; // 10%
mapping(address => uint256) public lastValidPrice;
function validatePriceDeviation(uint256 newPrice, uint256 lastPrice) internal view {
if (lastPrice == 0) return; // First price update
uint256 deviation = newPrice > lastPrice ?
((newPrice - lastPrice) * 10000) / lastPrice :
((lastPrice - newPrice) * 10000) / lastPrice;
if (deviation > MAX_PRICE_DEVIATION) {
// Use emergency price or pause operations
revert("Price deviation too high");
}
}
3. Utilization Rate Limits
Prevent bank runs and liquidity crises:
function withdraw(address asset, uint256 amount, address to) external {
// Check utilization rate
uint256 totalSupplied = getAssetTotalSupplied(asset);
uint256 totalBorrowed = getAssetTotalBorrowed(asset);
uint256 utilizationRate = totalBorrowed * 10000 / totalSupplied;
require(utilizationRate <= MAX_UTILIZATION_RATE, "Utilization too high");
// Check available liquidity
uint256 availableLiquidity = totalSupplied - totalBorrowed;
require(amount <= availableLiquidity, "Insufficient liquidity");
}
Operational Security
1. Multi-Signature Requirements
Critical operations require multiple signatures:
contract ProtocolMultisig {
uint256 public constant REQUIRED_SIGNATURES = 3;
uint256 public constant TOTAL_SIGNERS = 5;
mapping(bytes32 => uint256) public confirmations;
mapping(bytes32 => mapping(address => bool)) public hasConfirmed;
function submitTransaction(address target, bytes calldata data) external returns (bytes32 txId) {
require(isSigner[msg.sender], "Not a signer");
txId = keccak256(abi.encode(target, data, block.timestamp));
confirmations[txId] = 1;
hasConfirmed[txId][msg.sender] = true;
emit TransactionSubmitted(txId, target, data);
}
function confirmTransaction(bytes32 txId) external {
require(isSigner[msg.sender], "Not a signer");
require(!hasConfirmed[txId][msg.sender], "Already confirmed");
hasConfirmed[txId][msg.sender] = true;
confirmations[txId]++;
if (confirmations[txId] >= REQUIRED_SIGNATURES) {
executeTransaction(txId);
}
}
}
2. Time-Delayed Execution
Critical changes require time delays:
struct DelayedOperation {
address target;
bytes data;
uint256 executeAfter;
bool executed;
}
mapping(bytes32 => DelayedOperation) public delayedOps;
function scheduleOperation(address target, bytes calldata data, uint256 delay) external {
require(hasRole(ADMIN_ROLE, msg.sender), "Not admin");
bytes32 opId = keccak256(abi.encode(target, data, block.timestamp));
delayedOps[opId] = DelayedOperation({
target: target,
data: data,
executeAfter: block.timestamp + delay,
executed: false
});
emit OperationScheduled(opId, target, data, block.timestamp + delay);
}
3. Emergency Withdrawal Mechanism
Users can withdraw funds during emergencies:
function emergencyWithdraw(address asset) external whenPaused {
require(emergencyMode, "Not in emergency mode");
uint256 userBalance = IERC20(colToken).balanceOf(msg.sender);
require(userBalance > 0, "No balance to withdraw");
// Bypass normal health checks during emergency
colToken.burn(msg.sender, userBalance);
vault.emergencyUnlock(msg.sender, userBalance);
emit EmergencyWithdrawal(msg.sender, asset, userBalance);
}
Upgrade Security
1. UUPS Proxy Pattern
Secure upgrade mechanism using OpenZeppelin UUPS:
contract Pool is UUPSUpgradeable, AccessManagedUpgradeable {
function _authorizeUpgrade(address newImplementation)
internal
override
onlyRole(ADMIN_ROLE)
{
// Additional upgrade validation
require(isValidImplementation(newImplementation), "Invalid implementation");
}
function isValidImplementation(address impl) internal view returns (bool) {
// Check implementation has required functions
return impl.code.length > 0 &&
IERC165(impl).supportsInterface(type(IPool).interfaceId);
}
}
2. Storage Layout Protection
Prevent storage collisions during upgrades:
abstract contract PoolStorageV1 {
struct PoolStorage {
mapping(address => CollateralConfiguration) collateralConfigs;
mapping(address => DebtConfiguration) debtConfigs;
EnumerableSet.AddressSet collateralAssetList;
EnumerableSet.AddressSet debtAssetList;
IChainlinkOracleManager oracleManager;
}
// keccak256(abi.encode(uint256(keccak256("zeur.storage.Pool")) - 1)) & ~bytes32(uint256(0xff))
bytes32 private constant POOL_STORAGE_LOCATION = 0x1234...;
function _getPoolStorage() internal pure returns (PoolStorage storage $) {
assembly {
$.slot := POOL_STORAGE_LOCATION
}
}
}
Monitoring and Alerting
1. Event Monitoring
Comprehensive event logging for monitoring:
event Supply(address indexed user, address indexed asset, uint256 amount);
event Withdraw(address indexed user, address indexed asset, uint256 amount);
event Borrow(address indexed user, address indexed asset, uint256 amount);
event Repay(address indexed user, address indexed asset, uint256 amount);
event Liquidation(
address indexed liquidator,
address indexed user,
address collateralAsset,
address debtAsset,
uint256 debtAmount,
uint256 collateralSeized
);
// Security events
event EmergencyPause(address indexed caller, string reason);
event PriceDeviation(address indexed asset, uint256 oldPrice, uint256 newPrice);
event UnauthorizedAccess(address indexed caller, bytes4 selector);
2. Health Factor Monitoring
Real-time position monitoring:
function batchCheckHealthFactors(address[] calldata users)
external
view
returns (uint256[] memory healthFactors)
{
healthFactors = new uint256[](users.length);
for (uint i = 0; i < users.length; i++) {
UserAccountData memory userData = getUserAccountData(users[i]);
healthFactors[i] = userData.healthFactor;
// Emit warning if close to liquidation
if (userData.healthFactor < 1.1e18 && userData.healthFactor >= 1e18) {
emit HealthFactorWarning(users[i], userData.healthFactor);
}
}
}
3. Anomaly Detection
Automated detection of unusual patterns:
contract SecurityMonitor {
uint256 public constant LARGE_OPERATION_THRESHOLD = 1000000e18; // $1M
uint256 public constant RAPID_OPERATION_WINDOW = 1 hours;
mapping(address => uint256) public lastLargeOperation;
mapping(address => uint256) public operationCount;
function checkAnomalies(address user, uint256 amount) external {
// Large operation detection
if (amount > LARGE_OPERATION_THRESHOLD) {
emit LargeOperation(user, amount);
}
// Rapid operation detection
if (block.timestamp - lastLargeOperation[user] < RAPID_OPERATION_WINDOW) {
operationCount[user]++;
if (operationCount[user] > 5) {
emit RapidOperations(user, operationCount[user]);
}
} else {
operationCount[user] = 1;
}
lastLargeOperation[user] = block.timestamp;
}
}
Audit and Formal Verification
1. Code Analysis
Security measures for code quality:
// Static analysis with Slither
// Formal verification with Certora
// Fuzzing with Echidna
// Invariant tests
contract PoolInvariants {
function invariant_totalDebtLessThanCollateral() public view {
uint256 totalCollateralValue = calculateTotalCollateralValue();
uint256 totalDebtValue = calculateTotalDebtValue();
// Total debt should never exceed total collateral value
assert(totalDebtValue <= totalCollateralValue);
}
function invariant_healthFactorCalculation() public view {
// Health factor calculation should be consistent
for (uint i = 0; i < users.length; i++) {
UserAccountData memory userData = getUserAccountData(users[i]);
uint256 calculatedHF = calculateHealthFactor(users[i]);
assert(userData.healthFactor == calculatedHF);
}
}
}
2. External Audits
Multi-party audit process:
Smart Contract Audits: Multiple independent audit firms
Economic Model Review: Tokenomics and incentive analysis
Integration Testing: End-to-end protocol testing
Stress Testing: High-load and edge case testing
3. Bug Bounty Program
Continuous security improvement:
contract BugBounty {
enum Severity { Low, Medium, High, Critical }
mapping(Severity => uint256) public bountyAmounts;
constructor() {
bountyAmounts[Severity.Low] = 1000e6; // $1,000
bountyAmounts[Severity.Medium] = 5000e6; // $5,000
bountyAmounts[Severity.High] = 25000e6; // $25,000
bountyAmounts[Severity.Critical] = 100000e6; // $100,000
}
}
Incident Response
1. Emergency Response Plan
Structured response to security incidents:
enum EmergencyLevel { None, Low, Medium, High, Critical }
struct IncidentResponse {
EmergencyLevel level;
address responder;
uint256 timestamp;
string description;
bool resolved;
}
mapping(uint256 => IncidentResponse) public incidents;
function declareEmergency(
EmergencyLevel level,
string calldata description
) external onlyEmergency returns (uint256 incidentId) {
incidentId = nextIncidentId++;
incidents[incidentId] = IncidentResponse({
level: level,
responder: msg.sender,
timestamp: block.timestamp,
description: description,
resolved: false
});
// Automatic responses based on level
if (level >= EmergencyLevel.High) {
pauseProtocol();
}
if (level == EmergencyLevel.Critical) {
enableEmergencyWithdrawals();
}
emit EmergencyDeclared(incidentId, level, description);
}
2. Recovery Procedures
Systematic recovery from incidents:
function initiateRecovery(uint256 incidentId) external onlyAdmin {
IncidentResponse storage incident = incidents[incidentId];
require(!incident.resolved, "Already resolved");
// Gradual recovery process
if (incident.level >= EmergencyLevel.High) {
// Step 1: Resume core functions
unpauseCollateralOperations();
// Step 2: Resume borrowing (after validation)
require(validateSystemHealth(), "System not healthy");
unpauseBorrowingOperations();
// Step 3: Resume full operations
unpauseProtocol();
}
incident.resolved = true;
emit IncidentResolved(incidentId, msg.sender);
}
Security Best Practices
1. Defense in Depth
Multiple security layers:
Smart contract security patterns
Economic incentive alignment
Operational security controls
Monitoring and alerting systems
External audits and reviews
2. Fail-Safe Defaults
System defaults to safe state:
Assets default to paused until explicitly enabled
Emergency functions default to most restrictive settings
User operations require explicit allowances
3. Principle of Least Privilege
Minimal necessary permissions:
Role-based access control
Time-limited permissions where possible
Regular permission audits and cleanup
The comprehensive security framework ensures ZEUR protocol maintains the highest standards of security while providing efficient and user-friendly DeFi services.
Last updated