Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions src/IDAIPermit.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.24;

interface IDAIPermit {
function permit(
address holder,
address spender,
uint256 nonce,
uint256 expiry,
bool allowed,
uint8 v,
bytes32 r,
bytes32 s
)
external;
}
59 changes: 59 additions & 0 deletions src/Membership.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
pragma solidity 0.8.24;

import { IPriceCalculator } from "./IPriceCalculator.sol";
import { IDAIPermit } from "./IDAIPermit.sol";
import { IERC20 } from "openzeppelin-contracts/contracts/token/ERC20/IERC20.sol";
import { SafeERC20 } from "openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol";
import { ERC20Permit } from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol";
Expand Down Expand Up @@ -151,9 +152,9 @@
internal
onlyInitializing
{
require(_minMembershipRateLimit <= _maxMembershipRateLimit);

Check warning on line 155 in src/Membership.sol

View workflow job for this annotation

GitHub Actions / lint

Provide an error message for require
require(_maxMembershipRateLimit <= _maxTotalRateLimit);

Check warning on line 156 in src/Membership.sol

View workflow job for this annotation

GitHub Actions / lint

Provide an error message for require
require(_activeDurationForNewMemberships > 0);

Check warning on line 157 in src/Membership.sol

View workflow job for this annotation

GitHub Actions / lint

Provide an error message for require
// Note: grace period duration may be equal to zero

priceCalculator = IPriceCalculator(_priceCalculator);
Expand Down Expand Up @@ -266,6 +267,64 @@
IERC20(token).safeTransferFrom(_owner, address(this), depositAmount);
}

/// @dev acquire a membership and transfer the deposit to the contract
/// Uses DAI permit extension allowing approvals to be made via signatures
/// @param _owner The address of the token owner who is giving permission and will own the membership.
/// @param _deadline The timestamp until when the permit is valid.
/// @param _nonce The nonce used for the permission
/// @param _v The recovery byte of the signature.
/// @param _r Half of the ECDSA signature pair.
/// @param _s Half of the ECDSA signature pair.
/// @param _idCommitment the idCommitment of the new membership
/// @param _rateLimit the membership rate limit
/// @return index the index of the new membership in the membership set
/// @return indexReused true if the index was reused, false otherwise
function _acquireMembershipWithDAIPermit(
address _owner,
uint256 _deadline,
uint256 _nonce,
uint8 _v,
bytes32 _r,
bytes32 _s,
uint256 _idCommitment,
uint32 _rateLimit
)
internal
returns (uint32 index, bool indexReused)
{
// Check if the rate limit is valid
if (!isValidMembershipRateLimit(_rateLimit)) {
revert InvalidMembershipRateLimit();
}

currentTotalRateLimit += _rateLimit;

// Determine if we exceed the total rate limit
if (currentTotalRateLimit > maxTotalRateLimit) {
revert CannotExceedMaxTotalRateLimit();
}

(address token, uint256 depositAmount) = priceCalculator.calculate(_rateLimit);

IDAIPermit(token).permit(_owner, address(this), _nonce, _deadline, true, _v, _r, _s);

// Possibly reuse an index of an erased membership
(index, indexReused) = _getFreeIndex();

memberships[_idCommitment] = MembershipInfo({
holder: _owner,
activeDuration: activeDurationForNewMemberships,
gracePeriodStartTimestamp: block.timestamp + uint256(activeDurationForNewMemberships),
gracePeriodDuration: gracePeriodDurationForNewMemberships,
token: token,
depositAmount: depositAmount,
rateLimit: _rateLimit,
index: index
});

IERC20(token).safeTransferFrom(_owner, address(this), depositAmount);
}

/// @notice Checks if a rate limit is within the allowed bounds
/// @param rateLimit The rate limit
/// @return true if the rate limit is within the allowed bounds, false otherwise
Expand Down
65 changes: 57 additions & 8 deletions src/WakuRlnV2.sol
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,15 @@
/// Invalid pagination query
error InvalidPaginationQuery(uint256 startIndex, uint256 endIndex);

struct MembershipPermitParams {
/// @notice The idCommitment of the new membership
uint256 idCommitment;
/// @notice The rate limit of the new membership
uint32 rateLimit;
/// @notice The list of idCommitments of expired memberships to erase
uint256[] idCommitmentsToErase;
}

