Nibbl contest - WatchPug's results

NFT fractionalization protocol with guaranteed liquidity and price based buyout.

General Information

Platform: Code4rena

Start Date: 21/06/2022

Pot Size: $30,000 USDC

Total HM: 12

Participants: 96

Period: 3 days

Judge: HardlyDifficult

Total Solo HM: 5

Id: 140

League: ETH

Nibbl

Findings Distribution

Researcher Performance

Rank: 6/96

Findings: 3

Award: $1,914.55

🌟 Selected for report: 1

πŸš€ Solo Findings: 0

Findings Information

🌟 Selected for report: WatchPug

Also found by: hansfriese

Labels

bug
2 (Med Risk)
disagree with severity
sponsor acknowledged

Awards

631.623 USDC - $631.62

External Links

Lines of code

https://github.com/code-423n4/2022-06-nibbl/blob/8c3dbd6adf350f35c58b31723d42117765644110/contracts/Twav/Twav.sol#L35-L42

Vulnerability details

function _getTwav() internal view returns(uint256 _twav){
    if (twavObservations[TWAV_BLOCK_NUMBERS - 1].timestamp != 0) {
        uint8 _index = ((twavObservationsIndex + TWAV_BLOCK_NUMBERS) - 1) % TWAV_BLOCK_NUMBERS;
        TwavObservation memory _twavObservationCurrent = twavObservations[(_index)];
        TwavObservation memory _twavObservationPrev = twavObservations[(_index + 1) % TWAV_BLOCK_NUMBERS];
        _twav = (_twavObservationCurrent.cumulativeValuation - _twavObservationPrev.cumulativeValuation) / (_twavObservationCurrent.timestamp - _twavObservationPrev.timestamp);
    }
}

Since _blockTimestamp is uint32, subtraction underflow is desired at _twavObservationCurrent.timestamp - _twavObservationPrev.timestamp.

See: https://github.com/Uniswap/v2-periphery/blob/master/contracts/examples/ExampleOracleSimple.sol#L43

