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 {
(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++) {
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) {
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.
function purchase() external onlyAuthorized {
(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++) {
address asset = assets[i];
uint256 price = swan.getListingPrice(asset);
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
Contract can become unable to pay for oracle services
Future oracle requests will fail
Contract gets stuck in current phase
User funds get locked due to inability to progress through phases
Let's say:
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);
}