-
Notifications
You must be signed in to change notification settings - Fork 2.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(protocol): allow L1 timelock to be the L2 owner #15358
Changes from all commits
525010e
3c2ab98
fa9e4c5
aecb92f
f781c83
c8fc9a4
ae9af5a
e1773f3
7ccffa7
91ac839
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,117 @@ | ||
// SPDX-License-Identifier: MIT | ||
// _____ _ _ _ _ | ||
// |_ _|_ _(_) |_____ | | __ _| |__ ___ | ||
// | |/ _` | | / / _ \ | |__/ _` | '_ (_-< | ||
// |_|\__,_|_|_\_\___/ |____\__,_|_.__/__/ | ||
|
||
pragma solidity 0.8.20; | ||
|
||
import "lib/openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; | ||
|
||
import "../common/EssentialContract.sol"; | ||
import "../signal/ISignalService.sol"; | ||
|
||
/// @title CrossChainOwned | ||
/// @notice This contract's owner lives on another chain who uses signal for transaction approval. | ||
abstract contract CrossChainOwned is EssentialContract { | ||
uint64 public ownerChainId; // slot 1 | ||
uint64 public nextTxId; | ||
uint256[49] private __gap; | ||
|
||
event TransactionExecuted(uint64 indexed txId, bytes32 indexed approvalHash); | ||
|
||
error INVALID_PARAMS(); | ||
error INVALID_EXECUTOR(); | ||
error TX_NOT_APPROVED(); | ||
error TX_REVERTED(); | ||
error NOT_CALLABLE(); | ||
|
||
function executeApprovedTransaction( | ||
bytes calldata txdata, | ||
bytes calldata proof, | ||
address executor | ||
) | ||
external | ||
{ | ||
if (executor != address(0) && executor != msg.sender) { | ||
revert INVALID_EXECUTOR(); | ||
} | ||
|
||
bytes32 approvalHash = _isTransactionApproved(txdata, proof, executor); | ||
if (approvalHash == 0) revert TX_NOT_APPROVED(); | ||
|
||
(bool success,) = address(this).call(txdata); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If we let There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, but we don't override |
||
if (!success) revert TX_REVERTED(); | ||
emit TransactionExecuted(nextTxId++, approvalHash); | ||
} | ||
|
||
function isTransactionApproved( | ||
bytes calldata txdata, | ||
bytes calldata proof, | ||
address executor | ||
) | ||
public | ||
view | ||
returns (bool) | ||
{ | ||
return _isTransactionApproved(txdata, proof, executor) != 0; | ||
} | ||
|
||
/// @notice Initializes the contract. | ||
/// @param _addressManager The address of the address manager. | ||
/// @param _ownerChainId The owner's deployment chain ID. | ||
// solhint-disable-next-line func-name-mixedcase | ||
function __CrossChainOwned_init( | ||
address _addressManager, | ||
uint64 _ownerChainId | ||
) | ||
internal | ||
virtual | ||
{ | ||
__Essential_init(_addressManager); | ||
|
||
if (_ownerChainId == 0 || _ownerChainId == block.chainid) revert INVALID_PARAMS(); | ||
ownerChainId = _ownerChainId; | ||
nextTxId = 1; | ||
} | ||
|
||
function _isTransactionApproved( | ||
bytes calldata txdata, | ||
bytes calldata proof, | ||
address executor | ||
) | ||
internal | ||
view | ||
returns (bytes32 approvalHash) | ||
{ | ||
if (bytes4(txdata) == this.executeApprovedTransaction.selector) revert NOT_CALLABLE(); | ||
|
||
bytes32 hash = keccak256( | ||
abi.encode("APPROVE_CROSS_CHAIN_TX", block.chainid, nextTxId, executor, txdata) | ||
); | ||
|
||
if (_isSignalReceived(hash, proof)) return hash; | ||
else return 0; | ||
} | ||
|
||
function _isSignalReceived( | ||
bytes32 signal, | ||
bytes calldata proof | ||
) | ||
internal | ||
view | ||
virtual | ||
returns (bool) | ||
{ | ||
return ISignalService(resolve("signal_service", false)).proveSignalReceived({ | ||
srcChainId: ownerChainId, | ||
app: owner(), | ||
signal: signal, | ||
proof: proof | ||
}); | ||
} | ||
|
||
function _checkOwner() internal view virtual override { | ||
if (msg.sender != address(this)) revert NOT_CALLABLE(); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,103 @@ | ||
// SPDX-License-Identifier: MIT | ||
pragma solidity 0.8.20; | ||
|
||
import "../TaikoTest.sol"; | ||
|
||
contract CrossChainOwnedContract is CrossChainOwned { | ||
uint256 public counter; | ||
|
||
function increment() external virtual onlyOwner { | ||
counter += 1; | ||
} | ||
|
||
function init(address addressManager) external initializer { | ||
__CrossChainOwned_init(addressManager, 12_345); | ||
} | ||
|
||
function _isSignalReceived( | ||
bytes32, /*signal*/ | ||
bytes calldata /*proof*/ | ||
) | ||
internal | ||
pure | ||
override | ||
returns (bool) | ||
{ | ||
return true; | ||
} | ||
} | ||
|
||
contract CrossChainOwnedContract2 is CrossChainOwnedContract { | ||
function increment() external override onlyOwner { | ||
counter -= 1; | ||
} | ||
} | ||
|
||
contract TestCrossChainOwned is TaikoTest { | ||
CrossChainOwnedContract public xchainowned; | ||
|
||
function setUp() public { | ||
address addressManager = deployProxy({ | ||
name: "address_manager", | ||
impl: address(new AddressManager()), | ||
data: abi.encodeCall(AddressManager.init, ()) | ||
}); | ||
xchainowned = CrossChainOwnedContract( | ||
deployProxy({ | ||
name: "contract", | ||
impl: address(new CrossChainOwnedContract()), | ||
data: abi.encodeCall(CrossChainOwnedContract.init, (addressManager)) | ||
}) | ||
); | ||
} | ||
|
||
function test_xchainowned_ower_cannot_be_msgsender() public { | ||
vm.startPrank(xchainowned.owner()); | ||
vm.expectRevert(CrossChainOwned.NOT_CALLABLE.selector); | ||
xchainowned.increment(); | ||
vm.stopPrank(); | ||
} | ||
|
||
function test_xchainowned_exec_tx() public { | ||
bytes memory proof = ""; | ||
bytes memory data = abi.encodeCall(xchainowned.increment, ()); | ||
|
||
assertEq(xchainowned.counter(), 0); | ||
xchainowned.executeApprovedTransaction(data, proof, address(0)); | ||
xchainowned.executeApprovedTransaction(data, proof, address(0)); | ||
assertEq(xchainowned.counter(), 2); | ||
|
||
vm.expectRevert(CrossChainOwned.INVALID_EXECUTOR.selector); | ||
xchainowned.executeApprovedTransaction(data, proof, Alice); | ||
|
||
vm.prank(Alice); | ||
xchainowned.executeApprovedTransaction(data, proof, Alice); | ||
assertEq(xchainowned.counter(), 3); | ||
} | ||
|
||
function test_xchainowned_exec_executeApprovedTransaction_revert() public { | ||
bytes memory proof = ""; | ||
bytes memory data = | ||
abi.encodeCall(xchainowned.executeApprovedTransaction, ("", "", address(1))); | ||
vm.expectRevert(CrossChainOwned.NOT_CALLABLE.selector); | ||
xchainowned.executeApprovedTransaction(data, proof, address(0)); | ||
} | ||
|
||
function test_xchainowned_exec_upgrade() public { | ||
bytes memory proof = ""; | ||
|
||
bytes memory incrementCall = abi.encodeCall(xchainowned.increment, ()); | ||
bytes memory upgradetoCall = | ||
abi.encodeCall(xchainowned.upgradeTo, (address(new CrossChainOwnedContract2()))); | ||
|
||
assertEq(xchainowned.counter(), 0); | ||
xchainowned.executeApprovedTransaction(incrementCall, proof, address(0)); | ||
assertEq(xchainowned.counter(), 1); | ||
|
||
xchainowned.executeApprovedTransaction(upgradetoCall, proof, address(0)); | ||
assertEq(xchainowned.counter(), 1); | ||
|
||
xchainowned.executeApprovedTransaction(incrementCall, proof, address(0)); | ||
assertEq(xchainowned.counter(), 0); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why does this function check its own proof instead of doing things similarly to what
checkProcessMessageContext
where it's called directly by the bridge contract and just the original sender on the source chain needs to be checked?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This new contract doesn't use the Bridge, instead, it interacts with the SignalService directly on the source chain directly.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Any reason for that? Seems like this type of thing will be a common thing that people have to do for these cross layer transactions. It feels a bit strange that even we wouldn't use the standard bridge to do this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using the SignalService is intuitive to me, but we can also use the Bridge. By using the bridge, maybe we can avoid the introduction of the new CrossChainOwned contract. I'll give it a try and then lets compare the two solutions.