When calling _chargeFee function in MultiInvoker contract with an amount not divisible by 1e12 it will always result in a DoS.
Vulnerability Detail
In MultiInvoker contract, when pulling USDC from user to wrap it to DSU, the following syntax is used:
USDC.pull(msg.sender, amount, true);
The third argument is to specify that amount has to be rounded up. For example, if amount is 1000000000000000001, when converting it to 6 decimals it will be 1000001 instead of 1000000.
However, the newly implemented _chargeFee function is using the following syntax, forgetting about the third argument:
function _chargeFee(address receiver, UFixed18 amount, bool wrapped) internal {
if (wrapped) {
USDC.pull(msg.sender, amount); // @audit-issue Not implementing third argument, DoS.
_handleWrap(receiver, amount);
} else {
USDC.pullTo(msg.sender, receiver, amount);
}
}
The lack of the third argument causes the amount to be truncated from 18 decimals to 6.
This is not the desired behaviour because later in the _handleWrap function it will always round up the amount if it's not divisible by 1e12. Both reserve.mint() and batcher.wrap() will always round up the amount so if it's not rounded up in _chargeFee, it will revert later because of the lack of USDC funds.
function _handleWrap(address receiver, UFixed18 amount) internal {
// If the batcher is 0 or doesn't have enough for this wrap, go directly to the reserve
if (address(batcher) == address(0) || amount.gt(DSU.balanceOf(address(batcher)))) {
reserve.mint(amount);
if (receiver != address(this)) DSU.push(receiver, amount);
} else {
// Wrap the USDC into DSU and return to the receiver
batcher.wrap(amount, receiver);
}
}
Imagine the following scenario:
Bob calls _chargeFee with an amount of 1000000000000100000 (or any amount not divisible by 1e12).
MultiInvoker converts 1000000000000100000 to 1e6 because it's not rounding up.
MultiInvoker pulls 1e6USDC from Bob.
MultiInvoker calls reserve.mint(amount), amount being 1000000000000100000.
Reserve contract converts 1000000000000100000 to 1000001 because it's rounding up.
Reserve tries to pull 1000001 USDC from MultiInvoker.
Call reverts because MultiInvoker only owns 1e6USDC.
Impact
This issue will cause the transaction to revert every time the _chargeFunction is called with an amount not divisible by 1e12 (amount % 1e12 != 0).
In _chargeFee function, it's recommended to pull USDC tokens from caller using the following syntax:
USDC.pull(msg.sender, amount, true);
This way the USDC amount received by the contract will be the same as the amount requested by the Batcher or Reserve contracts in _handleWrap, thus avoiding the DoS.
BLACK-PANDA-REACH
medium
DoS when calling
_chargeFee
inMultiInvoker
Summary
When calling
_chargeFee
function inMultiInvoker
contract with an amount not divisible by1e12
it will always result in a DoS.Vulnerability Detail
In
MultiInvoker
contract, when pullingUSDC
from user to wrap it toDSU
, the following syntax is used:The third argument is to specify that
amount
has to be rounded up. For example, ifamount
is1000000000000000001
, when converting it to 6 decimals it will be1000001
instead of1000000
.However, the newly implemented
_chargeFee
function is using the following syntax, forgetting about the third argument:The lack of the third argument causes the
amount
to be truncated from 18 decimals to 6.This is not the desired behaviour because later in the
_handleWrap
function it will always round up the amount if it's not divisible by1e12
. Bothreserve.mint()
andbatcher.wrap()
will always round up the amount so if it's not rounded up in_chargeFee
, it will revert later because of the lack ofUSDC
funds.Imagine the following scenario:
_chargeFee
with an amount of1000000000000100000
(or any amount not divisible by 1e12).MultiInvoker
converts1000000000000100000
to1e6
because it's not rounding up.MultiInvoker
pulls1e6
USDC
from Bob.MultiInvoker
callsreserve.mint(amount)
, amount being1000000000000100000
.Reserve
contract converts1000000000000100000
to1000001
because it's rounding up.Reserve
tries to pull1000001
USDC fromMultiInvoker
.MultiInvoker
only owns1e6
USDC
.Impact
This issue will cause the transaction to revert every time the
_chargeFunction
is called with an amount not divisible by1e12
(amount % 1e12 != 0
).Code Snippet
https://github.com/sherlock-audit/2023-05-perennial/blob/main/perennial-mono/packages/perennial/contracts/multiinvoker/MultiInvoker.sol#L379-L393
Tool used
Manual Review
Recommendation
In
_chargeFee
function, it's recommended to pullUSDC
tokens from caller using the following syntax:This way the
USDC
amount received by the contract will be the same as the amount requested by the Batcher or Reserve contracts in_handleWrap
, thus avoiding the DoS.