-
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): introduce ForkManager to improve protocol fork management #18508
base: main
Are you sure you want to change the base?
Changes from all commits
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,71 @@ | ||
// SPDX-License-Identifier: MIT | ||
pragma solidity ^0.8.24; | ||
|
||
import "@openzeppelin/contracts/proxy/utils/UUPSUpgradeable.sol"; | ||
import "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol"; | ||
|
||
/// @title ForkManager | ||
/// @custom:security-contact [email protected] | ||
/// @notice This contract serves as a base contract for managing up to two forks within the Taiko | ||
/// protocol. By default, all function calls are routed to the newFork address. | ||
/// Sub-contracts should override the shouldRouteToOldFork function to route specific function calls | ||
/// to the old fork address. | ||
/// These sub-contracts should be placed between a proxy and the actual fork implementations. When | ||
/// calling upgradeTo, the proxy should always upgrade to a new ForkManager implementation, not an | ||
/// actual fork implementation. | ||
/// It is strongly advised to name functions differently for the same functionality across the two | ||
/// forks, as it is not possible to route the same function to two different forks. | ||
/// | ||
/// +--> newFork | ||
/// PROXY -> FORK_MANAGER --| | ||
/// +--> oldFork | ||
|
||
contract ForkManager is UUPSUpgradeable, Ownable2StepUpgradeable { | ||
Comment on lines
+20
to
+23
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. An instance of this |
||
address public immutable oldFork; | ||
address public immutable newFork; | ||
|
||
error ForkAddressIsZero(); | ||
error InvalidParams(); | ||
|
||
constructor(address _oldFork, address _currFork) { | ||
require(_currFork != address(0) && _currFork != _oldFork, InvalidParams()); | ||
oldFork = _oldFork; | ||
newFork = _currFork; | ||
} | ||
|
||
fallback() external payable virtual { | ||
_fallback(); | ||
} | ||
|
||
receive() external payable virtual { | ||
_fallback(); | ||
} | ||
|
||
function isForkManager() public pure returns (bool) { | ||
return true; | ||
} | ||
|
||
function _fallback() internal virtual { | ||
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. Should not we restrict reentrancy ? 🤔 Not sure tho, cause propose/prove already protected. |
||
address fork = shouldRouteToOldFork(msg.sig) ? oldFork : newFork; | ||
require(fork != address(0), ForkAddressIsZero()); | ||
|
||
assembly { | ||
calldatacopy(0, 0, calldatasize()) | ||
let result := delegatecall(gas(), fork, 0, calldatasize(), 0, 0) | ||
returndatacopy(0, 0, returndatasize()) | ||
|
||
switch result | ||
case 0 { revert(0, returndatasize()) } | ||
default { return(0, returndatasize()) } | ||
} | ||
} | ||
|
||
function _authorizeUpgrade(address) internal virtual override onlyOwner { } | ||
|
||
/// @notice Determines if the call should be routed to the old fork. | ||
/// @dev This function is intended to be overridden in derived contracts to provide custom | ||
/// routing logic. | ||
/// @param _selector The function selector of the call. | ||
/// @return A boolean value indicating whether the call should be routed to the old fork. | ||
function shouldRouteToOldFork(bytes4 _selector) internal pure virtual returns (bool) { } | ||
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 a function has the same selector in oldFork and newFork, how can we clarify? |
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
// SPDX-License-Identifier: MIT | ||
pragma solidity ^0.8.24; | ||
|
||
import "../TaikoL1Test.sol"; | ||
import "src/layer1/fork/ForkManager.sol"; | ||
|
||
contract Fork is EssentialContract { | ||
bytes32 private immutable __name; | ||
|
||
constructor(bytes32 _name) { | ||
__name = _name; | ||
} | ||
|
||
function init() external initializer { | ||
__Essential_init(msg.sender); | ||
} | ||
|
||
function name() public view returns (bytes32) { | ||
return __name; | ||
} | ||
} | ||
|
||
contract ForkManager_RouteToOldFork is ForkManager { | ||
constructor(address _fork1, address _fork2) ForkManager(_fork1, _fork2) { } | ||
|
||
function shouldRouteToOldFork(bytes4 _selector) internal pure override returns (bool) { | ||
return _selector == Fork.name.selector; | ||
} | ||
} | ||
|
||
contract TestForkManager is TaikoL1Test { | ||
address fork1 = address(new Fork("fork1")); | ||
address fork2 = address(new Fork("fork2")); | ||
|
||
function test_ForkManager_default_routing() public { | ||
address proxy = deployProxy({ | ||
name: "main_proxy", | ||
impl: address(new ForkManager(address(0), fork1)), | ||
data: abi.encodeCall(Fork.init, ()) | ||
}); | ||
|
||
assertTrue(ForkManager(payable(proxy)).isForkManager()); | ||
assertEq(Fork(proxy).name(), "fork1"); | ||
|
||
// If we upgrade the proxy's impl to a fork, then alling isForkManager will throw, | ||
// so we should never do this in production. | ||
Fork(proxy).upgradeTo(fork2); | ||
vm.expectRevert(); | ||
ForkManager(payable(proxy)).isForkManager(); | ||
|
||
Fork(proxy).upgradeTo(address(new ForkManager(fork1, fork2))); | ||
assertEq(Fork(proxy).name(), "fork2"); | ||
} | ||
|
||
function test_ForkManager_routing_to_old_fork() public { | ||
address proxy = deployProxy({ | ||
name: "main_proxy", | ||
impl: address(new ForkManager_RouteToOldFork(fork1, fork2)), | ||
data: abi.encodeCall(Fork.init, ()) | ||
}); | ||
|
||
assertTrue(ForkManager(payable(proxy)).isForkManager()); | ||
assertEq(Fork(proxy).name(), "fork1"); | ||
|
||
Fork(proxy).upgradeTo(address(new ForkManager(fork1, fork2))); | ||
assertTrue(ForkManager(payable(proxy)).isForkManager()); | ||
assertEq(Fork(proxy).name(), "fork2"); | ||
} | ||
} |
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.
Do we need an
init
method here for proxy deployment?