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.

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 callunStake()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?
- 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.");
- 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];
- 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
- Thorough Testing: Smart contracts must be rigorously tested for edge cases, including reentrancy, repeated calls, and improper state updates.
- Code Reviews and Audits: Third-party audits by experienced blockchain security firms can help identify vulnerabilities before deployment.
- Fail-Safe Patterns: Always use best practices, such as clearing sensitive state variables and validating conditions in all functions.
Exploit Details
- Vulnerable contract
- Attacker Address
- 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.