Skip to content
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): add solver support for l2 to l1 eth bridging #18805

Open
wants to merge 3 commits into
base: pacaya_fork
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
235 changes: 235 additions & 0 deletions packages/protocol/contracts/shared/bridge/EtherBridgeWrapper.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,235 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

import "./Bridge.sol";
import "../../shared/based/ITaiko.sol";
import "src/layer1/based/ITaikoInbox.sol";
import "src/shared/libs/LibAddress.sol";
import "src/shared/common/EssentialContract.sol";
import "@openzeppelin/contracts/utils/Address.sol";

contract EtherBridgeWrapper is EssentialContract {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add Natspac comments please

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about we call it EtherVault? If this contract can be merged into ERC20Vault and we consider sending Ether is just another special instance of sending ERC20 tokens with a special token address 0x0, then I'd strongly prefer that implementation.

using Address for address;
using LibAddress for address;

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lets reserve a gap of 50 slots here, just in case.

/// @dev Represents an operation to send Ether to another chain.
struct EtherBridgeOp {
// Destination chain ID.
uint64 destChainId;
// The owner of the bridge message on the destination chain.
address destOwner;
// Recipient address.
address to;
// Processing fee for the relayer.
uint64 fee;
// Gas limit for the operation.
uint32 gasLimit;
// Amount of Ether to be sent.
uint256 amount;
// Added solver fee
uint256 solverFee;
}

/// @dev Represents an operation to solve an Ether bridging intent
struct SolverOp {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it possible to reuse the SolverOp defines in ERC20Vault (maybe extract to another file) and use 0x0 for EtherBridgeOp to indicate the asset is Ether.

Same recommendation for EtherBridgeOp.

uint256 nonce;
address to;
uint256 amount;
// Fields for L2 batch verification
uint64 l2BatchId;
bytes32 l2BatchMetaHash;
}

/// @notice Emitted when Ether is sent to another chain.
event EtherSent(
bytes32 indexed msgHash,
address indexed from,
address indexed to,
uint256 amount,
uint256 solverFee
);

/// @notice Emitted when Ether is received from another chain.
event EtherReceived(
bytes32 indexed msgHash,
address indexed from,
address indexed to,
address solver,
uint64 srcChainId,
uint256 amount,
uint256 solverFee
);

/// @notice Emitted when a bridging intent is solved
event EtherSolved(bytes32 indexed solverCondition, address solver);

error InvalidAmount();
error InsufficientValue();
error EtherBridgePermissionDenied();
error EtherBridgeInvalidToAddr();
error VaultNotOnL1();
error VaultMetahashMismatch();
error VaultAlreadySolved();

/// @notice Mapping from solver condition to the address of solver
mapping(bytes32 solverCondition => address solver) public solverConditionToSolver;

/// @notice Initializes the contract.
/// @param _owner The owner of this contract. msg.sender will be used if this value is zero.
/// @param _sharedResolver The {IResolver} used by multipel rollups.
function init(address _owner, address _sharedResolver) external initializer {
__Essential_init(_owner, _sharedResolver);
}

/// @notice Sends Ether to another chain.
/// @param _op Options for sending Ether.
/// @return message_ The constructed message.
function sendToken(EtherBridgeOp calldata _op)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sendEther?

external
payable
whenNotPaused
nonReentrant
returns (IBridge.Message memory message_)
{
if (_op.amount == 0) revert InvalidAmount();
if (msg.value < _op.amount + _op.fee + _op.solverFee) revert InsufficientValue();

address bridge = resolve(LibStrings.B_BRIDGE, false);

// Generate solver condition if solver fee is specified
bytes32 solverCondition;
if (_op.solverFee > 0) {
uint256 _nonce = IBridge(bridge).nextMessageId();
solverCondition = getSolverCondition(_nonce, _op.to, _op.amount);
}

bytes memory data = abi.encodeCall(
this.onMessageInvocation,
abi.encode(msg.sender, _op.to, _op.amount, _op.solverFee, solverCondition)
);

IBridge.Message memory message = IBridge.Message({
id: 0, // will receive a new value
from: address(0), // will receive a new value
srcChainId: 0, // will receive a new value
destChainId: _op.destChainId,
srcOwner: msg.sender,
destOwner: _op.destOwner != address(0) ? _op.destOwner : msg.sender,
to: resolve(_op.destChainId, name(), false),
value: _op.amount + _op.solverFee,
fee: _op.fee,
gasLimit: _op.gasLimit,
data: data
});

bytes32 msgHash;
(msgHash, message_) = IBridge(bridge).sendMessage{ value: msg.value }(message);

emit EtherSent({
msgHash: msgHash,
from: message_.srcOwner,
to: _op.to,
amount: _op.amount,
solverFee: _op.solverFee
});
}

/// @notice Handles incoming Ether bridge messages.
/// @param _data The encoded message data.
function onMessageInvocation(bytes calldata _data)
external
payable
whenNotPaused
nonReentrant
{
// `onlyFromBridge` checked in checkProcessMessageContext
IBridge.Context memory ctx = checkProcessMessageContext();

(address from, address to, uint256 amount, uint256 solverFee, bytes32 solverCondition) =
abi.decode(_data, (address, address, uint256, uint256, bytes32));

// Don't allow sending to disallowed addresses
checkToAddress(to);

address recipient = to;

// If the bridging intent has been solved, the solver becomes the recipient
address solver = solverConditionToSolver[solverCondition];
if (solver != address(0)) {
recipient = solver;
delete solverConditionToSolver[solverCondition];
}

// Transfer Ether to recipient
recipient.sendEtherAndVerify(amount + solverFee);

emit EtherReceived({
msgHash: ctx.msgHash,
from: from,
to: to,
solver: solver,
srcChainId: ctx.srcChainId,
amount: amount,
solverFee: solverFee
});
}

/// @notice Lets a solver fulfil a bridging intent by transferring Ether to the recipient.
/// @param _op Parameters for the solve operation
function solve(SolverOp memory _op) external payable nonReentrant whenNotPaused {
if (_op.l2BatchMetaHash != 0) {
// Verify that the required L2 batch containing the intent transaction has been proposed
address taiko = resolve(LibStrings.B_TAIKO, false);
if (!ITaiko(taiko).isOnL1()) revert VaultNotOnL1();

bytes32 l2BatchMetaHash = ITaikoInbox(taiko).getBatch(_op.l2BatchId).metaHash;
if (l2BatchMetaHash != _op.l2BatchMetaHash) revert VaultMetahashMismatch();
}

// Record the solver's address
bytes32 solverCondition = getSolverCondition(_op.nonce, _op.to, _op.amount);
if (solverConditionToSolver[solverCondition] != address(0)) revert VaultAlreadySolved();
solverConditionToSolver[solverCondition] = msg.sender;

// Transfer the Ether to the recipient
_op.to.sendEtherAndVerify(_op.amount);

emit EtherSolved(solverCondition, msg.sender);
}

/// @notice Returns the solver condition for a bridging intent
/// @param _nonce Unique numeric value to prevent nonce collision
/// @param _to Recipient on destination chain
/// @param _amount Amount of Ether expected by the recipient
/// @return solver condition
function getSolverCondition(
uint256 _nonce,
address _to,
uint256 _amount
)
public
pure
returns (bytes32)
{
return keccak256(abi.encodePacked(_nonce, _to, _amount));
}

function checkProcessMessageContext()
internal
view
onlyFromNamed(LibStrings.B_BRIDGE)
returns (IBridge.Context memory ctx_)
{
ctx_ = IBridge(msg.sender).context();
address selfOnSourceChain = resolve(ctx_.srcChainId, name(), false);
if (ctx_.from != selfOnSourceChain) revert EtherBridgePermissionDenied();
}

function checkToAddress(address _to) internal view {
if (_to == address(0) || _to == address(this)) revert EtherBridgeInvalidToAddr();
}

function name() public pure returns (bytes32) {
return LibStrings.B_ETHER_BRIDGE_WRAPPER;
}
}
1 change: 1 addition & 0 deletions packages/protocol/contracts/shared/libs/LibStrings.sol
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ library LibStrings {
bytes32 internal constant B_BRIDGE_WATCHDOG = bytes32("bridge_watchdog");
bytes32 internal constant B_BRIDGED_ERC1155 = bytes32("bridged_erc1155");
bytes32 internal constant B_BRIDGED_ERC20 = bytes32("bridged_erc20");
bytes32 internal constant B_ETHER_BRIDGE_WRAPPER = bytes32("ether_bridge_wrapper");
bytes32 internal constant B_BRIDGED_ERC721 = bytes32("bridged_erc721");
bytes32 internal constant B_CHAIN_WATCHDOG = bytes32("chain_watchdog");
bytes32 internal constant B_ERC1155_VAULT = bytes32("erc1155_vault");
Expand Down
11 changes: 11 additions & 0 deletions packages/protocol/test/shared/CommonTest.sol
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import "src/shared/tokenvault/ERC20Vault.sol";
import "src/shared/tokenvault/ERC721Vault.sol";
import "src/shared/tokenvault/ERC1155Vault.sol";
import "src/shared/bridge/Bridge.sol";
import "src/shared/bridge/EtherBridgeWrapper.sol";
import "src/shared/bridge/QuotaManager.sol";
import "src/layer1/token/TaikoToken.sol";
import "test/shared/helpers/SignalService_WithoutProofVerification.sol";
Expand Down Expand Up @@ -208,6 +209,16 @@ abstract contract CommonTest is Test, Script {
);
}

