mirror of
https://github.com/logos-messaging/logos-messaging-rlnv2-contract.git
synced 2026-01-02 14:03:07 +00:00
feat: DAI permit
This commit is contained in:
parent
b7e9a9b1bc
commit
0cb3563ced
16
src/IDAIPermit.sol
Normal file
16
src/IDAIPermit.sol
Normal file
@ -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;
|
||||
}
|
||||
@ -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";
|
||||
@ -266,6 +267,64 @@ abstract contract MembershipUpgradeable is Initializable {
|
||||
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
|
||||
|
||||
@ -20,6 +20,15 @@ error InvalidIdCommitment(uint256 idCommitment);
|
||||
/// 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 =
|
||||
@ -186,24 +195,25 @@ contract WakuRlnV2 is Initializable, OwnableUpgradeable, UUPSUpgradeable, Member
|
||||
/// @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");
|
||||
|
||||
// erase memberships without overwriting membership set data to zero (save gas)
|
||||
_eraseMemberships(idCommitmentsToErase, false);
|
||||
|
||||
@ -215,6 +225,45 @@ contract WakuRlnV2 is Initializable, OwnableUpgradeable, UUPSUpgradeable, Member
|
||||
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");
|
||||
|
||||
// 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
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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;
|
||||
@ -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);
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user