Kuiper contest - WatchPug's results

Automated portfolio protocol.

General Information

Platform: Code4rena

Start Date: 16/09/2021

Pot Size: $50,000 USDC

Total HM: 26

Participants: 30

Period: 7 days

Judge: GalloDaSballo

Total Solo HM: 17

Id: 36

League: ETH

Kuiper

Findings Distribution

Researcher Performance

Rank: 1/30

Findings: 6

Award: $7,266.85

🌟 Selected for report: 9

🚀 Solo Findings: 4

Findings Information

🌟 Selected for report: WatchPug

Labels

bug
3 (High Risk)
sponsor confirmed

Awards

3339.7729 USDC - $3,339.77

External Links

Handle

WatchPug

Vulnerability details

https://github.com/code-423n4/2021-09-defiProtocol/blob/main/contracts/contracts/Basket.sol#L102-L108

Given the auctionBurn() function will _burn() the auction bond without updating the ibRatio. Once the bond of a failed auction is burned, the proportional underlying tokens won't be able to be withdrawn, in other words, being frozen in the contract.

Proof of Concept

With the configuration of:

basket.ibRatio = 1e18 factory.bondPercentDiv = 400 basket.totalSupply = 400 basket.tokens = [BTC, ETH] basket.weights = [1, 1]

  1. Create an auction;
  2. Bond with 1 BASKET TOKEN;
  3. Wait for 24 hrs and call auctionBurn();

basket.ibRatio remains to be 1e18; basket.totalSupply = 399.

Burn 1 BASKET TOKEN will only get back 1 BTC and 1 ETH, which means, there are 1 BTC and 1 ETH frozen in the contract.

Change to:

function auctionBurn(uint256 amount) onlyAuction external override {
    handleFees();
    uint256 startSupply = totalSupply();
    _burn(msg.sender, amount);

    uint256 newIbRatio = ibRatio * startSupply / (startSupply - amount);
    ibRatio = newIbRatio;

    emit NewIBRatio(newIbRatio);
    emit Burned(msg.sender, amount);
}

#0 - GalloDaSballo

2021-12-19T00:10:38Z

The warden has identified a way for funds to be stuck without a way to recoup them, this is because ibRatio is not updated, while totalSupply is.

Because this is a specific accounting error, which is effectively a bug in the logic of the protocol, and funds can be irrevocably lost, this is a high severity finding

Findings Information

🌟 Selected for report: WatchPug

Labels

bug
2 (Med Risk)
sponsor confirmed

Awards

1001.9319 USDC - $1,001.93

External Links

Handle

WatchPug

Vulnerability details

https://github.com/code-423n4/2021-09-defiProtocol/blob/main/contracts/contracts/Basket.sol#L110-L129

function handleFees() private {
    if (lastFee == 0) {
        lastFee = block.timestamp;
    } else {
        uint256 startSupply = totalSupply();

        uint256 timeDiff = (block.timestamp - lastFee);
        uint256 feePct = timeDiff * licenseFee / ONE_YEAR;
        uint256 fee = startSupply * feePct / (BASE - feePct);

        _mint(publisher, fee * (BASE - factory.ownerSplit()) / BASE);
        _mint(Ownable(address(factory)).owner(), fee * factory.ownerSplit() / BASE);
        lastFee = block.timestamp;

        uint256 newIbRatio = ibRatio * startSupply / totalSupply();
        ibRatio = newIbRatio;

        emit NewIBRatio(ibRatio);
    }
}

timeDiff * licenseFee can be greater than ONE_YEAR when timeDiff and/or licenseFee is large enough, which makes feePct to be greater than BASE so that BASE - feePct will revert on underflow.

Impact

Minting and burning of the basket token are being disrupted until the publisher update the licenseFee.

Proof of Concept

  1. Create a basket with a licenseFee of 1e19 or 1000% per year and mint 1 basket token;
  2. The basket remain inactive (not being minted or burned) for 2 months;
  3. Calling mint and burn reverts at handleFees().

Limit the max value of feePct.

#0 - GalloDaSballo

2021-12-19T00:18:43Z

The finding is valid, there are conditions that would cause feePct to be greater than BASE

The conditions to trigger this seem to be:

  • Wait enough time
  • Have a high enough fee

