From edd63e75f95f659c676e41908696fbf0e7ea4e0f Mon Sep 17 00:00:00 2001 From: Ricardo Guilherme Schmidt <3esmit@gmail.com> Date: Wed, 4 Jun 2025 23:13:11 -0300 Subject: [PATCH] feat(RLN): implement KarmaRLN Closes #217, #218, #219 --- .gas-report | 80 +++---- .gas-snapshot | 29 +-- script/RLN.s.sol | 42 ++-- src/rln/RLN.sol | 213 ++++++++----------- test/RLN.t.sol | 543 ++++++++++++++++++++--------------------------- 5 files changed, 379 insertions(+), 528 deletions(-) diff --git a/.gas-report b/.gas-report index f9ada36..35f6ce5 100644 --- a/.gas-report +++ b/.gas-report @@ -4,13 +4,13 @@ +=================================================================================================================================================+ | Deployment Cost | Deployment Size | | | | | |-------------------------------------------------------------------------------------------+-----------------+-------+--------+--------+---------| -| 0 | 1374 | | | | | +| 295493 | 1374 | | | | | |-------------------------------------------------------------------------------------------+-----------------+-------+--------+--------+---------| | | | | | | | |-------------------------------------------------------------------------------------------+-----------------+-------+--------+--------+---------| | Function Name | Min | Avg | Median | Max | # Calls | |-------------------------------------------------------------------------------------------+-----------------+-------+--------+--------+---------| -| fallback | 5145 | 65850 | 33119 | 193478 | 3440 | +| fallback | 5145 | 64591 | 33119 | 193478 | 3571 | ╰-------------------------------------------------------------------------------------------+-----------------+-------+--------+--------+---------╯ ╭-----------------------------------------------------+-----------------+---------+---------+---------+---------╮ @@ -24,7 +24,7 @@ |-----------------------------------------------------+-----------------+---------+---------+---------+---------| | Function Name | Min | Avg | Median | Max | # Calls | |-----------------------------------------------------+-----------------+---------+---------+---------+---------| -| run | 4666141 | 4666141 | 4666141 | 4666141 | 176 | +| run | 4666141 | 4666141 | 4666141 | 4666141 | 186 | ╰-----------------------------------------------------+-----------------+---------+---------+---------+---------╯ ╭-----------------------------------------------------------+-----------------+---------+---------+---------+---------╮ @@ -80,7 +80,7 @@ |---------------------------------------------------------+-----------------+------+--------+------+---------| | Function Name | Min | Avg | Median | Max | # Calls | |---------------------------------------------------------+-----------------+------+--------+------+---------| -| activeNetworkConfig | 455 | 2076 | 455 | 4455 | 528 | +| activeNetworkConfig | 455 | 2090 | 455 | 4455 | 548 | ╰---------------------------------------------------------+-----------------+------+--------+------+---------╯ ╭---------------------------------------------------------------------+-----------------+---------+---------+---------+---------╮ @@ -114,11 +114,11 @@ |------------------------------+-----------------+--------+--------+--------+---------| | OPERATOR_ROLE | 262 | 262 | 262 | 262 | 2 | |------------------------------+-----------------+--------+--------+--------+---------| -| SLASHER_ROLE | 262 | 262 | 262 | 262 | 24 | +| SLASHER_ROLE | 262 | 262 | 262 | 262 | 34 | |------------------------------+-----------------+--------+--------+--------+---------| | accountSlashAmount | 2611 | 2611 | 2611 | 2611 | 2 | |------------------------------+-----------------+--------+--------+--------+---------| -| addRewardDistributor | 29975 | 63645 | 70903 | 70903 | 284 | +| addRewardDistributor | 29975 | 63560 | 70903 | 70903 | 304 | |------------------------------+-----------------+--------+--------+--------+---------| | allowance | 573 | 573 | 573 | 573 | 8 | |------------------------------+-----------------+--------+--------+--------+---------| @@ -130,13 +130,13 @@ |------------------------------+-----------------+--------+--------+--------+---------| | getRewardDistributors | 5132 | 7710 | 9644 | 9644 | 21 | |------------------------------+-----------------+--------+--------+--------+---------| -| grantRole | 29490 | 29490 | 29490 | 29490 | 29 | +| grantRole | 29490 | 29490 | 29490 | 29490 | 39 | |------------------------------+-----------------+--------+--------+--------+---------| | hasRole | 2754 | 2754 | 2754 | 2754 | 4 | |------------------------------+-----------------+--------+--------+--------+---------| -| initialize | 116796 | 116796 | 116796 | 116796 | 176 | +| initialize | 116796 | 116796 | 116796 | 116796 | 186 | |------------------------------+-----------------+--------+--------+--------+---------| -| mint | 4869 | 50368 | 51342 | 51342 | 550 | +| mint | 4869 | 50370 | 51342 | 51342 | 551 | |------------------------------+-----------------+--------+--------+--------+---------| | removeRewardDistributor | 5080 | 22644 | 29995 | 30358 | 28 | |------------------------------+-----------------+--------+--------+--------+---------| @@ -144,7 +144,7 @@ |------------------------------+-----------------+--------+--------+--------+---------| | setReward | 4845 | 144102 | 166754 | 166754 | 319 | |------------------------------+-----------------+--------+--------+--------+---------| -| slash | 4803 | 103614 | 85757 | 123125 | 519 | +| slash | 4803 | 103655 | 85757 | 123125 | 520 | |------------------------------+-----------------+--------+--------+--------+---------| | slashedAmountOf | 17682 | 28099 | 28120 | 28120 | 516 | |------------------------------+-----------------+--------+--------+--------+---------| @@ -396,7 +396,7 @@ +===============================================================================================================================================+ | Deployment Cost | Deployment Size | | | | | |------------------------------------------------------------------------------------------+-----------------+-------+--------+-------+---------| -| 1204853 | 6207 | | | | | +| 1204853 | 6015 | | | | | |------------------------------------------------------------------------------------------+-----------------+-------+--------+-------+---------| | | | | | | | |------------------------------------------------------------------------------------------+-----------------+-------+--------+-------+---------| @@ -436,39 +436,35 @@ +=====================================================================================+ | Deployment Cost | Deployment Size | | | | | |------------------------------+-----------------+--------+--------+--------+---------| -| 1396408 | 7094 | | | | | +| 1982512 | 9143 | | | | | |------------------------------+-----------------+--------+--------+--------+---------| | | | | | | | |------------------------------+-----------------+--------+--------+--------+---------| | Function Name | Min | Avg | Median | Max | # Calls | |------------------------------+-----------------+--------+--------+--------+---------| -| FEE_PERCENTAGE | 217 | 217 | 217 | 217 | 1 | +| DEFAULT_ADMIN_ROLE | 262 | 262 | 262 | 262 | 10 | |------------------------------+-----------------+--------+--------+--------+---------| -| FEE_RECEIVER | 226 | 226 | 226 | 226 | 1 | +| REGISTER_ROLE | 262 | 262 | 262 | 262 | 10 | |------------------------------+-----------------+--------+--------+--------+---------| -| FREEZE_PERIOD | 218 | 218 | 218 | 218 | 1 | +| SET_SIZE | 2339 | 2339 | 2339 | 2339 | 1 | |------------------------------+-----------------+--------+--------+--------+---------| -| MINIMAL_DEPOSIT | 241 | 241 | 241 | 241 | 1 | +| SLASHER_ROLE | 262 | 262 | 262 | 262 | 10 | |------------------------------+-----------------+--------+--------+--------+---------| -| SET_SIZE | 284 | 284 | 284 | 284 | 1 | +| exit | 7068 | 16233 | 18694 | 22939 | 3 | |------------------------------+-----------------+--------+--------+--------+---------| -| identityCommitmentIndex | 2362 | 2362 | 2362 | 2362 | 27 | +| hasRole | 2707 | 2707 | 2707 | 2707 | 30 | |------------------------------+-----------------+--------+--------+--------+---------| -| members | 6826 | 6826 | 6826 | 6826 | 18 | +| identityCommitmentIndex | 2340 | 2340 | 2340 | 2340 | 5 | |------------------------------+-----------------+--------+--------+--------+---------| -| register | 23877 | 105927 | 123897 | 126709 | 18 | +| initialize | 165036 | 165036 | 165036 | 165036 | 11 | |------------------------------+-----------------+--------+--------+--------+---------| -| release | 28093 | 37708 | 28192 | 66355 | 4 | +| members | 4669 | 4669 | 4669 | 4669 | 6 | |------------------------------+-----------------+--------+--------+--------+---------| -| slash | 23015 | 49020 | 34650 | 99415 | 6 | +| register | 6997 | 45345 | 53001 | 55801 | 11 | |------------------------------+-----------------+--------+--------+--------+---------| -| token | 292 | 292 | 292 | 292 | 1 | +| slash | 7111 | 54764 | 18737 | 138444 | 3 | |------------------------------+-----------------+--------+--------+--------+---------| -| verifier | 272 | 272 | 272 | 272 | 1 | -|------------------------------+-----------------+--------+--------+--------+---------| -| withdraw | 29336 | 79495 | 106774 | 106774 | 8 | -|------------------------------+-----------------+--------+--------+--------+---------| -| withdrawals | 6792 | 6792 | 6792 | 6792 | 5 | +| verifier | 2359 | 2359 | 2359 | 2359 | 1 | ╰------------------------------+-----------------+--------+--------+--------+---------╯ ╭--------------------------------------+-----------------+-------+--------+-------+---------╮ @@ -484,29 +480,11 @@ |--------------------------------------+-----------------+-------+--------+-------+---------| | changeResult | 21649 | 21649 | 21649 | 21649 | 2 | |--------------------------------------+-----------------+-------+--------+-------+---------| -| result | 2298 | 2298 | 2298 | 2298 | 5 | +| result | 2298 | 2298 | 2298 | 2298 | 3 | |--------------------------------------+-----------------+-------+--------+-------+---------| -| verifyProof | 4790 | 4790 | 4790 | 4790 | 9 | +| verifyProof | 4790 | 4790 | 4790 | 4790 | 4 | ╰--------------------------------------+-----------------+-------+--------+-------+---------╯ -╭-----------------------------------+-----------------+-------+--------+-------+---------╮ -| 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 | | | | | | +========================================================================================================================+ @@ -518,11 +496,11 @@ |-------------------------------------------------------------------+-----------------+-------+--------+-------+---------| | Function Name | Min | Avg | Median | Max | # Calls | |-------------------------------------------------------------------+-----------------+-------+--------+-------+---------| -| rewardsBalanceOfAccount | 549 | 1986 | 2549 | 2549 | 3675 | +| rewardsBalanceOfAccount | 549 | 1985 | 2549 | 2549 | 3679 | |-------------------------------------------------------------------+-----------------+-------+--------+-------+---------| | setTotalKarmaShares | 43589 | 43589 | 43589 | 43589 | 48 | |-------------------------------------------------------------------+-----------------+-------+--------+-------+---------| -| setUserKarmaShare | 24210 | 44068 | 44134 | 44266 | 530 | +| setUserKarmaShare | 24210 | 44107 | 44134 | 44266 | 531 | |-------------------------------------------------------------------+-----------------+-------+--------+-------+---------| | totalRewardsSupply | 2324 | 2324 | 2324 | 2324 | 48 | ╰-------------------------------------------------------------------+-----------------+-------+--------+-------+---------╯ @@ -546,7 +524,7 @@ +==================================================================================================+ | Deployment Cost | Deployment Size | | | | | |---------------------------------------------+-----------------+-------+--------+-------+---------| -| 770657 | 3987 | | | | | +| 770741 | 3987 | | | | | |---------------------------------------------+-----------------+-------+--------+-------+---------| | | | | | | | |---------------------------------------------+-----------------+-------+--------+-------+---------| diff --git a/.gas-snapshot b/.gas-snapshot index 640a5ef..d7df711 100644 --- a/.gas-snapshot +++ b/.gas-snapshot @@ -112,23 +112,16 @@ OverflowTest:testTotalSupply() (gas: 359391) OverflowTest:testTransfersNotAllowed() (gas: 61925) OverflowTest:test_RevertWhen_MintingCausesOverflow() (gas: 129592) OverflowTest:test_RevertWhen_SettingRewardCausesOverflow() (gas: 127920) -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) +RLNTest:test_exit_fails_when_invalid_proof() (gas: 195432) +RLNTest:test_exit_fails_when_not_registered() (gas: 64843) +RLNTest:test_exit_succeeds() (gas: 187485) +RLNTest:test_initial_state() (gas: 60571) +RLNTest:test_register_fails_when_duplicate_identity_commitment() (gas: 131752) +RLNTest:test_register_fails_when_index_exceeds_set_size() (gas: 2569410) +RLNTest:test_register_succeeds() (gas: 272680) +RLNTest:test_slash_fails_when_invalid_proof() (gas: 195506) +RLNTest:test_slash_fails_when_not_registered() (gas: 64886) +RLNTest:test_slash_succeeds() (gas: 438591) RemoveRewardDistributorTest:testAddKarmaDistributorOnlyAdmin() (gas: 438248) RemoveRewardDistributorTest:testBalanceOf() (gas: 456715) RemoveRewardDistributorTest:testBalanceOfWithNoSystemTotalKarma() (gas: 83783) @@ -156,7 +149,7 @@ SetRewardTest:test_RevertWhen_SenderIsNotOperator() (gas: 61893) SlashAmountOfTest:testAddKarmaDistributorOnlyAdmin() (gas: 438224) SlashAmountOfTest:testBalanceOf() (gas: 456642) SlashAmountOfTest:testBalanceOfWithNoSystemTotalKarma() (gas: 83783) -SlashAmountOfTest:testFuzz_SlashAmountOf(uint256,uint256,uint256) (runs: 1000, μ: 407786, ~: 408571) +SlashAmountOfTest:testFuzz_SlashAmountOf(uint256,uint256,uint256) (runs: 1000, μ: 408297, ~: 409081) SlashAmountOfTest:testMintOnlyAdmin() (gas: 429075) SlashAmountOfTest:testRemoveKarmaDistributorOnlyOwner() (gas: 163437) SlashAmountOfTest:testRemoveUnknownKarmaDistributor() (gas: 41654) diff --git a/script/RLN.s.sol b/script/RLN.s.sol index 35b9e2e..1f1bd45 100644 --- a/script/RLN.s.sol +++ b/script/RLN.s.sol @@ -1,27 +1,33 @@ -// SPDX-License-Identifier: Apache-2.0 OR MIT +// SPDX-License-Identifier: MIT pragma solidity ^0.8.26; -import "forge-std/Script.sol"; -import "../src/rln/RLN.sol"; -import "../src/rln/Verifier.sol"; +import { BaseScript } from "./Base.s.sol"; +import { DeploymentConfig } from "./DeploymentConfig.s.sol"; + +import { ERC1967Proxy } from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; + +import { Groth16Verifier } from "../src/rln/Verifier.sol"; +import { RLN } from "../src/rln/RLN.sol"; + +contract DeployRLNScript is BaseScript { + function run() public returns (RLN, DeploymentConfig) { + DeploymentConfig deploymentConfig = new DeploymentConfig(broadcaster); + (address deployer,) = deploymentConfig.activeNetworkConfig(); -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"); + address karmaAddress = vm.envAddress("KARMA_ADDRESS"); - vm.startBroadcast(); - - Groth16Verifier verifier = new Groth16Verifier(); - RLN rln = new RLN( - minimalDeposit, maximalRate, depth, feePercentage, feeReceiver, freezePeriod, token, address(verifier) - ); + vm.startBroadcast(deployer); + address verifier = (address)(new Groth16Verifier()); + // Deploy Karma logic contract + bytes memory initializeData = + abi.encodeCall(RLN.initialize, (deployer, deployer, deployer, depth, verifier, karmaAddress)); + address impl = address(new RLN()); + // Create upgradeable proxy + address proxy = address(new ERC1967Proxy(impl, initializeData)); vm.stopBroadcast(); + + return (RLN(proxy), deploymentConfig); } } diff --git a/src/rln/RLN.sol b/src/rln/RLN.sol index 0efbca7..c712943 100644 --- a/src/rln/RLN.sol +++ b/src/rln/RLN.sol @@ -1,211 +1,168 @@ // SPDX-License-Identifier: Apache-2.0 OR MIT -pragma solidity ^0.8.26; +pragma solidity 0.8.26; -import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import { Karma } from "../Karma.sol"; + +import { AccessControlUpgradeable } from "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol"; +import { Initializable } from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; +import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.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; +contract RLN is Initializable, UUPSUpgradeable, AccessControlUpgradeable { + bytes32 public constant SLASHER_ROLE = keccak256("SLASHER_ROLE"); + bytes32 public constant REGISTER_ROLE = keccak256("REGISTER_ROLE"); + + error RLN__InvalidProof(); + error RLN__MemberNotFound(); + error RLN__IdCommitmentAlreadyRegistered(); + error RLN__SetIsFull(); + error RLN__Unauthorized(); /// @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; + uint256 public SET_SIZE; /// @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; + mapping(uint256 commitment => User user) public members; - /// @dev Withdrawals logic. - mapping(uint256 => Withdrawal) public withdrawals; - - /// @dev ERC20 Token used for staking. - IERC20 public immutable token; + /// @dev Karma Token used for registering. + Karma public karma; /// @dev Groth16 verifier. - IVerifier public immutable verifier; + IVerifier public 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); + event MemberRegistered(uint256 identityCommitment, uint256 index); /// @dev Emmited when a member was withdrawn. /// @param index: index of `identityCommitment`; - event MemberWithdrawn(uint256 index); + event MemberExited(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; + constructor() { + _disableInitializers(); + } + + /// @dev Constructor. + /// @param _owner: address of the owner of the contract; + /// @param _slasher: address of the slasher; + /// @param _register: address of the register; /// @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, + function initialize( + address _owner, + address _slasher, + address _register, 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; + address _verifier, + address _token + ) + public + initializer + { + __UUPSUpgradeable_init(); + __AccessControl_init(); + _setupRole(DEFAULT_ADMIN_ROLE, _owner); + _setupRole(SLASHER_ROLE, _slasher); + _setupRole(REGISTER_ROLE, _register); SET_SIZE = 1 << depth; - FEE_PERCENTAGE = feePercentage; - FEE_RECEIVER = feeReceiver; - FREEZE_PERIOD = freezePeriod; - - token = IERC20(_token); + karma = Karma(_token); verifier = IVerifier(_verifier); } + /** + * @notice Authorizes contract upgrades via UUPS. + * @dev This function is only callable by the owner. + */ + function _authorizeUpgrade(address) internal view override { + if (!hasRole(DEFAULT_ADMIN_ROLE, msg.sender)) { + revert RLN__Unauthorized(); + } + } + /// @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 { + function register(uint256 identityCommitment, address user) external onlyRole(REGISTER_ROLE) { uint256 index = identityCommitmentIndex; + if (index >= SET_SIZE) { + revert RLN__SetIsFull(); + } + if (members[identityCommitment].userAddress != address(0)) { + revert RLN__IdCommitmentAlreadyRegistered(); + } - 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); + members[identityCommitment] = User(user, index); + emit MemberRegistered(identityCommitment, 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. + /// @dev Request for exit. /// @param identityCommitment: `identityCommitment`; /// @param proof: snarkjs's format generated proof (without public inputs) packed consequently. - function withdraw(uint256 identityCommitment, uint256[8] calldata proof) external { + function exit(uint256 identityCommitment, uint256[8] calldata proof) external onlyRole(REGISTER_ROLE) { 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"); + if (member.userAddress == address(0)) { + revert RLN__MemberNotFound(); + } + if (!_verifyProof(identityCommitment, proof)) { + revert RLN__InvalidProof(); + } - 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); + emit MemberExited(member.index); } /// @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"); - + function slash(uint256 identityCommitment, uint256[8] calldata proof) external onlyRole(SLASHER_ROLE) { 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"); + if (member.userAddress == address(0)) { + revert RLN__MemberNotFound(); + } + if (!_verifyProof(identityCommitment, proof)) { + revert RLN__InvalidProof(); + } + karma.slash(member.userAddress); 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); + emit MemberSlashed(member.index, msg.sender); } /// @dev Groth16 proof verification - function _verifyProof( - uint256 identityCommitment, - address receiver, - uint256[8] calldata proof - ) - internal - view - returns (bool) - { + function _verifyProof(uint256 identityCommitment, 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))] + [identityCommitment, uint256(0)] ); } } diff --git a/test/RLN.t.sol b/test/RLN.t.sol index f5f559e..a48bdd6 100644 --- a/test/RLN.t.sol +++ b/test/RLN.t.sol @@ -1,23 +1,18 @@ // SPDX-License-Identifier: Apache-2.0 OR MIT pragma solidity ^0.8.26; -import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; - -import "forge-std/Test.sol"; - -import "../src/rln/RLN.sol"; +import { Test } from "forge-std/Test.sol"; +import { ERC1967Proxy } from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; +import { RLN } from "../src/rln/RLN.sol"; import { IVerifier } from "../src/rln/IVerifier.sol"; +import { Karma } from "../src/Karma.sol"; +import { KarmaDistributorMock } from "./mocks/KarmaDistributorMock.sol"; +import { DeployKarmaScript } from "../script/DeployKarma.s.sol"; +import { DeployRLNScript } from "../script/RLN.s.sol"; -// A ERC20 token contract which allows arbitrary minting for testing -contract TestERC20 is ERC20 { - constructor() ERC20("TestERC20", "TST") { } +import { DeploymentConfig } from "../script/DeploymentConfig.s.sol"; - function mint(address to, uint256 amount) external { - _mint(to, amount); - } -} - -// A mock verifier which makes us skip the proof verification. +/// @dev A mock verifier that allows toggling proof validity. contract MockVerifier is IVerifier { bool public result; @@ -33,6 +28,7 @@ contract MockVerifier is IVerifier { ) external view + override returns (bool) { return result; @@ -44,352 +40,273 @@ contract MockVerifier is IVerifier { } contract RLNTest is Test { - event MemberRegistered(uint256 identityCommitment, uint256 messageLimit, uint256 index); - event MemberWithdrawn(uint256 index); - event MemberSlashed(uint256 index, address slasher); + RLN public rln; + MockVerifier public verifier; - RLN rln; - TestERC20 token; - MockVerifier verifier; + uint256 private constant DEPTH = 2; // for most tests + uint256 private constant SMALL_DEPTH = 1; // for “full” test - 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; + // Sample identity commitments + uint256 private identityCommitment0 = 1234; + uint256 private identityCommitment1 = 5678; + uint256 private identityCommitment2 = 9999; - 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 = + // Sample SNARK proof (8‐element array) + uint256[8] private mockProof = [uint256(0), uint256(1), uint256(2), uint256(3), uint256(4), uint256(5), uint256(6), uint256(7)]; + // Role‐holders + address private owner; + Karma private karma; + KarmaDistributorMock public distributor1; + KarmaDistributorMock public distributor2; + + address private adminAddr; + address private registerAddr; + address private slasherAddr; + + address private user1Addr = makeAddr("user1"); + address private user2Addr = makeAddr("user2"); + address private user3Addr = makeAddr("user3"); + function setUp() public { - token = new TestERC20(); + DeployKarmaScript karmaDeployment = new DeployKarmaScript(); + (Karma _karma, DeploymentConfig deploymentConfig) = karmaDeployment.run(); + karma = _karma; + (address deployer,) = deploymentConfig.activeNetworkConfig(); + owner = deployer; + distributor1 = new KarmaDistributorMock(); + distributor2 = new KarmaDistributorMock(); + + // Assign deterministic addresses + adminAddr = makeAddr("admin"); + registerAddr = makeAddr("register"); + slasherAddr = makeAddr("slasher"); + + // Deploy mock verifier verifier = new MockVerifier(); - rln = new RLN( - minimalDeposit, - maximalRate, - depth, - feePercentage, - feeReceiver, - freezePeriod, - address(token), - address(verifier) - ); + + // Deploy RLN via UUPS proxy with DEPTH = 2 + rln = _deployRLN(DEPTH, address(verifier), karma); + + // Sanity‐check that roles were assigned correctly + assertTrue(rln.hasRole(rln.DEFAULT_ADMIN_ROLE(), adminAddr)); + assertTrue(rln.hasRole(rln.REGISTER_ROLE(), registerAddr)); + assertTrue(rln.hasRole(rln.SLASHER_ROLE(), slasherAddr)); + + vm.startBroadcast(owner); + karma.addRewardDistributor(address(distributor1)); + karma.addRewardDistributor(address(distributor2)); + karma.grantRole(karma.SLASHER_ROLE(), address(rln)); + vm.stopBroadcast(); } + /// @dev Deploys a new RLN instance (behind ERC1967Proxy). + function _deployRLN(uint256 depth, address verifierAddr, Karma karmaToken) internal returns (RLN) { + bytes memory initData = abi.encodeCall( + RLN.initialize, + ( + adminAddr, + slasherAddr, + registerAddr, + depth, + verifierAddr, + address(karmaToken) // token address unused in these tests + ) + ); + address impl = address(new RLN()); + address proxy = address(new ERC1967Proxy(impl, initData)); + return RLN(proxy); + } + + /* ---------- INITIAL STATE ---------- */ + 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)); + // SET_SIZE should be 2^DEPTH = 4 + assertEq(rln.SET_SIZE(), uint256(1) << DEPTH); + + // No identities registered yet assertEq(rln.identityCommitmentIndex(), 0); + + // members(...) should return (address(0), 0) for any commitment + (address user0, uint256 idx0) = _memberData(identityCommitment0); + assertEq(user0, address(0)); + assertEq(idx0, 0); + + // Verifier address matches + assertEq(address(rln.verifier()), address(verifier)); } - /* register */ + /* ---------- REGISTER ---------- */ function test_register_succeeds() public { - // Test: register one user - register(user0, identityCommitment0, messageLimit0); - // Test: register second user - register(user1, identityCommitment1, messageLimit1); + // Register first identity + uint256 indexBefore = rln.identityCommitmentIndex(); + vm.startPrank(registerAddr); + vm.expectEmit(true, false, false, true); + emit RLN.MemberRegistered(identityCommitment0, indexBefore); + rln.register(identityCommitment0, user1Addr); + vm.stopPrank(); + + assertEq(rln.identityCommitmentIndex(), indexBefore + 1); + (address u0, uint256 i0) = _memberData(identityCommitment0); + assertEq(u0, user1Addr); + assertEq(i0, indexBefore); + + // Register second identity + indexBefore = rln.identityCommitmentIndex(); + vm.startPrank(registerAddr); + vm.expectEmit(true, false, false, true); + emit RLN.MemberRegistered(identityCommitment1, indexBefore); + rln.register(identityCommitment1, user2Addr); + vm.stopPrank(); + + assertEq(rln.identityCommitmentIndex(), indexBefore + 1); + (address u1, uint256 i1) = _memberData(identityCommitment1); + assertEq(u1, user2Addr); + assertEq(i1, indexBefore); } 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) - ); + // Deploy a small RLN with depth = 1 => SET_SIZE = 2 + RLN smallRLN = _deployRLN(SMALL_DEPTH, address(verifier), karma); + address smallRegister = registerAddr; - // Register the first user - _token.mint(user0, minimalDeposit); - vm.startPrank(user0); - _token.approve(address(smallRLN), minimalDeposit); - smallRLN.register(identityCommitment0, minimalDeposit); + // Fill up both slots + vm.startPrank(smallRegister); + smallRLN.register(identityCommitment0, user1Addr); + smallRLN.register(identityCommitment1, user2Addr); 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); + + // Now the set is full (2 members). Attempt a third registration. + vm.startPrank(smallRegister); + vm.expectRevert(RLN.RLN__SetIsFull.selector); + smallRLN.register(identityCommitment2, user3Addr); 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); + function test_register_fails_when_duplicate_identity_commitment() public { + // Register once + vm.startPrank(registerAddr); + rln.register(identityCommitment0, user1Addr); + vm.stopPrank(); + + // Attempt to register the same commitment again + vm.startPrank(registerAddr); + vm.expectRevert(RLN.RLN__IdCommitmentAlreadyRegistered.selector); + rln.register(identityCommitment0, user1Addr); 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); + /* ---------- EXIT ---------- */ + + function test_exit_succeeds() public { + // Register the identity + vm.startPrank(registerAddr); + rln.register(identityCommitment0, user1Addr); + vm.stopPrank(); + + // Ensure mock verifier returns true by default + assertTrue(verifier.result()); + + // Call exit with a valid proof + vm.startPrank(registerAddr); + vm.expectEmit(false, false, false, true); + emit RLN.MemberExited(0); + rln.exit(identityCommitment0, mockProof); + vm.stopPrank(); + + // After exit, the member record should be cleared + (address u0, uint256 i0) = _memberData(identityCommitment0); + assertEq(u0, address(0)); + assertEq(i0, 0); + } + + function test_exit_fails_when_not_registered() public { + // Attempt exit without prior registration + vm.startPrank(registerAddr); + vm.expectRevert(RLN.RLN__MemberNotFound.selector); + rln.exit(identityCommitment1, mockProof); vm.stopPrank(); } - /* withdraw */ + function test_exit_fails_when_invalid_proof() public { + // Register the identity + vm.startPrank(registerAddr); + rln.register(identityCommitment0, user1Addr); + vm.stopPrank(); - 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 + // Make proof invalid verifier.changeResult(false); - assertEq(verifier.result(), false); - vm.expectRevert("RLN, withdraw: invalid proof"); - rln.withdraw(identityCommitment0, mockProof); + assertFalse(verifier.result()); + + // Attempt exit with invalid proof + vm.startPrank(registerAddr); + vm.expectRevert(RLN.RLN__InvalidProof.selector); + rln.exit(identityCommitment0, mockProof); + vm.stopPrank(); } - /* 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 */ + /* ---------- 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); + uint256 distributorBalance = 50 ether; + vm.startPrank(owner); + karma.mint(user2Addr, 10 ether); // Mint Karma tokens to user2 + distributor1.setUserKarmaShare(user2Addr, distributorBalance); + vm.stopPrank(); - // 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); - } + // Register the identity first + vm.startPrank(registerAddr); + rln.register(identityCommitment1, user2Addr); + vm.stopPrank(); - 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); + // Retrieve the assigned index + (, uint256 index1) = _memberData(identityCommitment1); + + // Slash with a valid proof + vm.startPrank(slasherAddr); + vm.expectEmit(false, true, false, true); + emit RLN.MemberSlashed(index1, slasherAddr); + rln.slash(identityCommitment1, mockProof); + vm.stopPrank(); + + // After slash, the member record should be cleared + (address u1, uint256 i1) = _memberData(identityCommitment1); + assertEq(u1, address(0)); + assertEq(i1, 0); } 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); + // Attempt to slash a non‐existent identity + vm.startPrank(slasherAddr); + vm.expectRevert(RLN.RLN__MemberNotFound.selector); + rln.slash(identityCommitment0, mockProof); + vm.stopPrank(); } 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); + // Register the identity + vm.startPrank(registerAddr); + rln.register(identityCommitment0, user1Addr); 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); + // Make proof invalid + verifier.changeResult(false); + assertFalse(verifier.result()); + + // Attempt to slash with invalid proof + vm.startPrank(slasherAddr); + vm.expectRevert(RLN.RLN__InvalidProof.selector); + rln.slash(identityCommitment0, mockProof); + vm.stopPrank(); } - function getUnfrozenBlockHeight() public view returns (uint256) { - return block.number + freezePeriod + 1; - } + /* ========== HELPERS ========== */ - 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; + /// @dev Returns (userAddress, index) for a given identityCommitment. + function _memberData(uint256 commitment) internal view returns (address userAddress, uint256 index) { + (userAddress, index) = rln.members(commitment); + return (userAddress, index); } }