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: 24/105
Findings: 2
Award: $528.53
🌟 Selected for report: 0
🚀 Solo Findings: 0
492.0314 USDC - $492.03
SmartAccount.execTransaction allows the user to execute the transaction instead of the owner of the wallet, and the user will get ETH or gasToken as compensation, which requires the user to submit the owner's signature.
// Gnosis style transaction with optional repay in native tokens OR ERC20 /// @dev Allows to execute a Safe transaction confirmed by required number of owners and then pays the account that submitted the transaction. /// Note: The fees are always transferred, even if the user transaction fails. /// @param _tx Wallet transaction /// @param batchId batchId key for 2D nonces /// @param refundInfo Required information for gas refunds /// @param signatures Packed signature data ({bytes32 r}{bytes32 s}{uint8 v}) function execTransaction( Transaction memory _tx, uint256 batchId, FeeRefund memory refundInfo, bytes memory signatures ) public payable virtual override returns (bool success) { // initial gas = 21k + non_zero_bytes * 16 + zero_bytes * 4 // ~= 21k + calldata.length * [1/3 * 16 + 2/3 * 4] uint256 startGas = gasleft() + 21000 + msg.data.length * 8; //console.log("init %s", 21000 + msg.data.length * 8); bytes32 txHash; // Use scope here to limit variable lifetime and prevent `stack too deep` errors { bytes memory txHashData = encodeTransactionData( // Transaction info _tx, // Payment info refundInfo, // Signature info nonces[batchId] ); // Increase nonce and execute transaction. // Default space aka batchId is 0 nonces[batchId]++; txHash = keccak256(txHashData); checkSignatures(txHash, txHashData, signatures); }
The encodeTransactionData function is used to encode transaction data into a transaction hash, and the owner can sign the generated transaction hash. At the same time, when the user calls execTransaction, encodeTransactionData is also used to verify whether the transaction data submitted by the user conforms to the signature. But in encodeTransactionData, tokenGasPriceFactor is not included in the transaction hash, which means that any tokenGasPriceFactor submitted by the user can pass the check, and tokenGasPriceFactor will affect the number of gasTokens obtained by the user
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); } ... struct Transaction { address to; uint256 value; bytes data; Enum.Operation operation; uint256 targetTxGas; } struct FeeRefund { uint256 baseGas; uint256 gasPrice; //gasPrice or tokenGasPrice uint256 tokenGasPriceFactor; address gasToken; address payable refundReceiver; }struct Transaction { address to; uint256 value; bytes data; Enum.Operation operation; uint256 targetTxGas; } struct FeeRefund { uint256 baseGas; uint256 gasPrice; //gasPrice or tokenGasPrice uint256 tokenGasPriceFactor; address gasToken; address payable refundReceiver; }
Consider the following scenario. Alice signs a transaction. Since Alice does not have ETH as gas and msg.value, she hopes that Bob will execute the transaction instead of herself and compensate Bob with 1000 gasToken. The tokenGasPriceFactor in the transaction is 10000. Bob calls SmartAccount.execTransaction, where tokenGasPriceFactor == 1, since encodeTransactionData does not include tokenGasPriceFactor in the transaction hash, so bob passes the signature check.
In the end, bob got 1000 * 10000 / 1 == 10000000 gasToken.
} else { payment = (gasUsed + baseGas) * (gasPrice) / (tokenGasPriceFactor); require(transferToken(gasToken, receiver, payment), "BSA012"); }
https://github.com/code-423n4/2023-01-biconomy/blob/53c8c3823175aeb26dee5529eeefa81240a406ba/scw-contracts/contracts/smart-contract-wallet/SmartAccount.sol#L192-L219 https://github.com/code-423n4/2023-01-biconomy/blob/53c8c3823175aeb26dee5529eeefa81240a406ba/scw-contracts/contracts/smart-contract-wallet/SmartAccount.sol#L389-L417 https://github.com/code-423n4/2023-01-biconomy/blob/53c8c3823175aeb26dee5529eeefa81240a406ba/scw-contracts/contracts/smart-contract-wallet/SmartAccount.sol#L424-L446 https://github.com/code-423n4/2023-01-biconomy/blob/53c8c3823175aeb26dee5529eeefa81240a406ba/scw-contracts/contracts/smart-contract-wallet/SmartAccount.sol#L263-L266 https://github.com/code-423n4/2023-01-biconomy/blob/53c8c3823175aeb26dee5529eeefa81240a406ba/scw-contracts/contracts/smart-contract-wallet/BaseSmartAccount.sol#L12-L26
None
Change to
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.tokenGasPriceFactor refundInfo.gasToken, refundInfo.refundReceiver, _nonce ) ); return abi.encodePacked(bytes1(0x19), bytes1(0x01), domainSeparator(), safeTxHash); }
#0 - c4-judge
2023-01-17T06:15:48Z
gzeon-c4 marked the issue as duplicate of #492
#1 - c4-sponsor
2023-02-09T12:04:47Z
livingrockrises marked the issue as sponsor confirmed
#2 - c4-judge
2023-02-10T12:31:16Z
gzeon-c4 marked the issue as satisfactory