Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: permit #22

Merged
merged 1 commit into from
Nov 1, 2024
Merged
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
60 changes: 59 additions & 1 deletion src/Membership.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import { IPriceCalculator } from "./IPriceCalculator.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";
import { Initializable } from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";

// The rate limit is outside the expected limits
Expand Down Expand Up @@ -150,9 +151,9 @@
internal
onlyInitializing
{
require(_minMembershipRateLimit <= _maxMembershipRateLimit);

Check warning on line 154 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 155 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 156 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 @@ -208,6 +209,63 @@
IERC20(token).safeTransferFrom(_sender, address(this), depositAmount);
}

/// @dev acquire a membership and transfer the deposit to the contract
/// Uses the RC20 Permit extension allowing approvals to be made via signatures, as defined in
/// [EIP-2612](https://eips.ethereum.org/EIPS/eip-2612).
/// @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 _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 _acquireMembershipWithPermit(
address _owner,
uint256 _deadline,
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);

ERC20Permit(token).permit(_owner, address(this), depositAmount, _deadline, _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 All @@ -233,7 +291,7 @@
/// @dev Extend a grace-period membership
/// @param _sender the address of the transaction sender
/// @param _idCommitment the idCommitment of the membership
function _extendMembership(address _sender, uint256 _idCommitment) public {
function _extendMembership(address _sender, uint256 _idCommitment) internal {
MembershipInfo storage membership = memberships[_idCommitment];

if (!_isInPeriod(membership.gracePeriodStartTimestamp, membership.gracePeriodDuration)) {
Expand Down
49 changes: 44 additions & 5 deletions src/WakuRlnV2.sol
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,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 32 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 +47,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 50 in src/WakuRlnV2.sol

View workflow job for this annotation

GitHub Actions / lint

Error message for require is too long
_;
}

Expand Down Expand Up @@ -170,23 +170,62 @@
{
// erase memberships without overwriting membership set data to zero (save gas)
_eraseMemberships(idCommitmentsToErase, false);
_register(idCommitment, rateLimit);

(uint32 index, bool indexReused) = _acquireMembership(_msgSender(), idCommitment, rateLimit);

_upsertInTree(idCommitment, rateLimit, index, indexReused);

emit MembershipRegistered(idCommitment, rateLimit, index);
}

/// @notice Register a membership while erasing some expired memberships to reuse their rate limit.
/// Uses the RC20 Permit extension allowing approvals to be made via signatures, as defined in
/// [EIP-2612](https://eips.ethereum.org/EIPS/eip-2612).
/// @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 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
function registerWithPermit(
address owner,
uint256 deadline,
uint8 v,
bytes32 r,
bytes32 s,
uint256 idCommitment,
uint32 rateLimit,
uint256[] calldata idCommitmentsToErase
)
external
onlyValidIdCommitment(idCommitment)
noDuplicateMembership(idCommitment)
membershipSetNotFull
{
// erase memberships without overwriting membership set data to zero (save gas)
_eraseMemberships(idCommitmentsToErase, false);

(uint32 index, bool indexReused) =
_acquireMembershipWithPermit(owner, deadline, 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
function _register(uint256 idCommitment, uint32 rateLimit) internal {
(uint32 index, bool indexReused) = _acquireMembership(_msgSender(), idCommitment, rateLimit);
function _upsertInTree(uint256 idCommitment, uint32 rateLimit, uint32 index, bool indexReused) internal {
uint256 rateCommitment = PoseidonT3.hash([idCommitment, rateLimit]);
if (indexReused) {
LazyIMT.update(merkleTree, rateCommitment, index);
} else {
LazyIMT.insert(merkleTree, rateCommitment);
nextFreeIndex += 1;
}

emit MembershipRegistered(idCommitment, rateLimit, index);
}

/// @notice Returns the root of the Merkle tree that stores rate commitments of memberships
Expand Down Expand Up @@ -271,14 +310,14 @@
/// @notice Set the maximum total rate limit of all memberships in the membership set
/// @param _maxTotalRateLimit new maximum total rate limit (messages per epoch)
function setMaxTotalRateLimit(uint32 _maxTotalRateLimit) external onlyOwner {
require(maxMembershipRateLimit <= _maxTotalRateLimit);

Check warning on line 313 in src/WakuRlnV2.sol

View workflow job for this annotation

GitHub Actions / lint

Provide an error message for require
maxTotalRateLimit = _maxTotalRateLimit;
}

/// @notice Set the maximum rate limit of one membership
/// @param _maxMembershipRateLimit new maximum rate limit per membership (messages per epoch)
function setMaxMembershipRateLimit(uint32 _maxMembershipRateLimit) external onlyOwner {
require(minMembershipRateLimit <= _maxMembershipRateLimit);

Check warning on line 320 in src/WakuRlnV2.sol

View workflow job for this annotation

GitHub Actions / lint

Provide an error message for require
maxMembershipRateLimit = _maxMembershipRateLimit;
}

Expand Down
5 changes: 3 additions & 2 deletions test/TestToken.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@ pragma solidity >=0.8.19 <0.9.0;

import { BaseScript } from "../script/Base.s.sol";
import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import { ERC20Permit } from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol";

contract TestToken is ERC20 {
constructor() ERC20("TestToken", "TTT") { }
contract TestToken is ERC20, ERC20Permit {
constructor() ERC20("TestToken", "TTT") ERC20Permit("TestToken") { }

function mint(address to, uint256 amount) public {
_mint(to, amount);
Expand Down
43 changes: 43 additions & 0 deletions test/WakuRlnV2.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { TestToken } from "./TestToken.sol";
import { PoseidonT3 } from "poseidon-solidity/PoseidonT3.sol";
import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
import { ERC1967Proxy } from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";
import { ECDSA } from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; // For signature manipulation
import "forge-std/console.sol";

contract WakuRlnV2Test is Test {
Expand Down Expand Up @@ -121,6 +122,48 @@ contract WakuRlnV2Test is Test {
assertEq(w.currentTotalRateLimit(), membershipRateLimit);
}

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

// 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);

// Prepare the permit parameters
bytes32 permitHash = keccak256(
abi.encode(
keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"),
aliceAddr, // Owner of the membership
address(w), // Spender (The rln proxy contract)
price,
token.nonces(aliceAddr),
block.timestamp + 1 hours // Deadline
)
);

// Sign the permit hash using the owner's private key
(uint8 v, bytes32 r, bytes32 s) =
vm.sign(alicePrivK, ECDSA.toTypedDataHash(token.DOMAIN_SEPARATOR(), permitHash));

vm.resumeGasMetering();

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

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

function test__LinearPriceCalculation(uint32 membershipRateLimit) external view {
IPriceCalculator priceCalculator = w.priceCalculator();
uint256 pricePerMessagePerPeriod = LinearPriceCalculator(address(priceCalculator)).pricePerMessagePerEpoch();
Expand Down
Loading