Boot Finance contest - WatchPug's results

Custom DEX AMM for Defi Projects

General Information

Platform: Code4rena

Start Date: 04/11/2021

Pot Size: $50,000 USDC

Total HM: 20

Participants: 28

Period: 7 days

Judge: 0xean

Total Solo HM: 11

Id: 51

League: ETH

Boot Finance

Findings Distribution

Researcher Performance

Rank: 2/28

Findings: 8

Award: $7,101.60

🌟 Selected for report: 10

🚀 Solo Findings: 1

Findings Information

🌟 Selected for report: Reigada

Also found by: WatchPug

Labels

bug
duplicate
3 (High Risk)

Awards

1308.4274 USDC - $1,308.43

External Links

Handle

WatchPug

Vulnerability details

At L225 in _processWithdrawal(), it calls vestLock.vest() to vest 70% of the tokens bought.

However, PublicSale.sol contract never approve() mainToken to the vestLock contract, making _processWithdrawal() to revet at L225.

As a result, all the withdrawals will fail and all the funds will be frozen in the contract.

https://github.com/code-423n4/2021-11-bootfinance/blob/f102ee73eb320532c5a7c1e833f225c479577e39/tge/contracts/PublicSale.sol#L212-L229

function _processWithdrawal (uint _era, uint _day, address _member) private returns (uint value) {
    uint memberUnits = mapEraDay_MemberUnits[_era][_day][_member]; // Get Member Units
    if (memberUnits == 0) { 
        value = 0;                                                 // Do nothing if 0 (prevents revert)
    }
    else {
        value = getEmissionShare(_era, _day, _member);             // Get the emission Share for Member
        mapEraDay_MemberUnits[_era][_day][_member] = 0;            // Set to 0 since it will be withdrawn
        mapEraDay_UnitsRemaining[_era][_day] = mapEraDay_UnitsRemaining[_era][_day].sub(memberUnits);  // Decrement Member Units
        mapEraDay_EmissionRemaining[_era][_day] = mapEraDay_EmissionRemaining[_era][_day].sub(value);  // Decrement emission
        totalEmitted += value;                                     // Add to Total Emitted
        uint256 v_value = value * 3 / 10;                          // Transfer 30%, lock the rest in vesting contract             
        mainToken.transfer(_member, v_value);                      // ERC20 transfer function
        vestLock.vest(_member, value - v_value, 0);
        emit Withdrawal(msg.sender, _member, _era, _day, value, mapEraDay_EmissionRemaining[_era][_day]);
    }
    return value;
}

https://github.com/code-423n4/2021-11-bootfinance/blob/f102ee73eb320532c5a7c1e833f225c479577e39/vesting/contracts/Vesting.sol#L73-L98

function vest(address _beneficiary, uint256 _amount, uint256 _isRevocable) external payable whenNotPaused {
    require(_beneficiary != address(0), "Invalid address");
    require( _amount > 0, "amount must be positive");
    // require(totalVestedAmount.add(_amount) <= maxVestingAmount, 'maxVestingAmount is already vested');
    require(_isRevocable == 0 || _isRevocable == 1, "revocable must be 0 or 1");
    uint256 _unlockTimestamp = block.timestamp.add(unixYear);

    Timelock memory newVesting = Timelock(_amount, _unlockTimestamp);
    timelocks[_beneficiary].push(newVesting);

    if(_isRevocable == 0){
        benRevocable[_beneficiary] = [false,false];
    }
    else if(_isRevocable == 1){
        benRevocable[_beneficiary] = [true,false];
    }

    totalVestedAmount = totalVestedAmount.add(_amount);
    benTotal[_beneficiary] = benTotal[_beneficiary].add(_amount);

    // transfer to SC using delegate transfer
    // NOTE: the tokens has to be approved first by the caller to the SC using `approve()` method.
    vestingToken.transferFrom(msg.sender, address(this), _amount);

    emit TokenVested(_beneficiary, _amount, _unlockTimestamp, block.timestamp);
}

