From 458e07cc701d6f24c4c9dedaa0e22660cef5ee91 Mon Sep 17 00:00:00 2001 From: Ricardo Guilherme Schmidt <3esmit@gmail.com> Date: Wed, 21 May 2025 07:47:45 -0300 Subject: [PATCH] chore(RLN): import rln-contracts --- .env | 8 + .env.example | 8 + .gas-report | 118 ++++++++++--- .gas-snapshot | 37 ++-- .gitignore | 1 - script/RLN.s.sol | 27 +++ src/rln/IVerifier.sol | 14 ++ src/rln/RLN.sol | 209 ++++++++++++++++++++++ src/rln/Verifier.sol | 202 +++++++++++++++++++++ test/RLN.t.sol | 395 ++++++++++++++++++++++++++++++++++++++++++ 10 files changed, 987 insertions(+), 32 deletions(-) create mode 100644 .env create mode 100644 .env.example create mode 100644 script/RLN.s.sol create mode 100644 src/rln/IVerifier.sol create mode 100644 src/rln/RLN.sol create mode 100644 src/rln/Verifier.sol create mode 100644 test/RLN.t.sol diff --git a/.env b/.env new file mode 100644 index 0000000..881b03a --- /dev/null +++ b/.env @@ -0,0 +1,8 @@ +GOERLI_RPC_URL=https://rpc.ankr.com/eth_goerli +ERC20TOKEN=0xA13dE8dF4Ef9D6016F0826858D48045848429390 +MINIMAL_DEPOSIT=100 +MAXIMAL_RATE=65535 +DEPTH=20 +FEE_PERCENTAGE=5 +FEE_RECEIVER=0x90579a82BB296A6DA61D4d035E42Fc1aD75f090e +FREEZE_PERIOD=100 \ No newline at end of file diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..881b03a --- /dev/null +++ b/.env.example @@ -0,0 +1,8 @@ +GOERLI_RPC_URL=https://rpc.ankr.com/eth_goerli +ERC20TOKEN=0xA13dE8dF4Ef9D6016F0826858D48045848429390 +MINIMAL_DEPOSIT=100 +MAXIMAL_RATE=65535 +DEPTH=20 +FEE_PERCENTAGE=5 +FEE_RECEIVER=0x90579a82BB296A6DA61D4d035E42Fc1aD75f090e +FREEZE_PERIOD=100 \ No newline at end of file diff --git a/.gas-report b/.gas-report index 22c6059..4eade3e 100644 --- a/.gas-report +++ b/.gas-report @@ -186,7 +186,7 @@ |--------------------------------------------+-----------------+--------+--------+--------+---------| | getAccountVaults | 5230 | 5230 | 5230 | 5230 | 4 | |--------------------------------------------+-----------------+--------+--------+--------+---------| -| getVault | 13653 | 13653 | 13653 | 13653 | 4182 | +| getVault | 13653 | 13653 | 13653 | 13653 | 4181 | |--------------------------------------------+-----------------+--------+--------+--------+---------| | initialize | 92752 | 92752 | 92752 | 92752 | 95 | |--------------------------------------------+-----------------+--------+--------+--------+---------| @@ -194,7 +194,7 @@ |--------------------------------------------+-----------------+--------+--------+--------+---------| | leave | 66348 | 66348 | 66348 | 66348 | 2 | |--------------------------------------------+-----------------+--------+--------+--------+---------| -| lock | 7040 | 43591 | 46713 | 87964 | 1034 | +| lock | 7040 | 43473 | 46713 | 87673 | 1034 | |--------------------------------------------+-----------------+--------+--------+--------+---------| | migrateToVault | 9294 | 53513 | 17021 | 170715 | 4 | |--------------------------------------------+-----------------+--------+--------+--------+---------| @@ -212,7 +212,7 @@ |--------------------------------------------+-----------------+--------+--------+--------+---------| | rewardStartTime | 2364 | 2364 | 2364 | 2364 | 2 | |--------------------------------------------+-----------------+--------+--------+--------+---------| -| rewardsBalanceOf | 20295 | 24430 | 25908 | 26129 | 268 | +| rewardsBalanceOf | 20295 | 24409 | 25908 | 26129 | 268 | |--------------------------------------------+-----------------+--------+--------+--------+---------| | rewardsBalanceOfAccount | 62220 | 62220 | 62220 | 62220 | 1 | |--------------------------------------------+-----------------+--------+--------+--------+---------| @@ -222,33 +222,33 @@ |--------------------------------------------+-----------------+--------+--------+--------+---------| | setTrustedCodehash | 24238 | 24238 | 24238 | 24238 | 95 | |--------------------------------------------+-----------------+--------+--------+--------+---------| -| stake | 2639 | 131862 | 60725 | 228623 | 2670 | +| stake | 2639 | 132481 | 60725 | 228623 | 2670 | |--------------------------------------------+-----------------+--------+--------+--------+---------| | stakedBalanceOf | 2622 | 2622 | 2622 | 2622 | 1 | |--------------------------------------------+-----------------+--------+--------+--------+---------| | totalMP | 6805 | 8257 | 8257 | 9710 | 6 | |--------------------------------------------+-----------------+--------+--------+--------+---------| -| totalMPAccrued | 2385 | 2385 | 2385 | 2385 | 4162 | +| totalMPAccrued | 2385 | 2385 | 2385 | 2385 | 4161 | |--------------------------------------------+-----------------+--------+--------+--------+---------| -| totalMPStaked | 2429 | 2429 | 2429 | 2429 | 4165 | +| totalMPStaked | 2429 | 2429 | 2429 | 2429 | 4164 | |--------------------------------------------+-----------------+--------+--------+--------+---------| -| totalMaxMP | 2407 | 2407 | 2407 | 2407 | 4162 | +| totalMaxMP | 2407 | 2407 | 2407 | 2407 | 4161 | |--------------------------------------------+-----------------+--------+--------+--------+---------| | totalRewardsAccrued | 2407 | 2407 | 2407 | 2407 | 3 | |--------------------------------------------+-----------------+--------+--------+--------+---------| -| totalRewardsSupply | 6737 | 11068 | 11792 | 11903 | 290 | +| totalRewardsSupply | 6737 | 11058 | 11792 | 11903 | 290 | |--------------------------------------------+-----------------+--------+--------+--------+---------| | totalShares | 4597 | 4597 | 4597 | 4597 | 6 | |--------------------------------------------+-----------------+--------+--------+--------+---------| -| totalStaked | 2408 | 2408 | 2408 | 2408 | 4169 | +| totalStaked | 2408 | 2408 | 2408 | 2408 | 4168 | |--------------------------------------------+-----------------+--------+--------+--------+---------| -| unstake | 9886 | 41391 | 39781 | 79550 | 271 | +| unstake | 9886 | 41170 | 39781 | 79550 | 271 | |--------------------------------------------+-----------------+--------+--------+--------+---------| | updateAccount | 347677 | 347677 | 347677 | 347677 | 1 | |--------------------------------------------+-----------------+--------+--------+--------+---------| | updateGlobalState | 15820 | 25876 | 29230 | 29230 | 8 | |--------------------------------------------+-----------------+--------+--------+--------+---------| -| updateVault | 31948 | 34543 | 31948 | 110579 | 1024 | +| updateVault | 31948 | 34668 | 31948 | 110579 | 1023 | |--------------------------------------------+-----------------+--------+--------+--------+---------| | upgradeTo | 10279 | 10772 | 10279 | 12745 | 5 | |--------------------------------------------+-----------------+--------+--------+--------+---------| @@ -276,9 +276,9 @@ |----------------------------------------+-----------------+--------+--------+--------+---------| | leave | 12223 | 113137 | 84120 | 356508 | 5 | |----------------------------------------+-----------------+--------+--------+--------+---------| -| lock | 12151 | 59083 | 62251 | 103499 | 1035 | +| lock | 12151 | 58965 | 62251 | 103208 | 1035 | |----------------------------------------+-----------------+--------+--------+--------+---------| -| lockUntil | 2363 | 2363 | 2363 | 2363 | 7769 | +| lockUntil | 2363 | 2363 | 2363 | 2363 | 7766 | |----------------------------------------+-----------------+--------+--------+--------+---------| | migrateToVault | 24910 | 77530 | 32637 | 219937 | 4 | |----------------------------------------+-----------------+--------+--------+--------+---------| @@ -286,15 +286,15 @@ |----------------------------------------+-----------------+--------+--------+--------+---------| | register | 12742 | 78218 | 78761 | 78761 | 374 | |----------------------------------------+-----------------+--------+--------+--------+---------| -| stake | 12131 | 166249 | 76290 | 284275 | 2671 | +| stake | 12131 | 167018 | 76290 | 284275 | 2671 | |----------------------------------------+-----------------+--------+--------+--------+---------| | stakeManager | 393 | 393 | 393 | 393 | 373 | |----------------------------------------+-----------------+--------+--------+--------+---------| | trustStakeManager | 7650 | 7650 | 7650 | 7650 | 1 | |----------------------------------------+-----------------+--------+--------+--------+---------| -| unstake | 12108 | 58059 | 55296 | 110656 | 272 | +| unstake | 12108 | 57840 | 55296 | 110656 | 272 | |----------------------------------------+-----------------+--------+--------+--------+---------| -| updateLockUntil | 4432 | 20722 | 21532 | 21532 | 524 | +| updateLockUntil | 4432 | 20702 | 21532 | 21532 | 532 | |----------------------------------------+-----------------+--------+--------+--------+---------| | withdraw | 20817 | 20817 | 20817 | 20817 | 1 | |----------------------------------------+-----------------+--------+--------+--------+---------| @@ -312,9 +312,9 @@ |----------------------------------------------------+-----------------+-------+--------+--------+---------| | Function Name | Min | Avg | Median | Max | # Calls | |----------------------------------------------------+-----------------+-------+--------+--------+---------| -| fallback | 5208 | 12842 | 7353 | 374054 | 23167 | +| fallback | 5208 | 12845 | 7353 | 374054 | 23161 | |----------------------------------------------------+-----------------+-------+--------+--------+---------| -| implementation | 346 | 2131 | 2346 | 2346 | 4886 | +| implementation | 346 | 2128 | 2346 | 2346 | 4894 | ╰----------------------------------------------------+-----------------+-------+--------+--------+---------╯ ╭--------------------------------------------+-----------------+--------+--------+--------+---------╮ @@ -373,6 +373,82 @@ | urlSuffix | 3228 | 3228 | 3228 | 3228 | 1 | ╰------------------------------------------------------------------------------------------+-----------------+-------+--------+-------+---------╯ +╭------------------------------+-----------------+--------+--------+--------+---------╮ +| src/rln/RLN.sol:RLN Contract | | | | | | ++=====================================================================================+ +| Deployment Cost | Deployment Size | | | | | +|------------------------------+-----------------+--------+--------+--------+---------| +| 1396408 | 7094 | | | | | +|------------------------------+-----------------+--------+--------+--------+---------| +| | | | | | | +|------------------------------+-----------------+--------+--------+--------+---------| +| Function Name | Min | Avg | Median | Max | # Calls | +|------------------------------+-----------------+--------+--------+--------+---------| +| FEE_PERCENTAGE | 217 | 217 | 217 | 217 | 1 | +|------------------------------+-----------------+--------+--------+--------+---------| +| FEE_RECEIVER | 226 | 226 | 226 | 226 | 1 | +|------------------------------+-----------------+--------+--------+--------+---------| +| FREEZE_PERIOD | 218 | 218 | 218 | 218 | 1 | +|------------------------------+-----------------+--------+--------+--------+---------| +| MINIMAL_DEPOSIT | 241 | 241 | 241 | 241 | 1 | +|------------------------------+-----------------+--------+--------+--------+---------| +| SET_SIZE | 284 | 284 | 284 | 284 | 1 | +|------------------------------+-----------------+--------+--------+--------+---------| +| identityCommitmentIndex | 2362 | 2362 | 2362 | 2362 | 27 | +|------------------------------+-----------------+--------+--------+--------+---------| +| members | 6826 | 6826 | 6826 | 6826 | 18 | +|------------------------------+-----------------+--------+--------+--------+---------| +| register | 23877 | 105927 | 123897 | 126709 | 18 | +|------------------------------+-----------------+--------+--------+--------+---------| +| release | 28093 | 37708 | 28192 | 66355 | 4 | +|------------------------------+-----------------+--------+--------+--------+---------| +| slash | 23015 | 49020 | 34650 | 99415 | 6 | +|------------------------------+-----------------+--------+--------+--------+---------| +| token | 292 | 292 | 292 | 292 | 1 | +|------------------------------+-----------------+--------+--------+--------+---------| +| verifier | 272 | 272 | 272 | 272 | 1 | +|------------------------------+-----------------+--------+--------+--------+---------| +| withdraw | 29336 | 79495 | 106774 | 106774 | 8 | +|------------------------------+-----------------+--------+--------+--------+---------| +| withdrawals | 6792 | 6792 | 6792 | 6792 | 5 | +╰------------------------------+-----------------+--------+--------+--------+---------╯ + +╭--------------------------------------+-----------------+-------+--------+-------+---------╮ +| test/RLN.t.sol:MockVerifier Contract | | | | | | ++===========================================================================================+ +| Deployment Cost | Deployment Size | | | | | +|--------------------------------------+-----------------+-------+--------+-------+---------| +| 204321 | 642 | | | | | +|--------------------------------------+-----------------+-------+--------+-------+---------| +| | | | | | | +|--------------------------------------+-----------------+-------+--------+-------+---------| +| Function Name | Min | Avg | Median | Max | # Calls | +|--------------------------------------+-----------------+-------+--------+-------+---------| +| changeResult | 21649 | 21649 | 21649 | 21649 | 2 | +|--------------------------------------+-----------------+-------+--------+-------+---------| +| result | 2298 | 2298 | 2298 | 2298 | 5 | +|--------------------------------------+-----------------+-------+--------+-------+---------| +| verifyProof | 4790 | 4790 | 4790 | 4790 | 9 | +╰--------------------------------------+-----------------+-------+--------+-------+---------╯ + +╭-----------------------------------+-----------------+-------+--------+-------+---------╮ +| test/RLN.t.sol:TestERC20 Contract | | | | | | ++========================================================================================+ +| Deployment Cost | Deployment Size | | | | | +|-----------------------------------+-----------------+-------+--------+-------+---------| +| 765176 | 3558 | | | | | +|-----------------------------------+-----------------+-------+--------+-------+---------| +| | | | | | | +|-----------------------------------+-----------------+-------+--------+-------+---------| +| Function Name | Min | Avg | Median | Max | # Calls | +|-----------------------------------+-----------------+-------+--------+-------+---------| +| approve | 46175 | 46179 | 46175 | 46199 | 18 | +|-----------------------------------+-----------------+-------+--------+-------+---------| +| balanceOf | 2561 | 2561 | 2561 | 2561 | 62 | +|-----------------------------------+-----------------+-------+--------+-------+---------| +| mint | 51064 | 64368 | 68164 | 68188 | 18 | +╰-----------------------------------+-----------------+-------+--------+-------+---------╯ + ╭-------------------------------------------------------------------+-----------------+-------+--------+-------+---------╮ | test/mocks/KarmaDistributorMock.sol:KarmaDistributorMock Contract | | | | | | +========================================================================================================================+ @@ -418,11 +494,11 @@ |---------------------------------------------+-----------------+-------+--------+-------+---------| | Function Name | Min | Avg | Median | Max | # Calls | |---------------------------------------------+-----------------+-------+--------+-------+---------| -| approve | 29075 | 31544 | 29183 | 46259 | 2676 | +| approve | 29075 | 31546 | 29183 | 46259 | 2676 | |---------------------------------------------+-----------------+-------+--------+-------+---------| -| balanceOf | 2561 | 2561 | 2561 | 2561 | 4960 | +| balanceOf | 2561 | 2561 | 2561 | 2561 | 4959 | |---------------------------------------------+-----------------+-------+--------+-------+---------| -| mint | 33964 | 37189 | 34072 | 68248 | 2685 | +| mint | 33964 | 37192 | 34072 | 68248 | 2685 | ╰---------------------------------------------+-----------------+-------+--------+-------+---------╯ ╭-----------------------------------------------------------------------------+-----------------+--------+--------+--------+---------╮ diff --git a/.gas-snapshot b/.gas-snapshot index 86f0d7b..508e628 100644 --- a/.gas-snapshot +++ b/.gas-snapshot @@ -16,15 +16,15 @@ EmergencyExitTest:test_EmergencyExitToAlternateAddress() (gas: 479110) EmergencyExitTest:test_EmergencyExitWithLock() (gas: 452444) EmergencyExitTest:test_EmergencyExitWithRewards() (gas: 484810) EmergencyExitTest:test_OnlyOwnerCanEnableEmergencyMode() (gas: 39176) -FuzzTests:testFuzz_AccrueMP(uint128,uint64,uint64) (runs: 1009, μ: 586777, ~: 549070) -FuzzTests:testFuzz_AccrueMP_Relock(uint128,uint64,uint64,uint64) (runs: 1009, μ: 811994, ~: 777237) -FuzzTests:testFuzz_EmergencyExit(uint256,uint256) (runs: 1001, μ: 588323, ~: 578267) -FuzzTests:testFuzz_Lock(uint256,uint64) (runs: 1008, μ: 961825, ~: 961235) -FuzzTests:testFuzz_Relock(uint256,uint64,uint64) (runs: 1008, μ: 598425, ~: 574225) -FuzzTests:testFuzz_Rewards(uint256,uint256,uint256,uint16,uint16) (runs: 1001, μ: 650378, ~: 653205) -FuzzTests:testFuzz_Stake(uint256,uint64) (runs: 1008, μ: 375317, ~: 346086) -FuzzTests:testFuzz_Unstake(uint128,uint64,uint16,uint128) (runs: 1009, μ: 806735, ~: 780622) -FuzzTests:testFuzz_UpdateVault(uint128,uint64,uint64) (runs: 1009, μ: 586800, ~: 549093) +FuzzTests:testFuzz_AccrueMP(uint128,uint64,uint64) (runs: 1024, μ: 587151, ~: 549070) +FuzzTests:testFuzz_AccrueMP_Relock(uint128,uint64,uint64,uint64) (runs: 1024, μ: 812396, ~: 777234) +FuzzTests:testFuzz_EmergencyExit(uint256,uint256) (runs: 1007, μ: 588350, ~: 578267) +FuzzTests:testFuzz_Lock(uint256,uint64) (runs: 1025, μ: 961806, ~: 961235) +FuzzTests:testFuzz_Relock(uint256,uint64,uint64) (runs: 1025, μ: 599078, ~: 574234) +FuzzTests:testFuzz_Rewards(uint256,uint256,uint256,uint16,uint16) (runs: 1001, μ: 650369, ~: 653205) +FuzzTests:testFuzz_Stake(uint256,uint64) (runs: 1025, μ: 376001, ~: 346086) +FuzzTests:testFuzz_Unstake(uint128,uint64,uint16,uint128) (runs: 1024, μ: 806573, ~: 780622) +FuzzTests:testFuzz_UpdateVault(uint128,uint64,uint64) (runs: 1024, μ: 587174, ~: 549093) IntegrationTest:testStakeFoo() (gas: 2348931) KarmaNFTTest:testApproveNotAllowed() (gas: 10507) KarmaNFTTest:testGetApproved() (gas: 10531) @@ -58,7 +58,7 @@ LeaveTest:test_LeaveShouldKeepFundsLockedInStakeVault() (gas: 9938411) LeaveTest:test_LeaveShouldProperlyUpdateAccounting() (gas: 10011059) LeaveTest:test_RevertWhenStakeManagerIsTrusted() (gas: 333238) LeaveTest:test_TrustNewStakeManager() (gas: 9944491) -LockTest:test_LockFailsWithInvalidPeriod(uint256) (runs: 1008, μ: 384561, ~: 384588) +LockTest:test_LockFailsWithInvalidPeriod(uint256) (runs: 1025, μ: 384562, ~: 384588) LockTest:test_LockFailsWithNoStake() (gas: 89700) LockTest:test_LockFailsWithZero() (gas: 343310) LockTest:test_LockMultipleTimesExceedMaxLock() (gas: 746921) @@ -89,6 +89,23 @@ OverflowTest:testTotalSupply() (gas: 359166) OverflowTest:testTransfersNotAllowed() (gas: 61763) OverflowTest:test_RevertWhen_MintingCausesOverflow() (gas: 129464) OverflowTest:test_RevertWhen_SettingRewardCausesOverflow() (gas: 127792) +RLNTest:test_initial_state() (gas: 65400) +RLNTest:test_register_fails_when_amount_lt_minimal_deposit() (gas: 161453) +RLNTest:test_register_fails_when_duplicate_identity_commitments() (gas: 444949) +RLNTest:test_register_fails_when_index_exceeds_set_size() (gas: 2824923) +RLNTest:test_register_succeeds() (gas: 579610) +RLNTest:test_release_fails_when_freeze_period() (gas: 529887) +RLNTest:test_release_fails_when_no_withdrawal() (gas: 38343) +RLNTest:test_release_succeeds() (gas: 576511) +RLNTest:test_slash_fails_when_invalid_proof() (gas: 399809) +RLNTest:test_slash_fails_when_not_registered() (gas: 59667) +RLNTest:test_slash_fails_when_receiver_is_zero() (gas: 351215) +RLNTest:test_slash_fails_when_self_slashing() (gas: 360067) +RLNTest:test_slash_succeeds() (gas: 1024025) +RLNTest:test_withdraw_fails_when_already_underways() (gas: 468594) +RLNTest:test_withdraw_fails_when_invalid_proof() (gas: 399356) +RLNTest:test_withdraw_fails_when_not_registered() (gas: 57129) +RLNTest:test_withdraw_succeeds() (gas: 480413) RemoveRewardDistributorTest:testAddKarmaDistributorOnlyAdmin() (gas: 438045) RemoveRewardDistributorTest:testBalanceOf() (gas: 449366) RemoveRewardDistributorTest:testBalanceOfWithNoSystemTotalKarma() (gas: 69633) diff --git a/.gitignore b/.gitignore index 8e6ad86..a8cf2cc 100644 --- a/.gitignore +++ b/.gitignore @@ -4,7 +4,6 @@ node_modules out # files -*.env *.log .DS_Store .pnp.* diff --git a/script/RLN.s.sol b/script/RLN.s.sol new file mode 100644 index 0000000..2603092 --- /dev/null +++ b/script/RLN.s.sol @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: Apache-2.0 OR MIT +pragma solidity ^0.8.17; + +import "forge-std/Script.sol"; +import "../src/rln/RLN.sol"; +import "../src/rln/Verifier.sol"; + +contract RLNScript is Script { + function run() public { + uint256 minimalDeposit = vm.envUint("MINIMAL_DEPOSIT"); + uint256 maximalRate = vm.envUint("MAXIMAL_RATE"); + uint256 depth = vm.envUint("DEPTH"); + uint8 feePercentage = uint8(vm.envUint("FEE_PERCENTAGE")); + address feeReceiver = vm.envAddress("FEE_RECEIVER"); + uint256 freezePeriod = vm.envUint("FREEZE_PERIOD"); + address token = vm.envAddress("ERC20TOKEN"); + + vm.startBroadcast(); + + Groth16Verifier verifier = new Groth16Verifier(); + RLN rln = new RLN( + minimalDeposit, maximalRate, depth, feePercentage, feeReceiver, freezePeriod, token, address(verifier) + ); + + vm.stopBroadcast(); + } +} diff --git a/src/rln/IVerifier.sol b/src/rln/IVerifier.sol new file mode 100644 index 0000000..3b8a459 --- /dev/null +++ b/src/rln/IVerifier.sol @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: Apache-2.0 OR MIT +pragma solidity ^0.8.17; + +interface IVerifier { + function verifyProof( + uint256[2] calldata a, + uint256[2][2] calldata b, + uint256[2] calldata c, + uint256[2] calldata input + ) + external + view + returns (bool); +} diff --git a/src/rln/RLN.sol b/src/rln/RLN.sol new file mode 100644 index 0000000..ed673cd --- /dev/null +++ b/src/rln/RLN.sol @@ -0,0 +1,209 @@ +// SPDX-License-Identifier: Apache-2.0 OR MIT +pragma solidity ^0.8.17; + +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import { IVerifier } from "./IVerifier.sol"; + +/// @title Rate-Limiting Nullifier registry contract +/// @dev This contract allows you to register RLN commitment and withdraw/slash. +contract RLN { + using SafeERC20 for IERC20; + + /// @dev User metadata struct. + /// @param userAddress: address of depositor; + /// @param messageLimit: user's message limit (stakeAmount / MINIMAL_DEPOSIT). + struct User { + address userAddress; + uint256 messageLimit; + uint256 index; + } + + /// @dev Withdrawal time-lock struct + /// @param blockNumber: number of block when a withdraw was initialized; + /// @param messageLimit: amount of tokens to freeze/release; + /// @param receiver: address of receiver. + struct Withdrawal { + uint256 blockNumber; + uint256 amount; + address receiver; + } + + /// @dev Minimal membership deposit (stake amount) value - cost of 1 message. + uint256 public immutable MINIMAL_DEPOSIT; + + /// @dev Maximal rate. + uint256 public immutable MAXIMAL_RATE; + + /// @dev Registry set size (1 << DEPTH). + uint256 public immutable SET_SIZE; + + /// @dev Address of the fee receiver. + address public immutable FEE_RECEIVER; + + /// @dev Fee percentage. + uint8 public immutable FEE_PERCENTAGE; + + /// @dev Freeze period - number of blocks for which the withdrawal of money is frozen. + uint256 public immutable FREEZE_PERIOD; + + /// @dev Current index where identityCommitment will be stored. + uint256 public identityCommitmentIndex; + + /// @dev Registry set. The keys are `identityCommitment`s. + /// The values are addresses of accounts that call `register` transaction. + mapping(uint256 => User) public members; + + /// @dev Withdrawals logic. + mapping(uint256 => Withdrawal) public withdrawals; + + /// @dev ERC20 Token used for staking. + IERC20 public immutable token; + + /// @dev Groth16 verifier. + IVerifier public immutable verifier; + + /// @dev Emmited when a new member registered. + /// @param identityCommitment: `identityCommitment`; + /// @param messageLimit: user's message limit; + /// @param index: idCommitmentIndex value. + event MemberRegistered(uint256 identityCommitment, uint256 messageLimit, uint256 index); + + /// @dev Emmited when a member was withdrawn. + /// @param index: index of `identityCommitment`; + event MemberWithdrawn(uint256 index); + + /// @dev Emmited when a member was slashed. + /// @param index: index of `identityCommitment`; + /// @param slasher: address of slasher (msg.sender). + event MemberSlashed(uint256 index, address slasher); + + /// @param minimalDeposit: minimal membership deposit; + /// @param maximalRate: maximal rate; + /// @param depth: depth of the merkle tree; + /// @param feePercentage: fee percentage; + /// @param feeReceiver: address of the fee receiver; + /// @param freezePeriod: amount of blocks for withdrawal time-lock; + /// @param _token: address of the ERC20 contract; + /// @param _verifier: address of the Groth16 Verifier. + constructor( + uint256 minimalDeposit, + uint256 maximalRate, + uint256 depth, + uint8 feePercentage, + address feeReceiver, + uint256 freezePeriod, + address _token, + address _verifier + ) { + require(feeReceiver != address(0), "RLN, constructor: fee receiver cannot be 0x0 address"); + + MINIMAL_DEPOSIT = minimalDeposit; + MAXIMAL_RATE = maximalRate; + SET_SIZE = 1 << depth; + + FEE_PERCENTAGE = feePercentage; + FEE_RECEIVER = feeReceiver; + FREEZE_PERIOD = freezePeriod; + + token = IERC20(_token); + verifier = IVerifier(_verifier); + } + + /// @dev Adds `identityCommitment` to the registry set and takes the necessary stake amount. + /// + /// NOTE: The set must not be full. + /// + /// @param identityCommitment: `identityCommitment`; + /// @param amount: stake amount. + function register(uint256 identityCommitment, uint256 amount) external { + uint256 index = identityCommitmentIndex; + + require(index < SET_SIZE, "RLN, register: set is full"); + require(amount >= MINIMAL_DEPOSIT, "RLN, register: amount is lower than minimal deposit"); + require(amount % MINIMAL_DEPOSIT == 0, "RLN, register: amount should be a multiple of minimal deposit"); + require(members[identityCommitment].userAddress == address(0), "RLN, register: idCommitment already registered"); + + uint256 messageLimit = amount / MINIMAL_DEPOSIT; + require(messageLimit <= MAXIMAL_RATE, "RLN, register: message limit cannot be more than MAXIMAL_RATE"); + + token.safeTransferFrom(msg.sender, address(this), amount); + + members[identityCommitment] = User(msg.sender, messageLimit, index); + emit MemberRegistered(identityCommitment, messageLimit, index); + + unchecked { + identityCommitmentIndex = index + 1; + } + } + + /// @dev Request for withdraw and freeze the stake to prevent self-slashing. Stake can be + /// released after FREEZE_PERIOD blocks. + /// @param identityCommitment: `identityCommitment`; + /// @param proof: snarkjs's format generated proof (without public inputs) packed consequently. + function withdraw(uint256 identityCommitment, uint256[8] calldata proof) external { + User memory member = members[identityCommitment]; + require(member.userAddress != address(0), "RLN, withdraw: member doesn't exist"); + require(withdrawals[identityCommitment].blockNumber == 0, "RLN, release: such withdrawal exists"); + require(_verifyProof(identityCommitment, member.userAddress, proof), "RLN, withdraw: invalid proof"); + + uint256 withdrawAmount = member.messageLimit * MINIMAL_DEPOSIT; + withdrawals[identityCommitment] = Withdrawal(block.number, withdrawAmount, member.userAddress); + emit MemberWithdrawn(member.index); + } + + /// @dev Releases stake amount. + /// @param identityCommitment: `identityCommitment` of withdrawn user. + function release(uint256 identityCommitment) external { + Withdrawal memory withdrawal = withdrawals[identityCommitment]; + require(withdrawal.blockNumber != 0, "RLN, release: no such withdrawals"); + require(block.number - withdrawal.blockNumber > FREEZE_PERIOD, "RLN, release: cannot release yet"); + + delete withdrawals[identityCommitment]; + delete members[identityCommitment]; + + token.safeTransfer(withdrawal.receiver, withdrawal.amount); + } + + /// @dev Slashes identity with identityCommitment. + /// @param identityCommitment: `identityCommitment`; + /// @param receiver: stake receiver; + /// @param proof: snarkjs's format generated proof (without public inputs) packed consequently. + function slash(uint256 identityCommitment, address receiver, uint256[8] calldata proof) external { + require(receiver != address(0), "RLN, slash: empty receiver address"); + + User memory member = members[identityCommitment]; + require(member.userAddress != address(0), "RLN, slash: member doesn't exist"); + require(member.userAddress != receiver, "RLN, slash: self-slashing is prohibited"); + + require(_verifyProof(identityCommitment, receiver, proof), "RLN, slash: invalid proof"); + + delete members[identityCommitment]; + delete withdrawals[identityCommitment]; + + uint256 withdrawAmount = member.messageLimit * MINIMAL_DEPOSIT; + uint256 feeAmount = (FEE_PERCENTAGE * withdrawAmount) / 100; + + token.safeTransfer(receiver, withdrawAmount - feeAmount); + token.safeTransfer(FEE_RECEIVER, feeAmount); + emit MemberSlashed(member.index, receiver); + } + + /// @dev Groth16 proof verification + function _verifyProof( + uint256 identityCommitment, + address receiver, + uint256[8] calldata proof + ) + internal + view + returns (bool) + { + return verifier.verifyProof( + [proof[0], proof[1]], + [[proof[2], proof[3]], [proof[4], proof[5]]], + [proof[6], proof[7]], + [identityCommitment, uint256(uint160(receiver))] + ); + } +} diff --git a/src/rln/Verifier.sol b/src/rln/Verifier.sol new file mode 100644 index 0000000..4a1fc79 --- /dev/null +++ b/src/rln/Verifier.sol @@ -0,0 +1,202 @@ +// SPDX-License-Identifier: GPL-3.0 +/* + Copyright 2021 0KIMS association. + + This file is generated with [snarkJS](https://github.com/iden3/snarkjs). + + snarkJS is a free software: you can redistribute it and/or modify it + under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + snarkJS is distributed in the hope that it will be useful, but WITHOUT + ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public + License for more details. + + You should have received a copy of the GNU General Public License + along with snarkJS. If not, see . +*/ + +pragma solidity >=0.7.0 <0.9.0; + +contract Groth16Verifier { + // Scalar field size + uint256 constant r = + 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; + // Base field size + uint256 constant q = + 21_888_242_871_839_275_222_246_405_745_257_275_088_696_311_157_297_823_662_689_037_894_645_226_208_583; + + // Verification Key data + uint256 constant alphax = + 20_491_192_805_390_485_299_153_009_773_594_534_940_189_261_866_228_447_918_068_658_471_970_481_763_042; + uint256 constant alphay = + 9_383_485_363_053_290_200_918_347_156_157_836_566_562_967_994_039_712_273_449_902_621_266_178_545_958; + uint256 constant betax1 = + 4_252_822_878_758_300_859_123_897_981_450_591_353_533_073_413_197_771_768_651_442_665_752_259_397_132; + uint256 constant betax2 = + 6_375_614_351_688_725_206_403_948_262_868_962_793_625_744_043_794_305_715_222_011_528_459_656_738_731; + uint256 constant betay1 = + 21_847_035_105_528_745_403_288_232_691_147_584_728_191_162_732_299_865_338_377_159_692_350_059_136_679; + uint256 constant betay2 = + 10_505_242_626_370_262_277_552_901_082_094_356_697_409_835_680_220_590_971_873_171_140_371_331_206_856; + uint256 constant gammax1 = + 11_559_732_032_986_387_107_991_004_021_392_285_783_925_812_861_821_192_530_917_403_151_452_391_805_634; + uint256 constant gammax2 = + 10_857_046_999_023_057_135_944_570_762_232_829_481_370_756_359_578_518_086_990_519_993_285_655_852_781; + uint256 constant gammay1 = + 4_082_367_875_863_433_681_332_203_403_145_435_568_316_851_327_593_401_208_105_741_076_214_120_093_531; + uint256 constant gammay2 = + 8_495_653_923_123_431_417_604_973_247_489_272_438_418_190_587_263_600_148_770_280_649_306_958_101_930; + uint256 constant deltax1 = + 11_551_021_181_461_167_826_461_759_140_322_976_337_427_023_987_127_500_996_455_457_136_635_486_400_849; + uint256 constant deltax2 = + 16_440_331_697_014_556_916_876_897_072_677_669_680_121_386_347_171_004_539_353_252_116_876_383_229_648; + uint256 constant deltay1 = + 16_935_666_883_311_644_237_478_257_793_713_075_046_951_222_500_094_443_222_621_864_730_929_684_383_196; + uint256 constant deltay2 = + 15_542_122_065_551_809_636_611_024_015_159_689_732_218_122_471_819_623_718_488_423_844_152_861_811_271; + + uint256 constant IC0x = + 19_490_069_286_251_317_200_471_893_224_761_952_280_235_157_078_692_599_655_063_040_494_106_083_015_102; + uint256 constant IC0y = + 15_613_730_057_977_833_735_664_106_983_317_680_013_118_142_165_231_654_768_046_521_650_638_333_652_991; + + uint256 constant IC1x = + 1_563_543_155_852_853_229_359_605_494_188_815_884_199_915_022_658_219_002_707_722_789_976_065_966_419; + uint256 constant IC1y = + 858_819_375_930_654_753_672_617_171_465_307_097_688_802_650_498_051_619_587_167_586_479_724_200_799; + + uint256 constant IC2x = + 3_808_889_614_445_935_800_597_561_392_085_733_302_718_838_702_771_107_544_944_545_050_886_958_022_904; + uint256 constant IC2y = + 13_293_649_293_049_947_010_793_838_294_353_767_499_934_999_769_633_605_908_974_566_715_226_392_122_400; + + // Memory data + uint16 constant pVk = 0; + uint16 constant pPairing = 128; + + uint16 constant pLastMem = 896; + + function verifyProof( + uint256[2] calldata _pA, + uint256[2][2] calldata _pB, + uint256[2] calldata _pC, + uint256[2] calldata _pubSignals + ) + public + view + returns (bool) + { + assembly { + function checkField(v) { + if iszero(lt(v, q)) { + mstore(0, 0) + return(0, 0x20) + } + } + + // G1 function to multiply a G1 value(x,y) to value in an address + function g1_mulAccC(pR, x, y, s) { + let success + let mIn := mload(0x40) + mstore(mIn, x) + mstore(add(mIn, 32), y) + mstore(add(mIn, 64), s) + + success := staticcall(sub(gas(), 2000), 7, mIn, 96, mIn, 64) + + if iszero(success) { + mstore(0, 0) + return(0, 0x20) + } + + mstore(add(mIn, 64), mload(pR)) + mstore(add(mIn, 96), mload(add(pR, 32))) + + success := staticcall(sub(gas(), 2000), 6, mIn, 128, pR, 64) + + if iszero(success) { + mstore(0, 0) + return(0, 0x20) + } + } + + function checkPairing(pA, pB, pC, pubSignals, pMem) -> isOk { + let _pPairing := add(pMem, pPairing) + let _pVk := add(pMem, pVk) + + mstore(_pVk, IC0x) + mstore(add(_pVk, 32), IC0y) + + // Compute the linear combination vk_x + + g1_mulAccC(_pVk, IC1x, IC1y, calldataload(add(pubSignals, 0))) + + g1_mulAccC(_pVk, IC2x, IC2y, calldataload(add(pubSignals, 32))) + + // -A + mstore(_pPairing, calldataload(pA)) + mstore(add(_pPairing, 32), mod(sub(q, calldataload(add(pA, 32))), q)) + + // B + mstore(add(_pPairing, 64), calldataload(pB)) + mstore(add(_pPairing, 96), calldataload(add(pB, 32))) + mstore(add(_pPairing, 128), calldataload(add(pB, 64))) + mstore(add(_pPairing, 160), calldataload(add(pB, 96))) + + // alpha1 + mstore(add(_pPairing, 192), alphax) + mstore(add(_pPairing, 224), alphay) + + // beta2 + mstore(add(_pPairing, 256), betax1) + mstore(add(_pPairing, 288), betax2) + mstore(add(_pPairing, 320), betay1) + mstore(add(_pPairing, 352), betay2) + + // vk_x + mstore(add(_pPairing, 384), mload(add(pMem, pVk))) + mstore(add(_pPairing, 416), mload(add(pMem, add(pVk, 32)))) + + // gamma2 + mstore(add(_pPairing, 448), gammax1) + mstore(add(_pPairing, 480), gammax2) + mstore(add(_pPairing, 512), gammay1) + mstore(add(_pPairing, 544), gammay2) + + // C + mstore(add(_pPairing, 576), calldataload(pC)) + mstore(add(_pPairing, 608), calldataload(add(pC, 32))) + + // delta2 + mstore(add(_pPairing, 640), deltax1) + mstore(add(_pPairing, 672), deltax2) + mstore(add(_pPairing, 704), deltay1) + mstore(add(_pPairing, 736), deltay2) + + let success := staticcall(sub(gas(), 2000), 8, _pPairing, 768, _pPairing, 0x20) + + isOk := and(success, mload(_pPairing)) + } + + let pMem := mload(0x40) + mstore(0x40, add(pMem, pLastMem)) + + // Validate that all evaluations ∈ F + + checkField(calldataload(add(_pubSignals, 0))) + + checkField(calldataload(add(_pubSignals, 32))) + + checkField(calldataload(add(_pubSignals, 64))) + + // Validate all evaluations + let isValid := checkPairing(_pA, _pB, _pC, _pubSignals, pMem) + + mstore(0, isValid) + return(0, 0x20) + } + } +} diff --git a/test/RLN.t.sol b/test/RLN.t.sol new file mode 100644 index 0000000..aecb48e --- /dev/null +++ b/test/RLN.t.sol @@ -0,0 +1,395 @@ +// SPDX-License-Identifier: Apache-2.0 OR MIT +pragma solidity ^0.8.17; + +import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; + +import "forge-std/Test.sol"; + +import "../src/rln/RLN.sol"; +import { IVerifier } from "../src/rln/IVerifier.sol"; + +// A ERC20 token contract which allows arbitrary minting for testing +contract TestERC20 is ERC20 { + constructor() ERC20("TestERC20", "TST") { } + + function mint(address to, uint256 amount) external { + _mint(to, amount); + } +} + +// A mock verifier which makes us skip the proof verification. +contract MockVerifier is IVerifier { + bool public result; + + constructor() { + result = true; + } + + function verifyProof( + uint256[2] memory, + uint256[2][2] memory, + uint256[2] memory, + uint256[2] memory + ) + external + view + returns (bool) + { + return result; + } + + function changeResult(bool _result) external { + result = _result; + } +} + +contract RLNTest is Test { + event MemberRegistered(uint256 identityCommitment, uint256 messageLimit, uint256 index); + event MemberWithdrawn(uint256 index); + event MemberSlashed(uint256 index, address slasher); + + RLN rln; + TestERC20 token; + MockVerifier verifier; + + uint256 rlnInitialTokenBalance = 1_000_000; + uint256 minimalDeposit = 100; + uint256 maximalRate = 1 << 16 - 1; + uint256 depth = 20; + uint8 feePercentage = 10; + address feeReceiver = makeAddr("feeReceiver"); + uint256 freezePeriod = 1; + + uint256 identityCommitment0 = 1234; + uint256 identityCommitment1 = 5678; + + address user0 = makeAddr("user0"); + address user1 = makeAddr("user1"); + address slashedReceiver = makeAddr("slashedReceiver"); + + uint256 messageLimit0 = 2; + uint256 messageLimit1 = 3; + + uint256[8] mockProof = + [uint256(0), uint256(1), uint256(2), uint256(3), uint256(4), uint256(5), uint256(6), uint256(7)]; + + function setUp() public { + token = new TestERC20(); + verifier = new MockVerifier(); + rln = new RLN( + minimalDeposit, + maximalRate, + depth, + feePercentage, + feeReceiver, + freezePeriod, + address(token), + address(verifier) + ); + } + + function test_initial_state() public { + assertEq(rln.MINIMAL_DEPOSIT(), minimalDeposit); + assertEq(rln.SET_SIZE(), 1 << depth); + assertEq(rln.FEE_PERCENTAGE(), feePercentage); + assertEq(rln.FEE_RECEIVER(), feeReceiver); + assertEq(rln.FREEZE_PERIOD(), freezePeriod); + assertEq(address(rln.token()), address(token)); + assertEq(address(rln.verifier()), address(verifier)); + assertEq(rln.identityCommitmentIndex(), 0); + } + + /* register */ + + function test_register_succeeds() public { + // Test: register one user + register(user0, identityCommitment0, messageLimit0); + // Test: register second user + register(user1, identityCommitment1, messageLimit1); + } + + function test_register_fails_when_index_exceeds_set_size() public { + // Set size is (1 << smallDepth) = 2, and thus there can + // only be 2 members, otherwise reverts. + uint256 smallDepth = 1; + TestERC20 _token = new TestERC20(); + RLN smallRLN = new RLN( + minimalDeposit, maximalRate, smallDepth, feePercentage, feeReceiver, 0, address(_token), address(verifier) + ); + + // Register the first user + _token.mint(user0, minimalDeposit); + vm.startPrank(user0); + _token.approve(address(smallRLN), minimalDeposit); + smallRLN.register(identityCommitment0, minimalDeposit); + vm.stopPrank(); + // Register the second user + _token.mint(user1, minimalDeposit); + vm.startPrank(user1); + _token.approve(address(smallRLN), minimalDeposit); + smallRLN.register(identityCommitment1, minimalDeposit); + vm.stopPrank(); + // Now tree (set) is full. Try register the third. It should revert. + address user2 = makeAddr("user2"); + uint256 identityCommitment2 = 9999; + token.mint(user2, minimalDeposit); + vm.startPrank(user2); + token.approve(address(smallRLN), minimalDeposit); + // `register` should revert + vm.expectRevert("RLN, register: set is full"); + smallRLN.register(identityCommitment2, minimalDeposit); + vm.stopPrank(); + } + + function test_register_fails_when_amount_lt_minimal_deposit() public { + uint256 insufficientAmount = minimalDeposit - 1; + token.mint(user0, rlnInitialTokenBalance); + vm.startPrank(user0); + token.approve(address(rln), rlnInitialTokenBalance); + vm.expectRevert("RLN, register: amount is lower than minimal deposit"); + rln.register(identityCommitment0, insufficientAmount); + vm.stopPrank(); + } + + function test_register_fails_when_duplicate_identity_commitments() public { + // Register first with user0 with identityCommitment0 + register(user0, identityCommitment0, messageLimit0); + // Register again with user1 with identityCommitment0 + token.mint(user1, rlnInitialTokenBalance); + vm.startPrank(user1); + token.approve(address(rln), rlnInitialTokenBalance); + // `register` should revert + vm.expectRevert("RLN, register: idCommitment already registered"); + rln.register(identityCommitment0, rlnInitialTokenBalance); + vm.stopPrank(); + } + + /* withdraw */ + + function test_withdraw_succeeds() public { + // Register first + register(user0, identityCommitment0, messageLimit0); + // Make sure proof verification is skipped + assertEq(verifier.result(), true); + + // Withdraw user0 + // Ensure event is emitted + (,, uint256 index) = rln.members(identityCommitment0); + vm.expectEmit(true, true, false, true); + emit MemberWithdrawn(index); + rln.withdraw(identityCommitment0, mockProof); + // Check withdrawal entry is set correctly + (uint256 blockNumber, uint256 amount, address receiver) = rln.withdrawals(identityCommitment0); + assertEq(blockNumber, block.number); + assertEq(amount, getRegisterAmount(messageLimit0)); + assertEq(receiver, user0); + } + + function test_withdraw_fails_when_not_registered() public { + // Withdraw fails if the user has not registered before + vm.expectRevert("RLN, withdraw: member doesn't exist"); + rln.withdraw(identityCommitment0, mockProof); + } + + function test_withdraw_fails_when_already_underways() public { + // Register first + register(user0, identityCommitment0, messageLimit0); + // Withdraw user0 + rln.withdraw(identityCommitment0, mockProof); + // Withdraw again and it should fail + vm.expectRevert("RLN, release: such withdrawal exists"); + rln.withdraw(identityCommitment0, mockProof); + } + + function test_withdraw_fails_when_invalid_proof() public { + // Register first + register(user0, identityCommitment0, messageLimit0); + // Make sure mock verifier always return false + // And thus the proof is always considered invalid + verifier.changeResult(false); + assertEq(verifier.result(), false); + vm.expectRevert("RLN, withdraw: invalid proof"); + rln.withdraw(identityCommitment0, mockProof); + } + + /* release */ + + function test_release_succeeds() public { + // Register first + register(user0, identityCommitment0, messageLimit0); + // Withdraw user0 + // Make sure proof verification is skipped + assertEq(verifier.result(), true); + rln.withdraw(identityCommitment0, mockProof); + + // Test: release succeeds after freeze period + // Set block.number to `blockNumbersToRelease` + uint256 blockNumbersToRelease = getUnfrozenBlockHeight(); + vm.roll(blockNumbersToRelease); + + uint256 user0BalanceBefore = token.balanceOf(user0); + uint256 rlnBalanceBefore = token.balanceOf(address(rln)); + // Calls release and check balances + rln.release(identityCommitment0); + uint256 user0BalanceDiff = token.balanceOf(user0) - user0BalanceBefore; + uint256 rlnBalanceDiff = rlnBalanceBefore - token.balanceOf(address(rln)); + uint256 expectedUser0BalanceDiff = getRegisterAmount(messageLimit0); + assertEq(user0BalanceDiff, expectedUser0BalanceDiff); + assertEq(rlnBalanceDiff, expectedUser0BalanceDiff); + checkUserIsDeleted(identityCommitment0); + } + + function test_release_fails_when_no_withdrawal() public { + // Release fails if there is no withdrawal for the user + vm.expectRevert("RLN, release: no such withdrawals"); + rln.release(identityCommitment0); + } + + function test_release_fails_when_freeze_period() public { + // Register first + register(user0, identityCommitment0, messageLimit0); + // Make sure mock verifier always return true to skip proof verification + assertEq(verifier.result(), true); + // Withdraw user0 + rln.withdraw(identityCommitment0, mockProof); + // Ensure withdrawal is set + (uint256 blockNumber, uint256 amount, address receiver) = rln.withdrawals(identityCommitment0); + assertEq(blockNumber, block.number); + assertEq(amount, getRegisterAmount(messageLimit0)); + assertEq(receiver, user0); + + // Test: release fails in freeze period + vm.expectRevert("RLN, release: cannot release yet"); + rln.release(identityCommitment0); + // Set block.number to blockNumbersToRelease - 1, which is still in freeze period + uint256 blockNumbersToRelease = getUnfrozenBlockHeight(); + vm.roll(blockNumbersToRelease - 1); + vm.expectRevert("RLN, release: cannot release yet"); + rln.release(identityCommitment0); + } + + /* slash */ + + function test_slash_succeeds() public { + // Test: register and get slashed + register(user0, identityCommitment0, messageLimit0); + uint256 registerAmount = getRegisterAmount(messageLimit0); + uint256 slashFee = getSlashFee(registerAmount); + uint256 slashReward = registerAmount - slashFee; + uint256 slashedReceiverBalanceBefore = token.balanceOf(slashedReceiver); + uint256 rlnBalanceBefore = token.balanceOf(address(rln)); + uint256 feeReceiverBalanceBefore = token.balanceOf(feeReceiver); + // ensure event is emitted + (,, uint256 index) = rln.members(identityCommitment0); + vm.expectEmit(true, true, false, true); + emit MemberSlashed(index, slashedReceiver); + // Slash and check balances + rln.slash(identityCommitment0, slashedReceiver, mockProof); + uint256 slashedReceiverBalanceDiff = token.balanceOf(slashedReceiver) - slashedReceiverBalanceBefore; + uint256 rlnBalanceDiff = rlnBalanceBefore - token.balanceOf(address(rln)); + uint256 feeReceiverBalanceDiff = token.balanceOf(feeReceiver) - feeReceiverBalanceBefore; + assertEq(slashedReceiverBalanceDiff, slashReward); + assertEq(rlnBalanceDiff, registerAmount); + assertEq(feeReceiverBalanceDiff, slashFee); + // Check the record of user0 has been deleted + checkUserIsDeleted(identityCommitment0); + + // Test: register, withdraw, ang get slashed before release + register(user1, identityCommitment1, messageLimit1); + rln.withdraw(identityCommitment1, mockProof); + rln.slash(identityCommitment1, slashedReceiver, mockProof); + // Check the record of user1 has been deleted + checkUserIsDeleted(identityCommitment1); + } + + function test_slash_fails_when_receiver_is_zero() public { + // Register first + register(user0, identityCommitment0, messageLimit0); + // Try slash user0 and it fails because of the zero address + vm.expectRevert("RLN, slash: empty receiver address"); + rln.slash(identityCommitment0, address(0), mockProof); + } + + function test_slash_fails_when_not_registered() public { + // It fails if the user is not registered yet + vm.expectRevert("RLN, slash: member doesn't exist"); + rln.slash(identityCommitment0, slashedReceiver, mockProof); + } + + function test_slash_fails_when_self_slashing() public { + // `slash` fails when receiver is the same as the registered msg.sender + register(user0, identityCommitment0, messageLimit0); + vm.expectRevert("RLN, slash: self-slashing is prohibited"); + rln.slash(identityCommitment0, user0, mockProof); + } + + function test_slash_fails_when_invalid_proof() public { + // It fails if the proof is invalid + // Register first + register(user0, identityCommitment0, messageLimit0); + // Make sure mock verifier always return false + // And thus the proof is always considered invalid + verifier.changeResult(false); + assertEq(verifier.result(), false); + vm.expectRevert("RLN, slash: invalid proof"); + // Slash fails because of the invalid proof + rln.slash(identityCommitment0, slashedReceiver, mockProof); + } + + /* Helpers */ + function getRegisterAmount(uint256 messageLimit) public view returns (uint256) { + return messageLimit * minimalDeposit; + } + + function register(address user, uint256 identityCommitment, uint256 messageLimit) public { + // Mint to user first + uint256 registerTokenAmount = getRegisterAmount(messageLimit); + token.mint(user, registerTokenAmount); + // Remember the balance for later check + uint256 tokenRLNBefore = token.balanceOf(address(rln)); + uint256 tokenUserBefore = token.balanceOf(user); + uint256 identityCommitmentIndexBefore = rln.identityCommitmentIndex(); + // User approves to rln and calls register + vm.startPrank(user); + token.approve(address(rln), registerTokenAmount); + // Ensure event is emitted + vm.expectEmit(true, true, false, true); + emit MemberRegistered(identityCommitment, messageLimit, identityCommitmentIndexBefore); + rln.register(identityCommitment, registerTokenAmount); + vm.stopPrank(); + + // Check states + uint256 tokenRLNDiff = token.balanceOf(address(rln)) - tokenRLNBefore; + uint256 tokenUserDiff = tokenUserBefore - token.balanceOf(user); + // RLN state + assertEq(rln.identityCommitmentIndex(), identityCommitmentIndexBefore + 1); + assertEq(tokenRLNDiff, registerTokenAmount); + // User state + (address userAddress, uint256 actualMessageLimit, uint256 index) = rln.members(identityCommitment); + assertEq(userAddress, user); + assertEq(actualMessageLimit, messageLimit); + assertEq(index, identityCommitmentIndexBefore); + assertEq(tokenUserDiff, registerTokenAmount); + } + + function getUnfrozenBlockHeight() public view returns (uint256) { + return block.number + freezePeriod + 1; + } + + function checkUserIsDeleted(uint256 identityCommitment) public { + // User state + (address userAddress, uint256 actualMessageLimit, uint256 index) = rln.members(identityCommitment); + assertEq(userAddress, address(0)); + assertEq(actualMessageLimit, 0); + assertEq(index, 0); + // Withdrawal state + (uint256 blockNumber, uint256 amount, address receiver) = rln.withdrawals(identityCommitment); + assertEq(blockNumber, 0); + assertEq(amount, 0); + assertEq(receiver, address(0)); + } + + function getSlashFee(uint256 registerAmount) public view returns (uint256) { + return registerAmount * feePercentage / 100; + } +}