From 39c7b0b31ea4a525e8dd10808a4838526bbde3d3 Mon Sep 17 00:00:00 2001 From: rymnc <43716372+rymnc@users.noreply.github.com> Date: Wed, 29 Mar 2023 17:18:00 +0530 Subject: [PATCH] feat: coverage --- contracts/Rln.sol | 54 ++++------- test/RLN.t.sol | 222 +++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 238 insertions(+), 38 deletions(-) diff --git a/contracts/Rln.sol b/contracts/Rln.sol index 48b5362..22157e4 100644 --- a/contracts/Rln.sol +++ b/contracts/Rln.sol @@ -39,6 +39,7 @@ contract RLN { function registerBatch(uint256[] calldata idCommitments) external payable { uint256 idCommitmentlen = idCommitments.length; + require(idCommitmentlen > 0, "RLN, registerBatch: batch size zero"); require( idCommitmentIndex + idCommitmentlen <= SET_SIZE, "RLN, registerBatch: set is full" @@ -58,13 +59,10 @@ contract RLN { "RLN, _register: member already registered" ); require(idCommitmentIndex < SET_SIZE, "RLN, register: set is full"); - if (stake != 0) { - members[idCommitment] = true; - stakedAmounts[idCommitment] = stake; - } else { - members[idCommitment] = true; - stakedAmounts[idCommitment] = 0; - } + + members[idCommitment] = true; + stakedAmounts[idCommitment] = stake; + emit MemberRegistered(idCommitment, idCommitmentIndex); idCommitmentIndex += 1; } @@ -75,10 +73,6 @@ contract RLN { ) external { uint256 batchSize = secrets.length; require(batchSize != 0, "RLN, withdrawBatch: batch size zero"); - require( - batchSize == secrets.length, - "RLN, withdrawBatch: batch size mismatch secrets" - ); require( batchSize == receivers.length, "RLN, withdrawBatch: batch size mismatch receivers" @@ -92,14 +86,19 @@ contract RLN { _withdraw(secret, receiver); } - function withdraw(uint256 secret) external { - _withdraw(secret); - } - function _withdraw(uint256 secret, address payable receiver) internal { + require( + receiver != address(0), + "RLN, _withdraw: empty receiver address" + ); + + require( + receiver != address(this), + "RLN, _withdraw: cannot withdraw to RLN" + ); + // derive idCommitment uint256 idCommitment = hash(secret); - // check if member is registered require(members[idCommitment], "RLN, _withdraw: member not registered"); @@ -109,33 +108,14 @@ contract RLN { "RLN, _withdraw: member has no stake" ); - require( - receiver != address(0), - "RLN, _withdraw: empty receiver address" - ); + uint256 amountToTransfer = stakedAmounts[idCommitment]; // delete member members[idCommitment] = false; stakedAmounts[idCommitment] = 0; // refund deposit - (bool sent, ) = receiver.call{value: stakedAmounts[idCommitment]}(""); - require(sent, "transfer failed"); - - emit MemberWithdrawn(idCommitment); - } - - function _withdraw(uint256 secret) internal { - // derive idCommitment - uint256 idCommitment = hash(secret); - - // check if member is registered - require(members[idCommitment], "RLN, _withdraw: member not registered"); - - require(stakedAmounts[idCommitment] == 0, "RLN, _withdraw: staked"); - - // delete member - members[idCommitment] = false; + receiver.transfer(amountToTransfer); emit MemberWithdrawn(idCommitment); } diff --git a/test/RLN.t.sol b/test/RLN.t.sol index 9c7a260..fc28541 100644 --- a/test/RLN.t.sol +++ b/test/RLN.t.sol @@ -4,10 +4,36 @@ pragma solidity ^0.8.15; import "../contracts/PoseidonHasher.sol"; import "../contracts/Rln.sol"; import "forge-std/Test.sol"; +import "forge-std/StdCheats.sol"; import "forge-std/console.sol"; +contract ArrayUnique { + mapping(uint256 => bool) seen; + + constructor(uint256[] memory arr) { + for (uint256 i = 0; i < arr.length; i++) { + require(!seen[arr[i]], "ArrayUnique: duplicate value"); + seen[arr[i]] = true; + } + } +} + +function repeatElementIntoArray( + uint256 length, + address payable element +) pure returns (address payable[] memory) { + address payable[] memory arr = new address payable[](length); + for (uint256 i = 0; i < length; i++) { + arr[i] = element; + } + return arr; +} + contract RLNTest is Test { + using stdStorage for StdStorage; + RLN public rln; + PoseidonHasher public poseidon; uint256 public constant MEMBERSHIP_DEPOSIT = 1000000000000000; uint256 public constant DEPTH = 20; @@ -15,10 +41,18 @@ contract RLNTest is Test { /// @dev Setup the testing environment. function setUp() public { - PoseidonHasher poseidon = new PoseidonHasher(); + poseidon = new PoseidonHasher(); rln = new RLN(MEMBERSHIP_DEPOSIT, DEPTH, address(poseidon)); } + function isUniqueArray(uint256[] memory arr) internal returns (bool) { + try new ArrayUnique(arr) { + return true; + } catch { + return false; + } + } + /// @dev Ensure that you can hash a value. function test__Constants() public { assertEq(rln.MEMBERSHIP_DEPOSIT(), MEMBERSHIP_DEPOSIT); @@ -38,6 +72,7 @@ contract RLNTest is Test { rln.register{value: MEMBERSHIP_DEPOSIT}(idCommitment); assertEq(rln.stakedAmounts(idCommitment), MEMBERSHIP_DEPOSIT); assertEq(rln.members(idCommitment), true); + // TODO: use custom errors instead of revert strings vm.expectRevert(bytes("RLN, _register: member already registered")); rln.register{value: MEMBERSHIP_DEPOSIT}(idCommitment); } @@ -68,4 +103,189 @@ contract RLNTest is Test { vm.expectRevert(bytes("RLN, register: set is full")); tempRln.register{value: MEMBERSHIP_DEPOSIT}(idCommitmentSeed + setSize); } + + function test__ValidBatchRegistration( + uint256[] calldata idCommitments + ) public { + // assume that the array is unique, otherwise it triggers + // a revert that has already been tested + vm.assume(isUniqueArray(idCommitments) && idCommitments.length > 0); + uint256 idCommitmentlen = idCommitments.length; + rln.registerBatch{value: MEMBERSHIP_DEPOSIT * idCommitmentlen}( + idCommitments + ); + for (uint256 i = 0; i < idCommitmentlen; i++) { + assertEq(rln.stakedAmounts(idCommitments[i]), MEMBERSHIP_DEPOSIT); + assertEq(rln.members(idCommitments[i]), true); + } + } + + function test__InvalidBatchRegistration__FullSet( + uint256 idCommitmentSeed + ) public { + vm.assume(idCommitmentSeed < 2 ** 255 - SET_SIZE); + RLN tempRln = new RLN(MEMBERSHIP_DEPOSIT, 2, address(poseidon)); + uint256 setSize = tempRln.SET_SIZE(); + for (uint256 i = 0; i < setSize; i++) { + tempRln.register{value: MEMBERSHIP_DEPOSIT}(idCommitmentSeed + i); + } + assertEq(tempRln.idCommitmentIndex(), 4); + uint256[] memory idCommitments = new uint256[](1); + idCommitments[0] = idCommitmentSeed + setSize; + vm.expectRevert(bytes("RLN, registerBatch: set is full")); + tempRln.registerBatch{value: MEMBERSHIP_DEPOSIT}(idCommitments); + } + + function test__InvalidBatchRegistration__EmptyBatch() public { + uint256[] memory idCommitments = new uint256[](0); + vm.expectRevert(bytes("RLN, registerBatch: batch size zero")); + rln.registerBatch{value: MEMBERSHIP_DEPOSIT}(idCommitments); + } + + function test__InvalidBatchRegistration__InsufficientDeposit( + uint256[] calldata idCommitments + ) public { + vm.assume(isUniqueArray(idCommitments) && idCommitments.length > 0); + uint256 idCommitmentlen = idCommitments.length; + vm.expectRevert( + bytes("RLN, registerBatch: membership deposit is not satisfied") + ); + rln.registerBatch{value: MEMBERSHIP_DEPOSIT * idCommitmentlen - 1}( + idCommitments + ); + } + + function test__ValidWithdraw( + uint256 idSecretHash, + address payable to + ) public { + // avoid precompiles, etc + vm.assume(to != address(0)); + assumePayable(to); + uint256 idCommitment = poseidon.hash(idSecretHash); + + rln.register{value: MEMBERSHIP_DEPOSIT}(idCommitment); + assertEq(rln.stakedAmounts(idCommitment), MEMBERSHIP_DEPOSIT); + + uint256 balanceBefore = to.balance; + rln.withdraw(idSecretHash, to); + assertEq(rln.stakedAmounts(idCommitment), 0); + assertEq(rln.members(idCommitment), false); + assertEq(to.balance, balanceBefore + MEMBERSHIP_DEPOSIT); + } + + function test__InvalidWithdraw__ToZeroAddress() public { + uint256 idSecretHash = 19014214495641488759237505126948346942972912379615652741039992445865937985820; + uint256 idCommitment = poseidon.hash(idSecretHash); + rln.register{value: MEMBERSHIP_DEPOSIT}(idCommitment); + assertEq(rln.stakedAmounts(idCommitment), MEMBERSHIP_DEPOSIT); + vm.expectRevert(bytes("RLN, _withdraw: empty receiver address")); + rln.withdraw(idSecretHash, payable(address(0))); + } + + function test__InvalidWithdraw__ToRlnAddress() public { + uint256 idSecretHash = 19014214495641488759237505126948346942972912379615652741039992445865937985820; + uint256 idCommitment = poseidon.hash(idSecretHash); + rln.register{value: MEMBERSHIP_DEPOSIT}(idCommitment); + assertEq(rln.stakedAmounts(idCommitment), MEMBERSHIP_DEPOSIT); + vm.expectRevert(bytes("RLN, _withdraw: cannot withdraw to RLN")); + rln.withdraw(idSecretHash, payable(address(rln))); + } + + function test__InvalidWithdraw__InvalidIdCommitment( + uint256 idCommitment + ) public { + vm.expectRevert(bytes("RLN, _withdraw: member not registered")); + rln.withdraw(idCommitment, payable(address(this))); + } + + // this shouldn't be possible, but just in case + function test__InvalidWithdraw__NoStake( + uint256 idSecretHash, + address payable to + ) public { + // avoid precompiles, etc + vm.assume(to != address(0)); + assumePayable(to); + uint256 idCommitment = poseidon.hash(idSecretHash); + + rln.register{value: MEMBERSHIP_DEPOSIT}(idCommitment); + assertEq(rln.stakedAmounts(idCommitment), MEMBERSHIP_DEPOSIT); + + rln.withdraw(idSecretHash, to); + assertEq(rln.stakedAmounts(idCommitment), 0); + assertEq(rln.members(idCommitment), false); + + // manually set members[idCommitment] to true using vm + stdstore + .target(address(rln)) + .sig("members(uint256)") + .with_key(idCommitment) + .depth(0) + .checked_write(true); + + vm.expectRevert(bytes("RLN, _withdraw: member has no stake")); + rln.withdraw(idSecretHash, to); + } + + function test__ValidBatchWithdraw( + uint256[] calldata idSecretHashes, + address payable to + ) public { + // avoid precompiles, etc + vm.assume(to != address(0)); + vm.assume(isUniqueArray(idSecretHashes) && idSecretHashes.length > 0); + assumePayable(to); + uint256 idCommitmentlen = idSecretHashes.length; + uint256[] memory idCommitments = new uint256[](idCommitmentlen); + for (uint256 i = 0; i < idCommitmentlen; i++) { + idCommitments[i] = poseidon.hash(idSecretHashes[i]); + } + + rln.registerBatch{value: MEMBERSHIP_DEPOSIT * idCommitmentlen}( + idCommitments + ); + for (uint256 i = 0; i < idCommitmentlen; i++) { + assertEq(rln.stakedAmounts(idCommitments[i]), MEMBERSHIP_DEPOSIT); + } + + uint256 balanceBefore = to.balance; + rln.withdrawBatch( + idSecretHashes, + repeatElementIntoArray(idSecretHashes.length, to) + ); + for (uint256 i = 0; i < idCommitmentlen; i++) { + assertEq(rln.stakedAmounts(idCommitments[i]), 0); + assertEq(rln.members(idCommitments[i]), false); + } + assertEq( + to.balance, + balanceBefore + MEMBERSHIP_DEPOSIT * idCommitmentlen + ); + } + + function test__InvalidBatchWithdraw__EmptyBatch() public { + uint256[] memory idSecretHashes = new uint256[](0); + address payable[] memory to = new address payable[](0); + vm.expectRevert(bytes("RLN, withdrawBatch: batch size zero")); + rln.withdrawBatch(idSecretHashes, to); + } + + function test__InvalidBatchWithdraw__MismatchInputSize( + uint256[] calldata idSecretHashes, + address payable to + ) public { + vm.assume(isUniqueArray(idSecretHashes) && idSecretHashes.length > 0); + + vm.assume(to != address(0)); + assumePayable(to); + + vm.expectRevert( + bytes("RLN, withdrawBatch: batch size mismatch receivers") + ); + rln.withdrawBatch( + idSecretHashes, + repeatElementIntoArray(idSecretHashes.length + 1, to) + ); + } }