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: 9/30
Findings: 3
Award: $1,557.79
🌟 Selected for report: 5
🚀 Solo Findings: 1
🌟 Selected for report: gpersoon
1001.9319 USDC - $1,001.93
gpersoon
Suppose some unrelated ERC20 tokens end up in the basket contract (via an airdrop, a user mistake etc)
Then anyone can do a bondForRebalance() and settleAuction() to scoop these tokens.
The function settleAuction() allows you to specify an outputToken, so also completely unrelated tokens. Thus you can retrieve additional tokens with settleAuction()
https://github.com/code-423n4/2021-09-defiProtocol/blob/main/contracts/contracts/Auction.sol#L69 function settleAuction(.. address[] memory outputTokens, uint256[] memory outputWeights) public override { ... for (uint256 i = 0; i < outputTokens.length; i++) { IERC20(outputTokens[i]).safeTransferFrom(address(basket), msg.sender, outputWeights[i]); }
Check outputTokens are part of the previous basket tokens (e.g. basket.tokens() )
#0 - GalloDaSballo
2021-12-19T21:57:26Z
If the Auction contract has any ERC20 that is not checked against the require, those tokens can be taken away for free. The warden didn't directly mention this, but this can also apply to bounties. Any bounty specified in a token that is not protected can be taken without claiming the bounty (as the require won't check for it)
I think that the finding has appropriate severity, although the warden didn't directly mention the ability to steal bounties
🌟 Selected for report: gpersoon
gpersoon
The function handleFees() will revert if feePct >BASE ==> feePct = timeDiff * licenseFee / ONE_YEAR; ==> BASE < timeDiff * licenseFee / ONE_YEAR; ==> licenseFee > BASE * ONE_YEAR / timeDiff So licenseFee should have a maximum value to prevent this. Probably there is also a reasonable upperlimit to licenseFee the let the protocol function properly.
https://github.com/code-423n4/2021-09-defiProtocol/blob/main/contracts/contracts/Basket.sol#L110 function handleFees() private { .. uint256 timeDiff = (block.timestamp - lastFee); uint256 feePct = timeDiff * licenseFee / ONE_YEAR; uint256 fee = startSupply * feePct / (BASE - feePct); // will revert if feePct > BASE
Define an upperlimit for licenseFee Enforce this in the function proposeBasketLicense() of Factory.sol
#0 - GalloDaSballo
2021-12-04T17:36:59Z
Agree with finding, mitigation is to add a limit to licenseFee
90.1739 USDC - $90.17
gpersoon
When using the Openzeppelin upgradability pattern, the initialize() function should you the modifier initializer. However the initialize() function of Basket.sol doesn't have this modifier.
This won't give problems in practice because __ERC20_init() does have this modifier and prevents initialize() from being called twice. However forks of the projects or future developers might not be aware of this any make risky changes.
https://github.com/code-423n4/2021-09-defiProtocol/blob/main/contracts/contracts/Basket.sol#L36 import { ERC20Upgradeable } from "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol";
contract Basket is IBasket, ERC20Upgradeable { .. function initialize(IFactory.Proposal memory proposal, IAuction auction_) public override { ...
__ERC20_init(proposal.tokenName, proposal.tokenSymbol); }
Add the modifier initializer to the function initialize()
#0 - frank-beard
2021-10-19T17:44:37Z
#1 - GalloDaSballo
2021-12-04T17:41:42Z
Valid finding, as per he wardens findings, the initialize function can be called only once
90.1739 USDC - $90.17
gpersoon
Anyone can provide bounties with addBounty(). However anyone can add malicious tokens. If someone gets the bounty via settleAuction() they will probably try to sell them on an exchange. The malicious token might trick users in signing a transaction which drains their wallet or point them to a malicious website via an error message.
https://github.com/code-423n4/2021-09-defiProtocol/blob/main/contracts/contracts/Auction.sol#L126 function addBounty(IERC20 token, uint256 amount) public override returns (uint256) { // add bounty to basket token.safeTransferFrom(msg.sender, address(this), amount); // could be any token, including a malicious token _bounties.push(Bounty({ token: address(token), amount: amount, active: true }));
uint256 id = _bounties.length - 1; emit BountyAdded(token, amount, id); return id; }
Create a whitelist for bounty tokens.
#0 - frank-beard
2021-10-19T17:08:32Z
the auction settler can choose what bounties to claim, it is up to them to do due diligence on the active bounties
#1 - GalloDaSballo
2021-12-20T00:54:46Z
The finding is valid, any token can be used, even fishing ones. That said, the warden didn't specify any type of attack nor a specific POC. As such am downgrading this to low severity (other wardens have found very serious implications from this finding, but this finding lacks evidence and proof)
25.9609 USDC - $25.96
gpersoon
The function settleAuction() doesn't reset auctionBonder to 0. Although this does not give any problems for the logic, the value is no longer relevant after the auction and can be set to 0. Setting to 0 saves a bit of gas and also prevents mistakes in future code updates.
https://github.com/code-423n4/2021-09-defiProtocol/blob/main/contracts/contracts/Auction.sol#L69 function settleAuction( .. ) public override { .. require(msg.sender == auctionBonder); .. auctionOngoing = false; hasBonded = false;
Set auctionBonder to 0 at the end of the function settleAuction()
#0 - GalloDaSballo
2021-11-28T17:22:55Z
Duplicate of #40
15.5766 USDC - $15.58
gpersoon
If the functions mintTo and burn of Basket.sol are called twice in the same block then block.timestamp will stay the same and timeDiff ==0. Then it is not necessary to _mint () tokens, as this will be 0 tokens anyway.
So checking for timeDiff ==0 could save a bit of gas.
https://github.com/code-423n4/2021-09-defiProtocol/blob/main/contracts/contracts/Basket.sol#L110 function handleFees() private { if (lastFee == 0) { lastFee = block.timestamp; } else { 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); } }
Add an extra if in the following way:
function handleFees() private { if (lastFee == 0) { lastFee = block.timestamp; } else { uint256 startSupply = totalSupply(); uint256 timeDiff = (block.timestamp - lastFee); if (timeDiff !=0) { // ===> extra if 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); } } }
#0 - GalloDaSballo
2021-11-28T17:24:45Z
Agree that the check would save gas in that scenario, notice that it would increase cost for all other situations