Dria

Swan
NFTHardhat
21,000 USDC
View results
Submission Details
Severity: high
Invalid

Phase Bypass in Withdrawal Function Compromises Protocol Security Model

Summary

BuyerAgent contract's withdrawal mechanism allows token withdrawals during non-Withdraw phases, bypassing the protocol's core phase-based security model. This enables unauthorized timing of withdrawals that should be strictly phase-locked, potentially disrupting buying operations and compromising fund security.

The withdraw function implements insufficient phase protection by only enforcing a minimum balance check rather than strict phase validation: https://github.com/Cyfrin/2024-10-swan-dria/blob/c8686b199daadcef3161980022e12b66a5304f8e/contracts/swan/BuyerAgent.sol#L262-L277

function withdraw(uint96 _amount) public onlyAuthorized {
(, Phase phase,) = getRoundPhase();
// @Issue - Critical phase protection bypass: allows withdrawals in any phase
// if minimum balance is maintained
if (phase != Phase.Withdraw) {
if (treasury() < minFundAmount() + _amount) {
revert MinFundSubceeded(_amount);
}
}
// @Issue - Unprotected transfer execution regardless of phase
swan.token().transfer(owner(), _amount);
}

Vulnerability Details

In the withdraw() function of BuyerAgent.sol: https://github.com/Cyfrin/2024-10-swan-dria/blob/c8686b199daadcef3161980022e12b66a5304f8e/contracts/swan/BuyerAgent.sol#L262-L277

function withdraw(uint96 _amount) public onlyAuthorized {
(, Phase phase,) = getRoundPhase();
// if we are not in Withdraw phase, we must leave
// at least minFundAmount in the contract
if (phase != Phase.Withdraw) {
if (treasury() < minFundAmount() + _amount) {
revert MinFundSubceeded(_amount);
}
}
// transfer the tokens to the owner
swan.token().transfer(owner(), _amount);
}

The contract allows withdrawals in non-Withdraw phases as long as minFundAmount is maintained in the treasury. This contradicts the intended phase-based restriction pattern.

Proof of Concept:

  1. Contract is in Buy/Sell phase

  2. Treasury has more than minFundAmount + withdrawal amount

  3. Withdrawal succeeds despite not being in Withdraw phase

function withdraw(uint96 _amount) public onlyAuthorized {
(, Phase phase,) = getRoundPhase();
// @Issue - Insufficient phase protection
// The check only enforces minFundAmount but doesn't prevent withdrawals in non-Withdraw phases
// This allows unauthorized timing of withdrawals that should be phase-locked
if (phase != Phase.Withdraw) {
if (treasury() < minFundAmount() + _amount) {
revert MinFundSubceeded(_amount);
}
}
// @Issue - Direct transfer without phase enforcement
// Transfers can proceed in any phase as long as minFundAmount is maintained
swan.token().transfer(owner(), _amount);
}
  1. Phase Violation

  • The protocol's phase system is designed to separate concerns and maintain orderly operations

  • Allowing withdrawals outside the Withdraw phase breaks this fundamental security model

  • Malicious actors could time withdrawals to disrupt buying operations

  1. Economic Impact

  • During Buy phase, funds should be locked for potential purchases

  • Premature withdrawals could leave insufficient funds for planned transactions

  • This creates uncertainty about available capital during critical operations

  1. Protocol Design Violation

  • The three-phase system (Sell, Buy, Withdraw) is core to the protocol's operation

  • This vulnerability effectively nullifies the phase protection for withdrawals

  • It undermines the predictability and reliability of the protocol's state machine

The correct implementation should enforce strict phase-based access control before allowing any withdrawal operations, regardless of the treasury balance.

This is because the withdraw function only enforces a minimum balance check instead of strict phase enforcement. The conditional logic prioritizes fund availability over phase restrictions, allowing withdrawals to occur in any phase if sufficient funds exist.

Impact

  • Breaks phase isolation

  • Allows premature fund withdrawals

  • Could interfere with buying operations

Recommendations

function withdraw(uint96 _amount) public onlyAuthorized {
(, Phase phase,) = getRoundPhase();
+ // Enforce strict phase-based access control
+ if (phase != Phase.Withdraw) {
+ revert InvalidPhase(phase, Phase.Withdraw);
+ }
+
+ // Optional: Add additional balance checks after phase validation
+ if (treasury() < _amount) {
+ revert InsufficientBalance(_amount);
+ }
swan.token().transfer(owner(), _amount);
}

We enforces strict phase-based access control, maintaining the protocol's security model and preventing unauthorized withdrawals during critical operational phases.

Updates

Lead Judging Commences

inallhonesty Lead Judge
12 months ago
inallhonesty Lead Judge 12 months ago
Submission Judgement Published
Invalidated
Reason: Design choice

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.