Description
The SiloFacet::transferDeposit function allows any user to transfer their deposit to another user. However, the function does not check the amount being transfered. Worse still, if this amount is 0, it repeatedly pushes the same depositId into s.accts[account].depositIdList[token]:
function addDepositToAccount(
...
) internal {
AppStorage storage s = LibAppStorage.diamondStorage();
uint256 depositId = LibBytes.packAddressAndStem(token, stem);
@> if (s.accts[account].deposits[depositId].amount == 0) {
@> s.accts[account].depositIdList[token].push(depositId);
}
...
}
A user can call SiloFacet::transferDeposit with an amount of 0 and any recipient multiple times to inflate the recipient's array length, causing a DoS for all functions that use s.accts[account].depositIdList[token] in memory (due to excessive gas costs for memory) or in a loop (due to excessive gas costs for executing instructions).
The real issue is that this variable is used in critical parts of the code (tree representing where functions are called):
- SiloGettersFacet::getTokenDepositIdsForAccount - Full DoS
- SiloGettersFacet::getTokenDepositsForAccount - Full DoS
- SiloGettersFacet::getDepositsForAccount - Full DoS
- LibTokenSilo::findDepositIdForAccount - Full DoS
- LibTokenSilo::removeDepositIDfromAccountList - Full DoS
- LibTokenSilo::removeDepositFromAccount - DoS if all assets of a token is removed/withdraw/transfer
- LibConvert::_withdrawTokens - DoS if all assets of a token is removed/withdraw/transfer
- PipelineConvertFacet::pipelineConvert - DoS if all assets of a token is removed/withdraw/transfer
- ConvertFacet::convert - DoS if all assets of a token is removed/withdraw/transfer
- LibSilo::_removeDepositFromAccount - DoS if all assets of a token is removed/withdraw/transfer
- TokenSilo::_withdrawDeposit - DoS if all assets of a token is removed/withdraw/transfer
- SiloFacet::withdrawDeposit - DoS if all assets of a token is removed/withdraw/transfer
- TokenSilo::_transferDeposit - DoS if all assets of a token is removed/withdraw/transfer
- SiloFacet::transferDeposit - DoS if all assets of a token is removed/withdraw/transfer
- TokenSilo::_transferDeposits - DoS if all assets of a token is removed/withdraw/transfer
- SiloFacet::transferDeposits - DoS if all assets of a token is removed/withdraw/transfer
- LibSilo::_removeDepositsFromAccount - DoS if all assets of a token is removed/withdraw/transfer
- TokenSilo::_withdrawDeposits - DoS if all assets of a token is removed/withdraw/transfer
- SiloFacet::withdrawDeposits - DoS if all assets of a token is removed/withdraw/transfer
- EnrootFacet::enrootDeposit - DoS if all assets of a token is removed/withdraw/transfer
This bug only causes a full withdrawal/transfer/removal DoS because LibTokenSilo::removeDepositIDfromAccountList is called in LibTokenSilo::removeDepositFromAccount with this condition:
if (crateAmount > 0) {
delete s.accts[account].deposits[depositId];
removeDepositIDfromAccountList(account, token, depositId);
}
Risk
Likelyhood: High
Before the migration, executing this attack will be costly, but once the migration is completed, it can be executed on anyone at a very low cost.
Impact: Medium
Recommended Mitigation
Before pushing any new ID into the depositIdList array, check if the amount is 0.
function addDepositToAccount(
...
) internal {
AppStorage storage s = LibAppStorage.diamondStorage();
uint256 depositId = LibBytes.packAddressAndStem(token, stem);
// add a depositId to an account's depositList, if there is not an existing deposit.
- if (s.accts[account].deposits[depositId].amount == 0) {
+ if (s.accts[account].deposits[depositId].amount == 0 && amount > 0) {
s.accts[account].depositIdList[token].push(depositId);
}
...
}