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: 1/26
Findings: 4
Award: $5,841.96
🌟 Selected for report: 2
🚀 Solo Findings: 1
🌟 Selected for report: kenzo
5117.3992 USDC - $5,117.40
kenzo
handleFees
does not update lastFee
if startSupply == 0
.
This means that wrongly, extra fee tokens would be minted once the basket is resupplied and handleFees
is called again.
Loss of user funds. The extra minting of fee tokens comes on the expense of the regular basket token owners, which upon withdrawal would get less underlying than their true share, due to the dilution of their tokens' value.
Scenario:
handleFees
would be called upon mint, and would just return since totalSupply == 0. Note: It does not update lastFee
.} else if (startSupply == 0) { return;
handleFees
will be called and will calculate the fees according to the current supply and the time diff between now and lastFee
:uint256 timeDiff = (block.timestamp - lastFee);
https://github.com/code-423n4/2021-12-defiprotocol/blob/main/contracts/contracts/Basket.sol#L139
But as we saw, lastFee
wasn't updated in the previous step. lastFee
is still the time of 1 day before - when the last person burned his tokens and the basket supply was 0.
So now the basket will mint fees as if a whole day has passed since the last calculation, but actually it only needs to calculate the fees for the last block, since only then we had tokens in the basket.
Set lastFee = block.timestamp
if startSupply == 0
.
#0 - 0xleastwood
2022-03-26T07:08:56Z
The issue can be outlined as follows:
lastFee = block.timestamp
.handleFees()
did not set lastFee = block.timestamp
. As a result, fees are charged on the user's deposit for the entire time that the basket was inactive for.It seems that the basket is severely flawed in calculating fees on partially inactive baskets. This puts users' funds at direct risk of being lost. Malicious publishers can setup baskets as a sort of honeypot to abuse this behaviour.
#1 - 0xleastwood
2022-03-26T07:09:33Z
This was an interesting find! Kudos to the warden
kenzo
In fees calculation, division is being used in the midst of the calculation, not at the end of it. This leads to lost precision in fee amount (as solidity doesn't save remainder of division). Division should happen at the end to maintain precision.
Lost fees. The exact amount depends on the parameters set and being tested. According to a few tests I ran, it seems that in normal usage, 1% of fees are lost. In some cases even 7.5% of fees.
Division in the midst of a calculation:
uint256 feePct = timeDiff * licenseFee / ONE_YEAR; uint256 fee = startSupply * feePct / (BASE - feePct); _mint(publisher, fee * (BASE - factory.ownerSplit()) / BASE); _mint(Ownable(address(factory)).owner(), fee * factory.ownerSplit() / BASE);
https://github.com/code-423n4/2021-12-defiprotocol/blob/main/contracts/contracts/Basket.sol#L140:#L145 It's a little hard to share a POC script as it involves changing the .sol file so I tested it manually. But after moving the division to the end using the mitigation below, I saw 1%-7% increases in fees minted. Usually 1%.
We want to firstly do all multiplication and lastly do all the division. So remove the usage of feePct and instead set fee to be:
uint256 fee = startSupply * licenseFee * timeDiff / ONE_YEAR / (BASE - licenseFee);
#0 - frank-beard
2022-02-22T19:40:15Z
#1 - 0xleastwood
2022-03-27T03:05:33Z
Nice find! I think this qualifies as medium
risk due to the protocol regularly leaking value. This can be mitigated by performing division at the very end of the fee calculation. Marking this as the primary issue.
🌟 Selected for report: GiveMeTestEther
Also found by: Jujic, TomFrenchBlockchain, WatchPug, gzeon, kenzo, sirhashalot, ye0lde
kenzo
When declared, variables are already initialized to 0, so there is no need to do so explicitly.
Around 2000 gas for each variable.
ownerSplit in Factory:
ownerSplit = 0; //TODO: needed?
https://github.com/code-423n4/2021-12-defiprotocol/blob/main/contracts/contracts/Factory.sol#L22
Remove the redundant line.
#0 - 0xleastwood
2022-03-27T22:36:09Z
Duplicate of #45
🌟 Selected for report: ye0lde
Also found by: GiveMeTestEther, WatchPug, kenzo
kenzo
handleFees
emits the storage ibRatio instead of the local newIbRatio. This is more costly.
Unnecessary gas spent.
uint256 newIbRatio = ibRatio * startSupply / totalSupply(); ibRatio = newIbRatio; emit NewIBRatio(ibRatio);
Emit newIbRatio instead of ibRatio.
#0 - 0xleastwood
2022-03-27T23:06:41Z
Duplicate of #161
🌟 Selected for report: WatchPug
Also found by: GiveMeTestEther, TomFrenchBlockchain, danb, kenzo
kenzo
The function creates and populates a new array to check for duplicates, this is not necessary.
Some amount of gas unnecessarily spent.
The relevant area: https://github.com/code-423n4/2021-12-defiprotocol/blob/main/contracts/contracts/Basket.sol#L71:#L85
Change the check to the following:
for (uint i = 0; i < length; i++) { require(_tokens[i] != address(0)); require(_weights[i] > 0); for (uint256 x = 0; x < i; x++) { require(_tokens[i] != _tokens[x]); } }
#0 - 0xleastwood
2022-03-27T22:39:10Z
Duplicate of #118
🌟 Selected for report: TomFrenchBlockchain
Also found by: kenzo
18.4266 USDC - $18.43
kenzo
When using few times an unchanging value from external contract call, the result can be saved and used without recalling the external contract.
Some gas can be saved.
In settleAuction, the basket's totalSupply stays constant through the loop's iterations.
for (uint256 i = 0; i < pendingWeights.length; i++) { uint256 tokensNeeded = basketAsERC20.totalSupply() * pendingWeights[i] * newRatio / BASE / BASE; require(IERC20(pendingTokens[i]).balanceOf(address(basket)) >= tokensNeeded); }
https://github.com/code-423n4/2021-12-defiprotocol/blob/main/contracts/contracts/Auction.sol#L106
Manual analysis, hardhat
Save basketAsERC20.totalSupply() to a local variable outside the loop, and use that variable inside the loop.
#0 - 0xleastwood
2022-03-27T22:41:43Z
Duplicate of #38