diff --git a/contracts/IPriceCalculator.sol b/contracts/IPriceCalculator.sol new file mode 100644 index 0000000..335c0ba --- /dev/null +++ b/contracts/IPriceCalculator.sol @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.15; + +interface IPriceCalculator { + /// Returns the token and price to pay in `token` for some `_rateLimit` + /// @param _rateLimit the rate limit the user wants to acquire + function calculate(uint _rateLimit) external view returns (address, uint); +} \ No newline at end of file diff --git a/contracts/LinearPriceCalculator.sol b/contracts/LinearPriceCalculator.sol new file mode 100644 index 0000000..b450fab --- /dev/null +++ b/contracts/LinearPriceCalculator.sol @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.15; + +import {Ownable} from "openzeppelin-contracts/contracts/access/Ownable.sol"; +import {IPriceCalculator} from "./IPriceCalculator.sol"; + +/// @title Linear Price Calculator to determine the price to acquire a membership +contract LinearPriceCalculator is IPriceCalculator, Ownable { + address private token; + uint private pricePerMessage; + + constructor(address _token, uint16 _price) Ownable() { + token = _token; + pricePerMessage = _price; + } + + /// Set accepted token and price per message + /// @param _token The token accepted by the membership management for RLN + /// @param _price Price per message per epoch + function setTokenAndPrice(address _token, uint _price) external onlyOwner { + token = _token; + pricePerMessage = _price; + } + + function calculate(uint _rateLimit) external view returns (address, uint) { + return (token, _rateLimit * pricePerMessage); + } + +} diff --git a/contracts/Membership.sol b/contracts/Membership.sol new file mode 100644 index 0000000..9883666 --- /dev/null +++ b/contracts/Membership.sol @@ -0,0 +1,64 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.15; + +import {IPriceCalculator} from "./IPriceCalculator.sol"; +import "openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; +import "openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol"; +import "openzeppelin-contracts/contracts/utils/Context.sol"; + +error IncorrectAmount(); +error OnlyTokensAccepted(); +error TokenMismatch(); + +error InvalidRateLimit(); +error ExceedMaxRateLimitPerEpoch(); + +contract Membership { + using SafeERC20 for IERC20; + + IPriceCalculator public priceCalculator; + + uint public maxTotalRateLimitPerEpoch; + uint16 public maxRateLimitPerMembership; + uint16 public minRateLimitPerMembership; + + uint public totalRateLimitPerEpoch; + + function __Membership_init( + address _priceCalculator, + uint _maxTotalRateLimitPerEpoch, + uint16 _maxRateLimitPerMembership, + uint16 _minRateLimitPerMembership + ) internal { + priceCalculator = IPriceCalculator(_priceCalculator); + maxTotalRateLimitPerEpoch = _maxTotalRateLimitPerEpoch; + maxRateLimitPerMembership = _maxRateLimitPerMembership; + minRateLimitPerMembership = _minRateLimitPerMembership; + } + + function transferMembershipFees(address _from, uint _rateLimit) internal { + (address token, uint price) = priceCalculator.calculate(_rateLimit); + if (token == address(0)) { + if (msg.value != price) revert IncorrectAmount(); + } else { + if (msg.value != 0) revert OnlyTokensAccepted(); + IERC20(token).safeTransferFrom(_from, address(this), price); + } + } + + function acquireRateLimit(uint256[] memory commitments, uint _rateLimit) internal { + if ( + _rateLimit < minRateLimitPerMembership || + _rateLimit > maxRateLimitPerMembership + ) revert InvalidRateLimit(); + + uint newTotalRateLimitPerEpoch = totalRateLimitPerEpoch + _rateLimit; + if (newTotalRateLimitPerEpoch > maxTotalRateLimitPerEpoch) revert ExceedMaxRateLimitPerEpoch(); + + // TODO: store _rateLimit + // TODO: + // Epoch length epoch 10 minutes + // Membership expiration term T 180 days + // Membership grace period G 30 days + } +} diff --git a/contracts/WakuRlnRegistry.sol b/contracts/WakuRlnRegistry.sol index cc8c697..f5e395f 100644 --- a/contracts/WakuRlnRegistry.sol +++ b/contracts/WakuRlnRegistry.sol @@ -2,6 +2,7 @@ pragma solidity 0.8.15; import {WakuRln} from "./WakuRln.sol"; +import {Membership} from "./Membership.sol"; import {IPoseidonHasher} from "rln-contract/PoseidonHasher.sol"; import {UUPSUpgradeable} from "openzeppelin-contracts/contracts/proxy/utils/UUPSUpgradeable.sol"; import {OwnableUpgradeable} from "openzeppelin-contracts-upgradeable/contracts/access/OwnableUpgradeable.sol"; @@ -12,7 +13,7 @@ error NoStorageContractAvailable(); error IncompatibleStorage(); error IncompatibleStorageIndex(); -contract WakuRlnRegistry is OwnableUpgradeable, UUPSUpgradeable { +contract WakuRlnRegistry is OwnableUpgradeable, UUPSUpgradeable, Membership { uint16 public nextStorageIndex; mapping(uint16 => address) public storages; @@ -27,8 +28,14 @@ contract WakuRlnRegistry is OwnableUpgradeable, UUPSUpgradeable { _; } - function initialize(address _poseidonHasher) external initializer { + modifier onlyValidStorageIndex(uint16 storageIndex) { + if (storageIndex >= nextStorageIndex) revert NoStorageContractAvailable(); + _; + } + + function initialize(address _poseidonHasher, address _priceCalculator) external initializer { poseidonHasher = IPoseidonHasher(_poseidonHasher); + __Membership_init(_priceCalculator); __Ownable_init(); } @@ -54,6 +61,7 @@ contract WakuRlnRegistry is OwnableUpgradeable, UUPSUpgradeable { } function register(uint256[] calldata commitments) external onlyUsableStorage { + // TODO: modify function to receive rate limit // iteratively check if the storage contract is full, and increment the usingStorageIndex if it is while (true) { try WakuRln(storages[usingStorageIndex]).register(commitments) { @@ -72,16 +80,24 @@ contract WakuRlnRegistry is OwnableUpgradeable, UUPSUpgradeable { } } - function register(uint16 storageIndex, uint256[] calldata commitments) external { - if (storageIndex >= nextStorageIndex) revert NoStorageContractAvailable(); + function register(uint16 storageIndex, uint256[] calldata commitments) external onlyValidStorageIndex(storageIndex) { + // TODO: modify function to receive the ratelimit to buy + uint _rateLimit = 4; + acquireRateLimit(commitments, _rateLimit); + transferMembershipFees(_msgSender(), _rateLimit * commitments.length); WakuRln(storages[storageIndex]).register(commitments); } - function register(uint16 storageIndex, uint256 commitment) external { - if (storageIndex >= nextStorageIndex) revert NoStorageContractAvailable(); + function register(uint16 storageIndex, uint256 commitment) external payable onlyValidStorageIndex(storageIndex) { // optimize the gas used below uint256[] memory commitments = new uint256[](1); commitments[0] = commitment; + + // TODO: modify function to receive the number of messages + uint _rateLimit = 4; + acquireRateLimit(commitments, _rateLimit); + transferMembershipFees(_msgSender(), _rateLimit); + WakuRln(storages[storageIndex]).register(commitments); } diff --git a/deploy/002_deploy_price_calculator.ts b/deploy/002_deploy_price_calculator.ts new file mode 100644 index 0000000..54aab17 --- /dev/null +++ b/deploy/002_deploy_price_calculator.ts @@ -0,0 +1,16 @@ +import { HardhatRuntimeEnvironment } from "hardhat/types"; +import { DeployFunction } from "hardhat-deploy/types"; + +const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { + const { deployments, getUnnamedAccounts } = hre; + const { deploy } = deployments; + + const [deployer] = await getUnnamedAccounts(); + + await deploy("WakuSimplePriceCalculator", { + from: deployer, + log: true, + }); +}; +export default func; +func.tags = ["WakuSimplePriceCalculator"]; diff --git a/deploy/002_deploy_rln_registry.ts b/deploy/003_deploy_rln_registry.ts similarity index 68% rename from deploy/002_deploy_rln_registry.ts rename to deploy/003_deploy_rln_registry.ts index 71f266e..1ffe4fb 100644 --- a/deploy/002_deploy_rln_registry.ts +++ b/deploy/003_deploy_rln_registry.ts @@ -10,15 +10,24 @@ const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { const poseidonHasherAddress = (await deployments.get("PoseidonHasher")) .address; + const priceCalculatorAddress = ( + await deployments.get("WakuSimplePriceCalculator") + ).address; + const implRes = await deploy("WakuRlnRegistry_Implementation", { contract: "WakuRlnRegistry", from: deployer, log: true, }); - let initializeAbi = ["function initialize(address _poseidonHasher)"]; + let initializeAbi = [ + "function initialize(address _poseidonHasher, address _priceCalculator)", + ]; let iface = new hre.ethers.utils.Interface(initializeAbi); - const data = iface.encodeFunctionData("initialize", [poseidonHasherAddress]); + const data = iface.encodeFunctionData("initialize", [ + poseidonHasherAddress, + priceCalculatorAddress, + ]); await deploy("WakuRlnRegistry_Proxy", { contract: "ERC1967Proxy", @@ -30,4 +39,4 @@ const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { export default func; func.tags = ["WakuRlnRegistry"]; -func.dependencies = ["PoseidonHasher"]; +func.dependencies = ["PoseidonHasher", "WakuSimplePriceCalculator"]; diff --git a/deploy/003_deploy_rln.ts b/deploy/004_deploy_rln.ts similarity index 100% rename from deploy/003_deploy_rln.ts rename to deploy/004_deploy_rln.ts diff --git a/test/WakuRlnRegistry.t.sol b/test/WakuRlnRegistry.t.sol index 3b07cab..63c2cfc 100644 --- a/test/WakuRlnRegistry.t.sol +++ b/test/WakuRlnRegistry.t.sol @@ -2,6 +2,8 @@ pragma solidity ^0.8.15; import "../contracts/WakuRlnRegistry.sol"; +import "../contracts/IPriceCalculator.sol"; +import "../contracts/LinearPriceCalculator.sol"; import {PoseidonHasher} from "rln-contract/PoseidonHasher.sol"; import {DuplicateIdCommitment, FullTree} from "rln-contract/RlnBase.sol"; import {ERC1967Proxy} from "openzeppelin-contracts/contracts/proxy/ERC1967/ERC1967Proxy.sol"; @@ -15,11 +17,14 @@ contract WakuRlnRegistryTest is Test { WakuRlnRegistry public wakuRlnRegistry; PoseidonHasher public poseidonHasher; + IPriceCalculator public priceCalculator; function setUp() public { + LinearPriceCalculator p = new LinearPriceCalculator(); + priceCalculator = IPriceCalculator(address(p)); poseidonHasher = new PoseidonHasher(); address implementation = address(new WakuRlnRegistry()); - bytes memory data = abi.encodeCall(WakuRlnRegistry.initialize, address(poseidonHasher)); + bytes memory data = abi.encodeCall(WakuRlnRegistry.initialize, (address(poseidonHasher), address(priceCalculator))); address proxy = address(new ERC1967Proxy(implementation, data)); wakuRlnRegistry = WakuRlnRegistry(proxy); }