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