Closed sherlock-admin4 closed 1 month ago
As governance is assumed to configure parameters with extreme care (see contest readme) upchost() is assumed to be called in the spell that update chop
or dust
.
In any case, this code is unchanged from the original clipper, so out of scope.
chaduke
Medium
LockstakeClipper.take() might use a stale value of chost and allows an invalid partial purchase, leaving a debt that can potentially not be able to cover, loss of funds for the protocol.
Summary
LockstakeClipper.take()
might use a stale value ofchost
and allows an invalid partial purchase, leaving a debt that can potentially not be able to cover, loss of funds for the protocol.If
lot < 20 chost
, then the loss of funds might be > 5%. For smaller lot, the lost can be more. Besides, this can occur each timechost
is updated. Therefore, I mark this finding asmedium
.Root Cause
LockstakeClipper.take()
fails to call upchost() to bringchost
up to date, therefore it might use a stale value ofchost
. As a result, invalid partial purchase is possible - purchase that leaves remaining debt that is greater than the oldchost
but smaller than the newchost
.Internal pre-conditions
First of all,
chost
is mutable, if either_dust
ordog.chop(ilk)
has changed, thenchost
will change. This is why the functionupchost()
was designed to bringchost
up to date.An internal precodition is that either
_dust
ordog.chop(ilk)
is increased, but uphost() is not called to bringchost
up to date. In other words, the actualchost
has increased, but thechost
variable inLockstakeClipper
is stale and has a smaller value than the actual chost value.External pre-conditions
A user makes a partial purchase of the auction, which should not be allowed if
chost
was brought up to date, but is allowed with the stale value ofchost
. As a result, not-supposed-to-happen debt might never be covered, leading to a loss to the protocol.Attack Path
Suppose we have chost = 1,000 RAD (initial value) Ongoing auction: tab = 2,000 RAD lot = 10 WAD (collateral to sell) price = 200 RAD per WAD of collateral
Now
_dust
is doubled, as a result, the actualchost
should be updated to 2,000 RAD. However,upchost()
is not called. The state variablechost
remains 1,000 RAD.Suppose Alice takes a bid with 5 WAD to raise 1,000 RAD, leaving a remaining
tab
of 1,000 RAD. This is allowed since the remainingtab
is equal to the stalechost
. Ifchost
had been updated, then Alice had to buy the whole thing, and would have left no remaining debt to cover.Now the remaining 1,000 RAD debt is very likely hard to cover due to two reasons: 1) the price will decrease as the auction proceeds; 2) nobody will be incentivized to reset the auction since there is no incentive when
tab < _chost
(see redo()). The auction might be stuck until authorized intervention. In any case, the protocol will lose funds as a result of allowing invalid partial purchase.Impact
LockstakeClipper.take()
fails to call upchost() to bringchost
up to date, therefore it might use a stale value ofchost
. As a result, invalid partial purchase is possible - purchase that leaves remaining debt that is greater than the oldchost
but smaller than the newchost
. This debt might become a bad debt and causes loss to the protocol.PoC
In the following, an auction is set up as follows:
sale.pos: 0 sale.tab: 20000000000000000000000000000000000000000000000 sale.lot: 2000000000000000000000 sale.tot: 2000000000000000000000 sale.usr: 0x1a38b0201C9B6acBfadAD17af8d1062F63285413 sale.tic: 604411200 sale.top: 5000000000000000000000000000
The stale chost is: 10000000000000000000000000000000000000000000000
Although _dust is doubled, so the actual chost should be: 20000000000000000000000000000000000000000000000, since
clip.upchost()
is not called inside take(). Ali calls clip.take() to take an invalid partial purchase with the stale chost value. Ali might have to buy the whole debt if`chost
has been brought up to date.vm.prank(ali); clip.take({ id: 1, amt: 2 ether, max: 5000000000000000000000000000, who: address(ali), // sli is the keeper data: "" });
leaving the following auction:
sale.pos: 0 sale.tab: 10000000000000000000000000000000000000000000000 sale.lot: 1998000000000000000000 sale.tot: 2000000000000000000000 sale.usr: 0x1a38b0201C9B6acBfadAD17af8d1062F63285413 sale.tic: 604411200 sale.top: 5000000000000000000000000000
with a tab < the actual chost of 20000000000000000000000000000000000000000000000
Now suppose clip.upchost() is called, and now
chost
is updated to: 20000000000000000000000000000000000000000000000.The remaining debt might not be covered due to the decrease of price or no incentive for reset of the auction. The debt might become a bad debt and become a loss of the protocol.
Mitigation
Call
upchost
in the beginning of take():