Kuiper contest - WatchPug's results

Automated portfolio protocol.

General Information

Platform: Code4rena

Start Date: 08/12/2021

Pot Size: $30,000 ETH

Total HM: 12

Participants: 26

Period: 3 days

Judge: leastwood

Total Solo HM: 9

Id: 65

League: ETH

Kuiper

Findings Distribution

Researcher Performance

Rank: 3/26

Findings: 4

Award: $4,170.46

🌟 Selected for report: 16

🚀 Solo Findings: 1

Findings Information

🌟 Selected for report: TomFrenchBlockchain

Also found by: WatchPug

Labels

bug
duplicate
2 (Med Risk)
sponsor acknowledged

Awards

690.8489 USDC - $690.85

External Links

Handle

WatchPug

Vulnerability details

According to the newRatio formula in settleAuction(), the maximum value of newRatio is factory.auctionMultiplier() * basket.ibRatio().

However, since there is no validation for the value of minIbRatio when setting it, if the publisher publishes a newIndex with minIbRatio larger than factory.auctionMultiplier() * basket.ibRatio(), settleAuction() will always fail.

https://github.com/code-423n4/2021-12-defiprotocol/blob/205d3766044171e325df6a8bf2e79b37856eece1/contracts/contracts/Auction.sol#L97-L99

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

https://github.com/code-423n4/2021-12-defiprotocol/blob/205d3766044171e325df6a8bf2e79b37856eece1/contracts/contracts/Basket.sol#L216-L244

function publishNewIndex(address[] memory _tokens, uint256[] memory _weights, uint256 _minIbRatio) onlyPublisher public override {
    validateWeights(_tokens, _weights);

    if (pendingWeights.pending) {
        require(block.timestamp >= pendingWeights.timestamp + TIMELOCK_DURATION);
        if (auction.auctionOngoing() == false) {
            auction.startAuction();

            emit PublishedNewIndex(publisher);
        } else if (auction.hasBonded()) {

        } else {
            auction.killAuction();

            pendingWeights.tokens = _tokens;
            pendingWeights.weights = _weights;
            pendingWeights.timestamp = block.timestamp;
            pendingWeights.minIbRatio = _minIbRatio;
        }
    } else {
        pendingWeights.pending = true;
        pendingWeights.tokens = _tokens;
        pendingWeights.weights = _weights;
        pendingWeights.timestamp = block.timestamp;
        pendingWeights.minIbRatio = _minIbRatio;

        emit NewIndexSubmitted();
    }
}

PoC

  • bondPercentDiv = 400
  • basketToken.totalSupply = 40,000
  • factory.auctionMultiplier = 2
  • factory.auctionDecrement = 10,000
  • basket.ibRatio = 1e18
  1. The publisher called publishNewIndex() with _minIbRatio = 2e18
  2. Alice call bondForRebalance() just after 1 block, paid 100 basketToken
  3. Alice tries to settleAuction(), it will always fail because newRatio < minIbRatio
  • a = 2 * 1e18
  • b = 0.0001 * 1e18
  • newRatio = 1.9999 * 1e18;
  1. Anyone can call bondBurn() after 24 hrs, and Alice's 100 basketToken will be burned.

Recommendation

Change to:

function publishNewIndex(address[] memory _tokens, uint256[] memory _weights, uint256 _minIbRatio) onlyPublisher public override {
        validateWeights(_tokens, _weights);
        require(_minIbRatio < factory.auctionMultiplier() * ibRatio, "!_minIbRatio");

        if (pendingWeights.pending) {
            require(block.timestamp >= pendingWeights.timestamp + TIMELOCK_DURATION);
            if (auction.auctionOngoing() == false) {
                auction.startAuction();

                emit PublishedNewIndex(publisher);
            } else if (auction.hasBonded()) {

            } else {
                auction.killAuction();

                pendingWeights.tokens = _tokens;
                pendingWeights.weights = _weights;
                pendingWeights.timestamp = block.timestamp;
                pendingWeights.minIbRatio = _minIbRatio;
            }
        } else {
            pendingWeights.pending = true;
            pendingWeights.tokens = _tokens;
            pendingWeights.weights = _weights;
            pendingWeights.timestamp = block.timestamp;
            pendingWeights.minIbRatio = _minIbRatio;

            emit NewIndexSubmitted();
        }
    }

#0 - frank-beard

2022-02-22T19:20:27Z

while this could technically happen, it should be up to the auction rebalancer to make sure they can actually settle the auction whether that's how much capital is required, and possible issues with the new weights. i would consider this a low risk issue.

#1 - 0xleastwood

2022-03-27T02:21:37Z

I agree, I think it is expected that the auction rebalancer will check minIbRatio before calling bondForRebalance(). However, I actually think this is a duplicate of #53 as it also describes how an auction rebalancer can have their tokens burnt by a malicious publisher.

Findings Information

🌟 Selected for report: WatchPug

Labels

bug
2 (Med Risk)
sponsor confirmed

Awards

1535.2197 USDC - $1,535.22

External Links

Handle

WatchPug

Vulnerability details

https://github.com/code-423n4/2021-12-defiprotocol/blob/205d3766044171e325df6a8bf2e79b37856eece1/contracts/contracts/Auction.sol#L97-L102

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

    (address[] memory pendingTokens, uint256[] memory pendingWeights, uint256 minIbRatio) = basket.getPendingWeights();
    require(newRatio >= minIbRatio);

In the current implementation, newRatio is calculated and compared with minIbRatio in settleAuction().

However, if newRatio is less than minIbRatio, settleAuction() will always fail and there is no way for the bonder to cancel and get a refund.

PoC

Given:

  • bondPercentDiv = 400
  • basketToken.totalSupply = 40,000
  • factory.auctionMultiplier = 2
  • factory.auctionDecrement = 10,000
  • basket.ibRatio = 1e18
  • pendingWeights.minIbRatio = 1.9 * 1e18
  1. Alice called bondForRebalance() 2,000 blocks after the auction started, paid 100 basketToken for the bond;
  2. Alice tries to settleAuction(), it will always fail because newRatio < minIbRatio;
  • a = 2 * 1e18
  • b = 0.2 * 1e18
  • newRatio = 1.8 * 1e18;
  1. Bob calls bondBurn() one day after, 100 basketToken from Alice will been burned.

Recommendation

Move the minIbRatio check to bondForRebalance():

function bondForRebalance() public override {
        require(auctionOngoing);
        require(!hasBonded);

        bondTimestamp = block.timestamp;
        bondBlock = block.number;

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

        (address[] memory pendingTokens, uint256[] memory pendingWeights, uint256 minIbRatio) = basket.getPendingWeights();
        require(newRatio >= minIbRatio);

        IERC20 basketToken = IERC20(address(basket));
        bondAmount = basketToken.totalSupply() / factory.bondPercentDiv();
        basketToken.safeTransferFrom(msg.sender, address(this), bondAmount);
        hasBonded = true;
        auctionBonder = msg.sender;

        emit Bonded(msg.sender, bondAmount);
    }

#0 - 0xleastwood

2022-03-27T02:14:54Z

While this issue is correct and I think this is a safer way to handle the require(newRatio >= minIbRatio) check, there are a few assumptions that are made. For example, it is assumed that the user bonds their tokens without checking minIbRatio and a publisher is able to maliciously update minIbRatio which must first go through timelock. Based on this, I'm more inclined to downgrade this to medium severity as I think this more accurately reflects the threat model.

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