Kuiper contest - GiveMeTestEther'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: 4/26

Findings: 4

Award: $3,308.90

🌟 Selected for report: 9

🚀 Solo Findings: 2

Findings Information

🌟 Selected for report: GiveMeTestEther

Labels

bug
2 (Med Risk)
sponsor confirmed

Awards

1535.2197 USDC - $1,535.22

External Links

Handle

GiveMeTestEther

Vulnerability details

Impact

The fee calculation on L141 is wrong. It should only get divided by BASE and not (BASE - feePct)

Proof of Concept

This shows dividing only by BASE is correct: Assumptions:

  • BASE is 1e18 accordign to the code
  • timeDiff is exactly ONE_YEAR (for easier calculations)
  • startSupply is 1e18 (exactly one basket token, also represents 100% in fee terms)
  • licenseFee is 1e15 (0.1%) If we calculate the fee of one whole year and startSupply is one token (1e18, equal to 100%), the fee should be exactly the licenseFee (1e15, 0.1%),

uint256 timeDiff = ONE_YEAR; uint256 feePct = timeDiff * licenseFee / ONE_YEAR; => therefore we have: feePct = licenseFee which is 1e15 (0.1%) according to our assumptions uint256 fee = startSupply * feePct / BASE; // only divide by BASE => insert values => fee = 1e18 * licenseFee / 1e18 = licenseFee

This shows the math is wrong:

Assumptions:

  • BASE is 1e18 according to the code
  • timeDiff is exactly ONE_YEAR (for easier calculations)
  • startSupply is 1e18 (exactly one basket token, also represents 100% in fee terms)
  • licenseFee is 1e15 (0.1%) If we calculate the fee of one whole year and startSupply is one token (1e18, equal to 100%), the fee should be exactly the licenseFee (1e15, 0.1%), but the fee is bigger than that.

uint256 timeDiff = ONE_YEAR; uint256 feePct = timeDiff * licenseFee / ONE_YEAR; => therefore we have: feePct = licenseFee which is 1e15 (0.1%) according to our assumptions

