PoolTogether contest - cmichel's results

A protocol for no loss prize savings on Ethereum

General Information

Platform: Code4rena

Start Date: 17/06/2021

Pot Size: $60,000 USDC

Total HM: 12

Participants: 12

Period: 7 days

Judge: LSDan

Total Solo HM: 8

Id: 14

League: ETH

PoolTogether

Findings Distribution

Researcher Performance

Rank: 1/12

Findings: 8

Award: $19,268.70

🌟 Selected for report: 10

πŸš€ Solo Findings: 3

Findings Information

🌟 Selected for report: cmichel

Also found by: 0xRajeev

Labels

bug
3 (High Risk)
sponsor confirmed

Awards

2622.1545 USDC - $2,622.15

External Links

Handle

cmichel

Vulnerability details

YearnV2YieldSource._withdrawFromVault uses a wrong subtraction. When withdrawing from the vault one redeems yTokens for tokens, thus the token balance of the contract should increase after withdrawal. But the contract subtracts the currentBalance from the previousBalance:

uint256 yShares = _tokenToYShares(amount);
uint256 previousBalance = token.balanceOf(address(this));
// we accept losses to avoid being locked in the Vault (if losses happened for some reason)
if(maxLosses != 0) {
    vault.withdraw(yShares, address(this), maxLosses);
} else {
    vault.withdraw(yShares);
}
uint256 currentBalance = token.balanceOf(address(this));
// @audit-issue this seems wrong
return previousBalance.sub(currentBalance);

Impact

All vault withdrawals fail due to the integer underflow as the previousBalance is less than currentBalance. Users won't be able to get back their investment.

It should return currentBalance > previousBalance ? currentBalance - previousBalance : 0

Findings Information

🌟 Selected for report: cmichel

Labels

bug
3 (High Risk)
sponsor confirmed
BadgerYieldSource

Awards

5827.0101 USDC - $5,827.01

External Links

Handle

cmichel

Vulnerability details

When suppling to the BadgerYieldSource, some amount of badger is deposited to badgerSett and one receives badgerSett share tokens in return which are stored in the balances mapping of the user. So far this is correct.

The balanceOfToken function should then return the redeemable balance in badger for the user's badgerSett balance. It computes it as the pro-rata share of the user balance (compared to the total-supply of badgerSett) on the badger in the vault:

balances[addr].mul(
  badger.balanceOf(address(badgerSett))
).div(
  badgerSett.totalSupply()
)

However, badger.balanceOf(address(badgerSett)) is only a small amount of badger that is deployed in the vault ("Sett") due to most of the capital being deployed to the strategies. Therefore, it under-reports the actual balance:

Typically, a Sett will keep a small portion of deposited funds in reserve to handle small withdrawals cheaply. Badger Docs

Impact

Any contract or user calling the balanceOf function will receive a value that is far lower than the actual balance. Using this value as a basis for computations will lead to further errors in the integrations.

It should use badgerSett.balance() instead of badger.balanceOf(address(badgerSett)) to also account for "the balance in the Sett, the Controller, and the Strategy".

Findings Information

🌟 Selected for report: cmichel

Labels

bug
3 (High Risk)
sponsor confirmed

Awards

5827.0101 USDC - $5,827.01

External Links

Handle

cmichel

Vulnerability details

One can withdraw the entire PrizePool deposit by circumventing the timelock. Assume the user has no credits for ease of computation:

  • user calls withdrawWithTimelockFrom(user, amount=userBalance) with their entire balance. This "mints" an equivalent amount of timelock and resets _unlockTimestamps[user] = timestamp = blockTime + lockDuration.
  • user calls withdrawWithTimelockFrom(user, amount=0) again but this time withdrawing 0 amount. This will return a lockDuration of 0 and thus unlockTimestamp = blockTime. The inner _mintTimelock now resets _unlockTimestamps[user] = unlockTimestamp
  • As if (timestamp <= _currentTime()) is true, the full users amount is now transferred out to the user in the _sweepTimelockBalances call.

Impact