Recommendation

Consider adding approve() in constructor().

_mainToken.approve(address(_vestLock), type(uint256).max); 

#0 - chickenpie347

2022-01-04T01:36:07Z

Duplicate of #135

Findings Information

🌟 Selected for report: jonah1005

Also found by: WatchPug

Labels

bug
duplicate
3 (High Risk)

Awards

1308.4274 USDC - $1,308.43

External Links

Handle

WatchPug

Vulnerability details

https://github.com/code-423n4/2021-11-bootfinance/blob/f102ee73eb320532c5a7c1e833f225c479577e39/customswap/contracts/SwapUtils.sol#L1568-L1586

    uint256 initialTargetPricePrecise = _getTargetPricePrecise(self);
    uint256 futureTargetPricePrecise = futureTargetPrice_.mul(TARGET_PRICE_PRECISION);

    if (futureTargetPricePrecise < initialTargetPricePrecise) {
        require(
            futureTargetPricePrecise.mul(MAX_RELATIVE_PRICE_CHANGE).div(WEI_UNIT) >= initialTargetPricePrecise,
            "futureTargetPrice_ is too small"
        );
    } else {
        require(
            futureTargetPricePrecise <= initialTargetPricePrecise.mul(MAX_RELATIVE_PRICE_CHANGE).div(WEI_UNIT),
            "futureTargetPrice_ is too large"
        );
    }

    self.initialTargetPrice = initialTargetPricePrecise;
    self.futureTargetPrice = futureTargetPricePrecise;
    self.initialTargetPriceTime = block.timestamp;
    self.futureTargetPriceTime = futureTime_;

At L1571-1581, it compares futureTargetPricePrecise with initialTargetPricePrecise to make sure the change is within the MAX_RELATIVE_PRICE_CHANGE (a constant of 1%).

However, the current implementation is wrong and it actually compares 1% of futureTargetPricePrecise to initialTargetPricePrecise when futureTargetPricePrecise < initialTargetPricePrecise and futureTargetPricePrecise to 1% of initialTargetPricePrecise when futureTargetPricePrecise >= initialTargetPricePrecise.

This will always fail, making it impossible to change the target price.

Recommendation

Change to:

if (futureTargetPricePrecise < initialTargetPricePrecise) {
    require(
        initialTargetPricePrecise.sub(futureTargetPricePrecise).mul(WEI_UNIT) <= initialTargetPricePrecise.mul(MAX_RELATIVE_PRICE_CHANGE),
        "futureTargetPrice_ is too small"
    );
} else {
    require(
        futureTargetPricePrecise.sub(initialTargetPricePrecise).mul(WEI_UNIT) <= initialTargetPricePrecise.mul(MAX_RELATIVE_PRICE_CHANGE),
        "futureTargetPrice_ is too large"
    );
}

#0 - chickenpie347

2022-01-04T01:28:04Z

Duplicate of #143

Findings Information

🌟 Selected for report: WatchPug

Labels

bug
3 (High Risk)
sponsor confirmed

Awards

2907.6165 USDC - $2,907.62

External Links

Handle

WatchPug

Vulnerability details

Based on the context, the tokenPrecisionMultipliers used in price calculation should be calculated in realtime based on initialTargetPrice, futureTargetPrice, futureTargetPriceTime and current time, just like getA() and getA2().

However, in the current implementation, tokenPrecisionMultipliers used in price calculation is the stored value, it will only be changed when the owner called rampTargetPrice() and stopRampTargetPrice().

As a result, the targetPrice set by the owner will not be effective until another targetPrice is being set or stopRampTargetPrice() is called.

Recommendation

Consider adding Swap.targetPrice and changing the _xp() at L661 from:

https://github.com/code-423n4/2021-11-bootfinance/blob/f102ee73eb320532c5a7c1e833f225c479577e39/customswap/contracts/SwapUtils.sol#L661-L667

