Skip to content

Commit

Permalink
feat(protocol): allow DelegateOwner to delegatecall for batching (#17022
Browse files Browse the repository at this point in the history
)

Co-authored-by: dantaik <[email protected]>
Co-authored-by: D <[email protected]>
  • Loading branch information
3 people authored May 9, 2024
1 parent dbf4413 commit 7e1374e
Show file tree
Hide file tree
Showing 7 changed files with 337 additions and 132 deletions.
58 changes: 39 additions & 19 deletions packages/protocol/contracts/L2/DelegateOwner.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ pragma solidity 0.8.24;

import "../common/EssentialContract.sol";
import "../common/LibStrings.sol";
import "../libs/LibBytes.sol";
import "../bridge/IBridge.sol";

/// @title DelegateOwner
Expand All @@ -22,20 +23,27 @@ contract DelegateOwner is EssentialContract, IMessageInvocable {

uint256[48] private __gap;

/// @notice Emitted when a transaction is executed.
struct Call {
uint64 txId;
address target;
bool isDelegateCall;
bytes txdata;
}

/// @notice Emitted when a message is invoked.
/// @param txId The transaction ID.
/// @param target The target address.
/// @param isDelegateCall True if the call is a `delegatecall`.
/// @param selector The function selector.
event TransactionExecuted(uint64 indexed txId, address indexed target, bytes4 indexed selector);

/// @notice Emitted when this contract accepted the ownership of a target contract.
/// @param target The target address.
event OwnershipAccepted(address indexed target);
event MessageInvoked(
uint64 indexed txId, address indexed target, bool isDelegateCall, bytes4 indexed selector
);

error DO_DRYRUN_SUCCEEDED();
error DO_INVALID_PARAM();
error DO_INVALID_TX_ID();
error DO_PERMISSION_DENIED();
error DO_TX_REVERTED();
error DO_TARGET_CALL_REVERTED();

/// @notice Initializes the contract.
/// @param _realOwner The real owner on L1 that can send a cross-chain message to invoke
Expand Down Expand Up @@ -69,28 +77,40 @@ contract DelegateOwner is EssentialContract, IMessageInvocable {
payable
onlyFromNamed(LibStrings.B_BRIDGE)
{
(uint64 txId, address target, bytes memory txdata) =
abi.decode(_data, (uint64, address, bytes));

if (txId != nextTxId) revert DO_INVALID_TX_ID();

IBridge.Context memory ctx = IBridge(msg.sender).context();
if (ctx.srcChainId != l1ChainId || ctx.from != realOwner) {
revert DO_PERMISSION_DENIED();
}
nextTxId++;
// Sending ether along with the function call. Although this is sending Ether from this
// contract back to itself, txData's function can now be payable.
(bool success,) = target.call{ value: msg.value }(txdata);
if (!success) revert DO_TX_REVERTED();
_invokeCall(_data, true);
}

emit TransactionExecuted(txId, target, bytes4(txdata));
/// @notice Dryruns a message invocation but always revert.
/// If this tx is reverted with DO_TRY_RUN_SUCCEEDED, the try run is successful.
/// Note that this function shall not be used in transaction and is designed for offchain
/// simulation only.
function dryrunMessageInvocation(bytes calldata _data) external payable {
_invokeCall(_data, false);
revert DO_DRYRUN_SUCCEEDED();
}

function acceptOwnership(address target) external {
Ownable2StepUpgradeable(target).acceptOwnership();
emit OwnershipAccepted(target);
}

function transferOwnership(address) public pure override notImplemented { }

function _authorizePause(address, bool) internal pure override notImplemented { }

function _invokeCall(bytes calldata _data, bool _verifyTxId) internal {
Call memory call = abi.decode(_data, (Call));

if (_verifyTxId && call.txId != nextTxId++) revert DO_INVALID_TX_ID();

(bool success, bytes memory result) = call.isDelegateCall //
? call.target.delegatecall(call.txdata)
: call.target.call{ value: msg.value }(call.txdata);

if (!success) LibBytes.revertWithExtractedError(result);
emit MessageInvoked(call.txId, call.target, call.isDelegateCall, bytes4(call.txdata));
}
}
4 changes: 4 additions & 0 deletions packages/protocol/contracts/common/EssentialContract.sol
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,10 @@ abstract contract EssentialContract is UUPSUpgradeable, Ownable2StepUpgradeable,
_authorizePause(msg.sender, false);
}

