feat(RLN): implement KarmaRLN

Closes #217, #218, #219
This commit is contained in:
Ricardo Guilherme Schmidt
2025-06-04 23:13:11 -03:00
committed by r4bbit
parent 382b1c1643
commit d74854eb6a
5 changed files with 379 additions and 528 deletions

View File

@@ -4,13 +4,13 @@
+=================================================================================================================================================+ +=================================================================================================================================================+
| Deployment Cost | Deployment Size | | | | | | Deployment Cost | Deployment Size | | | | |
|-------------------------------------------------------------------------------------------+-----------------+-------+--------+--------+---------| |-------------------------------------------------------------------------------------------+-----------------+-------+--------+--------+---------|
| 0 | 1374 | | | | | | 295493 | 1374 | | | | |
|-------------------------------------------------------------------------------------------+-----------------+-------+--------+--------+---------| |-------------------------------------------------------------------------------------------+-----------------+-------+--------+--------+---------|
| | | | | | | | | | | | | |
|-------------------------------------------------------------------------------------------+-----------------+-------+--------+--------+---------| |-------------------------------------------------------------------------------------------+-----------------+-------+--------+--------+---------|
| Function Name | Min | Avg | Median | Max | # Calls | | 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 | | 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 | | 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 | | 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 | | accountSlashAmount | 2611 | 2611 | 2611 | 2611 | 2 |
|------------------------------+-----------------+--------+--------+--------+---------| |------------------------------+-----------------+--------+--------+--------+---------|
| addRewardDistributor | 29975 | 63645 | 70903 | 70903 | 284 | | addRewardDistributor | 29975 | 63560 | 70903 | 70903 | 304 |
|------------------------------+-----------------+--------+--------+--------+---------| |------------------------------+-----------------+--------+--------+--------+---------|
| allowance | 573 | 573 | 573 | 573 | 8 | | allowance | 573 | 573 | 573 | 573 | 8 |
|------------------------------+-----------------+--------+--------+--------+---------| |------------------------------+-----------------+--------+--------+--------+---------|
@@ -130,13 +130,13 @@
|------------------------------+-----------------+--------+--------+--------+---------| |------------------------------+-----------------+--------+--------+--------+---------|
| getRewardDistributors | 5132 | 7710 | 9644 | 9644 | 21 | | getRewardDistributors | 5132 | 7710 | 9644 | 9644 | 21 |
|------------------------------+-----------------+--------+--------+--------+---------| |------------------------------+-----------------+--------+--------+--------+---------|
| grantRole | 29490 | 29490 | 29490 | 29490 | 29 | | grantRole | 29490 | 29490 | 29490 | 29490 | 39 |
|------------------------------+-----------------+--------+--------+--------+---------| |------------------------------+-----------------+--------+--------+--------+---------|
| hasRole | 2754 | 2754 | 2754 | 2754 | 4 | | 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 | | removeRewardDistributor | 5080 | 22644 | 29995 | 30358 | 28 |
|------------------------------+-----------------+--------+--------+--------+---------| |------------------------------+-----------------+--------+--------+--------+---------|
@@ -144,7 +144,7 @@
|------------------------------+-----------------+--------+--------+--------+---------| |------------------------------+-----------------+--------+--------+--------+---------|
| setReward | 4845 | 144102 | 166754 | 166754 | 319 | | setReward | 4845 | 144102 | 166754 | 166754 | 319 |
|------------------------------+-----------------+--------+--------+--------+---------| |------------------------------+-----------------+--------+--------+--------+---------|
| slash | 4803 | 103614 | 85757 | 123125 | 519 | | slash | 4803 | 103655 | 85757 | 123125 | 520 |
|------------------------------+-----------------+--------+--------+--------+---------| |------------------------------+-----------------+--------+--------+--------+---------|
| slashedAmountOf | 17682 | 28099 | 28120 | 28120 | 516 | | slashedAmountOf | 17682 | 28099 | 28120 | 28120 | 516 |
|------------------------------+-----------------+--------+--------+--------+---------| |------------------------------+-----------------+--------+--------+--------+---------|
@@ -396,7 +396,7 @@
+===============================================================================================================================================+ +===============================================================================================================================================+
| Deployment Cost | Deployment Size | | | | | | Deployment Cost | Deployment Size | | | | |
|------------------------------------------------------------------------------------------+-----------------+-------+--------+-------+---------| |------------------------------------------------------------------------------------------+-----------------+-------+--------+-------+---------|
| 1204853 | 6207 | | | | | | 1204853 | 6015 | | | | |
|------------------------------------------------------------------------------------------+-----------------+-------+--------+-------+---------| |------------------------------------------------------------------------------------------+-----------------+-------+--------+-------+---------|
| | | | | | | | | | | | | |
|------------------------------------------------------------------------------------------+-----------------+-------+--------+-------+---------| |------------------------------------------------------------------------------------------+-----------------+-------+--------+-------+---------|
@@ -436,39 +436,35 @@
+=====================================================================================+ +=====================================================================================+
| Deployment Cost | Deployment Size | | | | | | Deployment Cost | Deployment Size | | | | |
|------------------------------+-----------------+--------+--------+--------+---------| |------------------------------+-----------------+--------+--------+--------+---------|
| 1396408 | 7094 | | | | | | 1982512 | 9143 | | | | |
|------------------------------+-----------------+--------+--------+--------+---------| |------------------------------+-----------------+--------+--------+--------+---------|
| | | | | | | | | | | | | |
|------------------------------+-----------------+--------+--------+--------+---------| |------------------------------+-----------------+--------+--------+--------+---------|
| Function Name | Min | Avg | Median | Max | # Calls | | 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 | | verifier | 2359 | 2359 | 2359 | 2359 | 1 |
|------------------------------+-----------------+--------+--------+--------+---------|
| withdraw | 29336 | 79495 | 106774 | 106774 | 8 |
|------------------------------+-----------------+--------+--------+--------+---------|
| withdrawals | 6792 | 6792 | 6792 | 6792 | 5 |
╰------------------------------+-----------------+--------+--------+--------+---------╯ ╰------------------------------+-----------------+--------+--------+--------+---------╯
╭--------------------------------------+-----------------+-------+--------+-------+---------╮ ╭--------------------------------------+-----------------+-------+--------+-------+---------╮
@@ -484,29 +480,11 @@
|--------------------------------------+-----------------+-------+--------+-------+---------| |--------------------------------------+-----------------+-------+--------+-------+---------|
| changeResult | 21649 | 21649 | 21649 | 21649 | 2 | | 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 | | | | | | | test/mocks/KarmaDistributorMock.sol:KarmaDistributorMock Contract | | | | | |
+========================================================================================================================+ +========================================================================================================================+
@@ -518,11 +496,11 @@
|-------------------------------------------------------------------+-----------------+-------+--------+-------+---------| |-------------------------------------------------------------------+-----------------+-------+--------+-------+---------|
| Function Name | Min | Avg | Median | Max | # Calls | | 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 | | setTotalKarmaShares | 43589 | 43589 | 43589 | 43589 | 48 |
|-------------------------------------------------------------------+-----------------+-------+--------+-------+---------| |-------------------------------------------------------------------+-----------------+-------+--------+-------+---------|
| setUserKarmaShare | 24210 | 44068 | 44134 | 44266 | 530 | | setUserKarmaShare | 24210 | 44107 | 44134 | 44266 | 531 |
|-------------------------------------------------------------------+-----------------+-------+--------+-------+---------| |-------------------------------------------------------------------+-----------------+-------+--------+-------+---------|
| totalRewardsSupply | 2324 | 2324 | 2324 | 2324 | 48 | | totalRewardsSupply | 2324 | 2324 | 2324 | 2324 | 48 |
╰-------------------------------------------------------------------+-----------------+-------+--------+-------+---------╯ ╰-------------------------------------------------------------------+-----------------+-------+--------+-------+---------╯
@@ -546,7 +524,7 @@
+==================================================================================================+ +==================================================================================================+
| Deployment Cost | Deployment Size | | | | | | Deployment Cost | Deployment Size | | | | |
|---------------------------------------------+-----------------+-------+--------+-------+---------| |---------------------------------------------+-----------------+-------+--------+-------+---------|
| 770657 | 3987 | | | | | | 770741 | 3987 | | | | |
|---------------------------------------------+-----------------+-------+--------+-------+---------| |---------------------------------------------+-----------------+-------+--------+-------+---------|
| | | | | | | | | | | | | |
|---------------------------------------------+-----------------+-------+--------+-------+---------| |---------------------------------------------+-----------------+-------+--------+-------+---------|

