The Smart Contract Weakness Classification and Test Cases (SWC) Registry is a set of Web3 vulnerabilities to avoid when writing smart contracts. It may seem daunting to understand every issue so I’ll do my best to demystify every issue and explain each vulnerability with real-world examples. This write-up is intended for those who are just starting out in solidity development and smart contract auditing.
SWC-113: DoS with Failed Call
Remember that all function executions in solidity are atomic in nature? This means that if something reverts inside the function, then everything reverts back to its original state. Also, remember that a contract can call a function in another contract (called external calls)? If the external call fails, then the original function will revert as well.
With that in mind, let’s take a look at some code and go through the code line by line.
Line 7: Solidity version (ideally above 0.8.0, see SWC-101 and SWC-102)
Line 9: The contract called Refunder
Line 11: A variable called refundAddresses with an array of addresses;
Line 12: A mapping called refunds that takes in an address type and returns a uint type
Line 14: Constructor (Called during contract deployment)
Line 15-16: Pushes 2 addresses into the refundAddresses array;
Line 20: A public function called refundAll() (Anyone can call this function)
Line 21-22: Refunds the two addresses one at a time with the for loop using send().
What’s the issue with this contract?
Take note of lines 21 and 22. The function refundAll() intends to refund the addresses one by one. Imagine there are 100 addresses that are in need of a refund. A single failure on send will hold up all funds because one revert means that the whole function stops working! In other words, if one particular address cannot receive funds, then all the other 99 addresses cannot receive their funds as well.
Can a transfer of funds even fail?
Yes, it is possible for a transfer of funds to fail, resulting in a particular address being unable to receive funds. One example is if the address is on the USDC blacklist. If a particular address is on the USDC blacklist, then the address cannot receive USDC (will be reverted). If refundAll() intends to transfer USDC to all 100 addresses in one transaction, then the function will definitely fail because the blacklisted address will cause the whole function to revert. Also, a contract can purposefully deny native funds from another contract by not having the receive() fallback function. If that’s the case, then refundAll() cannot be executed.
In addition, the contract may not have enough funds to refund everybody. Once the contract balance reaches zero, no other addresses can receive their refund because the transfer will fail. Once again, if one address cannot receive their refund in the refundAll() function, then all the addresses will not be able to receive their refund.
When approaching loops and transfers, always think: How can an attacker grieve the contract? Can the attacker create a malicious contract or block an address from receiving funds and thereby deny everyone else from receiving funds?
Summary
Avoid combining multiple calls in a single transaction, especially when calls are executed as part of a loop
Implement the contract logic to handle failed calls, for example, skipping the particular iteration of the loop.
Reference
https://swcregistry.io/docs/SWC-113