Dria

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

Phase Desynchronization in Purchase Execution Leads to Market Rule Violations

Summary

BuyerAgent's purchase mechanism lacks atomic phase validation, allowing purchases to execute across phase boundaries. This creates a high vulnerability where market operations intended for the Buy phase can execute during other phases, violating core protocol rules and potentially disrupting market dynamics.

function purchase() external onlyAuthorized {
// @Issue - Initial phase check without maintaining atomicity through execution
(uint256 round,) = _checkRoundPhase(Phase.Buy);
// @Issue - Long gap between check and execution with oracle operations
uint256 taskId = oraclePurchaseRequests[round];
bytes memory output = oracleResult(taskId);
address[] memory assets = abi.decode(output, (address[]));
// @Issue - Multiple purchases execute without phase re-validation
for (uint256 i = 0; i < assets.length; i++) {
address asset = assets[i];
swan.purchase(asset); // State-changing operation in potentially wrong phase
}
}

Vulnerability Details

While the contract has phase protection through _checkRoundPhase(), there's a potential race condition where the phase could change between phase check and execution: https://github.com/Cyfrin/2024-10-swan-dria/blob/c8686b199daadcef3161980022e12b66a5304f8e/contracts/swan/BuyerAgent.sol#L222-L256

function purchase() external onlyAuthorized {
// @Issue - Phase check happens here but state can change before actual purchase
(uint256 round,) = _checkRoundPhase(Phase.Buy);
// @Issue - Gap between phase check and purchase execution
uint256 taskId = oraclePurchaseRequests[round];
if (isOracleRequestProcessed[taskId]) {
revert TaskAlreadyProcessed();
}
bytes memory output = oracleResult(taskId);
address[] memory assets = abi.decode(output, (address[]));
// @Issue - Purchase execution happens here without re-validating phase
for (uint256 i = 0; i < assets.length; i++) {
address asset = assets[i];
uint256 price = swan.getListingPrice(asset);
spendings[round] += price;
if (spendings[round] > amountPerRound) {
revert BuyLimitExceeded(spendings[round], amountPerRound);
}
inventory[round].push(asset);
swan.purchase(asset);
}
isOracleRequestProcessed[taskId] = true;
}

This creates a critical timing issue in the protocol's phase management system. The core problem is the temporal gap between phase validation and purchase execution.

The danger comes from:

  1. Phase state can change between validation and execution

  2. Multiple assets are purchased in a loop without re-checking phase state

  3. Oracle result processing adds significant execution time between check and purchase

This puts the protocol at risk because:

  • Assets could be purchased in incorrect market phases

  • Price calculations may be invalid for the new phase

  • Market dynamics intended for the Buy phase could be violated

  • Protocol's phase-based economic model could be undermined

The impact is magnified when purchasing multiple assets, as each iteration increases the time window where the phase could have changed, potentially leading to larger-scale phase violation incidents.

This represents a fundamental threat to the protocol's phase-based security model and market timing mechanisms.

Proof of Concept:

  1. Contract is in Buy phase

  2. purchase() is called and passes phase check

  3. Block timestamp advances, changing phase to Withdraw

  4. Purchase execution continues in wrong phase

Impact

Market rules violation through cross-phase purchases

Recommendations

This solution maintains phase integrity throughout the entire purchase execution.

function purchase() external onlyAuthorized {
(uint256 round, Phase initialPhase) = _checkRoundPhase(Phase.Buy);
uint256 taskId = oraclePurchaseRequests[round];
bytes memory output = oracleResult(taskId);
address[] memory assets = abi.decode(output, (address[]));
+ // Validate phase hasn't changed
+ (uint256 currentRound, Phase currentPhase,) = getRoundPhase();
+ if (currentPhase != Phase.Buy || currentRound != round) {
+ revert InvalidPhase(currentPhase, Phase.Buy);
+ }
for (uint256 i = 0; i < assets.length; i++) {
+ // Re-validate phase for each purchase
+ (currentRound, currentPhase,) = getRoundPhase();
+ if (currentPhase != Phase.Buy || currentRound != round) {
+ revert InvalidPhase(currentPhase, Phase.Buy);
+ }
address asset = assets[i];
swan.purchase(asset);
}
}

Or we can implement atomic phase validation throughout the purchase execution to maintain market phase integrity and prevent cross-phase operations.

function purchase() external onlyAuthorized {
(uint256 round, Phase initialPhase) = _checkRoundPhase(Phase.Buy);
uint256 taskId = oraclePurchaseRequests[round];
bytes memory output = oracleResult(taskId);
address[] memory assets = abi.decode(output, (address[]));
+ // Add phase validation checkpoint
+ function validatePhase(uint256 expectedRound) internal view {
+ (uint256 currentRound, Phase currentPhase,) = getRoundPhase();
+ if (currentPhase != Phase.Buy || currentRound != expectedRound) {
+ revert InvalidPhase(currentPhase, Phase.Buy);
+ }
+ }
for (uint256 i = 0; i < assets.length; i++) {
+ validatePhase(round);
address asset = assets[i];
swan.purchase(asset);
}
}
Updates

Lead Judging Commences

inallhonesty Lead Judge 12 months ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

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