Merge pull request #2619 from vzotova/batch-policy

Creating multiple policies in one tx
pull/2672/head
Victoria 2021-04-28 08:38:10 +03:00 committed by GitHub
commit a71d1de553
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 388 additions and 41 deletions

View File

@ -0,0 +1 @@
PolicyManager: creating multiple policies in one tx

View File

@ -17,7 +17,7 @@ import "contracts/proxy/Upgradeable.sol";
/** /**
* @title PolicyManager * @title PolicyManager
* @notice Contract holds policy data and locks accrued policy fees * @notice Contract holds policy data and locks accrued policy fees
* @dev |v6.2.2| * @dev |v6.3.1|
*/ */
contract PolicyManager is Upgradeable { contract PolicyManager is Upgradeable {
using SafeERC20 for NuCypherToken; using SafeERC20 for NuCypherToken;
@ -253,68 +253,161 @@ contract PolicyManager is Upgradeable {
) )
external payable external payable
{ {
Policy storage policy = policies[_policyId];
require( require(
_policyId != RESERVED_POLICY_ID &&
policy.feeRate == 0 &&
!policy.disabled &&
_endTimestamp > block.timestamp && _endTimestamp > block.timestamp &&
msg.value > 0 msg.value > 0
); );
require(address(this).balance <= MAX_BALANCE); require(address(this).balance <= MAX_BALANCE);
uint16 currentPeriod = getCurrentPeriod(); uint16 currentPeriod = getCurrentPeriod();
uint16 endPeriod = uint16(_endTimestamp / secondsPerPeriod) + 1; uint16 endPeriod = uint16(_endTimestamp / secondsPerPeriod) + 1;
uint256 numberOfPeriods = endPeriod - currentPeriod; uint256 numberOfPeriods = endPeriod - currentPeriod;
policy.sponsor = msg.sender; uint128 feeRate = uint128(msg.value.div(_nodes.length) / numberOfPeriods);
policy.startTimestamp = uint64(block.timestamp); require(feeRate > 0 && feeRate * numberOfPeriods * _nodes.length == msg.value);
policy.endTimestamp = _endTimestamp;
policy.feeRate = uint128(msg.value.div(_nodes.length) / numberOfPeriods); Policy storage policy = createPolicy(_policyId, _policyOwner, _endTimestamp, feeRate, _nodes.length);
require(policy.feeRate > 0 && policy.feeRate * numberOfPeriods * _nodes.length == msg.value);
if (_policyOwner != msg.sender && _policyOwner != address(0)) {
policy.owner = _policyOwner;
}
for (uint256 i = 0; i < _nodes.length; i++) { for (uint256 i = 0; i < _nodes.length; i++) {
address node = _nodes[i]; address node = _nodes[i];
require(node != RESERVED_NODE); addFeeToNode(currentPeriod, endPeriod, node, feeRate, int256(feeRate));
NodeInfo storage nodeInfo = nodes[node];
require(nodeInfo.previousFeePeriod != 0 &&
nodeInfo.previousFeePeriod < currentPeriod &&
policy.feeRate >= getMinFeeRate(nodeInfo));
// Check default value for feeDelta
if (nodeInfo.feeDelta[currentPeriod] == DEFAULT_FEE_DELTA) {
nodeInfo.feeDelta[currentPeriod] = int256(policy.feeRate);
} else {
// Overflow protection removed, because ETH total supply less than uint255/int256
nodeInfo.feeDelta[currentPeriod] += int256(policy.feeRate);
}
if (nodeInfo.feeDelta[endPeriod] == DEFAULT_FEE_DELTA) {
nodeInfo.feeDelta[endPeriod] = -int256(policy.feeRate);
} else {
nodeInfo.feeDelta[endPeriod] -= int256(policy.feeRate);
}
// Reset to default value if needed
if (nodeInfo.feeDelta[currentPeriod] == 0) {
nodeInfo.feeDelta[currentPeriod] = DEFAULT_FEE_DELTA;
}
if (nodeInfo.feeDelta[endPeriod] == 0) {
nodeInfo.feeDelta[endPeriod] = DEFAULT_FEE_DELTA;
}
policy.arrangements.push(ArrangementInfo(node, 0, 0)); policy.arrangements.push(ArrangementInfo(node, 0, 0));
} }
}
/**
* @notice Create multiple policies with the same owner, nodes and length
* @dev Generate policy ids before creation
* @param _policyIds Policy ids
* @param _policyOwner Policy owner. Zero address means sender is owner
* @param _endTimestamp End timestamp of all policies in seconds
* @param _nodes Nodes that will handle all policies
*/
function createPolicies(
bytes16[] calldata _policyIds,
address _policyOwner,
uint64 _endTimestamp,
address[] calldata _nodes
)
external payable
{
require(
_endTimestamp > block.timestamp &&
msg.value > 0 &&
_policyIds.length > 1
);
require(address(this).balance <= MAX_BALANCE);
uint16 currentPeriod = getCurrentPeriod();
uint16 endPeriod = uint16(_endTimestamp / secondsPerPeriod) + 1;
uint256 numberOfPeriods = endPeriod - currentPeriod;
uint128 feeRate = uint128(msg.value.div(_nodes.length) / numberOfPeriods / _policyIds.length);
require(feeRate > 0 && feeRate * numberOfPeriods * _nodes.length * _policyIds.length == msg.value);
for (uint256 i = 0; i < _policyIds.length; i++) {
Policy storage policy = createPolicy(_policyIds[i], _policyOwner, _endTimestamp, feeRate, _nodes.length);
for (uint256 j = 0; j < _nodes.length; j++) {
policy.arrangements.push(ArrangementInfo(_nodes[j], 0, 0));
}
}
int256 fee = int256(_policyIds.length * feeRate);
for (uint256 i = 0; i < _nodes.length; i++) {
address node = _nodes[i];
addFeeToNode(currentPeriod, endPeriod, node, feeRate, fee);
}
}
/**
* @notice Create policy
* @param _policyId Policy id
* @param _policyOwner Policy owner. Zero address means sender is owner
* @param _endTimestamp End timestamp of the policy in seconds
* @param _feeRate Fee rate for policy
* @param _nodesLength Number of nodes that will handle policy
*/
function createPolicy(
bytes16 _policyId,
address _policyOwner,
uint64 _endTimestamp,
uint128 _feeRate,
uint256 _nodesLength
)
internal returns (Policy storage policy)
{
policy = policies[_policyId];
require(
_policyId != RESERVED_POLICY_ID &&
policy.feeRate == 0 &&
!policy.disabled
);
policy.sponsor = msg.sender;
policy.startTimestamp = uint64(block.timestamp);
policy.endTimestamp = _endTimestamp;
policy.feeRate = _feeRate;
if (_policyOwner != msg.sender && _policyOwner != address(0)) {
policy.owner = _policyOwner;
}
emit PolicyCreated( emit PolicyCreated(
_policyId, _policyId,
msg.sender, msg.sender,
_policyOwner == address(0) ? msg.sender : _policyOwner, _policyOwner == address(0) ? msg.sender : _policyOwner,
policy.feeRate, _feeRate,
policy.startTimestamp, policy.startTimestamp,
policy.endTimestamp, policy.endTimestamp,
_nodes.length _nodesLength
); );
} }
/**
* @notice Increase fee rate for specified node
* @param _currentPeriod Current period
* @param _endPeriod End period of policy
* @param _node Node that will handle policy
* @param _feeRate Fee rate for one policy
* @param _overallFeeRate Fee rate for all policies
*/
function addFeeToNode(
uint16 _currentPeriod,
uint16 _endPeriod,
address _node,
uint128 _feeRate,
int256 _overallFeeRate
)
internal
{
require(_node != RESERVED_NODE);
NodeInfo storage nodeInfo = nodes[_node];
require(nodeInfo.previousFeePeriod != 0 &&
nodeInfo.previousFeePeriod < _currentPeriod &&
_feeRate >= getMinFeeRate(nodeInfo));
// Check default value for feeDelta
if (nodeInfo.feeDelta[_currentPeriod] == DEFAULT_FEE_DELTA) {
nodeInfo.feeDelta[_currentPeriod] = _overallFeeRate;
} else {
// Overflow protection removed, because ETH total supply less than uint255/int256
nodeInfo.feeDelta[_currentPeriod] += _overallFeeRate;
}
if (nodeInfo.feeDelta[_endPeriod] == DEFAULT_FEE_DELTA) {
nodeInfo.feeDelta[_endPeriod] = -_overallFeeRate;
} else {
nodeInfo.feeDelta[_endPeriod] -= _overallFeeRate;
}
// Reset to default value if needed
if (nodeInfo.feeDelta[_currentPeriod] == 0) {
nodeInfo.feeDelta[_currentPeriod] = DEFAULT_FEE_DELTA;
}
if (nodeInfo.feeDelta[_endPeriod] == 0) {
nodeInfo.feeDelta[_endPeriod] = DEFAULT_FEE_DELTA;
}
}
/** /**
* @notice Get policy owner * @notice Get policy owner
*/ */

View File

@ -104,7 +104,7 @@ def test_create_revoke(testerchain, escrow, policy_manager):
# Can't create policy using timestamp from the past # Can't create policy using timestamp from the past
with pytest.raises((TransactionFailed, ValueError)): with pytest.raises((TransactionFailed, ValueError)):
tx = policy_manager.functions.createPolicy(policy_id, policy_sponsor, current_timestamp -1, [node1])\ tx = policy_manager.functions.createPolicy(policy_id, policy_sponsor, current_timestamp -1, [node1])\
.transact({'from': policy_sponsor}) .transact({'from': policy_sponsor, 'value': value})
testerchain.wait_for_receipt(tx) testerchain.wait_for_receipt(tx)
# Create policy # Create policy
@ -517,6 +517,240 @@ def test_create_revoke(testerchain, escrow, policy_manager):
testerchain.wait_for_receipt(tx) testerchain.wait_for_receipt(tx)
def test_create_multiple_policies(testerchain, escrow, policy_manager):
creator, policy_sponsor, bad_node, node1, node2, node3, policy_owner, *everyone_else = testerchain.client.accounts
rate = 20
one_period = 60 * 60
number_of_periods = 10
value = rate * number_of_periods
default_fee_delta = policy_manager.functions.DEFAULT_FEE_DELTA().call()
policy_sponsor_balance = testerchain.client.get_balance(policy_sponsor)
policy_created_log = policy_manager.events.PolicyCreated.createFilter(fromBlock='latest')
# Check registered nodes
assert 0 < policy_manager.functions.nodes(node1).call()[PREVIOUS_FEE_PERIOD_FIELD]
assert 0 < policy_manager.functions.nodes(node2).call()[PREVIOUS_FEE_PERIOD_FIELD]
assert 0 < policy_manager.functions.nodes(node3).call()[PREVIOUS_FEE_PERIOD_FIELD]
assert 0 == policy_manager.functions.nodes(bad_node).call()[PREVIOUS_FEE_PERIOD_FIELD]
current_timestamp = testerchain.w3.eth.getBlock('latest').timestamp
end_timestamp = current_timestamp + (number_of_periods - 1) * one_period
policy_id_1 = os.urandom(POLICY_ID_LENGTH)
policy_id_2 = os.urandom(POLICY_ID_LENGTH)
policies = [policy_id_1, policy_id_2]
# Try to create policy for bad (unregistered) node
with pytest.raises((TransactionFailed, ValueError)):
tx = policy_manager.functions.createPolicies(policies, policy_sponsor, end_timestamp, [bad_node])\
.transact({'from': policy_sponsor, 'value': 2 * value})
testerchain.wait_for_receipt(tx)
with pytest.raises((TransactionFailed, ValueError)):
tx = policy_manager.functions.createPolicies(policies, policy_sponsor, end_timestamp, [node1, bad_node])\
.transact({'from': policy_sponsor, 'value': 2 * value})
testerchain.wait_for_receipt(tx)
# Try to create policy with no ETH
with pytest.raises((TransactionFailed, ValueError)):
tx = policy_manager.functions.createPolicies(policies, policy_sponsor, end_timestamp, [node1])\
.transact({'from': policy_sponsor})
testerchain.wait_for_receipt(tx)
# Can't create policy using timestamp from the past
with pytest.raises((TransactionFailed, ValueError)):
tx = policy_manager.functions.createPolicies(policies, policy_sponsor, current_timestamp - 1, [node1])\
.transact({'from': policy_sponsor, 'value': 2 * value})
testerchain.wait_for_receipt(tx)
# Can't create two policies with the same id
with pytest.raises((TransactionFailed, ValueError)):
tx = policy_manager.functions.createPolicies([policy_id_1, policy_id_1], policy_sponsor, end_timestamp, [node1]) \
.transact({'from': policy_sponsor, 'value': 2 * value, 'gas_price': 0})
testerchain.wait_for_receipt(tx)
# Can't use createPolicies() method for only one policy
with pytest.raises((TransactionFailed, ValueError)):
tx = policy_manager.functions.createPolicies([policy_id_1], policy_sponsor, end_timestamp, [node1]) \
.transact({'from': policy_sponsor, 'value': value, 'gas_price': 0})
testerchain.wait_for_receipt(tx)
# Create policy
current_period = escrow.functions.getCurrentPeriod().call()
tx = policy_manager.functions.createPolicies(policies, policy_sponsor, end_timestamp, [node1])\
.transact({'from': policy_sponsor, 'value': 2 * value, 'gas_price': 0})
testerchain.wait_for_receipt(tx)
current_timestamp = testerchain.w3.eth.getBlock('latest').timestamp
# Check balances and policy info
assert 2 * value == testerchain.client.get_balance(policy_manager.address)
assert policy_sponsor_balance - 2 * value == testerchain.client.get_balance(policy_sponsor)
events = policy_created_log.get_all_entries()
assert len(events) == 2
for i, policy_id in enumerate(policies):
policy = policy_manager.functions.policies(policy_id).call()
assert policy_sponsor == policy[SPONSOR_FIELD]
assert NULL_ADDRESS == policy[OWNER_FIELD]
assert rate == policy[RATE_FIELD]
assert current_timestamp == policy[START_TIMESTAMP_FIELD]
assert end_timestamp == policy[END_TIMESTAMP_FIELD]
assert not policy[DISABLED_FIELD]
assert 1 == policy_manager.functions.getArrangementsLength(policy_id).call()
assert node1 == policy_manager.functions.getArrangementInfo(policy_id, 0).call()[0]
assert policy_sponsor == policy_manager.functions.getPolicyOwner(policy_id).call()
assert policy_manager.functions.getNodeFeeDelta(node1, current_period).call() == 2 * rate
assert policy_manager.functions.getNodeFeeDelta(node1, current_period + number_of_periods).call() == -2 * rate
event_args = events[i]['args']
assert policy_id == event_args['policyId']
assert policy_sponsor == event_args['sponsor']
assert policy_sponsor == event_args['owner']
assert rate == event_args['feeRate']
assert current_timestamp == event_args['startTimestamp']
assert end_timestamp == event_args['endTimestamp']
assert 1 == event_args['numberOfNodes']
# Can't create policy with the same id
policy_id_3 = os.urandom(POLICY_ID_LENGTH)
with pytest.raises((TransactionFailed, ValueError)):
tx = policy_manager.functions.createPolicies([policy_id_3, policy_id_1], policy_sponsor, end_timestamp, [node1])\
.transact({'from': policy_sponsor, 'value': 2 * value})
testerchain.wait_for_receipt(tx)
# Revoke policies
tx = policy_manager.functions.revokePolicy(policy_id_1).transact({'from': policy_sponsor, 'gas_price': 0})
testerchain.wait_for_receipt(tx)
tx = policy_manager.functions.revokePolicy(policy_id_2).transact({'from': policy_sponsor, 'gas_price': 0})
testerchain.wait_for_receipt(tx)
assert policy_manager.functions.policies(policy_id_1).call()[DISABLED_FIELD]
assert policy_manager.functions.policies(policy_id_2).call()[DISABLED_FIELD]
# Create new policy
testerchain.time_travel(hours=1)
current_period = escrow.functions.getCurrentPeriod().call()
for period_to_set_default in range(current_period, current_period + number_of_periods + 1):
tx = escrow.functions.ping(node1, 0, 0, period_to_set_default).transact()
testerchain.wait_for_receipt(tx)
tx = escrow.functions.ping(node2, 0, 0, period_to_set_default).transact()
testerchain.wait_for_receipt(tx)
current_timestamp = testerchain.w3.eth.getBlock('latest').timestamp
end_timestamp = current_timestamp + (number_of_periods - 1) * one_period
policy_id_1 = os.urandom(POLICY_ID_LENGTH)
policy_id_2 = os.urandom(POLICY_ID_LENGTH)
policies = [policy_id_1, policy_id_2]
tx = policy_manager.functions.createPolicies(policies, policy_owner, end_timestamp, [node1, node2, node3])\
.transact({'from': policy_sponsor, 'value': 6 * value, 'gas_price': 0})
testerchain.wait_for_receipt(tx)
current_timestamp = testerchain.w3.eth.getBlock('latest').timestamp
assert 6 * value == testerchain.client.get_balance(policy_manager.address)
assert policy_sponsor_balance - 6 * value == testerchain.client.get_balance(policy_sponsor)
events = policy_created_log.get_all_entries()
assert len(events) == 4
for i, policy_id in enumerate(policies):
policy = policy_manager.functions.policies(policy_id).call()
assert policy_sponsor == policy[SPONSOR_FIELD]
assert policy_owner == policy[OWNER_FIELD]
assert rate == policy[RATE_FIELD]
assert current_timestamp == policy[START_TIMESTAMP_FIELD]
assert end_timestamp == policy[END_TIMESTAMP_FIELD]
assert not policy[DISABLED_FIELD]
assert policy_owner == policy_manager.functions.getPolicyOwner(policy_id).call()
assert policy_manager.functions.getNodeFeeDelta(node1, current_period).call() == default_fee_delta
assert policy_manager.functions.getNodeFeeDelta(node1, current_period + number_of_periods).call() == -2 * rate
assert policy_manager.functions.getNodeFeeDelta(node2, current_period).call() == 2 * rate
assert policy_manager.functions.getNodeFeeDelta(node2, current_period + number_of_periods).call() == -2 * rate
assert policy_manager.functions.getNodeFeeDelta(node3, current_period).call() == 2 * rate
assert policy_manager.functions.getNodeFeeDelta(node3, current_period + number_of_periods).call() == -2 * rate
event_args = events[i + 2]['args']
assert policy_id == event_args['policyId']
assert policy_sponsor == event_args['sponsor']
assert policy_owner == event_args['owner']
assert rate == event_args['feeRate']
assert current_timestamp == event_args['startTimestamp']
assert end_timestamp == event_args['endTimestamp']
assert 3 == event_args['numberOfNodes']
# Revoke policies
tx = policy_manager.functions.revokePolicy(policy_id_1).transact({'from': policy_owner, 'gas_price': 0})
testerchain.wait_for_receipt(tx)
tx = policy_manager.functions.revokePolicy(policy_id_2).transact({'from': policy_owner, 'gas_price': 0})
testerchain.wait_for_receipt(tx)
assert policy_manager.functions.policies(policy_id_1).call()[DISABLED_FIELD]
assert policy_manager.functions.policies(policy_id_2).call()[DISABLED_FIELD]
# Can't create policy with wrong ETH value - when fee is not calculated by formula:
# numberOfNodes * feeRate * numberOfPeriods * numberOfPolicies
policy_id_1 = os.urandom(POLICY_ID_LENGTH)
policy_id_2 = os.urandom(POLICY_ID_LENGTH)
policies = [policy_id_1, policy_id_2]
with pytest.raises((TransactionFailed, ValueError)):
tx = policy_manager.functions.createPolicies(policies, policy_sponsor, end_timestamp, [node1])\
.transact({'from': policy_sponsor, 'value': value - 1})
testerchain.wait_for_receipt(tx)
min_rate, default_rate, max_rate = 10, 20, 30
tx = policy_manager.functions.setFeeRateRange(min_rate, default_rate, max_rate).transact({'from': creator})
testerchain.wait_for_receipt(tx)
# Set minimum fee rate for nodes
tx = policy_manager.functions.setMinFeeRate(10).transact({'from': node1})
testerchain.wait_for_receipt(tx)
tx = policy_manager.functions.setMinFeeRate(20).transact({'from': node2})
testerchain.wait_for_receipt(tx)
assert policy_manager.functions.nodes(node1).call()[MIN_FEE_RATE_FIELD] == 10
assert policy_manager.functions.nodes(node2).call()[MIN_FEE_RATE_FIELD] == 20
assert policy_manager.functions.getMinFeeRate(node1).call() == 10
assert policy_manager.functions.getMinFeeRate(node2).call() == 20
# Try to create policy with low rate
current_timestamp = testerchain.w3.eth.getBlock('latest').timestamp
end_timestamp = current_timestamp + 10
with pytest.raises((TransactionFailed, ValueError)):
tx = policy_manager.functions.createPolicies(policies, policy_sponsor, end_timestamp, [node1])\
.transact({'from': policy_sponsor, 'value': 2 * (min_rate - 1)})
testerchain.wait_for_receipt(tx)
with pytest.raises((TransactionFailed, ValueError)):
tx = policy_manager.functions.createPolicies(policies, policy_sponsor, end_timestamp, [node1, node2])\
.transact({'from': policy_sponsor, 'value': 2 * 2 * (min_rate + 1)})
testerchain.wait_for_receipt(tx)
# Create new policy
value = 2 * default_rate * number_of_periods
end_timestamp = current_timestamp + (number_of_periods - 1) * one_period
tx = policy_manager.functions.createPolicies(
policies, NULL_ADDRESS, end_timestamp, [node1, node2]) \
.transact({'from': policy_sponsor, 'value': 2 * value, 'gas_price': 0})
testerchain.wait_for_receipt(tx)
current_timestamp = testerchain.w3.eth.getBlock('latest').timestamp
assert 2 * value == testerchain.client.get_balance(policy_manager.address)
assert policy_sponsor_balance - 2 * value == testerchain.client.get_balance(policy_sponsor)
events = policy_created_log.get_all_entries()
assert len(events) == 6
for i, policy_id in enumerate(policies):
policy = policy_manager.functions.policies(policy_id).call()
assert policy_sponsor == policy[SPONSOR_FIELD]
assert NULL_ADDRESS == policy[OWNER_FIELD]
assert default_rate == policy[RATE_FIELD]
assert current_timestamp == policy[START_TIMESTAMP_FIELD]
assert end_timestamp == policy[END_TIMESTAMP_FIELD]
assert not policy[DISABLED_FIELD]
assert policy_sponsor == policy_manager.functions.getPolicyOwner(policy_id).call()
event_args = events[i + 4]['args']
assert policy_id == event_args['policyId']
assert policy_sponsor == event_args['sponsor']
assert policy_sponsor == event_args['owner']
assert rate == event_args['feeRate']
assert current_timestamp == event_args['startTimestamp']
assert end_timestamp == event_args['endTimestamp']
assert 2 == event_args['numberOfNodes']
def test_upgrading(testerchain, deploy_contract): def test_upgrading(testerchain, deploy_contract):
creator = testerchain.client.accounts[0] creator = testerchain.client.accounts[0]

View File

@ -448,7 +448,26 @@ def estimate_gas(analyzer: AnalyzeGas = None) -> None:
policy_functions.revokePolicy(policy_id_3), policy_functions.revokePolicy(policy_id_3),
{'from': alice2}) {'from': alice2})
for index in range(5): transact(staker_functions.commitToNextPeriod(), {'from': staker1})
transact(staker_functions.commitToNextPeriod(), {'from': staker2})
transact(staker_functions.commitToNextPeriod(), {'from': staker3})
testerchain.time_travel(periods=1)
#
# Batch granting
#
policy_id_1 = os.urandom(int(Policy.POLICY_ID_LENGTH))
policy_id_2 = os.urandom(int(Policy.POLICY_ID_LENGTH))
current_timestamp = testerchain.w3.eth.getBlock('latest').timestamp
end_timestamp = current_timestamp + (number_of_periods - 1) * one_period
value = 3 * number_of_periods * rate
transact_and_log("Creating 2 policies (3 nodes, 100 periods, pre-committed)",
policy_functions.createPolicies([policy_id_1, policy_id_2],
alice1,
end_timestamp,
[staker1, staker2, staker3]),
{'from': alice1, 'value': 2 * value})
for index in range(4):
transact(staker_functions.commitToNextPeriod(), {'from': staker1}) transact(staker_functions.commitToNextPeriod(), {'from': staker1})
testerchain.time_travel(periods=1) testerchain.time_travel(periods=1)
transact(staker_functions.mint(), {'from': staker1}) transact(staker_functions.mint(), {'from': staker1})