Unchecked Low Level Call
Overview
- Severity: Medium
- Confidence: Medium
- Affected Versions: All
What is the Unchecked Low Level vulnerability?
Solidity offers a variety of low-level calls (e.g call()
, delegatecall()
, staticcall()
) to facilitate inter-address functionality. These low-level calls will not revert on failure, but will instead return a Boolean value indicating success. If the developer fails to check this return value, incorrect logic due to silent failures may possibly result.
Further reading: Solidity Documentation: Members of Addresses
Technical example of vulnerable code
// SPDX-License-Identifier: Unlicense
pragma solidity ^0.8.0;
contract LiquidityPool {
address public rewardDistributor;
constructor(address _rewardDistributor) {
rewardDistributor = _rewardDistributor;
}
receive() external payable {
// Allow deposits to the liquidity pool
}
function triggerRewardDistribution() public {
// Attempt to call the RewardDistributor without checking success
rewardDistributor.call(abi.encodeWithSignature("distributeRewards()"));
// Additional logic related to the distribution process omitted for brevity
}
}
contract RewardDistributor {
mapping (address => bool) authorizedDistributors;
uint256 public totalRewardsDistributed;
constructor() {
authorizedDistributors[msg.sender] = true;
totalRewardsDistributed = 0;
}
function distributeRewards() public {
require(authorizedDistributors[msg.sender], "Only authorized addresses can distribute rewards.");
// Robust logic to distribute rewards omitted for brevity
totalRewardsDistributed += 1;
}
}
In the example above, two different contracts interact as part of a decentralized finance (DeFi) application: contract LiquidityPool
accepts ether deposits and can trigger rewards distribution handled by a second contract, RewardDistributor
. To call this contract, it uses the low-level call()
operation, without checking the return value in this case. Thus, it is possible for the delegated call to silently fail, possibly leading to incorrect accounting for distributed rewards.
Technical example of how to fix the vulnerability
// SPDX-License-Identifier: Unlicense
pragma solidity ^0.8.0;
contract LiquidityPoolUpdated {
address public rewardDistributor;
constructor(address _rewardDistributor) {
rewardDistributor = _rewardDistributor;
}
receive() external payable {
// Allow deposits to the liquidity pool
}
function triggerRewardDistribution() public {
(bool success, ) = rewardDistributor.call(abi.encodeWithSignature("distributeRewards()"));
require(success, "Unable to distribute rewards!");
// Additional logic related to the distribution process omitted for brevity
}
}
contract RewardDistributor {
mapping (address => bool) authorizedDistributors;
uint256 public totalRewardsDistributed;
constructor() {
authorizedDistributors[msg.sender] = true;
totalRewardsDistributed = 0;
}
function distributeRewards() public {
require(authorizedDistributors[msg.sender], "Only authorized addresses can distribute rewards.");
// Robust logic to distribute rewards omitted for brevity
totalRewardsDistributed += 1;
}
}
In the revised set of contracts above, LiquidityPoolUpdated
now captures and checks the return value of its low-level call()
to ensure that rewards were properly distributed, before proceeding with any additional logic.