Denial-of-Service Attack caused by gas limit
Denial-of-service (DoS) attack caused by gas limit typically targets blockchain networks, especially those using Ethereum or similar platforms where smart contracts execute operations based on gas fees. Gas is the fuel that powers transactions and smart contract executions on these networks, and each operation consumes a certain amount of gas. The gas limit is the maximum amount of gas that can be consumed by a transaction or smart contract execution.
Denial of Service — Caused by gas limit
userDepositIndex and userWithdrawIndex growing indefinitely
The contract is using userDepositsIndex
to track deposits and withdrawals for users.
/// @dev array of deposit receipts
Receipt[] public deposits;
/// @dev indexes of deposit receipts of an address
mapping(address => uint256[]) public userDepositsIndex;
userDepositsIndex
is expanded by depositUSDC
function:
function depositUSDC(uint256 _amount) external {
require(_amount >= minUSDCAmount, "deposit amount smaller than minimum OTC amount");
IERC20(usdc).transferFrom(msg.sender, address(this), _amount);
// update usd balance of user, add their receipt, and receipt index to user deposits index
usdBalance[msg.sender] = usdBalance[msg.sender] + _amount;
deposits.push(Receipt(msg.sender, _amount));
userDepositsIndex[msg.sender].push(deposits.length - 1);
emit USDCQueued(msg.sender, _amount, usdBalance[msg.sender], deposits.length - 1);
}
The state variable userDepositIndex
is growing indefinitely and might lead to expensive transactions and effectively a denial of service for the user
userDepositsIndex[msg.sender].push(deposits.length - 1);
When calling withdrawUSCD
function that requires iterations over the whole array.
function withdrawUSDC(uint256 _amount) external {
require(!isAuctionLive, "auction is live");
usdBalance[msg.sender] = usdBalance[msg.sender] - _amount;
require(
usdBalance[msg.sender] >= minUSDCAmount || usdBalance[msg.sender] == 0,
"remaining amount smaller than minimum, consider removing full balance"
);
// start withdrawing from the users last deposit
uint256 toRemove = _amount;
uint256 lastIndexP1 = userDepositsIndex[msg.sender].length;
for (uint256 i = lastIndexP1; i > 0; i--) {
Receipt storage r = deposits[userDepositsIndex[msg.sender][i - 1]];
if (r.amount > toRemove) {
r.amount -= toRemove;
toRemove = 0;
break;
} else {
toRemove -= r.amount;
delete deposits[userDepositsIndex[msg.sender][i - 1]];
}
}
IERC20(usdc).transfer(msg.sender, _amount);
emit USDCDeQueued(msg.sender, _amount, usdBalance[msg.sender]);
}
part of the function that interests us:
uint256 toRemove = _amount;
uint256 lastIndexP1 = userDepositsIndex[msg.sender].length;
for (uint256 i = lastIndexP1; i > 0; i--) {
Receipt storage r = deposits[userDepositsIndex[msg.sender][i - 1]];
if (r.amount > toRemove) {
r.amount -= toRemove;
toRemove = 0;
break;
} else {
toRemove -= r.amount;
delete deposits[userDepositsIndex[msg.sender][i - 1]];
}
}
And more specifically, it is in the loop that checks from an ever growing index to zero.
uint256 lastIndexP1 = userDepositsIndex[msg.sender].length;
for (uint256 i = lastIndexP1; i > 0; i--) {
[...]
Recommendation
To avoid this it is recommended to remove elements from the arrays userDepositsIndex
/userWithdrawsIndex
using pop()
function when deleting deposits.