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: 18/69
Findings: 2
Award: $449.07
š Selected for report: 0
š Solo Findings: 0
š 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
Judge has assessed an item in Issue #334 as M risk. The relevant finding follows:
Collateral.withdraw allows the manager to withdraw an arbitrary _amount of baseToken from Collateral.
The only check is in the ManagerWithdrawHook.hook call, where it checks the withdrawal does not drop the amount of _baseToken below (depositRecord.getGlobalNetDepositAmount() * minReservePercentage) / PERCENT_DENOMINATOR
A manager and the account holding SET_MIN_RESERVE_PERCENTAGE_ROLE can perform the following steps:
1 - call ManagerWithdrawHook.setMinReservePercentage(PERCENT_DENOMINATOR) 2 - call Collateral.managerWithdraw(baseToken.balanceOf(address(this)))
And rug all the baseToken in Collateral, stealing from depositors who are then unable to withdraw the baseToken they are entitled to.
The reason this attack is currently possible is because Collateral.deposit does not directly send the tokens to StrategyController. After a user deposit, the tokens are sitting in Collateral until the tokens are transferred to StrategyController, making this attack possible.
Recommendation Looking at the deposit flow diagram, a possible mitigation is to make the deposit process atomic, ensuring tokens are deposited into the Strategy in the same transaction as the users deposit into Collateral, to make the attack impossible (or at least lower its impact).
#0 - c4-judge
2022-12-19T13:41:46Z
Picodes marked the issue as duplicate of #254
#1 - c4-judge
2023-01-07T18:33:31Z
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
396.2326 USDC - $396.23
Issue | |
---|---|
L-01 | Collateral withdrawal process does not match the documentation |
L-02 | Use of draft dependencies |
L-03 | Rugging vector in Collateral |
L-04 | Users will be permanently blocked from depositing into Collateral after a certain threshold |
L-05 | _floorValuation and _ceilingValuation are not validated |
L-06 | AccountList manager can prevent users from redeeming Long and Short tokens |
L-07 | TokenSender.send amount sent to the recipient can be arbitrarily modified by the owner of FixedUintValue |
L-08 | TokenSender.send amount sent to the recipient can be arbitrarily modified by the SET_PRICE_MULTIPLIER_ROLE role |
L-09 | TokenSender.send amount sent to the recipient can be arbitrarily modified by the SET_SCALED_PRICE_LOWER_BOUND_ROLE role |
L-10 | SET_WITHDRAWALS_ALLOWED_ROLE can block withdrawals in Collateral at any time. |
Collateral
withdrawal process does not match the documentationUsers can deposit and withdraw from Collateral
.
As per the documentation:
Creates a request to allow a withdrawal for amount Collateral in a later block.
Looking at the sequence diagram, the withdrawal is a 2-step process, where a user should first call initiateWithdrawal
, before calling withdraw
in a future block.
The issue is that this is not reflected in the code:
initiateWithdrawal
function in Collateral
withdraw
and withdraw their baseToken
, without having to wait for delayedWithdrawalExpiry
.Amend the documentation to match the current contract behavior, or make the necessary changes in Collateral
to match the docs.
Collateral
uses the ERC20PermitUpgradeable.sol
from OpenZeppelin
, which is a draft contract.
Draft contracts may change with future developments and are not considered ready for use.
5: import "@openzeppelin/contracts-upgradeable/token/ERC20/extensions/draft-ERC20PermitUpgradeable.sol"; //@audit - L: use of draft
Collateral
Collateral.withdraw
allows the manager
to withdraw an arbitrary _amount
of baseToken
from Collateral
.
The only check is in the ManagerWithdrawHook.hook
call, where it checks the withdrawal does not drop the amount of _baseToken
below (depositRecord.getGlobalNetDepositAmount() * minReservePercentage) / PERCENT_DENOMINATOR
A manager
and the account holding SET_MIN_RESERVE_PERCENTAGE_ROLE
can perform the following steps:
1 - call ManagerWithdrawHook.setMinReservePercentage(PERCENT_DENOMINATOR)
2 - call Collateral.managerWithdraw(baseToken.balanceOf(address(this)))
And rug all the baseToken
in Collateral
, stealing from depositors who are then unable to withdraw
the baseToken
they are entitled to.
The reason this attack is currently possible is because Collateral.deposit
does not directly send the tokens to StrategyController
. After a user deposit, the tokens are sitting in Collateral
until the tokens are transferred to StrategyController
, making this attack possible.
Looking at the deposit flow diagram, a possible mitigation is to make the deposit process atomic, ensuring tokens are deposited into the Strategy
in the same transaction as the users deposit into Collateral
, to make the attack impossible (or at least lower its impact).
depositing
into Collateral
after a certain thresholdUpon withdrawing from Collateral
, DepositRecord.recordWithdrawal
is called to update the deposit states.
But it does not lower the user deposit states, only the global one.
The issue is that this check prevents users from depositing if userToDeposits[user]
exceeds userDepositCap
Because userToDeposits[user]
is not updated during a withdrawal, this means that there will be a point where a user will not be able to deposit anymore - once their total historical deposit reaches userDepositCap
.
This does impact the protocol negatively, as it poses an inconvenience on users. Users can simply use another EOA, but for smart contract wallets/multi-sigs, this means having to deal with the burden of re-deploying a new wallet and the logistics associated with it.
Subtract the _amount
from userToDeposits[_sender]
in DepositRecord.recordWithdrawal
.
You will also need to change the definition of DepositRecord.recordWithdrawal
to include a _sender
parameter.
This will also make the code match the documentation, which specifies:
finalAmount is subtracted from both the global and account-specific deposit totals.
Note that IDepositRecord
describes on the other hand that recordWithdrawal
does not deduct from msg.sender
's deposit total.
It is not clear which is the intended behavior, but for the reasons enounced above, recordWithdrawal
should update the account-specific deposit totals.
_floorValuation
and _ceilingValuation
are not validatedThe PrePOMarket
constructor validates most parameters, but does not do so for _floorValuation
and _ceilingValuation
.
More specifically, it does not check _ceilingValuation > _floorValuation
.
Ensure all parameters are validated:
44: require(_expiryTime > block.timestamp, "Invalid expiry"); 45: require(_ceilingLongPayout <= MAX_PAYOUT, "Ceiling cannot exceed 1"); + require(_ceilingValuation > _floorValuation, "wrong valuations");
PrePOMarket.redeem
allows users to redeem longAmount
Long and shortAmount
Short tokens for collateral.
The function makes a call to redeemHook.hook
, which checks the user is in the AccountList.
A malicious AccountList manager can front run a user's call to PrePOMarket.redeem
by calling AccountList.set
to remove the user from this list, making the call to redeem revert.
The user cannot retrieve their collateral.
Remove the _accountList.isIncluded(sender)
check from RedeemHook.hook
.
Note that there is no such check in Collateral.withdraw
: the redeeming process in PrePOMarket
should also get rid of it.
TokenSender.send
amount sent to the recipient can be arbitrarily modified by the owner of FixedUintValue
TokenSender.send
is called upon deposit and withdrawal in Collateral
, to send a user their reimbursement of fee in PPO
.
The issue is that amount is computed based on the value returned in _price.get()
.
This value can be arbitrarily modified in FixedUintValue.set()
by the owner.
If set high enough:
37: uint256 scaledPrice = (_price.get() * _priceMultiplier) / MULTIPLIER_DENOMINATOR; 38: if (scaledPrice <= _scaledPriceLowerBound) return; 39: uint256 outputAmount = (unconvertedAmount * _outputTokenDecimalsFactor) / scaledPrice;
scaledPrice
will be high enough, leading to outputAmount
being truncated and equal 0
.
The depositor does not receive any PPO
.
Consider using an oracle instead of an arbitrary value for the PPO
price in TokenSender.send
TokenSender.send
amount sent to the recipient can be arbitrarily modified by the SET_PRICE_MULTIPLIER_ROLE
roleThis is similar to the previous issue, but it concerns another variable.
TokenSender.send
is called upon deposit and withdrawal in Collateral
, to send a user their reimbursement of fee in PPO
.
The issue is that amount is computed based on the value returned in _priceMultiplier
, which can be arbitrarily modified in by the SET_PRICE_MULTIPLIER_ROLE
role.
If set high enough:
37: uint256 scaledPrice = (_price.get() * _priceMultiplier) / MULTIPLIER_DENOMINATOR; 38: if (scaledPrice <= _scaledPriceLowerBound) return; 39: uint256 outputAmount = (unconvertedAmount * _outputTokenDecimalsFactor) / scaledPrice;
scaledPrice
will be high enough, leading to outputAmount
being truncated and equal 0
.
The depositor does not receive any PPO
.
The problem is that there is a mismatch between _priceMuliplier
, that can be modified, and MULTIPLIER_DENOMINATOR
, which is a constant.
_priceMultiplier
should be immutable.
TokenSender.send
amount sent to the recipient can be arbitrarily modified by the SET_SCALED_PRICE_LOWER_BOUND_ROLE
roleThis is similar to the previous two issues, but it concerns another variable.
TokenSender.send
is called upon deposit and withdrawal in Collateral
, to send a user their reimbursement of fee in PPO
.
The issue is that if the computed scalePrice
is lower than _scaledPriceLowerBound
, the user does not receive any PPO
.
As this variable can be set to any value in setScaledPriceLowerBound by SET_SCALED_PRICE_LOWER_BOUND_ROLE
, the latter can at any moment prevent users from receiving their reimbursement of fees in PPO
.
setScaledPriceLowerBound
should have appropriate boundaries.
SET_WITHDRAWALS_ALLOWED_ROLE
can block withdrawals in Collateral
at any time.Users can deposit and withdraw from Collateral
.
As per the documentation, users can
create a request to allow a withdrawal in a later block
The issue is that this additional check makes it possible for the SET_WITHDRAWALS_ALLOWED_ROLE
role to DOS withdrawals at any time, by calling setWithdrawalsAllowed(false).
Either remove this function, or if deemed necessary for strategy reasons, consider adding a timelock to it.
#0 - c4-judge
2022-12-19T13:42:36Z
Picodes marked the issue as grade-a
#1 - c4-sponsor
2022-12-22T10:54:07Z
davidprepo marked the issue as sponsor disputed
#2 - ramenforbreakfast
2022-12-22T18:40:22Z
We are disputing this issue because we believe the issues raised are either incorrect or are just pointing out areas that don't extend beyond lack of trust in a role holder. Most of this report's inaccuracies stem from using our docs on our PrePO website that we declared invalid and out-of-date. We should have put this in the README rather than announcing in discord, but still makes this report invalid.
L-01 and L-03 both point out architecture that is nonexistent, initiateWithdrawal
, Strategy
, and StrategyController
are no longer relevant.
L-06-10 are just "role compromised, this setting can be changed in a way disadvantageous to users"
L-02, L-04-5 are correct, but have been mentioned in reports else where that are better.
#3 - Picodes
2023-01-07T17:58:08Z
1, 3 are invalid 6 is a duplicate of #267
The role setup of the repo is quite complex, and in the absence of clear documentation stating what are the roles of each trusted actor and the associated risks for users, I'll accept issues of the form "role compromised, this setting can be changed in a way disadvantageous to users" as valid NC QA issues (so worth little for the final grade) as it's worth reporting them for future users.