Dria

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

BuyerAgent Purchase Function Can Lock Protocol Due to Insufficient Oracle Fee Reserves

Summary

The BuyerAgent's purchase function fails to reserve funds for mandatory oracle fees, allowing spending that can trap the contract in its current phase.

function purchase() external onlyAuthorized {
// @audit-info No validation of minimum required balance for oracle operations
(uint256 round,) = _checkRoundPhase(Phase.Buy);
bytes memory output = oracleResult(taskId);
address[] memory assets = abi.decode(output, (address[]));
for (uint256 i = 0; i < assets.length; i++) {
// @audit-info Spending limit check doesn't account for oracle fees
spendings[round] += price;
if (spendings[round] > amountPerRound) {
revert BuyLimitExceeded(spendings[round], amountPerRound);
}

Connected to: https://github.com/Cyfrin/2024-10-swan-dria/blob/c8686b199daadcef3161980022e12b66a5304f8e/contracts/swan/BuyerAgent.sol#L152-L154

function minFundAmount() public view returns (uint256) {
// @audit-info This shows required minimum but isn't enforced in purchase
return amountPerRound + swan.getOracleFee();
}

Vulnerability Details

The spending check compares against amountPerRound, minFundAmount() includes both amountPerRound AND oracle fees. This means the contract could spend more than it has available for oracle fees.

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

function purchase() external onlyAuthorized {
(uint256 round,) = _checkRoundPhase(Phase.Buy);
// @audit-info No check if contract has enough funds before starting purchases
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);
// @audit-info Incorrect spending limit check - doesn't account for oracle fees
spendings[round] += price;
if (spendings[round] > amountPerRound) {
revert BuyLimitExceeded(spendings[round], amountPerRound);
}

The spending limit check only considers amountPerRound but ignores required oracle fees. The contract can spend up to amountPerRound on purchases, leaving insufficient funds for oracle operations.

Impact

  1. Contract can become unable to pay for oracle services

  2. Future oracle requests will fail

  3. Contract gets stuck in current phase

  4. User funds get locked due to inability to progress through phases

Let's say:

// 1. The BuyerAgent is deployed with:
// amountPerRound = 1000 ETH
// oracleFee = 50 ETH
// 2. Fund contract with 1000 ETH
// 3. Execute purchases totaling 960 ETH
// Contract state:
// - Treasury: 40 ETH
// - Required oracle fee: 50 ETH
// - Result: Contract stuck, cannot pay for oracle
// 4. Contract becomes permanently locked in Buy phase
// Unable to transition to Withdraw phase

This vulnerability directly impacts protocol functionality and user funds by allowing the contract to enter an unrecoverable state where it cannot progress through phases due to insufficient oracle fee reserves.

Tools Used

Vs

Recommendations

function purchase() external onlyAuthorized {
(uint256 round,) = _checkRoundPhase(Phase.Buy);
+ uint256 oracleFee = swan.getOracleFee();
+ require(treasury() >= oracleFee, "Insufficient oracle fee reserve");
bytes memory output = oracleResult(taskId);
address[] memory assets = abi.decode(output, (address[]));
for (uint256 i = 0; i < assets.length; i++) {
uint256 price = swan.getListingPrice(asset);
spendings[round] += price;
- if (spendings[round] > amountPerRound) {
+ if (spendings[round] > amountPerRound - oracleFee) {
revert BuyLimitExceeded(spendings[round], amountPerRound);
}
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.