function impl() public view returns (address) {
return _getImplementation();
}

/// @notice Returns true if the contract is paused, and false otherwise.
/// @return true if paused, false otherwise.
function paused() public view returns (bool) {
Expand Down
19 changes: 19 additions & 0 deletions packages/protocol/contracts/libs/LibBytes.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
pragma solidity 0.8.24;

library LibBytes {
error INNER_ERROR(bytes innerError);

// Taken from:
// https://github.com/0xPolygonHermez/zkevm-contracts/blob/main/contracts/PolygonZkEVMBridge.sol#L835-L860
/// @notice Function to convert returned data to string
Expand Down Expand Up @@ -29,4 +31,21 @@ library LibBytes {
return "";
}
}

// Taken from:
// https://github.com/boringcrypto/BoringSolidity/blob/master/contracts/BoringBatchable.sol
/// @dev Helper function to extract a useful revert message from a failed call.
/// If the returned data is malformed or not correctly abi encoded then this call can fail
/// itself.
function revertWithExtractedError(bytes memory _returnData) internal pure {
// If the _res length is less than 68, then
// the transaction failed with custom error or silently (without a revert message)
if (_returnData.length < 68) revert INNER_ERROR(_returnData);

assembly {
// Slice the sighash.
_returnData := add(_returnData, 0x04)
}
revert(abi.decode(_returnData, (string))); // All that remains is the revert string
}
}
215 changes: 215 additions & 0 deletions packages/protocol/test/L2/DelegateOwner.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.24;

import "../common/TestMulticall3.sol";
import "../TaikoTest.sol";

contract Target is EssentialContract {
function init(address _owner) external initializer {
__Essential_init(_owner);
}
}

