Platform: Code4rena
Start Date: 04/01/2023
Pot Size: $60,500 USDC
Total HM: 15
Participants: 105
Period: 5 days
Judge: gzeon
Total Solo HM: 1
Id: 200
League: ETH
Rank: 17/105
Findings: 2
Award: $676.14
🌟 Selected for report: 1
🚀 Solo Findings: 0
639.6409 USDC - $639.64
https://github.com/code-423n4/2023-01-biconomy/blob/main/scw-contracts/contracts/smart-contract-wallet/SmartAccount.sol#L288 https://github.com/code-423n4/2023-01-biconomy/blob/main/scw-contracts/contracts/smart-contract-wallet/SmartAccount.sol#L429-L444
The submitter of a transaction is paid back the transaction's gas costs either in ETH or in ERC20 tokens. With ERC20 tokens the following formula is used: $(gasUsed + baseGas) * gasPrice / tokenGasPriceFactor$. baseGas
, gasPrice
, and tokenGasPriceFactor
are values specified by the tx submitter. Since you don't want the submitter to choose arbitrary values and pay themselves as much as they want, those values are supposed to be signed off by the owner of the wallet. The signature of the user is included in the tx so that the contract can verify that all the values are correct. But, the tokenGasPriceFactor
value is not included in those checks. Thus, the submitter is able to simulate the tx with value $x$, get the user to sign that tx, and then submit it with $y$ for tokenGasPriceFactor
. That way they can increase the actual gas repayment and steal the user's funds.
In encodeTransactionData()
we can see that tokenGasPriceFactor
is not included:
function encodeTransactionData( Transaction memory _tx, FeeRefund memory refundInfo, uint256 _nonce ) public view returns (bytes memory) { bytes32 safeTxHash = keccak256( abi.encode( ACCOUNT_TX_TYPEHASH, _tx.to, _tx.value, keccak256(_tx.data), _tx.operation, _tx.targetTxGas, refundInfo.baseGas, refundInfo.gasPrice, refundInfo.gasToken, refundInfo.refundReceiver, _nonce ) ); return abi.encodePacked(bytes1(0x19), bytes1(0x01), domainSeparator(), safeTxHash); }
The value is used to determine the gas repayment in handlePayment()
and handlePaymentRevert()
:
function handlePayment( uint256 gasUsed, uint256 baseGas, uint256 gasPrice, uint256 tokenGasPriceFactor, address gasToken, address payable refundReceiver ) private nonReentrant returns (uint256 payment) { // uint256 startGas = gasleft(); // solhint-disable-next-line avoid-tx-origin address payable receiver = refundReceiver == address(0) ? payable(tx.origin) : refundReceiver; if (gasToken == address(0)) { // For ETH we will only adjust the gas price to not be higher than the actual used gas price payment = (gasUsed + baseGas) * (gasPrice < tx.gasprice ? gasPrice : tx.gasprice); (bool success,) = receiver.call{value: payment}(""); require(success, "BSA011"); } else { payment = (gasUsed + baseGas) * (gasPrice) / (tokenGasPriceFactor); require(transferToken(gasToken, receiver, payment), "BSA012"); } // uint256 requiredGas = startGas - gasleft(); //console.log("hp %s", requiredGas); }
That's called at the end of execTransaction()
:
if (refundInfo.gasPrice > 0) { //console.log("sent %s", startGas - gasleft()); // extraGas = gasleft(); payment = handlePayment(startGas - gasleft(), refundInfo.baseGas, refundInfo.gasPrice, refundInfo.tokenGasPriceFactor, refundInfo.gasToken, refundInfo.refundReceiver); emit WalletHandlePayment(txHash, payment); }
As an example, given that:
gasUsed = 1,000,000
baseGas = 100,000
gasPrice = 10,000,000,000
(10 gwei)tokenGasPriceFactor = 18
You get $(1,000,000 + 100,000) * 10,000,000,000 / 18 = 6.1111111e14$. If the submitter executes the transaction with tokenGasPriceFactor = 1
they get $1.1e16$ instead, i.e. 18 times more.
none
tokenGasPriceFactor
should be included in the encoded transaction data and thus verified by the user's signature.
#0 - c4-judge
2023-01-17T15:25:14Z
gzeon-c4 marked the issue as duplicate of #492
#1 - c4-sponsor
2023-01-25T08:24:49Z
livingrockrises marked the issue as sponsor confirmed
#2 - c4-judge
2023-02-10T12:31:16Z
gzeon-c4 marked the issue as satisfactory
#3 - c4-judge
2023-02-10T12:33:26Z
gzeon-c4 marked the issue as selected for report