diff --git a/packages/protocol/contracts/L1/hooks/AssignmentHook.sol b/packages/protocol/contracts/L1/hooks/AssignmentHook.sol index e085e11eaf8..adef51ea434 100644 --- a/packages/protocol/contracts/L1/hooks/AssignmentHook.sol +++ b/packages/protocol/contracts/L1/hooks/AssignmentHook.sol @@ -1,15 +1,53 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.24; +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import "@openzeppelin/contracts/utils/cryptography/SignatureChecker.sol"; import "../../common/EssentialContract.sol"; -import "./AssignmentHookBase.sol"; +import "../../common/LibStrings.sol"; +import "../../libs/LibAddress.sol"; +import "../ITaikoL1.sol"; +import "./IHook.sol"; /// @title AssignmentHook /// @notice A hook that handles prover assignment verification and fee processing. /// @custom:security-contact security@taiko.xyz -contract AssignmentHook is EssentialContract, AssignmentHookBase, IHook { +contract AssignmentHook is EssentialContract, IHook { + using LibAddress for address; + using SignatureChecker for address; + using SafeERC20 for IERC20; + + struct ProverAssignment { + address feeToken; + uint64 expiry; + uint64 maxBlockId; + uint64 maxProposedIn; + bytes32 metaHash; + bytes32 parentMetaHash; + TaikoData.TierFee[] tierFees; + bytes signature; + } + + struct Input { + ProverAssignment assignment; + uint256 tip; // A tip to L1 block builder + } + + event EtherPaymentFailed(address to, uint256 maxGas); + + /// @notice Max gas paying the prover. + /// @dev This should be large enough to prevent the worst cases for the prover. + /// To assure a trustless relationship between the proposer and the prover it's + /// the prover's job to make sure it can get paid within this limit. + uint256 public constant MAX_GAS_PAYING_PROVER = 50_000; + uint256[50] private __gap; + error HOOK_ASSIGNMENT_EXPIRED(); + error HOOK_ASSIGNMENT_INVALID_SIG(); + error HOOK_TIER_NOT_FOUND(); + /// @notice Initializes the contract. /// @param _owner The owner of this contract. msg.sender will be used if this value is zero. /// @param _addressManager The address of the {AddressManager} contract. @@ -28,10 +66,129 @@ contract AssignmentHook is EssentialContract, AssignmentHookBase, IHook { onlyFromNamed(LibStrings.B_TAIKO) nonReentrant { - _onBlockProposed(_blk, _meta, _data); + // Note that + // - 'msg.sender' is the TaikoL1 contract address + // - 'block.coinbase' is the L1 block builder + // - 'meta.coinbase' is the L2 block proposer (chosen by block's proposer) + + Input memory input = abi.decode(_data, (Input)); + ProverAssignment memory assignment = input.assignment; + + // Check assignment validity + if ( + block.timestamp > assignment.expiry + || assignment.metaHash != 0 && _blk.metaHash != assignment.metaHash + || assignment.parentMetaHash != 0 && _meta.parentMetaHash != assignment.parentMetaHash + || assignment.maxBlockId != 0 && _meta.id > assignment.maxBlockId + || assignment.maxProposedIn != 0 && block.number > assignment.maxProposedIn + ) { + revert HOOK_ASSIGNMENT_EXPIRED(); + } + + // Hash the assignment with the blobHash, this hash will be signed by + // the prover, therefore, we add a string as a prefix. + + // msg.sender is taikoL1Address + bytes32 hash = hashAssignment( + assignment, msg.sender, _meta.sender, _blk.assignedProver, _meta.blobHash + ); + + if (!_blk.assignedProver.isValidSignatureNow(hash, assignment.signature)) { + revert HOOK_ASSIGNMENT_INVALID_SIG(); + } + + // Send the liveness bond to the Taiko contract + IERC20 tko = IERC20(resolve(LibStrings.B_TAIKO_TOKEN, false)); + + // Note that we don't have to worry about + // https://github.com/crytic/slither/wiki/Detector-Documentation#arbitrary-from-in-transferfrom + // as `assignedProver` has provided a signature above to authorize this hook. + tko.safeTransferFrom(_blk.assignedProver, msg.sender, _blk.livenessBond); + + // Find the prover fee using the minimal tier + uint256 proverFee = _getProverFee(assignment.tierFees, _meta.minTier); + + // The proposer irrevocably pays a fee to the assigned prover, either in + // Ether or ERC20 tokens. + if (assignment.feeToken == address(0)) { + // Paying Ether even when proverFee is 0 to trigger a potential receive() function call. + // Note that this payment may fail if it cost more gas + bool success = _blk.assignedProver.sendEther(proverFee, MAX_GAS_PAYING_PROVER, ""); + if (!success) emit EtherPaymentFailed(_blk.assignedProver, MAX_GAS_PAYING_PROVER); + } else if (proverFee != 0 && _meta.sender != _blk.assignedProver) { + // Paying ERC20 tokens + IERC20(assignment.feeToken).safeTransferFrom( + _meta.sender, _blk.assignedProver, proverFee + ); + } + + // block.coinbase can be address(0) in tests + if (input.tip != 0 && block.coinbase != address(0)) { + address(block.coinbase).sendEtherAndVerify(input.tip); + } + + // Send all remaining Ether back to TaikoL1 contract + if (address(this).balance != 0) { + msg.sender.sendEtherAndVerify(address(this).balance); + } + } + + /// @notice Hashes the prover assignment. + /// @param _assignment The prover assignment. + /// @param _taikoL1Address The address of the TaikoL1 contract. + /// @param _blockProposer The block proposer address. + /// @param _assignedProver The assigned prover address. + /// @param _blobHash The blob hash. + /// @return The hash of the prover assignment. + function hashAssignment( + ProverAssignment memory _assignment, + address _taikoL1Address, + address _blockProposer, + address _assignedProver, + bytes32 _blobHash + ) + public + view + returns (bytes32) + { + // split up into two parts otherwise stack is too deep + bytes32 hash = keccak256( + abi.encode( + _assignment.metaHash, + _assignment.parentMetaHash, + _assignment.feeToken, + _assignment.expiry, + _assignment.maxBlockId, + _assignment.maxProposedIn, + _assignment.tierFees + ) + ); + + return keccak256( + abi.encodePacked( + LibStrings.B_PROVER_ASSIGNMENT, + ITaikoL1(_taikoL1Address).getConfig().chainId, + _taikoL1Address, + _blockProposer, + _assignedProver, + _blobHash, + hash, + address(this) + ) + ); } - function _getTaikoTokenAddress() internal view virtual override returns (address) { - return resolve(LibStrings.B_TAIKO_TOKEN, false); + function _getProverFee( + TaikoData.TierFee[] memory _tierFees, + uint16 _tierId + ) + private + pure + returns (uint256) + { + for (uint256 i; i < _tierFees.length; ++i) { + if (_tierFees[i].tier == _tierId) return _tierFees[i].fee; + } + revert HOOK_TIER_NOT_FOUND(); } } diff --git a/packages/protocol/contracts/L1/hooks/AssignmentHookBase.sol b/packages/protocol/contracts/L1/hooks/AssignmentHookBase.sol deleted file mode 100644 index e1a344ac5d6..00000000000 --- a/packages/protocol/contracts/L1/hooks/AssignmentHookBase.sol +++ /dev/null @@ -1,187 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.24; - -import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; -import "@openzeppelin/contracts/utils/cryptography/SignatureChecker.sol"; -import "../../common/LibStrings.sol"; -import "../../libs/LibAddress.sol"; -import "../ITaikoL1.sol"; -import "./IHook.sol"; - -/// @title AssignmentHookBase -/// @notice A hook that handles prover assignment verification and fee processing. -/// @custom:security-contact security@taiko.xyz -abstract contract AssignmentHookBase { - using LibAddress for address; - using SignatureChecker for address; - using SafeERC20 for IERC20; - - struct ProverAssignment { - address feeToken; - uint64 expiry; - uint64 maxBlockId; - uint64 maxProposedIn; - bytes32 metaHash; - bytes32 parentMetaHash; - TaikoData.TierFee[] tierFees; - bytes signature; - } - - struct Input { - ProverAssignment assignment; - uint256 tip; // A tip to L1 block builder - } - - error HOOK_ASSIGNMENT_EXPIRED(); - error HOOK_ASSIGNMENT_INVALID_SIG(); - error HOOK_TIER_NOT_FOUND(); - - function _onBlockProposed( - TaikoData.Block calldata _blk, - TaikoData.BlockMetadata calldata _meta, - bytes calldata _data - ) - internal - { - // Note that - // - 'msg.sender' is the TaikoL1 contract address - // - 'block.coinbase' is the L1 block builder - // - 'meta.coinbase' is the L2 block proposer (chosen by block's proposer) - - Input memory input = abi.decode(_data, (Input)); - ProverAssignment memory assignment = input.assignment; - - // Check assignment validity - if ( - block.timestamp > assignment.expiry - || assignment.metaHash != 0 && _blk.metaHash != assignment.metaHash - || assignment.parentMetaHash != 0 && _meta.parentMetaHash != assignment.parentMetaHash - || assignment.maxBlockId != 0 && _meta.id > assignment.maxBlockId - || assignment.maxProposedIn != 0 && block.number > assignment.maxProposedIn - ) { - revert HOOK_ASSIGNMENT_EXPIRED(); - } - - // Hash the assignment with the blobHash, this hash will be signed by - // the prover, therefore, we add a string as a prefix. - - // msg.sender is taikoL1Address - bytes32 hash = hashAssignment( - assignment, msg.sender, _meta.sender, _blk.assignedProver, _meta.blobHash - ); - - if (Address.isContract(_blk.assignedProver)) { - if (!_blk.assignedProver.isValidERC1271SignatureNow(hash, assignment.signature)) { - revert HOOK_ASSIGNMENT_INVALID_SIG(); - } - } else { - (address recovered, ECDSA.RecoverError error) = - ECDSA.tryRecover(hash, assignment.signature); - if (recovered != _blk.assignedProver || error != ECDSA.RecoverError.NoError) { - revert HOOK_ASSIGNMENT_INVALID_SIG(); - } - } - - // Send the liveness bond to the Taiko contract - IERC20 tko = IERC20(_getTaikoTokenAddress()); - - // Note that we don't have to worry about - // https://github.com/crytic/slither/wiki/Detector-Documentation#arbitrary-from-in-transferfrom - // as `assignedProver` has provided a signature above to authorize this hook. - tko.transferFrom(_blk.assignedProver, msg.sender, _blk.livenessBond); - - // Find the prover fee using the minimal tier - uint256 proverFee = _getProverFee(assignment.tierFees, _meta.minTier); - - // The proposer irrevocably pays a fee to the assigned prover, either in - // Ether or ERC20 tokens. - if (proverFee != 0) { - if (assignment.feeToken == address(0)) { - // Do not check `_meta.sender != _blk.assignedProver` as Ether has been forwarded - // from TaikoL1 to this hook. - _blk.assignedProver.sendEtherAndVerify(proverFee); - } else if (_meta.sender != _blk.assignedProver) { - if (assignment.feeToken == address(tko)) { - tko.transferFrom(_meta.sender, _blk.assignedProver, proverFee); // Paying TKO - } else { - // Other ERC20 - IERC20(assignment.feeToken).safeTransferFrom( - _meta.sender, _blk.assignedProver, proverFee - ); - } - } - } - - // block.coinbase can be address(0) in tests - if (input.tip != 0 && block.coinbase != address(0)) { - address(block.coinbase).sendEtherAndVerify(input.tip); - } - - // Send all remaining Ether back to TaikoL1 contract - if (address(this).balance != 0) { - msg.sender.sendEtherAndVerify(address(this).balance); - } - } - - /// @notice Hashes the prover assignment. - /// @param _assignment The prover assignment. - /// @param _taikoL1Address The address of the TaikoL1 contract. - /// @param _blockProposer The block proposer address. - /// @param _assignedProver The assigned prover address. - /// @param _blobHash The blob hash. - /// @return The hash of the prover assignment. - function hashAssignment( - ProverAssignment memory _assignment, - address _taikoL1Address, - address _blockProposer, - address _assignedProver, - bytes32 _blobHash - ) - public - view - returns (bytes32) - { - // split up into two parts otherwise stack is too deep - bytes32 hash = keccak256( - abi.encode( - _assignment.metaHash, - _assignment.parentMetaHash, - _assignment.feeToken, - _assignment.expiry, - _assignment.maxBlockId, - _assignment.maxProposedIn, - _assignment.tierFees - ) - ); - - return keccak256( - abi.encodePacked( - LibStrings.B_PROVER_ASSIGNMENT, - ITaikoL1(_taikoL1Address).getConfig().chainId, - _taikoL1Address, - _blockProposer, - _assignedProver, - _blobHash, - hash, - address(this) - ) - ); - } - - function _getProverFee( - TaikoData.TierFee[] memory _tierFees, - uint16 _tierId - ) - private - pure - returns (uint256) - { - for (uint256 i; i < _tierFees.length; ++i) { - if (_tierFees[i].tier == _tierId) return _tierFees[i].fee; - } - revert HOOK_TIER_NOT_FOUND(); - } - - function _getTaikoTokenAddress() internal view virtual returns (address); -} diff --git a/packages/protocol/contracts/L1/hooks/ProxylessAssignmentHook.sol b/packages/protocol/contracts/L1/hooks/ProxylessAssignmentHook.sol deleted file mode 100644 index 808eb871594..00000000000 --- a/packages/protocol/contracts/L1/hooks/ProxylessAssignmentHook.sol +++ /dev/null @@ -1,33 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.24; - -import "@openzeppelin/contracts/security/ReentrancyGuard.sol"; -import "./AssignmentHookBase.sol"; - -/// @title ProxylessAssignmentHook -/// @notice A hook that handles prover assignment verification and fee processing. -/// This contract is not proxy-able to reduce gas cost. -/// @custom:security-contact security@taiko.xyz -contract ProxylessAssignmentHook is ReentrancyGuard, AssignmentHookBase, IHook { - error HOOK_PERMISSION_DENIED(); - - address private constant _TAIKO_L1 = 0x06a9Ab27c7e2255df1815E6CC0168d7755Feb19a; - - /// @inheritdoc IHook - function onBlockProposed( - TaikoData.Block calldata _blk, - TaikoData.BlockMetadata calldata _meta, - bytes calldata _data - ) - external - payable - nonReentrant - { - if (msg.sender != _TAIKO_L1) revert HOOK_PERMISSION_DENIED(); - _onBlockProposed(_blk, _meta, _data); - } - - function _getTaikoTokenAddress() internal pure virtual override returns (address) { - return 0x10dea67478c5F8C5E2D90e5E9B26dBe60c54d800; - } -} diff --git a/packages/protocol/deployments/mainnet-contract-logs-L1.md b/packages/protocol/deployments/mainnet-contract-logs-L1.md index 059d1be7ff7..9054985c57e 100644 --- a/packages/protocol/deployments/mainnet-contract-logs-L1.md +++ b/packages/protocol/deployments/mainnet-contract-logs-L1.md @@ -200,11 +200,16 @@ - Upgrade to `0xE84DC8E2a21e59426542Ab040D77f81d6dB881eE` @commit`3ae25fd` @tx`0x2c455ae888a23c232bb5c7603657eda010ffadc602a74e626332bc06eaaa3b78` - Upgrade to `0x4b2743B869b85d5F7D8020566f92664995E4f3c5` @commit`a3faee0` @tx`eth:0x40A2aCCbd92BCA938b02010E17A5b8929b49130D` -#### proxyless_assignment_hook +#### assignment_hook -- impl: `0xA641a2d6C0112E5eC6BAF2FA40d519323A085248` +- proxy: `0x537a2f0D3a5879b41BCb5A2afE2EA5c4961796F6` +- impl: `0xe226fAd08E2f0AE68C32Eb5d8210fFeDB736Fb0d` +- owner: `admin.taiko.eth` - logs: - - deployed on Jun 6, 2024 @commit`a3faee0` @tx`eth:0x40A2aCCbd92BCA938b02010E17A5b8929b49130D` + - deployed on May 1, 2024 @commit`56dddf2b6` + - Upgraded from `0x4f664222C3fF6207558A745648B568D095dDA170` to `0xe226fAd08E2f0AE68C32Eb5d8210fFeDB736Fb0d` @commit`b90b932` @tx`0x416560cd96dc75ccffebe889e8d1ab3e08b33f814dc4a2bf7c6f9555071d1f6f` +- todo: + - upgrade assignment hook (remove a large Event) #### tier_provider diff --git a/packages/protocol/test/L1/TaikoL1TestBase.sol b/packages/protocol/test/L1/TaikoL1TestBase.sol index 0f46a525d33..525660c06fa 100644 --- a/packages/protocol/test/L1/TaikoL1TestBase.sol +++ b/packages/protocol/test/L1/TaikoL1TestBase.sol @@ -143,7 +143,7 @@ abstract contract TaikoL1TestBase is TaikoTest { // anyways uint256 msgValue = 2 ether; - AssignmentHookBase.ProverAssignment memory assignment = AssignmentHookBase.ProverAssignment({ + AssignmentHook.ProverAssignment memory assignment = AssignmentHook.ProverAssignment({ feeToken: address(0), tierFees: tierFees, expiry: uint64(block.timestamp + 60 minutes), diff --git a/packages/protocol/test/L1/TaikoL1TestGroupBase.sol b/packages/protocol/test/L1/TaikoL1TestGroupBase.sol index 20873f9afe3..b1709355e8b 100644 --- a/packages/protocol/test/L1/TaikoL1TestGroupBase.sol +++ b/packages/protocol/test/L1/TaikoL1TestGroupBase.sol @@ -32,7 +32,7 @@ abstract contract TaikoL1TestGroupBase is TaikoL1TestBase { tierFees[0] = TaikoData.TierFee(LibTiers.TIER_OPTIMISTIC, 1 ether); tierFees[1] = TaikoData.TierFee(LibTiers.TIER_SGX, 2 ether); - AssignmentHookBase.ProverAssignment memory assignment = AssignmentHookBase.ProverAssignment({ + AssignmentHook.ProverAssignment memory assignment = AssignmentHook.ProverAssignment({ feeToken: address(0), tierFees: tierFees, expiry: uint64(block.timestamp + 60 minutes),