contract TestDelegateOwner is TaikoTest {
address public owner;
address public remoteOwner;
Bridge public bridge;
SignalService public signalService;
AddressManager public addressManager;
DelegateOwner public delegateOwner;
TestMulticall3 public multicall;

uint64 remoteChainId = uint64(block.chainid + 1);
address remoteBridge = vm.addr(0x2000);

function setUp() public {
owner = vm.addr(0x1000);
vm.deal(owner, 100 ether);

remoteOwner = vm.addr(0x2000);

vm.startPrank(owner);

multicall = new TestMulticall3();

addressManager = AddressManager(
deployProxy({
name: "address_manager",
impl: address(new AddressManager()),
data: abi.encodeCall(AddressManager.init, (address(0)))
})
);

delegateOwner = DelegateOwner(
deployProxy({
name: "delegate_owner",
impl: address(new DelegateOwner()),
data: abi.encodeCall(
DelegateOwner.init, (remoteOwner, address(addressManager), remoteChainId)
),
registerTo: address(addressManager)
})
);

signalService = SkipProofCheckSignal(
deployProxy({
name: "signal_service",
impl: address(new SkipProofCheckSignal()),
data: abi.encodeCall(SignalService.init, (address(0), address(addressManager))),
registerTo: address(addressManager)
})
);

bridge = Bridge(
payable(
deployProxy({
name: "bridge",
impl: address(new Bridge()),
data: abi.encodeCall(Bridge.init, (address(0), address(addressManager))),
registerTo: address(addressManager)
})
)
);

addressManager.setAddress(remoteChainId, "bridge", remoteBridge);
vm.stopPrank();
}

function test_delegate_owner_single_non_delegatecall() public {
Target target1 = Target(
deployProxy({
name: "target1",
impl: address(new Target()),
data: abi.encodeCall(Target.init, (address(delegateOwner)))
})
);

bytes memory data = abi.encode(
DelegateOwner.Call(
uint64(0),
address(target1),
false, // CALL
abi.encodeCall(EssentialContract.pause, ())
)
);

vm.expectRevert(DelegateOwner.DO_DRYRUN_SUCCEEDED.selector);
delegateOwner.dryrunMessageInvocation(data);

IBridge.Message memory message;
message.from = remoteOwner;
message.destChainId = uint64(block.chainid);
message.srcChainId = remoteChainId;
message.destOwner = Bob;
message.data = abi.encodeCall(DelegateOwner.onMessageInvocation, (data));
message.to = address(delegateOwner);

vm.prank(Bob);
bridge.processMessage(message, "");

bytes32 hash = bridge.hashMessage(message);
assertTrue(bridge.messageStatus(hash) == IBridge.Status.DONE);

assertEq(delegateOwner.nextTxId(), 1);
assertTrue(target1.paused());
}

function test_delegate_owner_single_non_delegatecall_self() public {
address delegateOwnerImpl2 = address(new DelegateOwner());

bytes memory data = abi.encode(
DelegateOwner.Call(
uint64(0),
address(delegateOwner),
false, // CALL
abi.encodeCall(UUPSUpgradeable.upgradeTo, (delegateOwnerImpl2))
)
);

vm.expectRevert(DelegateOwner.DO_DRYRUN_SUCCEEDED.selector);
delegateOwner.dryrunMessageInvocation(data);

IBridge.Message memory message;
message.from = remoteOwner;
message.destChainId = uint64(block.chainid);
message.srcChainId = remoteChainId;
message.destOwner = Bob;
message.data = abi.encodeCall(DelegateOwner.onMessageInvocation, (data));
message.to = address(delegateOwner);

vm.prank(Bob);
bridge.processMessage(message, "");

bytes32 hash = bridge.hashMessage(message);
assertTrue(bridge.messageStatus(hash) == IBridge.Status.DONE);

assertEq(delegateOwner.nextTxId(), 1);
assertEq(delegateOwner.impl(), delegateOwnerImpl2);
}

function test_delegate_owner_delegate_multicall() public {
address impl1 = address(new Target());
address impl2 = address(new Target());

address delegateOwnerImpl2 = address(new DelegateOwner());

Target target1 = Target(
deployProxy({
name: "target1",
impl: impl1,
data: abi.encodeCall(Target.init, (address(delegateOwner)))
})
);
Target target2 = Target(
deployProxy({
name: "target2",
impl: impl1,
data: abi.encodeCall(Target.init, (address(delegateOwner)))
})
);

TestMulticall3.Call3[] memory calls = new TestMulticall3.Call3[](3);
calls[0].target = address(target1);
calls[0].allowFailure = false;
calls[0].callData = abi.encodeCall(EssentialContract.pause, ());

calls[1].target = address(target2);
calls[1].allowFailure = false;
calls[1].callData = abi.encodeCall(UUPSUpgradeable.upgradeTo, (impl2));

calls[2].target = address(delegateOwner);
calls[2].allowFailure = false;
calls[2].callData = abi.encodeCall(UUPSUpgradeable.upgradeTo, (delegateOwnerImpl2));

bytes memory data = abi.encode(
DelegateOwner.Call(
uint64(0),
address(multicall),
true, // DELEGATECALL
abi.encodeCall(TestMulticall3.aggregate3, (calls))
)
);

vm.expectRevert(DelegateOwner.DO_DRYRUN_SUCCEEDED.selector);
delegateOwner.dryrunMessageInvocation(data);

IBridge.Message memory message;
message.from = remoteOwner;
message.destChainId = uint64(block.chainid);
message.srcChainId = remoteChainId;
message.destOwner = Bob;
message.data = abi.encodeCall(DelegateOwner.onMessageInvocation, (data));
message.to = address(delegateOwner);

vm.prank(Bob);
bridge.processMessage(message, "");

bytes32 hash = bridge.hashMessage(message);
assertTrue(bridge.messageStatus(hash) == IBridge.Status.DONE);

assertEq(delegateOwner.nextTxId(), 1);
assertTrue(target1.paused());
assertEq(target2.impl(), impl2);
assertEq(delegateOwner.impl(), delegateOwnerImpl2);
}
}
Loading

0 comments on commit 7e1374e

Please sign in to comment.