function _xp(Swap storage self, uint256[] memory balances)
    internal
    view
    returns (uint256[] memory)
{
    return _xp(balances, self.tokenPrecisionMultipliers);
}

To:

function _xp(Swap storage self, uint256[] memory balances)
    internal
    view
    returns (uint256[] memory)
{
    uint256[2] memory tokenPrecisionMultipliers = self.tokenPrecisionMultipliers;
    tokenPrecisionMultipliers[0] = self.targetPrice.originalPrecisionMultipliers[0].mul(_getTargetPricePrecise(self)).div(WEI_UNIT)
    return _xp(balances, tokenPrecisionMultipliers);
}

Findings Information

🌟 Selected for report: nathaniel

Also found by: WatchPug, leastwood, pauliax

Labels

bug
duplicate
3 (High Risk)

Awards

529.9131 USDC - $529.91

External Links

Handle

WatchPug

Vulnerability details

https://github.com/code-423n4/2021-11-bootfinance/blob/f102ee73eb320532c5a7c1e833f225c479577e39/vesting/contracts/Vesting.sol#L193-L205

function claim() external whenNotPaused nonReentrant {
    require(benRevocable[msg.sender][1] == false, 'Account must not already be revoked.');
    uint256 amount = _claimableAmount(msg.sender).sub(benClaimed[msg.sender]);
    require(amount > 0, "Claimable amount must be positive");
    require(amount <= benTotal[msg.sender], "Cannot withdraw more than total vested amount");

    // transfer from SC
    benClaimed[msg.sender] = benClaimed[msg.sender].add(amount);
    totalClaimedAmount = totalClaimedAmount.add(amount);
    vestingToken.safeTransfer(msg.sender, amount);

    emit TokenClaimed(msg.sender, amount, block.timestamp);
}

At L195, function claim() will call function _claimableAmount(), which includes an unbounded for loop.

https://github.com/code-423n4/2021-11-bootfinance/blob/f102ee73eb320532c5a7c1e833f225c479577e39/vesting/contracts/Vesting.sol#L162-L188

function _claimableAmount(address _addr) private returns (uint256) {
    uint256 completely_vested = 0;
    uint256 partial_sum = 0;
    uint256 inc = 0;

    // iterate across all the vestings
    // & check if the releaseTimestamp is elapsed
    // then, add all the amounts as claimable amount
    for (uint256 i = benVestingIndex[_addr]; i < timelocks[_addr].length; i++) {
        if (block.timestamp >= timelocks[_addr][i].releaseTimestamp) {
            inc += 1;
            completely_vested = completely_vested.add(timelocks[_addr][i].amount);
        }
        else {
            uint256 iTimeStamp = timelocks[_addr][i].releaseTimestamp.sub(unixYear);
            uint256 claimable = block.timestamp.sub(iTimeStamp).mul(timelocks[_addr][i].amount).div(unixYear);
            partial_sum = partial_sum.add(claimable);
        }
    }

    benVestingIndex[_addr] +=inc;
    benVested[_addr][0] = benVested[_addr][0].add(completely_vested);
    benVested[_addr][1] = partial_sum;
    uint256 s = benVested[_addr][0].add(partial_sum);
    assert(s <= benTotal[_addr]);
    return s;
}

An attacker can call function vest() and add a lot of very small amounts of new vestings to the user, if the length of the user's vestings is large enough, the gas cost of function claim() can exceed the block limit, making it impossible for the user to claim.

Essentially, this allows an attacker to freeze (or burn, considering that the contract is not upgradable) the unclaimed funds of an arbitrary user.

Recommendation

Consider allowing users to claim() a specific range of vestings indexes.

#0 - chickenpie347

2022-01-04T01:37:06Z

Duplicate of #120

Findings Information

🌟 Selected for report: Reigada

Also found by: 0v3rf10w, Ruhum, WatchPug, cmichel, defsec, loop, pauliax

Labels

bug
duplicate
2 (Med Risk)

Awards

52.1514 USDC - $52.15

External Links

Handle

WatchPug

Vulnerability details

