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 {
(uint256 round,) = _checkRoundPhase(Phase.Buy);
uint256 taskId = oraclePurchaseRequests[round];
bytes memory output = oracleResult(taskId);
address[] memory assets = abi.decode(output, (address[]));
for (uint256 i = 0; i < assets.length; i++) {
address asset = assets[i];
swan.purchase(asset);
}
}
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 {
(uint256 round,) = _checkRoundPhase(Phase.Buy);
uint256 taskId = oraclePurchaseRequests[round];
if (isOracleRequestProcessed[taskId]) {
revert TaskAlreadyProcessed();
}
bytes memory output = oracleResult(taskId);
address[] memory assets = abi.decode(output, (address[]));
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:
Phase state can change between validation and execution
Multiple assets are purchased in a loop without re-checking phase state
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:
Contract is in Buy phase
purchase() is called and passes phase check
Block timestamp advances, changing phase to Withdraw
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);
}
}