Users don't need to wait for their deposit to contribute their fair share to the prize pool. They can join before the awards and leave right after without a penalty which leads to significant issues for the protocol. It's the superior strategy but it leads to no investments in the strategy to earn the actual interest.

The unlock timestamp should be increased by duration each time, instead of being reset to the duration.

#0 - asselstine

2021-06-25T00:15:07Z

Mitigation:

If a user's timelock balance is non-zero, the prize strategy rejects the ticket burn.

Findings Information

🌟 Selected for report: shw

Also found by: JMukesh, a_delamo, cmichel, gpersoon

Labels

bug
duplicate
2 (Med Risk)
disagree with severity

Awards

229.3861 USDC - $229.39

External Links

Handle

cmichel

Vulnerability details

The ERC20.transfer() and ERC20.transferFrom() functions return a boolean value indicating success. This parameter needs to be checked for success. Some tokens do not revert if the transfer failed but return false instead.

It is not checked in BadgerYieldSource.supplyTokenTo.

Impact

Tokens that don't actually perform the transfer and return false are still counted as a correct transfer.

As the badger is merely a parameter to the yield source it is not known which token & Sett will end up actually being used.

We recommend using OpenZeppelin’s SafeERC20 versions with the safeTransfer and safeTransferFrom functions that handle the return value check as well as non-standard-compliant tokens.

#0 - asselstine

2021-06-24T23:27:48Z

See https://github.com/code-423n4/2021-06-pooltogether-findings/issues/112

It's more of a 1 (Low Risk) because the subsequent deposit calls will fail. There is no advantage to be gained; the logic is simply poor.

#1 - dmvt

2021-07-31T21:07:46Z

duplicate of #112

Findings Information

🌟 Selected for report: shw

Also found by: cmichel

Labels

bug
duplicate
2 (Med Risk)

Awards

786.6464 USDC - $786.65

External Links

Handle

cmichel

Vulnerability details

BadgerYieldSource.redeemToken: no usage of SafeMath can lead to overflows here as the amount parameter is chosen by the attacker.

amount.mul(totalShares) + totalShares

Impact

It does most likely not have an impact, we still recommend using SafeMath.

Use SafeMath.

#0 - asselstine

2021-06-24T23:21:24Z

#1 - dmvt

2021-08-24T13:32:20Z

duplicate of #114

Findings Information

🌟 Selected for report: cmichel

Labels

bug
2 (Med Risk)
sponsor acknowledged

Awards

1748.103 USDC - $1,748.10

External Links

Handle

cmichel

Vulnerability details

The idea of YieldSourcePrizePool_canAwardExternal seems to be to disallow awarding the interest-bearing token of the yield source, like aTokens, cTokens, yTokens.

"@dev Different yield sources will hold the deposits as another kind of token: such a Compound's cToken. The prize strategy should not be allowed to move those tokens."

However, the code checks _externalToken != address(yieldSource) where yieldSource is the actual yield strategy contract and not the strategy's interest-bearing token. Note that the yieldSource is usually not even a token contract except for ATokenYieldSource and YearnV2YieldSource.

Impact

The _canAwardExternal does not work as expected. It might be possible to award the interest-bearing token which would lead to errors and loss of funds when trying to redeem underlying.

There doesn't seem to be a function to return the interest-bearing token. It needs to be added, similar to depositToken() which retrieves the underlying token.

#0 - asselstine

2021-06-25T22:13:53Z

This is an interesting one:

  • the yield source interface does not require the deposit be tokenized; the implementation is entirely up to the yield source.
  • the _canAwardExternal is a legacy of older code. Since it had to be included it was set to assume the yield source was tokenized.

Since yield sources are audited and analyzed, I think this is a pretty low risk. Additionally, not all of the yield sources are tokenized (Badger and Sushi are not), so it isn't a risk for them.

We could have canAwardExternal on the yield source itself, but it would add gas overhead.

#1 - aodhgan

2021-07-02T16:07:57Z

Could we add an check - function _canAwardExternal(address _externalToken) internal override view returns (bool) { return _externalToken != address(yieldSource) && _externalToken != address(yieldSource.depositToken()) }

