Security

Vestra DAO $500K Exploit: A Simple Breakdown

Vestra DAO $500K Exploit: A Simple Breakdown

Smart contracts are the backbone of decentralized finance (DeFi), but their design flaws can lead to catastrophic losses. In this case, Vestra DAO suffered a $500,000 exploit due to vulnerabilities in its staking contract.

Vestra DAO Exploit Visualization

What Happened?

Vestra DAO’s staking contract allowed users to lock tokens and earn rewards. However, a vulnerability in the unStake() function enabled malicious users to repeatedly withdraw funds beyond their actual stake. This drained the contract’s funds, leading to the exploit.

Here’s the vulnerable part of the code:

function unStake(uint8 maturity) external nonReentrant onlyMaturity(maturity) {
    address account = _msgSender();
    Stake storage user = stakes[account][maturity];
    require(
        user.stakeAmount > 0,
        "STAKE:LOCK:You have no active token lock staking."
    );
    uint64 currentTime = uint64(block.timestamp);
    MaturityData storage data = maturities[maturity];
    require(
        currentTime >= user.startTime + data.unlockTime,
        "STAKE:LOCK:You cannot leave before your time is up."
    );
    uint256 stakeAmount = user.stakeAmount;
    uint256 totalAmount = stakeAmount + user.yield;
    uint256 penaltyAmount = _penaltyCalculate(totalAmount, currentTime, user.endTime + data.lateUnStakeFee);
    if (penaltyAmount > 0) {
        ITokenBurn(token).burn(penaltyAmount);
        totalAmount -= penaltyAmount;
    }
    user.penalty = penaltyAmount;
    user.isActive = false;
    data.totalStaked -= stakeAmount;
    data.countUser--;
    data.totalPenalty += penaltyAmount;
    IERC20(token).safeTransfer(account, totalAmount);
    emit Unstake(account, stakeAmount, maturity, user.yield, user.startTime, penaltyAmount);
}

The Vulnerability

  • No Check for Repeated Claims: The contract did not properly clear the user’s staking data (user.stakeAmount, user.yield, etc.) after a successful unstake. This allowed users to call unStake() multiple times and withdraw the same funds repeatedly.
  • Staking Position Not Validated: The unStake() function failed to validate whether a staking position was active before processing the unstake request.

How Could This Exploit Have Been Prevented?

  1. Ensure the Staking Position is Active: Add a validation at the start of the unStake() function to check if the staking position is still active before proceeding.
require(user.isActive, "STAKE:LOCK:No active stake to withdraw.");
  1. Clear User Data After Successful Unstake: Immediately clear the staking data after a successful withdrawal to prevent repeated claims.
// Clear user's staking data after unstake
delete stakes[account][maturity];
  1. Use a Defensive Programming Pattern: Double-check all state transitions to ensure they happen correctly, particularly in sensitive functions like unStake().

Corrected Code

Here’s how the unStake() function should look after implementing the fixes:

function unStake(uint8 maturity) external nonReentrant onlyMaturity(maturity) {
    // Get the address of the caller of the function
    address account = _msgSender();
    
    // Retrieve the stake data for the caller based on maturity
    Stake storage user = stakes[account][maturity];
    // Ensure the user has an active stake to withdraw
    require(
        user.isActive,
        "STAKE:LOCK:No active stake to withdraw."
    );
    // Current timestamp
    uint64 currentTime = uint64(block.timestamp);
    // Retrieve maturity data for the specified maturity period
    MaturityData storage data = maturities[maturity];
    // Check if the user is trying to unstake before the unlock time
    require(
        currentTime >= user.startTime + data.unlockTime,
        "STAKE:LOCK:You cannot leave before your time is up."
    );
    // Safeguard: Ensure the stakeAmount is valid (added as a precaution against overflow)
    uint256 stakeAmount = user.stakeAmount;
    require(stakeAmount > 0, "STAKE:LOCK:Invalid stake amount.");
    // Calculate the total amount including yield
    uint256 totalAmount = stakeAmount + user.yield;
    // Calculate penalty for early or late unstake
    uint256 penaltyAmount = _penaltyCalculate(totalAmount, currentTime, user.endTime + data.lateUnStakeFee);
    if (penaltyAmount > 0) {
        // Burn the penalty amount from the total
        ITokenBurn(token).burn(penaltyAmount);
        totalAmount -= penaltyAmount; // Deduct penalty from total amount
    }
    // Clear user's stake data to prevent re-entrancy or double withdrawal
    delete stakes[account][maturity];
    // Update maturity data to reflect the unstake operation
    data.totalStaked -= stakeAmount; // Reduce the total staked amount
    data.countUser--;                // Decrement the user count for this maturity period
    data.totalPenalty += penaltyAmount; // Accumulate the penalty to track total penalties collected
    // Safely transfer the remaining total amount to the user
    IERC20(token).safeTransfer(account, totalAmount);
    // Emit an event to log the unstake action with details
    emit Unstake(account, stakeAmount, maturity, user.yield, user.startTime, penaltyAmount);
}

Lessons Learned

  1. Thorough Testing: Smart contracts must be rigorously tested for edge cases, including reentrancy, repeated calls, and improper state updates.
  2. Code Reviews and Audits: Third-party audits by experienced blockchain security firms can help identify vulnerabilities before deployment.
  3. Fail-Safe Patterns: Always use best practices, such as clearing sensitive state variables and validating conditions in all functions.

Exploit Details

  1. Vulnerable contract
  2. Attacker Address
  3. Attack Transactions: 0x213991ca, 0xa0dcf9b

The Vestra DAO exploit highlights the importance of secure coding practices in smart contracts. Such exploits can be avoided by incorporating proper checks and clearing user data after sensitive operations.

Security is non-negotiable.