Kuiper contest - 0x0x0x'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: 2/26

Findings: 5

Award: $5,120.53

🌟 Selected for report: 10

🚀 Solo Findings: 2

Findings Information

🌟 Selected for report: gzeon

Also found by: 0x0x0x, TomFrenchBlockchain

Labels

bug
duplicate
2 (Med Risk)

Awards

414.5093 USDC - $414.51

External Links

Handle

0x0x0x

Vulnerability details

Publisher can call changeLicenseFee to change fees. There is no maximum cap for this parameter. At first glance, because there is a timelock period of 1 day, the users have enough time to react.

But if the publisher can start an auction and bondForRebalance. The publisher can simply not settle the auction and block the basket for 1 day.

So the publisher can make it extremely hard (almost impossible) for users to react to the new license fee. With an extremely high license fee, almost all funds can get stolen by publisher (because of protocol fees also the factory owner gets his share).

Proof of concept

1- Publisher , calls publishNewIndex and proposes new weights.

2- After one day, Publisher calls changeLicenseFee. As license fee a really high amount is set, which does not overflow.

3- After 1-2 blocks, before anyone can react Publisher calls publishNewIndex, bondForRebalance.

4- When TIMELOCK_DURATION of changeLicenseFee ends, the publisher calls it to update the fee.

License fee definitely needs a maximum cap. Furthermore to make it really safe:

First option is to make sure there is a significant difference, between timelock duration for license fee and duration for bondBurn.

The better option is to make sure that an auction cannot be started, when there is a pendingLicenseFee and changeLicenseFee cannot be called during an auction (can also add a resetPendingLicenseFee function to be able to create an auction whenever wanted).

#0 - frank-beard

2022-02-22T19:10:55Z

#1 - frank-beard

2022-02-22T19:12:20Z

also while the publisher could block an auction, minting and burning is disabled during rebalances and thus no fees would be collected

Findings Information

🌟 Selected for report: 0x0x0x

Labels

bug
2 (Med Risk)
sponsor confirmed

Awards

1535.2197 USDC - $1,535.22

External Links

Handle

0x0x0x

Vulnerability details

The function is implemented as follows:

function auctionBurn(uint256 amount) onlyAuction nonReentrant external override { uint256 startSupply = totalSupply(); handleFees(startSupply); _burn(msg.sender, amount); uint256 newIbRatio = ibRatio * startSupply / (startSupply - amount); ibRatio = newIbRatio; emit NewIBRatio(newIbRatio); emit Burned(msg.sender, amount); }

When handleFees is called, totalSupply and ibRatio changes accordingly, but for newIbRatio calculation tokens minted in handleFees is not included. Therefore, ibRatio is calculated higher than it should be. This is dangerous, since last withdrawing user(s) lose their funds with this operation. In case this miscalculation happens more than once, newIbRatio will increase the miscalculation even faster and can result in serious amount of funds missing. At each time auctionBurn is called, at least 1 day (auction duration) of fees result in this miscalculation. Furthermore, all critical logic of this contract is based on ibRatio, this behaviour can create serious miscalculations.

Mitigation step

Rather than

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

A practical solution to this problem is calculating newIbRatio as follows:

uint256 supply = totalSupply(); uint256 newIbRatio = ibRatio * (supply + amount) / supply;

#0 - 0xleastwood

2022-03-26T07:14:21Z

The warden has identified an issue whereby newIbRatio uses an incorrect startSupply variable which is under-represented. As new tokens may be minted in handleFees(), this will lead to an incorrect ibRatio which is used in all other areas of the protocol. A lower ibRatio causes pushUnderlying() and pullUnderlying() to be improperly accounted for. As a result, burning basket tokens will redeem a smaller amount of underlying tokens and minting basket tokens will require a smaller amount of underlying tokens.

This causes the protocol to leak value from all basket token holders but it does not allow assets to be stolen. As such, I think this is better put as a medium severity issue.

Findings Information

🌟 Selected for report: 0x0x0x

Labels

bug
2 (Med Risk)
sponsor confirmed

Awards

1535.2197 USDC - $1,535.22

External Links

Handle

0x0x0x

Vulnerability details

When factory changes auctionMultiplier or auctionDecrement profitability of bonded auctions change. There is no protection against this behaviour. Furthermore, factory owners can decide to get all tokens from baskets where they are bonded for the auction.

Proof of concept

1- Factory owners call bondForRebalance for an auction.

2- Factory owners sets auctionMultiplier as 0 and auctionDecrement as maximum value

3- settleAuction is called. newRatio = 0, since a = b = 0. All tokens can be withdrawn with this call, since tokensNeeded = 0.

Extra notes

Furthermore, even the factory owners does not try to scam users. In case auctionMultiplier or auctionDecrement is changed, all current auctionBonder from Auctions can only call settleAuction with different constraints. Because of different constraints, users/bonder will lose/gain funds.

