diff --git a/packages/protocol/.eslintignore b/packages/protocol/.eslintignore index dcb9ae39eee..dcdd38e4805 100644 --- a/packages/protocol/.eslintignore +++ b/packages/protocol/.eslintignore @@ -3,4 +3,5 @@ artifacts cache coverage lib +contracts/test/thirdparty/ contracts/automata-attestation/ \ No newline at end of file diff --git a/packages/protocol/.solhintignore b/packages/protocol/.solhintignore index b98f25593a5..9597bc92c5d 100644 --- a/packages/protocol/.solhintignore +++ b/packages/protocol/.solhintignore @@ -3,5 +3,6 @@ lib/ contracts/test/TestLibRLPReader.sol **/contracts/thirdparty/**/*.sol /contracts/automata-attestation/ +test/thirdparty/ test/GasComparison.t.sol test/TestLn.sol \ No newline at end of file diff --git a/packages/protocol/contracts/L2/DelegateOwner.sol b/packages/protocol/contracts/L2/DelegateOwner.sol index eede69dac1b..eab5e8275da 100644 --- a/packages/protocol/contracts/L2/DelegateOwner.sol +++ b/packages/protocol/contracts/L2/DelegateOwner.sol @@ -38,9 +38,9 @@ contract DelegateOwner is EssentialContract, IMessageInvocable { /// @param txId The transaction ID. /// @param target The target address. /// @param isDelegateCall True if the call is a `delegatecall`. - /// @param selector The function selector. + /// @param txdata The transaction data. event MessageInvoked( - uint64 indexed txId, address indexed target, bool isDelegateCall, bytes4 indexed selector + uint64 indexed txId, address indexed target, bool isDelegateCall, bytes txdata ); /// @notice Emitted when the admin has been changed. @@ -123,7 +123,13 @@ contract DelegateOwner is EssentialContract, IMessageInvocable { function _invokeCall(bytes calldata _data, bool _verifyTxId) private { Call memory call = abi.decode(_data, (Call)); - if (_verifyTxId && call.txId != nextTxId++) revert DO_INVALID_TX_ID(); + if (call.txId == 0) { + call.txId = nextTxId; + } else if (_verifyTxId && call.txId != nextTxId) { + revert DO_INVALID_TX_ID(); + } + + nextTxId += 1; // By design, the target must be a contract address if the txdata is not empty if (call.txdata.length != 0 && !Address.isContract(call.target)) revert DO_INVALID_TARGET(); @@ -133,7 +139,7 @@ contract DelegateOwner is EssentialContract, IMessageInvocable { : call.target.call{ value: msg.value }(call.txdata); if (!success) LibBytes.revertWithExtractedError(result); - emit MessageInvoked(call.txId, call.target, call.isDelegateCall, bytes4(call.txdata)); + emit MessageInvoked(call.txId, call.target, call.isDelegateCall, call.txdata); } function _isAdminOrRemoteOwner(address _sender) private view returns (bool) { diff --git a/packages/protocol/deployments/mainnet-contract-logs-L2.md b/packages/protocol/deployments/mainnet-contract-logs-L2.md index d2cabf06b37..449a96d2ead 100644 --- a/packages/protocol/deployments/mainnet-contract-logs-L2.md +++ b/packages/protocol/deployments/mainnet-contract-logs-L2.md @@ -117,10 +117,10 @@ #### delegate_owner -- proxy: `0x904aa0aC002532f1410457484893107757683F53` -- impl: `0x9F0C40A474E0FB6b27D71c43Aff840B9c42f0C44` -- admin: `0x8F13E3a9dFf52e282884aA70eAe93F57DD601298` -- remoteOwner: `0x8F13E3a9dFf52e282884aA70eAe93F57DD601298` +- proxy: `0x5995941Df88F30Ac140515AA39832db963E2f863` +- impl: `0x1f0511cDae2fbfD93563469dA02b82dEd320C8Bd` +- admin: `0x3c181965C5cFAE61a9010A283e5e0C1445649810` // owned by Daniel W +- remoteOwner: `0x3c181965C5cFAE61a9010A283e5e0C1445649810` // owned by Daniel W - todo: - test various use cases - transfer remote owner to `admin.taiko.eth` diff --git a/packages/protocol/script/DeployL2DelegateOwner.s.sol b/packages/protocol/script/DeployL2DelegateOwner.s.sol index a44dc001239..108ea7abbed 100644 --- a/packages/protocol/script/DeployL2DelegateOwner.s.sol +++ b/packages/protocol/script/DeployL2DelegateOwner.s.sol @@ -7,8 +7,10 @@ import "../contracts/L2/DelegateOwner.sol"; // forge script --rpc-url https://rpc.mainnet.taiko.xyz script/DeployL2DelegateOwner.s.sol contract DeployL2DelegateOwner is DeployCapability { address public l2Sam = 0x1670000000000000000000000000000000000006; - address public l1Owner = 0x8F13E3a9dFf52e282884aA70eAe93F57DD601298; // Daniel's EOA - address public l2Admin = 0x8F13E3a9dFf52e282884aA70eAe93F57DD601298; // same + address public testAccount2 = 0x3c181965C5cFAE61a9010A283e5e0C1445649810; // owned by Daniel W + + address public l1Owner = testAccount2; + address public l2Admin = testAccount2; modifier broadcast() { vm.startBroadcast(); diff --git a/packages/protocol/script/L1DelegateOwnerBatching.s.sol b/packages/protocol/script/L1DelegateOwnerBatching.s.sol deleted file mode 100644 index c818703cabf..00000000000 --- a/packages/protocol/script/L1DelegateOwnerBatching.s.sol +++ /dev/null @@ -1,68 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.24; - -import "../test/DeployCapability.sol"; -import "../contracts/L2/DelegateOwner.sol"; -import "../contracts/bridge/Bridge.sol"; -import "../test/common/TestMulticall3.sol"; - -// forge script \ -// --rpc-url https://mainnet.infura.io/v3/... \ -// --private-key ... \ -// --legacy \ -// --broadcast \ -// script/DeployL2DelegateOwner.s.sol -contract L2DelegateOwnerBatching is DeployCapability { - address public l2Admin = 0x8F13E3a9dFf52e282884aA70eAe93F57DD601298; // same - address public l2DelegateOwner = 0x08c82ab90f86BF8d98440b96754a67411D656130; - address public constant l2Multicall3 = 0xcA11bde05977b3631167028862bE2a173976CA11; - - modifier broadcast() { - vm.startBroadcast(); - _; - vm.stopBroadcast(); - } - - function run() external broadcast { - TestMulticall3.Call3[] memory calls = new TestMulticall3.Call3[](2); - calls[0].target = 0x08c82ab90f86BF8d98440b96754a67411D656130; - calls[0].allowFailure = false; - calls[0].callData = - abi.encodeCall(DelegateOwner.setAdmin, (0x4757D97449acA795510b9f3152C6a9019A3545c3)); - - calls[1].target = 0xf4707c2821b3067bdF9c4D48eB133851FF3e7ea7; - calls[1].allowFailure = false; - calls[1].callData = - abi.encodeCall(UUPSUpgradeable.upgradeTo, (0x0167000000000000000000000000000000010002)); - - sendMessage(0, calls); - } - - function sendMessage(uint64 txId, TestMulticall3.Call3[] memory calls) internal { - IBridge.Message memory message; - message.fee = 20_000_000_000_000; - message.gasLimit = 2_000_000; - message.destChainId = 167_000; - - // TODO: What if l2Admin becomes 0x0? - message.srcOwner = l2Admin; - message.destOwner = l2Admin; - message.to = l2DelegateOwner; - - message.data = abi.encodeCall( - DelegateOwner.onMessageInvocation, - abi.encode( - DelegateOwner.Call( - txId, - l2Multicall3, - true, // DELEGATECALL - abi.encodeCall(TestMulticall3.aggregate3, (calls)) - ) - ) - ); - - Bridge(0xd60247c6848B7Ca29eDdF63AA924E53dB6Ddd8EC).sendMessage{ value: message.fee }( - message - ); - } -} diff --git a/packages/protocol/script/SendMessageToDelegateOwner.s.sol b/packages/protocol/script/SendMessageToDelegateOwner.s.sol new file mode 100644 index 00000000000..66bed56a7c2 --- /dev/null +++ b/packages/protocol/script/SendMessageToDelegateOwner.s.sol @@ -0,0 +1,57 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.24; + +import "forge-std/src/Script.sol"; +import "../contracts/L2/DelegateOwner.sol"; +import "../contracts/bridge/IBridge.sol"; +import "../test/thirdparty/muticall3/Multicall3.sol"; + +contract SendMessageToDelegateOwner is Script { + address public delegateOwner = 0x5995941Df88F30Ac140515AA39832db963E2f863; + address public delegateOwnerImpl = 0x1f0511cDae2fbfD93563469dA02b82dEd320C8Bd; + address public multicall3 = 0xcA11bde05977b3631167028862bE2a173976CA11; + address public l1Bridge = 0xd60247c6848B7Ca29eDdF63AA924E53dB6Ddd8EC; + address public testAccount1 = 0x3c181965C5cFAE61a9010A283e5e0C1445649810; // owned by Daniel W + + modifier broadcast() { + vm.startBroadcast(); + _; + vm.stopBroadcast(); + } + + function run() external broadcast { + Multicall3.Call3[] memory calls = new Multicall3.Call3[](2); + calls[0].target = delegateOwner; + calls[0].allowFailure = false; + calls[0].callData = + abi.encodeCall(DelegateOwner.setAdmin, (0x4757D97449acA795510b9f3152C6a9019A3545c3)); + + calls[1].target = delegateOwner; + calls[1].allowFailure = false; + calls[1].callData = abi.encodeCall(UUPSUpgradeable.upgradeTo, (delegateOwnerImpl)); + + DelegateOwner.Call memory dcall = DelegateOwner.Call({ + txId: 1, // Has to match with DelegateOwner's nextTxId or 0 + target: multicall3, + isDelegateCall: true, + txdata: abi.encodeCall(Multicall3.aggregate3, (calls)) + }); + + // Use https://bridge.taiko.xyz/relayer to manually trigger the message if necessary. + IBridge.Message memory message = IBridge.Message({ + id: 0, + fee: 0, + gasLimit: 1_000_000, // cannot be zero + from: msg.sender, + srcChainId: 1, + srcOwner: msg.sender, + destChainId: 167_000, + destOwner: delegateOwner, + to: delegateOwner, + value: 0, + data: abi.encodeCall(DelegateOwner.onMessageInvocation, abi.encode(dcall)) + }); + + IBridge(l1Bridge).sendMessage(message); + } +} diff --git a/packages/protocol/test/L2/DelegateOwner.t.sol b/packages/protocol/test/L2/DelegateOwner.t.sol index ef690e99d4f..f6dac448608 100644 --- a/packages/protocol/test/L2/DelegateOwner.t.sol +++ b/packages/protocol/test/L2/DelegateOwner.t.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.24; -import "../common/TestMulticall3.sol"; +import "../thirdparty/muticall3/Multicall3.sol"; import "../TaikoTest.sol"; contract Target is EssentialContract { @@ -17,7 +17,7 @@ contract TestDelegateOwner is TaikoTest { SignalService public signalService; AddressManager public addressManager; DelegateOwner public delegateOwner; - TestMulticall3 public multicall; + Multicall3 public multicall; uint64 remoteChainId = uint64(block.chainid + 1); address remoteBridge = vm.addr(0x2000); @@ -30,7 +30,7 @@ contract TestDelegateOwner is TaikoTest { vm.startPrank(owner); - multicall = new TestMulticall3(); + multicall = new Multicall3(); addressManager = AddressManager( deployProxy({ @@ -169,7 +169,7 @@ contract TestDelegateOwner is TaikoTest { }) ); - TestMulticall3.Call3[] memory calls = new TestMulticall3.Call3[](4); + Multicall3.Call3[] memory calls = new Multicall3.Call3[](4); calls[0].target = address(target1); calls[0].allowFailure = false; calls[0].callData = abi.encodeCall(EssentialContract.pause, ()); @@ -191,7 +191,7 @@ contract TestDelegateOwner is TaikoTest { uint64(0), address(multicall), true, // DELEGATECALL - abi.encodeCall(TestMulticall3.aggregate3, (calls)) + abi.encodeCall(Multicall3.aggregate3, (calls)) ) ); diff --git a/packages/protocol/test/common/TestMulticall3.sol b/packages/protocol/test/common/TestMulticall3.sol deleted file mode 100644 index bc0620169b5..00000000000 --- a/packages/protocol/test/common/TestMulticall3.sol +++ /dev/null @@ -1,57 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.24; - -/// @title Multicall3 -/// @notice Aggregate results from multiple function calls -/// @dev Multicall & Multicall2 backwards-compatible -/// @dev Aggregate methods are marked `payable` to save 24 gas per call -/// @author Michael Elliot -/// @author Joshua Levine -/// @author Nick Johnson -/// @author Andreas Bigger -/// @author Matt Solomon -contract TestMulticall3 { - struct Call3 { - address target; - bool allowFailure; - bytes callData; - } - - struct Result { - bool success; - bytes returnData; - } - - /// @notice Aggregate calls, ensuring each returns success if required - /// @param calls An array of Call3 structs - /// @return returnData An array of Result structs - function aggregate3(Call3[] calldata calls) - public - payable - returns (Result[] memory returnData) - { - uint256 length = calls.length; - returnData = new Result[](length); - Call3 calldata calli; - for (uint256 i = 0; i < length; ++i) { - Result memory result = returnData[i]; - calli = calls[i]; - (result.success, result.returnData) = calli.target.call(calli.callData); - assembly { - // Revert if the call fails and failure is not allowed - // `allowFailure := calldataload(add(calli, 0x20))` and `success := mload(result)` - if iszero(or(calldataload(add(calli, 0x20)), mload(result))) { - // set "Error(string)" signature: bytes32(bytes4(keccak256("Error(string)"))) - mstore(0x00, 0x08c379a000000000000000000000000000000000000000000000000000000000) - // set data offset - mstore(0x04, 0x0000000000000000000000000000000000000000000000000000000000000020) - // set length of revert string - mstore(0x24, 0x0000000000000000000000000000000000000000000000000000000000000017) - // set revert string: bytes32(abi.encodePacked("Multicall3: call failed")) - mstore(0x44, 0x4d756c746963616c6c333a2063616c6c206661696c6564000000000000000000) - revert(0x00, 0x64) - } - } - } - } -} diff --git a/packages/protocol/test/thirdparty/muticall3/Multicall3.sol b/packages/protocol/test/thirdparty/muticall3/Multicall3.sol new file mode 100644 index 00000000000..205cc0b7e6a --- /dev/null +++ b/packages/protocol/test/thirdparty/muticall3/Multicall3.sol @@ -0,0 +1,260 @@ +/** + * Submitted for verification at taikoscan.io on 2024-05-30 + */ + +// SPDX-License-Identifier: MIT +pragma solidity 0.8.24; + +/// @title Multicall3 +/// @notice Aggregate results from multiple function calls +/// @dev Multicall & Multicall2 backwards-compatible +/// @dev Aggregate methods are marked `payable` to save 24 gas per call +/// @author Michael Elliot +/// @author Joshua Levine +/// @author Nick Johnson +/// @author Andreas Bigger +/// @author Matt Solomon +contract Multicall3 { + struct Call { + address target; + bytes callData; + } + + struct Call3 { + address target; + bool allowFailure; + bytes callData; + } + + struct Call3Value { + address target; + bool allowFailure; + uint256 value; + bytes callData; + } + + struct Result { + bool success; + bytes returnData; + } + + /// @notice Backwards-compatible call aggregation with Multicall + /// @param calls An array of Call structs + /// @return blockNumber The block number where the calls were executed + /// @return returnData An array of bytes containing the responses + function aggregate(Call[] calldata calls) + public + payable + returns (uint256 blockNumber, bytes[] memory returnData) + { + blockNumber = block.number; + uint256 length = calls.length; + returnData = new bytes[](length); + Call calldata call; + for (uint256 i = 0; i < length;) { + bool success; + call = calls[i]; + (success, returnData[i]) = call.target.call(call.callData); + require(success, "Multicall3: call failed"); + unchecked { + ++i; + } + } + } + + /// @notice Backwards-compatible with Multicall2 + /// @notice Aggregate calls without requiring success + /// @param requireSuccess If true, require all calls to succeed + /// @param calls An array of Call structs + /// @return returnData An array of Result structs + function tryAggregate( + bool requireSuccess, + Call[] calldata calls + ) + public + payable + returns (Result[] memory returnData) + { + uint256 length = calls.length; + returnData = new Result[](length); + Call calldata call; + for (uint256 i = 0; i < length;) { + Result memory result = returnData[i]; + call = calls[i]; + (result.success, result.returnData) = call.target.call(call.callData); + if (requireSuccess) require(result.success, "Multicall3: call failed"); + unchecked { + ++i; + } + } + } + + /// @notice Backwards-compatible with Multicall2 + /// @notice Aggregate calls and allow failures using tryAggregate + /// @param calls An array of Call structs + /// @return blockNumber The block number where the calls were executed + /// @return blockHash The hash of the block where the calls were executed + /// @return returnData An array of Result structs + function tryBlockAndAggregate( + bool requireSuccess, + Call[] calldata calls + ) + public + payable + returns (uint256 blockNumber, bytes32 blockHash, Result[] memory returnData) + { + blockNumber = block.number; + blockHash = blockhash(block.number); + returnData = tryAggregate(requireSuccess, calls); + } + + /// @notice Backwards-compatible with Multicall2 + /// @notice Aggregate calls and allow failures using tryAggregate + /// @param calls An array of Call structs + /// @return blockNumber The block number where the calls were executed + /// @return blockHash The hash of the block where the calls were executed + /// @return returnData An array of Result structs + function blockAndAggregate(Call[] calldata calls) + public + payable + returns (uint256 blockNumber, bytes32 blockHash, Result[] memory returnData) + { + (blockNumber, blockHash, returnData) = tryBlockAndAggregate(true, calls); + } + + /// @notice Aggregate calls, ensuring each returns success if required + /// @param calls An array of Call3 structs + /// @return returnData An array of Result structs + function aggregate3(Call3[] calldata calls) + public + payable + returns (Result[] memory returnData) + { + uint256 length = calls.length; + returnData = new Result[](length); + Call3 calldata calli; + for (uint256 i = 0; i < length;) { + Result memory result = returnData[i]; + calli = calls[i]; + (result.success, result.returnData) = calli.target.call(calli.callData); + assembly { + // Revert if the call fails and failure is not allowed + // `allowFailure := calldataload(add(calli, 0x20))` and `success := mload(result)` + if iszero(or(calldataload(add(calli, 0x20)), mload(result))) { + // set "Error(string)" signature: bytes32(bytes4(keccak256("Error(string)"))) + mstore(0x00, 0x08c379a000000000000000000000000000000000000000000000000000000000) + // set data offset + mstore(0x04, 0x0000000000000000000000000000000000000000000000000000000000000020) + // set length of revert string + mstore(0x24, 0x0000000000000000000000000000000000000000000000000000000000000017) + // set revert string: bytes32(abi.encodePacked("Multicall3: call failed")) + mstore(0x44, 0x4d756c746963616c6c333a2063616c6c206661696c6564000000000000000000) + revert(0x00, 0x64) + } + } + unchecked { + ++i; + } + } + } + + /// @notice Aggregate calls with a msg value + /// @notice Reverts if msg.value is less than the sum of the call values + /// @param calls An array of Call3Value structs + /// @return returnData An array of Result structs + function aggregate3Value(Call3Value[] calldata calls) + public + payable + returns (Result[] memory returnData) + { + uint256 valAccumulator; + uint256 length = calls.length; + returnData = new Result[](length); + Call3Value calldata calli; + for (uint256 i = 0; i < length;) { + Result memory result = returnData[i]; + calli = calls[i]; + uint256 val = calli.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; + } + (result.success, result.returnData) = calli.target.call{ value: val }(calli.callData); + assembly { + // Revert if the call fails and failure is not allowed + // `allowFailure := calldataload(add(calli, 0x20))` and `success := mload(result)` + if iszero(or(calldataload(add(calli, 0x20)), mload(result))) { + // set "Error(string)" signature: bytes32(bytes4(keccak256("Error(string)"))) + mstore(0x00, 0x08c379a000000000000000000000000000000000000000000000000000000000) + // set data offset + mstore(0x04, 0x0000000000000000000000000000000000000000000000000000000000000020) + // set length of revert string + mstore(0x24, 0x0000000000000000000000000000000000000000000000000000000000000017) + // set revert string: bytes32(abi.encodePacked("Multicall3: call failed")) + mstore(0x44, 0x4d756c746963616c6c333a2063616c6c206661696c6564000000000000000000) + revert(0x00, 0x84) + } + } + unchecked { + ++i; + } + } + // Finally, make sure the msg.value = SUM(call[0...i].value) + require(msg.value == valAccumulator, "Multicall3: value mismatch"); + } + + /// @notice Returns the block hash for the given block number + /// @param blockNumber The block number + function getBlockHash(uint256 blockNumber) public view returns (bytes32 blockHash) { + blockHash = blockhash(blockNumber); + } + + /// @notice Returns the block number + function getBlockNumber() public view returns (uint256 blockNumber) { + blockNumber = block.number; + } + + /// @notice Returns the block coinbase + function getCurrentBlockCoinbase() public view returns (address coinbase) { + coinbase = block.coinbase; + } + + /// @notice Returns the block difficulty + function getCurrentBlockDifficulty() public view returns (uint256 difficulty) { + difficulty = block.difficulty; + } + + /// @notice Returns the block gas limit + function getCurrentBlockGasLimit() public view returns (uint256 gaslimit) { + gaslimit = block.gaslimit; + } + + /// @notice Returns the block timestamp + function getCurrentBlockTimestamp() public view returns (uint256 timestamp) { + timestamp = block.timestamp; + } + + /// @notice Returns the (ETH) balance of a given address + function getEthBalance(address addr) public view returns (uint256 balance) { + balance = addr.balance; + } + + /// @notice Returns the block hash of the last block + function getLastBlockHash() public view returns (bytes32 blockHash) { + unchecked { + blockHash = blockhash(block.number - 1); + } + } + + /// @notice Gets the base fee of the given block + /// @notice Can revert if the BASEFEE opcode is not implemented by the given chain + function getBasefee() public view returns (uint256 basefee) { + basefee = block.basefee; + } + + /// @notice Returns the chain id + function getChainId() public view returns (uint256 chainid) { + chainid = block.chainid; + } +}