Calling batchTransferOutAndCallV5 function and transferring ETH in a batch will be impossible due to the fact that the internally called _transferOutAndCallV5 nakedly handles msg.value, causing that the function will always fail after the first ETH transfer iteration.
Proof of Concept
Context
When msg.value is handled in a loop, the entire ETH sent is transferred in the first loop, so upon subsequent loops, this time the second, the amount of ETH sent by the sender left will now be zero while the function attempts to send the msg.value again but this time, the transaction will fail and revert breaking the batchTransferOutAndCallV5 functionality.
Bug location
Taking a look at the batchTransferOutAndCallV5 function, the _transferOutAndCallV5 function is called in a for loop while looping through the aggregationPayloads.
function batchTransferOutAndCallV5(
TransferOutAndCallData[] calldata aggregationPayloads
) external payable nonReentrant {
for (uint i = 0; i < aggregationPayloads.length; ++i) {
_transferOutAndCallV5(aggregationPayloads[i]);
}
}
The _transferOutAndCallV5 function queries if the asset is ETH, upon which msg.value is sent to the aggregationPayload target.
As has been established in the impact section, after the first iteration of sending ETH to the aggregationPayload target, further attempts to send ETH to the next aggregationPayload targets will fail as msg.value will be attempted to be sent again, causing a reversion as balance is not enough. This breaks the batchTransferOutAndCallV5 function, rendering it useless.
If for instance batchTransferOutAndCallV5 is called to transfer 2 ETH to 2 targets at 1 ETH each, the first iteration will send msg.value == which is 2 ETH to the first target in the first loop. And as we have established the msg.value will always remain at 2 ETH throughout the entire execution of the batchTransferOutAndCallV5 function. The msg.value will not automatically be reduced regardless of how many ETH has been transferred. Hence upon the second iteration an attempt is made to transfer another 2 ETH to the second target which will fail as the total remaining ETH sent is now 0, causing the entire function to revert.
Tools Used
Manual Review
Recommended Mitigation Steps
Consider fixing this by using the aggregatorPayload.fromAmount parameter instead as is done in the _transferOutV5 function.
Lines of code
https://github.com/code-423n4/2024-06-thorchain/blob/e5ae503d0dc2394a82242be6860eb538345152a1/ethereum/contracts/THORChain_Router.sol#L401 https://github.com/code-423n4/2024-06-thorchain/blob/e5ae503d0dc2394a82242be6860eb538345152a1/ethereum/contracts/THORChain_Router.sol#L307-L311
Vulnerability details
Impact
Calling
batchTransferOutAndCallV5
function and transferring ETH in a batch will be impossible due to the fact that the internally called_transferOutAndCallV5
nakedly handles msg.value, causing that the function will always fail after the first ETH transfer iteration.Proof of Concept
When
msg.value
is handled in a loop, the entireETH
sent is transferred in the first loop, so upon subsequent loops, this time the second, the amount ofETH
sent by the sender left will now be zero while the function attempts to send themsg.value
again but this time, the transaction will fail and revert breaking thebatchTransferOutAndCallV5
functionality.Taking a look at the
batchTransferOutAndCallV5
function, the_transferOutAndCallV5
function is called in a for loop while looping through theaggregationPayloads
.The
_transferOutAndCallV5
function queries if the asset is ETH, upon whichmsg.value
is sent to theaggregationPayload
target.As has been established in the impact section, after the first iteration of sending
ETH
to theaggregationPayload
target, further attempts to sendETH
to the nextaggregationPayload
targets will fail asmsg.value
will be attempted to be sent again, causing a reversion as balance is not enough. This breaks thebatchTransferOutAndCallV5
function, rendering it useless.If for instance
batchTransferOutAndCallV5
is called to transfer 2ETH
to 2 targets at 1ETH
each, the first iteration will send msg.value == which is 2ETH
to the first target in the first loop. And as we have established themsg.value
will always remain at 2ETH
throughout the entire execution of the batchTransferOutAndCallV5 function. Themsg.value
will not automatically be reduced regardless of how manyETH
has been transferred. Hence upon the second iteration an attempt is made to transfer another 2ETH
to the second target which will fail as the total remainingETH
sent is now 0, causing the entire function to revert.Tools Used
Manual Review
Recommended Mitigation Steps
Consider fixing this by using the aggregatorPayload.fromAmount parameter instead as is done in the
_transferOutV5
function.Assessed type
Loop