25930 - [SC - Insight] Malicious owner can update the DepositParams st...
Submitted on Nov 21st 2023 at 06:05:01 UTC by @JCN2023 for Boost | DeGate
Report ID: #25930
Report type: Smart Contract
Report severity: Insight
Target: https://etherscan.io/address/0x9C07A72177c5A05410cA338823e790876E79D73B#code
Impacts:
Griefing (e.g. no profit motive for an attacker, but damage to the users or the protocol)
Description
Bug Description
A lack of input validation in the Exchange::setDepositParams
function can lead to a malicious owner updating the DepositParams
values to arbitrary amounts, potentially resulting in an extremely large deposit fee being charged for each deposit
to the exchange. This large deposit fee and updated DepositParams
will result in a reversion on every deposit
call.
Details
The following code runs for each call to Exchange::deposit
:
Lines 38 - 72 in ExchangeDeposits::deposit
The internal function needChargeDepositFee
on line 66 is invoked and if it returns true
the depositFee
stored in the DepositState
struct will be amount of ETH that the user must pay during this deposit
call.
As we can see on line 72, if the user does not send enough ETH to cover this fee
the transaction will revert.
Therefore, If needChargeDepositFee(S)
always returns true
(indicating a fee is needed) and S.depositState.depositFee
was an extremely large number (type(uint256).max
), then all calls to Exchange::deposit
will revert on lines 72 of the code above.
A malicious owner can guarantee the condition described above by calling Exchange::setDepositParams
and performing the following updates to the contract state:
S.depositState.freeDepositMax = 0
S.depositState.freeDepositRemained = 0
S.depositState.freeSlotPerBlock = 0
S.depositState.depositFee = type(uint256).max
The Exchange::setDepositParams
function does not perform any sort of input validation and simply updates the DepositState
with the calldata values passed in the function call:
Lines 102 - 113 in ExchangeDeposits::setDepositParams
As we can see above, a malicious owner can update the 4 specified state variables to any arbitrary amounts. If the owner sets the state variables to the amounts previously specified, this will result in the internal function needChargeDepositFee
always returning true
.
Below is the code for the internal function ExchangeDeposits::needChargeDepositFee
:
With the specified state changes done, the freeDepositRemained
variable will always be 0
. Therefore, the conditional on line 130 will enter the second branch and the code on line 133 will run, setting the needCharge
variable to true
(indicating a fee is needed for the deposit
calls).
Thus, the condition on line 66 of ExchangeDeposits::deposit
will always be true
, which will result in calls to Exchange::deposit
to always revert on line 72, since depositState.depositFee
is equal to type(uint256).max
.
Impact
A malicious owner can call the Exchange::setDepositParams
function and set the depositState.depositFee
to type(uint256).max
and the other 3 state variables to 0
. This will effectively result in a DOS of the Exchange:deposit
function, disabling users from depositing into the protocol. The malicious owner does not profit from this action, but damage is done to the protocol/users by rendering a core feature of the protocol un-usable for users. Therefore, this action by the malicious owner will result in the following impact: Griefing
.
Risk Breakdown
The exploit itself is very easy to execute since it can only be done by the owner
. Only a single call to Exchange::setDepositParams
is needed to DOS the Exchange::deposit
function.
Recommendation
In order to maintain a truly trust-less system, the privileged Exchange::setDepositParams
function should validate the calldata inputs to ensure that the owner can not update the contract state with arbitrary values. An example of this would be validating that the depositState.depositFee
can not be set to a value greater than X
.
Proof of concept
Last updated