From 0743348d3043faca3f63963b6b888c14d6aad5ef Mon Sep 17 00:00:00 2001 From: invocamanman Date: Sat, 30 Oct 2021 00:43:57 +0200 Subject: [PATCH] first PoE happy flow --- README.md | 166 ++++++- contracts/BridgeMock.sol | 56 +++ contracts/ProofOfEfficiency.sol | 141 ++---- contracts/interfaces/BridgeInterface.sol | 9 + contracts/mockToken/VerifierRollupHelper.sol | 16 + hardhat.config.js | 4 - test/helpers/erc2612.js | 49 -- test/proofOfEfficiency.js | 447 ++++++------------- 8 files changed, 407 insertions(+), 481 deletions(-) create mode 100644 contracts/BridgeMock.sol create mode 100644 contracts/interfaces/BridgeInterface.sol create mode 100644 contracts/mockToken/VerifierRollupHelper.sol delete mode 100644 test/helpers/erc2612.js diff --git a/README.md b/README.md index fe2b23e16..39c4816cf 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,169 @@ -# Inital implementation of PoE +# PoE (& Rollup) +## Glossary +- PoE --> Proof-of-Efficiency model, consensus mechanism +- bridge L1 --> contract on ethereum mainnet that handles asset transfers between rollups +- bridge L2 --> channel to communicate with mainnet deployed on the rollup which controls minting/burning assets +## Specification +This protocol separates batch creation into two steps. Therefore, we find two parts: + +- **Sequencers**: collect L2 transactions from users. They select and create L2 batch in the network (sending ethereum transaction (encoded as RLP) with data of all selected L2 txs) + - collect L2 txs and propose batches + - pay MATIC to SC to propose a new batch (proportional to the number of transactions) + - only propose transactions (no propose states) + - 1 sequencer / 1 chainID + +> The sequencer needs to register to get a chainID (Users needs to chose a chainID to sign. That chainID could be sequencer specific or global. Meaning global that any sequencer could send that transactions and it will be valid) + +- **Aggregators**: create validity proof of a new state of the L2 (for one or multiple batches) + - they have specialized hardware to create ZKP + - the first aggregator that sends a valid proof wins the race and get rewarded (MATIC sent by the sequencer) + - the zkp must contain the ethereum address of the agregator + - invalid transactions are skipped like actual L1 txs + - The transactions will be processed, but if it is not valid it will have no effect on the state + +Then, the two steps are done one for each part: + +- `sendBatch`: the **sequencer** sends a group of L2 transactions +- `validateBatch`: the **aggregator** validates de batch + +![](https://i.imgur.com/dzDt6Zd.png) + +There are two state types: + +- Virtual state: state calculated from all pending transactions +- Confirmed state: state confirmed by zpk + +## General Flow + +### L1 transactions (deposit to RollupX) + +![](https://i.imgur.com/bY89lMN.png) + +3 Smart contracts: + +- Bridge SC (L1) +- Bridge SC (L2) +- PoE SC / Rollup (L1) + +The flow when a deposit to RollupA is made, is the following: + +- Deposit to RollupA (Bridge L1) is made +- The globalExitTree is updated (Bridge L1) : a leaf is created with de deposit balance +- When a batch is forged in RollupA, the stateRoot and exitRoot are updated +- Then, newExitRoot is sent to Bridge L1 and updated globalExitTree is received (RollupA) +- When a batch is forged, in Bridge L2 contracts it is also updated de globalExitTree. +- When the next batch has been forged, the deposit will be available for the user to claim. +- The user claims deposit and then, Bridge L2 contract must be updated (deposit nullifier) + +## Smart contract(s) + +### Actions + +- registerSequencer +- sendBatch +- validateBatch + +#### registerSequencer + +- staking --> maybe in the future (MATIC) +- mapping[ethAddr => struct] --> struct = { URL, chainID } + +``` +registerSequencer(address, URL) { + mappingSeq[address] = { URL, chainID } +} +``` + +#### sendBatch + +- params: calldata txs bytes (RLP encoded) --> `[]bytes` + `from (ethAddr), to (ethAddr), amount, fee, signature` +- State updates --> `mapping[numBatch] => struct = { H(txs), chainId/ethAddr}` + - input in the verify proof +- `sequencerCollateral` in MATIC + - pay MATIC aggregator + +``` +sendBatch([]bytes l2TxsData){ + l2TxsDataHash = H(l2TxsData) + mapping[lastConfirmedBatch + 1] = { H(l2TxsDataHash), mappingSeq[msg.sender].chainID } + // sequencerCollateral +} +``` + +> invalid L2 tx are selected as NOP + +#### validateBatch + +- params: `newLocalExitRoot`, `newStateRoot`, `batchNum` (sanity check), `proofA`, `proofB`, `proofC` +- input: + - `globalExitRoot`: global exit root + - `oldLocalExitRoot`: old local (rollup) exit root + - `newLocalExitRoot`: new + - `oldStateRoot` + - `newStateRoot` + - `hash(l2TxsData)` + - `chainID`: chainID + sequencerID + - `sequencerAddress` + +``` +**Buffer bytes notation** +[ 256 bits ] globalExitRoot +[ 256 bits ] oldLocalExitRoot +[ 256 bits ] newLocalExitRoot +[ 256 bits ] oldStateRoot +[ 256 bits ] newStateRoot +[ 256 bits ] hash(l2TxsData) +[ 16 bits ] chainID (sequencerID) +[ 160 bits ] sequencerAddress +``` + +- verify proof +- update roots +- Communicate with bridge + - push newLocalExitRoot + - get globalExitRoot + +``` +validateBatch(newLocalExitRoot, newStateRoot, batchNum, proofA, proofB, proofC) { + require(batchNum == lastConfirmedBatch + 1) + require(verifyProof) + lastConfirmedBatch++ + stateRootMap[lastConfirmedBatch] = newStateRoot + exitRootMap[lastConfirmedBatch] = newLocalExitRoot + bridge.submitLocalExitRoot(newLocalExitRoot) + lastGlobalExitRoot = bridge.globalExitRoot() +} +``` + +### Considerations / Simplifications + +- sendBatch: + - pay MATIC aggregator --> 1MATIC/tx (SIMPLIFIED) +- 1.3 bridgeL1: + - no bridge contract (it only returns true) + - genesisBlock + +### State + +- globalExitRoot +- localExitRoot +- stateRoot +- lastConfirmedBatch +- mapping[numBatch] = H(l2txs) +- mapping[ethAddr seq] = URL, chainID + +### Sequencer collateral + +- Adaptative algoritm to calculate the sequencer collateral + - Depends on the congestion of the network + - More tx/block, more collateral is needed for tx + - Recalculated every batch is aggregated + +## Questions after the first draft + +When calculating the MATIC fee, how can the contract know hwo many transactions are RPL encoded without decoding them?, could we code at the start some bytes indicating how many transactions will be? diff --git a/contracts/BridgeMock.sol b/contracts/BridgeMock.sol new file mode 100644 index 000000000..b2a9b7b42 --- /dev/null +++ b/contracts/BridgeMock.sol @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: AGPL-3.0 + +pragma solidity 0.8.9; + +import "@openzeppelin/contracts/access/Ownable.sol"; + +/** + * This is totally a mock contract, there's jsut enough to test the proof of efficiency contract + */ +contract BridgeMock is Ownable { + // Global exit root, this will be updated every time a batch is verified + bytes32 public currentGlobalExitRoot; + + // Rollup exit root, this will be updated every time a batch is verified + bytes32 public rollupExitRoot; + + // mainnet exit root, updated every deposit + bytes32 public mainnetExitRoot; + + // Rollup contract address + address public rollupAddress; + + /** + * @param _rollupAddress Rollup contract address + */ + constructor(address _rollupAddress) { + rollupAddress = _rollupAddress; + _updateGlobalExitRoot(); + } + + // register function? maybe governance should add exit trees? + //function register() public onlyOwner { + // + // } + function deposit() public { + //check deposit eth2.0 + // this will be just a mock function + mainnetExitRoot = bytes32(uint256(mainnetExitRoot) + 1); + _updateGlobalExitRoot(); + } + + function updateRollupExitRoot(bytes32 newRollupExitRoot) public { + require( + msg.sender == rollupAddress, + "BridgeMock::updateRollupExitRoot: ONLY_ROLLUP" + ); + rollupExitRoot = newRollupExitRoot; + _updateGlobalExitRoot(); + } + + function _updateGlobalExitRoot() internal { + currentGlobalExitRoot = keccak256( + abi.encode(mainnetExitRoot, rollupExitRoot) + ); + } +} diff --git a/contracts/ProofOfEfficiency.sol b/contracts/ProofOfEfficiency.sol index f7b36b542..2dab44847 100644 --- a/contracts/ProofOfEfficiency.sol +++ b/contracts/ProofOfEfficiency.sol @@ -1,11 +1,11 @@ // SPDX-License-Identifier: AGPL-3.0 - pragma solidity 0.8.9; import "@openzeppelin/contracts/access/Ownable.sol"; import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Burnable.sol"; import "./interfaces/VerifierRollupInterface.sol"; +import "./interfaces/BridgeInterface.sol"; /** * Contract responsible for managing the state and the updates of it of the L2 Hermez network. @@ -53,16 +53,16 @@ contract ProofOfEfficiency is Ownable { uint256 public lastVerifiedBatch; // Bridge address - address public bridgeAddress; + BridgeInterface public bridge; // Current state root - uint256 public currentStateRoot; // TODO should be a map stateRootMap[lastForgedBatch]??? + bytes32 public currentStateRoot; // TODO should be a map stateRootMap[lastForgedBatch]??? // Current local exit root - uint256 public currentLocalExitRoot; // TODO should be a map stateRootMap[lastForgedBatch]??? + bytes32 public currentLocalExitRoot; // TODO should be a map stateRootMap[lastForgedBatch]??? // Last fetched global exit root, this will be updated every time a batch is verified - uint256 public lastGlobalExitRoot; + bytes32 public lastGlobalExitRoot; VerifierRollupInterface public rollupVerifier; @@ -77,25 +77,26 @@ contract ProofOfEfficiency is Ownable { event SendBatch(uint256 indexed batchNum, address indexed sequencer); /** - * @dev Emitted when the owner increases the timeout + * @dev Emitted when a aggregator verifies a new batch */ - event NewWithdrawTimeout(uint256 newWithdrawTimeout); + event VerifyBatch(uint256 indexed batchNum, address indexed aggregator); /** - * @param _bridgeAddress Bridge contract address + * @param _bridge Bridge contract address + * @param _matic MATIC token address + * @param _rollupVerifier rollup verifier address */ constructor( - address _bridgeAddress, + BridgeInterface _bridge, IERC20 _matic, VerifierRollupInterface _rollupVerifier ) { - bridgeAddress = _bridgeAddress; + bridge = _bridge; matic = _matic; rollupVerifier = _rollupVerifier; // register this rollup and update the global exit root - // Bridge.registerRollup(currentLocalExitRoot) - // lastGlobalExitRoot = Bridge.globalExitRoot + lastGlobalExitRoot = bridge.currentGlobalExitRoot(); } /** @@ -125,16 +126,11 @@ contract ProofOfEfficiency is Ownable { * @param transactions L2 ethereum transactions EIP-155 with signature: * rlp(nonce, gasprice, gasLimit, to, value, data, chainid, 0, 0, v, r, s) * @param maticAmount Max amount of MATIC tokens that the sequencer is willing to pay - * @param _permitData Raw data of the call `permit` of the token */ - function sendBatch( - bytes memory transactions, - uint256 maticAmount, - bytes calldata _permitData - ) public { + function sendBatch(bytes memory transactions, uint256 maticAmount) public { // calculate matic collateral uint256 maticCollateral = calculateSequencerCollateral( - transactions.length //TODO how many transactions are here¿?¿?¿??¿?¿¿?¿? + transactions.length //TODO how many transactions are here¿?¿?¿??¿?¿¿?¿? ); require( @@ -142,10 +138,6 @@ contract ProofOfEfficiency is Ownable { "ProofOfEfficiency::sendBatch: NOT_ENOUGH_MATIC" ); - // receive MATIC tokens - if (_permitData.length != 0) { - _permit(address(matic), maticAmount, _permitData); - } matic.safeTransferFrom(msg.sender, address(this), maticCollateral); // Update sentBatches mapping @@ -153,7 +145,7 @@ contract ProofOfEfficiency is Ownable { sentBatches[lastBatchSent].batchL2HashData = keccak256(transactions); sentBatches[lastBatchSent].maticCollateral = maticCollateral; - // check if the sequencer is registered, if not, no one will claim the fees + // Check if the sequencer is registered, if not, no one will claim the fees if (sequencers[msg.sender].chainID != 0) { sentBatches[lastBatchSent].sequencerAddress = msg.sender; } @@ -171,8 +163,8 @@ contract ProofOfEfficiency is Ownable { * @param proofC zk-snark input */ function verifyBatch( - uint256 newLocalExitRoot, - uint256 newStateRoot, + bytes32 newLocalExitRoot, + bytes32 newStateRoot, uint256 batchNum, uint256[2] calldata proofA, uint256[2][2] calldata proofB, @@ -184,9 +176,10 @@ contract ProofOfEfficiency is Ownable { "ProofOfEfficiency::verifyBatch: BATCH_DOES_NOT_MATCH" ); + // Calculate Circuit Input BatchData memory currentBatch = sentBatches[batchNum]; - address sequencerAddress = currentBatch.sequencerAddress; - uint256 batchChainID = sequencers[sequencerAddress].chainID; + address sequencerAddress = currentBatch.sequencerAddress; // could be 0, if sequencer is not registered + uint256 batchChainID = sequencers[sequencerAddress].chainID; // could be 0, if sequencer is not registered uint256 input = uint256( sha256( abi.encodePacked( @@ -197,23 +190,30 @@ contract ProofOfEfficiency is Ownable { newLocalExitRoot, sequencerAddress, currentBatch.batchL2HashData, - batchChainID // could be 0, is that alright? + batchChainID ) ) ) % _RFIELD; - // verify proof + // Verify proof require( rollupVerifier.verifyProof(proofA, proofB, proofC, [input]), "ProofOfEfficiency::verifyBatch: INVALID_PROOF" ); - // update state + + // Update state lastVerifiedBatch++; currentStateRoot = newStateRoot; currentLocalExitRoot = newLocalExitRoot; - // Bridge.updateExitRoot(currentLocalExitRoot) - // lastGlobalExitRoot = Bridge.globalExitRoot + // Interact with bridge + bridge.updateRollupExitRoot(currentLocalExitRoot); + lastGlobalExitRoot = bridge.currentGlobalExitRoot(); + + // Get MATIC reward + matic.safeTransfer(msg.sender, currentBatch.maticCollateral); + + emit VerifyBatch(batchNum, msg.sender); } /** @@ -228,79 +228,4 @@ contract ProofOfEfficiency is Ownable { { return transactionNum * 1 ether; } - - /** - * @notice Function to extract the selector of a bytes calldata - * @param _data The calldata bytes - */ - function _getSelector(bytes memory _data) - private - pure - returns (bytes4 sig) - { - /* solhint-disable no-inline-assembly*/ - assembly { - sig := mload(add(_data, 32)) - } - } - - /** - * @notice Function to call token permit method of extended ERC20 - + @param token ERC20 token address - * @param _amount Quantity that is expected to be allowed - * @param _permitData Raw data of the call `permit` of the token - */ - function _permit( - address token, - uint256 _amount, - bytes calldata _permitData - ) internal returns (bool success, bytes memory returndata) { - bytes4 sig = _getSelector(_permitData); - require( - sig == _PERMIT_SIGNATURE, - "HezMaticMerge::_permit: NOT_VALID_CALL" - ); - ( - address owner, - address spender, - uint256 value, - uint256 deadline, - uint8 v, - bytes32 r, - bytes32 s - ) = abi.decode( - _permitData[4:], - (address, address, uint256, uint256, uint8, bytes32, bytes32) - ); - require( - owner == msg.sender, - "HezMaticMerge::_permit: PERMIT_OWNER_MUST_BE_THE_SENDER" - ); - require( - spender == address(this), - "HezMaticMerge::_permit: SPENDER_MUST_BE_THIS" - ); - require( - value == _amount, - "HezMaticMerge::_permit: PERMIT_AMOUNT_DOES_NOT_MATCH" - ); - - // we call without checking the result, in case it fails and he doesn't have enough balance - // the following transferFrom should be fail. This prevents DoS attacks from using a signature - // before the smartcontract call - /* solhint-disable avoid-low-level-calls*/ - return - address(token).call( - abi.encodeWithSelector( - _PERMIT_SIGNATURE, - owner, - spender, - value, - deadline, - v, - r, - s - ) - ); - } } diff --git a/contracts/interfaces/BridgeInterface.sol b/contracts/interfaces/BridgeInterface.sol new file mode 100644 index 000000000..11b7e0589 --- /dev/null +++ b/contracts/interfaces/BridgeInterface.sol @@ -0,0 +1,9 @@ +// SPDX-License-Identifier: AGPL-3.0 + +pragma solidity 0.8.9; + +interface BridgeInterface { + function currentGlobalExitRoot() external view returns (bytes32); + + function updateRollupExitRoot(bytes32 newRollupExitRoot) external; +} diff --git a/contracts/mockToken/VerifierRollupHelper.sol b/contracts/mockToken/VerifierRollupHelper.sol new file mode 100644 index 000000000..048fcf249 --- /dev/null +++ b/contracts/mockToken/VerifierRollupHelper.sol @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: AGPL-3.0 + +pragma solidity 0.8.9; + +import "../interfaces/VerifierRollupInterface.sol"; + +contract VerifierRollupHelper is VerifierRollupInterface { + function verifyProof( + uint256[2] calldata a, + uint256[2][2] calldata b, + uint256[2] calldata c, + uint256[1] calldata input + ) public view override returns (bool) { + return true; + } +} diff --git a/hardhat.config.js b/hardhat.config.js index f82ec6a6c..8f9965296 100644 --- a/hardhat.config.js +++ b/hardhat.config.js @@ -64,10 +64,6 @@ module.exports = { count: 20, }, }, - mainnet: { - url: "http://geth.dappnode:8545", - accounts: [`${process.env.PRIVATE_KEY}`] - }, }, gasReporter: { currency: "USD", diff --git a/test/helpers/erc2612.js b/test/helpers/erc2612.js deleted file mode 100644 index 0386fab12..000000000 --- a/test/helpers/erc2612.js +++ /dev/null @@ -1,49 +0,0 @@ -const { - ethers -} = require("hardhat"); -const { expect } = require("chai"); - -async function createPermitSignature(tokenContractInstance, wallet, spenderAddress, value, nonce, deadline) { - const chainId = (await tokenContractInstance.getChainId()); - const name = await tokenContractInstance.name(); - - // The domain - const domain = { - name: name, - version: "1", - chainId: chainId, - verifyingContract: tokenContractInstance.address - }; - - // The named list of all type definitions - const types = { - Permit: [ - { name: "owner", type: "address" }, - { name: "spender", type: "address" }, - { name: "value", type: "uint256" }, - { name: "nonce", type: "uint256" }, - { name: "deadline", type: "uint256" }, - ] - }; - - // The data to sign - const values = { - owner: wallet.address, - spender: spenderAddress, - value: value, - nonce: nonce, - deadline: deadline, - }; - - const rawSignature = await wallet._signTypedData(domain, types, values); - const signature = ethers.utils.splitSignature(rawSignature); - const recoveredAddressTyped = ethers.utils.verifyTypedData(domain, types, values, rawSignature); - expect(recoveredAddressTyped).to.be.equal(wallet.address); - - return signature; -} - - -module.exports = { - createPermitSignature -}; \ No newline at end of file diff --git a/test/proofOfEfficiency.js b/test/proofOfEfficiency.js index 84253d4eb..dcf1968a5 100644 --- a/test/proofOfEfficiency.js +++ b/test/proofOfEfficiency.js @@ -1,383 +1,192 @@ const { expect } = require("chai"); const { ethers } = require("hardhat"); -const { - createPermitSignature -} = require("./helpers/erc2612"); - describe("HezMaticMerge", function () { - const ABIbid = [ - "function permit(address,address,uint256,uint256,uint8,bytes32,bytes32)", - ]; - const iface = new ethers.utils.Interface(ABIbid); - - const swapRatio = 3500; // 3.5 factor - const duration = 3600; // 1 hour + let deployer; + let userAWallet; + let userBWallet; - const hermezTokenName = "Hermez Network Token"; - const hermezTokenSymbol = "HEZ"; - const hermezTokenInitialBalance = ethers.utils.parseEther("100000000"); + let verifierContract; + let bridgeContract; + let proofOfEfficiencyContract; + let maticTokenContract; const maticTokenName = "Matic Token"; const maticTokenSymbol = "MATIC"; const decimals = 18; const maticTokenInitialBalance = ethers.utils.parseEther("20000000"); - let deployer; - let governance; - let userAWallet; - let userBWallet; - - let hezMaticMergeContract; - let hermezTokenContract; - let maticTokenContract; - beforeEach("Deploy contract", async () => { // load signers const signers = await ethers.getSigners(); // assign signers deployer = signers[0]; - governance = signers[1]; - userAWallet = signers[2]; - userBWallet = signers[3]; + aggregator = signers[1]; + sequencer = signers[2]; - // deploy ERC20 tokens - const hezTokenFactory = await ethers.getContractFactory("HEZ"); - const maticTokenFactory = await ethers.getContractFactory("MaticToken"); - - hermezTokenContract = await hezTokenFactory.deploy( - deployer.address + // deploy mock verifier + const VerifierRollupHelperFactory = await ethers.getContractFactory( + "VerifierRollupHelper" ); + verifierContract = await VerifierRollupHelperFactory.deploy(); + // deploy MATIC + const maticTokenFactory = await ethers.getContractFactory("ERC20PermitMock"); maticTokenContract = await maticTokenFactory.deploy( maticTokenName, maticTokenSymbol, - decimals, + deployer.address, maticTokenInitialBalance ); - - await hermezTokenContract.deployed(); await maticTokenContract.deployed(); - // deploy hezMaticMergeContract - const HezMaticMergeFactory = await ethers.getContractFactory("HezMaticMerge"); - hezMaticMergeContract = await HezMaticMergeFactory.deploy( - hermezTokenContract.address, + // deploy bridge + const precalculatePoEAddress = await ethers.utils.getContractAddress( + { "from": deployer.address, "nonce": (await ethers.provider.getTransactionCount(deployer.address)) + 1 }); + const BridgeFactory = await ethers.getContractFactory("BridgeMock"); + bridgeContract = await BridgeFactory.deploy(precalculatePoEAddress); + await bridgeContract.deployed(); + + // deploy proof of efficiency + const ProofOfEfficiencyFactory = await ethers.getContractFactory("ProofOfEfficiency"); + proofOfEfficiencyContract = await ProofOfEfficiencyFactory.deploy( + bridgeContract.address, maticTokenContract.address, - duration - ); - - await hezMaticMergeContract.deployed(); - }); - - it("should check the constructor", async () => { - expect(await hezMaticMergeContract.hez()).to.be.equal(hermezTokenContract.address); - expect(await hezMaticMergeContract.matic()).to.be.equal(maticTokenContract.address); - expect(await hezMaticMergeContract.SWAP_RATIO()).to.be.equal(swapRatio); - - const deployedTimestamp = (await ethers.provider.getBlock(hezMaticMergeContract.deployTransaction.blockNumber)).timestamp; - expect(await hezMaticMergeContract.withdrawTimeout()).to.be.equal(deployedTimestamp + duration); - }); - - it("shouldn't be able to swap HEZ for MATIC", async () => { - // distribute tokens - const hezMaticMergeAmount = ethers.utils.parseEther("100"); - const userWalletAmount = ethers.utils.parseEther("1"); - - await maticTokenContract.connect(deployer).transfer(hezMaticMergeContract.address, hezMaticMergeAmount); - await hermezTokenContract.connect(deployer).transfer(userAWallet.address, userWalletAmount); - await hermezTokenContract.connect(deployer).transfer(userBWallet.address, userWalletAmount); - - // assert token amounts - expect(await maticTokenContract.balanceOf(hezMaticMergeContract.address)).to.be.equal(hezMaticMergeAmount); - expect(await hermezTokenContract.balanceOf(userAWallet.address)).to.be.equal(userWalletAmount); - expect(await hermezTokenContract.balanceOf(userBWallet.address)).to.be.equal(userWalletAmount); - expect(await maticTokenContract.balanceOf(userAWallet.address)).to.be.equal(0); - expect(await maticTokenContract.balanceOf(userBWallet.address)).to.be.equal(0); - - // try to swap 10 HEZ for 35 MATIC - const amountToBridgeInt = 10; - const amountToBridge = ethers.utils.parseEther(amountToBridgeInt.toString()); - - const deadline = ethers.constants.MaxUint256; - const value = amountToBridge; - const nonce = await hermezTokenContract.nonces(userAWallet.address); - const { v, r, s } = await createPermitSignature( - hermezTokenContract, - userAWallet, - hezMaticMergeContract.address, - value, - nonce, - deadline + verifierContract.address ); + await proofOfEfficiencyContract.deployed(); + expect(proofOfEfficiencyContract.address).to.be.equal(precalculatePoEAddress); - const dataPermit = iface.encodeFunctionData("permit", [ - userAWallet.address, - hezMaticMergeContract.address, - value, - deadline, - v, - r, - s - ]); - - await expect(hezMaticMergeContract.connect(userAWallet).hezToMatic(amountToBridge, dataPermit) - ).to.be.revertedWith("MATH:SUB_UNDERFLOW"); + // fund sequencer address with Matic tokens + await maticTokenContract.transfer(sequencer.address, ethers.utils.parseEther("100")); }); - it("should be able to swap HEZ for MATIC", async () => { - // distribute tokens - const hezMaticMergeAmount = ethers.utils.parseEther("100"); - const userWalletAmount = ethers.utils.parseEther("10"); - - await maticTokenContract.connect(deployer).transfer(hezMaticMergeContract.address, hezMaticMergeAmount); - await hermezTokenContract.connect(deployer).transfer(userAWallet.address, userWalletAmount); - await hermezTokenContract.connect(deployer).transfer(userBWallet.address, userWalletAmount); - - // assert token amounts - expect(await maticTokenContract.balanceOf(hezMaticMergeContract.address)).to.be.equal(hezMaticMergeAmount); - expect(await hermezTokenContract.balanceOf(userAWallet.address)).to.be.equal(userWalletAmount); - expect(await hermezTokenContract.balanceOf(userBWallet.address)).to.be.equal(userWalletAmount); - expect(await maticTokenContract.balanceOf(userAWallet.address)).to.be.equal(0); - expect(await maticTokenContract.balanceOf(userBWallet.address)).to.be.equal(0); - - // swap 1 HEZ for 3.5 MATIC - const amountToBridgeInt = 1; - const amountBridgedInt = amountToBridgeInt * swapRatio / 1000; - const amountToBridge = ethers.utils.parseEther(amountToBridgeInt.toString()); - const amountBridged = amountToBridge.mul(swapRatio).div(1000); - - const deadline = ethers.constants.MaxUint256; - const value = amountToBridge; - const nonce = await hermezTokenContract.nonces(userAWallet.address); - const { v, r, s } = await createPermitSignature( - hermezTokenContract, - userAWallet, - hezMaticMergeContract.address, - value, - nonce, - deadline - ); + it("should check the constructor parameters", async () => { + expect(await bridgeContract.rollupAddress()).to.be.equal(proofOfEfficiencyContract.address); + expect(await bridgeContract.rollupExitRoot()).to.be.equal(ethers.BigNumber.from(0)); + expect(await bridgeContract.mainnetExitRoot()).to.be.equal(ethers.BigNumber.from(0)); - const dataPermit = iface.encodeFunctionData("permit", [ - userAWallet.address, - hezMaticMergeContract.address, - value, - deadline, - v, - r, - s - ]); - - const txSwap = await hezMaticMergeContract.connect(userAWallet).hezToMatic(amountToBridge, dataPermit); - const receiptSwap = await txSwap.wait(); - - // approve event - const approveEvent = hermezTokenContract.interface.parseLog(receiptSwap.events[0]); - expect(approveEvent.name).to.be.equal("Approval"); - expect(approveEvent.args.owner).to.be.equal(userAWallet.address); - expect(approveEvent.args.spender).to.be.equal(hezMaticMergeContract.address); - expect(approveEvent.args.value).to.be.equal(amountToBridge); - - // transferFrom event - const transferFromEvent = hermezTokenContract.interface.parseLog(receiptSwap.events[1]); - expect(transferFromEvent.name).to.be.equal("Transfer"); - expect(transferFromEvent.args.from).to.be.equal(userAWallet.address); - expect(transferFromEvent.args.to).to.be.equal(hezMaticMergeContract.address); - expect(transferFromEvent.args.value).to.be.equal(amountToBridge); - - // burn event - const burnEvent = hermezTokenContract.interface.parseLog(receiptSwap.events[2]); - expect(burnEvent.name).to.be.equal("Transfer"); - expect(burnEvent.args.from).to.be.equal(hezMaticMergeContract.address); - expect(burnEvent.args.to).to.be.equal("0x0000000000000000000000000000000000000000"); - expect(burnEvent.args.value).to.be.equal(amountToBridge); - - // transfer matic token Event - const transfermaticTokenEvent = maticTokenContract.interface.parseLog(receiptSwap.events[3]); - expect(transfermaticTokenEvent.name).to.be.equal("Transfer"); - expect(transfermaticTokenEvent.args.from).to.be.equal(hezMaticMergeContract.address); - expect(transfermaticTokenEvent.args.to).to.be.equal(userAWallet.address); - expect(transfermaticTokenEvent.args.value).to.be.equal(amountBridged); - - // HezToMatic event - const granteeEvent = receiptSwap.events[4]; - expect(granteeEvent.event).to.be.equal("HezToMatic"); - expect(granteeEvent.args.grantee).to.be.equal(userAWallet.address); - expect(granteeEvent.args.hezAmount).to.be.equal(amountToBridge); - expect(granteeEvent.args.maticAmount).to.be.equal(amountBridged); - - // check balances - expect(await hermezTokenContract.balanceOf(userAWallet.address)).to.be.equal(userWalletAmount.sub(amountToBridge)); - expect(await hermezTokenContract.balanceOf(hezMaticMergeContract.address)).to.be.equal(0); - expect(await maticTokenContract.balanceOf(userAWallet.address)).to.be.equal(amountBridged); - expect(await maticTokenContract.balanceOf(hezMaticMergeContract.address)).to.be.equal(hezMaticMergeAmount.sub(amountBridged)); - expect(amountBridged).to.be.equal(ethers.utils.parseEther(amountBridgedInt.toString())); + expect(await proofOfEfficiencyContract.bridge()).to.be.equal(bridgeContract.address); + expect(await proofOfEfficiencyContract.matic()).to.be.equal(maticTokenContract.address); + expect(await proofOfEfficiencyContract.rollupVerifier()).to.be.equal(verifierContract.address); }); - it("shouldn't be able to withdrawTokens if is not the owner, or the timeout is not reached for MATIC", async () => { - - await expect( - hezMaticMergeContract.connect(userAWallet).withdrawTokens(maticTokenContract.address, 1) - ).to.be.revertedWith("Ownable: caller is not the owner"); - - await expect( - hezMaticMergeContract.connect(userAWallet).withdrawTokens("0x0000000000000000000000000000000000000000", 1) - ).to.be.revertedWith("Ownable: caller is not the owner"); - - await expect( - hezMaticMergeContract.connect(deployer).withdrawTokens(maticTokenContract.address, 1) - ).to.be.revertedWith("HezMaticMerge::withdrawTokens: TIMEOUT_NOT_REACHED"); - - await expect( - hezMaticMergeContract.connect(deployer).withdrawTokens("0x0000000000000000000000000000000000000000", 1) - ).to.be.revertedWith("Address: call to non-contract"); + it("should register a sequencer", async () => { + // register a sequencer + const sequencerURL = "http://exampleURL" + const sequencerAddress = deployer.address; + await expect(proofOfEfficiencyContract.registerSequencer(sequencerURL)) + .to.emit(proofOfEfficiencyContract, "SetSequencer") + .withArgs(sequencerAddress, sequencerURL); + + // check the stored sequencer struct + const sequencerStruct = await proofOfEfficiencyContract.sequencers(sequencerAddress) + expect(sequencerStruct.sequencerURL).to.be.equal(sequencerURL); + expect(sequencerStruct.chainID).to.be.equal(ethers.BigNumber.from(1)); + + // update the sequencer URL + const sequencerURL2 = "http://exampleURL2" + await expect(proofOfEfficiencyContract.registerSequencer(sequencerURL2)) + .to.emit(proofOfEfficiencyContract, "SetSequencer") + .withArgs(sequencerAddress, sequencerURL2); + + // check the stored sequencer struct + const sequencerStruct2 = await proofOfEfficiencyContract.sequencers(sequencerAddress) + expect(sequencerStruct2.sequencerURL).to.be.equal(sequencerURL2); + expect(sequencerStruct2.chainID).to.be.equal(ethers.BigNumber.from(1)); }); - it("should be able to withdrawTokens ", async () => { - // send tokens to HezMaticMerge contract - await maticTokenContract.connect(deployer).transfer(hezMaticMergeContract.address, maticTokenInitialBalance); - await hermezTokenContract.connect(deployer).transfer(hezMaticMergeContract.address, hermezTokenInitialBalance); + it("should send batch of transactions", async () => { + const l2tx = "0x123456" + const maticAmount = ethers.utils.parseEther(((l2tx.length - 2) / 2).toString()) // for now the price depends on the bytes + const sequencerAddress = deployer.address - // assert balances HezMaticMerge - expect(await maticTokenContract.balanceOf(hezMaticMergeContract.address)).to.be.equal(maticTokenInitialBalance); - expect(await hermezTokenContract.balanceOf(hezMaticMergeContract.address)).to.be.equal(hermezTokenInitialBalance); + expect(maticAmount.toString()).to.be.equal((await proofOfEfficiencyContract.calculateSequencerCollateral(l2tx.length - 2) / 2).toString()); - // assert balances deployer - expect(await hermezTokenContract.balanceOf(deployer.address)).to.be.equal(0); - expect(await maticTokenContract.balanceOf(deployer.address)).to.be.equal(0); + // revert because the maxMatic amount is less than the necessary to pay + await expect(proofOfEfficiencyContract.sendBatch(l2tx, maticAmount.sub(1))) + .to.be.revertedWith("ProofOfEfficiency::sendBatch: NOT_ENOUGH_MATIC"); - // assert withdraw of MATIC can't be done until timeout is reached - const withdrawTimeout = (await hezMaticMergeContract.withdrawTimeout()).toNumber(); - let currentTimestamp = (await ethers.provider.getBlock()).timestamp; + // revert because tokens were not approved + await expect(proofOfEfficiencyContract.sendBatch(l2tx, maticAmount)) + .to.be.revertedWith("ERC20: transfer amount exceeds allowance"); - expect(withdrawTimeout).to.be.greaterThan(currentTimestamp); - await expect( - hezMaticMergeContract.connect(deployer).withdrawTokens(maticTokenContract.address, maticTokenInitialBalance) - ).to.be.revertedWith("HezMaticMerge::withdrawTokens: TIMEOUT_NOT_REACHED"); - - // withdraw HEZ tokens without timeout restrictions - - // assert only owner can withdraw the tokens - await expect( - hezMaticMergeContract.connect(governance).withdrawTokens(hermezTokenContract.address, hermezTokenInitialBalance) - ).to.be.revertedWith("Ownable: caller is not the owner"); - - await expect(hezMaticMergeContract.connect(deployer).withdrawTokens(hermezTokenContract.address, hermezTokenInitialBalance) - ).to.emit(hezMaticMergeContract, "WithdrawTokens") - .withArgs(hermezTokenContract.address, hermezTokenInitialBalance); - - // assert balances HezMaticMerge - expect(await maticTokenContract.balanceOf(hezMaticMergeContract.address)).to.be.equal(maticTokenInitialBalance); - expect(await hermezTokenContract.balanceOf(hezMaticMergeContract.address)).to.be.equal(0); - - // assert balances deployer - expect(await hermezTokenContract.balanceOf(deployer.address)).to.be.equal(hermezTokenInitialBalance); - expect(await maticTokenContract.balanceOf(deployer.address)).to.be.equal(0); + const initialOwnerBalance = await maticTokenContract.balanceOf( + await deployer.getAddress() + ); - // assert no more tokens can be withdrawed if there's no balance await expect( - hezMaticMergeContract.connect(deployer).withdrawTokens(hermezTokenContract.address, 1) - ).to.be.revertedWith("MATH:SUB_UNDERFLOW"); + maticTokenContract.approve(proofOfEfficiencyContract.address, maticAmount) + ).to.emit(maticTokenContract, "Approval"); - // advance time and withdraw MATIC - currentTimestamp = (await ethers.provider.getBlock()).timestamp; - await ethers.provider.send("evm_increaseTime", [withdrawTimeout - currentTimestamp + 1]); - await ethers.provider.send("evm_mine"); + const lastBatchSent = await proofOfEfficiencyContract.lastBatchSent(); - currentTimestamp = (await ethers.provider.getBlock()).timestamp; - expect(withdrawTimeout).to.be.lessThan(currentTimestamp); + await expect(proofOfEfficiencyContract.sendBatch(l2tx, maticAmount)) + .to.emit(proofOfEfficiencyContract, "SendBatch") + .withArgs(lastBatchSent.add(1), sequencerAddress); - await expect(hezMaticMergeContract.connect(deployer).withdrawTokens(maticTokenContract.address, maticTokenInitialBalance) - ).to.emit(hezMaticMergeContract, "WithdrawTokens") - .withArgs(maticTokenContract.address, maticTokenInitialBalance); - - // assert balances HezMaticMerge - expect(await maticTokenContract.balanceOf(hezMaticMergeContract.address)).to.be.equal(0); - expect(await hermezTokenContract.balanceOf(hezMaticMergeContract.address)).to.be.equal(0); - - // assert balances deployer - expect(await hermezTokenContract.balanceOf(deployer.address)).to.be.equal(hermezTokenInitialBalance); - expect(await maticTokenContract.balanceOf(deployer.address)).to.be.equal(maticTokenInitialBalance); + const finalOwnerBalance = await maticTokenContract.balanceOf( + await deployer.getAddress() + ); + expect(finalOwnerBalance).to.equal( + ethers.BigNumber.from(initialOwnerBalance).sub(ethers.BigNumber.from(maticAmount)) + ); }); - it("should be able to update withdrawLeftOver ", async () => { - // send tokens to HezMaticMerge contract - await maticTokenContract.connect(deployer).transfer(hezMaticMergeContract.address, maticTokenInitialBalance); - - // assert balances - expect(await maticTokenContract.balanceOf(hezMaticMergeContract.address)).to.be.equal(maticTokenInitialBalance); - expect(await maticTokenContract.balanceOf(deployer.address)).to.be.equal(0); - - // assert withdraw can't be done until timeout is reached - const withdrawTimeout = (await hezMaticMergeContract.withdrawTimeout()).toNumber(); - let currentTimestamp = (await ethers.provider.getBlock()).timestamp; - - expect(withdrawTimeout).to.be.greaterThan(currentTimestamp); + it("should forge the batch", async () => { + const l2tx = "0x123456" + const maticAmount = ethers.utils.parseEther(((l2tx.length - 2) / 2).toString()) // for now the price depends on the bytes - await expect( - hezMaticMergeContract.connect(deployer).withdrawTokens(maticTokenContract.address, maticTokenInitialBalance) - ).to.be.revertedWith("HezMaticMerge::withdrawTokens: TIMEOUT_NOT_REACHED"); - - // advance time and withdraw leftovers - await ethers.provider.send("evm_increaseTime", [withdrawTimeout - currentTimestamp + 1]); - await ethers.provider.send("evm_mine"); - - currentTimestamp = (await ethers.provider.getBlock()).timestamp; - expect(withdrawTimeout).to.be.lessThan(currentTimestamp); + const aggregatorAddress = aggregator.address + const sequencerAddress = sequencer.address + // sequencer send the batch await expect( - hezMaticMergeContract.connect(governance).setWithdrawTimeout(withdrawTimeout) - ).to.be.revertedWith("Ownable: caller is not the owner"); + maticTokenContract.connect(sequencer).approve(proofOfEfficiencyContract.address, maticAmount) + ).to.emit(maticTokenContract, "Approval"); - await expect( - hezMaticMergeContract.connect(deployer).setWithdrawTimeout(withdrawTimeout) - ).to.be.revertedWith("HezMaticMerge::setWithdrawTimeout: NEW_TIMEOUT_MUST_BE_HIGHER"); + const lastBatchSent = await proofOfEfficiencyContract.lastBatchSent(); - await expect( - hezMaticMergeContract.connect(deployer).setWithdrawTimeout(currentTimestamp) - ).to.emit(hezMaticMergeContract, "NewWithdrawTimeout") - .withArgs(currentTimestamp); + await expect(proofOfEfficiencyContract.connect(sequencer).sendBatch(l2tx, maticAmount)) + .to.emit(proofOfEfficiencyContract, "SendBatch") + .withArgs(lastBatchSent.add(1), sequencerAddress); - await expect( - hezMaticMergeContract.connect(deployer).setWithdrawTimeout(currentTimestamp + 3000) - ).to.emit(hezMaticMergeContract, "NewWithdrawTimeout") - .withArgs(currentTimestamp + 3000); + // aggregator forge the batch + const newLocalExitRoot = "0x0000000000000000000000000000000000000000000000000000000000000000"; + const newStateRoot = "0x0000000000000000000000000000000000000000000000000000000000000000"; + const batchNum = (await proofOfEfficiencyContract.lastVerifiedBatch()).add(1); + const proofA = ["0", "0"]; + const proofB = [ + ["0", "0"], + ["0", "0"], + ]; + const proofC = ["0", "0"]; - await expect( - hezMaticMergeContract.connect(deployer).withdrawTokens(maticTokenContract.address, maticTokenInitialBalance) - ).to.be.revertedWith("HezMaticMerge::withdrawTokens: TIMEOUT_NOT_REACHED"); - }); - it("should be able to transfer ownership", async () => { - // send tokens to HezMaticMerge contract - await maticTokenContract.connect(deployer).transfer(hezMaticMergeContract.address, maticTokenInitialBalance); - await hermezTokenContract.connect(deployer).transfer(hezMaticMergeContract.address, hermezTokenInitialBalance); - - // check current owner - expect(await hezMaticMergeContract.owner()).to.be.equal(deployer.address); + const initialAggregatorMatic = await maticTokenContract.balanceOf( + await aggregator.getAddress() + ); - // transfer ownership await expect( - hezMaticMergeContract.connect(governance).transferOwnership(governance.address) - ).to.be.revertedWith("Ownable: caller is not the owner"); - - await expect(hezMaticMergeContract.connect(deployer).transferOwnership(governance.address) - ).to.emit(hezMaticMergeContract, "OwnershipTransferred") - .withArgs(deployer.address, governance.address); - - // check new owner premissions - expect(await hezMaticMergeContract.owner()).to.be.equal(governance.address); + proofOfEfficiencyContract.connect(aggregator).verifyBatch( + newLocalExitRoot, newStateRoot, batchNum.sub(1), proofA, proofB, proofC + ) + ).to.be.revertedWith("ProofOfEfficiency::verifyBatch: BATCH_DOES_NOT_MATCH"); await expect( - hezMaticMergeContract.connect(deployer).withdrawTokens(hermezTokenContract.address, 0) - ).to.be.revertedWith("Ownable: caller is not the owner"); + proofOfEfficiencyContract.connect(aggregator).verifyBatch( + newLocalExitRoot, newStateRoot, batchNum, proofA, proofB, proofC + ) + ).to.emit(proofOfEfficiencyContract, "VerifyBatch") + .withArgs(batchNum, aggregatorAddress); - await expect( - hezMaticMergeContract.connect(governance).withdrawTokens(hermezTokenContract.address, hermezTokenInitialBalance) - ).to.emit(hezMaticMergeContract, "WithdrawTokens") - .withArgs(hermezTokenContract.address, hermezTokenInitialBalance); + const finalAggregatorMatic = await maticTokenContract.balanceOf( + await aggregator.getAddress() + ); + expect(finalAggregatorMatic).to.equal( + ethers.BigNumber.from(initialAggregatorMatic).add(ethers.BigNumber.from(maticAmount)) + ); }); }); \ No newline at end of file