From a23066b46712ac109cbbe27c1c696dd529b331c9 Mon Sep 17 00:00:00 2001 From: ilitteri Date: Tue, 22 Oct 2024 17:26:01 -0300 Subject: [PATCH 1/6] Add openzeppelin-contracts dep installation --- crates/l2/Makefile | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/l2/Makefile b/crates/l2/Makefile index 72e88081f..c2c21864d 100644 --- a/crates/l2/Makefile +++ b/crates/l2/Makefile @@ -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 From 165d1e84fe58fc8803120454cf36f48800e872d0 Mon Sep 17 00:00:00 2001 From: ilitteri Date: Tue, 22 Oct 2024 18:28:02 -0300 Subject: [PATCH 2/6] Implement withdrawals in `CommonBridge` - Added publishWithdrawals method - Added claimWithdrawal method - Added claimedWithdrawals mapping - Added blockWithdrawalsLogs --- crates/l2/contracts/src/l1/CommonBridge.sol | 83 +++++++++++++++------ 1 file changed, 62 insertions(+), 21 deletions(-) diff --git a/crates/l2/contracts/src/l1/CommonBridge.sol b/crates/l2/contracts/src/l1/CommonBridge.sol index 1b338da3b..b827a1ac3 100644 --- a/crates/l2/contracts/src/l1/CommonBridge.sol +++ b/crates/l2/contracts/src/l1/CommonBridge.sol @@ -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 { @@ -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); } } From 57c3aca9af22e85afd05ce716e7fa0fd1c049626 Mon Sep 17 00:00:00 2001 From: ilitteri Date: Tue, 22 Oct 2024 18:28:46 -0300 Subject: [PATCH 3/6] Update `ICommonBridge` - Added publishWithdrawals method - Added claimWithdrawal method - Added WithdrawalsPublished event - Added WithdrawalClaimed event --- .../src/l1/interfaces/ICommonBridge.sol | 68 ++++++++++++++++--- 1 file changed, 60 insertions(+), 8 deletions(-) diff --git a/crates/l2/contracts/src/l1/interfaces/ICommonBridge.sol b/crates/l2/contracts/src/l1/interfaces/ICommonBridge.sol index 1fd73e025..237013e91 100644 --- a/crates/l2/contracts/src/l1/interfaces/ICommonBridge.sol +++ b/crates/l2/contracts/src/l1/interfaces/ICommonBridge.sol @@ -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(); @@ -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; } From 190122089b03aa8b74e5548130d81416f716935d Mon Sep 17 00:00:00 2001 From: ilitteri Date: Tue, 22 Oct 2024 18:29:06 -0300 Subject: [PATCH 4/6] Implement `commit` method in `OnChainProposer` --- .../l2/contracts/src/l1/OnChainProposer.sol | 46 ++++++++++++++++++- 1 file changed, 44 insertions(+), 2 deletions(-) diff --git a/crates/l2/contracts/src/l1/OnChainProposer.sol b/crates/l2/contracts/src/l1/OnChainProposer.sol index 8c41f3ced..04c555350 100644 --- a/crates/l2/contracts/src/l1/OnChainProposer.sol +++ b/crates/l2/contracts/src/l1/OnChainProposer.sol @@ -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 From 58010b9153837dc96a7cbc1173de9f1ee47862bb Mon Sep 17 00:00:00 2001 From: ilitteri Date: Tue, 22 Oct 2024 18:29:26 -0300 Subject: [PATCH 5/6] Update `IOnChainProposer` with new `commit` def --- .../src/l1/interfaces/IOnChainProposer.sol | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/crates/l2/contracts/src/l1/interfaces/IOnChainProposer.sol b/crates/l2/contracts/src/l1/interfaces/IOnChainProposer.sol index 7088f69af..417bbd049 100644 --- a/crates/l2/contracts/src/l1/interfaces/IOnChainProposer.sol +++ b/crates/l2/contracts/src/l1/interfaces/IOnChainProposer.sol @@ -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 From 73d2b8f609495863db18403804333595deec1f18 Mon Sep 17 00:00:00 2001 From: ilitteri Date: Tue, 22 Oct 2024 18:29:32 -0300 Subject: [PATCH 6/6] Update deploy script --- crates/l2/contracts/script/DeployL1.s.sol | 48 ++++++++++++++++++----- 1 file changed, 39 insertions(+), 9 deletions(-) diff --git a/crates/l2/contracts/script/DeployL1.s.sol b/crates/l2/contracts/script/DeployL1.s.sol index 4f6d4b0e9..ad2c2f0b7 100644 --- a/crates/l2/contracts/script/DeployL1.s.sol +++ b/crates/l2/contracts/script/DeployL1.s.sol @@ -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); } }