Platform: Code4rena
Start Date: 09/12/2022
Pot Size: $36,500 USDC
Total HM: 9
Participants: 69
Period: 3 days
Judge: Picodes
Total Solo HM: 2
Id: 190
League: ETH
Rank: 1/69
Findings: 3
Award: $3,864.68
🌟 Selected for report: 1
🚀 Solo Findings: 1
🌟 Selected for report: csanuragjain
3783.7224 USDC - $3,783.72
User can bypass the userWithdrawLimitPerPeriod
check by transferring balance to another account
userWithdrawLimitPerPeriod
is set to 1000
2000
and wants to withdraw everything instantly1000
amountfunction withdraw(uint256 _amount) external override nonReentrant { uint256 _baseTokenAmount = (_amount * baseTokenDenominator) / 1e18; uint256 _fee = (_baseTokenAmount * withdrawFee) / FEE_DENOMINATOR; if (withdrawFee > 0) { require(_fee > 0, "fee = 0"); } else { require(_baseTokenAmount > 0, "amount = 0"); } _burn(msg.sender, _amount); uint256 _baseTokenAmountAfterFee = _baseTokenAmount - _fee; if (address(withdrawHook) != address(0)) { baseToken.approve(address(withdrawHook), _fee); withdrawHook.hook(msg.sender, _baseTokenAmount, _baseTokenAmountAfterFee); baseToken.approve(address(withdrawHook), 0); } baseToken.transfer(msg.sender, _baseTokenAmountAfterFee); emit Withdraw(msg.sender, _baseTokenAmountAfterFee, _fee); }
1000
amount cannot be withdrawn since userWithdrawLimitPerPeriod
is reachedfunction hook( address _sender, uint256 _amountBeforeFee, uint256 _amountAfterFee ) external override onlyCollateral { ... require(userToAmountWithdrawnThisPeriod[_sender] + _amountBeforeFee <= userWithdrawLimitPerPeriod, "user withdraw limit exceeded"); ... }
User simply transfer his balance to his another account and withdraw from that account
Since withdraw limit is tied to account so this new account will be allowed to make withdrawal thus bypassing userWithdrawLimitPerPeriod
User should only be allowed to transfer leftover limit. For example if User already utilized limit X then he should only be able to transfer userWithdrawLimitPerPeriod-X
#0 - c4-sponsor
2022-12-20T02:21:31Z
ramenforbreakfast marked the issue as sponsor confirmed
#1 - c4-judge
2023-01-07T11:10:36Z
Picodes marked the issue as satisfactory
🌟 Selected for report: obront
Also found by: 8olidity, HE1M, Madalad, Trust, cccz, csanuragjain, deliriusz, hansfriese, hihen, joestakey, rvierdiiev, wait, zaskoh
52.8446 USDC - $52.84
It seems MANAGER_WITHDRAW_ROLE
can steal entire user balance if SET_MIN_RESERVE_PERCENTAGE_ROLE
has set setMinReservePercentage
to 0%
SET_MIN_RESERVE_PERCENTAGE_ROLE
role sets minReservePercentage
to 0%function setMinReservePercentage(uint256 _newMinReservePercentage) external override onlyRole(SET_MIN_RESERVE_PERCENTAGE_ROLE) { require(_newMinReservePercentage <= PERCENT_DENOMINATOR, ">100%"); minReservePercentage = _newMinReservePercentage; emit MinReservePercentageChange(_newMinReservePercentage); }
MANAGER_WITHDRAW_ROLE
role simply calls the managerWithdraw
with contract balance as _amount
function managerWithdraw(uint256 _amount) external override onlyRole(MANAGER_WITHDRAW_ROLE) nonReentrant { if (address(managerWithdrawHook) != address(0)) managerWithdrawHook.hook(msg.sender, _amount, _amount); baseToken.transfer(manager, _amount); }
hook
which allow to withdraw everything above reservefunction hook( address, uint256, uint256 _amountAfterFee ) external view override { require( collateral.getReserve() - _amountAfterFee >= getMinReserve(), "reserve would fall below minimum" ); }
getMinReserve
is implemented as below. Since minReservePercentage is 0 so getMinReserve becomes 0function getMinReserve() public view override returns (uint256) { return (depositRecord.getGlobalNetDepositAmount() * minReservePercentage) / PERCENT_DENOMINATOR; }
collateral.getReserve() - _amountAfterFee
which is approx entire balance (without fees)function getReserve() external view override returns (uint256) { return baseToken.balanceOf(address(this)); }
Do not allow SET_MIN_RESERVE_PERCENTAGE_ROLE
to set setMinReservePercentage
to 0
#0 - hansfriese
2022-12-14T18:22:42Z
duplicate of #254
#1 - c4-judge
2022-12-17T10:08:00Z
Picodes marked the issue as duplicate of #254
#2 - c4-judge
2023-01-01T17:44:21Z
Picodes marked the issue as satisfactory
🌟 Selected for report: 0xSmartContract
Also found by: 0Kage, 0x52, 0xAgro, 0xNazgul, 0xTraub, 0xhacksmithh, Awesome, Aymen0909, Bnke0x0, Englave, Janio, Mukund, Parth, RaymondFam, Rolezn, SmartSek, Tointer, UdarTeam, Udsen, Zarf, caventa, chaduke, csanuragjain, deliriusz, gz627, idkwhatimdoing, izhelyazkov, joestakey, neumo, obront, oyc_109, rvierdiiev, shark, trustindistrust, wait, yongskiws
28.124 USDC - $28.12
Issue: While depositing contract is making use of transferFrom function which is insecure and the function fails to check the outcome of the transferFrom function (whether it was success or not)
function deposit(address _recipient, uint256 _amount) external override nonReentrant returns (uint256) { uint256 _fee = (_amount * depositFee) / FEE_DENOMINATOR; if (depositFee > 0) { require(_fee > 0, "fee = 0"); } else { require(_amount > 0, "amount = 0"); } baseToken.transferFrom(msg.sender, address(this), _amount); ... }
Recommendation: Use safeTransferFrom which checks the outcome of the transfer and fails if transfer was not success
#0 - c4-judge
2022-12-19T14:31:24Z
Picodes marked the issue as grade-b
#1 - ramenforbreakfast
2022-12-21T23:05:23Z
This entry should be disregarded due quality of submission (just a mention of using safe transfer)
#2 - c4-sponsor
2022-12-21T23:39:26Z
ramenforbreakfast marked the issue as sponsor disputed
#3 - Picodes
2023-01-07T18:20:24Z
@ramenforbreakfast note that the grade also takes into account duplicate QA reports, that's why I gave grade-b