When opening or closing a position, a positionFee is taken from the user's collateral but the fee is sometimes rounded down to zero if called with enough low values.
Vulnerability Detail
In Product contract, functions openTakeFor, _closeTake, openMakeFor and _closeMake make some calculations to get the positionFee that the user must pay for opening or closing positions.
The calculations are equal in 4 functions, we'll take openTakeFor as an example:
As you can see, there's some division performed before the multiplication and that can carry precision loss.
In products that latestOracleVersion.price is a small number, when calling the function with an amount being low enough the user can avoid paying the position fee.
Imagine the following scenario:
We have a product with a latestOracleVersion.price of 1e10 and a takerFee of 15e14 (0.0015 in ETH).
A user wants to open a taker position of 1e18 but doesn't want to pay the fees.
Instead of calling openTake with an amount of 1e18 it calls openTake 100000000 times with a value of 1e10.
Because of the precision loss, the position fee calculated will be zero so the user won't pay any fees.
Everyone can call the openTakeFor, _closeTake, openMakeFor and _closeMake functions and avoid paying any position fees. Later, when settling a new oracle version, position fees are going to be distributed to makers so it can cause protocol insolvency.
BLACK-PANDA-REACH
medium
Loss of Precision when calculating
positionFee
Summary
When opening or closing a position, a
positionFee
is taken from the user's collateral but the fee is sometimes rounded down to zero if called with enough low values.Vulnerability Detail
In
Product
contract, functionsopenTakeFor
,_closeTake
,openMakeFor
and_closeMake
make some calculations to get thepositionFee
that the user must pay for opening or closing positions.The calculations are equal in 4 functions, we'll take
openTakeFor
as an example:This uses the
mul
function fromUFixed18
library:So the calculation of
positionFee
can be translated to the following expression:As you can see, there's some division performed before the multiplication and that can carry precision loss.
In products that
latestOracleVersion.price
is a small number, when calling the function with an amount being low enough the user can avoid paying the position fee.Imagine the following scenario:
latestOracleVersion.price
of1e10
and atakerFee
of15e14
(0.0015 in ETH).1e18
but doesn't want to pay the fees.openTake
with an amount of1e18
it callsopenTake
100000000 times with a value of1e10
.Impact
Everyone can call the
openTakeFor
,_closeTake
,openMakeFor
and_closeMake
functions and avoid paying any position fees. Later, when settling a new oracle version, position fees are going to be distributed to makers so it can cause protocol insolvency.Code Snippet
Snippet in
openTakeFor
function: https://github.com/sherlock-audit/2023-05-perennial/blob/main/perennial-mono/packages/perennial/contracts/product/Product.sol#L223Snippet in
_closeTake
function: https://github.com/sherlock-audit/2023-05-perennial/blob/main/perennial-mono/packages/perennial/contracts/product/Product.sol#L264Snippet in
openMakeFor
function: https://github.com/sherlock-audit/2023-05-perennial/blob/main/perennial-mono/packages/perennial/contracts/product/Product.sol#L305Snippet in
_closeMake
function: https://github.com/sherlock-audit/2023-05-perennial/blob/main/perennial-mono/packages/perennial/contracts/product/Product.sol#L347Tool used
Manual Review
Recommendation
After the position fee calculation, it's recommended to check that if the set fee is not zero, the calculated fee must not be zero.
Example solution with
openTakeFor
: