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: 12/30
Findings: 3
Award: $1,340.29
🌟 Selected for report: 2
🚀 Solo Findings: 1
🌟 Selected for report: csanuragjain
1001.9319 USDC - $1,001.93
csanuragjain
It was observed that Publisher is allowed to create a basket with zero token and weight. This can lead to user fund stealing as described in below poc The issue was discovered in validateWeights function of Basket contract
Proposal memory proposal = Proposal({ licenseFee: 10, tokenName: abc, tokenSymbol: aa, proposer: 0xabc, tokens: {}, weights: {}, basket: address(0) });
function validateWeights(address[] memory _tokens, uint256[] memory _weights) public override pure { require(_tokens.length == _weights.length); uint256 length = _tokens.length; address[] memory tokenList = new address[](length); // check uniqueness of tokens and not token(0) for (uint i = 0; i < length; i++) { ... } }
_proposals.push(proposal);
function createBasket(uint256 idNumber) external override returns (IBasket) { Proposal memory bProposal = _proposals[idNumber]; require(bProposal.basket == address(0)); .... for (uint256 i = 0; i < bProposal.weights.length; i++) { ... } ... return newBasket; }
Since no weights and tokens were in this proposal so no token transfer is required (bProposal.weights.length will be 0 so loop won't run)
Basket gets created and user becomes publisher for this basket
newBasket.mintTo(BASE, msg.sender); _proposals[idNumber].basket = address(newBasket);
function mint(uint256 amount) public override { mintTo(amount, msg.sender); } function mintTo(uint256 amount, address to) public override { ... pullUnderlying(amount, msg.sender); _mint(to, amount); ... }
function pullUnderlying(uint256 amount, address from) private { for (uint256 i = 0; i < weights.length; i++) { uint256 tokenAmount = amount * weights[i] * ibRatio / BASE / BASE; IERC20(tokens[i]).safeTransferFrom(from, address(this), tokenAmount); } }
_mint(to, amount);
function publishNewIndex(address[] memory _tokens, uint256[] memory _weights) onlyPublisher public override { validateWeights(_tokens, _weights); 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(); pendingWeights.tokens = _tokens; pendingWeights.weights = _weights; pendingWeights.block = block.number; } } else { pendingWeights.pending = true; pendingWeights.tokens = _tokens; pendingWeights.weights = _weights; pendingWeights.block = block.number; } }
Publisher calls the publishNewIndex again which starts the Auction. This auction is later settled using the settleAuction function in Auction contract
Publisher owned address can now call burn and get the amount 10 even though he never made the payment since his balanceOf(msg.sender) = 10 (Step 9)
function burn(uint256 amount) public override { require(auction.auctionOngoing() == false); require(amount > 0); require(balanceOf(msg.sender) >= amount); handleFees(); pushUnderlying(amount, msg.sender); _burn(msg.sender, amount); emit Burned(msg.sender, amount); }
Change validateWeights to check for 0 length token
function validateWeights(address[] memory _tokens, uint256[] memory _weights) public override pure { require(_tokens.length>0); ... }
#0 - GalloDaSballo
2021-12-20T00:47:56Z
The warden identified a way to the publisher to effectively mint a ton of shares by setting weights to zero, for them to then change the weights and use their shares to dilute depositors.
This may be a potential feature of the protocol at this time as there's no check for how the ratios relate to themselves
The implications are actually pretty deep and I highly recommend the sponsor to consider the fact that if weights can be changed at any time by the publisher, they can effectively dilute / re-base the basket at any time very aggressively, to the detriment of users.
As for the the finding by the warden, they have shown a specific way for the publisher to dilute other depositors, by setting up a basket with zero tokens initially.
Becasue this is a specific attack that relies on external conditions, I'm downgrading the severity to medium
🌟 Selected for report: leastwood
Also found by: cmichel, csanuragjain, itsmeSTYJ, joeysantoro, kenzo, pauliax
4.3799 USDC - $4.38
csanuragjain
Gas optimization can be done on validateWeights function of Basket.sol contract
function validateWeights(address[] memory _tokens, uint256[] memory _weights) public override pure { require(_tokens.length == _weights.length); uint256 length = _tokens.length; address[] memory tokenList = new address[](length); // check uniqueness of tokens and not token(0) for (uint i = 0; i < length; i++) { require(_tokens[i] != address(0)); require(_weights[i] > 0); for (uint256 x = 0; x < tokenList.length; x++) { require(_tokens[i] != tokenList[x]); } tokenList[i] = _tokens[i]; } }
for (uint i = 0; i < length; i++) { require(_tokens[i] != address(0)); require(_weights[i] > 0); for (uint256 x = 0; x < tokenList.length; x++) { require(_tokens[i] != tokenList[x]); } tokenList[i] = _tokens[i]; }
for (uint i = 0; i < length; i++) { ... require(tokenList[_tokens[i]]==0); tokenList[_tokens[i]]=1; ... }
We are setting tokenList[_tokens[i]] as 1 for each token
In case of duplicate require(tokenList[_tokens[i]]==0); will fail
Use something like below:
for (uint i = 0; i < length; i++) { ... require(tokenList[_tokens[i]]==0); tokenList[_tokens[i]]=1; ... }
#0 - GalloDaSballo
2021-11-28T17:44:19Z
Duplicate of #160
🌟 Selected for report: csanuragjain
333.9773 USDC - $333.98
csanuragjain
It was observed that _bounties variable is global per basket. Also you are allowed to add 0 amount in bounty. This means if user adds uint256 max times bounty with amount 0, no one can add further bounty on this basket
function addBounty(IERC20 token, uint256 amount) public override returns (uint256) { // add bounty to basket token.safeTransferFrom(msg.sender, address(this), amount); _bounties.push(Bounty({ token: address(token), amount: amount, active: true })); uint256 id = _bounties.length - 1; emit BountyAdded(token, amount, id); return id; }
_bounties should be cleared once auction has been settled
#0 - GalloDaSballo
2021-12-05T16:25:11Z
The finding is valid, severity is correct as well as this would impair the functionality of just the bounties for that specific basket