Platform: Code4rena
Start Date: 22/09/2023
Pot Size: $100,000 USDC
Total HM: 15
Participants: 175
Period: 14 days
Judge: alcueca
Total Solo HM: 4
Id: 287
League: ETH
Rank: 175/175
Findings: 1
Award: $0.03
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: 0xTheC0der
Also found by: 0x180db, 0xDING99YA, 0xRstStn, 0xTiwa, 0xWaitress, 0xblackskull, 0xfuje, 3docSec, Aamir, Black_Box_DD, HChang26, Hama, Inspecktor, John_Femi, Jorgect, Kek, KingNFT, Kow, Limbooo, MIQUINHO, MrPotatoMagic, NoTechBG, Noro, Pessimistic, QiuhaoLi, SovaSlava, SpicyMeatball, T1MOH, TangYuanShen, Vagner, Viktor_Cortess, Yanchuan, _eperezok, alexweb3, alexxander, ast3ros, ayden, bin2chen, blutorque, btk, ciphermarco, ether_sky, gumgumzum, gztttt, hals, imare, its_basu, joaovwfreire, josephdara, klau5, kodyvim, ladboy233, marqymarq10, mert_eren, minhtrng, n1punp, nobody2018, oada, orion, peakbolt, peritoflores, perseverancesuccess, pfapostol, rvierdiiev, stuxy, tank, unsafesol, ustas, windhustler, zambody, zzzitron
0.0282 USDC - $0.03
Loss of funds
There are three vulnerabilities in the payableCall()
function that enable attackers to steal user funds.
/// @inheritdoc IVirtualAccount function payableCall(PayableCall[] calldata calls) public payable returns (bytes[] memory returnData) { uint256 valAccumulator; uint256 length = calls.length; returnData = new bytes[](length); PayableCall calldata _call; for (uint256 i = 0; i < length;) { _call = calls[i]; uint256 val = _call.value; // Humanity will be a Type V Kardashev Civilization before this overflows - andreas // ~ 10^25 Wei in existence << ~ 10^76 size uint fits in a uint256 unchecked { valAccumulator += val; } bool success; if (isContract(_call.target)) (success, returnData[i]) = _call.target.call{value: val}(_call.callData); if (!success) revert CallFailed(); unchecked { ++i; } } // Finally, make sure the msg.value = SUM(call[0...i].value) if (msg.value != valAccumulator) revert CallFailed(); }
​ First vulnerability. The payableCall
is unprotected
function payableCall(PayableCall[] calldata calls) public payable returns (bytes[] memory returnData) {
​ This vulnerability enables calling external contracts on behalf of Virtual Account.
This can be use to steal ERC20 tokens for example.
Second vulnerability An overflow can be exploited .
The _call.value
are sent as a parameter, so it is possible to overflow this.
uint256 val = _call.value; // Humanity will be a Type V Kardashev Civilization before this overflows - andreas // ~ 10^25 Wei in existence << ~ 10^76 size uint fits in a uint256 unchecked { valAccumulator += val; }
Third vulnerability EOA are ignored in the batch call and the succeed depends on the previous call.
If you send an EOA in the first call, the transaction will revert, but in the second call, it will succeed.
bool success; if (isContract(_call.target)) (success, returnData[i]) = _call.target.call{value: val}(_call.callData); if (!success) revert CallFailed();
The attacker will construct the following malicious batch call
uint balanceToSteal = address(VirtualAccount).balance; // We will steal all the ETH address attackerContract = '0x...' //This call is just to set the succeed variable to TRUE for the next call PayableCall p1 = new PayableCall(attackerContract,"", 0) // This call is just ignored but helps to overflow PayableCall p2 = new PayableCall(anyEOA,bytes(0),MAX_UINT-balanceToSteal+1) // Here we drain the contract PayableCall p3 = new PayableCall(attackerContract,"",balanceToSteal) /// This call PayableCalls[] attack = [p1,p2,p3]; The attacker will call `payableCall(attack)` without any ether.
The transaction will succeed
​ You need to fix this
Add a modifier requiresApprovedCaller()
to protect payableCall()
Set succeed
to false if the call is an EOA
if (isContract(call.target))
(success, returnData[i]) = _call.target.call{value: val}(call.callData); else success = false;
Access Control
#0 - c4-pre-sort
2023-10-09T07:21:45Z
0xA5DF marked the issue as duplicate of #888
#1 - c4-pre-sort
2023-10-09T10:45:23Z
0xA5DF marked the issue as sufficient quality report
#2 - alcueca
2023-10-26T11:34:10Z
One of those rare cases where an AI report that was not checked actually is right.
#3 - c4-judge
2023-10-26T11:34:16Z
alcueca marked the issue as partial-25
#4 - peritoflores
2023-11-09T20:19:00Z
Hi, guys...... I know you are working hard and you marked this as dup. However, in this submission I am explaining how to drain ETH which is not trivial. The submission you marked as principal only shows how to steal tokens. To drain ETH you need to exploit a vulnerability in the function that is already there.
#5 - stalinMacias
2023-11-09T21:08:53Z
I'll try to help you a little bit here, I think you miss a key point.
The moment the batch call either fails the external call or the target destination is not a contract, the whole tx will be reverted.
From your report:
If you send an EOA in the first call, the transaction will revert, but in the second call, it will succeed.
You are already acknowledging that the first call will revert the tx, so, if the tx is reverted, the execution is halted and all the changes are wiped out, the "attacker" would basically be wasting gas
the succeed depends on the previous call.
No, it does not depend on the previous call, if you take a closer look at the code, before each cal, the success bool variable is declared, so, it's default value is false, it doesn't matter if the previous call was true, on each new call the variable is defined, and as such, it's value is reset to the default, which is false
I hope this makes sense for you and helps you to understand what did you miss 🫡
#6 - peritoflores
2023-11-10T18:14:57Z
Hi @stalinMacias ... Sorry but isContract
is where you introduced the bug.... you will pass to the next call....