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
about 1 year ago
inallhonesty Lead Judge about 1 year ago
Submission Judgement Published
Invalidated
Reason: Design choice

Support

FAQs

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

Give us feedback!