From f3d085df8d7c881b7aed642442b7bc76c362b0ab Mon Sep 17 00:00:00 2001 From: Richard Ramos Date: Tue, 3 Sep 2024 13:33:09 -0400 Subject: [PATCH 01/21] feat: membership --- .gas-snapshot | 25 +- pnpm-lock.yaml | 2459 ++++++++++++++++++--------------- script/Deploy.s.sol | 8 +- src/IPriceCalculator.sol | 11 + src/LinearPriceCalculator.sol | 36 + src/Membership.sol | 427 ++++++ src/WakuRlnV2.sol | 173 ++- test/TestToken.sol | 12 + test/WakuRlnV2.t.sol | 537 ++++++- 9 files changed, 2505 insertions(+), 1183 deletions(-) create mode 100644 src/IPriceCalculator.sol create mode 100644 src/LinearPriceCalculator.sol create mode 100644 src/Membership.sol create mode 100644 test/TestToken.sol diff --git a/.gas-snapshot b/.gas-snapshot index 4d6ad85..31c715d 100644 --- a/.gas-snapshot +++ b/.gas-snapshot @@ -1,14 +1,11 @@ -WakuRlnV2Test:test__IdCommitmentToMetadata__DoesntExist() (gas: 16726) -WakuRlnV2Test:test__InvalidPaginationQuery__EndIndexGTcommitmentIndex() (gas: 18249) -WakuRlnV2Test:test__InvalidPaginationQuery__StartIndexGTEndIndex() (gas: 16130) -WakuRlnV2Test:test__InvalidRegistration__DuplicateIdCommitment() (gas: 99502) -WakuRlnV2Test:test__InvalidRegistration__FullTree() (gas: 14328) -WakuRlnV2Test:test__InvalidRegistration__InvalidIdCommitment__LargerThanField() (gas: 15229) -WakuRlnV2Test:test__InvalidRegistration__InvalidIdCommitment__Zero() (gas: 14004) -WakuRlnV2Test:test__InvalidRegistration__InvalidUserMessageLimit__LargerThanMax() (gas: 17703) -WakuRlnV2Test:test__InvalidRegistration__InvalidUserMessageLimit__Zero() (gas: 14085) -WakuRlnV2Test:test__Upgrade() (gas: 3791109) -WakuRlnV2Test:test__ValidPaginationQuery(uint32) (runs: 1000, μ: 445155, ~: 159972) -WakuRlnV2Test:test__ValidPaginationQuery__OneElement() (gas: 119773) -WakuRlnV2Test:test__ValidRegistration(uint256,uint32) (runs: 1000, μ: 124408, ~: 124408) -WakuRlnV2Test:test__ValidRegistration__kats() (gas: 96616) \ No newline at end of file +WakuRlnV2Test:test__IdCommitmentToMetadata__DoesntExist() (gas: 27478) +WakuRlnV2Test:test__InsertionNormalOrder(uint32) (runs: 1001, μ: 1123469, ~: 494837) +WakuRlnV2Test:test__InvalidPaginationQuery__EndIndexGTcommitmentIndex() (gas: 18261) +WakuRlnV2Test:test__InvalidPaginationQuery__StartIndexGTEndIndex() (gas: 16108) +WakuRlnV2Test:test__InvalidRegistration__DuplicateIdCommitment() (gas: 249370) +WakuRlnV2Test:test__InvalidRegistration__InvalidIdCommitment__Zero() (gas: 12135) +WakuRlnV2Test:test__Upgrade() (gas: 6728616) +WakuRlnV2Test:test__ValidPaginationQuery(uint32) (runs: 1002, μ: 226428, ~: 52927) +WakuRlnV2Test:test__ValidPaginationQuery__OneElement() (gas: 262351) +WakuRlnV2Test:test__ValidRegistration(uint256,uint32) (runs: 1001, μ: 268741, ~: 268741) +WakuRlnV2Test:test__ValidRegistration__kats() (gas: 238744) \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f534cf6..1680e81 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1,288 +1,1247 @@ -lockfileVersion: '6.0' +lockfileVersion: '9.0' settings: autoInstallPeers: true excludeLinksFromLockfile: false -dependencies: - '@zk-kit/imt.sol': - specifier: https://gitpkg.vercel.app/privacy-scaling-explorations/zk-kit/packages/imt.sol?0699fd1e5ad3683ae0090e0626f75d7834145500 - version: '@gitpkg.vercel.app/privacy-scaling-explorations/zk-kit/packages/imt.sol?0699fd1e5ad3683ae0090e0626f75d7834145500' - poseidon-solidity: - specifier: ^0.0.5 - version: 0.0.5 - -devDependencies: - commit-and-tag-version: - specifier: ^12.2.0 - version: 12.2.0 - prettier: - specifier: ^3.0.0 - version: 3.0.0 - solhint-community: - specifier: ^3.6.0 - version: 3.6.0 +importers: + + .: + dependencies: + '@zk-kit/imt.sol': + specifier: https://gitpkg.vercel.app/privacy-scaling-explorations/zk-kit/packages/imt.sol?0699fd1e5ad3683ae0090e0626f75d7834145500 + version: imt.sol@https://gitpkg.vercel.app/privacy-scaling-explorations/zk-kit/packages/imt.sol?0699fd1e5ad3683ae0090e0626f75d7834145500 + poseidon-solidity: + specifier: ^0.0.5 + version: 0.0.5 + devDependencies: + commit-and-tag-version: + specifier: ^12.2.0 + version: 12.2.0 + prettier: + specifier: ^3.0.0 + version: 3.0.0 + solhint-community: + specifier: ^3.6.0 + version: 3.6.0 packages: - /@asamuzakjp/dom-selector@2.0.2: + '@asamuzakjp/dom-selector@2.0.2': resolution: {integrity: sha512-x1KXOatwofR6ZAYzXRBL5wrdV0vwNxlTCK9NCuLqAzQYARqGcvFwiJA6A1ERuh+dgeA4Dxm3JBYictIes+SqUQ==} + + '@babel/code-frame@7.22.5': + resolution: {integrity: sha512-Xmwn266vad+6DAqEB2A6V/CcZVp62BbwVmcOJc2RPuwih1kw02TjQvWVWlcKGbBPd+8/0V5DEkOcizRGYsspYQ==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.22.5': + resolution: {integrity: sha512-aJXu+6lErq8ltp+JhkJUfk1MTGyuA4v7f3pA+BJ5HLfNC6nAQ0Cpi9uOquUj8Hehg0aUiHzWQbOVJGao6ztBAQ==} + engines: {node: '>=6.9.0'} + + '@babel/highlight@7.22.5': + resolution: {integrity: sha512-BSKlD1hgnedS5XRnGOljZawtag7H1yPfQp0tdNJCHoH6AZ+Pcm9VvkrK59/Yy593Ypg0zMxH2BxD1VPYUQ7UIw==} + engines: {node: '>=6.9.0'} + + '@hutson/parse-repository-url@3.0.2': + resolution: {integrity: sha512-H9XAx3hc0BQHY6l+IFSWHDySypcXsvsuLhgYLUGywmJ5pswRVQJUHpOsobnLYp2ZUaUlKiKDrgWWhosOwAEM8Q==} + engines: {node: '>=6.9.0'} + + '@solidity-parser/parser@0.16.1': + resolution: {integrity: sha512-PdhRFNhbTtu3x8Axm0uYpqOy/lODYQK+MlYSgqIsq2L8SFYEHJPHNUiOTAJbDGzNjjr1/n9AcIayxafR/fWmYw==} + + '@types/minimist@1.2.5': + resolution: {integrity: sha512-hov8bUuiLiyFPGyFPE1lwWhmzYbirOXQNNo40+y3zow8aFVTeyn3VWL0VFFfdNddA8S4Vf0Tc062rzyNr7Paag==} + + '@types/normalize-package-data@2.4.4': + resolution: {integrity: sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==} + + JSONStream@1.3.5: + resolution: {integrity: sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==} + hasBin: true + + add-stream@1.0.0: + resolution: {integrity: sha512-qQLMr+8o0WC4FZGQTcJiKBVC59JylcPSrTtk6usvmIDFUOCKegapy1VHQwRbFMOFyb/inzUVqHs+eMYKDM1YeQ==} + + agent-base@7.1.0: + resolution: {integrity: sha512-o/zjMZRhJxny7OyEF+Op8X+efiELC7k7yOjMzgfzVqOzXqkBkWI79YoTdOtsuWd5BWhAGAuOY/Xa6xpiaWXiNg==} + engines: {node: '>= 14'} + + ajv@6.12.6: + resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + + ajv@8.12.0: + resolution: {integrity: sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-styles@3.2.1: + resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==} + engines: {node: '>=4'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + antlr4@4.13.0: + resolution: {integrity: sha512-zooUbt+UscjnWyOrsuY/tVFL4rwrAGwOivpQmvmUDE22hy/lUA467Rc1rcixyRwcRUIXFYBwv7+dClDSHdmmew==} + engines: {node: '>=16'} + + antlr4ts@0.5.0-alpha.4: + resolution: {integrity: sha512-WPQDt1B74OfPv/IMS2ekXAKkTZIHl88uMetg6q3OTqgFxZ/dxDXI0EWLyZid/1Pe6hTftyg5N7gel5wNAGxXyQ==} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + array-ify@1.0.0: + resolution: {integrity: sha512-c5AMf34bKdvPhQ7tBGhqkgKNUzMr4WUs+WDtC2ZUGOUncbxKMTvqxYctiseW3+L4bA8ec+GcZ6/A/FW4m8ukng==} + + arrify@1.0.1: + resolution: {integrity: sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA==} + engines: {node: '>=0.10.0'} + + ast-parents@0.0.1: + resolution: {integrity: sha512-XHusKxKz3zoYk1ic8Un640joHbFMhbqneyoZfoKnEGtf2ey9Uh/IdpcQplODdO/kENaMIWsD0nJm4+wX3UNLHA==} + + astral-regex@2.0.0: + resolution: {integrity: sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==} + engines: {node: '>=8'} + + asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + bidi-js@1.0.3: + resolution: {integrity: sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==} + + brace-expansion@1.1.11: + resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} + + brace-expansion@2.0.1: + resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} + + buffer-from@1.1.2: + resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + + callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + + camelcase-keys@6.2.2: + resolution: {integrity: sha512-YrwaA0vEKazPBkn0ipTiMpSajYDSe+KjQfrjhcBMxJt/znbvlHd8Pw/Vamaz5EB4Wfhs3SUR3Z9mwRu/P3s3Yg==} + engines: {node: '>=8'} + + camelcase@5.3.1: + resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==} + engines: {node: '>=6'} + + chalk@2.4.2: + resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} + engines: {node: '>=4'} + + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + + cliui@7.0.4: + resolution: {integrity: sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==} + + cliui@8.0.1: + resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} + engines: {node: '>=12'} + + color-convert@1.9.3: + resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.3: + resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + + commander@10.0.1: + resolution: {integrity: sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==} + engines: {node: '>=14'} + + commit-and-tag-version@12.2.0: + resolution: {integrity: sha512-t8kvDy8dVTCF/j2u7Elt/J3tzCPyh16ssK8ECpj44R6jJiCUJMhWwZ36L652iChvqz4BAem9XU7WyuMYHlmSvA==} + engines: {node: '>=18'} + hasBin: true + + compare-func@2.0.0: + resolution: {integrity: sha512-zHig5N+tPWARooBnb0Zx1MFcdfpyJrfTJ3Y5L+IFvUm8rM74hHz66z0gw0x4tijh5CorKkKUCnW82R2vmpeCRA==} + + concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + + concat-stream@2.0.0: + resolution: {integrity: sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==} + engines: {'0': node >= 6.0} + + conventional-changelog-angular@5.0.13: + resolution: {integrity: sha512-i/gipMxs7s8L/QeuavPF2hLnJgH6pEZAttySB6aiQLWcX3puWDL3ACVmvBhJGxnAy52Qc15ua26BufY6KpmrVA==} + engines: {node: '>=10'} + + conventional-changelog-atom@2.0.8: + resolution: {integrity: sha512-xo6v46icsFTK3bb7dY/8m2qvc8sZemRgdqLb/bjpBsH2UyOS8rKNTgcb5025Hri6IpANPApbXMg15QLb1LJpBw==} + engines: {node: '>=10'} + + conventional-changelog-codemirror@2.0.8: + resolution: {integrity: sha512-z5DAsn3uj1Vfp7po3gpt2Boc+Bdwmw2++ZHa5Ak9k0UKsYAO5mH1UBTN0qSCuJZREIhX6WU4E1p3IW2oRCNzQw==} + engines: {node: '>=10'} + + conventional-changelog-config-spec@2.1.0: + resolution: {integrity: sha512-IpVePh16EbbB02V+UA+HQnnPIohgXvJRxHcS5+Uwk4AT5LjzCZJm5sp/yqs5C6KZJ1jMsV4paEV13BN1pvDuxQ==} + + conventional-changelog-conventionalcommits@4.6.3: + resolution: {integrity: sha512-LTTQV4fwOM4oLPad317V/QNQ1FY4Hju5qeBIM1uTHbrnCE+Eg4CdRZ3gO2pUeR+tzWdp80M2j3qFFEDWVqOV4g==} + engines: {node: '>=10'} + + conventional-changelog-conventionalcommits@6.1.0: + resolution: {integrity: sha512-3cS3GEtR78zTfMzk0AizXKKIdN4OvSh7ibNz6/DPbhWWQu7LqE/8+/GqSodV+sywUR2gpJAdP/1JFf4XtN7Zpw==} + engines: {node: '>=14'} + + conventional-changelog-core@4.2.4: + resolution: {integrity: sha512-gDVS+zVJHE2v4SLc6B0sLsPiloR0ygU7HaDW14aNJE1v4SlqJPILPl/aJC7YdtRE4CybBf8gDwObBvKha8Xlyg==} + engines: {node: '>=10'} + + conventional-changelog-ember@2.0.9: + resolution: {integrity: sha512-ulzIReoZEvZCBDhcNYfDIsLTHzYHc7awh+eI44ZtV5cx6LVxLlVtEmcO+2/kGIHGtw+qVabJYjdI5cJOQgXh1A==} + engines: {node: '>=10'} + + conventional-changelog-eslint@3.0.9: + resolution: {integrity: sha512-6NpUCMgU8qmWmyAMSZO5NrRd7rTgErjrm4VASam2u5jrZS0n38V7Y9CzTtLT2qwz5xEChDR4BduoWIr8TfwvXA==} + engines: {node: '>=10'} + + conventional-changelog-express@2.0.6: + resolution: {integrity: sha512-SDez2f3iVJw6V563O3pRtNwXtQaSmEfTCaTBPCqn0oG0mfkq0rX4hHBq5P7De2MncoRixrALj3u3oQsNK+Q0pQ==} + engines: {node: '>=10'} + + conventional-changelog-jquery@3.0.11: + resolution: {integrity: sha512-x8AWz5/Td55F7+o/9LQ6cQIPwrCjfJQ5Zmfqi8thwUEKHstEn4kTIofXub7plf1xvFA2TqhZlq7fy5OmV6BOMw==} + engines: {node: '>=10'} + + conventional-changelog-jshint@2.0.9: + resolution: {integrity: sha512-wMLdaIzq6TNnMHMy31hql02OEQ8nCQfExw1SE0hYL5KvU+JCTuPaDO+7JiogGT2gJAxiUGATdtYYfh+nT+6riA==} + engines: {node: '>=10'} + + conventional-changelog-preset-loader@2.3.4: + resolution: {integrity: sha512-GEKRWkrSAZeTq5+YjUZOYxdHq+ci4dNwHvpaBC3+ENalzFWuCWa9EZXSuZBpkr72sMdKB+1fyDV4takK1Lf58g==} + engines: {node: '>=10'} + + conventional-changelog-preset-loader@3.0.0: + resolution: {integrity: sha512-qy9XbdSLmVnwnvzEisjxdDiLA4OmV3o8db+Zdg4WiFw14fP3B6XNz98X0swPPpkTd/pc1K7+adKgEDM1JCUMiA==} + engines: {node: '>=14'} + + conventional-changelog-writer@5.0.1: + resolution: {integrity: sha512-5WsuKUfxW7suLblAbFnxAcrvf6r+0b7GvNaWUwUIk0bXMnENP/PEieGKVUQrjPqwPT4o3EPAASBXiY6iHooLOQ==} + engines: {node: '>=10'} + hasBin: true + + conventional-changelog@3.1.25: + resolution: {integrity: sha512-ryhi3fd1mKf3fSjbLXOfK2D06YwKNic1nC9mWqybBHdObPd8KJ2vjaXZfYj1U23t+V8T8n0d7gwnc9XbIdFbyQ==} + engines: {node: '>=10'} + + conventional-commits-filter@2.0.7: + resolution: {integrity: sha512-ASS9SamOP4TbCClsRHxIHXRfcGCnIoQqkvAzCSbZzTFLfcTqJVugB0agRgsEELsqaeWgsXv513eS116wnlSSPA==} + engines: {node: '>=10'} + + conventional-commits-filter@3.0.0: + resolution: {integrity: sha512-1ymej8b5LouPx9Ox0Dw/qAO2dVdfpRFq28e5Y0jJEU8ZrLdy0vOSkkIInwmxErFGhg6SALro60ZrwYFVTUDo4Q==} + engines: {node: '>=14'} + + conventional-commits-parser@3.2.4: + resolution: {integrity: sha512-nK7sAtfi+QXbxHCYfhpZsfRtaitZLIA6889kFIouLvz6repszQDgxBu7wf2WbU+Dco7sAnNCJYERCwt54WPC2Q==} + engines: {node: '>=10'} + hasBin: true + + conventional-commits-parser@4.0.0: + resolution: {integrity: sha512-WRv5j1FsVM5FISJkoYMR6tPk07fkKT0UodruX4je86V4owk451yjXAKzKAPOs9l7y59E2viHUS9eQ+dfUA9NSg==} + engines: {node: '>=14'} + hasBin: true + + conventional-recommended-bump@7.0.1: + resolution: {integrity: sha512-Ft79FF4SlOFvX4PkwFDRnaNiIVX7YbmqGU0RwccUaiGvgp3S0a8ipR2/Qxk31vclDNM+GSdJOVs2KrsUCjblVA==} + engines: {node: '>=14'} + hasBin: true + + core-util-is@1.0.3: + resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} + + cosmiconfig@8.2.0: + resolution: {integrity: sha512-3rTMnFJA1tCOPwRxtgF4wd7Ab2qvDbL8jX+3smjIbS4HlZBagTlpERbdN7iAbWlrfxE3M8c27kTwTawQ7st+OQ==} + engines: {node: '>=14'} + + css-tree@2.3.1: + resolution: {integrity: sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==} + engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} + + cssstyle@4.0.1: + resolution: {integrity: sha512-8ZYiJ3A/3OkDd093CBT/0UKDWry7ak4BdPTFP2+QEP7cmhouyq/Up709ASSj2cK02BbZiMgk7kYjZNS4QP5qrQ==} + engines: {node: '>=18'} + + dargs@7.0.0: + resolution: {integrity: sha512-2iy1EkLdlBzQGvbweYRFxmFath8+K7+AKB0TlhHWkNuH+TmovaMH/Wp7V7R4u7f4SnX3OgLsU9t1NI9ioDnUpg==} + engines: {node: '>=8'} + + data-urls@5.0.0: + resolution: {integrity: sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==} + engines: {node: '>=18'} + + dateformat@3.0.3: + resolution: {integrity: sha512-jyCETtSl3VMZMWeRo7iY1FL19ges1t55hMo5yaam4Jrsm5EPL89UQkoQRyiI+Yf4k8r2ZpdngkV8hr1lIdjb3Q==} + + debug@4.3.4: + resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + decamelize-keys@1.1.1: + resolution: {integrity: sha512-WiPxgEirIV0/eIOMcnFBA3/IJZAZqKnwAwWyvvdi4lsr1WCN22nhdf/3db3DoZcUjTV2SqfzIwNyp6y2xs3nmg==} + engines: {node: '>=0.10.0'} + + decamelize@1.2.0: + resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==} + engines: {node: '>=0.10.0'} + + decimal.js@10.4.3: + resolution: {integrity: sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==} + + delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + + detect-indent@6.1.0: + resolution: {integrity: sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==} + engines: {node: '>=8'} + + detect-newline@3.1.0: + resolution: {integrity: sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==} + engines: {node: '>=8'} + + dot-prop@5.3.0: + resolution: {integrity: sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==} + engines: {node: '>=8'} + + dotgitignore@2.1.0: + resolution: {integrity: sha512-sCm11ak2oY6DglEPpCB8TixLjWAxd3kJTs6UIcSasNYxXdFPV+YKlye92c8H4kKFqV5qYMIh7d+cYecEg0dIkA==} + engines: {node: '>=6'} + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + entities@4.5.0: + resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} + engines: {node: '>=0.12'} + + error-ex@1.3.2: + resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==} + + escalade@3.1.2: + resolution: {integrity: sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==} + engines: {node: '>=6'} + + escape-string-regexp@1.0.5: + resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} + engines: {node: '>=0.8.0'} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-diff@1.3.0: + resolution: {integrity: sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==} + + fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + + figures@3.2.0: + resolution: {integrity: sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==} + engines: {node: '>=8'} + + find-up@2.1.0: + resolution: {integrity: sha512-NWzkk0jSJtTt08+FBFMvXoeZnOJD+jTtsRmBYbAIzJdX6l7dLgR7CTubCM5/eDdPUBvLCeVasP1brfVR/9/EZQ==} + engines: {node: '>=4'} + + find-up@3.0.0: + resolution: {integrity: sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==} + engines: {node: '>=6'} + + find-up@4.1.0: + resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} + engines: {node: '>=8'} + + find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + + form-data@4.0.0: + resolution: {integrity: sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==} + engines: {node: '>= 6'} + + fs.realpath@1.0.0: + resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + + get-pkg-repo@4.2.1: + resolution: {integrity: sha512-2+QbHjFRfGB74v/pYWjd5OhU3TDIC2Gv/YKUTk/tCvAz0pkn/Mz6P3uByuBimLOcPvN2jYdScl3xGFSrx0jEcA==} + engines: {node: '>=6.9.0'} + hasBin: true + + git-raw-commits@2.0.11: + resolution: {integrity: sha512-VnctFhw+xfj8Va1xtfEqCUD2XDrbAPSJx+hSrE5K7fGdjZruW7XV+QOrN7LF/RJyvspRiD2I0asWsxFp0ya26A==} + engines: {node: '>=10'} + hasBin: true + + git-raw-commits@3.0.0: + resolution: {integrity: sha512-b5OHmZ3vAgGrDn/X0kS+9qCfNKWe4K/jFnhwzVWWg0/k5eLa3060tZShrRg8Dja5kPc+YjS0Gc6y7cRr44Lpjw==} + engines: {node: '>=14'} + hasBin: true + + git-remote-origin-url@2.0.0: + resolution: {integrity: sha512-eU+GGrZgccNJcsDH5LkXR3PB9M958hxc7sbA8DFJjrv9j4L2P/eZfKhM+QD6wyzpiv+b1BpK0XrYCxkovtjSLw==} + engines: {node: '>=4'} + + git-semver-tags@4.1.1: + resolution: {integrity: sha512-OWyMt5zBe7xFs8vglMmhM9lRQzCWL3WjHtxNNfJTMngGym7pC1kh8sP6jevfydJ6LP3ZvGxfb6ABYgPUM0mtsA==} + engines: {node: '>=10'} + hasBin: true + + git-semver-tags@5.0.1: + resolution: {integrity: sha512-hIvOeZwRbQ+7YEUmCkHqo8FOLQZCEn18yevLHADlFPZY02KJGsu5FZt9YW/lybfK2uhWFI7Qg/07LekJiTv7iA==} + engines: {node: '>=14'} + hasBin: true + + gitconfiglocal@1.0.0: + resolution: {integrity: sha512-spLUXeTAVHxDtKsJc8FkFVgFtMdEN9qPGpL23VfSHx4fP4+Ds097IXLvymbnDH8FnmxX5Nr9bPw3A+AQ6mWEaQ==} + + glob@8.1.0: + resolution: {integrity: sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==} + engines: {node: '>=12'} + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + handlebars@4.7.8: + resolution: {integrity: sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==} + engines: {node: '>=0.4.7'} + hasBin: true + + hard-rejection@2.1.0: + resolution: {integrity: sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA==} + engines: {node: '>=6'} + + has-flag@3.0.0: + resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} + engines: {node: '>=4'} + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + hasown@2.0.1: + resolution: {integrity: sha512-1/th4MHjnwncwXsIW6QMzlvYL9kG5e/CpVvLRZe4XPa8TOUNbCELqmvhDmnkNsAjwaG4+I8gJJL0JBvTTLO9qA==} + engines: {node: '>= 0.4'} + + hosted-git-info@2.8.9: + resolution: {integrity: sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==} + + hosted-git-info@4.1.0: + resolution: {integrity: sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==} + engines: {node: '>=10'} + + html-encoding-sniffer@4.0.0: + resolution: {integrity: sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==} + engines: {node: '>=18'} + + http-proxy-agent@7.0.2: + resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} + engines: {node: '>= 14'} + + https-proxy-agent@7.0.4: + resolution: {integrity: sha512-wlwpilI7YdjSkWaQ/7omYBMTliDcmCN8OLihO6I9B86g06lMyAoqgoDpV0XqoaPOKj+0DIdAvnsWfyAAhmimcg==} + engines: {node: '>= 14'} + + iconv-lite@0.6.3: + resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} + engines: {node: '>=0.10.0'} + + ignore@5.2.4: + resolution: {integrity: sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==} + engines: {node: '>= 4'} + + import-fresh@3.3.0: + resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==} + engines: {node: '>=6'} + + imt.sol@https://gitpkg.vercel.app/privacy-scaling-explorations/zk-kit/packages/imt.sol?0699fd1e5ad3683ae0090e0626f75d7834145500: + resolution: {tarball: https://gitpkg.vercel.app/privacy-scaling-explorations/zk-kit/packages/imt.sol?0699fd1e5ad3683ae0090e0626f75d7834145500} + name: imt.sol + version: 0.0.0 + + indent-string@4.0.0: + resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} + engines: {node: '>=8'} + + inflight@1.0.6: + resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + ini@1.3.8: + resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} + + is-arrayish@0.2.1: + resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} + + is-core-module@2.13.1: + resolution: {integrity: sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==} + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + is-obj@2.0.0: + resolution: {integrity: sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==} + engines: {node: '>=8'} + + is-plain-obj@1.1.0: + resolution: {integrity: sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==} + engines: {node: '>=0.10.0'} + + is-potential-custom-element-name@1.0.1: + resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} + + is-text-path@1.0.1: + resolution: {integrity: sha512-xFuJpne9oFz5qDaodwmmG08e3CawH/2ZV8Qqza1Ko7Sk8POWbkRdwIoAWVhqvq0XeUzANEhKo2n0IXUGBm7A/w==} + engines: {node: '>=0.10.0'} + + isarray@1.0.0: + resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + js-yaml@4.1.0: + resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} + hasBin: true + + jsdom@23.2.0: + resolution: {integrity: sha512-L88oL7D/8ufIES+Zjz7v0aes+oBMh2Xnh3ygWvL0OaICOomKEPKuPnIfBJekiXr+BHbbMjrWn/xqrDQuxFTeyA==} + engines: {node: '>=18'} + peerDependencies: + canvas: ^2.11.2 + peerDependenciesMeta: + canvas: + optional: true + + json-parse-better-errors@1.0.2: + resolution: {integrity: sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==} + + json-parse-even-better-errors@2.3.1: + resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} + + json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + + json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + + json-stringify-safe@5.0.1: + resolution: {integrity: sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==} + + jsonparse@1.3.1: + resolution: {integrity: sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==} + engines: {'0': node >= 0.2.0} + + kind-of@6.0.3: + resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==} + engines: {node: '>=0.10.0'} + + lines-and-columns@1.2.4: + resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + + load-json-file@4.0.0: + resolution: {integrity: sha512-Kx8hMakjX03tiGTLAIdJ+lL0htKnXjEZN6hk/tozf/WOuYGdZBJrZ+rCJRbVCugsjB3jMLn9746NsQIf5VjBMw==} + engines: {node: '>=4'} + + locate-path@2.0.0: + resolution: {integrity: sha512-NCI2kiDkyR7VeEKm27Kda/iQHyKJe1Bu0FlTbYp3CqJu+9IFe9bLyAjMxf5ZDDbEg+iMPzB5zYyUTSm8wVTKmA==} + engines: {node: '>=4'} + + locate-path@3.0.0: + resolution: {integrity: sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==} + engines: {node: '>=6'} + + locate-path@5.0.0: + resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} + engines: {node: '>=8'} + + locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + + lodash.ismatch@4.4.0: + resolution: {integrity: sha512-fPMfXjGQEV9Xsq/8MTSgUf255gawYRbjwMyDbcvDhXgV7enSZA0hynz6vMPnpAb5iONEzBHBPsT+0zes5Z301g==} + + lodash.truncate@4.4.2: + resolution: {integrity: sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==} + + lodash@4.17.21: + resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + + lru-cache@6.0.0: + resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} + engines: {node: '>=10'} + + map-obj@1.0.1: + resolution: {integrity: sha512-7N/q3lyZ+LVCp7PzuxrJr4KMbBE2hW7BT7YNia330OFxIf4d3r5zVpicP2650l7CPN6RM9zOJRl3NGpqSiw3Eg==} + engines: {node: '>=0.10.0'} + + map-obj@4.3.0: + resolution: {integrity: sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ==} + engines: {node: '>=8'} + + mdn-data@2.0.30: + resolution: {integrity: sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==} + + meow@8.1.2: + resolution: {integrity: sha512-r85E3NdZ+mpYk1C6RjPFEMSE+s1iZMuHtsHAqY0DT3jZczl0diWUZ8g6oU7h0M9cD2EL+PzaYghhCLzR0ZNn5Q==} + engines: {node: '>=10'} + + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + + min-indent@1.0.1: + resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} + engines: {node: '>=4'} + + minimatch@3.1.2: + resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + + minimatch@5.1.6: + resolution: {integrity: sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==} + engines: {node: '>=10'} + + minimist-options@4.1.0: + resolution: {integrity: sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A==} + engines: {node: '>= 6'} + + minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + + modify-values@1.0.1: + resolution: {integrity: sha512-xV2bxeN6F7oYjZWTe/YPAy6MN2M+sL4u/Rlm2AHCIVGfo2p1yGmBHQ6vHehl4bRTZBdHu3TSkWdYgkwpYzAGSw==} + engines: {node: '>=0.10.0'} + + ms@2.1.2: + resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} + + neo-async@2.6.2: + resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} + + normalize-package-data@2.5.0: + resolution: {integrity: sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==} + + normalize-package-data@3.0.3: + resolution: {integrity: sha512-p2W1sgqij3zMMyRC067Dg16bfzVH+w7hyegmpIvZ4JNjqtGOVAIvLmjBx3yP7YTe9vKJgkoNOPjwQGogDoMXFA==} + engines: {node: '>=10'} + + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + + p-limit@1.3.0: + resolution: {integrity: sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==} + engines: {node: '>=4'} + + p-limit@2.3.0: + resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} + engines: {node: '>=6'} + + p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + + p-locate@2.0.0: + resolution: {integrity: sha512-nQja7m7gSKuewoVRen45CtVfODR3crN3goVQ0DDZ9N3yHxgpkuBhZqsaiotSQRrADUrne346peY7kT3TSACykg==} + engines: {node: '>=4'} + + p-locate@3.0.0: + resolution: {integrity: sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==} + engines: {node: '>=6'} + + p-locate@4.1.0: + resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} + engines: {node: '>=8'} + + p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + + p-try@1.0.0: + resolution: {integrity: sha512-U1etNYuMJoIz3ZXSrrySFjsXQTWOx2/jdi86L+2pRvph/qMKL6sbcCYdH23fqsbm8TH2Gn0OybpT4eSFlCVHww==} + engines: {node: '>=4'} + + p-try@2.2.0: + resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} + engines: {node: '>=6'} + + parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + + parse-json@4.0.0: + resolution: {integrity: sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw==} + engines: {node: '>=4'} + + parse-json@5.2.0: + resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} + engines: {node: '>=8'} + + parse5@7.1.2: + resolution: {integrity: sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==} + + path-exists@3.0.0: + resolution: {integrity: sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==} + engines: {node: '>=4'} + + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + + path-type@3.0.0: + resolution: {integrity: sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==} + engines: {node: '>=4'} + + path-type@4.0.0: + resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} + engines: {node: '>=8'} + + pify@2.3.0: + resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==} + engines: {node: '>=0.10.0'} + + pify@3.0.0: + resolution: {integrity: sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==} + engines: {node: '>=4'} + + pluralize@8.0.0: + resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==} + engines: {node: '>=4'} + + poseidon-solidity@0.0.5: + resolution: {integrity: sha512-NzrvSwHzvZgT4hvg2GyGqeR+UOU/eLSEt4wAoXEua+VaR7NTKKwx1X9bPlh1VMBEVEno+IWvkRBbidFGzTeAqQ==} + + prettier@2.8.8: + resolution: {integrity: sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==} + engines: {node: '>=10.13.0'} + hasBin: true + + prettier@3.0.0: + resolution: {integrity: sha512-zBf5eHpwHOGPC47h0zrPyNn+eAEIdEzfywMoYn2XPi0P44Zp0tSq64rq0xAREh4auw2cJZHo9QUob+NqCQky4g==} + engines: {node: '>=14'} + hasBin: true + + process-nextick-args@2.0.1: + resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} + + psl@1.9.0: + resolution: {integrity: sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==} + + punycode@2.3.0: + resolution: {integrity: sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==} + engines: {node: '>=6'} + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + q@1.5.1: + resolution: {integrity: sha512-kV/CThkXo6xyFEZUugw/+pIOywXcDbFYgSct5cT3gqlbkBE1SJdwy6UQoZvodiWF/ckQLZyDE/Bu1M6gVu5lVw==} + engines: {node: '>=0.6.0', teleport: '>=0.2.0'} + + querystringify@2.2.0: + resolution: {integrity: sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==} + + quick-lru@4.0.1: + resolution: {integrity: sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==} + engines: {node: '>=8'} + + read-pkg-up@3.0.0: + resolution: {integrity: sha512-YFzFrVvpC6frF1sz8psoHDBGF7fLPc+llq/8NB43oagqWkx8ar5zYtsTORtOjw9W2RHLpWP+zTWwBvf1bCmcSw==} + engines: {node: '>=4'} + + read-pkg-up@7.0.1: + resolution: {integrity: sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==} + engines: {node: '>=8'} + + read-pkg@3.0.0: + resolution: {integrity: sha512-BLq/cCO9two+lBgiTYNqD6GdtK8s4NpaWrl6/rCO9w0TUS8oJl7cmToOZfRYllKTISY6nt1U7jQ53brmKqY6BA==} + engines: {node: '>=4'} + + read-pkg@5.2.0: + resolution: {integrity: sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==} + engines: {node: '>=8'} + + readable-stream@2.3.8: + resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==} + + readable-stream@3.6.2: + resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} + engines: {node: '>= 6'} + + redent@3.0.0: + resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==} + engines: {node: '>=8'} + + require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + + requires-port@1.0.0: + resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==} + + resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + + resolve@1.22.8: + resolution: {integrity: sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==} + hasBin: true + + rrweb-cssom@0.6.0: + resolution: {integrity: sha512-APM0Gt1KoXBz0iIkkdB/kfvGOwC4UuJFeG/c+yV7wSc7q96cG/kJ0HiYCnzivD9SB53cLV1MlHFNfOuPaadYSw==} + + safe-buffer@5.1.2: + resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} + + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + saxes@6.0.0: + resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} + engines: {node: '>=v12.22.7'} + + semver@5.7.2: + resolution: {integrity: sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==} + hasBin: true + + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + + semver@7.6.0: + resolution: {integrity: sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==} + engines: {node: '>=10'} + hasBin: true + + slice-ansi@4.0.0: + resolution: {integrity: sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==} + engines: {node: '>=10'} + + solhint-community@3.6.0: + resolution: {integrity: sha512-3WGi8nB9VSdC7B3xawktFoQkJEgX6rsUe7jWZJteDBdix+tAOGN+2ZhRr7Cyv1s+h5BRLSsNOXoh7Vg9KEeQbg==} + hasBin: true + + source-map-js@1.0.2: + resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==} + engines: {node: '>=0.10.0'} + + source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + + spdx-correct@3.2.0: + resolution: {integrity: sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==} + + spdx-exceptions@2.5.0: + resolution: {integrity: sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==} + + spdx-expression-parse@3.0.1: + resolution: {integrity: sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==} + + spdx-license-ids@3.0.17: + resolution: {integrity: sha512-sh8PWc/ftMqAAdFiBu6Fy6JUOYjqDJBJvIhpfDMyHrr0Rbp5liZqd4TjtQ/RgfLjKFZb+LMx5hpml5qOWy0qvg==} + + split2@3.2.2: + resolution: {integrity: sha512-9NThjpgZnifTkJpzTZ7Eue85S49QwpNhZTq6GRJwObb6jnLFNGB7Qm73V5HewTROPyxD0C29xqmaI68bQtV+hg==} + + split@1.0.1: + resolution: {integrity: sha512-mTyOoPbrivtXnwnIxZRFYRrPNtEFKlpB2fvjSnCQUiAA6qAZzqwna5envK4uk6OIeP17CsdF3rSBGYVBsU0Tkg==} + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + string_decoder@1.1.1: + resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} + + string_decoder@1.3.0: + resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-bom@3.0.0: + resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} + engines: {node: '>=4'} + + strip-indent@3.0.0: + resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==} + engines: {node: '>=8'} + + supports-color@5.5.0: + resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} + engines: {node: '>=4'} + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + + symbol-tree@3.2.4: + resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} + + table@6.8.1: + resolution: {integrity: sha512-Y4X9zqrCftUhMeH2EptSSERdVKt/nEdijTOacGD/97EKjhQ/Qs8RTlEGABSJNNN8lac9kheH+af7yAkEWlgneA==} + engines: {node: '>=10.0.0'} + + text-extensions@1.9.0: + resolution: {integrity: sha512-wiBrwC1EhBelW12Zy26JeOUkQ5mRu+5o8rpsJk5+2t+Y5vE7e842qtZDQ2g1NpX/29HdyFeJ4nSIhI47ENSxlQ==} + engines: {node: '>=0.10'} + + text-table@0.2.0: + resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} + + through2@2.0.5: + resolution: {integrity: sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==} + + through2@4.0.2: + resolution: {integrity: sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw==} + + through@2.3.8: + resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==} + + tough-cookie@4.1.3: + resolution: {integrity: sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw==} + engines: {node: '>=6'} + + tr46@5.0.0: + resolution: {integrity: sha512-tk2G5R2KRwBd+ZN0zaEXpmzdKyOYksXwywulIX95MBODjSzMIuQnQ3m8JxgbhnL1LeVo7lqQKsYa1O3Htl7K5g==} + engines: {node: '>=18'} + + trim-newlines@3.0.1: + resolution: {integrity: sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw==} + engines: {node: '>=8'} + + type-fest@0.18.1: + resolution: {integrity: sha512-OIAYXk8+ISY+qTOwkHtKqzAuxchoMiD9Udx+FSGQDuiRR+PJKJHc2NJAXlbhkGwTt/4/nKZxELY1w3ReWOL8mw==} + engines: {node: '>=10'} + + type-fest@0.6.0: + resolution: {integrity: sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==} + engines: {node: '>=8'} + + type-fest@0.8.1: + resolution: {integrity: sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==} + engines: {node: '>=8'} + + typedarray@0.0.6: + resolution: {integrity: sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==} + + uglify-js@3.17.4: + resolution: {integrity: sha512-T9q82TJI9e/C1TAxYvfb16xO120tMVFZrGA3f9/P4424DNu6ypK103y0GPFVa17yotwSyZW5iYXgjYHkGrJW/g==} + engines: {node: '>=0.8.0'} + hasBin: true + + universalify@0.2.0: + resolution: {integrity: sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==} + engines: {node: '>= 4.0.0'} + + uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + + url-parse@1.5.10: + resolution: {integrity: sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==} + + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + + validate-npm-package-license@3.0.4: + resolution: {integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==} + + w3c-xmlserializer@5.0.0: + resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==} + engines: {node: '>=18'} + + webidl-conversions@7.0.0: + resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} + engines: {node: '>=12'} + + whatwg-encoding@3.1.1: + resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==} + engines: {node: '>=18'} + + whatwg-mimetype@4.0.0: + resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==} + engines: {node: '>=18'} + + whatwg-url@14.0.0: + resolution: {integrity: sha512-1lfMEm2IEr7RIV+f4lUNPOqfFL+pO+Xw3fJSqmjX9AbXcXcYOkCe1P6+9VBZB6n94af16NfZf+sSk0JCBZC9aw==} + engines: {node: '>=18'} + + wordwrap@1.0.0: + resolution: {integrity: sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==} + + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + + ws@8.16.0: + resolution: {integrity: sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + xml-name-validator@5.0.0: + resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==} + engines: {node: '>=18'} + + xmlchars@2.2.0: + resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} + + xtend@4.0.2: + resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} + engines: {node: '>=0.4'} + + y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + + yallist@4.0.0: + resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} + + yargs-parser@20.2.9: + resolution: {integrity: sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==} + engines: {node: '>=10'} + + yargs-parser@21.1.1: + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} + engines: {node: '>=12'} + + yargs@16.2.0: + resolution: {integrity: sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==} + engines: {node: '>=10'} + + yargs@17.7.2: + resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} + engines: {node: '>=12'} + + yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + +snapshots: + + '@asamuzakjp/dom-selector@2.0.2': dependencies: bidi-js: 1.0.3 css-tree: 2.3.1 is-potential-custom-element-name: 1.0.1 - dev: true - /@babel/code-frame@7.22.5: - resolution: {integrity: sha512-Xmwn266vad+6DAqEB2A6V/CcZVp62BbwVmcOJc2RPuwih1kw02TjQvWVWlcKGbBPd+8/0V5DEkOcizRGYsspYQ==} - engines: {node: '>=6.9.0'} + '@babel/code-frame@7.22.5': dependencies: '@babel/highlight': 7.22.5 - dev: true - /@babel/helper-validator-identifier@7.22.5: - resolution: {integrity: sha512-aJXu+6lErq8ltp+JhkJUfk1MTGyuA4v7f3pA+BJ5HLfNC6nAQ0Cpi9uOquUj8Hehg0aUiHzWQbOVJGao6ztBAQ==} - engines: {node: '>=6.9.0'} - dev: true + '@babel/helper-validator-identifier@7.22.5': {} - /@babel/highlight@7.22.5: - resolution: {integrity: sha512-BSKlD1hgnedS5XRnGOljZawtag7H1yPfQp0tdNJCHoH6AZ+Pcm9VvkrK59/Yy593Ypg0zMxH2BxD1VPYUQ7UIw==} - engines: {node: '>=6.9.0'} + '@babel/highlight@7.22.5': dependencies: '@babel/helper-validator-identifier': 7.22.5 chalk: 2.4.2 js-tokens: 4.0.0 - dev: true - /@hutson/parse-repository-url@3.0.2: - resolution: {integrity: sha512-H9XAx3hc0BQHY6l+IFSWHDySypcXsvsuLhgYLUGywmJ5pswRVQJUHpOsobnLYp2ZUaUlKiKDrgWWhosOwAEM8Q==} - engines: {node: '>=6.9.0'} - dev: true + '@hutson/parse-repository-url@3.0.2': {} - /@solidity-parser/parser@0.16.1: - resolution: {integrity: sha512-PdhRFNhbTtu3x8Axm0uYpqOy/lODYQK+MlYSgqIsq2L8SFYEHJPHNUiOTAJbDGzNjjr1/n9AcIayxafR/fWmYw==} + '@solidity-parser/parser@0.16.1': dependencies: antlr4ts: 0.5.0-alpha.4 - dev: true - /@types/minimist@1.2.5: - resolution: {integrity: sha512-hov8bUuiLiyFPGyFPE1lwWhmzYbirOXQNNo40+y3zow8aFVTeyn3VWL0VFFfdNddA8S4Vf0Tc062rzyNr7Paag==} - dev: true + '@types/minimist@1.2.5': {} - /@types/normalize-package-data@2.4.4: - resolution: {integrity: sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==} - dev: true + '@types/normalize-package-data@2.4.4': {} - /JSONStream@1.3.5: - resolution: {integrity: sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==} - hasBin: true + JSONStream@1.3.5: dependencies: jsonparse: 1.3.1 through: 2.3.8 - dev: true - /add-stream@1.0.0: - resolution: {integrity: sha512-qQLMr+8o0WC4FZGQTcJiKBVC59JylcPSrTtk6usvmIDFUOCKegapy1VHQwRbFMOFyb/inzUVqHs+eMYKDM1YeQ==} - dev: true + add-stream@1.0.0: {} - /agent-base@7.1.0: - resolution: {integrity: sha512-o/zjMZRhJxny7OyEF+Op8X+efiELC7k7yOjMzgfzVqOzXqkBkWI79YoTdOtsuWd5BWhAGAuOY/Xa6xpiaWXiNg==} - engines: {node: '>= 14'} + agent-base@7.1.0: dependencies: debug: 4.3.4 transitivePeerDependencies: - supports-color - dev: true - /ajv@6.12.6: - resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + ajv@6.12.6: dependencies: fast-deep-equal: 3.1.3 fast-json-stable-stringify: 2.1.0 json-schema-traverse: 0.4.1 uri-js: 4.4.1 - dev: true - /ajv@8.12.0: - resolution: {integrity: sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==} + ajv@8.12.0: dependencies: fast-deep-equal: 3.1.3 json-schema-traverse: 1.0.0 require-from-string: 2.0.2 uri-js: 4.4.1 - dev: true - /ansi-regex@5.0.1: - resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} - engines: {node: '>=8'} - dev: true + ansi-regex@5.0.1: {} - /ansi-styles@3.2.1: - resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==} - engines: {node: '>=4'} + ansi-styles@3.2.1: dependencies: color-convert: 1.9.3 - dev: true - /ansi-styles@4.3.0: - resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} - engines: {node: '>=8'} + ansi-styles@4.3.0: dependencies: color-convert: 2.0.1 - dev: true - /antlr4@4.13.0: - resolution: {integrity: sha512-zooUbt+UscjnWyOrsuY/tVFL4rwrAGwOivpQmvmUDE22hy/lUA467Rc1rcixyRwcRUIXFYBwv7+dClDSHdmmew==} - engines: {node: '>=16'} - dev: true + antlr4@4.13.0: {} - /antlr4ts@0.5.0-alpha.4: - resolution: {integrity: sha512-WPQDt1B74OfPv/IMS2ekXAKkTZIHl88uMetg6q3OTqgFxZ/dxDXI0EWLyZid/1Pe6hTftyg5N7gel5wNAGxXyQ==} - dev: true + antlr4ts@0.5.0-alpha.4: {} - /argparse@2.0.1: - resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} - dev: true + argparse@2.0.1: {} - /array-ify@1.0.0: - resolution: {integrity: sha512-c5AMf34bKdvPhQ7tBGhqkgKNUzMr4WUs+WDtC2ZUGOUncbxKMTvqxYctiseW3+L4bA8ec+GcZ6/A/FW4m8ukng==} - dev: true + array-ify@1.0.0: {} - /arrify@1.0.1: - resolution: {integrity: sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA==} - engines: {node: '>=0.10.0'} - dev: true + arrify@1.0.1: {} - /ast-parents@0.0.1: - resolution: {integrity: sha512-XHusKxKz3zoYk1ic8Un640joHbFMhbqneyoZfoKnEGtf2ey9Uh/IdpcQplODdO/kENaMIWsD0nJm4+wX3UNLHA==} - dev: true + ast-parents@0.0.1: {} - /astral-regex@2.0.0: - resolution: {integrity: sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==} - engines: {node: '>=8'} - dev: true + astral-regex@2.0.0: {} - /asynckit@0.4.0: - resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} - dev: true + asynckit@0.4.0: {} - /balanced-match@1.0.2: - resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} - dev: true + balanced-match@1.0.2: {} - /bidi-js@1.0.3: - resolution: {integrity: sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==} + bidi-js@1.0.3: dependencies: require-from-string: 2.0.2 - dev: true - /brace-expansion@1.1.11: - resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} + brace-expansion@1.1.11: dependencies: balanced-match: 1.0.2 concat-map: 0.0.1 - dev: true - /brace-expansion@2.0.1: - resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} + brace-expansion@2.0.1: dependencies: balanced-match: 1.0.2 - dev: true - /buffer-from@1.1.2: - resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} - dev: true + buffer-from@1.1.2: {} - /callsites@3.1.0: - resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} - engines: {node: '>=6'} - dev: true + callsites@3.1.0: {} - /camelcase-keys@6.2.2: - resolution: {integrity: sha512-YrwaA0vEKazPBkn0ipTiMpSajYDSe+KjQfrjhcBMxJt/znbvlHd8Pw/Vamaz5EB4Wfhs3SUR3Z9mwRu/P3s3Yg==} - engines: {node: '>=8'} + camelcase-keys@6.2.2: dependencies: camelcase: 5.3.1 map-obj: 4.3.0 quick-lru: 4.0.1 - dev: true - /camelcase@5.3.1: - resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==} - engines: {node: '>=6'} - dev: true + camelcase@5.3.1: {} - /chalk@2.4.2: - resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} - engines: {node: '>=4'} + chalk@2.4.2: dependencies: ansi-styles: 3.2.1 escape-string-regexp: 1.0.5 supports-color: 5.5.0 - dev: true - /chalk@4.1.2: - resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} - engines: {node: '>=10'} + chalk@4.1.2: dependencies: ansi-styles: 4.3.0 supports-color: 7.2.0 - dev: true - /cliui@7.0.4: - resolution: {integrity: sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==} + cliui@7.0.4: dependencies: string-width: 4.2.3 strip-ansi: 6.0.1 wrap-ansi: 7.0.0 - dev: true - /cliui@8.0.1: - resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} - engines: {node: '>=12'} + cliui@8.0.1: dependencies: string-width: 4.2.3 strip-ansi: 6.0.1 wrap-ansi: 7.0.0 - dev: true - /color-convert@1.9.3: - resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} + color-convert@1.9.3: dependencies: color-name: 1.1.3 - dev: true - /color-convert@2.0.1: - resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} - engines: {node: '>=7.0.0'} + color-convert@2.0.1: dependencies: color-name: 1.1.4 - dev: true - /color-name@1.1.3: - resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==} - dev: true + color-name@1.1.3: {} - /color-name@1.1.4: - resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} - dev: true + color-name@1.1.4: {} - /combined-stream@1.0.8: - resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} - engines: {node: '>= 0.8'} + combined-stream@1.0.8: dependencies: delayed-stream: 1.0.0 - dev: true - /commander@10.0.1: - resolution: {integrity: sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==} - engines: {node: '>=14'} - dev: true + commander@10.0.1: {} - /commit-and-tag-version@12.2.0: - resolution: {integrity: sha512-t8kvDy8dVTCF/j2u7Elt/J3tzCPyh16ssK8ECpj44R6jJiCUJMhWwZ36L652iChvqz4BAem9XU7WyuMYHlmSvA==} - engines: {node: '>=18'} - hasBin: true + commit-and-tag-version@12.2.0: dependencies: chalk: 2.4.2 conventional-changelog: 3.1.25 @@ -304,74 +1263,47 @@ packages: - canvas - supports-color - utf-8-validate - dev: true - /compare-func@2.0.0: - resolution: {integrity: sha512-zHig5N+tPWARooBnb0Zx1MFcdfpyJrfTJ3Y5L+IFvUm8rM74hHz66z0gw0x4tijh5CorKkKUCnW82R2vmpeCRA==} + compare-func@2.0.0: dependencies: array-ify: 1.0.0 dot-prop: 5.3.0 - dev: true - /concat-map@0.0.1: - resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} - dev: true + concat-map@0.0.1: {} - /concat-stream@2.0.0: - resolution: {integrity: sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==} - engines: {'0': node >= 6.0} + concat-stream@2.0.0: dependencies: buffer-from: 1.1.2 inherits: 2.0.4 readable-stream: 3.6.2 typedarray: 0.0.6 - dev: true - /conventional-changelog-angular@5.0.13: - resolution: {integrity: sha512-i/gipMxs7s8L/QeuavPF2hLnJgH6pEZAttySB6aiQLWcX3puWDL3ACVmvBhJGxnAy52Qc15ua26BufY6KpmrVA==} - engines: {node: '>=10'} + conventional-changelog-angular@5.0.13: dependencies: compare-func: 2.0.0 q: 1.5.1 - dev: true - /conventional-changelog-atom@2.0.8: - resolution: {integrity: sha512-xo6v46icsFTK3bb7dY/8m2qvc8sZemRgdqLb/bjpBsH2UyOS8rKNTgcb5025Hri6IpANPApbXMg15QLb1LJpBw==} - engines: {node: '>=10'} + conventional-changelog-atom@2.0.8: dependencies: q: 1.5.1 - dev: true - /conventional-changelog-codemirror@2.0.8: - resolution: {integrity: sha512-z5DAsn3uj1Vfp7po3gpt2Boc+Bdwmw2++ZHa5Ak9k0UKsYAO5mH1UBTN0qSCuJZREIhX6WU4E1p3IW2oRCNzQw==} - engines: {node: '>=10'} + conventional-changelog-codemirror@2.0.8: dependencies: q: 1.5.1 - dev: true - /conventional-changelog-config-spec@2.1.0: - resolution: {integrity: sha512-IpVePh16EbbB02V+UA+HQnnPIohgXvJRxHcS5+Uwk4AT5LjzCZJm5sp/yqs5C6KZJ1jMsV4paEV13BN1pvDuxQ==} - dev: true + conventional-changelog-config-spec@2.1.0: {} - /conventional-changelog-conventionalcommits@4.6.3: - resolution: {integrity: sha512-LTTQV4fwOM4oLPad317V/QNQ1FY4Hju5qeBIM1uTHbrnCE+Eg4CdRZ3gO2pUeR+tzWdp80M2j3qFFEDWVqOV4g==} - engines: {node: '>=10'} + conventional-changelog-conventionalcommits@4.6.3: dependencies: compare-func: 2.0.0 lodash: 4.17.21 q: 1.5.1 - dev: true - /conventional-changelog-conventionalcommits@6.1.0: - resolution: {integrity: sha512-3cS3GEtR78zTfMzk0AizXKKIdN4OvSh7ibNz6/DPbhWWQu7LqE/8+/GqSodV+sywUR2gpJAdP/1JFf4XtN7Zpw==} - engines: {node: '>=14'} + conventional-changelog-conventionalcommits@6.1.0: dependencies: compare-func: 2.0.0 - dev: true - /conventional-changelog-core@4.2.4: - resolution: {integrity: sha512-gDVS+zVJHE2v4SLc6B0sLsPiloR0ygU7HaDW14aNJE1v4SlqJPILPl/aJC7YdtRE4CybBf8gDwObBvKha8Xlyg==} - engines: {node: '>=10'} + conventional-changelog-core@4.2.4: dependencies: add-stream: 1.0.0 conventional-changelog-writer: 5.0.1 @@ -387,58 +1319,33 @@ packages: read-pkg: 3.0.0 read-pkg-up: 3.0.0 through2: 4.0.2 - dev: true - /conventional-changelog-ember@2.0.9: - resolution: {integrity: sha512-ulzIReoZEvZCBDhcNYfDIsLTHzYHc7awh+eI44ZtV5cx6LVxLlVtEmcO+2/kGIHGtw+qVabJYjdI5cJOQgXh1A==} - engines: {node: '>=10'} + conventional-changelog-ember@2.0.9: dependencies: q: 1.5.1 - dev: true - /conventional-changelog-eslint@3.0.9: - resolution: {integrity: sha512-6NpUCMgU8qmWmyAMSZO5NrRd7rTgErjrm4VASam2u5jrZS0n38V7Y9CzTtLT2qwz5xEChDR4BduoWIr8TfwvXA==} - engines: {node: '>=10'} + conventional-changelog-eslint@3.0.9: dependencies: q: 1.5.1 - dev: true - /conventional-changelog-express@2.0.6: - resolution: {integrity: sha512-SDez2f3iVJw6V563O3pRtNwXtQaSmEfTCaTBPCqn0oG0mfkq0rX4hHBq5P7De2MncoRixrALj3u3oQsNK+Q0pQ==} - engines: {node: '>=10'} + conventional-changelog-express@2.0.6: dependencies: q: 1.5.1 - dev: true - /conventional-changelog-jquery@3.0.11: - resolution: {integrity: sha512-x8AWz5/Td55F7+o/9LQ6cQIPwrCjfJQ5Zmfqi8thwUEKHstEn4kTIofXub7plf1xvFA2TqhZlq7fy5OmV6BOMw==} - engines: {node: '>=10'} + conventional-changelog-jquery@3.0.11: dependencies: q: 1.5.1 - dev: true - /conventional-changelog-jshint@2.0.9: - resolution: {integrity: sha512-wMLdaIzq6TNnMHMy31hql02OEQ8nCQfExw1SE0hYL5KvU+JCTuPaDO+7JiogGT2gJAxiUGATdtYYfh+nT+6riA==} - engines: {node: '>=10'} + conventional-changelog-jshint@2.0.9: dependencies: compare-func: 2.0.0 q: 1.5.1 - dev: true - /conventional-changelog-preset-loader@2.3.4: - resolution: {integrity: sha512-GEKRWkrSAZeTq5+YjUZOYxdHq+ci4dNwHvpaBC3+ENalzFWuCWa9EZXSuZBpkr72sMdKB+1fyDV4takK1Lf58g==} - engines: {node: '>=10'} - dev: true + conventional-changelog-preset-loader@2.3.4: {} - /conventional-changelog-preset-loader@3.0.0: - resolution: {integrity: sha512-qy9XbdSLmVnwnvzEisjxdDiLA4OmV3o8db+Zdg4WiFw14fP3B6XNz98X0swPPpkTd/pc1K7+adKgEDM1JCUMiA==} - engines: {node: '>=14'} - dev: true + conventional-changelog-preset-loader@3.0.0: {} - /conventional-changelog-writer@5.0.1: - resolution: {integrity: sha512-5WsuKUfxW7suLblAbFnxAcrvf6r+0b7GvNaWUwUIk0bXMnENP/PEieGKVUQrjPqwPT4o3EPAASBXiY6iHooLOQ==} - engines: {node: '>=10'} - hasBin: true + conventional-changelog-writer@5.0.1: dependencies: conventional-commits-filter: 2.0.7 dateformat: 3.0.3 @@ -449,11 +1356,8 @@ packages: semver: 6.3.1 split: 1.0.1 through2: 4.0.2 - dev: true - /conventional-changelog@3.1.25: - resolution: {integrity: sha512-ryhi3fd1mKf3fSjbLXOfK2D06YwKNic1nC9mWqybBHdObPd8KJ2vjaXZfYj1U23t+V8T8n0d7gwnc9XbIdFbyQ==} - engines: {node: '>=10'} + conventional-changelog@3.1.25: dependencies: conventional-changelog-angular: 5.0.13 conventional-changelog-atom: 2.0.8 @@ -466,28 +1370,18 @@ packages: conventional-changelog-jquery: 3.0.11 conventional-changelog-jshint: 2.0.9 conventional-changelog-preset-loader: 2.3.4 - dev: true - /conventional-commits-filter@2.0.7: - resolution: {integrity: sha512-ASS9SamOP4TbCClsRHxIHXRfcGCnIoQqkvAzCSbZzTFLfcTqJVugB0agRgsEELsqaeWgsXv513eS116wnlSSPA==} - engines: {node: '>=10'} + conventional-commits-filter@2.0.7: dependencies: lodash.ismatch: 4.4.0 modify-values: 1.0.1 - dev: true - /conventional-commits-filter@3.0.0: - resolution: {integrity: sha512-1ymej8b5LouPx9Ox0Dw/qAO2dVdfpRFq28e5Y0jJEU8ZrLdy0vOSkkIInwmxErFGhg6SALro60ZrwYFVTUDo4Q==} - engines: {node: '>=14'} + conventional-commits-filter@3.0.0: dependencies: lodash.ismatch: 4.4.0 modify-values: 1.0.1 - dev: true - /conventional-commits-parser@3.2.4: - resolution: {integrity: sha512-nK7sAtfi+QXbxHCYfhpZsfRtaitZLIA6889kFIouLvz6repszQDgxBu7wf2WbU+Dco7sAnNCJYERCwt54WPC2Q==} - engines: {node: '>=10'} - hasBin: true + conventional-commits-parser@3.2.4: dependencies: JSONStream: 1.3.5 is-text-path: 1.0.1 @@ -495,23 +1389,15 @@ packages: meow: 8.1.2 split2: 3.2.2 through2: 4.0.2 - dev: true - /conventional-commits-parser@4.0.0: - resolution: {integrity: sha512-WRv5j1FsVM5FISJkoYMR6tPk07fkKT0UodruX4je86V4owk451yjXAKzKAPOs9l7y59E2viHUS9eQ+dfUA9NSg==} - engines: {node: '>=14'} - hasBin: true + conventional-commits-parser@4.0.0: dependencies: JSONStream: 1.3.5 is-text-path: 1.0.1 meow: 8.1.2 split2: 3.2.2 - dev: true - /conventional-recommended-bump@7.0.1: - resolution: {integrity: sha512-Ft79FF4SlOFvX4PkwFDRnaNiIVX7YbmqGU0RwccUaiGvgp3S0a8ipR2/Qxk31vclDNM+GSdJOVs2KrsUCjblVA==} - engines: {node: '>=14'} - hasBin: true + conventional-recommended-bump@7.0.1: dependencies: concat-stream: 2.0.0 conventional-changelog-preset-loader: 3.0.0 @@ -520,293 +1406,165 @@ packages: git-raw-commits: 3.0.0 git-semver-tags: 5.0.1 meow: 8.1.2 - dev: true - /core-util-is@1.0.3: - resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} - dev: true + core-util-is@1.0.3: {} - /cosmiconfig@8.2.0: - resolution: {integrity: sha512-3rTMnFJA1tCOPwRxtgF4wd7Ab2qvDbL8jX+3smjIbS4HlZBagTlpERbdN7iAbWlrfxE3M8c27kTwTawQ7st+OQ==} - engines: {node: '>=14'} + cosmiconfig@8.2.0: dependencies: import-fresh: 3.3.0 js-yaml: 4.1.0 parse-json: 5.2.0 path-type: 4.0.0 - dev: true - /css-tree@2.3.1: - resolution: {integrity: sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==} - engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} + css-tree@2.3.1: dependencies: mdn-data: 2.0.30 source-map-js: 1.0.2 - dev: true - /cssstyle@4.0.1: - resolution: {integrity: sha512-8ZYiJ3A/3OkDd093CBT/0UKDWry7ak4BdPTFP2+QEP7cmhouyq/Up709ASSj2cK02BbZiMgk7kYjZNS4QP5qrQ==} - engines: {node: '>=18'} + cssstyle@4.0.1: dependencies: rrweb-cssom: 0.6.0 - dev: true - /dargs@7.0.0: - resolution: {integrity: sha512-2iy1EkLdlBzQGvbweYRFxmFath8+K7+AKB0TlhHWkNuH+TmovaMH/Wp7V7R4u7f4SnX3OgLsU9t1NI9ioDnUpg==} - engines: {node: '>=8'} - dev: true + dargs@7.0.0: {} - /data-urls@5.0.0: - resolution: {integrity: sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==} - engines: {node: '>=18'} + data-urls@5.0.0: dependencies: whatwg-mimetype: 4.0.0 whatwg-url: 14.0.0 - dev: true - /dateformat@3.0.3: - resolution: {integrity: sha512-jyCETtSl3VMZMWeRo7iY1FL19ges1t55hMo5yaam4Jrsm5EPL89UQkoQRyiI+Yf4k8r2ZpdngkV8hr1lIdjb3Q==} - dev: true + dateformat@3.0.3: {} - /debug@4.3.4: - resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} - engines: {node: '>=6.0'} - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true + debug@4.3.4: dependencies: ms: 2.1.2 - dev: true - /decamelize-keys@1.1.1: - resolution: {integrity: sha512-WiPxgEirIV0/eIOMcnFBA3/IJZAZqKnwAwWyvvdi4lsr1WCN22nhdf/3db3DoZcUjTV2SqfzIwNyp6y2xs3nmg==} - engines: {node: '>=0.10.0'} + decamelize-keys@1.1.1: dependencies: decamelize: 1.2.0 map-obj: 1.0.1 - dev: true - /decamelize@1.2.0: - resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==} - engines: {node: '>=0.10.0'} - dev: true + decamelize@1.2.0: {} - /decimal.js@10.4.3: - resolution: {integrity: sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==} - dev: true + decimal.js@10.4.3: {} - /delayed-stream@1.0.0: - resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} - engines: {node: '>=0.4.0'} - dev: true + delayed-stream@1.0.0: {} - /detect-indent@6.1.0: - resolution: {integrity: sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==} - engines: {node: '>=8'} - dev: true + detect-indent@6.1.0: {} - /detect-newline@3.1.0: - resolution: {integrity: sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==} - engines: {node: '>=8'} - dev: true + detect-newline@3.1.0: {} - /dot-prop@5.3.0: - resolution: {integrity: sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==} - engines: {node: '>=8'} + dot-prop@5.3.0: dependencies: is-obj: 2.0.0 - dev: true - /dotgitignore@2.1.0: - resolution: {integrity: sha512-sCm11ak2oY6DglEPpCB8TixLjWAxd3kJTs6UIcSasNYxXdFPV+YKlye92c8H4kKFqV5qYMIh7d+cYecEg0dIkA==} - engines: {node: '>=6'} + dotgitignore@2.1.0: dependencies: find-up: 3.0.0 minimatch: 3.1.2 - dev: true - /emoji-regex@8.0.0: - resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} - dev: true + emoji-regex@8.0.0: {} - /entities@4.5.0: - resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} - engines: {node: '>=0.12'} - dev: true + entities@4.5.0: {} - /error-ex@1.3.2: - resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==} + error-ex@1.3.2: dependencies: is-arrayish: 0.2.1 - dev: true - /escalade@3.1.2: - resolution: {integrity: sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==} - engines: {node: '>=6'} - dev: true + escalade@3.1.2: {} - /escape-string-regexp@1.0.5: - resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} - engines: {node: '>=0.8.0'} - dev: true + escape-string-regexp@1.0.5: {} - /fast-deep-equal@3.1.3: - resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} - dev: true + fast-deep-equal@3.1.3: {} - /fast-diff@1.3.0: - resolution: {integrity: sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==} - dev: true + fast-diff@1.3.0: {} - /fast-json-stable-stringify@2.1.0: - resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} - dev: true + fast-json-stable-stringify@2.1.0: {} - /figures@3.2.0: - resolution: {integrity: sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==} - engines: {node: '>=8'} + figures@3.2.0: dependencies: escape-string-regexp: 1.0.5 - dev: true - /find-up@2.1.0: - resolution: {integrity: sha512-NWzkk0jSJtTt08+FBFMvXoeZnOJD+jTtsRmBYbAIzJdX6l7dLgR7CTubCM5/eDdPUBvLCeVasP1brfVR/9/EZQ==} - engines: {node: '>=4'} + find-up@2.1.0: dependencies: locate-path: 2.0.0 - dev: true - /find-up@3.0.0: - resolution: {integrity: sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==} - engines: {node: '>=6'} + find-up@3.0.0: dependencies: locate-path: 3.0.0 - dev: true - /find-up@4.1.0: - resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} - engines: {node: '>=8'} + find-up@4.1.0: dependencies: locate-path: 5.0.0 path-exists: 4.0.0 - dev: true - /find-up@5.0.0: - resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} - engines: {node: '>=10'} + find-up@5.0.0: dependencies: locate-path: 6.0.0 path-exists: 4.0.0 - dev: true - /form-data@4.0.0: - resolution: {integrity: sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==} - engines: {node: '>= 6'} + form-data@4.0.0: dependencies: asynckit: 0.4.0 combined-stream: 1.0.8 mime-types: 2.1.35 - dev: true - /fs.realpath@1.0.0: - resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} - dev: true + fs.realpath@1.0.0: {} - /function-bind@1.1.2: - resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} - dev: true + function-bind@1.1.2: {} - /get-caller-file@2.0.5: - resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} - engines: {node: 6.* || 8.* || >= 10.*} - dev: true + get-caller-file@2.0.5: {} - /get-pkg-repo@4.2.1: - resolution: {integrity: sha512-2+QbHjFRfGB74v/pYWjd5OhU3TDIC2Gv/YKUTk/tCvAz0pkn/Mz6P3uByuBimLOcPvN2jYdScl3xGFSrx0jEcA==} - engines: {node: '>=6.9.0'} - hasBin: true + get-pkg-repo@4.2.1: dependencies: '@hutson/parse-repository-url': 3.0.2 hosted-git-info: 4.1.0 through2: 2.0.5 yargs: 16.2.0 - dev: true - /git-raw-commits@2.0.11: - resolution: {integrity: sha512-VnctFhw+xfj8Va1xtfEqCUD2XDrbAPSJx+hSrE5K7fGdjZruW7XV+QOrN7LF/RJyvspRiD2I0asWsxFp0ya26A==} - engines: {node: '>=10'} - hasBin: true + git-raw-commits@2.0.11: dependencies: dargs: 7.0.0 lodash: 4.17.21 meow: 8.1.2 split2: 3.2.2 through2: 4.0.2 - dev: true - /git-raw-commits@3.0.0: - resolution: {integrity: sha512-b5OHmZ3vAgGrDn/X0kS+9qCfNKWe4K/jFnhwzVWWg0/k5eLa3060tZShrRg8Dja5kPc+YjS0Gc6y7cRr44Lpjw==} - engines: {node: '>=14'} - hasBin: true + git-raw-commits@3.0.0: dependencies: dargs: 7.0.0 meow: 8.1.2 split2: 3.2.2 - dev: true - /git-remote-origin-url@2.0.0: - resolution: {integrity: sha512-eU+GGrZgccNJcsDH5LkXR3PB9M958hxc7sbA8DFJjrv9j4L2P/eZfKhM+QD6wyzpiv+b1BpK0XrYCxkovtjSLw==} - engines: {node: '>=4'} + git-remote-origin-url@2.0.0: dependencies: gitconfiglocal: 1.0.0 pify: 2.3.0 - dev: true - /git-semver-tags@4.1.1: - resolution: {integrity: sha512-OWyMt5zBe7xFs8vglMmhM9lRQzCWL3WjHtxNNfJTMngGym7pC1kh8sP6jevfydJ6LP3ZvGxfb6ABYgPUM0mtsA==} - engines: {node: '>=10'} - hasBin: true + git-semver-tags@4.1.1: dependencies: meow: 8.1.2 semver: 6.3.1 - dev: true - /git-semver-tags@5.0.1: - resolution: {integrity: sha512-hIvOeZwRbQ+7YEUmCkHqo8FOLQZCEn18yevLHADlFPZY02KJGsu5FZt9YW/lybfK2uhWFI7Qg/07LekJiTv7iA==} - engines: {node: '>=14'} - hasBin: true + git-semver-tags@5.0.1: dependencies: meow: 8.1.2 semver: 7.6.0 - dev: true - /gitconfiglocal@1.0.0: - resolution: {integrity: sha512-spLUXeTAVHxDtKsJc8FkFVgFtMdEN9qPGpL23VfSHx4fP4+Ds097IXLvymbnDH8FnmxX5Nr9bPw3A+AQ6mWEaQ==} + gitconfiglocal@1.0.0: dependencies: ini: 1.3.8 - dev: true - /glob@8.1.0: - resolution: {integrity: sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==} - engines: {node: '>=12'} + glob@8.1.0: dependencies: fs.realpath: 1.0.0 inflight: 1.0.6 inherits: 2.0.4 minimatch: 5.1.6 once: 1.4.0 - dev: true - /graceful-fs@4.2.11: - resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} - dev: true + graceful-fs@4.2.11: {} - /handlebars@4.7.8: - resolution: {integrity: sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==} - engines: {node: '>=0.4.7'} - hasBin: true + handlebars@4.7.8: dependencies: minimist: 1.2.8 neo-async: 2.6.2 @@ -814,167 +1572,94 @@ packages: wordwrap: 1.0.0 optionalDependencies: uglify-js: 3.17.4 - dev: true - /hard-rejection@2.1.0: - resolution: {integrity: sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA==} - engines: {node: '>=6'} - dev: true + hard-rejection@2.1.0: {} - /has-flag@3.0.0: - resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} - engines: {node: '>=4'} - dev: true + has-flag@3.0.0: {} - /has-flag@4.0.0: - resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} - engines: {node: '>=8'} - dev: true + has-flag@4.0.0: {} - /hasown@2.0.1: - resolution: {integrity: sha512-1/th4MHjnwncwXsIW6QMzlvYL9kG5e/CpVvLRZe4XPa8TOUNbCELqmvhDmnkNsAjwaG4+I8gJJL0JBvTTLO9qA==} - engines: {node: '>= 0.4'} + hasown@2.0.1: dependencies: function-bind: 1.1.2 - dev: true - /hosted-git-info@2.8.9: - resolution: {integrity: sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==} - dev: true + hosted-git-info@2.8.9: {} - /hosted-git-info@4.1.0: - resolution: {integrity: sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==} - engines: {node: '>=10'} + hosted-git-info@4.1.0: dependencies: lru-cache: 6.0.0 - dev: true - /html-encoding-sniffer@4.0.0: - resolution: {integrity: sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==} - engines: {node: '>=18'} + html-encoding-sniffer@4.0.0: dependencies: whatwg-encoding: 3.1.1 - dev: true - /http-proxy-agent@7.0.2: - resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} - engines: {node: '>= 14'} + http-proxy-agent@7.0.2: dependencies: agent-base: 7.1.0 debug: 4.3.4 transitivePeerDependencies: - supports-color - dev: true - /https-proxy-agent@7.0.4: - resolution: {integrity: sha512-wlwpilI7YdjSkWaQ/7omYBMTliDcmCN8OLihO6I9B86g06lMyAoqgoDpV0XqoaPOKj+0DIdAvnsWfyAAhmimcg==} - engines: {node: '>= 14'} + https-proxy-agent@7.0.4: dependencies: agent-base: 7.1.0 debug: 4.3.4 transitivePeerDependencies: - supports-color - dev: true - /iconv-lite@0.6.3: - resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} - engines: {node: '>=0.10.0'} + iconv-lite@0.6.3: dependencies: safer-buffer: 2.1.2 - dev: true - /ignore@5.2.4: - resolution: {integrity: sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==} - engines: {node: '>= 4'} - dev: true + ignore@5.2.4: {} - /import-fresh@3.3.0: - resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==} - engines: {node: '>=6'} + import-fresh@3.3.0: dependencies: parent-module: 1.0.1 resolve-from: 4.0.0 - dev: true - /indent-string@4.0.0: - resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} - engines: {node: '>=8'} - dev: true + imt.sol@https://gitpkg.vercel.app/privacy-scaling-explorations/zk-kit/packages/imt.sol?0699fd1e5ad3683ae0090e0626f75d7834145500: + dependencies: + poseidon-solidity: 0.0.5 - /inflight@1.0.6: - resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} + indent-string@4.0.0: {} + + inflight@1.0.6: dependencies: once: 1.4.0 wrappy: 1.0.2 - dev: true - /inherits@2.0.4: - resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} - dev: true + inherits@2.0.4: {} - /ini@1.3.8: - resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} - dev: true + ini@1.3.8: {} - /is-arrayish@0.2.1: - resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} - dev: true + is-arrayish@0.2.1: {} - /is-core-module@2.13.1: - resolution: {integrity: sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==} + is-core-module@2.13.1: dependencies: hasown: 2.0.1 - dev: true - /is-fullwidth-code-point@3.0.0: - resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} - engines: {node: '>=8'} - dev: true + is-fullwidth-code-point@3.0.0: {} - /is-obj@2.0.0: - resolution: {integrity: sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==} - engines: {node: '>=8'} - dev: true + is-obj@2.0.0: {} - /is-plain-obj@1.1.0: - resolution: {integrity: sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==} - engines: {node: '>=0.10.0'} - dev: true + is-plain-obj@1.1.0: {} - /is-potential-custom-element-name@1.0.1: - resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} - dev: true + is-potential-custom-element-name@1.0.1: {} - /is-text-path@1.0.1: - resolution: {integrity: sha512-xFuJpne9oFz5qDaodwmmG08e3CawH/2ZV8Qqza1Ko7Sk8POWbkRdwIoAWVhqvq0XeUzANEhKo2n0IXUGBm7A/w==} - engines: {node: '>=0.10.0'} + is-text-path@1.0.1: dependencies: text-extensions: 1.9.0 - dev: true - /isarray@1.0.0: - resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} - dev: true + isarray@1.0.0: {} - /js-tokens@4.0.0: - resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} - dev: true + js-tokens@4.0.0: {} - /js-yaml@4.1.0: - resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} - hasBin: true + js-yaml@4.1.0: dependencies: - argparse: 2.0.1 - dev: true - - /jsdom@23.2.0: - resolution: {integrity: sha512-L88oL7D/8ufIES+Zjz7v0aes+oBMh2Xnh3ygWvL0OaICOomKEPKuPnIfBJekiXr+BHbbMjrWn/xqrDQuxFTeyA==} - engines: {node: '>=18'} - peerDependencies: - canvas: ^2.11.2 - peerDependenciesMeta: - canvas: - optional: true + argparse: 2.0.1 + + jsdom@23.2.0: dependencies: '@asamuzakjp/dom-selector': 2.0.2 cssstyle: 4.0.1 @@ -1001,118 +1686,65 @@ packages: - bufferutil - supports-color - utf-8-validate - dev: true - /json-parse-better-errors@1.0.2: - resolution: {integrity: sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==} - dev: true + json-parse-better-errors@1.0.2: {} - /json-parse-even-better-errors@2.3.1: - resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} - dev: true + json-parse-even-better-errors@2.3.1: {} - /json-schema-traverse@0.4.1: - resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} - dev: true + json-schema-traverse@0.4.1: {} - /json-schema-traverse@1.0.0: - resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} - dev: true + json-schema-traverse@1.0.0: {} - /json-stringify-safe@5.0.1: - resolution: {integrity: sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==} - dev: true + json-stringify-safe@5.0.1: {} - /jsonparse@1.3.1: - resolution: {integrity: sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==} - engines: {'0': node >= 0.2.0} - dev: true + jsonparse@1.3.1: {} - /kind-of@6.0.3: - resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==} - engines: {node: '>=0.10.0'} - dev: true + kind-of@6.0.3: {} - /lines-and-columns@1.2.4: - resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} - dev: true + lines-and-columns@1.2.4: {} - /load-json-file@4.0.0: - resolution: {integrity: sha512-Kx8hMakjX03tiGTLAIdJ+lL0htKnXjEZN6hk/tozf/WOuYGdZBJrZ+rCJRbVCugsjB3jMLn9746NsQIf5VjBMw==} - engines: {node: '>=4'} + load-json-file@4.0.0: dependencies: graceful-fs: 4.2.11 parse-json: 4.0.0 pify: 3.0.0 strip-bom: 3.0.0 - dev: true - /locate-path@2.0.0: - resolution: {integrity: sha512-NCI2kiDkyR7VeEKm27Kda/iQHyKJe1Bu0FlTbYp3CqJu+9IFe9bLyAjMxf5ZDDbEg+iMPzB5zYyUTSm8wVTKmA==} - engines: {node: '>=4'} + locate-path@2.0.0: dependencies: p-locate: 2.0.0 path-exists: 3.0.0 - dev: true - /locate-path@3.0.0: - resolution: {integrity: sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==} - engines: {node: '>=6'} + locate-path@3.0.0: dependencies: p-locate: 3.0.0 path-exists: 3.0.0 - dev: true - /locate-path@5.0.0: - resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} - engines: {node: '>=8'} + locate-path@5.0.0: dependencies: p-locate: 4.1.0 - dev: true - /locate-path@6.0.0: - resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} - engines: {node: '>=10'} + locate-path@6.0.0: dependencies: p-locate: 5.0.0 - dev: true - /lodash.ismatch@4.4.0: - resolution: {integrity: sha512-fPMfXjGQEV9Xsq/8MTSgUf255gawYRbjwMyDbcvDhXgV7enSZA0hynz6vMPnpAb5iONEzBHBPsT+0zes5Z301g==} - dev: true + lodash.ismatch@4.4.0: {} - /lodash.truncate@4.4.2: - resolution: {integrity: sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==} - dev: true + lodash.truncate@4.4.2: {} - /lodash@4.17.21: - resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} - dev: true + lodash@4.17.21: {} - /lru-cache@6.0.0: - resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} - engines: {node: '>=10'} + lru-cache@6.0.0: dependencies: yallist: 4.0.0 - dev: true - /map-obj@1.0.1: - resolution: {integrity: sha512-7N/q3lyZ+LVCp7PzuxrJr4KMbBE2hW7BT7YNia330OFxIf4d3r5zVpicP2650l7CPN6RM9zOJRl3NGpqSiw3Eg==} - engines: {node: '>=0.10.0'} - dev: true + map-obj@1.0.1: {} - /map-obj@4.3.0: - resolution: {integrity: sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ==} - engines: {node: '>=8'} - dev: true + map-obj@4.3.0: {} - /mdn-data@2.0.30: - resolution: {integrity: sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==} - dev: true + mdn-data@2.0.30: {} - /meow@8.1.2: - resolution: {integrity: sha512-r85E3NdZ+mpYk1C6RjPFEMSE+s1iZMuHtsHAqY0DT3jZczl0diWUZ8g6oU7h0M9cD2EL+PzaYghhCLzR0ZNn5Q==} - engines: {node: '>=10'} + meow@8.1.2: dependencies: '@types/minimist': 1.2.5 camelcase-keys: 6.2.2 @@ -1125,308 +1757,171 @@ packages: trim-newlines: 3.0.1 type-fest: 0.18.1 yargs-parser: 20.2.9 - dev: true - /mime-db@1.52.0: - resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} - engines: {node: '>= 0.6'} - dev: true + mime-db@1.52.0: {} - /mime-types@2.1.35: - resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} - engines: {node: '>= 0.6'} + mime-types@2.1.35: dependencies: mime-db: 1.52.0 - dev: true - /min-indent@1.0.1: - resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} - engines: {node: '>=4'} - dev: true + min-indent@1.0.1: {} - /minimatch@3.1.2: - resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + minimatch@3.1.2: dependencies: brace-expansion: 1.1.11 - dev: true - /minimatch@5.1.6: - resolution: {integrity: sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==} - engines: {node: '>=10'} + minimatch@5.1.6: dependencies: brace-expansion: 2.0.1 - dev: true - /minimist-options@4.1.0: - resolution: {integrity: sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A==} - engines: {node: '>= 6'} + minimist-options@4.1.0: dependencies: arrify: 1.0.1 is-plain-obj: 1.1.0 kind-of: 6.0.3 - dev: true - /minimist@1.2.8: - resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} - dev: true + minimist@1.2.8: {} - /modify-values@1.0.1: - resolution: {integrity: sha512-xV2bxeN6F7oYjZWTe/YPAy6MN2M+sL4u/Rlm2AHCIVGfo2p1yGmBHQ6vHehl4bRTZBdHu3TSkWdYgkwpYzAGSw==} - engines: {node: '>=0.10.0'} - dev: true + modify-values@1.0.1: {} - /ms@2.1.2: - resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} - dev: true + ms@2.1.2: {} - /neo-async@2.6.2: - resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} - dev: true + neo-async@2.6.2: {} - /normalize-package-data@2.5.0: - resolution: {integrity: sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==} + normalize-package-data@2.5.0: dependencies: hosted-git-info: 2.8.9 resolve: 1.22.8 semver: 5.7.2 validate-npm-package-license: 3.0.4 - dev: true - /normalize-package-data@3.0.3: - resolution: {integrity: sha512-p2W1sgqij3zMMyRC067Dg16bfzVH+w7hyegmpIvZ4JNjqtGOVAIvLmjBx3yP7YTe9vKJgkoNOPjwQGogDoMXFA==} - engines: {node: '>=10'} + normalize-package-data@3.0.3: dependencies: hosted-git-info: 4.1.0 is-core-module: 2.13.1 semver: 7.6.0 validate-npm-package-license: 3.0.4 - dev: true - /once@1.4.0: - resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + once@1.4.0: dependencies: wrappy: 1.0.2 - dev: true - /p-limit@1.3.0: - resolution: {integrity: sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==} - engines: {node: '>=4'} + p-limit@1.3.0: dependencies: p-try: 1.0.0 - dev: true - /p-limit@2.3.0: - resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} - engines: {node: '>=6'} + p-limit@2.3.0: dependencies: p-try: 2.2.0 - dev: true - /p-limit@3.1.0: - resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} - engines: {node: '>=10'} + p-limit@3.1.0: dependencies: yocto-queue: 0.1.0 - dev: true - /p-locate@2.0.0: - resolution: {integrity: sha512-nQja7m7gSKuewoVRen45CtVfODR3crN3goVQ0DDZ9N3yHxgpkuBhZqsaiotSQRrADUrne346peY7kT3TSACykg==} - engines: {node: '>=4'} + p-locate@2.0.0: dependencies: p-limit: 1.3.0 - dev: true - /p-locate@3.0.0: - resolution: {integrity: sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==} - engines: {node: '>=6'} + p-locate@3.0.0: dependencies: p-limit: 2.3.0 - dev: true - /p-locate@4.1.0: - resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} - engines: {node: '>=8'} + p-locate@4.1.0: dependencies: p-limit: 2.3.0 - dev: true - /p-locate@5.0.0: - resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} - engines: {node: '>=10'} + p-locate@5.0.0: dependencies: p-limit: 3.1.0 - dev: true - /p-try@1.0.0: - resolution: {integrity: sha512-U1etNYuMJoIz3ZXSrrySFjsXQTWOx2/jdi86L+2pRvph/qMKL6sbcCYdH23fqsbm8TH2Gn0OybpT4eSFlCVHww==} - engines: {node: '>=4'} - dev: true + p-try@1.0.0: {} - /p-try@2.2.0: - resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} - engines: {node: '>=6'} - dev: true + p-try@2.2.0: {} - /parent-module@1.0.1: - resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} - engines: {node: '>=6'} + parent-module@1.0.1: dependencies: callsites: 3.1.0 - dev: true - /parse-json@4.0.0: - resolution: {integrity: sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw==} - engines: {node: '>=4'} + parse-json@4.0.0: dependencies: error-ex: 1.3.2 json-parse-better-errors: 1.0.2 - dev: true - /parse-json@5.2.0: - resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} - engines: {node: '>=8'} + parse-json@5.2.0: dependencies: '@babel/code-frame': 7.22.5 error-ex: 1.3.2 json-parse-even-better-errors: 2.3.1 lines-and-columns: 1.2.4 - dev: true - /parse5@7.1.2: - resolution: {integrity: sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==} + parse5@7.1.2: dependencies: entities: 4.5.0 - dev: true - /path-exists@3.0.0: - resolution: {integrity: sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==} - engines: {node: '>=4'} - dev: true + path-exists@3.0.0: {} - /path-exists@4.0.0: - resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} - engines: {node: '>=8'} - dev: true + path-exists@4.0.0: {} - /path-parse@1.0.7: - resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} - dev: true + path-parse@1.0.7: {} - /path-type@3.0.0: - resolution: {integrity: sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==} - engines: {node: '>=4'} + path-type@3.0.0: dependencies: pify: 3.0.0 - dev: true - /path-type@4.0.0: - resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} - engines: {node: '>=8'} - dev: true + path-type@4.0.0: {} - /pify@2.3.0: - resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==} - engines: {node: '>=0.10.0'} - dev: true + pify@2.3.0: {} - /pify@3.0.0: - resolution: {integrity: sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==} - engines: {node: '>=4'} - dev: true + pify@3.0.0: {} - /pluralize@8.0.0: - resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==} - engines: {node: '>=4'} - dev: true + pluralize@8.0.0: {} - /poseidon-solidity@0.0.5: - resolution: {integrity: sha512-NzrvSwHzvZgT4hvg2GyGqeR+UOU/eLSEt4wAoXEua+VaR7NTKKwx1X9bPlh1VMBEVEno+IWvkRBbidFGzTeAqQ==} - dev: false + poseidon-solidity@0.0.5: {} - /prettier@2.8.8: - resolution: {integrity: sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==} - engines: {node: '>=10.13.0'} - hasBin: true - requiresBuild: true - dev: true + prettier@2.8.8: optional: true - /prettier@3.0.0: - resolution: {integrity: sha512-zBf5eHpwHOGPC47h0zrPyNn+eAEIdEzfywMoYn2XPi0P44Zp0tSq64rq0xAREh4auw2cJZHo9QUob+NqCQky4g==} - engines: {node: '>=14'} - hasBin: true - dev: true + prettier@3.0.0: {} - /process-nextick-args@2.0.1: - resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} - dev: true + process-nextick-args@2.0.1: {} - /psl@1.9.0: - resolution: {integrity: sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==} - dev: true + psl@1.9.0: {} - /punycode@2.3.0: - resolution: {integrity: sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==} - engines: {node: '>=6'} - dev: true + punycode@2.3.0: {} - /punycode@2.3.1: - resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} - engines: {node: '>=6'} - dev: true + punycode@2.3.1: {} - /q@1.5.1: - resolution: {integrity: sha512-kV/CThkXo6xyFEZUugw/+pIOywXcDbFYgSct5cT3gqlbkBE1SJdwy6UQoZvodiWF/ckQLZyDE/Bu1M6gVu5lVw==} - engines: {node: '>=0.6.0', teleport: '>=0.2.0'} - dev: true + q@1.5.1: {} - /querystringify@2.2.0: - resolution: {integrity: sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==} - dev: true + querystringify@2.2.0: {} - /quick-lru@4.0.1: - resolution: {integrity: sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==} - engines: {node: '>=8'} - dev: true + quick-lru@4.0.1: {} - /read-pkg-up@3.0.0: - resolution: {integrity: sha512-YFzFrVvpC6frF1sz8psoHDBGF7fLPc+llq/8NB43oagqWkx8ar5zYtsTORtOjw9W2RHLpWP+zTWwBvf1bCmcSw==} - engines: {node: '>=4'} + read-pkg-up@3.0.0: dependencies: find-up: 2.1.0 read-pkg: 3.0.0 - dev: true - /read-pkg-up@7.0.1: - resolution: {integrity: sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==} - engines: {node: '>=8'} + read-pkg-up@7.0.1: dependencies: find-up: 4.1.0 read-pkg: 5.2.0 type-fest: 0.8.1 - dev: true - /read-pkg@3.0.0: - resolution: {integrity: sha512-BLq/cCO9two+lBgiTYNqD6GdtK8s4NpaWrl6/rCO9w0TUS8oJl7cmToOZfRYllKTISY6nt1U7jQ53brmKqY6BA==} - engines: {node: '>=4'} + read-pkg@3.0.0: dependencies: load-json-file: 4.0.0 normalize-package-data: 2.5.0 path-type: 3.0.0 - dev: true - /read-pkg@5.2.0: - resolution: {integrity: sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==} - engines: {node: '>=8'} + read-pkg@5.2.0: dependencies: '@types/normalize-package-data': 2.4.4 normalize-package-data: 2.5.0 parse-json: 5.2.0 type-fest: 0.6.0 - dev: true - /readable-stream@2.3.8: - resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==} + readable-stream@2.3.8: dependencies: core-util-is: 1.0.3 inherits: 2.0.4 @@ -1435,106 +1930,59 @@ packages: safe-buffer: 5.1.2 string_decoder: 1.1.1 util-deprecate: 1.0.2 - dev: true - /readable-stream@3.6.2: - resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} - engines: {node: '>= 6'} + readable-stream@3.6.2: dependencies: inherits: 2.0.4 string_decoder: 1.3.0 util-deprecate: 1.0.2 - dev: true - /redent@3.0.0: - resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==} - engines: {node: '>=8'} + redent@3.0.0: dependencies: indent-string: 4.0.0 strip-indent: 3.0.0 - dev: true - /require-directory@2.1.1: - resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} - engines: {node: '>=0.10.0'} - dev: true + require-directory@2.1.1: {} - /require-from-string@2.0.2: - resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} - engines: {node: '>=0.10.0'} - dev: true + require-from-string@2.0.2: {} - /requires-port@1.0.0: - resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==} - dev: true + requires-port@1.0.0: {} - /resolve-from@4.0.0: - resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} - engines: {node: '>=4'} - dev: true + resolve-from@4.0.0: {} - /resolve@1.22.8: - resolution: {integrity: sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==} - hasBin: true + resolve@1.22.8: dependencies: is-core-module: 2.13.1 path-parse: 1.0.7 supports-preserve-symlinks-flag: 1.0.0 - dev: true - /rrweb-cssom@0.6.0: - resolution: {integrity: sha512-APM0Gt1KoXBz0iIkkdB/kfvGOwC4UuJFeG/c+yV7wSc7q96cG/kJ0HiYCnzivD9SB53cLV1MlHFNfOuPaadYSw==} - dev: true + rrweb-cssom@0.6.0: {} - /safe-buffer@5.1.2: - resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} - dev: true + safe-buffer@5.1.2: {} - /safe-buffer@5.2.1: - resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} - dev: true + safe-buffer@5.2.1: {} - /safer-buffer@2.1.2: - resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} - dev: true + safer-buffer@2.1.2: {} - /saxes@6.0.0: - resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} - engines: {node: '>=v12.22.7'} + saxes@6.0.0: dependencies: xmlchars: 2.2.0 - dev: true - /semver@5.7.2: - resolution: {integrity: sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==} - hasBin: true - dev: true + semver@5.7.2: {} - /semver@6.3.1: - resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} - hasBin: true - dev: true + semver@6.3.1: {} - /semver@7.6.0: - resolution: {integrity: sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==} - engines: {node: '>=10'} - hasBin: true + semver@7.6.0: dependencies: lru-cache: 6.0.0 - dev: true - /slice-ansi@4.0.0: - resolution: {integrity: sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==} - engines: {node: '>=10'} + slice-ansi@4.0.0: dependencies: ansi-styles: 4.3.0 astral-regex: 2.0.0 is-fullwidth-code-point: 3.0.0 - dev: true - /solhint-community@3.6.0: - resolution: {integrity: sha512-3WGi8nB9VSdC7B3xawktFoQkJEgX6rsUe7jWZJteDBdix+tAOGN+2ZhRr7Cyv1s+h5BRLSsNOXoh7Vg9KEeQbg==} - hasBin: true + solhint-community@3.6.0: dependencies: '@solidity-parser/parser': 0.16.1 ajv: 6.12.6 @@ -1555,328 +2003,178 @@ packages: text-table: 0.2.0 optionalDependencies: prettier: 2.8.8 - dev: true - /source-map-js@1.0.2: - resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==} - engines: {node: '>=0.10.0'} - dev: true + source-map-js@1.0.2: {} - /source-map@0.6.1: - resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} - engines: {node: '>=0.10.0'} - dev: true + source-map@0.6.1: {} - /spdx-correct@3.2.0: - resolution: {integrity: sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==} + spdx-correct@3.2.0: dependencies: spdx-expression-parse: 3.0.1 spdx-license-ids: 3.0.17 - dev: true - /spdx-exceptions@2.5.0: - resolution: {integrity: sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==} - dev: true + spdx-exceptions@2.5.0: {} - /spdx-expression-parse@3.0.1: - resolution: {integrity: sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==} + spdx-expression-parse@3.0.1: dependencies: spdx-exceptions: 2.5.0 spdx-license-ids: 3.0.17 - dev: true - /spdx-license-ids@3.0.17: - resolution: {integrity: sha512-sh8PWc/ftMqAAdFiBu6Fy6JUOYjqDJBJvIhpfDMyHrr0Rbp5liZqd4TjtQ/RgfLjKFZb+LMx5hpml5qOWy0qvg==} - dev: true + spdx-license-ids@3.0.17: {} - /split2@3.2.2: - resolution: {integrity: sha512-9NThjpgZnifTkJpzTZ7Eue85S49QwpNhZTq6GRJwObb6jnLFNGB7Qm73V5HewTROPyxD0C29xqmaI68bQtV+hg==} + split2@3.2.2: dependencies: readable-stream: 3.6.2 - dev: true - /split@1.0.1: - resolution: {integrity: sha512-mTyOoPbrivtXnwnIxZRFYRrPNtEFKlpB2fvjSnCQUiAA6qAZzqwna5envK4uk6OIeP17CsdF3rSBGYVBsU0Tkg==} + split@1.0.1: dependencies: through: 2.3.8 - dev: true - /string-width@4.2.3: - resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} - engines: {node: '>=8'} + string-width@4.2.3: dependencies: emoji-regex: 8.0.0 is-fullwidth-code-point: 3.0.0 strip-ansi: 6.0.1 - dev: true - /string_decoder@1.1.1: - resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} + string_decoder@1.1.1: dependencies: safe-buffer: 5.1.2 - dev: true - /string_decoder@1.3.0: - resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + string_decoder@1.3.0: dependencies: safe-buffer: 5.2.1 - dev: true - /strip-ansi@6.0.1: - resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} - engines: {node: '>=8'} + strip-ansi@6.0.1: dependencies: ansi-regex: 5.0.1 - dev: true - /strip-bom@3.0.0: - resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} - engines: {node: '>=4'} - dev: true + strip-bom@3.0.0: {} - /strip-indent@3.0.0: - resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==} - engines: {node: '>=8'} + strip-indent@3.0.0: dependencies: min-indent: 1.0.1 - dev: true - /supports-color@5.5.0: - resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} - engines: {node: '>=4'} + supports-color@5.5.0: dependencies: has-flag: 3.0.0 - dev: true - /supports-color@7.2.0: - resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} - engines: {node: '>=8'} + supports-color@7.2.0: dependencies: has-flag: 4.0.0 - dev: true - /supports-preserve-symlinks-flag@1.0.0: - resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} - engines: {node: '>= 0.4'} - dev: true + supports-preserve-symlinks-flag@1.0.0: {} - /symbol-tree@3.2.4: - resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} - dev: true + symbol-tree@3.2.4: {} - /table@6.8.1: - resolution: {integrity: sha512-Y4X9zqrCftUhMeH2EptSSERdVKt/nEdijTOacGD/97EKjhQ/Qs8RTlEGABSJNNN8lac9kheH+af7yAkEWlgneA==} - engines: {node: '>=10.0.0'} + table@6.8.1: dependencies: ajv: 8.12.0 lodash.truncate: 4.4.2 slice-ansi: 4.0.0 string-width: 4.2.3 strip-ansi: 6.0.1 - dev: true - /text-extensions@1.9.0: - resolution: {integrity: sha512-wiBrwC1EhBelW12Zy26JeOUkQ5mRu+5o8rpsJk5+2t+Y5vE7e842qtZDQ2g1NpX/29HdyFeJ4nSIhI47ENSxlQ==} - engines: {node: '>=0.10'} - dev: true + text-extensions@1.9.0: {} - /text-table@0.2.0: - resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} - dev: true + text-table@0.2.0: {} - /through2@2.0.5: - resolution: {integrity: sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==} + through2@2.0.5: dependencies: readable-stream: 2.3.8 xtend: 4.0.2 - dev: true - /through2@4.0.2: - resolution: {integrity: sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw==} + through2@4.0.2: dependencies: readable-stream: 3.6.2 - dev: true - /through@2.3.8: - resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==} - dev: true + through@2.3.8: {} - /tough-cookie@4.1.3: - resolution: {integrity: sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw==} - engines: {node: '>=6'} + tough-cookie@4.1.3: dependencies: psl: 1.9.0 punycode: 2.3.0 universalify: 0.2.0 url-parse: 1.5.10 - dev: true - /tr46@5.0.0: - resolution: {integrity: sha512-tk2G5R2KRwBd+ZN0zaEXpmzdKyOYksXwywulIX95MBODjSzMIuQnQ3m8JxgbhnL1LeVo7lqQKsYa1O3Htl7K5g==} - engines: {node: '>=18'} + tr46@5.0.0: dependencies: punycode: 2.3.1 - dev: true - /trim-newlines@3.0.1: - resolution: {integrity: sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw==} - engines: {node: '>=8'} - dev: true + trim-newlines@3.0.1: {} - /type-fest@0.18.1: - resolution: {integrity: sha512-OIAYXk8+ISY+qTOwkHtKqzAuxchoMiD9Udx+FSGQDuiRR+PJKJHc2NJAXlbhkGwTt/4/nKZxELY1w3ReWOL8mw==} - engines: {node: '>=10'} - dev: true + type-fest@0.18.1: {} - /type-fest@0.6.0: - resolution: {integrity: sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==} - engines: {node: '>=8'} - dev: true + type-fest@0.6.0: {} - /type-fest@0.8.1: - resolution: {integrity: sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==} - engines: {node: '>=8'} - dev: true + type-fest@0.8.1: {} - /typedarray@0.0.6: - resolution: {integrity: sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==} - dev: true + typedarray@0.0.6: {} - /uglify-js@3.17.4: - resolution: {integrity: sha512-T9q82TJI9e/C1TAxYvfb16xO120tMVFZrGA3f9/P4424DNu6ypK103y0GPFVa17yotwSyZW5iYXgjYHkGrJW/g==} - engines: {node: '>=0.8.0'} - hasBin: true - requiresBuild: true - dev: true + uglify-js@3.17.4: optional: true - /universalify@0.2.0: - resolution: {integrity: sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==} - engines: {node: '>= 4.0.0'} - dev: true + universalify@0.2.0: {} - /uri-js@4.4.1: - resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + uri-js@4.4.1: dependencies: punycode: 2.3.0 - dev: true - /url-parse@1.5.10: - resolution: {integrity: sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==} + url-parse@1.5.10: dependencies: querystringify: 2.2.0 requires-port: 1.0.0 - dev: true - /util-deprecate@1.0.2: - resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} - dev: true + util-deprecate@1.0.2: {} - /validate-npm-package-license@3.0.4: - resolution: {integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==} + validate-npm-package-license@3.0.4: dependencies: spdx-correct: 3.2.0 spdx-expression-parse: 3.0.1 - dev: true - /w3c-xmlserializer@5.0.0: - resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==} - engines: {node: '>=18'} + w3c-xmlserializer@5.0.0: dependencies: xml-name-validator: 5.0.0 - dev: true - /webidl-conversions@7.0.0: - resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} - engines: {node: '>=12'} - dev: true + webidl-conversions@7.0.0: {} - /whatwg-encoding@3.1.1: - resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==} - engines: {node: '>=18'} + whatwg-encoding@3.1.1: dependencies: iconv-lite: 0.6.3 - dev: true - /whatwg-mimetype@4.0.0: - resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==} - engines: {node: '>=18'} - dev: true + whatwg-mimetype@4.0.0: {} - /whatwg-url@14.0.0: - resolution: {integrity: sha512-1lfMEm2IEr7RIV+f4lUNPOqfFL+pO+Xw3fJSqmjX9AbXcXcYOkCe1P6+9VBZB6n94af16NfZf+sSk0JCBZC9aw==} - engines: {node: '>=18'} + whatwg-url@14.0.0: dependencies: tr46: 5.0.0 webidl-conversions: 7.0.0 - dev: true - /wordwrap@1.0.0: - resolution: {integrity: sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==} - dev: true + wordwrap@1.0.0: {} - /wrap-ansi@7.0.0: - resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} - engines: {node: '>=10'} + wrap-ansi@7.0.0: dependencies: ansi-styles: 4.3.0 string-width: 4.2.3 strip-ansi: 6.0.1 - dev: true - /wrappy@1.0.2: - resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} - dev: true + wrappy@1.0.2: {} - /ws@8.16.0: - resolution: {integrity: sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ==} - engines: {node: '>=10.0.0'} - peerDependencies: - bufferutil: ^4.0.1 - utf-8-validate: '>=5.0.2' - peerDependenciesMeta: - bufferutil: - optional: true - utf-8-validate: - optional: true - dev: true + ws@8.16.0: {} - /xml-name-validator@5.0.0: - resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==} - engines: {node: '>=18'} - dev: true + xml-name-validator@5.0.0: {} - /xmlchars@2.2.0: - resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} - dev: true + xmlchars@2.2.0: {} - /xtend@4.0.2: - resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} - engines: {node: '>=0.4'} - dev: true + xtend@4.0.2: {} - /y18n@5.0.8: - resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} - engines: {node: '>=10'} - dev: true + y18n@5.0.8: {} - /yallist@4.0.0: - resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} - dev: true + yallist@4.0.0: {} - /yargs-parser@20.2.9: - resolution: {integrity: sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==} - engines: {node: '>=10'} - dev: true + yargs-parser@20.2.9: {} - /yargs-parser@21.1.1: - resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} - engines: {node: '>=12'} - dev: true + yargs-parser@21.1.1: {} - /yargs@16.2.0: - resolution: {integrity: sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==} - engines: {node: '>=10'} + yargs@16.2.0: dependencies: cliui: 7.0.4 escalade: 3.1.2 @@ -1885,11 +2183,8 @@ packages: string-width: 4.2.3 y18n: 5.0.8 yargs-parser: 20.2.9 - dev: true - /yargs@17.7.2: - resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} - engines: {node: '>=12'} + yargs@17.7.2: dependencies: cliui: 8.0.1 escalade: 3.1.2 @@ -1898,17 +2193,5 @@ packages: string-width: 4.2.3 y18n: 5.0.8 yargs-parser: 21.1.1 - dev: true - /yocto-queue@0.1.0: - resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} - engines: {node: '>=10'} - dev: true - - '@gitpkg.vercel.app/privacy-scaling-explorations/zk-kit/packages/imt.sol?0699fd1e5ad3683ae0090e0626f75d7834145500': - resolution: {tarball: https://gitpkg.vercel.app/privacy-scaling-explorations/zk-kit/packages/imt.sol?0699fd1e5ad3683ae0090e0626f75d7834145500} - name: imt.sol - version: 0.0.0 - dependencies: - poseidon-solidity: 0.0.5 - dev: false + yocto-queue@0.1.0: {} diff --git a/script/Deploy.s.sol b/script/Deploy.s.sol index 74145c5..affd2de 100644 --- a/script/Deploy.s.sol +++ b/script/Deploy.s.sol @@ -2,18 +2,22 @@ pragma solidity >=0.8.19 <=0.9.0; import { WakuRlnV2 } from "../src/WakuRlnV2.sol"; +import { LinearPriceCalculator } from "../src/LinearPriceCalculator.sol"; import { PoseidonT3 } from "poseidon-solidity/PoseidonT3.sol"; import { LazyIMT } from "@zk-kit/imt.sol/LazyIMT.sol"; import { ERC1967Proxy } from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; import { BaseScript } from "./Base.s.sol"; -import { DeploymentConfig } from "./DeploymentConfig.s.sol"; contract Deploy is BaseScript { function run() public broadcast returns (WakuRlnV2 w, address impl) { + // TODO: Use the correct values when deploying to mainnet + address priceCalcAddr = address(new LinearPriceCalculator(address(0), 0.05 ether)); + // TODO: set DAI address 0x6B175474E89094C44Da98b954EedeAC495271d0F impl = address(new WakuRlnV2()); - bytes memory data = abi.encodeCall(WakuRlnV2.initialize, 100); + bytes memory data = abi.encodeCall(WakuRlnV2.initialize, (priceCalcAddr, 160_000, 20, 600, 30 days, 5 days)); + // (priceCalcAddr, 160000, 20, 600, 30 days, 5 days) address proxy = address(new ERC1967Proxy(impl, data)); w = WakuRlnV2(proxy); } diff --git a/src/IPriceCalculator.sol b/src/IPriceCalculator.sol new file mode 100644 index 0000000..4d67ce6 --- /dev/null +++ b/src/IPriceCalculator.sol @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.24; + +interface IPriceCalculator { + /// Returns the token and price to pay in `token` for some `_rateLimit` + /// @param _rateLimit the rate limit the user wants to acquire + /// @param _numberOfPeriods the number of periods the user wants to acquire + /// @return address of the erc20 token + /// @return uint price to pay for acquiring the specified `_rateLimit` + function calculate(uint256 _rateLimit, uint32 _numberOfPeriods) external view returns (address, uint256); +} diff --git a/src/LinearPriceCalculator.sol b/src/LinearPriceCalculator.sol new file mode 100644 index 0000000..dc3f7ed --- /dev/null +++ b/src/LinearPriceCalculator.sol @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.24; + +import { Ownable } from "openzeppelin-contracts/contracts/access/Ownable.sol"; +import { IPriceCalculator } from "./IPriceCalculator.sol"; + +/// @title Linear Price Calculator to determine the price to acquire a membership +contract LinearPriceCalculator is IPriceCalculator, Ownable { + /// @notice Address of the ERC20 token accepted by this contract. Address(0) represents ETH + address public token; + + /// @notice The price per message per epoch per period + uint256 public pricePerMessagePerPeriod; + + constructor(address _token, uint256 _pricePerPeriod) Ownable() { + token = _token; + pricePerMessagePerPeriod = _pricePerPeriod; + } + + /// Set accepted token and price per message per epoch per period + /// @param _token The token accepted by the membership management for RLN + /// @param _pricePerPeriod Price per message per epoch + function setTokenAndPrice(address _token, uint256 _pricePerPeriod) external onlyOwner { + token = _token; + pricePerMessagePerPeriod = _pricePerPeriod; + } + + /// Returns the token and price to pay in `token` for some `_rateLimit` + /// @param _rateLimit the rate limit the user wants to acquire + /// @param _numberOfPeriods the number of periods the user wants to acquire + /// @return address of the erc20 token + /// @return uint price to pay for acquiring the specified `_rateLimit` + function calculate(uint256 _rateLimit, uint32 _numberOfPeriods) external view returns (address, uint256) { + return (token, uint256(_numberOfPeriods) * uint256(_rateLimit) * pricePerMessagePerPeriod); + } +} diff --git a/src/Membership.sol b/src/Membership.sol new file mode 100644 index 0000000..52d316c --- /dev/null +++ b/src/Membership.sol @@ -0,0 +1,427 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.24; + +import { IPriceCalculator } from "./IPriceCalculator.sol"; +import "openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; +import "openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol"; +import "openzeppelin-contracts/contracts/utils/Context.sol"; + +// The number of periods should be greater than zero +error NumberOfPeriodsCantBeZero(); + +// ETH Amount passed as value on the transaction is not correct +error IncorrectAmount(); + +// An eth value was assigned in the transaction and only tokens were expected +error OnlyTokensAccepted(); + +// The specified rate limit was not correct or within the expected limits +error InvalidRateLimit(); + +// It's not possible to acquire the rate limit due to exceeding the expected limits +// even after attempting to erase expired memberships +error ExceedMaxRateLimitPerEpoch(); + +// This membership is not in grace period yet +error NotInGracePeriod(uint256 idCommitment); + +// The sender is not the holder of the membership +error NotHolder(uint256 idCommitment); + +// This membership cannot be erased (either it is not expired or not in grace period and/or not the owner) +error CantEraseMembership(uint256 idCommitment); + +contract Membership { + using SafeERC20 for IERC20; + + /// @notice Address of the Price Calculator used to calculate the price of a new membership + IPriceCalculator public priceCalculator; + + /// @notice Maximum total rate limit of all memberships in the tree + uint32 public maxTotalRateLimitPerEpoch; + + /// @notice Maximum rate limit of one membership + uint32 public maxRateLimitPerMembership; + + /// @notice Minimum rate limit of one membership + uint32 public minRateLimitPerMembership; + + /// @notice Membership billing period + uint32 public billingPeriod; + + /// @notice Membership grace period + uint32 public gracePeriod; + + /// @notice balances available to withdraw + mapping(address => mapping(address => uint256)) public balancesToWithdraw; // holder -> token -> balance + + /// @notice Total rate limit of all memberships in the tree + uint256 public totalRateLimitPerEpoch; + + /// @notice List of registered memberships (IDCommitment to membership metadata) + mapping(uint256 => MembershipInfo) public members; + + /// @notice The index of the next member to be registered + uint32 public commitmentIndex; + + /// @notice track available indices that are available due to expired memberships being removed + uint32[] public availableExpiredIndices; + + /// @dev Oldest membership + uint256 public head = 0; + + /// @dev Newest membership + uint256 public tail = 0; + + struct MembershipInfo { + /// @notice idCommitment of the previous membership + uint256 prev; + /// @notice idCommitment of the next membership + uint256 next; + /// @notice amount of the token used to acquire this membership + uint256 amount; + /// @notice numPeriods + uint32 numberOfPeriods; + /// @notice timestamp of when the grace period starts for this membership + uint256 gracePeriodStartDate; + /// @notice duration of the grace period + uint32 gracePeriod; + /// @notice the user message limit of each member + uint32 userMessageLimit; + /// @notice the index of the member in the set + uint32 index; + /// @notice address of the owner of this membership + address holder; + /// @notice token used to acquire this membership + address token; + } + + /// @notice Emitted when a membership is erased due to having exceeded the grace period or the owner having chosen + /// to not extend it + /// @param idCommitment the idCommitment of the member + /// @param userMessageLimit the rate limit of this membership + /// @param index the index of the membership in the merkle tree + event MemberExpired(uint256 idCommitment, uint32 userMessageLimit, uint32 index); + + /// @notice Emitted when a membership in grace period is extended + /// @param idCommitment the idCommitment of the member + /// @param userMessageLimit the rate limit of this membership + /// @param index the index of the membership in the merkle tree + /// @param newExpirationDate the new expiration date of this membership + event MemberExtended(uint256 idCommitment, uint32 userMessageLimit, uint32 index, uint256 newExpirationDate); + + /// @dev contract initializer + /// @param _priceCalculator Address of an instance of IPriceCalculator + /// @param _maxTotalRateLimitPerEpoch Maximum total rate limit of all memberships in the tree + /// @param _minRateLimitPerMembership Minimum rate limit of one membership + /// @param _maxRateLimitPerMembership Maximum rate limit of one membership + /// @param _expirationTerm Membership expiration term + /// @param _gracePeriod Membership grace period + function __Membership_init( + address _priceCalculator, + uint32 _maxTotalRateLimitPerEpoch, + uint32 _minRateLimitPerMembership, + uint32 _maxRateLimitPerMembership, + uint32 _expirationTerm, + uint32 _gracePeriod + ) + internal + { + priceCalculator = IPriceCalculator(_priceCalculator); + maxTotalRateLimitPerEpoch = _maxTotalRateLimitPerEpoch; + maxRateLimitPerMembership = _maxRateLimitPerMembership; + minRateLimitPerMembership = _minRateLimitPerMembership; + billingPeriod = _expirationTerm; + gracePeriod = _gracePeriod; + } + + /// @notice Checks if a user message limit is valid. This does not take into account whether we the total + /// memberships have reached already the `maxTotalRateLimitPerEpoch` + /// @param userMessageLimit The user message limit + /// @return true if the user message limit is valid, false otherwise + function isValidUserMessageLimit(uint32 userMessageLimit) external view returns (bool) { + return userMessageLimit >= minRateLimitPerMembership && userMessageLimit <= maxRateLimitPerMembership; + } + + /// @dev acquire a membership and trasnfer the fees to the contract + /// @param _sender address of the owner of the new membership + /// @param _idCommitment the idcommitment of the new membership + /// @param _rateLimit the user message limit + /// @param _numberOfPeriods the number of periods the user wants to acquire + /// @return index the index in the merkle tree + /// @return reusedIndex indicates whether a new leaf is being used or if using an existing leaf in the merkle tree + function _acquireMembership( + address _sender, + uint256 _idCommitment, + uint32 _rateLimit, + uint32 _numberOfPeriods + ) + internal + returns (uint32 index, bool reusedIndex) + { + if (_numberOfPeriods == 0) revert NumberOfPeriodsCantBeZero(); + + (address token, uint256 amount) = priceCalculator.calculate(_rateLimit, _numberOfPeriods); + (index, reusedIndex) = + _setupMembershipDetails(_sender, _idCommitment, _rateLimit, _numberOfPeriods, token, amount); + _transferFees(_sender, token, amount); + } + + function _transferFees(address _from, address _token, uint256 _amount) internal { + if (_token == address(0)) { + if (msg.value != _amount) revert IncorrectAmount(); + } else { + if (msg.value != 0) revert OnlyTokensAccepted(); + IERC20(_token).safeTransferFrom(_from, address(this), _amount); + } + } + + /// @dev Setup a new membership. If there are not enough remaining rate limit to acquire + /// a new membership, it will attempt to erase existing memberships and reuse one of the + /// slots helds by the membership + /// @param _sender holder of the membership. Generally `msg.sender` + /// @param _idCommitment IDCommitment + /// @param _rateLimit User message limit + /// @param _numberOfPeriods the number of periods the user wants to acquire + /// @param _token Address of the token used to acquire the membership + /// @param _amount Amount of the token used to acquire the membership + /// @return index membership index on the merkle tree + /// @return reusedIndex indicates whether the index returned was a reused slot on the tree or not + function _setupMembershipDetails( + address _sender, + uint256 _idCommitment, + uint32 _rateLimit, + uint32 _numberOfPeriods, + address _token, + uint256 _amount + ) + internal + returns (uint32 index, bool reusedIndex) + { + if (_rateLimit < minRateLimitPerMembership || _rateLimit > maxRateLimitPerMembership) { + revert InvalidRateLimit(); + } + + // Attempt to free expired membership slots + while (totalRateLimitPerEpoch + _rateLimit > maxTotalRateLimitPerEpoch) { + // Determine if there are any available spot in the membership map + // by looking at the oldest membership. If it's expired, we can free it + MembershipInfo memory oldestMembership = members[head]; + + if ( + oldestMembership.holder != address(0) // membership has a holder + && isExpired(oldestMembership.gracePeriodStartDate) + ) { + emit MemberExpired(head, oldestMembership.userMessageLimit, oldestMembership.index); + + // Deduct the expired membership rate limit + totalRateLimitPerEpoch -= oldestMembership.userMessageLimit; + + // Promote the next oldest membership to oldest + uint256 nextOldest = oldestMembership.next; + head = nextOldest; + if (nextOldest != 0) { + members[nextOldest].prev = 0; + } + + // Move balance from expired membership to holder balance + balancesToWithdraw[oldestMembership.holder][oldestMembership.token] += oldestMembership.amount; + + availableExpiredIndices.push(oldestMembership.index); + + delete members[head]; + } else { + revert ExceedMaxRateLimitPerEpoch(); + } + } + + uint256 prev = 0; + if (tail != 0) { + MembershipInfo storage latestMembership = members[tail]; + latestMembership.next = _idCommitment; + prev = tail; + } else { + // First item + // TODO: test adding memberships after the list has been emptied + head = _idCommitment; + } + + totalRateLimitPerEpoch += _rateLimit; + + // Reuse available slots from previously removed expired memberships + uint256 arrLen = availableExpiredIndices.length; + if (arrLen != 0) { + index = availableExpiredIndices[arrLen - 1]; + availableExpiredIndices.pop(); + reusedIndex = true; + } else { + index = commitmentIndex; + } + + members[_idCommitment] = MembershipInfo({ + holder: _sender, + gracePeriodStartDate: block.timestamp + (uint256(billingPeriod) * uint256(_numberOfPeriods)), + gracePeriod: gracePeriod, + numberOfPeriods: _numberOfPeriods, + token: _token, + amount: _amount, + userMessageLimit: _rateLimit, + next: 0, // It's the last value, so point to nowhere + prev: prev, + index: index + }); + + tail = _idCommitment; + } + + /// @dev Extend a membership expiration date. Membership must be on grace period + /// @param _sender the address of the holder of the membership + /// @param _idCommitment the idCommitment of the membership + function _extendMembership(address _sender, uint256 _idCommitment) public { + MembershipInfo storage mdetails = members[_idCommitment]; + + if (!_isGracePeriod(mdetails.gracePeriodStartDate, mdetails.gracePeriod, mdetails.numberOfPeriods)) { + // TODO: can a membership that has exceeded the expired period be extended? + revert NotInGracePeriod(_idCommitment); + } + + if (_sender != mdetails.holder) revert NotHolder(_idCommitment); + + uint256 newExpirationDate = block.timestamp + (uint256(billingPeriod) * uint256(mdetails.numberOfPeriods)); + + uint256 mdetailsNext = mdetails.next; + uint256 mdetailsPrev = mdetails.prev; + + // Remove current membership references + if (mdetailsPrev != 0) { + members[mdetailsPrev].next = mdetailsNext; + } else { + head = mdetailsNext; + } + + if (mdetailsNext != 0) { + members[mdetailsNext].prev = mdetailsPrev; + } else { + tail = mdetailsPrev; + } + + // Move membership to the end (since it will be the newest) + mdetails.next = 0; + mdetails.prev = tail; + mdetails.gracePeriodStartDate = newExpirationDate; + mdetails.gracePeriod = gracePeriod; + + members[tail].next = _idCommitment; + tail = _idCommitment; + + emit MemberExtended(_idCommitment, mdetails.userMessageLimit, mdetails.index, newExpirationDate); + } + + /// @dev Determine whether a timestamp is considered to be expired or not after exceeding the grace period + /// @param _gracePeriodStartDate timestamp in which the grace period starts + /// @param _gracePeriod duration of the grace period + /// @param _numberOfPeriods the number of periods the user wants to acquire + function _isExpired( + uint256 _gracePeriodStartDate, + uint32 _gracePeriod, + uint32 _numberOfPeriods + ) + internal + view + returns (bool) + { + return block.timestamp > _gracePeriodStartDate + (uint256(_gracePeriod) * uint256(_numberOfPeriods)); + } + + /// @notice Determine if a membership is expired (has exceeded the grace period) + /// @param _idCommitment the idCommitment of the membership + function isExpired(uint256 _idCommitment) public view returns (bool) { + MembershipInfo memory m = members[_idCommitment]; + return _isExpired(m.gracePeriodStartDate, m.gracePeriod, m.numberOfPeriods); + } + + function expirationDate(uint256 _idCommitment) public view returns (uint256) { + MembershipInfo memory m = members[_idCommitment]; + return m.gracePeriodStartDate + (uint256(m.gracePeriod) * uint256(m.numberOfPeriods)); + } + + /// @dev Determine whether a timestamp is considered to be in grace period or not + /// @param _gracePeriodStartDate timestamp in which the grace period starts + /// @param _gracePeriod duration of the grace period + /// @param _numberOfPeriods the number of periods the user wants to acquire + function _isGracePeriod( + uint256 _gracePeriodStartDate, + uint32 _gracePeriod, + uint32 _numberOfPeriods + ) + internal + view + returns (bool) + { + uint256 blockTimestamp = block.timestamp; + return blockTimestamp >= _gracePeriodStartDate + && blockTimestamp <= _gracePeriodStartDate + (uint256(_gracePeriod) * uint256(_numberOfPeriods)); + } + + /// @notice Determine if a membership is in grace period + /// @param _idCommitment the idCommitment of the membership + function isGracePeriod(uint256 _idCommitment) public view returns (bool) { + MembershipInfo memory m = members[_idCommitment]; + return _isGracePeriod(m.gracePeriodStartDate, m.gracePeriod, m.numberOfPeriods); + } + + /// @dev Remove expired memberships or owned memberships in grace period. + /// @param _sender address of the sender of transaction (will be used to check memberships in grace period) + /// @param _idCommitment IDCommitment of the membership to erase + function _eraseMembership(address _sender, uint256 _idCommitment, MembershipInfo memory _mdetails) internal { + bool membershipExpired = + _isExpired(_mdetails.gracePeriodStartDate, _mdetails.gracePeriod, _mdetails.numberOfPeriods); + bool isGracePeriodAndOwned = _isGracePeriod( + _mdetails.gracePeriodStartDate, _mdetails.gracePeriod, _mdetails.numberOfPeriods + ) && _mdetails.holder == _sender; + + if (!membershipExpired && !isGracePeriodAndOwned) revert CantEraseMembership(_idCommitment); + + emit MemberExpired(head, _mdetails.userMessageLimit, _mdetails.index); + + // Move balance from expired membership to holder balance + balancesToWithdraw[_mdetails.holder][_mdetails.token] += _mdetails.amount; + + // Deduct the expired membership rate limit + totalRateLimitPerEpoch -= _mdetails.userMessageLimit; + + // Remove current membership references + if (_mdetails.prev != 0) { + members[_mdetails.prev].next = _mdetails.next; + } else { + head = _mdetails.next; + } + + if (_mdetails.next != 0) { + members[_mdetails.next].prev = _mdetails.prev; + } else { + tail = _mdetails.prev; + } + + availableExpiredIndices.push(_mdetails.index); + + delete members[_idCommitment]; + } + + /// @dev Withdraw any available balance in tokens after a membership is erased. + /// @param _sender the address of the owner of the tokens + /// @param _token the address of the token to withdraw. Use 0x000...000 to withdraw ETH + function _withdraw(address _sender, address _token) internal { + uint256 amount = balancesToWithdraw[_sender][_token]; + require(amount > 0, "Insufficient balance"); + + balancesToWithdraw[_sender][_token] = 0; + if (_token == address(0)) { + // ETH + (bool success,) = _sender.call{ value: amount }(""); + require(success, "eth transfer failed"); + } else { + IERC20(_token).safeTransfer(_sender, amount); + } + } +} diff --git a/src/WakuRlnV2.sol b/src/WakuRlnV2.sol index dcc567b..19c8683 100644 --- a/src/WakuRlnV2.sol +++ b/src/WakuRlnV2.sol @@ -8,6 +8,9 @@ import { OwnableUpgradeable } from "@openzeppelin/contracts-upgradeable/access/O import { Initializable } from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; +import { Membership } from "./Membership.sol"; +import { IPriceCalculator } from "./IPriceCalculator.sol"; + /// The tree is full error FullTree(); @@ -23,34 +26,17 @@ error InvalidUserMessageLimit(uint32 messageLimit); /// Invalid pagination query error InvalidPaginationQuery(uint256 startIndex, uint256 endIndex); -contract WakuRlnV2 is Initializable, OwnableUpgradeable, UUPSUpgradeable { +contract WakuRlnV2 is Initializable, OwnableUpgradeable, UUPSUpgradeable, Membership { /// @notice The Field uint256 public constant Q = 21_888_242_871_839_275_222_246_405_745_257_275_088_548_364_400_416_034_343_698_204_186_575_808_495_617; - /// @notice The max message limit per epoch - uint32 public MAX_MESSAGE_LIMIT; - /// @notice The depth of the merkle tree uint8 public constant DEPTH = 20; /// @notice The size of the merkle tree, i.e 2^depth uint32 public SET_SIZE; - /// @notice The index of the next member to be registered - uint32 public commitmentIndex; - - /// @notice the membership metadata of the member - struct MembershipInfo { - /// @notice the user message limit of each member - uint32 userMessageLimit; - /// @notice the index of the member in the set - uint32 index; - } - - /// @notice the member metadata - mapping(uint256 => MembershipInfo) public memberInfo; - /// @notice the deployed block number uint32 public deployedBlockNumber; @@ -69,21 +55,39 @@ contract WakuRlnV2 is Initializable, OwnableUpgradeable, UUPSUpgradeable { _; } - /// @notice the modifier to check if the userMessageLimit is valid - /// @param userMessageLimit The user message limit - modifier onlyValidUserMessageLimit(uint32 userMessageLimit) { - if (!isValidUserMessageLimit(userMessageLimit)) revert InvalidUserMessageLimit(userMessageLimit); - _; - } - constructor() { _disableInitializers(); } - function initialize(uint32 maxMessageLimit) public initializer { + /// @dev contract initializer + /// @param _priceCalculator Address of an instance of IPriceCalculator + /// @param _maxTotalRateLimitPerEpoch Maximum total rate limit of all memberships in the tree + /// @param _minRateLimitPerMembership Minimum rate limit of one membership + /// @param _maxRateLimitPerMembership Maximum rate limit of one membership + /// @param _billingPeriod Membership billing period + /// @param _gracePeriod Membership grace period + function initialize( + address _priceCalculator, + uint32 _maxTotalRateLimitPerEpoch, + uint16 _minRateLimitPerMembership, + uint16 _maxRateLimitPerMembership, + uint32 _billingPeriod, + uint32 _gracePeriod + ) + public + initializer + { __Ownable_init(); __UUPSUpgradeable_init(); - MAX_MESSAGE_LIMIT = maxMessageLimit; + __Membership_init( + _priceCalculator, + _maxTotalRateLimitPerEpoch, + _minRateLimitPerMembership, + _maxRateLimitPerMembership, + _billingPeriod, + _gracePeriod + ); + SET_SIZE = uint32(1 << DEPTH); deployedBlockNumber = uint32(block.number); LazyIMT.init(imtData, DEPTH); @@ -99,13 +103,6 @@ contract WakuRlnV2 is Initializable, OwnableUpgradeable, UUPSUpgradeable { return idCommitment != 0 && idCommitment < Q; } - /// @notice Checks if a user message limit is valid - /// @param userMessageLimit The user message limit - /// @return true if the user message limit is valid, false otherwise - function isValidUserMessageLimit(uint32 userMessageLimit) public view returns (bool) { - return userMessageLimit > 0 && userMessageLimit <= MAX_MESSAGE_LIMIT; - } - /// @notice Returns the rateCommitment of a member /// @param index The index of the member /// @return The rateCommitment of the member @@ -117,9 +114,9 @@ contract WakuRlnV2 is Initializable, OwnableUpgradeable, UUPSUpgradeable { /// @param idCommitment The idCommitment of the member /// @return The metadata of the member (userMessageLimit, index, rateCommitment) function idCommitmentToMetadata(uint256 idCommitment) public view returns (uint32, uint32, uint256) { - MembershipInfo memory member = memberInfo[idCommitment]; - // we cannot call indexToCommitment for 0 index if the member doesn't exist - if (member.userMessageLimit == 0) { + MembershipInfo memory member = members[idCommitment]; + // we cannot call indexToCommitment if the member doesn't exist + if (member.holder == address(0)) { return (0, 0, 0); } return (member.userMessageLimit, member.index, indexToCommitment(member.index)); @@ -133,34 +130,46 @@ contract WakuRlnV2 is Initializable, OwnableUpgradeable, UUPSUpgradeable { return rateCommitment != 0; } - /// Allows a user to register as a member + /// @notice Allows a user to register as a member /// @param idCommitment The idCommitment of the member /// @param userMessageLimit The message limit of the member + /// @param numberOfPeriods The number of periods to acquire function register( uint256 idCommitment, - uint32 userMessageLimit + uint32 userMessageLimit, + uint32 numberOfPeriods // TODO: is there a maximum number of periods allowed? ) external + payable onlyValidIdCommitment(idCommitment) - onlyValidUserMessageLimit(userMessageLimit) { - _register(idCommitment, userMessageLimit); + if (memberExists(idCommitment)) revert DuplicateIdCommitment(); + + uint32 index; + bool reusedIndex; + (index, reusedIndex) = _acquireMembership(_msgSender(), idCommitment, userMessageLimit, numberOfPeriods); + + _register(idCommitment, userMessageLimit, index, reusedIndex); } - /// Registers a member + /// @dev Registers a member /// @param idCommitment The idCommitment of the member /// @param userMessageLimit The message limit of the member - function _register(uint256 idCommitment, uint32 userMessageLimit) internal { - if (memberExists(idCommitment)) revert DuplicateIdCommitment(); + /// @param index Indicates the index in the merkle tree + /// @param reusedIndex indicates whether we're inserting a new element in the merkle tree or updating a existing + /// leaf + function _register(uint256 idCommitment, uint32 userMessageLimit, uint32 index, bool reusedIndex) internal { if (commitmentIndex >= SET_SIZE) revert FullTree(); uint256 rateCommitment = PoseidonT3.hash([idCommitment, userMessageLimit]); - MembershipInfo memory member = MembershipInfo({ userMessageLimit: userMessageLimit, index: commitmentIndex }); - LazyIMT.insert(imtData, rateCommitment); - memberInfo[idCommitment] = member; + if (reusedIndex) { + LazyIMT.update(imtData, rateCommitment, index); + } else { + LazyIMT.insert(imtData, rateCommitment); + commitmentIndex += 1; + } - emit MemberRegistered(rateCommitment, commitmentIndex); - commitmentIndex += 1; + emit MemberRegistered(rateCommitment, index); } /// @notice Returns the commitments of a range of members @@ -195,4 +204,70 @@ contract WakuRlnV2 is Initializable, OwnableUpgradeable, UUPSUpgradeable { } return castedProof; } + + /// @notice Extend a membership expiration date. Memberships must be on grace period + /// @param idCommitments list of idcommitments + function extend(uint256[] calldata idCommitments) external { + for (uint256 i = 0; i < idCommitments.length; i++) { + uint256 idCommitment = idCommitments[i]; + _extendMembership(_msgSender(), idCommitment); + } + } + + /// @notice Remove expired memberships or owned memberships in grace period. + /// The user can determine offchain which expired memberships slots + /// are available, and proceed to free them. + /// This is also used to erase memberships in grace period if they're + /// held by the sender. The sender can then withdraw the tokens. + /// @param idCommitments list of idcommitments of the memberships + function eraseMemberships(uint256[] calldata idCommitments) external { + for (uint256 i = 0; i < idCommitments.length; i++) { + uint256 idCommitment = idCommitments[i]; + MembershipInfo memory mdetails = members[idCommitment]; + _eraseMembership(_msgSender(), idCommitment, mdetails); + LazyIMT.update(imtData, 0, mdetails.index); + } + } + + /// @notice Withdraw any available balance in tokens after a membership is erased. + /// @param token The address of the token to withdraw. Use 0x000...000 to withdraw ETH + function withdraw(address token) external { + _withdraw(_msgSender(), token); + } + + /// @notice Set the address of the price calculator + /// @param _priceCalculator new price calculator address + function setPriceCalculator(address _priceCalculator) external onlyOwner { + priceCalculator = IPriceCalculator(_priceCalculator); + } + + /// @notice Set the maximum total rate limit of all memberships in the tree + /// @param _maxTotalRateLimitPerEpoch new value + function setMaxTotalRateLimitPerEpoch(uint32 _maxTotalRateLimitPerEpoch) external onlyOwner { + maxTotalRateLimitPerEpoch = _maxTotalRateLimitPerEpoch; + } + + /// @notice Set the maximum rate limit of one membership + /// @param _maxRateLimitPerMembership new value + function setMaxRateLimitPerMembership(uint32 _maxRateLimitPerMembership) external onlyOwner { + maxRateLimitPerMembership = _maxRateLimitPerMembership; + } + + /// @notice Set the minimum rate limit of one membership + /// @param _minRateLimitPerMembership new value + function setMinRateLimitPerMembership(uint32 _minRateLimitPerMembership) external onlyOwner { + minRateLimitPerMembership = _minRateLimitPerMembership; + } + + /// @notice Set the membership billing period + /// @param _billingPeriod new value + function setBillingPeriod(uint32 _billingPeriod) external onlyOwner { + billingPeriod = _billingPeriod; + } + + /// @notice Set the membership grace period + /// @param _gracePeriod new value + function setGracePeriod(uint32 _gracePeriod) external onlyOwner { + gracePeriod = _gracePeriod; + } } diff --git a/test/TestToken.sol b/test/TestToken.sol new file mode 100644 index 0000000..9c84cee --- /dev/null +++ b/test/TestToken.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.19 <0.9.0; + +import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; + +contract TestToken is ERC20 { + constructor() ERC20("TestToken", "TTT") { } + + function mint(address to, uint256 amount) public { + _mint(to, amount); + } +} diff --git a/test/WakuRlnV2.t.sol b/test/WakuRlnV2.t.sol index 56147fb..245826a 100644 --- a/test/WakuRlnV2.t.sol +++ b/test/WakuRlnV2.t.sol @@ -5,33 +5,47 @@ import { Test } from "forge-std/Test.sol"; import { Deploy } from "../script/Deploy.s.sol"; import { DeploymentConfig } from "../script/DeploymentConfig.s.sol"; import "../src/WakuRlnV2.sol"; // solhint-disable-line +import "../src/Membership.sol"; // solhint-disable-line +import "../src/LinearPriceCalculator.sol"; // solhint-disable-line +import "./TestToken.sol"; import { PoseidonT3 } from "poseidon-solidity/PoseidonT3.sol"; import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; import { ERC1967Proxy } from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; +import "forge-std/console.sol"; contract WakuRlnV2Test is Test { WakuRlnV2 internal w; address internal impl; DeploymentConfig internal deploymentConfig; + TestToken internal token; address internal deployer; function setUp() public virtual { Deploy deployment = new Deploy(); (w, impl) = deployment.run(); + + token = new TestToken(); } function test__ValidRegistration__kats() external { vm.pauseGasMetering(); + // Merkle tree leaves are calculated using 2 as rateLimit + vm.prank(w.owner()); + w.setMinRateLimitPerMembership(2); + uint256 idCommitment = 2; uint32 userMessageLimit = 2; + uint32 numberOfPeriods = 1; + (, uint256 price) = w.priceCalculator().calculate(userMessageLimit, numberOfPeriods); vm.resumeGasMetering(); - w.register(idCommitment, userMessageLimit); + w.register{ value: price }(idCommitment, userMessageLimit, numberOfPeriods); vm.pauseGasMetering(); assertEq(w.commitmentIndex(), 1); assertEq(w.memberExists(idCommitment), true); - (uint32 fetchedUserMessageLimit, uint32 index) = w.memberInfo(idCommitment); + (,,,,,, uint32 fetchedUserMessageLimit, uint32 index, address holder,) = w.members(idCommitment); assertEq(fetchedUserMessageLimit, userMessageLimit); + assertEq(holder, address(this)); assertEq(index, 0); // kats from zerokit uint256 rateCommitment = @@ -74,11 +88,19 @@ contract WakuRlnV2Test is Test { vm.resumeGasMetering(); } - function test__ValidRegistration(uint256 idCommitment, uint32 userMessageLimit) external { - vm.assume(w.isValidCommitment(idCommitment) && w.isValidUserMessageLimit(userMessageLimit)); + function test__ValidRegistration(uint32 userMessageLimit, uint32 numberOfPeriods) external { + vm.pauseGasMetering(); + uint256 idCommitment = 2; + vm.assume(numberOfPeriods > 0 && numberOfPeriods < 100); + (, uint256 price) = w.priceCalculator().calculate(userMessageLimit, numberOfPeriods); + uint256 minUserMessageLimit = w.minRateLimitPerMembership(); + uint256 maxUserMessageLimit = w.maxRateLimitPerMembership(); + vm.assume(userMessageLimit >= minUserMessageLimit && userMessageLimit <= maxUserMessageLimit); + vm.assume(w.isValidUserMessageLimit(userMessageLimit)); + vm.resumeGasMetering(); assertEq(w.memberExists(idCommitment), false); - w.register(idCommitment, userMessageLimit); + w.register{ value: price }(idCommitment, userMessageLimit, numberOfPeriods); uint256 rateCommitment = PoseidonT3.hash([idCommitment, userMessageLimit]); (uint32 fetchedUserMessageLimit, uint32 index, uint256 fetchedRateCommitment) = @@ -86,6 +108,95 @@ contract WakuRlnV2Test is Test { assertEq(fetchedUserMessageLimit, userMessageLimit); assertEq(index, 0); assertEq(fetchedRateCommitment, rateCommitment); + + assertEq(address(w).balance, price); + assertEq(w.totalRateLimitPerEpoch(), userMessageLimit); + } + + function test__InsertionNormalOrder(uint32 idCommitmentsLength) external { + vm.assume(idCommitmentsLength > 0 && idCommitmentsLength <= 50); + + uint32 userMessageLimit = w.minRateLimitPerMembership(); + uint32 numberOfPeriods = 1; + (, uint256 price) = w.priceCalculator().calculate(userMessageLimit, 1); + + // Register some commitments + for (uint256 i = 0; i < idCommitmentsLength; i++) { + uint256 idCommitment = i + 1; + w.register{ value: price }(idCommitment, userMessageLimit, numberOfPeriods); + (uint256 prev, uint256 next,,,,,,,,) = w.members(idCommitment); + // new membership will always be the tail + assertEq(next, 0); + assertEq(w.tail(), idCommitment); + // current membership prevLink will always point to previous membership + assertEq(prev, idCommitment - 1); + } + assertEq(w.head(), 1); + assertEq(w.tail(), idCommitmentsLength); + + // Ensure that prev and next are chained correctly + for (uint256 i = 0; i < idCommitmentsLength; i++) { + uint256 idCommitment = i + 1; + (uint256 prev, uint256 next,,,,,,,,) = w.members(idCommitment); + + assertEq(prev, idCommitment - 1); + if (i == idCommitmentsLength - 1) { + assertEq(next, 0); + } else { + assertEq(next, idCommitment + 1); + } + } + } + + function test__LinearPriceCalculation(uint32 userMessageLimit, uint32 numberOfPeriods) external view { + IPriceCalculator priceCalculator = w.priceCalculator(); + uint256 pricePerMessagePerPeriod = LinearPriceCalculator(address(priceCalculator)).pricePerMessagePerPeriod(); + assertNotEq(pricePerMessagePerPeriod, 0); + uint256 expectedPrice = uint256(userMessageLimit) * uint256(numberOfPeriods) * pricePerMessagePerPeriod; + (, uint256 price) = w.priceCalculator().calculate(userMessageLimit, numberOfPeriods); + assertEq(price, expectedPrice); + } + + function test__RegistrationWithTokens( + uint256 idCommitment, + uint32 userMessageLimit, + uint32 numberOfPeriods + ) + external + { + vm.pauseGasMetering(); + vm.assume(numberOfPeriods > 0); + LinearPriceCalculator priceCalculator = LinearPriceCalculator(address(w.priceCalculator())); + vm.prank(priceCalculator.owner()); + priceCalculator.setTokenAndPrice(address(token), 5 wei); + (, uint256 price) = w.priceCalculator().calculate(userMessageLimit, numberOfPeriods); + uint256 minUserMessageLimit = w.minRateLimitPerMembership(); + uint256 maxUserMessageLimit = w.maxRateLimitPerMembership(); + vm.assume(userMessageLimit >= minUserMessageLimit && userMessageLimit <= maxUserMessageLimit); + vm.assume(w.isValidCommitment(idCommitment) && w.isValidUserMessageLimit(userMessageLimit)); + vm.resumeGasMetering(); + + token.mint(address(this), price); + token.approve(address(w), price); + w.register(idCommitment, userMessageLimit, numberOfPeriods); + assertEq(token.balanceOf(address(w)), price); + assertEq(token.balanceOf(address(this)), 0); + } + + function test__InvalidETHAmount(uint256 idCommitment, uint32 userMessageLimit, uint32 numberOfPeriods) external { + vm.pauseGasMetering(); + vm.assume(numberOfPeriods > 0 && numberOfPeriods < 100); + uint256 minUserMessageLimit = w.minRateLimitPerMembership(); + uint256 maxUserMessageLimit = w.maxRateLimitPerMembership(); + vm.assume(userMessageLimit >= minUserMessageLimit && userMessageLimit <= maxUserMessageLimit); + vm.assume(w.isValidCommitment(idCommitment) && w.isValidUserMessageLimit(userMessageLimit)); + (, uint256 price) = w.priceCalculator().calculate(userMessageLimit, numberOfPeriods); + vm.resumeGasMetering(); + + vm.expectRevert(abi.encodeWithSelector(IncorrectAmount.selector)); + w.register{ value: price - 1 }(idCommitment, userMessageLimit, numberOfPeriods); + vm.expectRevert(abi.encodeWithSelector(IncorrectAmount.selector)); + w.register{ value: price + 1 }(idCommitment, userMessageLimit, numberOfPeriods); } function test__IdCommitmentToMetadata__DoesntExist() external view { @@ -97,43 +208,398 @@ contract WakuRlnV2Test is Test { } function test__InvalidRegistration__InvalidIdCommitment__Zero() external { + vm.pauseGasMetering(); uint256 idCommitment = 0; uint32 userMessageLimit = 2; + uint32 numberOfPeriods = 2; + (, uint256 price) = w.priceCalculator().calculate(userMessageLimit, numberOfPeriods); + vm.resumeGasMetering(); + vm.expectRevert(abi.encodeWithSelector(InvalidIdCommitment.selector, 0)); - w.register(idCommitment, userMessageLimit); + w.register{ value: price }(idCommitment, userMessageLimit, numberOfPeriods); } function test__InvalidRegistration__InvalidIdCommitment__LargerThanField() external { + vm.pauseGasMetering(); + uint32 userMessageLimit = 20; + uint32 numberOfPeriods = 3; + (, uint256 price) = w.priceCalculator().calculate(userMessageLimit, numberOfPeriods); + vm.resumeGasMetering(); + uint256 idCommitment = w.Q() + 1; - uint32 userMessageLimit = 2; vm.expectRevert(abi.encodeWithSelector(InvalidIdCommitment.selector, idCommitment)); - w.register(idCommitment, userMessageLimit); + w.register{ value: price }(idCommitment, userMessageLimit, numberOfPeriods); + } + + function test__InvalidRegistration__InvalidUserMessageLimit__MinMax() external { + uint256 idCommitment = 2; + + uint32 invalidMin = w.minRateLimitPerMembership() - 1; + uint32 invalidMax = w.maxRateLimitPerMembership() + 1; + + vm.expectRevert(abi.encodeWithSelector(InvalidRateLimit.selector)); + w.register(idCommitment, invalidMin, 1); + + vm.expectRevert(abi.encodeWithSelector(InvalidRateLimit.selector)); + w.register(idCommitment, invalidMax, 1); + } + + function test__ValidRegistrationExtend(uint32 userMessageLimit, uint32 numberOfPeriods) external { + vm.pauseGasMetering(); + uint256 idCommitment = 2; + vm.assume(numberOfPeriods > 0 && numberOfPeriods < 100); + (, uint256 price) = w.priceCalculator().calculate(userMessageLimit, numberOfPeriods); + vm.assume( + userMessageLimit >= w.minRateLimitPerMembership() && userMessageLimit <= w.maxRateLimitPerMembership() + ); + vm.assume(w.isValidUserMessageLimit(userMessageLimit)); + vm.resumeGasMetering(); + + w.register{ value: price }(idCommitment, userMessageLimit, numberOfPeriods); + (,,,, uint256 gracePeriodStartDate,,,,,) = w.members(idCommitment); + + assertFalse(w.isGracePeriod(idCommitment)); + assertFalse(w.isExpired(idCommitment)); + + vm.warp(gracePeriodStartDate); + + assertTrue(w.isGracePeriod(idCommitment)); + assertFalse(w.isExpired(idCommitment)); + + // Registering other memberships just to check linkage is correct + for (uint256 i = 1; i < 5; i++) { + w.register{ value: price }(idCommitment + i, userMessageLimit, numberOfPeriods); + } + + assertEq(w.head(), idCommitment); + + uint256[] memory commitmentsToExtend = new uint256[](1); + commitmentsToExtend[0] = idCommitment; + + // Attempt to extend the membership (but it is not owned by us) + address randomAddress = vm.addr(block.timestamp); + vm.prank(randomAddress); + vm.expectRevert(abi.encodeWithSelector(NotHolder.selector, commitmentsToExtend[0])); + w.extend(commitmentsToExtend); + + // Attempt to extend the membership (but now we are the owner) + w.extend(commitmentsToExtend); + + (,,,, uint256 newGracePeriodStartDate,,,,,) = w.members(idCommitment); + + assertEq(block.timestamp + (uint256(w.billingPeriod()) * uint256(numberOfPeriods)), newGracePeriodStartDate); + assertFalse(w.isGracePeriod(idCommitment)); + assertFalse(w.isExpired(idCommitment)); + + // Verify list order is correct + assertEq(w.tail(), idCommitment); + assertEq(w.head(), idCommitment + 1); + + // Ensure that prev and next are chained correctly + for (uint256 i = 0; i < 5; i++) { + uint256 currIdCommitment = idCommitment + i; + (uint256 prev, uint256 next,,,,,,,,) = w.members(currIdCommitment); + console.log("idCommitment: %s - prev: %s - next: %s", currIdCommitment, prev, next); + if (i == 0) { + // Verifying links of extended idCommitment + assertEq(next, 0); + assertEq(prev, idCommitment + 4); + } else if (i == 1) { + // The second element in the chain became the oldest + assertEq(next, currIdCommitment + 1); + assertEq(prev, 0); + } else if (i == 4) { + assertEq(prev, currIdCommitment - 1); + assertEq(next, idCommitment); + } else { + // The rest of the elements maintain their order + assertEq(prev, currIdCommitment - 1); + assertEq(next, currIdCommitment + 1); + } + } + + // TODO: should it be possible to extend expired memberships? + + // Attempt to extend a non grace period membership + commitmentsToExtend[0] = idCommitment + 1; + vm.expectRevert(abi.encodeWithSelector(NotInGracePeriod.selector, commitmentsToExtend[0])); + w.extend(commitmentsToExtend); + } + + function test__ValidRegistrationExpiry(uint32 userMessageLimit, uint32 numberOfPeriods) external { + vm.pauseGasMetering(); + uint256 idCommitment = 2; + vm.assume(numberOfPeriods > 0 && numberOfPeriods < 100); + (, uint256 price) = w.priceCalculator().calculate(userMessageLimit, numberOfPeriods); + vm.assume( + userMessageLimit >= w.minRateLimitPerMembership() && userMessageLimit <= w.maxRateLimitPerMembership() + ); + vm.assume(w.isValidUserMessageLimit(userMessageLimit)); + vm.resumeGasMetering(); + + w.register{ value: price }(idCommitment, userMessageLimit, numberOfPeriods); + + (,,, uint32 fetchedNumberOfPeriods, uint256 fetchedGracePeriodStartDate, uint32 fetchedGracePeriod,,,,) = + w.members(idCommitment); + + uint256 expectedExpirationDate = + fetchedGracePeriodStartDate + (uint256(fetchedGracePeriod) * uint256(fetchedNumberOfPeriods)); + uint256 expirationDate = w.expirationDate(idCommitment); + + assertEq(expectedExpirationDate, expirationDate); + + vm.warp(expirationDate + 1); + + assertFalse(w.isGracePeriod(idCommitment)); + assertTrue(w.isExpired(idCommitment)); + + // Registering other memberships just to check linkage is correct + for (uint256 i = 1; i <= 5; i++) { + w.register{ value: price }(idCommitment + i, userMessageLimit, numberOfPeriods); + } + + assertEq(w.head(), idCommitment); + assertEq(w.tail(), idCommitment + 5); + } + + function test__RegistrationWhenMaxRateLimitIsReached() external { + // TODO: implement + // TODO: validate elements are chained correctly + // TODO: validate reuse of index + } + + function test__RegistrationWhenMaxRateLimitIsReachedAndSingleExpiredMemberAvailable() external { + // TODO: implement + // TODO: validate elements are chained correctly + // TODO: validate reuse of index + // TODO: validate balance + } + + function test__RegistrationWhenMaxRateLimitIsReachedAndMultipleExpiredMembersAvailable() external { + // TODO: implement + // TODO: validate elements are chained correctly + // TODO: validate reuse of index + // TODO: validate balance + } + + function test__RemoveExpiredMemberships(uint32 userMessageLimit, uint32 numberOfPeriods) external { + vm.pauseGasMetering(); + uint256 idCommitment = 2; + vm.assume(numberOfPeriods > 0 && numberOfPeriods < 100); + (, uint256 price) = w.priceCalculator().calculate(userMessageLimit, numberOfPeriods); + vm.assume( + userMessageLimit >= w.minRateLimitPerMembership() && userMessageLimit <= w.maxRateLimitPerMembership() + ); + vm.assume(w.isValidUserMessageLimit(userMessageLimit)); + vm.resumeGasMetering(); + + uint256 time = block.timestamp; + for (uint256 i = 0; i < 5; i++) { + w.register{ value: price }(idCommitment + i, userMessageLimit, numberOfPeriods); + time += 100; + vm.warp(time); + } + + // Expiring the first 3 + uint256 expirationDate = w.expirationDate(idCommitment + 2); + vm.warp(expirationDate + 1); + for (uint256 i = 0; i < 5; i++) { + if (i <= 2) { + assertTrue(w.isExpired(idCommitment + i)); + } else { + assertFalse(w.isExpired(idCommitment + i)); + } + } + + uint256[] memory commitmentsToErase = new uint256[](2); + commitmentsToErase[0] = idCommitment + 1; + commitmentsToErase[1] = idCommitment + 2; + w.eraseMemberships(commitmentsToErase); + + address holder; + + (,,,,,,,, holder,) = w.members(idCommitment + 1); + assertEq(holder, address(0)); + + (,,,,,,,, holder,) = w.members(idCommitment + 2); + assertEq(holder, address(0)); + + // Verify list order is correct + uint256 prev; + uint256 next; + (prev, next,,,,,,,,) = w.members(idCommitment); + assertEq(prev, 0); + assertEq(next, idCommitment + 3); + (prev, next,,,,,,,,) = w.members(idCommitment + 3); + assertEq(prev, idCommitment); + assertEq(next, idCommitment + 4); + (prev, next,,,,,,,,) = w.members(idCommitment + 4); + assertEq(prev, idCommitment + 3); + assertEq(next, 0); + assertEq(w.head(), idCommitment); + assertEq(w.tail(), idCommitment + 4); + + // Attempting to call erase when some of the commitments can't be erased yet + // idCommitment can be erased (in grace period), but idCommitment + 4 is still active + (,,,, uint256 gracePeriodStartDate,,,,,) = w.members(idCommitment + 4); + vm.warp(gracePeriodStartDate - 1); + commitmentsToErase[0] = idCommitment; + commitmentsToErase[1] = idCommitment + 4; + vm.expectRevert(abi.encodeWithSelector(CantEraseMembership.selector, idCommitment + 4)); + w.eraseMemberships(commitmentsToErase); + } + + function test__RemoveAllExpiredMemberships(uint32 idCommitmentsLength) external { + vm.pauseGasMetering(); + vm.assume(idCommitmentsLength > 1 && idCommitmentsLength <= 100); + uint32 userMessageLimit = w.minRateLimitPerMembership(); + uint32 numberOfPeriods = 5; + (, uint256 price) = w.priceCalculator().calculate(userMessageLimit, numberOfPeriods); + vm.resumeGasMetering(); + + uint256 time = block.timestamp; + for (uint256 i = 1; i <= idCommitmentsLength; i++) { + w.register{ value: price }(i, userMessageLimit, numberOfPeriods); + time += 100; + vm.warp(time); + } + + uint256 expirationDate = w.expirationDate(idCommitmentsLength); + vm.warp(expirationDate + 1); + for (uint256 i = 1; i <= 5; i++) { + assertTrue(w.isExpired(i)); + } + + uint256[] memory commitmentsToErase = new uint256[](idCommitmentsLength); + for (uint256 i = 0; i < idCommitmentsLength; i++) { + commitmentsToErase[i] = i + 1; + } + w.eraseMemberships(commitmentsToErase); + + // No memberships registered + assertEq(w.head(), 0); + assertEq(w.tail(), 0); + + for (uint256 i = 10; i <= idCommitmentsLength + 10; i++) { + w.register{ value: price }(i, userMessageLimit, numberOfPeriods); + assertEq(w.tail(), i); + } + + // Verify list order is correct + assertEq(w.head(), 10); + assertEq(w.tail(), idCommitmentsLength + 10); + uint256 prev; + uint256 next; + (prev, next,,,,,,,,) = w.members(10); + assertEq(prev, 0); + assertEq(next, 11); + (prev, next,,,,,,,,) = w.members(idCommitmentsLength + 10); + assertEq(prev, idCommitmentsLength + 9); + assertEq(next, 0); } - function test__InvalidRegistration__InvalidUserMessageLimit__Zero() external { + function test__WithdrawETH(uint32 userMessageLimit, uint32 numberOfPeriods) external { + vm.pauseGasMetering(); uint256 idCommitment = 2; - uint32 userMessageLimit = 0; - vm.expectRevert(abi.encodeWithSelector(InvalidUserMessageLimit.selector, 0)); - w.register(idCommitment, userMessageLimit); + vm.assume(numberOfPeriods > 0 && numberOfPeriods < 100); + (, uint256 price) = w.priceCalculator().calculate(userMessageLimit, numberOfPeriods); + vm.assume( + userMessageLimit >= w.minRateLimitPerMembership() && userMessageLimit <= w.maxRateLimitPerMembership() + ); + vm.assume(w.isValidUserMessageLimit(userMessageLimit)); + vm.resumeGasMetering(); + + w.register{ value: price }(idCommitment, userMessageLimit, numberOfPeriods); + + (,,,, uint256 gracePeriodStartDate,,,,,) = w.members(idCommitment); + + vm.warp(gracePeriodStartDate); + + uint256[] memory commitmentsToErase = new uint256[](1); + commitmentsToErase[0] = idCommitment; + w.eraseMemberships(commitmentsToErase); + + uint256 availableBalance = w.balancesToWithdraw(address(this), address(0)); + + assertEq(availableBalance, price); + assertEq(address(w).balance, price); + + uint256 balanceBeforeWithdraw = address(this).balance; + + w.withdraw(address(0)); + + uint256 balanceAfterWithdraw = address(this).balance; + + availableBalance = w.balancesToWithdraw(address(this), address(0)); + assertEq(availableBalance, 0); + assertEq(address(w).balance, 0); + assertEq(balanceBeforeWithdraw + price, balanceAfterWithdraw); } - function test__InvalidRegistration__InvalidUserMessageLimit__LargerThanMax() external { + function test__WithdrawToken(uint32 userMessageLimit, uint32 numberOfPeriods) external { + vm.pauseGasMetering(); uint256 idCommitment = 2; - uint32 userMessageLimit = w.MAX_MESSAGE_LIMIT() + 1; - vm.expectRevert(abi.encodeWithSelector(InvalidUserMessageLimit.selector, userMessageLimit)); - w.register(idCommitment, userMessageLimit); + LinearPriceCalculator priceCalculator = LinearPriceCalculator(address(w.priceCalculator())); + vm.assume(numberOfPeriods > 0 && numberOfPeriods < 100); + vm.prank(priceCalculator.owner()); + priceCalculator.setTokenAndPrice(address(token), 5 wei); + (, uint256 price) = w.priceCalculator().calculate(userMessageLimit, numberOfPeriods); + token.mint(address(this), price); + vm.assume( + userMessageLimit >= w.minRateLimitPerMembership() && userMessageLimit <= w.maxRateLimitPerMembership() + ); + vm.assume(w.isValidUserMessageLimit(userMessageLimit)); + vm.resumeGasMetering(); + + token.approve(address(w), price); + w.register(idCommitment, userMessageLimit, numberOfPeriods); + + (,,,, uint256 gracePeriodStartDate,,,,,) = w.members(idCommitment); + + vm.warp(gracePeriodStartDate); + + uint256[] memory commitmentsToErase = new uint256[](1); + commitmentsToErase[0] = idCommitment; + w.eraseMemberships(commitmentsToErase); + + uint256 availableBalance = w.balancesToWithdraw(address(this), address(token)); + + assertEq(availableBalance, price); + assertEq(token.balanceOf(address(w)), price); + + uint256 balanceBeforeWithdraw = token.balanceOf(address(this)); + + w.withdraw(address(token)); + + uint256 balanceAfterWithdraw = token.balanceOf(address(this)); + + availableBalance = w.balancesToWithdraw(address(this), address(token)); + assertEq(availableBalance, 0); + assertEq(token.balanceOf(address(w)), 0); + assertEq(balanceBeforeWithdraw + price, balanceAfterWithdraw); } function test__InvalidRegistration__DuplicateIdCommitment() external { + vm.pauseGasMetering(); uint256 idCommitment = 2; - uint32 userMessageLimit = 2; - w.register(idCommitment, userMessageLimit); + uint32 userMessageLimit = w.minRateLimitPerMembership(); + uint32 numberOfPeriods = 2; + (, uint256 price) = w.priceCalculator().calculate(userMessageLimit, numberOfPeriods); + vm.resumeGasMetering(); + + w.register{ value: price }(idCommitment, userMessageLimit, numberOfPeriods); vm.expectRevert(DuplicateIdCommitment.selector); - w.register(idCommitment, userMessageLimit); + w.register{ value: price }(idCommitment, userMessageLimit, numberOfPeriods); } + // TODO: state has changed due to adding new variables. Update this + /* function test__InvalidRegistration__FullTree() external { + vm.pauseGasMetering(); uint32 userMessageLimit = 2; + (, uint256 price) = w.priceCalculator().calculate(userMessageLimit); + vm.resumeGasMetering(); + // we progress the tree to the last leaf /*| Name | Type | Slot | Offset | Bytes | |---------------------|-----------------------------------------------------|------|--------|-------| @@ -143,13 +609,15 @@ contract WakuRlnV2Test is Test { | memberInfo | mapping(uint256 => struct WakuRlnV2.MembershipInfo) | 1 | 0 | 32 | | deployedBlockNumber | uint32 | 2 | 0 | 4 | | imtData | struct LazyIMTData | 3 | 0 | 64 |*/ - // we set MAX_MESSAGE_LIMIT to 20 (unaltered) - // we set SET_SIZE to 4294967295 (1 << 20) (unaltered) - // we set commitmentIndex to 4294967295 (1 << 20) (altered) - vm.store(address(w), bytes32(uint256(201)), 0x0000000000000000000000000000000000000000ffffffffffffffff00000014); + // we set MAX_MESSAGE_LIMIT to 20 (unaltered) + // we set SET_SIZE to 4294967295 (1 << 20) (unaltered) + // we set commitmentIndex to 4294967295 (1 << 20) (altered) + /* vm.store(address(w), bytes32(uint256(201)), + 0x0000000000000000000000000000000000000000ffffffffffffffff00000014); vm.expectRevert(FullTree.selector); - w.register(1, userMessageLimit); + w.register{ value: price }(1, userMessageLimit); } + */ function test__InvalidPaginationQuery__StartIndexGTEndIndex() external { vm.expectRevert(abi.encodeWithSelector(InvalidPaginationQuery.selector, 1, 0)); @@ -162,9 +630,14 @@ contract WakuRlnV2Test is Test { } function test__ValidPaginationQuery__OneElement() external { - uint32 userMessageLimit = 2; + vm.pauseGasMetering(); uint256 idCommitment = 1; - w.register(idCommitment, userMessageLimit); + uint32 userMessageLimit = w.minRateLimitPerMembership(); + uint32 numberOfPeriods = 2; + (, uint256 price) = w.priceCalculator().calculate(userMessageLimit, numberOfPeriods); + vm.resumeGasMetering(); + + w.register{ value: price }(idCommitment, userMessageLimit, numberOfPeriods); uint256[] memory commitments = w.getCommitments(0, 0); assertEq(commitments.length, 1); uint256 rateCommitment = PoseidonT3.hash([idCommitment, userMessageLimit]); @@ -172,12 +645,14 @@ contract WakuRlnV2Test is Test { } function test__ValidPaginationQuery(uint32 idCommitmentsLength) external { + vm.pauseGasMetering(); vm.assume(idCommitmentsLength > 0 && idCommitmentsLength <= 100); - uint32 userMessageLimit = 2; + uint32 userMessageLimit = w.minRateLimitPerMembership(); + uint32 numberOfPeriods = 2; + (, uint256 price) = w.priceCalculator().calculate(userMessageLimit, numberOfPeriods); - vm.pauseGasMetering(); for (uint256 i = 0; i < idCommitmentsLength; i++) { - w.register(i + 1, userMessageLimit); + w.register{ value: price }(i + 1, userMessageLimit, numberOfPeriods); } vm.resumeGasMetering(); @@ -191,7 +666,7 @@ contract WakuRlnV2Test is Test { function test__Upgrade() external { address testImpl = address(new WakuRlnV2()); - bytes memory data = abi.encodeCall(WakuRlnV2.initialize, 20); + bytes memory data = abi.encodeCall(WakuRlnV2.initialize, (address(0), 100, 1, 10, 10 minutes, 4 minutes)); address proxy = address(new ERC1967Proxy(testImpl, data)); address newImpl = address(new WakuRlnV2()); @@ -207,4 +682,6 @@ contract WakuRlnV2Test is Test { ); assertEq(fetchedImpl, newImpl); } + + receive() external payable { } } From fe018930dc41aa3288deb46e0df827630ca30f79 Mon Sep 17 00:00:00 2001 From: Richard Ramos Date: Fri, 6 Sep 2024 09:58:56 -0400 Subject: [PATCH 02/21] chore: more test units and fixes --- src/Membership.sol | 73 +++++++++++++++++++++++++------------------- src/WakuRlnV2.sol | 9 ++++++ test/WakuRlnV2.t.sol | 42 +++++++++++++++++++++++-- 3 files changed, 89 insertions(+), 35 deletions(-) diff --git a/src/Membership.sol b/src/Membership.sol index 52d316c..c1be2e5 100644 --- a/src/Membership.sol +++ b/src/Membership.sol @@ -6,6 +6,8 @@ import "openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; import "openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol"; import "openzeppelin-contracts/contracts/utils/Context.sol"; +import "forge-std/console.sol"; + // The number of periods should be greater than zero error NumberOfPeriodsCantBeZero(); @@ -20,7 +22,7 @@ error InvalidRateLimit(); // It's not possible to acquire the rate limit due to exceeding the expected limits // even after attempting to erase expired memberships -error ExceedMaxRateLimitPerEpoch(); +error ExceedAvailableMaxRateLimitPerEpoch(); // This membership is not in grace period yet error NotInGracePeriod(uint256 idCommitment); @@ -80,7 +82,7 @@ contract Membership { uint256 next; /// @notice amount of the token used to acquire this membership uint256 amount; - /// @notice numPeriods + /// @notice numberOfPeriods uint32 numberOfPeriods; /// @notice timestamp of when the grace period starts for this membership uint256 gracePeriodStartDate; @@ -202,36 +204,44 @@ contract Membership { revert InvalidRateLimit(); } - // Attempt to free expired membership slots - while (totalRateLimitPerEpoch + _rateLimit > maxTotalRateLimitPerEpoch) { - // Determine if there are any available spot in the membership map - // by looking at the oldest membership. If it's expired, we can free it - MembershipInfo memory oldestMembership = members[head]; - - if ( - oldestMembership.holder != address(0) // membership has a holder - && isExpired(oldestMembership.gracePeriodStartDate) - ) { - emit MemberExpired(head, oldestMembership.userMessageLimit, oldestMembership.index); - - // Deduct the expired membership rate limit - totalRateLimitPerEpoch -= oldestMembership.userMessageLimit; - - // Promote the next oldest membership to oldest - uint256 nextOldest = oldestMembership.next; - head = nextOldest; - if (nextOldest != 0) { - members[nextOldest].prev = 0; + // Determine if we exceed the total rate limit + if (totalRateLimitPerEpoch + _rateLimit > maxTotalRateLimitPerEpoch) { + if (head == 0) revert ExceedAvailableMaxRateLimitPerEpoch(); // List is empty + + // Attempt to free expired membership slots + while (totalRateLimitPerEpoch + _rateLimit > maxTotalRateLimitPerEpoch) { + // Determine if there are any available spot in the membership map + // by looking at the oldest membership. If it's expired, we can free it + MembershipInfo memory oldestMembership = members[head]; + if ( + oldestMembership.holder != address(0) // membership has a holder + && _isExpired( + oldestMembership.gracePeriodStartDate, + oldestMembership.gracePeriod, + oldestMembership.numberOfPeriods + ) + ) { + emit MemberExpired(head, oldestMembership.userMessageLimit, oldestMembership.index); + + // Deduct the expired membership rate limit + totalRateLimitPerEpoch -= oldestMembership.userMessageLimit; + + // Promote the next oldest membership to oldest + uint256 nextOldest = oldestMembership.next; + head = nextOldest; + if (nextOldest != 0) { + members[nextOldest].prev = 0; + } + + // Move balance from expired membership to holder balance + balancesToWithdraw[oldestMembership.holder][oldestMembership.token] += oldestMembership.amount; + + availableExpiredIndices.push(oldestMembership.index); + + delete members[head]; + } else { + revert ExceedAvailableMaxRateLimitPerEpoch(); } - - // Move balance from expired membership to holder balance - balancesToWithdraw[oldestMembership.holder][oldestMembership.token] += oldestMembership.amount; - - availableExpiredIndices.push(oldestMembership.index); - - delete members[head]; - } else { - revert ExceedMaxRateLimitPerEpoch(); } } @@ -242,7 +252,6 @@ contract Membership { prev = tail; } else { // First item - // TODO: test adding memberships after the list has been emptied head = _idCommitment; } diff --git a/src/WakuRlnV2.sol b/src/WakuRlnV2.sol index 19c8683..69c23a7 100644 --- a/src/WakuRlnV2.sol +++ b/src/WakuRlnV2.sol @@ -77,6 +77,11 @@ contract WakuRlnV2 is Initializable, OwnableUpgradeable, UUPSUpgradeable, Member public initializer { + require(_maxTotalRateLimitPerEpoch >= maxRateLimitPerMembership); + require(_maxRateLimitPerMembership > minRateLimitPerMembership); + require(_minRateLimitPerMembership > 0); + require(_billingPeriod > 0); + __Ownable_init(); __UUPSUpgradeable_init(); __Membership_init( @@ -244,24 +249,28 @@ contract WakuRlnV2 is Initializable, OwnableUpgradeable, UUPSUpgradeable, Member /// @notice Set the maximum total rate limit of all memberships in the tree /// @param _maxTotalRateLimitPerEpoch new value function setMaxTotalRateLimitPerEpoch(uint32 _maxTotalRateLimitPerEpoch) external onlyOwner { + require(_maxTotalRateLimitPerEpoch >= maxRateLimitPerMembership); maxTotalRateLimitPerEpoch = _maxTotalRateLimitPerEpoch; } /// @notice Set the maximum rate limit of one membership /// @param _maxRateLimitPerMembership new value function setMaxRateLimitPerMembership(uint32 _maxRateLimitPerMembership) external onlyOwner { + require(_maxRateLimitPerMembership >= minRateLimitPerMembership); maxRateLimitPerMembership = _maxRateLimitPerMembership; } /// @notice Set the minimum rate limit of one membership /// @param _minRateLimitPerMembership new value function setMinRateLimitPerMembership(uint32 _minRateLimitPerMembership) external onlyOwner { + require(_minRateLimitPerMembership > 0); minRateLimitPerMembership = _minRateLimitPerMembership; } /// @notice Set the membership billing period /// @param _billingPeriod new value function setBillingPeriod(uint32 _billingPeriod) external onlyOwner { + require(_billingPeriod > 0); billingPeriod = _billingPeriod; } diff --git a/test/WakuRlnV2.t.sol b/test/WakuRlnV2.t.sol index 245826a..86467a4 100644 --- a/test/WakuRlnV2.t.sol +++ b/test/WakuRlnV2.t.sol @@ -363,15 +363,51 @@ contract WakuRlnV2Test is Test { } function test__RegistrationWhenMaxRateLimitIsReached() external { - // TODO: implement - // TODO: validate elements are chained correctly - // TODO: validate reuse of index + vm.pauseGasMetering(); + vm.startPrank(w.owner()); + w.setMinRateLimitPerMembership(1); + w.setMaxRateLimitPerMembership(5); + w.setMaxTotalRateLimitPerEpoch(5); + vm.stopPrank(); + vm.resumeGasMetering(); + + bool isValid = w.isValidUserMessageLimit(6); + assertFalse(isValid); + + // Exceeds the max rate limit per user + uint32 userMessageLimit = 10; + (, uint256 price) = w.priceCalculator().calculate(userMessageLimit, 1); + vm.expectRevert(abi.encodeWithSelector(InvalidRateLimit.selector)); + w.register{ value: price }(1, userMessageLimit, 1); + + // Should register succesfully + userMessageLimit = 4; + (, price) = w.priceCalculator().calculate(userMessageLimit, 1); + w.register{ value: price }(2, userMessageLimit, 1); + + // Exceeds the rate limit + userMessageLimit = 2; + (, price) = w.priceCalculator().calculate(userMessageLimit, 1); + vm.expectRevert(abi.encodeWithSelector(ExceedAvailableMaxRateLimitPerEpoch.selector)); + w.register{ value: price }(3, userMessageLimit, 1); + + // Should register succesfully + userMessageLimit = 1; + (, price) = w.priceCalculator().calculate(userMessageLimit, 1); + w.register{ value: price }(3, userMessageLimit, 1); + + // We ran out of rate limit again + userMessageLimit = 1; + (, price) = w.priceCalculator().calculate(userMessageLimit, 1); + vm.expectRevert(abi.encodeWithSelector(ExceedAvailableMaxRateLimitPerEpoch.selector)); + w.register{ value: price }(4, userMessageLimit, 1); } function test__RegistrationWhenMaxRateLimitIsReachedAndSingleExpiredMemberAvailable() external { // TODO: implement // TODO: validate elements are chained correctly // TODO: validate reuse of index + // TODO: validate that expired event is emitted // TODO: validate balance } From 5ddfc3554d34d156c0b7d87678cba17fae9e8964 Mon Sep 17 00:00:00 2001 From: Richard Ramos Date: Fri, 6 Sep 2024 11:25:46 -0400 Subject: [PATCH 03/21] chore: more test units and fixes --- src/Membership.sol | 21 +++++----- test/WakuRlnV2.t.sol | 91 +++++++++++++++++++++++++++++++++++++++----- 2 files changed, 94 insertions(+), 18 deletions(-) diff --git a/src/Membership.sol b/src/Membership.sol index c1be2e5..772b7ae 100644 --- a/src/Membership.sol +++ b/src/Membership.sol @@ -6,8 +6,6 @@ import "openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; import "openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol"; import "openzeppelin-contracts/contracts/utils/Context.sol"; -import "forge-std/console.sol"; - // The number of periods should be greater than zero error NumberOfPeriodsCantBeZero(); @@ -226,19 +224,22 @@ contract Membership { // Deduct the expired membership rate limit totalRateLimitPerEpoch -= oldestMembership.userMessageLimit; + // Remove the element from the list + delete members[head]; + // Promote the next oldest membership to oldest uint256 nextOldest = oldestMembership.next; head = nextOldest; if (nextOldest != 0) { members[nextOldest].prev = 0; + } else { + tail = 0; } // Move balance from expired membership to holder balance balancesToWithdraw[oldestMembership.holder][oldestMembership.token] += oldestMembership.amount; availableExpiredIndices.push(oldestMembership.index); - - delete members[head]; } else { revert ExceedAvailableMaxRateLimitPerEpoch(); } @@ -275,7 +276,7 @@ contract Membership { token: _token, amount: _amount, userMessageLimit: _rateLimit, - next: 0, // It's the last value, so point to nowhere + next: 0, // It's the newest value, so point to nowhere prev: prev, index: index }); @@ -296,7 +297,7 @@ contract Membership { if (_sender != mdetails.holder) revert NotHolder(_idCommitment); - uint256 newExpirationDate = block.timestamp + (uint256(billingPeriod) * uint256(mdetails.numberOfPeriods)); + uint256 newGracePeriodStartDate = block.timestamp + (uint256(billingPeriod) * uint256(mdetails.numberOfPeriods)); uint256 mdetailsNext = mdetails.next; uint256 mdetailsPrev = mdetails.prev; @@ -317,13 +318,13 @@ contract Membership { // Move membership to the end (since it will be the newest) mdetails.next = 0; mdetails.prev = tail; - mdetails.gracePeriodStartDate = newExpirationDate; + mdetails.gracePeriodStartDate = newGracePeriodStartDate; mdetails.gracePeriod = gracePeriod; members[tail].next = _idCommitment; tail = _idCommitment; - emit MemberExtended(_idCommitment, mdetails.userMessageLimit, mdetails.index, newExpirationDate); + emit MemberExtended(_idCommitment, mdetails.userMessageLimit, mdetails.index, newGracePeriodStartDate); } /// @dev Determine whether a timestamp is considered to be expired or not after exceeding the grace period @@ -349,9 +350,11 @@ contract Membership { return _isExpired(m.gracePeriodStartDate, m.gracePeriod, m.numberOfPeriods); } + /// @notice Returns the timestamp on which a membership can be considered expired + /// @param _idCommitment the idCommitment of the membership function expirationDate(uint256 _idCommitment) public view returns (uint256) { MembershipInfo memory m = members[_idCommitment]; - return m.gracePeriodStartDate + (uint256(m.gracePeriod) * uint256(m.numberOfPeriods)); + return m.gracePeriodStartDate + (uint256(m.gracePeriod) * uint256(m.numberOfPeriods)) + 1; } /// @dev Determine whether a timestamp is considered to be in grace period or not diff --git a/test/WakuRlnV2.t.sol b/test/WakuRlnV2.t.sol index 86467a4..827a037 100644 --- a/test/WakuRlnV2.t.sol +++ b/test/WakuRlnV2.t.sol @@ -283,6 +283,8 @@ contract WakuRlnV2Test is Test { w.extend(commitmentsToExtend); // Attempt to extend the membership (but now we are the owner) + vm.expectEmit(true, false, false, false); // only check the first parameter of the event (the idCommitment) + emit Membership.MemberExtended(idCommitment, 0, 0, 0); w.extend(commitmentsToExtend); (,,,, uint256 newGracePeriodStartDate,,,,,) = w.members(idCommitment); @@ -343,12 +345,12 @@ contract WakuRlnV2Test is Test { w.members(idCommitment); uint256 expectedExpirationDate = - fetchedGracePeriodStartDate + (uint256(fetchedGracePeriod) * uint256(fetchedNumberOfPeriods)); + fetchedGracePeriodStartDate + (uint256(fetchedGracePeriod) * uint256(fetchedNumberOfPeriods)) + 1; uint256 expirationDate = w.expirationDate(idCommitment); assertEq(expectedExpirationDate, expirationDate); - vm.warp(expirationDate + 1); + vm.warp(expirationDate); assertFalse(w.isGracePeriod(idCommitment)); assertTrue(w.isExpired(idCommitment)); @@ -404,11 +406,62 @@ contract WakuRlnV2Test is Test { } function test__RegistrationWhenMaxRateLimitIsReachedAndSingleExpiredMemberAvailable() external { - // TODO: implement - // TODO: validate elements are chained correctly - // TODO: validate reuse of index - // TODO: validate that expired event is emitted - // TODO: validate balance + vm.pauseGasMetering(); + vm.startPrank(w.owner()); + w.setMinRateLimitPerMembership(1); + w.setMaxRateLimitPerMembership(5); + w.setMaxTotalRateLimitPerEpoch(5); + vm.stopPrank(); + vm.resumeGasMetering(); + + uint32 userMessageLimitA = 2; + uint32 totalUserMessageLimit = userMessageLimitA; + (, uint256 priceA) = w.priceCalculator().calculate(userMessageLimitA, 1); + w.register{ value: priceA }(1, userMessageLimitA, 1); + + (,,,, uint256 gracePeriodStartDate,,, uint32 indexA,,) = w.members(1); + vm.warp(gracePeriodStartDate + 1); + + // Exceeds the rate limit, but if the first were expired, it should register + // It is in grace period so can't be erased + assertTrue(w.isGracePeriod(1)); + assertFalse(w.isExpired(1)); + uint32 userMessageLimitB = 4; + (, uint256 priceB) = w.priceCalculator().calculate(userMessageLimitB, 1); + (, priceB) = w.priceCalculator().calculate(userMessageLimitB, 1); + vm.expectRevert(abi.encodeWithSelector(ExceedAvailableMaxRateLimitPerEpoch.selector)); + w.register{ value: priceB }(2, userMessageLimitB, 1); + + // FFW until the membership is expired so we can get rid of it + uint256 expirationDate = w.expirationDate(1); + vm.warp(expirationDate); + assertTrue(w.isExpired(1)); + + // It should succeed now + vm.expectEmit(); + emit Membership.MemberExpired(1, userMessageLimitA, indexA); + w.register{ value: priceB }(2, userMessageLimitB, 1); + + // The previous expired membership should have been erased + (,,,,,,,, address holder,) = w.members(1); + assertEq(holder, address(0)); + + uint32 expectedUserMessageLimit = totalUserMessageLimit - userMessageLimitA + userMessageLimitB; + assertEq(expectedUserMessageLimit, w.totalRateLimitPerEpoch()); + + // The new commitment should be the only element in the list + assertEq(w.head(), 2); + assertEq(w.tail(), 2); + (uint256 prev, uint256 next,,,,,, uint32 indexB,,) = w.members(2); + assertEq(prev, 0); + assertEq(next, 0); + + // Index should have been reused + assertEq(indexA, indexB); + + // The balance available for withdrawal should match the amount of the expired membership + uint256 availableBalance = w.balancesToWithdraw(address(this), address(0)); + assertEq(availableBalance, priceA); } function test__RegistrationWhenMaxRateLimitIsReachedAndMultipleExpiredMembersAvailable() external { @@ -416,8 +469,20 @@ contract WakuRlnV2Test is Test { // TODO: validate elements are chained correctly // TODO: validate reuse of index // TODO: validate balance + + // TODO: check that the expired memberships are gone + // TODO: check the usermessage limits (total) + // TODO: check that it reused the index of the one gone + // TODO: check head and tail are correct and next and prev + // TODO: validate that expired event is emitted + // TODO: validate balance } + function test__RegistrationWhenMaxRateLimitIsReachedAndMultipleExpiredMembersAvailableButTheyDontHaveEnoughRateLimit( + ) + external + { } + function test__RemoveExpiredMemberships(uint32 userMessageLimit, uint32 numberOfPeriods) external { vm.pauseGasMetering(); uint256 idCommitment = 2; @@ -438,7 +503,7 @@ contract WakuRlnV2Test is Test { // Expiring the first 3 uint256 expirationDate = w.expirationDate(idCommitment + 2); - vm.warp(expirationDate + 1); + vm.warp(expirationDate); for (uint256 i = 0; i < 5; i++) { if (i <= 2) { assertTrue(w.isExpired(idCommitment + i)); @@ -450,6 +515,11 @@ contract WakuRlnV2Test is Test { uint256[] memory commitmentsToErase = new uint256[](2); commitmentsToErase[0] = idCommitment + 1; commitmentsToErase[1] = idCommitment + 2; + + vm.expectEmit(true, false, false, false); // only check the first parameter of the event (the idCommitment) + emit Membership.MemberExpired(commitmentsToErase[0], 0, 0); + vm.expectEmit(true, false, false, false); // only check the first parameter of the event (the idCommitment) + emit Membership.MemberExpired(commitmentsToErase[0], 0, 0); w.eraseMemberships(commitmentsToErase); address holder; @@ -501,7 +571,7 @@ contract WakuRlnV2Test is Test { } uint256 expirationDate = w.expirationDate(idCommitmentsLength); - vm.warp(expirationDate + 1); + vm.warp(expirationDate); for (uint256 i = 1; i <= 5; i++) { assertTrue(w.isExpired(i)); } @@ -509,7 +579,10 @@ contract WakuRlnV2Test is Test { uint256[] memory commitmentsToErase = new uint256[](idCommitmentsLength); for (uint256 i = 0; i < idCommitmentsLength; i++) { commitmentsToErase[i] = i + 1; + vm.expectEmit(true, false, false, false); // only check the first parameter of the event (the idCommitment) + emit Membership.MemberExpired(i + 1, 0, 0); } + w.eraseMemberships(commitmentsToErase); // No memberships registered From 9a1c1dc33d2c6099d4b731b98167eb46521a92ef Mon Sep 17 00:00:00 2001 From: Richard Ramos Date: Fri, 6 Sep 2024 12:09:04 -0400 Subject: [PATCH 04/21] refactor: minor optimization when expiring memberships --- src/Membership.sol | 15 ++++++++------- test/WakuRlnV2.t.sol | 40 +++++++++++++++++++++++++++++++++++----- 2 files changed, 43 insertions(+), 12 deletions(-) diff --git a/src/Membership.sol b/src/Membership.sol index 772b7ae..7fa48b4 100644 --- a/src/Membership.sol +++ b/src/Membership.sol @@ -228,13 +228,7 @@ contract Membership { delete members[head]; // Promote the next oldest membership to oldest - uint256 nextOldest = oldestMembership.next; - head = nextOldest; - if (nextOldest != 0) { - members[nextOldest].prev = 0; - } else { - tail = 0; - } + head = oldestMembership.next; // Move balance from expired membership to holder balance balancesToWithdraw[oldestMembership.holder][oldestMembership.token] += oldestMembership.amount; @@ -244,6 +238,13 @@ contract Membership { revert ExceedAvailableMaxRateLimitPerEpoch(); } } + + // Ensure new head and tail are pointing to the correct memberships + if (head != 0) { + members[head].prev = 0; + } else { + tail = 0; + } } uint256 prev = 0; diff --git a/test/WakuRlnV2.t.sol b/test/WakuRlnV2.t.sol index 827a037..f2274a0 100644 --- a/test/WakuRlnV2.t.sol +++ b/test/WakuRlnV2.t.sol @@ -465,16 +465,46 @@ contract WakuRlnV2Test is Test { } function test__RegistrationWhenMaxRateLimitIsReachedAndMultipleExpiredMembersAvailable() external { - // TODO: implement - // TODO: validate elements are chained correctly + vm.pauseGasMetering(); + vm.startPrank(w.owner()); + w.setMinRateLimitPerMembership(1); + w.setMaxRateLimitPerMembership(5); + w.setMaxTotalRateLimitPerEpoch(5); + vm.stopPrank(); + vm.resumeGasMetering(); + + (, uint256 price) = w.priceCalculator().calculate(1, 10); + w.register{ value: price }(1, 1, 10); + vm.warp(block.timestamp + 100); + w.register{ value: price }(2, 1, 10); + vm.warp(block.timestamp + 100); + uint256 expirationDate = w.expirationDate(2); + vm.warp(expirationDate); + w.register{ value: price }(3, 1, 10); + + // Make sure only the first 2 memberships are expired + assertTrue(w.isExpired(1)); + assertTrue(w.isExpired(2)); + assertFalse(w.isExpired(3) || w.isGracePeriod(3)); + + // Attempt to register a membership that will require to expire 2 memberships + // Currently there is 2 available, and we want to register 4 + // If we remove first membership, we'll have 3 available + // If we also remove the second, we'll have 4 available + vm.expectEmit(true, false, false, false); + emit Membership.MemberExpired(1, 0, 0); + vm.expectEmit(true, false, false, false); + emit Membership.MemberExpired(2, 0, 0); + (, price) = w.priceCalculator().calculate(4, 10); + w.register{ value: price }(4, 4, 10); + // TODO: validate reuse of index // TODO: validate balance - - // TODO: check that the expired memberships are gone + // TODO: check that the expired memberships are gone and membership 3 is still there // TODO: check the usermessage limits (total) // TODO: check that it reused the index of the one gone // TODO: check head and tail are correct and next and prev - // TODO: validate that expired event is emitted + // TODO: there should be at least a single index available // TODO: validate balance } From 9251400051a2774ffb07dd3545fca78cc3d9031d Mon Sep 17 00:00:00 2001 From: Richard Ramos Date: Fri, 6 Sep 2024 16:12:19 -0400 Subject: [PATCH 05/21] chore: some fixes and gas optimizations --- src/Membership.sol | 88 +++++++++++++++++++++++++------------------ test/WakuRlnV2.t.sol | 90 ++++++++++++++++++++++++++++++++++++-------- 2 files changed, 126 insertions(+), 52 deletions(-) diff --git a/src/Membership.sol b/src/Membership.sol index 7fa48b4..e7d5744 100644 --- a/src/Membership.sol +++ b/src/Membership.sol @@ -202,33 +202,39 @@ contract Membership { revert InvalidRateLimit(); } + // Storing in local variable to not access the storage frequently + // And we're using/modifying these variables in each iteration + uint256 _head = head; + uint256 _tail = tail; + uint256 _totalRateLimitPerEpoch = totalRateLimitPerEpoch; + uint32 _maxTotalRateLimitPerEpoch = maxTotalRateLimitPerEpoch; + // Determine if we exceed the total rate limit - if (totalRateLimitPerEpoch + _rateLimit > maxTotalRateLimitPerEpoch) { - if (head == 0) revert ExceedAvailableMaxRateLimitPerEpoch(); // List is empty + if (_totalRateLimitPerEpoch + _rateLimit > _maxTotalRateLimitPerEpoch) { + if (_head == 0) revert ExceedAvailableMaxRateLimitPerEpoch(); // List is empty // Attempt to free expired membership slots - while (totalRateLimitPerEpoch + _rateLimit > maxTotalRateLimitPerEpoch) { + while (_totalRateLimitPerEpoch + _rateLimit > _maxTotalRateLimitPerEpoch && _head != 0) { // Determine if there are any available spot in the membership map // by looking at the oldest membership. If it's expired, we can free it - MembershipInfo memory oldestMembership = members[head]; + MembershipInfo memory oldestMembership = members[_head]; if ( - oldestMembership.holder != address(0) // membership has a holder - && _isExpired( - oldestMembership.gracePeriodStartDate, - oldestMembership.gracePeriod, - oldestMembership.numberOfPeriods - ) + _isExpired( + oldestMembership.gracePeriodStartDate, + oldestMembership.gracePeriod, + oldestMembership.numberOfPeriods + ) ) { - emit MemberExpired(head, oldestMembership.userMessageLimit, oldestMembership.index); + emit MemberExpired(_head, oldestMembership.userMessageLimit, oldestMembership.index); // Deduct the expired membership rate limit - totalRateLimitPerEpoch -= oldestMembership.userMessageLimit; + _totalRateLimitPerEpoch -= oldestMembership.userMessageLimit; // Remove the element from the list - delete members[head]; + delete members[_head]; // Promote the next oldest membership to oldest - head = oldestMembership.next; + _head = oldestMembership.next; // Move balance from expired membership to holder balance balancesToWithdraw[oldestMembership.holder][oldestMembership.token] += oldestMembership.amount; @@ -240,24 +246,22 @@ contract Membership { } // Ensure new head and tail are pointing to the correct memberships - if (head != 0) { - members[head].prev = 0; + if (_head != 0) { + members[_head].prev = 0; } else { - tail = 0; + _tail = 0; } } - uint256 prev = 0; - if (tail != 0) { - MembershipInfo storage latestMembership = members[tail]; - latestMembership.next = _idCommitment; - prev = tail; + if (_tail != 0) { + members[_tail].next = _idCommitment; } else { // First item - head = _idCommitment; + _head = _idCommitment; } - totalRateLimitPerEpoch += _rateLimit; + // Adding the rate limit of the new registration + _totalRateLimitPerEpoch += _rateLimit; // Reuse available slots from previously removed expired memberships uint256 arrLen = availableExpiredIndices.length; @@ -269,6 +273,7 @@ contract Membership { index = commitmentIndex; } + totalRateLimitPerEpoch = _totalRateLimitPerEpoch; members[_idCommitment] = MembershipInfo({ holder: _sender, gracePeriodStartDate: block.timestamp + (uint256(billingPeriod) * uint256(_numberOfPeriods)), @@ -278,10 +283,10 @@ contract Membership { amount: _amount, userMessageLimit: _rateLimit, next: 0, // It's the newest value, so point to nowhere - prev: prev, + prev: _tail, index: index }); - + head = _head; tail = _idCommitment; } @@ -300,29 +305,40 @@ contract Membership { uint256 newGracePeriodStartDate = block.timestamp + (uint256(billingPeriod) * uint256(mdetails.numberOfPeriods)); - uint256 mdetailsNext = mdetails.next; - uint256 mdetailsPrev = mdetails.prev; + uint256 next = mdetails.next; + uint256 prev = mdetails.prev; + uint256 _tail = tail; + uint256 _head = head; // Remove current membership references - if (mdetailsPrev != 0) { - members[mdetailsPrev].next = mdetailsNext; + if (prev != 0) { + members[prev].next = next; } else { - head = mdetailsNext; + _head = next; } - if (mdetailsNext != 0) { - members[mdetailsNext].prev = mdetailsPrev; + if (next != 0) { + members[next].prev = prev; } else { - tail = mdetailsPrev; + _tail = prev; } // Move membership to the end (since it will be the newest) mdetails.next = 0; - mdetails.prev = tail; + mdetails.prev = _tail; mdetails.gracePeriodStartDate = newGracePeriodStartDate; mdetails.gracePeriod = gracePeriod; - members[tail].next = _idCommitment; + // Link previous tail with membership that was just extended + if (_tail != 0) { + members[_tail].next = _idCommitment; + } else { + // There are no other items in the list. + // The head will become the extended commitment + _head = _idCommitment; + } + + head = _head; tail = _idCommitment; emit MemberExtended(_idCommitment, mdetails.userMessageLimit, mdetails.index, newGracePeriodStartDate); diff --git a/test/WakuRlnV2.t.sol b/test/WakuRlnV2.t.sol index f2274a0..24c9b92 100644 --- a/test/WakuRlnV2.t.sol +++ b/test/WakuRlnV2.t.sol @@ -328,6 +328,38 @@ contract WakuRlnV2Test is Test { w.extend(commitmentsToExtend); } + function test__ValidRegistrationExtendSingleMembership(uint32 userMessageLimit, uint32 numberOfPeriods) external { + vm.pauseGasMetering(); + uint256 idCommitment = 2; + vm.assume(numberOfPeriods > 0 && numberOfPeriods < 100); + (, uint256 price) = w.priceCalculator().calculate(userMessageLimit, numberOfPeriods); + vm.assume( + userMessageLimit >= w.minRateLimitPerMembership() && userMessageLimit <= w.maxRateLimitPerMembership() + ); + vm.assume(w.isValidUserMessageLimit(userMessageLimit)); + vm.resumeGasMetering(); + + w.register{ value: price }(idCommitment, userMessageLimit, numberOfPeriods); + (,,,, uint256 gracePeriodStartDate,,,,,) = w.members(idCommitment); + + vm.warp(gracePeriodStartDate); + + uint256[] memory commitmentsToExtend = new uint256[](1); + commitmentsToExtend[0] = idCommitment; + + // Extend the membership + vm.expectEmit(true, false, false, false); // only check the first parameter of the event (the idCommitment) + emit Membership.MemberExtended(idCommitment, 0, 0, 0); + w.extend(commitmentsToExtend); + + // Verify list order is correct + assertEq(w.tail(), idCommitment); + assertEq(w.head(), idCommitment); + (uint256 prev, uint256 next,,,,,,,,) = w.members(idCommitment); + assertEq(next, 0); + assertEq(prev, 0); + } + function test__ValidRegistrationExpiry(uint32 userMessageLimit, uint32 numberOfPeriods) external { vm.pauseGasMetering(); uint256 idCommitment = 2; @@ -473,39 +505,63 @@ contract WakuRlnV2Test is Test { vm.stopPrank(); vm.resumeGasMetering(); - (, uint256 price) = w.priceCalculator().calculate(1, 10); - w.register{ value: price }(1, 1, 10); + (, uint256 priceA) = w.priceCalculator().calculate(1, 10); + w.register{ value: priceA }(1, 1, 10); vm.warp(block.timestamp + 100); - w.register{ value: price }(2, 1, 10); + w.register{ value: priceA }(2, 1, 10); vm.warp(block.timestamp + 100); uint256 expirationDate = w.expirationDate(2); vm.warp(expirationDate); - w.register{ value: price }(3, 1, 10); + w.register{ value: priceA }(3, 1, 10); // Make sure only the first 2 memberships are expired assertTrue(w.isExpired(1)); assertTrue(w.isExpired(2)); assertFalse(w.isExpired(3) || w.isGracePeriod(3)); + (,,,,,,, uint32 index1,,) = w.members(1); + (,,,,,,, uint32 index2,,) = w.members(2); + // Attempt to register a membership that will require to expire 2 memberships // Currently there is 2 available, and we want to register 4 // If we remove first membership, we'll have 3 available // If we also remove the second, we'll have 4 available vm.expectEmit(true, false, false, false); - emit Membership.MemberExpired(1, 0, 0); + emit Membership.MemberExpired(1, 0, 0); vm.expectEmit(true, false, false, false); emit Membership.MemberExpired(2, 0, 0); - (, price) = w.priceCalculator().calculate(4, 10); - w.register{ value: price }(4, 4, 10); - - // TODO: validate reuse of index - // TODO: validate balance - // TODO: check that the expired memberships are gone and membership 3 is still there - // TODO: check the usermessage limits (total) - // TODO: check that it reused the index of the one gone - // TODO: check head and tail are correct and next and prev - // TODO: there should be at least a single index available - // TODO: validate balance + (, uint256 priceB) = w.priceCalculator().calculate(4, 10); + w.register{ value: priceB }(4, 4, 10); + + // idCommitment4 will use the last removed index available (since we push to an array) + (,,,,,,, uint32 index4,,) = w.members(4); + assertEq(index4, index2); + + // the index of the first removed membership is still available for further registrations + assertEq(index1, w.availableExpiredIndices(0)); + + // The previous expired memberships should have been erased + (,,,,,,,, address holder,) = w.members(1); + assertEq(holder, address(0)); + (,,,,,,,, holder,) = w.members(2); + assertEq(holder, address(0)); + + // The total rate limit used should be those from idCommitment 3 and 4 + assertEq(5, w.totalRateLimitPerEpoch()); + + // There should only be 2 memberships, the non expired and the new one + assertEq(w.head(), 3); + assertEq(w.tail(), 4); + (uint256 prev, uint256 next,,,,,,,,) = w.members(3); + assertEq(prev, 0); + assertEq(next, 4); + (prev, next,,,,,,,,) = w.members(4); + assertEq(prev, 3); + assertEq(next, 0); + + // The balance available for withdrawal should match the amount of the expired membership + uint256 availableBalance = w.balancesToWithdraw(address(this), address(0)); + assertEq(availableBalance, priceA * 2); } function test__RegistrationWhenMaxRateLimitIsReachedAndMultipleExpiredMembersAvailableButTheyDontHaveEnoughRateLimit( @@ -513,6 +569,8 @@ contract WakuRlnV2Test is Test { external { } + function test__indexReuse() external { } + function test__RemoveExpiredMemberships(uint32 userMessageLimit, uint32 numberOfPeriods) external { vm.pauseGasMetering(); uint256 idCommitment = 2; From a369de965d892a8532b9ebe9aa3cb128673d68d0 Mon Sep 17 00:00:00 2001 From: Richard Ramos Date: Fri, 6 Sep 2024 16:52:57 -0400 Subject: [PATCH 06/21] chore: more tests --- test/WakuRlnV2.t.sol | 37 +++++++++++++++++++++++++++++++++++-- 1 file changed, 35 insertions(+), 2 deletions(-) diff --git a/test/WakuRlnV2.t.sol b/test/WakuRlnV2.t.sol index 24c9b92..263381b 100644 --- a/test/WakuRlnV2.t.sol +++ b/test/WakuRlnV2.t.sol @@ -567,9 +567,42 @@ contract WakuRlnV2Test is Test { function test__RegistrationWhenMaxRateLimitIsReachedAndMultipleExpiredMembersAvailableButTheyDontHaveEnoughRateLimit( ) external - { } + { + vm.pauseGasMetering(); + vm.startPrank(w.owner()); + w.setMinRateLimitPerMembership(1); + w.setMaxRateLimitPerMembership(5); + w.setMaxTotalRateLimitPerEpoch(5); + vm.stopPrank(); + vm.resumeGasMetering(); + + (, uint256 priceA) = w.priceCalculator().calculate(1, 10); + w.register{ value: priceA }(1, 1, 10); + vm.warp(block.timestamp + 100); + w.register{ value: priceA }(2, 1, 10); + vm.warp(block.timestamp + 100); + uint256 expirationDate = w.expirationDate(2); + vm.warp(expirationDate); + w.register{ value: priceA }(3, 1, 10); + + // Make sure only the first 2 memberships are expired + assertTrue(w.isExpired(1)); + assertTrue(w.isExpired(2)); + assertFalse(w.isExpired(3) || w.isGracePeriod(3)); - function test__indexReuse() external { } + // Attempt to register a membership that will require to expire 2 memberships + // Currently there is 2 available, and we want to register 5 + // If we remove first membership, we'll have 3 available + // If we also remove the second, we'll have 4 available, but it is still not enough + // for registering + (, uint256 priceB) = w.priceCalculator().calculate(5, 10); + vm.expectRevert(abi.encodeWithSelector(ExceedAvailableMaxRateLimitPerEpoch.selector)); + w.register{ value: priceB }(4, 5, 10); + } + + function test__indexReuse() external { + // TODO: implement + } function test__RemoveExpiredMemberships(uint32 userMessageLimit, uint32 numberOfPeriods) external { vm.pauseGasMetering(); From 0d2f43e98b2a34720386c269f3a37b5c64ee93e0 Mon Sep 17 00:00:00 2001 From: Richard Ramos Date: Fri, 6 Sep 2024 19:06:03 -0400 Subject: [PATCH 07/21] fix: fulltree test --- test/WakuRlnV2.t.sol | 43 +++++++++++++++++++++++++------------------ 1 file changed, 25 insertions(+), 18 deletions(-) diff --git a/test/WakuRlnV2.t.sol b/test/WakuRlnV2.t.sol index 263381b..843edf0 100644 --- a/test/WakuRlnV2.t.sol +++ b/test/WakuRlnV2.t.sol @@ -601,7 +601,7 @@ contract WakuRlnV2Test is Test { } function test__indexReuse() external { - // TODO: implement + revert("TODO: implement"); } function test__RemoveExpiredMemberships(uint32 userMessageLimit, uint32 numberOfPeriods) external { @@ -822,32 +822,39 @@ contract WakuRlnV2Test is Test { w.register{ value: price }(idCommitment, userMessageLimit, numberOfPeriods); } - // TODO: state has changed due to adding new variables. Update this - /* function test__InvalidRegistration__FullTree() external { vm.pauseGasMetering(); - uint32 userMessageLimit = 2; - (, uint256 price) = w.priceCalculator().calculate(userMessageLimit); + uint32 userMessageLimit = 20; + (, uint256 price) = w.priceCalculator().calculate(userMessageLimit, 1); vm.resumeGasMetering(); // we progress the tree to the last leaf + /*| Name | Type | Slot | Offset | Bytes | |---------------------|-----------------------------------------------------|------|--------|-------| - | MAX_MESSAGE_LIMIT | uint32 | 0 | 0 | 4 | - | SET_SIZE | uint32 | 0 | 4 | 4 | - | commitmentIndex | uint32 | 0 | 8 | 4 | - | memberInfo | mapping(uint256 => struct WakuRlnV2.MembershipInfo) | 1 | 0 | 32 | - | deployedBlockNumber | uint32 | 2 | 0 | 4 | - | imtData | struct LazyIMTData | 3 | 0 | 64 |*/ - // we set MAX_MESSAGE_LIMIT to 20 (unaltered) - // we set SET_SIZE to 4294967295 (1 << 20) (unaltered) - // we set commitmentIndex to 4294967295 (1 << 20) (altered) - /* vm.store(address(w), bytes32(uint256(201)), - 0x0000000000000000000000000000000000000000ffffffffffffffff00000014); + | commitmentIndex | uint32 | 206 | 0 | 4 | */ + + /* + Pro tip: to easily find the storage slot of a variable, without having to calculate the storage layout + based on the variable declaration, set the variable to an easily grepable value like 0xDEADBEEF, and then + execute: + ``` + for (uint256 i = 0; i <= 500; i++) { + bytes32 slot0Value = vm.load(address(w), bytes32(i)); + console.log("%s", i); + console.logBytes32(slot0Value); + } + revert(); + ``` + Search the value in the output (i.e. `DEADBEEF`) to determine the storage slot being used. + If the storage layout changes, update the next line accordingly + */ + + // we set commitmentIndex to 4294967295 (1 << 20) = 0x00100000 + vm.store(address(w), bytes32(uint256(206)), 0x0000000000000000000000000000000000000000000000000000000000100000); vm.expectRevert(FullTree.selector); - w.register{ value: price }(1, userMessageLimit); + w.register{ value: price }(1, userMessageLimit, 1); } - */ function test__InvalidPaginationQuery__StartIndexGTEndIndex() external { vm.expectRevert(abi.encodeWithSelector(InvalidPaginationQuery.selector, 1, 0)); From f91269a1b0ff674e291b428c13676b92fb519730 Mon Sep 17 00:00:00 2001 From: Richard Ramos Date: Fri, 6 Sep 2024 19:28:50 -0400 Subject: [PATCH 08/21] fix: lint --- src/Membership.sol | 69 ++++++++++++++++++++++++-------------------- test/TestToken.sol | 2 +- test/WakuRlnV2.t.sol | 10 +++---- 3 files changed, 43 insertions(+), 38 deletions(-) diff --git a/src/Membership.sol b/src/Membership.sol index e7d5744..a095a67 100644 --- a/src/Membership.sol +++ b/src/Membership.sol @@ -2,9 +2,8 @@ pragma solidity 0.8.24; import { IPriceCalculator } from "./IPriceCalculator.sol"; -import "openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; -import "openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol"; -import "openzeppelin-contracts/contracts/utils/Context.sol"; +import { IERC20 } from "openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; +import { SafeERC20 } from "openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol"; // The number of periods should be greater than zero error NumberOfPeriodsCantBeZero(); @@ -53,13 +52,13 @@ contract Membership { uint32 public gracePeriod; /// @notice balances available to withdraw - mapping(address => mapping(address => uint256)) public balancesToWithdraw; // holder -> token -> balance + mapping(address holder => mapping(address token => uint256 balance)) public balancesToWithdraw; /// @notice Total rate limit of all memberships in the tree uint256 public totalRateLimitPerEpoch; - /// @notice List of registered memberships (IDCommitment to membership metadata) - mapping(uint256 => MembershipInfo) public members; + /// @notice List of registered memberships + mapping(uint256 idCommitment => MembershipInfo member) public members; /// @notice The index of the next member to be registered uint32 public commitmentIndex; @@ -219,30 +218,28 @@ contract Membership { // by looking at the oldest membership. If it's expired, we can free it MembershipInfo memory oldestMembership = members[_head]; if ( - _isExpired( + !_isExpired( oldestMembership.gracePeriodStartDate, oldestMembership.gracePeriod, oldestMembership.numberOfPeriods ) - ) { - emit MemberExpired(_head, oldestMembership.userMessageLimit, oldestMembership.index); + ) revert ExceedAvailableMaxRateLimitPerEpoch(); - // Deduct the expired membership rate limit - _totalRateLimitPerEpoch -= oldestMembership.userMessageLimit; + emit MemberExpired(_head, oldestMembership.userMessageLimit, oldestMembership.index); - // Remove the element from the list - delete members[_head]; + // Deduct the expired membership rate limit + _totalRateLimitPerEpoch -= oldestMembership.userMessageLimit; - // Promote the next oldest membership to oldest - _head = oldestMembership.next; + // Remove the element from the list + delete members[_head]; - // Move balance from expired membership to holder balance - balancesToWithdraw[oldestMembership.holder][oldestMembership.token] += oldestMembership.amount; + // Promote the next oldest membership to oldest + _head = oldestMembership.next; - availableExpiredIndices.push(oldestMembership.index); - } else { - revert ExceedAvailableMaxRateLimitPerEpoch(); - } + // Move balance from expired membership to holder balance + balancesToWithdraw[oldestMembership.holder][oldestMembership.token] += oldestMembership.amount; + + availableExpiredIndices.push(oldestMembership.index); } // Ensure new head and tail are pointing to the correct memberships @@ -264,14 +261,7 @@ contract Membership { _totalRateLimitPerEpoch += _rateLimit; // Reuse available slots from previously removed expired memberships - uint256 arrLen = availableExpiredIndices.length; - if (arrLen != 0) { - index = availableExpiredIndices[arrLen - 1]; - availableExpiredIndices.pop(); - reusedIndex = true; - } else { - index = commitmentIndex; - } + (index, reusedIndex) = _nextIndex(); totalRateLimitPerEpoch = _totalRateLimitPerEpoch; members[_idCommitment] = MembershipInfo({ @@ -290,6 +280,21 @@ contract Membership { tail = _idCommitment; } + /// @dev reuse available slots from previously removed expired memberships + /// @return index index to use + /// @return reusedIndex indicates whether it is reusing an existing index, or using a new one + function _nextIndex() internal returns (uint32 index, bool reusedIndex) { + // Reuse available slots from previously removed expired memberships + uint256 arrLen = availableExpiredIndices.length; + if (arrLen != 0) { + index = availableExpiredIndices[arrLen - 1]; + availableExpiredIndices.pop(); + reusedIndex = true; + } else { + index = commitmentIndex; + } + } + /// @dev Extend a membership expiration date. Membership must be on grace period /// @param _sender the address of the holder of the membership /// @param _idCommitment the idCommitment of the membership @@ -303,7 +308,7 @@ contract Membership { if (_sender != mdetails.holder) revert NotHolder(_idCommitment); - uint256 newGracePeriodStartDate = block.timestamp + (uint256(billingPeriod) * uint256(mdetails.numberOfPeriods)); + uint256 gracePeriodStartDate = block.timestamp + (uint256(billingPeriod) * uint256(mdetails.numberOfPeriods)); uint256 next = mdetails.next; uint256 prev = mdetails.prev; @@ -326,7 +331,7 @@ contract Membership { // Move membership to the end (since it will be the newest) mdetails.next = 0; mdetails.prev = _tail; - mdetails.gracePeriodStartDate = newGracePeriodStartDate; + mdetails.gracePeriodStartDate = gracePeriodStartDate; mdetails.gracePeriod = gracePeriod; // Link previous tail with membership that was just extended @@ -341,7 +346,7 @@ contract Membership { head = _head; tail = _idCommitment; - emit MemberExtended(_idCommitment, mdetails.userMessageLimit, mdetails.index, newGracePeriodStartDate); + emit MemberExtended(_idCommitment, mdetails.userMessageLimit, mdetails.index, gracePeriodStartDate); } /// @dev Determine whether a timestamp is considered to be expired or not after exceeding the grace period diff --git a/test/TestToken.sol b/test/TestToken.sol index 9c84cee..90870cd 100644 --- a/test/TestToken.sol +++ b/test/TestToken.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity >=0.8.19 <0.9.0; -import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; contract TestToken is ERC20 { constructor() ERC20("TestToken", "TTT") { } diff --git a/test/WakuRlnV2.t.sol b/test/WakuRlnV2.t.sol index 843edf0..5d9c810 100644 --- a/test/WakuRlnV2.t.sol +++ b/test/WakuRlnV2.t.sol @@ -6,8 +6,9 @@ import { Deploy } from "../script/Deploy.s.sol"; import { DeploymentConfig } from "../script/DeploymentConfig.s.sol"; import "../src/WakuRlnV2.sol"; // solhint-disable-line import "../src/Membership.sol"; // solhint-disable-line -import "../src/LinearPriceCalculator.sol"; // solhint-disable-line -import "./TestToken.sol"; +import { IPriceCalculator } from "../src/IPriceCalculator.sol"; +import { LinearPriceCalculator } from "../src/LinearPriceCalculator.sol"; +import { TestToken } from "./TestToken.sol"; import { PoseidonT3 } from "poseidon-solidity/PoseidonT3.sol"; import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; import { ERC1967Proxy } from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; @@ -564,8 +565,7 @@ contract WakuRlnV2Test is Test { assertEq(availableBalance, priceA * 2); } - function test__RegistrationWhenMaxRateLimitIsReachedAndMultipleExpiredMembersAvailableButTheyDontHaveEnoughRateLimit( - ) + function test__RegistrationWhenMaxRateLimitReachedAndMultipleExpiredMembersAvailableWithoutEnoughRateLimit() external { vm.pauseGasMetering(); @@ -920,5 +920,5 @@ contract WakuRlnV2Test is Test { assertEq(fetchedImpl, newImpl); } - receive() external payable { } + receive() external payable { } // solhint-disable-line } From 251d8907d4d7ee71e77fdb9ae08e1d4cd32f9f23 Mon Sep 17 00:00:00 2001 From: Richard Ramos Date: Sat, 7 Sep 2024 17:58:19 -0400 Subject: [PATCH 09/21] chore: more tests --- test/WakuRlnV2.t.sol | 49 ++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 47 insertions(+), 2 deletions(-) diff --git a/test/WakuRlnV2.t.sol b/test/WakuRlnV2.t.sol index 5d9c810..d20b065 100644 --- a/test/WakuRlnV2.t.sol +++ b/test/WakuRlnV2.t.sol @@ -600,8 +600,53 @@ contract WakuRlnV2Test is Test { w.register{ value: priceB }(4, 5, 10); } - function test__indexReuse() external { - revert("TODO: implement"); + function test__indexReuse_eraseMemberships(uint32 idCommitmentsLength) external { + vm.assume(idCommitmentsLength > 0 && idCommitmentsLength < 50); + + (, uint256 price) = w.priceCalculator().calculate(20, 1); + uint32 index; + uint256[] memory commitmentsToErase = new uint256[](idCommitmentsLength); + for (uint256 i = 1; i <= idCommitmentsLength; i++) { + w.register{ value: price }(i, 20, 1); + (,,,,,,, index,,) = w.members(i); + assertEq(index, w.commitmentIndex() - 1); // TODO: renname commitmentIndex to nextCommitmentIndex + commitmentsToErase[i - 1] = i; + } + + // time travel to the moment we can erase all expired memberships + uint256 expirationDate = w.expirationDate(idCommitmentsLength); + vm.warp(expirationDate); + w.eraseMemberships(commitmentsToErase); + + // Verify that expired indices match what we expect + for (uint32 i = 0; i < idCommitmentsLength; i++) { + assertEq(i, w.availableExpiredIndices(i)); + } + + uint32 currCommitmentIndex = w.commitmentIndex(); + for (uint256 i = 1; i <= idCommitmentsLength; i++) { + uint256 idCommitment = i + 10; + uint256 expectedReusedIndexPos = idCommitmentsLength - i; + uint32 expectedIndex = w.availableExpiredIndices(expectedReusedIndexPos); + w.register{ value: price }(idCommitment, 20, 1); + (,,,,,,, index,,) = w.members(idCommitment); + assertEq(expectedIndex, index); + // Should have been removed from the list + vm.expectRevert(); + w.availableExpiredIndices(expectedReusedIndexPos); + // Should not have been affected + assertEq(currCommitmentIndex, w.commitmentIndex()); + } + + // No indexes should be available for reuse + vm.expectRevert(); + w.availableExpiredIndices(0); + + // Should use a new index since we got rid of all available indexes + w.register{ value: price }(100, 20, 1); + (,,,,,,, index,,) = w.members(100); + assertEq(index, currCommitmentIndex); + assertEq(currCommitmentIndex + 1, w.commitmentIndex()); } function test__RemoveExpiredMemberships(uint32 userMessageLimit, uint32 numberOfPeriods) external { From 0cd80d639c1f6b3b0b8311dc6f1c1777e9a36323 Mon Sep 17 00:00:00 2001 From: Richard Ramos Date: Mon, 9 Sep 2024 12:20:58 -0400 Subject: [PATCH 10/21] refactor: use expiration term instead of billing period --- .gas-snapshot | 40 +++-- script/Deploy.s.sol | 3 +- src/IPriceCalculator.sol | 3 +- src/LinearPriceCalculator.sol | 15 +- src/Membership.sol | 82 +++-------- src/WakuRlnV2.sol | 16 +- test/WakuRlnV2.t.sol | 269 +++++++++++++++------------------- 7 files changed, 189 insertions(+), 239 deletions(-) diff --git a/.gas-snapshot b/.gas-snapshot index 31c715d..689702b 100644 --- a/.gas-snapshot +++ b/.gas-snapshot @@ -1,11 +1,29 @@ -WakuRlnV2Test:test__IdCommitmentToMetadata__DoesntExist() (gas: 27478) -WakuRlnV2Test:test__InsertionNormalOrder(uint32) (runs: 1001, μ: 1123469, ~: 494837) -WakuRlnV2Test:test__InvalidPaginationQuery__EndIndexGTcommitmentIndex() (gas: 18261) -WakuRlnV2Test:test__InvalidPaginationQuery__StartIndexGTEndIndex() (gas: 16108) -WakuRlnV2Test:test__InvalidRegistration__DuplicateIdCommitment() (gas: 249370) -WakuRlnV2Test:test__InvalidRegistration__InvalidIdCommitment__Zero() (gas: 12135) -WakuRlnV2Test:test__Upgrade() (gas: 6728616) -WakuRlnV2Test:test__ValidPaginationQuery(uint32) (runs: 1002, μ: 226428, ~: 52927) -WakuRlnV2Test:test__ValidPaginationQuery__OneElement() (gas: 262351) -WakuRlnV2Test:test__ValidRegistration(uint256,uint32) (runs: 1001, μ: 268741, ~: 268741) -WakuRlnV2Test:test__ValidRegistration__kats() (gas: 238744) \ No newline at end of file +WakuRlnV2Test:test__IdCommitmentToMetadata__DoesntExist() (gas: 27535) +WakuRlnV2Test:test__InsertionNormalOrder(uint32) (runs: 1004, μ: 1093558, ~: 496185) +WakuRlnV2Test:test__InvalidETHAmount(uint256,uint32) (runs: 1004, μ: 322023, ~: 322023) +WakuRlnV2Test:test__InvalidPaginationQuery__EndIndexGTcommitmentIndex() (gas: 18378) +WakuRlnV2Test:test__InvalidPaginationQuery__StartIndexGTEndIndex() (gas: 16225) +WakuRlnV2Test:test__InvalidRegistration__DuplicateIdCommitment() (gas: 249303) +WakuRlnV2Test:test__InvalidRegistration__FullTree() (gas: 162941) +WakuRlnV2Test:test__InvalidRegistration__InvalidIdCommitment__LargerThanField() (gas: 13471) +WakuRlnV2Test:test__InvalidRegistration__InvalidIdCommitment__Zero() (gas: 12182) +WakuRlnV2Test:test__InvalidRegistration__InvalidUserMessageLimit__MinMax() (gas: 63423) +WakuRlnV2Test:test__LinearPriceCalculation(uint32) (runs: 1015, μ: 25964, ~: 25964) +WakuRlnV2Test:test__RegistrationWhenMaxRateLimitIsReached() (gas: 547839) +WakuRlnV2Test:test__RegistrationWhenMaxRateLimitIsReachedAndMultipleExpiredMembersAvailable() (gas: 739752) +WakuRlnV2Test:test__RegistrationWhenMaxRateLimitIsReachedAndSingleExpiredMemberAvailable() (gas: 388429) +WakuRlnV2Test:test__RegistrationWhenMaxRateLimitReachedAndMultipleExpiredMembersAvailableWithoutEnoughRateLimit() (gas: 748699) +WakuRlnV2Test:test__RegistrationWithTokens(uint256,uint32) (runs: 1004, μ: 318681, ~: 318681) +WakuRlnV2Test:test__RemoveAllExpiredMemberships(uint32) (runs: 1004, μ: 6262152, ~: 1315256) +WakuRlnV2Test:test__RemoveExpiredMemberships(uint32) (runs: 1003, μ: 1075445, ~: 1075445) +WakuRlnV2Test:test__Upgrade() (gas: 6990368) +WakuRlnV2Test:test__ValidPaginationQuery(uint32) (runs: 1005, μ: 222425, ~: 53007) +WakuRlnV2Test:test__ValidPaginationQuery__OneElement() (gas: 262398) +WakuRlnV2Test:test__ValidRegistration(uint32) (runs: 1003, μ: 267715, ~: 267715) +WakuRlnV2Test:test__ValidRegistrationExpiry(uint32) (runs: 1003, μ: 1280765, ~: 1280765) +WakuRlnV2Test:test__ValidRegistrationExtend(uint32) (runs: 1003, μ: 1123986, ~: 1123986) +WakuRlnV2Test:test__ValidRegistrationExtendSingleMembership(uint32) (runs: 1003, μ: 257339, ~: 257339) +WakuRlnV2Test:test__ValidRegistration__kats() (gas: 238726) +WakuRlnV2Test:test__WithdrawETH(uint32) (runs: 1003, μ: 243355, ~: 243356) +WakuRlnV2Test:test__WithdrawToken(uint32) (runs: 1003, μ: 314604, ~: 314605) +WakuRlnV2Test:test__indexReuse_eraseMemberships(uint32) (runs: 1004, μ: 2187085, ~: 964428) \ No newline at end of file diff --git a/script/Deploy.s.sol b/script/Deploy.s.sol index affd2de..2e0f354 100644 --- a/script/Deploy.s.sol +++ b/script/Deploy.s.sol @@ -16,8 +16,7 @@ contract Deploy is BaseScript { address priceCalcAddr = address(new LinearPriceCalculator(address(0), 0.05 ether)); // TODO: set DAI address 0x6B175474E89094C44Da98b954EedeAC495271d0F impl = address(new WakuRlnV2()); - bytes memory data = abi.encodeCall(WakuRlnV2.initialize, (priceCalcAddr, 160_000, 20, 600, 30 days, 5 days)); - // (priceCalcAddr, 160000, 20, 600, 30 days, 5 days) + bytes memory data = abi.encodeCall(WakuRlnV2.initialize, (priceCalcAddr, 160_000, 20, 600, 180 days, 30 days)); address proxy = address(new ERC1967Proxy(impl, data)); w = WakuRlnV2(proxy); } diff --git a/src/IPriceCalculator.sol b/src/IPriceCalculator.sol index 4d67ce6..ff2700e 100644 --- a/src/IPriceCalculator.sol +++ b/src/IPriceCalculator.sol @@ -4,8 +4,7 @@ pragma solidity 0.8.24; interface IPriceCalculator { /// Returns the token and price to pay in `token` for some `_rateLimit` /// @param _rateLimit the rate limit the user wants to acquire - /// @param _numberOfPeriods the number of periods the user wants to acquire /// @return address of the erc20 token /// @return uint price to pay for acquiring the specified `_rateLimit` - function calculate(uint256 _rateLimit, uint32 _numberOfPeriods) external view returns (address, uint256); + function calculate(uint256 _rateLimit) external view returns (address, uint256); } diff --git a/src/LinearPriceCalculator.sol b/src/LinearPriceCalculator.sol index dc3f7ed..14cdbb7 100644 --- a/src/LinearPriceCalculator.sol +++ b/src/LinearPriceCalculator.sol @@ -9,12 +9,12 @@ contract LinearPriceCalculator is IPriceCalculator, Ownable { /// @notice Address of the ERC20 token accepted by this contract. Address(0) represents ETH address public token; - /// @notice The price per message per epoch per period - uint256 public pricePerMessagePerPeriod; + /// @notice The price per message per epoch + uint256 public pricePerMessagePerEpoch; - constructor(address _token, uint256 _pricePerPeriod) Ownable() { + constructor(address _token, uint256 _pricePerMessagePerEpoch) Ownable() { token = _token; - pricePerMessagePerPeriod = _pricePerPeriod; + pricePerMessagePerEpoch = _pricePerMessagePerEpoch; } /// Set accepted token and price per message per epoch per period @@ -22,15 +22,14 @@ contract LinearPriceCalculator is IPriceCalculator, Ownable { /// @param _pricePerPeriod Price per message per epoch function setTokenAndPrice(address _token, uint256 _pricePerPeriod) external onlyOwner { token = _token; - pricePerMessagePerPeriod = _pricePerPeriod; + pricePerMessagePerEpoch = _pricePerPeriod; } /// Returns the token and price to pay in `token` for some `_rateLimit` /// @param _rateLimit the rate limit the user wants to acquire - /// @param _numberOfPeriods the number of periods the user wants to acquire /// @return address of the erc20 token /// @return uint price to pay for acquiring the specified `_rateLimit` - function calculate(uint256 _rateLimit, uint32 _numberOfPeriods) external view returns (address, uint256) { - return (token, uint256(_numberOfPeriods) * uint256(_rateLimit) * pricePerMessagePerPeriod); + function calculate(uint256 _rateLimit) external view returns (address, uint256) { + return (token, uint256(_rateLimit) * pricePerMessagePerEpoch); } } diff --git a/src/Membership.sol b/src/Membership.sol index a095a67..a6bfc1a 100644 --- a/src/Membership.sol +++ b/src/Membership.sol @@ -5,9 +5,6 @@ import { IPriceCalculator } from "./IPriceCalculator.sol"; import { IERC20 } from "openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; import { SafeERC20 } from "openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol"; -// The number of periods should be greater than zero -error NumberOfPeriodsCantBeZero(); - // ETH Amount passed as value on the transaction is not correct error IncorrectAmount(); @@ -46,7 +43,7 @@ contract Membership { uint32 public minRateLimitPerMembership; /// @notice Membership billing period - uint32 public billingPeriod; + uint32 public expirationTerm; /// @notice Membership grace period uint32 public gracePeriod; @@ -79,8 +76,6 @@ contract Membership { uint256 next; /// @notice amount of the token used to acquire this membership uint256 amount; - /// @notice numberOfPeriods - uint32 numberOfPeriods; /// @notice timestamp of when the grace period starts for this membership uint256 gracePeriodStartDate; /// @notice duration of the grace period @@ -130,7 +125,7 @@ contract Membership { maxTotalRateLimitPerEpoch = _maxTotalRateLimitPerEpoch; maxRateLimitPerMembership = _maxRateLimitPerMembership; minRateLimitPerMembership = _minRateLimitPerMembership; - billingPeriod = _expirationTerm; + expirationTerm = _expirationTerm; gracePeriod = _gracePeriod; } @@ -146,23 +141,18 @@ contract Membership { /// @param _sender address of the owner of the new membership /// @param _idCommitment the idcommitment of the new membership /// @param _rateLimit the user message limit - /// @param _numberOfPeriods the number of periods the user wants to acquire /// @return index the index in the merkle tree /// @return reusedIndex indicates whether a new leaf is being used or if using an existing leaf in the merkle tree function _acquireMembership( address _sender, uint256 _idCommitment, - uint32 _rateLimit, - uint32 _numberOfPeriods + uint32 _rateLimit ) internal returns (uint32 index, bool reusedIndex) { - if (_numberOfPeriods == 0) revert NumberOfPeriodsCantBeZero(); - - (address token, uint256 amount) = priceCalculator.calculate(_rateLimit, _numberOfPeriods); - (index, reusedIndex) = - _setupMembershipDetails(_sender, _idCommitment, _rateLimit, _numberOfPeriods, token, amount); + (address token, uint256 amount) = priceCalculator.calculate(_rateLimit); + (index, reusedIndex) = _setupMembershipDetails(_sender, _idCommitment, _rateLimit, token, amount); _transferFees(_sender, token, amount); } @@ -181,7 +171,6 @@ contract Membership { /// @param _sender holder of the membership. Generally `msg.sender` /// @param _idCommitment IDCommitment /// @param _rateLimit User message limit - /// @param _numberOfPeriods the number of periods the user wants to acquire /// @param _token Address of the token used to acquire the membership /// @param _amount Amount of the token used to acquire the membership /// @return index membership index on the merkle tree @@ -190,7 +179,6 @@ contract Membership { address _sender, uint256 _idCommitment, uint32 _rateLimit, - uint32 _numberOfPeriods, address _token, uint256 _amount ) @@ -217,13 +205,9 @@ contract Membership { // Determine if there are any available spot in the membership map // by looking at the oldest membership. If it's expired, we can free it MembershipInfo memory oldestMembership = members[_head]; - if ( - !_isExpired( - oldestMembership.gracePeriodStartDate, - oldestMembership.gracePeriod, - oldestMembership.numberOfPeriods - ) - ) revert ExceedAvailableMaxRateLimitPerEpoch(); + if (!_isExpired(oldestMembership.gracePeriodStartDate, oldestMembership.gracePeriod)) { + revert ExceedAvailableMaxRateLimitPerEpoch(); + } emit MemberExpired(_head, oldestMembership.userMessageLimit, oldestMembership.index); @@ -266,9 +250,8 @@ contract Membership { totalRateLimitPerEpoch = _totalRateLimitPerEpoch; members[_idCommitment] = MembershipInfo({ holder: _sender, - gracePeriodStartDate: block.timestamp + (uint256(billingPeriod) * uint256(_numberOfPeriods)), + gracePeriodStartDate: block.timestamp + uint256(expirationTerm), gracePeriod: gracePeriod, - numberOfPeriods: _numberOfPeriods, token: _token, amount: _amount, userMessageLimit: _rateLimit, @@ -301,14 +284,13 @@ contract Membership { function _extendMembership(address _sender, uint256 _idCommitment) public { MembershipInfo storage mdetails = members[_idCommitment]; - if (!_isGracePeriod(mdetails.gracePeriodStartDate, mdetails.gracePeriod, mdetails.numberOfPeriods)) { - // TODO: can a membership that has exceeded the expired period be extended? + if (!_isGracePeriod(mdetails.gracePeriodStartDate, mdetails.gracePeriod)) { revert NotInGracePeriod(_idCommitment); } if (_sender != mdetails.holder) revert NotHolder(_idCommitment); - uint256 gracePeriodStartDate = block.timestamp + (uint256(billingPeriod) * uint256(mdetails.numberOfPeriods)); + uint256 gracePeriodStartDate = block.timestamp + uint256(expirationTerm); uint256 next = mdetails.next; uint256 prev = mdetails.prev; @@ -352,67 +334,47 @@ contract Membership { /// @dev Determine whether a timestamp is considered to be expired or not after exceeding the grace period /// @param _gracePeriodStartDate timestamp in which the grace period starts /// @param _gracePeriod duration of the grace period - /// @param _numberOfPeriods the number of periods the user wants to acquire - function _isExpired( - uint256 _gracePeriodStartDate, - uint32 _gracePeriod, - uint32 _numberOfPeriods - ) - internal - view - returns (bool) - { - return block.timestamp > _gracePeriodStartDate + (uint256(_gracePeriod) * uint256(_numberOfPeriods)); + function _isExpired(uint256 _gracePeriodStartDate, uint32 _gracePeriod) internal view returns (bool) { + return block.timestamp > _gracePeriodStartDate + uint256(_gracePeriod); } /// @notice Determine if a membership is expired (has exceeded the grace period) /// @param _idCommitment the idCommitment of the membership function isExpired(uint256 _idCommitment) public view returns (bool) { MembershipInfo memory m = members[_idCommitment]; - return _isExpired(m.gracePeriodStartDate, m.gracePeriod, m.numberOfPeriods); + return _isExpired(m.gracePeriodStartDate, m.gracePeriod); } /// @notice Returns the timestamp on which a membership can be considered expired /// @param _idCommitment the idCommitment of the membership function expirationDate(uint256 _idCommitment) public view returns (uint256) { MembershipInfo memory m = members[_idCommitment]; - return m.gracePeriodStartDate + (uint256(m.gracePeriod) * uint256(m.numberOfPeriods)) + 1; + return m.gracePeriodStartDate + uint256(m.gracePeriod) + 1; } /// @dev Determine whether a timestamp is considered to be in grace period or not /// @param _gracePeriodStartDate timestamp in which the grace period starts /// @param _gracePeriod duration of the grace period - /// @param _numberOfPeriods the number of periods the user wants to acquire - function _isGracePeriod( - uint256 _gracePeriodStartDate, - uint32 _gracePeriod, - uint32 _numberOfPeriods - ) - internal - view - returns (bool) - { + function _isGracePeriod(uint256 _gracePeriodStartDate, uint32 _gracePeriod) internal view returns (bool) { uint256 blockTimestamp = block.timestamp; - return blockTimestamp >= _gracePeriodStartDate - && blockTimestamp <= _gracePeriodStartDate + (uint256(_gracePeriod) * uint256(_numberOfPeriods)); + return + blockTimestamp >= _gracePeriodStartDate && blockTimestamp <= _gracePeriodStartDate + uint256(_gracePeriod); } /// @notice Determine if a membership is in grace period /// @param _idCommitment the idCommitment of the membership function isGracePeriod(uint256 _idCommitment) public view returns (bool) { MembershipInfo memory m = members[_idCommitment]; - return _isGracePeriod(m.gracePeriodStartDate, m.gracePeriod, m.numberOfPeriods); + return _isGracePeriod(m.gracePeriodStartDate, m.gracePeriod); } /// @dev Remove expired memberships or owned memberships in grace period. /// @param _sender address of the sender of transaction (will be used to check memberships in grace period) /// @param _idCommitment IDCommitment of the membership to erase function _eraseMembership(address _sender, uint256 _idCommitment, MembershipInfo memory _mdetails) internal { - bool membershipExpired = - _isExpired(_mdetails.gracePeriodStartDate, _mdetails.gracePeriod, _mdetails.numberOfPeriods); - bool isGracePeriodAndOwned = _isGracePeriod( - _mdetails.gracePeriodStartDate, _mdetails.gracePeriod, _mdetails.numberOfPeriods - ) && _mdetails.holder == _sender; + bool membershipExpired = _isExpired(_mdetails.gracePeriodStartDate, _mdetails.gracePeriod); + bool isGracePeriodAndOwned = + _isGracePeriod(_mdetails.gracePeriodStartDate, _mdetails.gracePeriod) && _mdetails.holder == _sender; if (!membershipExpired && !isGracePeriodAndOwned) revert CantEraseMembership(_idCommitment); diff --git a/src/WakuRlnV2.sol b/src/WakuRlnV2.sol index 69c23a7..e9a710e 100644 --- a/src/WakuRlnV2.sol +++ b/src/WakuRlnV2.sol @@ -138,11 +138,9 @@ contract WakuRlnV2 is Initializable, OwnableUpgradeable, UUPSUpgradeable, Member /// @notice Allows a user to register as a member /// @param idCommitment The idCommitment of the member /// @param userMessageLimit The message limit of the member - /// @param numberOfPeriods The number of periods to acquire function register( uint256 idCommitment, - uint32 userMessageLimit, - uint32 numberOfPeriods // TODO: is there a maximum number of periods allowed? + uint32 userMessageLimit ) external payable @@ -152,7 +150,7 @@ contract WakuRlnV2 is Initializable, OwnableUpgradeable, UUPSUpgradeable, Member uint32 index; bool reusedIndex; - (index, reusedIndex) = _acquireMembership(_msgSender(), idCommitment, userMessageLimit, numberOfPeriods); + (index, reusedIndex) = _acquireMembership(_msgSender(), idCommitment, userMessageLimit); _register(idCommitment, userMessageLimit, index, reusedIndex); } @@ -267,11 +265,11 @@ contract WakuRlnV2 is Initializable, OwnableUpgradeable, UUPSUpgradeable, Member minRateLimitPerMembership = _minRateLimitPerMembership; } - /// @notice Set the membership billing period - /// @param _billingPeriod new value - function setBillingPeriod(uint32 _billingPeriod) external onlyOwner { - require(_billingPeriod > 0); - billingPeriod = _billingPeriod; + /// @notice Set the membership expiration term + /// @param _expirationTerm new value + function setBillingPeriod(uint32 _expirationTerm) external onlyOwner { + require(_expirationTerm > 0); + expirationTerm = _expirationTerm; } /// @notice Set the membership grace period diff --git a/test/WakuRlnV2.t.sol b/test/WakuRlnV2.t.sol index d20b065..d57851e 100644 --- a/test/WakuRlnV2.t.sol +++ b/test/WakuRlnV2.t.sol @@ -37,14 +37,13 @@ contract WakuRlnV2Test is Test { uint256 idCommitment = 2; uint32 userMessageLimit = 2; - uint32 numberOfPeriods = 1; - (, uint256 price) = w.priceCalculator().calculate(userMessageLimit, numberOfPeriods); + (, uint256 price) = w.priceCalculator().calculate(userMessageLimit); vm.resumeGasMetering(); - w.register{ value: price }(idCommitment, userMessageLimit, numberOfPeriods); + w.register{ value: price }(idCommitment, userMessageLimit); vm.pauseGasMetering(); assertEq(w.commitmentIndex(), 1); assertEq(w.memberExists(idCommitment), true); - (,,,,,, uint32 fetchedUserMessageLimit, uint32 index, address holder,) = w.members(idCommitment); + (,,,,, uint32 fetchedUserMessageLimit, uint32 index, address holder,) = w.members(idCommitment); assertEq(fetchedUserMessageLimit, userMessageLimit); assertEq(holder, address(this)); assertEq(index, 0); @@ -89,11 +88,10 @@ contract WakuRlnV2Test is Test { vm.resumeGasMetering(); } - function test__ValidRegistration(uint32 userMessageLimit, uint32 numberOfPeriods) external { + function test__ValidRegistration(uint32 userMessageLimit) external { vm.pauseGasMetering(); uint256 idCommitment = 2; - vm.assume(numberOfPeriods > 0 && numberOfPeriods < 100); - (, uint256 price) = w.priceCalculator().calculate(userMessageLimit, numberOfPeriods); + (, uint256 price) = w.priceCalculator().calculate(userMessageLimit); uint256 minUserMessageLimit = w.minRateLimitPerMembership(); uint256 maxUserMessageLimit = w.maxRateLimitPerMembership(); vm.assume(userMessageLimit >= minUserMessageLimit && userMessageLimit <= maxUserMessageLimit); @@ -101,7 +99,7 @@ contract WakuRlnV2Test is Test { vm.resumeGasMetering(); assertEq(w.memberExists(idCommitment), false); - w.register{ value: price }(idCommitment, userMessageLimit, numberOfPeriods); + w.register{ value: price }(idCommitment, userMessageLimit); uint256 rateCommitment = PoseidonT3.hash([idCommitment, userMessageLimit]); (uint32 fetchedUserMessageLimit, uint32 index, uint256 fetchedRateCommitment) = @@ -118,14 +116,13 @@ contract WakuRlnV2Test is Test { vm.assume(idCommitmentsLength > 0 && idCommitmentsLength <= 50); uint32 userMessageLimit = w.minRateLimitPerMembership(); - uint32 numberOfPeriods = 1; - (, uint256 price) = w.priceCalculator().calculate(userMessageLimit, 1); + (, uint256 price) = w.priceCalculator().calculate(userMessageLimit); // Register some commitments for (uint256 i = 0; i < idCommitmentsLength; i++) { uint256 idCommitment = i + 1; - w.register{ value: price }(idCommitment, userMessageLimit, numberOfPeriods); - (uint256 prev, uint256 next,,,,,,,,) = w.members(idCommitment); + w.register{ value: price }(idCommitment, userMessageLimit); + (uint256 prev, uint256 next,,,,,,,) = w.members(idCommitment); // new membership will always be the tail assertEq(next, 0); assertEq(w.tail(), idCommitment); @@ -138,7 +135,7 @@ contract WakuRlnV2Test is Test { // Ensure that prev and next are chained correctly for (uint256 i = 0; i < idCommitmentsLength; i++) { uint256 idCommitment = i + 1; - (uint256 prev, uint256 next,,,,,,,,) = w.members(idCommitment); + (uint256 prev, uint256 next,,,,,,,) = w.members(idCommitment); assertEq(prev, idCommitment - 1); if (i == idCommitmentsLength - 1) { @@ -149,28 +146,21 @@ contract WakuRlnV2Test is Test { } } - function test__LinearPriceCalculation(uint32 userMessageLimit, uint32 numberOfPeriods) external view { + function test__LinearPriceCalculation(uint32 userMessageLimit) external view { IPriceCalculator priceCalculator = w.priceCalculator(); - uint256 pricePerMessagePerPeriod = LinearPriceCalculator(address(priceCalculator)).pricePerMessagePerPeriod(); + uint256 pricePerMessagePerPeriod = LinearPriceCalculator(address(priceCalculator)).pricePerMessagePerEpoch(); assertNotEq(pricePerMessagePerPeriod, 0); - uint256 expectedPrice = uint256(userMessageLimit) * uint256(numberOfPeriods) * pricePerMessagePerPeriod; - (, uint256 price) = w.priceCalculator().calculate(userMessageLimit, numberOfPeriods); + uint256 expectedPrice = uint256(userMessageLimit) * pricePerMessagePerPeriod; + (, uint256 price) = w.priceCalculator().calculate(userMessageLimit); assertEq(price, expectedPrice); } - function test__RegistrationWithTokens( - uint256 idCommitment, - uint32 userMessageLimit, - uint32 numberOfPeriods - ) - external - { + function test__RegistrationWithTokens(uint256 idCommitment, uint32 userMessageLimit) external { vm.pauseGasMetering(); - vm.assume(numberOfPeriods > 0); LinearPriceCalculator priceCalculator = LinearPriceCalculator(address(w.priceCalculator())); vm.prank(priceCalculator.owner()); priceCalculator.setTokenAndPrice(address(token), 5 wei); - (, uint256 price) = w.priceCalculator().calculate(userMessageLimit, numberOfPeriods); + (, uint256 price) = w.priceCalculator().calculate(userMessageLimit); uint256 minUserMessageLimit = w.minRateLimitPerMembership(); uint256 maxUserMessageLimit = w.maxRateLimitPerMembership(); vm.assume(userMessageLimit >= minUserMessageLimit && userMessageLimit <= maxUserMessageLimit); @@ -179,25 +169,24 @@ contract WakuRlnV2Test is Test { token.mint(address(this), price); token.approve(address(w), price); - w.register(idCommitment, userMessageLimit, numberOfPeriods); + w.register(idCommitment, userMessageLimit); assertEq(token.balanceOf(address(w)), price); assertEq(token.balanceOf(address(this)), 0); } - function test__InvalidETHAmount(uint256 idCommitment, uint32 userMessageLimit, uint32 numberOfPeriods) external { + function test__InvalidETHAmount(uint256 idCommitment, uint32 userMessageLimit) external { vm.pauseGasMetering(); - vm.assume(numberOfPeriods > 0 && numberOfPeriods < 100); uint256 minUserMessageLimit = w.minRateLimitPerMembership(); uint256 maxUserMessageLimit = w.maxRateLimitPerMembership(); vm.assume(userMessageLimit >= minUserMessageLimit && userMessageLimit <= maxUserMessageLimit); vm.assume(w.isValidCommitment(idCommitment) && w.isValidUserMessageLimit(userMessageLimit)); - (, uint256 price) = w.priceCalculator().calculate(userMessageLimit, numberOfPeriods); + (, uint256 price) = w.priceCalculator().calculate(userMessageLimit); vm.resumeGasMetering(); vm.expectRevert(abi.encodeWithSelector(IncorrectAmount.selector)); - w.register{ value: price - 1 }(idCommitment, userMessageLimit, numberOfPeriods); + w.register{ value: price - 1 }(idCommitment, userMessageLimit); vm.expectRevert(abi.encodeWithSelector(IncorrectAmount.selector)); - w.register{ value: price + 1 }(idCommitment, userMessageLimit, numberOfPeriods); + w.register{ value: price + 1 }(idCommitment, userMessageLimit); } function test__IdCommitmentToMetadata__DoesntExist() external view { @@ -212,24 +201,22 @@ contract WakuRlnV2Test is Test { vm.pauseGasMetering(); uint256 idCommitment = 0; uint32 userMessageLimit = 2; - uint32 numberOfPeriods = 2; - (, uint256 price) = w.priceCalculator().calculate(userMessageLimit, numberOfPeriods); + (, uint256 price) = w.priceCalculator().calculate(userMessageLimit); vm.resumeGasMetering(); vm.expectRevert(abi.encodeWithSelector(InvalidIdCommitment.selector, 0)); - w.register{ value: price }(idCommitment, userMessageLimit, numberOfPeriods); + w.register{ value: price }(idCommitment, userMessageLimit); } function test__InvalidRegistration__InvalidIdCommitment__LargerThanField() external { vm.pauseGasMetering(); uint32 userMessageLimit = 20; - uint32 numberOfPeriods = 3; - (, uint256 price) = w.priceCalculator().calculate(userMessageLimit, numberOfPeriods); + (, uint256 price) = w.priceCalculator().calculate(userMessageLimit); vm.resumeGasMetering(); uint256 idCommitment = w.Q() + 1; vm.expectRevert(abi.encodeWithSelector(InvalidIdCommitment.selector, idCommitment)); - w.register{ value: price }(idCommitment, userMessageLimit, numberOfPeriods); + w.register{ value: price }(idCommitment, userMessageLimit); } function test__InvalidRegistration__InvalidUserMessageLimit__MinMax() external { @@ -239,25 +226,24 @@ contract WakuRlnV2Test is Test { uint32 invalidMax = w.maxRateLimitPerMembership() + 1; vm.expectRevert(abi.encodeWithSelector(InvalidRateLimit.selector)); - w.register(idCommitment, invalidMin, 1); + w.register(idCommitment, invalidMin); vm.expectRevert(abi.encodeWithSelector(InvalidRateLimit.selector)); - w.register(idCommitment, invalidMax, 1); + w.register(idCommitment, invalidMax); } - function test__ValidRegistrationExtend(uint32 userMessageLimit, uint32 numberOfPeriods) external { + function test__ValidRegistrationExtend(uint32 userMessageLimit) external { vm.pauseGasMetering(); uint256 idCommitment = 2; - vm.assume(numberOfPeriods > 0 && numberOfPeriods < 100); - (, uint256 price) = w.priceCalculator().calculate(userMessageLimit, numberOfPeriods); + (, uint256 price) = w.priceCalculator().calculate(userMessageLimit); vm.assume( userMessageLimit >= w.minRateLimitPerMembership() && userMessageLimit <= w.maxRateLimitPerMembership() ); vm.assume(w.isValidUserMessageLimit(userMessageLimit)); vm.resumeGasMetering(); - w.register{ value: price }(idCommitment, userMessageLimit, numberOfPeriods); - (,,,, uint256 gracePeriodStartDate,,,,,) = w.members(idCommitment); + w.register{ value: price }(idCommitment, userMessageLimit); + (,,, uint256 gracePeriodStartDate,,,,,) = w.members(idCommitment); assertFalse(w.isGracePeriod(idCommitment)); assertFalse(w.isExpired(idCommitment)); @@ -269,7 +255,7 @@ contract WakuRlnV2Test is Test { // Registering other memberships just to check linkage is correct for (uint256 i = 1; i < 5; i++) { - w.register{ value: price }(idCommitment + i, userMessageLimit, numberOfPeriods); + w.register{ value: price }(idCommitment + i, userMessageLimit); } assertEq(w.head(), idCommitment); @@ -288,9 +274,9 @@ contract WakuRlnV2Test is Test { emit Membership.MemberExtended(idCommitment, 0, 0, 0); w.extend(commitmentsToExtend); - (,,,, uint256 newGracePeriodStartDate,,,,,) = w.members(idCommitment); + (,,, uint256 newGracePeriodStartDate,,,,,) = w.members(idCommitment); - assertEq(block.timestamp + (uint256(w.billingPeriod()) * uint256(numberOfPeriods)), newGracePeriodStartDate); + assertEq(block.timestamp + uint256(w.expirationTerm()), newGracePeriodStartDate); assertFalse(w.isGracePeriod(idCommitment)); assertFalse(w.isExpired(idCommitment)); @@ -301,7 +287,7 @@ contract WakuRlnV2Test is Test { // Ensure that prev and next are chained correctly for (uint256 i = 0; i < 5; i++) { uint256 currIdCommitment = idCommitment + i; - (uint256 prev, uint256 next,,,,,,,,) = w.members(currIdCommitment); + (uint256 prev, uint256 next,,,,,,,) = w.members(currIdCommitment); console.log("idCommitment: %s - prev: %s - next: %s", currIdCommitment, prev, next); if (i == 0) { // Verifying links of extended idCommitment @@ -329,19 +315,18 @@ contract WakuRlnV2Test is Test { w.extend(commitmentsToExtend); } - function test__ValidRegistrationExtendSingleMembership(uint32 userMessageLimit, uint32 numberOfPeriods) external { + function test__ValidRegistrationExtendSingleMembership(uint32 userMessageLimit) external { vm.pauseGasMetering(); uint256 idCommitment = 2; - vm.assume(numberOfPeriods > 0 && numberOfPeriods < 100); - (, uint256 price) = w.priceCalculator().calculate(userMessageLimit, numberOfPeriods); + (, uint256 price) = w.priceCalculator().calculate(userMessageLimit); vm.assume( userMessageLimit >= w.minRateLimitPerMembership() && userMessageLimit <= w.maxRateLimitPerMembership() ); vm.assume(w.isValidUserMessageLimit(userMessageLimit)); vm.resumeGasMetering(); - w.register{ value: price }(idCommitment, userMessageLimit, numberOfPeriods); - (,,,, uint256 gracePeriodStartDate,,,,,) = w.members(idCommitment); + w.register{ value: price }(idCommitment, userMessageLimit); + (,,, uint256 gracePeriodStartDate,,,,,) = w.members(idCommitment); vm.warp(gracePeriodStartDate); @@ -356,29 +341,26 @@ contract WakuRlnV2Test is Test { // Verify list order is correct assertEq(w.tail(), idCommitment); assertEq(w.head(), idCommitment); - (uint256 prev, uint256 next,,,,,,,,) = w.members(idCommitment); + (uint256 prev, uint256 next,,,,,,,) = w.members(idCommitment); assertEq(next, 0); assertEq(prev, 0); } - function test__ValidRegistrationExpiry(uint32 userMessageLimit, uint32 numberOfPeriods) external { + function test__ValidRegistrationExpiry(uint32 userMessageLimit) external { vm.pauseGasMetering(); uint256 idCommitment = 2; - vm.assume(numberOfPeriods > 0 && numberOfPeriods < 100); - (, uint256 price) = w.priceCalculator().calculate(userMessageLimit, numberOfPeriods); + (, uint256 price) = w.priceCalculator().calculate(userMessageLimit); vm.assume( userMessageLimit >= w.minRateLimitPerMembership() && userMessageLimit <= w.maxRateLimitPerMembership() ); vm.assume(w.isValidUserMessageLimit(userMessageLimit)); vm.resumeGasMetering(); - w.register{ value: price }(idCommitment, userMessageLimit, numberOfPeriods); + w.register{ value: price }(idCommitment, userMessageLimit); - (,,, uint32 fetchedNumberOfPeriods, uint256 fetchedGracePeriodStartDate, uint32 fetchedGracePeriod,,,,) = - w.members(idCommitment); + (,,, uint256 fetchedGracePeriodStartDate, uint32 fetchedGracePeriod,,,,) = w.members(idCommitment); - uint256 expectedExpirationDate = - fetchedGracePeriodStartDate + (uint256(fetchedGracePeriod) * uint256(fetchedNumberOfPeriods)) + 1; + uint256 expectedExpirationDate = fetchedGracePeriodStartDate + uint256(fetchedGracePeriod) + 1; uint256 expirationDate = w.expirationDate(idCommitment); assertEq(expectedExpirationDate, expirationDate); @@ -390,7 +372,7 @@ contract WakuRlnV2Test is Test { // Registering other memberships just to check linkage is correct for (uint256 i = 1; i <= 5; i++) { - w.register{ value: price }(idCommitment + i, userMessageLimit, numberOfPeriods); + w.register{ value: price }(idCommitment + i, userMessageLimit); } assertEq(w.head(), idCommitment); @@ -411,31 +393,31 @@ contract WakuRlnV2Test is Test { // Exceeds the max rate limit per user uint32 userMessageLimit = 10; - (, uint256 price) = w.priceCalculator().calculate(userMessageLimit, 1); + (, uint256 price) = w.priceCalculator().calculate(userMessageLimit); vm.expectRevert(abi.encodeWithSelector(InvalidRateLimit.selector)); - w.register{ value: price }(1, userMessageLimit, 1); + w.register{ value: price }(1, userMessageLimit); // Should register succesfully userMessageLimit = 4; - (, price) = w.priceCalculator().calculate(userMessageLimit, 1); - w.register{ value: price }(2, userMessageLimit, 1); + (, price) = w.priceCalculator().calculate(userMessageLimit); + w.register{ value: price }(2, userMessageLimit); // Exceeds the rate limit userMessageLimit = 2; - (, price) = w.priceCalculator().calculate(userMessageLimit, 1); + (, price) = w.priceCalculator().calculate(userMessageLimit); vm.expectRevert(abi.encodeWithSelector(ExceedAvailableMaxRateLimitPerEpoch.selector)); - w.register{ value: price }(3, userMessageLimit, 1); + w.register{ value: price }(3, userMessageLimit); // Should register succesfully userMessageLimit = 1; - (, price) = w.priceCalculator().calculate(userMessageLimit, 1); - w.register{ value: price }(3, userMessageLimit, 1); + (, price) = w.priceCalculator().calculate(userMessageLimit); + w.register{ value: price }(3, userMessageLimit); // We ran out of rate limit again userMessageLimit = 1; - (, price) = w.priceCalculator().calculate(userMessageLimit, 1); + (, price) = w.priceCalculator().calculate(userMessageLimit); vm.expectRevert(abi.encodeWithSelector(ExceedAvailableMaxRateLimitPerEpoch.selector)); - w.register{ value: price }(4, userMessageLimit, 1); + w.register{ value: price }(4, userMessageLimit); } function test__RegistrationWhenMaxRateLimitIsReachedAndSingleExpiredMemberAvailable() external { @@ -449,10 +431,10 @@ contract WakuRlnV2Test is Test { uint32 userMessageLimitA = 2; uint32 totalUserMessageLimit = userMessageLimitA; - (, uint256 priceA) = w.priceCalculator().calculate(userMessageLimitA, 1); - w.register{ value: priceA }(1, userMessageLimitA, 1); + (, uint256 priceA) = w.priceCalculator().calculate(userMessageLimitA); + w.register{ value: priceA }(1, userMessageLimitA); - (,,,, uint256 gracePeriodStartDate,,, uint32 indexA,,) = w.members(1); + (,,, uint256 gracePeriodStartDate,,, uint32 indexA,,) = w.members(1); vm.warp(gracePeriodStartDate + 1); // Exceeds the rate limit, but if the first were expired, it should register @@ -460,10 +442,10 @@ contract WakuRlnV2Test is Test { assertTrue(w.isGracePeriod(1)); assertFalse(w.isExpired(1)); uint32 userMessageLimitB = 4; - (, uint256 priceB) = w.priceCalculator().calculate(userMessageLimitB, 1); - (, priceB) = w.priceCalculator().calculate(userMessageLimitB, 1); + (, uint256 priceB) = w.priceCalculator().calculate(userMessageLimitB); + (, priceB) = w.priceCalculator().calculate(userMessageLimitB); vm.expectRevert(abi.encodeWithSelector(ExceedAvailableMaxRateLimitPerEpoch.selector)); - w.register{ value: priceB }(2, userMessageLimitB, 1); + w.register{ value: priceB }(2, userMessageLimitB); // FFW until the membership is expired so we can get rid of it uint256 expirationDate = w.expirationDate(1); @@ -473,10 +455,10 @@ contract WakuRlnV2Test is Test { // It should succeed now vm.expectEmit(); emit Membership.MemberExpired(1, userMessageLimitA, indexA); - w.register{ value: priceB }(2, userMessageLimitB, 1); + w.register{ value: priceB }(2, userMessageLimitB); // The previous expired membership should have been erased - (,,,,,,,, address holder,) = w.members(1); + (,,,,,,, address holder,) = w.members(1); assertEq(holder, address(0)); uint32 expectedUserMessageLimit = totalUserMessageLimit - userMessageLimitA + userMessageLimitB; @@ -485,7 +467,7 @@ contract WakuRlnV2Test is Test { // The new commitment should be the only element in the list assertEq(w.head(), 2); assertEq(w.tail(), 2); - (uint256 prev, uint256 next,,,,,, uint32 indexB,,) = w.members(2); + (uint256 prev, uint256 next,,,,, uint32 indexB,,) = w.members(2); assertEq(prev, 0); assertEq(next, 0); @@ -506,22 +488,22 @@ contract WakuRlnV2Test is Test { vm.stopPrank(); vm.resumeGasMetering(); - (, uint256 priceA) = w.priceCalculator().calculate(1, 10); - w.register{ value: priceA }(1, 1, 10); + (, uint256 priceA) = w.priceCalculator().calculate(1); + w.register{ value: priceA }(1, 1); vm.warp(block.timestamp + 100); - w.register{ value: priceA }(2, 1, 10); + w.register{ value: priceA }(2, 1); vm.warp(block.timestamp + 100); uint256 expirationDate = w.expirationDate(2); vm.warp(expirationDate); - w.register{ value: priceA }(3, 1, 10); + w.register{ value: priceA }(3, 1); // Make sure only the first 2 memberships are expired assertTrue(w.isExpired(1)); assertTrue(w.isExpired(2)); assertFalse(w.isExpired(3) || w.isGracePeriod(3)); - (,,,,,,, uint32 index1,,) = w.members(1); - (,,,,,,, uint32 index2,,) = w.members(2); + (,,,,,, uint32 index1,,) = w.members(1); + (,,,,,, uint32 index2,,) = w.members(2); // Attempt to register a membership that will require to expire 2 memberships // Currently there is 2 available, and we want to register 4 @@ -531,20 +513,20 @@ contract WakuRlnV2Test is Test { emit Membership.MemberExpired(1, 0, 0); vm.expectEmit(true, false, false, false); emit Membership.MemberExpired(2, 0, 0); - (, uint256 priceB) = w.priceCalculator().calculate(4, 10); - w.register{ value: priceB }(4, 4, 10); + (, uint256 priceB) = w.priceCalculator().calculate(4); + w.register{ value: priceB }(4, 4); // idCommitment4 will use the last removed index available (since we push to an array) - (,,,,,,, uint32 index4,,) = w.members(4); + (,,,,,, uint32 index4,,) = w.members(4); assertEq(index4, index2); // the index of the first removed membership is still available for further registrations assertEq(index1, w.availableExpiredIndices(0)); // The previous expired memberships should have been erased - (,,,,,,,, address holder,) = w.members(1); + (,,,,,,, address holder,) = w.members(1); assertEq(holder, address(0)); - (,,,,,,,, holder,) = w.members(2); + (,,,,,,, holder,) = w.members(2); assertEq(holder, address(0)); // The total rate limit used should be those from idCommitment 3 and 4 @@ -553,10 +535,10 @@ contract WakuRlnV2Test is Test { // There should only be 2 memberships, the non expired and the new one assertEq(w.head(), 3); assertEq(w.tail(), 4); - (uint256 prev, uint256 next,,,,,,,,) = w.members(3); + (uint256 prev, uint256 next,,,,,,,) = w.members(3); assertEq(prev, 0); assertEq(next, 4); - (prev, next,,,,,,,,) = w.members(4); + (prev, next,,,,,,,) = w.members(4); assertEq(prev, 3); assertEq(next, 0); @@ -576,14 +558,14 @@ contract WakuRlnV2Test is Test { vm.stopPrank(); vm.resumeGasMetering(); - (, uint256 priceA) = w.priceCalculator().calculate(1, 10); - w.register{ value: priceA }(1, 1, 10); + (, uint256 priceA) = w.priceCalculator().calculate(1); + w.register{ value: priceA }(1, 1); vm.warp(block.timestamp + 100); - w.register{ value: priceA }(2, 1, 10); + w.register{ value: priceA }(2, 1); vm.warp(block.timestamp + 100); uint256 expirationDate = w.expirationDate(2); vm.warp(expirationDate); - w.register{ value: priceA }(3, 1, 10); + w.register{ value: priceA }(3, 1); // Make sure only the first 2 memberships are expired assertTrue(w.isExpired(1)); @@ -595,20 +577,20 @@ contract WakuRlnV2Test is Test { // If we remove first membership, we'll have 3 available // If we also remove the second, we'll have 4 available, but it is still not enough // for registering - (, uint256 priceB) = w.priceCalculator().calculate(5, 10); + (, uint256 priceB) = w.priceCalculator().calculate(5); vm.expectRevert(abi.encodeWithSelector(ExceedAvailableMaxRateLimitPerEpoch.selector)); - w.register{ value: priceB }(4, 5, 10); + w.register{ value: priceB }(4, 5); } function test__indexReuse_eraseMemberships(uint32 idCommitmentsLength) external { vm.assume(idCommitmentsLength > 0 && idCommitmentsLength < 50); - (, uint256 price) = w.priceCalculator().calculate(20, 1); + (, uint256 price) = w.priceCalculator().calculate(20); uint32 index; uint256[] memory commitmentsToErase = new uint256[](idCommitmentsLength); for (uint256 i = 1; i <= idCommitmentsLength; i++) { - w.register{ value: price }(i, 20, 1); - (,,,,,,, index,,) = w.members(i); + w.register{ value: price }(i, 20); + (,,,,,, index,,) = w.members(i); assertEq(index, w.commitmentIndex() - 1); // TODO: renname commitmentIndex to nextCommitmentIndex commitmentsToErase[i - 1] = i; } @@ -628,8 +610,8 @@ contract WakuRlnV2Test is Test { uint256 idCommitment = i + 10; uint256 expectedReusedIndexPos = idCommitmentsLength - i; uint32 expectedIndex = w.availableExpiredIndices(expectedReusedIndexPos); - w.register{ value: price }(idCommitment, 20, 1); - (,,,,,,, index,,) = w.members(idCommitment); + w.register{ value: price }(idCommitment, 20); + (,,,,,, index,,) = w.members(idCommitment); assertEq(expectedIndex, index); // Should have been removed from the list vm.expectRevert(); @@ -643,17 +625,16 @@ contract WakuRlnV2Test is Test { w.availableExpiredIndices(0); // Should use a new index since we got rid of all available indexes - w.register{ value: price }(100, 20, 1); - (,,,,,,, index,,) = w.members(100); + w.register{ value: price }(100, 20); + (,,,,,, index,,) = w.members(100); assertEq(index, currCommitmentIndex); assertEq(currCommitmentIndex + 1, w.commitmentIndex()); } - function test__RemoveExpiredMemberships(uint32 userMessageLimit, uint32 numberOfPeriods) external { + function test__RemoveExpiredMemberships(uint32 userMessageLimit) external { vm.pauseGasMetering(); uint256 idCommitment = 2; - vm.assume(numberOfPeriods > 0 && numberOfPeriods < 100); - (, uint256 price) = w.priceCalculator().calculate(userMessageLimit, numberOfPeriods); + (, uint256 price) = w.priceCalculator().calculate(userMessageLimit); vm.assume( userMessageLimit >= w.minRateLimitPerMembership() && userMessageLimit <= w.maxRateLimitPerMembership() ); @@ -662,7 +643,7 @@ contract WakuRlnV2Test is Test { uint256 time = block.timestamp; for (uint256 i = 0; i < 5; i++) { - w.register{ value: price }(idCommitment + i, userMessageLimit, numberOfPeriods); + w.register{ value: price }(idCommitment + i, userMessageLimit); time += 100; vm.warp(time); } @@ -690,22 +671,22 @@ contract WakuRlnV2Test is Test { address holder; - (,,,,,,,, holder,) = w.members(idCommitment + 1); + (,,,,,,, holder,) = w.members(idCommitment + 1); assertEq(holder, address(0)); - (,,,,,,,, holder,) = w.members(idCommitment + 2); + (,,,,,,, holder,) = w.members(idCommitment + 2); assertEq(holder, address(0)); // Verify list order is correct uint256 prev; uint256 next; - (prev, next,,,,,,,,) = w.members(idCommitment); + (prev, next,,,,,,,) = w.members(idCommitment); assertEq(prev, 0); assertEq(next, idCommitment + 3); - (prev, next,,,,,,,,) = w.members(idCommitment + 3); + (prev, next,,,,,,,) = w.members(idCommitment + 3); assertEq(prev, idCommitment); assertEq(next, idCommitment + 4); - (prev, next,,,,,,,,) = w.members(idCommitment + 4); + (prev, next,,,,,,,) = w.members(idCommitment + 4); assertEq(prev, idCommitment + 3); assertEq(next, 0); assertEq(w.head(), idCommitment); @@ -713,7 +694,7 @@ contract WakuRlnV2Test is Test { // Attempting to call erase when some of the commitments can't be erased yet // idCommitment can be erased (in grace period), but idCommitment + 4 is still active - (,,,, uint256 gracePeriodStartDate,,,,,) = w.members(idCommitment + 4); + (,,, uint256 gracePeriodStartDate,,,,,) = w.members(idCommitment + 4); vm.warp(gracePeriodStartDate - 1); commitmentsToErase[0] = idCommitment; commitmentsToErase[1] = idCommitment + 4; @@ -725,13 +706,12 @@ contract WakuRlnV2Test is Test { vm.pauseGasMetering(); vm.assume(idCommitmentsLength > 1 && idCommitmentsLength <= 100); uint32 userMessageLimit = w.minRateLimitPerMembership(); - uint32 numberOfPeriods = 5; - (, uint256 price) = w.priceCalculator().calculate(userMessageLimit, numberOfPeriods); + (, uint256 price) = w.priceCalculator().calculate(userMessageLimit); vm.resumeGasMetering(); uint256 time = block.timestamp; for (uint256 i = 1; i <= idCommitmentsLength; i++) { - w.register{ value: price }(i, userMessageLimit, numberOfPeriods); + w.register{ value: price }(i, userMessageLimit); time += 100; vm.warp(time); } @@ -756,7 +736,7 @@ contract WakuRlnV2Test is Test { assertEq(w.tail(), 0); for (uint256 i = 10; i <= idCommitmentsLength + 10; i++) { - w.register{ value: price }(i, userMessageLimit, numberOfPeriods); + w.register{ value: price }(i, userMessageLimit); assertEq(w.tail(), i); } @@ -765,28 +745,27 @@ contract WakuRlnV2Test is Test { assertEq(w.tail(), idCommitmentsLength + 10); uint256 prev; uint256 next; - (prev, next,,,,,,,,) = w.members(10); + (prev, next,,,,,,,) = w.members(10); assertEq(prev, 0); assertEq(next, 11); - (prev, next,,,,,,,,) = w.members(idCommitmentsLength + 10); + (prev, next,,,,,,,) = w.members(idCommitmentsLength + 10); assertEq(prev, idCommitmentsLength + 9); assertEq(next, 0); } - function test__WithdrawETH(uint32 userMessageLimit, uint32 numberOfPeriods) external { + function test__WithdrawETH(uint32 userMessageLimit) external { vm.pauseGasMetering(); uint256 idCommitment = 2; - vm.assume(numberOfPeriods > 0 && numberOfPeriods < 100); - (, uint256 price) = w.priceCalculator().calculate(userMessageLimit, numberOfPeriods); + (, uint256 price) = w.priceCalculator().calculate(userMessageLimit); vm.assume( userMessageLimit >= w.minRateLimitPerMembership() && userMessageLimit <= w.maxRateLimitPerMembership() ); vm.assume(w.isValidUserMessageLimit(userMessageLimit)); vm.resumeGasMetering(); - w.register{ value: price }(idCommitment, userMessageLimit, numberOfPeriods); + w.register{ value: price }(idCommitment, userMessageLimit); - (,,,, uint256 gracePeriodStartDate,,,,,) = w.members(idCommitment); + (,,, uint256 gracePeriodStartDate,,,,,) = w.members(idCommitment); vm.warp(gracePeriodStartDate); @@ -811,14 +790,13 @@ contract WakuRlnV2Test is Test { assertEq(balanceBeforeWithdraw + price, balanceAfterWithdraw); } - function test__WithdrawToken(uint32 userMessageLimit, uint32 numberOfPeriods) external { + function test__WithdrawToken(uint32 userMessageLimit) external { vm.pauseGasMetering(); uint256 idCommitment = 2; LinearPriceCalculator priceCalculator = LinearPriceCalculator(address(w.priceCalculator())); - vm.assume(numberOfPeriods > 0 && numberOfPeriods < 100); vm.prank(priceCalculator.owner()); priceCalculator.setTokenAndPrice(address(token), 5 wei); - (, uint256 price) = w.priceCalculator().calculate(userMessageLimit, numberOfPeriods); + (, uint256 price) = w.priceCalculator().calculate(userMessageLimit); token.mint(address(this), price); vm.assume( userMessageLimit >= w.minRateLimitPerMembership() && userMessageLimit <= w.maxRateLimitPerMembership() @@ -827,9 +805,9 @@ contract WakuRlnV2Test is Test { vm.resumeGasMetering(); token.approve(address(w), price); - w.register(idCommitment, userMessageLimit, numberOfPeriods); + w.register(idCommitment, userMessageLimit); - (,,,, uint256 gracePeriodStartDate,,,,,) = w.members(idCommitment); + (,,, uint256 gracePeriodStartDate,,,,,) = w.members(idCommitment); vm.warp(gracePeriodStartDate); @@ -858,19 +836,18 @@ contract WakuRlnV2Test is Test { vm.pauseGasMetering(); uint256 idCommitment = 2; uint32 userMessageLimit = w.minRateLimitPerMembership(); - uint32 numberOfPeriods = 2; - (, uint256 price) = w.priceCalculator().calculate(userMessageLimit, numberOfPeriods); + (, uint256 price) = w.priceCalculator().calculate(userMessageLimit); vm.resumeGasMetering(); - w.register{ value: price }(idCommitment, userMessageLimit, numberOfPeriods); + w.register{ value: price }(idCommitment, userMessageLimit); vm.expectRevert(DuplicateIdCommitment.selector); - w.register{ value: price }(idCommitment, userMessageLimit, numberOfPeriods); + w.register{ value: price }(idCommitment, userMessageLimit); } function test__InvalidRegistration__FullTree() external { vm.pauseGasMetering(); uint32 userMessageLimit = 20; - (, uint256 price) = w.priceCalculator().calculate(userMessageLimit, 1); + (, uint256 price) = w.priceCalculator().calculate(userMessageLimit); vm.resumeGasMetering(); // we progress the tree to the last leaf @@ -898,7 +875,7 @@ contract WakuRlnV2Test is Test { // we set commitmentIndex to 4294967295 (1 << 20) = 0x00100000 vm.store(address(w), bytes32(uint256(206)), 0x0000000000000000000000000000000000000000000000000000000000100000); vm.expectRevert(FullTree.selector); - w.register{ value: price }(1, userMessageLimit, 1); + w.register{ value: price }(1, userMessageLimit); } function test__InvalidPaginationQuery__StartIndexGTEndIndex() external { @@ -915,11 +892,10 @@ contract WakuRlnV2Test is Test { vm.pauseGasMetering(); uint256 idCommitment = 1; uint32 userMessageLimit = w.minRateLimitPerMembership(); - uint32 numberOfPeriods = 2; - (, uint256 price) = w.priceCalculator().calculate(userMessageLimit, numberOfPeriods); + (, uint256 price) = w.priceCalculator().calculate(userMessageLimit); vm.resumeGasMetering(); - w.register{ value: price }(idCommitment, userMessageLimit, numberOfPeriods); + w.register{ value: price }(idCommitment, userMessageLimit); uint256[] memory commitments = w.getCommitments(0, 0); assertEq(commitments.length, 1); uint256 rateCommitment = PoseidonT3.hash([idCommitment, userMessageLimit]); @@ -930,11 +906,10 @@ contract WakuRlnV2Test is Test { vm.pauseGasMetering(); vm.assume(idCommitmentsLength > 0 && idCommitmentsLength <= 100); uint32 userMessageLimit = w.minRateLimitPerMembership(); - uint32 numberOfPeriods = 2; - (, uint256 price) = w.priceCalculator().calculate(userMessageLimit, numberOfPeriods); + (, uint256 price) = w.priceCalculator().calculate(userMessageLimit); for (uint256 i = 0; i < idCommitmentsLength; i++) { - w.register{ value: price }(i + 1, userMessageLimit, numberOfPeriods); + w.register{ value: price }(i + 1, userMessageLimit); } vm.resumeGasMetering(); From 7f60c538b320a50e12bf0c47fd3ccf0fc81e659e Mon Sep 17 00:00:00 2001 From: Richard Ramos Date: Mon, 9 Sep 2024 14:58:28 -0400 Subject: [PATCH 11/21] fix: remove ETH support --- script/Deploy.s.sol | 4 +- src/LinearPriceCalculator.sol | 2 + src/Membership.sol | 23 ++-- test/WakuRlnV2.t.sol | 198 ++++++++++++++++------------------ 4 files changed, 100 insertions(+), 127 deletions(-) diff --git a/script/Deploy.s.sol b/script/Deploy.s.sol index 2e0f354..10c778a 100644 --- a/script/Deploy.s.sol +++ b/script/Deploy.s.sol @@ -11,9 +11,9 @@ import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils import { BaseScript } from "./Base.s.sol"; contract Deploy is BaseScript { - function run() public broadcast returns (WakuRlnV2 w, address impl) { + function run(address _token) public broadcast returns (WakuRlnV2 w, address impl) { // TODO: Use the correct values when deploying to mainnet - address priceCalcAddr = address(new LinearPriceCalculator(address(0), 0.05 ether)); + address priceCalcAddr = address(new LinearPriceCalculator(_token, 0.05 ether)); // TODO: set DAI address 0x6B175474E89094C44Da98b954EedeAC495271d0F impl = address(new WakuRlnV2()); bytes memory data = abi.encodeCall(WakuRlnV2.initialize, (priceCalcAddr, 160_000, 20, 600, 180 days, 30 days)); diff --git a/src/LinearPriceCalculator.sol b/src/LinearPriceCalculator.sol index 14cdbb7..d0165af 100644 --- a/src/LinearPriceCalculator.sol +++ b/src/LinearPriceCalculator.sol @@ -13,6 +13,7 @@ contract LinearPriceCalculator is IPriceCalculator, Ownable { uint256 public pricePerMessagePerEpoch; constructor(address _token, uint256 _pricePerMessagePerEpoch) Ownable() { + require(_token != address(0), "only tokens can be used"); token = _token; pricePerMessagePerEpoch = _pricePerMessagePerEpoch; } @@ -21,6 +22,7 @@ contract LinearPriceCalculator is IPriceCalculator, Ownable { /// @param _token The token accepted by the membership management for RLN /// @param _pricePerPeriod Price per message per epoch function setTokenAndPrice(address _token, uint256 _pricePerPeriod) external onlyOwner { + require(_token != address(0), "only tokens can be used"); token = _token; pricePerMessagePerEpoch = _pricePerPeriod; } diff --git a/src/Membership.sol b/src/Membership.sol index a6bfc1a..ea558b6 100644 --- a/src/Membership.sol +++ b/src/Membership.sol @@ -5,9 +5,6 @@ import { IPriceCalculator } from "./IPriceCalculator.sol"; import { IERC20 } from "openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; import { SafeERC20 } from "openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol"; -// ETH Amount passed as value on the transaction is not correct -error IncorrectAmount(); - // An eth value was assigned in the transaction and only tokens were expected error OnlyTokensAccepted(); @@ -157,12 +154,8 @@ contract Membership { } function _transferFees(address _from, address _token, uint256 _amount) internal { - if (_token == address(0)) { - if (msg.value != _amount) revert IncorrectAmount(); - } else { - if (msg.value != 0) revert OnlyTokensAccepted(); - IERC20(_token).safeTransferFrom(_from, address(this), _amount); - } + if (msg.value != 0) revert OnlyTokensAccepted(); + IERC20(_token).safeTransferFrom(_from, address(this), _amount); } /// @dev Setup a new membership. If there are not enough remaining rate limit to acquire @@ -406,18 +399,14 @@ contract Membership { /// @dev Withdraw any available balance in tokens after a membership is erased. /// @param _sender the address of the owner of the tokens - /// @param _token the address of the token to withdraw. Use 0x000...000 to withdraw ETH + /// @param _token the address of the token to withdraw. function _withdraw(address _sender, address _token) internal { + require(_token != address(0), "ETH is not allowed"); + uint256 amount = balancesToWithdraw[_sender][_token]; require(amount > 0, "Insufficient balance"); balancesToWithdraw[_sender][_token] = 0; - if (_token == address(0)) { - // ETH - (bool success,) = _sender.call{ value: amount }(""); - require(success, "eth transfer failed"); - } else { - IERC20(_token).safeTransfer(_sender, amount); - } + IERC20(_token).safeTransfer(_sender, amount); } } diff --git a/test/WakuRlnV2.t.sol b/test/WakuRlnV2.t.sol index d57851e..0a029d8 100644 --- a/test/WakuRlnV2.t.sol +++ b/test/WakuRlnV2.t.sol @@ -23,12 +23,17 @@ contract WakuRlnV2Test is Test { address internal deployer; function setUp() public virtual { + token = new TestToken(); + Deploy deployment = new Deploy(); - (w, impl) = deployment.run(); + (w, impl) = deployment.run(address(token)); - token = new TestToken(); + // Minting a large number of tokens to not have to worry about + // Not having enough balance + token.mint(address(this), 100_000_000 ether); } + function test__ValidRegistration__kats() external { vm.pauseGasMetering(); // Merkle tree leaves are calculated using 2 as rateLimit @@ -39,7 +44,8 @@ contract WakuRlnV2Test is Test { uint32 userMessageLimit = 2; (, uint256 price) = w.priceCalculator().calculate(userMessageLimit); vm.resumeGasMetering(); - w.register{ value: price }(idCommitment, userMessageLimit); + token.approve(address(w), price); + w.register(idCommitment, userMessageLimit); vm.pauseGasMetering(); assertEq(w.commitmentIndex(), 1); assertEq(w.memberExists(idCommitment), true); @@ -99,7 +105,8 @@ contract WakuRlnV2Test is Test { vm.resumeGasMetering(); assertEq(w.memberExists(idCommitment), false); - w.register{ value: price }(idCommitment, userMessageLimit); + token.approve(address(w), price); + w.register(idCommitment, userMessageLimit); uint256 rateCommitment = PoseidonT3.hash([idCommitment, userMessageLimit]); (uint32 fetchedUserMessageLimit, uint32 index, uint256 fetchedRateCommitment) = @@ -108,7 +115,7 @@ contract WakuRlnV2Test is Test { assertEq(index, 0); assertEq(fetchedRateCommitment, rateCommitment); - assertEq(address(w).balance, price); + assertEq(token.balanceOf(address(w)), price); assertEq(w.totalRateLimitPerEpoch(), userMessageLimit); } @@ -121,7 +128,8 @@ contract WakuRlnV2Test is Test { // Register some commitments for (uint256 i = 0; i < idCommitmentsLength; i++) { uint256 idCommitment = i + 1; - w.register{ value: price }(idCommitment, userMessageLimit); + token.approve(address(w), price); + w.register(idCommitment, userMessageLimit); (uint256 prev, uint256 next,,,,,,,) = w.members(idCommitment); // new membership will always be the tail assertEq(next, 0); @@ -155,26 +163,7 @@ contract WakuRlnV2Test is Test { assertEq(price, expectedPrice); } - function test__RegistrationWithTokens(uint256 idCommitment, uint32 userMessageLimit) external { - vm.pauseGasMetering(); - LinearPriceCalculator priceCalculator = LinearPriceCalculator(address(w.priceCalculator())); - vm.prank(priceCalculator.owner()); - priceCalculator.setTokenAndPrice(address(token), 5 wei); - (, uint256 price) = w.priceCalculator().calculate(userMessageLimit); - uint256 minUserMessageLimit = w.minRateLimitPerMembership(); - uint256 maxUserMessageLimit = w.maxRateLimitPerMembership(); - vm.assume(userMessageLimit >= minUserMessageLimit && userMessageLimit <= maxUserMessageLimit); - vm.assume(w.isValidCommitment(idCommitment) && w.isValidUserMessageLimit(userMessageLimit)); - vm.resumeGasMetering(); - - token.mint(address(this), price); - token.approve(address(w), price); - w.register(idCommitment, userMessageLimit); - assertEq(token.balanceOf(address(w)), price); - assertEq(token.balanceOf(address(this)), 0); - } - - function test__InvalidETHAmount(uint256 idCommitment, uint32 userMessageLimit) external { + function test__InvalidTokenAmount(uint256 idCommitment, uint32 userMessageLimit) external { vm.pauseGasMetering(); uint256 minUserMessageLimit = w.minRateLimitPerMembership(); uint256 maxUserMessageLimit = w.maxRateLimitPerMembership(); @@ -183,10 +172,9 @@ contract WakuRlnV2Test is Test { (, uint256 price) = w.priceCalculator().calculate(userMessageLimit); vm.resumeGasMetering(); - vm.expectRevert(abi.encodeWithSelector(IncorrectAmount.selector)); - w.register{ value: price - 1 }(idCommitment, userMessageLimit); - vm.expectRevert(abi.encodeWithSelector(IncorrectAmount.selector)); - w.register{ value: price + 1 }(idCommitment, userMessageLimit); + token.approve(address(w), price - 1); + vm.expectRevert(bytes("ERC20: insufficient allowance")); + w.register(idCommitment, userMessageLimit); } function test__IdCommitmentToMetadata__DoesntExist() external view { @@ -204,8 +192,9 @@ contract WakuRlnV2Test is Test { (, uint256 price) = w.priceCalculator().calculate(userMessageLimit); vm.resumeGasMetering(); + token.approve(address(w), price); vm.expectRevert(abi.encodeWithSelector(InvalidIdCommitment.selector, 0)); - w.register{ value: price }(idCommitment, userMessageLimit); + w.register(idCommitment, userMessageLimit); } function test__InvalidRegistration__InvalidIdCommitment__LargerThanField() external { @@ -215,8 +204,9 @@ contract WakuRlnV2Test is Test { vm.resumeGasMetering(); uint256 idCommitment = w.Q() + 1; + token.approve(address(w), price); vm.expectRevert(abi.encodeWithSelector(InvalidIdCommitment.selector, idCommitment)); - w.register{ value: price }(idCommitment, userMessageLimit); + w.register(idCommitment, userMessageLimit); } function test__InvalidRegistration__InvalidUserMessageLimit__MinMax() external { @@ -242,7 +232,8 @@ contract WakuRlnV2Test is Test { vm.assume(w.isValidUserMessageLimit(userMessageLimit)); vm.resumeGasMetering(); - w.register{ value: price }(idCommitment, userMessageLimit); + token.approve(address(w), price); + w.register(idCommitment, userMessageLimit); (,,, uint256 gracePeriodStartDate,,,,,) = w.members(idCommitment); assertFalse(w.isGracePeriod(idCommitment)); @@ -255,7 +246,8 @@ contract WakuRlnV2Test is Test { // Registering other memberships just to check linkage is correct for (uint256 i = 1; i < 5; i++) { - w.register{ value: price }(idCommitment + i, userMessageLimit); + token.approve(address(w), price); + w.register(idCommitment + i, userMessageLimit); } assertEq(w.head(), idCommitment); @@ -307,8 +299,6 @@ contract WakuRlnV2Test is Test { } } - // TODO: should it be possible to extend expired memberships? - // Attempt to extend a non grace period membership commitmentsToExtend[0] = idCommitment + 1; vm.expectRevert(abi.encodeWithSelector(NotInGracePeriod.selector, commitmentsToExtend[0])); @@ -325,7 +315,8 @@ contract WakuRlnV2Test is Test { vm.assume(w.isValidUserMessageLimit(userMessageLimit)); vm.resumeGasMetering(); - w.register{ value: price }(idCommitment, userMessageLimit); +token.approve(address(w), price); + w.register(idCommitment, userMessageLimit); (,,, uint256 gracePeriodStartDate,,,,,) = w.members(idCommitment); vm.warp(gracePeriodStartDate); @@ -356,7 +347,8 @@ contract WakuRlnV2Test is Test { vm.assume(w.isValidUserMessageLimit(userMessageLimit)); vm.resumeGasMetering(); - w.register{ value: price }(idCommitment, userMessageLimit); +token.approve(address(w), price); + w.register(idCommitment, userMessageLimit); (,,, uint256 fetchedGracePeriodStartDate, uint32 fetchedGracePeriod,,,,) = w.members(idCommitment); @@ -372,7 +364,8 @@ contract WakuRlnV2Test is Test { // Registering other memberships just to check linkage is correct for (uint256 i = 1; i <= 5; i++) { - w.register{ value: price }(idCommitment + i, userMessageLimit); + token.approve(address(w), price); + w.register(idCommitment + i, userMessageLimit); } assertEq(w.head(), idCommitment); @@ -394,30 +387,35 @@ contract WakuRlnV2Test is Test { // Exceeds the max rate limit per user uint32 userMessageLimit = 10; (, uint256 price) = w.priceCalculator().calculate(userMessageLimit); + token.approve(address(w), price); vm.expectRevert(abi.encodeWithSelector(InvalidRateLimit.selector)); - w.register{ value: price }(1, userMessageLimit); + w.register(1, userMessageLimit); // Should register succesfully userMessageLimit = 4; (, price) = w.priceCalculator().calculate(userMessageLimit); - w.register{ value: price }(2, userMessageLimit); + token.approve(address(w), price); + w.register(2, userMessageLimit); // Exceeds the rate limit userMessageLimit = 2; (, price) = w.priceCalculator().calculate(userMessageLimit); + token.approve(address(w), price); vm.expectRevert(abi.encodeWithSelector(ExceedAvailableMaxRateLimitPerEpoch.selector)); - w.register{ value: price }(3, userMessageLimit); + w.register(3, userMessageLimit); // Should register succesfully userMessageLimit = 1; (, price) = w.priceCalculator().calculate(userMessageLimit); - w.register{ value: price }(3, userMessageLimit); + token.approve(address(w), price); + w.register(3, userMessageLimit); // We ran out of rate limit again userMessageLimit = 1; (, price) = w.priceCalculator().calculate(userMessageLimit); + token.approve(address(w), price); vm.expectRevert(abi.encodeWithSelector(ExceedAvailableMaxRateLimitPerEpoch.selector)); - w.register{ value: price }(4, userMessageLimit); + w.register(4, userMessageLimit); } function test__RegistrationWhenMaxRateLimitIsReachedAndSingleExpiredMemberAvailable() external { @@ -432,7 +430,8 @@ contract WakuRlnV2Test is Test { uint32 userMessageLimitA = 2; uint32 totalUserMessageLimit = userMessageLimitA; (, uint256 priceA) = w.priceCalculator().calculate(userMessageLimitA); - w.register{ value: priceA }(1, userMessageLimitA); + token.approve(address(w), priceA); + w.register(1, userMessageLimitA); (,,, uint256 gracePeriodStartDate,,, uint32 indexA,,) = w.members(1); vm.warp(gracePeriodStartDate + 1); @@ -444,8 +443,9 @@ contract WakuRlnV2Test is Test { uint32 userMessageLimitB = 4; (, uint256 priceB) = w.priceCalculator().calculate(userMessageLimitB); (, priceB) = w.priceCalculator().calculate(userMessageLimitB); + token.approve(address(w), priceB); vm.expectRevert(abi.encodeWithSelector(ExceedAvailableMaxRateLimitPerEpoch.selector)); - w.register{ value: priceB }(2, userMessageLimitB); + w.register(2, userMessageLimitB); // FFW until the membership is expired so we can get rid of it uint256 expirationDate = w.expirationDate(1); @@ -455,7 +455,7 @@ contract WakuRlnV2Test is Test { // It should succeed now vm.expectEmit(); emit Membership.MemberExpired(1, userMessageLimitA, indexA); - w.register{ value: priceB }(2, userMessageLimitB); + w.register(2, userMessageLimitB); // The previous expired membership should have been erased (,,,,,,, address holder,) = w.members(1); @@ -475,7 +475,7 @@ contract WakuRlnV2Test is Test { assertEq(indexA, indexB); // The balance available for withdrawal should match the amount of the expired membership - uint256 availableBalance = w.balancesToWithdraw(address(this), address(0)); + uint256 availableBalance = w.balancesToWithdraw(address(this), address(token)); assertEq(availableBalance, priceA); } @@ -489,13 +489,16 @@ contract WakuRlnV2Test is Test { vm.resumeGasMetering(); (, uint256 priceA) = w.priceCalculator().calculate(1); - w.register{ value: priceA }(1, 1); + token.approve(address(w), priceA); + w.register(1, 1); vm.warp(block.timestamp + 100); - w.register{ value: priceA }(2, 1); + token.approve(address(w), priceA); + w.register(2, 1); vm.warp(block.timestamp + 100); uint256 expirationDate = w.expirationDate(2); vm.warp(expirationDate); - w.register{ value: priceA }(3, 1); + token.approve(address(w), priceA); + w.register(3, 1); // Make sure only the first 2 memberships are expired assertTrue(w.isExpired(1)); @@ -509,12 +512,13 @@ contract WakuRlnV2Test is Test { // Currently there is 2 available, and we want to register 4 // If we remove first membership, we'll have 3 available // If we also remove the second, we'll have 4 available - vm.expectEmit(true, false, false, false); + (, uint256 priceB) = w.priceCalculator().calculate(4); + token.approve(address(w), priceB); + vm.expectEmit(true, false, false, false); emit Membership.MemberExpired(1, 0, 0); vm.expectEmit(true, false, false, false); emit Membership.MemberExpired(2, 0, 0); - (, uint256 priceB) = w.priceCalculator().calculate(4); - w.register{ value: priceB }(4, 4); + w.register(4, 4); // idCommitment4 will use the last removed index available (since we push to an array) (,,,,,, uint32 index4,,) = w.members(4); @@ -543,7 +547,7 @@ contract WakuRlnV2Test is Test { assertEq(next, 0); // The balance available for withdrawal should match the amount of the expired membership - uint256 availableBalance = w.balancesToWithdraw(address(this), address(0)); + uint256 availableBalance = w.balancesToWithdraw(address(this), address(token)); assertEq(availableBalance, priceA * 2); } @@ -559,13 +563,16 @@ contract WakuRlnV2Test is Test { vm.resumeGasMetering(); (, uint256 priceA) = w.priceCalculator().calculate(1); - w.register{ value: priceA }(1, 1); + token.approve(address(w), priceA); + w.register(1, 1); vm.warp(block.timestamp + 100); - w.register{ value: priceA }(2, 1); + token.approve(address(w), priceA); + w.register(2, 1); vm.warp(block.timestamp + 100); uint256 expirationDate = w.expirationDate(2); vm.warp(expirationDate); - w.register{ value: priceA }(3, 1); + token.approve(address(w), priceA); + w.register(3, 1); // Make sure only the first 2 memberships are expired assertTrue(w.isExpired(1)); @@ -578,8 +585,9 @@ contract WakuRlnV2Test is Test { // If we also remove the second, we'll have 4 available, but it is still not enough // for registering (, uint256 priceB) = w.priceCalculator().calculate(5); + token.approve(address(w), priceB); vm.expectRevert(abi.encodeWithSelector(ExceedAvailableMaxRateLimitPerEpoch.selector)); - w.register{ value: priceB }(4, 5); + w.register(4, 5); } function test__indexReuse_eraseMemberships(uint32 idCommitmentsLength) external { @@ -589,7 +597,8 @@ contract WakuRlnV2Test is Test { uint32 index; uint256[] memory commitmentsToErase = new uint256[](idCommitmentsLength); for (uint256 i = 1; i <= idCommitmentsLength; i++) { - w.register{ value: price }(i, 20); + token.approve(address(w), price); + w.register(i, 20); (,,,,,, index,,) = w.members(i); assertEq(index, w.commitmentIndex() - 1); // TODO: renname commitmentIndex to nextCommitmentIndex commitmentsToErase[i - 1] = i; @@ -610,7 +619,8 @@ contract WakuRlnV2Test is Test { uint256 idCommitment = i + 10; uint256 expectedReusedIndexPos = idCommitmentsLength - i; uint32 expectedIndex = w.availableExpiredIndices(expectedReusedIndexPos); - w.register{ value: price }(idCommitment, 20); + token.approve(address(w), price); + w.register(idCommitment, 20); (,,,,,, index,,) = w.members(idCommitment); assertEq(expectedIndex, index); // Should have been removed from the list @@ -625,7 +635,8 @@ contract WakuRlnV2Test is Test { w.availableExpiredIndices(0); // Should use a new index since we got rid of all available indexes - w.register{ value: price }(100, 20); + token.approve(address(w), price); + w.register(100, 20); (,,,,,, index,,) = w.members(100); assertEq(index, currCommitmentIndex); assertEq(currCommitmentIndex + 1, w.commitmentIndex()); @@ -643,7 +654,8 @@ contract WakuRlnV2Test is Test { uint256 time = block.timestamp; for (uint256 i = 0; i < 5; i++) { - w.register{ value: price }(idCommitment + i, userMessageLimit); + token.approve(address(w), price); + w.register(idCommitment + i, userMessageLimit); time += 100; vm.warp(time); } @@ -711,7 +723,8 @@ contract WakuRlnV2Test is Test { uint256 time = block.timestamp; for (uint256 i = 1; i <= idCommitmentsLength; i++) { - w.register{ value: price }(i, userMessageLimit); + token.approve(address(w), price); + w.register(i, userMessageLimit); time += 100; vm.warp(time); } @@ -736,7 +749,8 @@ contract WakuRlnV2Test is Test { assertEq(w.tail(), 0); for (uint256 i = 10; i <= idCommitmentsLength + 10; i++) { - w.register{ value: price }(i, userMessageLimit); + token.approve(address(w), price); + w.register(i, userMessageLimit); assertEq(w.tail(), i); } @@ -753,43 +767,6 @@ contract WakuRlnV2Test is Test { assertEq(next, 0); } - function test__WithdrawETH(uint32 userMessageLimit) external { - vm.pauseGasMetering(); - uint256 idCommitment = 2; - (, uint256 price) = w.priceCalculator().calculate(userMessageLimit); - vm.assume( - userMessageLimit >= w.minRateLimitPerMembership() && userMessageLimit <= w.maxRateLimitPerMembership() - ); - vm.assume(w.isValidUserMessageLimit(userMessageLimit)); - vm.resumeGasMetering(); - - w.register{ value: price }(idCommitment, userMessageLimit); - - (,,, uint256 gracePeriodStartDate,,,,,) = w.members(idCommitment); - - vm.warp(gracePeriodStartDate); - - uint256[] memory commitmentsToErase = new uint256[](1); - commitmentsToErase[0] = idCommitment; - w.eraseMemberships(commitmentsToErase); - - uint256 availableBalance = w.balancesToWithdraw(address(this), address(0)); - - assertEq(availableBalance, price); - assertEq(address(w).balance, price); - - uint256 balanceBeforeWithdraw = address(this).balance; - - w.withdraw(address(0)); - - uint256 balanceAfterWithdraw = address(this).balance; - - availableBalance = w.balancesToWithdraw(address(this), address(0)); - assertEq(availableBalance, 0); - assertEq(address(w).balance, 0); - assertEq(balanceBeforeWithdraw + price, balanceAfterWithdraw); - } - function test__WithdrawToken(uint32 userMessageLimit) external { vm.pauseGasMetering(); uint256 idCommitment = 2; @@ -839,9 +816,12 @@ contract WakuRlnV2Test is Test { (, uint256 price) = w.priceCalculator().calculate(userMessageLimit); vm.resumeGasMetering(); - w.register{ value: price }(idCommitment, userMessageLimit); + token.approve(address(w), price); + w.register(idCommitment, userMessageLimit); + + token.approve(address(w), price); vm.expectRevert(DuplicateIdCommitment.selector); - w.register{ value: price }(idCommitment, userMessageLimit); + w.register(idCommitment, userMessageLimit); } function test__InvalidRegistration__FullTree() external { @@ -874,8 +854,9 @@ contract WakuRlnV2Test is Test { // we set commitmentIndex to 4294967295 (1 << 20) = 0x00100000 vm.store(address(w), bytes32(uint256(206)), 0x0000000000000000000000000000000000000000000000000000000000100000); + token.approve(address(w), price); vm.expectRevert(FullTree.selector); - w.register{ value: price }(1, userMessageLimit); + w.register(1, userMessageLimit); } function test__InvalidPaginationQuery__StartIndexGTEndIndex() external { @@ -895,7 +876,8 @@ contract WakuRlnV2Test is Test { (, uint256 price) = w.priceCalculator().calculate(userMessageLimit); vm.resumeGasMetering(); - w.register{ value: price }(idCommitment, userMessageLimit); +token.approve(address(w), price); + w.register(idCommitment, userMessageLimit); uint256[] memory commitments = w.getCommitments(0, 0); assertEq(commitments.length, 1); uint256 rateCommitment = PoseidonT3.hash([idCommitment, userMessageLimit]); @@ -909,7 +891,8 @@ contract WakuRlnV2Test is Test { (, uint256 price) = w.priceCalculator().calculate(userMessageLimit); for (uint256 i = 0; i < idCommitmentsLength; i++) { - w.register{ value: price }(i + 1, userMessageLimit); + token.approve(address(w), price); + w.register(i + 1, userMessageLimit); } vm.resumeGasMetering(); @@ -940,5 +923,4 @@ contract WakuRlnV2Test is Test { assertEq(fetchedImpl, newImpl); } - receive() external payable { } // solhint-disable-line } From 6ba9a5b6d83b0007ef9677cf99d45f2c26d22694 Mon Sep 17 00:00:00 2001 From: Richard Ramos Date: Mon, 9 Sep 2024 15:11:05 -0400 Subject: [PATCH 12/21] fix: lint --- test/WakuRlnV2.t.sol | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/test/WakuRlnV2.t.sol b/test/WakuRlnV2.t.sol index 0a029d8..5693263 100644 --- a/test/WakuRlnV2.t.sol +++ b/test/WakuRlnV2.t.sol @@ -33,7 +33,6 @@ contract WakuRlnV2Test is Test { token.mint(address(this), 100_000_000 ether); } - function test__ValidRegistration__kats() external { vm.pauseGasMetering(); // Merkle tree leaves are calculated using 2 as rateLimit @@ -315,7 +314,7 @@ contract WakuRlnV2Test is Test { vm.assume(w.isValidUserMessageLimit(userMessageLimit)); vm.resumeGasMetering(); -token.approve(address(w), price); + token.approve(address(w), price); w.register(idCommitment, userMessageLimit); (,,, uint256 gracePeriodStartDate,,,,,) = w.members(idCommitment); @@ -347,7 +346,7 @@ token.approve(address(w), price); vm.assume(w.isValidUserMessageLimit(userMessageLimit)); vm.resumeGasMetering(); -token.approve(address(w), price); + token.approve(address(w), price); w.register(idCommitment, userMessageLimit); (,,, uint256 fetchedGracePeriodStartDate, uint32 fetchedGracePeriod,,,,) = w.members(idCommitment); @@ -514,7 +513,7 @@ token.approve(address(w), price); // If we also remove the second, we'll have 4 available (, uint256 priceB) = w.priceCalculator().calculate(4); token.approve(address(w), priceB); - vm.expectEmit(true, false, false, false); + vm.expectEmit(true, false, false, false); emit Membership.MemberExpired(1, 0, 0); vm.expectEmit(true, false, false, false); emit Membership.MemberExpired(2, 0, 0); @@ -566,12 +565,12 @@ token.approve(address(w), price); token.approve(address(w), priceA); w.register(1, 1); vm.warp(block.timestamp + 100); - token.approve(address(w), priceA); + token.approve(address(w), priceA); w.register(2, 1); vm.warp(block.timestamp + 100); uint256 expirationDate = w.expirationDate(2); vm.warp(expirationDate); - token.approve(address(w), priceA); + token.approve(address(w), priceA); w.register(3, 1); // Make sure only the first 2 memberships are expired @@ -585,7 +584,7 @@ token.approve(address(w), price); // If we also remove the second, we'll have 4 available, but it is still not enough // for registering (, uint256 priceB) = w.priceCalculator().calculate(5); - token.approve(address(w), priceB); + token.approve(address(w), priceB); vm.expectRevert(abi.encodeWithSelector(ExceedAvailableMaxRateLimitPerEpoch.selector)); w.register(4, 5); } @@ -635,7 +634,7 @@ token.approve(address(w), price); w.availableExpiredIndices(0); // Should use a new index since we got rid of all available indexes - token.approve(address(w), price); + token.approve(address(w), price); w.register(100, 20); (,,,,,, index,,) = w.members(100); assertEq(index, currCommitmentIndex); @@ -654,7 +653,7 @@ token.approve(address(w), price); uint256 time = block.timestamp; for (uint256 i = 0; i < 5; i++) { - token.approve(address(w), price); + token.approve(address(w), price); w.register(idCommitment + i, userMessageLimit); time += 100; vm.warp(time); @@ -816,7 +815,7 @@ token.approve(address(w), price); (, uint256 price) = w.priceCalculator().calculate(userMessageLimit); vm.resumeGasMetering(); - token.approve(address(w), price); + token.approve(address(w), price); w.register(idCommitment, userMessageLimit); token.approve(address(w), price); @@ -876,7 +875,7 @@ token.approve(address(w), price); (, uint256 price) = w.priceCalculator().calculate(userMessageLimit); vm.resumeGasMetering(); -token.approve(address(w), price); + token.approve(address(w), price); w.register(idCommitment, userMessageLimit); uint256[] memory commitments = w.getCommitments(0, 0); assertEq(commitments.length, 1); @@ -922,5 +921,4 @@ token.approve(address(w), price); ); assertEq(fetchedImpl, newImpl); } - } From c66f72897b0402d25d3f85737a11130692ef2b61 Mon Sep 17 00:00:00 2001 From: Richard Ramos Date: Mon, 9 Sep 2024 16:47:07 -0400 Subject: [PATCH 13/21] fix: type for rate limit in price calculator --- src/IPriceCalculator.sol | 2 +- src/LinearPriceCalculator.sol | 2 +- src/WakuRlnV2.sol | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/IPriceCalculator.sol b/src/IPriceCalculator.sol index ff2700e..0cdf38e 100644 --- a/src/IPriceCalculator.sol +++ b/src/IPriceCalculator.sol @@ -6,5 +6,5 @@ interface IPriceCalculator { /// @param _rateLimit the rate limit the user wants to acquire /// @return address of the erc20 token /// @return uint price to pay for acquiring the specified `_rateLimit` - function calculate(uint256 _rateLimit) external view returns (address, uint256); + function calculate(uint32 _rateLimit) external view returns (address, uint256); } diff --git a/src/LinearPriceCalculator.sol b/src/LinearPriceCalculator.sol index d0165af..8b54f95 100644 --- a/src/LinearPriceCalculator.sol +++ b/src/LinearPriceCalculator.sol @@ -31,7 +31,7 @@ contract LinearPriceCalculator is IPriceCalculator, Ownable { /// @param _rateLimit the rate limit the user wants to acquire /// @return address of the erc20 token /// @return uint price to pay for acquiring the specified `_rateLimit` - function calculate(uint256 _rateLimit) external view returns (address, uint256) { + function calculate(uint32 _rateLimit) external view returns (address, uint256) { return (token, uint256(_rateLimit) * pricePerMessagePerEpoch); } } diff --git a/src/WakuRlnV2.sol b/src/WakuRlnV2.sol index e9a710e..75e53aa 100644 --- a/src/WakuRlnV2.sol +++ b/src/WakuRlnV2.sol @@ -69,8 +69,8 @@ contract WakuRlnV2 is Initializable, OwnableUpgradeable, UUPSUpgradeable, Member function initialize( address _priceCalculator, uint32 _maxTotalRateLimitPerEpoch, - uint16 _minRateLimitPerMembership, - uint16 _maxRateLimitPerMembership, + uint32 _minRateLimitPerMembership, + uint32 _maxRateLimitPerMembership, uint32 _billingPeriod, uint32 _gracePeriod ) From 12e837a1e270a27fcb76cfa239ceea6448c3c371 Mon Sep 17 00:00:00 2001 From: Richard Ramos Date: Mon, 9 Sep 2024 17:54:29 -0400 Subject: [PATCH 14/21] feat: allow choosing automatic membership erases and specific membership removal on register --- src/Membership.sol | 14 ++++++++++---- src/WakuRlnV2.sol | 27 ++++++++++++++++++++++++--- 2 files changed, 34 insertions(+), 7 deletions(-) diff --git a/src/Membership.sol b/src/Membership.sol index ea558b6..1fad7a1 100644 --- a/src/Membership.sol +++ b/src/Membership.sol @@ -138,18 +138,21 @@ contract Membership { /// @param _sender address of the owner of the new membership /// @param _idCommitment the idcommitment of the new membership /// @param _rateLimit the user message limit + /// @param _eraseIfNeeded Erase expired memberships if the `_rateLimit` exceeds the available rate limit /// @return index the index in the merkle tree /// @return reusedIndex indicates whether a new leaf is being used or if using an existing leaf in the merkle tree function _acquireMembership( address _sender, uint256 _idCommitment, - uint32 _rateLimit + uint32 _rateLimit, + bool _eraseIfNeeded ) internal returns (uint32 index, bool reusedIndex) { (address token, uint256 amount) = priceCalculator.calculate(_rateLimit); - (index, reusedIndex) = _setupMembershipDetails(_sender, _idCommitment, _rateLimit, token, amount); + (index, reusedIndex) = + _setupMembershipDetails(_sender, _idCommitment, _rateLimit, token, amount, _eraseIfNeeded); _transferFees(_sender, token, amount); } @@ -166,6 +169,7 @@ contract Membership { /// @param _rateLimit User message limit /// @param _token Address of the token used to acquire the membership /// @param _amount Amount of the token used to acquire the membership + /// @param _eraseIfNeeded Erase expired memberships if the `_rateLimit` exceeds the available rate limit /// @return index membership index on the merkle tree /// @return reusedIndex indicates whether the index returned was a reused slot on the tree or not function _setupMembershipDetails( @@ -173,7 +177,8 @@ contract Membership { uint256 _idCommitment, uint32 _rateLimit, address _token, - uint256 _amount + uint256 _amount, + bool _eraseIfNeeded ) internal returns (uint32 index, bool reusedIndex) @@ -191,7 +196,8 @@ contract Membership { // Determine if we exceed the total rate limit if (_totalRateLimitPerEpoch + _rateLimit > _maxTotalRateLimitPerEpoch) { - if (_head == 0) revert ExceedAvailableMaxRateLimitPerEpoch(); // List is empty + if (_head == 0 || !_eraseIfNeeded) revert ExceedAvailableMaxRateLimitPerEpoch(); // List is empty or can't + // erase memberships automatically // Attempt to free expired membership slots while (_totalRateLimitPerEpoch + _rateLimit > _maxTotalRateLimitPerEpoch && _head != 0) { diff --git a/src/WakuRlnV2.sol b/src/WakuRlnV2.sol index 75e53aa..01504df 100644 --- a/src/WakuRlnV2.sol +++ b/src/WakuRlnV2.sol @@ -138,19 +138,40 @@ contract WakuRlnV2 is Initializable, OwnableUpgradeable, UUPSUpgradeable, Member /// @notice Allows a user to register as a member /// @param idCommitment The idCommitment of the member /// @param userMessageLimit The message limit of the member + function register(uint256 idCommitment, uint32 userMessageLimit) external onlyValidIdCommitment(idCommitment) { + if (memberExists(idCommitment)) revert DuplicateIdCommitment(); + + uint32 index; + bool reusedIndex; + (index, reusedIndex) = _acquireMembership(_msgSender(), idCommitment, userMessageLimit, true); + + _register(idCommitment, userMessageLimit, index, reusedIndex); + } + + /// @notice Allows a user to register as a member + /// @param idCommitment The idCommitment of the member + /// @param userMessageLimit The message limit of the member + /// @param membershipsToErase List of expired idCommitments to erase function register( uint256 idCommitment, - uint32 userMessageLimit + uint32 userMessageLimit, + uint256[] calldata membershipsToErase ) external - payable onlyValidIdCommitment(idCommitment) { if (memberExists(idCommitment)) revert DuplicateIdCommitment(); + for (uint256 i = 0; i < membershipsToErase.length; i++) { + uint256 idCommitmentToErase = membershipsToErase[i]; + MembershipInfo memory mdetails = members[idCommitmentToErase]; + _eraseMembership(_msgSender(), idCommitmentToErase, mdetails); + LazyIMT.update(imtData, 0, mdetails.index); + } + uint32 index; bool reusedIndex; - (index, reusedIndex) = _acquireMembership(_msgSender(), idCommitment, userMessageLimit); + (index, reusedIndex) = _acquireMembership(_msgSender(), idCommitment, userMessageLimit, false); _register(idCommitment, userMessageLimit, index, reusedIndex); } From 22ddb35d34000fa62209067eee18499b80973202 Mon Sep 17 00:00:00 2001 From: Richard Ramos Date: Tue, 10 Sep 2024 10:43:31 -0400 Subject: [PATCH 15/21] chore: more test units --- test/WakuRlnV2.t.sol | 65 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/test/WakuRlnV2.t.sol b/test/WakuRlnV2.t.sol index 5693263..ffa1580 100644 --- a/test/WakuRlnV2.t.sol +++ b/test/WakuRlnV2.t.sol @@ -371,6 +371,71 @@ contract WakuRlnV2Test is Test { assertEq(w.tail(), idCommitment + 5); } + function test__ValidRegistrationWithEraseList() external { + vm.pauseGasMetering(); + vm.startPrank(w.owner()); + w.setMinRateLimitPerMembership(20); + w.setMaxRateLimitPerMembership(100); + w.setMaxTotalRateLimitPerEpoch(100); + vm.stopPrank(); + vm.resumeGasMetering(); + + (, uint256 price) = w.priceCalculator().calculate(20); + + for (uint256 i = 1; i <= 5; i++) { + token.approve(address(w), price); + w.register(i, 20); + // Make sure they're expired + vm.warp(w.expirationDate(i)); + } + + // Time travel to a point in which the last commitment is active + (,,, uint256 gracePeriodStartDate,,,,,) = w.members(5); + vm.warp(gracePeriodStartDate - 1); + + // Ensure that this is the case + assertTrue(w.isExpired(4)); + assertFalse(w.isExpired(5)); + assertFalse(w.isGracePeriod(5)); + + (, price) = w.priceCalculator().calculate(60); + token.approve(address(w), price); + + // Attempt to expire 3 commitments including one that can't be erased (the last one) + uint256[] memory commitmentsToErase = new uint256[](3); + commitmentsToErase[0] = 1; + commitmentsToErase[1] = 2; + commitmentsToErase[2] = 5; // This one is still active + token.approve(address(w), price); + vm.expectRevert(abi.encodeWithSelector(CantEraseMembership.selector, 5)); + w.register(6, 60, commitmentsToErase); + + // Attempt to expire 3 commitments that can be erased + commitmentsToErase[2] = 4; + vm.expectEmit(true, false, false, false); + emit Membership.MemberExpired(1, 0, 0); + vm.expectEmit(true, false, false, false); + emit Membership.MemberExpired(2, 0, 0); + vm.expectEmit(true, false, false, false); + emit Membership.MemberExpired(4, 0, 0); + w.register(6, 60, commitmentsToErase); + + // Ensure that the chosen memberships were erased and others unaffected + address holder; + (,,,,,,, holder,) = w.members(1); + assertEq(holder, address(0)); + (,,,,,,, holder,) = w.members(2); + assertEq(holder, address(0)); + (,,,,,,, holder,) = w.members(3); + assertEq(holder, address(this)); + (,,,,,,, holder,) = w.members(4); + assertEq(holder, address(0)); + (,,,,,,, holder,) = w.members(5); + assertEq(holder, address(this)); + (,,,,,,, holder,) = w.members(6); + assertEq(holder, address(this)); + } + function test__RegistrationWhenMaxRateLimitIsReached() external { vm.pauseGasMetering(); vm.startPrank(w.owner()); From a658315e03bac321510a0c8d03dddf3734de52b9 Mon Sep 17 00:00:00 2001 From: Richard Ramos Date: Wed, 11 Sep 2024 10:36:02 -0400 Subject: [PATCH 16/21] chore: rename `commitmentIndex` to `nextCommitmentIndex` --- src/Membership.sol | 6 +++--- src/WakuRlnV2.sol | 8 ++++---- test/WakuRlnV2.t.sol | 18 +++++++++--------- 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/src/Membership.sol b/src/Membership.sol index 1fad7a1..30b7911 100644 --- a/src/Membership.sol +++ b/src/Membership.sol @@ -54,8 +54,8 @@ contract Membership { /// @notice List of registered memberships mapping(uint256 idCommitment => MembershipInfo member) public members; - /// @notice The index of the next member to be registered - uint32 public commitmentIndex; + /// @notice The index on the merkle tree for the next member to be registered + uint32 public nextCommitmentIndex; /// @notice track available indices that are available due to expired memberships being removed uint32[] public availableExpiredIndices; @@ -273,7 +273,7 @@ contract Membership { availableExpiredIndices.pop(); reusedIndex = true; } else { - index = commitmentIndex; + index = nextCommitmentIndex; } } diff --git a/src/WakuRlnV2.sol b/src/WakuRlnV2.sol index 01504df..101ee1f 100644 --- a/src/WakuRlnV2.sol +++ b/src/WakuRlnV2.sol @@ -96,7 +96,7 @@ contract WakuRlnV2 is Initializable, OwnableUpgradeable, UUPSUpgradeable, Member SET_SIZE = uint32(1 << DEPTH); deployedBlockNumber = uint32(block.number); LazyIMT.init(imtData, DEPTH); - commitmentIndex = 0; + nextCommitmentIndex = 0; } function _authorizeUpgrade(address newImplementation) internal override onlyOwner { } // solhint-disable-line @@ -183,14 +183,14 @@ contract WakuRlnV2 is Initializable, OwnableUpgradeable, UUPSUpgradeable, Member /// @param reusedIndex indicates whether we're inserting a new element in the merkle tree or updating a existing /// leaf function _register(uint256 idCommitment, uint32 userMessageLimit, uint32 index, bool reusedIndex) internal { - if (commitmentIndex >= SET_SIZE) revert FullTree(); + if (nextCommitmentIndex >= SET_SIZE) revert FullTree(); uint256 rateCommitment = PoseidonT3.hash([idCommitment, userMessageLimit]); if (reusedIndex) { LazyIMT.update(imtData, rateCommitment, index); } else { LazyIMT.insert(imtData, rateCommitment); - commitmentIndex += 1; + nextCommitmentIndex += 1; } emit MemberRegistered(rateCommitment, index); @@ -202,7 +202,7 @@ contract WakuRlnV2 is Initializable, OwnableUpgradeable, UUPSUpgradeable, Member /// @return The commitments of the members function getCommitments(uint32 startIndex, uint32 endIndex) public view returns (uint256[] memory) { if (startIndex > endIndex) revert InvalidPaginationQuery(startIndex, endIndex); - if (endIndex > commitmentIndex) revert InvalidPaginationQuery(startIndex, endIndex); + if (endIndex > nextCommitmentIndex) revert InvalidPaginationQuery(startIndex, endIndex); uint256[] memory commitments = new uint256[](endIndex - startIndex + 1); for (uint32 i = startIndex; i <= endIndex; i++) { diff --git a/test/WakuRlnV2.t.sol b/test/WakuRlnV2.t.sol index ffa1580..635f377 100644 --- a/test/WakuRlnV2.t.sol +++ b/test/WakuRlnV2.t.sol @@ -46,7 +46,7 @@ contract WakuRlnV2Test is Test { token.approve(address(w), price); w.register(idCommitment, userMessageLimit); vm.pauseGasMetering(); - assertEq(w.commitmentIndex(), 1); + assertEq(w.nextCommitmentIndex(), 1); assertEq(w.memberExists(idCommitment), true); (,,,,, uint32 fetchedUserMessageLimit, uint32 index, address holder,) = w.members(idCommitment); assertEq(fetchedUserMessageLimit, userMessageLimit); @@ -664,7 +664,7 @@ contract WakuRlnV2Test is Test { token.approve(address(w), price); w.register(i, 20); (,,,,,, index,,) = w.members(i); - assertEq(index, w.commitmentIndex() - 1); // TODO: renname commitmentIndex to nextCommitmentIndex + assertEq(index, w.nextCommitmentIndex() - 1); commitmentsToErase[i - 1] = i; } @@ -678,7 +678,7 @@ contract WakuRlnV2Test is Test { assertEq(i, w.availableExpiredIndices(i)); } - uint32 currCommitmentIndex = w.commitmentIndex(); + uint32 currnextCommitmentIndex = w.nextCommitmentIndex(); for (uint256 i = 1; i <= idCommitmentsLength; i++) { uint256 idCommitment = i + 10; uint256 expectedReusedIndexPos = idCommitmentsLength - i; @@ -691,7 +691,7 @@ contract WakuRlnV2Test is Test { vm.expectRevert(); w.availableExpiredIndices(expectedReusedIndexPos); // Should not have been affected - assertEq(currCommitmentIndex, w.commitmentIndex()); + assertEq(currnextCommitmentIndex, w.nextCommitmentIndex()); } // No indexes should be available for reuse @@ -702,8 +702,8 @@ contract WakuRlnV2Test is Test { token.approve(address(w), price); w.register(100, 20); (,,,,,, index,,) = w.members(100); - assertEq(index, currCommitmentIndex); - assertEq(currCommitmentIndex + 1, w.commitmentIndex()); + assertEq(index, currnextCommitmentIndex); + assertEq(currnextCommitmentIndex + 1, w.nextCommitmentIndex()); } function test__RemoveExpiredMemberships(uint32 userMessageLimit) external { @@ -898,7 +898,7 @@ contract WakuRlnV2Test is Test { /*| Name | Type | Slot | Offset | Bytes | |---------------------|-----------------------------------------------------|------|--------|-------| - | commitmentIndex | uint32 | 206 | 0 | 4 | */ + | nextCommitmentIndex | uint32 | 206 | 0 | 4 | */ /* Pro tip: to easily find the storage slot of a variable, without having to calculate the storage layout @@ -916,7 +916,7 @@ contract WakuRlnV2Test is Test { If the storage layout changes, update the next line accordingly */ - // we set commitmentIndex to 4294967295 (1 << 20) = 0x00100000 + // we set nextCommitmentIndex to 4294967295 (1 << 20) = 0x00100000 vm.store(address(w), bytes32(uint256(206)), 0x0000000000000000000000000000000000000000000000000000000000100000); token.approve(address(w), price); vm.expectRevert(FullTree.selector); @@ -928,7 +928,7 @@ contract WakuRlnV2Test is Test { w.getCommitments(1, 0); } - function test__InvalidPaginationQuery__EndIndexGTcommitmentIndex() external { + function test__InvalidPaginationQuery__EndIndexGTnextCommitmentIndex() external { vm.expectRevert(abi.encodeWithSelector(InvalidPaginationQuery.selector, 0, 2)); w.getCommitments(0, 2); } From 3fbcbfe624f6c81e23132c5a757caa5ab25c9d12 Mon Sep 17 00:00:00 2001 From: Richard Ramos Date: Wed, 11 Sep 2024 11:15:43 -0400 Subject: [PATCH 17/21] chore: update readme and script to allow running on anvil --- README.md | 9 ++++++++- script/Deploy.s.sol | 21 ++++++++++++++++++--- test/TestToken.sol | 7 +++++++ test/WakuRlnV2.t.sol | 4 ++-- 4 files changed, 35 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 486a37f..f822ff8 100644 --- a/README.md +++ b/README.md @@ -59,7 +59,14 @@ $ forge coverage #### Deploy to Anvil: ```sh -$ forge script script/Deploy.s.sol --broadcast --fork-url http://localhost:8545 +$ TOKEN_ADDRESS=0x1122334455667788990011223344556677889900 forge script script/Deploy.s.sol --broadcast --rpc-url localhost --tc Deploy +``` + +Replace the `TOKEN_ADDRESS` value by a token address you have deployed on anvil. A `TestToken` is available in +`test/TestToken.sol` and can be deployed with + +```sh +forge script test/TestToken.sol --broadcast --rpc-url localhost --tc TestTokenFactory ``` For this script to work, you need to have a `MNEMONIC` environment variable set to a valid diff --git a/script/Deploy.s.sol b/script/Deploy.s.sol index 10c778a..ed9c70e 100644 --- a/script/Deploy.s.sol +++ b/script/Deploy.s.sol @@ -11,15 +11,30 @@ import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils import { BaseScript } from "./Base.s.sol"; contract Deploy is BaseScript { - function run(address _token) public broadcast returns (WakuRlnV2 w, address impl) { - // TODO: Use the correct values when deploying to mainnet + function run() public broadcast returns (WakuRlnV2 w, address impl) { + address _token = _getTokenAddress(); + return deploy(_token); + } + + function deploy(address _token) public returns (WakuRlnV2 w, address impl) { address priceCalcAddr = address(new LinearPriceCalculator(_token, 0.05 ether)); - // TODO: set DAI address 0x6B175474E89094C44Da98b954EedeAC495271d0F impl = address(new WakuRlnV2()); bytes memory data = abi.encodeCall(WakuRlnV2.initialize, (priceCalcAddr, 160_000, 20, 600, 180 days, 30 days)); address proxy = address(new ERC1967Proxy(impl, data)); w = WakuRlnV2(proxy); } + + function _getTokenAddress() internal view returns (address) { + try vm.envAddress("TOKEN_ADDRESS") returns (address passedAddress) { + return passedAddress; + } catch { + if (block.chainid == 1) { + return 0x6B175474E89094C44Da98b954EedeAC495271d0F; // DAI address on mainnet + } else { + revert("no TOKEN_ADDRESS was specified"); + } + } + } } contract DeployLibs is BaseScript { diff --git a/test/TestToken.sol b/test/TestToken.sol index 90870cd..51ac926 100644 --- a/test/TestToken.sol +++ b/test/TestToken.sol @@ -1,6 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity >=0.8.19 <0.9.0; +import { BaseScript } from "../script/Base.s.sol"; import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; contract TestToken is ERC20 { @@ -10,3 +11,9 @@ contract TestToken is ERC20 { _mint(to, amount); } } + +contract TestTokenFactory is BaseScript { + function run() public broadcast returns (address) { + return address(new TestToken()); + } +} diff --git a/test/WakuRlnV2.t.sol b/test/WakuRlnV2.t.sol index 635f377..79068a0 100644 --- a/test/WakuRlnV2.t.sol +++ b/test/WakuRlnV2.t.sol @@ -26,7 +26,7 @@ contract WakuRlnV2Test is Test { token = new TestToken(); Deploy deployment = new Deploy(); - (w, impl) = deployment.run(address(token)); + (w, impl) = deployment.deploy(address(token)); // Minting a large number of tokens to not have to worry about // Not having enough balance @@ -898,7 +898,7 @@ contract WakuRlnV2Test is Test { /*| Name | Type | Slot | Offset | Bytes | |---------------------|-----------------------------------------------------|------|--------|-------| - | nextCommitmentIndex | uint32 | 206 | 0 | 4 | */ + | nextCommitmentIndex | uint32 | 206 | 0 | 4 | */ /* Pro tip: to easily find the storage slot of a variable, without having to calculate the storage layout From df502c78ff78262406f2baa6652d79a99fca50a4 Mon Sep 17 00:00:00 2001 From: Richard Ramos Date: Thu, 12 Sep 2024 15:25:25 -0400 Subject: [PATCH 18/21] fix: code review --- .gas-snapshot | 57 +++++++++++++++++------------------ src/LinearPriceCalculator.sol | 13 +++++--- src/Membership.sol | 45 ++++++++++++++++++++++----- src/WakuRlnV2.sol | 27 ++++++++--------- test/WakuRlnV2.t.sol | 26 ++++++++-------- 5 files changed, 99 insertions(+), 69 deletions(-) diff --git a/.gas-snapshot b/.gas-snapshot index 689702b..7f2222b 100644 --- a/.gas-snapshot +++ b/.gas-snapshot @@ -1,29 +1,28 @@ -WakuRlnV2Test:test__IdCommitmentToMetadata__DoesntExist() (gas: 27535) -WakuRlnV2Test:test__InsertionNormalOrder(uint32) (runs: 1004, μ: 1093558, ~: 496185) -WakuRlnV2Test:test__InvalidETHAmount(uint256,uint32) (runs: 1004, μ: 322023, ~: 322023) -WakuRlnV2Test:test__InvalidPaginationQuery__EndIndexGTcommitmentIndex() (gas: 18378) -WakuRlnV2Test:test__InvalidPaginationQuery__StartIndexGTEndIndex() (gas: 16225) -WakuRlnV2Test:test__InvalidRegistration__DuplicateIdCommitment() (gas: 249303) -WakuRlnV2Test:test__InvalidRegistration__FullTree() (gas: 162941) -WakuRlnV2Test:test__InvalidRegistration__InvalidIdCommitment__LargerThanField() (gas: 13471) -WakuRlnV2Test:test__InvalidRegistration__InvalidIdCommitment__Zero() (gas: 12182) -WakuRlnV2Test:test__InvalidRegistration__InvalidUserMessageLimit__MinMax() (gas: 63423) -WakuRlnV2Test:test__LinearPriceCalculation(uint32) (runs: 1015, μ: 25964, ~: 25964) -WakuRlnV2Test:test__RegistrationWhenMaxRateLimitIsReached() (gas: 547839) -WakuRlnV2Test:test__RegistrationWhenMaxRateLimitIsReachedAndMultipleExpiredMembersAvailable() (gas: 739752) -WakuRlnV2Test:test__RegistrationWhenMaxRateLimitIsReachedAndSingleExpiredMemberAvailable() (gas: 388429) -WakuRlnV2Test:test__RegistrationWhenMaxRateLimitReachedAndMultipleExpiredMembersAvailableWithoutEnoughRateLimit() (gas: 748699) -WakuRlnV2Test:test__RegistrationWithTokens(uint256,uint32) (runs: 1004, μ: 318681, ~: 318681) -WakuRlnV2Test:test__RemoveAllExpiredMemberships(uint32) (runs: 1004, μ: 6262152, ~: 1315256) -WakuRlnV2Test:test__RemoveExpiredMemberships(uint32) (runs: 1003, μ: 1075445, ~: 1075445) -WakuRlnV2Test:test__Upgrade() (gas: 6990368) -WakuRlnV2Test:test__ValidPaginationQuery(uint32) (runs: 1005, μ: 222425, ~: 53007) -WakuRlnV2Test:test__ValidPaginationQuery__OneElement() (gas: 262398) -WakuRlnV2Test:test__ValidRegistration(uint32) (runs: 1003, μ: 267715, ~: 267715) -WakuRlnV2Test:test__ValidRegistrationExpiry(uint32) (runs: 1003, μ: 1280765, ~: 1280765) -WakuRlnV2Test:test__ValidRegistrationExtend(uint32) (runs: 1003, μ: 1123986, ~: 1123986) -WakuRlnV2Test:test__ValidRegistrationExtendSingleMembership(uint32) (runs: 1003, μ: 257339, ~: 257339) -WakuRlnV2Test:test__ValidRegistration__kats() (gas: 238726) -WakuRlnV2Test:test__WithdrawETH(uint32) (runs: 1003, μ: 243355, ~: 243356) -WakuRlnV2Test:test__WithdrawToken(uint32) (runs: 1003, μ: 314604, ~: 314605) -WakuRlnV2Test:test__indexReuse_eraseMemberships(uint32) (runs: 1004, μ: 2187085, ~: 964428) \ No newline at end of file +WakuRlnV2Test:test__IdCommitmentToMetadata__DoesntExist() (gas: 27547) +WakuRlnV2Test:test__InsertionNormalOrder(uint32) (runs: 1005, μ: 1284765, ~: 577056) +WakuRlnV2Test:test__InvalidPaginationQuery__EndIndexGTnextCommitmentIndex() (gas: 18290) +WakuRlnV2Test:test__InvalidPaginationQuery__StartIndexGTEndIndex() (gas: 16181) +WakuRlnV2Test:test__InvalidRegistration__DuplicateIdCommitment() (gas: 322752) +WakuRlnV2Test:test__InvalidRegistration__FullTree() (gas: 239810) +WakuRlnV2Test:test__InvalidRegistration__InvalidIdCommitment__LargerThanField() (gas: 36509) +WakuRlnV2Test:test__InvalidRegistration__InvalidIdCommitment__Zero() (gas: 35220) +WakuRlnV2Test:test__InvalidRegistration__InvalidUserMessageLimit__MinMax() (gas: 63679) +WakuRlnV2Test:test__InvalidTokenAmount(uint256,uint32) (runs: 1004, μ: 207863, ~: 207863) +WakuRlnV2Test:test__LinearPriceCalculation(uint32) (runs: 1015, μ: 26049, ~: 26049) +WakuRlnV2Test:test__RegistrationWhenMaxRateLimitIsReached() (gas: 638698) +WakuRlnV2Test:test__RegistrationWhenMaxRateLimitIsReachedAndMultipleExpiredMembersAvailable() (gas: 906196) +WakuRlnV2Test:test__RegistrationWhenMaxRateLimitIsReachedAndSingleExpiredMemberAvailable() (gas: 461585) +WakuRlnV2Test:test__RegistrationWhenMaxRateLimitReachedAndMultipleExpiredMembersAvailableWithoutEnoughRateLimit() (gas: 869622) +WakuRlnV2Test:test__RemoveAllExpiredMemberships(uint32) (runs: 1005, μ: 7359251, ~: 1585880) +WakuRlnV2Test:test__RemoveExpiredMemberships(uint32) (runs: 1003, μ: 1248285, ~: 1248286) +WakuRlnV2Test:test__Upgrade() (gas: 7482875) +WakuRlnV2Test:test__ValidPaginationQuery(uint32) (runs: 1006, μ: 227626, ~: 52991) +WakuRlnV2Test:test__ValidPaginationQuery__OneElement() (gas: 319333) +WakuRlnV2Test:test__ValidRegistration(uint32) (runs: 1003, μ: 325571, ~: 325571) +WakuRlnV2Test:test__ValidRegistrationExpiry(uint32) (runs: 1003, μ: 1456074, ~: 1456074) +WakuRlnV2Test:test__ValidRegistrationExtend(uint32) (runs: 1003, μ: 1276008, ~: 1276008) +WakuRlnV2Test:test__ValidRegistrationExtendSingleMembership(uint32) (runs: 1003, μ: 314390, ~: 314390) +WakuRlnV2Test:test__ValidRegistrationWithEraseList() (gas: 1601871) +WakuRlnV2Test:test__ValidRegistration__kats() (gas: 295683) +WakuRlnV2Test:test__WithdrawToken(uint32) (runs: 1003, μ: 301025, ~: 301027) +WakuRlnV2Test:test__indexReuse_eraseMemberships(uint32) (runs: 1005, μ: 2702739, ~: 1165772) \ No newline at end of file diff --git a/src/LinearPriceCalculator.sol b/src/LinearPriceCalculator.sol index 8b54f95..08f2ef5 100644 --- a/src/LinearPriceCalculator.sol +++ b/src/LinearPriceCalculator.sol @@ -1,19 +1,22 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.24; -import { Ownable } from "openzeppelin-contracts/contracts/access/Ownable.sol"; +import { Ownable2Step } from "openzeppelin-contracts/contracts/access/Ownable2Step.sol"; import { IPriceCalculator } from "./IPriceCalculator.sol"; +/// Address 0x0000...0000 was used instead of an ERC20 token address +error OnlyERC20TokensAllowed(); + /// @title Linear Price Calculator to determine the price to acquire a membership -contract LinearPriceCalculator is IPriceCalculator, Ownable { - /// @notice Address of the ERC20 token accepted by this contract. Address(0) represents ETH +contract LinearPriceCalculator is IPriceCalculator, Ownable2Step { + /// @notice Address of the ERC20 token accepted by this contract. address public token; /// @notice The price per message per epoch uint256 public pricePerMessagePerEpoch; - constructor(address _token, uint256 _pricePerMessagePerEpoch) Ownable() { - require(_token != address(0), "only tokens can be used"); + constructor(address _token, uint256 _pricePerMessagePerEpoch) Ownable2Step() { + if (_token == address(0)) revert OnlyERC20TokensAllowed(); token = _token; pricePerMessagePerEpoch = _pricePerMessagePerEpoch; } diff --git a/src/Membership.sol b/src/Membership.sol index 30b7911..3018849 100644 --- a/src/Membership.sol +++ b/src/Membership.sol @@ -4,9 +4,7 @@ pragma solidity 0.8.24; import { IPriceCalculator } from "./IPriceCalculator.sol"; import { IERC20 } from "openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; import { SafeERC20 } from "openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol"; - -// An eth value was assigned in the transaction and only tokens were expected -error OnlyTokensAccepted(); +import { Initializable } from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; // The specified rate limit was not correct or within the expected limits error InvalidRateLimit(); @@ -24,7 +22,7 @@ error NotHolder(uint256 idCommitment); // This membership cannot be erased (either it is not expired or not in grace period and/or not the owner) error CantEraseMembership(uint256 idCommitment); -contract Membership { +abstract contract MembershipUpgradeable is Initializable { using SafeERC20 for IERC20; /// @notice Address of the Price Calculator used to calculate the price of a new membership @@ -108,7 +106,28 @@ contract Membership { /// @param _maxRateLimitPerMembership Maximum rate limit of one membership /// @param _expirationTerm Membership expiration term /// @param _gracePeriod Membership grace period - function __Membership_init( + function __MembershipUpgradeable_init( + address _priceCalculator, + uint32 _maxTotalRateLimitPerEpoch, + uint32 _minRateLimitPerMembership, + uint32 _maxRateLimitPerMembership, + uint32 _expirationTerm, + uint32 _gracePeriod + ) + internal + onlyInitializing + { + __MembershipUpgradeable_init_unchained( + _priceCalculator, + _maxTotalRateLimitPerEpoch, + _minRateLimitPerMembership, + _maxRateLimitPerMembership, + _expirationTerm, + _gracePeriod + ); + } + + function __MembershipUpgradeable_init_unchained( address _priceCalculator, uint32 _maxTotalRateLimitPerEpoch, uint32 _minRateLimitPerMembership, @@ -117,7 +136,13 @@ contract Membership { uint32 _gracePeriod ) internal + onlyInitializing { + require(_maxTotalRateLimitPerEpoch >= maxRateLimitPerMembership); + require(_maxRateLimitPerMembership > minRateLimitPerMembership); + require(_minRateLimitPerMembership > 0); + require(_expirationTerm > 0); + priceCalculator = IPriceCalculator(_priceCalculator); maxTotalRateLimitPerEpoch = _maxTotalRateLimitPerEpoch; maxRateLimitPerMembership = _maxRateLimitPerMembership; @@ -157,7 +182,6 @@ contract Membership { } function _transferFees(address _from, address _token, uint256 _amount) internal { - if (msg.value != 0) revert OnlyTokensAccepted(); IERC20(_token).safeTransferFrom(_from, address(this), _amount); } @@ -377,7 +401,7 @@ contract Membership { if (!membershipExpired && !isGracePeriodAndOwned) revert CantEraseMembership(_idCommitment); - emit MemberExpired(head, _mdetails.userMessageLimit, _mdetails.index); + emit MemberExpired(_idCommitment, _mdetails.userMessageLimit, _mdetails.index); // Move balance from expired membership to holder balance balancesToWithdraw[_mdetails.holder][_mdetails.token] += _mdetails.amount; @@ -415,4 +439,11 @@ contract Membership { balancesToWithdraw[_sender][_token] = 0; IERC20(_token).safeTransfer(_sender, amount); } + + /** + * @dev This empty reserved space is put in place to allow future versions to add new + * variables without shifting down storage in the inheritance chain. + * See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps + */ + uint256[50] private __gap; } diff --git a/src/WakuRlnV2.sol b/src/WakuRlnV2.sol index 101ee1f..76e360a 100644 --- a/src/WakuRlnV2.sol +++ b/src/WakuRlnV2.sol @@ -4,11 +4,11 @@ pragma solidity 0.8.24; import { LazyIMT, LazyIMTData } from "@zk-kit/imt.sol/LazyIMT.sol"; import { PoseidonT3 } from "poseidon-solidity/PoseidonT3.sol"; -import { OwnableUpgradeable } from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; +import { Ownable2StepUpgradeable } from "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol"; import { Initializable } from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; -import { Membership } from "./Membership.sol"; +import { MembershipUpgradeable } from "./Membership.sol"; import { IPriceCalculator } from "./IPriceCalculator.sol"; /// The tree is full @@ -26,7 +26,7 @@ error InvalidUserMessageLimit(uint32 messageLimit); /// Invalid pagination query error InvalidPaginationQuery(uint256 startIndex, uint256 endIndex); -contract WakuRlnV2 is Initializable, OwnableUpgradeable, UUPSUpgradeable, Membership { +contract WakuRlnV2 is Initializable, Ownable2StepUpgradeable, UUPSUpgradeable, MembershipUpgradeable { /// @notice The Field uint256 public constant Q = 21_888_242_871_839_275_222_246_405_745_257_275_088_548_364_400_416_034_343_698_204_186_575_808_495_617; @@ -64,32 +64,27 @@ contract WakuRlnV2 is Initializable, OwnableUpgradeable, UUPSUpgradeable, Member /// @param _maxTotalRateLimitPerEpoch Maximum total rate limit of all memberships in the tree /// @param _minRateLimitPerMembership Minimum rate limit of one membership /// @param _maxRateLimitPerMembership Maximum rate limit of one membership - /// @param _billingPeriod Membership billing period + /// @param _expirationTerm Membership expiration term /// @param _gracePeriod Membership grace period function initialize( address _priceCalculator, uint32 _maxTotalRateLimitPerEpoch, uint32 _minRateLimitPerMembership, uint32 _maxRateLimitPerMembership, - uint32 _billingPeriod, + uint32 _expirationTerm, uint32 _gracePeriod ) public initializer { - require(_maxTotalRateLimitPerEpoch >= maxRateLimitPerMembership); - require(_maxRateLimitPerMembership > minRateLimitPerMembership); - require(_minRateLimitPerMembership > 0); - require(_billingPeriod > 0); - __Ownable_init(); __UUPSUpgradeable_init(); - __Membership_init( + __MembershipUpgradeable_init( _priceCalculator, _maxTotalRateLimitPerEpoch, _minRateLimitPerMembership, _maxRateLimitPerMembership, - _billingPeriod, + _expirationTerm, _gracePeriod ); @@ -120,8 +115,8 @@ contract WakuRlnV2 is Initializable, OwnableUpgradeable, UUPSUpgradeable, Member /// @return The metadata of the member (userMessageLimit, index, rateCommitment) function idCommitmentToMetadata(uint256 idCommitment) public view returns (uint32, uint32, uint256) { MembershipInfo memory member = members[idCommitment]; - // we cannot call indexToCommitment if the member doesn't exist - if (member.holder == address(0)) { + // we cannot call indexToCommitment for 0 index if the member doesn't exist + if (member.userMessageLimit == 0) { return (0, 0, 0); } return (member.userMessageLimit, member.index, indexToCommitment(member.index)); @@ -165,6 +160,7 @@ contract WakuRlnV2 is Initializable, OwnableUpgradeable, UUPSUpgradeable, Member for (uint256 i = 0; i < membershipsToErase.length; i++) { uint256 idCommitmentToErase = membershipsToErase[i]; MembershipInfo memory mdetails = members[idCommitmentToErase]; + if (mdetails.userMessageLimit == 0) revert InvalidIdCommitment(idCommitmentToErase); _eraseMembership(_msgSender(), idCommitmentToErase, mdetails); LazyIMT.update(imtData, 0, mdetails.index); } @@ -248,6 +244,7 @@ contract WakuRlnV2 is Initializable, OwnableUpgradeable, UUPSUpgradeable, Member for (uint256 i = 0; i < idCommitments.length; i++) { uint256 idCommitment = idCommitments[i]; MembershipInfo memory mdetails = members[idCommitment]; + if (mdetails.userMessageLimit == 0) revert InvalidIdCommitment(idCommitment); _eraseMembership(_msgSender(), idCommitment, mdetails); LazyIMT.update(imtData, 0, mdetails.index); } @@ -288,7 +285,7 @@ contract WakuRlnV2 is Initializable, OwnableUpgradeable, UUPSUpgradeable, Member /// @notice Set the membership expiration term /// @param _expirationTerm new value - function setBillingPeriod(uint32 _expirationTerm) external onlyOwner { + function setExpirationTerm(uint32 _expirationTerm) external onlyOwner { require(_expirationTerm > 0); expirationTerm = _expirationTerm; } diff --git a/test/WakuRlnV2.t.sol b/test/WakuRlnV2.t.sol index 79068a0..4818887 100644 --- a/test/WakuRlnV2.t.sol +++ b/test/WakuRlnV2.t.sol @@ -262,7 +262,7 @@ contract WakuRlnV2Test is Test { // Attempt to extend the membership (but now we are the owner) vm.expectEmit(true, false, false, false); // only check the first parameter of the event (the idCommitment) - emit Membership.MemberExtended(idCommitment, 0, 0, 0); + emit MembershipUpgradeable.MemberExtended(idCommitment, 0, 0, 0); w.extend(commitmentsToExtend); (,,, uint256 newGracePeriodStartDate,,,,,) = w.members(idCommitment); @@ -325,7 +325,7 @@ contract WakuRlnV2Test is Test { // Extend the membership vm.expectEmit(true, false, false, false); // only check the first parameter of the event (the idCommitment) - emit Membership.MemberExtended(idCommitment, 0, 0, 0); + emit MembershipUpgradeable.MemberExtended(idCommitment, 0, 0, 0); w.extend(commitmentsToExtend); // Verify list order is correct @@ -413,11 +413,11 @@ contract WakuRlnV2Test is Test { // Attempt to expire 3 commitments that can be erased commitmentsToErase[2] = 4; vm.expectEmit(true, false, false, false); - emit Membership.MemberExpired(1, 0, 0); + emit MembershipUpgradeable.MemberExpired(1, 0, 0); vm.expectEmit(true, false, false, false); - emit Membership.MemberExpired(2, 0, 0); + emit MembershipUpgradeable.MemberExpired(2, 0, 0); vm.expectEmit(true, false, false, false); - emit Membership.MemberExpired(4, 0, 0); + emit MembershipUpgradeable.MemberExpired(4, 0, 0); w.register(6, 60, commitmentsToErase); // Ensure that the chosen memberships were erased and others unaffected @@ -518,7 +518,7 @@ contract WakuRlnV2Test is Test { // It should succeed now vm.expectEmit(); - emit Membership.MemberExpired(1, userMessageLimitA, indexA); + emit MembershipUpgradeable.MemberExpired(1, userMessageLimitA, indexA); w.register(2, userMessageLimitB); // The previous expired membership should have been erased @@ -579,9 +579,9 @@ contract WakuRlnV2Test is Test { (, uint256 priceB) = w.priceCalculator().calculate(4); token.approve(address(w), priceB); vm.expectEmit(true, false, false, false); - emit Membership.MemberExpired(1, 0, 0); + emit MembershipUpgradeable.MemberExpired(1, 0, 0); vm.expectEmit(true, false, false, false); - emit Membership.MemberExpired(2, 0, 0); + emit MembershipUpgradeable.MemberExpired(2, 0, 0); w.register(4, 4); // idCommitment4 will use the last removed index available (since we push to an array) @@ -740,9 +740,9 @@ contract WakuRlnV2Test is Test { commitmentsToErase[1] = idCommitment + 2; vm.expectEmit(true, false, false, false); // only check the first parameter of the event (the idCommitment) - emit Membership.MemberExpired(commitmentsToErase[0], 0, 0); + emit MembershipUpgradeable.MemberExpired(commitmentsToErase[0], 0, 0); vm.expectEmit(true, false, false, false); // only check the first parameter of the event (the idCommitment) - emit Membership.MemberExpired(commitmentsToErase[0], 0, 0); + emit MembershipUpgradeable.MemberExpired(commitmentsToErase[0], 0, 0); w.eraseMemberships(commitmentsToErase); address holder; @@ -803,7 +803,7 @@ contract WakuRlnV2Test is Test { for (uint256 i = 0; i < idCommitmentsLength; i++) { commitmentsToErase[i] = i + 1; vm.expectEmit(true, false, false, false); // only check the first parameter of the event (the idCommitment) - emit Membership.MemberExpired(i + 1, 0, 0); + emit MembershipUpgradeable.MemberExpired(i + 1, 0, 0); } w.eraseMemberships(commitmentsToErase); @@ -898,7 +898,7 @@ contract WakuRlnV2Test is Test { /*| Name | Type | Slot | Offset | Bytes | |---------------------|-----------------------------------------------------|------|--------|-------| - | nextCommitmentIndex | uint32 | 206 | 0 | 4 | */ + | nextCommitmentIndex | uint32 | 256 | 0 | 4 | */ /* Pro tip: to easily find the storage slot of a variable, without having to calculate the storage layout @@ -917,7 +917,7 @@ contract WakuRlnV2Test is Test { */ // we set nextCommitmentIndex to 4294967295 (1 << 20) = 0x00100000 - vm.store(address(w), bytes32(uint256(206)), 0x0000000000000000000000000000000000000000000000000000000000100000); + vm.store(address(w), bytes32(uint256(256)), 0x0000000000000000000000000000000000000000000000000000000000100000); token.approve(address(w), price); vm.expectRevert(FullTree.selector); w.register(1, userMessageLimit); From 522599c704702856e41a7abc5e2dba8ef2c0fb3d Mon Sep 17 00:00:00 2001 From: Richard Ramos Date: Mon, 23 Sep 2024 17:18:54 -0400 Subject: [PATCH 19/21] fix: more efficient duplicate members check --- src/WakuRlnV2.sol | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/src/WakuRlnV2.sol b/src/WakuRlnV2.sol index 76e360a..0275205 100644 --- a/src/WakuRlnV2.sol +++ b/src/WakuRlnV2.sol @@ -55,6 +55,11 @@ contract WakuRlnV2 is Initializable, Ownable2StepUpgradeable, UUPSUpgradeable, M _; } + modifier noDuplicateMembers(uint256 idCommitment) { + if (members[idCommitment].userMessageLimit != 0) revert DuplicateIdCommitment(); + _; + } + constructor() { _disableInitializers(); } @@ -133,9 +138,14 @@ contract WakuRlnV2 is Initializable, Ownable2StepUpgradeable, UUPSUpgradeable, M /// @notice Allows a user to register as a member /// @param idCommitment The idCommitment of the member /// @param userMessageLimit The message limit of the member - function register(uint256 idCommitment, uint32 userMessageLimit) external onlyValidIdCommitment(idCommitment) { - if (memberExists(idCommitment)) revert DuplicateIdCommitment(); - + function register( + uint256 idCommitment, + uint32 userMessageLimit + ) + external + onlyValidIdCommitment(idCommitment) + noDuplicateMembers(idCommitment) + { uint32 index; bool reusedIndex; (index, reusedIndex) = _acquireMembership(_msgSender(), idCommitment, userMessageLimit, true); @@ -154,9 +164,8 @@ contract WakuRlnV2 is Initializable, Ownable2StepUpgradeable, UUPSUpgradeable, M ) external onlyValidIdCommitment(idCommitment) + noDuplicateMembers(idCommitment) { - if (memberExists(idCommitment)) revert DuplicateIdCommitment(); - for (uint256 i = 0; i < membershipsToErase.length; i++) { uint256 idCommitmentToErase = membershipsToErase[i]; MembershipInfo memory mdetails = members[idCommitmentToErase]; From 3fea0a52013a92fb5bcb760a57e5b85c2723d8b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?rich=CE=9Brd?= Date: Tue, 8 Oct 2024 08:58:43 -0400 Subject: [PATCH 20/21] refactor: do not keep track of membership registration order (#14) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: do not keep track of membership registration order * unify terminology for mdetails * more DRY in LinearPriceCalculator * improve terminology consistency (#16) * refactor, minor fixes * do not reset ongoing grace period on extension (cf. spec change) * minor renaming, comments * refactor, group functions by funcitonality * extract membership expiration calculation to internal function * save active duration per membership (must carry over after extension) * optional lazy erasure from membership set * minor fixes in tests * fix: off-by-one: end index can't be equal to next free index * minor refactoring, comments * define period boundaries: start inclusive, end exclusive * separate eraseMembership functions to user-focused (lazy) and admin-focused (tree cleanup) * minor fix to maintain line lengths * unify membership-related events * add test for zero grace period * fix typo in comment Co-authored-by: richΛrd --------- Co-authored-by: richΛrd * code review * refactor: unique register function * fix: split errors of `_eraseMembershipLazily` * chore: minor naming clarifications --------- Co-authored-by: Sergei Tikhomirov --- .gas-snapshot | 52 +-- script/Deploy.s.sol | 1 - src/LinearPriceCalculator.sol | 23 +- src/Membership.sol | 551 ++++++++++------------- src/WakuRlnV2.sol | 363 ++++++++------- test/WakuRlnV2.t.sol | 815 +++++++++++++--------------------- 6 files changed, 754 insertions(+), 1051 deletions(-) diff --git a/.gas-snapshot b/.gas-snapshot index 7f2222b..8c37d59 100644 --- a/.gas-snapshot +++ b/.gas-snapshot @@ -1,28 +1,24 @@ -WakuRlnV2Test:test__IdCommitmentToMetadata__DoesntExist() (gas: 27547) -WakuRlnV2Test:test__InsertionNormalOrder(uint32) (runs: 1005, μ: 1284765, ~: 577056) -WakuRlnV2Test:test__InvalidPaginationQuery__EndIndexGTnextCommitmentIndex() (gas: 18290) -WakuRlnV2Test:test__InvalidPaginationQuery__StartIndexGTEndIndex() (gas: 16181) -WakuRlnV2Test:test__InvalidRegistration__DuplicateIdCommitment() (gas: 322752) -WakuRlnV2Test:test__InvalidRegistration__FullTree() (gas: 239810) -WakuRlnV2Test:test__InvalidRegistration__InvalidIdCommitment__LargerThanField() (gas: 36509) -WakuRlnV2Test:test__InvalidRegistration__InvalidIdCommitment__Zero() (gas: 35220) -WakuRlnV2Test:test__InvalidRegistration__InvalidUserMessageLimit__MinMax() (gas: 63679) -WakuRlnV2Test:test__InvalidTokenAmount(uint256,uint32) (runs: 1004, μ: 207863, ~: 207863) -WakuRlnV2Test:test__LinearPriceCalculation(uint32) (runs: 1015, μ: 26049, ~: 26049) -WakuRlnV2Test:test__RegistrationWhenMaxRateLimitIsReached() (gas: 638698) -WakuRlnV2Test:test__RegistrationWhenMaxRateLimitIsReachedAndMultipleExpiredMembersAvailable() (gas: 906196) -WakuRlnV2Test:test__RegistrationWhenMaxRateLimitIsReachedAndSingleExpiredMemberAvailable() (gas: 461585) -WakuRlnV2Test:test__RegistrationWhenMaxRateLimitReachedAndMultipleExpiredMembersAvailableWithoutEnoughRateLimit() (gas: 869622) -WakuRlnV2Test:test__RemoveAllExpiredMemberships(uint32) (runs: 1005, μ: 7359251, ~: 1585880) -WakuRlnV2Test:test__RemoveExpiredMemberships(uint32) (runs: 1003, μ: 1248285, ~: 1248286) -WakuRlnV2Test:test__Upgrade() (gas: 7482875) -WakuRlnV2Test:test__ValidPaginationQuery(uint32) (runs: 1006, μ: 227626, ~: 52991) -WakuRlnV2Test:test__ValidPaginationQuery__OneElement() (gas: 319333) -WakuRlnV2Test:test__ValidRegistration(uint32) (runs: 1003, μ: 325571, ~: 325571) -WakuRlnV2Test:test__ValidRegistrationExpiry(uint32) (runs: 1003, μ: 1456074, ~: 1456074) -WakuRlnV2Test:test__ValidRegistrationExtend(uint32) (runs: 1003, μ: 1276008, ~: 1276008) -WakuRlnV2Test:test__ValidRegistrationExtendSingleMembership(uint32) (runs: 1003, μ: 314390, ~: 314390) -WakuRlnV2Test:test__ValidRegistrationWithEraseList() (gas: 1601871) -WakuRlnV2Test:test__ValidRegistration__kats() (gas: 295683) -WakuRlnV2Test:test__WithdrawToken(uint32) (runs: 1003, μ: 301025, ~: 301027) -WakuRlnV2Test:test__indexReuse_eraseMemberships(uint32) (runs: 1005, μ: 2702739, ~: 1165772) \ No newline at end of file +WakuRlnV2Test:test__IdCommitmentToMetadata__DoesntExist() (gas: 23299) +WakuRlnV2Test:test__InvalidPaginationQuery__EndIndexGTnextCommitmentIndex() (gas: 18307) +WakuRlnV2Test:test__InvalidPaginationQuery__StartIndexGTEndIndex() (gas: 16131) +WakuRlnV2Test:test__InvalidRegistration__DuplicateIdCommitment() (gas: 272654) +WakuRlnV2Test:test__InvalidRegistration__FullTree() (gas: 190004) +WakuRlnV2Test:test__InvalidRegistration__InvalidIdCommitment__LargerThanField() (gas: 36492) +WakuRlnV2Test:test__InvalidRegistration__InvalidIdCommitment__Zero() (gas: 35192) +WakuRlnV2Test:test__InvalidRegistration__InvalidUserMessageLimit__MinMax() (gas: 55026) +WakuRlnV2Test:test__InvalidTokenAmount(uint256,uint32) (runs: 1006, μ: 158053, ~: 158053) +WakuRlnV2Test:test__LinearPriceCalculation(uint32) (runs: 1015, μ: 26026, ~: 26026) +WakuRlnV2Test:test__RegistrationWhenMaxRateLimitIsReached() (gas: 527384) +WakuRlnV2Test:test__RemoveAllExpiredMemberships(uint32) (runs: 1004, μ: 3577547, ~: 653139) +WakuRlnV2Test:test__RemoveExpiredMemberships(uint32) (runs: 1003, μ: 1044941, ~: 1044943) +WakuRlnV2Test:test__Upgrade() (gas: 6932864) +WakuRlnV2Test:test__ValidPaginationQuery(uint32) (runs: 1005, μ: 227459, ~: 52991) +WakuRlnV2Test:test__ValidPaginationQuery__OneElement() (gas: 269528) +WakuRlnV2Test:test__ValidRegistration(uint32) (runs: 1003, μ: 275279, ~: 275279) +WakuRlnV2Test:test__ValidRegistrationExpiry(uint32) (runs: 1003, μ: 256301, ~: 256301) +WakuRlnV2Test:test__ValidRegistrationExtend(uint32) (runs: 1003, μ: 474309, ~: 474309) +WakuRlnV2Test:test__ValidRegistrationExtendSingleMembership(uint32) (runs: 1003, μ: 263787, ~: 263787) +WakuRlnV2Test:test__ValidRegistrationWithEraseList() (gas: 1380002) +WakuRlnV2Test:test__ValidRegistration__kats() (gas: 245878) +WakuRlnV2Test:test__WithdrawToken(uint32) (runs: 1003, μ: 260362, ~: 260364) +WakuRlnV2Test:test__indexReuse_eraseMemberships(uint32) (runs: 1004, μ: 2377343, ~: 975838) \ No newline at end of file diff --git a/script/Deploy.s.sol b/script/Deploy.s.sol index ed9c70e..cc8b1ba 100644 --- a/script/Deploy.s.sol +++ b/script/Deploy.s.sol @@ -6,7 +6,6 @@ import { LinearPriceCalculator } from "../src/LinearPriceCalculator.sol"; import { PoseidonT3 } from "poseidon-solidity/PoseidonT3.sol"; import { LazyIMT } from "@zk-kit/imt.sol/LazyIMT.sol"; import { ERC1967Proxy } from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; -import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; import { BaseScript } from "./Base.s.sol"; diff --git a/src/LinearPriceCalculator.sol b/src/LinearPriceCalculator.sol index 08f2ef5..0bf6bd2 100644 --- a/src/LinearPriceCalculator.sol +++ b/src/LinearPriceCalculator.sol @@ -5,7 +5,7 @@ import { Ownable2Step } from "openzeppelin-contracts/contracts/access/Ownable2St import { IPriceCalculator } from "./IPriceCalculator.sol"; /// Address 0x0000...0000 was used instead of an ERC20 token address -error OnlyERC20TokensAllowed(); +error OnlyTokensAllowed(); /// @title Linear Price Calculator to determine the price to acquire a membership contract LinearPriceCalculator is IPriceCalculator, Ownable2Step { @@ -16,18 +16,23 @@ contract LinearPriceCalculator is IPriceCalculator, Ownable2Step { uint256 public pricePerMessagePerEpoch; constructor(address _token, uint256 _pricePerMessagePerEpoch) Ownable2Step() { - if (_token == address(0)) revert OnlyERC20TokensAllowed(); - token = _token; - pricePerMessagePerEpoch = _pricePerMessagePerEpoch; + _setTokenAndPrice(_token, _pricePerMessagePerEpoch); + } + + /// Set accepted token and price per message per epoch per period + /// @param _token The token accepted by RLN membership management + /// @param _pricePerMessagePerEpoch Price per message per epoch + function setTokenAndPrice(address _token, uint256 _pricePerMessagePerEpoch) external onlyOwner { + _setTokenAndPrice(_token, _pricePerMessagePerEpoch); } /// Set accepted token and price per message per epoch per period - /// @param _token The token accepted by the membership management for RLN - /// @param _pricePerPeriod Price per message per epoch - function setTokenAndPrice(address _token, uint256 _pricePerPeriod) external onlyOwner { - require(_token != address(0), "only tokens can be used"); + /// @param _token The token accepted by RLN membership management + /// @param _pricePerMessagePerEpoch Price per message per epoch + function _setTokenAndPrice(address _token, uint256 _pricePerMessagePerEpoch) internal { + if (_token == address(0)) revert OnlyTokensAllowed(); token = _token; - pricePerMessagePerEpoch = _pricePerPeriod; + pricePerMessagePerEpoch = _pricePerMessagePerEpoch; } /// Returns the token and price to pay in `token` for some `_rateLimit` diff --git a/src/Membership.sol b/src/Membership.sol index 3018849..ac08bb4 100644 --- a/src/Membership.sol +++ b/src/Membership.sol @@ -6,21 +6,27 @@ import { IERC20 } from "openzeppelin-contracts/contracts/token/ERC20/IERC20.sol" import { SafeERC20 } from "openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol"; import { Initializable } from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; -// The specified rate limit was not correct or within the expected limits -error InvalidRateLimit(); +// The rate limit is outside the expected limits +error InvalidMembershipRateLimit(); -// It's not possible to acquire the rate limit due to exceeding the expected limits +// Cannot acquire the rate limit for a new membership due to exceeding the expected limits // even after attempting to erase expired memberships -error ExceedAvailableMaxRateLimitPerEpoch(); +error CannotExceedMaxTotalRateLimit(); -// This membership is not in grace period yet -error NotInGracePeriod(uint256 idCommitment); +// The membership is not in its grace period (cannot be extended) +error CannotExtendNonGracePeriodMembership(uint256 idCommitment); -// The sender is not the holder of the membership -error NotHolder(uint256 idCommitment); +// The sender is not the holder of this membership (cannot extend) +error NonHolderCannotExtend(uint256 idCommitment); -// This membership cannot be erased (either it is not expired or not in grace period and/or not the owner) -error CantEraseMembership(uint256 idCommitment); +// The membership is still active (cannot be erased) +error CannotEraseActiveMembership(uint256 idCommitment); + +// The sender is not the holder of this membership (cannot erase) +error NonHolderCannotEraseGracePeriodMembership(uint256 idCommitment); + +// The membership does not exist +error MembershipDoesNotExist(uint256 idCommitment); abstract contract MembershipUpgradeable is Initializable { using SafeERC20 for IERC20; @@ -28,415 +34,320 @@ abstract contract MembershipUpgradeable is Initializable { /// @notice Address of the Price Calculator used to calculate the price of a new membership IPriceCalculator public priceCalculator; - /// @notice Maximum total rate limit of all memberships in the tree - uint32 public maxTotalRateLimitPerEpoch; + /// @notice Maximum total rate limit of all memberships in the membership set (messages per epoch) + uint32 public maxTotalRateLimit; /// @notice Maximum rate limit of one membership - uint32 public maxRateLimitPerMembership; + uint32 public maxMembershipRateLimit; /// @notice Minimum rate limit of one membership - uint32 public minRateLimitPerMembership; - - /// @notice Membership billing period - uint32 public expirationTerm; - - /// @notice Membership grace period - uint32 public gracePeriod; + uint32 public minMembershipRateLimit; - /// @notice balances available to withdraw - mapping(address holder => mapping(address token => uint256 balance)) public balancesToWithdraw; + /// @notice Membership active period duration (A in the spec) + uint32 public activeDurationForNewMemberships; - /// @notice Total rate limit of all memberships in the tree - uint256 public totalRateLimitPerEpoch; + /// @notice Membership grace period duration (G in the spec) + uint32 public gracePeriodDurationForNewMemberships; - /// @notice List of registered memberships - mapping(uint256 idCommitment => MembershipInfo member) public members; + /// @notice Deposits available for withdrawal + /// Note: amount of deposits unavailable for withdrawal are stored in MembershipInfo elements. + mapping(address holder => mapping(address token => uint256 balance)) public depositsToWithdraw; - /// @notice The index on the merkle tree for the next member to be registered - uint32 public nextCommitmentIndex; + /// @notice Current total rate limit of all memberships in the membership set (messages per epoch) + uint256 public currentTotalRateLimit; - /// @notice track available indices that are available due to expired memberships being removed - uint32[] public availableExpiredIndices; + /// @notice List of memberships in the membership set + mapping(uint256 idCommitment => MembershipInfo membership) public memberships; - /// @dev Oldest membership - uint256 public head = 0; + /// @notice The index in the membership set for the next membership to be registered + uint32 public nextFreeIndex; - /// @dev Newest membership - uint256 public tail = 0; + /// @notice Indices of erased memberships that can be reused for new registrations + uint32[] public indicesOfLazilyErasedMemberships; struct MembershipInfo { - /// @notice idCommitment of the previous membership - uint256 prev; - /// @notice idCommitment of the next membership - uint256 next; - /// @notice amount of the token used to acquire this membership - uint256 amount; - /// @notice timestamp of when the grace period starts for this membership - uint256 gracePeriodStartDate; - /// @notice duration of the grace period - uint32 gracePeriod; - /// @notice the user message limit of each member - uint32 userMessageLimit; - /// @notice the index of the member in the set + /// @notice the deposit amount (in tokens) to register this membership + uint256 depositAmount; + /// @notice the duration of the active period of this membership + uint32 activeDuration; + /// @notice the start of the grace period (= the end of the active period) + uint256 gracePeriodStartTimestamp; + /// @notice the duration of the grace period of this membership + uint32 gracePeriodDuration; + /// @notice the membership rate limit + uint32 rateLimit; + /// @notice the index of the membership in the membership set uint32 index; - /// @notice address of the owner of this membership + /// @notice the address of the holder of this membership address holder; - /// @notice token used to acquire this membership + /// @notice the token used to make the deposit to register this membership address token; } - /// @notice Emitted when a membership is erased due to having exceeded the grace period or the owner having chosen - /// to not extend it - /// @param idCommitment the idCommitment of the member - /// @param userMessageLimit the rate limit of this membership - /// @param index the index of the membership in the merkle tree - event MemberExpired(uint256 idCommitment, uint32 userMessageLimit, uint32 index); - - /// @notice Emitted when a membership in grace period is extended - /// @param idCommitment the idCommitment of the member - /// @param userMessageLimit the rate limit of this membership - /// @param index the index of the membership in the merkle tree - /// @param newExpirationDate the new expiration date of this membership - event MemberExtended(uint256 idCommitment, uint32 userMessageLimit, uint32 index, uint256 newExpirationDate); + /// Emitted when a new membership is added to the membership set + /// @param idCommitment the idCommitment of the membership + /// @param membershipRateLimit the rate limit of this membership + /// @param index The index of the membership in the membership set + event MembershipRegistered(uint256 idCommitment, uint256 membershipRateLimit, uint32 index); + + /// @notice Emitted when a membership is expired (exceeded its grace period and not extended) + /// @param idCommitment the idCommitment of the membership + /// @param membershipRateLimit the rate limit of this membership + /// @param index the index of the membership in the membership set + event MembershipExpired(uint256 idCommitment, uint32 membershipRateLimit, uint32 index); + + /// @notice Emitted when a membership is erased by its holder during grace period + /// @param idCommitment the idCommitment of the membership + /// @param membershipRateLimit the rate limit of this membership + /// @param index the index of the membership in the membership set + event MembershipErased(uint256 idCommitment, uint32 membershipRateLimit, uint32 index); + + /// @notice Emitted when a membership in its grace period is extended (i.e., is back to Active state) + /// @param idCommitment the idCommitment of the membership + /// @param membershipRateLimit the rate limit of this membership + /// @param index the index of the membership in the membership set + /// @param newGracePeriodStartTimestamp the new grace period start timestamp of this membership + event MembershipExtended( + uint256 idCommitment, uint32 membershipRateLimit, uint32 index, uint256 newGracePeriodStartTimestamp + ); /// @dev contract initializer /// @param _priceCalculator Address of an instance of IPriceCalculator - /// @param _maxTotalRateLimitPerEpoch Maximum total rate limit of all memberships in the tree - /// @param _minRateLimitPerMembership Minimum rate limit of one membership - /// @param _maxRateLimitPerMembership Maximum rate limit of one membership - /// @param _expirationTerm Membership expiration term - /// @param _gracePeriod Membership grace period + /// @param _maxTotalRateLimit Maximum total rate limit of all memberships in the membership set + /// @param _minMembershipRateLimit Minimum rate limit of each membership + /// @param _maxMembershipRateLimit Maximum rate limit of each membership + /// @param _activeDurationForNewMemberships Active state duration of each membership + /// @param _gracePeriodDurationForNewMemberships Grace period duration of each membership function __MembershipUpgradeable_init( address _priceCalculator, - uint32 _maxTotalRateLimitPerEpoch, - uint32 _minRateLimitPerMembership, - uint32 _maxRateLimitPerMembership, - uint32 _expirationTerm, - uint32 _gracePeriod + uint32 _maxTotalRateLimit, + uint32 _minMembershipRateLimit, + uint32 _maxMembershipRateLimit, + uint32 _activeDurationForNewMemberships, + uint32 _gracePeriodDurationForNewMemberships ) internal onlyInitializing { __MembershipUpgradeable_init_unchained( _priceCalculator, - _maxTotalRateLimitPerEpoch, - _minRateLimitPerMembership, - _maxRateLimitPerMembership, - _expirationTerm, - _gracePeriod + _maxTotalRateLimit, + _minMembershipRateLimit, + _maxMembershipRateLimit, + _activeDurationForNewMemberships, + _gracePeriodDurationForNewMemberships ); } function __MembershipUpgradeable_init_unchained( address _priceCalculator, - uint32 _maxTotalRateLimitPerEpoch, - uint32 _minRateLimitPerMembership, - uint32 _maxRateLimitPerMembership, - uint32 _expirationTerm, - uint32 _gracePeriod + uint32 _maxTotalRateLimit, + uint32 _minMembershipRateLimit, + uint32 _maxMembershipRateLimit, + uint32 _activeDurationForNewMemberships, + uint32 _gracePeriodDurationForNewMemberships ) internal onlyInitializing { - require(_maxTotalRateLimitPerEpoch >= maxRateLimitPerMembership); - require(_maxRateLimitPerMembership > minRateLimitPerMembership); - require(_minRateLimitPerMembership > 0); - require(_expirationTerm > 0); + require(0 < _minMembershipRateLimit); + require(_minMembershipRateLimit <= _maxMembershipRateLimit); + require(_maxMembershipRateLimit <= _maxTotalRateLimit); + require(_activeDurationForNewMemberships > 0); + // Note: grace period duration may be equal to zero priceCalculator = IPriceCalculator(_priceCalculator); - maxTotalRateLimitPerEpoch = _maxTotalRateLimitPerEpoch; - maxRateLimitPerMembership = _maxRateLimitPerMembership; - minRateLimitPerMembership = _minRateLimitPerMembership; - expirationTerm = _expirationTerm; - gracePeriod = _gracePeriod; - } - - /// @notice Checks if a user message limit is valid. This does not take into account whether we the total - /// memberships have reached already the `maxTotalRateLimitPerEpoch` - /// @param userMessageLimit The user message limit - /// @return true if the user message limit is valid, false otherwise - function isValidUserMessageLimit(uint32 userMessageLimit) external view returns (bool) { - return userMessageLimit >= minRateLimitPerMembership && userMessageLimit <= maxRateLimitPerMembership; + maxTotalRateLimit = _maxTotalRateLimit; + minMembershipRateLimit = _minMembershipRateLimit; + maxMembershipRateLimit = _maxMembershipRateLimit; + activeDurationForNewMemberships = _activeDurationForNewMemberships; + gracePeriodDurationForNewMemberships = _gracePeriodDurationForNewMemberships; } - /// @dev acquire a membership and trasnfer the fees to the contract - /// @param _sender address of the owner of the new membership - /// @param _idCommitment the idcommitment of the new membership - /// @param _rateLimit the user message limit - /// @param _eraseIfNeeded Erase expired memberships if the `_rateLimit` exceeds the available rate limit - /// @return index the index in the merkle tree - /// @return reusedIndex indicates whether a new leaf is being used or if using an existing leaf in the merkle tree + /// @dev acquire a membership and transfer the deposit to the contract + /// @param _sender the address of the transaction sender + /// @param _idCommitment the idCommitment of the new membership + /// @param _rateLimit the membership rate limit + /// @return index the index of the new membership in the membership set + /// @return indexReused true if the index was reused, false otherwise function _acquireMembership( address _sender, uint256 _idCommitment, - uint32 _rateLimit, - bool _eraseIfNeeded - ) - internal - returns (uint32 index, bool reusedIndex) - { - (address token, uint256 amount) = priceCalculator.calculate(_rateLimit); - (index, reusedIndex) = - _setupMembershipDetails(_sender, _idCommitment, _rateLimit, token, amount, _eraseIfNeeded); - _transferFees(_sender, token, amount); - } - - function _transferFees(address _from, address _token, uint256 _amount) internal { - IERC20(_token).safeTransferFrom(_from, address(this), _amount); - } - - /// @dev Setup a new membership. If there are not enough remaining rate limit to acquire - /// a new membership, it will attempt to erase existing memberships and reuse one of the - /// slots helds by the membership - /// @param _sender holder of the membership. Generally `msg.sender` - /// @param _idCommitment IDCommitment - /// @param _rateLimit User message limit - /// @param _token Address of the token used to acquire the membership - /// @param _amount Amount of the token used to acquire the membership - /// @param _eraseIfNeeded Erase expired memberships if the `_rateLimit` exceeds the available rate limit - /// @return index membership index on the merkle tree - /// @return reusedIndex indicates whether the index returned was a reused slot on the tree or not - function _setupMembershipDetails( - address _sender, - uint256 _idCommitment, - uint32 _rateLimit, - address _token, - uint256 _amount, - bool _eraseIfNeeded + uint32 _rateLimit ) internal - returns (uint32 index, bool reusedIndex) + returns (uint32 index, bool indexReused) { - if (_rateLimit < minRateLimitPerMembership || _rateLimit > maxRateLimitPerMembership) { - revert InvalidRateLimit(); + // Check if the rate limit is valid + if (!isValidMembershipRateLimit(_rateLimit)) { + revert InvalidMembershipRateLimit(); } - // Storing in local variable to not access the storage frequently - // And we're using/modifying these variables in each iteration - uint256 _head = head; - uint256 _tail = tail; - uint256 _totalRateLimitPerEpoch = totalRateLimitPerEpoch; - uint32 _maxTotalRateLimitPerEpoch = maxTotalRateLimitPerEpoch; + currentTotalRateLimit += _rateLimit; // Determine if we exceed the total rate limit - if (_totalRateLimitPerEpoch + _rateLimit > _maxTotalRateLimitPerEpoch) { - if (_head == 0 || !_eraseIfNeeded) revert ExceedAvailableMaxRateLimitPerEpoch(); // List is empty or can't - // erase memberships automatically - - // Attempt to free expired membership slots - while (_totalRateLimitPerEpoch + _rateLimit > _maxTotalRateLimitPerEpoch && _head != 0) { - // Determine if there are any available spot in the membership map - // by looking at the oldest membership. If it's expired, we can free it - MembershipInfo memory oldestMembership = members[_head]; - if (!_isExpired(oldestMembership.gracePeriodStartDate, oldestMembership.gracePeriod)) { - revert ExceedAvailableMaxRateLimitPerEpoch(); - } - - emit MemberExpired(_head, oldestMembership.userMessageLimit, oldestMembership.index); - - // Deduct the expired membership rate limit - _totalRateLimitPerEpoch -= oldestMembership.userMessageLimit; - - // Remove the element from the list - delete members[_head]; - - // Promote the next oldest membership to oldest - _head = oldestMembership.next; - - // Move balance from expired membership to holder balance - balancesToWithdraw[oldestMembership.holder][oldestMembership.token] += oldestMembership.amount; - - availableExpiredIndices.push(oldestMembership.index); - } - - // Ensure new head and tail are pointing to the correct memberships - if (_head != 0) { - members[_head].prev = 0; - } else { - _tail = 0; - } + if (currentTotalRateLimit > maxTotalRateLimit) { + revert CannotExceedMaxTotalRateLimit(); } - if (_tail != 0) { - members[_tail].next = _idCommitment; - } else { - // First item - _head = _idCommitment; - } - - // Adding the rate limit of the new registration - _totalRateLimitPerEpoch += _rateLimit; + (address token, uint256 depositAmount) = priceCalculator.calculate(_rateLimit); - // Reuse available slots from previously removed expired memberships - (index, reusedIndex) = _nextIndex(); + // Possibly reuse an index of an erased membership + (index, indexReused) = _getFreeIndex(); - totalRateLimitPerEpoch = _totalRateLimitPerEpoch; - members[_idCommitment] = MembershipInfo({ + memberships[_idCommitment] = MembershipInfo({ holder: _sender, - gracePeriodStartDate: block.timestamp + uint256(expirationTerm), - gracePeriod: gracePeriod, - token: _token, - amount: _amount, - userMessageLimit: _rateLimit, - next: 0, // It's the newest value, so point to nowhere - prev: _tail, + activeDuration: activeDurationForNewMemberships, + gracePeriodStartTimestamp: block.timestamp + uint256(activeDurationForNewMemberships), + gracePeriodDuration: gracePeriodDurationForNewMemberships, + token: token, + depositAmount: depositAmount, + rateLimit: _rateLimit, index: index }); - head = _head; - tail = _idCommitment; + + IERC20(token).safeTransferFrom(_sender, address(this), depositAmount); } - /// @dev reuse available slots from previously removed expired memberships - /// @return index index to use - /// @return reusedIndex indicates whether it is reusing an existing index, or using a new one - function _nextIndex() internal returns (uint32 index, bool reusedIndex) { - // Reuse available slots from previously removed expired memberships - uint256 arrLen = availableExpiredIndices.length; - if (arrLen != 0) { - index = availableExpiredIndices[arrLen - 1]; - availableExpiredIndices.pop(); - reusedIndex = true; + /// @notice Checks if a rate limit is within the allowed bounds + /// @param rateLimit The rate limit + /// @return true if the rate limit is within the allowed bounds, false otherwise + function isValidMembershipRateLimit(uint32 rateLimit) public view returns (bool) { + return minMembershipRateLimit <= rateLimit && rateLimit <= maxMembershipRateLimit; + } + + /// @dev Get a free index (possibly reuse an index of an erased membership) + /// @return index index to be used for the new membership registration + /// @return indexReused indicates whether the index was reused from an erased membership + function _getFreeIndex() internal returns (uint32 index, bool indexReused) { + uint256 numIndices = indicesOfLazilyErasedMemberships.length; + if (numIndices != 0) { + // Reuse the index of the latest erased membership + index = indicesOfLazilyErasedMemberships[numIndices - 1]; + indicesOfLazilyErasedMemberships.pop(); + indexReused = true; } else { - index = nextCommitmentIndex; + index = nextFreeIndex; } } - /// @dev Extend a membership expiration date. Membership must be on grace period - /// @param _sender the address of the holder of the membership + /// @dev Extend a grace-period membership + /// @param _sender the address of the transaction sender /// @param _idCommitment the idCommitment of the membership function _extendMembership(address _sender, uint256 _idCommitment) public { - MembershipInfo storage mdetails = members[_idCommitment]; + MembershipInfo storage membership = memberships[_idCommitment]; - if (!_isGracePeriod(mdetails.gracePeriodStartDate, mdetails.gracePeriod)) { - revert NotInGracePeriod(_idCommitment); + if (!_isInPeriod(membership.gracePeriodStartTimestamp, membership.gracePeriodDuration)) { + revert CannotExtendNonGracePeriodMembership(_idCommitment); } - if (_sender != mdetails.holder) revert NotHolder(_idCommitment); + if (_sender != membership.holder) revert NonHolderCannotExtend(_idCommitment); - uint256 gracePeriodStartDate = block.timestamp + uint256(expirationTerm); + // Note: we add the new active period to the end of the ongoing grace period + uint256 newGracePeriodStartTimestamp = + membership.gracePeriodStartTimestamp + membership.gracePeriodDuration + uint256(membership.activeDuration); - uint256 next = mdetails.next; - uint256 prev = mdetails.prev; - uint256 _tail = tail; - uint256 _head = head; + membership.gracePeriodStartTimestamp = newGracePeriodStartTimestamp; - // Remove current membership references - if (prev != 0) { - members[prev].next = next; - } else { - _head = next; - } + emit MembershipExtended( + _idCommitment, membership.rateLimit, membership.index, membership.gracePeriodStartTimestamp + ); + } - if (next != 0) { - members[next].prev = prev; - } else { - _tail = prev; - } + /// @dev Erase expired memberships or owned grace-period memberships. + /// @param _sender the address of the transaction sender + /// @param _idCommitment idCommitment of the membership to erase + function _eraseMembershipLazily(address _sender, uint256 _idCommitment) internal returns (uint32 index) { + MembershipInfo memory membership = memberships[_idCommitment]; - // Move membership to the end (since it will be the newest) - mdetails.next = 0; - mdetails.prev = _tail; - mdetails.gracePeriodStartDate = gracePeriodStartDate; - mdetails.gracePeriod = gracePeriod; + if (membership.rateLimit == 0) revert MembershipDoesNotExist(_idCommitment); - // Link previous tail with membership that was just extended - if (_tail != 0) { - members[_tail].next = _idCommitment; - } else { - // There are no other items in the list. - // The head will become the extended commitment - _head = _idCommitment; + bool membershipExpired = _isAfterPeriod(membership.gracePeriodStartTimestamp, membership.gracePeriodDuration); + bool membershipIsInGracePeriod = + _isInPeriod(membership.gracePeriodStartTimestamp, membership.gracePeriodDuration); + bool isHolder = (membership.holder == _sender); + + if (!membershipExpired && !membershipIsInGracePeriod) { + revert CannotEraseActiveMembership(_idCommitment); + } else if (membershipIsInGracePeriod && !isHolder) { + revert NonHolderCannotEraseGracePeriodMembership(_idCommitment); } - head = _head; - tail = _idCommitment; + // Move deposit balance from the membership to be erased to holder deposit balance + depositsToWithdraw[membership.holder][membership.token] += membership.depositAmount; - emit MemberExtended(_idCommitment, mdetails.userMessageLimit, mdetails.index, gracePeriodStartDate); - } + // Deduct the rate limit of this membership from the total rate limit + currentTotalRateLimit -= membership.rateLimit; - /// @dev Determine whether a timestamp is considered to be expired or not after exceeding the grace period - /// @param _gracePeriodStartDate timestamp in which the grace period starts - /// @param _gracePeriod duration of the grace period - function _isExpired(uint256 _gracePeriodStartDate, uint32 _gracePeriod) internal view returns (bool) { - return block.timestamp > _gracePeriodStartDate + uint256(_gracePeriod); - } + // Mark this membership as reusable + indicesOfLazilyErasedMemberships.push(membership.index); - /// @notice Determine if a membership is expired (has exceeded the grace period) - /// @param _idCommitment the idCommitment of the membership - function isExpired(uint256 _idCommitment) public view returns (bool) { - MembershipInfo memory m = members[_idCommitment]; - return _isExpired(m.gracePeriodStartDate, m.gracePeriod); + // Erase this membership from the memberships mapping + delete memberships[_idCommitment]; + + if (membershipExpired) { + emit MembershipExpired(_idCommitment, membership.rateLimit, membership.index); + } + emit MembershipErased(_idCommitment, membership.rateLimit, membership.index); + + // This index will be used to erase the data from the Merkle tree that represents the membership set + return membership.index; } - /// @notice Returns the timestamp on which a membership can be considered expired + /// @notice Determine if a membership is in its grace period /// @param _idCommitment the idCommitment of the membership - function expirationDate(uint256 _idCommitment) public view returns (uint256) { - MembershipInfo memory m = members[_idCommitment]; - return m.gracePeriodStartDate + uint256(m.gracePeriod) + 1; + function isInGracePeriod(uint256 _idCommitment) public view returns (bool) { + MembershipInfo memory membership = memberships[_idCommitment]; + return _isInPeriod(membership.gracePeriodStartTimestamp, membership.gracePeriodDuration); } - /// @dev Determine whether a timestamp is considered to be in grace period or not - /// @param _gracePeriodStartDate timestamp in which the grace period starts - /// @param _gracePeriod duration of the grace period - function _isGracePeriod(uint256 _gracePeriodStartDate, uint32 _gracePeriod) internal view returns (bool) { - uint256 blockTimestamp = block.timestamp; - return - blockTimestamp >= _gracePeriodStartDate && blockTimestamp <= _gracePeriodStartDate + uint256(_gracePeriod); + /// @dev Determine whether the current timestamp is within a given period + /// @param _start timestamp in which the period starts (inclusive) + /// @param _duration duration of the period (end timestamp exclusive) + function _isInPeriod(uint256 _start, uint32 _duration) internal view returns (bool) { + uint256 timeNow = block.timestamp; + return (_start <= timeNow && timeNow < _start + uint256(_duration)); } - /// @notice Determine if a membership is in grace period + /// @notice Determine if a membership is expired /// @param _idCommitment the idCommitment of the membership - function isGracePeriod(uint256 _idCommitment) public view returns (bool) { - MembershipInfo memory m = members[_idCommitment]; - return _isGracePeriod(m.gracePeriodStartDate, m.gracePeriod); + function isExpired(uint256 _idCommitment) public view returns (bool) { + MembershipInfo memory membership = memberships[_idCommitment]; + return _isAfterPeriod(membership.gracePeriodStartTimestamp, membership.gracePeriodDuration); } - /// @dev Remove expired memberships or owned memberships in grace period. - /// @param _sender address of the sender of transaction (will be used to check memberships in grace period) - /// @param _idCommitment IDCommitment of the membership to erase - function _eraseMembership(address _sender, uint256 _idCommitment, MembershipInfo memory _mdetails) internal { - bool membershipExpired = _isExpired(_mdetails.gracePeriodStartDate, _mdetails.gracePeriod); - bool isGracePeriodAndOwned = - _isGracePeriod(_mdetails.gracePeriodStartDate, _mdetails.gracePeriod) && _mdetails.holder == _sender; - - if (!membershipExpired && !isGracePeriodAndOwned) revert CantEraseMembership(_idCommitment); - - emit MemberExpired(_idCommitment, _mdetails.userMessageLimit, _mdetails.index); - - // Move balance from expired membership to holder balance - balancesToWithdraw[_mdetails.holder][_mdetails.token] += _mdetails.amount; - - // Deduct the expired membership rate limit - totalRateLimitPerEpoch -= _mdetails.userMessageLimit; - - // Remove current membership references - if (_mdetails.prev != 0) { - members[_mdetails.prev].next = _mdetails.next; - } else { - head = _mdetails.next; - } - - if (_mdetails.next != 0) { - members[_mdetails.next].prev = _mdetails.prev; - } else { - tail = _mdetails.prev; - } + /// @dev Determine whether the current timestamp is after a given period + /// @param _start timestamp in which the period starts (inclusive) + /// @param _duration duration of the period (end timestamp exclusive) + function _isAfterPeriod(uint256 _start, uint32 _duration) internal view returns (bool) { + uint256 timeNow = block.timestamp; + return (_timestampAfterPeriod(_start, _duration) <= timeNow); + } - availableExpiredIndices.push(_mdetails.index); + /// @notice Returns the timestamp on which a membership can be considered expired (i.e. when its grace period ends) + /// @param _idCommitment the idCommitment of the membership + function membershipExpirationTimestamp(uint256 _idCommitment) public view returns (uint256) { + MembershipInfo memory membership = memberships[_idCommitment]; + return _timestampAfterPeriod(membership.gracePeriodStartTimestamp, membership.gracePeriodDuration); + } - delete members[_idCommitment]; + /// @dev Returns the first timestamp after a specified period + /// @param _start timestamp in which the period starts (inclusive) + /// @param _duration duration of the period (exclusive) + function _timestampAfterPeriod(uint256 _start, uint32 _duration) internal pure returns (uint256) { + return _start + uint256(_duration); } - /// @dev Withdraw any available balance in tokens after a membership is erased. - /// @param _sender the address of the owner of the tokens - /// @param _token the address of the token to withdraw. + /// @dev Withdraw any available deposit balance in tokens after a membership is erased. + /// @param _sender the address of the transaction sender (who withdraws their tokens) + /// @param _token the address of the token to withdraw function _withdraw(address _sender, address _token) internal { require(_token != address(0), "ETH is not allowed"); - uint256 amount = balancesToWithdraw[_sender][_token]; - require(amount > 0, "Insufficient balance"); + uint256 amount = depositsToWithdraw[_sender][_token]; + require(amount > 0, "Insufficient deposit balance"); - balancesToWithdraw[_sender][_token] = 0; + depositsToWithdraw[_sender][_token] = 0; IERC20(_token).safeTransfer(_sender, amount); } diff --git a/src/WakuRlnV2.sol b/src/WakuRlnV2.sol index 0275205..965bbaa 100644 --- a/src/WakuRlnV2.sol +++ b/src/WakuRlnV2.sol @@ -11,18 +11,12 @@ import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils import { MembershipUpgradeable } from "./Membership.sol"; import { IPriceCalculator } from "./IPriceCalculator.sol"; -/// The tree is full -error FullTree(); - -/// Member is already registered +/// A membership with this idCommitment is already registered error DuplicateIdCommitment(); /// Invalid idCommitment error InvalidIdCommitment(uint256 idCommitment); -/// Invalid userMessageLimit -error InvalidUserMessageLimit(uint32 messageLimit); - /// Invalid pagination query error InvalidPaginationQuery(uint256 startIndex, uint256 endIndex); @@ -31,32 +25,35 @@ contract WakuRlnV2 is Initializable, Ownable2StepUpgradeable, UUPSUpgradeable, M uint256 public constant Q = 21_888_242_871_839_275_222_246_405_745_257_275_088_548_364_400_416_034_343_698_204_186_575_808_495_617; - /// @notice The depth of the merkle tree - uint8 public constant DEPTH = 20; + /// @notice The depth of the Merkle tree that stores rate commitments of memberships + uint8 public constant MERKLE_TREE_DEPTH = 20; - /// @notice The size of the merkle tree, i.e 2^depth - uint32 public SET_SIZE; + /// @notice The maximum membership set size is the size of the Merkle tree (2 ^ depth) + uint32 public MAX_MEMBERSHIP_SET_SIZE; - /// @notice the deployed block number + /// @notice The block number at which this contract was deployed uint32 public deployedBlockNumber; - /// @notice the stored imt data - LazyIMTData public imtData; - - /// Emitted when a new member is added to the set - /// @param rateCommitment the rateCommitment of the member - /// @param index The index of the member in the set - event MemberRegistered(uint256 rateCommitment, uint32 index); + /// @notice The Merkle tree that stores rate commitments of memberships + LazyIMTData public merkleTree; - /// @notice the modifier to check if the idCommitment is valid - /// @param idCommitment The idCommitment of the member + /// @notice Сheck if the idCommitment is valid + /// @param idCommitment The idCommitment of the membership modifier onlyValidIdCommitment(uint256 idCommitment) { - if (!isValidCommitment(idCommitment)) revert InvalidIdCommitment(idCommitment); + if (!isValidIdCommitment(idCommitment)) revert InvalidIdCommitment(idCommitment); _; } - modifier noDuplicateMembers(uint256 idCommitment) { - if (members[idCommitment].userMessageLimit != 0) revert DuplicateIdCommitment(); + /// @notice Сheck that the membership with this idCommitment is not already in the membership set + /// @param idCommitment The idCommitment of the membership + modifier noDuplicateMembership(uint256 idCommitment) { + require(!isInMembershipSet(idCommitment), "Duplicate idCommitment: membership already exists"); + _; + } + + /// @notice Check that the membership set is not full + modifier membershipSetNotFull() { + require(nextFreeIndex < MAX_MEMBERSHIP_SET_SIZE, "Membership set is full"); _; } @@ -64,19 +61,19 @@ contract WakuRlnV2 is Initializable, Ownable2StepUpgradeable, UUPSUpgradeable, M _disableInitializers(); } - /// @dev contract initializer + /// @dev Contract initializer /// @param _priceCalculator Address of an instance of IPriceCalculator - /// @param _maxTotalRateLimitPerEpoch Maximum total rate limit of all memberships in the tree - /// @param _minRateLimitPerMembership Minimum rate limit of one membership - /// @param _maxRateLimitPerMembership Maximum rate limit of one membership - /// @param _expirationTerm Membership expiration term + /// @param _maxTotalRateLimit Maximum total rate limit of all memberships in the membership set + /// @param _minMembershipRateLimit Minimum rate limit of one membership + /// @param _maxMembershipRateLimit Maximum rate limit of one membership + /// @param _activeDuration Membership active duration /// @param _gracePeriod Membership grace period function initialize( address _priceCalculator, - uint32 _maxTotalRateLimitPerEpoch, - uint32 _minRateLimitPerMembership, - uint32 _maxRateLimitPerMembership, - uint32 _expirationTerm, + uint32 _maxTotalRateLimit, + uint32 _minMembershipRateLimit, + uint32 _maxMembershipRateLimit, + uint32 _activeDuration, uint32 _gracePeriod ) public @@ -86,181 +83,181 @@ contract WakuRlnV2 is Initializable, Ownable2StepUpgradeable, UUPSUpgradeable, M __UUPSUpgradeable_init(); __MembershipUpgradeable_init( _priceCalculator, - _maxTotalRateLimitPerEpoch, - _minRateLimitPerMembership, - _maxRateLimitPerMembership, - _expirationTerm, + _maxTotalRateLimit, + _minMembershipRateLimit, + _maxMembershipRateLimit, + _activeDuration, _gracePeriod ); - SET_SIZE = uint32(1 << DEPTH); + MAX_MEMBERSHIP_SET_SIZE = uint32(1 << MERKLE_TREE_DEPTH); deployedBlockNumber = uint32(block.number); - LazyIMT.init(imtData, DEPTH); - nextCommitmentIndex = 0; + LazyIMT.init(merkleTree, MERKLE_TREE_DEPTH); + nextFreeIndex = 0; } function _authorizeUpgrade(address newImplementation) internal override onlyOwner { } // solhint-disable-line - /// @notice Checks if a commitment is valid - /// @param idCommitment The idCommitment of the member - /// @return true if the commitment is valid, false otherwise - function isValidCommitment(uint256 idCommitment) public pure returns (bool) { - return idCommitment != 0 && idCommitment < Q; + /// @notice Checks if an idCommitment is valid (between 0 and Q, both exclusive) + /// @param idCommitment The idCommitment of the membership + /// @return true if the idCommitment is valid, false otherwise + function isValidIdCommitment(uint256 idCommitment) public pure returns (bool) { + return 0 < idCommitment && idCommitment < Q; } - /// @notice Returns the rateCommitment of a member - /// @param index The index of the member - /// @return The rateCommitment of the member - function indexToCommitment(uint32 index) internal view returns (uint256) { - return imtData.elements[LazyIMT.indexForElement(0, index)]; + /// @notice Checks if a membership is in the membership set + /// @param idCommitment The idCommitment of the membership + /// @return true if the membership is in the membership set, false otherwise + function isInMembershipSet(uint256 idCommitment) public view returns (bool) { + (,, uint256 rateCommitment) = getMembershipInfo(idCommitment); + return rateCommitment != 0; } - /// @notice Returns the metadata of a member - /// @param idCommitment The idCommitment of the member - /// @return The metadata of the member (userMessageLimit, index, rateCommitment) - function idCommitmentToMetadata(uint256 idCommitment) public view returns (uint32, uint32, uint256) { - MembershipInfo memory member = members[idCommitment]; - // we cannot call indexToCommitment for 0 index if the member doesn't exist - if (member.userMessageLimit == 0) { + /// @notice Returns the membership info (rate limit, index, rateCommitment) by its idCommitment + /// @param idCommitment The idCommitment of the membership + /// @return The membership info (rateLimit, index, rateCommitment) + function getMembershipInfo(uint256 idCommitment) public view returns (uint32, uint32, uint256) { + MembershipInfo memory membership = memberships[idCommitment]; + // we cannot call getRateCommmitment for 0 index if the membership doesn't exist + if (membership.rateLimit == 0) { return (0, 0, 0); } - return (member.userMessageLimit, member.index, indexToCommitment(member.index)); + return (membership.rateLimit, membership.index, _getRateCommmitment(membership.index)); } - /// @notice Checks if a member exists - /// @param idCommitment The idCommitment of the member - /// @return true if the member exists, false otherwise - function memberExists(uint256 idCommitment) public view returns (bool) { - (,, uint256 rateCommitment) = idCommitmentToMetadata(idCommitment); - return rateCommitment != 0; - } - - /// @notice Allows a user to register as a member - /// @param idCommitment The idCommitment of the member - /// @param userMessageLimit The message limit of the member - function register( - uint256 idCommitment, - uint32 userMessageLimit + /// @notice Returns the rateCommitments of memberships within an index range + /// @param startIndex The start index of the range (inclusive) + /// @param endIndex The end index of the range (inclusive) + /// @return The rateCommitments of the memberships + function getRateCommitmentsInRangeBoundsInclusive( + uint32 startIndex, + uint32 endIndex ) - external - onlyValidIdCommitment(idCommitment) - noDuplicateMembers(idCommitment) + public + view + returns (uint256[] memory) { - uint32 index; - bool reusedIndex; - (index, reusedIndex) = _acquireMembership(_msgSender(), idCommitment, userMessageLimit, true); + if (startIndex > endIndex) revert InvalidPaginationQuery(startIndex, endIndex); + if (endIndex >= nextFreeIndex) revert InvalidPaginationQuery(startIndex, endIndex); + + uint256[] memory rateCommitments = new uint256[](endIndex - startIndex + 1); + for (uint32 i = startIndex; i <= endIndex; i++) { + rateCommitments[i - startIndex] = _getRateCommmitment(i); + } + return rateCommitments; + } - _register(idCommitment, userMessageLimit, index, reusedIndex); + /// @notice Returns the rateCommitment of a membership at a given index + /// @param index The index of the membership in the membership set + /// @return The rateCommitment of the membership + function _getRateCommmitment(uint32 index) internal view returns (uint256) { + return merkleTree.elements[LazyIMT.indexForElement(0, index)]; } - /// @notice Allows a user to register as a member - /// @param idCommitment The idCommitment of the member - /// @param userMessageLimit The message limit of the member - /// @param membershipsToErase List of expired idCommitments to erase + /// @notice Register a membership while erasing some expired memberships to reuse their rate limit + /// @param idCommitment The idCommitment of the new membership + /// @param rateLimit The rate limit of the new membership + /// @param idCommitmentsToErase The list of idCommitments of expired memberships to erase function register( uint256 idCommitment, - uint32 userMessageLimit, - uint256[] calldata membershipsToErase + uint32 rateLimit, + uint256[] calldata idCommitmentsToErase ) external onlyValidIdCommitment(idCommitment) - noDuplicateMembers(idCommitment) + noDuplicateMembership(idCommitment) + membershipSetNotFull { - for (uint256 i = 0; i < membershipsToErase.length; i++) { - uint256 idCommitmentToErase = membershipsToErase[i]; - MembershipInfo memory mdetails = members[idCommitmentToErase]; - if (mdetails.userMessageLimit == 0) revert InvalidIdCommitment(idCommitmentToErase); - _eraseMembership(_msgSender(), idCommitmentToErase, mdetails); - LazyIMT.update(imtData, 0, mdetails.index); - } - - uint32 index; - bool reusedIndex; - (index, reusedIndex) = _acquireMembership(_msgSender(), idCommitment, userMessageLimit, false); - - _register(idCommitment, userMessageLimit, index, reusedIndex); + // erase memberships without overwriting membership set data to zero (save gas) + _eraseMemberships(idCommitmentsToErase, false); + _register(idCommitment, rateLimit); } - /// @dev Registers a member - /// @param idCommitment The idCommitment of the member - /// @param userMessageLimit The message limit of the member - /// @param index Indicates the index in the merkle tree - /// @param reusedIndex indicates whether we're inserting a new element in the merkle tree or updating a existing - /// leaf - function _register(uint256 idCommitment, uint32 userMessageLimit, uint32 index, bool reusedIndex) internal { - if (nextCommitmentIndex >= SET_SIZE) revert FullTree(); - - uint256 rateCommitment = PoseidonT3.hash([idCommitment, userMessageLimit]); - if (reusedIndex) { - LazyIMT.update(imtData, rateCommitment, index); + /// @dev Register a membership (internal function) + /// @param idCommitment The idCommitment of the membership + /// @param rateLimit The rate limit of the membership + function _register(uint256 idCommitment, uint32 rateLimit) internal { + (uint32 index, bool indexReused) = _acquireMembership(_msgSender(), idCommitment, rateLimit); + uint256 rateCommitment = PoseidonT3.hash([idCommitment, rateLimit]); + if (indexReused) { + LazyIMT.update(merkleTree, rateCommitment, index); } else { - LazyIMT.insert(imtData, rateCommitment); - nextCommitmentIndex += 1; + LazyIMT.insert(merkleTree, rateCommitment); + nextFreeIndex += 1; } - emit MemberRegistered(rateCommitment, index); + emit MembershipRegistered(idCommitment, rateLimit, index); } - /// @notice Returns the commitments of a range of members - /// @param startIndex The start index of the range - /// @param endIndex The end index of the range - /// @return The commitments of the members - function getCommitments(uint32 startIndex, uint32 endIndex) public view returns (uint256[] memory) { - if (startIndex > endIndex) revert InvalidPaginationQuery(startIndex, endIndex); - if (endIndex > nextCommitmentIndex) revert InvalidPaginationQuery(startIndex, endIndex); - - uint256[] memory commitments = new uint256[](endIndex - startIndex + 1); - for (uint32 i = startIndex; i <= endIndex; i++) { - commitments[i - startIndex] = indexToCommitment(i); - } - return commitments; - } - - /// @notice Returns the root of the IMT - /// @return The root of the IMT + /// @notice Returns the root of the Merkle tree that stores rate commitments of memberships + /// @return The root of the Merkle tree that stores rate commitments of memberships function root() external view returns (uint256) { - return LazyIMT.root(imtData, DEPTH); + return LazyIMT.root(merkleTree, MERKLE_TREE_DEPTH); } - /// @notice Returns the merkle proof elements of a given membership - /// @param index The index of the member - /// @return The merkle proof elements of the member - function merkleProofElements(uint40 index) public view returns (uint256[DEPTH] memory) { - uint256[DEPTH] memory castedProof; - uint256[] memory proof = LazyIMT.merkleProofElements(imtData, index, DEPTH); - for (uint8 i = 0; i < DEPTH; i++) { - castedProof[i] = proof[i]; + /// @notice Returns the Merkle proof that a given membership is in the membership set + /// @param index The index of the membership + /// @return The Merkle proof (an array of MERKLE_TREE_DEPTH elements) + function getMerkleProof(uint40 index) public view returns (uint256[MERKLE_TREE_DEPTH] memory) { + uint256[] memory dynamicSizeProof = LazyIMT.merkleProofElements(merkleTree, index, MERKLE_TREE_DEPTH); + uint256[MERKLE_TREE_DEPTH] memory fixedSizeProof; + for (uint8 i = 0; i < MERKLE_TREE_DEPTH; i++) { + fixedSizeProof[i] = dynamicSizeProof[i]; } - return castedProof; + return fixedSizeProof; } - /// @notice Extend a membership expiration date. Memberships must be on grace period - /// @param idCommitments list of idcommitments - function extend(uint256[] calldata idCommitments) external { + /// @notice Extend a grace-period membership under the same conditions + /// @param idCommitments list of idCommitments of memberships to extend + function extendMemberships(uint256[] calldata idCommitments) external { for (uint256 i = 0; i < idCommitments.length; i++) { - uint256 idCommitment = idCommitments[i]; - _extendMembership(_msgSender(), idCommitment); + _extendMembership(_msgSender(), idCommitments[i]); } } - /// @notice Remove expired memberships or owned memberships in grace period. - /// The user can determine offchain which expired memberships slots - /// are available, and proceed to free them. - /// This is also used to erase memberships in grace period if they're - /// held by the sender. The sender can then withdraw the tokens. - /// @param idCommitments list of idcommitments of the memberships + /// @notice Erase expired memberships or owned grace-period memberships + /// The user can select expired memberships offchain, and proceed to erase them. + /// The holder can use this function to erase their own grace-period memberships. + /// The holder can then withdraw the deposited tokens. + /// @param idCommitments The list of idCommitments of the memberships to erase + /// set function eraseMemberships(uint256[] calldata idCommitments) external { - for (uint256 i = 0; i < idCommitments.length; i++) { - uint256 idCommitment = idCommitments[i]; - MembershipInfo memory mdetails = members[idCommitment]; - if (mdetails.userMessageLimit == 0) revert InvalidIdCommitment(idCommitment); - _eraseMembership(_msgSender(), idCommitment, mdetails); - LazyIMT.update(imtData, 0, mdetails.index); + _eraseMemberships(idCommitments, false); + } + + /// @notice Erase expired memberships or owned grace-period memberships + /// Optionally, also erase rate commitment data from the membership set (clean-up). + /// Compared to eraseMemberships(idCommitments), + /// this function decreases Merkle tree size and spends more gas (if eraseFromMembershipSet == true). + /// @param idCommitments The list of idCommitments of the memberships to erase + /// @param eraseFromMembershipSet Indicates whether to erase membership data from the membership set + function eraseMemberships(uint256[] calldata idCommitments, bool eraseFromMembershipSet) external { + _eraseMemberships(idCommitments, eraseFromMembershipSet); + } + + /// @dev Erase memberships from the list of idCommitments + /// @param idCommitmentsToErase The idCommitments of memberships to erase from storage + /// @param eraseFromMembershipSet Indicates whether to erase membership data from the membership set + function _eraseMemberships(uint256[] calldata idCommitmentsToErase, bool eraseFromMembershipSet) internal { + // eraseFromMembershipSet == true means full clean-up. + // Erase memberships from memberships array (free up the rate limit and index), + // and erase the rate commitment from the membership set (reduce the Merkle tree size). + // eraseFromMembershipSet == false means lazy erasure. + // Only erase memberships from the memberships array (consume less gas). + // Merkle tree data will be overwritten when the correspondind index is reused. + for (uint256 i = 0; i < idCommitmentsToErase.length; i++) { + // Erase the membership from the memberships array in contract storage + uint32 indexToErase = _eraseMembershipLazily(_msgSender(), idCommitmentsToErase[i]); + // Optionally, also erase the rate commitment data from the membership set. + // This does not affect the total rate limit control, or index reusal for new membership registrations. + if (eraseFromMembershipSet) { + LazyIMT.update(merkleTree, 0, indexToErase); + } } } - /// @notice Withdraw any available balance in tokens after a membership is erased. - /// @param token The address of the token to withdraw. Use 0x000...000 to withdraw ETH + /// @notice Withdraw any available deposit balance in tokens after a membership is erased + /// @param token The address of the token to withdraw function withdraw(address token) external { _withdraw(_msgSender(), token); } @@ -271,37 +268,39 @@ contract WakuRlnV2 is Initializable, Ownable2StepUpgradeable, UUPSUpgradeable, M priceCalculator = IPriceCalculator(_priceCalculator); } - /// @notice Set the maximum total rate limit of all memberships in the tree - /// @param _maxTotalRateLimitPerEpoch new value - function setMaxTotalRateLimitPerEpoch(uint32 _maxTotalRateLimitPerEpoch) external onlyOwner { - require(_maxTotalRateLimitPerEpoch >= maxRateLimitPerMembership); - maxTotalRateLimitPerEpoch = _maxTotalRateLimitPerEpoch; + /// @notice Set the maximum total rate limit of all memberships in the membership set + /// @param _maxTotalRateLimit new maximum total rate limit (messages per epoch) + function setMaxTotalRateLimit(uint32 _maxTotalRateLimit) external onlyOwner { + require(maxMembershipRateLimit <= _maxTotalRateLimit); + maxTotalRateLimit = _maxTotalRateLimit; } /// @notice Set the maximum rate limit of one membership - /// @param _maxRateLimitPerMembership new value - function setMaxRateLimitPerMembership(uint32 _maxRateLimitPerMembership) external onlyOwner { - require(_maxRateLimitPerMembership >= minRateLimitPerMembership); - maxRateLimitPerMembership = _maxRateLimitPerMembership; + /// @param _maxMembershipRateLimit new maximum rate limit per membership (messages per epoch) + function setMaxMembershipRateLimit(uint32 _maxMembershipRateLimit) external onlyOwner { + require(minMembershipRateLimit <= _maxMembershipRateLimit); + maxMembershipRateLimit = _maxMembershipRateLimit; } /// @notice Set the minimum rate limit of one membership - /// @param _minRateLimitPerMembership new value - function setMinRateLimitPerMembership(uint32 _minRateLimitPerMembership) external onlyOwner { - require(_minRateLimitPerMembership > 0); - minRateLimitPerMembership = _minRateLimitPerMembership; + /// @param _minMembershipRateLimit new minimum rate limit per membership (messages per epoch) + function setMinMembershipRateLimit(uint32 _minMembershipRateLimit) external onlyOwner { + require(_minMembershipRateLimit > 0); + require(_minMembershipRateLimit <= maxMembershipRateLimit); + minMembershipRateLimit = _minMembershipRateLimit; } - /// @notice Set the membership expiration term - /// @param _expirationTerm new value - function setExpirationTerm(uint32 _expirationTerm) external onlyOwner { - require(_expirationTerm > 0); - expirationTerm = _expirationTerm; + /// @notice Set the active duration for new memberships (terms of existing memberships don't change) + /// @param _activeDurationForNewMembership new active duration + function setActiveDuration(uint32 _activeDurationForNewMembership) external onlyOwner { + require(_activeDurationForNewMembership > 0); + activeDurationForNewMemberships = _activeDurationForNewMembership; } - /// @notice Set the membership grace period - /// @param _gracePeriod new value - function setGracePeriod(uint32 _gracePeriod) external onlyOwner { - gracePeriod = _gracePeriod; + /// @notice Set the grace period for new memberships (terms of existing memberships don't change) + /// @param _gracePeriodDurationForNewMembership new grace period duration + function setGracePeriodDuration(uint32 _gracePeriodDurationForNewMembership) external onlyOwner { + // Note: grace period duration may be equal to zero + gracePeriodDurationForNewMemberships = _gracePeriodDurationForNewMembership; } } diff --git a/test/WakuRlnV2.t.sol b/test/WakuRlnV2.t.sol index 4818887..49c7898 100644 --- a/test/WakuRlnV2.t.sol +++ b/test/WakuRlnV2.t.sol @@ -22,6 +22,8 @@ contract WakuRlnV2Test is Test { address internal deployer; + uint256[] noIdCommitmentsToErase = new uint256[](0); + function setUp() public virtual { token = new TestToken(); @@ -37,19 +39,19 @@ contract WakuRlnV2Test is Test { vm.pauseGasMetering(); // Merkle tree leaves are calculated using 2 as rateLimit vm.prank(w.owner()); - w.setMinRateLimitPerMembership(2); + w.setMinMembershipRateLimit(2); uint256 idCommitment = 2; - uint32 userMessageLimit = 2; - (, uint256 price) = w.priceCalculator().calculate(userMessageLimit); + uint32 membershipRateLimit = 2; + (, uint256 price) = w.priceCalculator().calculate(membershipRateLimit); vm.resumeGasMetering(); token.approve(address(w), price); - w.register(idCommitment, userMessageLimit); + w.register(idCommitment, membershipRateLimit, noIdCommitmentsToErase); vm.pauseGasMetering(); - assertEq(w.nextCommitmentIndex(), 1); - assertEq(w.memberExists(idCommitment), true); - (,,,,, uint32 fetchedUserMessageLimit, uint32 index, address holder,) = w.members(idCommitment); - assertEq(fetchedUserMessageLimit, userMessageLimit); + assertEq(w.nextFreeIndex(), 1); + assertEq(w.isInMembershipSet(idCommitment), true); + (,,,, uint32 membershipRateLimit1, uint32 index, address holder,) = w.memberships(idCommitment); + assertEq(membershipRateLimit1, membershipRateLimit); assertEq(holder, address(this)); assertEq(index, 0); // kats from zerokit @@ -59,12 +61,11 @@ contract WakuRlnV2Test is Test { w.root(), 13_801_897_483_540_040_307_162_267_952_866_411_686_127_372_014_953_358_983_481_592_640_000_001_877_295 ); - (uint32 fetchedUserMessageLimit2, uint32 index2, uint256 rateCommitment2) = - w.idCommitmentToMetadata(idCommitment); - assertEq(fetchedUserMessageLimit2, userMessageLimit); + (uint32 membershipRateLimit2, uint32 index2, uint256 rateCommitment2) = w.getMembershipInfo(idCommitment); + assertEq(membershipRateLimit2, membershipRateLimit); assertEq(index2, 0); assertEq(rateCommitment2, rateCommitment); - uint256[20] memory proof = w.merkleProofElements(0); + uint256[20] memory proof = w.getMerkleProof(0); uint256[20] memory expectedProof = [ 0, 14_744_269_619_966_411_208_579_211_824_598_458_697_587_494_354_926_760_081_771_325_075_741_142_829_156, @@ -93,93 +94,58 @@ contract WakuRlnV2Test is Test { vm.resumeGasMetering(); } - function test__ValidRegistration(uint32 userMessageLimit) external { + function test__ValidRegistration(uint32 membershipRateLimit) external { vm.pauseGasMetering(); uint256 idCommitment = 2; - (, uint256 price) = w.priceCalculator().calculate(userMessageLimit); - uint256 minUserMessageLimit = w.minRateLimitPerMembership(); - uint256 maxUserMessageLimit = w.maxRateLimitPerMembership(); - vm.assume(userMessageLimit >= minUserMessageLimit && userMessageLimit <= maxUserMessageLimit); - vm.assume(w.isValidUserMessageLimit(userMessageLimit)); + (, uint256 price) = w.priceCalculator().calculate(membershipRateLimit); + uint256 minMembershipRateLimit = w.minMembershipRateLimit(); + uint256 maxMembershipRateLimit = w.maxMembershipRateLimit(); + vm.assume(minMembershipRateLimit <= membershipRateLimit && membershipRateLimit <= maxMembershipRateLimit); + vm.assume(w.isValidMembershipRateLimit(membershipRateLimit)); vm.resumeGasMetering(); - assertEq(w.memberExists(idCommitment), false); + assertEq(w.isInMembershipSet(idCommitment), false); token.approve(address(w), price); - w.register(idCommitment, userMessageLimit); - uint256 rateCommitment = PoseidonT3.hash([idCommitment, userMessageLimit]); + w.register(idCommitment, membershipRateLimit, noIdCommitmentsToErase); + uint256 rateCommitment = PoseidonT3.hash([idCommitment, membershipRateLimit]); - (uint32 fetchedUserMessageLimit, uint32 index, uint256 fetchedRateCommitment) = - w.idCommitmentToMetadata(idCommitment); - assertEq(fetchedUserMessageLimit, userMessageLimit); + (uint32 fetchedMembershipRateLimit, uint32 index, uint256 fetchedRateCommitment) = + w.getMembershipInfo(idCommitment); + assertEq(fetchedMembershipRateLimit, membershipRateLimit); assertEq(index, 0); assertEq(fetchedRateCommitment, rateCommitment); assertEq(token.balanceOf(address(w)), price); - assertEq(w.totalRateLimitPerEpoch(), userMessageLimit); - } - - function test__InsertionNormalOrder(uint32 idCommitmentsLength) external { - vm.assume(idCommitmentsLength > 0 && idCommitmentsLength <= 50); - - uint32 userMessageLimit = w.minRateLimitPerMembership(); - (, uint256 price) = w.priceCalculator().calculate(userMessageLimit); - - // Register some commitments - for (uint256 i = 0; i < idCommitmentsLength; i++) { - uint256 idCommitment = i + 1; - token.approve(address(w), price); - w.register(idCommitment, userMessageLimit); - (uint256 prev, uint256 next,,,,,,,) = w.members(idCommitment); - // new membership will always be the tail - assertEq(next, 0); - assertEq(w.tail(), idCommitment); - // current membership prevLink will always point to previous membership - assertEq(prev, idCommitment - 1); - } - assertEq(w.head(), 1); - assertEq(w.tail(), idCommitmentsLength); - - // Ensure that prev and next are chained correctly - for (uint256 i = 0; i < idCommitmentsLength; i++) { - uint256 idCommitment = i + 1; - (uint256 prev, uint256 next,,,,,,,) = w.members(idCommitment); - - assertEq(prev, idCommitment - 1); - if (i == idCommitmentsLength - 1) { - assertEq(next, 0); - } else { - assertEq(next, idCommitment + 1); - } - } + assertEq(w.currentTotalRateLimit(), membershipRateLimit); } - function test__LinearPriceCalculation(uint32 userMessageLimit) external view { + function test__LinearPriceCalculation(uint32 membershipRateLimit) external view { IPriceCalculator priceCalculator = w.priceCalculator(); uint256 pricePerMessagePerPeriod = LinearPriceCalculator(address(priceCalculator)).pricePerMessagePerEpoch(); assertNotEq(pricePerMessagePerPeriod, 0); - uint256 expectedPrice = uint256(userMessageLimit) * pricePerMessagePerPeriod; - (, uint256 price) = w.priceCalculator().calculate(userMessageLimit); + uint256 expectedPrice = uint256(membershipRateLimit) * pricePerMessagePerPeriod; + (, uint256 price) = w.priceCalculator().calculate(membershipRateLimit); assertEq(price, expectedPrice); } - function test__InvalidTokenAmount(uint256 idCommitment, uint32 userMessageLimit) external { + function test__InvalidTokenAmount(uint256 idCommitment, uint32 membershipRateLimit) external { vm.pauseGasMetering(); - uint256 minUserMessageLimit = w.minRateLimitPerMembership(); - uint256 maxUserMessageLimit = w.maxRateLimitPerMembership(); - vm.assume(userMessageLimit >= minUserMessageLimit && userMessageLimit <= maxUserMessageLimit); - vm.assume(w.isValidCommitment(idCommitment) && w.isValidUserMessageLimit(userMessageLimit)); - (, uint256 price) = w.priceCalculator().calculate(userMessageLimit); + uint256 minMembershipRateLimit = w.minMembershipRateLimit(); + uint256 maxMembershipRateLimit = w.maxMembershipRateLimit(); + vm.assume(minMembershipRateLimit <= membershipRateLimit && membershipRateLimit <= maxMembershipRateLimit); + vm.assume(w.isValidIdCommitment(idCommitment) && w.isValidMembershipRateLimit(membershipRateLimit)); + (, uint256 price) = w.priceCalculator().calculate(membershipRateLimit); vm.resumeGasMetering(); token.approve(address(w), price - 1); vm.expectRevert(bytes("ERC20: insufficient allowance")); - w.register(idCommitment, userMessageLimit); + w.register(idCommitment, membershipRateLimit, noIdCommitmentsToErase); } function test__IdCommitmentToMetadata__DoesntExist() external view { uint256 idCommitment = 2; - (uint32 userMessageLimit, uint32 index, uint256 rateCommitment) = w.idCommitmentToMetadata(idCommitment); - assertEq(userMessageLimit, 0); + (uint32 membershipRateLimit, uint32 index, uint256 rateCommitment) = w.getMembershipInfo(idCommitment); + assertEq(membershipRateLimit, 0); assertEq(index, 0); assertEq(rateCommitment, 0); } @@ -187,546 +153,404 @@ contract WakuRlnV2Test is Test { function test__InvalidRegistration__InvalidIdCommitment__Zero() external { vm.pauseGasMetering(); uint256 idCommitment = 0; - uint32 userMessageLimit = 2; - (, uint256 price) = w.priceCalculator().calculate(userMessageLimit); + uint32 membershipRateLimit = 2; + (, uint256 price) = w.priceCalculator().calculate(membershipRateLimit); vm.resumeGasMetering(); token.approve(address(w), price); vm.expectRevert(abi.encodeWithSelector(InvalidIdCommitment.selector, 0)); - w.register(idCommitment, userMessageLimit); + w.register(idCommitment, membershipRateLimit, noIdCommitmentsToErase); } function test__InvalidRegistration__InvalidIdCommitment__LargerThanField() external { vm.pauseGasMetering(); - uint32 userMessageLimit = 20; - (, uint256 price) = w.priceCalculator().calculate(userMessageLimit); + uint32 membershipRateLimit = 20; + (, uint256 price) = w.priceCalculator().calculate(membershipRateLimit); vm.resumeGasMetering(); uint256 idCommitment = w.Q() + 1; token.approve(address(w), price); vm.expectRevert(abi.encodeWithSelector(InvalidIdCommitment.selector, idCommitment)); - w.register(idCommitment, userMessageLimit); + w.register(idCommitment, membershipRateLimit, noIdCommitmentsToErase); } - function test__InvalidRegistration__InvalidUserMessageLimit__MinMax() external { + function test__InvalidRegistration__InvalidMembershipRateLimit__MinMax() external { uint256 idCommitment = 2; - uint32 invalidMin = w.minRateLimitPerMembership() - 1; - uint32 invalidMax = w.maxRateLimitPerMembership() + 1; + uint32 invalidMin = w.minMembershipRateLimit() - 1; + uint32 invalidMax = w.maxMembershipRateLimit() + 1; - vm.expectRevert(abi.encodeWithSelector(InvalidRateLimit.selector)); - w.register(idCommitment, invalidMin); + vm.expectRevert(abi.encodeWithSelector(InvalidMembershipRateLimit.selector)); + w.register(idCommitment, invalidMin, noIdCommitmentsToErase); - vm.expectRevert(abi.encodeWithSelector(InvalidRateLimit.selector)); - w.register(idCommitment, invalidMax); + vm.expectRevert(abi.encodeWithSelector(InvalidMembershipRateLimit.selector)); + w.register(idCommitment, invalidMax, noIdCommitmentsToErase); } - function test__ValidRegistrationExtend(uint32 userMessageLimit) external { + function test__ValidRegistrationExtend(uint32 membershipRateLimit) external { vm.pauseGasMetering(); uint256 idCommitment = 2; - (, uint256 price) = w.priceCalculator().calculate(userMessageLimit); + (, uint256 price) = w.priceCalculator().calculate(membershipRateLimit); vm.assume( - userMessageLimit >= w.minRateLimitPerMembership() && userMessageLimit <= w.maxRateLimitPerMembership() + w.minMembershipRateLimit() <= membershipRateLimit && membershipRateLimit <= w.maxMembershipRateLimit() ); - vm.assume(w.isValidUserMessageLimit(userMessageLimit)); + vm.assume(w.isValidMembershipRateLimit(membershipRateLimit)); vm.resumeGasMetering(); token.approve(address(w), price); - w.register(idCommitment, userMessageLimit); - (,,, uint256 gracePeriodStartDate,,,,,) = w.members(idCommitment); + w.register(idCommitment, membershipRateLimit, noIdCommitmentsToErase); + (,, uint256 gracePeriodStartTimestamp,,,,,) = w.memberships(idCommitment); - assertFalse(w.isGracePeriod(idCommitment)); + assertFalse(w.isInGracePeriod(idCommitment)); assertFalse(w.isExpired(idCommitment)); - vm.warp(gracePeriodStartDate); + vm.warp(gracePeriodStartTimestamp); - assertTrue(w.isGracePeriod(idCommitment)); + assertTrue(w.isInGracePeriod(idCommitment)); assertFalse(w.isExpired(idCommitment)); - // Registering other memberships just to check linkage is correct - for (uint256 i = 1; i < 5; i++) { - token.approve(address(w), price); - w.register(idCommitment + i, userMessageLimit); - } - - assertEq(w.head(), idCommitment); - uint256[] memory commitmentsToExtend = new uint256[](1); commitmentsToExtend[0] = idCommitment; // Attempt to extend the membership (but it is not owned by us) address randomAddress = vm.addr(block.timestamp); vm.prank(randomAddress); - vm.expectRevert(abi.encodeWithSelector(NotHolder.selector, commitmentsToExtend[0])); - w.extend(commitmentsToExtend); + vm.expectRevert(abi.encodeWithSelector(NonHolderCannotExtend.selector, commitmentsToExtend[0])); + w.extendMemberships(commitmentsToExtend); // Attempt to extend the membership (but now we are the owner) vm.expectEmit(true, false, false, false); // only check the first parameter of the event (the idCommitment) - emit MembershipUpgradeable.MemberExtended(idCommitment, 0, 0, 0); - w.extend(commitmentsToExtend); + emit MembershipUpgradeable.MembershipExtended(idCommitment, 0, 0, 0); - (,,, uint256 newGracePeriodStartDate,,,,,) = w.members(idCommitment); + (, uint256 oldActiveDuration, uint256 oldGracePeriodStartTimestamp, uint32 oldGracePeriodDuration,,,,) = + w.memberships(idCommitment); + w.extendMemberships(commitmentsToExtend); + (, uint256 newActiveDuration, uint256 newGracePeriodStartTimestamp, uint32 newGracePeriodDuration,,,,) = + w.memberships(idCommitment); - assertEq(block.timestamp + uint256(w.expirationTerm()), newGracePeriodStartDate); - assertFalse(w.isGracePeriod(idCommitment)); + assertEq(oldActiveDuration, newActiveDuration); + assertEq(oldGracePeriodDuration, newGracePeriodDuration); + assertEq( + oldGracePeriodStartTimestamp + oldGracePeriodDuration + newActiveDuration, newGracePeriodStartTimestamp + ); + assertFalse(w.isInGracePeriod(idCommitment)); assertFalse(w.isExpired(idCommitment)); - // Verify list order is correct - assertEq(w.tail(), idCommitment); - assertEq(w.head(), idCommitment + 1); - - // Ensure that prev and next are chained correctly - for (uint256 i = 0; i < 5; i++) { - uint256 currIdCommitment = idCommitment + i; - (uint256 prev, uint256 next,,,,,,,) = w.members(currIdCommitment); - console.log("idCommitment: %s - prev: %s - next: %s", currIdCommitment, prev, next); - if (i == 0) { - // Verifying links of extended idCommitment - assertEq(next, 0); - assertEq(prev, idCommitment + 4); - } else if (i == 1) { - // The second element in the chain became the oldest - assertEq(next, currIdCommitment + 1); - assertEq(prev, 0); - } else if (i == 4) { - assertEq(prev, currIdCommitment - 1); - assertEq(next, idCommitment); - } else { - // The rest of the elements maintain their order - assertEq(prev, currIdCommitment - 1); - assertEq(next, currIdCommitment + 1); - } - } - // Attempt to extend a non grace period membership + token.approve(address(w), price); + w.register(idCommitment + 1, membershipRateLimit, noIdCommitmentsToErase); commitmentsToExtend[0] = idCommitment + 1; - vm.expectRevert(abi.encodeWithSelector(NotInGracePeriod.selector, commitmentsToExtend[0])); - w.extend(commitmentsToExtend); + vm.expectRevert(abi.encodeWithSelector(CannotExtendNonGracePeriodMembership.selector, commitmentsToExtend[0])); + w.extendMemberships(commitmentsToExtend); } - function test__ValidRegistrationExtendSingleMembership(uint32 userMessageLimit) external { + function test__ValidRegistrationNoGracePeriod(uint32 membershipRateLimit) external { vm.pauseGasMetering(); uint256 idCommitment = 2; - (, uint256 price) = w.priceCalculator().calculate(userMessageLimit); + (, uint256 price) = w.priceCalculator().calculate(membershipRateLimit); vm.assume( - userMessageLimit >= w.minRateLimitPerMembership() && userMessageLimit <= w.maxRateLimitPerMembership() + w.minMembershipRateLimit() <= membershipRateLimit && membershipRateLimit <= w.maxMembershipRateLimit() ); - vm.assume(w.isValidUserMessageLimit(userMessageLimit)); + vm.assume(w.isValidMembershipRateLimit(membershipRateLimit)); + + vm.startPrank(w.owner()); + w.setGracePeriodDuration(0); + vm.stopPrank(); + vm.resumeGasMetering(); token.approve(address(w), price); - w.register(idCommitment, userMessageLimit); - (,,, uint256 gracePeriodStartDate,,,,,) = w.members(idCommitment); + w.register(idCommitment, membershipRateLimit, noIdCommitmentsToErase); - vm.warp(gracePeriodStartDate); + (,, uint256 gracePeriodStartTimestamp, uint32 gracePeriodDuration,,,,) = w.memberships(idCommitment); + + assertEq(gracePeriodDuration, 0); + + assertFalse(w.isInGracePeriod(idCommitment)); + assertFalse(w.isExpired(idCommitment)); + + uint256 expectedExpirationTimestamp = gracePeriodStartTimestamp + uint256(gracePeriodDuration); + uint256 membershipExpirationTimestamp = w.membershipExpirationTimestamp(idCommitment); + + assertEq(expectedExpirationTimestamp, membershipExpirationTimestamp); + + vm.warp(membershipExpirationTimestamp); + + assertFalse(w.isInGracePeriod(idCommitment)); + assertTrue(w.isExpired(idCommitment)); + } + + function test__ValidRegistrationExtendSingleMembership(uint32 membershipRateLimit) external { + vm.pauseGasMetering(); + uint256 idCommitment = 2; + (, uint256 price) = w.priceCalculator().calculate(membershipRateLimit); + vm.assume( + w.minMembershipRateLimit() <= membershipRateLimit && membershipRateLimit <= w.maxMembershipRateLimit() + ); + vm.assume(w.isValidMembershipRateLimit(membershipRateLimit)); + vm.resumeGasMetering(); + + token.approve(address(w), price); + w.register(idCommitment, membershipRateLimit, noIdCommitmentsToErase); + uint256 ogExpirationTimestamp = w.membershipExpirationTimestamp(idCommitment); + (,, uint256 gracePeriodStartTimestamp,,,,,) = w.memberships(idCommitment); + + vm.warp(gracePeriodStartTimestamp); uint256[] memory commitmentsToExtend = new uint256[](1); commitmentsToExtend[0] = idCommitment; // Extend the membership vm.expectEmit(true, false, false, false); // only check the first parameter of the event (the idCommitment) - emit MembershipUpgradeable.MemberExtended(idCommitment, 0, 0, 0); - w.extend(commitmentsToExtend); - - // Verify list order is correct - assertEq(w.tail(), idCommitment); - assertEq(w.head(), idCommitment); - (uint256 prev, uint256 next,,,,,,,) = w.members(idCommitment); - assertEq(next, 0); - assertEq(prev, 0); + emit MembershipUpgradeable.MembershipExtended(idCommitment, 0, 0, 0); + w.extendMemberships(commitmentsToExtend); + + (,, uint256 newGracePeriodStartTimestamp, uint32 newGracePeriodDuration,,,,) = w.memberships(idCommitment); + uint256 expectedExpirationTimestamp = newGracePeriodStartTimestamp + uint256(newGracePeriodDuration); + uint256 membershipExpirationTimestamp = w.membershipExpirationTimestamp(idCommitment); + assertEq(expectedExpirationTimestamp, membershipExpirationTimestamp); + assertTrue(expectedExpirationTimestamp > ogExpirationTimestamp); } - function test__ValidRegistrationExpiry(uint32 userMessageLimit) external { + function test__ValidRegistrationExpiry(uint32 membershipRateLimit) external { vm.pauseGasMetering(); uint256 idCommitment = 2; - (, uint256 price) = w.priceCalculator().calculate(userMessageLimit); + (, uint256 price) = w.priceCalculator().calculate(membershipRateLimit); vm.assume( - userMessageLimit >= w.minRateLimitPerMembership() && userMessageLimit <= w.maxRateLimitPerMembership() + w.minMembershipRateLimit() <= membershipRateLimit && membershipRateLimit <= w.maxMembershipRateLimit() ); - vm.assume(w.isValidUserMessageLimit(userMessageLimit)); + vm.assume(w.isValidMembershipRateLimit(membershipRateLimit)); vm.resumeGasMetering(); token.approve(address(w), price); - w.register(idCommitment, userMessageLimit); + w.register(idCommitment, membershipRateLimit, noIdCommitmentsToErase); - (,,, uint256 fetchedGracePeriodStartDate, uint32 fetchedGracePeriod,,,,) = w.members(idCommitment); + (,, uint256 fetchedgracePeriodStartTimestamp, uint32 fetchedGracePeriod,,,,) = w.memberships(idCommitment); - uint256 expectedExpirationDate = fetchedGracePeriodStartDate + uint256(fetchedGracePeriod) + 1; - uint256 expirationDate = w.expirationDate(idCommitment); + uint256 expectedExpirationTimestamp = fetchedgracePeriodStartTimestamp + uint256(fetchedGracePeriod); + uint256 membershipExpirationTimestamp = w.membershipExpirationTimestamp(idCommitment); - assertEq(expectedExpirationDate, expirationDate); + assertEq(expectedExpirationTimestamp, membershipExpirationTimestamp); - vm.warp(expirationDate); + vm.warp(membershipExpirationTimestamp); - assertFalse(w.isGracePeriod(idCommitment)); + assertFalse(w.isInGracePeriod(idCommitment)); assertTrue(w.isExpired(idCommitment)); - - // Registering other memberships just to check linkage is correct - for (uint256 i = 1; i <= 5; i++) { - token.approve(address(w), price); - w.register(idCommitment + i, userMessageLimit); - } - - assertEq(w.head(), idCommitment); - assertEq(w.tail(), idCommitment + 5); } function test__ValidRegistrationWithEraseList() external { vm.pauseGasMetering(); vm.startPrank(w.owner()); - w.setMinRateLimitPerMembership(20); - w.setMaxRateLimitPerMembership(100); - w.setMaxTotalRateLimitPerEpoch(100); + w.setMinMembershipRateLimit(20); + w.setMaxMembershipRateLimit(100); + w.setMaxTotalRateLimit(100); vm.stopPrank(); vm.resumeGasMetering(); - (, uint256 price) = w.priceCalculator().calculate(20); + (, uint256 priceA) = w.priceCalculator().calculate(20); for (uint256 i = 1; i <= 5; i++) { - token.approve(address(w), price); - w.register(i, 20); + token.approve(address(w), priceA); + w.register(i, 20, noIdCommitmentsToErase); // Make sure they're expired - vm.warp(w.expirationDate(i)); + vm.warp(w.membershipExpirationTimestamp(i)); } - // Time travel to a point in which the last commitment is active - (,,, uint256 gracePeriodStartDate,,,,,) = w.members(5); - vm.warp(gracePeriodStartDate - 1); + // Time travel to a point in which the last membership is active + (,, uint256 gracePeriodStartTimestamp,,,,,) = w.memberships(5); + vm.warp(gracePeriodStartTimestamp - 1); // Ensure that this is the case assertTrue(w.isExpired(4)); assertFalse(w.isExpired(5)); - assertFalse(w.isGracePeriod(5)); + assertFalse(w.isInGracePeriod(5)); - (, price) = w.priceCalculator().calculate(60); - token.approve(address(w), price); + (, uint256 priceB) = w.priceCalculator().calculate(60); + token.approve(address(w), priceB); - // Attempt to expire 3 commitments including one that can't be erased (the last one) + // Should fail. There's not enough free rate limit + vm.expectRevert(abi.encodeWithSelector(CannotExceedMaxTotalRateLimit.selector)); + w.register(6, 60, noIdCommitmentsToErase); + + // Attempt to erase 3 memberships including one that can't be erased (the last one) uint256[] memory commitmentsToErase = new uint256[](3); commitmentsToErase[0] = 1; commitmentsToErase[1] = 2; commitmentsToErase[2] = 5; // This one is still active - token.approve(address(w), price); - vm.expectRevert(abi.encodeWithSelector(CantEraseMembership.selector, 5)); + token.approve(address(w), priceB); + vm.expectRevert(abi.encodeWithSelector(CannotEraseActiveMembership.selector, 5)); w.register(6, 60, commitmentsToErase); - // Attempt to expire 3 commitments that can be erased + // Attempt to erase 3 memberships that can be erased commitmentsToErase[2] = 4; vm.expectEmit(true, false, false, false); - emit MembershipUpgradeable.MemberExpired(1, 0, 0); + emit MembershipUpgradeable.MembershipExpired(1, 0, 0); vm.expectEmit(true, false, false, false); - emit MembershipUpgradeable.MemberExpired(2, 0, 0); + emit MembershipUpgradeable.MembershipExpired(2, 0, 0); vm.expectEmit(true, false, false, false); - emit MembershipUpgradeable.MemberExpired(4, 0, 0); + emit MembershipUpgradeable.MembershipExpired(4, 0, 0); w.register(6, 60, commitmentsToErase); // Ensure that the chosen memberships were erased and others unaffected address holder; - (,,,,,,, holder,) = w.members(1); + (,,,,,, holder,) = w.memberships(1); assertEq(holder, address(0)); - (,,,,,,, holder,) = w.members(2); + (,,,,,, holder,) = w.memberships(2); assertEq(holder, address(0)); - (,,,,,,, holder,) = w.members(3); + (,,,,,, holder,) = w.memberships(3); assertEq(holder, address(this)); - (,,,,,,, holder,) = w.members(4); + (,,,,,, holder,) = w.memberships(4); assertEq(holder, address(0)); - (,,,,,,, holder,) = w.members(5); + (,,,,,, holder,) = w.memberships(5); assertEq(holder, address(this)); - (,,,,,,, holder,) = w.members(6); + (,,,,,, holder,) = w.memberships(6); assertEq(holder, address(this)); + + // The balance available for withdrawal should match the amount of the expired membership + uint256 availableBalance = w.depositsToWithdraw(address(this), address(token)); + assertEq(availableBalance, priceA * 3); } function test__RegistrationWhenMaxRateLimitIsReached() external { vm.pauseGasMetering(); vm.startPrank(w.owner()); - w.setMinRateLimitPerMembership(1); - w.setMaxRateLimitPerMembership(5); - w.setMaxTotalRateLimitPerEpoch(5); + w.setMinMembershipRateLimit(1); + w.setMaxMembershipRateLimit(5); + w.setMaxTotalRateLimit(5); vm.stopPrank(); vm.resumeGasMetering(); - bool isValid = w.isValidUserMessageLimit(6); + bool isValid = w.isValidMembershipRateLimit(6); assertFalse(isValid); - // Exceeds the max rate limit per user - uint32 userMessageLimit = 10; - (, uint256 price) = w.priceCalculator().calculate(userMessageLimit); + // Exceeds the max rate limit per membership + uint32 membershipRateLimit = 10; + (, uint256 price) = w.priceCalculator().calculate(membershipRateLimit); token.approve(address(w), price); - vm.expectRevert(abi.encodeWithSelector(InvalidRateLimit.selector)); - w.register(1, userMessageLimit); + vm.expectRevert(abi.encodeWithSelector(InvalidMembershipRateLimit.selector)); + w.register(1, membershipRateLimit, noIdCommitmentsToErase); // Should register succesfully - userMessageLimit = 4; - (, price) = w.priceCalculator().calculate(userMessageLimit); + membershipRateLimit = 4; + (, price) = w.priceCalculator().calculate(membershipRateLimit); token.approve(address(w), price); - w.register(2, userMessageLimit); + w.register(2, membershipRateLimit, noIdCommitmentsToErase); // Exceeds the rate limit - userMessageLimit = 2; - (, price) = w.priceCalculator().calculate(userMessageLimit); + membershipRateLimit = 2; + (, price) = w.priceCalculator().calculate(membershipRateLimit); token.approve(address(w), price); - vm.expectRevert(abi.encodeWithSelector(ExceedAvailableMaxRateLimitPerEpoch.selector)); - w.register(3, userMessageLimit); + vm.expectRevert(abi.encodeWithSelector(CannotExceedMaxTotalRateLimit.selector)); + w.register(3, membershipRateLimit, noIdCommitmentsToErase); // Should register succesfully - userMessageLimit = 1; - (, price) = w.priceCalculator().calculate(userMessageLimit); + membershipRateLimit = 1; + (, price) = w.priceCalculator().calculate(membershipRateLimit); token.approve(address(w), price); - w.register(3, userMessageLimit); + w.register(3, membershipRateLimit, noIdCommitmentsToErase); // We ran out of rate limit again - userMessageLimit = 1; - (, price) = w.priceCalculator().calculate(userMessageLimit); + membershipRateLimit = 1; + (, price) = w.priceCalculator().calculate(membershipRateLimit); token.approve(address(w), price); - vm.expectRevert(abi.encodeWithSelector(ExceedAvailableMaxRateLimitPerEpoch.selector)); - w.register(4, userMessageLimit); - } - - function test__RegistrationWhenMaxRateLimitIsReachedAndSingleExpiredMemberAvailable() external { - vm.pauseGasMetering(); - vm.startPrank(w.owner()); - w.setMinRateLimitPerMembership(1); - w.setMaxRateLimitPerMembership(5); - w.setMaxTotalRateLimitPerEpoch(5); - vm.stopPrank(); - vm.resumeGasMetering(); - - uint32 userMessageLimitA = 2; - uint32 totalUserMessageLimit = userMessageLimitA; - (, uint256 priceA) = w.priceCalculator().calculate(userMessageLimitA); - token.approve(address(w), priceA); - w.register(1, userMessageLimitA); - - (,,, uint256 gracePeriodStartDate,,, uint32 indexA,,) = w.members(1); - vm.warp(gracePeriodStartDate + 1); - - // Exceeds the rate limit, but if the first were expired, it should register - // It is in grace period so can't be erased - assertTrue(w.isGracePeriod(1)); - assertFalse(w.isExpired(1)); - uint32 userMessageLimitB = 4; - (, uint256 priceB) = w.priceCalculator().calculate(userMessageLimitB); - (, priceB) = w.priceCalculator().calculate(userMessageLimitB); - token.approve(address(w), priceB); - vm.expectRevert(abi.encodeWithSelector(ExceedAvailableMaxRateLimitPerEpoch.selector)); - w.register(2, userMessageLimitB); - - // FFW until the membership is expired so we can get rid of it - uint256 expirationDate = w.expirationDate(1); - vm.warp(expirationDate); - assertTrue(w.isExpired(1)); - - // It should succeed now - vm.expectEmit(); - emit MembershipUpgradeable.MemberExpired(1, userMessageLimitA, indexA); - w.register(2, userMessageLimitB); - - // The previous expired membership should have been erased - (,,,,,,, address holder,) = w.members(1); - assertEq(holder, address(0)); - - uint32 expectedUserMessageLimit = totalUserMessageLimit - userMessageLimitA + userMessageLimitB; - assertEq(expectedUserMessageLimit, w.totalRateLimitPerEpoch()); - - // The new commitment should be the only element in the list - assertEq(w.head(), 2); - assertEq(w.tail(), 2); - (uint256 prev, uint256 next,,,,, uint32 indexB,,) = w.members(2); - assertEq(prev, 0); - assertEq(next, 0); - - // Index should have been reused - assertEq(indexA, indexB); - - // The balance available for withdrawal should match the amount of the expired membership - uint256 availableBalance = w.balancesToWithdraw(address(this), address(token)); - assertEq(availableBalance, priceA); - } - - function test__RegistrationWhenMaxRateLimitIsReachedAndMultipleExpiredMembersAvailable() external { - vm.pauseGasMetering(); - vm.startPrank(w.owner()); - w.setMinRateLimitPerMembership(1); - w.setMaxRateLimitPerMembership(5); - w.setMaxTotalRateLimitPerEpoch(5); - vm.stopPrank(); - vm.resumeGasMetering(); - - (, uint256 priceA) = w.priceCalculator().calculate(1); - token.approve(address(w), priceA); - w.register(1, 1); - vm.warp(block.timestamp + 100); - token.approve(address(w), priceA); - w.register(2, 1); - vm.warp(block.timestamp + 100); - uint256 expirationDate = w.expirationDate(2); - vm.warp(expirationDate); - token.approve(address(w), priceA); - w.register(3, 1); - - // Make sure only the first 2 memberships are expired - assertTrue(w.isExpired(1)); - assertTrue(w.isExpired(2)); - assertFalse(w.isExpired(3) || w.isGracePeriod(3)); - - (,,,,,, uint32 index1,,) = w.members(1); - (,,,,,, uint32 index2,,) = w.members(2); - - // Attempt to register a membership that will require to expire 2 memberships - // Currently there is 2 available, and we want to register 4 - // If we remove first membership, we'll have 3 available - // If we also remove the second, we'll have 4 available - (, uint256 priceB) = w.priceCalculator().calculate(4); - token.approve(address(w), priceB); - vm.expectEmit(true, false, false, false); - emit MembershipUpgradeable.MemberExpired(1, 0, 0); - vm.expectEmit(true, false, false, false); - emit MembershipUpgradeable.MemberExpired(2, 0, 0); - w.register(4, 4); - - // idCommitment4 will use the last removed index available (since we push to an array) - (,,,,,, uint32 index4,,) = w.members(4); - assertEq(index4, index2); - - // the index of the first removed membership is still available for further registrations - assertEq(index1, w.availableExpiredIndices(0)); - - // The previous expired memberships should have been erased - (,,,,,,, address holder,) = w.members(1); - assertEq(holder, address(0)); - (,,,,,,, holder,) = w.members(2); - assertEq(holder, address(0)); - - // The total rate limit used should be those from idCommitment 3 and 4 - assertEq(5, w.totalRateLimitPerEpoch()); - - // There should only be 2 memberships, the non expired and the new one - assertEq(w.head(), 3); - assertEq(w.tail(), 4); - (uint256 prev, uint256 next,,,,,,,) = w.members(3); - assertEq(prev, 0); - assertEq(next, 4); - (prev, next,,,,,,,) = w.members(4); - assertEq(prev, 3); - assertEq(next, 0); - - // The balance available for withdrawal should match the amount of the expired membership - uint256 availableBalance = w.balancesToWithdraw(address(this), address(token)); - assertEq(availableBalance, priceA * 2); - } - - function test__RegistrationWhenMaxRateLimitReachedAndMultipleExpiredMembersAvailableWithoutEnoughRateLimit() - external - { - vm.pauseGasMetering(); - vm.startPrank(w.owner()); - w.setMinRateLimitPerMembership(1); - w.setMaxRateLimitPerMembership(5); - w.setMaxTotalRateLimitPerEpoch(5); - vm.stopPrank(); - vm.resumeGasMetering(); - - (, uint256 priceA) = w.priceCalculator().calculate(1); - token.approve(address(w), priceA); - w.register(1, 1); - vm.warp(block.timestamp + 100); - token.approve(address(w), priceA); - w.register(2, 1); - vm.warp(block.timestamp + 100); - uint256 expirationDate = w.expirationDate(2); - vm.warp(expirationDate); - token.approve(address(w), priceA); - w.register(3, 1); - - // Make sure only the first 2 memberships are expired - assertTrue(w.isExpired(1)); - assertTrue(w.isExpired(2)); - assertFalse(w.isExpired(3) || w.isGracePeriod(3)); - - // Attempt to register a membership that will require to expire 2 memberships - // Currently there is 2 available, and we want to register 5 - // If we remove first membership, we'll have 3 available - // If we also remove the second, we'll have 4 available, but it is still not enough - // for registering - (, uint256 priceB) = w.priceCalculator().calculate(5); - token.approve(address(w), priceB); - vm.expectRevert(abi.encodeWithSelector(ExceedAvailableMaxRateLimitPerEpoch.selector)); - w.register(4, 5); + vm.expectRevert(abi.encodeWithSelector(CannotExceedMaxTotalRateLimit.selector)); + w.register(4, membershipRateLimit, noIdCommitmentsToErase); } function test__indexReuse_eraseMemberships(uint32 idCommitmentsLength) external { - vm.assume(idCommitmentsLength > 0 && idCommitmentsLength < 50); + vm.assume(0 < idCommitmentsLength && idCommitmentsLength < 50); (, uint256 price) = w.priceCalculator().calculate(20); uint32 index; uint256[] memory commitmentsToErase = new uint256[](idCommitmentsLength); + uint256 time = block.timestamp; for (uint256 i = 1; i <= idCommitmentsLength; i++) { token.approve(address(w), price); - w.register(i, 20); - (,,,,,, index,,) = w.members(i); - assertEq(index, w.nextCommitmentIndex() - 1); + w.register(i, 20, noIdCommitmentsToErase); + (,,,,, index,,) = w.memberships(i); + assertEq(index, w.nextFreeIndex() - 1); commitmentsToErase[i - 1] = i; + time += 100; + vm.warp(time); } + // None of the commitments can be deleted because they're still active + uint256[] memory singleCommitmentToErase = new uint256[](1); + for (uint256 i = 1; i <= idCommitmentsLength; i++) { + singleCommitmentToErase[0] = i; + vm.expectRevert(abi.encodeWithSelector(CannotEraseActiveMembership.selector, i)); + w.eraseMemberships(singleCommitmentToErase); + } + + // Fastfwd to commitment grace period, and try to erase it without being the owner + (,, uint256 gracePeriodStartTimestamp,,,,,) = w.memberships(1); + vm.warp(gracePeriodStartTimestamp); + assertTrue(w.isInGracePeriod(1)); + singleCommitmentToErase[0] = 1; + address randomAddress = vm.addr(block.timestamp); + vm.prank(randomAddress); + vm.expectRevert(abi.encodeWithSelector(NonHolderCannotEraseGracePeriodMembership.selector, 1)); + w.eraseMemberships(singleCommitmentToErase); + // time travel to the moment we can erase all expired memberships - uint256 expirationDate = w.expirationDate(idCommitmentsLength); - vm.warp(expirationDate); + uint256 membershipExpirationTimestamp = w.membershipExpirationTimestamp(idCommitmentsLength); + vm.warp(membershipExpirationTimestamp); w.eraseMemberships(commitmentsToErase); // Verify that expired indices match what we expect for (uint32 i = 0; i < idCommitmentsLength; i++) { - assertEq(i, w.availableExpiredIndices(i)); + assertEq(i, w.indicesOfLazilyErasedMemberships(i)); } - uint32 currnextCommitmentIndex = w.nextCommitmentIndex(); + uint32 expectedNextFreeIndex = w.nextFreeIndex(); for (uint256 i = 1; i <= idCommitmentsLength; i++) { uint256 idCommitment = i + 10; - uint256 expectedReusedIndexPos = idCommitmentsLength - i; - uint32 expectedIndex = w.availableExpiredIndices(expectedReusedIndexPos); + uint256 expectedindexReusedPos = idCommitmentsLength - i; + uint32 expectedReusedIndex = w.indicesOfLazilyErasedMemberships(expectedindexReusedPos); token.approve(address(w), price); - w.register(idCommitment, 20); - (,,,,,, index,,) = w.members(idCommitment); - assertEq(expectedIndex, index); + w.register(idCommitment, 20, noIdCommitmentsToErase); + (,,,,, index,,) = w.memberships(idCommitment); + assertEq(expectedReusedIndex, index); // Should have been removed from the list vm.expectRevert(); - w.availableExpiredIndices(expectedReusedIndexPos); + w.indicesOfLazilyErasedMemberships(expectedindexReusedPos); // Should not have been affected - assertEq(currnextCommitmentIndex, w.nextCommitmentIndex()); + assertEq(expectedNextFreeIndex, w.nextFreeIndex()); } - // No indexes should be available for reuse + // No indices should be available for reuse vm.expectRevert(); - w.availableExpiredIndices(0); + w.indicesOfLazilyErasedMemberships(0); - // Should use a new index since we got rid of all available indexes + // Should use a new index since we got rid of all reusable indexes token.approve(address(w), price); - w.register(100, 20); - (,,,,,, index,,) = w.members(100); - assertEq(index, currnextCommitmentIndex); - assertEq(currnextCommitmentIndex + 1, w.nextCommitmentIndex()); + w.register(100, 20, noIdCommitmentsToErase); + (,,,,, index,,) = w.memberships(100); + assertEq(index, expectedNextFreeIndex); + assertEq(expectedNextFreeIndex + 1, w.nextFreeIndex()); } - function test__RemoveExpiredMemberships(uint32 userMessageLimit) external { + function test__RemoveExpiredMemberships(uint32 membershipRateLimit) external { vm.pauseGasMetering(); uint256 idCommitment = 2; - (, uint256 price) = w.priceCalculator().calculate(userMessageLimit); + (, uint256 price) = w.priceCalculator().calculate(membershipRateLimit); vm.assume( - userMessageLimit >= w.minRateLimitPerMembership() && userMessageLimit <= w.maxRateLimitPerMembership() + w.minMembershipRateLimit() <= membershipRateLimit && membershipRateLimit <= w.maxMembershipRateLimit() ); - vm.assume(w.isValidUserMessageLimit(userMessageLimit)); + vm.assume(w.isValidMembershipRateLimit(membershipRateLimit)); vm.resumeGasMetering(); uint256 time = block.timestamp; for (uint256 i = 0; i < 5; i++) { token.approve(address(w), price); - w.register(idCommitment + i, userMessageLimit); + w.register(idCommitment + i, membershipRateLimit, noIdCommitmentsToErase); time += 100; vm.warp(time); } - // Expiring the first 3 - uint256 expirationDate = w.expirationDate(idCommitment + 2); - vm.warp(expirationDate); + // Expiring the first 3 memberships + uint256 membershipExpirationTimestamp = w.membershipExpirationTimestamp(idCommitment + 2); + vm.warp(membershipExpirationTimestamp); for (uint256 i = 0; i < 5; i++) { if (i <= 2) { assertTrue(w.isExpired(idCommitment + i)); @@ -740,61 +564,46 @@ contract WakuRlnV2Test is Test { commitmentsToErase[1] = idCommitment + 2; vm.expectEmit(true, false, false, false); // only check the first parameter of the event (the idCommitment) - emit MembershipUpgradeable.MemberExpired(commitmentsToErase[0], 0, 0); + emit MembershipUpgradeable.MembershipExpired(commitmentsToErase[0], 0, 0); vm.expectEmit(true, false, false, false); // only check the first parameter of the event (the idCommitment) - emit MembershipUpgradeable.MemberExpired(commitmentsToErase[0], 0, 0); + emit MembershipUpgradeable.MembershipExpired(commitmentsToErase[0], 0, 0); w.eraseMemberships(commitmentsToErase); address holder; - (,,,,,,, holder,) = w.members(idCommitment + 1); + (,,,,,, holder,) = w.memberships(idCommitment + 1); assertEq(holder, address(0)); - (,,,,,,, holder,) = w.members(idCommitment + 2); + (,,,,,, holder,) = w.memberships(idCommitment + 2); assertEq(holder, address(0)); - // Verify list order is correct - uint256 prev; - uint256 next; - (prev, next,,,,,,,) = w.members(idCommitment); - assertEq(prev, 0); - assertEq(next, idCommitment + 3); - (prev, next,,,,,,,) = w.members(idCommitment + 3); - assertEq(prev, idCommitment); - assertEq(next, idCommitment + 4); - (prev, next,,,,,,,) = w.members(idCommitment + 4); - assertEq(prev, idCommitment + 3); - assertEq(next, 0); - assertEq(w.head(), idCommitment); - assertEq(w.tail(), idCommitment + 4); - // Attempting to call erase when some of the commitments can't be erased yet // idCommitment can be erased (in grace period), but idCommitment + 4 is still active - (,,, uint256 gracePeriodStartDate,,,,,) = w.members(idCommitment + 4); - vm.warp(gracePeriodStartDate - 1); + (,, uint256 gracePeriodStartTimestamp,,,,,) = w.memberships(idCommitment + 4); + vm.warp(gracePeriodStartTimestamp - 1); commitmentsToErase[0] = idCommitment; commitmentsToErase[1] = idCommitment + 4; - vm.expectRevert(abi.encodeWithSelector(CantEraseMembership.selector, idCommitment + 4)); + vm.expectRevert(abi.encodeWithSelector(CannotEraseActiveMembership.selector, idCommitment + 4)); w.eraseMemberships(commitmentsToErase); } function test__RemoveAllExpiredMemberships(uint32 idCommitmentsLength) external { vm.pauseGasMetering(); - vm.assume(idCommitmentsLength > 1 && idCommitmentsLength <= 100); - uint32 userMessageLimit = w.minRateLimitPerMembership(); - (, uint256 price) = w.priceCalculator().calculate(userMessageLimit); + vm.assume(1 < idCommitmentsLength && idCommitmentsLength <= 100); + uint32 membershipRateLimit = w.minMembershipRateLimit(); + (, uint256 price) = w.priceCalculator().calculate(membershipRateLimit); vm.resumeGasMetering(); uint256 time = block.timestamp; for (uint256 i = 1; i <= idCommitmentsLength; i++) { token.approve(address(w), price); - w.register(i, userMessageLimit); + w.register(i, membershipRateLimit, noIdCommitmentsToErase); time += 100; vm.warp(time); } - uint256 expirationDate = w.expirationDate(idCommitmentsLength); - vm.warp(expirationDate); + uint256 membershipExpirationTimestamp = w.membershipExpirationTimestamp(idCommitmentsLength); + vm.warp(membershipExpirationTimestamp); for (uint256 i = 1; i <= 5; i++) { assertTrue(w.isExpired(i)); } @@ -803,60 +612,44 @@ contract WakuRlnV2Test is Test { for (uint256 i = 0; i < idCommitmentsLength; i++) { commitmentsToErase[i] = i + 1; vm.expectEmit(true, false, false, false); // only check the first parameter of the event (the idCommitment) - emit MembershipUpgradeable.MemberExpired(i + 1, 0, 0); + emit MembershipUpgradeable.MembershipExpired(i + 1, 0, 0); } w.eraseMemberships(commitmentsToErase); - // No memberships registered - assertEq(w.head(), 0); - assertEq(w.tail(), 0); - - for (uint256 i = 10; i <= idCommitmentsLength + 10; i++) { - token.approve(address(w), price); - w.register(i, userMessageLimit); - assertEq(w.tail(), i); + // Erased memberships are gone! + for (uint256 i = 0; i < commitmentsToErase.length; i++) { + (,,,, uint32 fetchedMembershipRateLimit,,,) = w.memberships(commitmentsToErase[i]); + assertEq(fetchedMembershipRateLimit, 0); } - - // Verify list order is correct - assertEq(w.head(), 10); - assertEq(w.tail(), idCommitmentsLength + 10); - uint256 prev; - uint256 next; - (prev, next,,,,,,,) = w.members(10); - assertEq(prev, 0); - assertEq(next, 11); - (prev, next,,,,,,,) = w.members(idCommitmentsLength + 10); - assertEq(prev, idCommitmentsLength + 9); - assertEq(next, 0); } - function test__WithdrawToken(uint32 userMessageLimit) external { + function test__WithdrawToken(uint32 membershipRateLimit) external { vm.pauseGasMetering(); uint256 idCommitment = 2; LinearPriceCalculator priceCalculator = LinearPriceCalculator(address(w.priceCalculator())); vm.prank(priceCalculator.owner()); priceCalculator.setTokenAndPrice(address(token), 5 wei); - (, uint256 price) = w.priceCalculator().calculate(userMessageLimit); + (, uint256 price) = w.priceCalculator().calculate(membershipRateLimit); token.mint(address(this), price); vm.assume( - userMessageLimit >= w.minRateLimitPerMembership() && userMessageLimit <= w.maxRateLimitPerMembership() + w.minMembershipRateLimit() <= membershipRateLimit && membershipRateLimit <= w.maxMembershipRateLimit() ); - vm.assume(w.isValidUserMessageLimit(userMessageLimit)); + vm.assume(w.isValidMembershipRateLimit(membershipRateLimit)); vm.resumeGasMetering(); token.approve(address(w), price); - w.register(idCommitment, userMessageLimit); + w.register(idCommitment, membershipRateLimit, noIdCommitmentsToErase); - (,,, uint256 gracePeriodStartDate,,,,,) = w.members(idCommitment); + (,, uint256 gracePeriodStartTimestamp,,,,,) = w.memberships(idCommitment); - vm.warp(gracePeriodStartDate); + vm.warp(gracePeriodStartTimestamp); uint256[] memory commitmentsToErase = new uint256[](1); commitmentsToErase[0] = idCommitment; w.eraseMemberships(commitmentsToErase); - uint256 availableBalance = w.balancesToWithdraw(address(this), address(token)); + uint256 availableBalance = w.depositsToWithdraw(address(this), address(token)); assertEq(availableBalance, price); assertEq(token.balanceOf(address(w)), price); @@ -867,7 +660,7 @@ contract WakuRlnV2Test is Test { uint256 balanceAfterWithdraw = token.balanceOf(address(this)); - availableBalance = w.balancesToWithdraw(address(this), address(token)); + availableBalance = w.depositsToWithdraw(address(this), address(token)); assertEq(availableBalance, 0); assertEq(token.balanceOf(address(w)), 0); assertEq(balanceBeforeWithdraw + price, balanceAfterWithdraw); @@ -876,29 +669,29 @@ contract WakuRlnV2Test is Test { function test__InvalidRegistration__DuplicateIdCommitment() external { vm.pauseGasMetering(); uint256 idCommitment = 2; - uint32 userMessageLimit = w.minRateLimitPerMembership(); - (, uint256 price) = w.priceCalculator().calculate(userMessageLimit); + uint32 membershipRateLimit = w.minMembershipRateLimit(); + (, uint256 price) = w.priceCalculator().calculate(membershipRateLimit); vm.resumeGasMetering(); token.approve(address(w), price); - w.register(idCommitment, userMessageLimit); + w.register(idCommitment, membershipRateLimit, noIdCommitmentsToErase); token.approve(address(w), price); - vm.expectRevert(DuplicateIdCommitment.selector); - w.register(idCommitment, userMessageLimit); + vm.expectRevert(bytes("Duplicate idCommitment: membership already exists")); + w.register(idCommitment, membershipRateLimit, noIdCommitmentsToErase); } function test__InvalidRegistration__FullTree() external { vm.pauseGasMetering(); - uint32 userMessageLimit = 20; - (, uint256 price) = w.priceCalculator().calculate(userMessageLimit); + uint32 membershipRateLimit = 20; + (, uint256 price) = w.priceCalculator().calculate(membershipRateLimit); vm.resumeGasMetering(); // we progress the tree to the last leaf /*| Name | Type | Slot | Offset | Bytes | |---------------------|-----------------------------------------------------|------|--------|-------| - | nextCommitmentIndex | uint32 | 256 | 0 | 4 | */ + | nextFreeIndex | uint32 | 256 | 0 | 4 | */ /* Pro tip: to easily find the storage slot of a variable, without having to calculate the storage layout @@ -916,55 +709,55 @@ contract WakuRlnV2Test is Test { If the storage layout changes, update the next line accordingly */ - // we set nextCommitmentIndex to 4294967295 (1 << 20) = 0x00100000 + // we set nextFreeIndex to 4294967295 (1 << 20) = 0x00100000 vm.store(address(w), bytes32(uint256(256)), 0x0000000000000000000000000000000000000000000000000000000000100000); token.approve(address(w), price); - vm.expectRevert(FullTree.selector); - w.register(1, userMessageLimit); + vm.expectRevert(bytes("Membership set is full")); + w.register(1, membershipRateLimit, noIdCommitmentsToErase); } function test__InvalidPaginationQuery__StartIndexGTEndIndex() external { vm.expectRevert(abi.encodeWithSelector(InvalidPaginationQuery.selector, 1, 0)); - w.getCommitments(1, 0); + w.getRateCommitmentsInRangeBoundsInclusive(1, 0); } - function test__InvalidPaginationQuery__EndIndexGTnextCommitmentIndex() external { + function test__InvalidPaginationQuery__EndIndexGTNextFreeIndex() external { vm.expectRevert(abi.encodeWithSelector(InvalidPaginationQuery.selector, 0, 2)); - w.getCommitments(0, 2); + w.getRateCommitmentsInRangeBoundsInclusive(0, 2); } function test__ValidPaginationQuery__OneElement() external { vm.pauseGasMetering(); uint256 idCommitment = 1; - uint32 userMessageLimit = w.minRateLimitPerMembership(); - (, uint256 price) = w.priceCalculator().calculate(userMessageLimit); + uint32 membershipRateLimit = w.minMembershipRateLimit(); + (, uint256 price) = w.priceCalculator().calculate(membershipRateLimit); vm.resumeGasMetering(); token.approve(address(w), price); - w.register(idCommitment, userMessageLimit); - uint256[] memory commitments = w.getCommitments(0, 0); + w.register(idCommitment, membershipRateLimit, noIdCommitmentsToErase); + uint256[] memory commitments = w.getRateCommitmentsInRangeBoundsInclusive(0, 0); assertEq(commitments.length, 1); - uint256 rateCommitment = PoseidonT3.hash([idCommitment, userMessageLimit]); + uint256 rateCommitment = PoseidonT3.hash([idCommitment, membershipRateLimit]); assertEq(commitments[0], rateCommitment); } function test__ValidPaginationQuery(uint32 idCommitmentsLength) external { vm.pauseGasMetering(); - vm.assume(idCommitmentsLength > 0 && idCommitmentsLength <= 100); - uint32 userMessageLimit = w.minRateLimitPerMembership(); - (, uint256 price) = w.priceCalculator().calculate(userMessageLimit); + vm.assume(0 < idCommitmentsLength && idCommitmentsLength <= 100); + uint32 membershipRateLimit = w.minMembershipRateLimit(); + (, uint256 price) = w.priceCalculator().calculate(membershipRateLimit); - for (uint256 i = 0; i < idCommitmentsLength; i++) { + for (uint256 i = 0; i <= idCommitmentsLength; i++) { token.approve(address(w), price); - w.register(i + 1, userMessageLimit); + w.register(i + 1, membershipRateLimit, noIdCommitmentsToErase); } vm.resumeGasMetering(); - uint256[] memory commitments = w.getCommitments(0, idCommitmentsLength); - assertEq(commitments.length, idCommitmentsLength + 1); + uint256[] memory rateCommitments = w.getRateCommitmentsInRangeBoundsInclusive(0, idCommitmentsLength - 1); + assertEq(rateCommitments.length, idCommitmentsLength); for (uint256 i = 0; i < idCommitmentsLength; i++) { - uint256 rateCommitment = PoseidonT3.hash([i + 1, userMessageLimit]); - assertEq(commitments[i], rateCommitment); + uint256 rateCommitment = PoseidonT3.hash([i + 1, membershipRateLimit]); + assertEq(rateCommitments[i], rateCommitment); } } From e8ee1d7c767c00501a4df3aa2e269681699d50a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?rich=CE=9Brd?= Date: Tue, 8 Oct 2024 14:38:44 -0400 Subject: [PATCH 21/21] feat: `Ownable2StepUpgradeable` to `OwnableUpgradeable` (#21) This allows the contract to transfer ownership to address 0x000...000 as defined in https://github.com/waku-org/specs/pull/34: `At some point, the _Owner_ SHOULD renounce their privileges, and the contract MUST become immutable`. The problem with `Ownable2StepUpgradeable` is that it requires accepting the ownership transfer, which is not possible with address 0x0 --- src/LinearPriceCalculator.sol | 6 +++--- src/WakuRlnV2.sol | 4 ++-- test/WakuRlnV2.t.sol | 5 ++--- 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/src/LinearPriceCalculator.sol b/src/LinearPriceCalculator.sol index 0bf6bd2..70d0664 100644 --- a/src/LinearPriceCalculator.sol +++ b/src/LinearPriceCalculator.sol @@ -1,21 +1,21 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.24; -import { Ownable2Step } from "openzeppelin-contracts/contracts/access/Ownable2Step.sol"; +import { Ownable } from "openzeppelin-contracts/contracts/access/Ownable.sol"; import { IPriceCalculator } from "./IPriceCalculator.sol"; /// Address 0x0000...0000 was used instead of an ERC20 token address error OnlyTokensAllowed(); /// @title Linear Price Calculator to determine the price to acquire a membership -contract LinearPriceCalculator is IPriceCalculator, Ownable2Step { +contract LinearPriceCalculator is IPriceCalculator, Ownable { /// @notice Address of the ERC20 token accepted by this contract. address public token; /// @notice The price per message per epoch uint256 public pricePerMessagePerEpoch; - constructor(address _token, uint256 _pricePerMessagePerEpoch) Ownable2Step() { + constructor(address _token, uint256 _pricePerMessagePerEpoch) Ownable() { _setTokenAndPrice(_token, _pricePerMessagePerEpoch); } diff --git a/src/WakuRlnV2.sol b/src/WakuRlnV2.sol index 965bbaa..7b831ef 100644 --- a/src/WakuRlnV2.sol +++ b/src/WakuRlnV2.sol @@ -4,7 +4,7 @@ pragma solidity 0.8.24; import { LazyIMT, LazyIMTData } from "@zk-kit/imt.sol/LazyIMT.sol"; import { PoseidonT3 } from "poseidon-solidity/PoseidonT3.sol"; -import { Ownable2StepUpgradeable } from "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol"; +import { OwnableUpgradeable } from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; import { Initializable } from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; @@ -20,7 +20,7 @@ error InvalidIdCommitment(uint256 idCommitment); /// Invalid pagination query error InvalidPaginationQuery(uint256 startIndex, uint256 endIndex); -contract WakuRlnV2 is Initializable, Ownable2StepUpgradeable, UUPSUpgradeable, MembershipUpgradeable { +contract WakuRlnV2 is Initializable, OwnableUpgradeable, UUPSUpgradeable, MembershipUpgradeable { /// @notice The Field uint256 public constant Q = 21_888_242_871_839_275_222_246_405_745_257_275_088_548_364_400_416_034_343_698_204_186_575_808_495_617; diff --git a/test/WakuRlnV2.t.sol b/test/WakuRlnV2.t.sol index 49c7898..a018c5c 100644 --- a/test/WakuRlnV2.t.sol +++ b/test/WakuRlnV2.t.sol @@ -691,8 +691,7 @@ contract WakuRlnV2Test is Test { /*| Name | Type | Slot | Offset | Bytes | |---------------------|-----------------------------------------------------|------|--------|-------| - | nextFreeIndex | uint32 | 256 | 0 | 4 | */ - + | nextFreeIndex | uint32 | 206 | 0 | 4 | */ /* Pro tip: to easily find the storage slot of a variable, without having to calculate the storage layout based on the variable declaration, set the variable to an easily grepable value like 0xDEADBEEF, and then @@ -710,7 +709,7 @@ contract WakuRlnV2Test is Test { */ // we set nextFreeIndex to 4294967295 (1 << 20) = 0x00100000 - vm.store(address(w), bytes32(uint256(256)), 0x0000000000000000000000000000000000000000000000000000000000100000); + vm.store(address(w), bytes32(uint256(206)), 0x0000000000000000000000000000000000000000000000000000000000100000); token.approve(address(w), price); vm.expectRevert(bytes("Membership set is full")); w.register(1, membershipRateLimit, noIdCommitmentsToErase);