Mitigation step

Save auctionDecrement and auctionMultiplier to global variables in Auction.sol, when startAuction is called.

#0 - frank-beard

2022-02-23T18:02:54Z

Adding in protection from the global governance is definitely important.

#1 - 0xleastwood

2022-03-27T01:46:37Z

The warden has identified an issue whereby the factory owner can rug-pull baskets through the re-balancing mechanism. Because newRatio = 0, the basket improperly checks the tokens needed in the contract. However, this assumes that the factory owner is malicious which satisfies medium severity due to assets not being at direct risk.

The sponsor has decided to add additional protections (potentially via timelock) to mitigate this issue.

#2 - 0xleastwood

2022-03-27T01:49:54Z

This rug-pull is made even more difficult by the fact that newRatio must be >= minIbRatio. Because minIbRatio is behind timelock, I think this rug vector is unlikely or at least can only be used to steal a fixed amount of funds.

Findings Information

🌟 Selected for report: 0x0x0x

Labels

bug
1 (Low Risk)
sponsor acknowledged

Awards

511.7399 USDC - $511.74

External Links

Handle

0x0x0x

Vulnerability details

If someone calls settleAuction and inputs more tokens than the person should, tokens get stuck. To avoid this, it would be better to compute inputTokens/inputWeights and outputTokens/outputWeights automatically, since they can be known beforehand. When a bonder sends a settleAuction request, any change to conditions might lead them to lose their funds(or revert). Furthermore, this is a serious attack vector and for example web hackers can abuse it, without even communicating with a different contract.

Mitigation step

Automate the calculation of inputTokens/inputWeights and outputTokens/outputWeights or add a mechanism to calculate excess amounts for everything and transfer them back to the sender.

#0 - frank-beard

2022-02-22T19:16:23Z

this is correct however we are not as concerned with having more tokens than is needed and future rebalances can correct this issue. i would consider this as a low/medium risk as perhaps a naive publisher could not recognize this and create issues when rebalancing.

#1 - 0xleastwood

2022-03-27T01:39:29Z

I don't think there will be any changes to the basket's ibRatio when an auction is ongoing. Basket.sol does not allow for such changes and Basket.handleFees() is the only function which updates ibRatio. At best, I think this is a low severity issue as automatically calculating the correct token amounts prevents bonded users from making mistakes upon calling settleAuction().

Findings Information

🌟 Selected for report: 0x0x0x

Labels

bug
1 (Low Risk)
sponsor acknowledged

Awards

511.7399 USDC - $511.74

External Links

Handle

0x0x0x

Vulnerability details

Lets say there is already as many tokens as maxSupply. A user calls burn with amount = 1. handleFees create more than 1 token. Therefore, maxSupply is exceeded. Furthermore, the same can also happen again, when maxSupply is already exceeded.

#0 - frank-beard

2022-02-23T18:11:10Z

While this could happen, this shouldn't affect any of the functionality of the basket and can be rectified by the publisher by resetting the maxSupply or more burns. Generally I would consider this a low risk issue.

#1 - 0xleastwood

2022-03-27T02:25:14Z

While I agree that this is an issue, it does not lead to any unexpected behaviour and as such I'd deem this as low risk as its more of a state handling issue.

Findings Information

🌟 Selected for report: 0x0x0x

Labels

bug
1 (Low Risk)
sponsor acknowledged

Awards

511.7399 USDC - $511.74

External Links

Handle

0x0x0x

Vulnerability details

When bondPercentDiv is set to 1, it is unlikely that someone can bond by using all tokens (Also because of fees factory would have some tokens). I think this is not intended behaviour.

Mitigation step

Add the condition that this parameter is more than 1 (rather than 0). Another option is to keep it as a lock mechanism, but add a comment about it in factory.

#0 - frank-beard

2022-02-23T18:13:15Z

We made the decision to keep some of those values open as ideally a dao/multisig will be modifying those parameters if needed. In this case, something like that should be caught in governance or before it is set.

Findings Information

🌟 Selected for report: 0x0x0x

Also found by: Jujic, pmerkleplant, ye0lde

Labels

bug
G (Gas Optimization)

Awards

7.4628 USDC - $7.46

External Links

Handle

0x0x0x

Vulnerability details

Impact

!= 0 is a cheaper operation compared to > 0, when dealing with uint.

Occurrences

./Basket.sol:68: require(_tokens.length > 0); ./Basket.sol:77: require(_weights[i] > 0); ./Basket.sol:93: require(amount > 0); ./Basket.sol:110: require(amount > 0); ./Basket.sol:292: require(tokenAmount > 0); ./Factory.sol:53: require(newBondPercentDiv > 0);

Findings Information

