Platform: Code4rena
Start Date: 04/03/2024
Pot Size: $140,000 USDC
Total HM: 19
Participants: 69
Period: 21 days
Judge: 0xean
Total Solo HM: 4
Id: 343
League: ETH
Rank: 13/69
Findings: 1
Award: $2,004.23
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: zzebra83
Also found by: MrPotatoMagic, ladboy233, mojito_auditor, monrel
2004.2337 USDC - $2,004.23
https://github.com/code-423n4/2024-03-taiko/blob/f58384f44dbf4c6535264a472322322705133b11/packages/protocol/contracts/L1/libs/LibProposing.sol#L68 https://github.com/code-423n4/2024-03-taiko/blob/f58384f44dbf4c6535264a472322322705133b11/packages/protocol/contracts/L1/hooks/AssignmentHook.sol#L115
When a block is proposed using the proposeBlock()
function, the sender can specify the params.coinbase
address. This is the L2 block proposer address, which receives fees from ETH deposits when processing deposits. It also pays fees to the assigned prover in either ETH or ERC20 tokens in the onBlockProposed()
function.
// The proposer irrevocably pays a fee to the assigned prover, either in // Ether or ERC20 tokens. if (assignment.feeToken == address(0)) { // Paying Ether _blk.assignedProver.sendEther(proverFee, MAX_GAS_PAYING_PROVER); } else { // Paying ERC20 tokens IERC20(assignment.feeToken).safeTransferFrom( _meta.coinbase, _blk.assignedProver, proverFee // @audit coinbase address could be arbitrary ); }
As shown, if the proposer pays in ERC20 tokens, it uses safeTransferFrom()
to directly transfer from the _meta.coinbase
address to the assignedProver
. As a result, the sender can specify the coinbase address as any address that has approved the contract, to pay the fee to the assigned prover.
There is no validation for params.coinbase
in the proposeBlock()
function. Even if it's set to address(0)
, it defaults to the sender's address.
function proposeBlock( ... ) internal returns (TaikoData.BlockMetadata memory meta_, TaikoData.EthDeposit[] memory deposits_) { ... if (params.coinbase == address(0)) { params.coinbase = msg.sender; } ... deposits_ = LibDepositing.processDeposits(_state, _config, params.coinbase); ... unchecked { meta_ = TaikoData.BlockMetadata({ l1Hash: blockhash(block.number - 1), difficulty: 0, // to be initialized below blobHash: 0, // to be initialized below extraData: params.extraData, depositsHash: keccak256(abi.encode(deposits_)), coinbase: params.coinbase, id: b.numBlocks, gasLimit: _config.blockMaxGasLimit, timestamp: uint64(block.timestamp), l1Height: uint64(block.number - 1), txListByteOffset: 0, // to be initialized below txListByteSize: 0, // to be initialized below minTier: 0, // to be initialized below blobUsed: _txList.length == 0, parentMetaHash: parentMetaHash }); } ... IHook(params.hookCalls[i].hook).onBlockProposed{ value: address(this).balance }( blk, meta_, params.hookCalls[i].data ); }
The meta_.coinbase
address is set to the params.coinbase
address. Then meta_
is passed as an input parameter when calling onBlockProposed()
. In this function, the _meta.coinbase
address will pay a fee to the assigned prover in ERC20 tokens, as shown in the previous code snippet.
Manual Review
Add a check to ensure the coinbase address has given permission to the sender to call proposeBlock()
on its behalf.
Other
#0 - c4-pre-sort
2024-03-28T00:28:00Z
minhquanym marked the issue as duplicate of #163
#1 - c4-judge
2024-04-10T11:20:10Z
0xean marked the issue as satisfactory