#2 - asselstine

2021-07-05T21:47:00Z

We could add another check, but it's still arbitrary. The point is that the yield source knows what token the prize pool may or may not hold, so without asking the yield source it's just a guess.

Let's leave it as-is

Findings Information

🌟 Selected for report: cmichel

Labels

bug
1 (Low Risk)
sponsor confirmed
ATokenYieldSource
SushiYieldSource
BadgerYieldSource
PrizePool
ControlledToken
StakePrizePool

Awards

582.701 USDC - $582.70

External Links

Handle

cmichel

Vulnerability details

Some parameters of functions are not checked for invalid values:

  • StakePrizePool.initialize: address _stakeToken not checked for non-zero or contract
  • ControlledToken.initialize: address controller not checked for non-zero or contract
  • PrizePool.withdrawReserve: address to not checked for non-zero, funds will be lost when sending to zero address
  • ATokenYieldSource.initialize: address _aToken, _lendingPoolAddressesProviderRegistry not checked for non-zero or contract
  • BadgerYieldSource.initialize: address badgerSettAddr, badgerAddr not checked for non-zero or contract
  • SushiYieldSource.constructor: address _sushiBar, _sushiAddr not checked for non-zero or contract

Impact

Wrong user input or wallets defaulting to the zero addresses for a missing input can lead to the contract needing to redeploy or wasted gas.

Validate the parameters.

#0 - PierrickGT

2021-06-28T17:51:28Z

#1 - PierrickGT

2021-07-01T14:50:44Z

#2 - PierrickGT

2021-07-01T16:10:25Z

#3 - PierrickGT

2021-07-01T16:35:15Z

#4 - PierrickGT

2021-07-01T18:08:32Z

#5 - PierrickGT

2021-07-02T07:53:19Z

@asselstine I'm not sure we want to check for non zero address in the PrizePool withdrawReserve function since this function is only callable by the Reserve and the owner of the Reserve contract. https://github.com/pooltogether/pooltogether-pool-contracts/blob/192429c808ad9714e9e05821386eb926150a009f/contracts/reserve/Reserve.sol#L32 https://github.com/pooltogether/pooltogether-pool-contracts/blob/4449bb2e4216511b7187b1ab420118c30af39eb7/contracts/prize-pool/PrizePool.sol#L473

#6 - asselstine

2021-07-05T23:07:40Z

Yeah @PierrickGT I don't think the withdrawReserve needs to do the check. Many tokens reject on transfer to zero anyway.

#7 - kamescg

2021-07-08T22:43:17Z

LGTM

Findings Information

🌟 Selected for report: cmichel

Labels

bug
1 (Low Risk)
sponsor confirmed
ATokenYieldSource

Awards

582.701 USDC - $582.70

External Links

Handle

cmichel

Vulnerability details

The ATokenYieldSource.redeemToken function burns aTokens and sends out underlying, however, it's used in a reverse way in the code: The balanceDiff is used as the depositToken that is transferred out but it's computed on the aTokens that were burned instead of on the depositToken received.

Impact

It should not directly lead to issues as aTokens are 1-to-1 with their underlying but we still recommend doing it correctly to make the code more robust against any possible rounding issues.

Compute balanceDiff on the underyling balance (depositToken), not on the aToken. Subtract the actual burned aTokens from the user shares.

#0 - PierrickGT

2021-06-28T12:24:58Z

I agree that we should compute balanceDiff on the underlying balance. Regarding the burn of the user's shares, we should keep it as is to verify first that the user has enough shares. This way if he doesn't, the code execution will revert before funds are withdrawn from Aave.

#1 - PierrickGT

2021-06-28T12:37:16Z

Findings Information

🌟 Selected for report: 0xRajeev

Also found by: cmichel, gpersoon, shw

Labels

bug
duplicate
G (Gas Optimization)

Awards

54.8467 USDC - $54.85

External Links

Handle

cmichel

Vulnerability details