uint256 fee = startSupply * feePct / (BASE - feePct); insert the values => fee = 1e18 * 1e15 / (1e18 - 1e15) => (factor out 1e15) => fee = 1e15 * 1e18 / (1e15 * ( 1e3 - 1) => (cancel 1e15) => 1e18 / ( 1e3 - 1)

math: if we increase the divisor but the dividend stays the same we get a smaller number e.g. (1 / (2-1)) is bigger than (1 / 2) apply this here => 1e18 / ( 1e3 - 1) > 1e18 / 1e3 => 1e18 / ( 1e3 - 1) > 1e15 this shows that the fee is higher than 1e15

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

Tools Used

Manual Analysis

  • only divide by BASE

#0 - 0xleastwood

2022-03-27T03:10:12Z

I think this is valid. Calculating fees as startSupply * feePct / (BASE - feePct) leads to an overestimation of fees charged on users.

Findings Information

🌟 Selected for report: GiveMeTestEther

Labels

bug
2 (Med Risk)
sponsor acknowledged

Awards

1535.2197 USDC - $1,535.22

External Links

Handle

GiveMeTestEther

Vulnerability details

Impact

The fee calculation is based on the totalSupply of the basket token. But some amount of the totalSupply represents the fees paid to the publisher/ protocol owner. Therefore the fees are "overcharged": because the fee amount is calculated on a part of already "paid" fees, should only take into account what is "owned" by the users and not the publisher/protocol owner.

Proof of Concept

L141: the fee percent is multiplied by startSupply (=basket token total supply) L144 & L145: publisher / protocol owner receive basket tokens as fees payment https://github.com/code-423n4/2021-12-defiprotocol/blob/205d3766044171e325df6a8bf2e79b37856eece1/contracts/contracts/Basket.sol#L141

Tools Used

Manual Analysis

  • account the fee amount in a storage variable: uint256 feeAmount;
  • subtract feeAmount from startSupply L141: uint256 fee = (startSupply - feeAmount) * feePct / (BASE - feePct); // note the other bug about only dividing by BASE
  • add the fee to feeAmount after the calculation: feeAmount += fee;
  • if publisher/protocol owner burn basket token, reduce the feeAmount etc.

#0 - frank-beard

2022-02-23T18:05:30Z

Generally we think that the amount of overcharged fees from this matter will be very negligible in the long term. It is worth noting that the fix proposed would not really solve the problem as not all tokens owned by the publisher/owner may be fees as well as they could just transfer those fees to another account and burn from there.

#1 - 0xleastwood

2022-03-27T02:33:24Z

I'm not sure if this is correct. startSupply is indeed equal to totalSupply() but it is queried before new tokens are minted to the factory owner and publisher. As such, the fees appear to be correctly charged. I'll have @frank-beard confirm this as I may have missed something.

#2 - 0xleastwood

2022-04-04T12:27:49Z

Considering @frank-beard has not replied to this, I'm gonna make the final judgement and keep this open. I think it makes sense to track the fees paid out to the factory owner and publisher and have this amount excluded from the fee calculations. Upon either party burning tokens, this fee amount tracker must be updated accordingly. Due to the added complexity, it is understandable that this issue would be deemed a wontfix by the sponsor.

#3 - frank-beard

2022-04-04T14:15:38Z

Yeah so I do agree that it does make sense generally to track the fees paid out and exclude, however in this case the impact is very low and yeah the complexity to deal with it doesn't seem worth it

Findings Information

🌟 Selected for report: GiveMeTestEther

Labels

bug
G (Gas Optimization)

Awards

40.9479 USDC - $40.95

External Links

Handle

GiveMeTestEther

Vulnerability details

Impact

Cache "address(basket)" in a local variable to save a few SLOADs. It gets read in three different loops of the settleAuction() function and during the assignment of "basketAsERC20".

Proof of Concept

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

Tools Used

Manual Analysis

  • new local variable before the first loop: address _basket = address(basket);
  • replace the occurrences of address(basket) with the local variable

Findings Information

🌟 Selected for report: GiveMeTestEther

Labels

bug
G (Gas Optimization)

Awards

40.9479 USDC - $40.95

External Links

Handle

GiveMeTestEther

Vulnerability details

Impact

Store the calculation of "basketToken.totalSupply() / factory.bondPercentDiv()" in a local variable. So we can use this local variable in the safeTransfer and the event emit (instead of reading the storage variable again with what we have assigned it in this function). We save a two SLOADs.

Proof of Concept

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

Tools Used

-Manual Analysis

  • introduce a local variable: uint256 _bondAmount = basketToken.totalSupply() / factory.bondPercentDiv();
  • assign to storage: bondAmount = _bondAmount ;
  • replace the occurrences of bondAmount in the safeTransfer and event emit.

Findings Information

🌟 Selected for report: GiveMeTestEther

Labels

bug
G (Gas Optimization)

Awards

40.9479 USDC - $40.95

External Links

Handle

GiveMeTestEther

Vulnerability details

Impact

The storage variable bondAmount is read twice in this function. Store it in a local variable and replace the occurrences with the local variable to save a SLOAD.

Proof of Concept

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

Tools Used

Manual Analysis

  • new local variable: uint256 _bondAmount = bondAmount;
  • replace the occurrences of bondAmount with the local variable

Findings Information

🌟 Selected for report: GiveMeTestEther

Also found by: Jujic, TomFrenchBlockchain, WatchPug, gzeon, kenzo, sirhashalot, ye0lde

Labels

bug
G (Gas Optimization)

Awards

2.4482 USDC - $2.45

External Links

Handle

GiveMeTestEther

Vulnerability details

Impact

No need to zero-initialize ownerSplit because default value is zero. Saves gas.

Proof of Concept

https://github.com/code-423n4/2021-12-defiprotocol/blob/205d3766044171e325df6a8bf2e79b37856eece1/contracts/contracts/Factory.sol#L22

Tools Used

Manual Analysis

  • remove L22

Findings Information

🌟 Selected for report: GiveMeTestEther

Labels

bug
G (Gas Optimization)

Awards

40.9479 USDC - $40.95

External Links

Handle

GiveMeTestEther

Vulnerability details

Impact

Reuse function argument "auction_" as argument in calling approveUnderlying on L56 instead of using the storage variable "auction". This saves a cold SLOAD.

Proof of Concept

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

Tools Used

Manual Analysis

  • rewrite L56 as: approveUnderlying(address(auction_));

Findings Information

🌟 Selected for report: GiveMeTestEther

Labels

bug
G (Gas Optimization)

Awards

40.9479 USDC - $40.95

External Links

Handle

GiveMeTestEther

Vulnerability details

Impact

L139: uint256 timeDiff = (block.timestamp - lastFee); timeDiff is always >= 0 because lastFee can only be set to block.timestamp in the same block or in a previous block. Therefore we don't need to check for underflow and can write L139 in an unchecked block

Proof of Concept

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

Tools Used

Manual Analysis

  • write L139 as: unchecked { uint256 timeDiff = (block.timestamp - lastFee); }

Findings Information

🌟 Selected for report: ye0lde

Also found by: GiveMeTestEther, WatchPug, kenzo

Labels

bug
duplicate
G (Gas Optimization)

Awards

7.4628 USDC - $7.46

External Links

Handle

GiveMeTestEther

Vulnerability details

Impact

On L269 & L271 reuse the function argument "newRatio" instead of the storage variable "ibRatio" to save two SLOADs.

Proof of Concept

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

Tools Used

Manual Analysis

  • reuse newRatio

#0 - 0xleastwood

2022-03-27T22:43:52Z

Duplicate of #161

Findings Information

🌟 Selected for report: GiveMeTestEther

Also found by: WatchPug

Labels

bug
G (Gas Optimization)

Awards

18.4266 USDC - $18.43

External Links

Handle

GiveMeTestEther

Vulnerability details

Impact

Assumption is that normal baskets have more than one token in a basket and therefore the loop gets iterated at least twice. The "ibRatio" is storage variable and could be cached in a local variable to save SLOAD in the loops.

Proof of Concept

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

Tools Used

Manual Analysis

  • cache ibRation in a local variable

Findings Information

🌟 Selected for report: WatchPug

Also found by: GiveMeTestEther, TomFrenchBlockchain, danb, kenzo

Labels

bug
duplicate
G (Gas Optimization)

Awards

5.3732 USDC - $5.37

External Links

Handle

GiveMeTestEther

Vulnerability details

Impact

The inner loop doesn't need to iterate the whole array length, only needs to compare the current token address (_tokens[i]) until to the last added element to tokenList array in the previous loop iteration.

Proof of Concept

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

Tools Used

Manual Anlysis

function validateWeights(address[] memory _tokens, uint256[] memory _weights) public override pure { require(_tokens.length > 0); require(_tokens.length == _weights.length); uint256 length = _tokens.length; address[] memory tokenList = new address[](length); // check uniqueness of tokens and not token(0) // change: take the first token out from the loop require(_tokens[0] != address(0)); require(_weights[0] > 0); tokenList[0] = _tokens[0]; // change: start at i = 1 for (uint i = 1; i < length; i++) { require(_tokens[i] != address(0)); require(_weights[i] > 0); // change: only compare the previous added tokenList elements and not the others that have initial 0-value => loop condition changed to "x < i" for (uint256 x = 0; x < i; x++) { require(_tokens[i] != tokenList[x]); } tokenList[i] = _tokens[i]; } }

#0 - 0xleastwood

2022-03-27T22:20:41Z

Duplicate of #118

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