From f7f892602a82c17d827b37d94aa50145fbd23aad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20N=C3=BA=C3=B1ez?= Date: Mon, 7 Feb 2022 17:23:36 +0100 Subject: [PATCH 1/7] Update SubscriptionManager to latest version See https://github.com/nucypher/nucypher-contracts/pull/2 --- .../source/contracts/SubscriptionManager.sol | 91 +++++++++++++------ 1 file changed, 63 insertions(+), 28 deletions(-) diff --git a/nucypher/blockchain/eth/sol/source/contracts/SubscriptionManager.sol b/nucypher/blockchain/eth/sol/source/contracts/SubscriptionManager.sol index 4bfaf969c..90cb3c19e 100644 --- a/nucypher/blockchain/eth/sol/source/contracts/SubscriptionManager.sol +++ b/nucypher/blockchain/eth/sol/source/contracts/SubscriptionManager.sol @@ -2,38 +2,56 @@ pragma solidity ^0.8.0; -contract SubscriptionManager { +import "@openzeppelin-upgradeable/contracts/access/AccessControlUpgradeable.sol"; +import "@openzeppelin-upgradeable/contracts/proxy/utils/Initializable.sol"; - uint256 private constant RATE_PER_DAY = 50 gwei; - uint256 public constant RATE_PER_SECOND = RATE_PER_DAY / 1 days; +contract SubscriptionManager is Initializable, AccessControlUpgradeable { - struct Policy { // TODO: Optimize struct layout + bytes32 public constant SET_RATE_ROLE = + keccak256("Power to set the fee rate"); + bytes32 public constant WITHDRAW_ROLE = + keccak256("Power to withdraw funds from SubscriptionManager"); + + // The layout of policy struct is optimized, so sponsor, timestamps and size + // fit in a single 256-word. + struct Policy { address payable sponsor; + uint32 startTimestamp; + uint32 endTimestamp; + uint16 size; // also known as `N` address owner; - uint64 startTimestamp; - uint64 endTimestamp; } event PolicyCreated( bytes16 indexed policyId, address indexed sponsor, address indexed owner, - uint64 startTimestamp, - uint64 endTimestamp + uint16 size, + uint32 startTimestamp, + uint32 endTimestamp ); - - address payable public owner; - mapping (bytes16 => Policy) public policies; - constructor(){ - owner = payable(msg.sender); + event FeeRateUpdated(uint256 oldFeeRate, uint256 newFeeRate); + + // Per-second service fee rate + uint256 public feeRate; + + // Mapping that stores policy structs, keyed by policy ID + mapping (bytes16 => Policy) internal _policies; + + function initialize(uint256 _feeRate) public initializer { + _setFeeRate(_feeRate); + _setupRole(SET_RATE_ROLE, msg.sender); + _setupRole(WITHDRAW_ROLE, msg.sender); + _setupRole(DEFAULT_ADMIN_ROLE, msg.sender); } function createPolicy( bytes16 _policyId, address _policyOwner, - uint64 _startTimestamp, - uint64 _endTimestamp + uint16 _size, + uint32 _startTimestamp, + uint32 _endTimestamp ) external payable { @@ -42,31 +60,33 @@ contract SubscriptionManager { block.timestamp < _endTimestamp, "Invalid timestamps" ); - uint64 duration = _endTimestamp - _startTimestamp; + uint32 duration = _endTimestamp - _startTimestamp; require( - duration > 0 && - msg.value == RATE_PER_SECOND * uint64(duration) + duration > 0 && _size > 0 && + msg.value == feeRate * _size * uint32(duration) ); - //Policy storage policy = - _createPolicy(_policyId, _policyOwner, _startTimestamp, _endTimestamp); + + _createPolicy(_policyId, _policyOwner, _size, _startTimestamp, _endTimestamp); } /** * @notice Create policy * @param _policyId Policy id * @param _policyOwner Policy owner. Zero address means sender is owner + * @param _size Number of nodes involved in the policy * @param _startTimestamp Start timestamp of the policy in seconds * @param _endTimestamp End timestamp of the policy in seconds */ function _createPolicy( bytes16 _policyId, address _policyOwner, - uint64 _startTimestamp, - uint64 _endTimestamp + uint16 _size, + uint32 _startTimestamp, + uint32 _endTimestamp ) internal returns (Policy storage policy) { - policy = policies[_policyId]; + policy = _policies[_policyId]; require( policy.endTimestamp < block.timestamp, "Policy is currently active" @@ -75,6 +95,7 @@ contract SubscriptionManager { policy.sponsor = payable(msg.sender); policy.startTimestamp = _startTimestamp; policy.endTimestamp = _endTimestamp; + policy.size = _size; if (_policyOwner != msg.sender && _policyOwner != address(0)) { policy.owner = _policyOwner; @@ -84,20 +105,34 @@ contract SubscriptionManager { _policyId, msg.sender, _policyOwner == address(0) ? msg.sender : _policyOwner, + _size, _startTimestamp, _endTimestamp ); } - function isPolicyActive(bytes16 _policyID) public view returns(bool){ - return policies[_policyID].endTimestamp > block.timestamp; + function getPolicy(bytes16 _policyID) public view returns(Policy memory){ + return _policies[_policyID]; } - function sweep(address payable recipient) external { - require(msg.sender == owner); + function isPolicyActive(bytes16 _policyID) public view returns(bool){ + return _policies[_policyID].endTimestamp > block.timestamp; + } + + function _setFeeRate(uint256 newFee) internal { + uint256 oldFee = feeRate; + feeRate = newFee; + emit FeeRateUpdated(oldFee, newFee); + } + + function setFeeRate(uint256 _rate_per_second) onlyRole(SET_RATE_ROLE) external { + _setFeeRate(_rate_per_second); + } + + function sweep(address payable recipient) onlyRole(WITHDRAW_ROLE) external { uint256 balance = address(this).balance; (bool sent, ) = recipient.call{value: balance}(""); require(sent, "Failed transfer"); } -} +} \ No newline at end of file From 1d0c9060745e591e914751e8c476da5f3e537276 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20N=C3=BA=C3=B1ez?= Date: Mon, 7 Feb 2022 17:48:36 +0100 Subject: [PATCH 2/7] Remove dependency for AccessControlUpgradeable This still produces the same ABI, which is what we need for agents. Actual tests and deployer are on the nucypher-contracts repo --- .../source/contracts/SubscriptionManager.sol | 17 ++++------------- 1 file changed, 4 insertions(+), 13 deletions(-) diff --git a/nucypher/blockchain/eth/sol/source/contracts/SubscriptionManager.sol b/nucypher/blockchain/eth/sol/source/contracts/SubscriptionManager.sol index 90cb3c19e..2c943cabd 100644 --- a/nucypher/blockchain/eth/sol/source/contracts/SubscriptionManager.sol +++ b/nucypher/blockchain/eth/sol/source/contracts/SubscriptionManager.sol @@ -2,15 +2,9 @@ pragma solidity ^0.8.0; -import "@openzeppelin-upgradeable/contracts/access/AccessControlUpgradeable.sol"; -import "@openzeppelin-upgradeable/contracts/proxy/utils/Initializable.sol"; +import "../zeppelin/proxy/Initializable.sol"; -contract SubscriptionManager is Initializable, AccessControlUpgradeable { - - bytes32 public constant SET_RATE_ROLE = - keccak256("Power to set the fee rate"); - bytes32 public constant WITHDRAW_ROLE = - keccak256("Power to withdraw funds from SubscriptionManager"); +contract SubscriptionManager is Initializable { // The layout of policy struct is optimized, so sponsor, timestamps and size // fit in a single 256-word. @@ -41,9 +35,6 @@ contract SubscriptionManager is Initializable, AccessControlUpgradeable { function initialize(uint256 _feeRate) public initializer { _setFeeRate(_feeRate); - _setupRole(SET_RATE_ROLE, msg.sender); - _setupRole(WITHDRAW_ROLE, msg.sender); - _setupRole(DEFAULT_ADMIN_ROLE, msg.sender); } function createPolicy( @@ -125,11 +116,11 @@ contract SubscriptionManager is Initializable, AccessControlUpgradeable { emit FeeRateUpdated(oldFee, newFee); } - function setFeeRate(uint256 _rate_per_second) onlyRole(SET_RATE_ROLE) external { + function setFeeRate(uint256 _rate_per_second) external { _setFeeRate(_rate_per_second); } - function sweep(address payable recipient) onlyRole(WITHDRAW_ROLE) external { + function sweep(address payable recipient) external { uint256 balance = address(this).balance; (bool sent, ) = recipient.call{value: balance}(""); require(sent, "Failed transfer"); From 5482809a703f0243c0d807a69d1f501571a30f09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20N=C3=BA=C3=B1ez?= Date: Mon, 7 Feb 2022 18:33:22 +0100 Subject: [PATCH 3/7] No need for sweep endpoint in the SubscriptionManager agent --- nucypher/blockchain/eth/agents.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/nucypher/blockchain/eth/agents.py b/nucypher/blockchain/eth/agents.py index 9b49fa1f4..77fc743b4 100644 --- a/nucypher/blockchain/eth/agents.py +++ b/nucypher/blockchain/eth/agents.py @@ -1003,13 +1003,6 @@ class SubscriptionManagerAgent(EthereumContractAgent): ) return receipt - @contract_api(TRANSACTION) - def sweep(self, recipient: ChecksumAddress, transacting_power: TransactingPower) -> TxReceipt: - """Collect fees (ETH) earned since last withdrawal""" - contract_function: ContractFunction = self.contract.functions.sweep(recipient) - receipt = self.blockchain.send_transaction(contract_function=contract_function, transacting_power=transacting_power) - return receipt - class AdjudicatorAgent(EthereumContractAgent): From 2546ee6685d356680afbfbf21f5e1c57e6497d2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20N=C3=BA=C3=B1ez?= Date: Mon, 7 Feb 2022 18:34:36 +0100 Subject: [PATCH 4/7] Adapt SubscriptionManager agent to changes in contract In particular, new "size" parameter and different Policy struct layout --- nucypher/blockchain/eth/agents.py | 11 +++++++---- nucypher/policy/payment.py | 1 + 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/nucypher/blockchain/eth/agents.py b/nucypher/blockchain/eth/agents.py index 77fc743b4..f0fbffad7 100644 --- a/nucypher/blockchain/eth/agents.py +++ b/nucypher/blockchain/eth/agents.py @@ -956,7 +956,7 @@ class SubscriptionManagerAgent(EthereumContractAgent): @contract_api(CONTRACT_CALL) def rate_per_second(self) -> Wei: - result = self.contract.functions.RATE_PER_SECOND().call() + result = self.contract.functions.feeRate().call() return Wei(result) @contract_api(CONTRACT_CALL) @@ -969,10 +969,11 @@ class SubscriptionManagerAgent(EthereumContractAgent): record = self.contract.functions.policies(policy_id).call() policy_info = self.PolicyInfo( sponsor=record[0], + start_timestamp=record[1], + end_timestamp=record[2], + size=record[3], # If the policyOwner addr is null, we return the sponsor addr instead of the owner. - owner=record[0] if record[1] == NULL_ADDRESS else record[1], - start_timestamp=record[2], - end_timestamp=record[3] + owner=record[0] if record[4] == NULL_ADDRESS else record[4] ) return policy_info @@ -984,6 +985,7 @@ class SubscriptionManagerAgent(EthereumContractAgent): def create_policy(self, policy_id: bytes, transacting_power: TransactingPower, + size: int, start_timestamp: Timestamp, end_timestamp: Timestamp, value: Wei, @@ -993,6 +995,7 @@ class SubscriptionManagerAgent(EthereumContractAgent): contract_function: ContractFunction = self.contract.functions.createPolicy( policy_id, owner_address, + size, start_timestamp, end_timestamp ) diff --git a/nucypher/policy/payment.py b/nucypher/policy/payment.py index c14f11fb4..5146b1244 100644 --- a/nucypher/policy/payment.py +++ b/nucypher/policy/payment.py @@ -161,6 +161,7 @@ class SubscriptionManagerPayment(ContractPayment): receipt = self.agent.create_policy( value=policy.value, # wei policy_id=bytes(policy.hrac), # bytes16 _policyID + size=len(policy.kfrags), # uint16 start_timestamp=policy.commencement, # uint16 end_timestamp=policy.expiration, # uint16 transacting_power=policy.publisher.transacting_power From da177f2c7791c0ca4e1217ebede20a6053d70d84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20N=C3=BA=C3=B1ez?= Date: Mon, 7 Feb 2022 18:38:59 +0100 Subject: [PATCH 5/7] Adapt SubscriptionManager agent and payments to new "size" parameter Policy value is now: value = feeRate * size * duration --- nucypher/blockchain/eth/agents.py | 2 +- nucypher/policy/payment.py | 19 +++++++++++-------- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/nucypher/blockchain/eth/agents.py b/nucypher/blockchain/eth/agents.py index f0fbffad7..dc08c3333 100644 --- a/nucypher/blockchain/eth/agents.py +++ b/nucypher/blockchain/eth/agents.py @@ -955,7 +955,7 @@ class SubscriptionManagerAgent(EthereumContractAgent): # @contract_api(CONTRACT_CALL) - def rate_per_second(self) -> Wei: + def fee_rate(self) -> Wei: result = self.contract.functions.feeRate().call() return Wei(result) diff --git a/nucypher/policy/payment.py b/nucypher/policy/payment.py index 5146b1244..da37479d3 100644 --- a/nucypher/policy/payment.py +++ b/nucypher/policy/payment.py @@ -51,6 +51,7 @@ class PaymentMethod(ReencryptionPrerequisite, ABC): commencement: int # epoch expiration: int # epoch duration: int # seconds or periods + shares: int @abstractmethod def pay(self, policy: Policy) -> Dict: @@ -128,6 +129,7 @@ class FreeReencryptions(PaymentMethod): return 0 def quote(self, + shares: int, commencement: Optional[Timestamp] = None, expiration: Optional[Timestamp] = None, duration: Optional[int] = None, @@ -136,6 +138,7 @@ class FreeReencryptions(PaymentMethod): return self.Quote( value=0, rate=0, + shares=shares, duration=duration, commencement=commencement, expiration=expiration @@ -170,10 +173,11 @@ class SubscriptionManagerPayment(ContractPayment): @property def rate(self) -> Wei: - fixed_rate = self.agent.rate_per_second() + fixed_rate = self.agent.fee_rate() return Wei(fixed_rate) def quote(self, + shares: int, commencement: Optional[Timestamp] = None, expiration: Optional[Timestamp] = None, duration: Optional[int] = None, @@ -206,19 +210,18 @@ class SubscriptionManagerPayment(ContractPayment): q = self.Quote( rate=Wei(self.rate), - value=Wei(self.rate * duration), + value=Wei(self.rate * duration * shares), + shares=shares, commencement=Timestamp(commencement), expiration=Timestamp(expiration), duration=duration ) return q - def validate_price(self, value: Wei, duration: Wei, *args, **kwargs) -> bool: - if value and duration: - if duration != value // self.rate: - raise ValueError(f"Invalid duration ({duration}) for value ({value}).") - if value != duration * self.rate: - raise ValueError(f"Invalid value ({value}) for duration ({duration}).") + def validate_price(self, value: Wei, duration: Wei, shares: int, *args, **kwargs) -> bool: + expected_price = Wei(shares * duration * self.rate) + if value != expected_price: + raise ValueError(f"Policy value ({value}) doesn't match expected value ({expected_price})") return True From 0028327bfd68ce65b665f464a6c97a5c0fd68ff2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20N=C3=BA=C3=B1ez?= Date: Mon, 7 Feb 2022 19:10:53 +0100 Subject: [PATCH 6/7] Add initial fee rate to economics class --- nucypher/blockchain/economics.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/nucypher/blockchain/economics.py b/nucypher/blockchain/economics.py index 4073caac5..423f3399c 100644 --- a/nucypher/blockchain/economics.py +++ b/nucypher/blockchain/economics.py @@ -17,6 +17,9 @@ along with nucypher. If not, see . from typing import Tuple, Optional +from web3 import Web3 +from web3.types import Wei + from nucypher.blockchain.eth.agents import ( ContractAgency, PREApplicationAgent @@ -29,6 +32,7 @@ class Economics: _default_min_authorization = TToken(40_000, 'T').to_units() _default_min_operator_seconds = 60 * 60 * 24 # one day in seconds + _default_fee_rate = Wei(Web3.toWei(1, 'gwei')) # TODO: Reintroduce Adjudicator # Slashing parameters @@ -44,6 +48,7 @@ class Economics: def __init__(self, min_operator_seconds: int = _default_min_operator_seconds, min_authorization: int = _default_min_authorization, + fee_rate: Wei = _default_fee_rate, # Adjudicator # hash_algorithm: int = _default_hash_algorithm, @@ -73,6 +78,7 @@ class Economics: self.min_operator_seconds = min_operator_seconds self.min_authorization = min_authorization + self.fee_rate = fee_rate @property def pre_application_deployment_parameters(self) -> Tuple[int, ...]: From 5dae09b969bf3d1e84e415187656befef61e8112 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20N=C3=BA=C3=B1ez?= Date: Mon, 7 Feb 2022 19:11:16 +0100 Subject: [PATCH 7/7] Add initialization step to SubscriptionManagerDeployer --- nucypher/blockchain/eth/deployers.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/nucypher/blockchain/eth/deployers.py b/nucypher/blockchain/eth/deployers.py index c8fcfe469..209e6ac13 100644 --- a/nucypher/blockchain/eth/deployers.py +++ b/nucypher/blockchain/eth/deployers.py @@ -883,7 +883,7 @@ class SubscriptionManagerDeployer(BaseContractDeployer, OwnableContractMixin): agency = SubscriptionManagerAgent contract_name = agency.contract_name - deployment_steps = ('contract_deployment',) + deployment_steps = ('contract_deployment', 'initialize') _upgradeable = False _ownable = True @@ -906,8 +906,19 @@ class SubscriptionManagerDeployer(BaseContractDeployer, OwnableContractMixin): gas_limit=gas_limit, confirmations=confirmations, **constructor_kwargs) + self._contract = contract - return {self.deployment_steps[0]: deployment_receipt} + + tx_args = {} + if gas_limit: + tx_args.update({'gas': gas_limit}) # TODO: Gas management - 842 + initialize_function = contract.functions.initialize(self.economics.fee_rate) + initialize_receipt = self.blockchain.send_transaction(contract_function=initialize_function, + transacting_power=transacting_power, + payload=tx_args, + confirmations=confirmations) + return {self.deployment_steps[0]: deployment_receipt, + self.deployment_steps[1]: initialize_receipt} class StakingInterfaceRouterDeployer(OwnableContractMixin, ProxyContractDeployer):