Because this can happen under specific conditions, I will grade this finding as medium severity:

I would highly recommend the sponsor to consider the possibility of capping the licenseFee to make it easier to predict cases in which the operation can revert

Findings Information

🌟 Selected for report: WatchPug

Labels

bug
2 (Med Risk)
sponsor confirmed

Awards

1001.9319 USDC - $1,001.93

External Links

Handle

WatchPug

Vulnerability details

The newRatio that determines tokensNeeded to settle the auction is calculated based on auctionMultiplier, bondTimestamp - auctionStart and auctionDecrement.

uint256 a = factory.auctionMultiplier() * basket.ibRatio();
uint256 b = (bondTimestamp - auctionStart) * BASE / factory.auctionDecrement();
uint256 newRatio = a - b;

However, if an auction is bonded late (bondTimestamp - auctionStart is a large number), and/or the auctionMultiplier is small enough, and/or the auctionDecrement is small enough, that makes b to be greater than a, so that uint256 newRatio = a - b; will revert on underflow.

This might seem to be an edge case issue, but considering that a rebalance auction of a bag of shitcoin to high-value tokens might just end up being bonded at the last minute, with a newRatio near zero. When we take the time between the bonder submits the transaction and it got packed into a block, it's quite possible that the final bondTimestamp gets large enough to revet a - b.

Impact

An auction successfully bonded by a regular user won't be able to be settled, and the user will lose the bond.

Proof of Concept

With the configuration of:

basket.ibRatio = 1e18 factory.auctionDecrement = 5760 (Blocks per day) factory.auctionMultiplier = 2

  1. Create an auction;
  2. The auction remain inactive (not get bonded) for more than 2 days (>11,520 blocks);
  3. Call bondForRebalance() and it will succeed;
  4. Calling settleAuction() will always revert.

Calculate and require newRatio > 0 in bondForRebalance(), or limit the max value of decrement and make sure newRatio always > 0 in settleAuction().

#0 - GalloDaSballo

2021-12-19T00:22:29Z

Finding is valid, there are cases that can cause a - b to revert

The finding is reliant on external condition (multiplier being low and / or bonding late) as such I believe this to be a medium severity finding.

Note for the sponsor: The cause for reverts should get clearer with time (as people use the protocol), you definitely want to model these revert behaviour to avoid edge cases that will make funds stuck

Findings Information

🌟 Selected for report: WatchPug

Labels

bug
2 (Med Risk)
sponsor confirmed

Awards

1001.9319 USDC - $1,001.93

External Links

Handle

WatchPug

Vulnerability details

https://github.com/code-423n4/2021-09-defiProtocol/blob/main/contracts/contracts/Auction.sol#L143

function withdrawBounty(uint256[] memory bountyIds) internal {
    // withdraw bounties
    for (uint256 i = 0; i < bountyIds.length; i++) {
        Bounty memory bounty = _bounties[bountyIds[i]];
        require(bounty.active);

        IERC20(bounty.token).transfer(msg.sender, bounty.amount);
        bounty.active = false;

        emit BountyClaimed(msg.sender, bounty.token, bounty.amount, bountyIds[i]);
    }
}

In the withdrawBounty function, bounty.active should be set to false when the bounty is claimed.

However, since bounty is stored in memory, the state update will not succeed.

Impact

An auction successfully bonded by a regular user won't be able to be settled if they passed seemly active bountyIds, and the bonder will lose the bond.

Proof of Concept

  1. Create an auction;
  2. Add a bounty;
  3. Auction settled with bounty claimed;
  4. Create a new auction;
  5. Add a new bounty;
  6. Calling settleAuction() with the bountyIds of the 2 seemly active bounties always reverts.

Change to:

Bounty storage bounty = _bounties[bountyIds[i]];

#0 - frank-beard

2021-10-06T16:29:04Z

#1 - GalloDaSballo

2021-12-19T15:48:08Z

Finding is valid, because the warden didn't provide a POC of how to steal user funds, the finding is of medium severity

AuditHub

A portfolio for auditors, a security profile for protocols, a hub for web3 security.

Built bymalatrax © 2024

Auditors

Browse

Contests

Browse

Get in touch

ContactTwitter