Kuiper contest - cmichel's results

Automated portfolio protocol.

General Information

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

Kuiper

Findings Distribution

Researcher Performance

Rank: 2/30

Findings: 4

Award: $4,800.92

🌟 Selected for report: 8

πŸš€ Solo Findings: 1

Findings Information

🌟 Selected for report: cmichel

Labels

bug
3 (High Risk)
sponsor confirmed

Awards

3339.7729 USDC - $3,339.77

External Links

Handle

cmichel

Vulnerability details

Note that the Basket contract approved the Auction contract with all tokens and the settleAuction function allows the auction bonder to transfer all funds out of the basket to themselves. The only limiting factor is the check afterwards that needs to be abided by. It checks if enough tokens are still in the basket after settlement:

// this is the safety check if basket still has all the tokens after removing arbitrary amounts 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); }

The bonder can pass in any inputTokens, even malicious ones they created. This allows them to re-enter the settleAuction multiple times for the same auction.

Calling this function at the correct time (such that bondTimestamp - auctionStart makes newRatio < basket.ibRatio()), the attacker can drain more funds each time, eventually draining the entire basket.

POC

Assume that the current basket.ibRatio is 1e18 (the initial value). The basket publisher calls basket.publishNewIndex with some tokens and weights. For simplicity, assume that the pending tokens are the same as tokens as before, only the weights are different, i.e., this would just rebalance the portfolio. The function call then starts the auction.

The important step to note is that the tokensNeeded value in settleAuction determines how many tokens need to stay in the basket. If we can continuously lower this value, we can keep removing tokens from the basket until it is empty.

The tokensNeeded variable is computed as basketAsERC20.totalSupply() * pendingWeights[i] * newRatio / BASE / BASE. The only variable that changes in the computation when re-entering the function is newRatio (no basket tokens are burned, and the pending weights are never cleared).

Thus if we can show that newRatio decreases on each re-entrant call, we can move out more and more funds each time.

newRatio decreases on each call

After some time, the attacker calls bondForRebalance. This determines the bondTimestamp - auctionStart value in settleAuction. The attack is possible as soon as newRatio < basket.ibRatio(). For example, using the standard parameters the calculation would be:

// a = 2 * ibRatio
uint256 a = factory.auctionMultiplier() * basket.ibRatio();
// b = (bondTimestamp - auctionStart) * 1e14
uint256 b = (bondTimestamp - auctionStart) * BASE / factory.auctionDecrement();
// newRatio = a - b = 2 * ibRatio - (bondTimestamp - auctionStart) * 1e14
uint256 newRatio = a - b;

With our initial assumption of ibRatio = 1e18 and calling bondForRebalance after 11,000 seconds (~3 hours) we will get our result that newRatio is less than the initial ibRatio:

newRatio = a - b = 2 * 1e18 - (11000) * 1e14 = 2e18 - 1.1e18 = 0.9e18 < 1e18 = basket.ibRatio

This seems to be a reasonable value (when the pending tokens and weights are equal in value to the previous ones) as no other bonder would want to call this earlier such when newRatio > basket.ibRatio as they would put in more total value in tokens as they can take out of the basket.

re-enter on settleAuction

The attacker creates a custom token attackerToken that re-enters the Auction.settleAuction function on transferFrom with parameters we will specify.

They call settleAuction with inputTokens = [attackerToken] to re-enter several times.

In the inner-most call where newRatio = 0.9e18, they choose the inputTokens/outputTokens parameters in a way to pass the initial require(IERC20(pendingTokens[i]).balanceOf(address(basket)) >= tokensNeeded); check - transferring out any other tokens of basket with outputTokens.

The function will continue to run and call basket.setNewWeights(); and basket.updateIBRatio(newRatio); which will set the new weights (but not clear the pending ones) and set the new basket.ibRatio.

Execution then jumps to the 2nd inner call after the IERC20(inputTokens[i]=attackerToken).safeTransferFrom(...) and has the chance to transfer out tokens again. It will compute newRatio with the new lowered basket.ibRatio of 0.9e18: newRatio = a - b = 2 * 0.9e18 - 1.1e18 = 0.7e18. Therefore, tokensNeeded is lowered as well and the attacker was allowed to transfer out more tokens having carefully chosen outputWeights.

This repeats with newRatio = 0.3.

The attack is quite complicated and requires carefully precomputing and then setting the parameters, as well as sending back the bondAmount tokens to the auction contract which are then each time transferred back in the function body. But I believe this should work.

Impact

The basket funds can be stolen.

Add re-entrancy checks (for example, OpenZeppelin's "locks") to the settleAuction function.

#0 - GalloDaSballo

2021-12-19T15:25:19Z

Let's dissect the finding to prove whether it's valid or not.

First of all, we do have the pre-conditions for re-entrancy:

  • Allows any token as input (untrusted input)
  • No re-entrancy modifier
  • Checks are performed at the beginning, transfers in the middle, and state changes at the end (violation of check effect interaction pattern)

So in any case this is a report for reEntrancy (medium severity)

However, the warden is showing a specific attack vector that, if proven, allows to steal the majority of funds from the basket.

Let's investigate (with a single re-entrant call example):

The require checks at the top pass, as we're rebalancing the basket, we'll make sure to use an additional inputToken (malicious token), that will call settleAuction again.

This second call (Call B), will execute as normal, extracting the correct amount of value in return for rebalancing, the new ibRatio is 0.9 as shown by the warden POC.

Call B ends by setting ibRatio to 0.9, and hasBonded to false (which will cause a revert if you try to perform this without re-entrancy)

However, we have already entered and Call A can resume, it now has ibRatio set to 0.9 which allows it to further extract value (as tokensNeeded decreases as ibRatio decreases)

This can be extended to have further re-entrant calls and can be effectively executed until the basket is hollowed out

This is a Miro Board to highlight the dynamics of the exploit: https://miro.com/app/board/uXjVOZk4gxw=/?invite_link_id=246345621880

Huge props to the warden, brilliant find!

#1 - GalloDaSballo

2021-12-19T15:28:11Z

For mitigation:

  • Adding a re-entrancy check would be the place to start
  • Requiring the list of input tokens to match the output can also be useful to avoid any other shenanigans
  • Setting the time difference (bondTimestamp, auctionStart) to be 0 would also negate the ability to further manipulate the ibRatio

Findings Information

🌟 Selected for report: jonah1005

Also found by: 0xRajeev, 0xsanson, cmichel, itsmeSTYJ

Labels

bug
duplicate
2 (Med Risk)
sponsor confirmed

Awards

131.4735 USDC - $131.47

External Links

Handle

cmichel

Vulnerability details

The ERC20.approve() function returns a boolean value indicating success. This parameter needs to be checked for success.

The Basket.approveUnderlying function does not check the return value of the IERC20(tokens[i]).approve call. Neither does it work with tokens that don't return a boolean.

Impact

Tokens that don't actually grant the allowance and return false won't work. Also, tokens that don't correctly implement the latest EIP20 spec will not work as they revert the transaction because of the missing return value.

We recommend using OpenZeppelin’s SafeERC20 versions with the safeApprove function that handles the return value check as well as non-standard-compliant tokens.

#0 - frank-beard

2021-09-30T18:44:41Z

duplicate of #260

#1 - GalloDaSballo

2021-12-19T15:53:37Z

Duplicate of #35

AuditHub

A portfolio for auditors, a security profile for protocols, a hub for web3 security.

Built bymalatrax Β© 2024

Auditors

Browse

Contests

Browse

Get in touch

ContactTwitter