-
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): add solver support for l2 to l1 eth bridging #18805
base: pacaya_fork
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,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 { | ||
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. 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 |
||
using Address for address; | ||
using LibAddress for address; | ||
|
||
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. 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 { | ||
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. Is it possible to reuse the SolverOp defines in ERC20Vault (maybe extract to another file) and use Same recommendation for |
||
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) | ||
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. 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; | ||
} | ||
} |
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; | ||
} | ||
} |
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.
Add Natspac comments please