Dria

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

Unauthorized withdrawals during buy phase can break purchase functionality

Summary

The BuyerAgent contract allows withdrawals during the Buy phase as long as the minimum fund amount is maintained. This can lead to a situation where the contract has insufficient funds to execute purchases, potentially breaking its core functionality.

Vulnerability Details

The vulnerability exists in the withdraw function of the BuyerAgent contract:
https://github.com/Cyfrin/2024-10-swan-dria/blob/c8686b199daadcef3161980022e12b66a5304f8e/contracts/swan/BuyerAgent.sol#L267-L272

// File: contracts/swan/BuyerAgent.sol#L262-L277
function withdraw(uint96 _amount) public onlyAuthorized {
(, Phase phase,) = getRoundPhase();
// if we are not in Withdraw phase, we must leave
// at least minFundAmount in the contract
if (phase != Phase.Withdraw) {
// instead of checking `treasury - _amount < minFoundAmount`
// we check this way to prevent underflows
if (treasury() < minFundAmount() + _amount) {
revert MinFundSubceeded(_amount);
}
}
// transfer the tokens to the owner of Buyer
swan.token().transfer(owner(), _amount);
}

The function only checks that minFundAmount remains in the contract after withdrawal, without considering whether the remaining funds are sufficient for pending or future purchases during the Buy phase.

Impact

This issue allows authorized users to withdraw funds during the Buy phase, potentially leaving the contract with insufficient funds to execute purchases. This breaks the core functionality of the BuyerAgent contract.

POC Test:

describe("POC: Withdraw during Buy phase can break purchase", function () {
const ASSET_PRICE = parseEther("0.05");
beforeEach(async function () {
token = await deployTokenFixture(dria, SUPPLY);
MARKET_PARAMETERS.timestamp = (await ethers.provider
.getBlock("latest")
.then((block) => block?.timestamp)) as bigint;
({ swan } = await deploySwanFixture(dria, token, STAKES, FEES, MARKET_PARAMETERS, ORACLE_PARAMETERS));
await transferTokens(token, [[buyerAgentOwner.address, parseEther("1")]]);
buyerAgent = await loadFixture(
buyerAgentDeployer(swan, {
name: "Name of the agent",
description: "Description of the agent",
royaltyFee: ROYALTY_FEE,
amountPerRound: AMOUNT_PER_ROUND,
owner: buyerAgentOwner,
})
);
const minFundAmount = await buyerAgent.minFundAmount();
const requiredAmount = ASSET_PRICE + minFundAmount;
await token.connect(buyerAgentOwner).transfer(await buyerAgent.getAddress(), requiredAmount);
});
it("should demonstrate the vulnerability in withdraw during Buy phase", async function () {
await time.increase(MARKET_PARAMETERS.sellInterval);
const [, phase] = await buyerAgent.getRoundPhase();
expect(phase).to.equal(Phase.Buy);
const initialBalance = await buyerAgent.treasury();
const minFundAmount = await buyerAgent.minFundAmount();
const withdrawAmount = initialBalance - minFundAmount;
await buyerAgent.connect(buyerAgentOwner).withdraw(withdrawAmount);
expect(await buyerAgent.treasury()).to.equal(minFundAmount);
expect(await buyerAgent.treasury()).to.be.lessThan(ASSET_PRICE + minFundAmount);
});
});

Test Result:

BuyerAgent
POC: Withdraw during Buy phase can break purchase
✔ should demonstrate the vulnerability in withdraw during Buy phase

Tools Used

Manual code review
Hardhat testing framework
Typescript/Solidity

Recommendations

Restrict withdrawals to only the Withdraw phase:

function withdraw(uint96 _amount) public onlyAuthorized {
(, Phase phase,) = getRoundPhase();
if (phase != Phase.Withdraw) {
revert InvalidPhase(phase, Phase.Withdraw);
}
swan.token().transfer(owner(), _amount);
}

Alternative: Implement a lock mechanism during the Buy phase when a purchase request is pending, ensuring sufficient funds remain for the purchase.

Alternative: Add a check to ensure that sufficient funds remain for at least one purchase during the Buy phase:

if (phase == Phase.Buy && treasury() - _amount < minFundAmount() + amountPerRound) {
revert InsufficientFundsForPurchase();
}
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.