Dria

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

Race Condition in Oracle Request Processing Enables Double Execution

Summary

Race condition in BuyerAgent.sol allows multiple transactions to process the same oracle request, leading to duplicate purchases and state updates.

// @audit-info State mappings expose race condition
mapping(uint256 round => uint256 taskId) public oraclePurchaseRequests;
mapping(uint256 round => uint256 taskId) public oracleStateRequests;
mapping(uint256 taskId => bool isProcessed) public isOracleRequestProcessed;
// @audit-info Non-atomic processing check
function purchase() external onlyAuthorized {
uint256 taskId = oraclePurchaseRequests[round];
if (isOracleRequestProcessed[taskId]) {
revert TaskAlreadyProcessed();
}
// Purchase execution
isOracleRequestProcessed[taskId] = true;
}

Vulnerability Details

The issue occurs in the following scenario:

  1. A purchase/state update oracle request is made for a specific round

  2. The request is processed once successfully

  3. The same request can potentially be processed again in the same round

While the contract has the isOracleRequestProcessed mapping to prevent double processing:

contracts/swan/BuyerAgent.sol#L97: https://github.com/Cyfrin/2024-10-swan-dria/blob/c8686b199daadcef3161980022e12b66a5304f8e/contracts/swan/BuyerAgent.sol#L97

mapping(uint256 taskId => bool isProcessed) public isOracleRequestProcessed;

The validation in purchase() and updateState() functions checks this flag: https://github.com/Cyfrin/2024-10-swan-dria/blob/c8686b199daadcef3161980022e12b66a5304f8e/contracts/swan/BuyerAgent.sol#L206-L208

https://github.com/Cyfrin/2024-10-swan-dria/blob/c8686b199daadcef3161980022e12b66a5304f8e/contracts/swan/BuyerAgent.sol#L228-L230

if (isOracleRequestProcessed[taskId]) {
revert TaskAlreadyProcessed();
}

However, there's a race condition where

  1. Multiple transactions could read isOracleRequestProcessed[taskId] as false

  2. Process the same task

  3. Before the mapping is updated to true

This could lead to:

  • Double execution of purchases

  • Duplicate state updates

  • Exceeding round spending limits

mapping contracts/swan/BuyerAgent.sol#https://github.com/Cyfrin/2024-10-swan-dria/blob/c8686b199daadcef3161980022e12b66a5304f8e/contracts/swan/BuyerAgent.sol#L89-L97

// @audit-info Oracle request state tracking lacks atomic guarantees
mapping(uint256 round => uint256 taskId) public oraclePurchaseRequests;
mapping(uint256 round => uint256 taskId) public oracleStateRequests;
mapping(uint256 taskId => bool isProcessed) public isOracleRequestProcessed;

The updateState contracts/swan/BuyerAgent.sol# https://github.com/Cyfrin/2024-10-swan-dria/blob/c8686b199daadcef3161980022e12b66a5304f8e/contracts/swan/BuyerAgent.sol#L200-L216

function updateState() external onlyAuthorized {
(uint256 round,) = _checkRoundPhase(Phase.Withdraw);
uint256 taskId = oracleStateRequests[round];
// @audit-info Race condition: Multiple txs can read isProcessed as false
if (isOracleRequestProcessed[taskId]) {
revert TaskAlreadyProcessed();
}
bytes memory newState = oracleResult(taskId);
state = newState;
// @audit-info Non-atomic state update allows double processing
isOracleRequestProcessed[taskId] = true;
}

The function purchase contracts/swan/BuyerAgent.sol# https://github.com/Cyfrin/2024-10-swan-dria/blob/c8686b199daadcef3161980022e12b66a5304f8e/contracts/swan/BuyerAgent.sol#L222-L255

function purchase() external onlyAuthorized {
(uint256 round,) = _checkRoundPhase(Phase.Buy);
uint256 taskId = oraclePurchaseRequests[round];
// @audit-info Same race condition vulnerability as updateState()
if (isOracleRequestProcessed[taskId]) {
revert TaskAlreadyProcessed();
}
// Process purchases...
// @audit-info Non-atomic completion flag update
isOracleRequestProcessed[taskId] = true;
}

The vulnerabilities create a race condition in oracle request processing. The non-atomic nature of the task completion checks and updates allows multiple transactions to process the same oracle request before the isProcessed flag is set.

Root Cause:

  • Non-atomic task processing allows multiple transactions to execute the same oracle request

  • Lack of synchronization between task completion check and state updates

  • Missing reentrancy protection on critical state changes

When we look at this example.

1. User A calls purchase() for taskId=1
2. User B front-runs with same taskId=1
3. Both read isOracleRequestProcessed[1] = false
4. Both execute purchase logic
5. Both update isOracleRequestProcessed[1] = true
6. Result: Double purchase executed

Impact

  1. Double execution of purchase orders leading to excessive spending

  2. Multiple state updates from single oracle responses

  3. Potential violation of round spending limits

Tools Used

Vs

Recommendations

Implement atomic task processing using

  1. Add a mutex lock during task processing

  2. Use OpenZeppelin's ReentrancyGuard

  3. Implement atomic task completion updates

Updates

Lead Judging Commences

inallhonesty Lead Judge
12 months ago
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.