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(l2): prepare contracts for withdraw #944

Open
wants to merge 6 commits into
base: l2_withdrawal_handling
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
1 change: 1 addition & 0 deletions crates/l2/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ restart-local-l1: down-local-l1 init-local-l1 ## 🔄 Restarts the L1 Lambda Eth
contract-deps: ## 📦 Installs the dependencies for the L1 contracts
mkdir -p ${FOUNDRY_PROJECT_HOME}
forge install foundry-rs/forge-std --no-git --root ${FOUNDRY_PROJECT_HOME} || exit 0
forge install OpenZeppelin/openzeppelin-contracts --no-git --root ${FOUNDRY_PROJECT_HOME} || exit 0

clean-contract-deps: ## 🧹 Cleans the dependencies for the L1 contracts.
rm -rf contracts/lib
Expand Down
48 changes: 39 additions & 9 deletions crates/l2/contracts/script/DeployL1.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -12,27 +12,57 @@ contract DeployL1Script is Script {
/// in the genesis file. The same contract with the same address is deployed
/// in every testnet, so if this script is run in a testnet instead of in a
/// local environment, it should work.
address constant DETERMINISTIC_CREATE2_ADDRESS = 0x4e59b44847b379578588920cA78FbF26c0B4956C;
address constant DETERMINISTIC_CREATE2_ADDRESS =
0x4e59b44847b379578588920cA78FbF26c0B4956C;

function setUp() public {}

function run() public {
console.log("Deploying L1 contracts");

deployOnChainProposer();
deployCommonBridge(msg.sender);
bytes32 salt = bytes32(0);

address commonBridge = vm.computeCreate2Address(
salt,
keccak256(type(CommonBridge).creationCode),
DETERMINISTIC_CREATE2_ADDRESS
);
address onChainProposer = vm.computeCreate2Address(
salt,
keccak256(type(OnChainProposer).creationCode),
DETERMINISTIC_CREATE2_ADDRESS
);

deployOnChainProposer(commonBridge, salt);
deployCommonBridge(msg.sender, onChainProposer, salt);
}

function deployOnChainProposer() internal {
function deployOnChainProposer(
address commonBridge,
bytes32 salt
) internal {
bytes memory bytecode = type(OnChainProposer).creationCode;
bytes32 salt = bytes32(0);
address contractAddress = Utils.deployWithCreate2(bytecode, salt, DETERMINISTIC_CREATE2_ADDRESS, "");
address contractAddress = Utils.deployWithCreate2(
bytecode,
salt,
DETERMINISTIC_CREATE2_ADDRESS,
abi.encode(commonBridge)
);
console.log("OnChainProposer deployed at:", contractAddress);
}

function deployCommonBridge(address owner) internal {
function deployCommonBridge(
address owner,
address onChainProposer,
bytes32 salt
) internal {
bytes memory bytecode = type(CommonBridge).creationCode;
bytes32 salt = bytes32(0);
address contractAddress = Utils.deployWithCreate2(bytecode, salt, DETERMINISTIC_CREATE2_ADDRESS, abi.encode(owner));
address contractAddress = Utils.deployWithCreate2(
bytecode,
salt,
DETERMINISTIC_CREATE2_ADDRESS,
abi.encode(owner, onChainProposer)
);
console.log("CommonBridge deployed at:", contractAddress);
}
}
83 changes: 62 additions & 21 deletions crates/l2/contracts/src/l1/CommonBridge.sol
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,34 @@ import "./interfaces/ICommonBridge.sol";
/// @title CommonBridge contract.
/// @author LambdaClass
contract CommonBridge is ICommonBridge, Ownable, ReentrancyGuard {
constructor(address owner) Ownable(owner) {}
/// @notice Mapping of unclaimed withdrawals. A withdrawal is unclaimed if
/// there is a non-zero value in the mapping (a merkle root) for the hash
/// of the L2 transaction that requested the withdrawal.
/// @dev The key is the hash of the L2 transaction that requested the
/// withdrawal.
/// @dev The value is a boolean indicating if the withdrawal was claimed or not.
mapping(bytes32 => bool) public claimedWithdrawals;

struct WithdrawalData {
address to;
uint256 amount;
/// @notice Mapping of merkle roots to the L2 withdrawal transaction logs.
/// @dev The key is the L2 block number where the logs were emitted.
/// @dev The value is the merkle root of the logs.
/// @dev If there exist a merkle root for a given block number it means
/// that the logs were published on L1, and that that block was committed.
mapping(uint256 => bytes32) public blockWithdrawalsLogs;

address public immutable ON_CHAIN_PROPOSER;

modifier onlyOnChainProposer() {
require(
msg.sender == ON_CHAIN_PROPOSER,
"CommonBridge: caller is not the OnChainProposer"
);
_;
}

mapping(bytes32 l2TxHash => WithdrawalData) public pendingWithdrawals;
constructor(address owner, address onChainProposer) Ownable(owner) {
ON_CHAIN_PROPOSER = onChainProposer;
}

/// @inheritdoc ICommonBridge
function deposit(address to) public payable {
Expand All @@ -32,26 +52,47 @@ contract CommonBridge is ICommonBridge, Ownable, ReentrancyGuard {
}

/// @inheritdoc ICommonBridge
function startWithdrawal(
WithdrawalTransaction[] calldata transactions
) public onlyOwner {
for (uint256 i = 0; i < transactions.length; i++) {
pendingWithdrawals[transactions[i].l2TxHash] = WithdrawalData(
transactions[i].to,
transactions[i].amount
);
}
function publishWithdrawals(
uint256 withdrawalLogsBlockNumber,
bytes32 withdrawalsLogsMerkleRoot
) public onlyOnChainProposer {
require(
blockWithdrawalsLogs[withdrawalLogsBlockNumber] == bytes32(0),
"CommonBridge: withdrawal logs already published"
);
blockWithdrawalsLogs[
withdrawalLogsBlockNumber
] = withdrawalsLogsMerkleRoot;
emit WithdrawalsPublished(
withdrawalLogsBlockNumber,
withdrawalsLogsMerkleRoot
);
}

function finalizeWithdrawal(bytes32 l2TxHash) public nonReentrant {
/// @inheritdoc ICommonBridge
function claimWithdrawal(
bytes32 l2WithdrawalTxHash,
uint256 claimedAmount,
uint256 withdrawalBlockNumber,
bytes32[] calldata //withdrawalProof
) public nonReentrant {
require(
msg.sender == pendingWithdrawals[l2TxHash].to,
"CommonBridge: withdrawal not found"
blockWithdrawalsLogs[withdrawalBlockNumber] != bytes32(0),
"CommonBridge: the block that emitted the withdrawal logs was not committed"
);

delete pendingWithdrawals[l2TxHash];
payable(msg.sender).call{value: pendingWithdrawals[l2TxHash].amount}(
""
require(
claimedWithdrawals[l2WithdrawalTxHash] == false,
"CommonBridge: the withdrawal was already claimed"
);
// TODO: Verify the proof.
require(true, "CommonBridge: invalid withdrawal proof");

(bool success, ) = payable(msg.sender).call{value: claimedAmount}("");

require(success, "CommonBridge: failed to send the claimed amount");

claimedWithdrawals[l2WithdrawalTxHash] = true;

emit WithdrawalClaimed(l2WithdrawalTxHash, msg.sender, claimedAmount);
}
}
46 changes: 44 additions & 2 deletions crates/l2/contracts/src/l1/OnChainProposer.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,55 @@
pragma solidity 0.8.27;

import "./interfaces/IOnChainProposer.sol";
import {CommonBridge} from "./CommonBridge.sol";
import {ICommonBridge} from "./interfaces/ICommonBridge.sol";

/// @title OnChainProposer contract.
/// @author LambdaClass
contract OnChainProposer is IOnChainProposer {
/// @notice The commitments of the committed blocks.
/// @dev If a block is committed, the commitment is stored here.
/// @dev If a block was not committed yet, it won't be here.
/// @dev It is used by other contracts to verify if a block was committed.
mapping(uint256 => bytes32) public blockCommitments;

/// @notice The verified blocks.
/// @dev If a block is verified, the block hash is stored here.
/// @dev If a block was not verified yet, it won't be here.
/// @dev It is used by other contracts to verify if a block was verified.
mapping(uint256 => bool) public verifiedBlocks;

address public immutable BRIDGE;

constructor(address bridge) {
BRIDGE = bridge;
}

/// @inheritdoc IOnChainProposer
function commit(bytes32 currentBlockCommitment) external override {
emit BlockCommitted(currentBlockCommitment);
function commit(
uint256 blockNumber,
bytes32 newL2StateRoot,
bytes32 withdrawalsLogsMerkleRoot
) external override {
require(
!verifiedBlocks[blockNumber],
"OnChainProposer: block already verified"
);
require(
blockCommitments[blockNumber] == bytes32(0),
"OnChainProposer: block already committed"
);
bytes32 blockCommitment = keccak256(
abi.encode(blockNumber, newL2StateRoot, withdrawalsLogsMerkleRoot)
);
blockCommitments[blockNumber] = blockCommitment;
if (withdrawalsLogsMerkleRoot != bytes32(0)) {
ICommonBridge(BRIDGE).publishWithdrawals(
blockNumber,
withdrawalsLogsMerkleRoot
);
}
emit BlockCommitted(blockCommitment);
}

/// @inheritdoc IOnChainProposer
Expand Down
68 changes: 60 additions & 8 deletions crates/l2/contracts/src/l1/interfaces/ICommonBridge.sol
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,32 @@ interface ICommonBridge {
/// deposit in L2. Could be used to track the status of the deposit finalization
/// on L2. You can use this hash to retrive the tx data.
/// It is the result of keccak(abi.encode(transaction)).
event DepositInitiated(uint256 indexed amount, address indexed to, bytes32 indexed l2MintTxHash);
event DepositInitiated(
uint256 indexed amount,
address indexed to,
bytes32 indexed l2MintTxHash
);

/// @notice L2 withdrawals have been published on L1.
/// @dev Event emitted when the L2 withdrawals are published on L1.
/// @param withdrawalLogsBlockNumber the block number in L2 where the
/// withdrawal logs were emitted.
/// @param withdrawalsLogsMerkleRoot the merkle root of the withdrawal logs.
event WithdrawalsPublished(
uint256 indexed withdrawalLogsBlockNumber,
bytes32 indexed withdrawalsLogsMerkleRoot
);

/// @notice A withdrawal has been claimed.
/// @dev Event emitted when a withdrawal is claimed.
/// @param l2WithdrawalTxHash the hash of the L2 withdrawal transaction.
/// @param claimee the address that claimed the withdrawal.
/// @param claimedAmount the amount that was claimed.
event WithdrawalClaimed(
bytes32 indexed l2WithdrawalTxHash,
address indexed claimee,
uint256 indexed claimedAmount
);

/// @notice Error for when the deposit amount is 0.
error AmountToDepositIsZero();
Expand All @@ -32,12 +57,39 @@ interface ICommonBridge {
/// @param to, the address in L2 to which the tokens will be minted to.
function deposit(address to) external payable;

/// @notice Method that starts an L2 ETH withdrawal process.
/// @param transactions the withdrawal transactions including beneficiary, amount
/// and L2 transaction hash.
function startWithdrawal(WithdrawalTransaction[] calldata transactions) external;
/// @notice Publishes the L2 withdrawals on L1.
/// @dev This method is used by the L2 OnChainOperator to publish the L2
/// withdrawals when an L2 block is committed.
/// @param withdrawalLogsBlockNumber the block number in L2 where the
/// withdrawal logs were emitted.
/// @param withdrawalsLogsMerkleRoot the merkle root of the withdrawal logs.
function publishWithdrawals(
uint256 withdrawalLogsBlockNumber,
bytes32 withdrawalsLogsMerkleRoot
) external;

/// @notice Method that finalizes an L2 ETH withdrawal process.
/// @param l2TxHash the hash of the transaction in L2 that requests the withdrawal.
function finalizeWithdrawal(bytes32 l2TxHash) external;
/// @notice Method that claims an L2 withdrawal.
/// @dev For a user to claim a withdrawal, this method verifies:
/// - The l2WithdrawalBlockNumber was committed. If the given block was not
/// committed, this means that the withdrawal was not published on L1.
/// - The l2WithdrawalBlockNumber was verified. If the given block was not
/// verified, this means that the withdrawal claim was not enabled.
/// - The withdrawal was not claimed yet. This is to avoid double claims.
/// - The withdrawal proof is valid. This is, there exists a merkle path
/// from the withdrawal log to the withdrawal root, hence the claimed
/// withdrawal exists.
/// @dev We do not need to check that the claimee is the same as the
/// beneficiary of the withdrawal, because the withdrawal proof already
/// contains the beneficiary.
/// @param l2WithdrawalTxHash the hash of the L2 withdrawal transaction.
/// @param claimedAmount the amount that will be claimed.
/// @param withdrawalProof the merkle path to the withdrawal log.
/// @param l2WithdrawalBlockNumber the block number where the withdrawal log
/// was emitted.
function claimWithdrawal(
bytes32 l2WithdrawalTxHash,
uint256 claimedAmount,
uint256 l2WithdrawalBlockNumber,
bytes32[] calldata withdrawalProof
) external;
}
17 changes: 12 additions & 5 deletions crates/l2/contracts/src/l1/interfaces/IOnChainProposer.sol
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,18 @@ interface IOnChainProposer {
/// @dev Event emitted when a block is verified.
event BlockVerified(bytes32 indexed blockHash);

/// @notice Method used to commit an L2 block to be proved.
/// @dev This method is used by the operator when a block is ready to be
/// proved.
/// @param currentBlockCommitment is the committment to the block to be proved.
function commit(bytes32 currentBlockCommitment) external;
/// @notice Commits to an L2 block.
/// @dev Committing to an L2 block means to store the block's commitment
/// and to publish withdrawals if any.
/// @param blockNumber the number of the block to be committed.
/// @param newL2StateRoot the new L2 state root of the block to be committed.
/// @param withdrawalsLogsMerkleRoot the merkle root of the withdrawal logs
/// of the block to be committed.
function commit(
uint256 blockNumber,
bytes32 newL2StateRoot,
bytes32 withdrawalsLogsMerkleRoot
) external;

/// @notice Method used to verify an L2 block proof.
/// @dev This method is used by the operator when a block is ready to be
Expand Down
Loading