Calling ERC20.transfer() without handling the returned value is unsafe.

https://github.com/code-423n4/2021-11-bootfinance/blob/f102ee73eb320532c5a7c1e833f225c479577e39/tge/contracts/PublicSale.sol#L212-L229

function _processWithdrawal (uint _era, uint _day, address _member) private returns (uint value) {
        uint memberUnits = mapEraDay_MemberUnits[_era][_day][_member]; // Get Member Units
        if (memberUnits == 0) { 
            value = 0;                                                 // Do nothing if 0 (prevents revert)
        }
        else {
            value = getEmissionShare(_era, _day, _member);             // Get the emission Share for Member
            mapEraDay_MemberUnits[_era][_day][_member] = 0;            // Set to 0 since it will be withdrawn
            mapEraDay_UnitsRemaining[_era][_day] = mapEraDay_UnitsRemaining[_era][_day].sub(memberUnits);  // Decrement Member Units
            mapEraDay_EmissionRemaining[_era][_day] = mapEraDay_EmissionRemaining[_era][_day].sub(value);  // Decrement emission
            totalEmitted += value;                                     // Add to Total Emitted
            uint256 v_value = value * 3 / 10;                          // Transfer 30%, lock the rest in vesting contract             
            mainToken.transfer(_member, v_value);                      // ERC20 transfer function
            vestLock.vest(_member, value - v_value, 0);
            emit Withdrawal(msg.sender, _member, _era, _day, value, mapEraDay_EmissionRemaining[_era][_day]);
        }
        return value;
    }

Recommendation

Consider using OpenZeppelin's SafeERC20 library with safe versions of transfer functions.

#0 - chickenpie347

2022-01-04T01:28:36Z

Duplicate of #31

Findings Information

🌟 Selected for report: gpersoon

Also found by: WatchPug, cmichel, hyh, leastwood, pauliax

Labels

bug
duplicate
2 (Med Risk)

Awards

85.8459 USDC - $85.85

External Links

Handle

WatchPug

Vulnerability details

vest() can be called by anyone with an arbitrary _beneficiary address to add a Timelock (vesting) to the _beneficiary.

At L83-88, it changes the global storage of revokable settings for the _beneficiary.

This allows anyone to change the revokable settings for other users. Non-revocable vestings can later be changed into revokable, and then be revoked, causing the user to lose funds.

https://github.com/code-423n4/2021-11-bootfinance/blob/7c457b2b5ba6b2c887dafdf7428fd577e405d652/vesting/contracts/Vesting.sol#L73-L98

function vest(address _beneficiary, uint256 _amount, uint256 _isRevocable) external payable whenNotPaused {
    require(_beneficiary != address(0), "Invalid address");
    require( _amount > 0, "amount must be positive");
    // require(totalVestedAmount.add(_amount) <= maxVestingAmount, 'maxVestingAmount is already vested');
    require(_isRevocable == 0 || _isRevocable == 1, "revocable must be 0 or 1");
    uint256 _unlockTimestamp = block.timestamp.add(unixYear);

    Timelock memory newVesting = Timelock(_amount, _unlockTimestamp);
    timelocks[_beneficiary].push(newVesting);

    if(_isRevocable == 0){
        benRevocable[_beneficiary] = [false,false];
    }
    else if(_isRevocable == 1){
        benRevocable[_beneficiary] = [true,false];
    }

    totalVestedAmount = totalVestedAmount.add(_amount);
    benTotal[_beneficiary] = benTotal[_beneficiary].add(_amount);

    // transfer to SC using delegate transfer
    // NOTE: the tokens has to be approved first by the caller to the SC using `approve()` method.
    vestingToken.transferFrom(msg.sender, address(this), _amount);

    emit TokenVested(_beneficiary, _amount, _unlockTimestamp, block.timestamp);
}

Recommendation

Consider making the revokable settings set per Timelock instead of per address.

#0 - chickenpie347

2021-11-16T13:53:33Z

Addressed in #132.

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