Closed sherlock-admin4 closed 1 month ago
This is clearly specified in the scope...
"wipeAll and wipe do not drip because it is actually not convenient for the user to do a drip call on wipping. Then, if we force the drip, we are incentivizing users to repay directly to the vat (which is possible) instead of using the engine for that. We are mimicking the old proxy actions behaviour, where we drip for drawing, as otherwise the user can lose money, but not forcing the drip on wiping so users actually use this function."
chaduke
High
LockstakeEngine.wipe()
does not use the most current stability rate, as a result, the user will repay less than he is supposed to and the protocol will lose funds.Summary
LockstakeEngine.wipe()
does not use the most current stability rate, as a result, the user might repay less than he is supposed to and the protocol will lose funds.Even
jug.drip(ilk)
is called periodically to bring stablilty rate up to date, a user can still front-run these update transactions and use the old rate to repay less debt (or pay the same amount but cancel more debt fromart
).The same problem for
wipeAll
. Our analysis will focus onwipe()
below.This attack can be replayed indefinitely, therefore, I mark this finding as
high
.Root Cause
LockstakeEngine.wipe()
does not calljug.drip(ilk)
first to calculate the most up-to-date stablity fee. Instead, it uses the old rate to calculatedart
and the amount to deduct from the remaining debtart
. Therefore, the used rate is smaller than what it is supposed to be, leading to deduct more debt fromart
then it is supposed to. This essentially allows the borrower to replay less than he is supposed to. A loss of funds for the protocol. This occurs to every borrower, so I marked this as high.https://github.com/sherlock-audit/2024-06-makerdao-endgame/blob/dba30d7a676c20dfed3bda8c52fd6702e2e85bb1/lockstake/src/LockstakeEngine.sol#L391-L399
Internal pre-conditions
Nobody calls
jug.drip(ilk)
right before the call ofwipe()
. Or the borrower front-runs ajug.drip(ilk)
transaction to take advantage of the old rate to pay less for his debt.External pre-conditions
Time elapsed since last call of
jug.drip(ilk)
, so there is a new rate that needs to be calcualted.Attack Path
In the following, we show Bob opens a urn with 2000 ether of collateral and borrows 20 ether of nst:
Balances for urn mkr bal: 0 nst bal: 0 vat.dai: 0 vat.sin: 0 vat.gem(ilk, a): 0 ink: 2000000000000000000000 art: 10000000000000000000
Bob's balances: mkr bal: 98000000000000000000000 nst bal: 20000000000000000000 vat.dai: 0 vat.sin: 0 vat.gem(ilk, a): 0 ink: 0 art: 0
After 100 days (this is an exaggeration but it shows the negative effect of not using a current rate), the new stability rate is:
wipe()
function uses the old rate of 2000000000000000000000000000. Suppose Bob repays 10 ether of nst, then it will cancel half of the debt, with a remaining debt ofart = 5000000000000000000
.On the other hand, if the
wipe()
function uses the new rate of 2180484675123196105405976966. Then Bob repays 10 ether of nst, due to the increase of rate, less than half of the debt will be cancelled, with a remaining debt ofart = 5413863663391715485
.In summary, due to the use of an old rate of 2000000000000000000000000000 instead of the new rate of 2180484675123196105405976966. Bob was able to repay more debt than he is supposed to with 10 ether of nst. This leads to the loss of funds for the protcol. This occurs frequently for each borrower, thus I mark this finding as high.
Impact
LockstakeEngine.wipe()
does not use the most current stability rate, as a result, the user will repay less than he is supposed to and the protocol will lose funds.PoC
comment/uncomment the line
dss.jug.drip(ilk);
to simulate the use of old rate or new rate. This also simulates the front-running situation.One can see that with old/new rate, the remaining debt for Bob is not the same. Bob will be able to repay more debt with the same 10 ether of nst when he uses the old rate (maybe via front-running
dss.jug.drip(ilk);
).run
forge test --match-test testWipe1 -vv
.Mitigation
Call
jug.drip(ilk)
at the beginning of the wipe() and wipeAll() function so that they will always use the new rate.