Any time transferOutAndCallV5 function was called with a fee on transfer token like PAXG (Whitelisted) and the aggregator contract have 0 balance of the token, the transaction will fail because a value that is greater than the value received by the aggregator contract was used in calling swapOutV5 function look at the scenario below.
Assume a user deposit 1 PAXG token with an intention of swapping to another token.
Bifrost will listen to deposit event and call transferOutAndCallV5 function with a TransferOutAndCallData.fromAmount of 1 PAXG - fee because depositWithExpiry function pass (fromAmount - fee1) to deposit event.
The transferOutAndCallV5 function will transfer the token again to aggregator contract. Another transfer fee will be deducted now we have (fromAmount - fee1 - fee2) in aggregator contract.
But (fromAmount - fee1) was used to call swapOutV5 function of the aggregator contract, Its greater than what the contract recieved.
The swapOutV5 function approve (fromAmount - fee1) to swapRouter. At the end the transaction will fail because the aggregator contract will not have enough token (it recieved fromAmount - fee1 - fee2 but approve fromAmount - fee1).
Check below codes also consider reading comments.
function _transferOutAndCallV5(TransferOutAndCallData calldata aggregationPayload) private {
// ..
// transfer aggregationPayload.fromAmount token to aggregator but it will receive aggregationPayload.fromAmount - fee
(bool transferSuccess, bytes memory data) = aggregationPayload.fromAsset.call(abi.encodeWithSignature("transfer(address,uint256)", aggregationPayload.target, aggregationPayload.fromAmount));
require(transferSuccess && (data.length == 0 || abi.decode(data, (bool))), "Failed to transfer token before dex agg call");
(bool _dexAggSuccess, ) = aggregationPayload.target.call{value: 0}(abi.encodeWithSignature("swapOutV5(address,uint256,address,address,uint256,bytes,string)",
aggregationPayload.fromAsset,
aggregationPayload.fromAmount, // original aggregationPayload.fromAmount was used in the swapOutV5 instead of aggregationPayload.fromAmount - fee
aggregationPayload.toAsset,
aggregationPayload.recipient,
aggregationPayload.amountOutMin,
aggregationPayload.payload,
aggregationPayload.originAddress
));
// ..
}
function swapOutV5(address fromAsset, uint256 fromAmount, address toAsset, address recipient, uint256 amountOutMin, bytes memory payload, string memory originAddress) public payable nonReentrant {
// ..
safeApprove(fromAsset, address(swapRouter), 0);
safeApprove(fromAsset, address(swapRouter), fromAmount); // Its (fromAmount - fee1)
// ..
// (fromAmount - fee1) was also used to call swapExactTokensForEth
(bool aggSuccess, ) = address(swapRouter).call(abi.encodeWithSignature("swapExactTokensForETH(uint256,uint256,address[],address,uint256)", fromAmount, amountOutMin, path, recipient, type(uint).max));
// ..
}
Impact
swapOutV5 operation will fail and leads to users lost of transfer fee
Proof of Concept
Below test proof that what the aggregator contract recieved is less than what its try to approve.
Add a new file name FeeOnTransferToken in contracts folder and paste below code.
Lines of code
https://github.com/code-423n4/2024-06-thorchain/blob/e3fd3c75ff994dce50d6eb66eb290d467bd494f5/ethereum/contracts/THORChain_Router.sol#L391 https://github.com/code-423n4/2024-06-thorchain/blob/e3fd3c75ff994dce50d6eb66eb290d467bd494f5/ethereum/contracts/THORChain_Router.sol#L364 https://github.com/code-423n4/2024-06-thorchain/blob/e3fd3c75ff994dce50d6eb66eb290d467bd494f5/ethereum/contracts/THORChain_Aggregator.sol#L184 https://github.com/code-423n4/2024-06-thorchain/blob/e3fd3c75ff994dce50d6eb66eb290d467bd494f5/ethereum/contracts/THORChain_Aggregator.sol#L203
Vulnerability details
Any time
transferOutAndCallV5
function was called with a fee on transfer token like PAXG (Whitelisted) and the aggregator contract have 0 balance of the token, the transaction will fail because a value that is greater than the value received by the aggregator contract was used in callingswapOutV5
function look at the scenario below.transferOutAndCallV5
function with a TransferOutAndCallData.fromAmount of 1 PAXG - fee becausedepositWithExpiry
function pass (fromAmount - fee1) to deposit event.transferOutAndCallV5
function will transfer the token again to aggregator contract. Another transfer fee will be deducted now we have (fromAmount - fee1 - fee2) in aggregator contract.swapOutV5
function of the aggregator contract, Its greater than what the contract recieved.swapOutV5
function approve (fromAmount - fee1) to swapRouter. At the end the transaction will fail because the aggregator contract will not have enough token (it recieved fromAmount - fee1 - fee2 but approve fromAmount - fee1).Check below codes also consider reading comments.
Impact
swapOutV5 operation will fail and leads to users lost of transfer fee
Proof of Concept
Below test proof that what the aggregator contract recieved is less than what its try to approve.
Tools Used
Manual review
Recommended Mitigation Steps
Use the (fromAmount - fee1 - fee2) for calling
swapOutV5
or make sure the aggregator contract does not have zero value of the calling token anytime.Assessed type
ERC20