contract WakuRlnV2 is Initializable, OwnableUpgradeable, UUPSUpgradeable, MembershipUpgradeable {
/// @notice The Field
uint256 public constant Q =
Expand All @@ -29,7 +38,7 @@
uint8 public constant MERKLE_TREE_DEPTH = 20;

/// @notice The maximum membership set size is the size of the Merkle tree (2 ^ depth)
uint32 public MAX_MEMBERSHIP_SET_SIZE;

Check warning on line 41 in src/WakuRlnV2.sol

View workflow job for this annotation

GitHub Actions / lint

Variable name must be in mixedCase

/// @notice The block number at which this contract was deployed
uint32 public deployedBlockNumber;
Expand All @@ -47,7 +56,7 @@
/// @notice Сheck that the membership with this idCommitment is not already in the membership set
/// @param idCommitment The idCommitment of the membership
modifier noDuplicateMembership(uint256 idCommitment) {
require(!isInMembershipSet(idCommitment), "Duplicate idCommitment: membership already exists");

Check warning on line 59 in src/WakuRlnV2.sol

View workflow job for this annotation

GitHub Actions / lint

Error message for require is too long
_;
}

Expand Down Expand Up @@ -186,24 +195,25 @@
/// @param v The recovery byte of the signature.
/// @param r Half of the ECDSA signature pair.
/// @param s Half of the ECDSA signature pair.
/// @param idCommitment The idCommitment of the new membership
/// @param rateLimit The rate limit of the new membership
/// @param idCommitmentsToErase The list of idCommitments of expired memberships to erase
/// @param membership Parameters for the new membership
function registerWithPermit(
address owner,
uint256 deadline,
uint8 v,
bytes32 r,
bytes32 s,
uint256 idCommitment,
uint32 rateLimit,
uint256[] calldata idCommitmentsToErase
MembershipPermitParams calldata membership
)
external
onlyValidIdCommitment(idCommitment)
noDuplicateMembership(idCommitment)
membershipSetNotFull
{
uint256 idCommitment = membership.idCommitment;
uint32 rateLimit = membership.rateLimit;
uint256[] calldata idCommitmentsToErase = membership.idCommitmentsToErase;

if (!isValidIdCommitment(idCommitment)) revert InvalidIdCommitment(idCommitment);
require(!isInMembershipSet(idCommitment), "Duplicate idCommitment: membership already exists");

Check warning on line 215 in src/WakuRlnV2.sol

View workflow job for this annotation

GitHub Actions / lint

Error message for require is too long

// erase memberships without overwriting membership set data to zero (save gas)
_eraseMemberships(idCommitmentsToErase, false);

Expand All @@ -215,6 +225,45 @@
emit MembershipRegistered(idCommitment, rateLimit, index);
}

/// @notice Register a membership while erasing some expired memberships to reuse their rate limit.
/// Uses the DAI Permit
/// @param owner The address of the token owner who is giving permission and will own the membership.
/// @param deadline The timestamp until when the permit is valid.
/// @param nonce The nonce used for the permission
/// @param v The recovery byte of the signature.
/// @param r Half of the ECDSA signature pair.
/// @param s Half of the ECDSA signature pair.
/// @param membership Parameters for the new membership
function registerWithDAIPermit(
address owner,
uint256 deadline,
uint256 nonce,
uint8 v,
bytes32 r,
bytes32 s,
MembershipPermitParams calldata membership
)
external
membershipSetNotFull
{
uint256 idCommitment = membership.idCommitment;
uint32 rateLimit = membership.rateLimit;
uint256[] calldata idCommitmentsToErase = membership.idCommitmentsToErase;

if (!isValidIdCommitment(idCommitment)) revert InvalidIdCommitment(idCommitment);
require(!isInMembershipSet(idCommitment), "Duplicate idCommitment: membership already exists");

Check warning on line 254 in src/WakuRlnV2.sol

View workflow job for this annotation

GitHub Actions / lint

Error message for require is too long

// erase memberships without overwriting membership set data to zero (save gas)
_eraseMemberships(idCommitmentsToErase, false);

(uint32 index, bool indexReused) =
_acquireMembershipWithDAIPermit(owner, deadline, nonce, v, r, s, idCommitment, rateLimit);

_upsertInTree(idCommitment, rateLimit, index, indexReused);

emit MembershipRegistered(idCommitment, rateLimit, index);
}

/// @dev Register a membership (internal function)
/// @param idCommitment The idCommitment of the membership
/// @param rateLimit The rate limit of the membership
Expand Down
45 changes: 44 additions & 1 deletion test/TestToken.sol
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,54 @@ import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import { ERC20Permit } from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol";

contract TestToken is ERC20, ERC20Permit {
constructor() ERC20("TestToken", "TTT") ERC20Permit("TestToken") { }
bytes32 public DAI_DOMAIN_SEPARATOR;
bytes32 public constant PERMIT_TYPEHASH = 0xea2aa0a1be11a07ed86d755c93467f4f82362b452371d1ba94d1715123511acb;

constructor() ERC20("TestToken", "TTT") ERC20Permit("TestToken") {
DAI_DOMAIN_SEPARATOR = keccak256(
abi.encode(
keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"),
keccak256(bytes("TestToken")),
keccak256(bytes("1")),
block.chainid,
address(this)
)
);
}

function mint(address to, uint256 amount) public {
_mint(to, amount);
}

function permit(
address holder,
address spender,
uint256 nonce,
uint256 expiry,
bool allowed,
uint8 v,
bytes32 r,
bytes32 s
)
external
{
bytes32 digest = keccak256(
abi.encodePacked(
"\x19\x01",
DAI_DOMAIN_SEPARATOR,
keccak256(abi.encode(PERMIT_TYPEHASH, holder, spender, nonce, expiry, allowed))
)
);

require(holder != address(0));
require(holder == ecrecover(digest, v, r, s));
require(expiry == 0 || block.timestamp <= expiry);
require(nonce == _useNonce(holder));

uint256 value = allowed ? type(uint256).max : 0;

_approve(holder, spender, value);
}
}

contract TestTokenFactory is BaseScript {
Expand Down
62 changes: 55 additions & 7 deletions test/WakuRlnV2.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -124,9 +124,14 @@ contract WakuRlnV2Test is Test {

function test__ValidRegistrationWithPermit() external {
vm.pauseGasMetering();
uint256 idCommitment = 2;
uint32 membershipRateLimit = w.minMembershipRateLimit();
(, uint256 price) = w.priceCalculator().calculate(membershipRateLimit);

MembershipPermitParams memory membership = MembershipPermitParams({
idCommitment: 2,
rateLimit: w.minMembershipRateLimit(),
idCommitmentsToErase: noIdCommitmentsToErase
});

(, uint256 price) = w.priceCalculator().calculate(membership.rateLimit);

// Creating an owner for a membership (Alice)
uint256 alicePrivK = 0xA11CE;
Expand Down Expand Up @@ -154,12 +159,55 @@ contract WakuRlnV2Test is Test {
vm.resumeGasMetering();

// Call the function on-chain using the generated signature
w.registerWithPermit(
aliceAddr, block.timestamp + 1 hours, v, r, s, idCommitment, membershipRateLimit, noIdCommitmentsToErase
w.registerWithPermit(aliceAddr, block.timestamp + 1 hours, v, r, s, membership);

(,,,, uint32 fetchedMembershipRateLimit,, address holder,) = w.memberships(membership.idCommitment);
assertEq(fetchedMembershipRateLimit, membership.rateLimit);
assertEq(holder, aliceAddr);
assertEq(token.balanceOf(address(w)), price);
}

function test__ValidRegistrationWithDAIPermit() external {
vm.pauseGasMetering();

MembershipPermitParams memory membership = MembershipPermitParams({
idCommitment: 2,
rateLimit: w.minMembershipRateLimit(),
idCommitmentsToErase: noIdCommitmentsToErase
});

(, uint256 price) = w.priceCalculator().calculate(membership.rateLimit);

// Creating an owner for a membership (Alice)
uint256 alicePrivK = 0xA11CE;
address aliceAddr = vm.addr(alicePrivK);

// Minting some tokens so Alice can register a membership
token.mint(aliceAddr, price);

// Sign the permit hash using the owner's private key
bytes32 permitHash = keccak256(
abi.encode(
keccak256("Permit(address holder,address spender,uint256 nonce,uint256 expiry,bool allowed)"),
aliceAddr, // Owner of the membership
address(w), // Spender (The rln proxy contract)
token.nonces(aliceAddr),
block.timestamp + 1 hours,
true
)
);

(,,,, uint32 fetchedMembershipRateLimit,, address holder,) = w.memberships(idCommitment);
assertEq(fetchedMembershipRateLimit, membershipRateLimit);
// Sign the permit hash using the owner's private key
(uint8 v, bytes32 r, bytes32 s) =
vm.sign(alicePrivK, ECDSA.toTypedDataHash(token.DAI_DOMAIN_SEPARATOR(), permitHash));

vm.resumeGasMetering();

// Call the function on-chain using the generated signature
w.registerWithDAIPermit(aliceAddr, block.timestamp + 1 hours, token.nonces(aliceAddr), v, r, s, membership);

(,,,, uint32 fetchedMembershipRateLimit,, address holder,) = w.memberships(membership.idCommitment);
assertEq(fetchedMembershipRateLimit, membership.rateLimit);
assertEq(holder, aliceAddr);
assertEq(token.balanceOf(address(w)), price);
}
Expand Down
Loading