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
Rank: 4/26
Findings: 4
Award: $3,308.90
🌟 Selected for report: 9
🚀 Solo Findings: 2
🌟 Selected for report: GiveMeTestEther
1535.2197 USDC - $1,535.22
GiveMeTestEther
The fee calculation on L141 is wrong. It should only get divided by BASE and not (BASE - feePct)
This shows dividing only by BASE is correct: Assumptions:
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:
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
Manual Analysis
#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.
🌟 Selected for report: GiveMeTestEther
1535.2197 USDC - $1,535.22
GiveMeTestEther
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.
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
Manual Analysis
#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
🌟 Selected for report: GiveMeTestEther
GiveMeTestEther
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".
Manual Analysis
🌟 Selected for report: GiveMeTestEther
GiveMeTestEther
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.
-Manual Analysis
🌟 Selected for report: GiveMeTestEther
GiveMeTestEther
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.
Manual Analysis
🌟 Selected for report: GiveMeTestEther
Also found by: Jujic, TomFrenchBlockchain, WatchPug, gzeon, kenzo, sirhashalot, ye0lde
GiveMeTestEther
No need to zero-initialize ownerSplit because default value is zero. Saves gas.
Manual Analysis
🌟 Selected for report: GiveMeTestEther
GiveMeTestEther
Reuse function argument "auction_" as argument in calling approveUnderlying on L56 instead of using the storage variable "auction". This saves a cold SLOAD.
Manual Analysis
🌟 Selected for report: GiveMeTestEther
GiveMeTestEther
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
Manual Analysis
🌟 Selected for report: ye0lde
Also found by: GiveMeTestEther, WatchPug, kenzo
GiveMeTestEther
On L269 & L271 reuse the function argument "newRatio" instead of the storage variable "ibRatio" to save two SLOADs.
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
Manual Analysis
#0 - 0xleastwood
2022-03-27T22:43:52Z
Duplicate of #161
🌟 Selected for report: GiveMeTestEther
Also found by: WatchPug
GiveMeTestEther
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.
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
Manual Analysis
🌟 Selected for report: WatchPug
Also found by: GiveMeTestEther, TomFrenchBlockchain, danb, kenzo
GiveMeTestEther
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.
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