diff --git a/.vscode/settings.json b/.vscode/settings.json index c414779..b461971 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -8,7 +8,7 @@ "solidity.remappingsUnix": [ "@openzeppelin/=/home/vscode/.brownie/packages/OpenZeppelin/openzeppelin-contracts@4.7.0", "@chainlink/=/home/vscode/.brownie/packages/smartcontractkit/chainlink@1.6.0", - "@etherisc/gif-interface/=/home/vscode/.brownie/packages/etherisc/gif-interface@ac0714e", + "@etherisc/gif-interface/=/home/vscode/.brownie/packages/etherisc/gif-interface@c958220", ], "peacock.remoteColor": "1D3C43", "workbench.colorCustomizations": { diff --git a/brownie-config.yaml b/brownie-config.yaml index 92b92d1..5736b28 100644 --- a/brownie-config.yaml +++ b/brownie-config.yaml @@ -24,7 +24,7 @@ compiler: remappings: - "@openzeppelin=OpenZeppelin/openzeppelin-contracts@4.7.0" - "@chainlink=smartcontractkit/chainlink@1.6.0" - - "@etherisc/gif-interface=etherisc/gif-interface@ac0714e" + - "@etherisc/gif-interface=etherisc/gif-interface@c958220" # packages below will be added to brownie # you may use 'brownie pm list' after 'brownie compile' @@ -34,7 +34,7 @@ dependencies: # github dependency format: /@ - OpenZeppelin/openzeppelin-contracts@4.7.0 - smartcontractkit/chainlink@1.6.0 - - etherisc/gif-interface@ac0714e + - etherisc/gif-interface@c958220 # exclude open zeppeling contracts when calculating test coverage # https://eth-brownie.readthedocs.io/en/v1.10.3/config.html#exclude_paths @@ -49,6 +49,7 @@ reports: - Context - Ownable - EnumerableMap + - EnumerableSet - ERC1967Proxy - ERC20 - ERC721 diff --git a/contracts/modules/PoolController.sol b/contracts/modules/PoolController.sol index b425270..449f8e6 100644 --- a/contracts/modules/PoolController.sol +++ b/contracts/modules/PoolController.sol @@ -11,11 +11,15 @@ import "@etherisc/gif-interface/contracts/components/IComponent.sol"; import "@etherisc/gif-interface/contracts/components/IRiskpool.sol"; +import "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; + contract PoolController is IPool, CoreController { + using EnumerableSet for EnumerableSet.UintSet; + // used for representation of collateralization // collateralization between 0 and 1 (1=100%) // value might be larger when overcollateralization @@ -34,7 +38,7 @@ contract PoolController is mapping(uint256 /* riskpoolId */ => uint256 /* maxmimumNumberOfActiveBundles */) private _maxmimumNumberOfActiveBundlesForRiskpoolId; - mapping(uint256 /* riskpoolId */ => uint256 /* numberOfActiveBundles */) private _numberOfActiveBundlesForRiskpoolId; + mapping(uint256 /* riskpoolId */ => EnumerableSet.UintSet /* active bundle id set */) private _activeBundleIdsForRiskpoolId; uint256 [] private _riskpoolIds; @@ -300,18 +304,45 @@ contract PoolController is return _riskpoolIdForProductId[productId]; } - function increaseNumberOfActiveBundles(uint256 riskpoolId) external - onlyRiskpoolService + function activeBundles(uint256 riskpoolId) external view returns(uint256 numberOfActiveBundles) { + return EnumerableSet.length(_activeBundleIdsForRiskpoolId[riskpoolId]); + } + + function getActiveBundleId(uint256 riskpoolId, uint256 bundleIdx) external view returns(uint256 bundleId) { + require( + bundleIdx < EnumerableSet.length(_activeBundleIdsForRiskpoolId[riskpoolId]), + "ERROR:POL-041:BUNDLE_IDX_TOO_LARGE" + ); + + return EnumerableSet.at(_activeBundleIdsForRiskpoolId[riskpoolId], bundleIdx); + } + + function addBundleIdToActiveSet(uint256 riskpoolId, uint256 bundleId) + external + onlyRiskpoolService { - require(_numberOfActiveBundlesForRiskpoolId[riskpoolId] < _maxmimumNumberOfActiveBundlesForRiskpoolId[riskpoolId], "ERROR:POL-041:MAXIMUM_NUMBER_OF_ACTIVE_BUNDLES_REACHED"); - _numberOfActiveBundlesForRiskpoolId[riskpoolId]++; + require( + !EnumerableSet.contains(_activeBundleIdsForRiskpoolId[riskpoolId], bundleId), + "ERROR:POL-042:BUNDLE_ID_ALREADY_IN_SET" + ); + require( + EnumerableSet.length(_activeBundleIdsForRiskpoolId[riskpoolId]) < _maxmimumNumberOfActiveBundlesForRiskpoolId[riskpoolId], + "ERROR:POL-043:MAXIMUM_NUMBER_OF_ACTIVE_BUNDLES_REACHED" + ); + + EnumerableSet.add(_activeBundleIdsForRiskpoolId[riskpoolId], bundleId); } - function decreaseNumberOfActiveBundles(uint256 riskpoolId) external - onlyRiskpoolService + function removeBundleIdFromActiveSet(uint256 riskpoolId, uint256 bundleId) + external + onlyRiskpoolService { - require(_numberOfActiveBundlesForRiskpoolId[riskpoolId] > 0, "ERROR:POL-042:NO_ACTIVE_BUNDLES"); - _numberOfActiveBundlesForRiskpoolId[riskpoolId]--; + require( + EnumerableSet.contains(_activeBundleIdsForRiskpoolId[riskpoolId], bundleId), + "ERROR:POL-044:BUNDLE_ID_NOT_IN_SET" + ); + + EnumerableSet.remove(_activeBundleIdsForRiskpoolId[riskpoolId], bundleId); } function getFullCollateralizationLevel() external pure returns (uint256) { diff --git a/contracts/services/InstanceService.sol b/contracts/services/InstanceService.sol index 10f2a1b..f1cee36 100644 --- a/contracts/services/InstanceService.sol +++ b/contracts/services/InstanceService.sol @@ -276,11 +276,17 @@ contract InstanceService is return _pool.getRiskpool(riskpoolId).balance; } + function activeBundles(uint256 riskpoolId) external override view returns(uint256 numberOfActiveBundles) { + return _pool.activeBundles(riskpoolId); + } + + function getActiveBundleId(uint256 riskpoolId, uint256 bundleIdx) external override view returns(uint256 bundleId) { + return _pool.getActiveBundleId(riskpoolId, bundleIdx); + } function getMaximumNumberOfActiveBundles(uint256 riskpoolId) external override view returns(uint256 maximumNumberOfActiveBundles) { return _pool.getMaximumNumberOfActiveBundles(riskpoolId); } - /* bundle */ function getBundleToken() external override view returns(IBundleToken token) { BundleToken bundleToken = _bundle.getToken(); diff --git a/contracts/services/RiskpoolService.sol b/contracts/services/RiskpoolService.sol index fab2a9d..dc42917 100644 --- a/contracts/services/RiskpoolService.sol +++ b/contracts/services/RiskpoolService.sol @@ -10,8 +10,6 @@ import "@etherisc/gif-interface/contracts/components/IComponent.sol"; import "@etherisc/gif-interface/contracts/modules/IBundle.sol"; import "@etherisc/gif-interface/contracts/services/IRiskpoolService.sol"; -// TODO create/lock/unlock notify poolcontroller about the action and poolcontoller updates active bundles - contract RiskpoolService is IRiskpoolService, CoreController @@ -127,8 +125,9 @@ contract RiskpoolService is returns(uint256 bundleId) { uint256 riskpoolId = _component.getComponentId(_msgSender()); - _pool.increaseNumberOfActiveBundles(riskpoolId); bundleId = _bundle.create(owner, riskpoolId, filter, 0); + + _pool.addBundleIdToActiveSet(riskpoolId, bundleId); (uint256 fee, uint256 netCapital) = _treasury.processCapital(bundleId, initialCapital); @@ -182,7 +181,7 @@ contract RiskpoolService is onlyOwningRiskpool(bundleId, true) { uint256 riskpoolId = _component.getComponentId(_msgSender()); - _pool.decreaseNumberOfActiveBundles(riskpoolId); + _pool.removeBundleIdFromActiveSet(riskpoolId, bundleId); _bundle.lock(bundleId); } @@ -192,7 +191,7 @@ contract RiskpoolService is onlyOwningRiskpool(bundleId, true) { uint256 riskpoolId = _component.getComponentId(_msgSender()); - _pool.increaseNumberOfActiveBundles(riskpoolId); + _pool.addBundleIdToActiveSet(riskpoolId, bundleId); _bundle.unlock(bundleId); } @@ -202,10 +201,11 @@ contract RiskpoolService is onlyOwningRiskpool(bundleId, true) { uint256 riskpoolId = _component.getComponentId(_msgSender()); - // only decrease active bundles when riskpool is active - locked riskpool is not counted towards active bundles - if (_component.getComponentState(riskpoolId) == IComponent.ComponentState.Active) { - _pool.decreaseNumberOfActiveBundles(riskpoolId); + + if (_bundle.getState(bundleId) == IBundle.BundleState.Active) { + _pool.removeBundleIdFromActiveSet(riskpoolId, bundleId); } + _bundle.close(bundleId); } diff --git a/contracts/test/TestSet.sol b/contracts/test/TestSet.sol deleted file mode 100644 index f97da39..0000000 --- a/contracts/test/TestSet.sol +++ /dev/null @@ -1,28 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -pragma solidity ^0.8.0; - -import "@etherisc/gif-interface/contracts/components/IdSet.sol"; - - -contract TestSet is IdSet { - - function add(uint256 id) public { - _addIdToSet(id); - } - - function remove(uint256 id) public { - _removeIdfromSet(id); - } - - function contains(uint256 id) public view returns(bool) { - return _containsIdInSet(id); - } - - function size() public view returns(uint256) { - return _idSetSize(); - } - - function intAt(uint256 idx) public view returns(uint256 id) { - return _idInSetAt(idx); - } -} \ No newline at end of file diff --git a/tests/test_basicriskpool_bundle_allocation_01.py b/tests/test_basicriskpool_bundle_allocation_01.py index 54bba09..a6d0531 100644 --- a/tests/test_basicriskpool_bundle_allocation_01.py +++ b/tests/test_basicriskpool_bundle_allocation_01.py @@ -157,3 +157,58 @@ def test_bundle_allocation_with_three_equal_bundles( ) = bundle assert 3000 == lockedCapital + + +def test_failing_bundle_allocation( + instance: GifInstance, + testCoin, + gifTestProduct: GifTestProduct, + riskpoolKeeper: Account, + owner: Account, + customer: Account, + feeOwner: Account, + capitalOwner: Account +): + num_bundles = 2 + product = gifTestProduct.getContract() + riskpool = gifTestProduct.getRiskpool().getContract() + riskpool.setMaximumNumberOfActiveBundles(num_bundles, {'from': riskpoolKeeper}) + instanceService = instance.getInstanceService() + + initialFunding = 1000 + + # fund the riskpools + for _ in range(num_bundles): + fund_riskpool(instance, owner, capitalOwner, riskpool, riskpoolKeeper, testCoin, initialFunding) + + # create minimal policy application + premium = 100 + sumInsured = 1200 + metaData = bytes(0) + applicationData = bytes(0) + + testCoin.transfer(customer, premium, {'from': owner}) + testCoin.approve(instance.getTreasury(), premium, {'from': customer}) + + tx = product.applyForPolicy( + premium, + sumInsured, + metaData, + applicationData, + {'from': customer}) + + processId = tx.return_value + + print('processId {}'.format(processId)) + print(tx.info()) + + assert 'LogRiskpoolCollateralizationFailed' in tx.events + assert len(tx.events['LogRiskpoolCollateralizationFailed']) == 1 + + failedEvent = tx.events['LogRiskpoolCollateralizationFailed'][0] + assert failedEvent['processId'] == processId + assert failedEvent['amount'] == sumInsured + + application = instanceService.getApplication(processId) + applicationState = application[0] + assert applicationState == 0 # enum ApplicationState {Applied, Revoked, Underwritten, Declined} diff --git a/tests/test_bundle_create_use_burn.py b/tests/test_bundle_create_use_burn.py index 025fbea..447b5a8 100644 --- a/tests/test_bundle_create_use_burn.py +++ b/tests/test_bundle_create_use_burn.py @@ -308,10 +308,10 @@ def test_close_and_burn_bundle( assert testCoin.balanceOf(bundleOwner) == bundleOwnerBefore + netWithdrawalAmount # check that close results in blocking all other actions on the bundle - with brownie.reverts('ERROR:POL-042:NO_ACTIVE_BUNDLES'): + with brownie.reverts('ERROR:BUC-052:CLOSED_INVALID_TRANSITION'): riskpool.closeBundle(bundleId, {'from': bundleOwner}) - with brownie.reverts('ERROR:POL-042:NO_ACTIVE_BUNDLES'): + with brownie.reverts('ERROR:POL-044:BUNDLE_ID_NOT_IN_SET'): riskpool.lockBundle(bundleId, {'from': bundleOwner}) with brownie.reverts('ERROR:BUC-052:CLOSED_INVALID_TRANSITION'): diff --git a/tests/test_riskpool_active_bundles.py b/tests/test_riskpool_active_bundles.py index 6fe9563..dd4cc2f 100644 --- a/tests/test_riskpool_active_bundles.py +++ b/tests/test_riskpool_active_bundles.py @@ -41,11 +41,14 @@ def test_create_bundle_max_active( testCoin.transfer(riskpoolKeeper, 10 * initialFunding, {'from': owner}) testCoin.approve(instance.getTreasury(), 10 * initialFunding, {'from': riskpoolKeeper}) - riskpool.bundles() == 1 + assert riskpool.bundles() == 1 + assert riskpool.activeBundles() == 1 + assert instanceService.activeBundles(riskpoolId) == 1 + bundle1 = riskpool.getBundle(0) # ensure creation of another bundle is not allowed (max active bundles is 1 by default) - with brownie.reverts("ERROR:POL-041:MAXIMUM_NUMBER_OF_ACTIVE_BUNDLES_REACHED"): + with brownie.reverts("ERROR:POL-043:MAXIMUM_NUMBER_OF_ACTIVE_BUNDLES_REACHED"): riskpool.createBundle( bytes(0), initialFunding, @@ -53,11 +56,19 @@ def test_create_bundle_max_active( riskpool.closeBundle(bundle1[0], {'from': riskpoolKeeper}) + assert riskpool.bundles() == 1 + assert riskpool.activeBundles() == 0 + assert instanceService.activeBundles(riskpoolId) == 0 + riskpool.createBundle( bytes(0), initialFunding, {'from': riskpoolKeeper}) + assert riskpool.bundles() == 2 + assert riskpool.activeBundles() == 1 + assert instanceService.activeBundles(riskpoolId) == 1 + # ensure a seconds bundle can be added when setting max active bundles to 2 riskpool.setMaximumNumberOfActiveBundles(2, {'from': riskpoolKeeper}) riskpool.createBundle( @@ -65,11 +76,14 @@ def test_create_bundle_max_active( initialFunding, {'from': riskpoolKeeper}) - riskpool.bundles() == 2 + assert riskpool.bundles() == 3 + assert riskpool.activeBundles() == 2 + assert instanceService.activeBundles(riskpoolId) == 2 + bundle2 = riskpool.getBundle(1) - bundle3 = riskpool.getBundle(1) + bundle3 = riskpool.getBundle(2) - with brownie.reverts("ERROR:POL-041:MAXIMUM_NUMBER_OF_ACTIVE_BUNDLES_REACHED"): + with brownie.reverts("ERROR:POL-043:MAXIMUM_NUMBER_OF_ACTIVE_BUNDLES_REACHED"): riskpool.createBundle( bytes(0), initialFunding, @@ -78,16 +92,28 @@ def test_create_bundle_max_active( # ensure another bundle can be created only after locking one bundle riskpool.lockBundle(bundle2[0], {'from': riskpoolKeeper}) + assert riskpool.bundles() == 3 + assert riskpool.activeBundles() == 1 + assert instanceService.activeBundles(riskpoolId) == 1 + riskpool.createBundle( bytes(0), initialFunding, {'from': riskpoolKeeper}) + assert riskpool.bundles() == 4 + assert riskpool.activeBundles() == 2 + assert instanceService.activeBundles(riskpoolId) == 2 + # ensure locked bundle cannot be unlocked while max active bundles are in use - with brownie.reverts("ERROR:POL-041:MAXIMUM_NUMBER_OF_ACTIVE_BUNDLES_REACHED"): + with brownie.reverts("ERROR:POL-043:MAXIMUM_NUMBER_OF_ACTIVE_BUNDLES_REACHED"): riskpool.unlockBundle(bundle2[0], {'from': riskpoolKeeper}) - riskpool.closeBundle(bundle2[0], {'from': riskpoolKeeper}) + riskpool.closeBundle(bundle3[0], {'from': riskpoolKeeper}) + + assert riskpool.bundles() == 4 + assert riskpool.activeBundles() == 1 + assert instanceService.activeBundles(riskpoolId) == 1 # ensure bundles can be created after closing one more bundle riskpool.createBundle( @@ -95,3 +121,7 @@ def test_create_bundle_max_active( initialFunding, {'from': riskpoolKeeper}) + assert riskpool.bundles() == 5 + assert riskpool.activeBundles() == 2 + assert instanceService.activeBundles(riskpoolId) == 2 + diff --git a/tests/test_testset.py b/tests/test_testset.py deleted file mode 100644 index 07c8514..0000000 --- a/tests/test_testset.py +++ /dev/null @@ -1,170 +0,0 @@ -import binascii -import brownie -import pytest - -from brownie import TestSet - -# enforce function isolation for tests below -@pytest.fixture(autouse=True) -def isolation(fn_isolation): - pass - -def test_empty_set(owner): - intSet = _deploySet(owner); - - assert intSet.size() == 0 - assert intSet.contains(42) == False - - -def test_add_elements(owner): - intSet = _deploySet(owner); - - intSet.add(2) - intSet.add(3) - intSet.add(5) - intSet.add(42) - - assert intSet.size() == 4 - assert intSet.contains(1) == False - assert intSet.contains(2) == True - assert intSet.contains(3) == True - assert intSet.contains(4) == False - assert intSet.contains(5) == True - assert intSet.contains(42) == True - - -def test_remove_1st_element(owner): - intSet = _deploySet(owner); - - intSet.add(1) - intSet.add(2) - intSet.add(3) - intSet.add(4) - intSet.add(5) - - assert intSet.size() == 5 - assert intSet.contains(1) == True - assert intSet.contains(2) == True - assert intSet.contains(3) == True - assert intSet.contains(4) == True - assert intSet.contains(5) == True - - intSet.remove(1) - - assert intSet.size() == 4 - assert intSet.contains(1) == False - assert intSet.contains(2) == True - assert intSet.contains(3) == True - assert intSet.contains(4) == True - assert intSet.contains(5) == True - - -def test_remove_middle_element(owner): - intSet = _deploySet(owner); - - intSet.add(1) - intSet.add(2) - intSet.add(3) - intSet.add(4) - intSet.add(5) - - assert intSet.size() == 5 - assert intSet.contains(1) == True - assert intSet.contains(2) == True - assert intSet.contains(3) == True - assert intSet.contains(4) == True - assert intSet.contains(5) == True - - intSet.remove(3) - - assert intSet.size() == 4 - assert intSet.contains(1) == True - assert intSet.contains(2) == True - assert intSet.contains(3) == False - assert intSet.contains(4) == True - assert intSet.contains(5) == True - - -def test_remove_last_element(owner): - intSet = _deploySet(owner); - - intSet.add(1) - intSet.add(2) - intSet.add(3) - intSet.add(4) - intSet.add(5) - - assert intSet.size() == 5 - assert intSet.contains(1) == True - assert intSet.contains(2) == True - assert intSet.contains(3) == True - assert intSet.contains(4) == True - assert intSet.contains(5) == True - - intSet.remove(5) - - assert intSet.size() == 4 - assert intSet.contains(1) == True - assert intSet.contains(2) == True - assert intSet.contains(3) == True - assert intSet.contains(4) == True - assert intSet.contains(5) == False - - -def test_add_remove_elements(owner): - intSet = _deploySet(owner); - - intSet.add(1) - intSet.add(2) - intSet.add(3) - - assert intSet.size() == 3 - assert intSet.contains(1) == True - assert intSet.contains(2) == True - assert intSet.contains(3) == True - - # adding element already in set should not change anything - intSet.add(1) - assert intSet.size() == 3 - assert intSet.contains(1) == True - assert intSet.contains(2) == True - assert intSet.contains(3) == True - - # remove 1st elemnt - intSet.remove(1) - assert intSet.size() == 2 - assert intSet.contains(1) == False - assert intSet.contains(2) == True - assert intSet.contains(3) == True - - # removing an element not in the set should not change anything - intSet.remove(1) - assert intSet.size() == 2 - assert intSet.contains(1) == False - assert intSet.contains(2) == True - assert intSet.contains(3) == True - - # readding the removed element should product the initial situation - intSet.add(1) - assert intSet.size() == 3 - assert intSet.contains(1) == True - assert intSet.contains(2) == True - assert intSet.contains(3) == True - - -def test_int_at(owner): - intSet = _deploySet(owner); - - for i in range(5): - intSet.add(i) - - for i in range(intSet.size()): - assert intSet.contains(i) - assert i == intSet.intAt(i) - - with brownie.reverts("ERROR:SET-001:INDEX_TOO_LARGE"): - assert 42 == intSet.intAt(5) - - -def _deploySet(owner) -> TestSet: - return TestSet.deploy({'from': owner}) \ No newline at end of file