scaledQuoteTokenAmount isn't updated to be collateral sell value in the quote token constraint case of _calculateTakeFlowsAndBondChange
Summary
scaledQuoteTokenAmount isn't C * p, but C * p * (1 - BFP) for quote token amount constraint case of _calculateTakeFlowsAndBondChange().
Vulnerability Detail
First case of the _calculateTakeFlowsAndBondChange() logic needs to use scaledQuoteTokenAmount in two steps, first as a constraint, then as a total collateral value. The second update is now missed. It affects kicker's reward as the difference takes place when borrowerPrice < auctionPrice, i.e. when vars.isRewarded is true.
Impact
scaledQuoteTokenAmount is then used for kicker's bond change calculation, so in the quote token constraint case kickers will have the reward based on C * p * (1 - BFP). As this value is proportional to BFP, the higher the reward should be, the more incorrect it will be, i.e. (1 - BFP) * BFP instead of BFP.
As this is regular functionality, there is no low probability prerequisites, and kicker's reward loss is material, setting the severity to be high.
Code Snippet
_calculateTakeFlowsAndBondChange() has first logic branch where quote token used to purchase is a constraint:
if (vars.isRewarded) {
// take is above neutralPrice, Kicker is rewarded
vars.bondChange = Maths.wmul(vars.scaledQuoteTokenAmount, uint256(vars.bpf));
} else {
// take is above neutralPrice, Kicker is penalized
vars.bondChange = Maths.wmul(vars.scaledQuoteTokenAmount, uint256(-vars.bpf));
}
And it is vars.scaledQuoteTokenAmount = CollateralAmount * Price in 2nd and 3rd cases, not not in 1st:
function _calculateTakeFlowsAndBondChange(
uint256 totalCollateral_,
uint256 inflator_,
uint256 collateralScale_,
TakeLocalVars memory vars
) internal pure returns (
TakeLocalVars memory
) {
// price is the current auction price, which is the price paid by the LENDER for collateral
// from the borrower point of view, the price is actually (1-bpf) * price, as the rewards to the
// bond holder are effectively paid for by the borrower.
uint256 borrowerPayoffFactor = (vars.isRewarded) ? Maths.WAD - uint256(vars.bpf) : Maths.WAD;
uint256 borrowerPrice = (vars.isRewarded) ? Maths.wmul(borrowerPayoffFactor, vars.auctionPrice) : vars.auctionPrice;
// If there is no unscaled quote token bound, then we pass in max, but that cannot be scaled without an overflow. So we check in the line below.
vars.scaledQuoteTokenAmount = (vars.unscaledDeposit != type(uint256).max) ? Maths.wmul(vars.unscaledDeposit, vars.bucketScale) : type(uint256).max;
uint256 borrowerCollateralValue = Maths.wmul(totalCollateral_, borrowerPrice);
if (vars.scaledQuoteTokenAmount <= vars.borrowerDebt && vars.scaledQuoteTokenAmount <= borrowerCollateralValue) {
// quote token used to purchase is constraining factor
vars.collateralAmount = _roundToScale(Maths.wdiv(vars.scaledQuoteTokenAmount, borrowerPrice), collateralScale_);
vars.t0RepayAmount = Maths.wdiv(vars.scaledQuoteTokenAmount, inflator_);
vars.unscaledQuoteTokenAmount = vars.unscaledDeposit;
} else if (vars.borrowerDebt <= borrowerCollateralValue) {
// borrower debt is constraining factor
vars.collateralAmount = _roundToScale(Maths.wdiv(vars.borrowerDebt, borrowerPrice), collateralScale_);
vars.t0RepayAmount = vars.t0Debt;
vars.unscaledQuoteTokenAmount = Maths.wdiv(vars.borrowerDebt, vars.bucketScale);
vars.scaledQuoteTokenAmount = (vars.isRewarded) ? Maths.wdiv(vars.borrowerDebt, borrowerPayoffFactor) : vars.borrowerDebt;
} else {
// collateral available is constraint
vars.collateralAmount = totalCollateral_;
vars.t0RepayAmount = Maths.wdiv(borrowerCollateralValue, inflator_);
vars.unscaledQuoteTokenAmount = Maths.wdiv(borrowerCollateralValue, vars.bucketScale);
vars.scaledQuoteTokenAmount = Maths.wmul(vars.collateralAmount, vars.auctionPrice);
}
if (vars.isRewarded) {
// take is above neutralPrice, Kicker is rewarded
vars.bondChange = Maths.wmul(vars.scaledQuoteTokenAmount, uint256(vars.bpf));
} else {
// take is above neutralPrice, Kicker is penalized
vars.bondChange = Maths.wmul(vars.scaledQuoteTokenAmount, uint256(-vars.bpf));
}
return vars;
}
vars.scaledQuoteTokenAmount in the first case has dual role, at first it is a constraint, then it is C * p computation base value, so it is to be used iteratively, first as a constraint, then updated to be CollateralAmount * Price.
Tool used
Manual Review
Recommendation
Consider setting the scaledQuoteTokenAmount to the C * p as a final step of quote token amount constraint case:
hyh
high
scaledQuoteTokenAmount isn't updated to be collateral sell value in the quote token constraint case of _calculateTakeFlowsAndBondChange
Summary
scaledQuoteTokenAmount
isn'tC * p
, butC * p * (1 - BFP)
for quote token amount constraint case of _calculateTakeFlowsAndBondChange().Vulnerability Detail
First case of the _calculateTakeFlowsAndBondChange() logic needs to use
scaledQuoteTokenAmount
in two steps, first as a constraint, then as a total collateral value. The second update is now missed. It affects kicker's reward as the difference takes place whenborrowerPrice < auctionPrice
, i.e. whenvars.isRewarded
is true.Impact
scaledQuoteTokenAmount
is then used for kicker's bond change calculation, so in the quote token constraint case kickers will have the reward based onC * p * (1 - BFP)
. As this value is proportional toBFP
, the higher the reward should be, the more incorrect it will be, i.e.(1 - BFP) * BFP
instead ofBFP
.As this is regular functionality, there is no low probability prerequisites, and kicker's reward loss is material, setting the severity to be high.
Code Snippet
_calculateTakeFlowsAndBondChange() has first logic branch where quote token used to purchase is a constraint:
https://github.com/sherlock-audit/2023-01-ajna/blob/main/contracts/src/libraries/external/Auctions.sol#L1139-L1149
vars.scaledQuoteTokenAmount
is used for thebondChange
calculation and per documentation has to be equal toC * p
:https://github.com/sherlock-audit/2023-01-ajna/blob/main/contracts/src/libraries/external/Auctions.sol#L1166-L1172
And it is
vars.scaledQuoteTokenAmount = CollateralAmount * Price
in 2nd and 3rd cases, not not in 1st:https://github.com/sherlock-audit/2023-01-ajna/blob/main/contracts/src/libraries/external/Auctions.sol#L1124-L1175
vars.scaledQuoteTokenAmount
in the first case has dual role, at first it is a constraint, then it isC * p
computation base value, so it is to be used iteratively, first as a constraint, then updated to beCollateralAmount * Price
.Tool used
Manual Review
Recommendation
Consider setting the
scaledQuoteTokenAmount
to theC * p
as a final step of quote token amount constraint case:https://github.com/sherlock-audit/2023-01-ajna/blob/main/contracts/src/libraries/external/Auctions.sol#L1139-L1149