Defi Security Considerations

Security guidance specific to decentralized finance protocols. DeFi protocols face unique security challenges due to their financial nature, composability, and...

Last updated: January 14, 2026

DeFi Security Considerations

Security guidance specific to decentralized finance protocols.

Overview

DeFi protocols face unique security challenges due to their financial nature, composability, and adversarial environment. This guide covers DeFi-specific security considerations.


Oracle Security

Price Oracle Risks

Risk Description
Manipulation Attacker manipulates price feed
Stale data Oracle returns outdated prices
Flash loan attacks Price manipulation via flash loans
Single point of failure Reliance on one oracle

Secure Oracle Pattern

import "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol";

contract SecurePriceFeed {
    AggregatorV3Interface internal priceFeed;
    uint256 public constant STALE_THRESHOLD = 1 hours;

    function getPrice() public view returns (uint256) {
        (
            uint80 roundId,
            int256 price,
            ,
            uint256 updatedAt,
            uint80 answeredInRound
        ) = priceFeed.latestRoundData();

        // Check for stale data
        require(
            updatedAt >= block.timestamp - STALE_THRESHOLD,
            "Stale price data"
        );

        // Check round completeness
        require(answeredInRound >= roundId, "Incomplete round");

        // Check for valid price
        require(price > 0, "Invalid price");

        return uint256(price);
    }
}

TWAP for Manipulation Resistance

// Use time-weighted average price
function getTWAP(address pool, uint32 period) public view returns (uint256) {
    uint32[] memory secondsAgos = new uint32;
    secondsAgos[0] = period;
    secondsAgos[1] = 0;

    (int56[] memory tickCumulatives, ) = IUniswapV3Pool(pool).observe(secondsAgos);

    int56 tickCumulativesDelta = tickCumulatives[1] - tickCumulatives[0];
    int24 tick = int24(tickCumulativesDelta / int56(uint56(period)));

    return OracleLibrary.getQuoteAtTick(tick, amount, tokenIn, tokenOut);
}

Flash Loan Protection

Understanding Flash Loans

Flash loans allow borrowing without collateral if repaid in same transaction:

  • Can manipulate prices
  • Can drain liquidity pools
  • Can exploit reentrancy
  • Can bypass governance

Protection Patterns

contract FlashLoanSafe {
    mapping(address => uint256) private _lastBlock;

    // Prevent same-block operations
    modifier noFlashLoan() {
        require(
            _lastBlock[msg.sender] != block.number,
            "Same block not allowed"
        );
        _lastBlock[msg.sender] = block.number;
        _;
    }

    function sensitiveOperation() external noFlashLoan {
        // Protected from flash loan manipulation
    }
}

Delay-Based Protection

contract DelayedWithdraw {
    struct Withdrawal {
        uint256 amount;
        uint256 unlockTime;
    }

    mapping(address => Withdrawal) public withdrawals;
    uint256 public constant DELAY = 1 days;

    function requestWithdraw(uint256 amount) external {
        withdrawals[msg.sender] = Withdrawal({
            amount: amount,
            unlockTime: block.timestamp + DELAY
        });
    }

    function executeWithdraw() external {
        Withdrawal memory w = withdrawals[msg.sender];
        require(block.timestamp >= w.unlockTime, "Too early");
        delete withdrawals[msg.sender];
        // ... transfer
    }
}

Liquidity Pool Security

Slippage Protection

function swap(
    uint256 amountIn,
    uint256 minAmountOut,  // Slippage protection
    uint256 deadline       // Deadline protection
) external {
    require(block.timestamp <= deadline, "Transaction expired");

    uint256 amountOut = calculateSwapOutput(amountIn);
    require(amountOut >= minAmountOut, "Insufficient output");

    // Execute swap
}

First Depositor Attack Protection

contract SafeVault {
    uint256 public constant MINIMUM_LIQUIDITY = 1000;

    function deposit(uint256 amount) external returns (uint256 shares) {
        uint256 supply = totalSupply;

        if (supply == 0) {
            // First deposit: lock minimum liquidity
            shares = amount - MINIMUM_LIQUIDITY;
            _mint(address(0), MINIMUM_LIQUIDITY);  // Burn forever
        } else {
            shares = amount * supply / totalAssets();
        }

        require(shares > 0, "Zero shares");
        _mint(msg.sender, shares);
    }
}

Governance Security

Timelock for Critical Changes

