Platform: Code4rena
Start Date: 03/02/2022
Pot Size: $75,000 USDC
Total HM: 42
Participants: 52
Period: 7 days
Judge: GalloDaSballo
Total Solo HM: 21
Id: 83
League: ETH
Rank: 16/52
Findings: 5
Award: $1,275.51
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: GalloDaSballo
https://github.com/code-423n4/2022-02-concur/blob/72b5216bfeaa7c52983060ebfc56e72e0aa8e3b0/contracts/USDMPegRecovery.sol#L90-L108 https://github.com/code-423n4/2022-02-concur/blob/72b5216bfeaa7c52983060ebfc56e72e0aa8e3b0/contracts/USDMPegRecovery.sol#L110-L128
Curve.fi pools charge fees when adding or removing liquidity. The only time fees are not applied are when withdrawals are done using remove_liquidity()
. USDMPegRecovery
keeps track of tokens deposited and withdrawn, but does not keep track of fees assessed on these operations. The pool in use by this token assesses a 0.040% fee as well as a 50.000% of 0.040% admin fee.
A whale, or a user/miner using same-block cross-transaction flashloans, can repeatedly deposit and withdraw a large number of tokens, which causes large dollar-value fees to be assessed by the pool, but the number of tokens the user gets is unaffected. The attacker can do this repeatedly to drain all contract funds.
These are two separate instances of the issue, as the attacker only needs to exploit one of the functions, then wait for the guardian to call add_liquidity()
or remove_liquidity()
. The exploits can be applied separately to each of the two tokens.
function deposit(Liquidity calldata _deposits) external { Liquidity memory total = totalLiquidity; Liquidity memory user = userLiquidity[msg.sender]; if(_deposits.usdm > 0) { usdm.safeTransferFrom(msg.sender, address(this), uint256(_deposits.usdm)); total.usdm += _deposits.usdm; user.usdm += _deposits.usdm; } if(_deposits.pool3 > 0) { require(totalLiquidity.usdm > 4000000e18, "usdm low"); pool3.safeTransferFrom(msg.sender, address(this), uint256(_deposits.pool3)); total.pool3 += _deposits.pool3; user.pool3 += _deposits.pool3; } totalLiquidity = total; userLiquidity[msg.sender] = user; emit Deposit(msg.sender, _deposits); }
function withdraw(Liquidity calldata _withdrawal) external { Liquidity memory total = totalLiquidity; Liquidity memory user = userLiquidity[msg.sender]; if(_withdrawal.usdm > 0) { require(unlockable, "!unlock usdm"); usdm.safeTransfer(msg.sender, uint256(_withdrawal.usdm)); total.usdm -= _withdrawal.usdm; user.usdm -= _withdrawal.usdm; } if(_withdrawal.pool3 > 0) { pool3.safeTransfer(msg.sender, uint256(_withdrawal.pool3)); total.pool3 -= _withdrawal.pool3; user.pool3 -= _withdrawal.pool3; } totalLiquidity = total; userLiquidity[msg.sender] = user; emit Withdraw(msg.sender, _withdrawal); }
Code inspection
Keep track of fees and reduce deposits/withdrawals accordingly
#0 - r2moon
2022-02-18T01:13:56Z
duplicated with https://github.com/code-423n4/2022-02-concur-findings/issues/188
#1 - GalloDaSballo
2022-04-19T01:17:25Z
Duplicate of #70
215.0546 USDC - $215.05
https://github.com/code-423n4/2022-02-concur/blob/72b5216bfeaa7c52983060ebfc56e72e0aa8e3b0/contracts/ConvexStakingWrapper.sol#L243 https://github.com/code-423n4/2022-02-concur/blob/72b5216bfeaa7c52983060ebfc56e72e0aa8e3b0/contracts/ConvexStakingWrapper.sol#L267 https://github.com/code-423n4/2022-02-concur/blob/72b5216bfeaa7c52983060ebfc56e72e0aa8e3b0/contracts/StakingRewards.sol#L121 https://github.com/code-423n4/2022-02-concur/blob/02d286253cd5570d4e595527618366f77627cdaf/contracts/StakingRewards.sol#L174 https://github.com/code-423n4/2022-02-concur/blob/72b5216bfeaa7c52983060ebfc56e72e0aa8e3b0/contracts/ConcurRewardPool.sol#L37 https://github.com/code-423n4/2022-02-concur/blob/72b5216bfeaa7c52983060ebfc56e72e0aa8e3b0/contracts/Shelter.sol#L57
Some ERC20 tokens, such as USDT, allow for charging a fee any time transfer()
or transferFrom()
is called. If a contract does not allow for amounts to change after transfers, subsequent transfer operations based on the original amount will revert()
due to the contract having an insufficient balance.
The current implementation does not work properly with fee-on-transfer tokens when depositing/withdrawing lp tokens, depositing/withdrawing rewards, and withdrawing tokens from the shelter, whereas the code from which these changes were forked is able to handle them.
The following calls are separate instances of this issue where the call may suddenly start reverting for a fee-on-transfer token when the fee is turned on right before the function call, or has bee on prior to the call: https://github.com/code-423n4/2022-02-concur/blob/72b5216bfeaa7c52983060ebfc56e72e0aa8e3b0/contracts/ConvexStakingWrapper.sol#L243 https://github.com/code-423n4/2022-02-concur/blob/72b5216bfeaa7c52983060ebfc56e72e0aa8e3b0/contracts/ConvexStakingWrapper.sol#L267 https://github.com/code-423n4/2022-02-concur/blob/72b5216bfeaa7c52983060ebfc56e72e0aa8e3b0/contracts/StakingRewards.sol#L121 https://github.com/code-423n4/2022-02-concur/blob/02d286253cd5570d4e595527618366f77627cdaf/contracts/StakingRewards.sol#L174 https://github.com/code-423n4/2022-02-concur/blob/72b5216bfeaa7c52983060ebfc56e72e0aa8e3b0/contracts/ConcurRewardPool.sol#L37 https://github.com/code-423n4/2022-02-concur/blob/72b5216bfeaa7c52983060ebfc56e72e0aa8e3b0/contracts/Shelter.sol#L57
Code inspection
One way to mitigate the issue is to measure the contract's balance right before and after the asset-transferring functions.
#0 - GalloDaSballo
2022-04-20T16:14:07Z
Dup of #180
🌟 Selected for report: ShadowyNoobDev
Also found by: 0xw4rd3n, CertoraInc, Czar102, GalloDaSballo, Heartless, IllIllI, Jujic, Randyyy, Rhynorater, Sleepy, SolidityScan, ckksec, kirk-baird, leastwood, pauliax, peritoflores, reassor
7.97 USDC - $7.97
Some ERC20 tokens have code that lets their users register callbacks to be triggered when that user's account has the token sent to it. One such standard is ERC777 which is backwards-compatible with ERC20 code. An example of an ERC777 token is imBTC, which was used in the dForce hack where the attacker was able to use reentrancy to drain $25 million. This vulnerability also involves tokens that support recipient callback and involves reentrancy, but does not allow the hacker to exfiltrate more than rewards pushed but not claimed, in a specific pool that has a reward token with callbacks.
Using a reentrancy bug, an attacker is able to drain the rewards already pushed, but not claimed by reward recipients, to the ConcurRewardPool, for any pool whose reward token has recipient callbacks. I've marked this as 'medium' because there are not currently any pools with ERC777 token rewards, though users can create such pools at any time by creating a factory pool.
function claimRewards(address[] calldata _tokens) external override { for (uint256 i = 0; i < _tokens.length; i++) { uint256 getting = reward[msg.sender][_tokens[i]]; IERC20(_tokens[i]).safeTransfer(msg.sender, getting); reward[msg.sender][_tokens[i]] = 0; } }
The reward
map for the msg.sender
is only set to zero after safeTransfer()
is called. If _tokens[i]
is an ERC777 token, the attacker can register a recipient callback which calls claimRewards()
recursively, allowing the attacker to drain all of the _tokens[i]
that the contract holds. Because the ConcurRewardPool
holds all rewards, rather than segregating them per-pool or per-account, the attacker is able to drain the rewards of a specific token, for all pools and all accounts. The attacker is able to do the attacker each time he/she is given a reward, and can choose to execute it or not, based on monitoring the blockchain for Withdraw
events emitted for MasterChef
for pools of interest.
Code inspection
Set the reward to zero before calling safeTransfer()
#0 - r2moon
2022-02-18T01:37:56Z
duplicated with https://github.com/code-423n4/2022-02-concur-findings/issues/260
#1 - GalloDaSballo
2022-04-17T16:24:15Z
Duplicate of #86
🌟 Selected for report: hickuphh3
Also found by: 0x1f8b, 0xw4rd3n, BouSalman, CertoraInc, Czar102, Dravee, IllIllI, Randyyy, Rhynorater, Ruhum, ShadowyNoobDev, Sleepy, SolidityScan, WatchPug, bitbopper, cccz, cryptphi, csanuragjain, defsec, gzeon, harleythedog, hubble, hyh, kenta, kirk-baird, leastwood, mtz, pauliax, peritoflores, rfa, robee, samruna, throttle, wuwe1, ye0lde
270.7635 USDC - $270.76
Title: Public function massUpdatePools()
can be broken by a malicious token
If a malicious user adds a factory pool with a custom erc20 token where the token reverts if any of its functions are called from the MasterChef
address, any time this public function is called it will revert.
The public function can be made to revert every time, which impacts the function of the protocol. I'm not sure if this is Low or Medium, so I'm submitting this as my QA report and leaving it up to the judge. Please elaborate on the rationale so I can tag future reports correctly.
function massUpdatePools() public { uint length = poolInfo.length; for (uint _pid = 0; _pid < length; ++_pid) { updatePool(_pid); } } // Update reward variables of the given pool to be up-to-date. function updatePool(uint _pid) public { PoolInfo storage pool = poolInfo[_pid]; if (block.number <= pool.lastRewardBlock) { return; } uint lpSupply = pool.depositToken.balanceOf(address(this)); if (lpSupply == 0 || pool.allocPoint == 0) { pool.lastRewardBlock = block.number; return; } if(block.number >= endBlock) { pool.lastRewardBlock = block.number; return; } uint multiplier = getMultiplier(pool.lastRewardBlock, block.number); uint concurReward = multiplier.mul(concurPerBlock).mul(pool.allocPoint).div(totalAllocPoint); pool.accConcurPerShare = pool.accConcurPerShare.add(concurReward.mul(_concurShareMultiplier).div(lpSupply)); pool.lastRewardBlock = block.number; }
The balanceOf()
function can be made to revert every time, which means the mass update will always fail. Any individual pool with a malicious token has its own updatePool()
calls broken.
Code inspection
Use try-catch when iterating over all pools in a single function
#0 - GalloDaSballo
2022-04-26T20:50:25Z
Judging of this finding is fairly nuanced and in a different context could be interpreted differently.
Ultimately a big deciding factor for me is that having already went through hundreds of other findings, I know that massUpdatePools
is not used (a vulnerability in it of itself, for other reasons)
But because I know that, I also know that you can't DOS the entire reward system as a malicious pool with a reverting token would just brick itself.
Technically even the pool with address(0) (which afik lacks the fallback function) would cause reverts in massUpdatePool.
Either way, I believe the warden has found a very interesting type of Admin Privilege which ultimately would allow the pool math to be broken, interestingly enough the admin would need to be "generous" enough to create a stacking pool and then malicious enough to set a token that reverts (or doesn't exist)
In lack of the remove
function any pool added cannot be revoked, so that should also be taken into consideration.
All in all, with a different codebase I may have evaluated this differently, but in this case the codebase has other flaws that make it so that if the admin did set the wrong pool, while there would be no way to remove it, people could still sidestep it, withdraw from it and ultimately have a new pool set up with proper reward math.
So for that reason (easily sidesteppable, main focus of DOS denied because of other vulnerabilities) I believe the finding to be very interesting, valid but ultimately of Low Severity
#1 - GalloDaSballo
2022-04-26T20:51:09Z
Adding #165 will give it 3++ as the finding here is unique
🌟 Selected for report: WatchPug
Also found by: 0x0x0x, 0x1f8b, 0x510c, 0xNot0rious, 0xngndev, BouSalman, CertoraInc, Dravee, Heartless, IllIllI, Jujic, Randyyy, Ruhum, ShadowyNoobDev, Sleepy, SolidityScan, Tomio, bitbopper, csanuragjain, defsec, gzeon, hickuphh3, kenta, mtz, pauliax, peritoflores, rfa, robee, sabtikw, throttle, wuwe1, ye0lde
64.8796 USDC - $64.88
> 0
costs more gas than != 0
when use in a require()
statement. The project has the following instances of this issue:
https://github.com/code-423n4/2022-02-concur/blob/72b5216bfeaa7c52983060ebfc56e72e0aa8e3b0/contracts/MasterChef.sol#L186
https://github.com/code-423n4/2022-02-concur/blob/72b5216bfeaa7c52983060ebfc56e72e0aa8e3b0/contracts/StakingRewards.sol#L94
https://github.com/code-423n4/2022-02-concur/blob/72b5216bfeaa7c52983060ebfc56e72e0aa8e3b0/contracts/StakingRewards.sol#L108
#0 - r2moon
2022-02-15T15:23:48Z
duplicated with https://github.com/code-423n4/2022-02-concur-findings/issues/265
#1 - GalloDaSballo
2022-04-04T15:03:18Z
18 gas saved