Closed 0xJuancito closed 1 year ago
We appreciate the report, but this attack considers an admin account zeroing the borrowing fees, which falls into the limitation "Attacks that require access to leaked private keys or trusted addresses".
We appreciate the report, but this attack considers an admin account zeroing the borrowing fees, which falls into the limitation "Attacks that require access to leaked private keys or trusted addresses".
borrowingFee
value can be configured between 0
and 1000
(0 - 10%), which are valid values that can be set for any collateral. So, this doesn't either fall under admin misconfiguration either.@0xfornax Please consider reviewing this issue as it is valid in terms of the contest as described on the points above, and will put the protocol at serious risk if not addressed.
I can help with an additional suggestion for a coded mitigation if needed.
Snippet mentioned in the issue showing that this is issue happens for valid values:
function setBorrowingFee(
address _collateral,
uint256 borrowingFee
)
public
override
onlyOwner
safeCheck("Borrowing Fee Floor", _collateral, borrowingFee, 0, 1000) /// 0% - 10% <======
Additionally, some tests that show the usage of zero fees (the attack works for any fee below 0.45% still):
This attack requires an admin key as only admins can lower the borrowing fee to under 0.5%.
This attack requires an admin key as only admins can change the borrowing fee.
This issue does not suggest that an attacker changes the borrowing fee.
Borrowing fees are configurable by admins. It is expected that an admin can set borrowing fees under 0.5% for some collateral at any given time. This value is validated on code.
As soon as an admin sets this valid value under 0.45%, an attacker can perform the attack and mint any amount of tokens.
If the intention of the protocol is not to set any value under 0.45%, this means that this issue is valid, as this is an allowed value.
As shown on the previous comments, there are even with tests showcasing this behavior. Meaning that the protocol accepts these values as values that can be set for some new collateral at any point.
The line safeCheck("Borrowing Fee Floor", _collateral, borrowingFee, 0, 1000) /// 0% - 10%
indicates that these limits are intentional.
On top of that, the vulnerability does not rely on the configured value, but on the miscalculation addressed on the increaseDebt
function, where the borrowing fee is not considered.
Hello Juancito!
Thank you for expressing your concern. We genuinely appreciate your interest in assisting us in building a secure platform. I understand that there may have been a slight misunderstanding regarding the functionality of our FeeCollector, and I would be delighted to provide a clearer explanation to alleviate any confusion.
Whenever your debt increases, the platform applies a borrowing fee (emit BorrowingFeePaid event). This fee amount is then sent as an argument to the FeeCollector.increaseDebt(_feeAmount) function. In cases where you acquire a new loan resulting in an increase in debt, but the borrowing fee is set to zero, no amount is sent to the FeeCollector contract.
When a borrowing fee is applicable, we allocate a portion equivalent to one week's worth of interest, denoted as the MIN_FEE_FRACTION, and collect it for the platform (emit FeeCollected event). The remaining fee balance is retained and can be refunded to you if you repay the loan before it reaches the expiration period (which is six months minus the one week you have already paid for).
Each time you make a partial repayment on your loan, you receive a proportional refund, indicated by the FeeRefunded event. This adjustment updates your refundable record accordingly. Additionally, we also collect a portion of the expired refund, as some time has passed, and you are no longer eligible for that particular portion.
It is essential to understand that a user can never receive a refund exceeding the total fees they have paid. The fee balances are maintained on a per-user basis rather than as a cumulative total.
What you experienced in your test scenario was the refunding of a portion of the initial 0.5% borrowing fee as you made partial repayments on your first loan.
I have made enhancements to your test case and included relevant code in our "hats-issue-242" branch. If you wish to witness it in action, please feel free to check it out (BorrowerOperationsTest.js).
I hope this clarifies matters for you. Please feel free to reach out if you have any further questions or concerns.
Cheers!
Thanks @gbirckan for taking the time to assess the validity of the issue.
The returned fees were due to a misplacement of the await adminContract.setBorrowingFee(erc20.address, "0")
in the test, placing it after the vessel was open.
It is clarified. I appreciate the time you took.
Best!
Impact
Unrestricted debt token minting.
Borrowers can make profit of any amount of debt token in expense of the protocol, by adjusting their positions continuously when borrowing fees are lower than refund fees.
Borrowing fees are configurable, and have minimum and maximum values. There are even tests showcasing fees of 0%, which will lead the most damage to the protocol. Any value < 0.45% makes the attack possible.
Proof of Concept
The problem lies on the
refundableFeeAmount
value inFeeCollector::increaseDebt()
.If that value is bigger than the
borrowingFee
for the collateral, malicious borrowers can decrease and increase their debt in the position continuously, while taking profit from fees on each transaction.The result will be minting debt tokens indefinitely, while their positions remains the same (both debt and collateral). They also don't have to provide extra collateral (other than the one they instantly recover on the attack).
Link to code
The attack can be performed by calling
BorrowerOperations::adjustPosition()
:The
setBorrowingFee
expects values from 0% - 10%.With the current setup and
MIN_FEE_FRACTION
defined inFeeCollector
. AnyborrowingFee < 0.45%
results in profit for the borrower taken from the protocolLink to code
Add this test to the
describe("BorrowerOperations Mechanisms")
inBorrowerOperationsTest.js
:Tools Used
Manual review
Recommended Mitigation Steps
_sRecord.amount
inFeeCollector
should be calculated based on the correspondingborrowingFee
for the collateral, so that refunded fees can't exceed borrower fees. This way the protocol is safe from someone trying to take profit on fees.This means that when calling
FeeCollector::increasingDebt()
, the recordedrefundableFeeAmount
has to be lower than the borrowing fees: