Skip to content

Commit

Permalink
feat: permit (#22)
Browse files Browse the repository at this point in the history
  • Loading branch information
richard-ramos authored Nov 1, 2024
1 parent e46ce5a commit 5d8fd57
Show file tree
Hide file tree
Showing 4 changed files with 149 additions and 8 deletions.
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 @@ pragma solidity 0.8.24;
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 @@ -208,6 +209,63 @@ abstract contract MembershipUpgradeable is Initializable {
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 @@ abstract contract MembershipUpgradeable is Initializable {
/// @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 @@ -170,23 +170,62 @@ contract WakuRlnV2 is Initializable, OwnableUpgradeable, UUPSUpgradeable, Member
{
// 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
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

0 comments on commit 5d8fd57

Please sign in to comment.