View File

@@ -112,23 +112,16 @@ OverflowTest:testTotalSupply() (gas: 359391)
OverflowTest:testTransfersNotAllowed() (gas: 61925) OverflowTest:testTransfersNotAllowed() (gas: 61925)
OverflowTest:test_RevertWhen_MintingCausesOverflow() (gas: 129592) OverflowTest:test_RevertWhen_MintingCausesOverflow() (gas: 129592)
OverflowTest:test_RevertWhen_SettingRewardCausesOverflow() (gas: 127920) OverflowTest:test_RevertWhen_SettingRewardCausesOverflow() (gas: 127920)
RLNTest:test_initial_state() (gas: 65400) RLNTest:test_exit_fails_when_invalid_proof() (gas: 195432)
RLNTest:test_register_fails_when_amount_lt_minimal_deposit() (gas: 161453) RLNTest:test_exit_fails_when_not_registered() (gas: 64843)
RLNTest:test_register_fails_when_duplicate_identity_commitments() (gas: 444949) RLNTest:test_exit_succeeds() (gas: 187485)
RLNTest:test_register_fails_when_index_exceeds_set_size() (gas: 2824923) RLNTest:test_initial_state() (gas: 60571)
RLNTest:test_register_succeeds() (gas: 579610) RLNTest:test_register_fails_when_duplicate_identity_commitment() (gas: 131752)
RLNTest:test_release_fails_when_freeze_period() (gas: 529887) RLNTest:test_register_fails_when_index_exceeds_set_size() (gas: 2569410)
RLNTest:test_release_fails_when_no_withdrawal() (gas: 38343) RLNTest:test_register_succeeds() (gas: 272680)
RLNTest:test_release_succeeds() (gas: 576511) RLNTest:test_slash_fails_when_invalid_proof() (gas: 195506)
RLNTest:test_slash_fails_when_invalid_proof() (gas: 399809) RLNTest:test_slash_fails_when_not_registered() (gas: 64886)
RLNTest:test_slash_fails_when_not_registered() (gas: 59667) RLNTest:test_slash_succeeds() (gas: 438591)
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: 438248) RemoveRewardDistributorTest:testAddKarmaDistributorOnlyAdmin() (gas: 438248)
RemoveRewardDistributorTest:testBalanceOf() (gas: 456715) RemoveRewardDistributorTest:testBalanceOf() (gas: 456715)
RemoveRewardDistributorTest:testBalanceOfWithNoSystemTotalKarma() (gas: 83783) RemoveRewardDistributorTest:testBalanceOfWithNoSystemTotalKarma() (gas: 83783)
@@ -156,7 +149,7 @@ SetRewardTest:test_RevertWhen_SenderIsNotOperator() (gas: 61893)
SlashAmountOfTest:testAddKarmaDistributorOnlyAdmin() (gas: 438224) SlashAmountOfTest:testAddKarmaDistributorOnlyAdmin() (gas: 438224)
SlashAmountOfTest:testBalanceOf() (gas: 456642) SlashAmountOfTest:testBalanceOf() (gas: 456642)
SlashAmountOfTest:testBalanceOfWithNoSystemTotalKarma() (gas: 83783) 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:testMintOnlyAdmin() (gas: 429075)
SlashAmountOfTest:testRemoveKarmaDistributorOnlyOwner() (gas: 163437) SlashAmountOfTest:testRemoveKarmaDistributorOnlyOwner() (gas: 163437)
SlashAmountOfTest:testRemoveUnknownKarmaDistributor() (gas: 41654) SlashAmountOfTest:testRemoveUnknownKarmaDistributor() (gas: 41654)