contract Timelock {
    uint256 public constant DELAY = 2 days;
    mapping(bytes32 => uint256) public queuedTransactions;

    function queueTransaction(
        address target,
        bytes calldata data
    ) external onlyAdmin returns (bytes32) {
        bytes32 txHash = keccak256(abi.encode(target, data, block.timestamp));
        queuedTransactions[txHash] = block.timestamp + DELAY;
        return txHash;
    }

    function executeTransaction(
        address target,
        bytes calldata data,
        uint256 eta
    ) external onlyAdmin {
        bytes32 txHash = keccak256(abi.encode(target, data, eta));
        require(queuedTransactions[txHash] != 0, "Not queued");
        require(block.timestamp >= queuedTransactions[txHash], "Not ready");

        delete queuedTransactions[txHash];
        (bool success, ) = target.call(data);
        require(success);
    }
}

Vote Escrow Pattern

// Prevent flash loan governance attacks
function vote(uint256 proposalId, bool support) external {
    require(
        balanceOf[msg.sender] > 0 &&
        lastTransfer[msg.sender] < proposalSnapshot[proposalId],
        "Must hold tokens before proposal"
    );
    // ... voting logic
}

Token Security

Fee-on-Transfer Token Handling

function depositWithFeeToken(IERC20 token, uint256 amount) external {
    uint256 balanceBefore = token.balanceOf(address(this));
    token.safeTransferFrom(msg.sender, address(this), amount);
    uint256 received = token.balanceOf(address(this)) - balanceBefore;

    // Use 'received' not 'amount'
    deposits[msg.sender] += received;
}

Rebasing Token Handling

contract RebaseAwareVault {
    // Track shares, not amounts
    mapping(address => uint256) public shares;
    uint256 public totalShares;

    function deposit(uint256 amount) external {
        uint256 totalAssets = token.balanceOf(address(this));
        uint256 newShares = totalShares == 0
            ? amount
            : amount * totalShares / totalAssets;

        shares[msg.sender] += newShares;
        totalShares += newShares;

        token.safeTransferFrom(msg.sender, address(this), amount);
    }

    function withdraw(uint256 shareAmount) external {
        uint256 totalAssets = token.balanceOf(address(this));
        uint256 amount = shareAmount * totalAssets / totalShares;

        shares[msg.sender] -= shareAmount;
        totalShares -= shareAmount;

        token.safeTransfer(msg.sender, amount);
    }
}

Composability Risks

Protocol Dependency Risks

Risk Mitigation
Protocol upgrade breaks integration Pin to specific versions
Protocol gets exploited Limit exposure, circuit breakers
Protocol changes fees Account for variable fees
Protocol gets paused Have fallback mechanisms

Circuit Breakers

contract ProtectedVault {
    uint256 public constant MAX_SINGLE_WITHDRAWAL = 1000 ether;
    uint256 public constant DAILY_LIMIT = 10000 ether;
    uint256 public dailyWithdrawn;
    uint256 public lastWithdrawDay;

    function withdraw(uint256 amount) external {
        require(amount <= MAX_SINGLE_WITHDRAWAL, "Amount too large");

        uint256 today = block.timestamp / 1 days;
        if (today > lastWithdrawDay) {
            dailyWithdrawn = 0;
            lastWithdrawDay = today;
        }

        require(dailyWithdrawn + amount <= DAILY_LIMIT, "Daily limit exceeded");
        dailyWithdrawn += amount;

        // ... withdrawal logic
    }
}

Common DeFi Attack Vectors

1. Price Manipulation

Attack: Manipulate oracle price to profit
Mitigation: TWAP oracles, multiple sources, bounds checks

2. Sandwich Attacks

Attack: Front-run and back-run user transactions
Mitigation: Private mempools, slippage protection, commit-reveal

3. Reentrancy in DeFi

Attack: Re-enter during callback to drain funds
Mitigation: CEI pattern, reentrancy guards, checks before external calls

4. Governance Attacks

Attack: Flash loan to pass malicious proposals
Mitigation: Time locks, snapshot voting, quorum requirements

5. Economic Exploits

Attack: Profit from protocol design flaws
Mitigation: Economic modeling, invariant testing, rate limiting


Testing DeFi Protocols

Invariant Testing

function invariant_totalAssetsMatchesDeposits() public {
    assertGe(
        vault.totalAssets(),
        vault.totalDeposited(),
        "Assets must match deposits"
    );
}

function invariant_noFreeMoney() public {
    uint256 before = vault.totalAssets();
    // ... operations
    uint256 after = vault.totalAssets();

    assertLe(
        after,
        before + maxExpectedGain,
        "Unexpected asset increase"
    );
}

Economic Simulation

Test edge cases:

  • Very large deposits/withdrawals
  • Multiple users competing
  • Extreme price movements
  • Flash loan scenarios
  • Partial liquidations

Audit Focus Areas

When reviewing DeFi code, prioritize:

  1. Oracle interactions - Stale data, manipulation
  2. External calls - Reentrancy, return values
  3. Token handling - Non-standard tokens
  4. Access control - Admin functions
  5. Economic logic - Rounding, precision
  6. Upgrade paths - Storage collisions

Next Steps