diff --git a/.eslintrc b/.eslintrc index 149807292..20cae5175 100644 --- a/.eslintrc +++ b/.eslintrc @@ -8,5 +8,8 @@ "import/namespace": "off", "import/no-unresolved": "off", "import/order": "off" - } + }, + "ignorePatterns": [ + "**/lib/*" + ] } diff --git a/.gitmodules b/.gitmodules index 7bc18d7ad..e0abe69d3 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,6 @@ [submodule "l1-contracts/lib/forge-std"] path = l1-contracts/lib/forge-std url = https://github.com/foundry-rs/forge-std +[submodule "l1-contracts/lib/murky"] + path = l1-contracts/lib/murky + url = https://github.com/dmfxyz/murky diff --git a/l1-contracts/contracts/dev-contracts/test/TransactionValidatorTest.sol b/l1-contracts/contracts/dev-contracts/test/TransactionValidatorTest.sol deleted file mode 100644 index 3ec4ded29..000000000 --- a/l1-contracts/contracts/dev-contracts/test/TransactionValidatorTest.sol +++ /dev/null @@ -1,19 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity 0.8.20; - -import "../../zksync/libraries/TransactionValidator.sol"; -import "../../zksync/interfaces/IMailbox.sol"; - -contract TransactionValidatorTest { - function validateL1ToL2Transaction( - IMailbox.L2CanonicalTransaction memory _transaction, - uint256 _priorityTxMaxGasLimit - ) external pure { - TransactionValidator.validateL1ToL2Transaction(_transaction, abi.encode(_transaction), _priorityTxMaxGasLimit); - } - - function validateUpgradeTransaction(IMailbox.L2CanonicalTransaction memory _transaction) external pure { - TransactionValidator.validateUpgradeTransaction(_transaction); - } -} diff --git a/l1-contracts/lib/murky b/l1-contracts/lib/murky new file mode 160000 index 000000000..40de6e801 --- /dev/null +++ b/l1-contracts/lib/murky @@ -0,0 +1 @@ +Subproject commit 40de6e80117f39cda69d71b07b7c824adac91b29 diff --git a/l1-contracts/remappings.txt b/l1-contracts/remappings.txt index 5c40cafaf..d882fbd10 100644 --- a/l1-contracts/remappings.txt +++ b/l1-contracts/remappings.txt @@ -4,4 +4,5 @@ ds-test/=lib/forge-std/lib/ds-test/src/ eth-gas-reporter/=node_modules/eth-gas-reporter/ forge-std/=lib/forge-std/src/ hardhat/=node_modules/hardhat/ -solpp/=cache/solpp-generated-contracts/ \ No newline at end of file +solpp/=cache/solpp-generated-contracts/ +murky/=lib/murky/src/ diff --git a/l1-contracts/test/foundry/unit/concrete/Merkle/Merkle.t.sol b/l1-contracts/test/foundry/unit/concrete/Merkle/Merkle.t.sol new file mode 100644 index 000000000..2ec6591b6 --- /dev/null +++ b/l1-contracts/test/foundry/unit/concrete/Merkle/Merkle.t.sol @@ -0,0 +1,66 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.20; + +import {Test} from "forge-std/Test.sol"; +import {MerkleTest} from "solpp/dev-contracts/test/MerkleTest.sol"; +import {MerkleTreeNoSort} from "./MerkleTreeNoSort.sol"; + +contract MerkleTestTest is Test { + MerkleTreeNoSort merkleTree; + MerkleTest merkleTest; + bytes32[] elements; + bytes32 root; + + function setUp() public { + merkleTree = new MerkleTreeNoSort(); + merkleTest = new MerkleTest(); + + for (uint256 i = 0; i < 65; i++) { + elements.push(keccak256(abi.encodePacked(i))); + } + + root = merkleTree.getRoot(elements); + } + + function testElements(uint256 i) public { + vm.assume(i < elements.length); + bytes32 leaf = elements[i]; + bytes32[] memory proof = merkleTree.getProof(elements, i); + + bytes32 rootFromContract = merkleTest.calculateRoot(proof, i, leaf); + + assertEq(rootFromContract, root); + } + + function testFirstElement() public { + testElements(0); + } + + function testLastElement() public { + testElements(elements.length - 1); + } + + function testEmptyProof_shouldRevert() public { + bytes32 leaf = elements[0]; + bytes32[] memory proof; + + vm.expectRevert(bytes("xc")); + merkleTest.calculateRoot(proof, 0, leaf); + } + + function testLeafIndexTooBig_shouldRevert() public { + bytes32 leaf = elements[0]; + bytes32[] memory proof = merkleTree.getProof(elements, 0); + + vm.expectRevert(bytes("px")); + merkleTest.calculateRoot(proof, 2 ** 255, leaf); + } + + function testProofLengthTooLarge_shouldRevert() public { + bytes32 leaf = elements[0]; + bytes32[] memory proof = new bytes32[](256); + + vm.expectRevert(bytes("bt")); + merkleTest.calculateRoot(proof, 0, leaf); + } +} diff --git a/l1-contracts/test/foundry/unit/concrete/Merkle/MerkleTreeNoSort.sol b/l1-contracts/test/foundry/unit/concrete/Merkle/MerkleTreeNoSort.sol new file mode 100644 index 000000000..3f4f64aa4 --- /dev/null +++ b/l1-contracts/test/foundry/unit/concrete/Merkle/MerkleTreeNoSort.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.20; + +import "murky/common/MurkyBase.sol"; + +contract MerkleTreeNoSort is MurkyBase { + /******************** + * HASHING FUNCTION * + ********************/ + + /// The original Merkle tree contains the ascending sort and concat prior to hashing, so we need to override it + function hashLeafPairs(bytes32 left, bytes32 right) public pure override returns (bytes32 _hash) { + assembly { + mstore(0x0, left) + mstore(0x20, right) + _hash := keccak256(0x0, 0x40) + } + } +} diff --git a/l1-contracts/test/foundry/unit/concrete/PriorityQueue/OnEmptyQueue.sol b/l1-contracts/test/foundry/unit/concrete/PriorityQueue/OnEmptyQueue.sol new file mode 100644 index 000000000..98a0e3e22 --- /dev/null +++ b/l1-contracts/test/foundry/unit/concrete/PriorityQueue/OnEmptyQueue.sol @@ -0,0 +1,22 @@ +pragma solidity 0.8.20; + +import {PriorityQueueSharedTest} from "./_PriorityQueue_Shared.t.sol"; + +contract OnEmptyQueueTest is PriorityQueueSharedTest { + function test_gets() public { + assertEq(0, priorityQueue.getSize()); + assertEq(0, priorityQueue.getFirstUnprocessedPriorityTx()); + assertEq(0, priorityQueue.getTotalPriorityTxs()); + assertTrue(priorityQueue.isEmpty()); + } + + function test_failGetFront() public { + vm.expectRevert(bytes("D")); + priorityQueue.front(); + } + + function test_failPopFront() public { + vm.expectRevert(bytes("s")); + priorityQueue.popFront(); + } +} diff --git a/l1-contracts/test/foundry/unit/concrete/PriorityQueue/PopOperations.sol b/l1-contracts/test/foundry/unit/concrete/PriorityQueue/PopOperations.sol new file mode 100644 index 000000000..c5146838e --- /dev/null +++ b/l1-contracts/test/foundry/unit/concrete/PriorityQueue/PopOperations.sol @@ -0,0 +1,71 @@ +pragma solidity 0.8.20; + +import {PriorityQueueSharedTest} from "./_PriorityQueue_Shared.t.sol"; +import {PriorityOperation} from "../../../../../cache/solpp-generated-contracts/dev-contracts/test/PriorityQueueTest.sol"; + +contract PopOperationsTest is PriorityQueueSharedTest { + uint public constant NUMBER_OPERATIONS = 10; + + function setUp() public { + push_mock_entries(NUMBER_OPERATIONS); + } + + function test_after_pop() public { + assertEq(NUMBER_OPERATIONS, priorityQueue.getSize()); + + PriorityOperation memory front = priorityQueue.popFront(); + assertEq(keccak256(abi.encode(0)), front.canonicalTxHash); + assertEq(uint64(0), front.expirationTimestamp); + assertEq(uint192(0), front.layer2Tip); + + assertEq(NUMBER_OPERATIONS - 1, priorityQueue.getSize()); + assertEq(1, priorityQueue.getFirstUnprocessedPriorityTx()); + assertEq(NUMBER_OPERATIONS, priorityQueue.getTotalPriorityTxs()); + assertFalse(priorityQueue.isEmpty()); + + // Ok - one more pop + PriorityOperation memory front2 = priorityQueue.popFront(); + assertEq(keccak256(abi.encode(1)), front2.canonicalTxHash); + assertEq(uint64(1), front2.expirationTimestamp); + assertEq(uint192(1), front2.layer2Tip); + + assertEq(NUMBER_OPERATIONS - 2, priorityQueue.getSize()); + assertEq(2, priorityQueue.getFirstUnprocessedPriorityTx()); + assertEq(NUMBER_OPERATIONS, priorityQueue.getTotalPriorityTxs()); + assertFalse(priorityQueue.isEmpty()); + } + + function test_pop_until_limit() public { + for (uint i = 0; i < NUMBER_OPERATIONS; ++i) { + PriorityOperation memory front = priorityQueue.popFront(); + assertEq(keccak256(abi.encode(i)), front.canonicalTxHash); + } + + assertEq(0, priorityQueue.getSize()); + assertEq(NUMBER_OPERATIONS, priorityQueue.getFirstUnprocessedPriorityTx()); + assertEq(NUMBER_OPERATIONS, priorityQueue.getTotalPriorityTxs()); + assertTrue(priorityQueue.isEmpty()); + + // And now let's push something. + + PriorityOperation memory dummyOp = PriorityOperation({ + canonicalTxHash: keccak256(abi.encode(300)), + expirationTimestamp: uint64(300), + layer2Tip: uint192(300) + }); + priorityQueue.pushBack(dummyOp); + + assertEq(1, priorityQueue.getSize()); + assertEq(NUMBER_OPERATIONS, priorityQueue.getFirstUnprocessedPriorityTx()); + assertEq(NUMBER_OPERATIONS + 1, priorityQueue.getTotalPriorityTxs()); + assertFalse(priorityQueue.isEmpty()); + + PriorityOperation memory front_end = priorityQueue.popFront(); + assertEq(keccak256(abi.encode(300)), front_end.canonicalTxHash); + assertTrue(priorityQueue.isEmpty()); + + // And now let's go over the limit and fail. + vm.expectRevert(bytes.concat("s")); + priorityQueue.popFront(); + } +} diff --git a/l1-contracts/test/foundry/unit/concrete/PriorityQueue/PushOperations.sol b/l1-contracts/test/foundry/unit/concrete/PriorityQueue/PushOperations.sol new file mode 100644 index 000000000..0fb540d00 --- /dev/null +++ b/l1-contracts/test/foundry/unit/concrete/PriorityQueue/PushOperations.sol @@ -0,0 +1,25 @@ +pragma solidity 0.8.20; + +import {PriorityQueueSharedTest} from "./_PriorityQueue_Shared.t.sol"; +import {PriorityOperation} from "../../../../../cache/solpp-generated-contracts/dev-contracts/test/PriorityQueueTest.sol"; + +contract PushOperationsTest is PriorityQueueSharedTest { + uint public constant NUMBER_OPERATIONS = 10; + + function setUp() public { + push_mock_entries(NUMBER_OPERATIONS); + } + + function test_front() public { + assertEq(NUMBER_OPERATIONS, priorityQueue.getSize()); + PriorityOperation memory front = priorityQueue.front(); + assertEq(keccak256(abi.encode(0)), front.canonicalTxHash); + assertEq(uint64(0), front.expirationTimestamp); + assertEq(uint192(0), front.layer2Tip); + // This is 'front' and not popFront, so the amount should not change. + assertEq(NUMBER_OPERATIONS, priorityQueue.getSize()); + assertEq(0, priorityQueue.getFirstUnprocessedPriorityTx()); + assertEq(NUMBER_OPERATIONS, priorityQueue.getTotalPriorityTxs()); + assertFalse(priorityQueue.isEmpty()); + } +} diff --git a/l1-contracts/test/foundry/unit/concrete/PriorityQueue/_PriorityQueue_Shared.t.sol b/l1-contracts/test/foundry/unit/concrete/PriorityQueue/_PriorityQueue_Shared.t.sol new file mode 100644 index 000000000..a219aea17 --- /dev/null +++ b/l1-contracts/test/foundry/unit/concrete/PriorityQueue/_PriorityQueue_Shared.t.sol @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.20; + +import {Test} from "forge-std/Test.sol"; +import {PriorityQueueTest, PriorityOperation} from "solpp/dev-contracts/test/PriorityQueueTest.sol"; + +contract PriorityQueueSharedTest is Test { + PriorityQueueTest internal priorityQueue; + + constructor() { + priorityQueue = new PriorityQueueTest(); + } + + // Pushes 'count' entries into the priority queue. + function push_mock_entries(uint count) public { + for (uint i = 0; i < count; ++i) { + PriorityOperation memory dummyOp = PriorityOperation({ + canonicalTxHash: keccak256(abi.encode(i)), + expirationTimestamp: uint64(i), + layer2Tip: uint192(i) + }); + priorityQueue.pushBack(dummyOp); + } + } +} diff --git a/l1-contracts/test/foundry/unit/concrete/TransactionValidator/ValidateL1L2Tx.t.sol b/l1-contracts/test/foundry/unit/concrete/TransactionValidator/ValidateL1L2Tx.t.sol new file mode 100644 index 000000000..9d3a35523 --- /dev/null +++ b/l1-contracts/test/foundry/unit/concrete/TransactionValidator/ValidateL1L2Tx.t.sol @@ -0,0 +1,64 @@ +pragma solidity 0.8.20; + +import {TransactionValidatorSharedTest} from "./_TransactionValidator_Shared.t.sol"; +import {IMailbox} from "solpp/zksync/interfaces/IMailbox.sol"; +import {TransactionValidator} from "solpp/zksync/libraries/TransactionValidator.sol"; + +contract ValidateL1L2TxTest is TransactionValidatorSharedTest { + function test_BasicRequestL1L2() public pure { + IMailbox.L2CanonicalTransaction memory testTx = createTestTransaction(); + testTx.gasLimit = 500000; + validateL1ToL2Transaction(testTx, 500000); + } + + function test_RevertWhen_GasLimitDoesntCoverOverhead() public { + IMailbox.L2CanonicalTransaction memory testTx = createTestTransaction(); + // The limit is so low, that it doesn't even cover the overhead + testTx.gasLimit = 0; + vm.expectRevert(bytes("my")); + validateL1ToL2Transaction(testTx, 500000); + } + + function test_RevertWhen_GasLimitHigherThanMax() public { + IMailbox.L2CanonicalTransaction memory testTx = createTestTransaction(); + // We should fail, if user asks for too much gas. + // Notice, that we subtract the transaction overhead costs from the user's gas limit + // before checking that it is below the max gas limit. + uint256 priorityTxMaxGasLimit = 500000; + testTx.gasLimit = priorityTxMaxGasLimit + 1000000; + vm.expectRevert(bytes("ui")); + validateL1ToL2Transaction(testTx, priorityTxMaxGasLimit); + } + + function test_RevertWhen_TooMuchPubdata() public { + IMailbox.L2CanonicalTransaction memory testTx = createTestTransaction(); + // We should fail, if user's transaction could output too much pubdata. + // We can allow only 99k of pubdata (otherwise we'd exceed the ethereum calldata limits). + + uint256 priorityTxMaxGasLimit = 500000; + testTx.gasLimit = priorityTxMaxGasLimit; + // So if the pubdata costs per byte is 1 - then this transaction could produce 500k of pubdata. + // (hypothetically, assuming all the gas was spent on writing). + testTx.gasPerPubdataByteLimit = 1; + vm.expectRevert(bytes("uk")); + validateL1ToL2Transaction(testTx, priorityTxMaxGasLimit); + } + + function test_RevertWhen_BelowMinimumCost() public { + IMailbox.L2CanonicalTransaction memory testTx = createTestTransaction(); + uint256 priorityTxMaxGasLimit = 500000; + testTx.gasLimit = 200000; + vm.expectRevert(bytes("up")); + validateL1ToL2Transaction(testTx, priorityTxMaxGasLimit); + } + + function test_RevertWhen_HugePubdata() public { + IMailbox.L2CanonicalTransaction memory testTx = createTestTransaction(); + uint256 priorityTxMaxGasLimit = 500000; + testTx.gasLimit = 400000; + // Setting huge pubdata limit should cause the panic. + testTx.gasPerPubdataByteLimit = type(uint256).max; + vm.expectRevert(); + validateL1ToL2Transaction(testTx, priorityTxMaxGasLimit); + } +} diff --git a/l1-contracts/test/foundry/unit/concrete/TransactionValidator/ValidateUpgradeTransaction.t.sol b/l1-contracts/test/foundry/unit/concrete/TransactionValidator/ValidateUpgradeTransaction.t.sol new file mode 100644 index 000000000..14cd4c0eb --- /dev/null +++ b/l1-contracts/test/foundry/unit/concrete/TransactionValidator/ValidateUpgradeTransaction.t.sol @@ -0,0 +1,100 @@ +pragma solidity 0.8.20; + +import {TransactionValidatorSharedTest} from "./_TransactionValidator_Shared.t.sol"; +import {IMailbox} from "solpp/zksync/interfaces/IMailbox.sol"; +import {TransactionValidator} from "solpp/zksync/libraries/TransactionValidator.sol"; + +contract ValidateUpgradeTxTest is TransactionValidatorSharedTest { + function test_BasicRequest() public pure { + IMailbox.L2CanonicalTransaction memory testTx = createUpgradeTransaction(); + TransactionValidator.validateUpgradeTransaction(testTx); + } + + function test_RevertWhen_RequestNotFromSystemContract() public { + IMailbox.L2CanonicalTransaction memory testTx = createUpgradeTransaction(); + // only system contracts (address < 2^16) are allowed to send upgrade transactions. + testTx.from = uint256(1000000000); + vm.expectRevert(bytes("ua")); + TransactionValidator.validateUpgradeTransaction(testTx); + } + + function test_RevertWhen_RequestNotToSystemContract() public { + IMailbox.L2CanonicalTransaction memory testTx = createUpgradeTransaction(); + // Now the 'to' address it too large. + testTx.to = uint256(type(uint160).max) + 100; + vm.expectRevert(bytes("ub")); + TransactionValidator.validateUpgradeTransaction(testTx); + } + + function test_RevertWhen_PaymasterIsNotZero() public { + IMailbox.L2CanonicalTransaction memory testTx = createUpgradeTransaction(); + // Paymaster must be 0 - otherwise we revert. + testTx.paymaster = 1; + vm.expectRevert(bytes("uc")); + TransactionValidator.validateUpgradeTransaction(testTx); + } + + function test_RevertWhen_ValueIsNotZero() public { + IMailbox.L2CanonicalTransaction memory testTx = createUpgradeTransaction(); + // Value must be 0 - otherwise we revert. + testTx.value = 1; + vm.expectRevert(bytes("ud")); + TransactionValidator.validateUpgradeTransaction(testTx); + } + + function test_RevertWhen_Reserved0IsNonZero() public { + IMailbox.L2CanonicalTransaction memory testTx = createUpgradeTransaction(); + // reserved 0 must be 0 - otherwise we revert. + testTx.reserved[0] = 1; + vm.expectRevert(bytes("ue")); + TransactionValidator.validateUpgradeTransaction(testTx); + } + + function test_RevertWhen_Reserved1IsTooLarge() public { + IMailbox.L2CanonicalTransaction memory testTx = createUpgradeTransaction(); + // reserved 1 must be a valid address + testTx.reserved[1] = uint256(type(uint160).max) + 100; + vm.expectRevert(bytes("uf")); + TransactionValidator.validateUpgradeTransaction(testTx); + } + + function test_RevertWhen_Reserved2IsNonZero() public { + IMailbox.L2CanonicalTransaction memory testTx = createUpgradeTransaction(); + // reserved 2 must be 0 - otherwise we revert. + testTx.reserved[2] = 1; + vm.expectRevert(bytes("ug")); + TransactionValidator.validateUpgradeTransaction(testTx); + } + + function test_RevertWhen_Reserved3IsNonZero() public { + IMailbox.L2CanonicalTransaction memory testTx = createUpgradeTransaction(); + // reserved 3 be 0 - otherwise we revert. + testTx.reserved[3] = 1; + vm.expectRevert(bytes("uo")); + TransactionValidator.validateUpgradeTransaction(testTx); + } + + function test_RevertWhen_NonZeroSignature() public { + IMailbox.L2CanonicalTransaction memory testTx = createUpgradeTransaction(); + // Signature must be 0 - otherwise we revert. + testTx.signature = bytes("hello"); + vm.expectRevert(bytes("uh")); + TransactionValidator.validateUpgradeTransaction(testTx); + } + + function test_RevertWhen_PaymasterInputNonZero() public { + IMailbox.L2CanonicalTransaction memory testTx = createUpgradeTransaction(); + // PaymasterInput must be 0 - otherwise we revert. + testTx.paymasterInput = bytes("hi"); + vm.expectRevert(bytes("ul")); + TransactionValidator.validateUpgradeTransaction(testTx); + } + + function test_RevertWhen_ReservedDynamicIsNonZero() public { + IMailbox.L2CanonicalTransaction memory testTx = createUpgradeTransaction(); + // ReservedDynamic must be 0 - otherwise we revert. + testTx.reservedDynamic = bytes("something"); + vm.expectRevert(bytes("um")); + TransactionValidator.validateUpgradeTransaction(testTx); + } +} diff --git a/l1-contracts/test/foundry/unit/concrete/TransactionValidator/_TransactionValidator_Shared.t.sol b/l1-contracts/test/foundry/unit/concrete/TransactionValidator/_TransactionValidator_Shared.t.sol new file mode 100644 index 000000000..cd728d680 --- /dev/null +++ b/l1-contracts/test/foundry/unit/concrete/TransactionValidator/_TransactionValidator_Shared.t.sol @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.20; + +import {Test} from "forge-std/Test.sol"; +import {IMailbox} from "solpp/zksync/interfaces/IMailbox.sol"; +//import {TransactionValidator} from "solpp/zksync/libraries/TransactionValidator.sol"; +import {TransactionValidator} from "cache/solpp-generated-contracts/zksync/libraries/TransactionValidator.sol"; + +contract TransactionValidatorSharedTest is Test { + constructor() {} + + function createTestTransaction() public pure returns (IMailbox.L2CanonicalTransaction memory testTx) { + testTx = IMailbox.L2CanonicalTransaction({ + txType: 0, + from: uint256(uint160(1_000_000_000)), + to: uint256(uint160(0)), + gasLimit: 500000, + gasPerPubdataByteLimit: 800, + maxFeePerGas: uint256(0), + maxPriorityFeePerGas: uint256(0), + paymaster: uint256(0), + nonce: uint256(0), + value: 0, + reserved: [uint256(0), uint256(0), uint256(0), uint256(0)], + data: new bytes(0), + signature: new bytes(0), + factoryDeps: new uint256[](0), + paymasterInput: new bytes(0), + reservedDynamic: new bytes(0) + }); + } + + function createUpgradeTransaction() public pure returns (IMailbox.L2CanonicalTransaction memory testTx) { + testTx = createTestTransaction(); + testTx.from = uint256(0x8001); + testTx.to = uint256(0x8007); + } + + function validateL1ToL2Transaction( + IMailbox.L2CanonicalTransaction memory _transaction, + uint256 _priorityTxMaxGasLimit + ) public pure { + TransactionValidator.validateL1ToL2Transaction(_transaction, abi.encode(_transaction), _priorityTxMaxGasLimit); + } +} diff --git a/l1-contracts/test/foundry/unit/concrete/UncheckedMath/UncheckedAdd.t.sol b/l1-contracts/test/foundry/unit/concrete/UncheckedMath/UncheckedAdd.t.sol new file mode 100644 index 000000000..847818a65 --- /dev/null +++ b/l1-contracts/test/foundry/unit/concrete/UncheckedMath/UncheckedAdd.t.sol @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.20; + +import {UncheckedMathTest} from "./_UncheckedMath_Shared.t.sol"; +import {UncheckedMath} from "solpp/common/libraries/UncheckedMath.sol"; + +contract UncheckedAddTest is UncheckedMathTest { + using UncheckedMath for uint256; + + function test_Add() public { + uint256 a = 1234; + uint256 b = 4321; + uint256 c = a.uncheckedAdd(b); + assertEq(c, 5555); + } + + function test_AddWithOverflow() public { + uint256 a = type(uint256).max; + uint256 b = 1; + + // uncheckedAdd does not fail + uint256 c = a.uncheckedAdd(b); + assertEq(c, 0); + + // regular addition fails with overflow + vm.expectRevert(); + a + b; + } +} diff --git a/l1-contracts/test/foundry/unit/concrete/UncheckedMath/UncheckedInc.t.sol b/l1-contracts/test/foundry/unit/concrete/UncheckedMath/UncheckedInc.t.sol new file mode 100644 index 000000000..aa1a669f7 --- /dev/null +++ b/l1-contracts/test/foundry/unit/concrete/UncheckedMath/UncheckedInc.t.sol @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.20; + +import {UncheckedMathTest} from "./_UncheckedMath_Shared.t.sol"; +import {UncheckedMath} from "solpp/common/libraries/UncheckedMath.sol"; + +contract UncheckedIncTest is UncheckedMathTest { + using UncheckedMath for uint256; + + function test_Inc() public { + uint256 a = 1234; + uint256 c = a.uncheckedInc(); + assertEq(c, 1235); + } + + function test_IncWithOverflow() public { + uint256 a = type(uint256).max; + + // uncheckedInc does not fail + uint256 c = a.uncheckedInc(); + assertEq(c, 0); + + // regular addition fails with overflow + vm.expectRevert(); + a + 1; + } +} diff --git a/l1-contracts/test/foundry/unit/concrete/UncheckedMath/_UncheckedMath_Shared.t.sol b/l1-contracts/test/foundry/unit/concrete/UncheckedMath/_UncheckedMath_Shared.t.sol new file mode 100644 index 000000000..badc0233e --- /dev/null +++ b/l1-contracts/test/foundry/unit/concrete/UncheckedMath/_UncheckedMath_Shared.t.sol @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.20; + +import {Test} from "forge-std/Test.sol"; + +contract UncheckedMathTest is Test {} diff --git a/l1-contracts/test/foundry/unit/concrete/Verifier/Verifier.t.sol b/l1-contracts/test/foundry/unit/concrete/Verifier/Verifier.t.sol new file mode 100644 index 000000000..94dd208a7 --- /dev/null +++ b/l1-contracts/test/foundry/unit/concrete/Verifier/Verifier.t.sol @@ -0,0 +1,163 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.20; + +import {Test} from "forge-std/Test.sol"; +import {VerifierTest} from "solpp/dev-contracts/test/VerifierTest.sol"; +import {Verifier} from "solpp/zksync/Verifier.sol"; + +contract VerifierTestTest is Test { + uint256 Q_MOD = 21888242871839275222246405745257275088696311157297823662689037894645226208583; + uint256 R_MOD = 21888242871839275222246405745257275088548364400416034343698204186575808495617; + + uint256[] public publicInputs; + uint256[] public serializedProof; + uint256[] public recursiveAggregationInput; + + Verifier public verifier; + + function setUp() public virtual { + publicInputs.push(17257057577815541751225964212897374444694342989384539141520877492729); + + serializedProof.push(10032255692304426541958487424837706541667730769782503366592797609781788557424); + serializedProof.push(11856023086316274558845067687080284266010851703055534566998849536424959073766); + serializedProof.push(1946976494418613232642071265529572704802622739887191787991738703483400525159); + serializedProof.push(1328106069458824013351862477593422369726189688844441245167676630500797673929); + serializedProof.push(15488976127650523079605218040232167291115155239002840072043251018873550258833); + serializedProof.push(4352460820258659596860226525221943504756149602617718032378962471842121872064); + serializedProof.push(10499239305859992443759785453270906003243074359959242371675950941500942473773); + serializedProof.push(21347231097799123231227724221565041889687686131480556177475242020711996173235); + serializedProof.push(21448274562455512652922184359722637546669181231038098300951155169465175447933); + serializedProof.push(5224615512030263722410009061780530125927659699046094954022444377569738464640); + serializedProof.push(457781538876079938778845275495204146302569607395268192839148474821758081582); + serializedProof.push(18861735728246155975127314860333796285284072325207684293054713266899263027595); + serializedProof.push(16303944945368742900183889655415585360236645961122617249176044814801835577336); + serializedProof.push(13035945439947210396602249585896632733250124877036427100939804737514358838409); + serializedProof.push(5344210729159253547334947774998425118220137275601995670629358314205854915831); + serializedProof.push(5798533246034358556434877465898581616792677631188370022078168611592512620805); + serializedProof.push(17389657286129893116489015409587246992530648956814855147744210777822507444908); + serializedProof.push(2287244647342394712608648573347732257083870498255199596324312699868511383792); + serializedProof.push(4008043766112513713076111464601725311991199944328610186851424132679188418647); + serializedProof.push(1192776719848445147414966176395169615865534126881763324071908049917030138759); + serializedProof.push(21297794452895123333253856666749932934399762330444876027734824957603009458926); + serializedProof.push(17125994169200693606182326100834606153690416627082476471630567824088261322122); + serializedProof.push(13696978282153979214307382954559709118587582183649354744253374201589715565327); + serializedProof.push(19885518441500677676836488338931187143852666523909650686513498826535451677070); + serializedProof.push(1205434280320863211046275554464591162919269140938371417889032165323835178587); + serializedProof.push(17633172995805911347980792921300006225132501482343225088847242025756974009163); + serializedProof.push(16438080406761371143473961144300947125022788905488819913014533292593141026205); + serializedProof.push(5069081552536259237104332491140391551180511112980430307676595350165020188468); + serializedProof.push(21217317205917200275887696442048162383709998732382676029165079037795626916156); + serializedProof.push(19474466610515117278975027596198570980840609656738255347763182823792179771539); + serializedProof.push(9744176601826774967534277982058590459006781888895542911226406188087317156914); + serializedProof.push(13171230402193025939763214267878900142876558410430734782028402821166810894141); + serializedProof.push(11775403006142607980192261369108550982244126464568678337528680604943636677964); + serializedProof.push(6903612341636669639883555213872265187697278660090786759295896380793937349335); + serializedProof.push(10197105415769290664169006387603164525075746474380469980600306405504981186043); + serializedProof.push(10143152486514437388737642096964118742712576889537781270260677795662183637771); + serializedProof.push(7662095231333811948165764727904932118187491073896301295018543320499906824310); + serializedProof.push(929422796511992741418500336817719055655694499787310043166783539202506987065); + serializedProof.push(13837024938095280064325737989251964639823205065380219552242839155123572433059); + serializedProof.push(11738888513780631372636453609299803548810759208935038785934252961078387526204); + serializedProof.push(16528875312985292109940444015943812939751717229020635856725059316776921546668); + serializedProof.push(17525167117689648878398809303253004706004801107861280044640132822626802938868); + serializedProof.push(7419167499813234488108910149511390953153207250610705609008080038658070088540); + serializedProof.push(11628425014048216611195735618191126626331446742771562481735017471681943914146); + + verifier = new VerifierTest(); + } + + function testShouldVerify() public view { + bool success = verifier.verify(publicInputs, serializedProof, recursiveAggregationInput); + assert(success); + } + + function testShouldVerifyWithDirtyBits() public view { + uint256[] memory newPublicInputs = publicInputs; + newPublicInputs[0] += uint256(bytes32(0xe000000000000000000000000000000000000000000000000000000000000000)); + + bool success = verifier.verify(newPublicInputs, serializedProof, recursiveAggregationInput); + assert(success); + } + + function testEllipticCurvePointsOverModulo() public view { + uint256[] memory newSerializedProof = serializedProof; + newSerializedProof[0] += Q_MOD; + newSerializedProof[1] += Q_MOD; + newSerializedProof[1] += Q_MOD; + + bool success = verifier.verify(publicInputs, newSerializedProof, recursiveAggregationInput); + assert(success); + } + + function testFrOverModulo() public view { + uint256[] memory newSerializedProof = serializedProof; + newSerializedProof[22] += R_MOD; + + bool success = verifier.verify(publicInputs, newSerializedProof, recursiveAggregationInput); + assert(success); + } + + function testMoreThanOnePublicInput_shouldRevert() public { + uint256[] memory newPublicInputs = new uint256[](2); + newPublicInputs[0] = publicInputs[0]; + newPublicInputs[1] = publicInputs[0]; + + vm.expectRevert(bytes("loadProof: Proof is invalid")); + verifier.verify(newPublicInputs, serializedProof, recursiveAggregationInput); + } + + function testEmptyPublicInput_shouldRevert() public { + uint256[] memory newPublicInputs; + + vm.expectRevert(bytes("loadProof: Proof is invalid")); + verifier.verify(newPublicInputs, serializedProof, recursiveAggregationInput); + } + + function testMoreThan44WordsProof_shouldRevert() public { + uint256[] memory newSerializedProof = new uint256[](serializedProof.length + 1); + + for (uint256 i = 0; i < serializedProof.length; i++) { + newSerializedProof[i] = serializedProof[i]; + } + newSerializedProof[newSerializedProof.length - 1] = serializedProof[serializedProof.length - 1]; + + vm.expectRevert(bytes("loadProof: Proof is invalid")); + verifier.verify(publicInputs, newSerializedProof, recursiveAggregationInput); + } + + function testEmptyProof_shouldRevert() public { + uint256[] memory newSerializedProof; + + vm.expectRevert(bytes("loadProof: Proof is invalid")); + verifier.verify(publicInputs, newSerializedProof, recursiveAggregationInput); + } + + function testNotEmptyRecursiveAggregationInput_shouldRevert() public { + uint256[] memory newRecursiveAggregationInput = publicInputs; + + vm.expectRevert(bytes("loadProof: Proof is invalid")); + verifier.verify(publicInputs, serializedProof, newRecursiveAggregationInput); + } + + function testEllipticCurvePointAtInfinity_shouldRevert() public { + uint256[] memory newSerializedProof = serializedProof; + newSerializedProof[0] = 0; + newSerializedProof[1] = 0; + + vm.expectRevert(bytes("loadProof: Proof is invalid")); + verifier.verify(publicInputs, newSerializedProof, recursiveAggregationInput); + } + + function testInvalidPublicInput_shouldRevert() public { + uint256[] memory newPublicInputs = publicInputs; + newPublicInputs[0] = 0; + + vm.expectRevert(bytes("invalid quotient evaluation")); + verifier.verify(newPublicInputs, serializedProof, recursiveAggregationInput); + } + + function testVerificationKeyHash() public virtual { + bytes32 verificationKeyHash = verifier.verificationKeyHash(); + assertEq(verificationKeyHash, 0x6625fa96781746787b58306d414b1e25bd706d37d883a9b3acf57b2bd5e0de52); + } +} diff --git a/l1-contracts/test/foundry/unit/concrete/Verifier/VerifierRecursive.t.sol b/l1-contracts/test/foundry/unit/concrete/Verifier/VerifierRecursive.t.sol new file mode 100644 index 000000000..480bf2c73 --- /dev/null +++ b/l1-contracts/test/foundry/unit/concrete/Verifier/VerifierRecursive.t.sol @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.20; + +import {VerifierTestTest} from "./Verifier.t.sol"; +import {VerifierRecursiveTest} from "solpp/dev-contracts/test/VerifierRecursiveTest.sol"; + +contract VerifierRecursiveTestTest is VerifierTestTest { + function setUp() public override { + super.setUp(); + + recursiveAggregationInput.push(2257920826825449939414463854743099397427742128922725774525544832270890253504); + recursiveAggregationInput.push(9091218701914748532331969127001446391756173432977615061129552313204917562530); + recursiveAggregationInput.push(16188304989094043810949359833767911976672882599560690320245309499206765021563); + recursiveAggregationInput.push(3201093556796962656759050531176732990872300033146738631772984017549903765305); + + verifier = new VerifierRecursiveTest(); + } + + function testMoreThan4WordsRecursiveInput_shouldRevert() public { + uint256[] memory newRecursiveAggregationInput = new uint256[](recursiveAggregationInput.length + 1); + + for (uint256 i = 0; i < recursiveAggregationInput.length; i++) { + newRecursiveAggregationInput[i] = recursiveAggregationInput[i]; + } + newRecursiveAggregationInput[newRecursiveAggregationInput.length - 1] = recursiveAggregationInput[ + recursiveAggregationInput.length - 1 + ]; + + vm.expectRevert(bytes("loadProof: Proof is invalid")); + verifier.verify(publicInputs, serializedProof, newRecursiveAggregationInput); + } + + function testEmptyRecursiveInput_shouldRevert() public { + uint256[] memory newRecursiveAggregationInput; + + vm.expectRevert(bytes("loadProof: Proof is invalid")); + verifier.verify(publicInputs, serializedProof, newRecursiveAggregationInput); + } + + function testInvalidRecursiveInput_shouldRevert() public { + uint256[] memory newRecursiveAggregationInput = new uint256[](4); + newRecursiveAggregationInput[0] = 1; + newRecursiveAggregationInput[1] = 2; + newRecursiveAggregationInput[2] = 1; + newRecursiveAggregationInput[3] = 2; + + vm.expectRevert(bytes("finalPairing: pairing failure")); + verifier.verify(publicInputs, serializedProof, newRecursiveAggregationInput); + } + + function testVerificationKeyHash() public override { + bytes32 verificationKeyHash = verifier.verificationKeyHash(); + assertEq(verificationKeyHash, 0x88b3ddc4ed85974c7e14297dcad4097169440305c05fdb6441ca8dfd77cd7fa7); + } +} diff --git a/l1-contracts/test/unit_tests/merkle_test.spec.ts b/l1-contracts/test/unit_tests/merkle_test.spec.ts deleted file mode 100644 index 9ba239317..000000000 --- a/l1-contracts/test/unit_tests/merkle_test.spec.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { expect } from "chai"; -import * as hardhat from "hardhat"; -import type { MerkleTest } from "../../typechain"; -import { MerkleTestFactory } from "../../typechain"; -import { MerkleTree } from "merkletreejs"; -import { getCallRevertReason } from "./utils"; -import * as ethers from "ethers"; - -describe("Merkle lib tests", function () { - let merkleTest: MerkleTest; - - before(async () => { - const contractFactory = await hardhat.ethers.getContractFactory("MerkleTest"); - const contract = await contractFactory.deploy(); - merkleTest = MerkleTestFactory.connect(contract.address, contract.signer); - }); - - describe("should calculate root correctly", function () { - let elements; - let merkleTree; - - before(async () => { - elements = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" - .split("") - .map((val) => ethers.utils.toUtf8Bytes(val)); - merkleTree = new MerkleTree(elements, ethers.utils.keccak256, { hashLeaves: true }); - }); - - it("first element", async () => { - const index = 0; - const leaf = ethers.utils.keccak256(elements[index]); - const proof = merkleTree.getHexProof(leaf, index); - - const rootFromContract = await merkleTest.calculateRoot(proof, index, leaf); - expect(rootFromContract).to.equal(merkleTree.getHexRoot()); - }); - - it("middle element", async () => { - const index = Math.ceil(elements.length / 2); - const leaf = ethers.utils.keccak256(elements[index]); - const proof = merkleTree.getHexProof(leaf, index); - - const rootFromContract = await merkleTest.calculateRoot(proof, index, leaf); - expect(rootFromContract).to.equal(merkleTree.getHexRoot()); - }); - - it("last element", async () => { - const index = elements.length - 1; - const leaf = ethers.utils.keccak256(elements[index]); - const proof = merkleTree.getHexProof(leaf, index); - - const rootFromContract = await merkleTest.calculateRoot(proof, index, leaf); - expect(rootFromContract).to.equal(merkleTree.getHexRoot()); - }); - }); - - it("should fail trying calculate root with empty path", async () => { - const revertReason = await getCallRevertReason(merkleTest.calculateRoot([], 0, ethers.constants.HashZero)); - expect(revertReason).equal("xc"); - }); - - it("should fail trying calculate root with too big leaf index", async () => { - const bigIndex = ethers.BigNumber.from(2).pow(255); - const revertReason = await getCallRevertReason( - merkleTest.calculateRoot([ethers.constants.HashZero], bigIndex, ethers.constants.HashZero) - ); - expect(revertReason).equal("px"); - }); -}); diff --git a/l1-contracts/test/unit_tests/priority_queue_test.spec.ts b/l1-contracts/test/unit_tests/priority_queue_test.spec.ts deleted file mode 100644 index a59c1e2f1..000000000 --- a/l1-contracts/test/unit_tests/priority_queue_test.spec.ts +++ /dev/null @@ -1,133 +0,0 @@ -import { expect } from "chai"; -import * as hardhat from "hardhat"; -import { ethers } from "hardhat"; -import type { PriorityQueueTest } from "../../typechain"; -import { PriorityQueueTestFactory } from "../../typechain"; -import { getCallRevertReason } from "./utils"; - -describe("Priority queue tests", function () { - let priorityQueueTest: PriorityQueueTest; - const queue = []; - - before(async () => { - const contractFactory = await hardhat.ethers.getContractFactory("PriorityQueueTest"); - const contract = await contractFactory.deploy(); - priorityQueueTest = PriorityQueueTestFactory.connect(contract.address, contract.signer); - }); - - describe("on empty queue", function () { - it("getSize", async () => { - const size = await priorityQueueTest.getSize(); - expect(size).equal(0); - }); - - it("getFirstUnprocessedPriorityTx", async () => { - const firstUnprocessedTx = await priorityQueueTest.getFirstUnprocessedPriorityTx(); - expect(firstUnprocessedTx).equal(0); - }); - - it("getTotalPriorityTxs", async () => { - const totalPriorityTxs = await priorityQueueTest.getTotalPriorityTxs(); - expect(totalPriorityTxs).equal(0); - }); - - it("isEmpty", async () => { - const isEmpty = await priorityQueueTest.isEmpty(); - expect(isEmpty).equal(true); - }); - - it("failed to get front", async () => { - const revertReason = await getCallRevertReason(priorityQueueTest.front()); - expect(revertReason).equal("D"); - }); - - it("failed to pop", async () => { - const revertReason = await getCallRevertReason(priorityQueueTest.popFront()); - expect(revertReason).equal("s"); - }); - }); - - describe("push operations", function () { - const NUMBER_OPERATIONS = 10; - - before(async () => { - for (let i = 0; i < NUMBER_OPERATIONS; ++i) { - const dummyOp = { canonicalTxHash: ethers.constants.HashZero, expirationTimestamp: i, layer2Tip: i }; - queue.push(dummyOp); - await priorityQueueTest.pushBack(dummyOp); - } - }); - - it("front", async () => { - const frontElement = await priorityQueueTest.front(); - - expect(frontElement.canonicalTxHash).equal(queue[0].canonicalTxHash); - expect(frontElement.expirationTimestamp).equal(queue[0].expirationTimestamp); - expect(frontElement.layer2Tip).equal(queue[0].layer2Tip); - }); - - it("getSize", async () => { - const size = await priorityQueueTest.getSize(); - expect(size).equal(queue.length); - }); - - it("getFirstUnprocessedPriorityTx", async () => { - const firstUnprocessedTx = await priorityQueueTest.getFirstUnprocessedPriorityTx(); - expect(firstUnprocessedTx).equal(0); - }); - - it("getTotalPriorityTxs", async () => { - const totalPriorityTxs = await priorityQueueTest.getTotalPriorityTxs(); - expect(totalPriorityTxs).equal(queue.length); - }); - - it("isEmpty", async () => { - const isEmpty = await priorityQueueTest.isEmpty(); - expect(isEmpty).equal(false); - }); - }); - - describe("pop operations", function () { - const NUMBER_OPERATIONS = 4; - - before(async () => { - for (let i = 0; i < NUMBER_OPERATIONS; ++i) { - const frontElement = await priorityQueueTest.front(); - expect(frontElement.canonicalTxHash).equal(queue[0].canonicalTxHash); - expect(frontElement.expirationTimestamp).equal(queue[0].expirationTimestamp); - expect(frontElement.layer2Tip).equal(queue[0].layer2Tip); - - await priorityQueueTest.popFront(); - queue.shift(); - } - }); - - it("front", async () => { - const frontElement = await priorityQueueTest.front(); - - expect(frontElement.canonicalTxHash).equal(queue[0].canonicalTxHash); - expect(frontElement.expirationTimestamp).equal(queue[0].expirationTimestamp); - expect(frontElement.layer2Tip).equal(queue[0].layer2Tip); - }); - - it("getSize", async () => { - const size = await priorityQueueTest.getSize(); - expect(size).equal(queue.length); - }); - - it("getFirstUnprocessedPriorityTx", async () => { - const firstUnprocessedTx = await priorityQueueTest.getFirstUnprocessedPriorityTx(); - expect(firstUnprocessedTx).equal(NUMBER_OPERATIONS); - }); - - it("getTotalPriorityTxs", async () => { - const totalPriorityTxs = await priorityQueueTest.getTotalPriorityTxs(); - expect(totalPriorityTxs).equal(queue.length + NUMBER_OPERATIONS); - }); - - it("isEmpty", async () => { - const isEmpty = await priorityQueueTest.isEmpty(); - expect(isEmpty).equal(false); - }); - }); -}); diff --git a/l1-contracts/test/unit_tests/transaction_validator_test.spec.ts b/l1-contracts/test/unit_tests/transaction_validator_test.spec.ts deleted file mode 100644 index a8683c368..000000000 --- a/l1-contracts/test/unit_tests/transaction_validator_test.spec.ts +++ /dev/null @@ -1,213 +0,0 @@ -import { expect } from "chai"; -import * as hardhat from "hardhat"; -import type { TransactionValidatorTest } from "../../typechain"; -import { TransactionValidatorTestFactory } from "../../typechain"; -import { getCallRevertReason } from "./utils"; -import * as ethers from "ethers"; - -describe("TransactionValidator tests", function () { - let tester: TransactionValidatorTest; - before(async () => { - const testerFactory = await hardhat.ethers.getContractFactory("TransactionValidatorTest"); - const testerContract = await testerFactory.deploy(); - tester = TransactionValidatorTestFactory.connect(testerContract.address, testerContract.signer); - }); - - describe("validateL1ToL2Transaction", function () { - it("Should not revert when all parameters are valid", async () => { - await tester.validateL1ToL2Transaction(createTestTransaction({}), 500000); - }); - - it("Should revert when provided gas limit doesnt cover transaction overhead", async () => { - const result = await getCallRevertReason( - tester.validateL1ToL2Transaction( - createTestTransaction({ - gasLimit: 0, - }), - 500000 - ) - ); - expect(result).equal("my"); - }); - - it("Should revert when needed gas is higher than the max", async () => { - const result = await getCallRevertReason(tester.validateL1ToL2Transaction(createTestTransaction({}), 0)); - expect(result).equal("ui"); - }); - - it("Should revert when transaction can output more pubdata than processable", async () => { - const result = await getCallRevertReason( - tester.validateL1ToL2Transaction( - createTestTransaction({ - gasPerPubdataByteLimit: 1, - }), - 500000 - ) - ); - expect(result).equal("uk"); - }); - - it("Should revert when transaction gas doesnt pay the minimum costs", async () => { - const result = await getCallRevertReason( - tester.validateL1ToL2Transaction( - createTestTransaction({ - gasLimit: 200000, - }), - 500000 - ) - ); - expect(result).equal("up"); - }); - }); - - describe("validateUpgradeTransaction", function () { - it("Should not revert when all parameters are valid", async () => { - await tester.validateUpgradeTransaction(createTestTransaction({})); - }); - - it("Should revert when from is too large", async () => { - const result = await getCallRevertReason( - tester.validateUpgradeTransaction( - createTestTransaction({ - from: ethers.BigNumber.from(2).pow(16), - }) - ) - ); - expect(result).equal("ua"); - }); - - it("Should revert when to is too large", async () => { - const result = await getCallRevertReason( - tester.validateUpgradeTransaction( - createTestTransaction({ - to: ethers.BigNumber.from(2).pow(161), - }) - ) - ); - expect(result).equal("ub"); - }); - - it("Should revert when paymaster is non-zero", async () => { - const result = await getCallRevertReason( - tester.validateUpgradeTransaction( - createTestTransaction({ - paymaster: 1, - }) - ) - ); - expect(result).equal("uc"); - }); - - it("Should revert when value is non-zero", async () => { - const result = await getCallRevertReason( - tester.validateUpgradeTransaction( - createTestTransaction({ - value: 1, - }) - ) - ); - expect(result).equal("ud"); - }); - - it("Should revert when reserved[0] is non-zero", async () => { - const result = await getCallRevertReason( - tester.validateUpgradeTransaction( - createTestTransaction({ - reserved: [1, 0, 0, 0], - }) - ) - ); - expect(result).equal("ue"); - }); - - it("Should revert when reserved[1] is too large", async () => { - const result = await getCallRevertReason( - tester.validateUpgradeTransaction( - createTestTransaction({ - reserved: [0, ethers.BigNumber.from(2).pow(161), 0, 0], - }) - ) - ); - expect(result).equal("uf"); - }); - - it("Should revert when reserved[2] is non-zero", async () => { - const result = await getCallRevertReason( - tester.validateUpgradeTransaction( - createTestTransaction({ - reserved: [0, 0, 1, 0], - }) - ) - ); - expect(result).equal("ug"); - }); - - it("Should revert when reserved[3] is non-zero", async () => { - const result = await getCallRevertReason( - tester.validateUpgradeTransaction( - createTestTransaction({ - reserved: [0, 0, 0, 1], - }) - ) - ); - expect(result).equal("uo"); - }); - - it("Should revert when signature has non-zero length", async () => { - const result = await getCallRevertReason( - tester.validateUpgradeTransaction( - createTestTransaction({ - signature: "0xaa", - }) - ) - ); - expect(result).equal("uh"); - }); - - it("Should revert when paymaster input has non-zero length", async () => { - const result = await getCallRevertReason( - tester.validateUpgradeTransaction( - createTestTransaction({ - paymasterInput: "0xaa", - }) - ) - ); - expect(result).equal("ul"); - }); - - it("Should revert when reserved dynamic field has non-zero length", async () => { - const result = await getCallRevertReason( - tester.validateUpgradeTransaction( - createTestTransaction({ - reservedDynamic: "0xaa", - }) - ) - ); - expect(result).equal("um"); - }); - }); -}); - -function createTestTransaction(overrides) { - return Object.assign( - { - txType: 0, - from: ethers.BigNumber.from(2).pow(16).sub(1), - to: 0, - gasLimit: 500000, - gasPerPubdataByteLimit: 800, - maxFeePerGas: 0, - maxPriorityFeePerGas: 0, - paymaster: 0, - nonce: 0, - value: 0, - reserved: [0, 0, 0, 0], - data: "0x", - signature: "0x", - factoryDeps: [], - paymasterInput: "0x", - reservedDynamic: "0x", - }, - overrides - ); -} diff --git a/l1-contracts/test/unit_tests/verifier.spec.ts b/l1-contracts/test/unit_tests/verifier.spec.ts deleted file mode 100644 index 437c168b9..000000000 --- a/l1-contracts/test/unit_tests/verifier.spec.ts +++ /dev/null @@ -1,397 +0,0 @@ -import * as hardhat from "hardhat"; -import { expect } from "chai"; -import type { VerifierTest, VerifierRecursiveTest } from "../../typechain"; -import { VerifierTestFactory } from "../../typechain"; -import { getCallRevertReason } from "./utils"; -import { ethers } from "hardhat"; - -describe("Verifier test", function () { - const Q_MOD = "21888242871839275222246405745257275088696311157297823662689037894645226208583"; - const R_MOD = "21888242871839275222246405745257275088548364400416034343698204186575808495617"; - - const PROOF = { - publicInputs: ["0xa3dd954bb76c1474c1a04f04870cc75bcaf66ec23c0303c87fb119f9"], - serializedProof: [ - "0x162e0e35310fa1265df0051490fad590e875a98b4e7781ce1bb2698887e24070", - "0x1a3645718b688a382a00b99059f9488daf624d04ceb39b5553f0a1a0d508dde6", - "0x44df31be22763cde0700cc784f70758b944096a11c9b32bfb4f559d9b6a9567", - "0x2efae700419dd3fa0bebf5404efef2f3b5f8f2288c595ec219a05607e9971c9", - "0x223e7327348fd30effc617ee9fa7e28117869f149719cf93c20788cb78adc291", - "0x99f67d073880787c73d54bc2509c1611ac6f48fbe3b5214b4dc2f3cb3a572c0", - "0x17365bde1bbcd62561764ddd8b2d562edbe1c07519cd23f03831b694c6665a2d", - "0x2f321ac8e18ab998f8fe370f3b5114598881798ccc6eac24d7f4161c15fdabb3", - "0x2f6b4b0f4973f2f6e2fa5ecd34602b20b56f0e4fb551b011af96e555fdc1197d", - "0xb8d070fec07e8467425605015acba755f54db7f566c6704818408d927419d80", - "0x103185cff27eef6e8090373749a8065129fcc93482bd6ea4db1808725b6da2e", - "0x29b35d35c22deda2ac9dd56a9f6a145871b1b6557e165296f804297160d5f98b", - "0x240bb4b0b7e30e71e8af2d908e72bf47b6496aab1e1f7cb32f2604d79f76cff8", - "0x1cd2156a0f0c1944a8a3359618ff978b27eb42075c667960817be624ce161489", - "0xbd0b75112591ab1b4a6a3e03fb76368419b78e4b95ee773b8ef5e7848695cf7", - "0xcd1da7fcfc27d2d9e9743e80951694995b162298d4109428fcf1c9a90f24905", - "0x2672327da3fdec6c58e8a0d33ca94e059da0787e9221a2a0ac412692cc962aac", - "0x50e88db23f7582691a0fb7e5c95dd713e54188833fe1d241e3e32a98dfeb0f0", - "0x8dc78ede51774238b0984b02ac7fcf8b0a8dfcb6ca733b90c6b44aac4551057", - "0x2a3167374e2d54e47ce865ef222346adf7a27d4174820a637cf656899238387", - "0x2f161fddcebb9ed8740c14d3a782efcf6f0ad069371194f87bcc04f9e9baf2ee", - "0x25dcf81d1721eab45e86ccfee579eaa4e54a4a80a19edf784f24cc1ee831e58a", - "0x1e483708e664ced677568d93b3b4f505e9d2968f802e04b31873f7d8f635fb0f", - "0x2bf6cdf920d353ba8bda932b72bf6ff6a93aa831274a5dc3ea6ea647a446d18e", - "0x2aa406a77d9143221165e066adfcc9281b9c90afdcee4336eda87f85d2bfe5b", - "0x26fc05b152609664e624a233e52e12252a0cae9d2a86a36717300063faca4b4b", - "0x24579fb180a63e5594644f4726c5af6d091aee4ee64c2c2a37d98f646a9c8d9d", - "0xb34ff9cbae3a9afe40e80a46e7d1419380e210a0e9595f61eb3a300aaef9f34", - "0x2ee89372d00fd0e32a46d513f7a80a1ae64302f33bc4b100384327a443c0193c", - "0x2b0e285154aef9e8af0777190947379df37da05cf342897bf1de1bc40e497893", - "0x158b022dd94b2c5c44994a5be28b2f570f1187277430ed9307517fa0c830d432", - "0x1d1ea6f83308f30e544948e221d6b313367eccfe54ec05dfa757f023b5758f3d", - "0x1a08a4549273627eadafe47379be8e997306f5b9567618b38c93a0d58eb6c54c", - "0xf434e5d987974afdd7f45a0f84fb800ecbbcdf2eeb302e415371e1d08ba4ad7", - "0x168b5b6d46176887125f13423384b8e8dd4fd947aac832d8d15b87865580b5fb", - "0x166cd223e74511332e2df4e7ad7a82c3871ed0305a5708521702c5e62e11a30b", - "0x10f0979b9797e30f8fe15539518c7f4dfc98c7acb1490da60088b6ff908a4876", - "0x20e08df88bbafc9a810fa8e2324c36b5513134477207763849ed4a0b6bd9639", - "0x1e977a84137396a3cfb17565ecfb5b60dffb242c7aab4afecaa45ebd2c83e0a3", - "0x19f3f9b6c6868a0e2a7453ff8949323715817869f8a25075308aa34a50c1ca3c", - "0x248b030bbfab25516cca23e7937d4b3b46967292ef6dfd3df25fcfe289d53fac", - "0x26bee4a0a5c8b76caa6b73172fa7760bd634c28d2c2384335b74f5d18e3933f4", - "0x106719993b9dacbe46b17f4e896c0c9c116d226c50afe2256dca1e81cd510b5c", - "0x19b5748fd961f755dd3c713d09014bd12adbb739fa1d2160067a312780a146a2", - ], - recursiveAggregationInput: [], - }; - let verifier: VerifierTest; - - before(async function () { - const verifierFactory = await hardhat.ethers.getContractFactory("VerifierTest"); - const verifierContract = await verifierFactory.deploy(); - verifier = VerifierTestFactory.connect(verifierContract.address, verifierContract.signer); - }); - - it("Should verify proof", async () => { - // Call the verifier directly (though the call, not static call) to add the save the consumed gas into the statistic. - const calldata = verifier.interface.encodeFunctionData("verify", [ - PROOF.publicInputs, - PROOF.serializedProof, - PROOF.recursiveAggregationInput, - ]); - await verifier.fallback({ data: calldata }); - - // Check that proof is verified - const result = await verifier.verify(PROOF.publicInputs, PROOF.serializedProof, PROOF.recursiveAggregationInput); - expect(result, "proof verification failed").true; - }); - - describe("Should verify valid proof with fields values in non standard format", function () { - it("Public input with dirty bits over Fr mask", async () => { - const validProof = JSON.parse(JSON.stringify(PROOF)); - // Fill dirty bits - validProof.publicInputs[0] = ethers.BigNumber.from(validProof.publicInputs[0]) - .add("0xe000000000000000000000000000000000000000000000000000000000000000") - .toHexString(); - const result = await verifier.verify( - validProof.publicInputs, - validProof.serializedProof, - validProof.recursiveAggregationInput - ); - expect(result, "proof verification failed").true; - }); - - it("Elliptic curve points over modulo", async () => { - const validProof = JSON.parse(JSON.stringify(PROOF)); - // Add modulo to points - validProof.serializedProof[0] = ethers.BigNumber.from(validProof.serializedProof[0]).add(Q_MOD); - validProof.serializedProof[1] = ethers.BigNumber.from(validProof.serializedProof[1]).add(Q_MOD).add(Q_MOD); - const result = await verifier.verify( - validProof.publicInputs, - validProof.serializedProof, - validProof.recursiveAggregationInput - ); - expect(result, "proof verification failed").true; - }); - - it("Fr over modulo", async () => { - const validProof = JSON.parse(JSON.stringify(PROOF)); - // Add modulo to number - validProof.serializedProof[22] = ethers.BigNumber.from(validProof.serializedProof[22]).add(R_MOD); - const result = await verifier.verify( - validProof.publicInputs, - validProof.serializedProof, - validProof.recursiveAggregationInput - ); - expect(result, "proof verification failed").true; - }); - }); - - describe("Should revert on invalid input", function () { - it("More than 1 public inputs", async () => { - const invalidProof = JSON.parse(JSON.stringify(PROOF)); - // Add one more public input to proof - invalidProof.publicInputs.push(invalidProof.publicInputs[0]); - const revertReason = await getCallRevertReason( - verifier.verify(invalidProof.publicInputs, invalidProof.serializedProof, invalidProof.recursiveAggregationInput) - ); - expect(revertReason).equal("loadProof: Proof is invalid"); - }); - - it("Empty public inputs", async () => { - const revertReason = await getCallRevertReason( - verifier.verify([], PROOF.serializedProof, PROOF.recursiveAggregationInput) - ); - expect(revertReason).equal("loadProof: Proof is invalid"); - }); - - it("More than 44 words for proof", async () => { - const invalidProof = JSON.parse(JSON.stringify(PROOF)); - // Add one more "serialized proof" input - invalidProof.serializedProof.push(invalidProof.serializedProof[0]); - const revertReason = await getCallRevertReason( - verifier.verify(invalidProof.publicInputs, invalidProof.serializedProof, invalidProof.recursiveAggregationInput) - ); - expect(revertReason).equal("loadProof: Proof is invalid"); - }); - - it("Empty serialized proof", async () => { - const revertReason = await getCallRevertReason( - verifier.verify(PROOF.publicInputs, [], PROOF.recursiveAggregationInput) - ); - expect(revertReason).equal("loadProof: Proof is invalid"); - }); - - it("Not empty recursive aggregation input", async () => { - const invalidProof = JSON.parse(JSON.stringify(PROOF)); - // Add one more "recursive aggregation input" value - invalidProof.recursiveAggregationInput.push(invalidProof.publicInputs[0]); - const revertReason = await getCallRevertReason( - verifier.verify(invalidProof.publicInputs, invalidProof.serializedProof, invalidProof.recursiveAggregationInput) - ); - expect(revertReason).equal("loadProof: Proof is invalid"); - }); - - it("Elliptic curve point at infinity", async () => { - const invalidProof = JSON.parse(JSON.stringify(PROOF)); - // Change first point to point at infinity (encode as (0, 0) on EVM) - invalidProof.serializedProof[0] = ethers.constants.HashZero; - invalidProof.serializedProof[1] = ethers.constants.HashZero; - const revertReason = await getCallRevertReason( - verifier.verify(invalidProof.publicInputs, invalidProof.serializedProof, invalidProof.recursiveAggregationInput) - ); - expect(revertReason).equal("loadProof: Proof is invalid"); - }); - }); - - it("Should failed with invalid public input", async () => { - const revertReason = await getCallRevertReason( - verifier.verify([ethers.constants.HashZero], PROOF.serializedProof, PROOF.recursiveAggregationInput) - ); - expect(revertReason).equal("invalid quotient evaluation"); - }); - - it("Should return correct Verification key hash", async () => { - const vksHash = await verifier.verificationKeyHash(); - expect(vksHash).equal("0x6625fa96781746787b58306d414b1e25bd706d37d883a9b3acf57b2bd5e0de52"); - }); -}); - -describe("Verifier with recursive part test", function () { - const Q_MOD = "21888242871839275222246405745257275088696311157297823662689037894645226208583"; - const R_MOD = "21888242871839275222246405745257275088548364400416034343698204186575808495617"; - - const PROOF = { - publicInputs: ["0xa3dd954bb76c1474c1a04f04870cc75bcaf66ec23c0303c87fb119f9"], - serializedProof: [ - "0x162e0e35310fa1265df0051490fad590e875a98b4e7781ce1bb2698887e24070", - "0x1a3645718b688a382a00b99059f9488daf624d04ceb39b5553f0a1a0d508dde6", - "0x44df31be22763cde0700cc784f70758b944096a11c9b32bfb4f559d9b6a9567", - "0x2efae700419dd3fa0bebf5404efef2f3b5f8f2288c595ec219a05607e9971c9", - "0x223e7327348fd30effc617ee9fa7e28117869f149719cf93c20788cb78adc291", - "0x99f67d073880787c73d54bc2509c1611ac6f48fbe3b5214b4dc2f3cb3a572c0", - "0x17365bde1bbcd62561764ddd8b2d562edbe1c07519cd23f03831b694c6665a2d", - "0x2f321ac8e18ab998f8fe370f3b5114598881798ccc6eac24d7f4161c15fdabb3", - "0x2f6b4b0f4973f2f6e2fa5ecd34602b20b56f0e4fb551b011af96e555fdc1197d", - "0xb8d070fec07e8467425605015acba755f54db7f566c6704818408d927419d80", - "0x103185cff27eef6e8090373749a8065129fcc93482bd6ea4db1808725b6da2e", - "0x29b35d35c22deda2ac9dd56a9f6a145871b1b6557e165296f804297160d5f98b", - "0x240bb4b0b7e30e71e8af2d908e72bf47b6496aab1e1f7cb32f2604d79f76cff8", - "0x1cd2156a0f0c1944a8a3359618ff978b27eb42075c667960817be624ce161489", - "0xbd0b75112591ab1b4a6a3e03fb76368419b78e4b95ee773b8ef5e7848695cf7", - "0xcd1da7fcfc27d2d9e9743e80951694995b162298d4109428fcf1c9a90f24905", - "0x2672327da3fdec6c58e8a0d33ca94e059da0787e9221a2a0ac412692cc962aac", - "0x50e88db23f7582691a0fb7e5c95dd713e54188833fe1d241e3e32a98dfeb0f0", - "0x8dc78ede51774238b0984b02ac7fcf8b0a8dfcb6ca733b90c6b44aac4551057", - "0x2a3167374e2d54e47ce865ef222346adf7a27d4174820a637cf656899238387", - "0x2f161fddcebb9ed8740c14d3a782efcf6f0ad069371194f87bcc04f9e9baf2ee", - "0x25dcf81d1721eab45e86ccfee579eaa4e54a4a80a19edf784f24cc1ee831e58a", - "0x1e483708e664ced677568d93b3b4f505e9d2968f802e04b31873f7d8f635fb0f", - "0x2bf6cdf920d353ba8bda932b72bf6ff6a93aa831274a5dc3ea6ea647a446d18e", - "0x2aa406a77d9143221165e066adfcc9281b9c90afdcee4336eda87f85d2bfe5b", - "0x26fc05b152609664e624a233e52e12252a0cae9d2a86a36717300063faca4b4b", - "0x24579fb180a63e5594644f4726c5af6d091aee4ee64c2c2a37d98f646a9c8d9d", - "0xb34ff9cbae3a9afe40e80a46e7d1419380e210a0e9595f61eb3a300aaef9f34", - "0x2ee89372d00fd0e32a46d513f7a80a1ae64302f33bc4b100384327a443c0193c", - "0x2b0e285154aef9e8af0777190947379df37da05cf342897bf1de1bc40e497893", - "0x158b022dd94b2c5c44994a5be28b2f570f1187277430ed9307517fa0c830d432", - "0x1d1ea6f83308f30e544948e221d6b313367eccfe54ec05dfa757f023b5758f3d", - "0x1a08a4549273627eadafe47379be8e997306f5b9567618b38c93a0d58eb6c54c", - "0xf434e5d987974afdd7f45a0f84fb800ecbbcdf2eeb302e415371e1d08ba4ad7", - "0x168b5b6d46176887125f13423384b8e8dd4fd947aac832d8d15b87865580b5fb", - "0x166cd223e74511332e2df4e7ad7a82c3871ed0305a5708521702c5e62e11a30b", - "0x10f0979b9797e30f8fe15539518c7f4dfc98c7acb1490da60088b6ff908a4876", - "0x20e08df88bbafc9a810fa8e2324c36b5513134477207763849ed4a0b6bd9639", - "0x1e977a84137396a3cfb17565ecfb5b60dffb242c7aab4afecaa45ebd2c83e0a3", - "0x19f3f9b6c6868a0e2a7453ff8949323715817869f8a25075308aa34a50c1ca3c", - "0x248b030bbfab25516cca23e7937d4b3b46967292ef6dfd3df25fcfe289d53fac", - "0x26bee4a0a5c8b76caa6b73172fa7760bd634c28d2c2384335b74f5d18e3933f4", - "0x106719993b9dacbe46b17f4e896c0c9c116d226c50afe2256dca1e81cd510b5c", - "0x19b5748fd961f755dd3c713d09014bd12adbb739fa1d2160067a312780a146a2", - ], - recursiveAggregationInput: [ - "0x04fdf01a2faedb9e3a620bc1cd8ceb4b0adac04631bdfa9e7e9fc15e35693cc0", - "0x1419728b438cc9afa63ab4861753e0798e29e08aac0da17b2c7617b994626ca2", - "0x23ca418458f6bdc30dfdbc13b80c604f8864619582eb247d09c8e4703232897b", - "0x0713c1371914ac18d7dced467a8a60eeca0f3d80a2cbd5dcc75abb6cbab39f39", - ], - }; - let verifier: VerifierRecursiveTest; - - before(async function () { - const verifierFactory = await hardhat.ethers.getContractFactory("VerifierRecursiveTest"); - const verifierContract = await verifierFactory.deploy(); - verifier = VerifierTestFactory.connect(verifierContract.address, verifierContract.signer); - }); - - it("Should verify proof", async () => { - // Call the verifier directly (though the call, not static call) to add the save the consumed gas into the statistic. - const calldata = verifier.interface.encodeFunctionData("verify", [ - PROOF.publicInputs, - PROOF.serializedProof, - PROOF.recursiveAggregationInput, - ]); - await verifier.fallback({ data: calldata }); - - // Check that proof is verified - const result = await verifier.verify(PROOF.publicInputs, PROOF.serializedProof, PROOF.recursiveAggregationInput); - expect(result, "proof verification failed").true; - }); - - describe("Should verify valid proof with fields values in non standard format", function () { - it("Public input with dirty bits over Fr mask", async () => { - const validProof = JSON.parse(JSON.stringify(PROOF)); - // Fill dirty bits - validProof.publicInputs[0] = ethers.BigNumber.from(validProof.publicInputs[0]) - .add("0xe000000000000000000000000000000000000000000000000000000000000000") - .toHexString(); - const result = await verifier.verify( - validProof.publicInputs, - validProof.serializedProof, - validProof.recursiveAggregationInput - ); - expect(result, "proof verification failed").true; - }); - - it("Elliptic curve points over modulo", async () => { - const validProof = JSON.parse(JSON.stringify(PROOF)); - // Add modulo to points - validProof.serializedProof[0] = ethers.BigNumber.from(validProof.serializedProof[0]).add(Q_MOD); - validProof.serializedProof[1] = ethers.BigNumber.from(validProof.serializedProof[1]).add(Q_MOD).add(Q_MOD); - const result = await verifier.verify( - validProof.publicInputs, - validProof.serializedProof, - validProof.recursiveAggregationInput - ); - expect(result, "proof verification failed").true; - }); - - it("Fr over modulo", async () => { - const validProof = JSON.parse(JSON.stringify(PROOF)); - // Add modulo to number - validProof.serializedProof[22] = ethers.BigNumber.from(validProof.serializedProof[22]).add(R_MOD); - const result = await verifier.verify( - validProof.publicInputs, - validProof.serializedProof, - validProof.recursiveAggregationInput - ); - expect(result, "proof verification failed").true; - }); - }); - - describe("Should revert on invalid input", function () { - it("More than 1 public inputs", async () => { - const invalidProof = JSON.parse(JSON.stringify(PROOF)); - // Add one more public input to proof - invalidProof.publicInputs.push(invalidProof.publicInputs[0]); - const revertReason = await getCallRevertReason( - verifier.verify(invalidProof.publicInputs, invalidProof.serializedProof, invalidProof.recursiveAggregationInput) - ); - expect(revertReason).equal("loadProof: Proof is invalid"); - }); - - it("Empty public inputs", async () => { - const revertReason = await getCallRevertReason( - verifier.verify([], PROOF.serializedProof, PROOF.recursiveAggregationInput) - ); - expect(revertReason).equal("loadProof: Proof is invalid"); - }); - - it("More than 44 words for proof", async () => { - const invalidProof = JSON.parse(JSON.stringify(PROOF)); - // Add one more "serialized proof" input - invalidProof.serializedProof.push(invalidProof.serializedProof[0]); - const revertReason = await getCallRevertReason( - verifier.verify(invalidProof.publicInputs, invalidProof.serializedProof, invalidProof.recursiveAggregationInput) - ); - expect(revertReason).equal("loadProof: Proof is invalid"); - }); - - it("Empty serialized proof", async () => { - const revertReason = await getCallRevertReason( - verifier.verify(PROOF.publicInputs, [], PROOF.recursiveAggregationInput) - ); - expect(revertReason).equal("loadProof: Proof is invalid"); - }); - - it("More than 4 words for recursive aggregation input", async () => { - const invalidProof = JSON.parse(JSON.stringify(PROOF)); - // Add one more "recursive aggregation input" value - invalidProof.recursiveAggregationInput.push(invalidProof.recursiveAggregationInput[0]); - const revertReason = await getCallRevertReason( - verifier.verify(invalidProof.publicInputs, invalidProof.serializedProof, invalidProof.recursiveAggregationInput) - ); - expect(revertReason).equal("loadProof: Proof is invalid"); - }); - - it("Empty recursive aggregation input", async () => { - const revertReason = await getCallRevertReason(verifier.verify(PROOF.publicInputs, PROOF.serializedProof, [])); - expect(revertReason).equal("loadProof: Proof is invalid"); - }); - - it("Elliptic curve point at infinity", async () => { - const invalidProof = JSON.parse(JSON.stringify(PROOF)); - // Change first point to point at infinity (encode as (0, 0) on EVM) - invalidProof.serializedProof[0] = ethers.constants.HashZero; - invalidProof.serializedProof[1] = ethers.constants.HashZero; - const revertReason = await getCallRevertReason( - verifier.verify(invalidProof.publicInputs, invalidProof.serializedProof, invalidProof.recursiveAggregationInput) - ); - expect(revertReason).equal("loadProof: Proof is invalid"); - }); - }); - - it("Should failed with invalid public input", async () => { - const revertReason = await getCallRevertReason( - verifier.verify([ethers.constants.HashZero], PROOF.serializedProof, PROOF.recursiveAggregationInput) - ); - expect(revertReason).equal("invalid quotient evaluation"); - }); - - it("Should failed with invalid recursive aggregative input", async () => { - const revertReason = await getCallRevertReason( - verifier.verify(PROOF.publicInputs, PROOF.serializedProof, [1, 2, 1, 2]) - ); - expect(revertReason).equal("finalPairing: pairing failure"); - }); - - it("Should return correct Verification key hash", async () => { - const vksHash = await verifier.verificationKeyHash(); - expect(vksHash).equal("0x88b3ddc4ed85974c7e14297dcad4097169440305c05fdb6441ca8dfd77cd7fa7"); - }); -}); diff --git a/system-contracts/test/L2EthToken.spec.ts b/system-contracts/test/L2EthToken.spec.ts new file mode 100644 index 000000000..d7d8923f9 --- /dev/null +++ b/system-contracts/test/L2EthToken.spec.ts @@ -0,0 +1,237 @@ +import { expect } from "chai"; +import { ethers, network } from "hardhat"; +import type { Wallet } from "zksync-web3"; +import type { L2EthToken } from "../typechain"; +import { L2EthTokenFactory } from "../typechain"; +import { deployContractOnAddress, getWallets, loadArtifact, provider } from "./shared/utils"; +import type { BigNumber } from "ethers"; +import { TEST_BOOTLOADER_FORMAL_ADDRESS, TEST_ETH_TOKEN_SYSTEM_CONTRACT_ADDRESS } from "./shared/constants"; +import { prepareEnvironment, setResult } from "./shared/mocks"; +import { randomBytes } from "crypto"; + +describe("L2EthToken tests", () => { + const richWallet = getWallets()[0]; + let wallets: Array; + let l2EthToken: L2EthToken; + let bootloaderAccount: ethers.Signer; + let mailboxIface: ethers.utils.Interface; + + before(async () => { + await prepareEnvironment(); + await deployContractOnAddress(TEST_ETH_TOKEN_SYSTEM_CONTRACT_ADDRESS, "L2EthToken"); + l2EthToken = L2EthTokenFactory.connect(TEST_ETH_TOKEN_SYSTEM_CONTRACT_ADDRESS, richWallet); + bootloaderAccount = await ethers.getImpersonatedSigner(TEST_BOOTLOADER_FORMAL_ADDRESS); + mailboxIface = new ethers.utils.Interface((await loadArtifact("IMailbox")).abi); + }); + + beforeEach(async () => { + wallets = Array.from({ length: 2 }, () => ethers.Wallet.createRandom().connect(provider)); + }); + + after(async function () { + await network.provider.request({ + method: "hardhat_stopImpersonatingAccount", + params: [TEST_BOOTLOADER_FORMAL_ADDRESS], + }); + }); + + describe("mint", () => { + it("called by bootlader", async () => { + const initialSupply: BigNumber = await l2EthToken.totalSupply(); + const initialBalanceOfWallet: BigNumber = await l2EthToken.balanceOf(wallets[0].address); + const amountToMint: BigNumber = ethers.utils.parseEther("10.0"); + + await expect(l2EthToken.connect(bootloaderAccount).mint(wallets[0].address, amountToMint)) + .to.emit(l2EthToken, "Mint") + .withArgs(wallets[0].address, amountToMint); + + const finalSupply: BigNumber = await l2EthToken.totalSupply(); + const balanceOfWallet: BigNumber = await l2EthToken.balanceOf(wallets[0].address); + expect(finalSupply).to.equal(initialSupply.add(amountToMint)); + expect(balanceOfWallet).to.equal(initialBalanceOfWallet.add(amountToMint)); + }); + + it("not called by bootloader", async () => { + const amountToMint: BigNumber = ethers.utils.parseEther("10.0"); + await expect(l2EthToken.connect(wallets[0]).mint(wallets[0].address, amountToMint)).to.be.rejectedWith( + "Callable only by the bootloader" + ); + }); + }); + + describe("transfer", () => { + it("transfer successfully", async () => { + await ( + await l2EthToken.connect(bootloaderAccount).mint(wallets[0].address, ethers.utils.parseEther("100.0")) + ).wait(); + + const senderBalanceBeforeTransfer: BigNumber = await l2EthToken.balanceOf(wallets[0].address); + const recipientBalanceBeforeTransfer: BigNumber = await l2EthToken.balanceOf(wallets[1].address); + + const amountToTransfer = ethers.utils.parseEther("10.0"); + + await expect( + l2EthToken.connect(bootloaderAccount).transferFromTo(wallets[0].address, wallets[1].address, amountToTransfer) + ) + .to.emit(l2EthToken, "Transfer") + .withArgs(wallets[0].address, wallets[1].address, amountToTransfer); + + const senderBalanceAfterTransfer: BigNumber = await l2EthToken.balanceOf(wallets[0].address); + const recipientBalanceAfterTransfer: BigNumber = await l2EthToken.balanceOf(wallets[1].address); + expect(senderBalanceAfterTransfer).to.be.eq(senderBalanceBeforeTransfer.sub(amountToTransfer)); + expect(recipientBalanceAfterTransfer).to.be.eq(recipientBalanceBeforeTransfer.add(amountToTransfer)); + }); + + it("no tranfser due to insufficient balance", async () => { + await ( + await l2EthToken.connect(bootloaderAccount).mint(wallets[0].address, ethers.utils.parseEther("5.0")) + ).wait(); + const amountToTransfer: BigNumber = ethers.utils.parseEther("6.0"); + + await expect( + l2EthToken.connect(bootloaderAccount).transferFromTo(wallets[0].address, wallets[1].address, amountToTransfer) + ).to.be.rejectedWith("Transfer amount exceeds balance"); + }); + + it("no transfer - require special access", async () => { + const maliciousWallet: Wallet = ethers.Wallet.createRandom().connect(provider); + await ( + await l2EthToken.connect(bootloaderAccount).mint(maliciousWallet.address, ethers.utils.parseEther("20.0")) + ).wait(); + + const amountToTransfer: BigNumber = ethers.utils.parseEther("20.0"); + + await expect( + l2EthToken + .connect(maliciousWallet) + .transferFromTo(maliciousWallet.address, wallets[1].address, amountToTransfer) + ).to.be.rejectedWith("Only system contracts with special access can call this method"); + }); + }); + + describe("balanceOf", () => { + it("walletFrom address", async () => { + const amountToMint: BigNumber = ethers.utils.parseEther("10.0"); + + await l2EthToken.connect(bootloaderAccount).mint(wallets[0].address, amountToMint); + const balance = await l2EthToken.balanceOf(wallets[0].address); + expect(balance).to.equal(amountToMint); + }); + + it("address larger than 20 bytes", async () => { + const amountToMint: BigNumber = ethers.utils.parseEther("123.0"); + + const res = await l2EthToken.connect(bootloaderAccount).mint(wallets[0].address, amountToMint); + await res.wait(); + const largerAddress = ethers.BigNumber.from( + "0x" + randomBytes(12).toString("hex") + wallets[0].address.slice(2) + ).toHexString(); + const balance = await l2EthToken.balanceOf(largerAddress); + + expect(balance).to.equal(amountToMint); + }); + }); + + describe("totalSupply", () => { + it("correct total supply", async () => { + const totalSupplyBefore = await l2EthToken.totalSupply(); + const amountToMint: BigNumber = ethers.utils.parseEther("10.0"); + + await l2EthToken.connect(bootloaderAccount).mint(wallets[0].address, amountToMint); + const totalSupply = await l2EthToken.totalSupply(); + + expect(totalSupply).to.equal(totalSupplyBefore.add(amountToMint)); + }); + }); + + describe("name", () => { + it("correct name", async () => { + const name = await l2EthToken.name(); + expect(name).to.equal("Ether"); + }); + }); + + describe("symbol", () => { + it("correct symbol", async () => { + const symbol = await l2EthToken.symbol(); + expect(symbol).to.equal("ETH"); + }); + }); + + describe("decimals", () => { + it("correct decimals", async () => { + const decimals = await l2EthToken.decimals(); + expect(decimals).to.equal(18); + }); + }); + + describe("withdraw", () => { + it("event, balance, totalsupply", async () => { + const amountToWithdraw: BigNumber = ethers.utils.parseEther("1.0"); + const message: string = ethers.utils.solidityPack( + ["bytes4", "address", "uint256"], + [mailboxIface.getSighash("finalizeEthWithdrawal"), wallets[1].address, amountToWithdraw] + ); + + await setResult("L1Messenger", "sendToL1", [message], { + failure: false, + returnData: ethers.utils.defaultAbiCoder.encode(["bytes32"], [ethers.utils.keccak256(message)]), + }); + + // To prevent underflow since initial values are 0's and we are substracting from them + const amountToMint: BigNumber = ethers.utils.parseEther("100.0"); + await (await l2EthToken.connect(bootloaderAccount).mint(l2EthToken.address, amountToMint)).wait(); + + const balanceBeforeWithdrawal: BigNumber = await l2EthToken.balanceOf(l2EthToken.address); + const totalSupplyBefore = await l2EthToken.totalSupply(); + + await expect(l2EthToken.connect(richWallet).withdraw(wallets[1].address, { value: amountToWithdraw })) + .to.emit(l2EthToken, "Withdrawal") + .withArgs(richWallet.address, wallets[1].address, amountToWithdraw); + + const balanceAfterWithdrawal: BigNumber = await l2EthToken.balanceOf(l2EthToken.address); + const totalSupplyAfter = await l2EthToken.totalSupply(); + + expect(balanceAfterWithdrawal).to.equal(balanceBeforeWithdrawal.sub(amountToWithdraw)); + expect(totalSupplyAfter).to.equal(totalSupplyBefore.sub(amountToWithdraw)); + }); + + it("event, balance, totalsupply, withdrawWithMessage", async () => { + const amountToWithdraw: BigNumber = ethers.utils.parseEther("1.0"); + const additionalData: string = ethers.utils.defaultAbiCoder.encode(["string"], ["additional data"]); + const message: string = ethers.utils.solidityPack( + ["bytes4", "address", "uint256", "address", "bytes"], + [ + mailboxIface.getSighash("finalizeEthWithdrawal"), + wallets[1].address, + amountToWithdraw, + richWallet.address, + additionalData, + ] + ); + + await setResult("L1Messenger", "sendToL1", [message], { + failure: false, + returnData: ethers.utils.defaultAbiCoder.encode(["bytes32"], [ethers.utils.keccak256(message)]), + }); + + // Consitency reasons - won't crash if test order reverse + const amountToMint: BigNumber = ethers.utils.parseEther("100.0"); + await (await l2EthToken.connect(bootloaderAccount).mint(l2EthToken.address, amountToMint)).wait(); + + const totalSupplyBefore = await l2EthToken.totalSupply(); + const balanceBeforeWithdrawal: BigNumber = await l2EthToken.balanceOf(l2EthToken.address); + await expect( + l2EthToken.connect(richWallet).withdrawWithMessage(wallets[1].address, additionalData, { + value: amountToWithdraw, + }) + ) + .to.emit(l2EthToken, "WithdrawalWithMessage") + .withArgs(richWallet.address, wallets[1].address, amountToWithdraw, additionalData); + const totalSupplyAfter = await l2EthToken.totalSupply(); + const balanceAfterWithdrawal: BigNumber = await l2EthToken.balanceOf(l2EthToken.address); + expect(balanceAfterWithdrawal).to.equal(balanceBeforeWithdrawal.sub(amountToWithdraw)); + expect(totalSupplyAfter).to.equal(totalSupplyBefore.sub(amountToWithdraw)); + }); + }); +});