function update() external {
    (uint price0Cumulative, uint price1Cumulative, uint32 blockTimestamp) =
        UniswapV2OracleLibrary.currentCumulativePrices(address(pair));
    uint32 timeElapsed = blockTimestamp - blockTimestampLast; // overflow is desired

Because the solidity version used by the current implementation is 0.8.10, and there are some breaking changes in Solidity v0.8.0:

Arithmetic operations revert on underflow and overflow.

Ref: https://docs.soliditylang.org/en/v0.8.13/080-breaking-changes.html#silent-changes-of-the-semantics

The timestamp subtraction may revert due to underflow.

Impact

Since _getTwav() is used in NibblVault.sol#_rejectBuyout(), if it reverts and there is a buyout, an essential feature of the NibblVault contract will be unavailable, causing users' funds to be frozen in the contract.

Recommendation

Change to:

function _updateTWAV(uint256 _valuation, uint32 _blockTimestamp) internal {
    uint32 _timeElapsed; 
    unchecked {
        _timeElapsed = _blockTimestamp - lastBlockTimeStamp;
    }

    uint256 _prevCumulativeValuation = twavObservations[((twavObservationsIndex + TWAV_BLOCK_NUMBERS) - 1) % TWAV_BLOCK_NUMBERS].cumulativeValuation;
    unchecked {
        twavObservations[twavObservationsIndex] = TwavObservation(_blockTimestamp, _prevCumulativeValuation + (_valuation * _timeElapsed)); //add the previous observation to make it cumulative
    }
    
    twavObservationsIndex = (twavObservationsIndex + 1) % TWAV_BLOCK_NUMBERS;
    lastBlockTimeStamp = _blockTimestamp;
}
function _getTwav() internal view returns(uint256 _twav){
    if (twavObservations[TWAV_BLOCK_NUMBERS - 1].timestamp != 0) {
        uint8 _index = ((twavObservationsIndex + TWAV_BLOCK_NUMBERS) - 1) % TWAV_BLOCK_NUMBERS;
        TwavObservation memory _twavObservationCurrent = twavObservations[(_index)];
        TwavObservation memory _twavObservationPrev = twavObservations[(_index + 1) % TWAV_BLOCK_NUMBERS];
        unchecked {
            _twav = (_twavObservationCurrent.cumulativeValuation - _twavObservationPrev.cumulativeValuation) / (_twavObservationCurrent.timestamp - _twavObservationPrev.timestamp);
        }
    }
}

#0 - KenzoAgada

2022-06-25T01:23:49Z

If I'm not mistaken, timestamp 4294967296 is 2106, I wouldn't call the contract breaking in 84 years a high severity issue. Plus the contract is truncating the timestamp on purpose. Seems to me more like a design choice and less of a bug.

#1 - mingwatch

2022-06-27T03:34:31Z

If I'm not mistaken, timestamp 4294967296 is 2106, I wouldn't call the contract breaking in 84 years a high severity issue. Plus the contract is truncating the timestamp on purpose. Seems to me more like a design choice and less of a bug.

https://github.com/code-423n4/2022-06-nibbl/blob/main/contracts/Twav/Twav.sol#L23-L25

function _updateTWAV(uint256 _valuation, uint32 _blockTimestamp) internal {
        uint32 _timeElapsed; 
        unchecked {
            _timeElapsed = _blockTimestamp - lastBlockTimeStamp;
        }

According to the above code, is obviously not a design choice.

#2 - KenzoAgada

2022-06-27T03:52:16Z

According to the above code, is obviously not a design choice.

Ah, I think I understand what you mean, it is not handled consistently.

#3 - HardlyDifficult

2022-06-30T21:43:50Z

it is not handled consistently

Thanks for the clarifications!

_getTwav would overflow once timestamps overflow uint32, but only when the current observation has overflowed while the previous observation did not.

The window for this vulnerability is very small, just at the time timestamp starts to overflow in 2106 - vaults active before or after that time should work as expected.

This appears to be a Medium risk finding. There's potentially a case to be made for high here but it's hard to make that call without a more complete POC included. The vault is an upgradeable contract so they have 84 years to sort this out -- but it does seem like an issue that should be fixed.

Findings Information

🌟 Selected for report: cccz

Also found by: Lambda, WatchPug, kenzo, xiaoming90, zzzitron

Labels

bug
duplicate
2 (Med Risk)

Awards

230.2266 USDC - $230.23

External Links

Lines of code

https://github.com/code-423n4/2022-06-nibbl/blob/8c3dbd6adf350f35c58b31723d42117765644110/contracts/NibblVault.sol#L300-L328

Vulnerability details

function buy(uint256 _minAmtOut, address _to) external override payable notBoughtOut lock whenNotPaused returns(uint256 _purchaseReturn) {
    //Make update on the first tx of the block
    if (status == Status.buyout) {
        uint32 _blockTimestamp = uint32(block.timestamp % 2**32);
        if (_blockTimestamp != lastBlockTimeStamp) {
            _updateTWAV(getCurrentValuation(), _blockTimestamp);   
            _rejectBuyout();
        }
    }
    uint256 _initialTokenSupply = initialTokenSupply;
    uint256 _totalSupply = totalSupply();
    if (_totalSupply >= _initialTokenSupply) {
        _purchaseReturn = _buyPrimaryCurve(msg.value, _totalSupply);
    } else {
        uint256 _lowerCurveDiff = getMaxSecondaryCurveBalance() - secondaryReserveBalance;
        if (_lowerCurveDiff >= msg.value) {
            _purchaseReturn = _buySecondaryCurve(msg.value, _totalSupply);
        } else {
            //Gas Optimization
            _purchaseReturn = _initialTokenSupply - _totalSupply;
            secondaryReserveBalance += _lowerCurveDiff;
            // _purchaseReturn = _buySecondaryCurve(_to, _lowerCurveDiff);
            _purchaseReturn += _buyPrimaryCurve(msg.value - _lowerCurveDiff, _totalSupply + _purchaseReturn);
        } 
    }
    require(_minAmtOut <= _purchaseReturn, "NibblVault: Return too low");
    _mint(_to, _purchaseReturn);
    emit Buy(msg.sender, _purchaseReturn, msg.value);
}

When the price is in the SecondaryCurve, and the user can buy with msg.value == getMaxSecondaryCurveBalance() - secondaryReserveBalance + 1 to escape the CuratorFee and AdminFee for the part in the SecondaryCurve of the purchase. See L320, _initialTokenSupply - _totalSupply is the part that escaped the fees.

It appears that this is caused by an incorrect code update intended for Gas Optimization. A very similar code in the sell() function was updated correctly: https://github.com/code-423n4/2022-06-nibbl/blob/8c3dbd6adf350f35c58b31723d42117765644110/contracts/NibblVault.sol#L373-L386

Impact

The issue will cause the protocol and the curators to lose part of their fees.

Recommendation

Change to:

if (_totalSupply >= _initialTokenSupply) {
    _purchaseReturn = _buyPrimaryCurve(msg.value, _totalSupply);
} else {
    uint256 _lowerCurveDiff = getMaxSecondaryCurveBalance() - secondaryReserveBalance;
    if (_lowerCurveDiff >= msg.value) {
        _purchaseReturn = _buySecondaryCurve(msg.value, _totalSupply);
    } else {
        //Gas Optimization
        _purchaseReturn = _initialTokenSupply - _totalSupply;
        secondaryReserveBalance += _lowerCurveDiff;
        _purchaseReturn = _chargeFee(_purchaseReturn);
        // _purchaseReturn = _buySecondaryCurve(_lowerCurveDiff, _totalSupply);
        _purchaseReturn += _buyPrimaryCurve(msg.value - _lowerCurveDiff, _totalSupply + _purchaseReturn);
    } 
}

#0 - mundhrakeshav

2022-06-26T10:16:04Z

#173

Findings Information

🌟 Selected for report: peritoflores

Also found by: WatchPug

Labels

bug
duplicate
2 (Med Risk)
sponsor acknowledged

Awards

1052.705 USDC - $1,052.70

External Links

Lines of code

https://github.com/code-423n4/2022-06-nibbl/blob/8c3dbd6adf350f35c58b31723d42117765644110/contracts/Twav/Twav.sol#L21-L31

Vulnerability details

function _updateTWAV(uint256 _valuation, uint32 _blockTimestamp) internal {
    uint32 _timeElapsed; 
    unchecked {
        _timeElapsed = _blockTimestamp - lastBlockTimeStamp;
    }

    uint256 _prevCumulativeValuation = twavObservations[((twavObservationsIndex + TWAV_BLOCK_NUMBERS) - 1) % TWAV_BLOCK_NUMBERS].cumulativeValuation;
    twavObservations[twavObservationsIndex] = TwavObservation(_blockTimestamp, _prevCumulativeValuation + (_valuation * _timeElapsed)); //add the previous observation to make it cumulative
    twavObservationsIndex = (twavObservationsIndex + 1) % TWAV_BLOCK_NUMBERS;
    lastBlockTimeStamp = _blockTimestamp;
}

Because the solidity version used by the current implementation is 0.8.10, and there are some breaking changes in Solidity v0.8.0:

Arithmetic operations revert on underflow and overflow.

Ref: https://docs.soliditylang.org/en/v0.8.13/080-breaking-changes.html#silent-changes-of-the-semantics

When _prevCumulativeValuation is big enough, _updateTWAV() will revert due to overflow.

Impact

Since _updateTWAV() is used in NibblVault.sol#buy() and NibblVault.sol#sell(), when it reverts due to overflow, the NibblVault contract as a whole will malfunction, and users' funds may be frozen in the contract.

Recommendation

Change to:

function _updateTWAV(uint256 _valuation, uint32 _blockTimestamp) internal {
    uint32 _timeElapsed; 
    unchecked {
        _timeElapsed = _blockTimestamp - lastBlockTimeStamp;
    }

    uint256 _prevCumulativeValuation = twavObservations[((twavObservationsIndex + TWAV_BLOCK_NUMBERS) - 1) % TWAV_BLOCK_NUMBERS].cumulativeValuation;
    unchecked {
        twavObservations[twavObservationsIndex] = TwavObservation(_blockTimestamp, _prevCumulativeValuation + (_valuation * _timeElapsed)); //add the previous observation to make it cumulative
    }
    
    twavObservationsIndex = (twavObservationsIndex + 1) % TWAV_BLOCK_NUMBERS;
    lastBlockTimeStamp = _blockTimestamp;
}

#0 - HardlyDifficult

2022-07-03T12:29:51Z

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