View File

@@ -1,27 +1,33 @@
// SPDX-License-Identifier: Apache-2.0 OR MIT // SPDX-License-Identifier: MIT
pragma solidity ^0.8.26; pragma solidity ^0.8.26;
import "forge-std/Script.sol"; import { BaseScript } from "./Base.s.sol";
import "../src/rln/RLN.sol"; import { DeploymentConfig } from "./DeploymentConfig.s.sol";
import "../src/rln/Verifier.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"); uint256 depth = vm.envUint("DEPTH");
uint8 feePercentage = uint8(vm.envUint("FEE_PERCENTAGE")); address karmaAddress = vm.envAddress("KARMA_ADDRESS");
address feeReceiver = vm.envAddress("FEE_RECEIVER");
uint256 freezePeriod = vm.envUint("FREEZE_PERIOD");
address token = vm.envAddress("ERC20TOKEN");
vm.startBroadcast(); vm.startBroadcast(deployer);
address verifier = (address)(new Groth16Verifier());
Groth16Verifier verifier = new Groth16Verifier(); // Deploy Karma logic contract
RLN rln = new RLN( bytes memory initializeData =
minimalDeposit, maximalRate, depth, feePercentage, feeReceiver, freezePeriod, token, address(verifier) 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(); vm.stopBroadcast();
return (RLN(proxy), deploymentConfig);
} }
} }

