Platform: Code4rena
Start Date: 16/09/2021
Pot Size: $50,000 USDC
Total HM: 26
Participants: 30
Period: 7 days
Judge: GalloDaSballo
Total Solo HM: 17
Id: 36
League: ETH
Rank: 5/30
Findings: 8
Award: $4,130.84
🌟 Selected for report: 6
🚀 Solo Findings: 2
901.7387 USDC - $901.74
itsmeSTYJ
All funds in the basket can be stolen.
settleAuction([malicious bounty IDs], [], [], [all tokens in the basket], [entire token balances excluding the amount needed to pass lines 96-98])
withdrawBounty
is called, it will remove the remaining funds in the contract that was used to pass lines 96-98.Note that lines 4-6 are all executed atomically within 1 function call.
Check balances after the bounties have been withdrawn instead of the other way round.
#0 - frank-beard
2021-09-28T21:43:51Z
#1 - GalloDaSballo
2021-12-19T15:44:27Z
Duplicate of #31
131.4735 USDC - $131.47
itsmeSTYJ
Unsafe ERC20 tokens that are compiled before solidity version 0.4.22 will not be able to be transferred out of the contract as the transfer function does not return a value on transfer i.e. they do not 100% comply w/ the current ERC20 token standard.
This means if these unsafe tokens are used to fund the bounty, the tokens are lost forever. You can read more about this here.
function withdrawBounty(uint256[] memory bountyIds) internal { // withdraw bounties for (uint256 i = 0; i < bountyIds.length; i++) { Bounty memory bounty = _bounties[bountyIds[i]]; require(bounty.active); IERC20(bounty.token).safeTransfer(msg.sender, bounty.amount); // replaced transfer with safeTransfer bounty.active = false; emit BountyClaimed(msg.sender, bounty.token, bounty.amount, bountyIds[i]); } }
#0 - frank-beard
2021-10-19T16:58:09Z
#1 - GalloDaSballo
2021-12-18T16:42:07Z
Duplicate of #196
450.8693 USDC - $450.87
itsmeSTYJ
USDT is the most popular example of this where there exists a flag to turn on a "fee per transaction". It is currently turned off but you do not know when it'll turn on. There might be more tokens like these in the future where a portion of the amount will be taken as fees whenever the tokens are transferred.
You want to make sure your protocol can handle edgecases like these where the amount that is requested to be transferred is less than the actual amount transferred.
I don't think there's a correct way to mitigate against this but as a start, you should check the tokenBalance before and after the transfer to get the actual amount transferred. Once you have the actual amount of tokens transferred you can either:
I would go for option 2 because it allows your protocol to use these tokens in the basket as opposed to rejecting them and it's easier to explain why the excess are refunded than to explain why the excess are kept.
Note that this fix should be applied to all lines of code where safeTransferFrom
is used.
#0 - frank-beard
2021-10-19T16:58:47Z
#1 - GalloDaSballo
2021-12-18T16:40:59Z
Duplicate of #236
🌟 Selected for report: itsmeSTYJ
1001.9319 USDC - $1,001.93
itsmeSTYJ
More fees are actually charged than intended
Assume that license fee is 10% i.e. 1e17 and time diff = half a year.
When you calculate feePct
, you expect to get 5e16 since that's 5% and the actual amount of fee to be charged should be totalSupply * feePct (5) / BASE (100) but on line 118, we are actually dividing by BASE - feePct i.e. 95.
5 / 95 = 0.052 instead of the intended 0.05.
Solution is to replace BASE - feePct
in the denominator with BASE
.
#0 - GalloDaSballo
2021-12-18T16:40:01Z
The warden identified an inconsistency with the math that charged more fees than intended
131.4735 USDC - $131.47
itsmeSTYJ
Normally I would classify this as low but because of how widespread USDT is, I believe it is important to handle this case simply for USDT. USDT doesn't allow you to approve allowance unless you first set it to 0.
If your basket contains USDT initially and the publisher tries to publishNewIndex with USDT (regardless of weighting), any bonder that tries to settle the auction will always fail because basket.setNewWeights
will always fail when it comes to approving max allowance for USDT.
The publisher might not be malicious but the publisher not knowing about this is not an excuse for users losing their bond.
function approveUnderlying(address spender) private { for (uint256 i = 0; i < weights.length; i++) { if(IERC20(tokens[i]).allowance(msg.sender, spender) > 0) { IERC20(tokens[i]).approve(spender, 0); // checks if allowance is non 0, approve to 0. } IERC20(tokens[i]).approve(spender, type(uint256).max); } }
#0 - frank-beard
2021-10-06T16:43:13Z
#1 - GalloDaSballo
2021-12-19T15:53:43Z
Duplicate of #35
🌟 Selected for report: itsmeSTYJ
1001.9319 USDC - $1,001.93
itsmeSTYJ
Worst case - no functions that contains handleFees()
can pass because line 118 will always underflow and revert. You only need feePct
to be bigger than BASE
for the handleFees()
function to fail which will result in a lot of gas wasted and potentially bond burnt.
I did not classify this as high risk because a simple fix would simply to reduce the licenseFee via changeLicenseFee
.
Add these require statement to the following functions:
require(newLicenseFee <= BASE, "changeLicenseFee: license fee cannot be greater than 100%");
require(licenseFee <= BASE, "proposeBasketLicense: license fee cannot be greater than 100%");
#0 - GalloDaSballo
2021-12-19T21:54:35Z
Agree with the finding, the warden highlighted an admin exploit that allows to DOS the basket. Adding a check not only prevents the exploit, but gives a security guarantee to the protocol users
🌟 Selected for report: itsmeSTYJ
333.9773 USDC - $333.98
itsmeSTYJ
If tokensNeeded is 0, it is possible to remove all the funds in the basket since no tokens are required to pass the balanceOf checks. The chances of this happening is very unlikely however it is better to be safe than sorry.
Add a require statement to check that the numerator (basketAsERC20.totalSupply() * pendingWeights[i] * newRatio
) is greater than or eq to the denominator (BASE * BASE
). This will ensure that it can never round down i.e. tokensNeeded can never be 0.
#0 - GalloDaSballo
2021-12-02T01:26:26Z
Agree with the finding, due to integer division the rounding can return a 0 there which can cause issues Given the low likelihood of this happening low severity is proper
🌟 Selected for report: leastwood
Also found by: cmichel, csanuragjain, itsmeSTYJ, joeysantoro, kenzo, pauliax
4.3799 USDC - $4.38
itsmeSTYJ
Gas optimisation, use a mapping instead of an array since you dont have a nested loop.
function validateWeights(address[] memory _tokens, uint256[] memory _weights) public override pure { require(_tokens.length == _weights.length); uint256 length = _tokens.length; mapping(address => boolean) tokenLists; // added mapping // address[] memory tokenList = new address[](length); // delete this array // check uniqueness of tokens and not token(0) for (uint i = 0; i < length; i++) { require(_tokens[i] != address(0)); require(_weights[i] > 0); require(!tokenLists[_tokens[i]], "validateWeights: tokens are not unique"); // The first time a token is encountered, this is false so !false = true. The second time a token is encountered, this will be true and !true = false and the require statement will fail. tokenLists[_tokens[i]] = true; // set mapping to true } }
#0 - GalloDaSballo
2021-11-26T17:27:57Z
Duplicate of #160
🌟 Selected for report: itsmeSTYJ
57.691 USDC - $57.69
itsmeSTYJ
Minor gas optimisation
function publishNewIndex(address[] memory _tokens, uint256[] memory _weights) onlyPublisher public override { // validateWeights(_tokens, _weights); // delete this if (pendingWeights.pending) { require(block.number >= pendingWeights.block + TIMELOCK_DURATION); if (auction.auctionOngoing() == false) { auction.startAuction(); emit PublishedNewIndex(publisher); } else if (auction.hasBonded()) { } else { auction.killAuction(); validateWeights(_tokens, _weights); // add here pendingWeights.tokens = _tokens; pendingWeights.weights = _weights; pendingWeights.block = block.number; } } else { validateWeights(_tokens, _weights); // add here pendingWeights.pending = true; pendingWeights.tokens = _tokens; pendingWeights.weights = _weights; pendingWeights.block = block.number; } }
#0 - GalloDaSballo
2021-11-26T17:26:45Z
Agree with the finding, saves gas in the best case
🌟 Selected for report: itsmeSTYJ
57.691 USDC - $57.69
itsmeSTYJ
Gas optimisation
The if branch in the handleFee() function is only there to handle the very first time handleFees are called. Thereafter, this condition will always fail so it makes more sense to initialize it with the initialize() function.
function initialize(IFactory.Proposal memory proposal, IAuction auction_) public override { publisher = proposal.proposer; licenseFee = proposal.licenseFee; factory = IFactory(msg.sender); auction = auction_; ibRatio = BASE; tokens = proposal.tokens; weights = proposal.weights; lastFee = block.timestamp; // updated lastFee here approveUnderlying(address(auction)); __ERC20_init(proposal.tokenName, proposal.tokenSymbol); } ... function handleFees() private { // if (lastFee == 0) { // delete this // lastFee = block.timestamp; // delete this // } else { // delete this uint256 startSupply = totalSupply(); uint256 timeDiff = (block.timestamp - lastFee); 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); lastFee = block.timestamp; uint256 newIbRatio = ibRatio * startSupply / totalSupply(); ibRatio = newIbRatio; emit NewIBRatio(ibRatio); // } // delete this }
#0 - GalloDaSballo
2021-11-26T17:27:26Z
Agree with finding, simplifies code and streamlines the logic
🌟 Selected for report: itsmeSTYJ
57.691 USDC - $57.69
itsmeSTYJ
Gas optimisation
You can save some gas when it comes to creating a new basket by transferring the tokens directly to the newly created Basket contract instead of transferring to the factory, approving the basket and then calling transferFrom to move the tokens to the basket.
You need to have a separate mintTo
function that can only be called by the Factory and doesn't contain the pullUnderlying(amount, msg.sender)
function.
#0 - frank-beard
2021-11-06T16:36:08Z
while this could work, we prefer to not add special functions for situations like that
#1 - GalloDaSballo
2021-11-26T17:29:03Z
Agree with the finding, and in contract to another warden's submission, there's a clear example of the logic and what it should do.
No problem for it being a nofix, but the finding is valid