diff --git a/ERCS/erc-7826.md b/ERCS/erc-7826.md new file mode 100644 index 0000000000..a9819779e9 --- /dev/null +++ b/ERCS/erc-7826.md @@ -0,0 +1,553 @@ +--- +eip: 7826 +title: Quantum Supremacy Bounty +description: A smart contract to prove a strict quantum supremacy and indicate requiring quantum-secure verification schemes. +author: Nicholas Papadopoulos (@nikojpapa) +discussions-to: https://ethereum-magicians.org/t/erc-7826-quantum-supremacy-bounty/21866 +status: Draft +type: Standards Track +category: ERC +created: 2023-06-26 +requires: 2470 +--- + +## Abstract + +This proposal introduces a smart contract containing a classically intractable puzzle that is expected to only be able to be solved using quantum computers. +The contract is funded with ETH, which can only be retrieved by solving the problem. +On-chain applications can then watch this contract to be aware of the quantum advantage milestone of solving this puzzle. +For example, Ethereum smart contract wallets can, using custom verification schemes such as those based on [ERC-4337](./erc-4337.md), watch this contract and fall back to a quantum secure signature verification scheme if and when it is solved. + +The contract, then, serves the two purposes of (1) showing proof of a strict quantum supremacy[^1] that is strong enough to +indicate concerns in RSA and ECDSA security, and (2) acting as an indicator to protect Ethereum assets by triggering quantum-secure +signature verification schemes. + +## Motivation + +Quantum supremacy[^1] is a demonstration of a quantum computer solving a problem that would take a classical computer an infeasible amount of time to solve. +Previous attempts have been made to demonstrate quantum supremacy, e.g. Kim[^2], Arute[^3] and Morvan[^4], +but they have been refuted or at least claimed to have no practical benefit, e.g. Begusic and Chan[^5], Pednault[^6], +and a quote from Sebastian Weidt (The Telegraph, "Supercomputer makes calculations in blink of an eye that take rivals 47 years", 2023). +Quantum supremacy, by its current definition, is irrespective of the usefulness of the problem. +This EIP, however, focuses on a stricter definition of a problem that indicates when an adversary may soon or already be able to bypass current Ethereum cryptography standards. +This contract serves as trustless, unbiased proof of this strong quantum supremacy by generating a classically intractable problem on chain, +to which even the creator does not know the solution. + +Since quantum computers are expected[^7] to break current security standards, +Ethereum assets are at risk. However, implementing quantum-secure +protocols can be costly and complicated. +In order to delay unnecessary costs, Ethereum assets can continue using current cryptographic standards and only fall back +to a quantum-secure scheme when there is reasonable risk of security failure due to quantum computers. +Therefore, this contract can serve to protect one's funds on Ethereum by acting as a trigger that activates when +strong quantum supremacy has been achieved by solving the classically intractable puzzle. + +## Specification + +### Parameters + +- In this contract, a "lock" refers to a generated puzzle for which a solution must be provided +in order to withdraw funds and mark the contract as solved. + +| Parameter | Value | +|------------------------------|---------------| +| `BIT_SIZE_OF_PRIMES` | `1024` | +| `COMMIT_REVEAL_TIME_GAP` | `1 day` | +| `ERC_7826_SINGLETON_ADDRESS` | `TBD` | +| `ESTIMATED_GAS_TO_SOLVE` | `600,000,000` | +| `NUMBER_OF_LOCKS` | `119` | + +### Puzzle + +The puzzles that this contract generates are of prime factorization, +where given a positive integer _n_, the objective is to find the set of prime numbers whose product is equal to _n_. + +### Requirements + +- This contract MUST generate each of the `NUMBER_OF_LOCKS` locks by generating an integer of exactly `3 * BIT_SIZE_OF_PRIMES` random bits. +- This contract MUST allow someone to provide the prime factorization of any lock. + If it is the correct solution and solves the last unsolved lock, then this contract MUST send all of its ETH to the solver and mark a publicly readable flag to indicate that this contract has been solved. + +### Deployment method + +- The contract MUST be deployed as a Singleton [ERC-2470](./erc-2470.md)). +- After deploying the contract with parameters of `NUMBER_OF_LOCKS` locks, each probabilistically generating an integer composed + of at least two `BIT_SIZE_OF_PRIMES`-bit primes, the contract's `triggerLockAccumulation()` method SHALL be called repeatedly until `generationIsDone == true`, i.e. all bits have been generated. + +### Providing solutions + +- The solution for each lock SHALL be provided separately. +- Providing solutions MUST follow a commit-reveal scheme to prevent front running. +- This scheme MUST require `COMMIT_REVEAL_TIME_GAP` between commit and reveal. + +### Rewarding the solver + +Upon solving the final solution, + - All funds in the contract MUST be sent to the solver + - The `solved` flag MUST be set to `true` + - Subsequent transactions to commit, reveal, or add funds to the contract MUST be reverted. + +### Bounty funds + +- Funds covering at least `ESTIMATED_GAS_TO_SOLVE` gas SHALL be sent to the contract as a bounty. As a rough estimate for an example, if the current market price is 23.80 Gwei per gas, the contract SHALL have at least 14.28 ETH as a bounty. + The funds must be updated to cover this amount as the value of gas increases in order to make solving the puzzle incentive compatible. +- The contract MUST accept any additional funds from any account as a donation to the bounty. + +## Rationale + +### Puzzle + +Prime factorization has a known, efficient, quantum solution[^8] +but is widely believed to be intractable for classical computers. This, then, reliably serves as a test for strong quantum supremacy since +finding a solution to this problem should only be doable by a quantum computer. + +### Bounty Funds + +The solver SHALL be reimbursed at least the cost of verifying the puzzle solutions. Therefore, to estimate the cost, an estimate +can be calculated by providing the solution of a known factorization. +The expected number of prime factors is on the order of log(log(_n_))[^9], +so the expected number of prime factors of a 3072-bit integer is less than 12. + +Deploying a contract with 119 locks of a provided 3072-bit integer having 16 factors, then providing that solution for +each of them, resulted in a cost of 583,338,223 gas. Providing a solution for a single lock cost 4,959,717 gas. +The majority of the cost comes from verifying that the provided factors are indeed prime with the Miller-Rabin primality test. + +Since the number of factors in this [test](../assets/eip-7826/test/bounty-contracts/prime-factoring-bounty/cost-of-solving-primes.test.ts) is greater than the expected number of factors of any integer, this may serve as an initial estimate of the cost to verify the solutions for randomly generated integers. Therefore, since the total cost is less than +`ESTIMATED_GAS_TO_SOLVE` gas, a bounty covering at least `ESTIMATED_GAS_TO_SOLVE` should be funded to the contract. Note, this minimum viable incentive in terms of ETH is a moving target, as a function of the current Ethereum gas market. To help ensure incentive compatibility of this bounty contract, the bounty funded should be at least many multiples over the current gas prices. + +## Test Cases + +- [Random Bytes Accumulator](../assets/erc-7826/test/bounty-contracts/support/random-bytes-accumulator.test.ts) +- [RSA-UFO Generation](../assets/erc-7826/test/bounty-contracts/prime-factoring-bounty/prime-factoring-bounty-with-rsa-ufo/prime-factoring-bounty-with-rsa-ufo.test.ts) +- [Prime Factoring Bounty](../assets/erc-7826/test/bounty-contracts/prime-factoring-bounty/prime-factoring-bounty-with-predetermined-locks/prime-factoring-bounty-with-predetermined-locks.test.ts) + +## Reference Implementation + +- [Quantum Supremacy Contract](../assets/erc-7826/contracts/bounty-contracts/prime-factoring-bounty/prime-factoring-bounty-with-rsa-ufo/PrimeFactoringBountyWithRsaUfo.sol) + +- Example proof-of-concept [account](../assets/erc-7826/contracts/bounty-fallback-account/BountyFallbackAccount.sol) + having a quantum secure verification scheme after quantum supremacy trigger + +## Security Considerations + +### Bit-length of the integers +Sander[^11] proves that difficult to factor numbers without a known factorization, called RSA-UFOs, can be generated. +A post by Anoncoin calculates that that the probability of generating an RSA-UFO is 0.16, and hence one would need to generate 119 integers to ensure a one in a billion chance of being insecure. +That is, $\log (10^{-9}) / \log (1 - 0.16) \approx 119$. +Therefore, this contract shall generate `NUMBER_OF_LOCKS` integers of `3 * BIT_SIZE_OF_PRIMES` bits each to achieve a one in a billion chance of being insecure. + +Note: Theorem 1 of the Sander[^11] paper states that Let $\xi \in (\frac{1}{3}, \frac{5}{12})$. Then the number of integers $\leq x$ that have two distinct prime factors $\geq x^\xi$ is $x(\frac{1}{2} \ln^2(\frac{1}{2\xi}) + O(\frac{1}{ln(x)}))$. +Since $O(\frac{1}{ln(x)})$ approaches $0$ as $x$ approaches $\infty$, the probability $\frac{1}{2} \ln^2(\frac{1}{2\xi}) \approx 0.082$. +As this differs from Anoncoin's results by only a factor of 2, this EIP chooses the more cost-friendly result of generting 119 locks while being confident that at least one secure lock will be generated. + +#### Predicted security +##### Classical +Burt Kaliski and RSA Laboratories ("TWIRL and RSA Key Size", 2003) recommends 3072-bit key sizes for RSA to be secure beyond 2030. + +##### Quantum +Breaking 256-bit elliptic curve encryption is expected[^12] to require 2,330 qubits, although with current fault-tolerant regime, it is expected[^13] that 13 * 10^6 physical qubits would be required to break this encryption within one day. + +### Front running and censorship + +One day is required before one can reveal a commitment. It is largely infeasible to censor an economically viable transaction for such a period of time. + +Assuming the reveal transaction is willing to pay market rate for transaction fees, the 1559 fee mechanism and its exponential adjustment makes it infeasible for an economic attacker to spam costly transactions to artificially increase the base-fee for an extended period of time. + +Additionally, even if a large percentage of the proposers collude to censor, the inclusion of the reveal transaction on chain will be delayed but only as a function of the ratio of censoring to non-censoring proposers. E.g., if 90% of proposers censor, then the reveal transaction will take 10x as long as expected to be included -- on the order of 120s given mainnet block times. If, instead, 99% of proposers censor, then the transaction will take ~100x as long to be included -- on the order of 1200s. Still in these extreme regimes, reveal times on the order of a day are safe. + +### Choosing the puzzle +The following are other options that were considered as the puzzle to be used along with the reasoning for not using them. + +#### Order-finding + +Order-finding can be defined as follows: given a positive integer _n_ and an integer _a_ coprime to _n_, +find the smallest positive integer _k_ such that _a_ ^ _k_ = 1 (mod _n_). + +Order-finding can be reduced[^10] to factoring, and vice-versa. Therefore, the puzzle must first generate hard-to-factor numbers with high probability as a modulus and then generate random numbers coprime to those moduli. + +To compare costs with the factoring puzzle, we may compare the contracts using these puzzles in two ways: (1) verifying known solutions and (2) deploying. + +To verify submitted solutions, an order-finding contract was [deployed](../assets/erc-7826/test/bounty-contracts/order-finding-bounty/cost-of-solving-order.test.ts) with a lock having a random 3072-bit +modulus and a random 3071-bit base. Cleve[^14] defines the quantum order-finding problem to have an order no greater than twice the bit size of the modulus, +i.e. 768 bytes. Therefore, 768 random solutions of byte size equal to its iteration were sent to the contract. +The maximum gas cost from these iterations was 4,835,737 gas, the minimum was 108,948, the mean was 2,472,370, and the median was 2,478,643. + +[Deploying](../assets/erc-7826/deploy/2_deploy_order_finding_bounty.ts) an order-finding contract with 119 locks at 3,072 bits resulted in a cost of 4,029,364,172 gas, which includes testing that the generated base was neither 1 +nor -1 and was coprime with the modulus. Alternatively, deploying without checking for being coprime could also use a +probabilistic method. Deploying the contract at 119 locks without checking for coprimality resulted in a cost of 242,370,598 gas. However, +since two randomly generated integers have about 0.61 chance of being coprime[^15], +one would need to generate 23 random pairs to have a one in a billion chance of having no coprime pairs. So, this probabilistic method +would also cost a large amount, possibly more, depending on the satisfactory probability. + +[Deploying](../assets/erc-7826/deploy/1_deploy_prime_factoring_bounty.ts) the prime factorization puzzle, on the other hand, resulted in a cost of 150,994,811 gas when generating 119 locks of size 3,072 bits. + +This opens up a debatable question as to which puzzle should be used based on which would be less costly and for which party. Order-finding has a much +higher deployment cost but also has a chance of costing far less to the solver, which would decrease the barrier to entry of +providing solutions to the problem. However, prime factorization likely costs less to deploy and, in the worst case of order-finding, +costs about the same to verify solutions. + +#### Sign a message without the secret key +The solver would need to sign a message, which the contract would verify to have been +correctly signed by the public key. + +Since quantum computers are not currently expected to be able to reverse hash functions, one could not sign a message with only the public address alone. +Hence, the contract could not simply randomly generate a public address with which the solver could sign a message. +Rather, it would need to generate a secret key in order to generate and sign messages that the solver could use to sign their own message. +This opens up trust issues, as the minter of the contract has the capability to see the secret key and therefore could provide the solution without needing a quantum computer. + +#### Factor a product of large, generated primes +Instead of generating an RSA-UFO, the contract could implement current RSA key generation protocols and first generate +two large primes to produces the product of the primes. +This method again has the flaw that the minter has the capability to see the primes, +and therefore some level of trust would need to be given that the minter would throw the values away. + +#### A Cryptographic Test of Quantumness +A paper in 2018[^16] provides a proof of quantumness protocol based on cryptographic methods and randomness. +However, this method requires a trapdoor, or information that is kept secret from the verifier. This cannot be guaranteed in a blockchain context and is therefore unsecure in the same way that factoring a product of large, generated primes is unsecure. + +#### Sampling problems +Harrow and Montanaro[^17] survey sampling problems, which have been proposed as a form of quantum supremacy verification. +The challenge for these is verifying that the given samples were indeed sampled from the desired probability distribution. +This contract must use a problem that is verifiable, and therefore sampling problems will not suffice. + +#### Decentralized trusted setup +This inherently has a trust factor, albeit very small. It requires that at least one person in the party is honest. +A fully trustless setup is preferred. However, further investigation may be done to potentially uncover a valid puzzle that uses a +decentralized setup and has an advantage (perhaps with a lower cost or a greater leading indicator) worth the additional trust. + +#### Verifiable Quantum Advantage without Structure +Yamakawa and Zhandry[^18] analyze a problem that seems promising as an option for this puzzle in which all that needs to be decided are the parameters for a suitable Folded Reed-Solomon code, as described in the paper. +This seems promising as a sooner leading indicator, as it would likely require fewer qubits to solve and therefore likely be solved before the integer factoring puzzle, allowing ETH funds to be protected with lower risk. +Furthermore, it would likely cost far less to deploy and verify solutions since the problem would not need to be generated probabilistically using many large numbers. + +This actually opens up the idea of puzzle advancement or alternatives in general. +There could be many puzzles developed in the future with different security levels or other advantages and tradeoffs. +These would provide a scale of warnings, where there would be a tradeoff for users. +On one end of the scale, the tradeoff would be a shorter risk of theft of their ETH but a sooner implementation of costly verification schemes. +On the other end, the tradeoff would be a longer risk of theft of their ETH but a later implementation of costly verification schemes. + +Hence, this integer factoring puzzle may serve as the latter of the extremes. +If this puzzle is solved, then one may assume that the power of quantum computers has already surpassed the ability to break ECDSA verification schemes. +This allows users to watch this contract as an extreme safeguard in the case that they want to save more ETH with a greater risk of theft by a quantum advantage. + + +## Copyright +Copyright and related rights waived via [CC0](../LICENSE.md). + +[^1]: + ```csl-json + { + "type": "article", + "id": 1, + "author": [{"family": "Preskill", "given": "John"}], + "DOI": "10.48550/arXiv.1203.5813", + "title": "Quantum computing and the entanglement frontier", + "original-date": { + "date-parts": [ + [2012, 11, 10] + ] + }, + "URL": "https://doi.org/10.48550/arXiv.1203.5813" + } + ``` +[^2]: + ```csl-json + { + "type": "article", + "id": 2, + "author": [{"family": "Kim", "given": "Youngseok"}, {"family": "Eddins", "given": "Andrew"}, {"family": "Anand", "given": "Sajant"}, {"family": "Wei", "given": "Ken Xuan"}, {"family": "van den Berg", "given": "Ewout"}, {"family": "Rosenblatt", "given": "Sami"}, {"family": "Nayfeh", "given": "Hasan"}, {"family": "Wu", "given": "Yantao"}, {"family": "Zaletel", "given": "Michael"}, {"family": "Temme", "given": "Kristan"}, {"family": "Kandala", "given": "Abhinav"}], + "DOI": "10.1038/s41586-023-06096-3", + "title": "Evidence for the utility of quantum computing before fault tolerance", + "original-date": { + "date-parts": [ + [2023, 6, 15] + ] + }, + "URL": "https://doi.org/10.1038/s41586-023-06096-3" + } + ``` +[^3]: + ```csl-json + { + "type": "article", + "id": 3, + "author": [{"family": "Arute", "given": "Frank"}, {"family": "Arya", "given": "Kunal"}, {"family": "Babbush", "given": "Ryan"}, {"family": "Bacon", "given": "Dave"}, {"family": "Bardin", "given": "Joseph C."}, {"family": "Barends", "given": "Rami"}, {"family": "Biswas", "given": "Rupak"}, {"family": "Boixo", "given": "Sergio"}, {"family": "Brandao", "given": "Fernando G. S. L."}, {"family": "Buell", "given": "David A."}, {"family": "Burkett", "given": "Brian"}, {"family": "Chen", "given": "Yu"}, {"family": "Chen", "given": "Zijun"}, {"family": "Chiaro", "given": "Ben"}, {"family": "Collins", "given": "Roberto"}, {"family": "Courtney", "given": "William"}, {"family": "Dunsworth", "given": "Andrew"}, {"family": "Farhi", "given": "Edward"}, {"family": "Foxen", "given": "Brooks"}, {"family": "Fowler", "given": "Austin"}, {"family": "Gidney", "given": "Craig"}, {"family": "Giustina", "given": "Marissa"}, {"family": "Graff", "given": "Rob"}, {"family": "Guerin", "given": "Keith"}, {"family": "Habegger", "given": "Steve"}, {"family": "Harrigan", "given": "Matthew P."}, {"family": "Hartmann", "given": "Michael J."}, {"family": "Ho", "given": "Alan"}, {"family": "Hoffmann", "given": "Markus"}, {"family": "Huang", "given": "Trent"}, {"family": "Humble", "given": "Travis S."}, {"family": "Isakov", "given": "Sergei V."}, {"family": "Jeffrey", "given": "Evan"}, {"family": "Jiang", "given": "Zhang"}, {"family": "Kafri", "given": "Dvir"}, {"family": "Kechedzhi", "given": "Kostyantyn"}, {"family": "Kelly", "given": "Julian"}, {"family": "Klimov", "given": "Paul V."}, {"family": "Knysh", "given": "Sergey"}, {"family": "Korotkov", "given": "Alexander"}, {"family": "Kostritsa", "given": "Fedor"}, {"family": "Landhuis", "given": "David"}, {"family": "Lindmark", "given": "Mike"}, {"family": "Lucero", "given": "Erik"}, {"family": "Lyakh", "given": "Dmitry"}, {"family": "Mandr{\\`a}", "given": "Salvatore"}, {"family": "McClean", "given": "Jarrod R."}, {"family": "McEwen", "given": "Matthew"}, {"family": "Megrant", "given": "Anthony"}, {"family": "Mi", "given": "Xiao"}, {"family": "Michielsen", "given": "Kristel"}, {"family": "Mohseni", "given": "Masoud"}, {"family": "Mutus", "given": "Josh"}, {"family": "Naaman", "given": "Ofer"}, {"family": "Neeley", "given": "Matthew"}, {"family": "Neill", "given": "Charles"}, {"family": "Niu", "given": "Murphy Yuezhen"}, {"family": "Ostby", "given": "Eric"}, {"family": "Petukhov", "given": "Andre"}, {"family": "Platt", "given": "John C."}, {"family": "Quintana", "given": "Chris"}, {"family": "Rieffel", "given": "Eleanor G."}, {"family": "Roushan", "given": "Pedram"}, {"family": "Rubin", "given": "Nicholas C."}, {"family": "Sank", "given": "Daniel"}, {"family": "Satzinger", "given": "Kevin J."}, {"family": "Smelyanskiy", "given": "Vadim"}, {"family": "Sung", "given": "Kevin J."}, {"family": "Trevithick", "given": "Matthew D."}, {"family": "Vainsencher", "given": "Amit"}, {"family": "Villalonga", "given": "Benjamin"}, {"family": "White", "given": "Theodore"}, {"family": "Yao", "given": "Z. Jamie"}, {"family": "Yeh", "given": "Ping"}, {"family": "Zalcman", "given": "Adam"}, {"family": "Neven", "given": "Hartmut"}, {"family": "Martinis", "given": "John M."}], + "DOI": "10.1038/s41586-019-1666-5", + "title": "Quantum supremacy using a programmable superconducting processor", + "original-date": { + "date-parts": [ + [2019, 8, 24] + ] + }, + "URL": "https://doi.org/10.1038/s41586-019-1666-5" + } + ``` +[^4]: + ```csl-json + { + "type": "article", + "id": 4, + "author": [{"family": "Morvan", "given": "A."}, {"family": "Villalonga", "given": "B."}, {"family": "Mi", "given": "X."}, {"family": "Mandr\u00e0", "given": "S."}, {"family": "Bengtsson", "given": "A."}, {"family": "V.", "given": "P."}, {"family": "Chen", "given": "Z."}, {"family": "Hong", "given": "S."}, {"family": "Erickson", "given": "C."}, {"family": "K.", "given": "I."}, {"family": "Chau", "given": "J."}, {"family": "Laun", "given": "G."}, {"family": "Movassagh", "given": "R."}, {"family": "Asfaw", "given": "A."}, {"family": "T.", "given": "L."}, {"family": "Peralta", "given": "R."}, {"family": "Abanin", "given": "D."}, {"family": "Acharya", "given": "R."}, {"family": "Allen", "given": "R."}, {"family": "I.", "given": "T."}, {"family": "Anderson", "given": "K."}, {"family": "Ansmann", "given": "M."}, {"family": "Arute", "given": "F."}, {"family": "Arya", "given": "K."}, {"family": "Atalaya", "given": "J."}, {"family": "C.", "given": "J."}, {"family": "Bilmes", "given": "A."}, {"family": "Bortoli", "given": "G."}, {"family": "Bourassa", "given": "A."}, {"family": "Bovaird", "given": "J."}, {"family": "Brill", "given": "L."}, {"family": "Broughton", "given": "M."}, {"family": "B.", "given": "B."}, {"family": "A.", "given": "D."}, {"family": "Burger", "given": "T."}, {"family": "Burkett", "given": "B."}, {"family": "Bushnell", "given": "N."}, {"family": "Campero", "given": "J."}, {"family": "S.", "given": "H."}, {"family": "Chiaro", "given": "B."}, {"family": "Chik", "given": "D."}, {"family": "Chou", "given": "C."}, {"family": "Cogan", "given": "J."}, {"family": "Collins", "given": "R."}, {"family": "Conner", "given": "P."}, {"family": "Courtney", "given": "W."}, {"family": "L.", "given": "A."}, {"family": "Curtin", "given": "B."}, {"family": "M.", "given": "D."}, {"family": "Del", "given": "A."}, {"family": "Demura", "given": "S."}, {"family": "Di", "given": "A."}, {"family": "Dunsworth", "given": "A."}, {"family": "Faoro", "given": "L."}, {"family": "Farhi", "given": "E."}, {"family": "Fatemi", "given": "R."}, {"family": "S.", "given": "V."}, {"family": "Flores", "given": "L."}, {"family": "Forati", "given": "E."}, {"family": "G.", "given": "A."}, {"family": "Foxen", "given": "B."}, {"family": "Garcia", "given": "G."}, {"family": "Genois", "given": "E."}, {"family": "Giang", "given": "W."}, {"family": "Gidney", "given": "C."}, {"family": "Gilboa", "given": "D."}, {"family": "Giustina", "given": "M."}, {"family": "Gosula", "given": "R."}, {"family": "Grajales", "given": "A."}, {"family": "A.", "given": "J."}, {"family": "Habegger", "given": "S."}, {"family": "C.", "given": "M."}, {"family": "Hansen", "given": "M."}, {"family": "P.", "given": "M."}, {"family": "D.", "given": "S."}, {"family": "Heu", "given": "P."}, {"family": "R.", "given": "M."}, {"family": "Huang", "given": "T."}, {"family": "Huff", "given": "A."}, {"family": "J.", "given": "W."}, {"family": "B.", "given": "L."}, {"family": "V.", "given": "S."}, {"family": "Iveland", "given": "J."}, {"family": "Jeffrey", "given": "E."}, {"family": "Jiang", "given": "Z."}, {"family": "Jones", "given": "C."}, {"family": "Juhas", "given": "P."}, {"family": "Kafri", "given": "D."}, {"family": "Khattar", "given": "T."}, {"family": "Khezri", "given": "M."}, {"family": "Kieferov\u00e1", "given": "M."}, {"family": "Kim", "given": "S."}, {"family": "Kitaev", "given": "A."}, {"family": "R.", "given": "A."}, {"family": "N.", "given": "A."}, {"family": "Kostritsa", "given": "F."}, {"family": "M.", "given": "J."}, {"family": "Landhuis", "given": "D."}, {"family": "Laptev", "given": "P."}, {"family": "-M.", "given": "K."}, {"family": "Laws", "given": "L."}, {"family": "Lee", "given": "J."}, {"family": "W.", "given": "K."}, {"family": "D.", "given": "Y."}, {"family": "J.", "given": "B."}, {"family": "T.", "given": "A."}, {"family": "Liu", "given": "W."}, {"family": "Locharla", "given": "A."}, {"family": "D.", "given": "F."}, {"family": "Martin", "given": "O."}, {"family": "Martin", "given": "S."}, {"family": "R.", "given": "J."}, {"family": "McEwen", "given": "M."}, {"family": "C.", "given": "K."}, {"family": "Mieszala", "given": "A."}, {"family": "Montazeri", "given": "S."}, {"family": "Mruczkiewicz", "given": "W."}, {"family": "Naaman", "given": "O."}, {"family": "Neeley", "given": "M."}, {"family": "Neill", "given": "C."}, {"family": "Nersisyan", "given": "A."}, {"family": "Newman", "given": "M."}, {"family": "H.", "given": "J."}, {"family": "Nguyen", "given": "A."}, {"family": "Nguyen", "given": "M."}, {"family": "Yuezhen", "given": "M."}, {"family": "E.", "given": "T."}, {"family": "Omonije", "given": "S."}, {"family": "Opremcak", "given": "A."}, {"family": "Petukhov", "given": "A."}, {"family": "Potter", "given": "R."}, {"family": "P.", "given": "L."}, {"family": "Quintana", "given": "C."}, {"family": "M.", "given": "D."}, {"family": "Rocque", "given": "C."}, {"family": "Roushan", "given": "P."}, {"family": "C.", "given": "N."}, {"family": "Saei", "given": "N."}, {"family": "Sank", "given": "D."}, {"family": "Sankaragomathi", "given": "K."}, {"family": "J.", "given": "K."}, {"family": "F.", "given": "H."}, {"family": "Schuster", "given": "C."}, {"family": "J.", "given": "M."}, {"family": "Shorter", "given": "A."}, {"family": "Shutty", "given": "N."}, {"family": "Shvarts", "given": "V."}, {"family": "Sivak", "given": "V."}, {"family": "Skruzny", "given": "J."}, {"family": "C.", "given": "W."}, {"family": "D.", "given": "R."}, {"family": "Sterling", "given": "G."}, {"family": "Strain", "given": "D."}, {"family": "Szalay", "given": "M."}, {"family": "Thor", "given": "D."}, {"family": "Torres", "given": "A."}, {"family": "Vidal", "given": "G."}, {"family": "Vollgraff", "given": "C."}, {"family": "White", "given": "T."}, {"family": "W.", "given": "B."}, {"family": "Xing", "given": "C."}, {"family": "J.", "given": "Z."}, {"family": "Yeh", "given": "P."}, {"family": "Yoo", "given": "J."}, {"family": "Young", "given": "G."}, {"family": "Zalcman", "given": "A."}, {"family": "Zhang", "given": "Y."}, {"family": "Zhu", "given": "N."}, {"family": "Zobrist", "given": "N."}, {"family": "G.", "given": "E."}, {"family": "Biswas", "given": "R."}, {"family": "Babbush", "given": "R."}, {"family": "Bacon", "given": "D."}, {"family": "Hilton", "given": "J."}, {"family": "Lucero", "given": "E."}, {"family": "Neven", "given": "H."}, {"family": "Megrant", "given": "A."}, {"family": "Kelly", "given": "J."}, {"family": "Aleiner", "given": "I."}, {"family": "Smelyanskiy", "given": "V."}, {"family": "Kechedzhi", "given": "K."}, {"family": "Chen", "given": "Y."}, {"family": "Boixo", "given": "S."}], + "DOI": "10.48550/arXiv.2304.11119", + "title": "Phase transition in Random Circuit Sampling", + "original-date": { + "date-parts": [ + [2023, 4, 21] + ] + }, + "URL": "https://doi.org/10.48550/arXiv.2304.11119" + } + ``` +[^5]: + ```csl-json + { + "type": "article", + "id": 5, + "author": [{"family": "Begušić", "given": "Tomislav"}, {"family": "Kin-Lic Chan", "given": "Garnet"}], + "DOI": "10.48550/arXiv.2306.16372", + "title": "Fast classical simulation of evidence for the utility of quantum computing before fault tolerance", + "original-date": { + "date-parts": [ + [2023, 6, 28] + ] + }, + "URL": "https://doi.org/10.48550/arXiv.2306.16372" + } + ``` +[^6]: + ```csl-json + { + "type": "article", + "id": 6, + "author": [{"family": "Pednault", "given": "Edwin"}, {"family": "Gunnels", "given": "John A."}, {"family": "Nannicini", "given": "Giacomo"}, {"family": "Horesh", "given": "Lior"}, {"family": "Wisnieff", "given": "Robert"}], + "DOI": "10.48550/arXiv.1910.09534", + "title": "Leveraging Secondary Storage to Simulate Deep 54-qubit Sycamore Circuits", + "original-date": { + "date-parts": [ + [2019, 8, 22] + ] + }, + "URL": "https://doi.org/10.48550/arXiv.1910.09534", + "custom": { + "additional-urls": [ + "https://api.semanticscholar.org/CorpusID:204800933" + ] + } + } + ``` +[^7]: + ```csl-json + { + "type": "article", + "id": 7, + "author": [{"family": "Castelvecchi", "given": "Davide"}], + "DOI": "10.1038/d41586-023-00017-0", + "title": "Are quantum computers about to break online privacy?", + "original-date": { + "date-parts": [ + [2023, 1, 6] + ] + }, + "URL": "https://doi.org/10.1038/d41586-023-00017-0" + } + ``` +[^8]: + ```csl-json + { + "type": "article", + "id": 8, + "author": [{"family": "Shor", "given": "Peter W."}], + "DOI": "10.1137/S0097539795293172", + "title": "Polynomial-Time Algorithms for Prime Factorization and Discrete Logarithms on a Quantum Computer", + "original-date": { + "date-parts": [ + [1995, 1, 25] + ] + }, + "URL": "https://doi.org/10.1137/S0097539795293172" + } + ``` +[^9]: + ```csl-json + { + "type": "article", + "id": 9, + "author": [{"family": "Erdös", "given": "P."}, {"family": "Kac", "given": "M."}], + "DOI": "10.2307/2371483", + "title": "The Gaussian Law of Errors in the Theory of Additive Number Theoretic Functions", + "original-date": { + "date-parts": [ + [1940, 4] + ] + }, + "URL": "https://www.semanticscholar.org/paper/The-Gaussian-Law-of-Errors-in-the-Theory-of-Number-Erd%C3%B6s-Kac/261864821aa770542be65dbe16640684ab786fa9", + "custom": { + "additional-urls": [ + "https://doi.org/10.2307/2371483" + ] + } + } + ``` +[^10]: + ```csl-json + { + "type": "article", + "id": 10, + "author": [{"family": "Woll", "given": "Heather"}], + "DOI": "10.1016/0890-5401(87)90030-7", + "title": "Reductions among number theoretic problems", + "original-date": { + "date-parts": [ + [1986, 7, 2] + ] + }, + "URL": "https://doi.org/10.1016/0890-5401(87)90030-7" + } + ``` +[^11]: + ```csl-json + { + "type": "paper-conference", + "id": 11, + "author": [{"family": "Sander", "given": "Tomas"}], + "DOI": "10.1007/978-3-540-47942-0_21", + "title": "Efficient Accumulators without Trapdoor Extended Abstract", + "original-date": { + "date-parts": [ + [1999, 9, 11] + ] + }, + "URL": "https://doi.org/10.1007/978-3-540-47942-0_21" + } + ``` +[^12]: + ```csl-json + { + "type": "article", + "id": 12, + "author": [{"family": "Roetteler", "given": "Martin"}, {"family": "Naehrig", "given": "Michael"}, {"family": "Svore", "given": "Krysta M."}, {"family": "Lauter", "given": "Kristin"}], + "DOI": "10.48550/arXiv.1706.06752", + "title": "Quantum resource estimates for computing elliptic curve discrete logarithms", + "original-date": { + "date-parts": [ + [2017, 8, 31] + ] + }, + "URL": "https://doi.org/10.48550/arXiv.1706.06752" + } + ``` +[^13]: + ```csl-json + { + "type": "article", + "id": 13, + "author": [{"family": "Webber", "given": "Mark"}, {"family": "Elfving", "given": "Vincent"}, {"family": "Weidt", "given": "Sebastian"}, {"family": "Hensinger", "given": "Winfried K."}], + "DOI": "10.1116/5.0073075", + "title": "The impact of hardware specifications on reaching quantum advantage in the fault tolerant regime", + "original-date": { + "date-parts": [ + [2022, 1, 15] + ] + }, + "URL": "https://doi.org/10.1116/5.0073075" + } + ``` +[^14]: + ```csl-json + { + "type": "article", + "id": 14, + "author": [{"family": "Cleve", "given": "Richard"}], + "DOI": "10.1016/j.ic.2004.04.001", + "title": "The query complexity of order-finding", + "original-date": { + "date-parts": [ + [1999, 11, 30] + ] + }, + "URL": "https://doi.org/10.1016/j.ic.2004.04.001", + "custom": { + "additional-urls": [ + "https://doi.org/10.48550/arXiv.quant-ph/9911124" + ] + } + } + ``` +[^15]: + ```csl-json + { + "type": "paper-conference", + "id": 15, + "author": [{"family": "Collins", "given": "George E."}, {"family": "Johnson", "given": "Jeremy R."}], + "DOI": "10.1007/3-540-51084-2_23", + "title": "The probability of relative primality of Gaussian integers", + "original-date": { + "date-parts": [ + [2005, 5, 27] + ] + }, + "URL": "https://doi.org/10.1007/3-540-51084-2_23" + } + ``` +[^16]: + ```csl-json + { + "type": "paper-conference", + "id": 16, + "author": [{"family": "Brakerski", "given": "Zvika"}, {"family": "Christiano", "given": "Paul"}, {"family": "Mahadev", "given": "Urmila"}, {"family": "Vazirani", "given": "Umesh"}, {"family": "Vidick", "given": "Thomas"}], + "DOI": "10.1109/FOCS.2018.00038", + "title": "A Cryptographic Test of Quantumness and Certifiable Randomness from a Single Quantum Device", + "original-date": { + "date-parts": [ + [2018, 7, 9] + ] + }, + "URL": "https://doi.org/10.48550/arXiv.1804.00640", + "custom": { + "additional-urls": [ + "https://doi.org/10.1109/FOCS.2018.00038" + ] + } + } + ``` +[^17]: + ```csl-json + { + "type": "article", + "id": 17, + "author": [{"family": "Harrow", "given": "Aram W."}, {"family": "Montanaro", "given": "Ashley"}], + "DOI": "10.1038/nature23458", + "title": "Quantum computational supremacy", + "original-date": { + "date-parts": [ + [2018, 7, 9] + ] + }, + "URL": "https://doi.org/10.48550/arXiv.1809.07442", + "custom": { + "additional-urls": [ + "https://doi.org/10.1038/nature23458" + ] + } + } + ``` +[^18]: + ```csl-json + { + "type": "paper-conference", + "id": 18, + "author": [{"family": "Yamakawa", "given": "T."}, {"family": "Zhandry", "given": "M."}], + "DOI": "10.1109/FOCS54457.2022.00014", + "title": "Verifiable Quantum Advantage without Structure", + "original-date": { + "date-parts": [ + [2022, 11] + ] + }, + "URL": "https://doi.org/10.48550/arXiv.2204.02063", + "custom": { + "additional-urls": [ + "https://doi.ieeecomputersociety.org/10.1109/FOCS54457.2022.00014" + ] + } + } + ``` diff --git a/assets/erc-7826/contracts/bounty-contracts/BountyContract.sol b/assets/erc-7826/contracts/bounty-contracts/BountyContract.sol new file mode 100644 index 0000000000..5ae992cf45 --- /dev/null +++ b/assets/erc-7826/contracts/bounty-contracts/BountyContract.sol @@ -0,0 +1,74 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.12; + +import "@openzeppelin/contracts/utils/Address.sol"; +import "./support/CommitRevealManager.sol"; +import "./support/LockManager.sol"; + +abstract contract BountyContract { + bool public solved; + + mapping(address => mapping(uint256 => Commit)) private commits; + Locks private locksDefault; + + constructor(uint256 numberOfLocksArg) { + locksDefault = LockManager.init(numberOfLocksArg); + } + + modifier requireUnsolved() { + require(!solved, 'Already solved'); + _; + } + + function locks() internal view virtual returns (Locks storage) { + return locksDefault; + } + + function _verifySolution(uint256 lockNumber, bytes memory solution) internal view virtual returns (bool); + + function getLock(uint256 lockNumber) public view returns (bytes[] memory) { + return LockManager.getLock(locks(), lockNumber); + } + + function numberOfLocks() public view returns (uint256) { + return locks().numberOfLocks; + } + + function commitSolution(uint256 lockNumber, bytes memory solutionHash) public requireUnsolved { + CommitRevealManager.commitSolution(commits, msg.sender, lockNumber, solutionHash); + } + + function getMyCommit(uint256 lockNumber) public view returns (bytes memory, uint256) { + return CommitRevealManager.getMyCommit(commits, msg.sender, lockNumber); + } + + function solve(uint256 lockNumber, bytes memory solution) public requireUnsolved { + require(CommitRevealManager.verifyReveal(commits, msg.sender, lockNumber, solution), "Solution hash doesn't match"); + require(_verifySolution(lockNumber, solution), 'Invalid solution'); + + LockManager.setLocksSolvedStatus(locks(), lockNumber, true); + if (LockManager.allLocksSolved(locks())) { + solved = true; + _sendBountyToSolver(); + } + } + + function _sendBountyToSolver() private { + Address.sendValue(payable(msg.sender), bounty()); + } + + function bounty() public view returns (uint256) { + return address(this).balance; + } + + receive() external payable { + addToBounty(); + } + + fallback() external payable { + addToBounty(); + } + + function addToBounty() public payable requireUnsolved { + } +} diff --git a/assets/erc-7826/contracts/bounty-contracts/order-finding-bounty/OrderFindingBounty.sol b/assets/erc-7826/contracts/bounty-contracts/order-finding-bounty/OrderFindingBounty.sol new file mode 100644 index 0000000000..1718d007ab --- /dev/null +++ b/assets/erc-7826/contracts/bounty-contracts/order-finding-bounty/OrderFindingBounty.sol @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.12; + +import "../BountyContract.sol"; +import "../support/BigNumbers.sol"; + +abstract contract OrderFindingBounty is BountyContract { + using BigNumbers for *; + + constructor(uint256 numberOfLocks) BountyContract(numberOfLocks) {} + + function _verifySolution(uint256 lockNumber, bytes memory solution) internal view override returns (bool) { + bytes[] memory lock = getLock(lockNumber); + require(lock.length > 0, 'Lock has not been generated yet.'); + bytes memory modulus = lock[0]; + bytes memory base = lock[1]; + + BigNumber memory answer = base.init(false).modexp(solution.init(false), modulus.init(false)); + return answer.eq(BigNumbers.one()); + } +} diff --git a/assets/erc-7826/contracts/bounty-contracts/order-finding-bounty/order-finding-bounty-with-lock-generation/OrderFindingAccumulator.sol b/assets/erc-7826/contracts/bounty-contracts/order-finding-bounty/order-finding-bounty-with-lock-generation/OrderFindingAccumulator.sol new file mode 100644 index 0000000000..54677941ec --- /dev/null +++ b/assets/erc-7826/contracts/bounty-contracts/order-finding-bounty/order-finding-bounty-with-lock-generation/OrderFindingAccumulator.sol @@ -0,0 +1,123 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.12; + +import "@openzeppelin/contracts/utils/math/Math.sol"; +import "solidity-bytes-utils/contracts/BytesLib.sol"; +import "../../support/AccumulatorUtils.sol"; +import "../../support/BigNumbers.sol"; +import "../../support/LockManager.sol"; + +struct Accumulator { + Locks locks; + bool generationIsDone; + uint8 parametersPerLock; + + bytes _currentBytes; + uint256 _currentLockNumber; + uint256 _bytesPerLock; + + BigNumber _a; + BigNumber _b; + BigNumber _baseToCheck; + uint256 _gcdIterationsPerCall; +} + + +library OrderFindingAccumulator { + using BigNumbers for *; + + uint8 private constant _BITS_PER_BYTE = 8; + + function init(uint256 numberOfLocks, uint256 bytesPerLock, uint256 gcdIterationsPerCall) internal pure returns (Accumulator memory accumulator) + { + accumulator.locks = LockManager.init(numberOfLocks); + accumulator.parametersPerLock = 2; + accumulator._bytesPerLock = bytesPerLock; + accumulator._gcdIterationsPerCall = gcdIterationsPerCall; + return accumulator; + } + + function accumulate(Accumulator storage accumulator, bytes memory randomBytes) internal { + if (accumulator.generationIsDone) return; + if (accumulator._baseToCheck.bitlen > 0) { + _isCoprime(accumulator); + return; + } + + accumulator._currentBytes = AccumulatorUtils.getRemainingBytes(randomBytes, accumulator._currentBytes, accumulator._bytesPerLock); + if (accumulator._currentBytes.length >= accumulator._bytesPerLock) { + if (accumulator.locks.vals[accumulator._currentLockNumber].length == 0) { + accumulator.locks.vals[accumulator._currentLockNumber] = new bytes[](accumulator.parametersPerLock); + } + + if (accumulator.locks.vals[accumulator._currentLockNumber][0].length == 0) { + _setFirstBit(accumulator._currentBytes); + accumulator.locks.vals[accumulator._currentLockNumber][0] = accumulator._currentBytes; + } else { + BigNumber memory modulus = accumulator.locks.vals[accumulator._currentLockNumber][0].init(false); + BigNumber memory base = accumulator._currentBytes.init(false).mod(modulus); + BigNumber memory negativeOne = modulus.sub(BigNumbers.one()); + + bool hasTrivialOrder = base.eq(BigNumbers.one()) || base.eq(negativeOne); + if (!hasTrivialOrder) { + accumulator._a = modulus; + accumulator._b = accumulator._baseToCheck = base; + return; + } + } + _resetBytes(accumulator); + } + } + + function _setFirstBit(bytes storage value) private { + value[0] |= bytes1(uint8(1 << 7)); + } + + /* Adapted rom https://gist.github.com/3esmit/8c0a63f17f2f2448cc1576eb27fe5910 + */ + function _isCoprime(Accumulator storage accumulator) private { + BigNumber memory a = accumulator._a; + BigNumber memory b = accumulator._b; + for (uint256 i = 0; i < accumulator._gcdIterationsPerCall; i++) { + bool checkIsFinished = b.isZero(); + if (checkIsFinished) { + bool isCoprime = a.eq(BigNumbers.one()); + if (isCoprime) _setBase(accumulator); + _resetBytes(accumulator); + return; + } else { + BigNumber memory temp = b; + b = a.mod(b); + a = temp; + } + } + accumulator._a = a; + accumulator._b = b; + } + + function _setBase(Accumulator storage accumulator) private { + accumulator.locks.vals[accumulator._currentLockNumber][1] = _slicePrefix(accumulator); + ++accumulator._currentLockNumber; + if (accumulator._currentLockNumber >= accumulator.locks.numberOfLocks) accumulator.generationIsDone = true; + } + + function _slicePrefix(Accumulator storage accumulator) private view returns (bytes memory) { + bytes memory value = accumulator._baseToCheck.val; + return BytesLib.slice(value, value.length - accumulator._bytesPerLock, accumulator._bytesPerLock); + } + + function _resetBytes(Accumulator storage accumulator) private { + accumulator._currentBytes = ''; + accumulator._a = BigNumber('', false, 0); + accumulator._b = BigNumber('', false, 0); + accumulator._baseToCheck = BigNumber('', false, 0); + } + + function isCheckingPrime(Accumulator storage accumulator) internal view returns (bool) { + return accumulator._baseToCheck.bitlen > 0; + } + + function currentPrimeCheck(Accumulator storage accumulator) internal view returns (bytes memory) { + return accumulator._b.val; + } +} diff --git a/assets/erc-7826/contracts/bounty-contracts/order-finding-bounty/order-finding-bounty-with-lock-generation/OrderFindingAccumulatorTestHelper.sol b/assets/erc-7826/contracts/bounty-contracts/order-finding-bounty/order-finding-bounty-with-lock-generation/OrderFindingAccumulatorTestHelper.sol new file mode 100644 index 0000000000..53de8d7156 --- /dev/null +++ b/assets/erc-7826/contracts/bounty-contracts/order-finding-bounty/order-finding-bounty-with-lock-generation/OrderFindingAccumulatorTestHelper.sol @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.12; + +import "./OrderFindingAccumulator.sol"; + +contract OrderFindingAccumulatorTestHelper { + Accumulator public accumulator; + + constructor(uint256 numberOfLocks, uint256 bytesPerPrime, uint256 gcdIterationsPerCall) { + accumulator = OrderFindingAccumulator.init(numberOfLocks, bytesPerPrime, gcdIterationsPerCall); + } + + function triggerAccumulate(bytes memory randomBytes) public { + OrderFindingAccumulator.accumulate(accumulator, randomBytes); + } + + function isCheckingPrime() public view returns (bool) { + return OrderFindingAccumulator.isCheckingPrime(accumulator); + } +} diff --git a/assets/erc-7826/contracts/bounty-contracts/order-finding-bounty/order-finding-bounty-with-lock-generation/OrderFindingBountyWithLockGeneration.sol b/assets/erc-7826/contracts/bounty-contracts/order-finding-bounty/order-finding-bounty-with-lock-generation/OrderFindingBountyWithLockGeneration.sol new file mode 100644 index 0000000000..da1278ce59 --- /dev/null +++ b/assets/erc-7826/contracts/bounty-contracts/order-finding-bounty/order-finding-bounty-with-lock-generation/OrderFindingBountyWithLockGeneration.sol @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.12; + +import "./OrderFindingAccumulator.sol"; +import "../OrderFindingBounty.sol"; + + +contract OrderFindingBountyWithLockGeneration is OrderFindingBounty { + uint256 private iteration; + + Accumulator private orderFindingAccumulator; + + constructor(uint256 numberOfLocks, uint256 byteSizeOfModulus, uint256 gcdIterationsPerCall) + OrderFindingBounty(numberOfLocks) + { + orderFindingAccumulator = OrderFindingAccumulator.init(numberOfLocks, byteSizeOfModulus, gcdIterationsPerCall); + } + + function locks() internal view override returns (Locks storage) { + return orderFindingAccumulator.locks; + } + + function isCheckingPrime() public view returns (bool) { + return OrderFindingAccumulator.isCheckingPrime(orderFindingAccumulator); + } + + function currentPrimeCheck() public view returns (bytes memory) { + return OrderFindingAccumulator.currentPrimeCheck(orderFindingAccumulator); + } + + function triggerLockAccumulation() public { + require(!orderFindingAccumulator.generationIsDone, 'Locks have already been generated'); + bytes memory randomNumber = ''; + if (!OrderFindingAccumulator.isCheckingPrime(orderFindingAccumulator)) randomNumber = _generateRandomBytes(); + OrderFindingAccumulator.accumulate(orderFindingAccumulator, randomNumber); + } + + function generationIsDone() public view returns (bool) { + return orderFindingAccumulator.generationIsDone; + } + + function _generateRandomBytes() private returns (bytes memory) { + return abi.encodePacked(keccak256(abi.encodePacked(block.difficulty, iteration++))); + } +} diff --git a/assets/erc-7826/contracts/bounty-contracts/order-finding-bounty/order-finding-bounty-with-predetermined-locks/OrderFindingBountyWithPredeterminedLocks.sol b/assets/erc-7826/contracts/bounty-contracts/order-finding-bounty/order-finding-bounty-with-predetermined-locks/OrderFindingBountyWithPredeterminedLocks.sol new file mode 100644 index 0000000000..d83f83add3 --- /dev/null +++ b/assets/erc-7826/contracts/bounty-contracts/order-finding-bounty/order-finding-bounty-with-predetermined-locks/OrderFindingBountyWithPredeterminedLocks.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.12; + +import "../OrderFindingBounty.sol"; + +contract OrderFindingBountyWithPredeterminedLocks is OrderFindingBounty { + constructor(uint256 numberOfLocks) + OrderFindingBounty(numberOfLocks) {} + + function setLock(uint256 lockNumber, bytes[] memory lock) public { + LockManager.setLock(locks(), lockNumber, lock); + } +} diff --git a/assets/erc-7826/contracts/bounty-contracts/prime-factoring-bounty/PrimeFactoringBounty.sol b/assets/erc-7826/contracts/bounty-contracts/prime-factoring-bounty/PrimeFactoringBounty.sol new file mode 100644 index 0000000000..8048f3f103 --- /dev/null +++ b/assets/erc-7826/contracts/bounty-contracts/prime-factoring-bounty/PrimeFactoringBounty.sol @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.12; + +import "@chainlink/contracts/src/v0.8/VRFConsumerBase.sol"; +import "solidity-bytes-utils/contracts/BytesLib.sol"; + +import "../BountyContract.sol"; +import "../support/BigNumbers.sol"; +import "./miller-rabin/MillerRabin.sol"; + +abstract contract PrimeFactoringBounty is BountyContract { + using BigNumbers for *; + + constructor(uint256 numberOfLocks) BountyContract(numberOfLocks) {} + + function _verifySolution(uint256 lockNumber, bytes memory solution) internal view override returns (bool) { + bytes[] memory primes = abi.decode(solution, (bytes[])); + BigNumber memory product = BigNumbers.one(); + for (uint256 i = 0; i < primes.length; i++) { + bytes memory primeFactor = primes[i]; + require(MillerRabin.isPrime(primeFactor), 'Given solution is not prime'); + product = product.mul(primeFactor.init(false)); + } + + BigNumber memory lock = getLock(lockNumber)[0].init(false); + return product.eq(lock); + } +} diff --git a/assets/erc-7826/contracts/bounty-contracts/prime-factoring-bounty/miller-rabin/MillerRabin.sol b/assets/erc-7826/contracts/bounty-contracts/prime-factoring-bounty/miller-rabin/MillerRabin.sol new file mode 100644 index 0000000000..932ad1a56f --- /dev/null +++ b/assets/erc-7826/contracts/bounty-contracts/prime-factoring-bounty/miller-rabin/MillerRabin.sol @@ -0,0 +1,135 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.12; + +import "../../support/BigNumbers.sol"; + +//From https://github.com/firoorg/solidity-BigNumber/blob/master/src/utils/Crypto.sol +library MillerRabin { + using BigNumbers for *; + + function isPrime(bytes memory primeCandidate) internal view returns (bool){ + BigNumber memory a = primeCandidate.init(false); + + BigNumber memory one = BigNumbers.one(); + BigNumber memory two = BigNumbers.two(); + + int compare = a.cmp(two,true); + if (compare < 0){ + // if value is < 2 + return false; + } + if(compare == 0){ + // if value is 2 + return true; + } + // if a is even and not 2 (checked): return false + if (!a.isOdd()) { + return false; + } + + BigNumber memory a1 = a.sub(one); + + uint k = getK(a1); + BigNumber memory a1_odd = a1.val.init(a1.neg); + a1_odd._shr(k); + + int j; + uint num_checks = primeChecksForSize(a.bitlen); + BigNumber memory check; + for (uint i = 0; i < num_checks; i++) { + + BigNumber memory randomness = randMod(a1, i); + check = randomness.add(one); + // now 1 <= check < a. + + j = witness(check, a, a1, a1_odd, k); + + if(j==-1 || j==1) return false; + } + + //if we've got to here, a is likely a prime. + return true; + } + + function getK( + BigNumber memory a1 + ) private pure returns (uint k){ + k = 0; + uint mask=1; + uint a1_ptr; + uint val; + assembly{ + a1_ptr := add(mload(a1),mload(mload(a1))) // get address of least significant portion of a + val := mload(a1_ptr) //load it + } + + //loop from least signifcant bits until we hit a set bit. increment k until this point. + for(bool bit_set = ((val & mask) != 0); !bit_set; bit_set = ((val & mask) != 0)){ + + if(((k+1) % 256) == 0){ //get next word should k reach 256. + a1_ptr -= 32; + assembly {val := mload(a1_ptr)} + mask = 1; + } + + mask*=2; // set next bit (left shift) + k++; // increment k + } + } + + function primeChecksForSize( + uint bit_size + ) private pure returns(uint checks){ + + checks = bit_size >= 1300 ? 2 : + bit_size >= 850 ? 3 : + bit_size >= 650 ? 4 : + bit_size >= 550 ? 5 : + bit_size >= 450 ? 6 : + bit_size >= 400 ? 7 : + bit_size >= 350 ? 8 : + bit_size >= 300 ? 9 : + bit_size >= 250 ? 12 : + bit_size >= 200 ? 15 : + bit_size >= 150 ? 18 : + /* b >= 100 */ 27; + } + + function randMod(BigNumber memory modulus, uint256 randNonce) private view returns (BigNumber memory) { + // from https://www.geeksforgeeks.org/random-number-generator-in-solidity-using-keccak256/ + uint256 unmodded = uint256(keccak256(abi.encodePacked(block.timestamp, msg.sender, randNonce))); + return unmodded.init(false).mod(modulus); + } + + function witness( + BigNumber memory w, + BigNumber memory a, + BigNumber memory a1, + BigNumber memory a1_odd, + uint k + ) private view returns (int){ + BigNumber memory one = BigNumbers.one(); + BigNumber memory two = BigNumbers.two(); + // returns - 0: likely prime, 1: composite number (definite non-prime). + + w = w.modexp(a1_odd, a); // w := w^a1_odd mod a + + if (w.cmp(one,true)==0) return 0; // probably prime. + + if (w.cmp(a1,true)==0) return 0; // w == -1 (mod a), 'a' is probably prime + + for (;k != 0; k=k-1) { + w = w.modexp(two,a); // w := w^2 mod a + + if (w.cmp(one,true)==0) return 1; // // 'a' is composite, otherwise a previous 'w' would have been == -1 (mod 'a') + + if (w.cmp(a1,true)==0) return 0; // w == -1 (mod a), 'a' is probably prime + + } + /* + * If we get here, 'w' is the (a-1)/2-th power of the original 'w', and + * it is neither -1 nor +1 -- so 'a' cannot be prime + */ + return 1; + } +} diff --git a/assets/erc-7826/contracts/bounty-contracts/prime-factoring-bounty/miller-rabin/MillerRabinTestHelper.sol b/assets/erc-7826/contracts/bounty-contracts/prime-factoring-bounty/miller-rabin/MillerRabinTestHelper.sol new file mode 100644 index 0000000000..ae56839d51 --- /dev/null +++ b/assets/erc-7826/contracts/bounty-contracts/prime-factoring-bounty/miller-rabin/MillerRabinTestHelper.sol @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.12; + +import "./MillerRabin.sol"; + +contract MillerRabinTestHelper { + function isPrime(bytes memory primeCandidate) public view returns (bool) { + return MillerRabin.isPrime(primeCandidate); + } +} diff --git a/assets/erc-7826/contracts/bounty-contracts/prime-factoring-bounty/prime-factoring-bounty-with-lock-generation/PrimeFactoringBountyWithLockGeneration.sol b/assets/erc-7826/contracts/bounty-contracts/prime-factoring-bounty/prime-factoring-bounty-with-lock-generation/PrimeFactoringBountyWithLockGeneration.sol new file mode 100644 index 0000000000..092ee99d6d --- /dev/null +++ b/assets/erc-7826/contracts/bounty-contracts/prime-factoring-bounty/prime-factoring-bounty-with-lock-generation/PrimeFactoringBountyWithLockGeneration.sol @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.12; + +import "@chainlink/contracts/src/v0.8/VRFConsumerBase.sol"; + +import "../../support/BigNumbers.sol"; +import "../miller-rabin/MillerRabin.sol"; +import "../PrimeFactoringBounty.sol"; +import "./RandomPrimeAccumulator.sol"; + +contract PrimeFactoringBountyWithLockGeneration is PrimeFactoringBounty, VRFConsumerBase { + using BigNumbers for *; + + bytes32 internal keyHash; + uint256 internal fee; + + RandomPrimeAccumulator private randomNumberAccumulator; + + constructor(uint256 numberOfLocks, uint256 primesPerLock, uint256 bytesPerPrime) + PrimeFactoringBounty(numberOfLocks) + VRFConsumerBase( + 0xdD3782915140c8f3b190B5D67eAc6dc5760C46E9, // VRF Coordinator + 0xa36085F69e2889c224210F603D836748e7dC0088 // LINK Token + ) + { + keyHash = 0x6c3699283bda56ad74f6b855546325b68d482e983852a7a82979cc4807b641f4; + fee = 0; //0.1 * 10 ** 18; // 0.1 LINK + + randomNumberAccumulator = new RandomPrimeAccumulator(numberOfLocks, primesPerLock, bytesPerPrime); + generateLargePrimes(); + } + + function generateLargePrimes() public returns (bytes32 requestId) { + require(LINK.balanceOf(address(this)) > fee, "Not enough LINK - fill contract with faucet"); + return requestRandomness(keyHash, fee); + } + + function fulfillRandomness(bytes32 requestId, uint256 randomness) internal override { + randomNumberAccumulator.accumulate(randomness); + if (!randomNumberAccumulator.isDone()) generateLargePrimes(); + else { + for (uint256 lockNumber = 0; lockNumber < randomNumberAccumulator.numberOfLocks(); lockNumber++) { + bytes[] memory lock = new bytes[](1); + lock[0] = randomNumberAccumulator.locks(lockNumber); + LockManager.setLock(locks(), lockNumber, lock); + } + } + } +} diff --git a/assets/erc-7826/contracts/bounty-contracts/prime-factoring-bounty/prime-factoring-bounty-with-lock-generation/RandomPrimeAccumulator.sol b/assets/erc-7826/contracts/bounty-contracts/prime-factoring-bounty/prime-factoring-bounty-with-lock-generation/RandomPrimeAccumulator.sol new file mode 100644 index 0000000000..cec6783361 --- /dev/null +++ b/assets/erc-7826/contracts/bounty-contracts/prime-factoring-bounty/prime-factoring-bounty-with-lock-generation/RandomPrimeAccumulator.sol @@ -0,0 +1,89 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.12; + +import "solidity-bytes-utils/contracts/BytesLib.sol"; + +import "../../support/BigNumbers.sol"; +import "../miller-rabin/MillerRabin.sol"; + +contract RandomPrimeAccumulator { + using BigNumbers for *; + + bytes[] public locks; + bool public isDone; + uint256 public numberOfLocks; + + uint256 private bytesPerPrime; + uint256 private primesPerLock; + bytes[] private primeNumbers; + bytes private primeCandidate; + uint256 private primesCounter; + uint256 private lockCounter; + + constructor(uint256 numberOfLocksInit, uint256 primesPerLockInit, uint256 bytesPerPrimeInit) { + numberOfLocks = numberOfLocksInit; + locks = new bytes[](numberOfLocks); + primesPerLock = primesPerLockInit; + bytesPerPrime = bytesPerPrimeInit; + primeNumbers = new bytes[](primesPerLock); + + _resetPrimeCandidate(); + isDone = false; + } + + function accumulate (uint256 randomNumber) public _isNotDone { + if (_primeCandidateIsReset()) randomNumber |= (1 << 255); + primeCandidate = BytesLib.concat(primeCandidate, abi.encodePacked(randomNumber)); + if (primeCandidate.length < bytesPerPrime) return; + primeCandidate = BytesLib.slice(primeCandidate, 0, bytesPerPrime); + +// BigNumber memory madeEven = primeCandidate.init(false).shr(1).shl(1); +// BigNumber memory oddPrimeCandidate = madeEven.add(BigNumbers.one()); +// primeCandidate = oddPrimeCandidate.val; + + if (MillerRabin.isPrime(primeCandidate)) { + for (uint256 i = 0; i < primesCounter; i++) { + bytes memory siblingPrime = primeNumbers[i]; + if (BytesLib.equal(siblingPrime, primeCandidate)) return; + } + + primeNumbers[primesCounter] = primeCandidate; + primesCounter++; + + if (primesCounter == primesPerLock) { + BigNumber memory lockCandidate = BigNumbers.one(); + for (uint256 primeComponentIndex = 0; primeComponentIndex < primeNumbers.length; primeComponentIndex++) { + lockCandidate = lockCandidate.mul(primeNumbers[primeComponentIndex].init(false)); + } + + for (uint256 i = 0; i < lockCounter; i++) { + bytes memory lock = locks[i]; + if (BytesLib.equal(lock, lockCandidate.val)) { + primesCounter--; + return; + } + } + + locks[lockCounter] = lockCandidate.val; + lockCounter++; + primesCounter = 0; + + if (lockCounter == locks.length) isDone = true; + } + } + _resetPrimeCandidate(); + } + + function _resetPrimeCandidate() private { + primeCandidate = ''; + } + + function _primeCandidateIsReset() private view returns (bool) { + return BytesLib.equal(primeCandidate, ''); + } + + modifier _isNotDone() { + require(!isDone, 'Already accumulated enough bits'); + _; + } +} diff --git a/assets/erc-7826/contracts/bounty-contracts/prime-factoring-bounty/prime-factoring-bounty-with-predetermined-locks/PrimeFactoringBountyWithPredeterminedLocks.sol b/assets/erc-7826/contracts/bounty-contracts/prime-factoring-bounty/prime-factoring-bounty-with-predetermined-locks/PrimeFactoringBountyWithPredeterminedLocks.sol new file mode 100644 index 0000000000..96bde27f51 --- /dev/null +++ b/assets/erc-7826/contracts/bounty-contracts/prime-factoring-bounty/prime-factoring-bounty-with-predetermined-locks/PrimeFactoringBountyWithPredeterminedLocks.sol @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.12; + +import "../PrimeFactoringBounty.sol"; + +contract PrimeFactoringBountyWithPredeterminedLocks is PrimeFactoringBounty { + constructor(uint256 numberOfLocks) + PrimeFactoringBounty(numberOfLocks) + {} + + function setLock(uint256 lockNumber, bytes[] memory lock) public { + LockManager.setLock(locks(), lockNumber, lock); + } +} diff --git a/assets/erc-7826/contracts/bounty-contracts/prime-factoring-bounty/prime-factoring-bounty-with-rsa-ufo/PrimeFactoringBountyWithRsaUfo.sol b/assets/erc-7826/contracts/bounty-contracts/prime-factoring-bounty/prime-factoring-bounty-with-rsa-ufo/PrimeFactoringBountyWithRsaUfo.sol new file mode 100644 index 0000000000..e57dbfad3e --- /dev/null +++ b/assets/erc-7826/contracts/bounty-contracts/prime-factoring-bounty/prime-factoring-bounty-with-rsa-ufo/PrimeFactoringBountyWithRsaUfo.sol @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.12; + +import "@openzeppelin/contracts/utils/math/Math.sol"; +import "solidity-bytes-utils/contracts/BytesLib.sol"; + +import "../../support/BigNumbers.sol"; +import "../PrimeFactoringBounty.sol"; +import "../../support/random-bytes-accumulator/RandomBytesAccumulator.sol"; + + +/* Using methods based on: + * - Sander, T. (1999). Efficient Accumulators without Trapdoor Extended Abstract. In: Information and Communication Security, V. Varadharajan and Y. Mu (editors), Second International Conference, ICICS’99, pages 252-262. + * - https://anoncoin.github.io/RSA_UFO/ + * + * The number of locks should be log(1-p) / log(1 - 0.16), where p is the probability that at least one lock + * is difficult to factor. + */ +contract PrimeFactoringBountyWithRsaUfo is PrimeFactoringBounty { + uint256 private iteration; + + Accumulator private randomBytesAccumulator; + + constructor(uint256 numberOfLocks, uint256 bytesPerPrime) + PrimeFactoringBounty(numberOfLocks) + { + randomBytesAccumulator = RandomBytesAccumulator.init(numberOfLocks, 3 * bytesPerPrime); + } + + function locks() internal view override returns (Locks storage) { + return randomBytesAccumulator.locks; + } + + function triggerLockAccumulation() public { + require(!generationIsDone(), 'Locks have already been generated'); + bytes memory randomNumber = abi.encodePacked(keccak256(abi.encodePacked(block.difficulty, iteration++))); + RandomBytesAccumulator.accumulate(randomBytesAccumulator, randomNumber); + } + + function generationIsDone() public view returns (bool) { + return randomBytesAccumulator.generationIsDone; + } +} diff --git a/assets/erc-7826/contracts/bounty-contracts/signature-bounty/SignatureBounty.sol b/assets/erc-7826/contracts/bounty-contracts/signature-bounty/SignatureBounty.sol new file mode 100644 index 0000000000..d3257dceb0 --- /dev/null +++ b/assets/erc-7826/contracts/bounty-contracts/signature-bounty/SignatureBounty.sol @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.12; + +import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; +import "solidity-bytes-utils/contracts/BytesLib.sol"; + +import "../BountyContract.sol"; + +contract SignatureBounty is BountyContract { + using ECDSA for bytes32; + + bytes32 public message; + + constructor(uint256 numberOfLocks) + BountyContract(numberOfLocks) {} + + function _verifySolution(uint256 lockNumber, bytes memory solution) internal view override returns (bool) { + address lock = BytesLib.toAddress(getLock(lockNumber)[0], 0); + address signerAddress = message.toEthSignedMessageHash().recover(solution); + return signerAddress == lock; + } +} diff --git a/assets/erc-7826/contracts/bounty-contracts/signature-bounty/signature-bounty-with-lock-generation/SignatureBountyWithLockGeneration.sol b/assets/erc-7826/contracts/bounty-contracts/signature-bounty/signature-bounty-with-lock-generation/SignatureBountyWithLockGeneration.sol new file mode 100644 index 0000000000..e4b17f2f9d --- /dev/null +++ b/assets/erc-7826/contracts/bounty-contracts/signature-bounty/signature-bounty-with-lock-generation/SignatureBountyWithLockGeneration.sol @@ -0,0 +1,47 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.12; + +import "solidity-bytes-utils/contracts/BytesLib.sol"; + +import "../../support/BigNumbers.sol"; +import "../../support/random-bytes-accumulator/RandomBytesAccumulator.sol"; +import "../SignatureBounty.sol"; + + +contract SignatureBountyWithLockGeneration is SignatureBounty { + uint256 private iteration; + + Accumulator private locksAccumulator; + Accumulator private messageAccumulator; + + uint8 private numberOfMessages = 1; + uint8 private messageByteSize = 32; + uint8 private publicKeyByteSize = 20; + + constructor(uint256 numberOfLocks) + SignatureBounty(numberOfLocks) + { + locksAccumulator = RandomBytesAccumulator.init(numberOfLocks, publicKeyByteSize); + messageAccumulator = RandomBytesAccumulator.init(numberOfMessages, messageByteSize); + } + + function locks() internal view override returns (Locks storage) { + return locksAccumulator.locks; + } + + function triggerLockAccumulation() public { + require(!generationIsDone(), 'Locks have already been generated'); + + bytes memory randomNumber = abi.encodePacked(keccak256(abi.encodePacked(block.difficulty, iteration++))); + if (messageAccumulator.generationIsDone) { + RandomBytesAccumulator.accumulate(locksAccumulator, randomNumber); + } else { + RandomBytesAccumulator.accumulate(messageAccumulator, randomNumber); + if (messageAccumulator.generationIsDone) message = BytesLib.toBytes32(messageAccumulator.locks.vals[0][0], 0); + } + } + + function generationIsDone() public view returns (bool) { + return locksAccumulator.generationIsDone; + } +} diff --git a/assets/erc-7826/contracts/bounty-contracts/signature-bounty/signature-bounty-with-predetermined-locks/SignatureBountyWithPredeterminedLocks.sol b/assets/erc-7826/contracts/bounty-contracts/signature-bounty/signature-bounty-with-predetermined-locks/SignatureBountyWithPredeterminedLocks.sol new file mode 100644 index 0000000000..26b052e3c6 --- /dev/null +++ b/assets/erc-7826/contracts/bounty-contracts/signature-bounty/signature-bounty-with-predetermined-locks/SignatureBountyWithPredeterminedLocks.sol @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.12; + +import "../SignatureBounty.sol"; + +contract SignatureBountyWithPredeterminedLocks is SignatureBounty { + using ECDSA for bytes32; + + constructor(bytes[][] memory publicKeys, bytes32 messageArg) + SignatureBounty(publicKeys.length) + { + message = messageArg; + for (uint256 lockNumber = 0; lockNumber < publicKeys.length; lockNumber++) { + LockManager.setLock(locks(), lockNumber, publicKeys[lockNumber]); + } + } +} diff --git a/assets/erc-7826/contracts/bounty-contracts/support/AccumulatorUtils.sol b/assets/erc-7826/contracts/bounty-contracts/support/AccumulatorUtils.sol new file mode 100644 index 0000000000..c6a7f76ff5 --- /dev/null +++ b/assets/erc-7826/contracts/bounty-contracts/support/AccumulatorUtils.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.12; + +import "@openzeppelin/contracts/utils/math/Math.sol"; +import "solidity-bytes-utils/contracts/BytesLib.sol"; + +library AccumulatorUtils { + function getRemainingBytes(bytes memory randomBytes, bytes memory currentBytes, uint256 bytesPerLock) internal pure returns (bytes memory) { + uint256 numBytesToAccumulate = Math.min(randomBytes.length, bytesPerLock - currentBytes.length); + bytes memory bytesToAccumulate = BytesLib.slice(randomBytes, 0, numBytesToAccumulate); + return BytesLib.concat(currentBytes, bytesToAccumulate); + } +} diff --git a/assets/erc-7826/contracts/bounty-contracts/support/BigNumbers.sol b/assets/erc-7826/contracts/bounty-contracts/support/BigNumbers.sol new file mode 100644 index 0000000000..484beb035b --- /dev/null +++ b/assets/erc-7826/contracts/bounty-contracts/support/BigNumbers.sol @@ -0,0 +1,1309 @@ +//From https://github.com/firoorg/solidity-BigNumber/blob/ca66e95ec3ef32250b0221076f7a10f0d8529bd8/src/BigNumbers.sol + +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.12; + +// Definition here allows both the lib and inheriting contracts to use BigNumber directly. + struct BigNumber { + bytes val; + bool neg; + uint bitlen; + } + +/** + * @notice BigNumbers library for Solidity. + */ +library BigNumbers { + + /// @notice the value for number 0 of a BigNumber instance. + bytes constant ZERO = hex"0000000000000000000000000000000000000000000000000000000000000000"; + /// @notice the value for number 1 of a BigNumber instance. + bytes constant ONE = hex"0000000000000000000000000000000000000000000000000000000000000001"; + /// @notice the value for number 2 of a BigNumber instance. + bytes constant TWO = hex"0000000000000000000000000000000000000000000000000000000000000002"; + + // ***************** BEGIN EXPOSED MANAGEMENT FUNCTIONS ****************** + /** @notice verify a BN instance + * @dev checks if the BN is in the correct format. operations should only be carried out on + * verified BNs, so it is necessary to call this if your function takes an arbitrary BN + * as input. + * + * @param bn BigNumber instance + */ + function verify( + BigNumber memory bn + ) internal pure { + uint msword; + bytes memory val = bn.val; + assembly {msword := mload(add(val,0x20))} //get msword of result + if(msword==0) require(isZero(bn)); + else require((bn.val.length % 32 == 0) && (msword>>((bn.bitlen%256)-1)==1)); + } + + /** @notice initialize a BN instance + * @dev wrapper function for _init. initializes from bytes value. + * Allows passing bitLength of value. This is NOT verified in the internal function. Only use where bitlen is + * explicitly known; otherwise use the other init function. + * + * @param val BN value. may be of any size. + * @param neg neg whether the BN is +/- + * @param bitlen bit length of output. + * @return BigNumber instance + */ + function init( + bytes memory val, + bool neg, + uint bitlen + ) internal view returns(BigNumber memory){ + return _init(val, neg, bitlen); + } + + /** @notice initialize a BN instance + * @dev wrapper function for _init. initializes from bytes value. + * + * @param val BN value. may be of any size. + * @param neg neg whether the BN is +/- + * @return BigNumber instance + */ + function init( + bytes memory val, + bool neg + ) internal view returns(BigNumber memory){ + return _init(val, neg, 0); + } + + /** @notice initialize a BN instance + * @dev wrapper function for _init. initializes from uint value (converts to bytes); + * tf. resulting BN is in the range -2^256-1 ... 2^256-1. + * + * @param val uint value. + * @param neg neg whether the BN is +/- + * @return BigNumber instance + */ + function init( + uint val, + bool neg + ) internal view returns(BigNumber memory){ + return _init(abi.encodePacked(val), neg, 0); + } + // ***************** END EXPOSED MANAGEMENT FUNCTIONS ****************** + + + + + // ***************** BEGIN EXPOSED CORE CALCULATION FUNCTIONS ****************** + /** @notice BigNumber addition: a + b. + * @dev add: Initially prepare BigNumbers for addition operation; internally calls actual addition/subtraction, + * depending on inputs. + * In order to do correct addition or subtraction we have to handle the sign. + * This function discovers the sign of the result based on the inputs, and calls the correct operation. + * + * @param a first BN + * @param b second BN + * @return r result - addition of a and b. + */ + function add( + BigNumber memory a, + BigNumber memory b + ) internal pure returns(BigNumber memory r) { + if(a.bitlen==0 && b.bitlen==0) return zero(); + if(a.bitlen==0) return b; + if(b.bitlen==0) return a; + bytes memory val; + uint bitlen; + int compare = cmp(a,b,false); + + if(a.neg || b.neg){ + if(a.neg && b.neg){ + if(compare>=0) (val, bitlen) = _add(a.val,b.val,a.bitlen); + else (val, bitlen) = _add(b.val,a.val,b.bitlen); + r.neg = true; + } + else { + if(compare==1){ + (val, bitlen) = _sub(a.val,b.val); + r.neg = a.neg; + } + else if(compare==-1){ + (val, bitlen) = _sub(b.val,a.val); + r.neg = !a.neg; + } + else return zero();//one pos and one neg, and same value. + } + } + else{ + if(compare>=0){ // a>=b + (val, bitlen) = _add(a.val,b.val,a.bitlen); + } + else { + (val, bitlen) = _add(b.val,a.val,b.bitlen); + } + r.neg = false; + } + + r.val = val; + r.bitlen = (bitlen); + } + + /** @notice BigNumber subtraction: a - b. + * @dev sub: Initially prepare BigNumbers for subtraction operation; internally calls actual addition/subtraction, + depending on inputs. + * In order to do correct addition or subtraction we have to handle the sign. + * This function discovers the sign of the result based on the inputs, and calls the correct operation. + * + * @param a first BN + * @param b second BN + * @return r result - subtraction of a and b. + */ + function sub( + BigNumber memory a, + BigNumber memory b + ) internal pure returns(BigNumber memory r) { + if(a.bitlen==0 && b.bitlen==0) return zero(); + bytes memory val; + int compare; + uint bitlen; + compare = cmp(a,b,false); + if(a.neg || b.neg) { + if(a.neg && b.neg){ + if(compare == 1) { + (val,bitlen) = _sub(a.val,b.val); + r.neg = true; + } + else if(compare == -1) { + + (val,bitlen) = _sub(b.val,a.val); + r.neg = false; + } + else return zero(); + } + else { + if(compare >= 0) (val,bitlen) = _add(a.val,b.val,a.bitlen); + else (val,bitlen) = _add(b.val,a.val,b.bitlen); + + r.neg = (a.neg) ? true : false; + } + } + else { + if(compare == 1) { + (val,bitlen) = _sub(a.val,b.val); + r.neg = false; + } + else if(compare == -1) { + (val,bitlen) = _sub(b.val,a.val); + r.neg = true; + } + else return zero(); + } + + r.val = val; + r.bitlen = (bitlen); + } + + /** @notice BigNumber multiplication: a * b. + * @dev mul: takes two BigNumbers and multiplys them. Order is irrelevant. + * multiplication achieved using modexp precompile: + * (a * b) = ((a + b)**2 - (a - b)**2) / 4 + * + * @param a first BN + * @param b second BN + * @return r result - multiplication of a and b. + */ + function mul( + BigNumber memory a, + BigNumber memory b + ) internal view returns(BigNumber memory r){ + + BigNumber memory lhs = add(a,b); + BigNumber memory fst = modexp(lhs, two(), _powModulus(lhs, 2)); // (a+b)^2 + + // no need to do subtraction part of the equation if a == b; if so, it has no effect on final result. + if(!eq(a,b)) { + BigNumber memory rhs = sub(a,b); + BigNumber memory snd = modexp(rhs, two(), _powModulus(rhs, 2)); // (a-b)^2 + r = _shr(sub(fst, snd) , 2); // (a * b) = (((a + b)**2 - (a - b)**2) / 4 + } + else { + r = _shr(fst, 2); // a==b ? (((a + b)**2 / 4 + } + } + + /** @notice BigNumber division verification: a * b. + * @dev div: takes three BigNumbers (a,b and result), and verifies that a/b == result. + * Performing BigNumber division on-chain is a significantly expensive operation. As a result, + * we expose the ability to verify the result of a division operation, which is a constant time operation. + * (a/b = result) == (a = b * result) + * Integer division only; therefore: + * verify ((b*result) + (a % (b*result))) == a. + * eg. 17/7 == 2: + * verify (7*2) + (17 % (7*2)) == 17. + * The function returns a bool on successful verification. The require statements will ensure that false can never + * be returned, however inheriting contracts may also want to put this function inside a require statement. + * + * @param a first BigNumber + * @param b second BigNumber + * @param r result BigNumber + * @return bool whether or not the operation was verified + */ + function divVerify( + BigNumber memory a, + BigNumber memory b, + BigNumber memory r + ) internal view returns(bool) { + + // first do zero check. + // if ab. + * + * @param a BigNumber + * @param b BigNumber + * @param signed whether to consider sign of inputs + * @return int result + */ + function cmp( + BigNumber memory a, + BigNumber memory b, + bool signed + ) internal pure returns(int){ + int trigger = 1; + if(signed){ + if(a.neg && b.neg) trigger = -1; + else if(a.neg==false && b.neg==true) return 1; + else if(a.neg==true && b.neg==false) return -1; + } + + if(a.bitlen>b.bitlen) return trigger; // 1*trigger + if(b.bitlen>a.bitlen) return -1*trigger; + + uint a_ptr; + uint b_ptr; + uint a_word; + uint b_word; + + uint len = a.val.length; //bitlen is same so no need to check length. + + assembly{ + a_ptr := add(mload(a),0x20) + b_ptr := add(mload(b),0x20) + } + + for(uint i=0; ib_word) return trigger; // 1*trigger + if(b_word>a_word) return -1*trigger; + + } + + return 0; //same value. + } + + /** @notice BigNumber equality + * @dev eq: returns true if a==b. sign always considered. + * + * @param a BigNumber + * @param b BigNumber + * @return boolean result + */ + function eq( + BigNumber memory a, + BigNumber memory b + ) internal pure returns(bool){ + int result = cmp(a, b, true); + return (result==0) ? true : false; + } + + /** @notice BigNumber greater than + * @dev eq: returns true if a>b. sign always considered. + * + * @param a BigNumber + * @param b BigNumber + * @return boolean result + */ + function gt( + BigNumber memory a, + BigNumber memory b + ) internal pure returns(bool){ + int result = cmp(a, b, true); + return (result==1) ? true : false; + } + + /** @notice BigNumber greater than or equal to + * @dev eq: returns true if a>=b. sign always considered. + * + * @param a BigNumber + * @param b BigNumber + * @return boolean result + */ + function gte( + BigNumber memory a, + BigNumber memory b + ) internal pure returns(bool){ + int result = cmp(a, b, true); + return (result==1 || result==0) ? true : false; + } + + /** @notice BigNumber less than + * @dev eq: returns true if a= the bitlength of the value the result is always 0 + if(bits >= bn.bitlen) return BigNumber(ZERO,false,0); + + // set bitlen initially as we will be potentially modifying 'bits' + bn.bitlen = bn.bitlen-(bits); + + // handle shifts greater than 256: + // if bits is greater than 256 we can simply remove any trailing words, by altering the BN length. + // we also update 'bits' so that it is now in the range 0..256. + assembly { + if or(gt(bits, 0x100), eq(bits, 0x100)) { + length := sub(length, mul(div(bits, 0x100), 0x20)) + mstore(mload(bn), length) + bits := mod(bits, 0x100) + } + + // if bits is multiple of 8 (byte size), we can simply use identity precompile for cheap memcopy. + // otherwise we shift each word, starting at the least signifcant word, one-by-one using the mask technique. + // TODO it is possible to do this without the last two operations, see SHL identity copy. + let bn_val_ptr := mload(bn) + switch eq(mod(bits, 8), 0) + case 1 { + let bytes_shift := div(bits, 8) + let in := mload(bn) + let inlength := mload(in) + let insize := add(inlength, 0x20) + let out := add(in, bytes_shift) + let outsize := sub(insize, bytes_shift) + let success := staticcall(450, 0x4, in, insize, out, insize) + mstore8(add(out, 0x1f), 0) // maintain our BN layout following identity call: + mstore(in, inlength) // set current length byte to 0, and reset old length. + } + default { + let mask + let lsw + let mask_shift := sub(0x100, bits) + let lsw_ptr := add(bn_val_ptr, length) + for { let i := length } eq(eq(i,0),0) { i := sub(i, 0x20) } { // for(int i=max_length; i!=0; i-=32) + switch eq(i,0x20) // if i==32: + case 1 { mask := 0 } // - handles lsword: no mask needed. + default { mask := mload(sub(lsw_ptr,0x20)) } // - else get mask (previous word) + lsw := shr(bits, mload(lsw_ptr)) // right shift current by bits + mask := shl(mask_shift, mask) // left shift next significant word by mask_shift + mstore(lsw_ptr, or(lsw,mask)) // store OR'd mask and shifted bits in-place + lsw_ptr := sub(lsw_ptr, 0x20) // point to next bits. + } + } + + // The following removes the leading word containing all zeroes in the result should it exist, + // as well as updating lengths and pointers as necessary. + let msw_ptr := add(bn_val_ptr,0x20) + switch eq(mload(msw_ptr), 0) + case 1 { + mstore(msw_ptr, sub(mload(bn_val_ptr), 0x20)) // store new length in new position + mstore(bn, msw_ptr) // update pointer from bn + } + default {} + } + + + return bn; + } + + /** @notice left shift BigNumber value + * @dev shr: left shift BigNumber a by 'bits' bits. + ensures the value is not negative before calling the private function. + * @param a BigNumber value to shift + * @param bits amount of bits to shift by + * @return result BigNumber + */ + function shl( + BigNumber memory a, + uint bits + ) internal view returns(BigNumber memory){ + require(!a.neg); + return _shl(a, bits); + } + + /** @notice sha3 hash a BigNumber. + * @dev hash: takes a BigNumber and performs sha3 hash on it. + * we hash each BigNumber WITHOUT it's first word - first word is a pointer to the start of the bytes value, + * and so is different for each struct. + * + * @param a BigNumber + * @return h bytes32 hash. + */ + function hash( + BigNumber memory a + ) internal pure returns(bytes32 h) { + //amount of words to hash = all words of the value and three extra words: neg, bitlen & value length. + assembly { + h := keccak256( add(a,0x20), add (mload(mload(a)), 0x60 ) ) + } + } + + /** @notice BigNumber full zero check + * @dev isZero: checks if the BigNumber is in the default zero format for BNs (ie. the result from zero()). + * + * @param a BigNumber + * @return boolean result. + */ + function isZero( + BigNumber memory a + ) internal pure returns(bool) { + return isZero(a.val) && a.val.length==0x20 && !a.neg && a.bitlen == 0; + } + + + /** @notice bytes zero check + * @dev isZero: checks if input bytes value resolves to zero. + * + * @param a bytes value + * @return boolean result. + */ + function isZero( + bytes memory a + ) internal pure returns(bool) { + uint msword; + uint msword_ptr; + assembly { + msword_ptr := add(a,0x20) + } + for(uint i=0; i 0) return false; + assembly { msword_ptr := add(msword_ptr, 0x20) } + } + return true; + + } + + /** @notice BigNumber value bit length + * @dev bitLength: returns BigNumber value bit length- ie. log2 (most significant bit of value) + * + * @param a BigNumber + * @return uint bit length result. + */ + function bitLength( + BigNumber memory a + ) internal pure returns(uint){ + return bitLength(a.val); + } + + /** @notice bytes bit length + * @dev bitLength: returns bytes bit length- ie. log2 (most significant bit of value) + * + * @param a bytes value + * @return r uint bit length result. + */ + function bitLength( + bytes memory a + ) internal pure returns(uint r){ + if(isZero(a)) return 0; + uint msword; + assembly { + msword := mload(add(a,0x20)) // get msword of input + } + r = bitLength(msword); // get bitlen of msword, add to size of remaining words. + assembly { + r := add(r, mul(sub(mload(a), 0x20) , 8)) // res += (val.length-32)*8; + } + } + + /** @notice uint bit length + @dev bitLength: get the bit length of a uint input - ie. log2 (most significant bit of 256 bit value (one EVM word)) + * credit: Tjaden Hess @ ethereum.stackexchange + * @param a uint value + * @return r uint bit length result. + */ + function bitLength( + uint a + ) internal pure returns (uint r){ + assembly { + switch eq(a, 0) + case 1 { + r := 0 + } + default { + let arg := a + a := sub(a,1) + a := or(a, div(a, 0x02)) + a := or(a, div(a, 0x04)) + a := or(a, div(a, 0x10)) + a := or(a, div(a, 0x100)) + a := or(a, div(a, 0x10000)) + a := or(a, div(a, 0x100000000)) + a := or(a, div(a, 0x10000000000000000)) + a := or(a, div(a, 0x100000000000000000000000000000000)) + a := add(a, 1) + let m := mload(0x40) + mstore(m, 0xf8f9cbfae6cc78fbefe7cdc3a1793dfcf4f0e8bbd8cec470b6a28a7a5a3e1efd) + mstore(add(m,0x20), 0xf5ecf1b3e9debc68e1d9cfabc5997135bfb7a7a3938b7b606b5b4b3f2f1f0ffe) + mstore(add(m,0x40), 0xf6e4ed9ff2d6b458eadcdf97bd91692de2d4da8fd2d0ac50c6ae9a8272523616) + mstore(add(m,0x60), 0xc8c0b887b0a8a4489c948c7f847c6125746c645c544c444038302820181008ff) + mstore(add(m,0x80), 0xf7cae577eec2a03cf3bad76fb589591debb2dd67e0aa9834bea6925f6a4a2e0e) + mstore(add(m,0xa0), 0xe39ed557db96902cd38ed14fad815115c786af479b7e83247363534337271707) + mstore(add(m,0xc0), 0xc976c13bb96e881cb166a933a55e490d9d56952b8d4e801485467d2362422606) + mstore(add(m,0xe0), 0x753a6d1b65325d0c552a4d1345224105391a310b29122104190a110309020100) + mstore(0x40, add(m, 0x100)) + let magic := 0x818283848586878898a8b8c8d8e8f929395969799a9b9d9e9faaeb6bedeeff + let shift := 0x100000000000000000000000000000000000000000000000000000000000000 + let _a := div(mul(a, magic), shift) + r := div(mload(add(m,sub(255,_a))), shift) + r := add(r, mul(256, gt(arg, 0x8000000000000000000000000000000000000000000000000000000000000000))) + // where a is a power of two, result needs to be incremented. we use the power of two trick here: if(arg & arg-1 == 0) ++r; + if eq(and(arg, sub(arg, 1)), 0) { + r := add(r, 1) + } + } + } + } + + /** @notice BigNumber zero value + @dev zero: returns zero encoded as a BigNumber + * @return zero encoded as BigNumber + */ + function zero( + ) internal pure returns(BigNumber memory) { + return BigNumber(ZERO, false, 0); + } + + /** @notice BigNumber one value + @dev one: returns one encoded as a BigNumber + * @return one encoded as BigNumber + */ + function one( + ) internal pure returns(BigNumber memory) { + return BigNumber(ONE, false, 1); + } + + /** @notice BigNumber two value + @dev two: returns two encoded as a BigNumber + * @return two encoded as BigNumber + */ + function two( + ) internal pure returns(BigNumber memory) { + return BigNumber(TWO, false, 2); + } + // ***************** END EXPOSED HELPER FUNCTIONS ****************** + + + + + + // ***************** START PRIVATE MANAGEMENT FUNCTIONS ****************** + /** @notice Create a new BigNumber. + @dev init: overloading allows caller to obtionally pass bitlen where it is known - as it is cheaper to do off-chain and verify on-chain. + * we assert input is in data structure as defined above, and that bitlen, if passed, is correct. + * 'copy' parameter indicates whether or not to copy the contents of val to a new location in memory (for example where you pass + * the contents of another variable's value in) + * @param val bytes - bignum value. + * @param neg bool - sign of value + * @param bitlen uint - bit length of value + * @return r BigNumber initialized value. + */ + function _init( + bytes memory val, + bool neg, + uint bitlen + ) private view returns(BigNumber memory r){ + // use identity at location 0x4 for cheap memcpy. + // grab contents of val, load starting from memory end, update memory end pointer. + assembly { + let data := add(val, 0x20) + let length := mload(val) + let out + let freemem := msize() + switch eq(mod(length, 0x20), 0) // if(val.length % 32 == 0) + case 1 { + out := add(freemem, 0x20) // freememory location + length word + mstore(freemem, length) // set new length + } + default { + let offset := sub(0x20, mod(length, 0x20)) // offset: 32 - (length % 32) + out := add(add(freemem, offset), 0x20) // freememory location + offset + length word + mstore(freemem, add(length, offset)) // set new length + } + pop(staticcall(450, 0x4, data, length, out, length)) // copy into 'out' memory location + mstore(0x40, add(freemem, add(mload(freemem), 0x20))) // update the free memory pointer + + // handle leading zero words. assume freemem is pointer to bytes value + let bn_length := mload(freemem) + for { } eq ( eq(bn_length, 0x20), 0) { } { // for(; length!=32; length-=32) + switch eq(mload(add(freemem, 0x20)),0) // if(msword==0): + case 1 { freemem := add(freemem, 0x20) } // update length pointer + default { break } // else: loop termination. non-zero word found + bn_length := sub(bn_length,0x20) + } + mstore(freemem, bn_length) + + mstore(r, freemem) // store new bytes value in r + mstore(add(r, 0x20), neg) // store neg value in r + } + + r.bitlen = bitlen == 0 ? bitLength(r.val) : bitlen; + } + // ***************** END PRIVATE MANAGEMENT FUNCTIONS ****************** + + + + + + // ***************** START PRIVATE CORE CALCULATION FUNCTIONS ****************** + /** @notice takes two BigNumber memory values and the bitlen of the max value, and adds them. + * @dev _add: This function is private and only callable from add: therefore the values may be of different sizes, + * in any order of size, and of different signs (handled in add). + * As values may be of different sizes, inputs are considered starting from the least significant + * words, working back. + * The function calculates the new bitlen (basically if bitlens are the same for max and min, + * max_bitlen++) and returns a new BigNumber memory value. + * + * @param max bytes - biggest value (determined from add) + * @param min bytes - smallest value (determined from add) + * @param max_bitlen uint - bit length of max value. + * @return bytes result - max + min. + * @return uint - bit length of result. + */ + function _add( + bytes memory max, + bytes memory min, + uint max_bitlen + ) private pure returns (bytes memory, uint) { + bytes memory result; + assembly { + + let result_start := msize() // Get the highest available block of memory + let carry := 0 + let uint_max := sub(0,1) + + let max_ptr := add(max, mload(max)) + let min_ptr := add(min, mload(min)) // point to last word of each byte array. + + let result_ptr := add(add(result_start,0x20), mload(max)) // set result_ptr end. + + for { let i := mload(max) } eq(eq(i,0),0) { i := sub(i, 0x20) } { // for(int i=max_length; i!=0; i-=32) + let max_val := mload(max_ptr) // get next word for 'max' + switch gt(i,sub(mload(max),mload(min))) // if(i>(max_length-min_length)). while + // 'min' words are still available. + case 1{ + let min_val := mload(min_ptr) // get next word for 'min' + mstore(result_ptr, add(add(max_val,min_val),carry)) // result_word = max_word+min_word+carry + switch gt(max_val, sub(uint_max,sub(min_val,carry))) // this switch block finds whether or + // not to set the carry bit for the + // next iteration. + case 1 { carry := 1 } + default { + switch and(eq(max_val,uint_max),or(gt(carry,0), gt(min_val,0))) + case 1 { carry := 1 } + default{ carry := 0 } + } + + min_ptr := sub(min_ptr,0x20) // point to next 'min' word + } + default{ // else: remainder after 'min' words are complete. + mstore(result_ptr, add(max_val,carry)) // result_word = max_word+carry + + switch and( eq(uint_max,max_val), eq(carry,1) ) // this switch block finds whether or + // not to set the carry bit for the + // next iteration. + case 1 { carry := 1 } + default { carry := 0 } + } + result_ptr := sub(result_ptr,0x20) // point to next 'result' word + max_ptr := sub(max_ptr,0x20) // point to next 'max' word + } + + switch eq(carry,0) + case 1{ result_start := add(result_start,0x20) } // if carry is 0, increment result_start, ie. + // length word for result is now one word + // position ahead. + default { mstore(result_ptr, 1) } // else if carry is 1, store 1; overflow has + // occured, so length word remains in the + // same position. + + result := result_start // point 'result' bytes value to the correct + // address in memory. + mstore(result,add(mload(max),mul(0x20,carry))) // store length of result. we are finished + // with the byte array. + + mstore(0x40, add(result,add(mload(result),0x20))) // Update freemem pointer to point to new + // end of memory. + + // we now calculate the result's bit length. + // with addition, if we assume that some a is at least equal to some b, then the resulting bit length will + // be a's bit length or (a's bit length)+1, depending on carry bit.this is cheaper than calling bitLength. + let msword := mload(add(result,0x20)) // get most significant word of result + // if(msword==1 || msword>>(max_bitlen % 256)==1): + if or( eq(msword, 1), eq(shr(mod(max_bitlen,256),msword),1) ) { + max_bitlen := add(max_bitlen, 1) // if msword's bit length is 1 greater + // than max_bitlen, OR overflow occured, + // new bitlen is max_bitlen+1. + } + } + + + return (result, max_bitlen); + } + + /** @notice takes two BigNumber memory values and subtracts them. + * @dev _sub: This function is private and only callable from add: therefore the values may be of different sizes, + * in any order of size, and of different signs (handled in add). + * As values may be of different sizes, inputs are considered starting from the least significant words, + * working back. + * The function calculates the new bitlen (basically if bitlens are the same for max and min, + * max_bitlen++) and returns a new BigNumber memory value. + * + * @param max bytes - biggest value (determined from add) + * @param min bytes - smallest value (determined from add) + * @return bytes result - max + min. + * @return uint - bit length of result. + */ + function _sub( + bytes memory max, + bytes memory min + ) internal pure returns (bytes memory, uint) { + bytes memory result; + uint carry = 0; + uint uint_max = type(uint256).max; + assembly { + + let result_start := msize() // Get the highest available block of memory + let max_len := mload(max) + let min_len := mload(min) // load lengths of inputs + + let len_diff := sub(max_len,min_len) // get differences in lengths. + + let max_ptr := add(max, max_len) + let min_ptr := add(min, min_len) // go to end of arrays + let result_ptr := add(result_start, max_len) // point to least significant result + // word. + let memory_end := add(result_ptr,0x20) // save memory_end to update free memory + // pointer at the end. + + for { let i := max_len } eq(eq(i,0),0) { i := sub(i, 0x20) } { // for(int i=max_length; i!=0; i-=32) + let max_val := mload(max_ptr) // get next word for 'max' + switch gt(i,len_diff) // if(i>(max_length-min_length)). while + // 'min' words are still available. + case 1{ + let min_val := mload(min_ptr) // get next word for 'min' + + mstore(result_ptr, sub(sub(max_val,min_val),carry)) // result_word = (max_word-min_word)-carry + + switch or(lt(max_val, add(min_val,carry)), + and(eq(min_val,uint_max), eq(carry,1))) // this switch block finds whether or + // not to set the carry bit for the next iteration. + case 1 { carry := 1 } + default { carry := 0 } + + min_ptr := sub(min_ptr,0x20) // point to next 'result' word + } + default { // else: remainder after 'min' words are complete. + + mstore(result_ptr, sub(max_val,carry)) // result_word = max_word-carry + + switch and( eq(max_val,0), eq(carry,1) ) // this switch block finds whether or + // not to set the carry bit for the + // next iteration. + case 1 { carry := 1 } + default { carry := 0 } + + } + result_ptr := sub(result_ptr,0x20) // point to next 'result' word + max_ptr := sub(max_ptr,0x20) // point to next 'max' word + } + + //the following code removes any leading words containing all zeroes in the result. + result_ptr := add(result_ptr,0x20) + + // for(result_ptr+=32;; result==0; result_ptr+=32) + for { } eq(mload(result_ptr), 0) { result_ptr := add(result_ptr,0x20) } { + result_start := add(result_start, 0x20) // push up the start pointer for the result + max_len := sub(max_len,0x20) // subtract a word (32 bytes) from the + // result length. + } + + result := result_start // point 'result' bytes value to + // the correct address in memory + + mstore(result,max_len) // store length of result. we + // are finished with the byte array. + + mstore(0x40, memory_end) // Update freemem pointer. + } + + uint new_bitlen = bitLength(result); // calculate the result's + // bit length. + + return (result, new_bitlen); + } + + /** @notice gets the modulus value necessary for calculating exponetiation. + * @dev _powModulus: we must pass the minimum modulus value which would return JUST the a^b part of the calculation + * in modexp. the rationale here is: + * if 'a' has n bits, then a^e has at most n*e bits. + * using this modulus in exponetiation will result in simply a^e. + * therefore the value may be many words long. + * This is done by: + * - storing total modulus byte length + * - storing first word of modulus with correct bit set + * - updating the free memory pointer to come after total length. + * + * @param a BigNumber base + * @param e uint exponent + * @return BigNumber modulus result + */ + function _powModulus( + BigNumber memory a, + uint e + ) private pure returns(BigNumber memory){ + bytes memory _modulus = ZERO; + uint mod_index; + + assembly { + mod_index := mul(mload(add(a, 0x40)), e) // a.bitlen * e is the max bitlength of result + let first_word_modulus := shl(mod(mod_index, 256), 1) // set bit in first modulus word. + mstore(_modulus, mul(add(div(mod_index,256),1),0x20)) // store length of modulus + mstore(add(_modulus,0x20), first_word_modulus) // set first modulus word + mstore(0x40, add(_modulus, add(mload(_modulus),0x20))) // update freemem pointer to be modulus index + // + length + } + + //create modulus BigNumber memory for modexp function + return BigNumber(_modulus, false, mod_index); + } + + /** @notice Modular Exponentiation: Takes bytes values for base, exp, mod and calls precompile for (base^exp)%^mod + * @dev modexp: Wrapper for built-in modexp (contract 0x5) as described here: + * https://github.com/ethereum/EIPs/pull/198 + * + * @param _b bytes base + * @param _e bytes base_inverse + * @param _m bytes exponent + * @param r bytes result. + */ + function _modexp( + bytes memory _b, + bytes memory _e, + bytes memory _m + ) private view returns(bytes memory r) { + assembly { + + let bl := mload(_b) + let el := mload(_e) + let ml := mload(_m) + + + let freemem := mload(0x40) // Free memory pointer is always stored at 0x40 + + + mstore(freemem, bl) // arg[0] = base.length @ +0 + + mstore(add(freemem,32), el) // arg[1] = exp.length @ +32 + + mstore(add(freemem,64), ml) // arg[2] = mod.length @ +64 + + // arg[3] = base.bits @ + 96 + // Use identity built-in (contract 0x4) as a cheap memcpy + let success := staticcall(450, 0x4, add(_b,32), bl, add(freemem,96), bl) + + // arg[4] = exp.bits @ +96+base.length + let size := add(96, bl) + success := staticcall(450, 0x4, add(_e,32), el, add(freemem,size), el) + + // arg[5] = mod.bits @ +96+base.length+exp.length + size := add(size,el) + success := staticcall(450, 0x4, add(_m,32), ml, add(freemem,size), ml) + + switch success case 0 { invalid() } //fail where we haven't enough gas to make the call + + // Total size of input = 96+base.length+exp.length+mod.length + size := add(size,ml) + // Invoke contract 0x5, put return value right after mod.length, @ +96 + success := staticcall(sub(gas(), 1350), 0x5, freemem, size, add(freemem, 0x60), ml) + + switch success case 0 { invalid() } //fail where we haven't enough gas to make the call + + let length := ml + let msword_ptr := add(freemem, 0x60) + + ///the following code removes any leading words containing all zeroes in the result. + for { } eq ( eq(length, 0x20), 0) { } { // for(; length!=32; length-=32) + switch eq(mload(msword_ptr),0) // if(msword==0): + case 1 { msword_ptr := add(msword_ptr, 0x20) } // update length pointer + default { break } // else: loop termination. non-zero word found + length := sub(length,0x20) + } + r := sub(msword_ptr,0x20) + mstore(r, length) + + // point to the location of the return value (length, bits) + //assuming mod length is multiple of 32, return value is already in the right format. + mstore(0x40, add(add(96, freemem),ml)) //deallocate freemem pointer + } + } + // ***************** END PRIVATE CORE CALCULATION FUNCTIONS ****************** + + + + + + // ***************** START PRIVATE HELPER FUNCTIONS ****************** + /** @notice left shift BigNumber memory 'dividend' by 'value' bits. + * @param bn value to shift + * @param bits amount of bits to shift by + * @return r result + */ + function _shl( + BigNumber memory bn, + uint bits + ) private view returns(BigNumber memory r) { + if(bits==0 || bn.bitlen==0) return bn; + + // we start by creating an empty bytes array of the size of the output, based on 'bits'. + // for that we must get the amount of extra words needed for the output. + uint length = bn.val.length; + // position of bitlen in most significnat word + uint bit_position = ((bn.bitlen-1) % 256) + 1; + // total extra words. we check if the bits remainder will add one more word. + uint extra_words = (bits / 256) + ( (bits % 256) >= (256 - bit_position) ? 1 : 0); + // length of output + uint total_length = length + (extra_words * 0x20); + + r.bitlen = bn.bitlen+(bits); + r.neg = bn.neg; + bits %= 256; + + + bytes memory bn_shift; + uint bn_shift_ptr; + // the following efficiently creates an empty byte array of size 'total_length' + assembly { + let freemem_ptr := mload(0x40) // get pointer to free memory + mstore(freemem_ptr, total_length) // store bytes length + let mem_end := add(freemem_ptr, total_length) // end of memory + mstore(mem_end, 0) // store 0 at memory end + bn_shift := freemem_ptr // set pointer to bytes + bn_shift_ptr := add(bn_shift, 0x20) // get bn_shift pointer + mstore(0x40, add(mem_end, 0x20)) // update freemem pointer + } + + // use identity for cheap copy if bits is multiple of 8. + if(bits % 8 == 0) { + // calculate the position of the first byte in the result. + uint bytes_pos = ((256-(((bn.bitlen-1)+bits) % 256))-1) / 8; + uint insize = (bn.bitlen / 8) + ((bn.bitlen % 8 != 0) ? 1 : 0); + assembly { + let in := add(add(mload(bn), 0x20), div(sub(256, bit_position), 8)) + let out := add(bn_shift_ptr, bytes_pos) + let success := staticcall(450, 0x4, in, insize, out, length) + } + r.val = bn_shift; + return r; + } + + + uint mask; + uint mask_shift = 0x100-bits; + uint msw; + uint msw_ptr; + + assembly { + msw_ptr := add(mload(bn), 0x20) + } + + // handle first word before loop if the shift adds any extra words. + // the loop would handle it if the bit shift doesn't wrap into the next word, + // so we check only for that condition. + if((bit_position+bits) > 256){ + assembly { + msw := mload(msw_ptr) + mstore(bn_shift_ptr, shr(mask_shift, msw)) + bn_shift_ptr := add(bn_shift_ptr, 0x20) + } + } + + // as a result of creating the empty array we just have to operate on the words in the original bn. + for(uint i=bn.val.length; i!=0; i-=0x20){ // for each word: + assembly { + msw := mload(msw_ptr) // get most significant word + switch eq(i,0x20) // if i==32: + case 1 { mask := 0 } // handles msword: no mask needed. + default { mask := mload(add(msw_ptr,0x20)) } // else get mask (next word) + msw := shl(bits, msw) // left shift current msw by 'bits' + mask := shr(mask_shift, mask) // right shift next significant word by mask_shift + mstore(bn_shift_ptr, or(msw,mask)) // store OR'd mask and shifted bits in-place + msw_ptr := add(msw_ptr, 0x20) + bn_shift_ptr := add(bn_shift_ptr, 0x20) + } + } + + r.val = bn_shift; + } + // ***************** END PRIVATE HELPER FUNCTIONS ****************** +} diff --git a/assets/erc-7826/contracts/bounty-contracts/support/CommitRevealManager.sol b/assets/erc-7826/contracts/bounty-contracts/support/CommitRevealManager.sol new file mode 100644 index 0000000000..7d0d5e217e --- /dev/null +++ b/assets/erc-7826/contracts/bounty-contracts/support/CommitRevealManager.sol @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.12; + +import "solidity-bytes-utils/contracts/BytesLib.sol"; + +struct Commit { + bytes solutionHash; + uint256 timestamp; +} + +library CommitRevealManager { + uint256 private constant ONE_DAY_IN_SECONDS = 86400; + + function commitSolution( + mapping(address => mapping(uint256 => Commit)) storage commits, + address sender, + uint256 lockNumber, + bytes memory solutionHash + ) internal { + Commit storage commit = commits[sender][lockNumber]; + commit.solutionHash = solutionHash; + commit.timestamp = block.timestamp; + } + + function getMyCommit( + mapping(address => mapping(uint256 => Commit)) storage commits, + address sender, + uint256 lockNumber + ) internal view returns (bytes memory, uint256) { + Commit storage commit = commits[sender][lockNumber]; + _requireCommitExists(commit); + return (commit.solutionHash, commit.timestamp); + } + + function verifyReveal( + mapping(address => mapping(uint256 => Commit)) storage commits, + address sender, + uint256 lockNumber, + bytes memory solution + ) internal view returns (bool) { + Commit storage commit = commits[sender][lockNumber]; + _requireCommitExists(commit); + require(block.timestamp - commit.timestamp >= ONE_DAY_IN_SECONDS, 'Cannot reveal within a day of the commit'); + + bytes memory solutionEncoding = abi.encode(sender, solution); + bytes32 solutionHash = keccak256(solutionEncoding); + return BytesLib.equal(abi.encodePacked(solutionHash), commit.solutionHash); + } + + function _requireCommitExists(Commit memory commit) private pure { + require(!BytesLib.equal(commit.solutionHash, ""), 'Not committed yet'); + } +} diff --git a/assets/erc-7826/contracts/bounty-contracts/support/LockManager.sol b/assets/erc-7826/contracts/bounty-contracts/support/LockManager.sol new file mode 100644 index 0000000000..9dd4aa44bc --- /dev/null +++ b/assets/erc-7826/contracts/bounty-contracts/support/LockManager.sol @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.12; + +struct Locks { + bytes[][] vals; + uint256 numberOfLocks; + bool[] solvedStatus; +} + +library LockManager { + function init(uint256 numberOfLocks) internal pure returns (Locks memory) { + return Locks( + new bytes[][](numberOfLocks), + numberOfLocks, + new bool[](numberOfLocks) + ); + } + + function setLock(Locks storage locks, uint256 lockNumber, bytes[] memory value) internal { + locks.vals[lockNumber] = value; + } + + function getLock(Locks storage locks, uint256 lockNumber) internal view returns (bytes[] memory) { + return locks.vals[lockNumber]; + } + + function setLocksSolvedStatus(Locks storage locks, uint256 lockNumber, bool status) internal { + locks.solvedStatus[lockNumber] = status; + } + + function allLocksSolved(Locks storage locks) internal view returns (bool) { + bool allSolved = true; + for (uint256 lockNumber = 0; lockNumber < locks.solvedStatus.length; lockNumber++) { + if (!locks.solvedStatus[lockNumber]) { + allSolved = false; + break; + } + } + return allSolved; + } +} diff --git a/assets/erc-7826/contracts/bounty-contracts/support/random-bytes-accumulator/RandomBytesAccumulator.sol b/assets/erc-7826/contracts/bounty-contracts/support/random-bytes-accumulator/RandomBytesAccumulator.sol new file mode 100644 index 0000000000..c93ca18d64 --- /dev/null +++ b/assets/erc-7826/contracts/bounty-contracts/support/random-bytes-accumulator/RandomBytesAccumulator.sol @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.12; + +import "../AccumulatorUtils.sol"; +import "../LockManager.sol"; + + +struct Accumulator { + Locks locks; + bool generationIsDone; + + bytes _currentLock; + uint256 _currentLockNumber; + uint256 _bytesPerLock; +} + + +library RandomBytesAccumulator { + + function init(uint256 numberOfLocks, uint256 bytesPerLock) internal pure returns (Accumulator memory accumulator) + { + accumulator.locks = LockManager.init(numberOfLocks); + accumulator._bytesPerLock = bytesPerLock; + return accumulator; + } + + function accumulate(Accumulator storage accumulator, bytes memory randomBytes) internal { + if (accumulator.generationIsDone) return; + + accumulator._currentLock = AccumulatorUtils.getRemainingBytes(randomBytes, accumulator._currentLock, accumulator._bytesPerLock); + if (accumulator._currentLock.length >= accumulator._bytesPerLock) { + accumulator.locks.vals[accumulator._currentLockNumber] = [accumulator._currentLock]; + ++accumulator._currentLockNumber; + accumulator._currentLock = ''; + } + if (accumulator._currentLockNumber >= accumulator.locks.numberOfLocks) accumulator.generationIsDone = true; + } +} diff --git a/assets/erc-7826/contracts/bounty-contracts/support/random-bytes-accumulator/RandomBytesAccumulatorTestHelper.sol b/assets/erc-7826/contracts/bounty-contracts/support/random-bytes-accumulator/RandomBytesAccumulatorTestHelper.sol new file mode 100644 index 0000000000..6b69b49692 --- /dev/null +++ b/assets/erc-7826/contracts/bounty-contracts/support/random-bytes-accumulator/RandomBytesAccumulatorTestHelper.sol @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.12; + +import "../../support/random-bytes-accumulator/RandomBytesAccumulator.sol"; + +contract RandomBytesAccumulatorTestHelper { + Accumulator public accumulator; + + constructor(uint256 numberOfLocks, uint256 bytesPerPrime) { + accumulator = RandomBytesAccumulator.init(numberOfLocks, bytesPerPrime); + } + + function triggerAccumulate(bytes memory randomBytes) public { + RandomBytesAccumulator.accumulate(accumulator, randomBytes); + } +} diff --git a/assets/erc-7826/contracts/bounty-fallback-account/BountyFallbackAccount.sol b/assets/erc-7826/contracts/bounty-fallback-account/BountyFallbackAccount.sol new file mode 100644 index 0000000000..7f565fe12d --- /dev/null +++ b/assets/erc-7826/contracts/bounty-fallback-account/BountyFallbackAccount.sol @@ -0,0 +1,95 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.12; + +import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; +import "solidity-bytes-utils/contracts/BytesLib.sol"; + +import "../samples/SimpleAccount.sol"; +import "../bounty-contracts/BountyContract.sol"; + +contract BountyFallbackAccount is SimpleAccount { + using ECDSA for bytes32; + + BountyContract private bountyContract; + bytes[][] private lamportKey; + uint256 private numberOfTests; + uint256 private testSizeInBytes; + uint16 private ecdsaLength; + + constructor(IEntryPoint anEntryPoint) SimpleAccount(anEntryPoint) { + } + + function initialize(address anOwner, bytes[][] memory publicKey, address payable bountyContractAddress) public initializer { + _initialize(anOwner, publicKey, bountyContractAddress); + } + + function _initialize(address anOwner, bytes[][] memory publicKey, address payable bountyContractAddress) internal { + bountyContract = BountyContract(bountyContractAddress); + + lamportKey = publicKey; + numberOfTests = publicKey[0].length; + testSizeInBytes = publicKey[0][0].length; + + ecdsaLength = 65; + + SimpleAccount.initialize(anOwner); + } + + function _validateSignature(UserOperation calldata userOp, bytes32 userOpHash) + internal override returns (uint256 validationData) { + bytes32 userOpHashEthSigned = userOpHash.toEthSignedMessageHash(); + if (!_ecdsaSignaturePasses(userOp.signature, userOpHashEthSigned)) + return SIG_VALIDATION_FAILED; + if (bountyContract.solved() && !_lamportSignaturePasses(userOp.signature, userOpHashEthSigned)) + return SIG_VALIDATION_FAILED; + _updateLamportKeys(userOp.signature); + return 0; + } + + function _ecdsaSignaturePasses(bytes memory signature, bytes32 userOpHashEthSigned) private view returns (bool) { + bytes memory ecdsaSignature = BytesLib.slice(signature, 0, ecdsaLength); + return owner == userOpHashEthSigned.recover(ecdsaSignature); + } + + function _lamportSignaturePasses(bytes memory signature, bytes32 userOpHashEthSigned) private view returns (bool) { + bytes[] memory hashedSignatureBytes = _getHashedSignatureBytes(signature); + return _hashedSignatureMatchesPublicKey(hashedSignatureBytes, userOpHashEthSigned); + } + + function _getHashedSignatureBytes(bytes memory signature) private view returns (bytes[] memory) { + bytes[] memory hashedSignatureBytes = new bytes[](numberOfTests); + for (uint256 testNumber = 0; testNumber < numberOfTests; testNumber++) { + bytes memory signatureByte = BytesLib.slice(signature, ecdsaLength + testSizeInBytes * testNumber, testSizeInBytes); + bytes32 valueToTest = keccak256(signatureByte); + hashedSignatureBytes[testNumber] = BytesLib.slice(_bytes32ToBytes(valueToTest), 0, testSizeInBytes); + } + return hashedSignatureBytes; + } + + function _bytes32ToBytes(bytes32 bytesFrom) private pure returns (bytes memory) { + return abi.encodePacked(bytesFrom); + } + + function _hashedSignatureMatchesPublicKey(bytes[] memory hashedSignatureBytes, bytes32 userOpHashEthSigned) private view returns (bool) { + uint256 hashInt = uint256(userOpHashEthSigned); + for (uint256 testNumber = 0; testNumber < numberOfTests; testNumber++) { + uint256 bit = (hashInt >> testNumber) & 1; + bytes memory hashedSignatureByte = hashedSignatureBytes[testNumber]; + if (!BytesLib.equal(lamportKey[bit][testNumber], hashedSignatureByte)) + return false; + } + return true; + } + + function _updateLamportKeys(bytes memory signature) private { + uint256 sizeOfLamportKey = testSizeInBytes * testSizeInBytes; + uint256 startOfNewLamport = ecdsaLength + sizeOfLamportKey; + for (uint256 lamportKeyNumber = 0; lamportKeyNumber < lamportKey.length; lamportKeyNumber++) { + uint256 startOfKey = startOfNewLamport + sizeOfLamportKey * lamportKeyNumber; + for (uint256 testNumber = 0; testNumber < numberOfTests; testNumber++) { + bytes memory signatureByte = BytesLib.slice(signature, startOfKey + testSizeInBytes * testNumber, testSizeInBytes); + lamportKey[lamportKeyNumber][testNumber] = signatureByte; + } + } + } +} diff --git a/assets/erc-7826/contracts/bounty-fallback-account/BountyFallbackAccountFactory.sol b/assets/erc-7826/contracts/bounty-fallback-account/BountyFallbackAccountFactory.sol new file mode 100644 index 0000000000..fc9c4b2ac7 --- /dev/null +++ b/assets/erc-7826/contracts/bounty-fallback-account/BountyFallbackAccountFactory.sol @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.12; + +import "@openzeppelin/contracts/utils/Create2.sol"; +import "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; + +import "./BountyFallbackAccount.sol"; + +contract BountyFallbackAccountFactory { + BountyFallbackAccount public immutable accountImplementation; + + constructor(IEntryPoint _entryPoint) { + accountImplementation = new BountyFallbackAccount(_entryPoint); + } + + function createAccount(address owner, uint256 salt, bytes[][] memory lamportKey, address payable bountyContractAddress) public returns (BountyFallbackAccount ret) { + address addr = getAddress(owner, salt, lamportKey, bountyContractAddress); + uint codeSize = addr.code.length; + if (codeSize > 0) { + return BountyFallbackAccount(payable(addr)); + } + ret = BountyFallbackAccount(payable(new ERC1967Proxy{salt : bytes32(salt)}( + address(accountImplementation), + abi.encodeCall(BountyFallbackAccount.initialize, (owner, lamportKey, bountyContractAddress)) + ))); + } + + function getAddress(address owner, uint256 salt, bytes[][] memory lamportKey, address payable bountyContractAddress) public view returns (address) { + return Create2.computeAddress(bytes32(salt), keccak256(abi.encodePacked( + type(ERC1967Proxy).creationCode, + abi.encode( + address(accountImplementation), + abi.encodeCall(BountyFallbackAccount.initialize, (owner, lamportKey, bountyContractAddress)) + ) + ))); + } +} diff --git a/assets/erc-7826/contracts/samples/SimpleAccount.sol b/assets/erc-7826/contracts/samples/SimpleAccount.sol new file mode 100644 index 0000000000..65fe9e68f2 --- /dev/null +++ b/assets/erc-7826/contracts/samples/SimpleAccount.sol @@ -0,0 +1,153 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.12; + +/* solhint-disable avoid-low-level-calls */ +/* solhint-disable no-inline-assembly */ +/* solhint-disable reason-string */ + +import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; +import "@openzeppelin/contracts/proxy/utils/Initializable.sol"; +import "@openzeppelin/contracts/proxy/utils/UUPSUpgradeable.sol"; + +import "../core/BaseAccount.sol"; + +/** + * minimal account. + * this is sample minimal account. + * has execute, eth handling methods + * has a single signer that can send requests through the entryPoint. + */ +contract SimpleAccount is BaseAccount, UUPSUpgradeable, Initializable { + using ECDSA for bytes32; + + //filler member, to push the nonce and owner to the same slot + // the "Initializeble" class takes 2 bytes in the first slot + bytes28 private _filler; + + //explicit sizes of nonce, to fit a single storage cell with "owner" + uint96 private _nonce; + address public owner; + + IEntryPoint private immutable _entryPoint; + + event SimpleAccountInitialized(IEntryPoint indexed entryPoint, address indexed owner); + + modifier onlyOwner() { + _onlyOwner(); + _; + } + + /// @inheritdoc BaseAccount + function nonce() public view virtual override returns (uint256) { + return _nonce; + } + + /// @inheritdoc BaseAccount + function entryPoint() public view virtual override returns (IEntryPoint) { + return _entryPoint; + } + + + // solhint-disable-next-line no-empty-blocks + receive() external payable {} + + constructor(IEntryPoint anEntryPoint) { + _entryPoint = anEntryPoint; + _disableInitializers(); + } + + function _onlyOwner() internal view { + //directly from EOA owner, or through the account itself (which gets redirected through execute()) + require(msg.sender == owner || msg.sender == address(this), "only owner"); + } + + /** + * execute a transaction (called directly from owner, or by entryPoint) + */ + function execute(address dest, uint256 value, bytes calldata func) external { + _requireFromEntryPointOrOwner(); + _call(dest, value, func); + } + + /** + * execute a sequence of transactions + */ + function executeBatch(address[] calldata dest, bytes[] calldata func) external { + _requireFromEntryPointOrOwner(); + require(dest.length == func.length, "wrong array lengths"); + for (uint256 i = 0; i < dest.length; i++) { + _call(dest[i], 0, func[i]); + } + } + + /** + * @dev The _entryPoint member is immutable, to reduce gas consumption. To upgrade EntryPoint, + * a new implementation of SimpleAccount must be deployed with the new EntryPoint address, then upgrading + * the implementation by calling `upgradeTo()` + */ + function initialize(address anOwner) public virtual initializer { + _initialize(anOwner); + } + + function _initialize(address anOwner) internal virtual { + owner = anOwner; + emit SimpleAccountInitialized(_entryPoint, owner); + } + + // Require the function call went through EntryPoint or owner + function _requireFromEntryPointOrOwner() internal view { + require(msg.sender == address(entryPoint()) || msg.sender == owner, "account: not Owner or EntryPoint"); + } + + /// implement template method of BaseAccount + function _validateAndUpdateNonce(UserOperation calldata userOp) internal override { + require(_nonce++ == userOp.nonce, "account: invalid nonce"); + } + + /// implement template method of BaseAccount + function _validateSignature(UserOperation calldata userOp, bytes32 userOpHash) + internal override virtual returns (uint256 validationData) { + bytes32 hash = userOpHash.toEthSignedMessageHash(); + if (owner != hash.recover(userOp.signature)) + return SIG_VALIDATION_FAILED; + return 0; + } + + function _call(address target, uint256 value, bytes memory data) internal { + (bool success, bytes memory result) = target.call{value : value}(data); + if (!success) { + assembly { + revert(add(result, 32), mload(result)) + } + } + } + + /** + * check current account deposit in the entryPoint + */ + function getDeposit() public view returns (uint256) { + return entryPoint().balanceOf(address(this)); + } + + /** + * deposit more funds for this account in the entryPoint + */ + function addDeposit() public payable { + entryPoint().depositTo{value : msg.value}(address(this)); + } + + /** + * withdraw value from the account's deposit + * @param withdrawAddress target to send to + * @param amount to withdraw + */ + function withdrawDepositTo(address payable withdrawAddress, uint256 amount) public onlyOwner { + entryPoint().withdrawTo(withdrawAddress, amount); + } + + function _authorizeUpgrade(address newImplementation) internal view override { + (newImplementation); + _onlyOwner(); + } +} + diff --git a/assets/erc-7826/deploy/1_deploy_prime_factoring_bounty.ts b/assets/erc-7826/deploy/1_deploy_prime_factoring_bounty.ts new file mode 100644 index 0000000000..946be3dec8 --- /dev/null +++ b/assets/erc-7826/deploy/1_deploy_prime_factoring_bounty.ts @@ -0,0 +1,39 @@ +import { HardhatRuntimeEnvironment } from 'hardhat/types' +import { DeployFunction } from 'hardhat-deploy/types' +import { Create2Factory } from '../src/Create2Factory' +import { ethers } from 'hardhat' +import { BigNumber } from 'ethers' + +const MAX_GAS_LIMIT_OPTION = { gasLimit: BigNumber.from('0x1c9c380') } + +const deployPrimeFactoringBounty: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { + const provider = ethers.provider + const from = await provider.getSigner().getAddress() + await new Create2Factory(ethers.provider).deployFactory() + + const numberOfLocks = 119 + const primeByteLength = 128 + let gasUsed = BigNumber.from(0) + + const deployResult = await hre.deployments.deploy( + 'PrimeFactoringBountyWithRsaUfo', { + ...MAX_GAS_LIMIT_OPTION, + from, + args: [numberOfLocks, primeByteLength], + gasLimit: 6e6, + deterministicDeployment: true + }) + console.log('==PrimeFactoringBounty addr=', deployResult.address) + gasUsed = gasUsed.add(deployResult.receipt?.gasUsed) + + const bounty = await ethers.getContractAt('PrimeFactoringBountyWithRsaUfo', deployResult.address) + while (!(await bounty.generationIsDone())) { + const tx = await bounty.triggerLockAccumulation() + const receipt = await tx.wait() + gasUsed = gasUsed.add(receipt.gasUsed) + } + console.log('==PrimeFactoringBounty gasUsed=', gasUsed.toHexString()) +} + +module.exports = deployPrimeFactoringBounty +module.exports.tags = ['PrimeFactoringBounty'] diff --git a/assets/erc-7826/deploy/2_deploy_order_finding_bounty.ts b/assets/erc-7826/deploy/2_deploy_order_finding_bounty.ts new file mode 100644 index 0000000000..2e2547ea33 --- /dev/null +++ b/assets/erc-7826/deploy/2_deploy_order_finding_bounty.ts @@ -0,0 +1,60 @@ +import { HardhatRuntimeEnvironment } from 'hardhat/types' +import { DeployFunction } from 'hardhat-deploy/types' +import { Create2Factory } from '../src/Create2Factory' +import { ethers } from 'hardhat' +import { BigNumber } from 'ethers' +import * as fs from 'fs' + +const MAX_GAS_LIMIT_OPTION = { gasLimit: BigNumber.from('0x1c9c380') } + +const deployOrderFindingBounty: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { + const provider = ethers.provider + const from = await provider.getSigner().getAddress() + await new Create2Factory(ethers.provider).deployFactory() + + const numberOfLocks = 119 + const byteSizeOfModulus = 128 * 3 + const gcdIterationsPerCall = 2 ** 11 + let gasUsed = BigNumber.from(0) + let maxGas = BigNumber.from(0) + let numberOfAccumulations = 0 + + const deployResult = await hre.deployments.deploy( + 'OrderFindingBountyWithLockGeneration', { + ...MAX_GAS_LIMIT_OPTION, + from, + args: [numberOfLocks, byteSizeOfModulus, gcdIterationsPerCall], + gasLimit: 6e6, + deterministicDeployment: true + }) + console.log('==OrderFindingBounty addr=', deployResult.address) + gasUsed = gasUsed.add(deployResult.receipt?.gasUsed) + + const bounty = await ethers.getContractAt('OrderFindingBountyWithLockGeneration', deployResult.address) + while (!(await bounty.callStatic.generationIsDone())) { + ++numberOfAccumulations + const tx = await bounty.triggerLockAccumulation(MAX_GAS_LIMIT_OPTION) + const receipt = await tx.wait() + gasUsed = gasUsed.add(receipt.gasUsed) + if (receipt.gasUsed.gt(maxGas)) maxGas = receipt.gasUsed + + if (await bounty.callStatic.isCheckingPrime()) console.log('_b: ', (await bounty.currentPrimeCheck())) + } + + console.log('==OrderFindingBounty gasUsed=', gasUsed.toHexString()) + console.log('==OrderFindingBounty maxGas=', maxGas.toHexString()) + const [modulus, base] = await bounty.getLock(0) + console.log('Modulus: ', modulus) + console.log('Base: ', base) + console.log(`Number of accumulations: ${numberOfAccumulations}`) + + fs.writeFile( + 'Output.txt', + `gasUsed: ${gasUsed.toHexString()};\n\nMax Gas: ${maxGas.toHexString()};\n\nModulus: ${modulus as string};\n\nBase: ${base as string};\n\nNumber of Accumulations: ${numberOfAccumulations}`, + (err) => { + if (err != null) throw err + }) +} + +module.exports = deployOrderFindingBounty +module.exports.tags = ['OrderFindingBounty'] diff --git a/assets/erc-7826/src/Create2Factory.ts b/assets/erc-7826/src/Create2Factory.ts new file mode 100644 index 0000000000..b0e78cf433 --- /dev/null +++ b/assets/erc-7826/src/Create2Factory.ts @@ -0,0 +1,120 @@ +// from: https://github.com/Arachnid/deterministic-deployment-proxy +import { BigNumber, BigNumberish, ethers, Signer } from 'ethers' +import { arrayify, hexConcat, hexlify, hexZeroPad, keccak256 } from 'ethers/lib/utils' +import { Provider } from '@ethersproject/providers' +import { TransactionRequest } from '@ethersproject/abstract-provider' + +export class Create2Factory { + factoryDeployed = false + + // from: https://github.com/Arachnid/deterministic-deployment-proxy + static readonly contractAddress = '0x4e59b44847b379578588920ca78fbf26c0b4956c' + static readonly factoryTx = '0xf8a58085174876e800830186a08080b853604580600e600039806000f350fe7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe03601600081602082378035828234f58015156039578182fd5b8082525050506014600cf31ba02222222222222222222222222222222222222222222222222222222222222222a02222222222222222222222222222222222222222222222222222222222222222' + static readonly factoryDeployer = '0x3fab184622dc19b6109349b94811493bf2a45362' + static readonly deploymentGasPrice = 100e9 + static readonly deploymentGasLimit = 100000 + static readonly factoryDeploymentFee = (Create2Factory.deploymentGasPrice * Create2Factory.deploymentGasLimit).toString() + + constructor (readonly provider: Provider, + readonly signer = (provider as ethers.providers.JsonRpcProvider).getSigner()) { + } + + /** + * deploy a contract using our deterministic deployer. + * The deployer is deployed (unless it is already deployed) + * NOTE: this transaction will fail if already deployed. use getDeployedAddress to check it first. + * @param initCode delpoyment code. can be a hex string or factory.getDeploymentTransaction(..) + * @param salt specific salt for deployment + * @param gasLimit gas limit or 'estimate' to use estimateGas. by default, calculate gas based on data size. + */ + async deploy (initCode: string | TransactionRequest, salt: BigNumberish = 0, gasLimit?: BigNumberish | 'estimate'): Promise { + await this.deployFactory() + if (typeof initCode !== 'string') { + // eslint-disable-next-line @typescript-eslint/no-base-to-string + initCode = (initCode as TransactionRequest).data!.toString() + } + + const addr = Create2Factory.getDeployedAddress(initCode, salt) + if (await this.provider.getCode(addr).then(code => code.length) > 2) { + return addr + } + + const deployTx = { + to: Create2Factory.contractAddress, + data: this.getDeployTransactionCallData(initCode, salt) + } + if (gasLimit === 'estimate') { + gasLimit = await this.signer.estimateGas(deployTx) + } + + // manual estimation (its bit larger: we don't know actual deployed code size) + if (gasLimit === undefined) { + gasLimit = arrayify(initCode) + .map(x => x === 0 ? 4 : 16) + .reduce((sum, x) => sum + x) + + 200 * initCode.length / 2 + // actual is usually somewhat smaller (only deposited code, not entire constructor) + 6 * Math.ceil(initCode.length / 64) + // hash price. very minor compared to deposit costs + 32000 + + 21000 + + // deployer requires some extra gas + gasLimit = Math.floor(gasLimit * 64 / 63) + } + + const ret = await this.signer.sendTransaction({ ...deployTx, gasLimit }) + await ret.wait() + if (await this.provider.getCode(addr).then(code => code.length) === 2) { + throw new Error('failed to deploy') + } + return addr + } + + getDeployTransactionCallData (initCode: string, salt: BigNumberish = 0): string { + const saltBytes32 = hexZeroPad(hexlify(salt), 32) + return hexConcat([ + saltBytes32, + initCode + ]) + } + + /** + * return the deployed address of this code. + * (the deployed address to be used by deploy() + * @param initCode + * @param salt + */ + static getDeployedAddress (initCode: string, salt: BigNumberish): string { + const saltBytes32 = hexZeroPad(hexlify(salt), 32) + return '0x' + keccak256(hexConcat([ + '0xff', + Create2Factory.contractAddress, + saltBytes32, + keccak256(initCode) + ])).slice(-40) + } + + // deploy the factory, if not already deployed. + async deployFactory (signer?: Signer): Promise { + if (await this._isFactoryDeployed()) { + return + } + await (signer ?? this.signer).sendTransaction({ + to: Create2Factory.factoryDeployer, + value: BigNumber.from(Create2Factory.factoryDeploymentFee) + }) + await this.provider.sendTransaction(Create2Factory.factoryTx) + if (!await this._isFactoryDeployed()) { + throw new Error('fatal: failed to deploy deterministic deployer') + } + } + + async _isFactoryDeployed (): Promise { + if (!this.factoryDeployed) { + const deployed = await this.provider.getCode(Create2Factory.contractAddress) + if (deployed.length > 2) { + this.factoryDeployed = true + } + } + return this.factoryDeployed + } +} diff --git a/assets/erc-7826/test/UserOp.ts b/assets/erc-7826/test/UserOp.ts new file mode 100644 index 0000000000..0872d477f5 --- /dev/null +++ b/assets/erc-7826/test/UserOp.ts @@ -0,0 +1,246 @@ +import { + arrayify, + defaultAbiCoder, + hexDataSlice, + keccak256 +} from 'ethers/lib/utils' +import { BigNumber, Contract, Signer, Wallet } from 'ethers' +import { AddressZero, callDataCost, rethrow } from './testutils' +import { ecsign, toRpcSig, keccak256 as keccak256_buffer } from 'ethereumjs-util' +import { + EntryPoint +} from '../typechain' +import { UserOperation } from './UserOperation' +import { Create2Factory } from '../src/Create2Factory' + +function encode (typevalues: Array<{ type: string, val: any }>, forSignature: boolean): string { + const types = typevalues.map(typevalue => typevalue.type === 'bytes' && forSignature ? 'bytes32' : typevalue.type) + const values = typevalues.map((typevalue) => typevalue.type === 'bytes' && forSignature ? keccak256(typevalue.val) : typevalue.val) + return defaultAbiCoder.encode(types, values) +} + +// export function packUserOp(op: UserOperation, hashBytes = true): string { +// if ( !hashBytes || true ) { +// return packUserOp1(op, hashBytes) +// } +// +// const opEncoding = Object.values(testUtil.interface.functions).find(func => func.name == 'packUserOp')!.inputs[0] +// let packed = defaultAbiCoder.encode([opEncoding], [{...op, signature:'0x'}]) +// packed = '0x'+packed.slice(64+2) //skip first dword (length) +// packed = packed.slice(0,packed.length-64) //remove signature (the zero-length) +// return packed +// } + +export function packUserOp (op: UserOperation, forSignature = true): string { + if (forSignature) { + // lighter signature scheme (must match UserOperation#pack): do encode a zero-length signature, but strip afterwards the appended zero-length value + const userOpType = { + components: [ + { type: 'address', name: 'sender' }, + { type: 'uint256', name: 'nonce' }, + { type: 'bytes', name: 'initCode' }, + { type: 'bytes', name: 'callData' }, + { type: 'uint256', name: 'callGasLimit' }, + { type: 'uint256', name: 'verificationGasLimit' }, + { type: 'uint256', name: 'preVerificationGas' }, + { type: 'uint256', name: 'maxFeePerGas' }, + { type: 'uint256', name: 'maxPriorityFeePerGas' }, + { type: 'bytes', name: 'paymasterAndData' }, + { type: 'bytes', name: 'signature' } + ], + name: 'userOp', + type: 'tuple' + } + let encoded = defaultAbiCoder.encode([userOpType as any], [{ ...op, signature: '0x' }]) + // remove leading word (total length) and trailing word (zero-length signature) + encoded = '0x' + encoded.slice(66, encoded.length - 64) + return encoded + } + const typevalues = [ + { type: 'address', val: op.sender }, + { type: 'uint256', val: op.nonce }, + { type: 'bytes', val: op.initCode }, + { type: 'bytes', val: op.callData }, + { type: 'uint256', val: op.callGasLimit }, + { type: 'uint256', val: op.verificationGasLimit }, + { type: 'uint256', val: op.preVerificationGas }, + { type: 'uint256', val: op.maxFeePerGas }, + { type: 'uint256', val: op.maxPriorityFeePerGas }, + { type: 'bytes', val: op.paymasterAndData } + ] + if (!forSignature) { + // for the purpose of calculating gas cost, also hash signature + typevalues.push({ type: 'bytes', val: op.signature }) + } + return encode(typevalues, forSignature) +} + +export function packUserOp1 (op: UserOperation): string { + return defaultAbiCoder.encode([ + 'address', // sender + 'uint256', // nonce + 'bytes32', // initCode + 'bytes32', // callData + 'uint256', // callGasLimit + 'uint', // verificationGasLimit + 'uint', // preVerificationGas + 'uint256', // maxFeePerGas + 'uint256', // maxPriorityFeePerGas + 'bytes32' // paymasterAndData + ], [ + op.sender, + op.nonce, + keccak256(op.initCode), + keccak256(op.callData), + op.callGasLimit, + op.verificationGasLimit, + op.preVerificationGas, + op.maxFeePerGas, + op.maxPriorityFeePerGas, + keccak256(op.paymasterAndData) + ]) +} + +export function getUserOpHash (op: UserOperation, entryPoint: string, chainId: number): string { + const userOpHash = keccak256(packUserOp(op, true)) + const enc = defaultAbiCoder.encode( + ['bytes32', 'address', 'uint256'], + [userOpHash, entryPoint, chainId]) + return keccak256(enc) +} + +export const DefaultsForUserOp: UserOperation = { + sender: AddressZero, + nonce: 0, + initCode: '0x', + callData: '0x', + callGasLimit: 0, + verificationGasLimit: 100000, // default verification gas. will add create2 cost (3200+200*length) if initCode exists + preVerificationGas: 21000, // should also cover calldata cost. + maxFeePerGas: 0, + maxPriorityFeePerGas: 1e9, + paymasterAndData: '0x', + signature: '0x' +} + +export function signUserOp (op: UserOperation, signer: Wallet, entryPoint: string, chainId: number): UserOperation { + const message = getUserOpHash(op, entryPoint, chainId) + const msg1 = Buffer.concat([ + Buffer.from('\x19Ethereum Signed Message:\n32', 'ascii'), + Buffer.from(arrayify(message)) + ]) + + const sig = ecsign(keccak256_buffer(msg1), Buffer.from(arrayify(signer.privateKey))) + // that's equivalent of: await signer.signMessage(message); + // (but without "async" + const signedMessage1 = toRpcSig(sig.v, sig.r, sig.s) + return { + ...op, + signature: signedMessage1 + } +} + +export function fillUserOpDefaults (op: Partial, defaults = DefaultsForUserOp): UserOperation { + const partial: any = { ...op } + // we want "item:undefined" to be used from defaults, and not override defaults, so we must explicitly + // remove those so "merge" will succeed. + for (const key in partial) { + if (partial[key] == null) { + // eslint-disable-next-line @typescript-eslint/no-dynamic-delete + delete partial[key] + } + } + const filled = { ...defaults, ...partial } + return filled +} + +// helper to fill structure: +// - default callGasLimit to estimate call from entryPoint to account (TODO: add overhead) +// if there is initCode: +// - calculate sender by eth_call the deployment code +// - default verificationGasLimit estimateGas of deployment code plus default 100000 +// no initCode: +// - update nonce from account.nonce() +// entryPoint param is only required to fill in "sender address when specifying "initCode" +// nonce: assume contract as "nonce()" function, and fill in. +// sender - only in case of construction: fill sender from initCode. +// callGasLimit: VERY crude estimation (by estimating call to account, and add rough entryPoint overhead +// verificationGasLimit: hard-code default at 100k. should add "create2" cost +export async function fillUserOp (op: Partial, entryPoint?: EntryPoint): Promise { + const op1 = { ...op } + const provider = entryPoint?.provider + if (op.initCode != null) { + const initAddr = hexDataSlice(op1.initCode!, 0, 20) + const initCallData = hexDataSlice(op1.initCode!, 20) + if (op1.nonce == null) op1.nonce = 0 + if (op1.sender == null) { + // hack: if the init contract is our known deployer, then we know what the address would be, without a view call + if (initAddr.toLowerCase() === Create2Factory.contractAddress.toLowerCase()) { + const ctr = hexDataSlice(initCallData, 32) + const salt = hexDataSlice(initCallData, 0, 32) + op1.sender = Create2Factory.getDeployedAddress(ctr, salt) + } else { + // console.log('\t== not our deployer. our=', Create2Factory.contractAddress, 'got', initAddr) + if (provider == null) throw new Error('no entrypoint/provider') + op1.sender = await entryPoint!.callStatic.getSenderAddress(op1.initCode!).catch(e => e.errorArgs.sender) + } + } + if (op1.verificationGasLimit == null) { + if (provider == null) throw new Error('no entrypoint/provider') + const initEstimate = await provider.estimateGas({ + from: entryPoint?.address, + to: initAddr, + data: initCallData, + gasLimit: 10e6 + }) + op1.verificationGasLimit = BigNumber.from(DefaultsForUserOp.verificationGasLimit).add(initEstimate) + } + } + if (op1.nonce == null) { + if (provider == null) throw new Error('must have entryPoint to autofill nonce') + const c = new Contract(op.sender!, ['function nonce() view returns(address)'], provider) + op1.nonce = await c.nonce().catch(rethrow()) + } + if (op1.callGasLimit == null && op.callData != null) { + if (provider == null) throw new Error('must have entryPoint for callGasLimit estimate') + const gasEtimated = await provider.estimateGas({ + from: entryPoint?.address, + to: op1.sender, + data: op1.callData + }) + + // console.log('estim', op1.sender,'len=', op1.callData!.length, 'res=', gasEtimated) + // estimateGas assumes direct call from entryPoint. add wrapper cost. + op1.callGasLimit = gasEtimated // .add(55000) + } + if (op1.maxFeePerGas == null) { + if (provider == null) throw new Error('must have entryPoint to autofill maxFeePerGas') + const block = await provider.getBlock('latest') + op1.maxFeePerGas = block.baseFeePerGas!.add(op1.maxPriorityFeePerGas ?? DefaultsForUserOp.maxPriorityFeePerGas) + } + // TODO: this is exactly what fillUserOp below should do - but it doesn't. + // adding this manually + if (op1.maxPriorityFeePerGas == null) { + op1.maxPriorityFeePerGas = DefaultsForUserOp.maxPriorityFeePerGas + } + const op2 = fillUserOpDefaults(op1) + // eslint-disable-next-line @typescript-eslint/no-base-to-string + if (op2.preVerificationGas.toString() === '0') { + // TODO: we don't add overhead, which is ~21000 for a single TX, but much lower in a batch. + op2.preVerificationGas = callDataCost(packUserOp(op2, false)) + } + return op2 +} + +export async function fillAndSign (op: Partial, signer: Wallet | Signer, entryPoint?: EntryPoint): Promise { + const provider = entryPoint?.provider + const op2 = await fillUserOp(op, entryPoint) + + const chainId = await provider!.getNetwork().then(net => net.chainId) + const message = arrayify(getUserOpHash(op2, entryPoint!.address, chainId)) + + return { + ...op2, + signature: await signer.signMessage(message) + } +} diff --git a/assets/erc-7826/test/UserOperation.ts b/assets/erc-7826/test/UserOperation.ts new file mode 100644 index 0000000000..8bed5ab330 --- /dev/null +++ b/assets/erc-7826/test/UserOperation.ts @@ -0,0 +1,16 @@ +import * as typ from './solidityTypes' + +export interface UserOperation { + + sender: typ.address + nonce: typ.uint256 + initCode: typ.bytes + callData: typ.bytes + callGasLimit: typ.uint256 + verificationGasLimit: typ.uint256 + preVerificationGas: typ.uint256 + maxFeePerGas: typ.uint256 + maxPriorityFeePerGas: typ.uint256 + paymasterAndData: typ.bytes + signature: typ.bytes +} diff --git a/assets/erc-7826/test/bounty-contracts/bounty-test-factory.ts b/assets/erc-7826/test/bounty-contracts/bounty-test-factory.ts new file mode 100644 index 0000000000..0d17401e73 --- /dev/null +++ b/assets/erc-7826/test/bounty-contracts/bounty-test-factory.ts @@ -0,0 +1,208 @@ +import { BigNumber } from 'ethers' +import { JsonRpcSigner } from '@ethersproject/providers/src.ts/json-rpc-provider' +import { ethers } from 'hardhat' +import { expect } from 'chai' +import BountyUtils, { submitSolution } from './bounty-utils' +import { BountyContract } from '../../typechain' +import { arrayify } from 'ethers/lib/utils' +import { Buffer } from 'buffer' +import { bytes } from '../solidityTypes' +import { time } from '@nomicfoundation/hardhat-network-helpers' + +const ONE_MINUTE_IN_SECONDS = 60 + +function getBountyTest (bountyUtils: BountyUtils) { + return () => { + let bounty: BountyContract + + beforeEach(async () => { + bounty = await bountyUtils.deployBounty() + }) + + describe('Withdraw', () => { + const arbitraryBountyAmount = BigNumber.from(100) + let arbitraryUser: JsonRpcSigner + let previousBalance: BigNumber + + async function getBalance (): Promise { + return await arbitraryUser.getBalance() + } + + before(async () => { + arbitraryUser = ethers.provider.getSigner(1) + }) + + beforeEach(async () => { + await bounty.addToBounty({ value: arbitraryBountyAmount }) + }) + + describe('Correct Solutions', () => { + beforeEach(async () => { + const result = await bountyUtils.solveBounty(bounty, getBalance) + previousBalance = result.userBalanceBeforeFinalTransaction + }) + + it('should have a bounty of zero afterwards', async () => { + expect(await bounty.bounty()).to.equal(0) + }) + + it('should send the bounty to the user', async () => { + const gasUsed = await bountyUtils.getLatestSolvedGasCost() + const newBalance = await arbitraryUser.getBalance() + const expectedBalance = previousBalance.sub(gasUsed).add(arbitraryBountyAmount) + expect(newBalance).to.equal(expectedBalance) + }) + + it('should set the bounty as solved', async () => { + expect(await bounty.solved()).to.equal(true) + }) + + it('should revert deposits if already solved', async () => { + const tx = bounty.addToBounty({ value: arbitraryBountyAmount }) + await expect(tx).to.be.revertedWith('Already solved') + }) + + it('should not allow further solve attempts if already solved', async () => { + const tx = submitSolution(0, Buffer.from(''), bounty) + await expect(tx).to.be.revertedWith('Already solved') + }) + }) + + describe('Incorrect Solutions', () => { + beforeEach(async () => { + previousBalance = await getBalance() + const tx = bountyUtils.solveBountyIncorrectly(bounty) + await expect(tx).to.be.revertedWith('Invalid solution') + }) + + it('should have full bounty afterwards', async () => { + expect(await bounty.bounty()).equal(arbitraryBountyAmount) + }) + + it('should not send the bounty to the user', async () => { + const latestTXGasCosts = await bountyUtils.getLatestSolvedGasCost() + const newBalance = await arbitraryUser.getBalance() + expect(newBalance).equal(previousBalance.sub(latestTXGasCosts)) + }) + + it('should keep the bounty as unsolved', async () => { + expect(await bounty.solved()).equal(false) + }) + }) + }) + + describe('Lock generation', () => { + it('should set locks as publicly available', async () => { + const locks = await bountyUtils.getLocks(bounty) + for (let i = 0; i < locks.length; i++) { + const expectedLock = locks[i] + for (let j = 0; j < expectedLock.length; j++) { + const bountyLock = Buffer.from(arrayify((await bounty.getLock(i))[j])) + expect(bountyLock).deep.equal(expectedLock[j]) + } + } + }) + }) + + describe('Add to bounty', () => { + const amountToAdd = 100 + const otherUser = ethers.provider.getSigner(1) + + async function testAddToBounty (func: () => Promise): Promise { + expect(await bounty.bounty()).to.equal(0) + await func() + expect(await bounty.bounty()).to.equal(amountToAdd) + } + + it('should allow adding to the bounty', async () => { + await testAddToBounty(async () => { + await bounty.connect(otherUser).addToBounty({ value: amountToAdd }) + }) + }) + + it('should allow adding to the bounty by sending funds to address using receive', async () => { + await testAddToBounty(async () => { + await otherUser.sendTransaction({ to: bounty.address, value: amountToAdd }) + }) + }) + + it('should allow adding to the bounty by sending funds to address using fallback', async () => { + await testAddToBounty(async () => { + const arbitrary_data = '0x54' + await otherUser.sendTransaction({ to: bounty.address, value: amountToAdd, data: arbitrary_data }) + }) + }) + }) + + describe('Commit reveal', () => { + const arbitraryLockNumber = 0 + const arbitrarySolutionHash = '0x0000000000000000000000000000000000000000000000000000000000000001' + const arbitrarySolutionHashBuffer = Buffer.from(arrayify(arbitrarySolutionHash)) + + it('should be able to retrieve commit info', async () => { + await bounty.commitSolution(arbitraryLockNumber, arbitrarySolutionHashBuffer) + const [hash, timestamp] = await bounty.callStatic.getMyCommit(arbitraryLockNumber) + expect(hash).to.be.eq(arbitrarySolutionHash) + + const blockNumBefore = await ethers.provider.getBlockNumber() + const blockBefore = await ethers.provider.getBlock(blockNumBefore) + const timestampBefore = blockBefore.timestamp + expect(timestamp).to.eq(timestampBefore) + }) + + it('should be able to override a commit', async () => { + const arbitrarySolutionHash2 = '0x0000000000000000000000000000000000000000000000000000000000000002' + await bounty.commitSolution(arbitraryLockNumber, arbitrarySolutionHashBuffer) + await bounty.commitSolution(arbitraryLockNumber, Buffer.from(arrayify(arbitrarySolutionHash2))) + const [hash] = await bounty.callStatic.getMyCommit(arbitraryLockNumber) + expect(hash).to.be.eq(arbitrarySolutionHash2) + }) + + it('should revert getting my commit if no commit was made', async () => { + const tx = bounty.getMyCommit(arbitraryLockNumber) + await expect(tx).to.be.revertedWith('Not committed yet') + }) + + it('should not allow commits if already solved', async () => { + await bountyUtils.solveBounty(bounty) + const tx = bounty.commitSolution(arbitraryLockNumber, arbitrarySolutionHashBuffer) + await expect(tx).to.be.revertedWith('Already solved') + }) + + it('should not allow a reveal without a commit', async () => { + const arbitrarySolution: bytes = '0x' + const tx = bounty.solve(arbitraryLockNumber, arbitrarySolution) + await expect(tx).to.be.revertedWith('Not committed yet') + }) + + it('should not allow a reveal within a day of the commit', async () => { + const arbitrarySolutions: bytes = '0x' + await bounty.commitSolution(arbitraryLockNumber, arbitrarySolutionHashBuffer) + + const justBeforeADay = bountyUtils.ONE_DAY_IN_SECONDS - ONE_MINUTE_IN_SECONDS + await time.increase(justBeforeADay) + const txReveal = bounty.solve(arbitraryLockNumber, arbitrarySolutions) + await expect(txReveal).to.be.revertedWith('Cannot reveal within a day of the commit') + }) + }) + + describe('Track individual lock status', () => { + const arbitraryBountyAmount = 1 + + beforeEach(async () => { + await bounty.addToBounty({ value: arbitraryBountyAmount }) + await bountyUtils.solveBountyPartially(bounty) + }) + + it('should not set the bounty as solved', async () => { + expect(await bounty.solved()).to.eq(false) + }) + + it('should not send the bounty funds', async () => { + expect(await bounty.bounty()).to.eq(arbitraryBountyAmount) + }) + }) + } +} + +export default getBountyTest diff --git a/assets/erc-7826/test/bounty-contracts/bounty-utils.ts b/assets/erc-7826/test/bounty-contracts/bounty-utils.ts new file mode 100644 index 0000000000..b295924e65 --- /dev/null +++ b/assets/erc-7826/test/bounty-contracts/bounty-utils.ts @@ -0,0 +1,104 @@ +import { BigNumber, ContractTransaction } from 'ethers' +import { BountyContract } from '../../typechain' +import { bytes } from '../solidityTypes' +import { ethers, web3 } from 'hardhat' +import { time } from '@nomicfoundation/hardhat-network-helpers' +import { keccak256 } from 'ethereumjs-util' +import { Buffer } from 'buffer' +import { arrayify } from 'ethers/lib/utils' + +const MAX_GAS_LIMIT_OPTION = { gasLimit: BigNumber.from('0x1c9c380') } +const ONE_DAY_IN_SECONDS = 86400 + +export class SolveAttemptResult { + public readonly userBalanceBeforeFinalTransaction + + constructor (userBalanceBeforeFinalTransaction: BigNumber) { + this.userBalanceBeforeFinalTransaction = userBalanceBeforeFinalTransaction + } +} + +export async function solveBountyReturningUserBalanceBeforeFinalSolution ( + solutions: bytes[], + bounty: BountyContract, + getUserBalance?: () => Promise): Promise { + let userBalanceBeforeFinalTransaction = BigNumber.from(0) + for (let i = 0; i < solutions.length; i++) { + if (getUserBalance !== undefined && i === solutions.length - 1) userBalanceBeforeFinalTransaction = await getUserBalance() + await submitSolution(i, solutions[i], bounty) + } + return new SolveAttemptResult(userBalanceBeforeFinalTransaction) +} + +export async function submitSolution (lockNumber: number, solution: bytes, bounty: BountyContract): Promise { + const arbitraryUser = ethers.provider.getSigner(1) + const solutionEncoding = web3.eth.abi.encodeParameters( + [ + 'address', + 'bytes' + ], [ + await arbitraryUser.getAddress(), + solution + ] + ) + const solutionHash = keccak256(Buffer.from(arrayify(solutionEncoding))) + + await bounty.connect(arbitraryUser).commitSolution(lockNumber, solutionHash, MAX_GAS_LIMIT_OPTION) + await time.increase(ONE_DAY_IN_SECONDS) + return bounty.connect(arbitraryUser).solve(lockNumber, solution, MAX_GAS_LIMIT_OPTION) +} + +export async function getLatestSolvedGasCost (numberOfSolutions: number): Promise { + const numberOfTransactionsPerSubmittedSolution = 2 + return await _getLastTransactionGasCost(numberOfTransactionsPerSubmittedSolution * numberOfSolutions) +} + +async function _getLastTransactionGasCost (numberOfTransactions: number): Promise { + // Thanks to https://ethereum.stackexchange.com/a/140971/120101 + const latestBlock = await ethers.provider.getBlock('latest') + let latestTxHashes: string[] = [] + let i = 0 + while (latestTxHashes.length < numberOfTransactions && latestBlock.number - i > 0) { + const block = await ethers.provider.getBlock(latestBlock.number - i++) + const numberOfTransactions = block.transactions.length + const remainingTransactions = numberOfTransactions - latestTxHashes.length + const startIndex = Math.max(0, numberOfTransactions - 1 - remainingTransactions) + const transactions = block.transactions.slice(startIndex, numberOfTransactions) + latestTxHashes = latestTxHashes.concat(transactions) + } + const latestTxReceipts = await Promise.all(latestTxHashes.map(async hash => + await ethers.provider.getTransactionReceipt(hash))) + const latestGasCosts = latestTxReceipts.map(receipt => + receipt.gasUsed.mul(receipt.effectiveGasPrice)) + return latestGasCosts.reduce((total, amount) => total.add(amount)) +} + +abstract class BountyUtils { + public ONE_DAY_IN_SECONDS = 86400 + + public async deployBounty (): Promise { + throw new Error('deploySignatureBounty() not implemented') + } + + public async getLocks (puzzle: BountyContract): Promise { + throw new Error('getLocks() not implemented') + } + + public async solveBounty (bounty: BountyContract, getUserBalance?: () => Promise): Promise { + throw new Error('solveBounty() not implemented') + } + + public async solveBountyPartially (bounty: BountyContract): Promise { + throw new Error('solveBountyPartially() not implemented') + } + + public async solveBountyIncorrectly (bounty: BountyContract): Promise { + throw new Error('solveBountyIncorrectly() not implemented') + } + + public async getLatestSolvedGasCost (): Promise { + throw new Error('getLatestSolvedGasCost() not implemented') + } +} + +export default BountyUtils diff --git a/assets/erc-7826/test/bounty-contracts/order-finding-bounty/cost-of-solving-order.test.ts b/assets/erc-7826/test/bounty-contracts/order-finding-bounty/cost-of-solving-order.test.ts new file mode 100644 index 0000000000..f1698092fe --- /dev/null +++ b/assets/erc-7826/test/bounty-contracts/order-finding-bounty/cost-of-solving-order.test.ts @@ -0,0 +1,73 @@ +import { bytes } from '../../solidityTypes' +import { + OrderFindingBountyWithPredeterminedLocks, + OrderFindingBountyWithPredeterminedLocks__factory +} from '../../../typechain' +import { ethers } from 'hardhat' +import { submitSolution } from '../bounty-utils' +import { expect } from 'chai' +import { BigNumber } from 'ethers' +import { randomBytes } from 'crypto' + +const HEX_PREFIX = '0x' + +describe.skip('Test the cost of solving the order finding bounty', () => { + let bounty: OrderFindingBountyWithPredeterminedLocks + + const locks = [ + [ + '0xccda5ed8b7b0a45eb02d23b07e62f088fbe14781ce0baf896605957519c2e0cdf8206066d6d1f7acaddeea0a5edc97277998024a093ee70358aabcf322c0b748ea4ac0cd884344c55564ab5a9d6d6ac7f89e67f488e84a0e19d0ced4c89bc818a35735f1b563234aea4da7c09fc150e7317a2efcf7f0b1741bd85671650e3e927d2eee89c5556a6a37ed619a36178a1b6b8790c0ffdc6c0438eac646f533c6252e6e6766501ba0392adde287e0e1f83360e590c9caa155b1285c6cd4563ed0d7456d22919fe118090b9fd00c3714daebfa21f5216a76b1ee6b46f135b9670b5465b7a089c9d7aad3acd3fbd65f98c5e625914744c19690ff1299685307458cb0504d1f8283872bdac22cc5bdbc39778fdd3dd57e87b58fe64bdcb547675ff8f85688cb807e913b584b0e4b5123da438acc793a1c7de8e9b42607e39750faec17f0a245bfaed21a0c4da06e419c9c36b876e8c207564e194920fa694754df4c6615c57bf984aa879d79c07b7ae4cb525936ddd6755690347c3e040454a2feb511', + '0x4ac7e1bccd690ac694c718ce779dc5dadeeee6825286ea188c9c0dedaf86faaf772128237f019611cb2fbf0ed29aa4165d3e1ac39312f8a16408bd362ba25c8603cbd364cfc71d380b4be892a6f686e19fdda3603264698c31fd3bb3ef069973e11ca4c1d19c4ae768195ce25955f7e41f0dc294a7e6f237886b80b9d4fde770ce54368a439e57665e4128b038a2d475a751e2e21b7d8b966298ba25e61dfa4bd6caf99fbea486168704a9d34056c1d99bdcc5ad8b21dd28ded5245c815a90650c21f26def86b336eed19e012e46a1fcf858ea516e36dccba3a5983216dc43edd3afd6e9cf1ef8437f69cc654507c965ddbd8bcfb0de20c6dbfc3f986a5f276f1c9ac944c11904c0568ab1d8c8819e064d30789db0de11b7afb91ed8c6351c5afd9b17ec08270236cfcc00766a06312cfd1bcfd0478e613045f89892ff8d093f345e2eabeefae620e155e026fb50c4f4fa0097149f3996a5d2c808d53e3cc7500d001c979b8677920d50835dee1ded126a684e13fb38b12c7d609dfda9bf2177' + ] + ] + + async function deployBounty (locks: bytes[][]): Promise { + const ethersSigner = ethers.provider.getSigner() + const bounty = await new OrderFindingBountyWithPredeterminedLocks__factory(ethersSigner).deploy(locks.length) + for (let i = 0; i < locks.length; i++) { + await bounty.setLock(i, locks[i]) + } + return bounty + } + + beforeEach(async () => { + bounty = await deployBounty(locks) + }) + + it('should find the gas cost to attempt a 3071-bit base, 3072-bit modulus with various sized exponents', async () => { + const gasCosts: BigNumber[] = [] + + const byteSizeOfModulus = locks[0][0].length - HEX_PREFIX.length + const maxOrderByteSize = 2 * byteSizeOfModulus + + for (let i = 1; i <= maxOrderByteSize; i++) { + const solution = randomBytes(i) + solution[0] = solution[0] | (1 << 7) + + const tx = submitSolution(0, solution, bounty) + await expect(tx, `Base ${locks[0][1]} worked with exponent 0x${solution.toString('hex')}`).to.be.reverted + + const latestBlock = await ethers.provider.getBlock('latest') + const latestTransactionHash = latestBlock.transactions[latestBlock.transactions.length - 1] + const latestReceipt = await ethers.provider.getTransactionReceipt(latestTransactionHash) + + const gasUsed = latestReceipt.gasUsed + console.log(`Gas for ${i}-byte solution is ${gasUsed.toHexString()}`) + gasCosts.push(gasUsed) + } + + const maxGas = gasCosts.reduce((acc, curr) => curr.lt(acc) ? curr : acc) + const minGas = gasCosts.reduce((acc, curr) => curr.gt(acc) ? curr : acc) + const meanGas = gasCosts.reduce((acc, curr) => acc.add(curr)).div(gasCosts.length) + + const sortedGasCosts = gasCosts.sort((a, b) => a.lt(b) ? -1 : 1) + const halfIndex = maxOrderByteSize / 2 + const medianGas = halfIndex % 1 === 0 + ? sortedGasCosts[halfIndex].add(sortedGasCosts[halfIndex + 1]).div(2) + : sortedGasCosts[Math.ceil(halfIndex)] + console.log(`Min gas: ${minGas.toHexString()}`) + console.log(`Max gas: ${maxGas.toHexString()}`) + console.log(`Mean gas: ${meanGas.toHexString()}`) + console.log(`Median gas: ${medianGas.toHexString()}`) + }) +}) diff --git a/assets/erc-7826/test/bounty-contracts/order-finding-bounty/order-finding-bounty-with-lock-generation/order-finding-accumulator.test.ts b/assets/erc-7826/test/bounty-contracts/order-finding-bounty/order-finding-bounty-with-lock-generation/order-finding-accumulator.test.ts new file mode 100644 index 0000000000..7f2fef85a3 --- /dev/null +++ b/assets/erc-7826/test/bounty-contracts/order-finding-bounty/order-finding-bounty-with-lock-generation/order-finding-accumulator.test.ts @@ -0,0 +1,305 @@ +import { OrderFindingAccumulatorTestHelper, OrderFindingAccumulatorTestHelper__factory } from '../../../../typechain' +import { ethers } from 'hardhat' +import { arrayify } from 'ethers/lib/utils' +import { expect } from 'chai' +import { Buffer } from 'buffer' +import { BigNumber } from 'ethers' + +const EMPTY_BYTES = '0x' + +describe('OrderFindingAccumulator', () => { + const ethersSigner = ethers.provider.getSigner() + + let testHelper: OrderFindingAccumulatorTestHelper + + async function accumulator (): Promise { + return await testHelper.accumulator() + } + + async function deployNewAccumulator ( + numberOfLocks: number, + bytesPerPrime: number, + gcdIterationsPerCall: number = 1 + ): Promise { + return await new OrderFindingAccumulatorTestHelper__factory(ethersSigner).deploy(numberOfLocks, bytesPerPrime, gcdIterationsPerCall) + } + + async function expectDone (expectedValue: boolean): Promise { + expect((await accumulator()).generationIsDone).to.be.eq(expectedValue) + } + + async function expectLockParameter (lockNumber: number, lockParameterNumber: number, expectedValue: string): Promise { + expect((await accumulator()).locks.vals[lockNumber][lockParameterNumber]).to.be.eq(expectedValue) + } + + async function expectLock (lockNumber: number, expectedValues: string[]): Promise { + for (let i = 0; i < (await accumulator()).parametersPerLock; i++) { + await expectLockParameter(lockNumber, i, expectedValues[i]) + } + } + + async function accumulateValues (hexStrings: string[]): Promise { + for (const hexString of hexStrings) { + await accumulateValueWithoutWaitingForPrimeCheck(hexString) + while ((await testHelper.callStatic.isCheckingPrime())) { + await testHelper.triggerAccumulate([]) + } + } + } + + async function accumulateValueWithoutWaitingForPrimeCheck (hexString: string): Promise { + await testHelper.triggerAccumulate(Buffer.from(arrayify(hexString))) + } + + describe('multiple gcd iterations', () => { + const modulus = '0x81' + const base = '0x02' + + async function deployNewAccumulatorWithSetGcdIterations (gcdIterationsPerCall: number): Promise { + const numberOfLocks = 1 + const bytesPerPrime = 1 + testHelper = await deployNewAccumulator(numberOfLocks, bytesPerPrime, gcdIterationsPerCall) + await initializeByteValues() + } + + async function initializeByteValues (): Promise { + const arbitraryValueToTriggerGcdProcess = EMPTY_BYTES + for (const val of [modulus, base, arbitraryValueToTriggerGcdProcess]) { + await accumulateValueWithoutWaitingForPrimeCheck(val) + } + } + + async function expectGcdProcessHasBegun (): Promise { + const currentA = BigNumber.from((await testHelper.accumulator())._a.val) + expect(currentA.eq(modulus)).to.eq(false) + } + + async function expectProperties (isCheckingPrime: boolean, expectedBase: string): Promise { + expect(await testHelper.callStatic.isCheckingPrime()).to.eq(isCheckingPrime) + await expectLockParameter(0, 1, expectedBase) + } + + it('should not finish with 1 gcd iteration', async () => { + await deployNewAccumulatorWithSetGcdIterations(1) + await expectGcdProcessHasBegun() + await expectProperties(true, EMPTY_BYTES) + }) + + it('should finish with exactly enough gcd iteration', async () => { + await deployNewAccumulatorWithSetGcdIterations(3) + await expectProperties(false, base) + }) + + it('should finish with more than enough gcd iterations', async () => { + await deployNewAccumulatorWithSetGcdIterations(4) + await expectProperties(false, base) + }) + }) + + describe('modulus and base', () => { + describe('single accumulations', () => { + beforeEach(async () => { + const numberOfLocks = 1 + const bytesPerPrime = 1 + testHelper = await deployNewAccumulator(numberOfLocks, bytesPerPrime) + }) + + describe('ensure the base is between 1 and -1', () => { + beforeEach(async () => { + await accumulateValues(['0x81']) + await expectLockParameter(0, 0, '0x81') + }) + + it('should only accept a base that is coprime with the modulus', async () => { + await accumulateValues(['0x06']) + await expectLockParameter(0, 1, EMPTY_BYTES) + await accumulateValues(['0x02']) + await expectLockParameter(0, 1, '0x02') + }) + + it('should modulo the base if it is greater than the modulus', async () => { + await accumulateValues(['0x83']) + await expectLockParameter(0, 1, '0x02') + }) + + it('should not accept a base equal to -1', async () => { + await accumulateValues(['0x80']) + await expectLockParameter(0, 1, EMPTY_BYTES) + }) + + it('should not accept a base equal to 1', async () => { + await accumulateValues(['0x01']) + await expectLockParameter(0, 1, EMPTY_BYTES) + }) + }) + + describe('setting the first bit of the modulus', () => { + it('should leave first bit of the modulus unchanged if already one', async () => { + await accumulateValues(['0x81']) + await expectLockParameter(0, 0, '0x81') + }) + + it('should set first bit of the modulus to one if zero', async () => { + await accumulateValues(['0x02']) + await expectLockParameter(0, 0, '0x82') + }) + }) + + it('should not set the first bit of the base', async () => { + await accumulateValues(['0x81', '0x02']) + await expectLockParameter(0, 1, '0x02') + }) + }) + + it('should not set the first bit of subsequent accumulations of the modulus', async () => { + const numberOfLocks = 1 + const bytesPerPrime = 2 + testHelper = await deployNewAccumulator(numberOfLocks, bytesPerPrime) + await accumulateValues(['0x81', '0x02']) + await expectLockParameter(0, 0, '0x8102') + }) + }) + + describe('exact right size input', () => { + beforeEach(async () => { + const numberOfLocks = 1 + const bytesPerPrime = 1 + testHelper = await deployNewAccumulator(numberOfLocks, bytesPerPrime) + await accumulateValues(['0xf5', '0x3d']) + }) + + it('should be marked as done', async () => { + await expectDone(true) + }) + + it('should have a lock matching the input', async () => { + await expectLock(0, ['0xf5', '0x3d']) + }) + }) + + describe('slicing off extra bytes', () => { + beforeEach(async () => { + const numberOfLocks = 1 + const bytesPerPrime = 1 + testHelper = await deployNewAccumulator(numberOfLocks, bytesPerPrime) + await accumulateValues(['0xf5d4', '0x3d8c']) + }) + + it('should be marked as done', async () => { + await expectDone(true) + }) + + it('should have a lock with only the necessary bytes', async () => { + await expectLock(0, ['0xf5', '0x3d']) + }) + }) + + describe('multiple accumulations per lock', () => { + beforeEach(async () => { + const numberOfLocks = 1 + const bytesPerPrime = 2 + testHelper = await deployNewAccumulator(numberOfLocks, bytesPerPrime) + await accumulateValues(['0xf5', '0x3d', '0x8c']) + }) + + describe('first accumulation', () => { + it('should not be marked as done', async () => { + await expectDone(false) + }) + + it('should have only the first parameter of the first lock', async () => { + await expectLockParameter(0, 0, '0xf53d') + await expectLockParameter(0, 1, EMPTY_BYTES) + }) + }) + + describe('second accumulation', () => { + beforeEach(async () => { + await accumulateValues(['0x00']) + }) + + it('should be marked as done', async () => { + await expectDone(true) + }) + + it('should have a lock equal to both inputs', async () => { + await expectLockParameter(0, 0, '0xf53d') + await expectLockParameter(0, 1, '0x8c00') + }) + }) + }) + + describe('multiple locks', () => { + beforeEach(async () => { + const numberOfLocks = 2 + const bytesPerPrime = 1 + testHelper = await deployNewAccumulator(numberOfLocks, bytesPerPrime) + await accumulateValues(['0xf5', '0x3d']) + }) + + describe('first accumulation', () => { + it('should not be marked as done', async () => { + await expectDone(false) + }) + + it('should have the first lock equal to the input', async () => { + await expectLock(0, ['0xf5', '0x3d']) + }) + + it('should not have a second lock', async () => { + expect((await accumulator()).locks.vals[1]).to.eql([]) + }) + }) + + describe('second accumulation', () => { + beforeEach(async () => { + await accumulateValues(['0x8c', '0x03']) + }) + + it('should be marked as done', async () => { + await expectDone(true) + }) + + it('should have the first lock equal to the first input', async () => { + await expectLock(0, ['0xf5', '0x3d']) + }) + + it('should have the second lock equal to the second input', async () => { + await expectLock(1, ['0x8c', '0x03']) + }) + }) + }) + + describe('already done', () => { + beforeEach(async () => { + const numberOfLocks = 1 + const bytesPerPrime = 1 + testHelper = await deployNewAccumulator(numberOfLocks, bytesPerPrime) + await accumulateValues(['0xf5', '0x3d']) + }) + + describe('first accumulation', () => { + it('should be marked as done', async () => { + await expectDone(true) + }) + + it('should have the first lock equal to the input', async () => { + await expectLock(0, ['0xf5', '0x3d']) + }) + }) + + describe('unnecessary, additional accumulation', () => { + beforeEach(async () => { + await accumulateValues(['0x8c', '0x00']) + }) + + it('should be marked as done', async () => { + await expectDone(true) + }) + + it('should have the first lock equal to the first input', async () => { + await expectLock(0, ['0xf5', '0x3d']) + }) + }) + }) +}) diff --git a/assets/erc-7826/test/bounty-contracts/order-finding-bounty/order-finding-bounty-with-lock-generation/order-finding-bounty-with-lock-generation.test.ts b/assets/erc-7826/test/bounty-contracts/order-finding-bounty/order-finding-bounty-with-lock-generation/order-finding-bounty-with-lock-generation.test.ts new file mode 100644 index 0000000000..61a452fd96 --- /dev/null +++ b/assets/erc-7826/test/bounty-contracts/order-finding-bounty/order-finding-bounty-with-lock-generation/order-finding-bounty-with-lock-generation.test.ts @@ -0,0 +1,77 @@ +import { + OrderFindingBountyWithLockGeneration__factory, OrderFindingBountyWithLockGeneration +} from '../../../../typechain' +import { ethers } from 'hardhat' +import { expect } from 'chai' +import { BigNumber } from 'ethers' +import { submitSolution } from '../../bounty-utils' + +const MAX_GAS_LIMIT_OPTION = { gasLimit: BigNumber.from('0x1c9c380') } + +describe('OrderFindingBountyWithLockGeneration', () => { + const ethersSigner = ethers.provider.getSigner() + + async function deployNewAccumulator (numberOfLocks: number, byteSizeOfModulus: number): Promise { + const bounty = await new OrderFindingBountyWithLockGeneration__factory(ethersSigner).deploy(numberOfLocks, byteSizeOfModulus) + while (!(await bounty.callStatic.generationIsDone())) { + await bounty.triggerLockAccumulation(MAX_GAS_LIMIT_OPTION) + } + return bounty + } + + it('should not allow a solution if generation is incomplete', async () => { + const numberOfLocks = 1 + const byteSizeOfModulus = 1 + const bounty = await new OrderFindingBountyWithLockGeneration__factory(ethersSigner) + .deploy(numberOfLocks, byteSizeOfModulus) + const arbitrarySolution = '0x01' + const tx = submitSolution(0, arbitrarySolution, bounty) + await expect(tx).to.be.revertedWith('Lock has not been generated yet.') + }) + + it('should revert lock generation if already done', async () => { + const numberOfLocks = 1 + const byteSizeOfModulus = 1 + const bounty = await deployNewAccumulator(numberOfLocks, byteSizeOfModulus) + const tx = bounty.triggerLockAccumulation() + await expect(tx).to.be.revertedWith('Locks have already been generated') + }) + + it('should generate different locks on each deploy', async () => { + const numberOfLocks = 1 + const byteSizeOfModulus = 1 + const orderFindingBountyWithLockGenerations = await Promise.all(Array(2).fill(0) + .map(async () => deployNewAccumulator(numberOfLocks, byteSizeOfModulus))) + const lockComponents = (await Promise.all(Array(2).fill(0) + .map(async (_, i) => Promise.all(Array(2).fill(0) + .map(async (_, j) => (await orderFindingBountyWithLockGenerations[i].getLock(0))[j]))))) + .flat() + expect((new Set(lockComponents)).size).to.be.eq(lockComponents.length) + }) + + describe('correctly sized locks', () => { + let numberOfLocks: number + let byteSizeOfModulus: number + let orderFindingBountyWithLockGeneration: OrderFindingBountyWithLockGeneration + + async function expectCorrectLockSizes (): Promise { + const hexCharactersPerByte = 2 + const hexPrefixLength = 2 + const expectedLockLength = hexCharactersPerByte * byteSizeOfModulus + hexPrefixLength + + const locks = (await Promise.all(new Array(numberOfLocks).fill(0) + .map(async (_, i) => Promise.all(new Array(2).fill(0) + .map(async (_, j) => (await orderFindingBountyWithLockGeneration.getLock(i))[j]))))) + .flat() + expect(locks.every(lock => lock.length === expectedLockLength)).to.be.eq(true) + expect(locks.slice(1).every(lock => lock !== locks[0])).to.be.eq(true) + } + + it('should correctly handle the trivial case', async () => { + numberOfLocks = 1 + byteSizeOfModulus = 1 + orderFindingBountyWithLockGeneration = await deployNewAccumulator(numberOfLocks, byteSizeOfModulus) + await expectCorrectLockSizes() + }) + }) +}) diff --git a/assets/erc-7826/test/bounty-contracts/order-finding-bounty/order-finding-bounty-with-predetermined-locks/order-finding-bounty-with-predetermined-locks-utils.ts b/assets/erc-7826/test/bounty-contracts/order-finding-bounty/order-finding-bounty-with-predetermined-locks/order-finding-bounty-with-predetermined-locks-utils.ts new file mode 100644 index 0000000000..83e4fbe363 --- /dev/null +++ b/assets/erc-7826/test/bounty-contracts/order-finding-bounty/order-finding-bounty-with-predetermined-locks/order-finding-bounty-with-predetermined-locks-utils.ts @@ -0,0 +1,67 @@ +import { bytes } from '../../../solidityTypes' +import { ethers } from 'hardhat' +import { BigNumber, ContractTransaction } from 'ethers' +import BountyUtils, { + getLatestSolvedGasCost, + SolveAttemptResult, + solveBountyReturningUserBalanceBeforeFinalSolution, + submitSolution +} from '../../bounty-utils' +import { + BountyContract, + OrderFindingBountyWithPredeterminedLocks, + OrderFindingBountyWithPredeterminedLocks__factory +} from '../../../../typechain' +import { arrayify } from 'ethers/lib/utils' +import { Buffer } from 'buffer' + +class OrderFindingBountyWithPredeterminedLocksUtils extends BountyUtils { + private readonly locksAndKeys = [ + { + lock: [15, 7], + key: 4 + }, + { + lock: [23, 17], + key: 22 + } + ] + + public async deployBounty (): Promise { + const ethersSigner = ethers.provider.getSigner() + const locks = await this.getLocks() + const bounty = await new OrderFindingBountyWithPredeterminedLocks__factory(ethersSigner).deploy(locks.length) + for (let i = 0; i < locks.length; i++) { + await bounty.setLock(i, locks[i]) + } + return bounty + } + + public async getLocks (): Promise { + return Promise.resolve(this.locksAndKeys.map(x => x.lock.map(y => Buffer.from(arrayify(y))))) + } + + public async solveBounty (bounty: BountyContract, getUserBalance?: () => Promise): Promise { + return solveBountyReturningUserBalanceBeforeFinalSolution(this._getKeys(), bounty, getUserBalance) + } + + public async solveBountyPartially (bounty: BountyContract): Promise { + const primes = this._getKeys() + await submitSolution(0, primes[0], bounty) + } + + public async solveBountyIncorrectly (bounty: BountyContract): Promise { + const keys = this._getKeys() + return await submitSolution(1, keys[0], bounty) + } + + private _getKeys (): bytes[] { + return this.locksAndKeys.map(lockAndKeys => Buffer.from(arrayify(lockAndKeys.key))) + } + + public async getLatestSolvedGasCost (): Promise { + return getLatestSolvedGasCost(this.locksAndKeys.length) + } +} + +export default OrderFindingBountyWithPredeterminedLocksUtils diff --git a/assets/erc-7826/test/bounty-contracts/order-finding-bounty/order-finding-bounty-with-predetermined-locks/order-finding-bounty-with-predetermined-locks.test.ts b/assets/erc-7826/test/bounty-contracts/order-finding-bounty/order-finding-bounty-with-predetermined-locks/order-finding-bounty-with-predetermined-locks.test.ts new file mode 100644 index 0000000000..ebc64c1209 --- /dev/null +++ b/assets/erc-7826/test/bounty-contracts/order-finding-bounty/order-finding-bounty-with-predetermined-locks/order-finding-bounty-with-predetermined-locks.test.ts @@ -0,0 +1,6 @@ +import getBountyTest from '../../bounty-test-factory' +import OrderFindingBountyWithPredeterminedLocksUtils from './order-finding-bounty-with-predetermined-locks-utils' + +const bountyUtils = new OrderFindingBountyWithPredeterminedLocksUtils() + +describe('OrderFindingBountyWithPredeterminedLocks', getBountyTest(bountyUtils)) diff --git a/assets/erc-7826/test/bounty-contracts/prime-factoring-bounty/cost-of-solving-primes.test.ts b/assets/erc-7826/test/bounty-contracts/prime-factoring-bounty/cost-of-solving-primes.test.ts new file mode 100644 index 0000000000..1b3a38bf84 --- /dev/null +++ b/assets/erc-7826/test/bounty-contracts/prime-factoring-bounty/cost-of-solving-primes.test.ts @@ -0,0 +1,79 @@ +import { bytes } from '../../solidityTypes' +import { + PrimeFactoringBountyWithPredeterminedLocks, + PrimeFactoringBountyWithPredeterminedLocks__factory +} from '../../../typechain' +import { ethers } from 'hardhat' +import { BigNumber } from 'ethers' +import { arrayify } from 'ethers/lib/utils' +import { expect } from 'chai' +import { submitSolution } from '../bounty-utils' + +describe.skip('Test the cost of solving the prime factoring bounty', () => { + let bounty: PrimeFactoringBountyWithPredeterminedLocks + let solutions: bytes[][] + let gasUsed: BigNumber + + async function deployBounty (locks: bytes[]): Promise { + const ethersSigner = ethers.provider.getSigner() + const bounty = await new PrimeFactoringBountyWithPredeterminedLocks__factory(ethersSigner).deploy(locks.length) + for (let i = 0; i < locks.length; i++) { + await bounty.setLock(i, locks[i]) + } + return bounty + } + + beforeEach(async () => { + const numberOfLocks = 119 + const bytesPerPrime = 128 + gasUsed = BigNumber.from(0) + + const primesOf100 = [ + BigNumber.from(2), + BigNumber.from(2), + BigNumber.from(5), + BigNumber.from(5) + ] + const primeFactors = primesOf100 + const primesThatGoIntoLockThreeTimes = [ + BigNumber.from('0x9deb56589d3dbc359f4d7ad556cd6114e6e0d5d380d45aff59fe564fe2d0c7e7'), + BigNumber.from('0xb237e0a87baa96360e7faa432a40fd550cea247ad83198a08674b6af0c8aab1f'), + BigNumber.from('0x98b506e93598a98579c9ce06a99d65d5a7694d9d739c270d5fa04abb4518af7b'), + BigNumber.from('0xcc3422fbc329d582d216c4b4b879e4873a155864d9e95e6722136ac94c3fdb21') + ] + for (const num of primesThatGoIntoLockThreeTimes) { + for (let i = 0; i < 3; i++) primeFactors.push(num) + } + + let lockOf3072BitsWithKnownDecomposition = BigNumber.from(1) + for (const num of primeFactors) { + lockOf3072BitsWithKnownDecomposition = lockOf3072BitsWithKnownDecomposition.mul(num) + } + + const numberOfBits = (lockOf3072BitsWithKnownDecomposition.toHexString().length - 2) * 4 + expect(numberOfBits).to.be.eq(bytesPerPrime * 8 * 3) + + const locks = new Array(numberOfLocks).fill(0).map(() => lockOf3072BitsWithKnownDecomposition.toHexString()) + const solutionsPerLock = primeFactors.map(x => Buffer.from(arrayify(x.toHexString()))) + solutions = locks.map(() => solutionsPerLock) + + bounty = await deployBounty(locks) + }) + + it('should find the gas cost to solve all locks', async () => { + for (let i = 0; i < solutions.length; i++) { + const tx = await submitSolution(i, solutions[i], bounty) + const receipt = await tx.wait() + gasUsed = gasUsed.add(receipt.gasUsed) + } + expect(gasUsed).to.equal(BigNumber.from(0x1b2befab)) + }) + + it('should find the gas cost to solve 1 lock', async () => { + const arbitraryLockNumber = 0 + const tx = await submitSolution(arbitraryLockNumber, solutions[arbitraryLockNumber], bounty) + const receipt = await tx.wait() + gasUsed = gasUsed.add(receipt.gasUsed) + expect(gasUsed).to.equal(BigNumber.from(0x4b423d)) + }) +}) diff --git a/assets/erc-7826/test/bounty-contracts/prime-factoring-bounty/miller-rabin.test.ts b/assets/erc-7826/test/bounty-contracts/prime-factoring-bounty/miller-rabin.test.ts new file mode 100644 index 0000000000..307528d6aa --- /dev/null +++ b/assets/erc-7826/test/bounty-contracts/prime-factoring-bounty/miller-rabin.test.ts @@ -0,0 +1,31 @@ +import { ethers } from 'hardhat' +import { MillerRabinTestHelper, MillerRabinTestHelper__factory } from '../../../typechain' +import { expect } from 'chai' +import { Buffer } from 'buffer' +import { arrayify } from 'ethers/lib/utils' + +describe('Miller-Rabin Primality Test', () => { + const ethersSigner = ethers.provider.getSigner() + let millerRabin: MillerRabinTestHelper + + before(async () => { + millerRabin = await new MillerRabinTestHelper__factory(ethersSigner).deploy() + }) + + it('should correctly identify some known prime numbers', async () => { + const somePrimes = ['0x02', `0x${(6703).toString(16)}`, '0xf699ff9e4915187f931a87e4a4c2b8b0c7df644d1dca77fceaa026c01463327a3ebfbe836bc3535a8e7f9e5d37b638034dbc6c9310b0ee7a691cab4f997120886443452bd889045db3ad0ea130506c705f13abe62a2d9e0af5687d5da8c1f8a893609b2114bd1a03bf20195661172aafc733888bfb3443272e191382f574fcad'] + await checkPrimes(somePrimes, true) + }) + + it('should correctly identify some known composite numbers', async () => { + const someComposites = ['0x04', `0x${(21).toString(16)}`, '0xf699ff9e4915187f931a87e4a4c2b8b0c7df644d1dca77fceaa026c01463327a3ebfbe836bc3535a8e7f9e5d37b638034dbc6c9310b0ee7a691cab4f997120886443452bd889045db3ad0ea130506c705f13abe62a2d9e0af5687d5da8c1f8a893609b2114bd1a03bf20195661172aafc733888bfb3443272e191382f574fcac'] + await checkPrimes(someComposites, false) + }) + + async function checkPrimes (numbersAsHexStrings: string[], expectedToBePrime: boolean): Promise { + const resultsPromise = numbersAsHexStrings + .map(primeCandidate => millerRabin.isPrime(Buffer.from(arrayify(primeCandidate)))) + const results = await Promise.all(resultsPromise) + expect(results.every(x => x === expectedToBePrime)).to.be.eq(true) + } +}) diff --git a/assets/erc-7826/test/bounty-contracts/prime-factoring-bounty/prime-factoring-bounty-with-lock-generation/random-number-accumulator.test.ts b/assets/erc-7826/test/bounty-contracts/prime-factoring-bounty/prime-factoring-bounty-with-lock-generation/random-number-accumulator.test.ts new file mode 100644 index 0000000000..84df7bae94 --- /dev/null +++ b/assets/erc-7826/test/bounty-contracts/prime-factoring-bounty/prime-factoring-bounty-with-lock-generation/random-number-accumulator.test.ts @@ -0,0 +1,209 @@ +import { ethers } from 'hardhat' +import { RandomNumberAccumulator, RandomNumberAccumulator__factory } from '../../../../typechain' +import { expect } from 'chai' +import { BigNumber } from 'ethers' +import { arrayify } from 'ethers/lib/utils' + +describe.skip('RandomNumberAccumulator', () => { + const BYTES_PER_uint256 = 32 + const BITS_PER_BYTE = 8 + const MAX_GAS_LIMIT_OPTION = { gasLimit: BigNumber.from('0x1c9c380') } + + const ethersSigner = ethers.provider.getSigner() + let randomNumberAccumulator: RandomNumberAccumulator + + const _256BitPrimes = [ + '0xc66f06e1b45c9c55073ed83708f390c86fd13e874d211d405abe0d293682ff03', + '0xf6876683602570c564a79e91b1887a8264a2119dee04cccd947e5f9603afd80b', + '0xa926b2b3664fca1e784a66de9e0d2e8ca75cbb4832104d21892a692e9068b4a9' + ].map(hex => BigNumber.from(hex)) + + it('should not finish if the first random number is prime, but there are not enough bits', async () => { + const numberOfLocks = 1 + const primesPerLock = 1 + const bytesPerPrime = BYTES_PER_uint256 + 1 + randomNumberAccumulator = await new RandomNumberAccumulator__factory(ethersSigner).deploy(numberOfLocks, primesPerLock, bytesPerPrime) + + await randomNumberAccumulator.accumulate(_256BitPrimes[0]) + expect(await randomNumberAccumulator.isDone()).to.be.eq(false) + }) + + it('should set the first bit of the first number to 1', async () => { + const numberOfLocks = 1 + const primesPerLock = 1 + const bytesPerPrime = BYTES_PER_uint256 + randomNumberAccumulator = await new RandomNumberAccumulator__factory(ethersSigner).deploy(numberOfLocks, primesPerLock, bytesPerPrime) + + const _256BitPrimeWithoutLeadingBit = _256BitPrimes[0].mask(255) + await randomNumberAccumulator.accumulate(_256BitPrimeWithoutLeadingBit) + expect(await randomNumberAccumulator.isDone()).to.be.eq(true) + }) + + it('should not set the first bit to 1 after the first number', async () => { + const numberOfLocks = 1 + const primesPerLock = 1 + const bytesPerPrime = BYTES_PER_uint256 * 2 + randomNumberAccumulator = await new RandomNumberAccumulator__factory(ethersSigner).deploy(numberOfLocks, primesPerLock, bytesPerPrime) + + const _512BitPrimeWhereTheCenterBitIs0 = BigNumber.from(arrayify('0xdf122aa1a14be816462ac30f4074c042e899276cfdf4f1c1943ba244edbc904a03faf637e7d554021160496e96dc35afc16758473036077af0ecda7290509a89')) + const firstHalf = _512BitPrimeWhereTheCenterBitIs0.shr(BYTES_PER_uint256 * BITS_PER_BYTE) + const secondHalf = _512BitPrimeWhereTheCenterBitIs0.mask(BYTES_PER_uint256 * BITS_PER_BYTE) + await randomNumberAccumulator.accumulate(firstHalf, MAX_GAS_LIMIT_OPTION) + await randomNumberAccumulator.accumulate(secondHalf, MAX_GAS_LIMIT_OPTION) + expect(await randomNumberAccumulator.isDone()).to.be.eq(true) + }) + + it('should always set the last bit', async () => { + expect.fail() + }) + + it('should not accumulate if already done', async () => { + const numberOfLocks = 1 + const primesPerLock = 1 + const bytesPerPrime = BYTES_PER_uint256 + randomNumberAccumulator = await new RandomNumberAccumulator__factory(ethersSigner).deploy(numberOfLocks, primesPerLock, bytesPerPrime) + + await randomNumberAccumulator.accumulate(_256BitPrimes[0], MAX_GAS_LIMIT_OPTION) + const tx = randomNumberAccumulator.accumulate(_256BitPrimes[0], MAX_GAS_LIMIT_OPTION) + await expect(tx).to.be.revertedWith('Already accumulated enough bits') + }) + + it('should append sequential numbers to reach the required bytes', async () => { + const numberOfLocks = 1 + const primesPerLock = 1 + const bytesPerPrime = BYTES_PER_uint256 * 2 + randomNumberAccumulator = await new RandomNumberAccumulator__factory(ethersSigner).deploy(numberOfLocks, primesPerLock, bytesPerPrime) + + const _512BitPrime = BigNumber.from(arrayify('0xdf122aa1a14be816462ac30f4074c042e899276cfdf4f1c1943ba244edbc904a03faf637e7d554021160496e96dc35afc16758473036077af0ecda7290509a89')) + const firstHalf = _512BitPrime.shr(BYTES_PER_uint256 * BITS_PER_BYTE) + const secondHalf = _512BitPrime.mask(BYTES_PER_uint256 * BITS_PER_BYTE) + await randomNumberAccumulator.accumulate(firstHalf, MAX_GAS_LIMIT_OPTION) + expect(await randomNumberAccumulator.isDone()).to.be.eq(false) + await randomNumberAccumulator.accumulate(secondHalf, MAX_GAS_LIMIT_OPTION) + expect(await randomNumberAccumulator.isDone()).to.be.eq(true) + }) + + it('should slice off extra bits', async () => { + const numberOfLocks = 1 + const primesPerLock = 1 + const bytesPerPrime = 1 + randomNumberAccumulator = await new RandomNumberAccumulator__factory(ethersSigner).deploy(numberOfLocks, primesPerLock, bytesPerPrime) + + const oneBytePrime = 0xbf + const remainingBits = (BYTES_PER_uint256 - bytesPerPrime) * BITS_PER_BYTE + const primeWithAdditionalBitsThatMakeItComposite = BigNumber.from(oneBytePrime).shl(remainingBits) + await randomNumberAccumulator.accumulate(primeWithAdditionalBitsThatMakeItComposite) + expect(await randomNumberAccumulator.isDone()).to.be.eq(true) + }) + + it('should use the first and last primes given a composite in between when requiring two primes and one lock', async () => { + const numberOfLocks = 1 + const primesPerLock = 2 + randomNumberAccumulator = await new RandomNumberAccumulator__factory(ethersSigner).deploy(numberOfLocks, primesPerLock, BYTES_PER_uint256) + + const second256BitPrime = BigNumber.from('0xf6876683602570c564a79e91b1887a8264a2119dee04cccd947e5f9603afd80b') + const arbitraryComposite = 0x8 + await randomNumberAccumulator.accumulate(_256BitPrimes[0]) + await randomNumberAccumulator.accumulate(arbitraryComposite) + await randomNumberAccumulator.accumulate(second256BitPrime) + + const lockGenerated = BigNumber.from(await randomNumberAccumulator.locks(0)) + const lockExpected = BigNumber.from('0xbf17a49f966c36768e3538f08e090b67bf4047dc6d9b37fea73ba093280d5fc51abc03c6ea95cd422422d2202c9665d113b520cfd15bfb1588f2ac0f3ad87d21') + expect(await randomNumberAccumulator.isDone()).to.be.eq(true) + expect(lockGenerated.eq(lockExpected)).to.eq(true) + }) + + it('should use only the prime numbers and pair them correctly for two locks', async () => { + const numberOfLocks = 2 + const primesPerLock = 2 + randomNumberAccumulator = await new RandomNumberAccumulator__factory(ethersSigner).deploy(numberOfLocks, primesPerLock, BYTES_PER_uint256) + + const additionalPrime = '0xf891cd1b2f83e43a89b2f6f867e45faf8fbbe0c38b77e6d7f18b1db49752b05d' + const arbitraryComposite = 0x8 + const orderToSend = [ + _256BitPrimes[0], + _256BitPrimes[1], + _256BitPrimes[2], + arbitraryComposite, + additionalPrime + ].map(hex => BigNumber.from(hex)) + for (const num of orderToSend) { + await randomNumberAccumulator.accumulate(num, MAX_GAS_LIMIT_OPTION) + } + + const locksGenerated = (await Promise.all([0, 1] + .map(async lockNumber => randomNumberAccumulator.locks(lockNumber)))) + .map(lockGenerated => BigNumber.from(lockGenerated)) + const locksExpected = [ + '0xbf17a49f966c36768e3538f08e090b67bf4047dc6d9b37fea73ba093280d5fc51abc03c6ea95cd422422d2202c9665d113b520cfd15bfb1588f2ac0f3ad87d21', + '0xa43dd38ef64e0142358d27c4098a5df134a34bc870dce70fe3bce664a43726c453bf692b1c629b5581cd3f2cd8840d34161764df2b5ecd11bba9691fff5fd165' + ].map(hex => BigNumber.from(hex)) + expect(await randomNumberAccumulator.isDone()).to.be.eq(true) + + const matchExpected = locksGenerated.every((generatedLock, lockNumber) => generatedLock.eq(locksExpected[lockNumber])) + expect(matchExpected).to.eq(true) + }) + + describe('prime candidate chosen, but is not actually prime', () => { + beforeEach(async () => { + const numberOfLocks = 1 + const primesPerLock = 1 + randomNumberAccumulator = await new RandomNumberAccumulator__factory(ethersSigner).deploy(numberOfLocks, primesPerLock, BYTES_PER_uint256) + + const arbitraryCompositeNumber = 8 + await randomNumberAccumulator.accumulate(arbitraryCompositeNumber) + }) + + it('should not be marked done', async () => { + expect(await randomNumberAccumulator.isDone()).to.be.eq(false) + }) + + it('should reset for the next qubit', async () => { + await randomNumberAccumulator.accumulate(_256BitPrimes[0], MAX_GAS_LIMIT_OPTION) + expect(await randomNumberAccumulator.isDone()).to.be.eq(true) + }) + }) + + describe('distinct primes', () => { + it('should not allow the same prime in a row', async () => { + const numberOfLocks = 1 + const primesPerLock = 2 + randomNumberAccumulator = await new RandomNumberAccumulator__factory(ethersSigner).deploy(numberOfLocks, primesPerLock, BYTES_PER_uint256) + + for (let i = 0; i < 2; i++) await randomNumberAccumulator.accumulate(_256BitPrimes[0], MAX_GAS_LIMIT_OPTION) + + expect(await randomNumberAccumulator.locks(0)).to.eq('0x') + expect(await randomNumberAccumulator.isDone()).to.be.eq(false) + }) + + it('should allow the same prime in separate locks', async () => { + const numberOfLocks = 2 + const primesPerLock = 2 + randomNumberAccumulator = await new RandomNumberAccumulator__factory(ethersSigner).deploy(numberOfLocks, primesPerLock, BYTES_PER_uint256) + + await randomNumberAccumulator.accumulate(_256BitPrimes[0], MAX_GAS_LIMIT_OPTION) + await randomNumberAccumulator.accumulate(_256BitPrimes[1], MAX_GAS_LIMIT_OPTION) + await randomNumberAccumulator.accumulate(_256BitPrimes[0], MAX_GAS_LIMIT_OPTION) + await randomNumberAccumulator.accumulate(_256BitPrimes[2], MAX_GAS_LIMIT_OPTION) + + expect(BigNumber.from(await randomNumberAccumulator.locks(0)).eq(BigNumber.from('0xbf17a49f966c36768e3538f08e090b67bf4047dc6d9b37fea73ba093280d5fc51abc03c6ea95cd422422d2202c9665d113b520cfd15bfb1588f2ac0f3ad87d21'))).to.be.eq(true) + expect(BigNumber.from(await randomNumberAccumulator.locks(1)).eq(BigNumber.from('0x831d4a8a474abdd91f0e2b3f1b0371457e19d9532415943af0a70e1bb9567982ec866c99ade6c800d1310d5ab97f82ac7c659e02b4df6b0307bf8cbc610074fb'))).to.be.eq(true) + expect(await randomNumberAccumulator.isDone()).to.be.eq(true) + }) + + it('should require distinct locks', async () => { + const numberOfLocks = 2 + const primesPerLock = 2 + randomNumberAccumulator = await new RandomNumberAccumulator__factory(ethersSigner).deploy(numberOfLocks, primesPerLock, BYTES_PER_uint256) + + for (let i = 0; i < 2; i++) { + await randomNumberAccumulator.accumulate(_256BitPrimes[0], MAX_GAS_LIMIT_OPTION) + await randomNumberAccumulator.accumulate(_256BitPrimes[1], MAX_GAS_LIMIT_OPTION) + } + + expect(BigNumber.from(await randomNumberAccumulator.locks(0)).eq(BigNumber.from('0xbf17a49f966c36768e3538f08e090b67bf4047dc6d9b37fea73ba093280d5fc51abc03c6ea95cd422422d2202c9665d113b520cfd15bfb1588f2ac0f3ad87d21'))).to.be.eq(true) + expect(await randomNumberAccumulator.locks(1)).to.be.eq('0x') + expect(await randomNumberAccumulator.isDone()).to.be.eq(false) + }) + }) +}) diff --git a/assets/erc-7826/test/bounty-contracts/prime-factoring-bounty/prime-factoring-bounty-with-predetermined-locks/prime-factoring-bounty-with-predetermined-locks-utils.ts b/assets/erc-7826/test/bounty-contracts/prime-factoring-bounty/prime-factoring-bounty-with-predetermined-locks/prime-factoring-bounty-with-predetermined-locks-utils.ts new file mode 100644 index 0000000000..ca876664e1 --- /dev/null +++ b/assets/erc-7826/test/bounty-contracts/prime-factoring-bounty/prime-factoring-bounty-with-predetermined-locks/prime-factoring-bounty-with-predetermined-locks-utils.ts @@ -0,0 +1,83 @@ +import { bytes } from '../../../solidityTypes' +import { ethers } from 'hardhat' +import { BigNumber, ContractTransaction } from 'ethers' +import BountyUtils, { + getLatestSolvedGasCost, + SolveAttemptResult, + solveBountyReturningUserBalanceBeforeFinalSolution, + submitSolution +} from '../../bounty-utils' +import { + BountyContract, + PrimeFactoringBountyWithPredeterminedLocks, + PrimeFactoringBountyWithPredeterminedLocks__factory +} from '../../../../typechain' +import { arrayify, defaultAbiCoder } from 'ethers/lib/utils' +import { Buffer } from 'buffer' + +class PrimeFactoringBountyWithPredeterminedLocksUtils extends BountyUtils { + private readonly locksAndKeys = [ + { + lock: '0x79cf5b2576e8227d0687fc5fba1533966056e98ae5dc79a81aacf78cf1b8b7adca788784349ac4a911fe5ae53cb342437082911dc767fc6f455ce0feb991d7db', + keys: [ + '0x98b506e93598a98579c9ce06a99d65d5a7694d9d739c270d5fa04abb4518af7b', + '0xcc3422fbc329d582d216c4b4b879e4873a155864d9e95e6722136ac94c3fdb21' + ] + }, + { + lock: '0x6df01a2f04a6364b150e5e628b856481c14973612d2e513b4eb082275f02a86176493753d0e1d2f6da721e6b90fb3c697068ecae9fe8f8a908a8bf01835581f9', + keys: [ + '0x9deb56589d3dbc359f4d7ad556cd6114e6e0d5d380d45aff59fe564fe2d0c7e7', + '0xb237e0a87baa96360e7faa432a40fd550cea247ad83198a08674b6af0c8aab1f' + ] + } + ] + + public async deployBounty (): Promise { + const ethersSigner = ethers.provider.getSigner() + const locks = await this.getLocks() + const bounty = await new PrimeFactoringBountyWithPredeterminedLocks__factory(ethersSigner).deploy(locks.length) + for (let i = 0; i < locks.length; i++) { + await bounty.setLock(i, locks[i]) + } + return bounty + } + + public async getLocks (): Promise { + return Promise.resolve(this.locksAndKeys.map(x => [Buffer.from(arrayify(x.lock))])) + } + + public async solveBounty (bounty: BountyContract, getUserBalance?: () => Promise): Promise { + const solutions = this._getPrimes().map(primes => this.encodeByteArray(primes)) + return solveBountyReturningUserBalanceBeforeFinalSolution(solutions, bounty, getUserBalance) + } + + public async solveBountyPartially (bounty: BountyContract): Promise { + const primes = this._getPrimes() + const solution = this.encodeByteArray(primes[0]) + await submitSolution(0, solution, bounty) + } + + public async solveBountyIncorrectly (bounty: BountyContract): Promise { + const primes = this._getPrimes() + const solution = this.encodeByteArray(primes[0]) + return await submitSolution(1, solution, bounty) + } + + private encodeByteArray (value: bytes[]): bytes { + return defaultAbiCoder.encode(['bytes[]'], [value]) + } + + private _getPrimes (): bytes[][] { + const primes = this.locksAndKeys.map(lockAndKeys => lockAndKeys.keys) + return primes.map(primesForLock => + primesForLock.map(prime => + Buffer.from(arrayify(prime)))) + } + + public async getLatestSolvedGasCost (): Promise { + return getLatestSolvedGasCost(this.locksAndKeys.length) + } +} + +export default PrimeFactoringBountyWithPredeterminedLocksUtils diff --git a/assets/erc-7826/test/bounty-contracts/prime-factoring-bounty/prime-factoring-bounty-with-predetermined-locks/prime-factoring-bounty-with-predetermined-locks.test.ts b/assets/erc-7826/test/bounty-contracts/prime-factoring-bounty/prime-factoring-bounty-with-predetermined-locks/prime-factoring-bounty-with-predetermined-locks.test.ts new file mode 100644 index 0000000000..25fda76e97 --- /dev/null +++ b/assets/erc-7826/test/bounty-contracts/prime-factoring-bounty/prime-factoring-bounty-with-predetermined-locks/prime-factoring-bounty-with-predetermined-locks.test.ts @@ -0,0 +1,6 @@ +import getBountyTest from '../../bounty-test-factory' +import PrimeFactoringBountyWithPredeterminedLocksUtils from './prime-factoring-bounty-with-predetermined-locks-utils' + +const bountyUtils = new PrimeFactoringBountyWithPredeterminedLocksUtils() + +describe('PrimeFactoringBountyWithPredeterminedLocks', getBountyTest(bountyUtils)) diff --git a/assets/erc-7826/test/bounty-contracts/prime-factoring-bounty/prime-factoring-bounty-with-rsa-ufo/prime-factoring-bounty-with-rsa-ufo.test.ts b/assets/erc-7826/test/bounty-contracts/prime-factoring-bounty/prime-factoring-bounty-with-rsa-ufo/prime-factoring-bounty-with-rsa-ufo.test.ts new file mode 100644 index 0000000000..e8b5d44e2c --- /dev/null +++ b/assets/erc-7826/test/bounty-contracts/prime-factoring-bounty/prime-factoring-bounty-with-rsa-ufo/prime-factoring-bounty-with-rsa-ufo.test.ts @@ -0,0 +1,68 @@ +import { + PrimeFactoringBountyWithRsaUfo__factory, PrimeFactoringBountyWithRsaUfo +} from '../../../../typechain' +import { ethers } from 'hardhat' +import { expect } from 'chai' + +describe('PrimeFactoringBountyWithRsaUfo', () => { + const ethersSigner = ethers.provider.getSigner() + + async function deployNewRsaUfoAccumulator (numberOfLocks: number, bytesPerPrime: number): Promise { + const bounty = await new PrimeFactoringBountyWithRsaUfo__factory(ethersSigner).deploy(numberOfLocks, bytesPerPrime) + while (!(await bounty.callStatic.generationIsDone())) { + await bounty.triggerLockAccumulation() + } + return bounty + } + + it('should revert lock generation if already done', async () => { + const numberOfLocks = 1 + const bytesPerPrime = 1 + const bounty = await deployNewRsaUfoAccumulator(numberOfLocks, bytesPerPrime) + const tx = bounty.triggerLockAccumulation() + await expect(tx).to.be.revertedWith('Locks have already been generated') + }) + + it('should generate different locks on each deploy', async () => { + const numberOfLocks = 1 + const bytesPerPrime = 1 + const primeFactoringBountyWithRsaUfos = await Promise.all(Array(2).fill(0) + .map(async () => deployNewRsaUfoAccumulator(numberOfLocks, bytesPerPrime))) + const firstLock = (await primeFactoringBountyWithRsaUfos[0].getLock(0))[0] + const secondLock = (await primeFactoringBountyWithRsaUfos[1].getLock(0))[0] + expect(firstLock).to.not.be.eq(secondLock) + }) + + describe('correctly sized locks', () => { + let numberOfLocks: number + let bytesPerPrime: number + let primeFactoringBountyWithRsaUfo: PrimeFactoringBountyWithRsaUfo + + async function expectCorrectLockSizes (): Promise { + const hexCharactersPerByte = 2 + const lockBytesPerPrimeByte = 3 + const hexPrefixLength = 2 + const expectedLockLength = hexCharactersPerByte * lockBytesPerPrimeByte * bytesPerPrime + hexPrefixLength + + const locks = await Promise.all(new Array(numberOfLocks).fill(0) + .map(async (_, i) => (await primeFactoringBountyWithRsaUfo.getLock(i))[0])) + expect(locks.length).to.be.eq(numberOfLocks) + expect(locks.every(lock => lock.length === expectedLockLength)).to.be.eq(true) + expect(locks.slice(1).every(lock => lock !== locks[0])).to.be.eq(true) + } + + it('should correctly handle the trivial case', async () => { + numberOfLocks = 1 + bytesPerPrime = 1 + primeFactoringBountyWithRsaUfo = await deployNewRsaUfoAccumulator(numberOfLocks, bytesPerPrime) + await expectCorrectLockSizes() + }) + + it('should correctly handle a larger case', async () => { + numberOfLocks = 7 + bytesPerPrime = 8 + primeFactoringBountyWithRsaUfo = await deployNewRsaUfoAccumulator(numberOfLocks, bytesPerPrime) + await expectCorrectLockSizes() + }) + }) +}) diff --git a/assets/erc-7826/test/bounty-contracts/signature-bounty/signature-bounty-with-lock-generation/signature-bounty-with-lock-generation.test.ts b/assets/erc-7826/test/bounty-contracts/signature-bounty/signature-bounty-with-lock-generation/signature-bounty-with-lock-generation.test.ts new file mode 100644 index 0000000000..0ea12feef1 --- /dev/null +++ b/assets/erc-7826/test/bounty-contracts/signature-bounty/signature-bounty-with-lock-generation/signature-bounty-with-lock-generation.test.ts @@ -0,0 +1,75 @@ +import { + SignatureBountyWithLockGeneration__factory, SignatureBountyWithLockGeneration +} from '../../../../typechain' +import { ethers } from 'hardhat' +import { expect } from 'chai' + +describe('SignatureBountyWithLockGeneration', () => { + const ethersSigner = ethers.provider.getSigner() + + async function deployNewAccumulator (numberOfLocks: number): Promise { + const bounty = await new SignatureBountyWithLockGeneration__factory(ethersSigner).deploy(numberOfLocks) + while (!(await bounty.callStatic.generationIsDone())) { + await bounty.triggerLockAccumulation() + } + return bounty + } + + it('should revert lock generation if already done', async () => { + const numberOfLocks = 1 + const bounty = await deployNewAccumulator(numberOfLocks) + const tx = bounty.triggerLockAccumulation() + await expect(tx).to.be.revertedWith('Locks have already been generated') + }) + + it('should generate different locks on each deploy', async () => { + const numberOfLocks = 1 + const SignatureBountyWithLockGenerations = await Promise.all(Array(2).fill(0) + .map(async () => deployNewAccumulator(numberOfLocks))) + const firstLock = (await SignatureBountyWithLockGenerations[0].getLock(0))[0] + const secondLock = (await SignatureBountyWithLockGenerations[1].getLock(0))[0] + expect(firstLock).to.not.be.eq(secondLock) + }) + + describe('correctly sized locks', () => { + const publicKeyByteSize = 20 + const bytesPerMessage = 32 + const hexCharactersPerByte = 2 + const hexPrefixLength = 2 + + let numberOfLocks: number + let bounty: SignatureBountyWithLockGeneration + + async function expectCorrectSizes (): Promise { + await expectCorrectLockSizes() + await expectCorrectMessageSize() + } + + async function expectCorrectLockSizes (): Promise { + const expectedLockLength = hexCharactersPerByte * publicKeyByteSize + hexPrefixLength + const locks = await Promise.all(new Array(numberOfLocks).fill(0) + .map(async (_, i) => (await bounty.getLock(i))[0])) + expect(locks.length).to.be.eq(numberOfLocks) + expect(locks.every(lock => lock.length === expectedLockLength)).to.be.eq(true) + expect(locks.slice(1).every(lock => lock !== locks[0])).to.be.eq(true) + } + + async function expectCorrectMessageSize (): Promise { + const expectedMessageLength = hexCharactersPerByte * bytesPerMessage + hexPrefixLength + const message = (await bounty.message()) + expect(message.length).to.eq(expectedMessageLength) + } + + it('should correctly handle the trivial case', async () => { + numberOfLocks = 1 + bounty = await deployNewAccumulator(numberOfLocks) + await expectCorrectSizes() + }) + + it('should correctly handle a larger case', async () => { + numberOfLocks = 7 + bounty = await deployNewAccumulator(numberOfLocks) + await expectCorrectSizes() + }) + }) +}) diff --git a/assets/erc-7826/test/bounty-contracts/signature-bounty/signature-bounty-with-predetermined-locks/signature-bounty-with-predetermined-locks-utils.ts b/assets/erc-7826/test/bounty-contracts/signature-bounty/signature-bounty-with-predetermined-locks/signature-bounty-with-predetermined-locks-utils.ts new file mode 100644 index 0000000000..49f85d3812 --- /dev/null +++ b/assets/erc-7826/test/bounty-contracts/signature-bounty/signature-bounty-with-predetermined-locks/signature-bounty-with-predetermined-locks-utils.ts @@ -0,0 +1,85 @@ +import { JsonRpcSigner } from '@ethersproject/providers/src.ts/json-rpc-provider' +import { bytes } from '../../../solidityTypes' +import { ethers, web3 } from 'hardhat' +import { BountyContract, SignatureBountyWithPredeterminedLocks, SignatureBountyWithPredeterminedLocks__factory } from '../../../../typechain' +import { BigNumber, ContractTransaction } from 'ethers' +import BountyUtils, { + getLatestSolvedGasCost, + SolveAttemptResult, + solveBountyReturningUserBalanceBeforeFinalSolution, + submitSolution +} from '../../bounty-utils' +import { arrayify } from 'ethers/lib/utils' +import { Buffer } from 'buffer' + +class SignatureBountyWithPredeterminedLocksUtils extends BountyUtils { + private readonly numberOfLocks: number + private readonly _publicKeys: bytes[][] + private _signatures: string[] + private readonly _signers: JsonRpcSigner[] + + constructor (numberOfLocks: number = 3) { + super() + this.numberOfLocks = numberOfLocks + this._publicKeys = [] + this._signatures = [] + this._signers = [] + } + + public async deployBounty (): Promise { + const ethersSigner = ethers.provider.getSigner() + const message = this.arbitraryMessage() + return await new SignatureBountyWithPredeterminedLocks__factory(ethersSigner).deploy(await this.getLocks(), message) + } + + public async getLocks (): Promise { + if (this._publicKeys.length === 0) { + for (const signer of this.signers) { + this._publicKeys.push([Buffer.from(arrayify(await signer.getAddress()))]) + } + } + return this._publicKeys + } + + public async solveBounty (bounty: SignatureBountyWithPredeterminedLocks, getUserBalance: () => Promise): Promise { + const signatures = await this.getSignatures(this.arbitraryMessage()) + return solveBountyReturningUserBalanceBeforeFinalSolution(signatures, bounty, getUserBalance) + } + + public async solveBountyPartially (bounty: SignatureBountyWithPredeterminedLocks): Promise { + const signatures = await this.getSignatures(this.arbitraryMessage()) + await submitSolution(0, signatures[0], bounty) + } + + public async solveBountyIncorrectly (bounty: SignatureBountyWithPredeterminedLocks): Promise { + const signatures = await this.getSignatures(this.arbitraryMessage()) + return submitSolution(1, signatures[0], bounty) + } + + private async getSignatures (message: string): Promise { + if (this._signatures.length === 0) { + this._signatures = await Promise.all(this.signers.map(async (signer) => + await web3.eth.sign(message, await signer.getAddress()))) + } + return this._signatures + } + + private arbitraryMessage (): string { + return web3.utils.sha3('arbitrary') as string + } + + private get signers (): JsonRpcSigner[] { + if (this._signers.length === 0) { + for (let i = 0; i < this.numberOfLocks; i++) { + this._signers.push(ethers.provider.getSigner(i)) + } + } + return this._signers + } + + public async getLatestSolvedGasCost (): Promise { + return getLatestSolvedGasCost(this._signatures.length) + } +} + +export default SignatureBountyWithPredeterminedLocksUtils diff --git a/assets/erc-7826/test/bounty-contracts/signature-bounty/signature-bounty-with-predetermined-locks/signature-bounty-with-predetermined-locks.test.ts b/assets/erc-7826/test/bounty-contracts/signature-bounty/signature-bounty-with-predetermined-locks/signature-bounty-with-predetermined-locks.test.ts new file mode 100644 index 0000000000..7051e42dc4 --- /dev/null +++ b/assets/erc-7826/test/bounty-contracts/signature-bounty/signature-bounty-with-predetermined-locks/signature-bounty-with-predetermined-locks.test.ts @@ -0,0 +1,6 @@ +import getBountyTests from '../../bounty-test-factory' +import SignatureBountyUtils from './signature-bounty-with-predetermined-locks-utils' + +const bountyUtils = new SignatureBountyUtils() + +describe('SignatureBountyWithPredeterminedLocks', getBountyTests(bountyUtils)) diff --git a/assets/erc-7826/test/bounty-contracts/support/random-bytes-accumulator.test.ts b/assets/erc-7826/test/bounty-contracts/support/random-bytes-accumulator.test.ts new file mode 100644 index 0000000000..8af64c93f6 --- /dev/null +++ b/assets/erc-7826/test/bounty-contracts/support/random-bytes-accumulator.test.ts @@ -0,0 +1,172 @@ +import { RandomBytesAccumulatorTestHelper, RandomBytesAccumulatorTestHelper__factory } from '../../../typechain' +import { ethers } from 'hardhat' +import { arrayify } from 'ethers/lib/utils' +import { expect } from 'chai' +import { Buffer } from 'buffer' + +describe('RandomBytesAccumulator', () => { + const ethersSigner = ethers.provider.getSigner() + let testHelper: RandomBytesAccumulatorTestHelper + + async function deployNewRandomBytesAccumulator (numberOfLocks: number, bytesPerPrime: number): Promise { + return await new RandomBytesAccumulatorTestHelper__factory(ethersSigner).deploy(numberOfLocks, bytesPerPrime) + } + + async function expectDone (expectedValue: boolean): Promise { + expect((await testHelper.accumulator()).generationIsDone).to.be.eq(expectedValue) + } + + async function expectLock (lockNumber: number, expectedValue: string): Promise { + const accumulator = await testHelper.accumulator() + const lockValue = accumulator.locks.vals[lockNumber][0] + expect(lockValue).to.be.eq(expectedValue) + } + + describe('exact right size input', () => { + beforeEach(async () => { + const numberOfLocks = 1 + const bytesPerPrime = 1 + testHelper = await deployNewRandomBytesAccumulator(numberOfLocks, bytesPerPrime) + + await testHelper.triggerAccumulate('0x02') + }) + + it('should be marked as done', async () => { + await expectDone(true) + }) + + it('should have a lock matching the input', async () => { + await expectLock(0, '0x02') + }) + }) + + describe('slicing off extra bytes', () => { + beforeEach(async () => { + const numberOfLocks = 1 + const bytesPerPrime = 1 + testHelper = await deployNewRandomBytesAccumulator(numberOfLocks, bytesPerPrime) + + await testHelper.triggerAccumulate(Buffer.concat([Buffer.from(arrayify('0x02')), Buffer.from(arrayify('0xa6'))])) + }) + + it('should be marked as done', async () => { + await expectDone(true) + }) + + it('should have a lock with only the necessary bytes', async () => { + await expectLock(0, '0x02') + }) + }) + + describe('multiple accumulations per lock', () => { + beforeEach(async () => { + const numberOfLocks = 1 + const bytesPerPrime = 2 + testHelper = await deployNewRandomBytesAccumulator(numberOfLocks, bytesPerPrime) + + await testHelper.triggerAccumulate('0x02') + }) + + describe('first accumulation', () => { + it('should not be marked as done', async () => { + await expectDone(false) + }) + + it('should have no locks', async () => { + const accumulator = await testHelper.accumulator() + expect(accumulator.locks.vals[0].length).to.eq(0) + }) + }) + + describe('second accumulation', () => { + beforeEach(async () => { + await testHelper.triggerAccumulate('0xa6') + }) + + it('should be marked as done', async () => { + await expectDone(true) + }) + + it('should have a lock equal to both inputs', async () => { + await expectLock(0, '0x02a6') + }) + }) + }) + + describe('multiple locks', () => { + beforeEach(async () => { + const numberOfLocks = 2 + const bytesPerPrime = 1 + testHelper = await deployNewRandomBytesAccumulator(numberOfLocks, bytesPerPrime) + + await testHelper.triggerAccumulate('0x02') + }) + + describe('first accumulation', () => { + it('should not be marked as done', async () => { + await expectDone(false) + }) + + it('should have the first lock equal to the input', async () => { + await expectLock(0, '0x02') + }) + + it('should have no second lock', async () => { + const accumulator = await testHelper.accumulator() + expect(accumulator.locks.vals[1].length).to.eq(0) + }) + }) + + describe('second accumulation', () => { + beforeEach(async () => { + await testHelper.triggerAccumulate('0xa6') + }) + + it('should be marked as done', async () => { + await expectDone(true) + }) + + it('should have the first lock equal to the first input', async () => { + await expectLock(0, '0x02') + }) + + it('should have the second lock equal to the second input', async () => { + await expectLock(1, '0xa6') + }) + }) + }) + + describe('already done', () => { + beforeEach(async () => { + const numberOfLocks = 1 + const bytesPerPrime = 1 + testHelper = await deployNewRandomBytesAccumulator(numberOfLocks, bytesPerPrime) + + await testHelper.triggerAccumulate('0x02') + }) + + describe('first accumulation', () => { + it('should be marked as done', async () => { + await expectDone(true) + }) + + it('should have the first lock equal to the input', async () => { + await expectLock(0, '0x02') + }) + }) + + describe('unnecessary, additional accumulation', () => { + beforeEach(async () => { + await testHelper.triggerAccumulate('0xa6') + }) + + it('should be marked as done', async () => { + await expectDone(true) + }) + + it('should have the first lock equal to the first input', async () => { + await expectLock(0, '0x02') + }) + }) + }) +}) diff --git a/assets/erc-7826/test/bounty-fallback-account/UserOpLamport.ts b/assets/erc-7826/test/bounty-fallback-account/UserOpLamport.ts new file mode 100644 index 0000000000..5225b0c291 --- /dev/null +++ b/assets/erc-7826/test/bounty-fallback-account/UserOpLamport.ts @@ -0,0 +1,11 @@ +import { UserOperation } from '../UserOperation' +import { getUserOpHash } from '../UserOp' +import { WalletLamport } from './wallet-lamport' + +export function signUserOpLamport (op: UserOperation, signer: WalletLamport, entryPoint: string, chainId: number): UserOperation { + const message = getUserOpHash(op, entryPoint, chainId) + return { + ...op, + signature: signer.signMessageLamport(message) + } +} diff --git a/assets/erc-7826/test/bounty-fallback-account/bounty-fallback-account.test.ts b/assets/erc-7826/test/bounty-fallback-account/bounty-fallback-account.test.ts new file mode 100644 index 0000000000..fbc32c4d85 --- /dev/null +++ b/assets/erc-7826/test/bounty-fallback-account/bounty-fallback-account.test.ts @@ -0,0 +1,223 @@ +import { ethers } from 'hardhat' +import { expect } from 'chai' +import { + BountyFallbackAccount, BountyFallbackAccountFactory, + BountyFallbackAccountFactory__factory, SignatureBounty, + TestUtil, + TestUtil__factory +} from '../../typechain' +import { + createAddress, + getBalance, + isDeployed, + ONE_ETH, + HashZero +} from '../testutils' +import { fillUserOpDefaults, getUserOpHash, packUserOp } from '../UserOp' +import { arrayify, parseEther } from 'ethers/lib/utils' +import { UserOperation } from '../UserOperation' +import { + createAccountLamport, + createAccountOwnerLamport +} from './testutils-lamport' +import { signUserOpLamport } from './UserOpLamport' +import { WalletLamport } from './wallet-lamport' +import { generateLamportKeys } from './lamport-utils' +import SignatureBountyUtils from '../bounty-contracts/signature-bounty/signature-bounty-with-predetermined-locks/signature-bounty-with-predetermined-locks-utils' +import { BigNumber } from 'ethers' + +const EDCSA_LENGTH = 65 + +describe('BountyFallbackAccount', function () { + const entryPoint = '0x'.padEnd(42, '2') + let accounts: string[] + let testUtil: TestUtil + let accountOwner: WalletLamport + const ethersSigner = ethers.provider.getSigner() + + const bountyUtils = new SignatureBountyUtils() + let bounty: SignatureBounty + + beforeEach(async function () { + bounty = await bountyUtils.deployBounty() + + accounts = await ethers.provider.listAccounts() + // ignore in geth.. this is just a sanity test. should be refactored to use a single-account mode.. + if (accounts.length < 2) this.skip() + testUtil = await new TestUtil__factory(ethersSigner).deploy() + accountOwner = createAccountOwnerLamport() + }) + + async function createFirstAccountLamport (): Promise<{ + proxy: BountyFallbackAccount + accountFactory: BountyFallbackAccountFactory + implementation: string + }> { + const keysLamport = generateLamportKeys() + return await createAccountLamport(ethers.provider.getSigner(), accounts[0], keysLamport.publicKeys, bounty.address, entryPoint) + } + + it('owner should be able to call transfer', async () => { + const { proxy: account } = await createFirstAccountLamport() + await ethersSigner.sendTransaction({ from: accounts[0], to: account.address, value: parseEther('2') }) + await account.execute(accounts[2], ONE_ETH, '0x') + }) + + it('other account should not be able to call transfer', async () => { + const { proxy: account } = await createFirstAccountLamport() + await expect(account.connect(ethers.provider.getSigner(1)).execute(accounts[2], ONE_ETH, '0x')) + .to.be.revertedWith('account: not Owner or EntryPoint') + }) + + it('should pack in js the same as solidity', async () => { + const op = await fillUserOpDefaults({ sender: accounts[0] }) + const packed = packUserOp(op) + expect(await testUtil.packUserOp(op)).to.equal(packed) + }) + + describe('#validateUserOp', () => { + let account: BountyFallbackAccount + let userOpHash: string + let preBalance: number + let expectedPay: number + + const actualGasPrice = 1e9 + + let nonceTracker: number + + let getUserOpLamport: () => UserOperation + let userOpLamportInitial: UserOperation + let userOpNoLamport: UserOperation + + beforeEach(async () => { + nonceTracker = 0 + + // that's the account of ethersSigner + const entryPoint = accounts[2]; + ({ proxy: account } = await createAccountLamport( + await ethers.getSigner(entryPoint), + accountOwner.baseWallet.address, + accountOwner.lamportKeys.publicKeys, + bounty.address, + entryPoint)) + await ethersSigner.sendTransaction({ from: accounts[0], to: account.address, value: parseEther('0.2') }) + const callGasLimit = 200000 + const verificationGasLimit = 100000 + const maxFeePerGas = 3e9 + const chainId = await ethers.provider.getNetwork().then(net => net.chainId) + + getUserOpLamport = () => signUserOpLamport(fillUserOpDefaults({ + sender: account.address, + callGasLimit, + verificationGasLimit, + maxFeePerGas + }), accountOwner, entryPoint, chainId) + + userOpLamportInitial = getUserOpLamport() + userOpHash = await getUserOpHash(userOpLamportInitial, entryPoint, chainId) + + userOpNoLamport = { + ...userOpLamportInitial, + signature: Buffer.concat([ + Buffer.from(arrayify(userOpLamportInitial.signature)).slice(0, EDCSA_LENGTH), + Buffer.from(new Array(userOpLamportInitial.signature.length - EDCSA_LENGTH).fill(0)) + ]) + } + + expectedPay = actualGasPrice * (callGasLimit + verificationGasLimit) + preBalance = await getBalance(account.address) + }) + + describe('before bounty is solved', function () { + beforeEach(async () => { + const ret = await account.validateUserOp(userOpNoLamport, userOpHash, expectedPay, { gasPrice: actualGasPrice }) + await ret.wait() + ++nonceTracker + }) + + it('should pay', async () => { + const postBalance = await getBalance(account.address) + expect(preBalance - postBalance).to.eql(expectedPay) + }) + + it('should increment nonce', async () => { + expect(await account.nonce()).to.equal(1) + }) + + it('should reject same TX on nonce error', async () => { + await expect(account.validateUserOp(userOpNoLamport, userOpHash, 0)).to.revertedWith('invalid nonce') + }) + + it('should return NO_SIG_VALIDATION on wrong ECDSA signature', async () => { + const deadline = await account.callStatic.validateUserOp({ ...userOpNoLamport, nonce: nonceTracker }, HashZero, 0) + expect(deadline).to.eq(1) + }) + + it('should return 0 on correct ECDSA signature but incorrect Lamport signature', async () => { + const deadline = await account.callStatic.validateUserOp({ ...userOpNoLamport, nonce: nonceTracker }, userOpHash, 0) + expect(deadline).to.eq(0) + }) + }) + + describe('after bounty is solved', () => { + beforeEach(async () => { + await bountyUtils.solveBounty(bounty) + }) + + it('should return NO_SIG_VALIDATION on wrong lamport signature', async () => { + const deadline = await account.callStatic.validateUserOp({ ...userOpNoLamport, nonce: nonceTracker }, userOpHash, 0) + expect(deadline).to.eq(1) + }) + + it('should return 0 on correct lamport signature', async () => { + const deadline = await account.callStatic.validateUserOp({ ...userOpLamportInitial, nonce: nonceTracker }, userOpHash, 0) + expect(deadline).to.eq(0) + }) + }) + + describe('lamport signature is updated', () => { + async function updateSignature (userOpLamport: UserOperation): Promise { + const txUsingFirstSignature = await account.validateUserOp({ ...userOpLamport, nonce: nonceTracker }, userOpHash, 0) + await txUsingFirstSignature.wait() + ++nonceTracker + } + + async function testSignature (userOpLamport: UserOperation): Promise { + return await account.callStatic.validateUserOp({ ...userOpLamport, nonce: nonceTracker }, userOpHash, 0) + } + + beforeEach(async () => { + await updateSignature(userOpLamportInitial) + await bountyUtils.solveBounty(bounty) + }) + + it('should not allow same lamport signature twice', async () => { + const txUsingFirstSignatureAgain = await testSignature(userOpLamportInitial) + expect(txUsingFirstSignatureAgain).to.eq(1) + }) + + it('should require updated lamport signature on subsequent transaction', async () => { + const txUsingUpdatedSignature = await testSignature(getUserOpLamport()) + expect(txUsingUpdatedSignature).to.eq(0) + }) + + it('should also update the lamport key after bounty is solved', async () => { + await updateSignature(getUserOpLamport()) + const txUsingUpdatedSignature = await testSignature(getUserOpLamport()) + expect(txUsingUpdatedSignature).to.eq(0) + }) + }) + }) + + context('BountyFallbackWalletFactory', () => { + it('sanity: check deployer', async () => { + const ownerAddr = createAddress() + const lamportKeys = generateLamportKeys() + const deployer = await new BountyFallbackAccountFactory__factory(ethersSigner).deploy(entryPoint) + const target = await deployer.callStatic.createAccount(ownerAddr, 1234, lamportKeys.publicKeys, bounty.address) + expect(await isDeployed(target)).to.eq(false) + await deployer.createAccount(ownerAddr, 1234, lamportKeys.publicKeys, bounty.address) + expect(await isDeployed(target)).to.eq(true) + }) + }) +}) diff --git a/assets/erc-7826/test/bounty-fallback-account/buffer-bit-utils.test.ts b/assets/erc-7826/test/bounty-fallback-account/buffer-bit-utils.test.ts new file mode 100644 index 0000000000..5745752158 --- /dev/null +++ b/assets/erc-7826/test/bounty-fallback-account/buffer-bit-utils.test.ts @@ -0,0 +1,11 @@ +import { getReversedBits } from './buffer-bit-utils' +import { expect } from 'chai' + +describe('BufferBitUtils', () => { + describe('Reversed Bits', () => { + it('should return 1 when given a buffer of 5 wanting 2 bits', () => { + const bits = getReversedBits(Buffer.from([5]), 2) + expect(bits).to.eql([1, 0]) + }) + }) +}) diff --git a/assets/erc-7826/test/bounty-fallback-account/buffer-bit-utils.ts b/assets/erc-7826/test/bounty-fallback-account/buffer-bit-utils.ts new file mode 100644 index 0000000000..bf395caa00 --- /dev/null +++ b/assets/erc-7826/test/bounty-fallback-account/buffer-bit-utils.ts @@ -0,0 +1,15 @@ +const BITS_PER_BYTE = 8 + +export function getReversedBits (buffer: Buffer, numberOfBits: number): number[] { + const bits = [] + let i = 0 + while (bits.length < numberOfBits) { + const byteInt = buffer.readUInt8(buffer.byteLength - i - 1) + for (let j = 0; j < BITS_PER_BYTE; j++) { + const b = (byteInt >> j) & 1 + bits.push(b) + } + ++i + } + return bits.slice(0, numberOfBits) +} diff --git a/assets/erc-7826/test/bounty-fallback-account/lamport-utils.ts b/assets/erc-7826/test/bounty-fallback-account/lamport-utils.ts new file mode 100644 index 0000000000..c3c6051464 --- /dev/null +++ b/assets/erc-7826/test/bounty-fallback-account/lamport-utils.ts @@ -0,0 +1,60 @@ +import { randomBytes } from 'crypto' +import { arrayify } from 'ethers/lib/utils' +import { keccak256 as keccak256_buffer } from 'ethereumjs-util/dist/hash' +import { Buffer } from 'buffer' +import { getReversedBits } from './buffer-bit-utils' + +// inspiration from https://zacharyratliff.org/Lamport-Signatures/ + +export function hashMessageWithEthHeader (message: string): Buffer { + const labeledMessage = Buffer.concat([ + Buffer.from('\x19Ethereum Signed Message:\n32', 'ascii'), + Buffer.from(arrayify(message)) + ]) + + return keccak256_buffer(labeledMessage) +} + +export class LamportKeys { + public readonly secretKeys: Buffer[][] + public readonly publicKeys: Buffer[][] + + constructor (secretKeys: Buffer[][], publicKeys: Buffer[][]) { + this.secretKeys = secretKeys + this.publicKeys = publicKeys + } +} + +export const DEFAULT_NUMBER_OF_TESTS_LAMPORT = 3 +export const DEFAULT_TEST_SIZE_IN_BYTES_LAMPORT = 3 + +export function generateLamportKeys ( + numberOfTests: number = DEFAULT_NUMBER_OF_TESTS_LAMPORT, + testSizeInBytes: number = DEFAULT_TEST_SIZE_IN_BYTES_LAMPORT +): LamportKeys { + const secretKeys: Buffer[][] = [[], []] + const publicKeys: Buffer[][] = [[], []] + + for (let i = 0; i < numberOfTests; i++) { + const secretKey1 = randomBytes(testSizeInBytes) + const secretKey2 = randomBytes(testSizeInBytes) + secretKeys[0].push(secretKey1) + secretKeys[1].push(secretKey2) + + publicKeys[0][i] = keccak256_buffer(secretKey1).slice(0, testSizeInBytes) + publicKeys[1][i] = keccak256_buffer(secretKey2).slice(0, testSizeInBytes) + } + + return new LamportKeys(secretKeys, publicKeys) +} + +export function signMessageLamport (hashedMessage: Buffer, secretKeys: Buffer[][]): Buffer { + const numberOfTests = secretKeys[0].length + const bits = getReversedBits(hashedMessage, numberOfTests) + + const sig = [] + for (let i = 0; i < numberOfTests; i++) { + sig[i] = secretKeys[bits[i]][i] + } + return Buffer.concat(sig) +} diff --git a/assets/erc-7826/test/bounty-fallback-account/testutils-lamport.ts b/assets/erc-7826/test/bounty-fallback-account/testutils-lamport.ts new file mode 100644 index 0000000000..bedfa6bc8d --- /dev/null +++ b/assets/erc-7826/test/bounty-fallback-account/testutils-lamport.ts @@ -0,0 +1,44 @@ +import { Signer } from 'ethers' +import { + BountyFallbackAccount, + BountyFallbackAccount__factory, + BountyFallbackAccountFactory, + BountyFallbackAccountFactory__factory +} from '../../typechain' +import { WalletLamport } from './wallet-lamport' +import { createAccountOwner } from '../testutils' +import { DEFAULT_NUMBER_OF_TESTS_LAMPORT, DEFAULT_TEST_SIZE_IN_BYTES_LAMPORT } from './lamport-utils' +import { address } from '../solidityTypes' +import { ethers } from 'hardhat' + +// create non-random account, so gas calculations are deterministic +export function createAccountOwnerLamport (numberOfTests: number = DEFAULT_NUMBER_OF_TESTS_LAMPORT, testSizeInBytes: number = DEFAULT_TEST_SIZE_IN_BYTES_LAMPORT, privateKey?: string): WalletLamport { + const wallet = privateKey == null ? createAccountOwner() : new ethers.Wallet(privateKey, ethers.provider) + return new WalletLamport(wallet, numberOfTests, testSizeInBytes) +} + +// Deploys an implementation and a proxy pointing to this implementation +export async function createAccountLamport ( + ethersSigner: Signer, + accountOwner: string, + lamportKey: Buffer[][], + bountyContractAddress: address, + entryPoint: string, + _factory?: BountyFallbackAccountFactory +): + Promise<{ + proxy: BountyFallbackAccount + accountFactory: BountyFallbackAccountFactory + implementation: string + }> { + const accountFactory = _factory ?? await new BountyFallbackAccountFactory__factory(ethersSigner).deploy(entryPoint) + const implementation = await accountFactory.accountImplementation() + await accountFactory.createAccount(accountOwner, 0, lamportKey, bountyContractAddress) + const accountAddress = await accountFactory.getAddress(accountOwner, 0, lamportKey, bountyContractAddress) + const proxy = BountyFallbackAccount__factory.connect(accountAddress, ethersSigner) + return { + implementation, + accountFactory, + proxy + } +} diff --git a/assets/erc-7826/test/bounty-fallback-account/wallet-lamport.ts b/assets/erc-7826/test/bounty-fallback-account/wallet-lamport.ts new file mode 100644 index 0000000000..9b0253017b --- /dev/null +++ b/assets/erc-7826/test/bounty-fallback-account/wallet-lamport.ts @@ -0,0 +1,47 @@ +import { Wallet } from 'ethers' +import { generateLamportKeys, hashMessageWithEthHeader, LamportKeys, signMessageLamport } from './lamport-utils' +import { ecsign, toRpcSig } from 'ethereumjs-util' +import { arrayify } from 'ethers/lib/utils' + +export class WalletLamport { + public readonly baseWallet: Wallet + public lamportKeys: LamportKeys + public lamportKeysNext: LamportKeys + + private readonly _getNewLamportKeys: () => LamportKeys + + constructor (baseWallet: Wallet, numberOfTests: number, testSizeInBytes: number) { + this.baseWallet = baseWallet + + this._getNewLamportKeys = () => generateLamportKeys(numberOfTests, testSizeInBytes) + this.lamportKeysNext = this._getNewLamportKeys() + this._updateLamportKeys() + } + + public signMessageLamport (message: string): Buffer { + const signature = this._getFullSignature(message) + this._updateLamportKeys() + return signature + } + + private _getFullSignature (message: string): Buffer { + const messageWithEthHeader = hashMessageWithEthHeader(message) + const signatureLamport = signMessageLamport(messageWithEthHeader, this.lamportKeys.secretKeys) + const signatureEcdsa = this._signMessageEcdsa(messageWithEthHeader) + return Buffer.concat([ + Buffer.from(arrayify(signatureEcdsa)), + signatureLamport, + ...this.lamportKeysNext.publicKeys.flat() + ]) + } + + private _signMessageEcdsa (message: Buffer): string { + const sig = ecsign(message, Buffer.from(arrayify(this.baseWallet.privateKey))) + return toRpcSig(sig.v, sig.r, sig.s) + } + + private _updateLamportKeys (): void { + this.lamportKeys = this.lamportKeysNext + this.lamportKeysNext = this._getNewLamportKeys() + } +} diff --git a/assets/erc-7826/test/debugTx.ts b/assets/erc-7826/test/debugTx.ts new file mode 100644 index 0000000000..dc22598eab --- /dev/null +++ b/assets/erc-7826/test/debugTx.ts @@ -0,0 +1,26 @@ +import { ethers } from 'hardhat' + +export interface DebugLog { + pc: number + op: string + gasCost: number + depth: number + stack: string[] + memory: string[] +} + +export interface DebugTransactionResult { + gas: number + failed: boolean + returnValue: string + structLogs: DebugLog[] +} + +export async function debugTransaction (txHash: string, disableMemory = true, disableStorage = true): Promise { + const debugTx = async (hash: string): Promise => await ethers.provider.send('debug_traceTransaction', [hash, { + disableMemory, + disableStorage + }]) + + return await debugTx(txHash) +} diff --git a/assets/erc-7826/test/solidityTypes.ts b/assets/erc-7826/test/solidityTypes.ts new file mode 100644 index 0000000000..5026ef9e36 --- /dev/null +++ b/assets/erc-7826/test/solidityTypes.ts @@ -0,0 +1,10 @@ +// define the same export types as used by export typechain/ethers +import { BigNumberish } from 'ethers' +import { BytesLike } from '@ethersproject/bytes' + +export type address = string +export type uint256 = BigNumberish +export type uint = BigNumberish +export type uint48 = BigNumberish +export type bytes = BytesLike +export type bytes32 = BytesLike diff --git a/assets/erc-7826/test/testutils.ts b/assets/erc-7826/test/testutils.ts new file mode 100644 index 0000000000..5204ca57b4 --- /dev/null +++ b/assets/erc-7826/test/testutils.ts @@ -0,0 +1,309 @@ +import { ethers } from 'hardhat' +import { + arrayify, + hexConcat, + keccak256, + parseEther +} from 'ethers/lib/utils' +import { BigNumber, BigNumberish, Contract, ContractReceipt, Signer, Wallet } from 'ethers' +import { + EntryPoint, + EntryPoint__factory, + IERC20, + IEntryPoint, + SimpleAccount, + SimpleAccountFactory__factory, + SimpleAccount__factory, SimpleAccountFactory, TestAggregatedAccountFactory +} from '../typechain' +import { BytesLike } from '@ethersproject/bytes' +import { expect } from 'chai' +import { Create2Factory } from '../src/Create2Factory' +import { debugTransaction } from './debugTx' +import { UserOperation } from './UserOperation' + +export const AddressZero = ethers.constants.AddressZero +export const HashZero = ethers.constants.HashZero +export const ONE_ETH = parseEther('1') +export const TWO_ETH = parseEther('2') +export const FIVE_ETH = parseEther('5') + +export const tostr = (x: any): string => x != null ? x.toString() : 'null' + +export function tonumber (x: any): number { + try { + return parseFloat(x.toString()) + } catch (e: any) { + console.log('=== failed to parseFloat:', x, (e).message) + return NaN + } +} + +// just throw 1eth from account[0] to the given address (or contract instance) +export async function fund (contractOrAddress: string | Contract, amountEth = '1'): Promise { + let address: string + if (typeof contractOrAddress === 'string') { + address = contractOrAddress + } else { + address = contractOrAddress.address + } + await ethers.provider.getSigner().sendTransaction({ to: address, value: parseEther(amountEth) }) +} + +export async function getBalance (address: string): Promise { + const balance = await ethers.provider.getBalance(address) + return parseInt(balance.toString()) +} + +export async function getTokenBalance (token: IERC20, address: string): Promise { + const balance = await token.balanceOf(address) + return parseInt(balance.toString()) +} + +let counter = 0 + +// create non-random account, so gas calculations are deterministic +export function createAccountOwner (): Wallet { + const privateKey = keccak256(Buffer.from(arrayify(BigNumber.from(++counter)))) + return new ethers.Wallet(privateKey, ethers.provider) + // return new ethers.Wallet('0x'.padEnd(66, privkeyBase), ethers.provider); +} + +export function createAddress (): string { + return createAccountOwner().address +} + +export function callDataCost (data: string): number { + return ethers.utils.arrayify(data) + .map(x => x === 0 ? 4 : 16) + .reduce((sum, x) => sum + x) +} + +export async function calcGasUsage (rcpt: ContractReceipt, entryPoint: EntryPoint, beneficiaryAddress?: string): Promise<{ actualGasCost: BigNumberish }> { + const actualGas = await rcpt.gasUsed + const logs = await entryPoint.queryFilter(entryPoint.filters.UserOperationEvent(), rcpt.blockHash) + const { actualGasCost, actualGasUsed } = logs[0].args + console.log('\t== actual gasUsed (from tx receipt)=', actualGas.toString()) + console.log('\t== calculated gasUsed (paid to beneficiary)=', actualGasUsed) + const tx = await ethers.provider.getTransaction(rcpt.transactionHash) + console.log('\t== gasDiff', actualGas.toNumber() - actualGasUsed.toNumber() - callDataCost(tx.data)) + if (beneficiaryAddress != null) { + expect(await getBalance(beneficiaryAddress)).to.eq(actualGasCost.toNumber()) + } + return { actualGasCost } +} + +// helper function to create the initCode to deploy the account, using our account factory. +export function getAccountInitCode (owner: string, factory: SimpleAccountFactory, salt = 0): BytesLike { + return hexConcat([ + factory.address, + factory.interface.encodeFunctionData('createAccount', [owner, salt]) + ]) +} + +export async function getAggregatedAccountInitCode (entryPoint: string, factory: TestAggregatedAccountFactory, salt = 0): Promise { + // the test aggregated account doesn't check the owner... + const owner = AddressZero + return hexConcat([ + factory.address, + factory.interface.encodeFunctionData('createAccount', [owner, salt]) + ]) +} + +// given the parameters as AccountDeployer, return the resulting "counterfactual address" that it would create. +export async function getAccountAddress (owner: string, factory: SimpleAccountFactory, salt = 0): Promise { + return await factory.getAddress(owner, salt) +} + +const panicCodes: { [key: number]: string } = { + // from https://docs.soliditylang.org/en/v0.8.0/control-structures.html + 0x01: 'assert(false)', + 0x11: 'arithmetic overflow/underflow', + 0x12: 'divide by zero', + 0x21: 'invalid enum value', + 0x22: 'storage byte array that is incorrectly encoded', + 0x31: '.pop() on an empty array.', + 0x32: 'array sout-of-bounds or negative index', + 0x41: 'memory overflow', + 0x51: 'zero-initialized variable of internal function type' +} + +// rethrow "cleaned up" exception. +// - stack trace goes back to method (or catch) line, not inner provider +// - attempt to parse revert data (needed for geth) +// use with ".catch(rethrow())", so that current source file/line is meaningful. +export function rethrow (): (e: Error) => void { + const callerStack = new Error().stack!.replace(/Error.*\n.*at.*\n/, '').replace(/.*at.* \(internal[\s\S]*/, '') + + if (arguments[0] != null) { + throw new Error('must use .catch(rethrow()), and NOT .catch(rethrow)') + } + return function (e: Error) { + const solstack = e.stack!.match(/((?:.* at .*\.sol.*\n)+)/) + const stack = (solstack != null ? solstack[1] : '') + callerStack + // const regex = new RegExp('error=.*"data":"(.*?)"').compile() + const found = /error=.*?"data":"(.*?)"/.exec(e.message) + let message: string + if (found != null) { + const data = found[1] + message = decodeRevertReason(data) ?? e.message + ' - ' + data.slice(0, 100) + } else { + message = e.message + } + const err = new Error(message) + err.stack = 'Error: ' + message + '\n' + stack + throw err + } +} + +export function decodeRevertReason (data: string, nullIfNoMatch = true): string | null { + const methodSig = data.slice(0, 10) + const dataParams = '0x' + data.slice(10) + + if (methodSig === '0x08c379a0') { + const [err] = ethers.utils.defaultAbiCoder.decode(['string'], dataParams) + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + return `Error(${err})` + } else if (methodSig === '0x00fa072b') { + const [opindex, paymaster, msg] = ethers.utils.defaultAbiCoder.decode(['uint256', 'address', 'string'], dataParams) + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + return `FailedOp(${opindex}, ${paymaster !== AddressZero ? paymaster : 'none'}, ${msg})` + } else if (methodSig === '0x4e487b71') { + const [code] = ethers.utils.defaultAbiCoder.decode(['uint256'], dataParams) + return `Panic(${panicCodes[code] ?? code} + ')` + } + if (!nullIfNoMatch) { + return data + } + return null +} + +let currentNode: string = '' + +// basic geth support +// - by default, has a single account. our code needs more. +export async function checkForGeth (): Promise { + // @ts-ignore + const provider = ethers.provider._hardhatProvider + + currentNode = await provider.request({ method: 'web3_clientVersion' }) + + console.log('node version:', currentNode) + // NOTE: must run geth with params: + // --http.api personal,eth,net,web3 + // --allow-insecure-unlock + if (currentNode.match(/geth/i) != null) { + for (let i = 0; i < 2; i++) { + const acc = await provider.request({ method: 'personal_newAccount', params: ['pass'] }).catch(rethrow) + await provider.request({ method: 'personal_unlockAccount', params: [acc, 'pass'] }).catch(rethrow) + await fund(acc, '10') + } + } +} + +// remove "array" members, convert values to strings. +// so Result obj like +// { '0': "a", '1': 20, first: "a", second: 20 } +// becomes: +// { first: "a", second: "20" } +export function objdump (obj: { [key: string]: any }): any { + return Object.keys(obj) + .filter(key => key.match(/^[\d_]/) == null) + .reduce((set, key) => ({ + ...set, + [key]: decodeRevertReason(obj[key].toString(), false) + }), {}) +} + +export async function checkForBannedOps (txHash: string, checkPaymaster: boolean): Promise { + const tx = await debugTransaction(txHash) + const logs = tx.structLogs + const blockHash = logs.map((op, index) => ({ op: op.op, index })).filter(op => op.op === 'NUMBER') + expect(blockHash.length).to.equal(2, 'expected exactly 2 call to NUMBER (Just before and after validateUserOperation)') + const validateAccountOps = logs.slice(0, blockHash[0].index - 1) + const validatePaymasterOps = logs.slice(blockHash[0].index + 1) + const ops = validateAccountOps.filter(log => log.depth > 1).map(log => log.op) + const paymasterOps = validatePaymasterOps.filter(log => log.depth > 1).map(log => log.op) + + expect(ops).to.include('POP', 'not a valid ops list: ' + JSON.stringify(ops)) // sanity + const bannedOpCodes = new Set(['GAS', 'BASEFEE', 'GASPRICE', 'NUMBER']) + expect(ops.filter((op, index) => { + // don't ban "GAS" op followed by "*CALL" + if (op === 'GAS' && (ops[index + 1].match(/CALL/) != null)) { + return false + } + return bannedOpCodes.has(op) + })).to.eql([]) + if (checkPaymaster) { + expect(paymasterOps).to.include('POP', 'not a valid ops list: ' + JSON.stringify(paymasterOps)) // sanity + expect(paymasterOps).to.not.include('BASEFEE') + expect(paymasterOps).to.not.include('GASPRICE') + expect(paymasterOps).to.not.include('NUMBER') + } +} + +/** + * process exception of ValidationResult + * usage: entryPoint.simulationResult(..).catch(simulationResultCatch) + */ +export function simulationResultCatch (e: any): any { + if (e.errorName !== 'ValidationResult') { + throw e + } + return e.errorArgs +} + +/** + * process exception of ValidationResultWithAggregation + * usage: entryPoint.simulationResult(..).catch(simulationResultWithAggregation) + */ +export function simulationResultWithAggregationCatch (e: any): any { + if (e.errorName !== 'ValidationResultWithAggregation') { + throw e + } + return e.errorArgs +} + +export async function deployEntryPoint (provider = ethers.provider): Promise { + const create2factory = new Create2Factory(provider) + const epf = new EntryPoint__factory(provider.getSigner()) + const addr = await create2factory.deploy(epf.bytecode, 0, process.env.COVERAGE != null ? 20e6 : 8e6) + return EntryPoint__factory.connect(addr, provider.getSigner()) +} + +export async function isDeployed (addr: string): Promise { + const code = await ethers.provider.getCode(addr) + return code.length > 2 +} + +// internal helper function: create a UserOpsPerAggregator structure, with no aggregator or signature +export function userOpsWithoutAgg (userOps: UserOperation[]): IEntryPoint.UserOpsPerAggregatorStruct[] { + return [{ + userOps, + aggregator: AddressZero, + signature: '0x' + }] +} + +// Deploys an implementation and a proxy pointing to this implementation +export async function createAccount ( + ethersSigner: Signer, + accountOwner: string, + entryPoint: string, + _factory?: SimpleAccountFactory +): + Promise<{ + proxy: SimpleAccount + accountFactory: SimpleAccountFactory + implementation: string + }> { + const accountFactory = _factory ?? await new SimpleAccountFactory__factory(ethersSigner).deploy(entryPoint) + const implementation = await accountFactory.accountImplementation() + await accountFactory.createAccount(accountOwner, 0) + const accountAddress = await accountFactory.getAddress(accountOwner, 0) + const proxy = SimpleAccount__factory.connect(accountAddress, ethersSigner) + return { + implementation, + accountFactory, + proxy + } +}