🌟 Selected for report: 0x0x0x

Also found by: Jujic, Meta0xNull, WatchPug, pmerkleplant, rishabh

Labels

bug
G (Gas Optimization)

Awards

4.0299 USDC - $4.03

External Links

Handle

0x0x0x

Vulnerability details

Proof of Concept

Example:

for (uint i = 0; i < arr.length; i++) { //Operations not effecting the length of the array. }

Loading length of array costs gas. Therefore, the length should be cached, if the length of the array doesn't change inside the loop. Furthermore, there is no need to assign the initial value 0. This costs extra gas.

Recommended implementation:

uint length = arr.length; for (uint i; i < length; i++) { //Operations not effecting the length of the array. }

By doing so the length is only loaded once rather than loading it as many times as iterations (Therefore, less gas is spent).

Occurences

./Auction.sol:88: for (uint256 i = 0; i < inputTokens.length; i++) { ./Auction.sol:92: for (uint256 i = 0; i < outputTokens.length; i++) { ./Auction.sol:105: for (uint256 i = 0; i < pendingWeights.length; i++) { ./Auction.sol:152: for (uint256 i = 0; i < bountyIds.length; i++) { ./Basket.sol:75: for (uint i = 0; i < length; i++) { ./Basket.sol:79: for (uint256 x = 0; x < tokenList.length; x++) { ./Basket.sol:275: for (uint256 i = 0; i < weights.length; i++) { ./Basket.sol:282: for (uint256 i = 0; i < weights.length; i++) { ./Basket.sol:290: for (uint256 i = 0; i < weights.length; i++) { ./Factory.sol:109: for (uint256 i = 0; i < bProposal.weights.length; i++) {

Findings Information

🌟 Selected for report: 0x0x0x

Also found by: ye0lde

Labels

bug
G (Gas Optimization)

Awards

18.4266 USDC - $18.43

External Links

Handle

0x0x0x

Vulnerability details

Concept

Negation(!) is a better optimized implementation of applying == false. It requires less gas and also the recommended approach to compute the negation.

In other words: x == false and !x compute the same result, but !x costs less gas.

Scope

./Auction.sol:37: require(auctionOngoing == false, 'ongoing auction'); ./Basket.sol:92: require(auction.auctionOngoing() == false); ./Basket.sol:109: require(auction.auctionOngoing() == false); ./Basket.sol:221: if (auction.auctionOngoing() == false) { ./Basket.sol:259: require(auction.auctionOngoing() == false);

Findings Information

🌟 Selected for report: 0x0x0x

Also found by: TomFrenchBlockchain, danb

Labels

bug
G (Gas Optimization)

Awards

11.0559 USDC - $11.06

External Links

Handle

0x0x0x

Vulnerability details

basket.sol#mintTo is as follows:

function mintTo(uint256 amount, address to) public nonReentrant override { require(auction.auctionOngoing() == false); require(amount > 0); uint256 startSupply = totalSupply(); require(startSupply + amount <= maxSupply); handleFees(startSupply); pullUnderlying(amount, msg.sender); _mint(to, amount); require(totalSupply() <= maxSupply); emit Minted(to, amount); }

To check, whether maxSupply is exceeded first the following statement is used:

require(startSupply + amount <= maxSupply);

At the end of the function once again, it is checked:

require(totalSupply() <= maxSupply);

Since second requirement already check whether maximum supply is exceeded, the first on is not required and consumes extra gas.

#0 - 0xleastwood

2022-03-27T10:35:03Z

I agree, the first require statement is redundant.

Findings Information

🌟 Selected for report: WatchPug

Also found by: 0x0x0x

Labels

bug
duplicate
G (Gas Optimization)

Awards

18.4266 USDC - $18.43

External Links

Handle

0x0x0x

Vulnerability details

In handleFees, when timeDiff = 0 the calculations for the fee are not required. Inside last else statement can an if statement added to avoid not needed calculations. This would save gas especially, when the contract is used more than once in a single block.

So this part

} else { uint256 timeDiff = (block.timestamp - lastFee);

Can be replaced with the following:

} else { uint256 timeDiff = (block.timestamp - lastFee); if (timeDiff == 0) { return; }

Or alternatively all remaining calculations can be inside an if (timeDiff != 0).

#0 - 0xleastwood

2022-03-27T22:19:13Z

Duplicate of #110

Findings Information

🌟 Selected for report: 0x0x0x

Labels

bug
G (Gas Optimization)

Awards

40.9479 USDC - $40.95

External Links

Handle

0x0x0x

Vulnerability details

In pushUnderlying and pullUnderlying, to compute the tokenAmount it is required to divide by BASE twice. Square of BASE can be saved in a constant to avoid applying this division twice. Simply a new variable called BASE2 (representing square of BASE) can apply the same logic for less gas.

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