In BadgerYieldSource.supplyTokenTo, the badgerSett variable is accessed four times. If each of these accesses corresponds to an SLOAD, it'll be better to cache the value once.

Findings Information

🌟 Selected for report: cmichel

Also found by: pauliax

Labels

bug
G (Gas Optimization)
sponsor confirmed
SushiYieldSource

Awards

135.4239 USDC - $135.42

External Links

Handle

cmichel

Vulnerability details

SushiYieldSource should approve the SushiBar once during initialization with the max value. This saves gas on every supplyTokenTo call as the approval can be removed from there.

#0 - asselstine

2021-06-25T21:58:01Z

We considered this, but it's possible for a malicious user to "drain" the approval of the contract, so there would need to be checks to see if approval dropped below a certain level. We opted to leave out the complexity.

#1 - asselstine

2021-06-26T17:54:37Z

Actually, we'll tackle this. We will:

  1. approve max on init
  2. provide a function to approve max again

Findings Information

🌟 Selected for report: cmichel

Labels

bug
G (Gas Optimization)
sponsor confirmed
ATokenYieldSource
IdleYieldSource

Awards

300.9419 USDC - $300.94

External Links

Handle

cmichel

Vulnerability details

ATokenYieldSource should approve the lending contract once during initialization with the max value. This saves gas on every supplyTokenTo/_depositToAave call as the approval can be removed from there.

#0 - asselstine

2021-06-25T21:31:43Z

We considered this, but it's possible for a malicious user to "drain" the approval of the contract, so there would need to be checks to see if approval dropped below a certain level. We opted to leave out the complexity.

#1 - asselstine

2021-06-26T17:54:55Z

Actually, we'll tackle this. We will:

  1. approve max on init
  2. provide a function to approve max again

#2 - PierrickGT

2021-06-29T16:24:02Z

Findings Information

🌟 Selected for report: cmichel

Labels

bug
G (Gas Optimization)
sponsor confirmed

Awards

300.9419 USDC - $300.94

External Links

Handle

cmichel

Vulnerability details

The credit is accrued twice in award. The first accrual happens implicitly when calling _mint through the ControlledToken(controlledToken).controllerMint call which then performs the PrizePool.beforeTokenTransfer hook which accrues credit. Then the explicit accrual is done again. It should be enough to only add the extraCredit without doing another accrual (calling _updateCreditBalance(..., newBalance= _applyCreditLimit(controlledToken, controlledTokenBalance, uint256(creditBalance.balance).add(credit).add(extra))) instead).

#0 - asselstine

2021-06-25T21:57:12Z

We could:

  • remove the credit accrual from the beforeTokenTransfer for minting
  • Call _accrueCredit everywhere that _mint is called, and for award ensure that we call it before mint and with the extra credit

Findings Information

🌟 Selected for report: cmichel

Also found by: pauliax

Labels

bug
G (Gas Optimization)
sponsor confirmed

Awards

135.4239 USDC - $135.42

External Links

Handle

cmichel

Vulnerability details

In PrizePool._updateCreditBalance the CreditBurned event is emitted even if nothing was burned. Not emitting this event when nothing happened can save gas and also seems better semantically.

Findings Information

🌟 Selected for report: axic

Also found by: cmichel

Labels

bug
duplicate
G (Gas Optimization)
disagree with severity
sponsor acknowledged

Awards

135.4239 USDC - $135.42

External Links

Handle

cmichel

Vulnerability details

The YieldSourcePrizePool.initializeYieldSourcePrizePool should use EIP-165 to detect valid yield sources instead of the "hack" with the depositToken function.

// A hack to determine whether it's an actual yield source
(bool succeeded,) = address(_yieldSource).staticcall(abi.encode(_yieldSource.depositToken.selector));
require(succeeded, "YieldSourcePrizePool/invalid-yield-source");

Impact

It's better to detect and check for the entire yield source interface instead of just the depositToken function as many contracts have a similar function.

Use EIP-165.

#0 - asselstine

2021-06-25T22:19:43Z

#1 - dmvt

2021-08-24T13:29:14Z

duplicate of #104

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