diff --git a/contracts/hermez/future_versions/HermezV2.sol b/contracts/hermez/future_versions/HermezV2.sol index 1babae8..b6688f9 100644 --- a/contracts/hermez/future_versions/HermezV2.sol +++ b/contracts/hermez/future_versions/HermezV2.sol @@ -8,7 +8,7 @@ import "./interfaces/VerifierWithdrawInterface.sol"; import "../../interfaces/IHermezAuctionProtocol.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -contract Hermez is InstantWithdrawManager { +contract HermezV2 is InstantWithdrawManagerV2 { struct VerifierRollup { VerifierRollupInterface verifierInterface; uint256 maxTx; // maximum rollup transactions in a batch: L2-tx + L1-tx transactions @@ -68,11 +68,11 @@ contract Hermez is InstantWithdrawManager { // Modulus zkSNARK uint256 constant _RFIELD = 21888242871839275222246405745257275088548364400416034343698204186575808495617; - // [6 bytes] lastIdx + [6 bytes] newLastIdx + [32 bytes] stateRoot + [32 bytes] newStRoot + [32 bytes] newExitRoot + + // [6 bytes] lastIdx + [6 bytes] newLastIdx + [32 bytes] stateRoot + [32 bytes] newStRoot + // [_MAX_L1_TX * _L1_USER_TOTALBYTES bytes] l1TxsData + totall1L2TxsDataLength + feeIdxCoordinatorLength + [2 bytes] chainID + [4 bytes] batchNum = // 18546 bytes + totall1L2TxsDataLength + feeIdxCoordinatorLength - uint256 constant _INPUT_SHA_CONSTANT_BYTES = 20082; + uint256 constant _INPUT_SHA_CONSTANT_BYTES = 20050; uint8 public constant ABSOLUTE_MAX_L1L2BATCHTIMEOUT = 240; @@ -98,15 +98,8 @@ contract Hermez is InstantWithdrawManager { // Each batch forged will have a correlated 'state root' mapping(uint32 => uint256) public stateRootMap; - // Each batch forged will have a correlated 'exit tree' represented by the exit root - mapping(uint32 => uint256) public exitRootsMap; - - // Each batch forged will have a correlated 'l1L2TxDataHash' - mapping(uint32 => bytes32) public l1L2TxsDataHashMap; - - // Mapping of exit nullifiers, only allowing each withdrawal to be made once - // rootId => (Idx => true/false) - mapping(uint32 => mapping(uint48 => bool)) public exitNullifierMap; + // Mapping of exit amounts, store all the amount already withdrawed of every account + mapping(uint48 => uint256) public exitAccumulateMap; // List of ERC20 tokens that can be used in rollup // ID = 0 will be reserved for ether @@ -161,8 +154,8 @@ contract Hermez is InstantWithdrawManager { // Event emitted when a withdrawal is done event WithdrawEvent( + uint256 indexed amountWithdraw, uint48 indexed idx, - uint32 indexed numExitRoot, bool indexed instantWithdraw ); @@ -173,40 +166,6 @@ contract Hermez is InstantWithdrawManager { uint64 withdrawalDelay ); - // Event emitted when the contract is updated to the new version - event hermezV2(); - - function updateVerifiers() external { - require( - msg.sender == address(0xb6D3f1056c015962fA66A4020E50522B58292D1E), - "Hermez::updateVerifiers ONLY_DEPLOYER" - ); - require( - rollupVerifiers[0].maxTx == 344, // Old verifier 344 tx - "Hermez::updateVerifiers VERIFIERS_ALREADY_UPDATED" - ); - rollupVerifiers[0] = VerifierRollup({ - verifierInterface: VerifierRollupInterface( - address(0x3DAa0B2a994b1BC60dB9e312aD0a8d87a1Bb16D2) // New verifier 400 tx - ), - maxTx: 400, - nLevels: 32 - }); - - rollupVerifiers[1] = VerifierRollup({ - verifierInterface: VerifierRollupInterface( - address(0x1DC4b451DFcD0e848881eDE8c7A99978F00b1342) // New verifier 2048 tx - ), - maxTx: 2048, - nLevels: 32 - }); - - withdrawVerifier = VerifierWithdrawInterface( - 0x4464A1E499cf5443541da6728871af1D5C4920ca - ); - emit hermezV2(); - } - /** * @dev Initializer function (equivalent to the constructor). Since we use * upgradeable smartcontracts the state vars have to be initialized here. @@ -219,9 +178,6 @@ contract Hermez is InstantWithdrawManager { address _tokenHEZ, uint8 _forgeL1L2BatchTimeout, uint256 _feeAddToken, - address _poseidon2Elements, - address _poseidon3Elements, - address _poseidon4Elements, address _hermezGovernanceAddress, uint64 _withdrawalDelay, address _withdrawDelayerContract @@ -248,12 +204,6 @@ contract Hermez is InstantWithdrawManager { // stateRootMap[0] = 0 --> genesis batch will have root = 0 tokenList.push(address(0)); // Token 0 is ETH - // initialize libs - _initializeHelpers( - _poseidon2Elements, - _poseidon3Elements, - _poseidon4Elements - ); _initializeWithdraw( _hermezGovernanceAddress, _withdrawalDelay, @@ -272,12 +222,11 @@ contract Hermez is InstantWithdrawManager { /** * @dev Forge a new batch providing the L2 Transactions, L1Corrdinator transactions and the proof. - * If the proof is succesfully verified, update the current state, adding a new state and exit root. + * If the proof is succesfully verified, update the current state, adding a new state. * In order to optimize the gas consumption the parameters `encodedL1CoordinatorTx`, `l1L2TxsData` and `feeIdxCoordinator` * are read directly from the calldata using assembly with the instruction `calldatacopy` * @param newLastIdx New total rollup accounts * @param newStRoot New state root - * @param newExitRoot New exit root * @param encodedL1CoordinatorTx Encoded L1-coordinator transactions * @param l1L2TxsData Encoded l2 data * @param feeIdxCoordinator Encoded idx accounts of the coordinator where the fees will be payed @@ -291,7 +240,6 @@ contract Hermez is InstantWithdrawManager { function forgeBatch( uint48 newLastIdx, uint256 newStRoot, - uint256 newExitRoot, bytes calldata encodedL1CoordinatorTx, bytes calldata l1L2TxsData, bytes calldata feeIdxCoordinator, @@ -327,7 +275,6 @@ contract Hermez is InstantWithdrawManager { uint256 input = _constructCircuitInput( newLastIdx, newStRoot, - newExitRoot, l1Batch, verifierIdx ); @@ -347,8 +294,6 @@ contract Hermez is InstantWithdrawManager { lastForgedBatch++; lastIdx = newLastIdx; stateRootMap[lastForgedBatch] = newStRoot; - exitRootsMap[lastForgedBatch] = newExitRoot; - l1L2TxsDataHashMap[lastForgedBatch] = sha256(l1L2TxsData); uint16 l1UserTxsLen; if (l1Batch) { @@ -533,71 +478,6 @@ contract Hermez is InstantWithdrawManager { ); } - ////////////// - // User operations - ///////////// - - /** - * @dev Withdraw to retrieve the tokens from the exit tree to the owner account - * Before this call an exit transaction must be done - * @param tokenID Token identifier - * @param amount Amount to retrieve - * @param babyPubKey Public key babyjubjub represented as point: sign + (Ay) - * @param numExitRoot Batch number where the exit transaction has been done - * @param siblings Siblings to demonstrate merkle tree proof - * @param idx Index of the exit tree account - * @param instantWithdraw true if is an instant withdraw - * Events: `WithdrawEvent` - */ - function withdrawMerkleProof( - uint32 tokenID, - uint192 amount, - uint256 babyPubKey, - uint32 numExitRoot, - uint256[] memory siblings, - uint48 idx, - bool instantWithdraw - ) external { - // numExitRoot is not checked because an invalid numExitRoot will bring to a 0 root - // and this is an empty tree. - // in case of instant withdraw assure that is available - if (instantWithdraw) { - require( - _processInstantWithdrawal(tokenList[tokenID], amount), - "Hermez::withdrawMerkleProof: INSTANT_WITHDRAW_WASTED_FOR_THIS_USD_RANGE" - ); - } - - // build 'key' and 'value' for exit tree - uint256[4] memory arrayState = _buildTreeState( - tokenID, - 0, - amount, - babyPubKey, - msg.sender - ); - uint256 stateHash = _hash4Elements(arrayState); - // get exit root given its index depth - uint256 exitRoot = exitRootsMap[numExitRoot]; - // check exit tree nullifier - require( - exitNullifierMap[numExitRoot][idx] == false, - "Hermez::withdrawMerkleProof: WITHDRAW_ALREADY_DONE" - ); - // check sparse merkle tree proof - require( - _smtVerifier(exitRoot, siblings, idx, stateHash) == true, - "Hermez::withdrawMerkleProof: SMT_PROOF_INVALID" - ); - - // set nullifier - exitNullifierMap[numExitRoot][idx] = true; - - _withdrawFunds(amount, tokenID, instantWithdraw); - - emit WithdrawEvent(idx, numExitRoot, instantWithdraw); - } - /** * @dev Withdraw to retrieve the tokens from the exit tree to the owner account * Before this call an exit transaction must be done @@ -605,8 +485,9 @@ contract Hermez is InstantWithdrawManager { * @param proofB zk-snark input * @param proofC zk-snark input * @param tokenID Token identifier - * @param amount Amount to retrieve - * @param numExitRoot Batch number where the exit transaction has been done + * @param amountExit Amount exit of the leaf + * @param amountWithdraw Amount to withdraw + * @param batchNum Batch number after exit transactions has been done * @param idx Index of the exit tree account * @param instantWithdraw true if is an instant withdraw * Events: `WithdrawEvent` @@ -616,28 +497,37 @@ contract Hermez is InstantWithdrawManager { uint256[2][2] calldata proofB, uint256[2] calldata proofC, uint32 tokenID, - uint192 amount, - uint32 numExitRoot, + uint192 amountExit, + uint192 amountWithdraw, + uint32 batchNum, uint48 idx, bool instantWithdraw ) external { // in case of instant withdraw assure that is available if (instantWithdraw) { require( - _processInstantWithdrawal(tokenList[tokenID], amount), + _processInstantWithdrawal(tokenList[tokenID], amountWithdraw), "Hermez::withdrawCircuit: INSTANT_WITHDRAW_WASTED_FOR_THIS_USD_RANGE" ); } require( - exitNullifierMap[numExitRoot][idx] == false, - "Hermez::withdrawCircuit: WITHDRAW_ALREADY_DONE" + amountWithdraw <= uint256(amountExit).sub(exitAccumulateMap[idx]), + "Hermez::withdrawCircuit: AMOUNT_WITHDRAW_LESS_THAN_ACCUMULATED" ); // get exit root given its index depth - uint256 exitRoot = exitRootsMap[numExitRoot]; + uint256 stateRoot = stateRootMap[batchNum]; uint256 input = uint256( - sha256(abi.encodePacked(exitRoot, msg.sender, tokenID, amount, idx)) + sha256( + abi.encodePacked( + stateRoot, + msg.sender, + tokenID, + amountExit, + idx + ) + ) ) % _RFIELD; // verify zk-snark circuit require( @@ -647,11 +537,11 @@ contract Hermez is InstantWithdrawManager { ); // set nullifier - exitNullifierMap[numExitRoot][idx] = true; + exitAccumulateMap[idx] += amountWithdraw; - _withdrawFunds(amount, tokenID, instantWithdraw); + _withdrawFunds(amountWithdraw, tokenID, instantWithdraw); - emit WithdrawEvent(idx, numExitRoot, instantWithdraw); + emit WithdrawEvent(amountWithdraw, idx, instantWithdraw); } ////////////// @@ -824,7 +714,7 @@ contract Hermez is InstantWithdrawManager { uint256 dPtr; uint256 dLen; - (dPtr, dLen) = _getCallData(3); + (dPtr, dLen) = _getCallData(2); uint256 l1CoordinatorLength = dLen / _L1_COORDINATOR_TOTALBYTES; uint256 l1UserLength; @@ -927,14 +817,12 @@ contract Hermez is InstantWithdrawManager { * @dev Calculate the circuit input hashing all the elements * @param newLastIdx New total rollup accounts * @param newStRoot New state root - * @param newExitRoot New exit root * @param l1Batch Indicates if this forge will be L2 or L1-L2 * @param verifierIdx Verifier index */ function _constructCircuitInput( uint48 newLastIdx, uint256 newStRoot, - uint256 newExitRoot, bool l1Batch, uint8 verifierIdx ) internal view returns (uint256) { @@ -961,7 +849,6 @@ contract Hermez is InstantWithdrawManager { // [6 bytes] newLastIdx + // [32 bytes] stateRoot + // [32 bytes] newStRoot + - // [32 bytes] newExitRoot + // [_MAX_L1_TX * _L1_USER_TOTALBYTES bytes] l1TxsData + // totall1L2TxsDataLength + // feeIdxCoordinatorLength + @@ -1001,9 +888,6 @@ contract Hermez is InstantWithdrawManager { mstore(ptr, newStRoot) ptr := add(ptr, 32) - - mstore(ptr, newExitRoot) - ptr := add(ptr, 32) } // Copy the L1TX Data @@ -1011,22 +895,23 @@ contract Hermez is InstantWithdrawManager { ptr += _MAX_L1_TX * _L1_USER_TOTALBYTES; // Copy the L2 TX Data from calldata - (dPtr, dLen) = _getCallData(4); + (dPtr, dLen) = _getCallData(3); require( dLen <= l1L2TxsDataLength, "Hermez::_constructCircuitInput: L2_TX_OVERFLOW" ); + + // L2 TX unused data is padded with 0 at the start + _fillZeros(ptr, l1L2TxsDataLength - dLen); + ptr += l1L2TxsDataLength - dLen; + assembly { calldatacopy(ptr, dPtr, dLen) } ptr += dLen; - // L2 TX unused data is padded with 0 at the end - _fillZeros(ptr, l1L2TxsDataLength - dLen); - ptr += l1L2TxsDataLength - dLen; - // Copy the FeeIdxCoordinator from the calldata - (dPtr, dLen) = _getCallData(5); + (dPtr, dLen) = _getCallData(4); require( dLen <= feeIdxCoordinatorLength, "Hermez::_constructCircuitInput: INVALID_FEEIDXCOORDINATOR_LENGTH" diff --git a/contracts/hermez/future_versions/interfaces/VerifierRollupInterface.sol b/contracts/hermez/future_versions/interfaces/VerifierRollupInterface.sol new file mode 100644 index 0000000..5b09e79 --- /dev/null +++ b/contracts/hermez/future_versions/interfaces/VerifierRollupInterface.sol @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: AGPL-3.0 + +pragma solidity ^0.6.12; + +/** + * @dev Define interface verifier + */ +interface VerifierRollupInterface { + function verifyProof( + uint256[2] calldata proofA, + uint256[2][2] calldata proofB, + uint256[2] calldata proofC, + uint256[1] calldata input + ) external view returns (bool); +} diff --git a/contracts/hermez/future_versions/interfaces/VerifierWithdrawInterface.sol b/contracts/hermez/future_versions/interfaces/VerifierWithdrawInterface.sol new file mode 100644 index 0000000..b9b367c --- /dev/null +++ b/contracts/hermez/future_versions/interfaces/VerifierWithdrawInterface.sol @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: AGPL-3.0 + +pragma solidity ^0.6.12; + +/** + * @dev Define interface verifier + */ +interface VerifierWithdrawInterface { + function verifyProof( + uint256[2] calldata proofA, + uint256[2][2] calldata proofB, + uint256[2] calldata proofC, + uint256[1] calldata input + ) external view returns (bool); +} diff --git a/contracts/hermez/future_versions/lib/HermezHelpersV2.sol b/contracts/hermez/future_versions/lib/HermezHelpersV2.sol new file mode 100644 index 0000000..985402e --- /dev/null +++ b/contracts/hermez/future_versions/lib/HermezHelpersV2.sol @@ -0,0 +1,319 @@ +// SPDX-License-Identifier: AGPL-3.0 + +pragma solidity ^0.6.12; + +import "@openzeppelin/contracts-upgradeable/proxy/Initializable.sol"; + +/** + * @dev Rollup helper functions + */ +contract HermezHelpersV2 is Initializable { + uint256 private constant _WORD_SIZE = 32; + + // bytes32 public constant EIP712DOMAIN_HASH = + // keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)") + bytes32 + public constant EIP712DOMAIN_HASH = 0x8b73c3c69bb8fe3d512ecc4cf759cc79239f7b179b0ffacaa9a75d522b39400f; + // bytes32 public constant NAME_HASH = + // keccak256("Hermez Network") + bytes32 + public constant NAME_HASH = 0xbe287413178bfeddef8d9753ad4be825ae998706a6dabff23978b59dccaea0ad; + // bytes32 public constant VERSION_HASH = + // keccak256("1") + bytes32 + public constant VERSION_HASH = 0xc89efdaa54c0f20c7adf612882df0950f5a951637e0307cdcb4c672f298b8bc6; + // bytes32 public constant AUTHORISE_TYPEHASH = + // keccak256("Authorise(string Provider,string Authorisation,bytes32 BJJKey)"); + bytes32 + public constant AUTHORISE_TYPEHASH = 0xafd642c6a37a2e6887dc4ad5142f84197828a904e53d3204ecb1100329231eaa; + // bytes32 public constant HERMEZ_NETWORK_HASH = keccak256(bytes("Hermez Network")), + bytes32 + public constant HERMEZ_NETWORK_HASH = 0xbe287413178bfeddef8d9753ad4be825ae998706a6dabff23978b59dccaea0ad; + // bytes32 public constant ACCOUNT_CREATION_HASH = keccak256(bytes("Account creation")), + bytes32 + public constant ACCOUNT_CREATION_HASH = 0xff946cf82975b1a2b6e6d28c9a76a4b8d7a1fd0592b785cb92771933310f9ee7; + + /** + * @dev Decode half floating precision. + * Max value encoded with this codification: 0x1f8def8800cca870c773f6eb4d980000000 (aprox 137 bits) + * @param float Float half precision encode number + * @return Decoded floating half precision + */ + function _float2Fix(uint40 float) internal pure returns (uint256) { + uint256 m = float & 0x7FFFFFFFF; + uint256 e = float >> 35; + + // never overflow, max "e" value is 32 + uint256 exp = 10**e; + + // never overflow, max "fix" value is 1023 * 10^32 + uint256 fix = m * exp; + + return fix; + } + + /** + * @dev Retrieve the DOMAIN_SEPARATOR hash + * @return domainSeparator hash used for sign messages + */ + function DOMAIN_SEPARATOR() public view returns (bytes32 domainSeparator) { + return + keccak256( + abi.encode( + EIP712DOMAIN_HASH, + NAME_HASH, + VERSION_HASH, + getChainId(), + address(this) + ) + ); + } + + /** + * @return chainId The current chainId where the smarctoncract is executed + */ + function getChainId() public pure returns (uint256 chainId) { + assembly { + chainId := chainid() + } + } + + /** + * @dev Retrieve ethereum address from a (defaultMessage + babyjub) signature + * @param babyjub Public key babyjubjub represented as point: sign + (Ay) + * @param r Signature parameter + * @param s Signature parameter + * @param v Signature parameter + * @return Ethereum address recovered from the signature + */ + function _checkSig( + bytes32 babyjub, + bytes32 r, + bytes32 s, + uint8 v + ) internal view returns (address) { + // from https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/cryptography/ECDSA.sol#L46 + // EIP-2 still allows signature malleability for ecrecover(). Remove this possibility and make the signature + // unique. Appendix F in the Ethereum Yellow paper (https://ethereum.github.io/yellowpaper/paper.pdf), defines + // the valid range for s in (281): 0 < s < secp256k1n ÷ 2 + 1, and for v in (282): v ∈ {27, 28}. Most + // signatures from current libraries generate a unique signature with an s-value in the lower half order. + // + // If your library generates malleable signatures, such as s-values in the upper range, calculate a new s-value + // with 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141 - s1 and flip v from 27 to 28 or + // vice versa. If your library also generates signatures with 0/1 for v instead 27/28, add 27 to v to accept + // these malleable signatures as well. + require( + uint256(s) <= + 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D576E7357A4501DDFE92F46681B20A0, + "HermezHelpers::_checkSig: INVALID_S_VALUE" + ); + + bytes32 encodeData = keccak256( + abi.encode( + AUTHORISE_TYPEHASH, + HERMEZ_NETWORK_HASH, + ACCOUNT_CREATION_HASH, + babyjub + ) + ); + + bytes32 messageDigest = keccak256( + abi.encodePacked("\x19\x01", DOMAIN_SEPARATOR(), encodeData) + ); + + address ethAddress = ecrecover(messageDigest, v, r, s); + + require( + ethAddress != address(0), + "HermezHelpers::_checkSig: INVALID_SIGNATURE" + ); + + return ethAddress; + } + + /** + * @dev return information from specific call data info + * @param posParam parameter number relative to 0 to extract the info + * @return ptr ptr to the call data position where the actual data starts + * @return len Length of the data + */ + function _getCallData(uint256 posParam) + internal + pure + returns (uint256 ptr, uint256 len) + { + assembly { + let pos := add(4, mul(posParam, 32)) + ptr := add(calldataload(pos), 4) + len := calldataload(ptr) + ptr := add(ptr, 32) + } + } + + /** + * @dev This package fills at least len zeros in memory and a maximum of len+31 + * @param ptr The position where it starts to fill zeros + * @param len The minimum quantity of zeros it's added + */ + function _fillZeros(uint256 ptr, uint256 len) internal pure { + assembly { + let ptrTo := ptr + ptr := add(ptr, len) + for { + + } lt(ptrTo, ptr) { + ptrTo := add(ptrTo, 32) + } { + mstore(ptrTo, 0) + } + } + } + + /** + * @dev Copy 'len' bytes from memory address 'src', to address 'dest'. + * From https://github.com/GNSPS/solidity-bytes-utils/blob/master/contracts/BytesLib.sol + * @param _preBytes bytes storage + * @param _postBytes Bytes array memory + */ + function _concatStorage(bytes storage _preBytes, bytes memory _postBytes) + internal + { + assembly { + // Read the first 32 bytes of _preBytes storage, which is the length + // of the array. (We don't need to use the offset into the slot + // because arrays use the entire slot.) + let fslot := sload(_preBytes_slot) + // Arrays of 31 bytes or less have an even value in their slot, + // while longer arrays have an odd value. The actual length is + // the slot divided by two for odd values, and the lowest order + // byte divided by two for even values. + // If the slot is even, bitwise and the slot with 255 and divide by + // two to get the length. If the slot is odd, bitwise and the slot + // with -1 and divide by two. + let slength := div( + and(fslot, sub(mul(0x100, iszero(and(fslot, 1))), 1)), + 2 + ) + let mlength := mload(_postBytes) + let newlength := add(slength, mlength) + // slength can contain both the length and contents of the array + // if length < 32 bytes so let's prepare for that + // v. http://solidity.readthedocs.io/en/latest/miscellaneous.html#layout-of-state-variables-in-storage + switch add(lt(slength, 32), lt(newlength, 32)) + case 2 { + // Since the new array still fits in the slot, we just need to + // update the contents of the slot. + // uint256(bytes_storage) = uint256(bytes_storage) + uint256(bytes_memory) + new_length + sstore( + _preBytes_slot, + // all the modifications to the slot are inside this + // next block + add( + // we can just add to the slot contents because the + // bytes we want to change are the LSBs + fslot, + add( + mul( + div( + // load the bytes from memory + mload(add(_postBytes, 0x20)), + // zero all bytes to the right + exp(0x100, sub(32, mlength)) + ), + // and now shift left the number of bytes to + // leave space for the length in the slot + exp(0x100, sub(32, newlength)) + ), + // increase length by the double of the memory + // bytes length + mul(mlength, 2) + ) + ) + ) + } + case 1 { + // The stored value fits in the slot, but the combined value + // will exceed it. + // get the keccak hash to get the contents of the array + mstore(0x0, _preBytes_slot) + let sc := add(keccak256(0x0, 0x20), div(slength, 32)) + + // save new length + sstore(_preBytes_slot, add(mul(newlength, 2), 1)) + + // The contents of the _postBytes array start 32 bytes into + // the structure. Our first read should obtain the `submod` + // bytes that can fit into the unused space in the last word + // of the stored array. To get this, we read 32 bytes starting + // from `submod`, so the data we read overlaps with the array + // contents by `submod` bytes. Masking the lowest-order + // `submod` bytes allows us to add that value directly to the + // stored value. + + let submod := sub(32, slength) + let mc := add(_postBytes, submod) + let end := add(_postBytes, mlength) + let mask := sub(exp(0x100, submod), 1) + + sstore( + sc, + add( + and( + fslot, + 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00 + ), + and(mload(mc), mask) + ) + ) + + for { + mc := add(mc, 0x20) + sc := add(sc, 1) + } lt(mc, end) { + sc := add(sc, 1) + mc := add(mc, 0x20) + } { + sstore(sc, mload(mc)) + } + + mask := exp(0x100, sub(mc, end)) + + sstore(sc, mul(div(mload(mc), mask), mask)) + } + default { + // get the keccak hash to get the contents of the array + mstore(0x0, _preBytes_slot) + // Start copying to the last used word of the stored array. + let sc := add(keccak256(0x0, 0x20), div(slength, 32)) + + // save new length + sstore(_preBytes_slot, add(mul(newlength, 2), 1)) + + // Copy over the first `submod` bytes of the new data as in + // case 1 above. + let slengthmod := mod(slength, 32) + let mlengthmod := mod(mlength, 32) + let submod := sub(32, slengthmod) + let mc := add(_postBytes, submod) + let end := add(_postBytes, mlength) + let mask := sub(exp(0x100, submod), 1) + + sstore(sc, add(sload(sc), and(mload(mc), mask))) + + for { + sc := add(sc, 1) + mc := add(mc, 0x20) + } lt(mc, end) { + sc := add(sc, 1) + mc := add(mc, 0x20) + } { + sstore(sc, mload(mc)) + } + + mask := exp(0x100, sub(mc, end)) + + sstore(sc, mul(div(mload(mc), mask), mask)) + } + } + } +} diff --git a/contracts/hermez/future_versions/lib/InstantWithdrawManagerV2.sol b/contracts/hermez/future_versions/lib/InstantWithdrawManagerV2.sol new file mode 100644 index 0000000..702c4f9 --- /dev/null +++ b/contracts/hermez/future_versions/lib/InstantWithdrawManagerV2.sol @@ -0,0 +1,335 @@ +// SPDX-License-Identifier: AGPL-3.0 + +pragma solidity 0.6.12; + +import "../../../interfaces/IWithdrawalDelayer.sol"; +import "./HermezHelpersV2.sol"; + +import "@openzeppelin/contracts/math/SafeMath.sol"; + +contract InstantWithdrawManagerV2 is HermezHelpersV2 { + using SafeMath for uint256; + + // Number of buckets + uint256 private constant _MAX_BUCKETS = 5; + + // Bucket array + uint256 public nBuckets; + mapping(int256 => uint256) public buckets; + + // Governance address + address public hermezGovernanceAddress; + + // Withdraw delay in seconds + uint64 public withdrawalDelay; + + // ERC20 decimals signature + // bytes4(keccak256(bytes("decimals()"))) + bytes4 private constant _ERC20_DECIMALS = 0x313ce567; + + uint256 private constant _MAX_WITHDRAWAL_DELAY = 2 weeks; + + // Withdraw delayer interface + IWithdrawalDelayer public withdrawDelayerContract; + + // Mapping tokenAddress --> (USD value)/token , default 0, means that token does not worth + // 2^64 = 1.8446744e+19 + // fixed point codification is used, 9 digits for integer part, 10 digits for decimal + // In other words, the USD value of a token base unit is multiplied by 1e10 + // MaxUSD value for a base unit token: 1844674407,3709551616$ + // MinUSD value for a base unit token: 1e-10$ + mapping(address => uint64) public tokenExchange; + + uint256 private constant _EXCHANGE_MULTIPLIER = 1e10; + + event UpdateBucketWithdraw( + uint8 indexed numBucket, + uint256 indexed blockStamp, + uint256 withdrawals + ); + + event UpdateWithdrawalDelay(uint64 newWithdrawalDelay); + event UpdateBucketsParameters(uint256[] arrayBuckets); + event UpdateTokenExchange(address[] addressArray, uint64[] valueArray); + event SafeMode(); + + function _initializeWithdraw( + address _hermezGovernanceAddress, + uint64 _withdrawalDelay, + address _withdrawDelayerContract + ) internal initializer { + hermezGovernanceAddress = _hermezGovernanceAddress; + withdrawalDelay = _withdrawalDelay; + withdrawDelayerContract = IWithdrawalDelayer(_withdrawDelayerContract); + } + + modifier onlyGovernance { + require( + msg.sender == hermezGovernanceAddress, + "InstantWithdrawManager::onlyGovernance: ONLY_GOVERNANCE_ADDRESS" + ); + _; + } + + /** + * @dev Attempt to use instant withdraw + * @param tokenAddress Token address + * @param amount Amount to withdraw + */ + function _processInstantWithdrawal(address tokenAddress, uint192 amount) + internal + returns (bool) + { + // find amount in USD and then the corresponding bucketIdx + uint256 amountUSD = _token2USD(tokenAddress, amount); + + if (amountUSD == 0) { + return true; + } + + // find the appropiate bucketId + int256 bucketIdx = _findBucketIdx(amountUSD); + if (bucketIdx == -1) return true; + + ( + uint256 ceilUSD, + uint256 blockStamp, + uint256 withdrawals, + uint256 rateBlocks, + uint256 rateWithdrawals, + uint256 maxWithdrawals + ) = unpackBucket(buckets[bucketIdx]); + + // update the bucket and check again if are withdrawals available + uint256 differenceBlocks = block.number.sub(blockStamp); + uint256 periods = differenceBlocks.div(rateBlocks); + + // add the withdrawals available + withdrawals = withdrawals.add(periods.mul(rateWithdrawals)); + if (withdrawals >= maxWithdrawals) { + withdrawals = maxWithdrawals; + blockStamp = block.number; + } else { + blockStamp = blockStamp.add(periods.mul(rateBlocks)); + } + + if (withdrawals == 0) return false; + + withdrawals = withdrawals.sub(1); + + // update the bucket with the new values + buckets[bucketIdx] = packBucket( + ceilUSD, + blockStamp, + withdrawals, + rateBlocks, + rateWithdrawals, + maxWithdrawals + ); + + emit UpdateBucketWithdraw(uint8(bucketIdx), blockStamp, withdrawals); + return true; + } + + /** + * @dev Update bucket parameters + * @param newBuckets Array of buckets to replace the current ones, this array includes the + * following parameters: [ceilUSD, withdrawals, rateBlocks, rateWithdrawals, maxWithdrawals] + */ + function updateBucketsParameters(uint256[] memory newBuckets) + external + onlyGovernance + { + uint256 n = newBuckets.length; + require( + n <= _MAX_BUCKETS, + "InstantWithdrawManager::updateBucketsParameters: MAX_NUM_BUCKETS" + ); + + nBuckets = n; + for (uint256 i = 0; i < n; i++) { + ( + uint256 ceilUSD, + , + uint256 withdrawals, + uint256 rateBlocks, + uint256 rateWithdrawals, + uint256 maxWithdrawals + ) = unpackBucket(newBuckets[i]); + require( + withdrawals <= maxWithdrawals, + "InstantWithdrawManager::updateBucketsParameters: WITHDRAWALS_MUST_BE_LESS_THAN_MAXWITHDRAWALS" + ); + require( + rateBlocks > 0, + "InstantWithdrawManager::updateBucketsParameters: RATE_BLOCKS_MUST_BE_MORE_THAN_0" + ); + buckets[int256(i)] = packBucket( + ceilUSD, + block.number, + withdrawals, + rateBlocks, + rateWithdrawals, + maxWithdrawals + ); + } + emit UpdateBucketsParameters(newBuckets); + } + + /** + * @dev Update token USD value + * @param addressArray Array of the token address + * @param valueArray Array of USD values + */ + function updateTokenExchange( + address[] memory addressArray, + uint64[] memory valueArray + ) external onlyGovernance { + require( + addressArray.length == valueArray.length, + "InstantWithdrawManager::updateTokenExchange: INVALID_ARRAY_LENGTH" + ); + for (uint256 i = 0; i < addressArray.length; i++) { + tokenExchange[addressArray[i]] = valueArray[i]; + } + emit UpdateTokenExchange(addressArray, valueArray); + } + + /** + * @dev Update WithdrawalDelay + * @param newWithdrawalDelay New WithdrawalDelay + * Events: `UpdateWithdrawalDelay` + */ + function updateWithdrawalDelay(uint64 newWithdrawalDelay) + external + onlyGovernance + { + require( + newWithdrawalDelay <= _MAX_WITHDRAWAL_DELAY, + "InstantWithdrawManager::updateWithdrawalDelay: EXCEED_MAX_WITHDRAWAL_DELAY" + ); + withdrawalDelay = newWithdrawalDelay; + emit UpdateWithdrawalDelay(newWithdrawalDelay); + } + + /** + * @dev Put the smartcontract in safe mode, only delayed withdrawals allowed, + * also update the 'withdrawalDelay' of the 'withdrawDelayer' contract + */ + function safeMode() external onlyGovernance { + // only 1 bucket that does not allow any instant withdraw + nBuckets = 1; + buckets[0] = packBucket(0xFFFFFFFF_FFFFFFFF_FFFFFFFF, 0, 0, 1, 0, 0); + withdrawDelayerContract.changeWithdrawalDelay(withdrawalDelay); + emit SafeMode(); + } + + function instantWithdrawalViewer(address tokenAddress, uint192 amount) + public + view + returns (bool) + { + uint256 amountUSD = _token2USD(tokenAddress, amount); + if (amountUSD == 0) return true; + + int256 bucketIdx = _findBucketIdx(amountUSD); + if (bucketIdx == -1) return true; + + ( + , + uint256 blockStamp, + uint256 withdrawals, + uint256 rateBlocks, + uint256 rateWithdrawals, + uint256 maxWithdrawals + ) = unpackBucket(buckets[bucketIdx]); + + uint256 differenceBlocks = block.number.sub(blockStamp); + uint256 periods = differenceBlocks.div(rateBlocks); + + withdrawals = withdrawals.add(periods.mul(rateWithdrawals)); + if (withdrawals > maxWithdrawals) withdrawals = maxWithdrawals; + + if (withdrawals == 0) return false; + + return true; + } + + function _token2USD(address tokenAddress, uint192 amount) + internal + view + returns (uint256) + { + if (tokenExchange[tokenAddress] == 0) return 0; + + uint256 baseUnitTokenUSD = (uint256(amount) * + uint256(tokenExchange[tokenAddress])) / _EXCHANGE_MULTIPLIER; + + uint8 decimals; + if (tokenAddress == address(0)) { + decimals = 18; + } else { + (bool success, bytes memory data) = tokenAddress.staticcall( + abi.encodeWithSelector(_ERC20_DECIMALS) + ); + if (success) { + decimals = abi.decode(data, (uint8)); + } + } + require( + decimals < 77, + "InstantWithdrawManager::_token2USD: TOKEN_DECIMALS_OVERFLOW" + ); + return baseUnitTokenUSD / (10**uint256(decimals)); + } + + function _findBucketIdx(uint256 amountUSD) internal view returns (int256) { + for (int256 i = 0; i < int256(nBuckets); i++) { + uint256 ceilUSD = buckets[i] & 0xFFFFFFFF_FFFFFFFF_FFFFFFFF; + if ( + (amountUSD <= ceilUSD) || + (ceilUSD == 0xFFFFFFFF_FFFFFFFF_FFFFFFFF) + ) { + return i; + } + } + return -1; + } + + function unpackBucket(uint256 bucket) + public + pure + returns ( + uint256 ceilUSD, + uint256 blockStamp, + uint256 withdrawals, + uint256 rateBlocks, + uint256 rateWithdrawals, + uint256 maxWithdrawals + ) + { + ceilUSD = bucket & 0xFFFFFFFF_FFFFFFFF_FFFFFFFF; + blockStamp = (bucket >> 96) & 0xFFFFFFFF; + withdrawals = (bucket >> 128) & 0xFFFFFFFF; + rateBlocks = (bucket >> 160) & 0xFFFFFFFF; + rateWithdrawals = (bucket >> 192) & 0xFFFFFFFF; + maxWithdrawals = (bucket >> 224) & 0xFFFFFFFF; + } + + function packBucket( + uint256 ceilUSD, + uint256 blockStamp, + uint256 withdrawals, + uint256 rateBlocks, + uint256 rateWithdrawals, + uint256 maxWithdrawals + ) public pure returns (uint256 ret) { + ret = + ceilUSD | + (blockStamp << 96) | + (withdrawals << 128) | + (rateBlocks << 160) | + (rateWithdrawals << 192) | + (maxWithdrawals << 224); + } +} diff --git a/contracts/hermez/future_versions/lib/verifier2048.sol b/contracts/hermez/future_versions/lib/verifier2048.sol new file mode 100644 index 0000000..66a4bf6 --- /dev/null +++ b/contracts/hermez/future_versions/lib/verifier2048.sol @@ -0,0 +1,230 @@ +// SPDX-License-Identifier: AGPL-3.0 + +// +// Copyright 2017 Christian Reitwiessner +// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: +// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// +// 2019 OKIMS +// ported to solidity 0.6 +// fixed linter warnings +// added requiere error messages +// +// +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.6.11; +library Pairing { + struct G1Point { + uint X; + uint Y; + } + // Encoding of field elements is: X[0] * z + X[1] + struct G2Point { + uint[2] X; + uint[2] Y; + } + /// @return the generator of G1 + function P1() internal pure returns (G1Point memory) { + return G1Point(1, 2); + } + /// @return the generator of G2 + function P2() internal pure returns (G2Point memory) { + // Original code point + return G2Point( + [11559732032986387107991004021392285783925812861821192530917403151452391805634, + 10857046999023057135944570762232829481370756359578518086990519993285655852781], + [4082367875863433681332203403145435568316851327593401208105741076214120093531, + 8495653923123431417604973247489272438418190587263600148770280649306958101930] + ); + +/* + // Changed by Jordi point + return G2Point( + [10857046999023057135944570762232829481370756359578518086990519993285655852781, + 11559732032986387107991004021392285783925812861821192530917403151452391805634], + [8495653923123431417604973247489272438418190587263600148770280649306958101930, + 4082367875863433681332203403145435568316851327593401208105741076214120093531] + ); +*/ + } + /// @return r the negation of p, i.e. p.addition(p.negate()) should be zero. + function negate(G1Point memory p) internal pure returns (G1Point memory r) { + // The prime q in the base field F_q for G1 + uint q = 21888242871839275222246405745257275088696311157297823662689037894645226208583; + if (p.X == 0 && p.Y == 0) + return G1Point(0, 0); + return G1Point(p.X, q - (p.Y % q)); + } + /// @return r the sum of two points of G1 + function addition(G1Point memory p1, G1Point memory p2) internal view returns (G1Point memory r) { + uint[4] memory input; + input[0] = p1.X; + input[1] = p1.Y; + input[2] = p2.X; + input[3] = p2.Y; + bool success; + // solium-disable-next-line security/no-inline-assembly + assembly { + success := staticcall(sub(gas(), 2000), 6, input, 0xc0, r, 0x60) + // Use "invalid" to make gas estimation work + switch success case 0 { invalid() } + } + require(success,"pairing-add-failed"); + } + /// @return r the product of a point on G1 and a scalar, i.e. + /// p == p.scalar_mul(1) and p.addition(p) == p.scalar_mul(2) for all points p. + function scalar_mul(G1Point memory p, uint s) internal view returns (G1Point memory r) { + uint[3] memory input; + input[0] = p.X; + input[1] = p.Y; + input[2] = s; + bool success; + // solium-disable-next-line security/no-inline-assembly + assembly { + success := staticcall(sub(gas(), 2000), 7, input, 0x80, r, 0x60) + // Use "invalid" to make gas estimation work + switch success case 0 { invalid() } + } + require (success,"pairing-mul-failed"); + } + /// @return the result of computing the pairing check + /// e(p1[0], p2[0]) * .... * e(p1[n], p2[n]) == 1 + /// For example pairing([P1(), P1().negate()], [P2(), P2()]) should + /// return true. + function pairing(G1Point[] memory p1, G2Point[] memory p2) internal view returns (bool) { + require(p1.length == p2.length,"pairing-lengths-failed"); + uint elements = p1.length; + uint inputSize = elements * 6; + uint[] memory input = new uint[](inputSize); + for (uint i = 0; i < elements; i++) + { + input[i * 6 + 0] = p1[i].X; + input[i * 6 + 1] = p1[i].Y; + input[i * 6 + 2] = p2[i].X[0]; + input[i * 6 + 3] = p2[i].X[1]; + input[i * 6 + 4] = p2[i].Y[0]; + input[i * 6 + 5] = p2[i].Y[1]; + } + uint[1] memory out; + bool success; + // solium-disable-next-line security/no-inline-assembly + assembly { + success := staticcall(sub(gas(), 2000), 8, add(input, 0x20), mul(inputSize, 0x20), out, 0x20) + // Use "invalid" to make gas estimation work + switch success case 0 { invalid() } + } + require(success,"pairing-opcode-failed"); + return out[0] != 0; + } + /// Convenience method for a pairing check for two pairs. + function pairingProd2(G1Point memory a1, G2Point memory a2, G1Point memory b1, G2Point memory b2) internal view returns (bool) { + G1Point[] memory p1 = new G1Point[](2); + G2Point[] memory p2 = new G2Point[](2); + p1[0] = a1; + p1[1] = b1; + p2[0] = a2; + p2[1] = b2; + return pairing(p1, p2); + } + /// Convenience method for a pairing check for three pairs. + function pairingProd3( + G1Point memory a1, G2Point memory a2, + G1Point memory b1, G2Point memory b2, + G1Point memory c1, G2Point memory c2 + ) internal view returns (bool) { + G1Point[] memory p1 = new G1Point[](3); + G2Point[] memory p2 = new G2Point[](3); + p1[0] = a1; + p1[1] = b1; + p1[2] = c1; + p2[0] = a2; + p2[1] = b2; + p2[2] = c2; + return pairing(p1, p2); + } + /// Convenience method for a pairing check for four pairs. + function pairingProd4( + G1Point memory a1, G2Point memory a2, + G1Point memory b1, G2Point memory b2, + G1Point memory c1, G2Point memory c2, + G1Point memory d1, G2Point memory d2 + ) internal view returns (bool) { + G1Point[] memory p1 = new G1Point[](4); + G2Point[] memory p2 = new G2Point[](4); + p1[0] = a1; + p1[1] = b1; + p1[2] = c1; + p1[3] = d1; + p2[0] = a2; + p2[1] = b2; + p2[2] = c2; + p2[3] = d2; + return pairing(p1, p2); + } +} +contract Verifier2048 { + using Pairing for *; + struct VerifyingKey { + Pairing.G1Point alfa1; + Pairing.G2Point beta2; + Pairing.G2Point gamma2; + Pairing.G2Point delta2; + Pairing.G1Point[] IC; + } + struct Proof { + Pairing.G1Point A; + Pairing.G2Point B; + Pairing.G1Point C; + } + function verifyingKey() internal pure returns (VerifyingKey memory vk) { + vk.alfa1 = Pairing.G1Point(20491192805390485299153009773594534940189261866228447918068658471970481763042,9383485363053290200918347156157836566562967994039712273449902621266178545958); + vk.beta2 = Pairing.G2Point([4252822878758300859123897981450591353533073413197771768651442665752259397132,6375614351688725206403948262868962793625744043794305715222011528459656738731], [21847035105528745403288232691147584728191162732299865338377159692350059136679,10505242626370262277552901082094356697409835680220590971873171140371331206856]); + vk.gamma2 = Pairing.G2Point([11559732032986387107991004021392285783925812861821192530917403151452391805634,10857046999023057135944570762232829481370756359578518086990519993285655852781], [4082367875863433681332203403145435568316851327593401208105741076214120093531,8495653923123431417604973247489272438418190587263600148770280649306958101930]); + vk.delta2 = Pairing.G2Point([8673164060677281422587901318357659867637935104233158996922122333146422237279,15563606032886964562804201493869545205619605857235454492122674907669376786878], [21269741638597126538493965795979688497186419692312587368172693524653500777446,11343431118305679115277934389219528358138675191033559078776998239638351310525]); + vk.IC = new Pairing.G1Point[](2); + vk.IC[0] = Pairing.G1Point(12119879304289515489450377772347995525642865047183452365268219414006773541285,3840793162571622921461450436086221374888135251116660291777965438473955890781); + vk.IC[1] = Pairing.G1Point(6662037772606761817213857807109518411822872260104083390346489534101781424172,10748861837349076691520056115569377681382704488493136534983327823574044025082); + + } + function verify(uint[] memory input, Proof memory proof) internal view returns (uint) { + uint256 snark_scalar_field = 21888242871839275222246405745257275088548364400416034343698204186575808495617; + VerifyingKey memory vk = verifyingKey(); + require(input.length + 1 == vk.IC.length,"verifier-bad-input"); + // Compute the linear combination vk_x + Pairing.G1Point memory vk_x = Pairing.G1Point(0, 0); + for (uint i = 0; i < input.length; i++) { + require(input[i] < snark_scalar_field,"verifier-gte-snark-scalar-field"); + vk_x = Pairing.addition(vk_x, Pairing.scalar_mul(vk.IC[i + 1], input[i])); + } + vk_x = Pairing.addition(vk_x, vk.IC[0]); + if (!Pairing.pairingProd4( + Pairing.negate(proof.A), proof.B, + vk.alfa1, vk.beta2, + vk_x, vk.gamma2, + proof.C, vk.delta2 + )) return 1; + return 0; + } + /// @return r bool true if proof is valid + function verifyProof( + uint[2] memory a, + uint[2][2] memory b, + uint[2] memory c, + uint[1] memory input + ) public view returns (bool r) { + Proof memory proof; + proof.A = Pairing.G1Point(a[0], a[1]); + proof.B = Pairing.G2Point([b[0][0], b[0][1]], [b[1][0], b[1][1]]); + proof.C = Pairing.G1Point(c[0], c[1]); + uint[] memory inputValues = new uint[](input.length); + for(uint i = 0; i < input.length; i++){ + inputValues[i] = input[i]; + } + if (verify(inputValues, proof) == 0) { + return true; + } else { + return false; + } + } +} diff --git a/contracts/hermez/future_versions/lib/verifier400.sol b/contracts/hermez/future_versions/lib/verifier400.sol new file mode 100644 index 0000000..9e5f32f --- /dev/null +++ b/contracts/hermez/future_versions/lib/verifier400.sol @@ -0,0 +1,230 @@ +// SPDX-License-Identifier: AGPL-3.0 + +// +// Copyright 2017 Christian Reitwiessner +// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: +// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// +// 2019 OKIMS +// ported to solidity 0.6 +// fixed linter warnings +// added requiere error messages +// +// +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.6.11; +library Pairing { + struct G1Point { + uint X; + uint Y; + } + // Encoding of field elements is: X[0] * z + X[1] + struct G2Point { + uint[2] X; + uint[2] Y; + } + /// @return the generator of G1 + function P1() internal pure returns (G1Point memory) { + return G1Point(1, 2); + } + /// @return the generator of G2 + function P2() internal pure returns (G2Point memory) { + // Original code point + return G2Point( + [11559732032986387107991004021392285783925812861821192530917403151452391805634, + 10857046999023057135944570762232829481370756359578518086990519993285655852781], + [4082367875863433681332203403145435568316851327593401208105741076214120093531, + 8495653923123431417604973247489272438418190587263600148770280649306958101930] + ); + +/* + // Changed by Jordi point + return G2Point( + [10857046999023057135944570762232829481370756359578518086990519993285655852781, + 11559732032986387107991004021392285783925812861821192530917403151452391805634], + [8495653923123431417604973247489272438418190587263600148770280649306958101930, + 4082367875863433681332203403145435568316851327593401208105741076214120093531] + ); +*/ + } + /// @return r the negation of p, i.e. p.addition(p.negate()) should be zero. + function negate(G1Point memory p) internal pure returns (G1Point memory r) { + // The prime q in the base field F_q for G1 + uint q = 21888242871839275222246405745257275088696311157297823662689037894645226208583; + if (p.X == 0 && p.Y == 0) + return G1Point(0, 0); + return G1Point(p.X, q - (p.Y % q)); + } + /// @return r the sum of two points of G1 + function addition(G1Point memory p1, G1Point memory p2) internal view returns (G1Point memory r) { + uint[4] memory input; + input[0] = p1.X; + input[1] = p1.Y; + input[2] = p2.X; + input[3] = p2.Y; + bool success; + // solium-disable-next-line security/no-inline-assembly + assembly { + success := staticcall(sub(gas(), 2000), 6, input, 0xc0, r, 0x60) + // Use "invalid" to make gas estimation work + switch success case 0 { invalid() } + } + require(success,"pairing-add-failed"); + } + /// @return r the product of a point on G1 and a scalar, i.e. + /// p == p.scalar_mul(1) and p.addition(p) == p.scalar_mul(2) for all points p. + function scalar_mul(G1Point memory p, uint s) internal view returns (G1Point memory r) { + uint[3] memory input; + input[0] = p.X; + input[1] = p.Y; + input[2] = s; + bool success; + // solium-disable-next-line security/no-inline-assembly + assembly { + success := staticcall(sub(gas(), 2000), 7, input, 0x80, r, 0x60) + // Use "invalid" to make gas estimation work + switch success case 0 { invalid() } + } + require (success,"pairing-mul-failed"); + } + /// @return the result of computing the pairing check + /// e(p1[0], p2[0]) * .... * e(p1[n], p2[n]) == 1 + /// For example pairing([P1(), P1().negate()], [P2(), P2()]) should + /// return true. + function pairing(G1Point[] memory p1, G2Point[] memory p2) internal view returns (bool) { + require(p1.length == p2.length,"pairing-lengths-failed"); + uint elements = p1.length; + uint inputSize = elements * 6; + uint[] memory input = new uint[](inputSize); + for (uint i = 0; i < elements; i++) + { + input[i * 6 + 0] = p1[i].X; + input[i * 6 + 1] = p1[i].Y; + input[i * 6 + 2] = p2[i].X[0]; + input[i * 6 + 3] = p2[i].X[1]; + input[i * 6 + 4] = p2[i].Y[0]; + input[i * 6 + 5] = p2[i].Y[1]; + } + uint[1] memory out; + bool success; + // solium-disable-next-line security/no-inline-assembly + assembly { + success := staticcall(sub(gas(), 2000), 8, add(input, 0x20), mul(inputSize, 0x20), out, 0x20) + // Use "invalid" to make gas estimation work + switch success case 0 { invalid() } + } + require(success,"pairing-opcode-failed"); + return out[0] != 0; + } + /// Convenience method for a pairing check for two pairs. + function pairingProd2(G1Point memory a1, G2Point memory a2, G1Point memory b1, G2Point memory b2) internal view returns (bool) { + G1Point[] memory p1 = new G1Point[](2); + G2Point[] memory p2 = new G2Point[](2); + p1[0] = a1; + p1[1] = b1; + p2[0] = a2; + p2[1] = b2; + return pairing(p1, p2); + } + /// Convenience method for a pairing check for three pairs. + function pairingProd3( + G1Point memory a1, G2Point memory a2, + G1Point memory b1, G2Point memory b2, + G1Point memory c1, G2Point memory c2 + ) internal view returns (bool) { + G1Point[] memory p1 = new G1Point[](3); + G2Point[] memory p2 = new G2Point[](3); + p1[0] = a1; + p1[1] = b1; + p1[2] = c1; + p2[0] = a2; + p2[1] = b2; + p2[2] = c2; + return pairing(p1, p2); + } + /// Convenience method for a pairing check for four pairs. + function pairingProd4( + G1Point memory a1, G2Point memory a2, + G1Point memory b1, G2Point memory b2, + G1Point memory c1, G2Point memory c2, + G1Point memory d1, G2Point memory d2 + ) internal view returns (bool) { + G1Point[] memory p1 = new G1Point[](4); + G2Point[] memory p2 = new G2Point[](4); + p1[0] = a1; + p1[1] = b1; + p1[2] = c1; + p1[3] = d1; + p2[0] = a2; + p2[1] = b2; + p2[2] = c2; + p2[3] = d2; + return pairing(p1, p2); + } +} +contract Verifier400 { + using Pairing for *; + struct VerifyingKey { + Pairing.G1Point alfa1; + Pairing.G2Point beta2; + Pairing.G2Point gamma2; + Pairing.G2Point delta2; + Pairing.G1Point[] IC; + } + struct Proof { + Pairing.G1Point A; + Pairing.G2Point B; + Pairing.G1Point C; + } + function verifyingKey() internal pure returns (VerifyingKey memory vk) { + vk.alfa1 = Pairing.G1Point(20491192805390485299153009773594534940189261866228447918068658471970481763042,9383485363053290200918347156157836566562967994039712273449902621266178545958); + vk.beta2 = Pairing.G2Point([4252822878758300859123897981450591353533073413197771768651442665752259397132,6375614351688725206403948262868962793625744043794305715222011528459656738731], [21847035105528745403288232691147584728191162732299865338377159692350059136679,10505242626370262277552901082094356697409835680220590971873171140371331206856]); + vk.gamma2 = Pairing.G2Point([11559732032986387107991004021392285783925812861821192530917403151452391805634,10857046999023057135944570762232829481370756359578518086990519993285655852781], [4082367875863433681332203403145435568316851327593401208105741076214120093531,8495653923123431417604973247489272438418190587263600148770280649306958101930]); + vk.delta2 = Pairing.G2Point([14867417606631193217040121898747161918533542783720480382917845205859990140462,7926493964972762282458341276015512236679139532948396383962686052334355500761], [20858360326920742665434502606063893110221975438792458711366325377409050777543,11425724857950841117996881236446784933901515676367441378576550354902227497234]); + vk.IC = new Pairing.G1Point[](2); + vk.IC[0] = Pairing.G1Point(8676718666848112535920078468055614937212033283211953673244901303301166209672,10150383064086068589682883618964870470206361186233206488150581475814478876859); + vk.IC[1] = Pairing.G1Point(18256138410557127475705157972721945040464891389818092951419656649695435740977,4145839438226689110675223672249137322908544158194159193736833789401028673936); + + } + function verify(uint[] memory input, Proof memory proof) internal view returns (uint) { + uint256 snark_scalar_field = 21888242871839275222246405745257275088548364400416034343698204186575808495617; + VerifyingKey memory vk = verifyingKey(); + require(input.length + 1 == vk.IC.length,"verifier-bad-input"); + // Compute the linear combination vk_x + Pairing.G1Point memory vk_x = Pairing.G1Point(0, 0); + for (uint i = 0; i < input.length; i++) { + require(input[i] < snark_scalar_field,"verifier-gte-snark-scalar-field"); + vk_x = Pairing.addition(vk_x, Pairing.scalar_mul(vk.IC[i + 1], input[i])); + } + vk_x = Pairing.addition(vk_x, vk.IC[0]); + if (!Pairing.pairingProd4( + Pairing.negate(proof.A), proof.B, + vk.alfa1, vk.beta2, + vk_x, vk.gamma2, + proof.C, vk.delta2 + )) return 1; + return 0; + } + /// @return r bool true if proof is valid + function verifyProof( + uint[2] memory a, + uint[2][2] memory b, + uint[2] memory c, + uint[1] memory input + ) public view returns (bool r) { + Proof memory proof; + proof.A = Pairing.G1Point(a[0], a[1]); + proof.B = Pairing.G2Point([b[0][0], b[0][1]], [b[1][0], b[1][1]]); + proof.C = Pairing.G1Point(c[0], c[1]); + uint[] memory inputValues = new uint[](input.length); + for(uint i = 0; i < input.length; i++){ + inputValues[i] = input[i]; + } + if (verify(inputValues, proof) == 0) { + return true; + } else { + return false; + } + } +} diff --git a/contracts/hermez/future_versions/lib/verifierWithdrawV2.sol b/contracts/hermez/future_versions/lib/verifierWithdrawV2.sol new file mode 100644 index 0000000..cb7a694 --- /dev/null +++ b/contracts/hermez/future_versions/lib/verifierWithdrawV2.sol @@ -0,0 +1,346 @@ +// SPDX-License-Identifier: AGPL-3.0 + +// +// Copyright 2017 Christian Reitwiessner +// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: +// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// +// 2019 OKIMS +// ported to solidity 0.6 +// fixed linter warnings +// added requiere error messages +// +// +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.6.11; + +library Pairing { + struct G1Point { + uint256 X; + uint256 Y; + } + // Encoding of field elements is: X[0] * z + X[1] + struct G2Point { + uint256[2] X; + uint256[2] Y; + } + + /// @return the generator of G1 + function P1() internal pure returns (G1Point memory) { + return G1Point(1, 2); + } + + /// @return the generator of G2 + function P2() internal pure returns (G2Point memory) { + // Original code point + return + G2Point( + [ + 11559732032986387107991004021392285783925812861821192530917403151452391805634, + 10857046999023057135944570762232829481370756359578518086990519993285655852781 + ], + [ + 4082367875863433681332203403145435568316851327593401208105741076214120093531, + 8495653923123431417604973247489272438418190587263600148770280649306958101930 + ] + ); + + /* + // Changed by Jordi point + return G2Point( + [10857046999023057135944570762232829481370756359578518086990519993285655852781, + 11559732032986387107991004021392285783925812861821192530917403151452391805634], + [8495653923123431417604973247489272438418190587263600148770280649306958101930, + 4082367875863433681332203403145435568316851327593401208105741076214120093531] + ); +*/ + } + + /// @return r the negation of p, i.e. p.addition(p.negate()) should be zero. + function negate(G1Point memory p) internal pure returns (G1Point memory r) { + // The prime q in the base field F_q for G1 + + + uint256 q + = 21888242871839275222246405745257275088696311157297823662689037894645226208583; + if (p.X == 0 && p.Y == 0) return G1Point(0, 0); + return G1Point(p.X, q - (p.Y % q)); + } + + /// @return r the sum of two points of G1 + function addition(G1Point memory p1, G1Point memory p2) + internal + view + returns (G1Point memory r) + { + uint256[4] memory input; + input[0] = p1.X; + input[1] = p1.Y; + input[2] = p2.X; + input[3] = p2.Y; + bool success; + // solium-disable-next-line security/no-inline-assembly + assembly { + success := staticcall(sub(gas(), 2000), 6, input, 0xc0, r, 0x60) + // Use "invalid" to make gas estimation work + switch success + case 0 { + invalid() + } + } + require(success, "pairing-add-failed"); + } + + /// @return r the product of a point on G1 and a scalar, i.e. + /// p == p.scalar_mul(1) and p.addition(p) == p.scalar_mul(2) for all points p. + function scalar_mul(G1Point memory p, uint256 s) + internal + view + returns (G1Point memory r) + { + uint256[3] memory input; + input[0] = p.X; + input[1] = p.Y; + input[2] = s; + bool success; + // solium-disable-next-line security/no-inline-assembly + assembly { + success := staticcall(sub(gas(), 2000), 7, input, 0x80, r, 0x60) + // Use "invalid" to make gas estimation work + switch success + case 0 { + invalid() + } + } + require(success, "pairing-mul-failed"); + } + + /// @return the result of computing the pairing check + /// e(p1[0], p2[0]) * .... * e(p1[n], p2[n]) == 1 + /// For example pairing([P1(), P1().negate()], [P2(), P2()]) should + /// return true. + function pairing(G1Point[] memory p1, G2Point[] memory p2) + internal + view + returns (bool) + { + require(p1.length == p2.length, "pairing-lengths-failed"); + uint256 elements = p1.length; + uint256 inputSize = elements * 6; + uint256[] memory input = new uint256[](inputSize); + for (uint256 i = 0; i < elements; i++) { + input[i * 6 + 0] = p1[i].X; + input[i * 6 + 1] = p1[i].Y; + input[i * 6 + 2] = p2[i].X[0]; + input[i * 6 + 3] = p2[i].X[1]; + input[i * 6 + 4] = p2[i].Y[0]; + input[i * 6 + 5] = p2[i].Y[1]; + } + uint256[1] memory out; + bool success; + // solium-disable-next-line security/no-inline-assembly + assembly { + success := staticcall( + sub(gas(), 2000), + 8, + add(input, 0x20), + mul(inputSize, 0x20), + out, + 0x20 + ) + // Use "invalid" to make gas estimation work + switch success + case 0 { + invalid() + } + } + require(success, "pairing-opcode-failed"); + return out[0] != 0; + } + + /// Convenience method for a pairing check for two pairs. + function pairingProd2( + G1Point memory a1, + G2Point memory a2, + G1Point memory b1, + G2Point memory b2 + ) internal view returns (bool) { + G1Point[] memory p1 = new G1Point[](2); + G2Point[] memory p2 = new G2Point[](2); + p1[0] = a1; + p1[1] = b1; + p2[0] = a2; + p2[1] = b2; + return pairing(p1, p2); + } + + /// Convenience method for a pairing check for three pairs. + function pairingProd3( + G1Point memory a1, + G2Point memory a2, + G1Point memory b1, + G2Point memory b2, + G1Point memory c1, + G2Point memory c2 + ) internal view returns (bool) { + G1Point[] memory p1 = new G1Point[](3); + G2Point[] memory p2 = new G2Point[](3); + p1[0] = a1; + p1[1] = b1; + p1[2] = c1; + p2[0] = a2; + p2[1] = b2; + p2[2] = c2; + return pairing(p1, p2); + } + + /// Convenience method for a pairing check for four pairs. + function pairingProd4( + G1Point memory a1, + G2Point memory a2, + G1Point memory b1, + G2Point memory b2, + G1Point memory c1, + G2Point memory c2, + G1Point memory d1, + G2Point memory d2 + ) internal view returns (bool) { + G1Point[] memory p1 = new G1Point[](4); + G2Point[] memory p2 = new G2Point[](4); + p1[0] = a1; + p1[1] = b1; + p1[2] = c1; + p1[3] = d1; + p2[0] = a2; + p2[1] = b2; + p2[2] = c2; + p2[3] = d2; + return pairing(p1, p2); + } +} + +contract VerifierWithdrawV2 { + using Pairing for *; + struct VerifyingKey { + Pairing.G1Point alfa1; + Pairing.G2Point beta2; + Pairing.G2Point gamma2; + Pairing.G2Point delta2; + Pairing.G1Point[] IC; + } + struct Proof { + Pairing.G1Point A; + Pairing.G2Point B; + Pairing.G1Point C; + } + + function verifyingKey() internal pure returns (VerifyingKey memory vk) { + vk.alfa1 = Pairing.G1Point( + 388810387696872229600535426988510367661191668538620278246358506880151278377, + 17012807097498437657437400703444959258860359960854381092881122887796891799764 + ); + + vk.beta2 = Pairing.G2Point( + [ + 20834219205012105637945707216009277384365408423076044151691072833174688573103, + 13134655269783470578064433288891955017063376221140036407895159623715719034212 + ], + [ + 13123146757901582440742420379387265563202471200050521454662146479289670350870, + 9424872748620702615302289225079312376316964854917092525872944035178853577805 + ] + ); + vk.gamma2 = Pairing.G2Point( + [ + 11559732032986387107991004021392285783925812861821192530917403151452391805634, + 10857046999023057135944570762232829481370756359578518086990519993285655852781 + ], + [ + 4082367875863433681332203403145435568316851327593401208105741076214120093531, + 8495653923123431417604973247489272438418190587263600148770280649306958101930 + ] + ); + vk.delta2 = Pairing.G2Point( + [ + 11559732032986387107991004021392285783925812861821192530917403151452391805634, + 10857046999023057135944570762232829481370756359578518086990519993285655852781 + ], + [ + 4082367875863433681332203403145435568316851327593401208105741076214120093531, + 8495653923123431417604973247489272438418190587263600148770280649306958101930 + ] + ); + vk.IC = new Pairing.G1Point[](2); + + vk.IC[0] = Pairing.G1Point( + 9709798335278487073135934214642788341483364749831766588850420953696105265177, + 6379602209297816575291953207502184116487138674423700332563836533325222425474 + ); + + vk.IC[1] = Pairing.G1Point( + 17228952008706007141311207908959456806520751866178136518819949659318129370336, + 11243267546602369974266049105154296218563916482012084687186341306379428706307 + ); + } + + function verify(uint256[] memory input, Proof memory proof) + internal + view + returns (uint256) + { + + uint256 snark_scalar_field + = 21888242871839275222246405745257275088548364400416034343698204186575808495617; + VerifyingKey memory vk = verifyingKey(); + require(input.length + 1 == vk.IC.length, "verifier-bad-input"); + // Compute the linear combination vk_x + Pairing.G1Point memory vk_x = Pairing.G1Point(0, 0); + for (uint256 i = 0; i < input.length; i++) { + require( + input[i] < snark_scalar_field, + "verifier-gte-snark-scalar-field" + ); + vk_x = Pairing.addition( + vk_x, + Pairing.scalar_mul(vk.IC[i + 1], input[i]) + ); + } + vk_x = Pairing.addition(vk_x, vk.IC[0]); + if ( + !Pairing.pairingProd4( + Pairing.negate(proof.A), + proof.B, + vk.alfa1, + vk.beta2, + vk_x, + vk.gamma2, + proof.C, + vk.delta2 + ) + ) return 1; + return 0; + } + + /// @return r bool true if proof is valid + function verifyProof( + uint256[2] memory a, + uint256[2][2] memory b, + uint256[2] memory c, + uint256[1] memory input + ) public view returns (bool r) { + Proof memory proof; + proof.A = Pairing.G1Point(a[0], a[1]); + proof.B = Pairing.G2Point([b[0][0], b[0][1]], [b[1][0], b[1][1]]); + proof.C = Pairing.G1Point(c[0], c[1]); + uint256[] memory inputValues = new uint256[](input.length); + for (uint256 i = 0; i < input.length; i++) { + inputValues[i] = input[i]; + } + if (verify(inputValues, proof) == 0) { + return true; + } else { + return false; + } + } +} diff --git a/contracts/hermez/future_versions/test/HermezHelpersTest.sol b/contracts/hermez/future_versions/test/HermezHelpersTest.sol new file mode 100644 index 0000000..7cfff49 --- /dev/null +++ b/contracts/hermez/future_versions/test/HermezHelpersTest.sol @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: AGPL-3.0 + +pragma solidity ^0.6.12; + +import "../lib/HermezHelpersV2.sol"; + +contract HermezHelpersTestV2 is HermezHelpersV2 { + constructor() public {} + + function float2FixTest(uint40 float) public pure returns (uint256) { + return _float2Fix(float); + } + + function checkSigTest( + bytes32 babyjub, + bytes32 r, + bytes32 s, + uint8 v + ) public view returns (address) { + return _checkSig(babyjub, r, s, v); + } +} diff --git a/contracts/hermez/future_versions/test/HermezTestV2.sol b/contracts/hermez/future_versions/test/HermezTestV2.sol new file mode 100644 index 0000000..c94a588 --- /dev/null +++ b/contracts/hermez/future_versions/test/HermezTestV2.sol @@ -0,0 +1,170 @@ +// SPDX-License-Identifier: AGPL-3.0 + +pragma solidity ^0.6.12; + +import "../HermezV2.sol"; + +contract HermezTestV2 is HermezV2 { + event ReturnUint256(uint256); + event ReturnBytes(bytes); + + function testVerifyWithdraw( + uint256[2] calldata proofA, + uint256[2][2] calldata proofB, + uint256[2] calldata proofC, + uint256[1] calldata input + ) public view returns (uint256) { + uint256 gasFirst = gasleft(); + withdrawVerifier.verifyProof(proofA, proofB, proofC, input); + uint256 gasLast = gasFirst - gasleft(); + return gasLast; + } + + function setLastIdx(uint48 newLastIdx) public { + lastIdx = newLastIdx; + } + + function getChainID() public view returns (uint256) { + uint256 chainID; + uint256 a = 0 % 6; + assembly { + chainID := chainid() + } + return chainID; + } + + function token2USDTest(address tokenAddress, uint192 amount) + public + view + returns (uint256) + { + return _token2USD(tokenAddress, amount); + } + + function findBucketIdxTest(uint256 amountUSD) public view returns (int256) { + return _findBucketIdx(amountUSD); + } + + function instantWithdrawalTest(address tokenAddress, uint192 amount) + public + { + require( + _processInstantWithdrawal(tokenAddress, amount), + "HermezTest::withdrawMerkleProof: INSTANT_WITHDRAW_WASTED_FOR_THIS_USD_RANGE" + ); + } + + uint256 private constant _L1_USER_BYTES = 72; + + function changeCurrentIdx(uint32 newCurrentIdx) public { + lastIdx = newCurrentIdx; + } + + function calculateInputTest( + uint32 newLastIdx, + uint256 newStRoot, + bytes calldata compressedL1CoordinatorTx, + bytes calldata l2TxsData, + bytes calldata feeIdxCoordinator, + bool l1Batch, + uint8 verifierIdx + ) public { + emit ReturnUint256( + _constructCircuitInput(newLastIdx, newStRoot, l1Batch, verifierIdx) + ); + } + + function forgeGasTest( + uint48 newLastIdx, + uint256 newStRoot, + bytes calldata encodedL1CoordinatorTx, + bytes calldata l1L2TxsData, + bytes calldata feeIdxCoordinator, + uint8 verifierIdx, + bool l1Batch, + uint256[2] calldata proofA, + uint256[2][2] calldata proofB, + uint256[2] calldata proofC + ) public { + // Assure data availability from regular ethereum nodes + // We include this line because it's easier to track the transaction data, as it will never be in an internal TX. + // In general this makes no sense, as callling this function from another smart contract will have to pay the calldata twice. + // But forcing, it avoids having to check. + require( + msg.sender == tx.origin, + "forgeBatch can't be called as a internal transaction" + ); + + // ask the auction if this coordinator is allow to forge + require( + hermezAuctionContract.canForge(msg.sender, block.number) == true, + "auction denied the forge" + ); + + if (!l1Batch) { + require( + block.number < (lastL1L2Batch + forgeL1L2BatchTimeout), // No overflow since forgeL1L2BatchTimeout is an uint8 + "L1L2Batch required" + ); + } + + // calculate input + uint256 input = _constructCircuitInput( + newLastIdx, + newStRoot, + l1Batch, + verifierIdx + ); + + // verify proof + require( + rollupVerifiers[verifierIdx].verifierInterface.verifyProof( + proofA, + proofB, + proofC, + [input] + ), + "invalid rollup proof" + ); + + // update state + lastForgedBatch++; + lastIdx = newLastIdx; + stateRootMap[lastForgedBatch] = newStRoot; + + uint16 l1UserTxsLen; + if (l1Batch) { + // restart the timeout + lastL1L2Batch = uint64(block.number); + // clear current queue + l1UserTxsLen = _clearQueue(); + } + + // auction must be aware that a batch is being forged + hermezAuctionContract.forge(msg.sender); + + emit ForgeBatch(lastForgedBatch, l1UserTxsLen); + emit ReturnUint256(gasleft()); + } + + function handleL1QueueTest( + uint48 newLastIdx, + uint256 newStRoot, + bytes calldata encodedL1CoordinatorTx, + bytes calldata l1L2TxsData, + bytes calldata feeIdxCoordinator, + uint8 verifierIdx, + bool l1Batch, + uint256[2] calldata proofA, + uint256[2][2] calldata proofB, + uint256[2] calldata proofC + ) public { + uint256 ptr; + bytes memory res = new bytes(_MAX_L1_TX * _L1_USER_TOTALBYTES); + assembly { + ptr := add(res, 0x20) + } + _buildL1Data(ptr, l1Batch); + emit ReturnBytes(res); + } +} diff --git a/contracts/hermez/future_versions/test/HermezV2MockV2.sol b/contracts/hermez/future_versions/test/HermezV2MockV2.sol new file mode 100644 index 0000000..667bc30 --- /dev/null +++ b/contracts/hermez/future_versions/test/HermezV2MockV2.sol @@ -0,0 +1,1143 @@ +// SPDX-License-Identifier: AGPL-3.0 + +pragma solidity 0.6.12; + +import "../lib/InstantWithdrawManagerV2.sol"; +import "../interfaces/VerifierRollupInterface.sol"; +import "../interfaces/VerifierWithdrawInterface.sol"; +import "../../../interfaces/IHermezAuctionProtocol.sol"; +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +contract HermezV2MockV2 is InstantWithdrawManagerV2 { + struct VerifierRollup { + VerifierRollupInterface verifierInterface; + uint256 maxTx; // maximum rollup transactions in a batch: L2-tx + L1-tx transactions + uint256 nLevels; // number of levels of the circuit + } + + // ERC20 signatures: + + // bytes4(keccak256(bytes("transfer(address,uint256)"))); + bytes4 constant _TRANSFER_SIGNATURE = 0xa9059cbb; + + // bytes4(keccak256(bytes("transferFrom(address,address,uint256)"))); + bytes4 constant _TRANSFER_FROM_SIGNATURE = 0x23b872dd; + + // bytes4(keccak256(bytes("approve(address,uint256)"))); + bytes4 constant _APPROVE_SIGNATURE = 0x095ea7b3; + + // ERC20 extensions: + + // bytes4(keccak256(bytes("permit(address,address,uint256,uint256,uint8,bytes32,bytes32)"))); + bytes4 constant _PERMIT_SIGNATURE = 0xd505accf; + + // First 256 indexes reserved, first user index will be the 256 + uint48 constant _RESERVED_IDX = 255; + + // IDX 1 is reserved for exits + uint48 constant _EXIT_IDX = 1; + + // Max load amount allowed (loadAmount: L1 --> L2) + uint256 constant _LIMIT_LOAD_AMOUNT = (1 << 128); + + // Max amount allowed (amount L2 --> L2) + uint256 constant _LIMIT_L2TRANSFER_AMOUNT = (1 << 192); + + // Max number of tokens allowed to be registered inside the rollup + uint256 constant _LIMIT_TOKENS = (1 << 32); + + // [65 bytes] compressedSignature + [32 bytes] fromBjj-compressed + [4 bytes] tokenId + uint256 constant _L1_COORDINATOR_TOTALBYTES = 101; + + // [20 bytes] fromEthAddr + [32 bytes] fromBjj-compressed + [6 bytes] fromIdx + + // [5 bytes] loadAmountFloat40 + [5 bytes] amountFloat40 + [4 bytes] tokenId + [6 bytes] toIdx + uint256 constant _L1_USER_TOTALBYTES = 78; + + // User TXs are the TX made by the user with a L1 TX + // Coordinator TXs are the L2 account creation made by the coordinator whose signature + // needs to be verified in L1. + // The maximum number of L1-user TXs and L1-coordinartor-TX is limited by the _MAX_L1_TX + // And the maximum User TX is _MAX_L1_USER_TX + + // Maximum L1-user transactions allowed to be queued in a batch + uint256 constant _MAX_L1_USER_TX = 128; + + // Maximum L1 transactions allowed to be queued in a batch + uint256 constant _MAX_L1_TX = 256; + + // Modulus zkSNARK + uint256 constant _RFIELD = 21888242871839275222246405745257275088548364400416034343698204186575808495617; + + // [6 bytes] lastIdx + [6 bytes] newLastIdx + [32 bytes] stateRoot + [32 bytes] newStRoot + + // [_MAX_L1_TX * _L1_USER_TOTALBYTES bytes] l1TxsData + totall1L2TxsDataLength + feeIdxCoordinatorLength + [2 bytes] chainID + [4 bytes] batchNum = + // 18546 bytes + totall1L2TxsDataLength + feeIdxCoordinatorLength + + uint256 constant _INPUT_SHA_CONSTANT_BYTES = 20050; + + uint8 public constant ABSOLUTE_MAX_L1L2BATCHTIMEOUT = 240; + + // This ethereum address is used internally for rollup accounts that don't have ethereum address, only Babyjubjub + // This non-ethereum accounts can be created by the coordinator and allow users to have a rollup + // account without needing an ethereum address + address constant _ETH_ADDRESS_INTERNAL_ONLY = address( + 0xFFfFfFffFFfffFFfFFfFFFFFffFFFffffFfFFFfF + ); + + // Verifiers array + VerifierRollup[] public rollupVerifiers; + + // Withdraw verifier interface + VerifierWithdrawInterface public withdrawVerifier; + + // Last account index created inside the rollup + uint48 public lastIdx; + + // Last batch forged + uint32 public lastForgedBatch; + + // Each batch forged will have a correlated 'state root' + mapping(uint32 => uint256) public stateRootMap; + + // Mapping of exit amounts, store all the amount already withdrawed of every account + mapping(uint48 => uint256) public exitAccumulateMap; + + // List of ERC20 tokens that can be used in rollup + // ID = 0 will be reserved for ether + address[] public tokenList; + + // Mapping addres of the token, with the tokenID associated + mapping(address => uint256) public tokenMap; + + // Fee for adding a new token to the rollup in HEZ tokens + uint256 public feeAddToken; + + // Contract interface of the hermez auction + IHermezAuctionProtocol public hermezAuctionContract; + + // Map of queues of L1-user-tx transactions, the transactions are stored in bytes32 sequentially + // The coordinator is forced to forge the next queue in the next L1-L2-batch + mapping(uint32 => bytes) public mapL1TxQueue; + + // Ethereum block where the last L1-L2-batch was forged + uint64 public lastL1L2Batch; + + // Queue index that will be forged in the next L1-L2-batch + uint32 public nextL1ToForgeQueue; + + // Queue index wich will be filled with the following L1-User-Tx + uint32 public nextL1FillingQueue; + + // Max ethereum blocks after the last L1-L2-batch, when exceeds the timeout only L1-L2-batch are allowed + uint8 public forgeL1L2BatchTimeout; + + // HEZ token address + address public tokenHEZ; + + // upgradability test + uint256 public version; + + // Event emitted when a L1-user transaction is called and added to the nextL1FillingQueue queue + event L1UserTxEvent( + uint32 indexed queueIndex, + uint8 indexed position, // Position inside the queue where the TX resides + bytes l1UserTx + ); + + // Event emitted when a new token is added + event AddToken(address indexed tokenAddress, uint32 tokenID); + + // Event emitted every time a batch is forged + event ForgeBatch(uint32 indexed batchNum, uint16 l1UserTxsLen); + + // Event emitted when the governance update the `forgeL1L2BatchTimeout` + event UpdateForgeL1L2BatchTimeout(uint8 newForgeL1L2BatchTimeout); + + // Event emitted when the governance update the `feeAddToken` + event UpdateFeeAddToken(uint256 newFeeAddToken); + + // Event emitted when a withdrawal is done + event WithdrawEvent( + uint256 indexed amountWithdraw, + uint48 indexed idx, + bool indexed instantWithdraw + ); + + // Event emitted when the contract is initialized + event InitializeHermezEvent( + uint8 forgeL1L2BatchTimeout, + uint256 feeAddToken, + uint64 withdrawalDelay + ); + + // upgradability test + function setVersion() public { + version = 2; + } + + function getVersion() external view returns (uint256) { + return version; + } + + /** + * @dev Initializer function (equivalent to the constructor). Since we use + * upgradeable smartcontracts the state vars have to be initialized here. + */ + function initializeHermez( + address[] memory _verifiers, + uint256[] memory _verifiersParams, + address _withdrawVerifier, + address _hermezAuctionContract, + address _tokenHEZ, + uint8 _forgeL1L2BatchTimeout, + uint256 _feeAddToken, + address _hermezGovernanceAddress, + uint64 _withdrawalDelay, + address _withdrawDelayerContract + ) external initializer { + require( + _hermezAuctionContract != address(0) && + _withdrawDelayerContract != address(0), + "Hermez::initializeHermez ADDRESS_0_NOT_VALID" + ); + + // set state variables + _initializeVerifiers(_verifiers, _verifiersParams); + withdrawVerifier = VerifierWithdrawInterface(_withdrawVerifier); + hermezAuctionContract = IHermezAuctionProtocol(_hermezAuctionContract); + tokenHEZ = _tokenHEZ; + forgeL1L2BatchTimeout = _forgeL1L2BatchTimeout; + feeAddToken = _feeAddToken; + + // set default state variables + lastIdx = _RESERVED_IDX; + // lastL1L2Batch = 0 --> first batch forced to be L1Batch + // nextL1ToForgeQueue = 0 --> First queue will be forged + nextL1FillingQueue = 1; + // stateRootMap[0] = 0 --> genesis batch will have root = 0 + tokenList.push(address(0)); // Token 0 is ETH + + _initializeWithdraw( + _hermezGovernanceAddress, + _withdrawalDelay, + _withdrawDelayerContract + ); + emit InitializeHermezEvent( + _forgeL1L2BatchTimeout, + _feeAddToken, + _withdrawalDelay + ); + } + + ////////////// + // Coordinator operations + ///////////// + + /** + * @dev Forge a new batch providing the L2 Transactions, L1Corrdinator transactions and the proof. + * If the proof is succesfully verified, update the current state, adding a new state. + * In order to optimize the gas consumption the parameters `encodedL1CoordinatorTx`, `l1L2TxsData` and `feeIdxCoordinator` + * are read directly from the calldata using assembly with the instruction `calldatacopy` + * @param newLastIdx New total rollup accounts + * @param newStRoot New state root + * @param encodedL1CoordinatorTx Encoded L1-coordinator transactions + * @param l1L2TxsData Encoded l2 data + * @param feeIdxCoordinator Encoded idx accounts of the coordinator where the fees will be payed + * @param verifierIdx Verifier index + * @param l1Batch Indicates if this batch will be L2 or L1-L2 + * @param proofA zk-snark input + * @param proofB zk-snark input + * @param proofC zk-snark input + * Events: `ForgeBatch` + */ + function forgeBatch( + uint48 newLastIdx, + uint256 newStRoot, + bytes calldata encodedL1CoordinatorTx, + bytes calldata l1L2TxsData, + bytes calldata feeIdxCoordinator, + uint8 verifierIdx, + bool l1Batch, + uint256[2] calldata proofA, + uint256[2][2] calldata proofB, + uint256[2] calldata proofC + ) external virtual { + // Assure data availability from regular ethereum nodes + // We include this line because it's easier to track the transaction data, as it will never be in an internal TX. + // In general this makes no sense, as callling this function from another smart contract will have to pay the calldata twice. + // But forcing, it avoids having to check. + require( + msg.sender == tx.origin, + "Hermez::forgeBatch: INTENAL_TX_NOT_ALLOWED" + ); + + // ask the auction if this coordinator is allow to forge + require( + hermezAuctionContract.canForge(msg.sender, block.number) == true, + "Hermez::forgeBatch: AUCTION_DENIED" + ); + + if (!l1Batch) { + require( + block.number < (lastL1L2Batch + forgeL1L2BatchTimeout), // No overflow since forgeL1L2BatchTimeout is an uint8 + "Hermez::forgeBatch: L1L2BATCH_REQUIRED" + ); + } + + // calculate input + uint256 input = _constructCircuitInput( + newLastIdx, + newStRoot, + l1Batch, + verifierIdx + ); + + // verify proof + require( + rollupVerifiers[verifierIdx].verifierInterface.verifyProof( + proofA, + proofB, + proofC, + [input] + ), + "Hermez::forgeBatch: INVALID_PROOF" + ); + + // update state + lastForgedBatch++; + lastIdx = newLastIdx; + stateRootMap[lastForgedBatch] = newStRoot; + + uint16 l1UserTxsLen; + if (l1Batch) { + // restart the timeout + lastL1L2Batch = uint64(block.number); + // clear current queue + l1UserTxsLen = _clearQueue(); + } + + // auction must be aware that a batch is being forged + hermezAuctionContract.forge(msg.sender); + + emit ForgeBatch(lastForgedBatch, l1UserTxsLen); + } + + ////////////// + // User L1 rollup tx + ///////////// + + // This are all the possible L1-User transactions: + // | fromIdx | toIdx | loadAmountF | amountF | tokenID(SC) | babyPubKey | l1-user-TX | + // |:-------:|:-----:|:-----------:|:-------:|:-----------:|:----------:|:-------------------------------:| + // | 0 | 0 | 0 | 0(SC) | X | !=0(SC) | createAccount | + // | 0 | 0 | !=0 | 0(SC) | X | !=0(SC) | createAccountDeposit | + // | 0 | 255+ | X | X | X | !=0(SC) | createAccountDepositAndTransfer | + // | 255+ | 0 | X | 0(SC) | X | 0(SC) | Deposit | + // | 255+ | 1 | 0 | X | X | 0(SC) | Exit | + // | 255+ | 255+ | 0 | X | X | 0(SC) | Transfer | + // | 255+ | 255+ | !=0 | X | X | 0(SC) | DepositAndTransfer | + // As can be seen in the table the type of transaction is determined basically by the "fromIdx" and "toIdx" + // The 'X' means that can be any valid value and does not change the l1-user-tx type + // Other parameters must be consistent, for example, if toIdx is 0, amountF must be 0, because there's no L2 transfer + + /** + * @dev Create a new rollup l1 user transaction + * @param babyPubKey Public key babyjubjub represented as point: sign + (Ay) + * @param fromIdx Index leaf of sender account or 0 if create new account + * @param loadAmountF Amount from L1 to L2 to sender account or new account + * @param amountF Amount transfered between L2 accounts + * @param tokenID Token identifier + * @param toIdx Index leaf of recipient account, or _EXIT_IDX if exit, or 0 if not transfer + * Events: `L1UserTxEvent` + */ + function addL1Transaction( + uint256 babyPubKey, + uint48 fromIdx, + uint40 loadAmountF, + uint40 amountF, + uint32 tokenID, + uint48 toIdx, + bytes calldata permit + ) external payable { + // check tokenID + require( + tokenID < tokenList.length, + "Hermez::addL1Transaction: TOKEN_NOT_REGISTERED" + ); + + // check loadAmount + uint256 loadAmount = _float2Fix(loadAmountF); + require( + loadAmount < _LIMIT_LOAD_AMOUNT, + "Hermez::addL1Transaction: LOADAMOUNT_EXCEED_LIMIT" + ); + + // deposit token or ether + if (loadAmount > 0) { + if (tokenID == 0) { + require( + loadAmount == msg.value, + "Hermez::addL1Transaction: LOADAMOUNT_ETH_DOES_NOT_MATCH" + ); + } else { + require( + msg.value == 0, + "Hermez::addL1Transaction: MSG_VALUE_NOT_EQUAL_0" + ); + if (permit.length != 0) { + _permit(tokenList[tokenID], loadAmount, permit); + } + uint256 prevBalance = IERC20(tokenList[tokenID]).balanceOf( + address(this) + ); + _safeTransferFrom( + tokenList[tokenID], + msg.sender, + address(this), + loadAmount + ); + uint256 postBalance = IERC20(tokenList[tokenID]).balanceOf( + address(this) + ); + require( + postBalance - prevBalance == loadAmount, + "Hermez::addL1Transaction: LOADAMOUNT_ERC20_DOES_NOT_MATCH" + ); + } + } + + // perform L1 User Tx + _addL1Transaction( + msg.sender, + babyPubKey, + fromIdx, + loadAmountF, + amountF, + tokenID, + toIdx + ); + } + + /** + * @dev Create a new rollup l1 user transaction + * @param ethAddress Ethereum addres of the sender account or new account + * @param babyPubKey Public key babyjubjub represented as point: sign + (Ay) + * @param fromIdx Index leaf of sender account or 0 if create new account + * @param loadAmountF Amount from L1 to L2 to sender account or new account + * @param amountF Amount transfered between L2 accounts + * @param tokenID Token identifier + * @param toIdx Index leaf of recipient account, or _EXIT_IDX if exit, or 0 if not transfer + * Events: `L1UserTxEvent` + */ + function _addL1Transaction( + address ethAddress, + uint256 babyPubKey, + uint48 fromIdx, + uint40 loadAmountF, + uint40 amountF, + uint32 tokenID, + uint48 toIdx + ) internal { + uint256 amount = _float2Fix(amountF); + require( + amount < _LIMIT_L2TRANSFER_AMOUNT, + "Hermez::_addL1Transaction: AMOUNT_EXCEED_LIMIT" + ); + + // toIdx can be: 0, _EXIT_IDX or (toIdx > _RESERVED_IDX) + if (toIdx == 0) { + require( + (amount == 0), + "Hermez::_addL1Transaction: AMOUNT_MUST_BE_0_IF_NOT_TRANSFER" + ); + } else { + if ((toIdx == _EXIT_IDX)) { + require( + (loadAmountF == 0), + "Hermez::_addL1Transaction: LOADAMOUNT_MUST_BE_0_IF_EXIT" + ); + } else { + require( + ((toIdx > _RESERVED_IDX) && (toIdx <= lastIdx)), + "Hermez::_addL1Transaction: INVALID_TOIDX" + ); + } + } + // fromIdx can be: 0 if create account or (fromIdx > _RESERVED_IDX) + if (fromIdx == 0) { + require( + babyPubKey != 0, + "Hermez::_addL1Transaction: INVALID_CREATE_ACCOUNT_WITH_NO_BABYJUB" + ); + } else { + require( + (fromIdx > _RESERVED_IDX) && (fromIdx <= lastIdx), + "Hermez::_addL1Transaction: INVALID_FROMIDX" + ); + require( + babyPubKey == 0, + "Hermez::_addL1Transaction: BABYJUB_MUST_BE_0_IF_NOT_CREATE_ACCOUNT" + ); + } + + _l1QueueAddTx( + ethAddress, + babyPubKey, + fromIdx, + loadAmountF, + amountF, + tokenID, + toIdx + ); + } + + /** + * @dev Withdraw to retrieve the tokens from the exit tree to the owner account + * Before this call an exit transaction must be done + * @param proofA zk-snark input + * @param proofB zk-snark input + * @param proofC zk-snark input + * @param tokenID Token identifier + * @param amountExit Amount exit of the leaf + * @param amountWithdraw Amount to withdraw + * @param batchNum Batch number after exit transactions has been done + * @param idx Index of the exit tree account + * @param instantWithdraw true if is an instant withdraw + * Events: `WithdrawEvent` + */ + function withdrawCircuit( + uint256[2] calldata proofA, + uint256[2][2] calldata proofB, + uint256[2] calldata proofC, + uint32 tokenID, + uint192 amountExit, + uint192 amountWithdraw, + uint32 batchNum, + uint48 idx, + bool instantWithdraw + ) external { + // in case of instant withdraw assure that is available + if (instantWithdraw) { + require( + _processInstantWithdrawal(tokenList[tokenID], amountWithdraw), + "Hermez::withdrawCircuit: INSTANT_WITHDRAW_WASTED_FOR_THIS_USD_RANGE" + ); + } + require( + amountWithdraw <= uint256(amountExit).sub(exitAccumulateMap[idx]), + "Hermez::withdrawCircuit: AMOUNT_WITHDRAW_LESS_THAN_ACCUMULATED" + ); + + // get exit root given its index depth + uint256 stateRoot = stateRootMap[batchNum]; + + uint256 input = uint256( + sha256( + abi.encodePacked( + stateRoot, + msg.sender, + tokenID, + amountExit, + idx + ) + ) + ) % _RFIELD; + // verify zk-snark circuit + require( + withdrawVerifier.verifyProof(proofA, proofB, proofC, [input]) == + true, + "Hermez::withdrawCircuit: INVALID_ZK_PROOF" + ); + + // set nullifier + exitAccumulateMap[idx] += amountWithdraw; + + _withdrawFunds(amountWithdraw, tokenID, instantWithdraw); + + emit WithdrawEvent(amountWithdraw, idx, instantWithdraw); + } + + ////////////// + // Governance methods + ///////////// + /** + * @dev Update ForgeL1L2BatchTimeout + * @param newForgeL1L2BatchTimeout New ForgeL1L2BatchTimeout + * Events: `UpdateForgeL1L2BatchTimeout` + */ + function updateForgeL1L2BatchTimeout(uint8 newForgeL1L2BatchTimeout) + external + onlyGovernance + { + require( + newForgeL1L2BatchTimeout <= ABSOLUTE_MAX_L1L2BATCHTIMEOUT, + "Hermez::updateForgeL1L2BatchTimeout: MAX_FORGETIMEOUT_EXCEED" + ); + forgeL1L2BatchTimeout = newForgeL1L2BatchTimeout; + emit UpdateForgeL1L2BatchTimeout(newForgeL1L2BatchTimeout); + } + + /** + * @dev Update feeAddToken + * @param newFeeAddToken New feeAddToken + * Events: `UpdateFeeAddToken` + */ + function updateFeeAddToken(uint256 newFeeAddToken) external onlyGovernance { + feeAddToken = newFeeAddToken; + emit UpdateFeeAddToken(newFeeAddToken); + } + + ////////////// + // Viewers + ///////////// + + /** + * @dev Retrieve the number of tokens added in rollup + * @return Number of tokens added in rollup + */ + function registerTokensCount() public view returns (uint256) { + return tokenList.length; + } + + /** + * @dev Retrieve the number of rollup verifiers + * @return Number of verifiers + */ + function rollupVerifiersLength() public view returns (uint256) { + return rollupVerifiers.length; + } + + ////////////// + // Internal/private methods + ///////////// + + /** + * @dev Inclusion of a new token to the rollup + * @param tokenAddress Smart contract token address + * Events: `AddToken` + */ + function addToken(address tokenAddress, bytes calldata permit) public { + require( + IERC20(tokenAddress).totalSupply() > 0, + "Hermez::addToken: TOTAL_SUPPLY_ZERO" + ); + uint256 currentTokens = tokenList.length; + require( + currentTokens < _LIMIT_TOKENS, + "Hermez::addToken: TOKEN_LIST_FULL" + ); + require( + tokenAddress != address(0), + "Hermez::addToken: ADDRESS_0_INVALID" + ); + require(tokenMap[tokenAddress] == 0, "Hermez::addToken: ALREADY_ADDED"); + + if (msg.sender != hermezGovernanceAddress) { + // permit and transfer HEZ tokens + if (permit.length != 0) { + _permit(tokenHEZ, feeAddToken, permit); + } + _safeTransferFrom( + tokenHEZ, + msg.sender, + hermezGovernanceAddress, + feeAddToken + ); + } + + tokenList.push(tokenAddress); + tokenMap[tokenAddress] = currentTokens; + + emit AddToken(tokenAddress, uint32(currentTokens)); + } + + /** + * @dev Initialize verifiers + * @param _verifiers verifiers address array + * @param _verifiersParams encoeded maxTx and nlevels of the verifier as follows: + * [8 bits]nLevels || [248 bits] maxTx + */ + function _initializeVerifiers( + address[] memory _verifiers, + uint256[] memory _verifiersParams + ) internal { + for (uint256 i = 0; i < _verifiers.length; i++) { + rollupVerifiers.push( + VerifierRollup({ + verifierInterface: VerifierRollupInterface(_verifiers[i]), + maxTx: (_verifiersParams[i] << 8) >> 8, + nLevels: _verifiersParams[i] >> (256 - 8) + }) + ); + } + } + + /** + * @dev Add L1-user-tx, add it to the correspoding queue + * l1Tx L1-user-tx encoded in bytes as follows: [20 bytes] fromEthAddr || [32 bytes] fromBjj-compressed || [4 bytes] fromIdx || + * [5 bytes] loadAmountFloat40 || [5 bytes] amountFloat40 || [4 bytes] tokenId || [4 bytes] toIdx + * @param ethAddress Ethereum address of the rollup account + * @param babyPubKey Public key babyjubjub represented as point: sign + (Ay) + * @param fromIdx Index account of the sender account + * @param loadAmountF Amount from L1 to L2 + * @param amountF Amount transfered between L2 accounts + * @param tokenID Token identifier + * @param toIdx Index leaf of recipient account + * Events: `L1UserTxEvent` + */ + function _l1QueueAddTx( + address ethAddress, + uint256 babyPubKey, + uint48 fromIdx, + uint40 loadAmountF, + uint40 amountF, + uint32 tokenID, + uint48 toIdx + ) internal { + bytes memory l1Tx = abi.encodePacked( + ethAddress, + babyPubKey, + fromIdx, + loadAmountF, + amountF, + tokenID, + toIdx + ); + + uint256 currentPosition = mapL1TxQueue[nextL1FillingQueue].length / + _L1_USER_TOTALBYTES; + + // concatenate storage byte array with the new l1Tx + _concatStorage(mapL1TxQueue[nextL1FillingQueue], l1Tx); + + emit L1UserTxEvent(nextL1FillingQueue, uint8(currentPosition), l1Tx); + if (currentPosition + 1 >= _MAX_L1_USER_TX) { + nextL1FillingQueue++; + } + } + + /** + * @dev return the current L1-user-tx queue adding the L1-coordinator-tx + * @param ptr Ptr where L1 data is set + * @param l1Batch if true, the include l1TXs from the queue + * [1 byte] V(ecdsa signature) || [32 bytes] S(ecdsa signature) || + * [32 bytes] R(ecdsa signature) || [32 bytes] fromBjj-compressed || [4 bytes] tokenId + */ + function _buildL1Data(uint256 ptr, bool l1Batch) internal view { + uint256 dPtr; + uint256 dLen; + + (dPtr, dLen) = _getCallData(2); + uint256 l1CoordinatorLength = dLen / _L1_COORDINATOR_TOTALBYTES; + + uint256 l1UserLength; + bytes memory l1UserTxQueue; + if (l1Batch) { + l1UserTxQueue = mapL1TxQueue[nextL1ToForgeQueue]; + l1UserLength = l1UserTxQueue.length / _L1_USER_TOTALBYTES; + } else { + l1UserLength = 0; + } + + require( + l1UserLength + l1CoordinatorLength <= _MAX_L1_TX, + "Hermez::_buildL1Data: L1_TX_OVERFLOW" + ); + + if (l1UserLength > 0) { + // Copy the queue to the ptr and update ptr + assembly { + let ptrFrom := add(l1UserTxQueue, 0x20) + let ptrTo := ptr + ptr := add(ptr, mul(l1UserLength, _L1_USER_TOTALBYTES)) + for { + + } lt(ptrTo, ptr) { + ptrTo := add(ptrTo, 32) + ptrFrom := add(ptrFrom, 32) + } { + mstore(ptrTo, mload(ptrFrom)) + } + } + } + + for (uint256 i = 0; i < l1CoordinatorLength; i++) { + uint8 v; // L1-Coordinator-Tx bytes[0] + bytes32 s; // L1-Coordinator-Tx bytes[1:32] + bytes32 r; // L1-Coordinator-Tx bytes[33:64] + bytes32 babyPubKey; // L1-Coordinator-Tx bytes[65:96] + uint256 tokenID; // L1-Coordinator-Tx bytes[97:100] + + assembly { + v := byte(0, calldataload(dPtr)) + dPtr := add(dPtr, 1) + + s := calldataload(dPtr) + dPtr := add(dPtr, 32) + + r := calldataload(dPtr) + dPtr := add(dPtr, 32) + + babyPubKey := calldataload(dPtr) + dPtr := add(dPtr, 32) + + tokenID := shr(224, calldataload(dPtr)) // 256-32 = 224 + dPtr := add(dPtr, 4) + } + + require( + tokenID < tokenList.length, + "Hermez::_buildL1Data: TOKEN_NOT_REGISTERED" + ); + + address ethAddress = _ETH_ADDRESS_INTERNAL_ONLY; + + // v must be >=27 --> EIP-155, v == 0 means no signature + if (v != 0) { + ethAddress = _checkSig(babyPubKey, r, s, v); + } + + // add L1-Coordinator-Tx to the L1-tx queue + assembly { + mstore(ptr, shl(96, ethAddress)) // 256 - 160 = 96, write ethAddress: bytes[0:19] + ptr := add(ptr, 20) + + mstore(ptr, babyPubKey) // write babyPubKey: bytes[20:51] + ptr := add(ptr, 32) + + mstore(ptr, 0) // write zeros + // [6 Bytes] fromIdx , + // [5 bytes] loadAmountFloat40 . + // [5 bytes] amountFloat40 + ptr := add(ptr, 16) + + mstore(ptr, shl(224, tokenID)) // 256 - 32 = 224 write tokenID: bytes[62:65] + ptr := add(ptr, 4) + + mstore(ptr, 0) // write [6 Bytes] toIdx + ptr := add(ptr, 6) + } + } + + _fillZeros( + ptr, + (_MAX_L1_TX - l1UserLength - l1CoordinatorLength) * + _L1_USER_TOTALBYTES + ); + } + + /** + * @dev Calculate the circuit input hashing all the elements + * @param newLastIdx New total rollup accounts + * @param newStRoot New state root + * @param l1Batch Indicates if this forge will be L2 or L1-L2 + * @param verifierIdx Verifier index + */ + function _constructCircuitInput( + uint48 newLastIdx, + uint256 newStRoot, + bool l1Batch, + uint8 verifierIdx + ) internal view returns (uint256) { + uint256 oldStRoot = stateRootMap[lastForgedBatch]; + uint256 oldLastIdx = lastIdx; + uint256 dPtr; // Pointer to the calldata parameter data + uint256 dLen; // Length of the calldata parameter + + // l1L2TxsData = l2Bytes * maxTx = + // ([(nLevels / 8) bytes] fromIdx + [(nLevels / 8) bytes] toIdx + [5 bytes] amountFloat40 + [1 bytes] fee) * maxTx = + // ((nLevels / 4) bytes + 3 bytes) * maxTx + uint256 l1L2TxsDataLength = ((rollupVerifiers[verifierIdx].nLevels / + 8) * + 2 + + 5 + + 1) * rollupVerifiers[verifierIdx].maxTx; + + // [(nLevels / 8) bytes] + uint256 feeIdxCoordinatorLength = (rollupVerifiers[verifierIdx] + .nLevels / 8) * 64; + + // the concatenation of all arguments could be done with abi.encodePacked(args), but is suboptimal, especially with a large bytes arrays + // [6 bytes] lastIdx + + // [6 bytes] newLastIdx + + // [32 bytes] stateRoot + + // [32 bytes] newStRoot + + // [_MAX_L1_TX * _L1_USER_TOTALBYTES bytes] l1TxsData + + // totall1L2TxsDataLength + + // feeIdxCoordinatorLength + + // [2 bytes] chainID + + // [4 bytes] batchNum = + // _INPUT_SHA_CONSTANT_BYTES bytes + totall1L2TxsDataLength + feeIdxCoordinatorLength + bytes memory inputBytes; + + uint256 ptr; // Position for writing the bufftr + + assembly { + let inputBytesLength := add( + add(_INPUT_SHA_CONSTANT_BYTES, l1L2TxsDataLength), + feeIdxCoordinatorLength + ) + + // Set inputBytes to the next free memory space + inputBytes := mload(0x40) + // Reserve the memory. 32 for the length , the input bytes and 32 + // extra bytes at the end for word manipulation + mstore(0x40, add(add(inputBytes, 0x40), inputBytesLength)) + + // Set the actua length of the input bytes + mstore(inputBytes, inputBytesLength) + + // Set The Ptr at the begining of the inputPubber + ptr := add(inputBytes, 32) + + mstore(ptr, shl(208, oldLastIdx)) // 256-48 = 208 + ptr := add(ptr, 6) + + mstore(ptr, shl(208, newLastIdx)) // 256-48 = 208 + ptr := add(ptr, 6) + + mstore(ptr, oldStRoot) + ptr := add(ptr, 32) + + mstore(ptr, newStRoot) + ptr := add(ptr, 32) + } + + // Copy the L1TX Data + _buildL1Data(ptr, l1Batch); + ptr += _MAX_L1_TX * _L1_USER_TOTALBYTES; + + // Copy the L2 TX Data from calldata + (dPtr, dLen) = _getCallData(3); + require( + dLen <= l1L2TxsDataLength, + "Hermez::_constructCircuitInput: L2_TX_OVERFLOW" + ); + assembly { + calldatacopy(ptr, dPtr, dLen) + } + ptr += dLen; + + // L2 TX unused data is padded with 0 at the end + _fillZeros(ptr, l1L2TxsDataLength - dLen); + ptr += l1L2TxsDataLength - dLen; + + // Copy the FeeIdxCoordinator from the calldata + (dPtr, dLen) = _getCallData(4); + require( + dLen <= feeIdxCoordinatorLength, + "Hermez::_constructCircuitInput: INVALID_FEEIDXCOORDINATOR_LENGTH" + ); + assembly { + calldatacopy(ptr, dPtr, dLen) + } + ptr += dLen; + _fillZeros(ptr, feeIdxCoordinatorLength - dLen); + ptr += feeIdxCoordinatorLength - dLen; + + // store 2 bytes of chainID at the end of the inputBytes + assembly { + mstore(ptr, shl(240, chainid())) // 256 - 16 = 240 + } + ptr += 2; + + uint256 batchNum = lastForgedBatch + 1; + + // store 4 bytes of batch number at the end of the inputBytes + assembly { + mstore(ptr, shl(224, batchNum)) // 256 - 32 = 224 + } + + return uint256(sha256(inputBytes)) % _RFIELD; + } + + /** + * @dev Clear the current queue, and update the `nextL1ToForgeQueue` and `nextL1FillingQueue` if needed + */ + function _clearQueue() internal returns (uint16) { + uint16 l1UserTxsLen = uint16( + mapL1TxQueue[nextL1ToForgeQueue].length / _L1_USER_TOTALBYTES + ); + delete mapL1TxQueue[nextL1ToForgeQueue]; + nextL1ToForgeQueue++; + if (nextL1ToForgeQueue == nextL1FillingQueue) { + nextL1FillingQueue++; + } + return l1UserTxsLen; + } + + /** + * @dev Withdraw the funds to the msg.sender if instant withdraw or to the withdraw delayer if delayed + * @param amount Amount to retrieve + * @param tokenID Token identifier + * @param instantWithdraw true if is an instant withdraw + */ + function _withdrawFunds( + uint192 amount, + uint32 tokenID, + bool instantWithdraw + ) internal { + if (instantWithdraw) { + _safeTransfer(tokenList[tokenID], msg.sender, amount); + } else { + if (tokenID == 0) { + withdrawDelayerContract.deposit{value: amount}( + msg.sender, + address(0), + amount + ); + } else { + address tokenAddress = tokenList[tokenID]; + + _safeApprove( + tokenAddress, + address(withdrawDelayerContract), + amount + ); + + withdrawDelayerContract.deposit( + msg.sender, + tokenAddress, + amount + ); + } + } + } + + /////////// + // helpers ERC20 functions + /////////// + + /** + * @dev Approve ERC20 + * @param token Token address + * @param to Recievers + * @param value Quantity of tokens to approve + */ + function _safeApprove( + address token, + address to, + uint256 value + ) internal { + /* solhint-disable avoid-low-level-calls */ + (bool success, bytes memory data) = token.call( + abi.encodeWithSelector(_APPROVE_SIGNATURE, to, value) + ); + require( + success && (data.length == 0 || abi.decode(data, (bool))), + "Hermez::_safeApprove: ERC20_APPROVE_FAILED" + ); + } + + /** + * @dev Transfer tokens or ether from the smart contract + * @param token Token address + * @param to Address to recieve the tokens + * @param value Quantity to transfer + */ + function _safeTransfer( + address token, + address to, + uint256 value + ) internal { + // address 0 is reserved for eth + if (token == address(0)) { + /* solhint-disable avoid-low-level-calls */ + (bool success, ) = msg.sender.call{value: value}(new bytes(0)); + require(success, "Hermez::_safeTransfer: ETH_TRANSFER_FAILED"); + } else { + /* solhint-disable avoid-low-level-calls */ + (bool success, bytes memory data) = token.call( + abi.encodeWithSelector(_TRANSFER_SIGNATURE, to, value) + ); + require( + success && (data.length == 0 || abi.decode(data, (bool))), + "Hermez::_safeTransfer: ERC20_TRANSFER_FAILED" + ); + } + } + + /** + * @dev transferFrom ERC20 + * Require approve tokens for this contract previously + * @param token Token address + * @param from Sender + * @param to Reciever + * @param value Quantity of tokens to send + */ + function _safeTransferFrom( + address token, + address from, + address to, + uint256 value + ) internal { + (bool success, bytes memory data) = token.call( + abi.encodeWithSelector(_TRANSFER_FROM_SIGNATURE, from, to, value) + ); + require( + success && (data.length == 0 || abi.decode(data, (bool))), + "Hermez::_safeTransferFrom: ERC20_TRANSFERFROM_FAILED" + ); + } + + /////////// + // helpers ERC20 extension functions + /////////// + + /** + * @notice Function to call token permit method of extended ERC20 + * @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 { + bytes4 sig = abi.decode(_permitData, (bytes4)); + require( + sig == _PERMIT_SIGNATURE, + "HermezAuctionProtocol::_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, + "Hermez::_permit: PERMIT_OWNER_MUST_BE_THE_SENDER" + ); + require( + spender == address(this), + "Hermez::_permit: SPENDER_MUST_BE_THIS" + ); + require( + value == _amount, + "Hermez::_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 */ + address(token).call( + abi.encodeWithSelector( + _PERMIT_SIGNATURE, + owner, + spender, + value, + deadline, + v, + r, + s + ) + ); + } +} diff --git a/contracts/hermez/future_versions/test/VerifierMock.sol b/contracts/hermez/future_versions/test/VerifierMock.sol new file mode 100644 index 0000000..5446f25 --- /dev/null +++ b/contracts/hermez/future_versions/test/VerifierMock.sol @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: AGPL-3.0 + +pragma solidity ^0.6.12; + +import "../../interfaces/VerifierRollupInterface.sol"; + +contract VerifierMock is VerifierRollupInterface { + function verifyProof( + uint256[2] calldata a, + uint256[2][2] calldata b, + uint256[2] calldata c, + uint256[1] calldata input + ) public override view returns (bool) { + return true; + } +} diff --git a/contracts/mock/HermezV2.sol b/contracts/mock/HermezV2Mock.sol similarity index 99% rename from contracts/mock/HermezV2.sol rename to contracts/mock/HermezV2Mock.sol index 1af8245..0d5b3ff 100644 --- a/contracts/mock/HermezV2.sol +++ b/contracts/mock/HermezV2Mock.sol @@ -8,7 +8,7 @@ import "../hermez/interfaces/VerifierWithdrawInterface.sol"; import "../interfaces/IHermezAuctionProtocol.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -contract HermezV2 is InstantWithdrawManager { +contract HermezV2Mock is InstantWithdrawManager { struct VerifierRollup { VerifierRollupInterface verifierInterface; uint256 maxTx; // maximum rollup transactions in a batch: L2-tx + L1-tx transactions diff --git a/package.json b/package.json index 4587b1c..6472a9b 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,7 @@ "author": "", "license": "AGPL-3.0", "devDependencies": { - "@nomiclabs/hardhat-ethers": "^2.0.1", + "@nomiclabs/hardhat-ethers": "^2.0.2", "@nomiclabs/hardhat-etherscan": "^2.1.1", "@nomiclabs/hardhat-solhint": "^2.0.0", "@nomiclabs/hardhat-waffle": "^2.0.1", @@ -43,9 +43,9 @@ "dotenv": "^8.2.0", "eth-gas-reporter": "^0.2.17", "ethereum-waffle": "^3.2.2", - "ethers": "5.0.30", + "ethers": "^5.0.30", "ffjavascript": "^0.2.10", - "hardhat": "^2.0.8", + "hardhat": "^2.5.0", "hardhat-gas-reporter": "^1.0.4", "hardhat-spdx-license-identifier": "^2.0.3", "mocha": "^8.1.1", @@ -58,9 +58,10 @@ "web3": "^1.3.4" }, "dependencies": { - "@hermeznetwork/commonjs": "0.3.0", + "@hermeznetwork/commonjs": "git://github.com/hermeznetwork/commonjs.git#master", + "@hermeznetwork/commonjsV1": "git://github.com/hermeznetwork/commonjs.git#feature/upgrade-v1", "axios": "^0.21.1", "readline": "^1.3.0", - "snarkjs": "^0.3.60" + "snarkjs": "^0.4.6" } } diff --git a/test/hermezHIP-0/.solcover.js b/test/hermezHIP-0/.solcover.js new file mode 100644 index 0000000..8b63051 --- /dev/null +++ b/test/hermezHIP-0/.solcover.js @@ -0,0 +1,4 @@ +module.exports = { + skipFiles: ["withdrawalDelayer", "mock", "auction", "math", "ERC777"], + providerOptions: {mnemonic: "explain tackle mirror kit van hammer degree position ginger unfair soup bonus", total_accounts:20}, +}; \ No newline at end of file diff --git a/test/hermezHIP-0/HermezERC20.test.js b/test/hermezHIP-0/HermezERC20.test.js new file mode 100644 index 0000000..e74e9c0 --- /dev/null +++ b/test/hermezHIP-0/HermezERC20.test.js @@ -0,0 +1,1178 @@ +const { expect } = require("chai"); +const { ethers } = require("hardhat"); +const SMTMemDB = require("circomlib").SMTMemDB; +const { time } = require("@openzeppelin/test-helpers"); +const Scalar = require("ffjavascript").Scalar; + +const { + l1UserTxCreateAccountDeposit, + l1UserTxDeposit, + l1UserTxDepositTransfer, + l1UserTxCreateAccountDepositTransfer, + l1UserTxForceTransfer, + l1UserTxForceExit, + l1CoordinatorTxEth, + l1CoordinatorTxBjj, + AddToken, + createAccounts, + ForgerTest, + calculateInputMaxTxLevels +} = require("./helpers/helpers"); +const { + float40, + HermezAccount, + txUtils, + stateUtils, + utils, + feeTable, + SMTTmpDb, + Constants, + RollupDB, + BatchBuilder, +} = require("@hermeznetwork/commonjsV1"); + +describe("Hermez ERC 20", function () { + let hardhatTokenERC20Mock; + let hardhatHermez; + let hardhatWithdrawalDelayer; + let hardhatHEZ; + + let owner; + let id1; + let id2; + let addrs; + let hermezGovernanceAddress; + let ownerWallet; + + let chainID; + let chainIDHex; + + const accounts = []; + for (let i = 0; i < 10; i++) { + accounts.push(new HermezAccount()); + } + const tokenInitialAmount = 1000000; + const maxL1Tx = 256; + const maxTx = 512; + const nLevels = 32; + const forgeL1L2BatchTimeout = 10; + const feeAddToken = 10; + const withdrawalDelay = 60 * 60 * 24 * 7 * 2; // 2 weeks + const INITIAL_DELAY = 0; + + beforeEach(async function () { + [ + owner, + governance, + id1, + id2, + ...addrs + ] = await ethers.getSigners(); + + hermezGovernanceAddress = governance.getAddress(); + + const chainIdProvider = (await ethers.provider.getNetwork()).chainId; + if (chainIdProvider == 1337) { // solcover, must be a jsonRPC wallet + const mnemonic = "explain tackle mirror kit van hammer degree position ginger unfair soup bonus"; + let ownerWalletTest = ethers.Wallet.fromMnemonic(mnemonic); + // ownerWalletTest = ownerWallet.connect(ethers.provider); + ownerWallet = owner; + ownerWallet.privateKey = ownerWalletTest.privateKey; + } + else { + ownerWallet = new ethers.Wallet("0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80", ethers.provider); + } + + // const privateKeyhardhat = + // "0xc5e8f61d1ab959b397eecc0a37a6517b8e67a0e7cf1f4bce5591f3ed80199122"; + // ownerWallet = new ethers.Wallet( + // privateKeyhardhat, + // ethers.provider + // ); + //ownerWallet = new ethers.Wallet("0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80", ethers.provider); + + // factory helpers + const TokenERC20Mock = await ethers.getContractFactory("ERC20Mock"); + const TokenERC20PermitMock = await ethers.getContractFactory("ERC20PermitMock"); + + const VerifierRollupHelper = await ethers.getContractFactory( + "VerifierMock" + ); + const VerifierWithdrawHelper = await ethers.getContractFactory( + "VerifierMock" + ); + + const HermezAuctionTest = await ethers.getContractFactory( + "HermezAuctionTest" + ); + const WithdrawalDelayer = await ethers.getContractFactory( + "WithdrawalDelayerTest" + ); + + // factory hermez + const Hermez = await ethers.getContractFactory("HermezTestV2"); + + // deploy tokens + hardhatTokenERC20Mock = await TokenERC20Mock.deploy( + "tokenname", + "TKN", + await owner.getAddress(), + tokenInitialAmount + ); + + hardhatHEZ = await TokenERC20PermitMock.deploy( + "tokenname", + "TKN", + await owner.getAddress(), + tokenInitialAmount + ); + + // deploy helpers + let hardhatVerifierRollupHelper = await VerifierRollupHelper.deploy(); + let hardhatVerifierWithdrawHelper = await VerifierWithdrawHelper.deploy(); + + let hardhatHermezAuctionTest = await HermezAuctionTest.deploy(); + + // deploy hermez + hardhatHermez = await Hermez.deploy(); + await hardhatHermez.deployed(); + + hardhatWithdrawalDelayer = await WithdrawalDelayer.deploy( + INITIAL_DELAY, + hardhatHermez.address, + hermezGovernanceAddress, + hermezGovernanceAddress + ); + + // deploy hermez + await hardhatHermez.initializeHermez( + [hardhatVerifierRollupHelper.address], + calculateInputMaxTxLevels([maxTx], [nLevels]), + hardhatVerifierWithdrawHelper.address, + hardhatHermezAuctionTest.address, + hardhatHEZ.address, + forgeL1L2BatchTimeout, + feeAddToken, + hermezGovernanceAddress, + withdrawalDelay, + hardhatWithdrawalDelayer.address + ); + + // wait until is deployed + await hardhatTokenERC20Mock.deployed(); + + const chainSC = await hardhatHermez.getChainID(); + chainID = chainSC.toNumber(); + chainIDHex = chainSC.toHexString(); + }); + + describe("test tokens contract", function () { + it("Should share tokens", async function () { + await hardhatTokenERC20Mock.transfer(await id1.getAddress(), 50); + const id1Balance = await hardhatTokenERC20Mock.balanceOf( + await id1.getAddress() + ); + expect(id1Balance).to.equal(50); + + await hardhatTokenERC20Mock.transfer(await id2.getAddress(), 50); + + const id2Balance = await hardhatTokenERC20Mock.balanceOf( + await id2.getAddress() + ); + expect(id2Balance).to.equal(50); + }); + }); + + describe("Utils", function () { + it("should revert if token with 0 supply", async function () { + + const TokenERC20Mock = await ethers.getContractFactory("ERC20Mock"); + // deploy tokens + const token_zero_supply = await TokenERC20Mock.deploy( + "tokenname", + "TKN", + await owner.getAddress(), + 0 + ); + await expect(AddToken( + hardhatHermez, + token_zero_supply, + hardhatHEZ, + ownerWallet, + feeAddToken + )).to.be.revertedWith("TOTAL_SUPPLY_ZERO"); + + }); + + it("Add Token", async function () { + await AddToken( + hardhatHermez, + hardhatTokenERC20Mock, + hardhatHEZ, + ownerWallet, + feeAddToken + ); + + expect(await hardhatHEZ.balanceOf(hermezGovernanceAddress)) + .to.be.equal(await hardhatHermez.feeAddToken()); + }); + }); + + // You can nest describe calls to create subsections. + describe("L1-user-Tx", function () { + it("createAccountDeposit", async function () { + await AddToken( + hardhatHermez, + hardhatTokenERC20Mock, + hardhatHEZ, + ownerWallet, + feeAddToken + ); + + const loadAmount = float40.round(1000); + const tokenID = 1; + const babyjub = `0x${accounts[0].bjjCompressed}`; + await l1UserTxCreateAccountDeposit( + loadAmount, + tokenID, + babyjub, + owner, + hardhatHermez, + hardhatTokenERC20Mock + ); + }); + + it("deposit", async function () { + await AddToken( + hardhatHermez, + hardhatTokenERC20Mock, + hardhatHEZ, + ownerWallet, + feeAddToken + ); + + // invalid operation in Hermez.sol, test purposes + hardhatHermez.changeCurrentIdx(257); + + const loadAmount = float40.round(1000); + const tokenID = 1; + const fromIdx = 256; + await l1UserTxDeposit( + loadAmount, + tokenID, + fromIdx, + owner, + hardhatHermez, + hardhatTokenERC20Mock + ); + }); + it("depositTransfer", async function () { + await AddToken( + hardhatHermez, + hardhatTokenERC20Mock, + hardhatHEZ, + ownerWallet, + feeAddToken + ); + + // invalid operation in Hermez.sol, test purposes + hardhatHermez.changeCurrentIdx(257); + + const loadAmount = float40.round(1000); + const tokenID = 1; + const fromIdx = 256; + const toIdx = 257; + const amountF = float40.fix2Float(10); + await l1UserTxDepositTransfer( + loadAmount, + tokenID, + fromIdx, + toIdx, + amountF, + owner, + hardhatHermez, + hardhatTokenERC20Mock + ); + }); + it("createAccountDepositTransfer", async function () { + await AddToken( + hardhatHermez, + hardhatTokenERC20Mock, + hardhatHEZ, + ownerWallet, + feeAddToken + ); + + // invalid operation in Hermez.sol, test purposes + hardhatHermez.changeCurrentIdx(257); + + const loadAmount = float40.round(1000); + const tokenID = 1; + const toIdx = 257; + const amountF = float40.fix2Float(10); + const babyjub = `0x${accounts[0].bjjCompressed}`; + await l1UserTxCreateAccountDepositTransfer( + loadAmount, + tokenID, + toIdx, + amountF, + babyjub, + owner, + hardhatHermez, + hardhatTokenERC20Mock + ); + }); + it("forceTransfer", async function () { + await AddToken( + hardhatHermez, + hardhatTokenERC20Mock, + hardhatHEZ, + ownerWallet, + feeAddToken + ); + + // invalid operation in Hermez.sol, test purposes + hardhatHermez.changeCurrentIdx(257); + + const tokenID = 1; + const fromIdx = 256; + const toIdx = 257; + const amountF = float40.fix2Float(10); + await l1UserTxForceTransfer( + tokenID, + fromIdx, + toIdx, + amountF, + owner, + hardhatHermez + ); + }); + it("forceExit", async function () { + await AddToken( + hardhatHermez, + hardhatTokenERC20Mock, + hardhatHEZ, + ownerWallet, + feeAddToken + ); + + // invalid operation in Hermez.sol, test purposes + hardhatHermez.changeCurrentIdx(257); + + const tokenID = 1; + const fromIdx = 256; + const amountF = float40.fix2Float(10); + await l1UserTxForceExit(tokenID, fromIdx, amountF, owner, hardhatHermez); + }); + }); + + describe("Forge Batch", function () { + it("test L1 deadline", async function () { + // dummy batch + const proofA = ["0", "0"]; + const proofB = [ + ["0", "0"], + ["0", "0"], + ]; + const proofC = ["0", "0"]; + + const newLastIdx = 255; // first idx + const newStateRoot = 0; + const compressedL1CoordinatorTx = "0x00"; + const L2TxsData = "0x00"; + const feeIdxCoordinator = `0x${utils.padZeros( + "", + ((nLevels * 64) / 8) * 2 + )}`; + const verifierIdx = 0; + + let currentBlock = await time.latestBlock(); + await time.advanceBlockTo( + currentBlock.toNumber() + forgeL1L2BatchTimeout + ); + // forgeL1L2BatchTimeout = 10 + + await expect( + hardhatHermez.forgeBatch( + newLastIdx, + newStateRoot, + compressedL1CoordinatorTx, + L2TxsData, + feeIdxCoordinator, + verifierIdx, + false, + proofA, + proofB, + proofC + ) + ).to.be.revertedWith("Hermez::forgeBatch: L1L2BATCH_REQUIRED"); + + // must forge an L1 batch + await hardhatHermez.forgeBatch( + newLastIdx, + newStateRoot, + compressedL1CoordinatorTx, + L2TxsData, + feeIdxCoordinator, + verifierIdx, + true, + proofA, + proofB, + proofC + ); + // can continue forging l2 batches + await hardhatHermez.forgeBatch( + newLastIdx, + newStateRoot, + compressedL1CoordinatorTx, + L2TxsData, + feeIdxCoordinator, + verifierIdx, + false, + proofA, + proofB, + proofC + ); + currentBlock = await time.latestBlock(); + await time.advanceBlockTo( + currentBlock.toNumber() + forgeL1L2BatchTimeout + ); + await expect( + hardhatHermez.forgeBatch( + newLastIdx, + newStateRoot, + compressedL1CoordinatorTx, + L2TxsData, + feeIdxCoordinator, + verifierIdx, + false, + proofA, + proofB, + proofC + ) + ).to.be.revertedWith("Hermez::forgeBatch: L1L2BATCH_REQUIRED"); + }); + + it("test feeIdxCoordinator", async function () { + + for (let i = 0; i < 65; i = i + 10) { + // dummy batch + const proofA = ["0", "0"]; + const proofB = [ + ["0", "0"], + ["0", "0"], + ]; + const proofC = ["0", "0"]; + + const newLastIdx = 255; + const newStateRoot = 0; + const compressedL1CoordinatorTx = "0x00"; + const L1L2TxsData = "0x00"; + const verifierIdx = 0; + const l1Batch = true; + + // test different paddings + const feeIdxCoordinator = `0x${utils.padZeros( + "", + ((nLevels / 8) * 2) * i + )}`; + + const tx = await hardhatHermez.calculateInputTest( + newLastIdx, + newStateRoot, + compressedL1CoordinatorTx, + L1L2TxsData, + feeIdxCoordinator, + l1Batch, + verifierIdx + ); + const receipt = await tx.wait(); + const input = receipt.events[0].args[0]; + + // check that the padding of the SC works as expected! + await expect( + hardhatHermez.calculateInputTest( + newLastIdx, + newStateRoot, + compressedL1CoordinatorTx, + L1L2TxsData, + "0x", + l1Batch, + verifierIdx + ) + ).to.emit(hardhatHermez, "ReturnUint256") + .withArgs(input); + + await hardhatHermez.forgeBatch( + newLastIdx, + newStateRoot, + compressedL1CoordinatorTx, + L1L2TxsData, + feeIdxCoordinator, + verifierIdx, + l1Batch, + proofA, + proofB, + proofC + ); + } + }); + + it("handle L1 Coordinator Queue Test", async function () { + const tokenID = 1; + const babyjub = `0x${accounts[0].bjjCompressed}`; + const currentQueue = await hardhatHermez.nextL1ToForgeQueue(); + + await AddToken( + hardhatHermez, + hardhatTokenERC20Mock, + hardhatHEZ, + ownerWallet, + feeAddToken + ); + const l1TxCoordiatorArray = []; + // L1-Tx Coordinator with eth signature + l1TxCoordiatorArray.push( + await l1CoordinatorTxEth(tokenID, babyjub, owner, hardhatHermez, chainIDHex) + ); + + // L1-Tx Coordinator without eth signature: + l1TxCoordiatorArray.push( + await l1CoordinatorTxBjj(tokenID, babyjub, hardhatHermez) + ); + + let stringL1CoordinatorTx = ""; + for (let tx of l1TxCoordiatorArray) { + stringL1CoordinatorTx = + stringL1CoordinatorTx + tx.l1TxCoordinatorbytes.slice(2); // retireve the 0x + } + + // simulate l1-tx batchbuilder: + const fromEthAddrB = 160; + const fromBjjCompressedB = 256; + const f40B = 40; + const tokenIDB = 32; + const maxIdxB = 48; + + const L1TxB = + fromEthAddrB + fromBjjCompressedB + 2 * maxIdxB + tokenIDB + 2 * f40B; + + let jsL1TxData = ""; + for (let tx of l1TxCoordiatorArray) { + jsL1TxData = jsL1TxData + tx.l1TxBytes.slice(2); + } + const dataNopTx = utils.padZeros("", (maxL1Tx - 2) * (L1TxB / 4)); + const simulateBatchbuilderL1TxData = `0x${jsL1TxData + dataNopTx}`; + + const proofA = ["0", "0"]; + const proofB = [ + ["0", "0"], + ["0", "0"], + ]; + const proofC = ["0", "0"]; + + await expect( + hardhatHermez.handleL1QueueTest( + 0, + 0, + `0x${stringL1CoordinatorTx}`, + "0x", + "0x", + 0, + 0, + proofA, + proofB, + proofC + ) + ) + .to.emit(hardhatHermez, "ReturnBytes") + .withArgs(simulateBatchbuilderL1TxData); + }); + + it("forge L1-Coordiator-tx Batch ", async function () { + const tokenID = 1; + const babyjub = `0x${accounts[0].bjjCompressed}`; + + const l1TxCoordiatorArray = []; + const rollupDB = await RollupDB(new SMTMemDB(), chainID); + const forgerTest = new ForgerTest( + maxTx, + maxL1Tx, + nLevels, + hardhatHermez, + rollupDB + ); + + await AddToken( + hardhatHermez, + hardhatTokenERC20Mock, + hardhatHEZ, + ownerWallet, + feeAddToken + ); + + l1TxCoordiatorArray.push( + await l1CoordinatorTxEth(tokenID, babyjub, owner, hardhatHermez, chainIDHex) + ); + + l1TxCoordiatorArray.push( + await l1CoordinatorTxBjj(tokenID, babyjub, hardhatHermez) + ); + + // forge Batch + await forgerTest.forgeBatch(true, [], l1TxCoordiatorArray); + + // after forge, next queue is empty + const currentQueue = await hardhatHermez.nextL1ToForgeQueue(); + expect("0x").to.equal(await hardhatHermez.mapL1TxQueue(currentQueue)); + }); + + it("expect L1-Tx Queue same as batchbuilder ", async function () { + const tokenID = 1; + const babyjub = `0x${accounts[0].bjjCompressed}`; + const loadAmount = float40.round(1000); + const fromIdx = 256; + const toIdx = 257; + const amountF = float40.fix2Float(10); + + const l1TxCoordiatorArray = []; + const l1TxUserArray = []; + + const rollupDB = await RollupDB(new SMTMemDB(), chainID); + const forgerTest = new ForgerTest( + maxTx, + maxL1Tx, + nLevels, + hardhatHermez, + rollupDB + ); + + await AddToken( + hardhatHermez, + hardhatTokenERC20Mock, + hardhatHEZ, + ownerWallet, + feeAddToken + ); + + // In order to add all the possible l1tx we need 2 accounts created in batchbuilder and rollup: + const numAccounts = 2; + await createAccounts( + forgerTest, + loadAmount, + tokenID, + babyjub, + owner, + hardhatHermez, + hardhatTokenERC20Mock, + numAccounts + ); + + // add user l1 tx + l1TxUserArray.push( + await l1UserTxCreateAccountDeposit( + loadAmount, + tokenID, + babyjub, + owner, + hardhatHermez, + hardhatTokenERC20Mock + ) + ); + l1TxUserArray.push( + await l1UserTxDeposit( + loadAmount, + tokenID, + fromIdx, + owner, + hardhatHermez, + hardhatTokenERC20Mock + ) + ); + l1TxUserArray.push( + await l1UserTxDepositTransfer( + loadAmount, + tokenID, + fromIdx, + toIdx, + amountF, + owner, + hardhatHermez, + hardhatTokenERC20Mock + ) + ); + l1TxUserArray.push( + await l1UserTxCreateAccountDepositTransfer( + loadAmount, + tokenID, + toIdx, + amountF, + babyjub, + owner, + hardhatHermez, + hardhatTokenERC20Mock + ) + ); + l1TxUserArray.push( + await l1UserTxForceTransfer( + tokenID, + fromIdx, + toIdx, + amountF, + owner, + hardhatHermez + ) + ); + l1TxUserArray.push( + await l1UserTxForceExit(tokenID, fromIdx, amountF, owner, hardhatHermez) + ); + + // forge empty batch, now the current queue is filled with the L1-User-Tx + await forgerTest.forgeBatch(true, [], []); + + // add L1-Tx Coordinator with eth signature + const cordinatorTxEth = await l1CoordinatorTxEth( + tokenID, + babyjub, + owner, + hardhatHermez, + chainIDHex + ); + + // add L1-Tx Coordinator without eth signature: + const coordinatorTxBjj = await l1CoordinatorTxBjj( + tokenID, + babyjub, + hardhatHermez + ); + + l1TxCoordiatorArray.push(cordinatorTxEth); + l1TxCoordiatorArray.push(coordinatorTxBjj); + + const fromEthAddrB = 160; + const fromBjjCompressedB = 256; + const f40B = 40; + const tokenIDB = 32; + const maxIdxB = 48; + + const L1TxB = + fromEthAddrB + fromBjjCompressedB + 2 * maxIdxB + tokenIDB + 2 * f40B; + // simulate l1-tx batchbuilder: + let jsL1TxData = ""; + for (let tx of l1TxUserArray) { + jsL1TxData = jsL1TxData + tx.slice(2); + } + for (let tx of l1TxCoordiatorArray) { + jsL1TxData = jsL1TxData + tx.l1TxBytes.slice(2); + } + const dataNopTx = utils.padZeros( + "", + (maxL1Tx - l1TxUserArray.length - l1TxCoordiatorArray.length) * + (L1TxB / 4) + ); + const simulateBatchbuilderL1TxData = `0x${jsL1TxData + dataNopTx}`; + + let stringL1CoordinatorTx = ""; + for (let tx of l1TxCoordiatorArray) { + stringL1CoordinatorTx = + stringL1CoordinatorTx + tx.l1TxCoordinatorbytes.slice(2); // retireve the 0x + } + + const proofA = ["0", "0"]; + const proofB = [ + ["0", "0"], + ["0", "0"], + ]; + const proofC = ["0", "0"]; + + await expect( + hardhatHermez.handleL1QueueTest( + 0, + 0, + `0x${stringL1CoordinatorTx}`, + "0x", + "0x", + 0, + true, + proofA, + proofB, + proofC + ) + ) + .to.emit(hardhatHermez, "ReturnBytes") + .withArgs(simulateBatchbuilderL1TxData); + }); + + it("forge L1 user & Coordiator Tx batch", async function () { + const tokenID = 1; + const babyjub = `0x${accounts[0].bjjCompressed}`; + const loadAmount = float40.round(1000); + const fromIdx = 256; + const toIdx = 257; + const amountF = float40.fix2Float(10); + + const l1TxUserArray = []; + + const rollupDB = await RollupDB(new SMTMemDB(), chainID); + const forgerTest = new ForgerTest( + maxTx, + maxL1Tx, + nLevels, + hardhatHermez, + rollupDB + ); + + await AddToken( + hardhatHermez, + hardhatTokenERC20Mock, + hardhatHEZ, + ownerWallet, + feeAddToken + ); + + // In order to add all the possible l1tx we need 2 accounts created in batchbuilder and rollup: + const numAccounts = 2; + await createAccounts( + forgerTest, + loadAmount, + tokenID, + babyjub, + owner, + hardhatHermez, + hardhatTokenERC20Mock, + numAccounts + ); + + // add user l1 tx + l1TxUserArray.push( + await l1UserTxCreateAccountDeposit( + loadAmount, + tokenID, + babyjub, + owner, + hardhatHermez, + hardhatTokenERC20Mock + ) + ); + l1TxUserArray.push( + await l1UserTxDeposit( + loadAmount, + tokenID, + fromIdx, + owner, + hardhatHermez, + hardhatTokenERC20Mock + ) + ); + l1TxUserArray.push( + await l1UserTxDepositTransfer( + loadAmount, + tokenID, + fromIdx, + toIdx, + amountF, + owner, + hardhatHermez, + hardhatTokenERC20Mock + ) + ); + l1TxUserArray.push( + await l1UserTxCreateAccountDepositTransfer( + loadAmount, + tokenID, + toIdx, + amountF, + babyjub, + owner, + hardhatHermez, + hardhatTokenERC20Mock + ) + ); + l1TxUserArray.push( + await l1UserTxForceTransfer( + tokenID, + fromIdx, + toIdx, + amountF, + owner, + hardhatHermez + ) + ); + l1TxUserArray.push( + await l1UserTxForceExit(tokenID, fromIdx, amountF, owner, hardhatHermez) + ); + + // forge empty batch + await forgerTest.forgeBatch(true, [], []); + + const l1TxCoordiatorArray = []; + + // add Coordiator tx + l1TxCoordiatorArray.push( + await l1CoordinatorTxEth(tokenID, babyjub, owner, hardhatHermez, chainIDHex) + ); + + l1TxCoordiatorArray.push( + await l1CoordinatorTxBjj(tokenID, babyjub, hardhatHermez) + ); + + // forge batch with all the L1 tx + await forgerTest.forgeBatch(true, l1TxUserArray, l1TxCoordiatorArray); + }); + + it("test instant withdraw circuit", async function () { + const tokenID = 1; + const babyjub = `0x${accounts[0].bjjCompressed}`; + const loadAmount = float40.round(1000); + const fromIdx = 256; + const amount = 10; + const amountF = float40.fix2Float(amount); + + const l1TxUserArray = []; + + const rollupDB = await RollupDB(new SMTMemDB(), chainID); + const forgerTest = new ForgerTest( + maxTx, + maxL1Tx, + nLevels, + hardhatHermez, + rollupDB + ); + + await AddToken( + hardhatHermez, + hardhatTokenERC20Mock, + hardhatHEZ, + ownerWallet, + feeAddToken + ); + + // Create account and exit some funds + const numAccounts = 1; + await createAccounts( + forgerTest, + loadAmount, + tokenID, + babyjub, + owner, + hardhatHermez, + hardhatTokenERC20Mock, + numAccounts + ); + + l1TxUserArray.push( + await l1UserTxForceExit(tokenID, fromIdx, amountF, owner, hardhatHermez) + ); + + const initialOwnerBalance = await hardhatTokenERC20Mock.balanceOf( + await owner.getAddress() + ); + + // forge empty batch + await forgerTest.forgeBatch(true, [], []); + + // forge batch with all the create account and exit + await forgerTest.forgeBatch(true, l1TxUserArray, []); + + // perform withdraw + const batchNum = await hardhatHermez.lastForgedBatch(); + const instantWithdraw = true; + const amountWithdraw = amount / 2; + const proofA = ["0", "0"]; + const proofB = [ + ["0", "0"], + ["0", "0"], + ]; + const proofC = ["0", "0"]; + + await expect( + hardhatHermez.withdrawCircuit( + proofA, + proofB, + proofC, + tokenID, + amount, + amountWithdraw, + batchNum, + fromIdx, + instantWithdraw + ) + ) + .to.emit(hardhatHermez, "WithdrawEvent") + .withArgs(amountWithdraw, fromIdx, instantWithdraw); + + expect(amountWithdraw).to.equal( + await hardhatHermez.exitAccumulateMap(fromIdx) + ); + + await expect( + hardhatHermez.withdrawCircuit( + proofA, + proofB, + proofC, + tokenID, + amount, + amountWithdraw*2, + batchNum, + fromIdx, + instantWithdraw + ) + ) + .to.be.revertedWith("Hermez::withdrawCircuit: AMOUNT_WITHDRAW_LESS_THAN_ACCUMULATED"); + + await expect( + hardhatHermez.withdrawCircuit( + proofA, + proofB, + proofC, + tokenID, + amount, + amountWithdraw, + batchNum, + fromIdx, + instantWithdraw + ) + ) + .to.emit(hardhatHermez, "WithdrawEvent") + .withArgs(amountWithdraw, fromIdx, instantWithdraw); + + const finalOwnerBalance = await hardhatTokenERC20Mock.balanceOf( + await owner.getAddress() + ); + expect(parseInt(finalOwnerBalance)).to.equal( + parseInt(initialOwnerBalance) + amount + ); + + expect(amount).to.equal( + await hardhatHermez.exitAccumulateMap(fromIdx) + ); + }); + it("test delayed withdraw circuit", async function () { + const tokenID = 1; + const babyjub = `0x${accounts[0].bjjCompressed}`; + const loadAmount = float40.round(1000); + const fromIdx = 256; + const amount = 10; + const amountF = float40.fix2Float(amount); + + const l1TxUserArray = []; + + const rollupDB = await RollupDB(new SMTMemDB(), chainID); + const forgerTest = new ForgerTest( + maxTx, + maxL1Tx, + nLevels, + hardhatHermez, + rollupDB + ); + + await AddToken( + hardhatHermez, + hardhatTokenERC20Mock, + hardhatHEZ, + ownerWallet, + feeAddToken + ); + + // Create account and exit some funds + const numAccounts = 1; + await createAccounts( + forgerTest, + loadAmount, + tokenID, + babyjub, + owner, + hardhatHermez, + hardhatTokenERC20Mock, + numAccounts + ); + + l1TxUserArray.push( + await l1UserTxForceExit(tokenID, fromIdx, amountF, owner, hardhatHermez) + ); + + const initialOwnerBalance = await hardhatTokenERC20Mock.balanceOf( + hardhatWithdrawalDelayer.address + ); + + // forge empty batch + await forgerTest.forgeBatch(true, [], []); + + // forge batch with all the create account and exit + await forgerTest.forgeBatch(true, l1TxUserArray, []); + + // perform withdraw + const batchNum = await hardhatHermez.lastForgedBatch(); + const instantWithdraw = false; + const state = await rollupDB.getStateByIdx(256); + const exitInfo = await rollupDB.getExitInfo(256, batchNum); + + const proofA = ["0", "0"]; + const proofB = [ + ["0", "0"], + ["0", "0"], + ]; + const proofC = ["0", "0"]; + + await expect( + hardhatHermez.withdrawCircuit( + proofA, + proofB, + proofC, + tokenID, + amount, + amount, + batchNum, + fromIdx, + instantWithdraw + ) + ) + .to.emit(hardhatHermez, "WithdrawEvent") + .withArgs(amount, fromIdx, instantWithdraw); + + const finalOwnerBalance = await hardhatTokenERC20Mock.balanceOf( + hardhatWithdrawalDelayer.address + ); + expect(parseInt(finalOwnerBalance)).to.equal( + parseInt(initialOwnerBalance) + amount + ); + }); + }); + describe("Governance update aprameters", function () { + it("update forgeL1L2BatchTimeout", async function () { + const newForgeL1Timeout = 100; + + expect(await hardhatHermez.forgeL1L2BatchTimeout()).to.equal( + forgeL1L2BatchTimeout + ); + + await expect( + hardhatHermez + .connect(governance) + .updateForgeL1L2BatchTimeout(newForgeL1Timeout) + ) + .to.emit(hardhatHermez, "UpdateForgeL1L2BatchTimeout") + .withArgs(newForgeL1Timeout); + expect(await hardhatHermez.forgeL1L2BatchTimeout()).to.equal( + newForgeL1Timeout + ); + + await expect( + hardhatHermez.connect(governance).updateForgeL1L2BatchTimeout(241) + ).to.be.revertedWith("Hermez::updateForgeL1L2BatchTimeout: MAX_FORGETIMEOUT_EXCEED"); + }); + + it("update FeeAddToken", async function () { + const newFeeAddToken = 100; + + expect(await hardhatHermez.feeAddToken()).to.equal(feeAddToken); + + await expect( + hardhatHermez.connect(governance).updateFeeAddToken(newFeeAddToken) + ) + .to.emit(hardhatHermez, "UpdateFeeAddToken") + .withArgs(newFeeAddToken); + expect(await hardhatHermez.feeAddToken()).to.equal(newFeeAddToken); + }); + }); +}); diff --git a/test/hermezHIP-0/HermezERC20Permit.test.js b/test/hermezHIP-0/HermezERC20Permit.test.js new file mode 100644 index 0000000..cef4609 --- /dev/null +++ b/test/hermezHIP-0/HermezERC20Permit.test.js @@ -0,0 +1,1150 @@ +const { expect } = require("chai"); +const { ethers } = require("hardhat"); +const SMTMemDB = require("circomlib").SMTMemDB; +const { time } = require("@openzeppelin/test-helpers"); +const Scalar = require("ffjavascript").Scalar; + +const { + l1UserTxCreateAccountDeposit, + l1UserTxDeposit, + l1UserTxDepositTransfer, + l1UserTxCreateAccountDepositTransfer, + l1UserTxForceTransfer, + l1UserTxForceExit, + l1CoordinatorTxEth, + l1CoordinatorTxBjj, + AddToken, + createAccounts, + ForgerTest, + calculateInputMaxTxLevels, + createPermitSignature +} = require("./helpers/helpers"); +const { + float40, + HermezAccount, + txUtils, + stateUtils, + utils, + feeTable, + SMTTmpDb, + Constants, + RollupDB, + BatchBuilder, +} = require("@hermeznetwork/commonjsV1"); + +const INITIAL_DELAY = 0; +const ABIbid = [ + "function permit(address,address,uint256,uint256,uint8,bytes32,bytes32)", +]; + +const iface = new ethers.utils.Interface(ABIbid); + +describe("Hermez ERC20 Permit", function () { + let hardhatTokenERC20PermitMock; + let hardhatHermez; + let hardhatWithdrawalDelayer; + + let owner; + let id1; + let id2; + let addrs; + let hermezGovernanceAddress; + let ownerWallet; + + let chainID; + let chainIDHex; + + const accounts = []; + for (let i = 0; i < 10; i++) { + accounts.push(new HermezAccount()); + } + const tokenInitialAmount = 1000000; + const maxL1Tx = 256; + const maxTx = 512; + const nLevels = 32; + const forgeL1L2BatchTimeout = 10; + const feeAddToken = 10; + const withdrawalDelay = 60 * 60 * 24 * 7 * 2; // 2 weeks + const emptyPermit = "0x"; + + beforeEach(async function () { + [ + owner, + governance, + , + id1, + id2, + ...addrs + ] = await ethers.getSigners(); + + hermezGovernanceAddress = governance.getAddress(); + + const chainIdProvider = (await ethers.provider.getNetwork()).chainId; + if (chainIdProvider == 1337) { // solcover, must be a jsonRPC wallet + const mnemonic = "explain tackle mirror kit van hammer degree position ginger unfair soup bonus"; + let ownerWalletTest = ethers.Wallet.fromMnemonic(mnemonic); + // ownerWalletTest = ownerWallet.connect(ethers.provider); + ownerWallet = owner; + ownerWallet.privateKey = ownerWalletTest.privateKey; + } + else { + ownerWallet = new ethers.Wallet("0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80", ethers.provider); + } + + // factory helpers + const TokenERC20PermitMock = await ethers.getContractFactory("ERC20PermitMock"); + const VerifierRollupHelper = await ethers.getContractFactory( + "VerifierMock" + ); + const VerifierWithdrawHelper = await ethers.getContractFactory( + "VerifierMock" + ); + + const HermezAuctionTest = await ethers.getContractFactory( + "HermezAuctionTest" + ); + const WithdrawalDelayer = await ethers.getContractFactory( + "WithdrawalDelayerTest" + ); + + + // factory hermez + const Hermez = await ethers.getContractFactory("HermezTestV2"); + + hardhatTokenERC20PermitMock = await TokenERC20PermitMock.deploy( + "tokenname", + "TKN", + await owner.getAddress(), + tokenInitialAmount + ); + + // deploy helpers + await hardhatTokenERC20PermitMock.deployed(); + let hardhatVerifierRollupHelper = await VerifierRollupHelper.deploy(); + let hardhatVerifierWithdrawHelper = await VerifierWithdrawHelper.deploy(); + + let hardhatHermezAuctionTest = await HermezAuctionTest.deploy(); + + // deploy hermez + hardhatHermez = await Hermez.deploy(); + await hardhatHermez.deployed(); + hardhatWithdrawalDelayer = await WithdrawalDelayer.deploy( + INITIAL_DELAY, + hardhatHermez.address, + hermezGovernanceAddress, + hermezGovernanceAddress + ); + + // deploy hermez + await hardhatHermez.initializeHermez( + [hardhatVerifierRollupHelper.address], + calculateInputMaxTxLevels([maxTx], [nLevels]), + hardhatVerifierWithdrawHelper.address, + hardhatHermezAuctionTest.address, + hardhatTokenERC20PermitMock.address, + forgeL1L2BatchTimeout, + feeAddToken, + hermezGovernanceAddress, + withdrawalDelay, + hardhatWithdrawalDelayer.address + ); + + // wait until is deployed + await hardhatTokenERC20PermitMock.deployed(); + + const chainSC = await hardhatHermez.getChainID(); + chainID = chainSC.toNumber(); + chainIDHex = chainSC.toHexString(); + }); + + describe("test tokens contract", function () { + it("Should share tokens", async function () { + await hardhatTokenERC20PermitMock.transfer(await id1.getAddress(), 50); + const id1Balance = await hardhatTokenERC20PermitMock.balanceOf( + await id1.getAddress() + ); + expect(id1Balance).to.equal(50); + + await hardhatTokenERC20PermitMock.transfer(await id2.getAddress(), 50); + + const id2Balance = await hardhatTokenERC20PermitMock.balanceOf( + await id2.getAddress() + ); + expect(id2Balance).to.equal(50); + }); + + }); + + describe("Utils", function () { + it("Add Token", async function () { + await AddToken( + hardhatHermez, + hardhatTokenERC20PermitMock, + hardhatTokenERC20PermitMock, + ownerWallet, + feeAddToken + ); + }); + + it("Add Token of a token that does not exist", async function () { + const fakeTokenAddress = "0xEEF9f339514298C6A857EfCfC1A762aF84438dEE"; + const addressOwner = await ownerWallet.getAddress(); + + const deadline = ethers.constants.MaxUint256; + const value = feeAddToken; + const nonce = await hardhatTokenERC20PermitMock.nonces(addressOwner); + const { v, r, s } = await createPermitSignature( + hardhatTokenERC20PermitMock, + ownerWallet, + hardhatHermez.address, + value, + nonce, + deadline + ); + + const data = iface.encodeFunctionData("permit", [ + await ownerWallet.getAddress(), + hardhatHermez.address, + value, + deadline, + v, + r, + s + ]); + + // Send data and amount + await expect(hardhatHermez.connect(ownerWallet).addToken(fakeTokenAddress, data)) + .to.be.reverted; + }); + }); + + // You can nest describe calls to create subsections. + describe("L1-user-Tx", function () { + + it("createAccountDeposit should revert cause token is not added", async function () { + const loadAmount = float40.round(1000); + const tokenID = 1; + const babyjub = `0x${accounts[0].bjjCompressed}`; + + // using ERC20 approach: approve and transferFrom, shoudl revert + await expect( + hardhatTokenERC20PermitMock.approve(hardhatHermez.address, loadAmount) + ).to.emit(hardhatTokenERC20PermitMock, "Approval"); + + const fromIdx0 = 0; + const amountF0 = 0; + const toIdx0 = 0; + + await expect( + hardhatHermez.addL1Transaction( + babyjub, + fromIdx0, + loadAmount, + amountF0, + tokenID, + toIdx0, + emptyPermit + ) + ).to.be.revertedWith("Hermez::addL1Transaction: TOKEN_NOT_REGISTERED"); + }); + + it("createAccountDeposit", async function () { + await AddToken( + hardhatHermez, + hardhatTokenERC20PermitMock, + hardhatTokenERC20PermitMock, + ownerWallet, + feeAddToken + ); + + const loadAmount = float40.round(1000); + const tokenID = 1; + const babyjub = `0x${accounts[0].bjjCompressed}`; + + // using erc20permit approach: + await l1UserTxCreateAccountDeposit( + loadAmount, + tokenID, + babyjub, + ownerWallet, + hardhatHermez, + hardhatTokenERC20PermitMock, + true + ); + + // using ERC20 approach: approve and transferFrom, shoudl revert + await expect( + hardhatTokenERC20PermitMock.approve(hardhatHermez.address, loadAmount) + ).to.emit(hardhatTokenERC20PermitMock, "Approval"); + + const fromIdx0 = 0; + const amountF0 = 0; + const toIdx0 = 0; + + await expect( + hardhatHermez.addL1Transaction( + babyjub, + fromIdx0, + loadAmount, + amountF0, + tokenID, + toIdx0, + emptyPermit + ) + ); + }); + + it("deposit", async function () { + await AddToken( + hardhatHermez, + hardhatTokenERC20PermitMock, + hardhatTokenERC20PermitMock, + ownerWallet, + feeAddToken + ); + + // invalid operation in Hermez.sol, test purposes + hardhatHermez.changeCurrentIdx(257); + + const loadAmount = float40.round(1000); + const tokenID = 1; + const fromIdx = 256; + // using erc20permit approach: + await l1UserTxDeposit( + loadAmount, + tokenID, + fromIdx, + ownerWallet, + hardhatHermez, + hardhatTokenERC20PermitMock, + true + ); + }); + it("depositTransfer", async function () { + await AddToken( + hardhatHermez, + hardhatTokenERC20PermitMock, + hardhatTokenERC20PermitMock, + ownerWallet, + feeAddToken + ); + + // invalid operation in Hermez.sol, test purposes + hardhatHermez.changeCurrentIdx(257); + + const loadAmount = float40.round(1000); + const tokenID = 1; + const fromIdx = 256; + const toIdx = 257; + const amountF = float40.fix2Float(10); + + // using erc20permit approach: + await l1UserTxDepositTransfer( + loadAmount, + tokenID, + fromIdx, + toIdx, + amountF, + ownerWallet, + hardhatHermez, + hardhatTokenERC20PermitMock, + true + ); + }); + it("createAccountDepositTransfer", async function () { + await AddToken( + hardhatHermez, + hardhatTokenERC20PermitMock, + hardhatTokenERC20PermitMock, + ownerWallet, + feeAddToken + ); + + // invalid operation in Hermez.sol, test purposes + hardhatHermez.changeCurrentIdx(257); + + const loadAmount = float40.round(1000); + const tokenID = 1; + const toIdx = 257; + const amountF = float40.fix2Float(10); + const babyjub = `0x${accounts[0].bjjCompressed}`; + + // using erc20permit approach: + await l1UserTxCreateAccountDepositTransfer( + loadAmount, + tokenID, + toIdx, + amountF, + babyjub, + ownerWallet, + hardhatHermez, + hardhatTokenERC20PermitMock, + true + ); + }); + it("forceTransfer", async function () { + await AddToken( + hardhatHermez, + hardhatTokenERC20PermitMock, + hardhatTokenERC20PermitMock, + ownerWallet, + feeAddToken + ); + + // invalid operation in Hermez.sol, test purposes + hardhatHermez.changeCurrentIdx(257); + + const tokenID = 1; + const fromIdx = 256; + const toIdx = 257; + const amountF = float40.fix2Float(10); + await l1UserTxForceTransfer( + tokenID, + fromIdx, + toIdx, + amountF, + ownerWallet, + hardhatHermez + ); + }); + it("forceExit", async function () { + await AddToken( + hardhatHermez, + hardhatTokenERC20PermitMock, + hardhatTokenERC20PermitMock, + ownerWallet, + feeAddToken + ); + + // invalid operation in Hermez.sol, test purposes + hardhatHermez.changeCurrentIdx(257); + + const tokenID = 1; + const fromIdx = 256; + const amountF = float40.fix2Float(10); + await l1UserTxForceExit(tokenID, fromIdx, amountF, owner, hardhatHermez); + }); + }); + + describe("Forge Batch", function () { + it("test L1 deadline", async function () { + // dummy batch + const proofA = ["0", "0"]; + const proofB = [ + ["0", "0"], + ["0", "0"], + ]; + const proofC = ["0", "0"]; + + const newLastIdx = 255; // first idx + const newStateRoot = 0; + const compressedL1CoordinatorTx = "0x00"; + const L2TxsData = "0x00"; + const feeIdxCoordinator = `0x${utils.padZeros( + "", + ((nLevels * 64) / 8) * 2 + )}`; + const verifierIdx = 0; + + let currentBlock = await time.latestBlock(); + await time.advanceBlockTo( + currentBlock.toNumber() + forgeL1L2BatchTimeout + ); + // forgeL1L2BatchTimeout = 10 + + await expect( + hardhatHermez.forgeBatch( + newLastIdx, + newStateRoot, + compressedL1CoordinatorTx, + L2TxsData, + feeIdxCoordinator, + verifierIdx, + false, + proofA, + proofB, + proofC + ) + ).to.be.revertedWith("Hermez::forgeBatch: L1L2BATCH_REQUIRED"); + + // must forge an L1 batch + await hardhatHermez.forgeBatch( + newLastIdx, + newStateRoot, + compressedL1CoordinatorTx, + L2TxsData, + feeIdxCoordinator, + verifierIdx, + true, + proofA, + proofB, + proofC + ); + // can continue forging l2 batches + await hardhatHermez.forgeBatch( + newLastIdx, + newStateRoot, + compressedL1CoordinatorTx, + L2TxsData, + feeIdxCoordinator, + verifierIdx, + false, + proofA, + proofB, + proofC + ); + currentBlock = await time.latestBlock(); + await time.advanceBlockTo( + currentBlock.toNumber() + forgeL1L2BatchTimeout + ); + await expect( + hardhatHermez.forgeBatch( + newLastIdx, + newStateRoot, + compressedL1CoordinatorTx, + L2TxsData, + feeIdxCoordinator, + verifierIdx, + false, + proofA, + proofB, + proofC + ) + ).to.be.revertedWith("Hermez::forgeBatch: L1L2BATCH_REQUIRED"); + }); + + it("handle L1 Coordinator Queue Test", async function () { + const tokenID = 1; + const babyjub = `0x${accounts[0].bjjCompressed}`; + const currentQueue = await hardhatHermez.nextL1ToForgeQueue(); + + await AddToken( + hardhatHermez, + hardhatTokenERC20PermitMock, + hardhatTokenERC20PermitMock, + ownerWallet, + feeAddToken + ); + const l1TxCoordiatorArray = []; + // L1-Tx Coordinator with eth signature + l1TxCoordiatorArray.push( + await l1CoordinatorTxEth(tokenID, babyjub, owner, hardhatHermez, chainIDHex) + ); + + // L1-Tx Coordinator without eth signature: + l1TxCoordiatorArray.push( + await l1CoordinatorTxBjj(tokenID, babyjub, hardhatHermez) + ); + + let stringL1CoordinatorTx = ""; + for (let tx of l1TxCoordiatorArray) { + stringL1CoordinatorTx = + stringL1CoordinatorTx + tx.l1TxCoordinatorbytes.slice(2); // retireve the 0x + } + + // simulate l1-tx batchbuilder: + const fromEthAddrB = 160; + const fromBjjCompressedB = 256; + const f40B = 40; + const tokenIDB = 32; + const maxIdxB = 48; + + const L1TxB = + fromEthAddrB + fromBjjCompressedB + 2 * maxIdxB + tokenIDB + 2 * f40B; + + let jsL1TxData = ""; + for (let tx of l1TxCoordiatorArray) { + jsL1TxData = jsL1TxData + tx.l1TxBytes.slice(2); + } + const dataNopTx = utils.padZeros("", (maxL1Tx - 2) * (L1TxB / 4)); + const simulateBatchbuilderL1TxData = `0x${jsL1TxData + dataNopTx}`; + + const proofA = ["0", "0"]; + const proofB = [ + ["0", "0"], + ["0", "0"], + ]; + const proofC = ["0", "0"]; + + await expect( + hardhatHermez.handleL1QueueTest( + 0, + 0, + `0x${stringL1CoordinatorTx}`, + "0x", + "0x", + 0, + 0, + proofA, + proofB, + proofC + ) + ) + .to.emit(hardhatHermez, "ReturnBytes") + .withArgs(simulateBatchbuilderL1TxData); + }); + + it("forge L1-Coordiator-tx Batch ", async function () { + const tokenID = 1; + const babyjub = `0x${accounts[0].bjjCompressed}`; + + const l1TxCoordiatorArray = []; + const rollupDB = await RollupDB(new SMTMemDB(), chainID); + const forgerTest = new ForgerTest( + maxTx, + maxL1Tx, + nLevels, + hardhatHermez, + rollupDB + ); + + await AddToken( + hardhatHermez, + hardhatTokenERC20PermitMock, + hardhatTokenERC20PermitMock, + ownerWallet, + feeAddToken + ); + + l1TxCoordiatorArray.push( + await l1CoordinatorTxEth(tokenID, babyjub, owner, hardhatHermez, chainIDHex) + ); + + l1TxCoordiatorArray.push( + await l1CoordinatorTxBjj(tokenID, babyjub, hardhatHermez) + ); + + // forge Batch + await forgerTest.forgeBatch(true, [], l1TxCoordiatorArray); + + // after forge, next queue is empty + const currentQueue = await hardhatHermez.nextL1ToForgeQueue(); + expect("0x").to.equal(await hardhatHermez.mapL1TxQueue(currentQueue)); + }); + + it("expect L1-Tx Queue same as batchbuilder ", async function () { + const tokenID = 1; + const babyjub = `0x${accounts[0].bjjCompressed}`; + const loadAmount = float40.round(1000); + const fromIdx = 256; + const toIdx = 257; + const amountF = float40.fix2Float(10); + + const l1TxCoordiatorArray = []; + const l1TxUserArray = []; + + const rollupDB = await RollupDB(new SMTMemDB(), chainID); + const forgerTest = new ForgerTest( + maxTx, + maxL1Tx, + nLevels, + hardhatHermez, + rollupDB + ); + + await AddToken( + hardhatHermez, + hardhatTokenERC20PermitMock, + hardhatTokenERC20PermitMock, + ownerWallet, + feeAddToken + ); + + // In order to add all the possible l1tx we need 2 accounts created in batchbuilder and rollup: + const numAccounts = 2; + await createAccounts( + forgerTest, + loadAmount, + tokenID, + babyjub, + ownerWallet, + hardhatHermez, + hardhatTokenERC20PermitMock, + numAccounts, + true + ); + + // add user l1 tx + l1TxUserArray.push( + await l1UserTxCreateAccountDeposit( + loadAmount, + tokenID, + babyjub, + ownerWallet, + hardhatHermez, + hardhatTokenERC20PermitMock, + true + ) + ); + l1TxUserArray.push( + await l1UserTxDeposit( + loadAmount, + tokenID, + fromIdx, + ownerWallet, + hardhatHermez, + hardhatTokenERC20PermitMock, + true + ) + ); + l1TxUserArray.push( + await l1UserTxDepositTransfer( + loadAmount, + tokenID, + fromIdx, + toIdx, + amountF, + ownerWallet, + hardhatHermez, + hardhatTokenERC20PermitMock, + true + ) + ); + l1TxUserArray.push( + await l1UserTxCreateAccountDepositTransfer( + loadAmount, + tokenID, + toIdx, + amountF, + babyjub, + ownerWallet, + hardhatHermez, + hardhatTokenERC20PermitMock, + true + ) + ); + l1TxUserArray.push( + await l1UserTxForceTransfer( + tokenID, + fromIdx, + toIdx, + amountF, + ownerWallet, + hardhatHermez + ) + ); + l1TxUserArray.push( + await l1UserTxForceExit(tokenID, fromIdx, amountF, ownerWallet, hardhatHermez) + ); + + // forge empty batch, now the current queue is filled with the L1-User-Tx + await forgerTest.forgeBatch(true, [], []); + + // add L1-Tx Coordinator with eth signature + const cordinatorTxEth = await l1CoordinatorTxEth( + tokenID, + babyjub, + ownerWallet, + hardhatHermez, + chainIDHex + ); + + // add L1-Tx Coordinator without eth signature: + const coordinatorTxBjj = await l1CoordinatorTxBjj( + tokenID, + babyjub, + hardhatHermez + ); + + l1TxCoordiatorArray.push(cordinatorTxEth); + l1TxCoordiatorArray.push(coordinatorTxBjj); + + const fromEthAddrB = 160; + const fromBjjCompressedB = 256; + const f40B = 40; + const tokenIDB = 32; + const maxIdxB = 48; + + const L1TxB = + fromEthAddrB + fromBjjCompressedB + 2 * maxIdxB + tokenIDB + 2 * f40B; + // simulate l1-tx batchbuilder: + let jsL1TxData = ""; + for (let tx of l1TxUserArray) { + jsL1TxData = jsL1TxData + tx.slice(2); + } + for (let tx of l1TxCoordiatorArray) { + jsL1TxData = jsL1TxData + tx.l1TxBytes.slice(2); + } + const dataNopTx = utils.padZeros( + "", + (maxL1Tx - l1TxUserArray.length - l1TxCoordiatorArray.length) * + (L1TxB / 4) + ); + const simulateBatchbuilderL1TxData = `0x${jsL1TxData + dataNopTx}`; + + let stringL1CoordinatorTx = ""; + for (let tx of l1TxCoordiatorArray) { + stringL1CoordinatorTx = + stringL1CoordinatorTx + tx.l1TxCoordinatorbytes.slice(2); // retireve the 0x + } + + const proofA = ["0", "0"]; + const proofB = [ + ["0", "0"], + ["0", "0"], + ]; + const proofC = ["0", "0"]; + + await expect( + hardhatHermez.handleL1QueueTest( + 0, + 0, + `0x${stringL1CoordinatorTx}`, + "0x", + "0x", + 0, + true, + proofA, + proofB, + proofC + ) + ) + .to.emit(hardhatHermez, "ReturnBytes") + .withArgs(simulateBatchbuilderL1TxData); + }); + + it("forge L1 user & Coordiator Tx batch", async function () { + const tokenID = 1; + const babyjub = `0x${accounts[0].bjjCompressed}`; + const loadAmount = float40.round(1000); + const fromIdx = 256; + const toIdx = 257; + const amountF = float40.fix2Float(10); + + const l1TxUserArray = []; + + const rollupDB = await RollupDB(new SMTMemDB(), chainID); + const forgerTest = new ForgerTest( + maxTx, + maxL1Tx, + nLevels, + hardhatHermez, + rollupDB + ); + + await AddToken( + hardhatHermez, + hardhatTokenERC20PermitMock, + hardhatTokenERC20PermitMock, + ownerWallet, + feeAddToken + ); + + // In order to add all the possible l1tx we need 2 accounts created in batchbuilder and rollup: + const numAccounts = 2; + await createAccounts( + forgerTest, + loadAmount, + tokenID, + babyjub, + ownerWallet, + hardhatHermez, + hardhatTokenERC20PermitMock, + numAccounts, + true + ); + + // add user l1 tx + l1TxUserArray.push( + await l1UserTxCreateAccountDeposit( + loadAmount, + tokenID, + babyjub, + ownerWallet, + hardhatHermez, + hardhatTokenERC20PermitMock, + true + ) + ); + + l1TxUserArray.push( + await l1UserTxDeposit( + loadAmount, + tokenID, + fromIdx, + ownerWallet, + hardhatHermez, + hardhatTokenERC20PermitMock, + true + ) + ); + l1TxUserArray.push( + await l1UserTxDepositTransfer( + loadAmount, + tokenID, + fromIdx, + toIdx, + amountF, + ownerWallet, + hardhatHermez, + hardhatTokenERC20PermitMock, + true + ) + ); + l1TxUserArray.push( + await l1UserTxCreateAccountDepositTransfer( + loadAmount, + tokenID, + toIdx, + amountF, + babyjub, + ownerWallet, + hardhatHermez, + hardhatTokenERC20PermitMock, + true + ) + ); + l1TxUserArray.push( + await l1UserTxForceTransfer( + tokenID, + fromIdx, + toIdx, + amountF, + ownerWallet, + hardhatHermez + ) + ); + l1TxUserArray.push( + await l1UserTxForceExit(tokenID, fromIdx, amountF, ownerWallet, hardhatHermez) + ); + + // forge empty batch + await forgerTest.forgeBatch(true, [], []); + + const l1TxCoordiatorArray = []; + + // add Coordiator tx + l1TxCoordiatorArray.push( + await l1CoordinatorTxEth(tokenID, babyjub, ownerWallet, hardhatHermez, chainIDHex) + ); + + l1TxCoordiatorArray.push( + await l1CoordinatorTxBjj(tokenID, babyjub, hardhatHermez) + ); + + // forge batch with all the L1 tx + await forgerTest.forgeBatch(true, l1TxUserArray, l1TxCoordiatorArray); + }); + + it("test instant withdraw circuit", async function () { + const tokenID = 1; + const babyjub = `0x${accounts[0].bjjCompressed}`; + const loadAmount = float40.round(1000); + const fromIdx = 256; + const amount = 10; + const amountF = float40.fix2Float(amount); + + const l1TxUserArray = []; + + const rollupDB = await RollupDB(new SMTMemDB(), chainID); + const forgerTest = new ForgerTest( + maxTx, + maxL1Tx, + nLevels, + hardhatHermez, + rollupDB + ); + + await AddToken( + hardhatHermez, + hardhatTokenERC20PermitMock, + hardhatTokenERC20PermitMock, + ownerWallet, + feeAddToken + ); + + // Create account and exit some funds + const numAccounts = 1; + await createAccounts( + forgerTest, + loadAmount, + tokenID, + babyjub, + ownerWallet, + hardhatHermez, + hardhatTokenERC20PermitMock, + numAccounts, + true + ); + + l1TxUserArray.push( + await l1UserTxForceExit(tokenID, fromIdx, amountF, owner, hardhatHermez) + ); + + const initialOwnerBalance = await hardhatTokenERC20PermitMock.balanceOf( + await owner.getAddress() + ); + + // forge empty batch + await forgerTest.forgeBatch(true, [], []); + + // forge batch with all the create account and exit + await forgerTest.forgeBatch(true, l1TxUserArray, []); + + + // perform withdraw + const batchNum = await hardhatHermez.lastForgedBatch(); + const instantWithdraw = true; + const amountWithdraw = amount / 2; + const proofA = ["0", "0"]; + const proofB = [ + ["0", "0"], + ["0", "0"], + ]; + const proofC = ["0", "0"]; + + await expect( + hardhatHermez.withdrawCircuit( + proofA, + proofB, + proofC, + tokenID, + amount, + amountWithdraw, + batchNum, + fromIdx, + instantWithdraw + ) + ) + .to.emit(hardhatHermez, "WithdrawEvent") + .withArgs(amountWithdraw, fromIdx, instantWithdraw); + + expect(amountWithdraw).to.equal( + await hardhatHermez.exitAccumulateMap(fromIdx) + ); + + await expect( + hardhatHermez.withdrawCircuit( + proofA, + proofB, + proofC, + tokenID, + amount, + amountWithdraw*2, + batchNum, + fromIdx, + instantWithdraw + ) + ) + .to.be.revertedWith("Hermez::withdrawCircuit: AMOUNT_WITHDRAW_LESS_THAN_ACCUMULATED"); + + await expect( + hardhatHermez.withdrawCircuit( + proofA, + proofB, + proofC, + tokenID, + amount, + amountWithdraw, + batchNum, + fromIdx, + instantWithdraw + ) + ) + .to.emit(hardhatHermez, "WithdrawEvent") + .withArgs(amountWithdraw, fromIdx, instantWithdraw); + + const finalOwnerBalance = await hardhatTokenERC20PermitMock.balanceOf( + await owner.getAddress() + ); + expect(parseInt(finalOwnerBalance)).to.equal( + parseInt(initialOwnerBalance) + amount + ); + + expect(amount).to.equal( + await hardhatHermez.exitAccumulateMap(fromIdx) + ); + }); + it("test delayed withdraw circuit", async function () { + const tokenID = 1; + const babyjub = `0x${accounts[0].bjjCompressed}`; + const loadAmount = float40.round(1000); + const fromIdx = 256; + const amount = 10; + const amountF = float40.fix2Float(amount); + + const l1TxUserArray = []; + + const rollupDB = await RollupDB(new SMTMemDB(), chainID); + const forgerTest = new ForgerTest( + maxTx, + maxL1Tx, + nLevels, + hardhatHermez, + rollupDB + ); + + await AddToken( + hardhatHermez, + hardhatTokenERC20PermitMock, + hardhatTokenERC20PermitMock, + ownerWallet, + feeAddToken + ); + + // Create account and exit some funds + const numAccounts = 1; + await createAccounts( + forgerTest, + loadAmount, + tokenID, + babyjub, + ownerWallet, + hardhatHermez, + hardhatTokenERC20PermitMock, + numAccounts, + true + ); + + l1TxUserArray.push( + await l1UserTxForceExit(tokenID, fromIdx, amountF, owner, hardhatHermez) + ); + + const initialOwnerBalance = await hardhatTokenERC20PermitMock.balanceOf( + hardhatWithdrawalDelayer.address + ); + + // forge empty batch + await forgerTest.forgeBatch(true, [], []); + + // forge batch with all the create account and exit + await forgerTest.forgeBatch(true, l1TxUserArray, []); + + // perform withdraw + const batchNum = await hardhatHermez.lastForgedBatch(); + const instantWithdraw = false; + const state = await rollupDB.getStateByIdx(256); + const exitInfo = await rollupDB.getExitInfo(256, batchNum); + + const proofA = ["0", "0"]; + const proofB = [ + ["0", "0"], + ["0", "0"], + ]; + const proofC = ["0", "0"]; + + await expect( + hardhatHermez.withdrawCircuit( + proofA, + proofB, + proofC, + tokenID, + amount, + amount, + batchNum, + fromIdx, + instantWithdraw + ) + ) + .to.emit(hardhatHermez, "WithdrawEvent") + .withArgs(amount, fromIdx, instantWithdraw); + + const finalOwnerBalance = await hardhatTokenERC20PermitMock.balanceOf( + hardhatWithdrawalDelayer.address + ); + expect(parseInt(finalOwnerBalance)).to.equal( + parseInt(initialOwnerBalance) + amount + ); + }); + }); +}); diff --git a/test/hermezHIP-0/HermezERC20Upgradability.test.js b/test/hermezHIP-0/HermezERC20Upgradability.test.js new file mode 100644 index 0000000..1d05232 --- /dev/null +++ b/test/hermezHIP-0/HermezERC20Upgradability.test.js @@ -0,0 +1,502 @@ +const { expect } = require("chai"); +const { ethers, upgrades } = require("hardhat"); +const SMTMemDB = require("circomlib").SMTMemDB; +const { time } = require("@openzeppelin/test-helpers"); +const Scalar = require("ffjavascript").Scalar; +const ProxyAdmin = require("@openzeppelin/upgrades-core/artifacts/ProxyAdmin.json"); +const { getAdminAddress } = require("@openzeppelin/upgrades-core"); + +const { + l1UserTxCreateAccountDeposit, + l1UserTxDeposit, + l1UserTxDepositTransfer, + l1UserTxCreateAccountDepositTransfer, + l1UserTxForceTransfer, + l1UserTxForceExit, + l1CoordinatorTxEth, + l1CoordinatorTxBjj, + AddToken, + createAccounts, + ForgerTest, + calculateInputMaxTxLevels +} = require("./helpers/helpers"); +const { + float40, + HermezAccount, + txUtils, + stateUtils, + utils, + feeTable, + SMTTmpDb, + Constants, + RollupDB, + BatchBuilder, +} = require("@hermeznetwork/commonjsV1"); + +describe("Hermez ERC 20 Upgradability", function () { + let hardhatTokenERC20Mock; + let hardhatHermez; + let hardhatWithdrawalDelayer; + let hardhatHEZ; + + let owner; + let id1; + let id2; + let addrs; + let hermezGovernanceAddress; + let ownerWallet; + + let chainID; + let chainIDHex; + + const accounts = []; + for (let i = 0; i < 10; i++) { + accounts.push(new HermezAccount()); + } + const tokenInitialAmount = 1000000; + const maxL1Tx = 256; + const maxTx = 512; + const nLevels = 32; + const forgeL1L2BatchTimeout = 10; + const feeAddToken = 10; + const withdrawalDelay = 60 * 60 * 24 * 7 * 2; // 2 weeks + const INITIAL_DELAY = 0; + + beforeEach(async function () { + [ + owner, + governance, + id1, + id2, + ...addrs + ] = await ethers.getSigners(); + + hermezGovernanceAddress = governance.getAddress(); + + const chainIdProvider = (await ethers.provider.getNetwork()).chainId; + if (chainIdProvider == 1337) { // solcover, must be a jsonRPC wallet + const mnemonic = "explain tackle mirror kit van hammer degree position ginger unfair soup bonus"; + let ownerWalletTest = ethers.Wallet.fromMnemonic(mnemonic); + // ownerWalletTest = ownerWallet.connect(ethers.provider); + ownerWallet = owner; + ownerWallet.privateKey = ownerWalletTest.privateKey; + } + else { + ownerWallet = new ethers.Wallet("0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80", ethers.provider); + } + + // const privateKeyhardhat = + // "0xc5e8f61d1ab959b397eecc0a37a6517b8e67a0e7cf1f4bce5591f3ed80199122"; + // ownerWallet = new ethers.Wallet( + // privateKeyhardhat, + // ethers.provider + // ); + //ownerWallet = new ethers.Wallet("0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80", ethers.provider); + + // factory helpers + const TokenERC20Mock = await ethers.getContractFactory("ERC20Mock"); + const TokenERC20PermitMock = await ethers.getContractFactory("ERC20PermitMock"); + + const VerifierRollupHelper = await ethers.getContractFactory( + "VerifierMock" + ); + const VerifierWithdrawHelper = await ethers.getContractFactory( + "VerifierMock" + ); + + const HermezAuctionTest = await ethers.getContractFactory( + "HermezAuctionTest" + ); + const WithdrawalDelayer = await ethers.getContractFactory( + "WithdrawalDelayerTest" + ); + + // deploy tokens + hardhatTokenERC20Mock = await TokenERC20Mock.deploy( + "tokenname", + "TKN", + await owner.getAddress(), + tokenInitialAmount + ); + + hardhatHEZ = await TokenERC20PermitMock.deploy( + "tokenname", + "TKN", + await owner.getAddress(), + tokenInitialAmount + ); + + // deploy helpers + let hardhatVerifierRollupHelper = await VerifierRollupHelper.deploy(); + let hardhatVerifierWithdrawHelper = await VerifierWithdrawHelper.deploy(); + + let hardhatHermezAuctionTest = await HermezAuctionTest.deploy(); + + + // factory hermez + const Hermez = await ethers.getContractFactory("HermezV2"); + + // Deploy hermez + hardhatHermez = await upgrades.deployProxy(Hermez, [], { + unsafeAllowCustomTypes: true, + initializer: undefined, + }); + await hardhatHermez.deployed(); + + hardhatWithdrawalDelayer = await WithdrawalDelayer.deploy( + INITIAL_DELAY, + hardhatHermez.address, + hermezGovernanceAddress, + hermezGovernanceAddress + ); + + // deploy hermez + await hardhatHermez.initializeHermez( + [hardhatVerifierRollupHelper.address], + calculateInputMaxTxLevels([maxTx], [nLevels]), + hardhatVerifierWithdrawHelper.address, + hardhatHermezAuctionTest.address, + hardhatHEZ.address, + forgeL1L2BatchTimeout, + feeAddToken, + hermezGovernanceAddress, + withdrawalDelay, + hardhatWithdrawalDelayer.address + ); + + const chainSC = await hardhatHermez.getChainId(); + chainID = chainSC.toNumber(); + chainIDHex = chainSC.toHexString(); + }); + + describe("Forge Batch", function () { + it("forge L1 user & Coordiator Tx batch", async function () { + const tokenID = 1; + const babyjub = `0x${accounts[0].bjjCompressed}`; + const loadAmount = float40.round(1000); + const fromIdx = 256; + const toIdx = 257; + const amountF = float40.fix2Float(10); + + const l1TxUserArray = []; + + const rollupDB = await RollupDB(new SMTMemDB(), chainID); + const forgerTest = new ForgerTest( + maxTx, + maxL1Tx, + nLevels, + hardhatHermez, + rollupDB + ); + + await AddToken( + hardhatHermez, + hardhatTokenERC20Mock, + hardhatHEZ, + ownerWallet, + feeAddToken + ); + + // In order to add all the possible l1tx we need 2 accounts created in batchbuilder and rollup: + const numAccounts = 2; + await createAccounts( + forgerTest, + loadAmount, + tokenID, + babyjub, + owner, + hardhatHermez, + hardhatTokenERC20Mock, + numAccounts + ); + + // add user l1 tx + l1TxUserArray.push( + await l1UserTxCreateAccountDeposit( + loadAmount, + tokenID, + babyjub, + owner, + hardhatHermez, + hardhatTokenERC20Mock + ) + ); + l1TxUserArray.push( + await l1UserTxDeposit( + loadAmount, + tokenID, + fromIdx, + owner, + hardhatHermez, + hardhatTokenERC20Mock + ) + ); + l1TxUserArray.push( + await l1UserTxDepositTransfer( + loadAmount, + tokenID, + fromIdx, + toIdx, + amountF, + owner, + hardhatHermez, + hardhatTokenERC20Mock + ) + ); + l1TxUserArray.push( + await l1UserTxCreateAccountDepositTransfer( + loadAmount, + tokenID, + toIdx, + amountF, + babyjub, + owner, + hardhatHermez, + hardhatTokenERC20Mock + ) + ); + l1TxUserArray.push( + await l1UserTxForceTransfer( + tokenID, + fromIdx, + toIdx, + amountF, + owner, + hardhatHermez + ) + ); + l1TxUserArray.push( + await l1UserTxForceExit(tokenID, fromIdx, amountF, owner, hardhatHermez) + ); + + // forge empty batch + await forgerTest.forgeBatch(true, [], []); + + const l1TxCoordiatorArray = []; + + // add Coordiator tx + l1TxCoordiatorArray.push( + await l1CoordinatorTxEth(tokenID, babyjub, owner, hardhatHermez, chainIDHex) + ); + + l1TxCoordiatorArray.push( + await l1CoordinatorTxBjj(tokenID, babyjub, hardhatHermez) + ); + + // upgrade contract and assure that the state is the same! + const HermezV2 = await ethers.getContractFactory("HermezV2MockV2"); + const newHermezV2 = HermezV2.attach(hardhatHermez.address); + await expect(newHermezV2.getVersion()).to.be.reverted; + await upgrades.upgradeProxy(hardhatHermez.address, HermezV2, { + unsafeAllowCustomTypes: true + }); + await newHermezV2.setVersion(); + expect(await newHermezV2.getVersion()).to.be.equal(2); + + // forge batch with all the L1 tx + await forgerTest.forgeBatch(true, l1TxUserArray, l1TxCoordiatorArray); + + }); + + it("test instant withdraw circuit", async function () { + const tokenID = 1; + const babyjub = `0x${accounts[0].bjjCompressed}`; + const loadAmount = float40.round(1000); + const fromIdx = 256; + const amount = 10; + const amountF = float40.fix2Float(amount); + + const l1TxUserArray = []; + + const rollupDB = await RollupDB(new SMTMemDB(), chainID); + const forgerTest = new ForgerTest( + maxTx, + maxL1Tx, + nLevels, + hardhatHermez, + rollupDB + ); + + await AddToken( + hardhatHermez, + hardhatTokenERC20Mock, + hardhatHEZ, + ownerWallet, + feeAddToken + ); + + // Create account and exit some funds + const numAccounts = 1; + await createAccounts( + forgerTest, + loadAmount, + tokenID, + babyjub, + owner, + hardhatHermez, + hardhatTokenERC20Mock, + numAccounts + ); + + l1TxUserArray.push( + await l1UserTxForceExit(tokenID, fromIdx, amountF, owner, hardhatHermez) + ); + + const initialOwnerBalance = await hardhatTokenERC20Mock.balanceOf( + await owner.getAddress() + ); + + // forge empty batch + await forgerTest.forgeBatch(true, [], []); + + // forge batch with all the create account and exit + await forgerTest.forgeBatch(true, l1TxUserArray, []); + + // perform withdraw + const numExitRoot = await hardhatHermez.lastForgedBatch(); + const instantWithdraw = true; + const state = await rollupDB.getStateByIdx(256); + const exitInfo = await rollupDB.getExitInfo(256, numExitRoot); + + // upgrade contract and assure that the state is the same! + const HermezV2 = await ethers.getContractFactory("HermezV2MockV2"); + const newHermezV2 = HermezV2.attach(hardhatHermez.address); + await expect(newHermezV2.getVersion()).to.be.reverted; + await upgrades.upgradeProxy(hardhatHermez.address, HermezV2, { + unsafeAllowCustomTypes: true + }); + await newHermezV2.setVersion(); + expect(await newHermezV2.getVersion()).to.be.equal(2); + + const proofA = ["0", "0"]; + const proofB = [ + ["0", "0"], + ["0", "0"], + ]; + const proofC = ["0", "0"]; + + await expect( + newHermezV2.withdrawCircuit( + proofA, + proofB, + proofC, + tokenID, + amount, + amount, + numExitRoot, + fromIdx, + instantWithdraw + ) + ) + .to.emit(newHermezV2, "WithdrawEvent") + .withArgs(amount, fromIdx, instantWithdraw); + const finalOwnerBalance = await hardhatTokenERC20Mock.balanceOf( + await owner.getAddress() + ); + expect(parseInt(finalOwnerBalance)).to.equal( + parseInt(initialOwnerBalance) + amount + ); + }); + it("test delayed withdraw circuit", async function () { + const tokenID = 1; + const babyjub = `0x${accounts[0].bjjCompressed}`; + const loadAmount = float40.round(1000); + const fromIdx = 256; + const amount = 10; + const amountF = float40.fix2Float(amount); + + const l1TxUserArray = []; + + const rollupDB = await RollupDB(new SMTMemDB(), chainID); + const forgerTest = new ForgerTest( + maxTx, + maxL1Tx, + nLevels, + hardhatHermez, + rollupDB + ); + + await AddToken( + hardhatHermez, + hardhatTokenERC20Mock, + hardhatHEZ, + ownerWallet, + feeAddToken + ); + + // Create account and exit some funds + const numAccounts = 1; + await createAccounts( + forgerTest, + loadAmount, + tokenID, + babyjub, + owner, + hardhatHermez, + hardhatTokenERC20Mock, + numAccounts + ); + + l1TxUserArray.push( + await l1UserTxForceExit(tokenID, fromIdx, amountF, owner, hardhatHermez) + ); + + const initialOwnerBalance = await hardhatTokenERC20Mock.balanceOf( + hardhatWithdrawalDelayer.address + ); + + // forge empty batch + await forgerTest.forgeBatch(true, [], []); + + // forge batch with all the create account and exit + await forgerTest.forgeBatch(true, l1TxUserArray, []); + + // upgrade contract and assure that the state is the same! + const HermezV2 = await ethers.getContractFactory("HermezV2MockV2"); + const newHermezV2 = HermezV2.attach(hardhatHermez.address); + await expect(newHermezV2.getVersion()).to.be.reverted; + await upgrades.upgradeProxy(hardhatHermez.address, HermezV2, { + unsafeAllowCustomTypes: true + }); + await newHermezV2.setVersion(); + expect(await newHermezV2.getVersion()).to.be.equal(2); + + // perform withdraw + const numExitRoot = await hardhatHermez.lastForgedBatch(); + const instantWithdraw = false; + const state = await rollupDB.getStateByIdx(256); + const exitInfo = await rollupDB.getExitInfo(256, numExitRoot); + + const proofA = ["0", "0"]; + const proofB = [ + ["0", "0"], + ["0", "0"], + ]; + const proofC = ["0", "0"]; + + await expect( + newHermezV2.withdrawCircuit( + proofA, + proofB, + proofC, + tokenID, + amount, + amount, + numExitRoot, + fromIdx, + instantWithdraw + ) + ) + .to.emit(newHermezV2, "WithdrawEvent") + .withArgs(amount, fromIdx, instantWithdraw); + + const finalOwnerBalance = await hardhatTokenERC20Mock.balanceOf( + hardhatWithdrawalDelayer.address + ); + expect(parseInt(finalOwnerBalance)).to.equal( + parseInt(initialOwnerBalance) + amount + ); + }); + }); +}); + diff --git a/test/hermezHIP-0/HermezEth.test.js b/test/hermezHIP-0/HermezEth.test.js new file mode 100644 index 0000000..f81d135 --- /dev/null +++ b/test/hermezHIP-0/HermezEth.test.js @@ -0,0 +1,638 @@ +const { expect } = require("chai"); +const { ethers } = require("hardhat"); +const SMTMemDB = require("circomlib").SMTMemDB; +const { time } = require("@openzeppelin/test-helpers"); +const Scalar = require("ffjavascript").Scalar; + +const { + l1UserTxCreateAccountDeposit, + l1UserTxDeposit, + l1UserTxDepositTransfer, + l1UserTxCreateAccountDepositTransfer, + l1UserTxForceTransfer, + l1UserTxForceExit, + l1CoordinatorTxEth, + l1CoordinatorTxBjj, + AddToken, + createAccounts, + ForgerTest, + calculateInputMaxTxLevels +} = require("./helpers/helpers"); +const { + float40, + HermezAccount, + txUtils, + stateUtils, + utils, + feeTable, + SMTTmpDb, + Constants, + RollupDB, + BatchBuilder, +} = require("@hermeznetwork/commonjsV1"); + +const babyjub0 = 0; +const fromIdx0 = 0; +const loadAmountF0 = 0; +const amountF0 = 0; +const tokenID0 = 0; +const toIdx0 = 0; +const emptyPermit = "0x"; +const INITIAL_DELAY = 0; + +describe("Hermez ETH test", function () { + let hardhatTokenERC20Mock; + let hardhatHermez; + let hardhatWithdrawalDelayer; + let hardhatHEZ; + let owner; + let id1; + let id2; + let addrs; + let hermezGovernanceAddress; + let ownerWallet; + + let chainID; + let chainIDHex; + + const accounts = []; + for (let i = 0; i < 10; i++) { + accounts.push(new HermezAccount()); + } + const tokenInitialAmount = 1000000; + const maxL1Tx = 256; + const maxTx = 512; + const nLevels = 32; + const forgeL1L2BatchTimeout = 10; + const feeAddToken = 10; + const withdrawalDelay = 60 * 60 * 24 * 7 * 2; // 2 weeks + + beforeEach(async function () { + [ + owner, + governance, + id1, + id2, + ...addrs + ] = await ethers.getSigners(); + + hermezGovernanceAddress = governance.getAddress(); + + const chainIdProvider = (await ethers.provider.getNetwork()).chainId; + if (chainIdProvider == 1337) { // solcover, must be a jsonRPC wallet + const mnemonic = "explain tackle mirror kit van hammer degree position ginger unfair soup bonus"; + let ownerWalletTest = ethers.Wallet.fromMnemonic(mnemonic); + // ownerWalletTest = ownerWallet.connect(ethers.provider); + ownerWallet = owner; + ownerWallet.privateKey = ownerWalletTest.privateKey; + } + else { + ownerWallet = new ethers.Wallet("0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80", ethers.provider); + } + + // factory helpers + const TokenERC20Mock = await ethers.getContractFactory("ERC20Mock"); + const TokenERC20PermitMock = await ethers.getContractFactory("ERC20PermitMock"); + + const VerifierRollupHelper = await ethers.getContractFactory( + "VerifierMock" + ); + const VerifierWithdrawHelper = await ethers.getContractFactory( + "VerifierMock" + ); + + const HermezAuctionTest = await ethers.getContractFactory( + "HermezAuctionTest" + ); + const WithdrawalDelayer = await ethers.getContractFactory( + "WithdrawalDelayerTest" + ); + + // factory hermez + const Hermez = await ethers.getContractFactory("HermezTestV2"); + + // deploy tokens + hardhatTokenERC20Mock = await TokenERC20Mock.deploy( + "tokenname", + "TKN", + await owner.getAddress(), + tokenInitialAmount + ); + + hardhatHEZ = await TokenERC20PermitMock.deploy( + "tokenname", + "TKN", + await owner.getAddress(), + tokenInitialAmount + ); + + let hardhatVerifierRollupHelper = await VerifierRollupHelper.deploy(); + let hardhatVerifierWithdrawHelper = await VerifierWithdrawHelper.deploy(); + + let hardhatHermezAuctionTest = await HermezAuctionTest.deploy(); + + // deploy hermez + hardhatHermez = await Hermez.deploy(); + await hardhatHermez.deployed(); + hardhatWithdrawalDelayer = await WithdrawalDelayer.deploy( + INITIAL_DELAY, + hardhatHermez.address, + hermezGovernanceAddress, + hermezGovernanceAddress + ); + + // deploy hermez + await hardhatHermez.initializeHermez( + [hardhatVerifierRollupHelper.address], + calculateInputMaxTxLevels([maxTx], [nLevels]), + hardhatVerifierWithdrawHelper.address, + hardhatHermezAuctionTest.address, + hardhatHEZ.address, + forgeL1L2BatchTimeout, + feeAddToken, + hermezGovernanceAddress, + withdrawalDelay, + hardhatWithdrawalDelayer.address + ); + + // wait until is deployed + await hardhatTokenERC20Mock.deployed(); + + const chainSC = await hardhatHermez.getChainID(); + chainID = chainSC.toNumber(); + chainIDHex = chainSC.toHexString(); + }); + + // You can nest describe calls to create subsections. + describe("L1-user-Tx", function () { + it("createAccountDeposit", async function () { + await AddToken( + hardhatHermez, + hardhatTokenERC20Mock, + hardhatHEZ, + ownerWallet, + feeAddToken + ); + const loadAmount = float40.round(1000); + const tokenID = 0; + const babyjub = `0x${accounts[0].bjjCompressed}`; + + // revert msg.value less than loadAmount + const loadAmountF = float40.fix2Float(loadAmount); + await expect( + hardhatHermez.addL1Transaction( + babyjub, + fromIdx0, + loadAmountF, + amountF0, + tokenID, + toIdx0, + emptyPermit, + { + value: loadAmount - Scalar.e(1), + gasPrice: 0, + } + ) + ).to.be.revertedWith("Hermez::addL1Transaction: LOADAMOUNT_ETH_DOES_NOT_MATCH"); + + await l1UserTxCreateAccountDeposit( + loadAmount, + tokenID, + babyjub, + ownerWallet, + hardhatHermez, + hardhatTokenERC20Mock + ); + }); + + it("deposit", async function () { + await AddToken( + hardhatHermez, + hardhatTokenERC20Mock, + hardhatHEZ, + ownerWallet, + feeAddToken + ); + // invalid operation in Hermez.sol, test purposes + hardhatHermez.changeCurrentIdx(257); + + const loadAmount = float40.round(1000); + const tokenID = 0; + const fromIdx = 256; + + // revert msg.value less than loadAmount + const loadAmountF = float40.fix2Float(loadAmount); + await expect( + hardhatHermez.addL1Transaction( + babyjub0, + fromIdx, + loadAmountF, + amountF0, + tokenID, + toIdx0, + emptyPermit, + { + value: loadAmount - Scalar.e(1), + gasPrice: 0, + } + ) + ).to.be.revertedWith("Hermez::addL1Transaction: LOADAMOUNT_ETH_DOES_NOT_MATCH"); + + await l1UserTxDeposit( + loadAmount, + tokenID, + fromIdx, + ownerWallet, + hardhatHermez, + hardhatTokenERC20Mock + ); + }); + it("depositTransfer", async function () { + await AddToken( + hardhatHermez, + hardhatTokenERC20Mock, + hardhatHEZ, + ownerWallet, + feeAddToken + ); + // invalid operation in Hermez.sol, test purposes + hardhatHermez.changeCurrentIdx(257); + + const loadAmount = float40.round(1000); + const tokenID = 0; + const fromIdx = 256; + const toIdx = 257; + const amountF = float40.fix2Float(10); + + // revert msg.value less than loadAmount + const loadAmountF = float40.fix2Float(loadAmount); + await expect( + hardhatHermez.addL1Transaction( + babyjub0, + fromIdx, + loadAmountF, + amountF, + tokenID, + toIdx, + emptyPermit, + { + value: loadAmount - Scalar.e(1), + gasPrice: 0, + } + ) + ).to.be.revertedWith("Hermez::addL1Transaction: LOADAMOUNT_ETH_DOES_NOT_MATCH"); + + await l1UserTxDepositTransfer( + loadAmount, + tokenID, + fromIdx, + toIdx, + amountF, + ownerWallet, + hardhatHermez, + hardhatTokenERC20Mock + ); + }); + it("createAccountDepositTransfer", async function () { + await AddToken( + hardhatHermez, + hardhatTokenERC20Mock, + hardhatHEZ, + ownerWallet, + feeAddToken + ); + // invalid operation in Hermez.sol, test purposes + hardhatHermez.changeCurrentIdx(257); + + const loadAmount = float40.round(1000); + const tokenID = 0; + const toIdx = 257; + const amountF = float40.fix2Float(10); + const babyjub = `0x${accounts[0].bjjCompressed}`; + + // revert msg.value less than loadAmount + const loadAmountF = float40.fix2Float(loadAmount); + await expect( + hardhatHermez.addL1Transaction( + babyjub, + fromIdx0, + loadAmountF, + amountF, + tokenID, + toIdx, + emptyPermit, + { + value: loadAmount - Scalar.e(1), + gasPrice: 0, + } + ) + ).to.be.revertedWith("Hermez::addL1Transaction: LOADAMOUNT_ETH_DOES_NOT_MATCH"); + + await l1UserTxCreateAccountDepositTransfer( + loadAmount, + tokenID, + toIdx, + amountF, + babyjub, + ownerWallet, + hardhatHermez, + hardhatTokenERC20Mock + ); + }); + }); + + describe("Forge Batch", function () { + it("forge L1 user & Coordiator Tx batch", async function () { + const tokenID = 0; + const babyjub = `0x${accounts[0].bjjCompressed}`; + const loadAmount = float40.round(1000); + const fromIdx = 256; + const toIdx = 257; + const amountF = float40.fix2Float(10); + + const l1TxUserArray = []; + + const rollupDB = await RollupDB(new SMTMemDB(), chainID); + const forgerTest = new ForgerTest( + maxTx, + maxL1Tx, + nLevels, + hardhatHermez, + rollupDB + ); + + await AddToken( + hardhatHermez, + hardhatTokenERC20Mock, + hardhatHEZ, + ownerWallet, + feeAddToken + ); + // In order to add all the possible l1tx we need 2 accounts created in batchbuilder and rollup: + const numAccounts = 2; + await createAccounts( + forgerTest, + loadAmount, + tokenID, + babyjub, + ownerWallet, + hardhatHermez, + hardhatTokenERC20Mock, + numAccounts + ); + + // add user l1 tx + l1TxUserArray.push( + await l1UserTxCreateAccountDeposit( + loadAmount, + tokenID, + babyjub, + ownerWallet, + hardhatHermez, + hardhatTokenERC20Mock + ) + ); + + l1TxUserArray.push( + await l1UserTxDeposit( + loadAmount, + tokenID, + fromIdx, + ownerWallet, + hardhatHermez, + hardhatTokenERC20Mock + ) + ); + l1TxUserArray.push( + await l1UserTxDepositTransfer( + loadAmount, + tokenID, + fromIdx, + toIdx, + amountF, + ownerWallet, + hardhatHermez, + hardhatTokenERC20Mock + ) + ); + l1TxUserArray.push( + await l1UserTxCreateAccountDepositTransfer( + loadAmount, + tokenID, + toIdx, + amountF, + babyjub, + ownerWallet, + hardhatHermez, + hardhatTokenERC20Mock + ) + ); + l1TxUserArray.push( + await l1UserTxForceTransfer( + tokenID, + fromIdx, + toIdx, + amountF, + ownerWallet, + hardhatHermez + ) + ); + l1TxUserArray.push( + await l1UserTxForceExit(tokenID, fromIdx, amountF, ownerWallet, hardhatHermez) + ); + + // forge empty batch + await forgerTest.forgeBatch(true, [], []); + + const l1TxCoordiatorArray = []; + + // add Coordiator tx + l1TxCoordiatorArray.push( + await l1CoordinatorTxEth(tokenID, babyjub, ownerWallet, hardhatHermez, chainIDHex) + ); + + l1TxCoordiatorArray.push( + await l1CoordinatorTxBjj(tokenID, babyjub, hardhatHermez) + ); + + // forge batch with all the L1 tx + await forgerTest.forgeBatch(true, l1TxUserArray, l1TxCoordiatorArray); + }); + + it("test instant withdraw circuit", async function () { + const tokenID = 0; + const babyjub = `0x${accounts[0].bjjCompressed}`; + const loadAmount = float40.round(1000); + const fromIdx = 256; + const amount = 10; + const amountF = float40.fix2Float(amount); + + const l1TxUserArray = []; + + const rollupDB = await RollupDB(new SMTMemDB(), chainID); + const forgerTest = new ForgerTest( + maxTx, + maxL1Tx, + nLevels, + hardhatHermez, + rollupDB + ); + + await AddToken( + hardhatHermez, + hardhatTokenERC20Mock, + hardhatHEZ, + ownerWallet, + feeAddToken + ); + // Create account and exit some funds + const numAccounts = 1; + await createAccounts( + forgerTest, + loadAmount, + tokenID, + babyjub, + ownerWallet, + hardhatHermez, + hardhatTokenERC20Mock, + numAccounts + ); + + l1TxUserArray.push( + await l1UserTxForceExit(tokenID, fromIdx, amountF, ownerWallet, hardhatHermez) + ); + // forge empty batch + await forgerTest.forgeBatch(true, [], []); + + // forge batch with all the create account and exit + await forgerTest.forgeBatch(true, l1TxUserArray, []); + + const initialOwnerBalance = await owner.getBalance(); + + // perform withdraw + const numExitRoot = await hardhatHermez.lastForgedBatch(); + const instantWithdraw = true; + const state = await rollupDB.getStateByIdx(256); + const exitInfo = await rollupDB.getExitInfo(256, numExitRoot); + + const proofA = ["0", "0"]; + const proofB = [ + ["0", "0"], + ["0", "0"], + ]; + const proofC = ["0", "0"]; + + await expect( + hardhatHermez.withdrawCircuit( + proofA, + proofB, + proofC, + tokenID, + amount, + amount, + numExitRoot, + fromIdx, + instantWithdraw, + { + gasPrice: 0, + } + ) + ) + .to.emit(hardhatHermez, "WithdrawEvent") + .withArgs(amount, fromIdx, instantWithdraw); + const finalOwnerBalance = await owner.getBalance(); + + expect(parseInt(finalOwnerBalance)).to.equal( + parseInt(initialOwnerBalance) + amount + ); + }); + + it("test delayed withdraw circuit with ether", async function () { + const tokenID = 0; + const babyjub = `0x${accounts[0].bjjCompressed}`; + const loadAmount = float40.round(1000); + const fromIdx = 256; + const amount = 10; + const amountF = float40.fix2Float(amount); + + const l1TxUserArray = []; + + const rollupDB = await RollupDB(new SMTMemDB(), chainID); + const forgerTest = new ForgerTest( + maxTx, + maxL1Tx, + nLevels, + hardhatHermez, + rollupDB + ); + + // Create account and exit some funds + const numAccounts = 1; + await createAccounts( + forgerTest, + loadAmount, + tokenID, + babyjub, + ownerWallet, + hardhatHermez, + null, // token contract but ether is used + numAccounts + ); + + l1TxUserArray.push( + await l1UserTxForceExit(tokenID, fromIdx, amountF, ownerWallet, hardhatHermez) + ); + + // forge empty batch + await forgerTest.forgeBatch(true, [], []); + + // forge batch with all the create account and exit + await forgerTest.forgeBatch(true, l1TxUserArray, []); + + const provider = owner.provider; + const initialWithdrawalBalance = await provider.getBalance( + hardhatWithdrawalDelayer.address + ); + + // perform withdraw + const numExitRoot = await hardhatHermez.lastForgedBatch(); + const instantWithdraw = false; + const state = await rollupDB.getStateByIdx(256); + const exitInfo = await rollupDB.getExitInfo(256, numExitRoot); + + const proofA = ["0", "0"]; + const proofB = [ + ["0", "0"], + ["0", "0"], + ]; + const proofC = ["0", "0"]; + + await expect( + hardhatHermez.withdrawCircuit( + proofA, + proofB, + proofC, + tokenID, + amount, + amount, + numExitRoot, + fromIdx, + instantWithdraw, + { + gasPrice: 0, + } + ) + ) + .to.emit(hardhatHermez, "WithdrawEvent") + .withArgs(amount, fromIdx, instantWithdraw); + + const finalWithdrawalBalance = await provider.getBalance( + hardhatWithdrawalDelayer.address + ); + + expect(parseInt(finalWithdrawalBalance)).to.equal( + parseInt(initialWithdrawalBalance) + amount + ); + }); + }); +}); diff --git a/test/hermezHIP-0/HermezForgeRealProverExitTODO.js b/test/hermezHIP-0/HermezForgeRealProverExitTODO.js new file mode 100644 index 0000000..4fac46a --- /dev/null +++ b/test/hermezHIP-0/HermezForgeRealProverExitTODO.js @@ -0,0 +1,275 @@ +const { expect } = require("chai"); +const { ethers } = require("../../node_modules/hardhat"); +const SMTMemDB = require("circomlib").SMTMemDB; +const { time } = require("@openzeppelin/test-helpers"); +const Scalar = require("ffjavascript").Scalar; + +const { + l1UserTxCreateAccountDeposit, + l1UserTxDeposit, + l1UserTxDepositTransfer, + l1UserTxCreateAccountDepositTransfer, + l1UserTxForceTransfer, + l1UserTxForceExit, + l1CoordinatorTxEth, + l1CoordinatorTxBjj, + AddToken, + createAccounts, + ForgerTest, + calculateInputMaxTxLevels +} = require("./helpers/helpers"); +const { + float16, + HermezAccount, + txUtils, + stateUtils, + utils, + feeTable, + SMTTmpDb, + Constants, + RollupDB, + BatchBuilder, +} = require("@hermeznetwork/commonjsV1"); + +describe("Hermez ERC 20", function () { + let hardhatTokenERC20Mock; + let hardhatHermez; + let hardhatWithdrawalDelayer; + let hardhatHEZ; + + let owner; + let id1; + let id2; + let addrs; + let hermezGovernanceAddress; + let ownerWallet; + + let chainID; + let chainIDHex; + + const accounts = []; + + // set accounts + for (let i = 0; i < 10; i++) { + const newHermezAccount = new HermezAccount(); + const newAccount = {}; + newAccount.hermezAccount = newHermezAccount; + newAccount.bjjCompressed = `0x${newHermezAccount.bjjCompressed}`; + newAccount.idx = 256 + i; // first idx --> 256 + accounts.push(newAccount); + } + + const tokenInitialAmount = 1000000; + const maxL1Tx = 256; + const maxTx = 376; + const nLevels = 32; + const forgeL1L2BatchTimeout = 10; + const feeAddToken = 10; + const withdrawalDelay = 60 * 60 * 24 * 7 * 2; // 2 weeks + const INITIAL_DELAY = 0; + + beforeEach(async function () { + [ + owner, + governance, + id1, + id2, + ...addrs + ] = await ethers.getSigners(); + + hermezGovernanceAddress = governance.getAddress(); + + const chainIdProvider = (await ethers.provider.getNetwork()).chainId; + if (chainIdProvider == 1337) { // solcover, must be a jsonRPC wallet + const mnemonic = "explain tackle mirror kit van hammer degree position ginger unfair soup bonus"; + let ownerWalletTest = ethers.Wallet.fromMnemonic(mnemonic); + // ownerWalletTest = ownerWallet.connect(ethers.provider); + ownerWallet = owner; + ownerWallet.privateKey = ownerWalletTest.privateKey; + } + else { + ownerWallet = new ethers.Wallet(ethers.provider._hardhatProvider._genesisAccounts[0].privateKey, ethers.provider); + } + + const mnemonic = "explain tackle mirror kit van hammer degree position ginger unfair soup bonus"; + let ownerWalletTest = ethers.Wallet.fromMnemonic(mnemonic); + console.log(ownerWallet.address, ownerWalletTest.address); + // const privateKeyhardhat = + // "0xc5e8f61d1ab959b397eecc0a37a6517b8e67a0e7cf1f4bce5591f3ed80199122"; + // ownerWallet = new ethers.Wallet( + // privateKeyhardhat, + // ethers.provider + // ); + //ownerWallet = new ethers.Wallet(ethers.provider._hardhatProvider._genesisAccounts[0].privateKey, ethers.provider); + + // factory helpers + const TokenERC20Mock = await ethers.getContractFactory("ERC20Mock"); + const TokenERC20PermitMock = await ethers.getContractFactory("ERC20PermitMock"); + + const VerifierRollupHelper = await ethers.getContractFactory( + "Verifier" + ); + const VerifierWithdrawHelper = await ethers.getContractFactory( + "VerifierMock" + ); + + const HermezAuctionTest = await ethers.getContractFactory( + "HermezAuctionTest" + ); + const WithdrawalDelayer = await ethers.getContractFactory( + "WithdrawalDelayerTest" + ); + + + + + // factory hermez + const Hermez = await ethers.getContractFactory("HermezTestV2"); + + // deploy tokens + hardhatTokenERC20Mock = await TokenERC20Mock.deploy( + "tokenname", + "TKN", + await owner.getAddress(), + tokenInitialAmount + ); + + hardhatHEZ = await TokenERC20PermitMock.deploy( + "tokenname", + "TKN", + await owner.getAddress(), + tokenInitialAmount + ); + + // deploy helpers + let hardhatVerifierRollupHelper = await VerifierRollupHelper.deploy(); + let hardhatVerifierWithdrawHelper = await VerifierWithdrawHelper.deploy(); + + let hardhatHermezAuctionTest = await HermezAuctionTest.deploy(); + + // deploy hermez + hardhatHermez = await Hermez.deploy(); + await hardhatHermez.deployed(); + + hardhatWithdrawalDelayer = await WithdrawalDelayer.deploy(); + await hardhatWithdrawalDelayer.withdrawalDelayerInitializer( + INITIAL_DELAY, + hardhatHermez.address, + hermezGovernanceAddress, + hermezGovernanceAddress + ); + + // deploy hermez + await hardhatHermez.initializeHermez( + [hardhatVerifierRollupHelper.address], + calculateInputMaxTxLevels([maxTx], [nLevels]), + hardhatVerifierWithdrawHelper.address, + hardhatHermezAuctionTest.address, + hardhatHEZ.address, + forgeL1L2BatchTimeout, + feeAddToken, + hermezGovernanceAddress, + withdrawalDelay, + hardhatWithdrawalDelayer.address + ); + + // wait until is deployed + await hardhatTokenERC20Mock.deployed(); + + const chainSC = await hardhatHermez.getChainID(); + chainID = chainSC.toNumber(); + chainIDHex = chainSC.toHexString(); + }); + + + describe("Forge Batch", function () { + this.timeout(0); + it("Create l2 Tx and forge them", async function () { + const tokenIdERC20 = await AddToken( + hardhatHermez, + hardhatTokenERC20Mock, + hardhatHEZ, + ownerWallet, + feeAddToken + ); + + const loadAmount = float16.float2Fix(float16.fix2Float(1000)); + + const l1TxUserArray = []; + l1TxUserArray.push(await l1UserTxCreateAccountDeposit( + loadAmount, + tokenIdERC20, + accounts[0].bjjCompressed, + owner, + hardhatHermez, + hardhatTokenERC20Mock + )); + l1TxUserArray.push(await l1UserTxCreateAccountDeposit( + loadAmount, + tokenIdERC20, + accounts[1].bjjCompressed, + owner, + hardhatHermez, + hardhatTokenERC20Mock + )); + + l1TxUserArray.push(await l1UserTxCreateAccountDeposit( + loadAmount, + tokenIdERC20, + accounts[2].bjjCompressed, + owner, + hardhatHermez, + hardhatTokenERC20Mock + )); + + const rollupDB = await RollupDB(new SMTMemDB(), chainID); + const forgerTest = new ForgerTest( + maxTx, + maxL1Tx, + nLevels, + hardhatHermez, + rollupDB, + true + ); + + // forge empty batch + await forgerTest.forgeBatch(true, [], []); + + // forge batch with all the L1 tx + await forgerTest.forgeBatch(true, l1TxUserArray, []); + + const l1TxCoordiatorArray = []; + + const l2TxUserArray = []; + + const tx = { + fromIdx: accounts[0].idx, + toIdx: Constants.exitIdx, //Constants.exitIdx + tokenID: tokenIdERC20.toNumber(), + amount: Scalar.e(40), + nonce: 0, + chainID: chainID, + userFee: 0, // 0 + }; + + accounts[0].hermezAccount.signTx(tx); + l2TxUserArray.push(tx); + + + l1TxCoordiatorArray.push( + await l1CoordinatorTxEth(tokenIdERC20, accounts[0].bjjCompressed, owner, hardhatHermez, chainIDHex) + ); + + await forgerTest.forgeBatch(true, [], l1TxCoordiatorArray, l2TxUserArray, true); + + + const s1 = await rollupDB.getStateByIdx(256); + expect(s1.sign).to.be.equal(accounts[0].hermezAccount.sign); + expect(s1.ay).to.be.equal(accounts[0].hermezAccount.ay); + // expect(s1.balance.toString()).to.be.equal(Scalar.e(950).toString()); // 1000(loadAmount) - 40(amount) - 10(fee) = 950 + expect(s1.tokenID).to.be.equal(tokenIdERC20); + expect(s1.nonce).to.be.equal(1); + }); + + }); +}); diff --git a/test/hermezHIP-0/HermezForgeRealProverTODO.js b/test/hermezHIP-0/HermezForgeRealProverTODO.js new file mode 100644 index 0000000..42169ec --- /dev/null +++ b/test/hermezHIP-0/HermezForgeRealProverTODO.js @@ -0,0 +1,512 @@ +const { expect } = require("chai"); +const { ethers } = require("../../node_modules/hardhat"); +const SMTMemDB = require("circomlib").SMTMemDB; +const { time } = require("@openzeppelin/test-helpers"); +const Scalar = require("ffjavascript").Scalar; + +const { + l1UserTxCreateAccountDeposit, + l1UserTxDeposit, + l1UserTxDepositTransfer, + l1UserTxCreateAccountDepositTransfer, + l1UserTxForceTransfer, + l1UserTxForceExit, + l1CoordinatorTxEth, + l1CoordinatorTxBjj, + AddToken, + createAccounts, + ForgerTest, + calculateInputMaxTxLevels +} = require("./helpers/helpers"); +const { + float16, + HermezAccount, + txUtils, + stateUtils, + utils, + feeTable, + SMTTmpDb, + Constants, + RollupDB, + BatchBuilder, +} = require("@hermeznetwork/commonjsV1"); + +describe("Hermez ERC 20", function () { + let hardhatTokenERC20Mock; + let hardhatHermez; + let hardhatWithdrawalDelayer; + let hardhatHEZ; + + let owner; + let id1; + let id2; + let addrs; + let hermezGovernanceAddress; + let ownerWallet; + + let chainID; + let chainIDHex; + + const accounts = []; + for (let i = 0; i < 10; i++) { + accounts.push(new HermezAccount()); + } + const tokenInitialAmount = 1000000; + const maxL1Tx = 256; + const maxTx = 376; + const nLevels = 32; + const forgeL1L2BatchTimeout = 10; + const feeAddToken = 10; + const withdrawalDelay = 60 * 60 * 24 * 7 * 2; // 2 weeks + const INITIAL_DELAY = 0; + + beforeEach(async function () { + [ + owner, + governance, + id1, + id2, + ...addrs + ] = await ethers.getSigners(); + + hermezGovernanceAddress = governance.getAddress(); + + const chainIdProvider = (await ethers.provider.getNetwork()).chainId; + if (chainIdProvider == 1337) { // solcover, must be a jsonRPC wallet + const mnemonic = "explain tackle mirror kit van hammer degree position ginger unfair soup bonus"; + let ownerWalletTest = ethers.Wallet.fromMnemonic(mnemonic); + // ownerWalletTest = ownerWallet.connect(ethers.provider); + ownerWallet = owner; + ownerWallet.privateKey = ownerWalletTest.privateKey; + } + else { + ownerWallet = new ethers.Wallet(ethers.provider._hardhatProvider._genesisAccounts[0].privateKey, ethers.provider); + } + + // const privateKeyhardhat = + // "0xc5e8f61d1ab959b397eecc0a37a6517b8e67a0e7cf1f4bce5591f3ed80199122"; + // ownerWallet = new ethers.Wallet( + // privateKeyhardhat, + // ethers.provider + // ); + //ownerWallet = new ethers.Wallet(ethers.provider._hardhatProvider._genesisAccounts[0].privateKey, ethers.provider); + + // factory helpers + const TokenERC20Mock = await ethers.getContractFactory("ERC20Mock"); + const TokenERC20PermitMock = await ethers.getContractFactory("ERC20PermitMock"); + + const VerifierRollupHelper = await ethers.getContractFactory( + "Verifier" + ); + const VerifierWithdrawHelper = await ethers.getContractFactory( + "VerifierMock" + ); + + const HermezAuctionTest = await ethers.getContractFactory( + "HermezAuctionTest" + ); + const WithdrawalDelayer = await ethers.getContractFactory( + "WithdrawalDelayerTest" + ); + + + + + // factory hermez + const Hermez = await ethers.getContractFactory("HermezTestV2"); + + // deploy tokens + hardhatTokenERC20Mock = await TokenERC20Mock.deploy( + "tokenname", + "TKN", + await owner.getAddress(), + tokenInitialAmount + ); + + hardhatHEZ = await TokenERC20PermitMock.deploy( + "tokenname", + "TKN", + await owner.getAddress(), + tokenInitialAmount + ); + + // deploy helpers + let hardhatVerifierRollupHelper = await VerifierRollupHelper.deploy(); + let hardhatVerifierWithdrawHelper = await VerifierWithdrawHelper.deploy(); + + let hardhatHermezAuctionTest = await HermezAuctionTest.deploy(); + + // deploy hermez + hardhatHermez = await Hermez.deploy(); + await hardhatHermez.deployed(); + + hardhatWithdrawalDelayer = await WithdrawalDelayer.deploy(); + await hardhatWithdrawalDelayer.withdrawalDelayerInitializer( + INITIAL_DELAY, + hardhatHermez.address, + hermezGovernanceAddress, + hermezGovernanceAddress + ); + + // deploy hermez + await hardhatHermez.initializeHermez( + [hardhatVerifierRollupHelper.address], + calculateInputMaxTxLevels([maxTx], [nLevels]), + hardhatVerifierWithdrawHelper.address, + hardhatHermezAuctionTest.address, + hardhatHEZ.address, + forgeL1L2BatchTimeout, + feeAddToken, + hermezGovernanceAddress, + withdrawalDelay, + hardhatWithdrawalDelayer.address + ); + + // wait until is deployed + await hardhatTokenERC20Mock.deployed(); + + const chainSC = await hardhatHermez.getChainID(); + chainID = chainSC.toNumber(); + chainIDHex = chainSC.toHexString(); + }); + + + describe("Forge Batch", function () { + this.timeout(0); + it("forge L1-Coordiator-tx Batch ", async function () { + const tokenID = 1; + const babyjub = `0x${accounts[0].bjjCompressed}`; + + const l1TxCoordiatorArray = []; + const rollupDB = await RollupDB(new SMTMemDB(), chainID); + const forgerTest = new ForgerTest( + maxTx, + maxL1Tx, + nLevels, + hardhatHermez, + rollupDB, + true + ); + + await AddToken( + hardhatHermez, + hardhatTokenERC20Mock, + hardhatHEZ, + ownerWallet, + feeAddToken + ); + + l1TxCoordiatorArray.push( + await l1CoordinatorTxEth(tokenID, babyjub, owner, hardhatHermez, chainIDHex) + ); + + l1TxCoordiatorArray.push( + await l1CoordinatorTxBjj(tokenID, babyjub, hardhatHermez) + ); + + // forge Batch + await forgerTest.forgeBatch(true, [], l1TxCoordiatorArray); + + // after forge, next queue is empty + const currentQueue = await hardhatHermez.nextL1ToForgeQueue(); + expect("0x").to.equal(await hardhatHermez.mapL1TxQueue(currentQueue)); + }); + + it("forge L1 user & Coordiator Tx batch", async function () { + const tokenID = 1; + const babyjub = `0x${accounts[0].bjjCompressed}`; + const loadAmount = float16.float2Fix(float16.fix2Float(1000)); + const fromIdx = 256; + const toIdx = 257; + const amountF = float16.fix2Float(10); + + const l1TxUserArray = []; + + const rollupDB = await RollupDB(new SMTMemDB(), chainID); + const forgerTest = new ForgerTest( + maxTx, + maxL1Tx, + nLevels, + hardhatHermez, + rollupDB, + true + ); + + await AddToken( + hardhatHermez, + hardhatTokenERC20Mock, + hardhatHEZ, + ownerWallet, + feeAddToken + ); + + // In order to add all the possible l1tx we need 2 accounts created in batchbuilder and rollup: + const numAccounts = 2; + await createAccounts( + forgerTest, + loadAmount, + tokenID, + babyjub, + owner, + hardhatHermez, + hardhatTokenERC20Mock, + numAccounts + ); + + // add user l1 tx + l1TxUserArray.push( + await l1UserTxCreateAccountDeposit( + loadAmount, + tokenID, + babyjub, + owner, + hardhatHermez, + hardhatTokenERC20Mock + ) + ); + l1TxUserArray.push( + await l1UserTxDeposit( + loadAmount, + tokenID, + fromIdx, + owner, + hardhatHermez, + hardhatTokenERC20Mock + ) + ); + l1TxUserArray.push( + await l1UserTxDepositTransfer( + loadAmount, + tokenID, + fromIdx, + toIdx, + amountF, + owner, + hardhatHermez, + hardhatTokenERC20Mock + ) + ); + l1TxUserArray.push( + await l1UserTxCreateAccountDepositTransfer( + loadAmount, + tokenID, + toIdx, + amountF, + babyjub, + owner, + hardhatHermez, + hardhatTokenERC20Mock + ) + ); + l1TxUserArray.push( + await l1UserTxForceTransfer( + tokenID, + fromIdx, + toIdx, + amountF, + owner, + hardhatHermez + ) + ); + l1TxUserArray.push( + await l1UserTxForceExit(tokenID, fromIdx, amountF, owner, hardhatHermez) + ); + + // forge empty batch + await forgerTest.forgeBatch(true, [], []); + + const l1TxCoordiatorArray = []; + + // add Coordiator tx + l1TxCoordiatorArray.push( + await l1CoordinatorTxEth(tokenID, babyjub, owner, hardhatHermez, chainIDHex) + ); + + l1TxCoordiatorArray.push( + await l1CoordinatorTxBjj(tokenID, babyjub, hardhatHermez) + ); + + // forge batch with all the L1 tx + await forgerTest.forgeBatch(true, l1TxUserArray, l1TxCoordiatorArray); + }); + + it("test instant withdraw merkle proof", async function () { + const tokenID = 1; + const babyjub = `0x${accounts[0].bjjCompressed}`; + const loadAmount = float16.float2Fix(float16.fix2Float(1000)); + const fromIdx = 256; + const amount = 10; + const amountF = float16.fix2Float(amount); + + const l1TxUserArray = []; + + const rollupDB = await RollupDB(new SMTMemDB(), chainID); + const forgerTest = new ForgerTest( + maxTx, + maxL1Tx, + nLevels, + hardhatHermez, + rollupDB, + true + ); + + await AddToken( + hardhatHermez, + hardhatTokenERC20Mock, + hardhatHEZ, + ownerWallet, + feeAddToken + ); + + // Create account and exit some funds + const numAccounts = 1; + await createAccounts( + forgerTest, + loadAmount, + tokenID, + babyjub, + owner, + hardhatHermez, + hardhatTokenERC20Mock, + numAccounts + ); + + l1TxUserArray.push( + await l1UserTxForceExit(tokenID, fromIdx, amountF, owner, hardhatHermez) + ); + + const initialOwnerBalance = await hardhatTokenERC20Mock.balanceOf( + await owner.getAddress() + ); + + // forge empty batch + await forgerTest.forgeBatch(true, [], []); + + // forge batch with all the create account and exit + await forgerTest.forgeBatch(true, l1TxUserArray, []); + + // perform withdraw + const numExitRoot = await hardhatHermez.lastForgedBatch(); + const instantWithdraw = true; + const state = await rollupDB.getStateByIdx(256); + const exitInfo = await rollupDB.getExitInfo(256, numExitRoot); + await expect( + hardhatHermez.withdrawMerkleProof( + tokenID, + amount, + babyjub, + numExitRoot, + exitInfo.siblings, + fromIdx, + instantWithdraw + ) + ) + .to.emit(hardhatHermez, "WithdrawEvent") + .withArgs(fromIdx, numExitRoot, instantWithdraw); + const finalOwnerBalance = await hardhatTokenERC20Mock.balanceOf( + await owner.getAddress() + ); + expect(parseInt(finalOwnerBalance)).to.equal( + parseInt(initialOwnerBalance) + amount + ); + }); + + it("test instant withdraw merkle proof with more leafs", async function () { + const tokenID = 1; + const babyjub = `0x${accounts[0].bjjCompressed}`; + const loadAmount = float16.float2Fix(float16.fix2Float(1000)); + const fromIdx = 256; + const amount = 10; + const amountF = float16.fix2Float(amount); + + const l1TxUserArray = []; + + const rollupDB = await RollupDB(new SMTMemDB(), chainID); + const forgerTest = new ForgerTest( + maxTx, + maxL1Tx, + nLevels, + hardhatHermez, + rollupDB, + true + ); + + await AddToken( + hardhatHermez, + hardhatTokenERC20Mock, + hardhatHEZ, + ownerWallet, + feeAddToken + ); + + // Create account and exit some funds + const numAccounts = 3; + await createAccounts( + forgerTest, + loadAmount, + tokenID, + babyjub, + owner, + hardhatHermez, + hardhatTokenERC20Mock, + numAccounts + ); + + l1TxUserArray.push( + await l1UserTxForceExit(tokenID, fromIdx, amountF, owner, hardhatHermez) + ); + l1TxUserArray.push( + await l1UserTxForceExit( + tokenID, + fromIdx + 1, + amountF, + owner, + hardhatHermez + ) + ); + l1TxUserArray.push( + await l1UserTxForceExit( + tokenID, + fromIdx + 2, + amountF, + owner, + hardhatHermez + ) + ); + const initialOwnerBalance = await hardhatTokenERC20Mock.balanceOf( + await owner.getAddress() + ); + + // forge empty batch + await forgerTest.forgeBatch(true, [], []); + + // forge batch with all the create account and exit + await forgerTest.forgeBatch(true, l1TxUserArray, []); + + // perform withdraw + const numExitRoot = await hardhatHermez.lastForgedBatch(); + const instantWithdraw = true; + const state = await rollupDB.getStateByIdx(256); + const exitInfo = await rollupDB.getExitInfo(256, numExitRoot); + await expect( + hardhatHermez.withdrawMerkleProof( + tokenID, + amount, + babyjub, + numExitRoot, + exitInfo.siblings, + fromIdx, + instantWithdraw + ) + ) + .to.emit(hardhatHermez, "WithdrawEvent") + .withArgs(fromIdx, numExitRoot, instantWithdraw); + const finalOwnerBalance = await hardhatTokenERC20Mock.balanceOf( + await owner.getAddress() + ); + expect(parseInt(finalOwnerBalance)).to.equal( + parseInt(initialOwnerBalance) + amount + ); + }); + }); +}); diff --git a/test/hermezHIP-0/HermezHelpers.test.js b/test/hermezHIP-0/HermezHelpers.test.js new file mode 100644 index 0000000..0cedb32 --- /dev/null +++ b/test/hermezHIP-0/HermezHelpers.test.js @@ -0,0 +1,93 @@ +const { expect } = require("chai"); +const { ethers } = require("hardhat"); +const poseidonHashJs = require("circomlib").poseidon; +const Scalar = require("ffjavascript").Scalar; +const { smt } = require("circomlib"); +const babyJub = require("circomlib").babyJub; +const utilsScalar = require("ffjavascript").utils; + +const { HermezAccount, stateUtils, txUtils } = require("@hermeznetwork/commonjsV1"); + +let tree; +const key1 = Scalar.e(7); +const value1 = Scalar.e(77); +const key2 = Scalar.e(8); +const value2 = Scalar.e(88); +const key3 = Scalar.e(32); +const value3 = Scalar.e(3232); + +async function fillSmtTree() { + tree = await smt.newMemEmptyTrie(); + + await tree.insert(key1, value1); + await tree.insert(key2, value2); + await tree.insert(key3, value3); +} + +describe("Hermez Helpers", function () { + let hardhatHermezHelpersTest; + + let owner; + let id1; + let id2; + let addrs; + let chainIDHex; + + const accounts = []; + for (let i = 0; i < 10; i++) { + accounts.push(new HermezAccount()); + } + + before(async function () { + [owner, id1, id2, ...addrs] = await ethers.getSigners(); + + let HermezHelpersTest = await ethers.getContractFactory( + "HermezHelpersTestV2" + ); + + hardhatHermezHelpersTest = await HermezHelpersTest.deploy(); + + await hardhatHermezHelpersTest.deployed(); + + const chainSC = await hardhatHermezHelpersTest.getChainId(); + chainIDHex = chainSC.toHexString(); + }); + + describe("utility helpers", function () { + it("checkSig", async function () { + const babyjub = accounts[0].bjjCompressed; + const flatSig = await txUtils.signBjjAuth(owner, babyjub, chainIDHex, hardhatHermezHelpersTest.address); + let sig = ethers.utils.splitSignature(flatSig); + + expect( + await hardhatHermezHelpersTest.checkSigTest( + `0x${babyjub}`, + sig.r, + sig.s, + sig.v + ) + ).to.equal(await owner.getAddress()); + }); + + it("float to fix", async () => { + const testVector = [ + [6 * 0x800000000 + 123, "123000000"], + [2 * 0x800000000 + 4545, "454500"], + [30 * 0x800000000 + 10235, "10235000000000000000000000000000000"], + [0, "0"], + [0x800000000, "0"], + [0x0001, "1"], + [31 * 0x800000000, "0"], + [0x800000000 + 1, "10"], + [0xFFFFFFFFFF, "343597383670000000000000000000000000000000"], + ]; + + for (let i = 0; i < testVector.length; i++) { + const resSm = await hardhatHermezHelpersTest.float2FixTest( + testVector[i][0] + ); + expect(Scalar.e(resSm).toString()).to.be.equal(testVector[i][1]); + } + }); + }); +}); diff --git a/test/hermezHIP-0/HermezIntegration.test.js b/test/hermezHIP-0/HermezIntegration.test.js new file mode 100644 index 0000000..971e27b --- /dev/null +++ b/test/hermezHIP-0/HermezIntegration.test.js @@ -0,0 +1,530 @@ +const { expect } = require("chai"); +const { ethers } = require("hardhat"); +const SMTMemDB = require("circomlib").SMTMemDB; +const { time } = require("@openzeppelin/test-helpers"); + +const { + l1UserTxCreateAccountDeposit, + l1UserTxDeposit, + l1UserTxDepositTransfer, + l1UserTxCreateAccountDepositTransfer, + l1UserTxForceTransfer, + l1UserTxForceExit, + l1CoordinatorTxEth, + l1CoordinatorTxBjj, + AddToken, + createAccounts, + ForgerTest, + calculateInputMaxTxLevels, + createPermitSignature +} = require("./helpers/helpers"); +const { + float40, + HermezAccount, + txUtils, + stateUtils, + utils, + feeTable, + SMTTmpDb, + Constants, + RollupDB, + BatchBuilder, +} = require("@hermeznetwork/commonjsV1"); + +const COORDINATOR_1_URL = "https://hermez.io"; +const BLOCKS_PER_SLOT = 40; +const bootCoordinatorURL = "https://boot.coordinator.io"; + +const MIN_BLOCKS = 81; +let ABIbid = [ + "function permit(address,address,uint256,uint256,uint8,bytes32,bytes32)", +]; + +let iface = new ethers.utils.Interface(ABIbid); +const INITIAL_DELAY = 0; + + +describe("Hermez integration", function () { + let hardhatTokenHermez; + let hardhatHermez; + let hardhatWithdrawalDelayer; + let hardhatHermezAuctionProtocol; + + let owner; + let id1; + let id2; + let addrs; + let hermezGovernanceAddress; + let ownerWallet; + + let chainID; + let chainIDHex; + + const accounts = []; + for (let i = 0; i < 10; i++) { + accounts.push(new HermezAccount()); + } + const tokenInitialAmount = ethers.utils.parseEther("100000"); + const maxL1Tx = 256; + const maxTx = 512; + const nLevels = 32; + const forgeL1L2BatchTimeout = 10; + const feeAddToken = 10; + const withdrawalDelay = 60 * 60 * 24 * 7 * 2; // 2 weeks + const INITIAL_DELAY = 60; //seconds + + beforeEach(async function () { + [ + owner, + governance, + forger1, + id1, + id2, + registryFunder, + hermezGovernanceAddress, + whiteHackGroupAddress, + donation, + ...addrs + ] = await ethers.getSigners(); + + hermezGovernanceAddress = await governance.getAddress(); + ownerAddress = await owner.getAddress(); + + const chainIdProvider = (await ethers.provider.getNetwork()).chainId; + if (chainIdProvider == 1337) { // solcover, must be a jsonRPC wallet + const mnemonic = "explain tackle mirror kit van hammer degree position ginger unfair soup bonus"; + let ownerWalletTest = ethers.Wallet.fromMnemonic(mnemonic); + // ownerWalletTest = ownerWallet.connect(ethers.provider); + ownerWallet = owner; + ownerWallet.privateKey = ownerWalletTest.privateKey; + } + else { + ownerWallet = new ethers.Wallet("0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80", ethers.provider); + } + + // factory + const Hermez = await ethers.getContractFactory("HermezTestV2"); + const TokenERC20PermitMock = await ethers.getContractFactory("ERC20PermitMock"); + const VerifierRollupHelper = await ethers.getContractFactory( + "VerifierMock" + ); + const VerifierWithdrawHelper = await ethers.getContractFactory( + "VerifierMock" + ); + const HermezAuctionProtocol = await ethers.getContractFactory( + "HermezAuctionProtocol" + ); + const WithdrawalDelayer = await ethers.getContractFactory( + "WithdrawalDelayer" + ); + + hardhatTokenHermez = await TokenERC20PermitMock.deploy( + "tokenname", + "TKN", + await owner.getAddress(), + tokenInitialAmount + ); + + await hardhatTokenHermez.deployed(); + let hardhatVerifierRollupHelper = await VerifierRollupHelper.deploy(); + let hardhatVerifierWithdrawHelper = await VerifierWithdrawHelper.deploy(); + + //deploy auction protocol + hardhatHermezAuctionProtocol = await HermezAuctionProtocol.deploy(); + + await hardhatHermezAuctionProtocol.deployed(); + + // deploy hermez and withdrawal delayer + let currentCount = await owner.getTransactionCount(); + + const WithdrawalDelayerAddress = ethers.utils.getContractAddress({ + nonce: currentCount + 1, + from: ownerAddress, + }); + const HermezAddress = ethers.utils.getContractAddress({ + nonce: currentCount + 2, + from: ownerAddress, + }); + + const latest = (await time.latestBlock()).toNumber(); + + + const outbidding = 1000; + const slotDeadline = 20; + const closedAuctionSlots = 2; + const openAuctionSlots = 4320; + const allocationRatio = [4000, 4000, 2000]; + await expect( + hardhatHermezAuctionProtocol.hermezAuctionProtocolInitializer( + hardhatTokenHermez.address, + latest + 1 + MIN_BLOCKS, + HermezAddress, + hermezGovernanceAddress, + await donation.getAddress(), // donation address + ownerAddress, // bootCoordinatorAddress + bootCoordinatorURL + ) + ) + .to.emit(hardhatHermezAuctionProtocol, "InitializeHermezAuctionProtocolEvent") + .withArgs( + await donation.getAddress(), // donation address + ownerAddress, // bootCoordinatorAddress + bootCoordinatorURL, + outbidding, + slotDeadline, + closedAuctionSlots, + openAuctionSlots, + allocationRatio + ); + + hardhatWithdrawalDelayer = await WithdrawalDelayer.deploy( + INITIAL_DELAY, + HermezAddress, + hermezGovernanceAddress, + await whiteHackGroupAddress.getAddress() + ); + + const filterInitialize = hardhatWithdrawalDelayer.filters.InitializeWithdrawalDelayerEvent(null, null, null); + const eventsInitialize = await hardhatWithdrawalDelayer.queryFilter(filterInitialize, 0, "latest"); + expect(eventsInitialize[0].args.initialWithdrawalDelay).to.be.equal(INITIAL_DELAY); + expect(eventsInitialize[0].args.initialHermezGovernanceAddress).to.be.equal(hermezGovernanceAddress); + expect(eventsInitialize[0].args.initialEmergencyCouncil).to.be.equal( await whiteHackGroupAddress.getAddress()); + + // deploy hermez + hardhatHermez = await Hermez.deploy(); + await hardhatHermez.deployed(); + + await expect( + hardhatHermez.initializeHermez( + [hardhatVerifierRollupHelper.address], + calculateInputMaxTxLevels([maxTx], [nLevels]), + hardhatVerifierWithdrawHelper.address, + hardhatHermezAuctionProtocol.address, + hardhatTokenHermez.address, + forgeL1L2BatchTimeout, + feeAddToken, + hermezGovernanceAddress, + withdrawalDelay, + WithdrawalDelayerAddress + )) + .to.emit(hardhatHermez, "InitializeHermezEvent") + .withArgs( + forgeL1L2BatchTimeout, + feeAddToken, + withdrawalDelay, + ); + + expect(hardhatWithdrawalDelayer.address).to.equal(WithdrawalDelayerAddress); + expect(hardhatHermez.address).to.equal(HermezAddress); + + const chainSC = await hardhatHermez.getChainID(); + chainID = chainSC.toNumber(); + chainIDHex = chainSC.toHexString(); + }); + + describe("Forge Batch", function () { + it("forge L1 user & Coordiator Tx batch using consensus mechanism", async function () { + // consensus operations + let startingBlock = ( + await hardhatHermezAuctionProtocol.genesisBlock() + ).toNumber(); + + await hardhatHermezAuctionProtocol + .connect(owner) + .setCoordinator(await owner.getAddress(), COORDINATOR_1_URL); + + + const value = ethers.utils.parseEther("100"); + const deadline = ethers.constants.MaxUint256; + const nonce = await hardhatTokenHermez.nonces(await owner.getAddress()); + + const { v, r, s } = await createPermitSignature( + hardhatTokenHermez, + ownerWallet, + hardhatHermezAuctionProtocol.address, + value, + nonce, + deadline + ); + + const dataPermit = iface.encodeFunctionData("permit", [ + await owner.getAddress(), + hardhatHermezAuctionProtocol.address, + value, + deadline, + v, + r, + s + ]); + + await hardhatHermezAuctionProtocol.processMultiBid( + value, + 3, + 8, + [true, true, true, true, true, true], + ethers.utils.parseEther("11"), + ethers.utils.parseEther("11"), + dataPermit + ); + + let block = startingBlock + 3 * BLOCKS_PER_SLOT; + + await time.advanceBlockTo(block); + + // hermez operations + const tokenID = 1; + const babyjub = `0x${accounts[0].bjjCompressed}`; + const loadAmount = float40.round(1000); + const fromIdx = 256; + const toIdx = 257; + const amountF = float40.fix2Float(10); + const l1TxUserArray = []; + const rollupDB = await RollupDB(new SMTMemDB(), chainID); + const forgerTest = new ForgerTest( + maxTx, + maxL1Tx, + nLevels, + hardhatHermez, + rollupDB + ); + await AddToken( + hardhatHermez, + hardhatTokenHermez, + hardhatTokenHermez, + ownerWallet, + feeAddToken + ); + + // In order to add all the possible l1tx we need 2 accounts created in batchbuilder and rollup: + const numAccounts = 2; + await createAccounts( + forgerTest, + loadAmount, + tokenID, + babyjub, + ownerWallet, + hardhatHermez, + hardhatTokenHermez, + numAccounts, + true + ); + + // add user l1 tx + l1TxUserArray.push( + await l1UserTxCreateAccountDeposit( + loadAmount, + tokenID, + babyjub, + ownerWallet, + hardhatHermez, + hardhatTokenHermez, + true + ) + ); + + l1TxUserArray.push( + await l1UserTxDeposit( + loadAmount, + tokenID, + fromIdx, + ownerWallet, + hardhatHermez, + hardhatTokenHermez, + true + ) + ); + l1TxUserArray.push( + await l1UserTxDepositTransfer( + loadAmount, + tokenID, + fromIdx, + toIdx, + amountF, + ownerWallet, + hardhatHermez, + hardhatTokenHermez, + true + ) + ); + l1TxUserArray.push( + await l1UserTxCreateAccountDepositTransfer( + loadAmount, + tokenID, + toIdx, + amountF, + babyjub, + ownerWallet, + hardhatHermez, + hardhatTokenHermez, + true + ) + ); + l1TxUserArray.push( + await l1UserTxForceTransfer( + tokenID, + fromIdx, + toIdx, + amountF, + ownerWallet, + hardhatHermez + ) + ); + l1TxUserArray.push( + await l1UserTxForceExit(tokenID, fromIdx, amountF, ownerWallet, hardhatHermez) + ); + + // forge empty batch + await forgerTest.forgeBatch(true, [], []); + + const l1TxCoordiatorArray = []; + + // add Coordiator tx + l1TxCoordiatorArray.push( + await l1CoordinatorTxEth(tokenID, babyjub, ownerWallet, hardhatHermez, chainIDHex) + ); + + l1TxCoordiatorArray.push( + await l1CoordinatorTxBjj(tokenID, babyjub, hardhatHermez) + ); + + // forge batch with all the L1 tx + await forgerTest.forgeBatch(true, l1TxUserArray, l1TxCoordiatorArray); + }); + it("test delayed withdraw with consensus mechanism and withdrawal delayer", async function () { + // consensus operations + let startingBlock = ( + await hardhatHermezAuctionProtocol.genesisBlock() + ).toNumber(); + + await hardhatHermezAuctionProtocol + .connect(owner) + .setCoordinator(await owner.getAddress(), COORDINATOR_1_URL); + + const value = ethers.utils.parseEther("100"); + const deadline = ethers.constants.MaxUint256; + const nonce = await hardhatTokenHermez.nonces(await owner.getAddress()); + + const { v, r, s } = await createPermitSignature( + hardhatTokenHermez, + ownerWallet, + hardhatHermezAuctionProtocol.address, + value, + nonce, + deadline + ); + + const dataPermit = iface.encodeFunctionData("permit", [ + await owner.getAddress(), + hardhatHermezAuctionProtocol.address, + value, + deadline, + v, + r, + s + ]); + + await hardhatHermezAuctionProtocol.processMultiBid( + value, + 3, + 8, + [true, true, true, true, true, true], + ethers.utils.parseEther("11"), + ethers.utils.parseEther("11"), + dataPermit + ); + + let block = startingBlock + 3 * BLOCKS_PER_SLOT; + + await time.advanceBlockTo(block); + + // hermez operations + + const tokenID = 1; + const babyjub = `0x${accounts[0].bjjCompressed}`; + const loadAmount = float40.round(1000); + const fromIdx = 256; + const amount = 10; + const amountF = float40.fix2Float(amount); + + const l1TxUserArray = []; + + const rollupDB = await RollupDB(new SMTMemDB(), chainID); + const forgerTest = new ForgerTest( + maxTx, + maxL1Tx, + nLevels, + hardhatHermez, + rollupDB + ); + + await AddToken( + hardhatHermez, + hardhatTokenHermez, + hardhatTokenHermez, + ownerWallet, + feeAddToken + ); + + // Create account and exit some funds + const numAccounts = 1; + await createAccounts( + forgerTest, + loadAmount, + tokenID, + babyjub, + ownerWallet, + hardhatHermez, + hardhatTokenHermez, + numAccounts, + true + ); + + l1TxUserArray.push( + await l1UserTxForceExit(tokenID, fromIdx, amountF, ownerWallet, hardhatHermez) + ); + + const initialOwnerBalance = await hardhatTokenHermez.balanceOf( + hardhatWithdrawalDelayer.address + ); + + // forge empty batch + await forgerTest.forgeBatch(true, [], []); + + // forge batch with all the create account and exit + await forgerTest.forgeBatch(true, l1TxUserArray, []); + + // perform withdraw + const proofA = ["0", "0"]; + const proofB = [ + ["0", "0"], + ["0", "0"], + ]; + const proofC = ["0", "0"]; + const instantWithdraw = false; + const batchNum = await hardhatHermez.lastForgedBatch(); + + await expect( + hardhatHermez.withdrawCircuit( + proofA, + proofB, + proofC, + tokenID, + amount, + amount, + batchNum, + fromIdx, + instantWithdraw + ) + ) + .to.emit(hardhatHermez, "WithdrawEvent") + .withArgs(amount, fromIdx, instantWithdraw); + + const finalOwnerBalance = await hardhatTokenHermez.balanceOf( + hardhatWithdrawalDelayer.address + ); + expect(parseInt(finalOwnerBalance)).to.equal( + parseInt(initialOwnerBalance) + 10 + ); + }); + }); +}); diff --git a/test/hermezHIP-0/HermezL2.test.js b/test/hermezHIP-0/HermezL2.test.js new file mode 100644 index 0000000..2d902e3 --- /dev/null +++ b/test/hermezHIP-0/HermezL2.test.js @@ -0,0 +1,422 @@ +const { expect } = require("chai"); +const { ethers } = require("hardhat"); +const SMTMemDB = require("circomlib").SMTMemDB; +const { time } = require("@openzeppelin/test-helpers"); +const Scalar = require("ffjavascript").Scalar; + +const { + l1UserTxCreateAccountDeposit, + l1UserTxDeposit, + l1UserTxDepositTransfer, + l1UserTxCreateAccountDepositTransfer, + l1UserTxForceTransfer, + l1UserTxForceExit, + l1CoordinatorTxEth, + l1CoordinatorTxBjj, + AddToken, + createAccounts, + ForgerTest, + calculateInputMaxTxLevels +} = require("./helpers/helpers"); +const { + float40, + HermezAccount, + txUtils, + stateUtils, + utils, + feeTable, + SMTTmpDb, + Constants, + RollupDB, + BatchBuilder, +} = require("@hermeznetwork/commonjsV1"); + +describe("Hermez ERC 20", function () { + let hardhatTokenERC20Mock; + let hardhatHermez; + let hardhatWithdrawalDelayer; + let hardhatHEZ; + + let owner; + let id1; + let id2; + let addrs; + let hermezGovernanceAddress; + let ownerWallet; + + let chainID; + let chainIDHex; + + const accounts = []; + + // set accounts + for (let i = 0; i < 10; i++) { + const newHermezAccount = new HermezAccount(); + const newAccount = {}; + newAccount.hermezAccount = newHermezAccount; + newAccount.bjjCompressed = `0x${newHermezAccount.bjjCompressed}`; + newAccount.idx = 256 + i; // first idx --> 256 + accounts.push(newAccount); + } + + + const tokenInitialAmount = ethers.BigNumber.from("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"); + const maxL1Tx = 256; + const maxTx = 512; + const nLevels = 32; + const forgeL1L2BatchTimeout = 10; + const feeAddToken = 10; + const withdrawalDelay = 60 * 60 * 24 * 7 * 2; // 2 weeks + const INITIAL_DELAY = 0; + + beforeEach(async function () { + [ + owner, + governance, + id1, + id2, + ...addrs + ] = await ethers.getSigners(); + + hermezGovernanceAddress = governance.getAddress(); + + const chainIdProvider = (await ethers.provider.getNetwork()).chainId; + if (chainIdProvider == 1337) { // solcover, must be a jsonRPC wallet + const mnemonic = "explain tackle mirror kit van hammer degree position ginger unfair soup bonus"; + let ownerWalletTest = ethers.Wallet.fromMnemonic(mnemonic); + // ownerWalletTest = ownerWallet.connect(ethers.provider); + ownerWallet = owner; + ownerWallet.privateKey = ownerWalletTest.privateKey; + } + else { + ownerWallet = new ethers.Wallet("0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80", ethers.provider); + } + + // const privateKeyhardhat = + // "0xc5e8f61d1ab959b397eecc0a37a6517b8e67a0e7cf1f4bce5591f3ed80199122"; + // ownerWallet = new ethers.Wallet( + // privateKeyhardhat, + // ethers.provider + // ); + //ownerWallet = new ethers.Wallet("0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80", ethers.provider); + + // factory helpers + const TokenERC20Mock = await ethers.getContractFactory("ERC20Mock"); + const TokenERC20PermitMock = await ethers.getContractFactory("ERC20PermitMock"); + + const VerifierRollupHelper = await ethers.getContractFactory( + "VerifierMock" + ); + const VerifierWithdrawHelper = await ethers.getContractFactory( + "VerifierMock" + ); + + const HermezAuctionTest = await ethers.getContractFactory( + "HermezAuctionTest" + ); + const WithdrawalDelayer = await ethers.getContractFactory( + "WithdrawalDelayerTest" + ); + + // factory hermez + const Hermez = await ethers.getContractFactory("HermezTestV2"); + + // deploy tokens + hardhatTokenERC20Mock = await TokenERC20Mock.deploy( + "tokenname", + "TKN", + await owner.getAddress(), + tokenInitialAmount + ); + + hardhatHEZ = await TokenERC20PermitMock.deploy( + "tokenname", + "TKN", + await owner.getAddress(), + tokenInitialAmount + ); + + // deploy helpers + let hardhatVerifierRollupHelper = await VerifierRollupHelper.deploy(); + let hardhatVerifierWithdrawHelper = await VerifierWithdrawHelper.deploy(); + + let hardhatHermezAuctionTest = await HermezAuctionTest.deploy(); + + // deploy hermez + hardhatHermez = await Hermez.deploy(); + await hardhatHermez.deployed(); + + hardhatWithdrawalDelayer = await WithdrawalDelayer.deploy( + INITIAL_DELAY, + hardhatHermez.address, + hermezGovernanceAddress, + hermezGovernanceAddress + ); + + // deploy hermez + await hardhatHermez.initializeHermez( + [hardhatVerifierRollupHelper.address], + calculateInputMaxTxLevels([maxTx], [nLevels]), + hardhatVerifierWithdrawHelper.address, + hardhatHermezAuctionTest.address, + hardhatHEZ.address, + forgeL1L2BatchTimeout, + feeAddToken, + hermezGovernanceAddress, + withdrawalDelay, + hardhatWithdrawalDelayer.address + ); + + // wait until is deployed + await hardhatTokenERC20Mock.deployed(); + + const chainSC = await hardhatHermez.getChainID(); + chainID = chainSC.toNumber(); + chainIDHex = chainSC.toHexString(); + }); + + describe("L2-user-Tx", function () { + this.timeout(0); + + it("Create l2 Tx and forge them", async function () { + const tokenIdERC20 = await AddToken( + hardhatHermez, + hardhatTokenERC20Mock, + hardhatHEZ, + ownerWallet, + feeAddToken + ); + + const loadAmount = float40.round(1000); + + const l1TxUserArray = []; + l1TxUserArray.push(await l1UserTxCreateAccountDeposit( + loadAmount, + tokenIdERC20, + accounts[0].bjjCompressed, + owner, + hardhatHermez, + hardhatTokenERC20Mock + )); + l1TxUserArray.push(await l1UserTxCreateAccountDeposit( + loadAmount, + tokenIdERC20, + accounts[1].bjjCompressed, + owner, + hardhatHermez, + hardhatTokenERC20Mock + )); + + const rollupDB = await RollupDB(new SMTMemDB(), chainID); + const forgerTest = new ForgerTest( + maxTx, + maxL1Tx, + nLevels, + hardhatHermez, + rollupDB + ); + + // forge empty batch + await forgerTest.forgeBatch(true, [], []); + + // forge batch with all the L1 tx + await forgerTest.forgeBatch(true, l1TxUserArray, []); + + const l2TxUserArray = []; + + const tx = { + fromIdx: accounts[0].idx, + toIdx: accounts[1].idx, + tokenID: tokenIdERC20, + amount: Scalar.e(40), + nonce: 0, + userFee: 152, // effective fee amount / 4 = 10 + }; + + accounts[0].hermezAccount.signTx(tx); + l2TxUserArray.push(tx); + + await forgerTest.forgeBatch(true, [], [], l2TxUserArray); + + const s1 = await rollupDB.getStateByIdx(256); + expect(s1.sign).to.be.equal(accounts[0].hermezAccount.sign); + expect(s1.ay).to.be.equal(accounts[0].hermezAccount.ay); + expect(s1.balance.toString()).to.be.equal(Scalar.e(950).toString()); // 1000(loadAmount) - 40(amount) - 10(fee) = 950 + expect(s1.tokenID).to.be.equal(tokenIdERC20); + expect(s1.nonce).to.be.equal(1); + }); + + it("Create a lot of l2 Tx and forge them", async function () { + const tokenIdERC20 = await AddToken( + hardhatHermez, + hardhatTokenERC20Mock, + hardhatHEZ, + ownerWallet, + feeAddToken + ); + + const loadAmount = float40.round(10000); + + const l1TxUserArray = []; + l1TxUserArray.push(await l1UserTxCreateAccountDeposit( + loadAmount, + tokenIdERC20, + accounts[0].bjjCompressed, + owner, + hardhatHermez, + hardhatTokenERC20Mock + )); + l1TxUserArray.push(await l1UserTxCreateAccountDeposit( + loadAmount, + tokenIdERC20, + accounts[1].bjjCompressed, + owner, + hardhatHermez, + hardhatTokenERC20Mock + )); + + const rollupDB = await RollupDB(new SMTMemDB(), chainID); + const forgerTest = new ForgerTest( + maxTx, + maxL1Tx, + nLevels, + hardhatHermez, + rollupDB + ); + + // forge empty batch + await forgerTest.forgeBatch(true, [], []); + + // forge batch with all the L1 tx + await forgerTest.forgeBatch(true, l1TxUserArray, []); + + const l2TxUserArray = []; + + const amount = 40; + + let nonce = 0; + for (let i = 0; i < 180; i++) { + const tx = { + fromIdx: accounts[0].idx, + toIdx: accounts[1].idx, + tokenID: tokenIdERC20, + amount: Scalar.e(amount), + nonce: nonce++, + userFee: 152, // effective fee amount / 4 = 10 + }; + accounts[0].hermezAccount.signTx(tx); + l2TxUserArray.push(tx); + } + await forgerTest.forgeBatch(true, [], [], l2TxUserArray); + + const s1 = await rollupDB.getStateByIdx(256); + expect(s1.sign).to.be.equal(accounts[0].hermezAccount.sign); + expect(s1.ay).to.be.equal(accounts[0].hermezAccount.ay); + expect(s1.balance.toString()).to.be.equal(Scalar.e(1000).toString()); // 10000(loadAmount) - 180*(40(amount) + 10(fee)) = 1000 + expect(s1.tokenID).to.be.equal(tokenIdERC20); + expect(s1.nonce).to.be.equal(nonce); + }); + + it("Create a lot of l2 Tx and L1tx and forge them", async function () { + const tokenIdERC20 = await AddToken( + hardhatHermez, + hardhatTokenERC20Mock, + hardhatHEZ, + ownerWallet, + feeAddToken + ); + + const loadAmount = float40.round(10000); + + const l1TxUserArray = []; + l1TxUserArray.push(await l1UserTxCreateAccountDeposit( + loadAmount, + tokenIdERC20, + accounts[0].bjjCompressed, + owner, + hardhatHermez, + hardhatTokenERC20Mock + )); + l1TxUserArray.push(await l1UserTxCreateAccountDeposit( + loadAmount, + tokenIdERC20, + accounts[1].bjjCompressed, + owner, + hardhatHermez, + hardhatTokenERC20Mock + )); + + const rollupDB = await RollupDB(new SMTMemDB(), chainID); + const forgerTest = new ForgerTest( + maxTx, + maxL1Tx, + nLevels, + hardhatHermez, + rollupDB + ); + + // forge empty batch + await forgerTest.forgeBatch(true, [], []); + + // forge batch with all the L1 tx + await forgerTest.forgeBatch(true, l1TxUserArray, []); + + const l2TxUserArray = []; + + const amount = 40; + + let nonce = 0; + for (let i = 0; i < 180; i++) { + const tx = { + fromIdx: accounts[0].idx, + toIdx: accounts[1].idx, + tokenID: tokenIdERC20, + amount: Scalar.e(amount), + nonce: nonce++, + userFee: 152, // effective fee amount / 4 = 10 + }; + accounts[0].hermezAccount.signTx(tx); + l2TxUserArray.push(tx); + } + + const l1TxUserArray2 = []; + + for (let i = 0; i < 127; i++) { + l1TxUserArray2.push( + await l1UserTxCreateAccountDeposit( + loadAmount, + tokenIdERC20, + accounts[0].bjjCompressed, + owner, + hardhatHermez, + hardhatTokenERC20Mock + ) + ); + } + + const l1TCoordinatorArray = []; + // add L1-Tx Coordinator with eth signature + for (let i = 0; i < 127; i++) { + l1TCoordinatorArray.push( + await l1CoordinatorTxEth( + tokenIdERC20, + accounts[0].bjjCompressed, + owner, + hardhatHermez, + chainIDHex + )); + } + // forge empty batch + await forgerTest.forgeBatch(true, [], []); + + await forgerTest.forgeBatch(true, l1TxUserArray2, l1TCoordinatorArray, l2TxUserArray); + + const s1 = await rollupDB.getStateByIdx(256); + expect(s1.sign).to.be.equal(accounts[0].hermezAccount.sign); + expect(s1.ay).to.be.equal(accounts[0].hermezAccount.ay); + expect(s1.balance.toString()).to.be.equal(Scalar.e(1000).toString()); // 10000(loadAmount) - 180*(40(amount) + 10(fee)) = 1000 + expect(s1.tokenID).to.be.equal(tokenIdERC20); + expect(s1.nonce).to.be.equal(nonce); + }); + }); +}); diff --git a/test/hermezHIP-0/HermezQueue.test.js b/test/hermezHIP-0/HermezQueue.test.js new file mode 100644 index 0000000..e784b80 --- /dev/null +++ b/test/hermezHIP-0/HermezQueue.test.js @@ -0,0 +1,295 @@ +const { expect } = require("chai"); +const { ethers } = require("hardhat"); +const SMTMemDB = require("circomlib").SMTMemDB; +const { + l1UserTxCreateAccountDeposit, + l1CoordinatorTxBjj, + AddToken, + ForgerTest, + calculateInputMaxTxLevels +} = require("./helpers/helpers"); +const { + float40, + HermezAccount, + txUtils, + stateUtils, + utils, + feeTable, + SMTTmpDb, + Constants, + RollupDB, + BatchBuilder, +} = require("@hermeznetwork/commonjsV1"); + +const INITIAL_DELAY = 0; + +describe("Hermez Queue", function () { + let hardhatTokenERC20Mock; + let hardhatHermez; + let hardhatWithdrawalDelayer; + let hardhatHEZ; + let owner; + let id1; + let id2; + let addrs; + let hermezGovernanceAddress; + let ownerWallet; + + const accounts = []; + for (let i = 0; i < 10; i++) { + accounts.push(new HermezAccount()); + } + const tokenInitialAmount = 1000000; + const maxL1Tx = 256; + const maxTx = 512; + const nLevels = 32; + const forgeL1L2BatchTimeout = 10; + let chainID; + const feeAddToken = 10; + const withdrawalDelay = 60 * 60 * 24 * 7 * 2; // 2 weeks + + beforeEach(async function () { + [ + owner, + governance, + id1, + id2, + ...addrs + ] = await ethers.getSigners(); + + hermezGovernanceAddress = governance.getAddress(); + + const chainIdProvider = (await ethers.provider.getNetwork()).chainId; + if (chainIdProvider == 1337) { // solcover, must be a jsonRPC wallet + const mnemonic = "explain tackle mirror kit van hammer degree position ginger unfair soup bonus"; + let ownerWalletTest = ethers.Wallet.fromMnemonic(mnemonic); + // ownerWalletTest = ownerWallet.connect(ethers.provider); + ownerWallet = owner; + ownerWallet.privateKey = ownerWalletTest.privateKey; + } + else { + ownerWallet = new ethers.Wallet("0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80", ethers.provider); + } + + // factory helpers + let TokenERC20Mock = await ethers.getContractFactory("ERC20Mock"); + const TokenERC20PermitMock = await ethers.getContractFactory("ERC20PermitMock"); + + const VerifierRollupHelper = await ethers.getContractFactory( + "VerifierMock" + ); + const VerifierWithdrawHelper = await ethers.getContractFactory( + "VerifierMock" + ); + let HermezAuctionTest = await ethers.getContractFactory( + "HermezAuctionTest" + ); + let WithdrawalDelayer = await ethers.getContractFactory( + "WithdrawalDelayerTest" + ); + + // factory hermez + let Hermez = await ethers.getContractFactory("HermezTestV2"); + + // deploy tokens + hardhatTokenERC20Mock = await TokenERC20Mock.deploy( + "tokenname", + "TKN", + await owner.getAddress(), + tokenInitialAmount + ); + + hardhatHEZ = await TokenERC20PermitMock.deploy( + "tokenname", + "TKN", + await owner.getAddress(), + tokenInitialAmount + ); + + let hardhatVerifierRollupHelper = await VerifierRollupHelper.deploy(); + let hardhatVerifierWithdrawHelper = await VerifierWithdrawHelper.deploy(); + + let hardhatHermezAuctionTest = await HermezAuctionTest.deploy(); + + // deploy hermez + hardhatHermez = await Hermez.deploy(); + await hardhatHermez.deployed(); + hardhatWithdrawalDelayer = await WithdrawalDelayer.deploy( + INITIAL_DELAY, + hardhatHermez.address, + hermezGovernanceAddress, + hermezGovernanceAddress + ); + + await hardhatHermez.initializeHermez( + [hardhatVerifierRollupHelper.address], + calculateInputMaxTxLevels([maxTx], [nLevels]), + hardhatVerifierWithdrawHelper.address, + hardhatHermezAuctionTest.address, + hardhatHEZ.address, + forgeL1L2BatchTimeout, + feeAddToken, + hermezGovernanceAddress, + withdrawalDelay, + hardhatWithdrawalDelayer.address + ); + + // wait until is deployed + await hardhatTokenERC20Mock.deployed(); + const chainSC = await hardhatHermez.getChainID(); + chainID = chainSC.toNumber(); + }); + + describe("Test Queue", function () { + it("Exceed 128 l1-user-tx", async function () { + this.timeout(0); + const tokenID = 1; + const babyjub = `0x${accounts[0].bjjCompressed}`; + const loadAmount = float40.round(1000); + const l1TxUserArray = []; + const rollupDB = await RollupDB(new SMTMemDB(), chainID); + const forgerTest = new ForgerTest( + maxTx, + maxL1Tx, + nLevels, + hardhatHermez, + rollupDB + ); + await AddToken( + hardhatHermez, + hardhatTokenERC20Mock, + hardhatHEZ, + ownerWallet, + feeAddToken + ); + const initialLastForge = await hardhatHermez.nextL1FillingQueue(); + const initialCurrentForge = await hardhatHermez.nextL1ToForgeQueue(); + // add l1-user-tx + for (let i = 0; i < 127; i++) + l1TxUserArray.push( + await l1UserTxCreateAccountDeposit( + loadAmount, + tokenID, + babyjub, + owner, + hardhatHermez, + hardhatTokenERC20Mock + ) + ); + // after 128 l1-user-tx still in the same queue + expect(initialLastForge).to.equal( + await hardhatHermez.nextL1FillingQueue() + ); + expect(initialCurrentForge).to.equal( + await hardhatHermez.nextL1ToForgeQueue() + ); + l1TxUserArray.push( + await l1UserTxCreateAccountDeposit( + loadAmount, + tokenID, + babyjub, + owner, + hardhatHermez, + hardhatTokenERC20Mock + ) + ); + // last Forge is updated at transaction 128 + const after128L1LastForge = await hardhatHermez.nextL1FillingQueue(); + const after128L1CurrentForge = await hardhatHermez.nextL1ToForgeQueue(); + expect(parseInt(initialLastForge) + 1).to.equal(after128L1LastForge); + expect(parseInt(initialCurrentForge)).to.equal(after128L1CurrentForge); + // forge empty batch + await forgerTest.forgeBatch(true, [], []); + const afterForgeLastForge = await hardhatHermez.nextL1FillingQueue(); + const afterForgeCurrentForge = await hardhatHermez.nextL1ToForgeQueue(); + expect(after128L1LastForge).to.equal(afterForgeLastForge); + expect(afterForgeCurrentForge).to.equal(after128L1CurrentForge + 1); + const l1TxCoordiatorArray = []; + // forge batch with all the L1 tx + await forgerTest.forgeBatch(true, l1TxUserArray, l1TxCoordiatorArray); + expect(parseInt(afterForgeLastForge) + 1).to.equal( + await hardhatHermez.nextL1FillingQueue() + ); + expect(parseInt(afterForgeCurrentForge) + 1).to.equal( + await hardhatHermez.nextL1ToForgeQueue() + ); + }); + it("Exceed max l1-tx", async function () { + this.timeout(0); + const tokenID = 1; + const babyjub = `0x${accounts[0].bjjCompressed}`; + const loadAmount = float40.round(1000); + const l1TxUserArray = []; + const rollupDB = await RollupDB(new SMTMemDB(), chainID); + const forgerTest = new ForgerTest( + maxTx, + maxL1Tx, + nLevels, + hardhatHermez, + rollupDB + ); + await AddToken( + hardhatHermez, + hardhatTokenERC20Mock, + hardhatHEZ, + ownerWallet, + feeAddToken + ); // add l1-user-tx + for (let i = 0; i < 128; i++) + l1TxUserArray.push( + await l1UserTxCreateAccountDeposit( + loadAmount, + tokenID, + babyjub, + owner, + hardhatHermez, + hardhatTokenERC20Mock + ) + ); + await forgerTest.forgeBatch(true, [], []); + const initialLastForge = await hardhatHermez.nextL1FillingQueue(); + const initialCurrentForge = await hardhatHermez.nextL1ToForgeQueue(); + const lastLastForge = await hardhatHermez.nextL1FillingQueue(); + const lastCurrentForge = await hardhatHermez.nextL1ToForgeQueue(); + const l1TxCoordiatorArray = []; + for (let i = 0; i < 129; i++) { + await l1TxCoordiatorArray.push( + await l1CoordinatorTxBjj(tokenID, babyjub, hardhatHermez) + ); + } + let stringL1CoordinatorTx = ""; + for (let tx of l1TxCoordiatorArray) { + stringL1CoordinatorTx = + stringL1CoordinatorTx + tx.l1TxCoordinatorbytes.slice(2); // retireve the 0x + } + // custom impossible forge, 128 L1-user-Tx + 129 L1-operator-Tx = 257 > MAX_L1_TX + const proofA = ["0", "0"]; + const proofB = [ + ["0", "0"], + ["0", "0"], + ]; + const proofC = ["0", "0"]; + const newLastIdx = 257; + const newStateRoot = 123; + const newExitRoot = 456; + const compressedL1CoordinatorTx = `0x${stringL1CoordinatorTx}`; + const L2TxsData = "0x00"; + const feeIdxCoordinator = "0x00"; + const verifierIdx = 0; + await expect( + hardhatHermez.forgeBatch( + newLastIdx, + newStateRoot, + compressedL1CoordinatorTx, + L2TxsData, + feeIdxCoordinator, + verifierIdx, + true, + proofA, + proofB, + proofC + ) + ).to.be.revertedWith("Hermez::_buildL1Data: L1_TX_OVERFLOW"); + }); + }); +}); diff --git a/test/hermezHIP-0/HermezWithdraw.test.js b/test/hermezHIP-0/HermezWithdraw.test.js new file mode 100644 index 0000000..d596e02 --- /dev/null +++ b/test/hermezHIP-0/HermezWithdraw.test.js @@ -0,0 +1,774 @@ +const { expect } = require("chai"); +const { ethers } = require("hardhat"); +const { time } = require("@openzeppelin/test-helpers"); +const { HermezAccount } = require("@hermeznetwork/commonjsV1"); +const { + AddToken, + calculateInputMaxTxLevels, + packBucket, + unpackBucket +} = require("./helpers/helpers"); +const Scalar = require("ffjavascript").Scalar; + +describe("Hermez instant withdraw manager", function () { + let hardhatTokenERC20Mock; + let hardhatHermez; + + let owner; + let id1; + let addrs; + let governance; + let ownerWallet; + + const accounts = []; + for (let i = 0; i < 10; i++) { + accounts.push(new HermezAccount()); + } + const tokenInitialAmount = 1000000; + const maxTx = 512; + const nLevels = 32; + const forgeL1L2BatchTimeout = 10; + const feeAddToken = 10; + const withdrawalDelay = 60 * 60 * 24 * 7 * 2; // 2 weeks + const numBuckets = 5; + const _EXCHANGE_MULTIPLIER = 1e10; + const INITIAL_DELAY = 0; + + + this.beforeEach(async function () { + [ + owner, + governance, + id1, + ...addrs + ] = await ethers.getSigners(); + + const chainIdProvider = (await ethers.provider.getNetwork()).chainId; + if (chainIdProvider == 1337) { // solcover, must be a jsonRPC wallet + const mnemonic = "explain tackle mirror kit van hammer degree position ginger unfair soup bonus"; + let ownerWalletTest = ethers.Wallet.fromMnemonic(mnemonic); + // ownerWalletTest = ownerWallet.connect(ethers.provider); + ownerWallet = owner; + ownerWallet.privateKey = ownerWalletTest.privateKey; + } + else { + ownerWallet = new ethers.Wallet("0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80", ethers.provider); + } + + const hermezGovernanceAddress = await governance.getAddress(); + + // factory helpers + const TokenERC20Mock = await ethers.getContractFactory("ERC20Mock"); + const TokenERC20PermitMock = await ethers.getContractFactory("ERC20PermitMock"); + + const VerifierRollupHelper = await ethers.getContractFactory( + "VerifierMock" + ); + const VerifierWithdrawHelper = await ethers.getContractFactory( + "VerifierMock" + ); + const HermezAuctionTest = await ethers.getContractFactory( + "HermezAuctionTest" + ); + const WithdrawalDelayer = await ethers.getContractFactory( + "WithdrawalDelayerTest" + ); + + // factory hermez + const Hermez = await ethers.getContractFactory("HermezTestV2"); + + // deploy helpers + + + + + hardhatTokenERC20Mock = await TokenERC20Mock.deploy( + "tokenname", + "TKN", + await owner.getAddress(), + tokenInitialAmount + ); + + hardhatHEZ = await TokenERC20PermitMock.deploy( + "tokenname", + "TKN", + await owner.getAddress(), + tokenInitialAmount + ); + + let hardhatVerifierRollupHelper = await VerifierRollupHelper.deploy(); + let hardhatVerifierWithdrawHelper = await VerifierWithdrawHelper.deploy(); + hardhatHermezAuctionTest = await HermezAuctionTest.deploy(); + + // deploy hermez + hardhatHermez = await Hermez.deploy(); + await hardhatHermez.deployed(); + hardhatWithdrawalDelayer = await WithdrawalDelayer.deploy( + INITIAL_DELAY, + hardhatHermez.address, + hermezGovernanceAddress, + hermezGovernanceAddress + ); + + await hardhatHermez.initializeHermez( + [hardhatVerifierRollupHelper.address], + calculateInputMaxTxLevels([maxTx], [nLevels]), + hardhatVerifierWithdrawHelper.address, + hardhatHermezAuctionTest.address, + hardhatHEZ.address, + forgeL1L2BatchTimeout, + feeAddToken, + hermezGovernanceAddress, + withdrawalDelay, + hardhatWithdrawalDelayer.address + ); + + // wait until is deployed + await hardhatTokenERC20Mock.deployed(); + const chainSC = await hardhatHermez.getChainID(); + chainID = chainSC.toNumber(); + }); + + describe("Instant withdraw functionality", function () { + it("test token/eth to usd", async function () { + const tokenAddress = hardhatTokenERC20Mock.address; + const ethereumAddress = "0x0000000000000000000000000000000000000000"; + const tokenPriceERC20 = 10 * _EXCHANGE_MULTIPLIER; //USD + const ethereumPrice = 1800 * _EXCHANGE_MULTIPLIER; //USD + + const addressArray = [tokenAddress, ethereumAddress]; + const valueArray = [tokenPriceERC20, ethereumPrice]; + + await expect(hardhatHermez + .connect(governance) + .updateTokenExchange(addressArray, valueArray)) + .to.emit(hardhatHermez, "UpdateTokenExchange") + .withArgs(addressArray, valueArray); + + expect( + await hardhatHermez.tokenExchange(tokenAddress) + ).to.equal(valueArray[0]); + + expect( + await hardhatHermez.tokenExchange(ethereumAddress) + ).to.equal(valueArray[1]); + + const tokenAmount = 2; + const tokenAmountDecimals = ethers.utils.parseEther( + tokenAmount.toString() + ); // 18 decimals + + const resultUSDToken = tokenPriceERC20 / _EXCHANGE_MULTIPLIER * tokenAmount; + const resultUSDEthereum = ethereumPrice / _EXCHANGE_MULTIPLIER * tokenAmount; + expect( + await hardhatHermez.token2USDTest(tokenAddress, tokenAmountDecimals) + ).to.equal(resultUSDToken); + expect( + await hardhatHermez.token2USDTest(ethereumAddress, tokenAmountDecimals) + ).to.equal(resultUSDEthereum); + }); + + it("test Helpers pack/unpack function matches SC", async function () { + const numBuckets = 5; + const buckets = []; + for (let i = 0; i < numBuckets; i++) { + const ceilUSD = (i + 1) * 10000000; + const blockStamp = 1020; + const withdrawals = 100005; + const rateBlocks = 1123123; + const rateWithdrawals = 1232143; + const maxWithdrawals = 100000000000000; + buckets.push({ + ceilUSD, + blockStamp, + withdrawals, + rateBlocks, + rateWithdrawals, + maxWithdrawals + }); + } + + for (let i = 0; i < buckets.length; i++) { + const packetBucketSC = await hardhatHermez.packBucket( + buckets[i].ceilUSD, + buckets[i].blockStamp, + buckets[i].withdrawals, + buckets[i].rateBlocks, + buckets[i].rateWithdrawals, + buckets[i].maxWithdrawals + ); + const packetBucketJs = packBucket(buckets[i]); + expect(packetBucketJs).to.be.equal(packetBucketSC); + + const unpackedBucketSC = await hardhatHermez.unpackBucket(packetBucketJs); + const unpackedBucketJs = unpackBucket(packetBucketJs); + + expect(unpackedBucketSC.ceilUSD).to.be.equal(ethers.BigNumber.from(unpackedBucketJs.ceilUSD)); + expect(unpackedBucketSC.blockStamp).to.be.equal(ethers.BigNumber.from(unpackedBucketJs.blockStamp)); + expect(unpackedBucketSC.withdrawals).to.be.equal(ethers.BigNumber.from(unpackedBucketJs.withdrawals)); + expect(unpackedBucketSC.rateBlocks).to.be.equal(ethers.BigNumber.from(unpackedBucketJs.rateBlocks)); + expect(unpackedBucketSC.rateWithdrawals).to.be.equal(ethers.BigNumber.from(unpackedBucketJs.rateWithdrawals)); + expect(unpackedBucketSC.maxWithdrawals).to.be.equal(ethers.BigNumber.from(unpackedBucketJs.maxWithdrawals)); + } + + + }); + + it("updateBucketsParameters ", async function () { + const buckets = []; + for (let i = 0; i < numBuckets; i++) { + const ceilUSD = (i + 1) * 1000; + const blockStamp = 0; // does not matter! + const withdrawals = 0; + const rateBlocks = i + 1; + const rateWithdrawals = i + 1; + const maxWithdrawals = 4000000000; // max value 4294967296; + buckets.push({ + ceilUSD, + blockStamp, + withdrawals, + rateBlocks, + rateWithdrawals, + maxWithdrawals + }); + } + + const bucketsPacked = buckets.map((bucket) => packBucket(bucket)); + await expect( + hardhatHermez.connect(governance).updateBucketsParameters(bucketsPacked) + ).to.emit(hardhatHermez, "UpdateBucketsParameters"); + + for (let i = 0; i < numBuckets; i++) { + const bucket = await hardhatHermez.buckets(i); + const unpackedBucket = unpackBucket(bucket._hex); + expect(buckets[i].ceilUSD).to.be.equal(ethers.BigNumber.from(unpackedBucket.ceilUSD)); + expect(buckets[i].withdrawals).to.be.equal(ethers.BigNumber.from(unpackedBucket.withdrawals)); + expect(buckets[i].rateBlocks).to.be.equal(ethers.BigNumber.from(unpackedBucket.rateBlocks)); + expect(buckets[i].rateWithdrawals).to.be.equal(ethers.BigNumber.from(unpackedBucket.rateWithdrawals)); + expect(buckets[i].maxWithdrawals).to.be.equal(ethers.BigNumber.from(unpackedBucket.maxWithdrawals)); + } + }); + + it("test instant withdraw with buckets", async function () { + const tokenAddress = hardhatTokenERC20Mock.address; + const buckets = []; + for (let i = 0; i < numBuckets; i++) { + const ceilUSD = (i + 1) * 1000; + const blockStamp = 0; // does not matter! + const withdrawals = 0; + const rateBlocks = (i + 1) * 2; + const rateWithdrawals = i + 1; + const maxWithdrawals = 4000000000; // max value 4294967296; + buckets.push({ + ceilUSD, + blockStamp, + withdrawals, + rateBlocks, + rateWithdrawals, + maxWithdrawals + }); + } + + const bucketsPacked = buckets.map((bucket) => packBucket(bucket)); + await expect( + hardhatHermez.connect(governance).updateBucketsParameters(bucketsPacked) + ).to.emit(hardhatHermez, "UpdateBucketsParameters"); + + + for (let i = 0; i < numBuckets; i++) { + const bucket = await hardhatHermez.buckets(i); + const unpackedBucket = unpackBucket(bucket._hex); + expect(buckets[i].ceilUSD).to.be.equal(ethers.BigNumber.from(unpackedBucket.ceilUSD)); + expect(buckets[i].withdrawals).to.be.equal(ethers.BigNumber.from(unpackedBucket.withdrawals)); + expect(buckets[i].rateBlocks).to.be.equal(ethers.BigNumber.from(unpackedBucket.rateBlocks)); + expect(buckets[i].rateWithdrawals).to.be.equal(ethers.BigNumber.from(unpackedBucket.rateWithdrawals)); + expect(buckets[i].maxWithdrawals).to.be.equal(ethers.BigNumber.from(unpackedBucket.maxWithdrawals)); + } + + for (let i = 0; i < numBuckets; i++) { + const ceilUSD = (i + 1) * 1000; + expect(await hardhatHermez.findBucketIdxTest(ceilUSD)).to.equal(i); + } + + // withdraw can be performed, because token has no value in rollup + expect( + await hardhatHermez.instantWithdrawalViewer(tokenAddress, 1e15) + ).to.be.equal(true); + + // add withdrawals and price + await AddToken( + hardhatHermez, + hardhatTokenERC20Mock, + hardhatHEZ, + ownerWallet, + feeAddToken + ); + const addressArray = [tokenAddress]; + const tokenPrice = 10; //USD + const valueArray = [tokenPrice * _EXCHANGE_MULTIPLIER]; + await expect(hardhatHermez + .connect(governance) + .updateTokenExchange(addressArray, valueArray)) + .to.emit(hardhatHermez, "UpdateTokenExchange") + .withArgs(addressArray, valueArray); + + expect( + await hardhatHermez.tokenExchange(tokenAddress) + ).to.equal(valueArray[0]); + + const tokenAmount = 10; // base unit 40000000000 + const tokenAmountDecimals = ethers.utils.parseEther( + tokenAmount.toString() + ); // 18 decimals + const resultUSD = tokenPrice * tokenAmount; + const bucketIdx = 0; + expect( + await hardhatHermez.token2USDTest(tokenAddress, tokenAmountDecimals) + ).to.equal(resultUSD); + expect(await hardhatHermez.findBucketIdxTest(resultUSD)).to.equal( + bucketIdx + ); + + // test instat withdrawal and bucket recharge + + // reset buckets + await hardhatHermez.connect(governance).updateBucketsParameters(bucketsPacked); + + await expect( + hardhatHermez.instantWithdrawalTest(tokenAddress, tokenAmountDecimals) + ).to.be.revertedWith("HermezTest::withdrawMerkleProof: INSTANT_WITHDRAW_WASTED_FOR_THIS_USD_RANGE"); + + let actualBucketSC = await hardhatHermez.buckets(bucketIdx); + let actualUnpackedBucket = unpackBucket(actualBucketSC._hex); + + expect(Scalar.toNumber(actualUnpackedBucket.withdrawals)).to.be.equal(0); + + const initialTimestamp = parseInt(actualUnpackedBucket.blockStamp); + const tokenRate = buckets[bucketIdx].rateBlocks; + + // 0 withdrawals + await time.advanceBlockTo(initialTimestamp + tokenRate * 2 - 1); + + const expectedIdx = bucketIdx; + let expectedWithdrawals = 1; + let expectedblockStamp = initialTimestamp + tokenRate * 2; + + // 2 withdrawals + await expect(hardhatHermez.instantWithdrawalTest( + tokenAddress, + tokenAmountDecimals + )).to.emit(hardhatHermez, "UpdateBucketWithdraw") + .withArgs(expectedIdx, expectedblockStamp, expectedWithdrawals); + + + actualBucketSC = await hardhatHermez.buckets(bucketIdx); + actualUnpackedBucket = unpackBucket(actualBucketSC._hex); + + expect(Scalar.toNumber(actualUnpackedBucket.withdrawals)).to.be.equal(expectedWithdrawals); + expect(Scalar.toNumber(actualUnpackedBucket.blockStamp)).to.be.equal(expectedblockStamp); + + await time.advanceBlockTo(initialTimestamp + tokenRate * 5 - 1); + + expectedWithdrawals = 3; + expectedblockStamp = initialTimestamp + tokenRate * 5; + + await expect(hardhatHermez.instantWithdrawalTest( + tokenAddress, + tokenAmountDecimals + )).to.emit(hardhatHermez, "UpdateBucketWithdraw") + .withArgs(expectedIdx, expectedblockStamp, expectedWithdrawals); + + actualBucketSC = await hardhatHermez.buckets(bucketIdx); + actualUnpackedBucket = unpackBucket(actualBucketSC._hex); + + // 1 + 3 - 1 = 3 withdrawals left + expect(Scalar.toNumber(actualUnpackedBucket.withdrawals)).to.be.equal(expectedWithdrawals); + expect(Scalar.toNumber(actualUnpackedBucket.blockStamp)).to.be.equal(expectedblockStamp); + + await hardhatHermez.instantWithdrawalTest( + tokenAddress, + tokenAmountDecimals + ); + actualBucketSC = await hardhatHermez.buckets(bucketIdx); + actualUnpackedBucket = unpackBucket(actualBucketSC._hex); + expect(Scalar.toNumber(actualUnpackedBucket.withdrawals)).to.be.equal(2); + }); + + it("test instant withdraw with buckets full, and ERC20Permit", async function () { + const tokenAddress = hardhatHEZ.address; + const buckets = []; + for (let i = 0; i < numBuckets; i++) { + const ceilUSD = (i + 1) * 1000; + const blockStamp = 0; // does not matter! + const withdrawals = 0; + const rateBlocks = (i + 1) * 4; + const rateWithdrawals = 3; + const maxWithdrawals = 4000000000; // max value 4294967296; + buckets.push({ + ceilUSD, + blockStamp, + withdrawals, + rateBlocks, + rateWithdrawals, + maxWithdrawals + }); + } + + const bucketsPacked = buckets.map((bucket) => packBucket(bucket)); + await expect( + hardhatHermez.connect(governance).updateBucketsParameters(bucketsPacked) + ).to.emit(hardhatHermez, "UpdateBucketsParameters"); + + + for (let i = 0; i < numBuckets; i++) { + const bucket = await hardhatHermez.buckets(i); + const unpackedBucket = unpackBucket(bucket._hex); + expect(buckets[i].ceilUSD).to.be.equal(ethers.BigNumber.from(unpackedBucket.ceilUSD)); + expect(buckets[i].withdrawals).to.be.equal(ethers.BigNumber.from(unpackedBucket.withdrawals)); + expect(buckets[i].rateBlocks).to.be.equal(ethers.BigNumber.from(unpackedBucket.rateBlocks)); + expect(buckets[i].rateWithdrawals).to.be.equal(ethers.BigNumber.from(unpackedBucket.rateWithdrawals)); + expect(buckets[i].maxWithdrawals).to.be.equal(ethers.BigNumber.from(unpackedBucket.maxWithdrawals)); + } + + // withdraw can be performed, because token has no value in rollup + expect( + await hardhatHermez.instantWithdrawalViewer(tokenAddress, 1e15) + ).to.be.equal(true); + + // add withdrawals and price + await AddToken( + hardhatHermez, + hardhatHEZ, + hardhatHEZ, + ownerWallet, + feeAddToken + ); + const addressArray = [hardhatHEZ.address]; + const tokenPrice = 10; + const valueArray = [tokenPrice * _EXCHANGE_MULTIPLIER]; + await expect(hardhatHermez + .connect(governance) + .updateTokenExchange(addressArray, valueArray)) + .to.emit(hardhatHermez, "UpdateTokenExchange") + .withArgs(addressArray, valueArray); + + expect(await hardhatHermez.tokenExchange(hardhatHEZ.address)).to.equal( + valueArray[0] + ); + + const tokenAmount = 10; + const resultUSD = tokenPrice * tokenAmount; + const tokenAmountDecimals = ethers.utils.parseEther( + tokenAmount.toString() + ); // 18 decimals + const bucketIdx = 0; + + expect( + await hardhatHermez.token2USDTest(tokenAddress, tokenAmountDecimals) + ).to.equal(resultUSD); + expect(await hardhatHermez.findBucketIdxTest(resultUSD)).to.equal( + bucketIdx + ); + + // no withdrawals yet + expect( + await hardhatHermez.instantWithdrawalViewer( + tokenAddress, + tokenAmountDecimals + ) + ).to.be.equal(false); + + // test instat withdrawal and bucket recharge + + // reset buckets + await hardhatHermez.connect(governance).updateBucketsParameters(bucketsPacked); + + await expect( + hardhatHermez.instantWithdrawalTest(tokenAddress, tokenAmountDecimals) + ).to.be.revertedWith("HermezTest::withdrawMerkleProof: INSTANT_WITHDRAW_WASTED_FOR_THIS_USD_RANGE"); + + let actualBucketSC = await hardhatHermez.buckets(bucketIdx); + let actualUnpackedBucket = unpackBucket(actualBucketSC._hex); + + expect(Scalar.toNumber(actualUnpackedBucket.withdrawals)).to.be.equal(0); + + const initialTimestamp = parseInt(actualUnpackedBucket.blockStamp); + const tokenRate = buckets[bucketIdx].rateBlocks; + + // 0 withdrawals + expect( + await hardhatHermez.instantWithdrawalViewer( + tokenAddress, + tokenAmountDecimals + ) + ).to.be.equal(false); + + // still 0 withdrawals + await time.advanceBlockTo(initialTimestamp + tokenRate - 1); + expect( + await hardhatHermez.instantWithdrawalViewer( + tokenAddress, + tokenAmountDecimals + ) + ).to.be.equal(false); + + await time.advanceBlockTo(initialTimestamp + tokenRate * 2 - 1); + expect( + await hardhatHermez.instantWithdrawalViewer( + tokenAddress, + tokenAmountDecimals + ) + ).to.be.equal(true); + + + const expectedIdx = bucketIdx; + let expectedWithdrawals = 2 * buckets[bucketIdx].rateWithdrawals - 1; // -1 actual withdrawal + let expectedblockStamp = initialTimestamp + tokenRate * 2; + + // 2 withdrawals + await expect(hardhatHermez.instantWithdrawalTest( + tokenAddress, + tokenAmountDecimals + )).to.emit(hardhatHermez, "UpdateBucketWithdraw") + .withArgs(expectedIdx, expectedblockStamp, expectedWithdrawals); + + + // 2 withdrawals + actualBucketSC = await hardhatHermez.buckets(bucketIdx); + actualUnpackedBucket = unpackBucket(actualBucketSC._hex); + + expect(Scalar.toNumber(actualUnpackedBucket.withdrawals)).to.be.equal(expectedWithdrawals); + expect(Scalar.toNumber(actualUnpackedBucket.blockStamp)).to.be.equal(expectedblockStamp); + + await time.advanceBlockTo(initialTimestamp + tokenRate * 5 - 1); + + + expectedWithdrawals = 5 * buckets[bucketIdx].rateWithdrawals - 2; // -2 previous and actual withdrawal + expectedblockStamp = initialTimestamp + tokenRate * 5; + + await expect(hardhatHermez.instantWithdrawalTest( + tokenAddress, + tokenAmountDecimals + )).to.emit(hardhatHermez, "UpdateBucketWithdraw") + .withArgs(expectedIdx, expectedblockStamp, expectedWithdrawals); + + actualBucketSC = await hardhatHermez.buckets(bucketIdx); + actualUnpackedBucket = unpackBucket(actualBucketSC._hex); + + // max withdawals = 4 + expect(Scalar.toNumber(actualUnpackedBucket.withdrawals)).to.be.equal(expectedWithdrawals); + expect(Scalar.toNumber(actualUnpackedBucket.blockStamp)).to.be.equal(expectedblockStamp); + + // withdraw could be performed + expect( + await hardhatHermez.instantWithdrawalViewer( + tokenAddress, + tokenAmountDecimals + ) + ).to.be.equal(true); + + await time.advanceBlockTo(initialTimestamp + tokenRate * 10 - 1); + + // withdraw + await hardhatHermez.instantWithdrawalTest( + tokenAddress, + tokenAmountDecimals + ); + + // still tokens for withdraw + expect( + await hardhatHermez.instantWithdrawalViewer( + tokenAddress, + tokenAmountDecimals + ) + ).to.be.equal(true); + + const lastBlock = await time.latestBlock(); + actualBucketSC = await hardhatHermez.buckets(bucketIdx); + actualUnpackedBucket = unpackBucket(actualBucketSC._hex); + + expectedWithdrawals = 10 * buckets[bucketIdx].rateWithdrawals - 3; // -3 previous withdrawals + + expect(Scalar.toNumber(actualUnpackedBucket.withdrawals)).to.be.equal(expectedWithdrawals); + // if withdrawals = maxWithdrawals, blockstamp is updated when withdraw + expect(Scalar.toNumber(actualUnpackedBucket.blockStamp)).to.be.equal(lastBlock.toNumber()); + + // add new buckets + const bucketsFull = []; + for (let i = 0; i < numBuckets; i++) { + const ceilUSD = (i + 1) * 1000; + const blockStamp = 0; // does not matter! + const withdrawals = 4; + const rateBlocks = (i + 1) * 4; + const rateWithdrawals = i + 1; + const maxWithdrawals = 4; // max value 4294967296; + bucketsFull.push({ + ceilUSD, + blockStamp, + withdrawals, + rateBlocks, + rateWithdrawals, + maxWithdrawals + }); + } + + const bucketsPackedFull = bucketsFull.map((bucket) => packBucket(bucket)); + await expect( + hardhatHermez.connect(governance).updateBucketsParameters(bucketsPackedFull) + ).to.emit(hardhatHermez, "UpdateBucketsParameters"); + + // test again withdraw will full bucket + await hardhatHermez.instantWithdrawalTest( + tokenAddress, + tokenAmountDecimals + ); + const lastBlock2 = await time.latestBlock(); + actualBucketSC = await hardhatHermez.buckets(bucketIdx); + actualUnpackedBucket = unpackBucket(actualBucketSC._hex); + expect(actualUnpackedBucket.withdrawals).to.be.equal(Scalar.e(3)); + // if withdrawals = maxWithdrawals, blockstamp is updated when withdraw + expect(actualUnpackedBucket.blockStamp).to.be.equal(Scalar.e(lastBlock2.toNumber())); + }); + + it("update WithdrawalDelay", async function () { + const newWithdrawalDelay = 100000; + + expect(await hardhatHermez.withdrawalDelay()).to.equal( + 60 * 60 * 24 * 7 * 2 // 2 weeks + ); + + await expect( + hardhatHermez + .connect(governance) + .updateWithdrawalDelay(newWithdrawalDelay)) + .to.emit(hardhatHermez, "UpdateWithdrawalDelay") + .withArgs(newWithdrawalDelay); + expect(await hardhatHermez.withdrawalDelay()).to.equal( + newWithdrawalDelay + ); + }); + + it("enable safeMode", async function () { + await expect(hardhatHermez.safeMode()).to.be.revertedWith( + "InstantWithdrawManager::onlyGovernance: ONLY_GOVERNANCE_ADDRESS" + ); + + const tokenAddress = hardhatTokenERC20Mock.address; + // no limitations! + expect( + await hardhatHermez.instantWithdrawalViewer(tokenAddress, 1e15) + ).to.be.equal(true); + + // add withdrawals and price + await AddToken( + hardhatHermez, + hardhatHEZ, + hardhatHEZ, + ownerWallet, + feeAddToken + ); + const addressArray = [tokenAddress]; + const tokenPrice = 10; + const valueArray = [tokenPrice * _EXCHANGE_MULTIPLIER]; + await expect(hardhatHermez + .connect(governance) + .updateTokenExchange(addressArray, valueArray)) + .to.emit(hardhatHermez, "UpdateTokenExchange") + .withArgs(addressArray, valueArray); + + // still no limitations! + expect( + await hardhatHermez.instantWithdrawalViewer(tokenAddress, 1e15) + ).to.be.equal(true); + + const buckets = []; + for (let i = 0; i < numBuckets; i++) { + if (i != 4) { + const ceilUSD = (i + 1) * 1000; + const blockStamp = 0; // does not matter! + const withdrawals = 100; + const rateBlocks = (i + 1) * 4; + const rateWithdrawals = 3; + const maxWithdrawals = 4000000000; // max value 4294967296; + buckets.push({ + ceilUSD, + blockStamp, + withdrawals, + rateBlocks, + rateWithdrawals, + maxWithdrawals + }); + } + else { + // last bucket + const ceilUSD = Scalar.fromString("0xffffffffffffffffffffffff", 16); + const blockStamp = 0; // does not matter! + const withdrawals = 0; + const rateBlocks = 1; + const rateWithdrawals = 0; + const maxWithdrawals = 0; // max value 4294967296; + buckets.push({ + ceilUSD, + blockStamp, + withdrawals, + rateBlocks, + rateWithdrawals, + maxWithdrawals + }); + } + } + const bucketsPacked = buckets.map((bucket) => packBucket(bucket)); + await expect( + hardhatHermez.connect(governance).updateBucketsParameters(bucketsPacked) + ).to.emit(hardhatHermez, "UpdateBucketsParameters"); + + + for (let i = 0; i < numBuckets; i++) { + const bucket = await hardhatHermez.buckets(i); + const unpackedBucket = unpackBucket(bucket._hex); + expect(buckets[i].ceilUSD).to.be.equal(ethers.BigNumber.from(unpackedBucket.ceilUSD)); + expect(buckets[i].withdrawals).to.be.equal(ethers.BigNumber.from(unpackedBucket.withdrawals)); + expect(buckets[i].rateBlocks).to.be.equal(ethers.BigNumber.from(unpackedBucket.rateBlocks)); + expect(buckets[i].rateWithdrawals).to.be.equal(ethers.BigNumber.from(unpackedBucket.rateWithdrawals)); + expect(buckets[i].maxWithdrawals).to.be.equal(ethers.BigNumber.from(unpackedBucket.maxWithdrawals)); + } + + // limitations! + const tokenAmount = 1e10; + const resultUSD = tokenPrice * tokenAmount; + const tokenAmountDecimals = ethers.utils.parseEther( + tokenAmount.toString() + ); // 18 decimals + expect( + await hardhatHermez.token2USDTest(tokenAddress, tokenAmountDecimals) + ).to.equal(resultUSD); + + expect( + await hardhatHermez.instantWithdrawalViewer(tokenAddress, tokenAmountDecimals) + ).to.be.equal(false); + expect( + await hardhatHermez.instantWithdrawalViewer(tokenAddress, 10) + ).to.be.equal(true); + + await expect(hardhatHermez.connect(governance).safeMode()).to.emit(hardhatHermez, "SafeMode"); + + const bucketSafe = await hardhatHermez.buckets(0); + const unpackedBucketSafe = unpackBucket(bucketSafe._hex); + + expect(await hardhatHermez.nBuckets()).to.be.equal(1); + expect(unpackedBucketSafe.ceilUSD.toString(16)).to.be.equal("ffffffffffffffffffffffff"); + expect(unpackedBucketSafe.withdrawals).to.be.equal(Scalar.e(0)); + expect(unpackedBucketSafe.rateBlocks).to.be.equal(Scalar.e(1)); + expect(unpackedBucketSafe.rateWithdrawals).to.be.equal(Scalar.e(0)); + expect(unpackedBucketSafe.maxWithdrawals).to.be.equal(Scalar.e(0)); + + expect( + await hardhatHermez.token2USDTest(tokenAddress, ethers.utils.parseEther("1")) + ).to.equal(10); + + expect( + await hardhatHermez.instantWithdrawalViewer(tokenAddress, ethers.utils.parseEther("1")) + ).to.be.equal(false); + }); + }); +}); diff --git a/test/hermezHIP-0/circuitTests/HermezWithdrawCircuit.test.js b/test/hermezHIP-0/circuitTests/HermezWithdrawCircuit.test.js new file mode 100644 index 0000000..2c8a8c1 --- /dev/null +++ b/test/hermezHIP-0/circuitTests/HermezWithdrawCircuit.test.js @@ -0,0 +1,635 @@ +const { expect } = require("chai"); +const { ethers } = require("hardhat"); +const SMTMemDB = require("circomlib").SMTMemDB; +const { time } = require("@openzeppelin/test-helpers"); +const Scalar = require("ffjavascript").Scalar; +const fs = require("fs"); +const path = require("path"); +const snarkjs = require("snarkjs"); + +const poseidonUnit = require("circomlib/src/poseidon_gencontract"); +const { + l1UserTxCreateAccountDeposit, + l1UserTxDeposit, + l1UserTxDepositTransfer, + l1UserTxCreateAccountDepositTransfer, + l1UserTxForceTransfer, + l1UserTxForceExit, + l1CoordinatorTxEth, + l1CoordinatorTxBjj, + AddToken, + createAccounts, + ForgerTest, + packBucket, + calculateInputMaxTxLevels +} = require("../helpers/helpers"); +const { + float40, + HermezAccount, + txUtils, + stateUtils, + utils, + feeTable, + SMTTmpDb, + Constants, + RollupDB, + BatchBuilder, + withdrawUtils +} = require("@hermeznetwork/commonjsV1"); +const { stringifyBigInts, unstringifyBigInts } = require("ffjavascript").utils; + +describe("Hermez ERC 20", function () { + let hardhatTokenERC20Mock; + let hardhatHermez; + let hardhatWithdrawalDelayer; + let hardhatHEZ; + let hardhatVerifierWithdrawHelper; + + let owner; + let id1; + let id2; + let addrs; + let hermezGovernanceAddress; + let ownerWallet; + + let chainID; + let chainIDHex; + + const accounts = []; + for (let i = 0; i < 10; i++) { + accounts.push(new HermezAccount()); + } + const tokenInitialAmount = ethers.BigNumber.from( + "0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF" + ); + const maxL1Tx = 256; + const maxTx = 512; + const nLevels = 32; + const forgeL1L2BatchTimeout = 10; + const feeAddToken = 10; + const withdrawalDelay = 60 * 60 * 24 * 7 * 2; // 2 weeks + const INITIAL_DELAY = 0; + let circuitPath = path.join(__dirname, "withdraw.test.circom"); + let circuit; + + beforeEach(async function () { + this.timeout(0); + [ + owner, + governance, + id1, + id2, + ...addrs + ] = await ethers.getSigners(); + + hermezGovernanceAddress = governance.getAddress(); + + const chainIdProvider = (await ethers.provider.getNetwork()).chainId; + chainID = chainIdProvider; + if (chainIdProvider == 1337) { // solcover, must be a jsonRPC wallet + const mnemonic = "explain tackle mirror kit van hammer degree position ginger unfair soup bonus"; + let ownerWalletTest = ethers.Wallet.fromMnemonic(mnemonic); + // ownerWalletTest = ownerWallet.connect(ethers.provider); + ownerWallet = owner; + ownerWallet.privateKey = ownerWalletTest.privateKey; + } + else { + ownerWallet = new ethers.Wallet("0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80", ethers.provider); + } + + // factory helpers + const TokenERC20Mock = await ethers.getContractFactory("ERC20Mock"); + const TokenERC20PermitMock = await ethers.getContractFactory("ERC20PermitMock"); + + const VerifierRollupHelper = await ethers.getContractFactory( + "VerifierMock" + ); + const VerifierWithdrawHelper = await ethers.getContractFactory( + "VerifierWithdrawV2" + ); + // VerifierWithdraw + + const HermezAuctionTest = await ethers.getContractFactory( + "HermezAuctionTest" + ); + const WithdrawalDelayer = await ethers.getContractFactory( + "WithdrawalDelayer" + ); + // factory hermez + const Hermez = await ethers.getContractFactory("HermezTestV2"); + + // deploy tokens + hardhatTokenERC20Mock = await TokenERC20Mock.deploy( + "tokenname", + "TKN", + await owner.getAddress(), + tokenInitialAmount + ); + + hardhatHEZ = await TokenERC20PermitMock.deploy( + "tokenname", + "TKN", + await owner.getAddress(), + tokenInitialAmount + ); + + // deploy helpers + let hardhatVerifierRollupHelper = await VerifierRollupHelper.deploy(); + hardhatVerifierWithdrawHelper = await VerifierWithdrawHelper.deploy(); + + let hardhatHermezAuctionTest = await HermezAuctionTest.deploy(); + + // deploy hermez + hardhatHermez = await Hermez.deploy(); + await hardhatHermez.deployed(); + + hardhatWithdrawalDelayer = await WithdrawalDelayer.deploy( + INITIAL_DELAY, + hardhatHermez.address, + hermezGovernanceAddress, + hermezGovernanceAddress + ); + + // deploy hermez + await hardhatHermez.initializeHermez( + [hardhatVerifierRollupHelper.address], + calculateInputMaxTxLevels([maxTx], [nLevels]), + hardhatVerifierWithdrawHelper.address, + hardhatHermezAuctionTest.address, + hardhatHEZ.address, + forgeL1L2BatchTimeout, + feeAddToken, + hermezGovernanceAddress, + withdrawalDelay, + hardhatWithdrawalDelayer.address + ); + + // wait until is deployed + await hardhatTokenERC20Mock.deployed(); + + // setup buckets + const buckets = []; + for (let i = 0; i < 5; i++) { + const ceilUSD = (i + 1) * 10000000; + const blockStamp = 0; // does not matter! + const withdrawals = 4000000000; + const rateBlocks = i + 1; + const rateWithdrawals = i + 1; + const maxWithdrawals = 4000000000; // max value 4294967296; + buckets.push({ + ceilUSD, + blockStamp, + withdrawals, + rateBlocks, + rateWithdrawals, + maxWithdrawals + }); + } + + const bucketsPacked = buckets.map((bucket) => packBucket(bucket)); + await expect( + hardhatHermez.connect(governance).updateBucketsParameters(bucketsPacked) + ).to.emit(hardhatHermez, "UpdateBucketsParameters"); + + const _EXCHANGE_MULTIPLIER = 1e10; + const tokenAddress = hardhatTokenERC20Mock.address; + const ethereumAddress = "0x0000000000000000000000000000000000000000"; + const tokenPriceERC20 = 10 * _EXCHANGE_MULTIPLIER; //USD + const ethereumPrice = 1800 * _EXCHANGE_MULTIPLIER; //USD + + const addressArray = [tokenAddress, ethereumAddress]; + const valueArray = [tokenPriceERC20, ethereumPrice]; + + await expect(hardhatHermez + .connect(governance) + .updateTokenExchange(addressArray, valueArray)) + .to.emit(hardhatHermez, "UpdateTokenExchange") + .withArgs(addressArray, valueArray); + + }); + + + // You can nest describe calls to create subsections. + + describe("Withdraw circuit", function () { + this.timeout(0); + it("test instant withdraw circuit", async function () { + const tokenID = 1; + const babyjub = `0x${accounts[0].bjjCompressed}`; + const loadAmount = float40.round(Scalar.fromString("1000000000000000000000")); + const loadAmountF = float40.fix2Float(loadAmount); + const fromIdx = 256; + const amount = 10; + const amountF = float40.fix2Float(amount); + + const l1TxUserArray = []; + + const rollupDB = await RollupDB(new SMTMemDB(), chainID); + const forgerTest = new ForgerTest( + maxTx, + maxL1Tx, + nLevels, + hardhatHermez, + rollupDB + ); + + await AddToken( + hardhatHermez, + hardhatTokenERC20Mock, + hardhatHEZ, + ownerWallet, + feeAddToken + ); + + // Create account and exit some funds + const numAccounts = 1; + await createAccounts( + forgerTest, + loadAmount, + tokenID, + babyjub, + owner, + hardhatHermez, + hardhatTokenERC20Mock, + numAccounts, + null, + false + ); + + l1TxUserArray.push( + await l1UserTxForceExit(tokenID, fromIdx, amountF, owner, hardhatHermez) + ); + + const initialOwnerBalance = await hardhatTokenERC20Mock.balanceOf( + await owner.getAddress() + ); + + // forge empty batch + await forgerTest.forgeBatch(true, [], [], [], false, true); + // forge the create accounts + await forgerTest.forgeBatch(true, l1TxUserArray, [], [], false, true); + + // circuit stuff + const batchNum = await hardhatHermez.lastForgedBatch(); + const exitInfo = await rollupDB.getExitInfo(fromIdx, batchNum); + const stateRoot = await rollupDB.getStateRoot(batchNum); + const input = {}; + const tmpExitInfo = exitInfo; + const tmpState = tmpExitInfo.state; + + // fill private inputs + input.rootState = stateRoot; + input.ethAddr = Scalar.fromString(tmpState.ethAddr, 16); + input.tokenID = tmpState.tokenID; + input.balance = tmpState.balance; + input.idx = tmpState.idx; + input.sign = tmpState.sign; + input.ay = Scalar.fromString(tmpState.ay, 16); + input.exitBalance = tmpState.exitBalance; + input.accumulatedHash = tmpState.accumulatedHash; + input.nonce = tmpState.nonce; + + let siblings = exitInfo.siblings; + while (siblings.length < (nLevels + 1)) siblings.push(Scalar.e(0)); + input.siblingsState = siblings; + + const prove = await snarkjs.groth16.fullProve(input, path.join(__dirname, "./circuits/withdraw.wasm"), path.join(__dirname, "./circuits/withdraw.zkey" )); + const vKey = JSON.parse(fs.readFileSync(path.join(__dirname, "./circuits/verification_key_withdraw.json"))); + const res = await snarkjs.groth16.verify(vKey, prove.publicSignals, prove.proof); + expect(res).to.be.true; + + const proofA = [prove.proof.pi_a[0], + prove.proof.pi_a[1] + ]; + const proofB = [ + [ + prove.proof.pi_b[0][1], + prove.proof.pi_b[0][0] + ], + [ + prove.proof.pi_b[1][1], + prove.proof.pi_b[1][0] + ] + ]; + const proofC = [prove.proof.pi_c[0], + prove.proof.pi_c[1] + ]; + + console.log("Gas testing"); + const gasVerifiProver = await hardhatVerifierWithdrawHelper.estimateGas.verifyProof(proofA, + proofB, + proofC, + prove.publicSignals + ); + + console.log("directly prover: ", gasVerifiProver.toNumber()); + const gasVerifiProof = await hardhatHermez.testVerifyWithdraw(proofA, + proofB, + proofC, + prove.publicSignals + ); + console.log("valid proof: ", gasVerifiProof.toNumber()); + + const gasVerifiInvalid = await hardhatHermez.testVerifyWithdraw(["0", "0"], + proofB, + proofC, + prove.publicSignals + ); + console.log("invalid proof: ", gasVerifiInvalid.toNumber()); + + const instantWithdraw = true; + const tx = await hardhatHermez.withdrawCircuit( + proofA, + proofB, + proofC, + tokenID, + amount, + amount, + batchNum, + fromIdx, + instantWithdraw + ); + console.log("withdraw circuit"); + console.log("Normal flow withdraw 1 leaf: ", (await tx.wait()).gasUsed.toNumber()); + const finalOwnerBalance = await hardhatTokenERC20Mock.balanceOf( + await owner.getAddress() + ); + expect(parseInt(finalOwnerBalance)).to.equal( + parseInt(initialOwnerBalance) + amount + ); + }); + + it("test instant withdraw merkle proof with more leafs", async function () { + const tokenID = 1; + const babyjub = `0x${accounts[0].bjjCompressed}`; + const loadAmount = float40.round(1000); + const fromIdx = 256; + const amount = 10; + const amountF = float40.fix2Float(amount); + + const l1TxUserArray = []; + + const rollupDB = await RollupDB(new SMTMemDB(), chainID); + const forgerTest = new ForgerTest( + maxTx, + maxL1Tx, + nLevels, + hardhatHermez, + rollupDB + ); + + await AddToken( + hardhatHermez, + hardhatTokenERC20Mock, + hardhatHEZ, + ownerWallet, + feeAddToken + ); + + // Create account and exit some funds + const numAccounts = 3; + await createAccounts( + forgerTest, + loadAmount, + tokenID, + babyjub, + owner, + hardhatHermez, + hardhatTokenERC20Mock, + numAccounts, + null, + true + ); + + l1TxUserArray.push( + await l1UserTxForceExit(tokenID, fromIdx, amountF, owner, hardhatHermez) + ); + l1TxUserArray.push( + await l1UserTxForceExit( + tokenID, + fromIdx + 1, + amountF, + owner, + hardhatHermez + ) + ); + l1TxUserArray.push( + await l1UserTxForceExit( + tokenID, + fromIdx + 2, + amountF, + owner, + hardhatHermez + ) + ); + const initialOwnerBalance = await hardhatTokenERC20Mock.balanceOf( + await owner.getAddress() + ); + + // forge empty batch + await forgerTest.forgeBatch(true, [], [], [], false, true); + // forge the create accounts + await forgerTest.forgeBatch(true, l1TxUserArray, [], [], false, true); + + // perform withdraw + const batchNum = await hardhatHermez.lastForgedBatch(); + const stateRoot = await rollupDB.getStateRoot(batchNum); + for (let i = 0; i < 3; i ++) { + const instantWithdraw = true; + const exitInfo = await rollupDB.getExitInfo(fromIdx + i, batchNum); + + const input = {}; + const tmpExitInfo = exitInfo; + const tmpState = tmpExitInfo.state; + + // fill private inputs + input.rootState = stateRoot; + input.ethAddr = Scalar.fromString(tmpState.ethAddr, 16); + input.tokenID = tmpState.tokenID; + input.balance = tmpState.balance; + input.idx = tmpState.idx; + input.sign = tmpState.sign; + input.ay = Scalar.fromString(tmpState.ay, 16); + input.exitBalance = tmpState.exitBalance; + input.accumulatedHash = tmpState.accumulatedHash; + input.nonce = tmpState.nonce; + + let siblings = tmpExitInfo.siblings; + while (siblings.length < (nLevels + 1)) siblings.push(Scalar.e(0)); + input.siblingsState = siblings; + + const prove = await snarkjs.groth16.fullProve(input, path.join(__dirname, "./circuits/withdraw.wasm"), path.join(__dirname, "./circuits/withdraw.zkey" )); + const proofA = [prove.proof.pi_a[0], + prove.proof.pi_a[1] + ]; + const proofB = [ + [ + prove.proof.pi_b[0][1], + prove.proof.pi_b[0][0] + ], + [ + prove.proof.pi_b[1][1], + prove.proof.pi_b[1][0] + ] + ]; + const proofC = [prove.proof.pi_c[0], + prove.proof.pi_c[1] + ]; + + const tx = await hardhatHermez.withdrawCircuit( + proofA, + proofB, + proofC, + tokenID, + amount, + amount, + batchNum, + fromIdx + i, + instantWithdraw + ); + console.log("gas used circuit 3 leafs: " + i); + console.log((await tx.wait()).gasUsed.toNumber()); + } + }); + + it("test delayed withdraw merkle proof with more leafs", async function () { + const tokenID = 1; + const babyjub = `0x${accounts[0].bjjCompressed}`; + const loadAmount = float40.round(1000); + const fromIdx = 256; + const amount = 10; + const amountF = float40.fix2Float(amount); + + const l1TxUserArray = []; + + const rollupDB = await RollupDB(new SMTMemDB(), chainID); + const forgerTest = new ForgerTest( + maxTx, + maxL1Tx, + nLevels, + hardhatHermez, + rollupDB + ); + + await AddToken( + hardhatHermez, + hardhatTokenERC20Mock, + hardhatHEZ, + ownerWallet, + feeAddToken + ); + + // Create account and exit some funds + const numAccounts = 3; + await createAccounts( + forgerTest, + loadAmount, + tokenID, + babyjub, + owner, + hardhatHermez, + hardhatTokenERC20Mock, + numAccounts, + null, + true + ); + + l1TxUserArray.push( + await l1UserTxForceExit(tokenID, fromIdx, amountF, owner, hardhatHermez) + ); + l1TxUserArray.push( + await l1UserTxForceExit( + tokenID, + fromIdx + 1, + amountF, + owner, + hardhatHermez + ) + ); + l1TxUserArray.push( + await l1UserTxForceExit( + tokenID, + fromIdx + 2, + amountF, + owner, + hardhatHermez + ) + ); + const initialOwnerBalance = await hardhatTokenERC20Mock.balanceOf( + await owner.getAddress() + ); + + // forge empty batch + await forgerTest.forgeBatch(true, [], [], [], false, true); + // forge the create accounts + await forgerTest.forgeBatch(true, l1TxUserArray, [], [], false, true); + + // perform withdraw + const batchNum = await hardhatHermez.lastForgedBatch(); + const stateRoot = await rollupDB.getStateRoot(batchNum); + for (let i = 0; i < 3; i ++) { + const instantWithdraw = false; + const exitInfo = await rollupDB.getExitInfo(fromIdx + i, batchNum); + + const input = {}; + const tmpExitInfo = exitInfo; + const tmpState = tmpExitInfo.state; + + // fill private inputs + // fill private inputs + input.rootState = stateRoot; + input.ethAddr = Scalar.fromString(tmpState.ethAddr, 16); + input.tokenID = tmpState.tokenID; + input.balance = tmpState.balance; + input.idx = tmpState.idx; + input.sign = tmpState.sign; + input.ay = Scalar.fromString(tmpState.ay, 16); + input.exitBalance = tmpState.exitBalance; + input.accumulatedHash = tmpState.accumulatedHash; + input.nonce = tmpState.nonce; + + let siblings = tmpExitInfo.siblings; + while (siblings.length < (nLevels + 1)) siblings.push(Scalar.e(0)); + input.siblingsState = siblings; + + const prove = await snarkjs.groth16.fullProve(input, path.join(__dirname, "./circuits/withdraw.wasm"), path.join(__dirname, "./circuits/withdraw.zkey" )); + const proofA = [prove.proof.pi_a[0], + prove.proof.pi_a[1] + ]; + const proofB = [ + [ + prove.proof.pi_b[0][1], + prove.proof.pi_b[0][0] + ], + [ + prove.proof.pi_b[1][1], + prove.proof.pi_b[1][0] + ] + ]; + const proofC = [prove.proof.pi_c[0], + prove.proof.pi_c[1] + ]; + + const tx = await hardhatHermez.withdrawCircuit( + proofA, + proofB, + proofC, + tokenID, + amount, + amount, + batchNum, + fromIdx + i, + instantWithdraw + ); + console.log("gas used circuit 3 leafs: " + i); + console.log((await tx.wait()).gasUsed.toNumber()); + } + }); + }); +}); +function padding256(n) { + let nstr = Scalar.e(n).toString(16); + while (nstr.length < 64) nstr = "0" + nstr; + nstr = `0x${nstr}`; + return nstr; +} diff --git a/test/hermezHIP-0/circuitTests/circuits/verification_key_withdraw.json b/test/hermezHIP-0/circuitTests/circuits/verification_key_withdraw.json new file mode 100644 index 0000000..f7ad562 --- /dev/null +++ b/test/hermezHIP-0/circuitTests/circuits/verification_key_withdraw.json @@ -0,0 +1,94 @@ +{ + "protocol": "groth16", + "curve": "bn128", + "nPublic": 1, + "vk_alpha_1": [ + "388810387696872229600535426988510367661191668538620278246358506880151278377", + "17012807097498437657437400703444959258860359960854381092881122887796891799764", + "1" + ], + "vk_beta_2": [ + [ + "13134655269783470578064433288891955017063376221140036407895159623715719034212", + "20834219205012105637945707216009277384365408423076044151691072833174688573103" + ], + [ + "9424872748620702615302289225079312376316964854917092525872944035178853577805", + "13123146757901582440742420379387265563202471200050521454662146479289670350870" + ], + [ + "1", + "0" + ] + ], + "vk_gamma_2": [ + [ + "10857046999023057135944570762232829481370756359578518086990519993285655852781", + "11559732032986387107991004021392285783925812861821192530917403151452391805634" + ], + [ + "8495653923123431417604973247489272438418190587263600148770280649306958101930", + "4082367875863433681332203403145435568316851327593401208105741076214120093531" + ], + [ + "1", + "0" + ] + ], + "vk_delta_2": [ + [ + "10857046999023057135944570762232829481370756359578518086990519993285655852781", + "11559732032986387107991004021392285783925812861821192530917403151452391805634" + ], + [ + "8495653923123431417604973247489272438418190587263600148770280649306958101930", + "4082367875863433681332203403145435568316851327593401208105741076214120093531" + ], + [ + "1", + "0" + ] + ], + "vk_alphabeta_12": [ + [ + [ + "4645845885061208033930139182187350307213617824726686023781053606320080236513", + "20774023937737088777059104897080003346369142648825031157728006245283691545615" + ], + [ + "4394085183632046305088049742454222847106458953640927949977189954224918735419", + "7121110531587644844074953967935295233575221287717266107019411090193325160996" + ], + [ + "3572497500950631745877684480411572456983722646653589847787486326132209850434", + "20732279951260394077777627223133276286097766567226823430551552667160234922950" + ] + ], + [ + [ + "3277241162640312769861741066386094536591412325986458519159506759778227904426", + "14118826624866050066186594452052195254684703419729631007864431597372281882861" + ], + [ + "1062933187311025878772341395857227696353937859655986736890018719130403385093", + "7848083740770416681110619870235297672457637277331564918073178948376952556340" + ], + [ + "8336286883308896407787733865783222290230326090096199322109087078826716700919", + "14764952568572344566348828767010681268794039151764193591226867958430675481151" + ] + ] + ], + "IC": [ + [ + "9709798335278487073135934214642788341483364749831766588850420953696105265177", + "6379602209297816575291953207502184116487138674423700332563836533325222425474", + "1" + ], + [ + "17228952008706007141311207908959456806520751866178136518819949659318129370336", + "11243267546602369974266049105154296218563916482012084687186341306379428706307", + "1" + ] + ] +} \ No newline at end of file diff --git a/test/hermezHIP-0/circuitTests/circuits/withdraw.wasm b/test/hermezHIP-0/circuitTests/circuits/withdraw.wasm new file mode 100644 index 0000000..3104e60 Binary files /dev/null and b/test/hermezHIP-0/circuitTests/circuits/withdraw.wasm differ diff --git a/test/hermezHIP-0/circuitTests/circuits/withdraw.zkey b/test/hermezHIP-0/circuitTests/circuits/withdraw.zkey new file mode 100644 index 0000000..ab0ccf9 Binary files /dev/null and b/test/hermezHIP-0/circuitTests/circuits/withdraw.zkey differ diff --git a/test/hermezHIP-0/gasPerformance/HermezForgeGasFast.js b/test/hermezHIP-0/gasPerformance/HermezForgeGasFast.js new file mode 100644 index 0000000..51e4bb3 --- /dev/null +++ b/test/hermezHIP-0/gasPerformance/HermezForgeGasFast.js @@ -0,0 +1,424 @@ +const { expect } = require("chai"); +const { ethers, upgrades } = require("hardhat"); +const SMTMemDB = require("circomlib").SMTMemDB; +const poseidonUnit = require("circomlib/src/poseidon_gencontract"); +const { + l1UserTxCreateAccountDeposit, + l1UserTxDeposit, + l1UserTxDepositTransfer, + l1UserTxCreateAccountDepositTransfer, + l1UserTxForceTransfer, + l1UserTxForceExit, + l1CoordinatorTxBjj, + l1CoordinatorTxEth, + AddToken, + ForgerTest, + calculateInputMaxTxLevels +} = require("../helpers/helpers"); +const { + float40, + HermezAccount, + txUtils, + stateUtils, + utils, + feeTable, + SMTTmpDb, + Constants, + RollupDB, + BatchBuilder, +} = require("@hermeznetwork/commonjsV1"); +const INITIAL_DELAY = 0; + +const gasLimit = 12500000; + +describe("Hermez gas performance", function () { + let hardhatTokenERC20Mock; + let hardhatHermez; + let hardhatWithdrawalDelayer; + let hardhatHEZ; + let owner; + let id1; + let id2; + let addrs; + let hermezGovernanceAddress; + let ownerWallet; + + let chainID; + let chainIDHex; + + const accounts = []; + for (let i = 0; i < 10; i++) { + accounts.push(new HermezAccount()); + } + const tokenInitialAmount = 1000000; + const maxL1Tx = 256; + const maxTx = 2000; + const nLevels = 32; + const forgeL1L2BatchTimeout = 10; + const feeAddToken = 10; + const withdrawalDelay = 60 * 60 * 24 * 7 * 2; // 2 weeks + + beforeEach(async function () { + [ + owner, + governance, + id1, + id2, + ...addrs + ] = await ethers.getSigners(); + + hermezGovernanceAddress = governance.getAddress(); + + const chainIdProvider = (await ethers.provider.getNetwork()).chainId; + if (chainIdProvider == 1337) { // solcover, must be a jsonRPC wallet + const mnemonic = "explain tackle mirror kit van hammer degree position ginger unfair soup bonus"; + let ownerWalletTest = ethers.Wallet.fromMnemonic(mnemonic); + // ownerWalletTest = ownerWallet.connect(ethers.provider); + ownerWallet = owner; + ownerWallet.privateKey = ownerWalletTest.privateKey; + } + else { + ownerWallet = new ethers.Wallet("0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80", ethers.provider); + } + + // factory helpers + const TokenERC20Mock = await ethers.getContractFactory("ERC20Mock"); + const TokenERC20PermitMock = await ethers.getContractFactory("ERC20PermitMock"); + + const VerifierRollupHelper = await ethers.getContractFactory( + "VerifierRollupHelper" + ); + const VerifierWithdrawHelper = await ethers.getContractFactory( + "VerifierWithdrawHelper" + ); + const HermezAuctionTest = await ethers.getContractFactory( + "HermezAuctionTest" + ); + const WithdrawalDelayer = await ethers.getContractFactory( + "WithdrawalDelayerTest" + ); + + // factory hermez + const Hermez = await ethers.getContractFactory("HermezTestV2"); + + // deploy tokens + hardhatTokenERC20Mock = await TokenERC20Mock.deploy( + "tokenname", + "TKN", + await owner.getAddress(), + tokenInitialAmount + ); + + hardhatHEZ = await TokenERC20PermitMock.deploy( + "tokenname", + "TKN", + await owner.getAddress(), + tokenInitialAmount + ); + + let hardhatVerifierRollupHelper = await VerifierRollupHelper.deploy(); + let hardhatVerifierWithdrawHelper = await VerifierWithdrawHelper.deploy(); + let hardhatHermezAuctionTest = await HermezAuctionTest.deploy(); + + // deploy hermez + hardhatHermez = await upgrades.deployProxy(Hermez, [], { + unsafeAllowCustomTypes: true, + initializer: undefined, + }); + await hardhatHermez.deployed(); + hardhatWithdrawalDelayer = await WithdrawalDelayer.deploy( + INITIAL_DELAY, + hardhatHermez.address, + hermezGovernanceAddress, + hermezGovernanceAddress + ); + + // deploy hermez + + await hardhatHermez.initializeHermez( + [hardhatVerifierRollupHelper.address], + calculateInputMaxTxLevels([maxTx], [nLevels]), + hardhatVerifierWithdrawHelper.address, + hardhatHermezAuctionTest.address, + hardhatHEZ.address, + forgeL1L2BatchTimeout, + feeAddToken, + hermezGovernanceAddress, + withdrawalDelay, + hardhatWithdrawalDelayer.address + ); + + // wait until is deployed + await hardhatTokenERC20Mock.deployed(); + const chainSC = await hardhatHermez.getChainID(); + chainID = chainSC.toNumber(); + chainIDHex = chainSC.toHexString(); + }); + + describe("Test Queue", function () { + it("Gas report l1 operator tx", async function () { + this.timeout(0); + const tokenID = 1; + const babyjub = `0x${accounts[0].bjjCompressed}`; + const l1TxCoordiatorArray = []; + + await AddToken( + hardhatHermez, + hardhatTokenERC20Mock, + hardhatHEZ, + ownerWallet, + feeAddToken + ); + const proofA = ["0", "0"]; + const proofB = [ + ["0", "0"], + ["0", "0"], + ]; + const proofC = ["0", "0"]; + + const newLastIdx = 257; + const newStateRoot = 123; + const compressedL1CoordinatorTx = "0x00"; + + const L2TxsData = "0x00"; + + const feeIdxCoordinator = `0x${utils.padZeros( + "", + ((nLevels * 64) / 8) * 2 + )}`; + const verifierIdx = 0; + const SCGasArray = []; + const wastedGasarray = []; + + let tx = await hardhatHermez.forgeGasTest( + newLastIdx, + newStateRoot, + compressedL1CoordinatorTx, + L2TxsData, + feeIdxCoordinator, + verifierIdx, + true, + proofA, + proofB, + proofC, + { gasLimit: gasLimit } + ); + + console.log( + "| Wasted SC | IncrementPrev | wastedGas | IncrementPrev | l1Coordinator-Tx |" + ); + console.log( + "| -------- | --------- | --------- | --------- | ---------- |" + ); + + const l1CoordinatorTx = await l1CoordinatorTxEth(tokenID, babyjub, owner, hardhatHermez, chainIDHex); + const multiplier = 12; + for (let i = 0; i < 22; i++) { + stringL1CoordinatorTx = ""; + for (let j = 0; j < i * multiplier; j++) { + stringL1CoordinatorTx = + stringL1CoordinatorTx + l1CoordinatorTx.l1TxCoordinatorbytes.slice(2); // retireve the 0x + } + let tx = await hardhatHermez.forgeGasTest( + newLastIdx, + newStateRoot, + `0x${stringL1CoordinatorTx}`, + L2TxsData, + feeIdxCoordinator, + verifierIdx, + true, + proofA, + proofB, + proofC, + { gasLimit: gasLimit } + ); + const receipt = await tx.wait(); + SCGasArray.push(gasLimit - receipt.events[1].args[0].toNumber()); + wastedGasarray.push(receipt.gasUsed.toNumber()); + + // hackmd table + let log = `| ${SCGasArray[i]} |`; + log += ` ${SCGasArray[i] - (SCGasArray[i - 1] ? SCGasArray[i - 1] : SCGasArray[i]) + } |`; + log += ` ${wastedGasarray[i]} |`; + log += ` ${wastedGasarray[i] - + (wastedGasarray[i - 1] ? wastedGasarray[i - 1] : wastedGasarray[i]) + } |`; + log += ` ${i*multiplier} |`; + console.log(log); + } + }); + + it("Gas report l1 user creataccount tx", async function () { + this.timeout(0); + const tokenID = 1; + const babyjub = `0x${accounts[0].bjjCompressed}`; + const loadAmount = float40.round(1); + + await AddToken( + hardhatHermez, + hardhatTokenERC20Mock, + hardhatHEZ, + ownerWallet, + feeAddToken + ); + const proofA = ["0", "0"]; + const proofB = [ + ["0", "0"], + ["0", "0"], + ]; + const proofC = ["0", "0"]; + + const newLastIdx = 257; + const newStateRoot = 123; + const compressedL1CoordinatorTx = "0x00"; + const L2TxsData = "0x00"; + const feeIdxCoordinator = `0x${utils.padZeros( + "", + ((nLevels * 64) / 8) * 2 + )}`; + const verifierIdx = 0; + const SCGasArray = []; + const wastedGasarray = []; + console.log( + "| Wasted gas SC | IncrementPrev | wastedGas | IncrementPrev | L1user-tx-createAccount |" + ); + console.log( + "| -------- | --------- | --------- | --------- | ---------- |" + ); + + const multiplier = 12; + + for (let i = 0; i < 11; i++) { + for (let j = 0; j < i*multiplier; j++) { + let txUser = await l1UserTxCreateAccountDeposit( + loadAmount, + tokenID, + babyjub, + ownerWallet, + hardhatHermez, + hardhatTokenERC20Mock + ); + } + + await hardhatHermez.forgeBatch( + newLastIdx, + newStateRoot, + compressedL1CoordinatorTx, + L2TxsData, + feeIdxCoordinator, + verifierIdx, + true, + proofA, + proofB, + proofC, + { gasLimit: gasLimit } + ); + + // forge with events + let tx = await hardhatHermez.forgeGasTest( + newLastIdx, + newStateRoot, + compressedL1CoordinatorTx, + L2TxsData, + feeIdxCoordinator, + verifierIdx, + true, + proofA, + proofB, + proofC, + { gasLimit: gasLimit } + ); + const receipt = await tx.wait(); + SCGasArray.push(gasLimit - receipt.events[1].args[0].toNumber()); + wastedGasarray.push(receipt.gasUsed.toNumber()); + + // hackmd table + let log = `| ${SCGasArray[i]} |`; + log += ` ${SCGasArray[i] - (SCGasArray[i - 1] ? SCGasArray[i - 1] : SCGasArray[i]) + } |`; + log += ` ${wastedGasarray[i]} |`; + log += ` ${wastedGasarray[i] - + (wastedGasarray[i - 1] ? wastedGasarray[i - 1] : wastedGasarray[i]) + } |`; + log += ` ${i*multiplier} |`; + console.log(log); + } + }); + + it("Gas report l2 tx", async function () { + this.timeout(0); + const proofA = ["0", "0"]; + const proofB = [ + ["0", "0"], + ["0", "0"], + ]; + const proofC = ["0", "0"]; + + const newLastIdx = 257; + const newStateRoot = 123; + const compressedL1CoordinatorTx = "0x00"; + let L2TxsData; + const feeIdxCoordinator = `0x${"1".repeat( + ((nLevels * 64) / 8) * 2 + )}`; + const verifierIdx = 0; + const SCGasArray = []; + const wastedGasarray = []; + + const multiplier = 37; + console.log( + "| Wasted gas SC | IncrementPrev | wastedGas | IncrementPrev | L2 |" + ); + console.log( + "| -------- | --------- | --------- | --------- | ---------- |" + ); + + for (let i = 0; i < 54; i++) { + L2TxsData = `0x${"1".repeat(((nLevels / 8) * 2 + 6) * i*multiplier * 2)}`; // maxL2Tx 376 + await hardhatHermez.forgeBatch( + newLastIdx, + newStateRoot, + compressedL1CoordinatorTx, + L2TxsData, + feeIdxCoordinator, + verifierIdx, + true, + proofA, + proofB, + proofC, + { gasLimit: gasLimit } + ); + + // forge with events + let tx = await hardhatHermez.forgeGasTest( + newLastIdx, + newStateRoot, + compressedL1CoordinatorTx, + L2TxsData, + feeIdxCoordinator, + verifierIdx, + true, + proofA, + proofB, + proofC, + { gasLimit: gasLimit } + ); + const receipt = await tx.wait(); + SCGasArray.push(gasLimit - receipt.events[1].args[0].toNumber()); + wastedGasarray.push(receipt.gasUsed.toNumber()); + + // hackmd table + let log = `| ${SCGasArray[i]} |`; + log += ` ${SCGasArray[i] - (SCGasArray[i - 1] ? SCGasArray[i - 1] : SCGasArray[i]) + } |`; + log += ` ${wastedGasarray[i]} |`; + log += ` ${wastedGasarray[i] - + (wastedGasarray[i - 1] ? wastedGasarray[i - 1] : wastedGasarray[i]) + } |`; + log += ` ${i*multiplier} |`; + console.log(log); + } + }); + }); +}); diff --git a/test/hermezHIP-0/helpers/erc2612.js b/test/hermezHIP-0/helpers/erc2612.js new file mode 100644 index 0000000..9082a89 --- /dev/null +++ b/test/hermezHIP-0/helpers/erc2612.js @@ -0,0 +1,42 @@ +const { + ethers +} = require("hardhat"); + +const PERMIT_TYPEHASH = ethers.utils.keccak256(ethers.utils.toUtf8Bytes("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)")); +async function createPermitDigest(token, owner, spender, value, nonce, deadline) { + const chainId = (await token.getChainId()); + const name = await token.name(); + let _domainSeparator = ethers.utils.keccak256( + ethers.utils.defaultAbiCoder.encode( + ["bytes32", "bytes32", "bytes32", "uint256", "address"], + [ + ethers.utils.keccak256( + ethers.utils.toUtf8Bytes( + "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)" + ) + ), + ethers.utils.keccak256(ethers.utils.toUtf8Bytes(name)), + ethers.utils.keccak256(ethers.utils.toUtf8Bytes("1")), + chainId, + token.address, + ] + ) + ); + + return ethers.utils.solidityKeccak256( + ["bytes1", "bytes1", "bytes32", "bytes32"], + [ + "0x19", + "0x01", + _domainSeparator, + ethers.utils.keccak256(ethers.utils.defaultAbiCoder.encode( + ["bytes32", "address", "address", "uint256", "uint256", "uint256"], + [PERMIT_TYPEHASH, owner, spender, value, nonce, deadline] + )) + ]); +} + +module.exports = { + createPermitDigest, + PERMIT_TYPEHASH +}; \ No newline at end of file diff --git a/test/hermezHIP-0/helpers/helpers.js b/test/hermezHIP-0/helpers/helpers.js new file mode 100644 index 0000000..3fc7dfb --- /dev/null +++ b/test/hermezHIP-0/helpers/helpers.js @@ -0,0 +1,1124 @@ +const { expect } = require("chai"); +const { ethers } = require("hardhat"); +const Scalar = require("ffjavascript").Scalar; +const axios = require("axios"); +const fs = require("fs"); +const path = require("path"); + +const { float40, txUtils, utils } = require("@hermeznetwork/commonjsV1"); +const { BigNumber } = require("ethers"); +const nLevels = 32; +const { + createPermitDigest +} = require("./erc2612"); +const { stringifyBigInts, unstringifyBigInts } = require("ffjavascript").utils; + +const L1_USER_BYTES = 78; // 20 ehtaddr, 32 babyjub, 4 token, 2 amountF, 2 loadAmountf, 6 fromIDx, 6 toidx + +const babyjub0 = 0; +const fromIdx0 = 0; +const loadAmountF0 = 0; +const amountF0 = 0; +const tokenID0 = 0; +const toIdx0 = 0; +const emptyPermit = "0x"; +let ABIbid = [ + "function permit(address,address,uint256,uint256,uint8,bytes32,bytes32)", +]; + +let iface = new ethers.utils.Interface(ABIbid); + +function sleep(ms) { + return new Promise(resolve => setTimeout(resolve, ms)); +} + +const pathInput = path.join(__dirname, "./inputPeta.json"); + +class ForgerTest { + constructor(maxTx, maxL1Tx, nLevels, hardhatHermez, rollupDB, realVerifier) { + this.rollupDB = rollupDB; + this.maxTx = maxTx; + this.maxL1Tx = maxL1Tx; + this.nLevels = nLevels; + this.hardhatHermez = hardhatHermez; + this.realVerifier = realVerifier; + + this.L1TxB = 544; + } + + async forgeBatch(l1Batch, l1TxUserArray, l1TxCoordiatorArray, l2txArray, log) { + const bb = await this.rollupDB.buildBatch( + this.maxTx, + this.nLevels, + this.maxL1Tx + ); + + let jsL1TxData = ""; + for (let tx of l1TxUserArray) { + bb.addTx(txUtils.decodeL1TxFull(tx)); + jsL1TxData = jsL1TxData + tx.slice(2); + } + + // check L1 user tx are the same in batchbuilder and contract + const currentQueue = await this.hardhatHermez.nextL1ToForgeQueue(); + const SCL1TxData = await this.hardhatHermez.mapL1TxQueue(currentQueue); + + expect(SCL1TxData).to.equal(`0x${jsL1TxData}`); + + + if (l1TxCoordiatorArray) { + for (let tx of l1TxCoordiatorArray) { + bb.addTx(txUtils.decodeL1TxFull(tx.l1TxBytes)); + } + } + + + if (l2txArray) { + for (let tx of l2txArray) { + bb.addTx(tx); + } + } + + // if(log) { + // bb.addToken(1); + // bb.addFeeIdx(259); + // } + + await bb.build(); + + let stringL1CoordinatorTx = ""; + for (let tx of l1TxCoordiatorArray) { + stringL1CoordinatorTx = + stringL1CoordinatorTx + tx.l1TxCoordinatorbytes.slice(2); // retireve the 0x + } + + + let proofA, proofB, proofC; + + if (this.realVerifier == true) { + // real verifier + const inputJson = stringifyBigInts(bb.getInput()); + if(log) { + fs.writeFileSync(pathInput, JSON.stringify(inputJson, null, 1)); + } + await axios.post("http://ec2-3-139-54-168.us-east-2.compute.amazonaws.com:3000/api/input", inputJson); + let response; + do { + await sleep(1000); + response = await axios.get("http://ec2-3-139-54-168.us-east-2.compute.amazonaws.com:3000/api/status"); + } while (response.data.status == "busy"); + + proofA = [JSON.parse(response.data.proof).pi_a[0], + JSON.parse(response.data.proof).pi_a[1] + ]; + proofB = [ + [ + JSON.parse(response.data.proof).pi_b[0][1], + JSON.parse(response.data.proof).pi_b[0][0] + ], + [ + JSON.parse(response.data.proof).pi_b[1][1], + JSON.parse(response.data.proof).pi_b[1][0] + ] + ]; + proofC = [JSON.parse(response.data.proof).pi_c[0], + JSON.parse(response.data.proof).pi_c[1] + ]; + + const input = JSON.parse(response.data.pubData); + expect(input[0]).to.equal(bb.getHashInputs().toString()); + } else { + // mock verifier + proofA = ["0", "0"]; + proofB = [ + ["0", "0"], + ["0", "0"], + ]; + proofC = ["0", "0"]; + } + + const newLastIdx = bb.getNewLastIdx(); + const newStateRoot = bb.getNewStateRoot(); + const compressedL1CoordinatorTx = `0x${stringL1CoordinatorTx}`; + const L1L2TxsData = bb.getL2TxsDataSM(); + const feeIdxCoordinator = bb.getFeeTxsDataSM(); + const verifierIdx = 0; + + let implementCalculateInputTest = false; + //InterfaceContract.getFunction(options.function); + + for (const functionSC in this.hardhatHermez.interface.functions) { + if (this.hardhatHermez.interface.functions[functionSC].name == "calculateInputTest") { + implementCalculateInputTest = true; + break; + } + } + + if (implementCalculateInputTest) + await expect( + this.hardhatHermez.calculateInputTest( + newLastIdx, + newStateRoot, + compressedL1CoordinatorTx, + L1L2TxsData, + feeIdxCoordinator, + l1Batch, + verifierIdx + ) + ) + .to.emit(this.hardhatHermez, "ReturnUint256") + .withArgs(bb.getHashInputs()); + + await expect( + this.hardhatHermez.forgeBatch( + newLastIdx, + newStateRoot, + compressedL1CoordinatorTx, + L1L2TxsData, + feeIdxCoordinator, + verifierIdx, + l1Batch, + proofA, + proofB, + proofC + ) + ).to.emit(this.hardhatHermez, "ForgeBatch") + .withArgs(bb.batchNumber, l1TxUserArray.length); + + await this.rollupDB.consolidate(bb); + } +} + +async function l1UserTxCreateAccountDeposit( + loadAmount, + tokenID, + babyjub, + wallet, + hardhatHermez, + hardhatTokenHermez, + isERC20Permit +) { + const loadAmountF = float40.fix2Float(loadAmount); + + // equivalent L1 transaction: + const l1TxcreateAccountDeposit = { + toIdx: 0, + tokenID: tokenID, + amountF: 0, + loadAmountF: loadAmountF, + fromIdx: 0, + fromBjjCompressed: babyjub, + fromEthAddr: await wallet.getAddress(), + }; + const l1Txbytes = `0x${txUtils.encodeL1TxFull(l1TxcreateAccountDeposit)}`; + + const lastQueue = await hardhatHermez.nextL1FillingQueue(); + + const lastQueueBytes = await hardhatHermez.mapL1TxQueue(lastQueue); + + const currentIndex = (lastQueueBytes.length - 2) / 2 / L1_USER_BYTES; // -2 --> 0x, /2 --> 2 hex digits = 1 byte + + if (tokenID != 0) { + if (!isERC20Permit) { + // tokens ERC20 + const initialOwnerBalance = await hardhatTokenHermez.balanceOf( + await wallet.getAddress() + ); + await expect( + hardhatTokenHermez.connect(wallet).approve(hardhatHermez.address, loadAmount) + ).to.emit(hardhatTokenHermez, "Approval"); + + // const gasCost = await hardhatHermez.estimateGas[ + // "addL1Transaction(uint256,uint48,uint40,uint40,uint32,uint48)" + // ](babyjub, fromIdx0, loadAmountF, amountF0, tokenID, toIdx0); + // console.log(gasCost.toNumber()); + + await expect( + hardhatHermez.connect(wallet).addL1Transaction( + babyjub, + fromIdx0, + loadAmountF, + amountF0, + tokenID, + toIdx0, + emptyPermit + ) + ) + .to.emit(hardhatHermez, "L1UserTxEvent") + .withArgs(lastQueue, currentIndex, l1Txbytes); + + const finalOwnerBalance = await hardhatTokenHermez.balanceOf( + await wallet.getAddress() + ); + + expect(finalOwnerBalance).to.equal( + BigNumber.from(initialOwnerBalance).sub(BigNumber.from(loadAmount)) + ); + } else { + // tokens ERC20Permit + const initialOwnerBalance = await hardhatTokenHermez.balanceOf( + await wallet.getAddress() + ); + + const deadline = ethers.constants.MaxUint256; + const value = loadAmount; + const nonce = await hardhatTokenHermez.nonces(await wallet.getAddress()); + const { v, r, s } = await createPermitSignature( + hardhatTokenHermez, + wallet, + hardhatHermez.address, + value, + nonce, + deadline + ); + + const data = iface.encodeFunctionData("permit", [ + await wallet.getAddress(), + hardhatHermez.address, + value, + deadline, + v, + r, + s + ]); + + // send l1tx wth permit signature + await expect( + hardhatHermez.connect(wallet).addL1Transaction( + babyjub, + fromIdx0, + loadAmountF, + amountF0, + tokenID, + toIdx0, + data + ) + ) + .to.emit(hardhatHermez, "L1UserTxEvent") + .withArgs(lastQueue, currentIndex, l1Txbytes); + + const finalOwnerBalance = await hardhatTokenHermez.balanceOf( + await wallet.getAddress() + ); + + expect(finalOwnerBalance).to.equal( + BigNumber.from(initialOwnerBalance).sub(BigNumber.from(loadAmount)) + ); + } + } else { + // ether + const initialOwnerBalance = await wallet.getBalance(); + await expect( + hardhatHermez.connect(wallet).addL1Transaction( + babyjub, + fromIdx0, + loadAmountF, + amountF0, + tokenID, + toIdx0, + emptyPermit, + { + value: loadAmount, + gasPrice: 0, + } + ) + ) + .to.emit(hardhatHermez, "L1UserTxEvent") + .withArgs(lastQueue, currentIndex, l1Txbytes); + + const finalOwnerBalance = await wallet.getBalance(); + + expect(finalOwnerBalance).to.equal( + BigNumber.from(initialOwnerBalance).sub(BigNumber.from(loadAmount)) + ); + } + + return l1Txbytes; +} + +async function l1UserTxDeposit( + loadAmount, + tokenID, + fromIdx, + wallet, + hardhatHermez, + hardhatTokenHermez, + isERC20Permit +) { + const loadAmountF = float40.fix2Float(loadAmount); + + // equivalent L1 transaction: + const l1TxDeposit = { + toIdx: 0, + tokenID: tokenID, + amountF: 0, + loadAmountF: loadAmountF, + fromIdx: fromIdx, + fromBjjCompressed: "0", + fromEthAddr: await wallet.getAddress(), + }; + + const l1Txbytes = `0x${txUtils.encodeL1TxFull(l1TxDeposit)}`; + + const lastQueue = await hardhatHermez.nextL1FillingQueue(); + + const lastQueueBytes = await hardhatHermez.mapL1TxQueue(lastQueue); + + const currentIndex = (lastQueueBytes.length - 2) / 2 / L1_USER_BYTES; // -2 --> 0x, /2 --> 2 hex digits = 1 byte + + if (tokenID != 0) { + if (!isERC20Permit) { + // tokens ERC20 + const initialOwnerBalance = await hardhatTokenHermez.balanceOf( + await wallet.getAddress() + ); + await expect( + hardhatTokenHermez.connect(wallet).approve(hardhatHermez.address, loadAmount) + ).to.emit(hardhatTokenHermez, "Approval"); + + await expect( + hardhatHermez.connect(wallet).addL1Transaction( + babyjub0, + fromIdx, + loadAmountF, + amountF0, + tokenID, + toIdx0, + emptyPermit + ) + ) + .to.emit(hardhatHermez, "L1UserTxEvent") + .withArgs(lastQueue, currentIndex, l1Txbytes); + + const finalOwnerBalance = await hardhatTokenHermez.balanceOf( + await wallet.getAddress() + ); + + expect(finalOwnerBalance).to.equal( + BigNumber.from(initialOwnerBalance).sub(BigNumber.from(loadAmount)) + ); + } else { + // tokens ERC20Permit + const initialOwnerBalance = await hardhatTokenHermez.balanceOf( + await wallet.getAddress() + ); + + const deadline = ethers.constants.MaxUint256; + const value = loadAmount; + const nonce = await hardhatTokenHermez.nonces(await wallet.getAddress()); + const { v, r, s } = await createPermitSignature( + hardhatTokenHermez, + wallet, + hardhatHermez.address, + value, + nonce, + deadline + ); + + const data = iface.encodeFunctionData("permit", [ + await wallet.getAddress(), + hardhatHermez.address, + value, + deadline, + v, + r, + s + ]); + + // send l1tx wth permit signature + await expect( + hardhatHermez.connect(wallet).addL1Transaction( + babyjub0, + fromIdx, + loadAmountF, + amountF0, + tokenID, + toIdx0, + data + ) + ) + .to.emit(hardhatHermez, "L1UserTxEvent") + .withArgs(lastQueue, currentIndex, l1Txbytes); + + const finalOwnerBalance = await hardhatTokenHermez.balanceOf( + await wallet.getAddress() + ); + + expect(finalOwnerBalance).to.equal( + BigNumber.from(initialOwnerBalance).sub(BigNumber.from(loadAmount)) + ); + } + } else { + // ether + const initialOwnerBalance = await wallet.getBalance(); + + await expect( + hardhatHermez.connect(wallet).addL1Transaction( + babyjub0, + fromIdx, + loadAmountF, + amountF0, + tokenID, + toIdx0, + emptyPermit, + { + value: loadAmount, + gasPrice: 0, + } + ) + ) + .to.emit(hardhatHermez, "L1UserTxEvent") + .withArgs(lastQueue, currentIndex, l1Txbytes); + + const finalOwnerBalance = await wallet.getBalance(); + + expect(finalOwnerBalance).to.equal( + BigNumber.from(initialOwnerBalance).sub(BigNumber.from(loadAmount)) + ); + } + + return l1Txbytes; +} + +async function l1UserTxDepositTransfer( + loadAmount, + tokenID, + fromIdx, + toIdx, + amountF, + wallet, + hardhatHermez, + hardhatTokenHermez, + isERC20Permit +) { + const loadAmountF = float40.fix2Float(loadAmount); + + // equivalent L1 transaction: + const l1TxDepositTransfer = { + toIdx: toIdx, + tokenID: tokenID, + amountF: amountF, + loadAmountF: loadAmountF, + fromIdx: fromIdx, + fromBjjCompressed: "0", + fromEthAddr: await wallet.getAddress(), + }; + + const l1Txbytes = `0x${txUtils.encodeL1TxFull(l1TxDepositTransfer)}`; + + const lastQueue = await hardhatHermez.nextL1FillingQueue(); + + const lastQueueBytes = await hardhatHermez.mapL1TxQueue(lastQueue); + + const currentIndex = (lastQueueBytes.length - 2) / 2 / L1_USER_BYTES; // -2 --> 0x, /2 --> 2 hex digits = 1 byte + + if (tokenID != 0) { + if (!isERC20Permit) { + // tokens ERC20 + const initialOwnerBalance = await hardhatTokenHermez.balanceOf( + await wallet.getAddress() + ); + await expect( + hardhatTokenHermez.connect(wallet).approve(hardhatHermez.address, loadAmount) + ).to.emit(hardhatTokenHermez, "Approval"); + + await expect( + hardhatHermez.connect(wallet).addL1Transaction( + babyjub0, + fromIdx, + loadAmountF, + amountF, + tokenID, + toIdx, + emptyPermit + ) + ) + .to.emit(hardhatHermez, "L1UserTxEvent") + .withArgs(lastQueue, currentIndex, l1Txbytes); + + const finalOwnerBalance = await hardhatTokenHermez.balanceOf( + await wallet.getAddress() + ); + + expect(finalOwnerBalance).to.equal( + BigNumber.from(initialOwnerBalance).sub(BigNumber.from(loadAmount)) + ); + } else { + // tokens ERC20Permit + const initialOwnerBalance = await hardhatTokenHermez.balanceOf( + await wallet.getAddress() + ); + + const deadline = ethers.constants.MaxUint256; + const value = loadAmount; + const nonce = await hardhatTokenHermez.nonces(await wallet.getAddress()); + const { v, r, s } = await createPermitSignature( + hardhatTokenHermez, + wallet, + hardhatHermez.address, + value, + nonce, + deadline + ); + + const data = iface.encodeFunctionData("permit", [ + await wallet.getAddress(), + hardhatHermez.address, + value, + deadline, + v, + r, + s + ]); + + // send l1tx wth permit signature + await expect( + hardhatHermez.connect(wallet).addL1Transaction( + babyjub0, + fromIdx, + loadAmountF, + amountF, + tokenID, + toIdx, + data + ) + ) + .to.emit(hardhatHermez, "L1UserTxEvent") + .withArgs(lastQueue, currentIndex, l1Txbytes); + + const finalOwnerBalance = await hardhatTokenHermez.balanceOf( + await wallet.getAddress() + ); + + expect(finalOwnerBalance).to.equal( + BigNumber.from(initialOwnerBalance).sub(BigNumber.from(loadAmount)) + ); + } + } else { + // ether + const initialOwnerBalance = await wallet.getBalance(); + + await expect( + hardhatHermez.connect(wallet).addL1Transaction( + babyjub0, + fromIdx, + loadAmountF, + amountF, + tokenID, + toIdx, + emptyPermit, + { + value: loadAmount, + gasPrice: 0, + } + ) + ) + .to.emit(hardhatHermez, "L1UserTxEvent") + .withArgs(lastQueue, currentIndex, l1Txbytes); + + const finalOwnerBalance = await wallet.getBalance(); + + expect(finalOwnerBalance).to.equal( + BigNumber.from(initialOwnerBalance).sub(BigNumber.from(loadAmount)) + ); + } + + return l1Txbytes; +} + +async function l1UserTxCreateAccountDepositTransfer( + loadAmount, + tokenID, + toIdx, + amountF, + babyjub, + wallet, + hardhatHermez, + hardhatTokenHermez, + isERC20Permit +) { + const loadAmountF = float40.fix2Float(loadAmount); + + // equivalent L1 transaction: + const l1TxCreateAccountDepositTransfer = { + toIdx: toIdx, + tokenID: tokenID, + amountF: amountF, + loadAmountF: loadAmountF, + fromIdx: 0, + fromBjjCompressed: babyjub, + fromEthAddr: await wallet.getAddress(), + }; + + const l1Txbytes = `0x${txUtils.encodeL1TxFull(l1TxCreateAccountDepositTransfer)}`; + + const lastQueue = await hardhatHermez.nextL1FillingQueue(); + + const lastQueueBytes = await hardhatHermez.mapL1TxQueue(lastQueue); + + const currentIndex = (lastQueueBytes.length - 2) / 2 / L1_USER_BYTES; // -2 --> 0x, /2 --> 2 hex digits = 1 byte + + if (tokenID != 0) { + if (!isERC20Permit) { + // tokens ERC20 + const initialOwnerBalance = await hardhatTokenHermez.balanceOf( + await wallet.getAddress() + ); + await expect( + hardhatTokenHermez.connect(wallet).approve(hardhatHermez.address, loadAmount) + ).to.emit(hardhatTokenHermez, "Approval"); + + await expect( + hardhatHermez.connect(wallet).addL1Transaction( + babyjub, + fromIdx0, + loadAmountF, + amountF, + tokenID, + toIdx, + emptyPermit + ) + ) + .to.emit(hardhatHermez, "L1UserTxEvent") + .withArgs(lastQueue, currentIndex, l1Txbytes); + + const finalOwnerBalance = await hardhatTokenHermez.balanceOf( + await wallet.getAddress() + ); + + expect(finalOwnerBalance).to.equal( + BigNumber.from(initialOwnerBalance).sub(BigNumber.from(loadAmount)) + ); + } else { + // tokens ERC20Permit + const initialOwnerBalance = await hardhatTokenHermez.balanceOf( + await wallet.getAddress() + ); + + const deadline = ethers.constants.MaxUint256; + const value = loadAmount; + const nonce = await hardhatTokenHermez.nonces(await wallet.getAddress()); + const { v, r, s } = await createPermitSignature( + hardhatTokenHermez, + wallet, + hardhatHermez.address, + value, + nonce, + deadline + ); + + const data = iface.encodeFunctionData("permit", [ + await wallet.getAddress(), + hardhatHermez.address, + value, + deadline, + v, + r, + s + ]); + + // send l1tx wth permit signature + await expect( + hardhatHermez.connect(wallet).addL1Transaction( + babyjub, + fromIdx0, + loadAmountF, + amountF, + tokenID, + toIdx, + data + ) + ) + .to.emit(hardhatHermez, "L1UserTxEvent") + .withArgs(lastQueue, currentIndex, l1Txbytes); + + const finalOwnerBalance = await hardhatTokenHermez.balanceOf( + await wallet.getAddress() + ); + + expect(finalOwnerBalance).to.equal( + BigNumber.from(initialOwnerBalance).sub(BigNumber.from(loadAmount)) + ); + } + } else { + // ether + const initialOwnerBalance = await wallet.getBalance(); + + await expect( + hardhatHermez.connect(wallet).addL1Transaction( + babyjub, + fromIdx0, + loadAmountF, + amountF, + tokenID, + toIdx, + emptyPermit, + { + value: loadAmount, + gasPrice: 0, + } + ) + ) + .to.emit(hardhatHermez, "L1UserTxEvent") + .withArgs(lastQueue, currentIndex, l1Txbytes); + + const finalOwnerBalance = await wallet.getBalance(); + + expect(finalOwnerBalance).to.equal( + BigNumber.from(initialOwnerBalance).sub(BigNumber.from(loadAmount)) + ); + } + return l1Txbytes; +} + +async function l1UserTxForceTransfer( + tokenID, + fromIdx, + toIdx, + amountF, + wallet, + hardhatHermez +) { + // equivalent L1 transaction: + const l1TxForceTransfer = { + toIdx: toIdx, + tokenID: tokenID, + amountF: amountF, + loadAmountF: 0, + fromIdx: fromIdx, + fromBjjCompressed: 0, + fromEthAddr: await wallet.getAddress(), + }; + + const l1Txbytes = `0x${txUtils.encodeL1TxFull(l1TxForceTransfer)}`; + + const lastQueue = await hardhatHermez.nextL1FillingQueue(); + + const lastQueueBytes = await hardhatHermez.mapL1TxQueue(lastQueue); + + const currentIndex = (lastQueueBytes.length - 2) / 2 / L1_USER_BYTES; // -2 --> 0x, /2 --> 2 hex digits = 1 byte + + await expect( + hardhatHermez.connect(wallet).addL1Transaction( + babyjub0, + fromIdx, + loadAmountF0, + amountF, + tokenID, + toIdx, + emptyPermit, + ) + ) + .to.emit(hardhatHermez, "L1UserTxEvent") + .withArgs(lastQueue, currentIndex, l1Txbytes); + + return l1Txbytes; +} + +async function l1UserTxForceExit( + tokenID, + fromIdx, + amountF, + wallet, + hardhatHermez +) { + const exitIdx = 1; + // equivalent L1 transaction: + const l1TxForceExit = { + toIdx: exitIdx, + tokenID: tokenID, + amountF: amountF, + loadAmountF: 0, + fromIdx: fromIdx, + fromBjjCompressed: 0, + fromEthAddr: await wallet.getAddress(), + }; + const l1Txbytes = `0x${txUtils.encodeL1TxFull(l1TxForceExit)}`; + + const lastQueue = await hardhatHermez.nextL1FillingQueue(); + + const lastQueueBytes = await hardhatHermez.mapL1TxQueue(lastQueue); + + const currentIndex = (lastQueueBytes.length - 2) / 2 / L1_USER_BYTES; // -2 --> 0x, /2 --> 2 hex digits = 1 byte + + await expect( + hardhatHermez.connect(wallet).addL1Transaction( + babyjub0, + fromIdx, + loadAmountF0, + amountF, + tokenID, + exitIdx, + emptyPermit, + ) + ) + .to.emit(hardhatHermez, "L1UserTxEvent") + .withArgs(lastQueue, currentIndex, l1Txbytes); + + return l1Txbytes; +} + +async function l1CoordinatorTxEth(tokenID, babyjub, wallet, hardhatHermez, chainIdHex) { + // equivalent L1 transaction: + + const flatSig = await txUtils.signBjjAuth(wallet, babyjub.slice(2), chainIdHex, hardhatHermez.address); + + let sig = ethers.utils.splitSignature(flatSig); + + const l1TxCoordinator = { + tokenID: tokenID, + fromBjjCompressed: babyjub, + r: sig.r, + s: sig.s, + v: sig.v, + fromEthAddr: await wallet.getAddress(), + }; + + const l1TxCoordinatorbytes = `0x${txUtils.encodeL1CoordinatorTx( + l1TxCoordinator + )}`; + const l1TxBytes = `0x${txUtils.encodeL1TxFull(l1TxCoordinator)}`; + + return { l1TxBytes, l1TxCoordinatorbytes }; +} + +async function l1CoordinatorTxBjj(tokenID, babyjub, hardhatHermez) { + const l1TxCoordinatorCreateBjj = { + tokenID: tokenID, + fromBjjCompressed: babyjub, + r: "0", + s: "0", + v: "0", + fromEthAddr: "0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF", + }; + + const l1TxCoordinatorbytes = `0x${txUtils.encodeL1CoordinatorTx( + l1TxCoordinatorCreateBjj + )}`; + const l1TxBytes = `0x${txUtils.encodeL1TxFull(l1TxCoordinatorCreateBjj)}`; + + return { + l1TxBytes, + l1TxCoordinatorbytes, + }; +} + +async function AddToken( + hardhatHermez, + hardhatToken, + hardhatHEZ, + wallet, + feeAddToken +) { + const addressOwner = await wallet.getAddress(); + + + const deadline = ethers.constants.MaxUint256; + const value = feeAddToken; + const nonce = await hardhatHEZ.nonces(addressOwner); + const { v, r, s } = await createPermitSignature( + hardhatHEZ, + wallet, + hardhatHermez.address, + value, + nonce, + deadline + ); + + const data = iface.encodeFunctionData("permit", [ + await wallet.getAddress(), + hardhatHermez.address, + value, + deadline, + v, + r, + s + ]); + + const initialOwnerBalance = await hardhatHEZ.balanceOf(addressOwner); + + const tokensAdded = await hardhatHermez.registerTokensCount(); + + // Send data and amount + await expect(hardhatHermez.connect(wallet).addToken(hardhatToken.address, data)) + .to.emit(hardhatHermez, "AddToken") + .withArgs(hardhatToken.address, tokensAdded); + + const finalOwnerBalance = await hardhatHEZ.balanceOf(addressOwner); + expect(finalOwnerBalance).to.equal( + BigNumber.from(initialOwnerBalance).sub(feeAddToken) + ); + + return tokensAdded; +} + +async function createAccounts( + forgerTest, + loadAmount, + tokenID, + babyjub, + wallet, + hardhatHermez, + hardhatToken, + numAccounts, + isERC20Permit +) { + const l1TxCreateAccounts = []; + + for (let i = 0; i < numAccounts; i++) { + l1TxCreateAccounts.push( + await l1UserTxCreateAccountDeposit( + loadAmount, + tokenID, + babyjub, + wallet, + hardhatHermez, + hardhatToken, + isERC20Permit + ) + ); + } + // forge empty batch, now the current queue is filled with the L1-User-Tx + await forgerTest.forgeBatch(true, [], []); + // forge the create accounts + await forgerTest.forgeBatch(true, l1TxCreateAccounts, []); +} + +function calculateInputMaxTxLevels(maxTxArray, nLevelsArray) { + let returnArray = []; + for (let i = 0; i < maxTxArray.length; i++) { + returnArray.push( + Scalar.add(Scalar.e(maxTxArray[i]), Scalar.shl(nLevelsArray[i], 256 - 8)) + ); + } + return returnArray; +} + +async function createPermitSignature(hardhatToken, wallet, spenderAddress, value, nonce, deadline) { + const digest = await createPermitDigest( + hardhatToken, + await wallet.getAddress(), + spenderAddress, + value, + nonce, + deadline + ); + + // must be a wallet not a signer! + const ownerPrivateKey = wallet.privateKey; + let signingKey = new ethers.utils.SigningKey(ownerPrivateKey); + + let { + v, + r, + s + } = signingKey.signDigest(digest); + + return { + v, + r, + s, + }; +} + + +/** + * Encode L1 tx data + * @param {Object} bucket - bucket object + * @returns {String} 32 hexadecimal bits of bucket encoded + */ +function packBucket(bucket) { + const ceilUSDB = 96; + const ceilValue = Scalar.band(bucket.ceilUSD, Scalar.fromString("0xFFFFFFFFFFFFFFFFFFFFFFFF", 16)); + + const blockStampB = 32; + const blockStampValue = Scalar.band(bucket.blockStamp, Scalar.fromString("0xFFFFFFFF", 16)); + + const withdrawalsB = 32; + const withdrawalsValue = Scalar.band(bucket.withdrawals, Scalar.fromString("0xFFFFFFFF", 16)); + + const rateBlocksB = 32; + const rateBlocksValue = Scalar.band(bucket.rateBlocks, Scalar.fromString("0xFFFFFFFF", 16)); + + const rateWithdrawalsB = 32; + const rateWithdrawalsValue = Scalar.band(bucket.rateWithdrawals, Scalar.fromString("0xFFFFFFFF", 16)); + + const maxWithdrawalsB = 32; + const maxWithdrawalsValue = Scalar.band(bucket.maxWithdrawals, Scalar.fromString("0xFFFFFFFF", 16)); + + let res = ceilValue; + let shift = ceilUSDB; + + res = Scalar.add(res, Scalar.shl(blockStampValue, shift)); + shift += blockStampB; + + res = Scalar.add(res, Scalar.shl(withdrawalsValue, shift)); + shift += withdrawalsB; + + res = Scalar.add(res, Scalar.shl(rateBlocksValue, shift)); + shift += rateBlocksB; + + res = Scalar.add(res, Scalar.shl(rateWithdrawalsValue, shift)); + shift += rateWithdrawalsB; + + res = Scalar.add(res, Scalar.shl(maxWithdrawalsValue, shift)); + + return utils.padding256(res); +} + +/** +* unpacl Bucket +* @param {String} bucketEncoded - 32 hexadecimal bits of bucket encoded +* @returns {Object} Object representing a Bucket +*/ +function unpackBucket(encodeBucket) { + const bucketScalar = Scalar.fromString(encodeBucket, 16); + + const ceilUSDB = 96; + const blockStampB = 32; + const withdrawalsB = 32; + const rateBlocksB = 32; + const rateWithdrawalsB = 32; + const maxWithdrawalsB = 32; + + let bucket = {}; + let shift = 0; + + bucket.ceilUSD = utils.extract(bucketScalar, shift, ceilUSDB); + shift += ceilUSDB; + + bucket.blockStamp = utils.extract(bucketScalar, shift, blockStampB); + shift += blockStampB; + + bucket.withdrawals = utils.extract(bucketScalar, shift, withdrawalsB); + shift += withdrawalsB; + + bucket.rateBlocks = utils.extract(bucketScalar, shift, rateBlocksB); + shift += rateBlocksB; + + bucket.rateWithdrawals = utils.extract(bucketScalar, shift, rateWithdrawalsB); + shift += rateWithdrawalsB; + + bucket.maxWithdrawals = utils.extract(bucketScalar, shift, maxWithdrawalsB); + + return bucket; +} + + +module.exports = { + ForgerTest, + l1UserTxCreateAccountDeposit, + l1UserTxDeposit, + l1UserTxDepositTransfer, + l1UserTxCreateAccountDepositTransfer, + l1UserTxForceTransfer, + l1UserTxForceExit, + l1CoordinatorTxEth, + l1CoordinatorTxBjj, + AddToken, + createAccounts, + calculateInputMaxTxLevels, + createPermitSignature, + packBucket, + unpackBucket +}; diff --git a/test/upgradability/CompleteUpgrade.test.js b/test/upgradability/CompleteUpgrade.test.js index a83c271..89b72d3 100644 --- a/test/upgradability/CompleteUpgrade.test.js +++ b/test/upgradability/CompleteUpgrade.test.js @@ -206,7 +206,7 @@ describe("upgradability test", function() { it("should be able to upgrade all", async () => { const HermezAuctionProtocolV2 = await ethers.getContractFactory("HermezAuctionProtocolV2"); const Timelock = await ethers.getContractFactory("Timelock"); - const HermezV2 = await ethers.getContractFactory("HermezV2"); + const HermezV2 = await ethers.getContractFactory("HermezV2Mock"); const newHermezAuctionProtocolV2 = HermezAuctionProtocolV2.attach(hermezAuctionProtocol.address); await expect(newHermezAuctionProtocolV2.getVersion()).to.be.reverted; diff --git a/test/upgradability/UpgradeHermez.test.js b/test/upgradability/UpgradeHermez.test.js index 8bd2b89..2890d75 100644 --- a/test/upgradability/UpgradeHermez.test.js +++ b/test/upgradability/UpgradeHermez.test.js @@ -196,7 +196,7 @@ describe("upgradability test Hermez", function() { }); it("should be able to upgrade Hermez", async () => { - const HermezV2 = await ethers.getContractFactory("HermezV2"); + const HermezV2 = await ethers.getContractFactory("HermezV2Mock"); const newHermezV2 = HermezV2.attach(hermez.address); await expect(newHermezV2.getVersion()).to.be.reverted; @@ -226,7 +226,7 @@ describe("upgradability test Hermez", function() { }); it("should be able to upgrade Hermez with prepareUpgrade", async () => { - const HermezV2 = await ethers.getContractFactory("HermezV2"); + const HermezV2 = await ethers.getContractFactory("HermezV2Mock"); const hermezV2 = await upgrades.prepareUpgrade(hermez.address, HermezV2, { unsafeAllowCustomTypes: true @@ -243,7 +243,7 @@ describe("upgradability test Hermez", function() { }); it("should be able to upgrade Hermez with prepareUpgrade after transferProxyAdminOwnership", async () => { - const HermezV2 = await ethers.getContractFactory("HermezV2"); + const HermezV2 = await ethers.getContractFactory("HermezV2Mock"); const hermezV2 = await upgrades.prepareUpgrade(hermez.address, HermezV2, { unsafeAllowCustomTypes: true @@ -264,7 +264,7 @@ describe("upgradability test Hermez", function() { it("should be able to upgrade using Timelock Hermez with prepareUpgrade", async () => { - const HermezV2 = await ethers.getContractFactory("HermezV2"); + const HermezV2 = await ethers.getContractFactory("HermezV2Mock"); const Timelock = await ethers.getContractFactory("Timelock"); const newHermezV2 = HermezV2.attach(hermez.address);