function deployEtherBridgeWrapper() internal returns (EtherBridgeWrapper) {
return EtherBridgeWrapper(
deploy({
name: "ether_bridge_wrapper",
impl: address(new EtherBridgeWrapper()),
data: abi.encodeCall(EtherBridgeWrapper.init, (address(0), address(resolver)))
})
);
}

function deployQuotaManager() internal returns (QuotaManager) {
return QuotaManager(
deploy({
Expand Down
67 changes: 67 additions & 0 deletions packages/protocol/test/shared/bridge/EtherBridgeWrapper.h.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

import "../CommonTest.sol";
import "src/shared/bridge/EtherBridgeWrapper.sol";
import "src/layer1/based/ITaikoInbox.sol";

contract PrankTaikoInbox {
ITaikoInbox.Batch internal batch;

function setBatch(ITaikoInbox.Batch memory _batch) external {
batch = _batch;
}

function getBatch(uint64) external view returns (ITaikoInbox.Batch memory) {
return batch;
}

function isOnL1() external pure returns (bool) {
return true;
}
}

contract PrankDestBridge {
EtherBridgeWrapper destWrapper;
TContext ctx;

struct TContext {
bytes32 msgHash;
address sender;
uint64 srcChainId;
}

constructor(EtherBridgeWrapper _wrapper) {
destWrapper = _wrapper;
}

function context() public view returns (TContext memory) {
return ctx;
}

function sendReceiveEtherToWrapper(
address from,
address to,
uint256 amount,
uint256 solverFee,
bytes32 solverCondition,
bytes32 msgHash,
address srcWrapper,
uint64 srcChainId,
uint256 mockLibInvokeMsgValue
)
public
{
ctx.sender = srcWrapper;
ctx.msgHash = msgHash;
ctx.srcChainId = srcChainId;

destWrapper.onMessageInvocation{ value: mockLibInvokeMsgValue }(
abi.encode(from, to, amount, solverFee, solverCondition)
);

ctx.sender = address(0);
ctx.msgHash = bytes32(0);
ctx.srcChainId = 0;
}
}
Loading
Loading