View File

@@ -1,211 +1,168 @@
// SPDX-License-Identifier: Apache-2.0 OR MIT // 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 { Karma } from "../Karma.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.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"; import { IVerifier } from "./IVerifier.sol";
/// @title Rate-Limiting Nullifier registry contract /// @title Rate-Limiting Nullifier registry contract
/// @dev This contract allows you to register RLN commitment and withdraw/slash. /// @dev This contract allows you to register RLN commitment and withdraw/slash.
contract RLN { contract RLN is Initializable, UUPSUpgradeable, AccessControlUpgradeable {
using SafeERC20 for IERC20; 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. /// @dev User metadata struct.
/// @param userAddress: address of depositor; /// @param userAddress: address of depositor;
/// @param messageLimit: user's message limit (stakeAmount / MINIMAL_DEPOSIT).
struct User { struct User {
address userAddress; address userAddress;
uint256 messageLimit;
uint256 index; 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). /// @dev Registry set size (1 << DEPTH).
uint256 public immutable SET_SIZE; uint256 public 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. /// @dev Current index where identityCommitment will be stored.
uint256 public identityCommitmentIndex; uint256 public identityCommitmentIndex;
/// @dev Registry set. The keys are `identityCommitment`s. /// @dev Registry set. The keys are `identityCommitment`s.
/// The values are addresses of accounts that call `register` transaction. /// 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. /// @dev Karma Token used for registering.
mapping(uint256 => Withdrawal) public withdrawals; Karma public karma;
/// @dev ERC20 Token used for staking.
IERC20 public immutable token;
/// @dev Groth16 verifier. /// @dev Groth16 verifier.
IVerifier public immutable verifier; IVerifier public verifier;
/// @dev Emmited when a new member registered. /// @dev Emmited when a new member registered.
/// @param identityCommitment: `identityCommitment`; /// @param identityCommitment: `identityCommitment`;
/// @param messageLimit: user's message limit;
/// @param index: idCommitmentIndex value. /// @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. /// @dev Emmited when a member was withdrawn.
/// @param index: index of `identityCommitment`; /// @param index: index of `identityCommitment`;
event MemberWithdrawn(uint256 index); event MemberExited(uint256 index);
/// @dev Emmited when a member was slashed. /// @dev Emmited when a member was slashed.
/// @param index: index of `identityCommitment`; /// @param index: index of `identityCommitment`;
/// @param slasher: address of slasher (msg.sender). /// @param slasher: address of slasher (msg.sender).
event MemberSlashed(uint256 index, address slasher); event MemberSlashed(uint256 index, address slasher);
/// @param minimalDeposit: minimal membership deposit; constructor() {
/// @param maximalRate: maximal rate; _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 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 _token: address of the ERC20 contract;
/// @param _verifier: address of the Groth16 Verifier. /// @param _verifier: address of the Groth16 Verifier.
constructor( function initialize(
uint256 minimalDeposit, address _owner,
uint256 maximalRate, address _slasher,
address _register,
uint256 depth, uint256 depth,
uint8 feePercentage, address _verifier,
address feeReceiver, address _token
uint256 freezePeriod, )
address _token, public
address _verifier initializer
) { {
require(feeReceiver != address(0), "RLN, constructor: fee receiver cannot be 0x0 address"); __UUPSUpgradeable_init();
__AccessControl_init();
MINIMAL_DEPOSIT = minimalDeposit; _setupRole(DEFAULT_ADMIN_ROLE, _owner);
MAXIMAL_RATE = maximalRate; _setupRole(SLASHER_ROLE, _slasher);
_setupRole(REGISTER_ROLE, _register);
SET_SIZE = 1 << depth; SET_SIZE = 1 << depth;
FEE_PERCENTAGE = feePercentage; karma = Karma(_token);
FEE_RECEIVER = feeReceiver;
FREEZE_PERIOD = freezePeriod;
token = IERC20(_token);
verifier = IVerifier(_verifier); 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. /// @dev Adds `identityCommitment` to the registry set and takes the necessary stake amount.
/// ///
/// NOTE: The set must not be full. /// NOTE: The set must not be full.
/// ///
/// @param identityCommitment: `identityCommitment`; /// @param identityCommitment: `identityCommitment`;
/// @param amount: stake amount. function register(uint256 identityCommitment, address user) external onlyRole(REGISTER_ROLE) {
function register(uint256 identityCommitment, uint256 amount) external {
uint256 index = identityCommitmentIndex; 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"); members[identityCommitment] = User(user, index);
require(amount >= MINIMAL_DEPOSIT, "RLN, register: amount is lower than minimal deposit"); emit MemberRegistered(identityCommitment, index);
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 { unchecked {
identityCommitmentIndex = index + 1; identityCommitmentIndex = index + 1;
} }
} }
/// @dev Request for withdraw and freeze the stake to prevent self-slashing. Stake can be /// @dev Request for exit.
/// released after FREEZE_PERIOD blocks.
/// @param identityCommitment: `identityCommitment`; /// @param identityCommitment: `identityCommitment`;
/// @param proof: snarkjs's format generated proof (without public inputs) packed consequently. /// @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]; User memory member = members[identityCommitment];
require(member.userAddress != address(0), "RLN, withdraw: member doesn't exist"); if (member.userAddress == address(0)) {
require(withdrawals[identityCommitment].blockNumber == 0, "RLN, release: such withdrawal exists"); revert RLN__MemberNotFound();
require(_verifyProof(identityCommitment, member.userAddress, proof), "RLN, withdraw: invalid proof"); }
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]; delete members[identityCommitment];
emit MemberExited(member.index);
token.safeTransfer(withdrawal.receiver, withdrawal.amount);
} }
/// @dev Slashes identity with identityCommitment. /// @dev Slashes identity with identityCommitment.
/// @param identityCommitment: `identityCommitment`; /// @param identityCommitment: `identityCommitment`;
/// @param receiver: stake receiver;
/// @param proof: snarkjs's format generated proof (without public inputs) packed consequently. /// @param proof: snarkjs's format generated proof (without public inputs) packed consequently.
function slash(uint256 identityCommitment, address receiver, uint256[8] calldata proof) external { function slash(uint256 identityCommitment, uint256[8] calldata proof) external onlyRole(SLASHER_ROLE) {
require(receiver != address(0), "RLN, slash: empty receiver address");
User memory member = members[identityCommitment]; User memory member = members[identityCommitment];
require(member.userAddress != address(0), "RLN, slash: member doesn't exist"); if (member.userAddress == address(0)) {
require(member.userAddress != receiver, "RLN, slash: self-slashing is prohibited"); revert RLN__MemberNotFound();
}
require(_verifyProof(identityCommitment, receiver, proof), "RLN, slash: invalid proof"); if (!_verifyProof(identityCommitment, proof)) {
revert RLN__InvalidProof();
}
karma.slash(member.userAddress);
delete members[identityCommitment]; delete members[identityCommitment];
delete withdrawals[identityCommitment];
uint256 withdrawAmount = member.messageLimit * MINIMAL_DEPOSIT; emit MemberSlashed(member.index, msg.sender);
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 /// @dev Groth16 proof verification
function _verifyProof( function _verifyProof(uint256 identityCommitment, uint256[8] calldata proof) internal view returns (bool) {
uint256 identityCommitment,
address receiver,
uint256[8] calldata proof
)
internal
view
returns (bool)
{
return verifier.verifyProof( return verifier.verifyProof(
[proof[0], proof[1]], [proof[0], proof[1]],
[[proof[2], proof[3]], [proof[4], proof[5]]], [[proof[2], proof[3]], [proof[4], proof[5]]],
[proof[6], proof[7]], [proof[6], proof[7]],
[identityCommitment, uint256(uint160(receiver))] [identityCommitment, uint256(0)]
); );
} }
} }

View File

@@ -1,23 +1,18 @@
// SPDX-License-Identifier: Apache-2.0 OR MIT // SPDX-License-Identifier: Apache-2.0 OR MIT
pragma solidity ^0.8.26; pragma solidity ^0.8.26;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import { Test } from "forge-std/Test.sol";
import { ERC1967Proxy } from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";
import "forge-std/Test.sol"; import { RLN } from "../src/rln/RLN.sol";
import "../src/rln/RLN.sol";
import { IVerifier } from "../src/rln/IVerifier.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 import { DeploymentConfig } from "../script/DeploymentConfig.s.sol";
contract TestERC20 is ERC20 {
constructor() ERC20("TestERC20", "TST") { }
function mint(address to, uint256 amount) external { /// @dev A mock verifier that allows toggling proof validity.
_mint(to, amount);
}
}
// A mock verifier which makes us skip the proof verification.
contract MockVerifier is IVerifier { contract MockVerifier is IVerifier {
bool public result; bool public result;
@@ -33,6 +28,7 @@ contract MockVerifier is IVerifier {
) )
external external
view view
override
returns (bool) returns (bool)
{ {
return result; return result;
@@ -44,352 +40,273 @@ contract MockVerifier is IVerifier {
} }
contract RLNTest is Test { contract RLNTest is Test {
event MemberRegistered(uint256 identityCommitment, uint256 messageLimit, uint256 index); RLN public rln;
event MemberWithdrawn(uint256 index); MockVerifier public verifier;
event MemberSlashed(uint256 index, address slasher);
RLN rln; uint256 private constant DEPTH = 2; // for most tests
TestERC20 token; uint256 private constant SMALL_DEPTH = 1; // for “full” test
MockVerifier verifier;
uint256 rlnInitialTokenBalance = 1_000_000; // Sample identity commitments
uint256 minimalDeposit = 100; uint256 private identityCommitment0 = 1234;
uint256 maximalRate = 1 << 16 - 1; uint256 private identityCommitment1 = 5678;
uint256 depth = 20; uint256 private identityCommitment2 = 9999;
uint8 feePercentage = 10;
address feeReceiver = makeAddr("feeReceiver");
uint256 freezePeriod = 1;
uint256 identityCommitment0 = 1234; // Sample SNARK proof (8element array)
uint256 identityCommitment1 = 5678; uint256[8] private mockProof =
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)]; [uint256(0), uint256(1), uint256(2), uint256(3), uint256(4), uint256(5), uint256(6), uint256(7)];
// Roleholders
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 { 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(); verifier = new MockVerifier();
rln = new RLN(
minimalDeposit, // Deploy RLN via UUPS proxy with DEPTH = 2
maximalRate, rln = _deployRLN(DEPTH, address(verifier), karma);
depth,
feePercentage, // Sanitycheck that roles were assigned correctly
feeReceiver, assertTrue(rln.hasRole(rln.DEFAULT_ADMIN_ROLE(), adminAddr));
freezePeriod, assertTrue(rln.hasRole(rln.REGISTER_ROLE(), registerAddr));
address(token), assertTrue(rln.hasRole(rln.SLASHER_ROLE(), slasherAddr));
address(verifier)
); 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 { function test_initial_state() public {
assertEq(rln.MINIMAL_DEPOSIT(), minimalDeposit); // SET_SIZE should be 2^DEPTH = 4
assertEq(rln.SET_SIZE(), 1 << depth); assertEq(rln.SET_SIZE(), uint256(1) << DEPTH);
assertEq(rln.FEE_PERCENTAGE(), feePercentage);
assertEq(rln.FEE_RECEIVER(), feeReceiver); // No identities registered yet
assertEq(rln.FREEZE_PERIOD(), freezePeriod);
assertEq(address(rln.token()), address(token));
assertEq(address(rln.verifier()), address(verifier));
assertEq(rln.identityCommitmentIndex(), 0); 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 { function test_register_succeeds() public {
// Test: register one user // Register first identity
register(user0, identityCommitment0, messageLimit0); uint256 indexBefore = rln.identityCommitmentIndex();
// Test: register second user vm.startPrank(registerAddr);
register(user1, identityCommitment1, messageLimit1); 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 { function test_register_fails_when_index_exceeds_set_size() public {
// Set size is (1 << smallDepth) = 2, and thus there can // Deploy a small RLN with depth = 1 => SET_SIZE = 2
// only be 2 members, otherwise reverts. RLN smallRLN = _deployRLN(SMALL_DEPTH, address(verifier), karma);
uint256 smallDepth = 1; address smallRegister = registerAddr;
TestERC20 _token = new TestERC20();
RLN smallRLN = new RLN(
minimalDeposit, maximalRate, smallDepth, feePercentage, feeReceiver, 0, address(_token), address(verifier)
);
// Register the first user // Fill up both slots
_token.mint(user0, minimalDeposit); vm.startPrank(smallRegister);
vm.startPrank(user0); smallRLN.register(identityCommitment0, user1Addr);
_token.approve(address(smallRLN), minimalDeposit); smallRLN.register(identityCommitment1, user2Addr);
smallRLN.register(identityCommitment0, minimalDeposit);
vm.stopPrank(); vm.stopPrank();
// Register the second user
_token.mint(user1, minimalDeposit); // Now the set is full (2 members). Attempt a third registration.
vm.startPrank(user1); vm.startPrank(smallRegister);
_token.approve(address(smallRLN), minimalDeposit); vm.expectRevert(RLN.RLN__SetIsFull.selector);
smallRLN.register(identityCommitment1, minimalDeposit); smallRLN.register(identityCommitment2, user3Addr);
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(); vm.stopPrank();
} }
function test_register_fails_when_amount_lt_minimal_deposit() public { function test_register_fails_when_duplicate_identity_commitment() public {
uint256 insufficientAmount = minimalDeposit - 1; // Register once
token.mint(user0, rlnInitialTokenBalance); vm.startPrank(registerAddr);
vm.startPrank(user0); rln.register(identityCommitment0, user1Addr);
token.approve(address(rln), rlnInitialTokenBalance); vm.stopPrank();
vm.expectRevert("RLN, register: amount is lower than minimal deposit");
rln.register(identityCommitment0, insufficientAmount); // Attempt to register the same commitment again
vm.startPrank(registerAddr);
vm.expectRevert(RLN.RLN__IdCommitmentAlreadyRegistered.selector);
rln.register(identityCommitment0, user1Addr);
vm.stopPrank(); vm.stopPrank();
} }
function test_register_fails_when_duplicate_identity_commitments() public { /* ---------- EXIT ---------- */
// Register first with user0 with identityCommitment0
register(user0, identityCommitment0, messageLimit0); function test_exit_succeeds() public {
// Register again with user1 with identityCommitment0 // Register the identity
token.mint(user1, rlnInitialTokenBalance); vm.startPrank(registerAddr);
vm.startPrank(user1); rln.register(identityCommitment0, user1Addr);
token.approve(address(rln), rlnInitialTokenBalance); vm.stopPrank();
// `register` should revert
vm.expectRevert("RLN, register: idCommitment already registered"); // Ensure mock verifier returns true by default
rln.register(identityCommitment0, rlnInitialTokenBalance); 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(); 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 { // Make proof invalid
// 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); verifier.changeResult(false);
assertEq(verifier.result(), false); assertFalse(verifier.result());
vm.expectRevert("RLN, withdraw: invalid proof");
rln.withdraw(identityCommitment0, mockProof); // Attempt exit with invalid proof
vm.startPrank(registerAddr);
vm.expectRevert(RLN.RLN__InvalidProof.selector);
rln.exit(identityCommitment0, mockProof);
vm.stopPrank();
} }
/* release */ /* ---------- SLASH ---------- */
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 { function test_slash_succeeds() public {
// Test: register and get slashed uint256 distributorBalance = 50 ether;
register(user0, identityCommitment0, messageLimit0); vm.startPrank(owner);
uint256 registerAmount = getRegisterAmount(messageLimit0); karma.mint(user2Addr, 10 ether); // Mint Karma tokens to user2
uint256 slashFee = getSlashFee(registerAmount); distributor1.setUserKarmaShare(user2Addr, distributorBalance);
uint256 slashReward = registerAmount - slashFee; vm.stopPrank();
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 the identity first
register(user1, identityCommitment1, messageLimit1); vm.startPrank(registerAddr);
rln.withdraw(identityCommitment1, mockProof); rln.register(identityCommitment1, user2Addr);
rln.slash(identityCommitment1, slashedReceiver, mockProof); vm.stopPrank();
// Check the record of user1 has been deleted
checkUserIsDeleted(identityCommitment1);
}
function test_slash_fails_when_receiver_is_zero() public { // Retrieve the assigned index
// Register first (, uint256 index1) = _memberData(identityCommitment1);
register(user0, identityCommitment0, messageLimit0);
// Try slash user0 and it fails because of the zero address // Slash with a valid proof
vm.expectRevert("RLN, slash: empty receiver address"); vm.startPrank(slasherAddr);
rln.slash(identityCommitment0, address(0), mockProof); 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 { function test_slash_fails_when_not_registered() public {
// It fails if the user is not registered yet // Attempt to slash a nonexistent identity
vm.expectRevert("RLN, slash: member doesn't exist"); vm.startPrank(slasherAddr);
rln.slash(identityCommitment0, slashedReceiver, mockProof); vm.expectRevert(RLN.RLN__MemberNotFound.selector);
} rln.slash(identityCommitment0, mockProof);
vm.stopPrank();
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 { function test_slash_fails_when_invalid_proof() public {
// It fails if the proof is invalid // Register the identity
// Register first vm.startPrank(registerAddr);
register(user0, identityCommitment0, messageLimit0); rln.register(identityCommitment0, user1Addr);
// 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(); vm.stopPrank();
// Check states // Make proof invalid
uint256 tokenRLNDiff = token.balanceOf(address(rln)) - tokenRLNBefore; verifier.changeResult(false);
uint256 tokenUserDiff = tokenUserBefore - token.balanceOf(user); assertFalse(verifier.result());
// RLN state
assertEq(rln.identityCommitmentIndex(), identityCommitmentIndexBefore + 1); // Attempt to slash with invalid proof
assertEq(tokenRLNDiff, registerTokenAmount); vm.startPrank(slasherAddr);
// User state vm.expectRevert(RLN.RLN__InvalidProof.selector);
(address userAddress, uint256 actualMessageLimit, uint256 index) = rln.members(identityCommitment); rln.slash(identityCommitment0, mockProof);
assertEq(userAddress, user); vm.stopPrank();
assertEq(actualMessageLimit, messageLimit);
assertEq(index, identityCommitmentIndexBefore);
assertEq(tokenUserDiff, registerTokenAmount);
} }
function getUnfrozenBlockHeight() public view returns (uint256) { /* ========== HELPERS ========== */
return block.number + freezePeriod + 1;
}
function checkUserIsDeleted(uint256 identityCommitment) public { /// @dev Returns (userAddress, index) for a given identityCommitment.
// User state function _memberData(uint256 commitment) internal view returns (address userAddress, uint256 index) {
(address userAddress, uint256 actualMessageLimit, uint256 index) = rln.members(identityCommitment); (userAddress, index) = rln.members(commitment);
assertEq(userAddress, address(0)); return (userAddress, index);
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;
} }
} }