Skip to content

Commit d698b26

Browse files
authored
Merge pull request #76 from etherisc/feature/pool-controller-move-to-checks-effects-interactions
move to checks-effects-interaction for payouts
2 parents 007ead3 + 096ab60 commit d698b26

6 files changed

Lines changed: 230 additions & 28 deletions

File tree

contracts/flows/PolicyDefaultFlow.sol

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import "../modules/TreasuryModule.sol";
99
import "../shared/WithRegistry.sol";
1010

1111
import "@etherisc/gif-interface/contracts/modules/IPolicy.sol";
12-
// import "@etherisc/gif-interface/contracts/modules/IQuery.sol";
1312
import "@etherisc/gif-interface/contracts/modules/IRegistry.sol";
1413
import "@etherisc/gif-interface/contracts/modules/IPool.sol";
1514

@@ -264,15 +263,17 @@ contract PolicyDefaultFlow is
264263
uint256 netPayoutAmount
265264
)
266265
{
267-
TreasuryModule treasury = getTreasuryContract();
268-
(feeAmount, netPayoutAmount) = treasury.processPayout(processId, payoutId);
266+
PoolController pool = getPoolContract();
267+
PolicyController policy = getPolicyContract();
268+
IPolicy.Payout memory payout = policy.getPayout(processId, payoutId);
269269

270-
// if payout successful: update book keeping of policy and riskpool
271-
IPolicy policy = getPolicyContract();
270+
// book keeping of riskpool and policy
271+
pool.processPayout(processId, payout.amount);
272272
policy.processPayout(processId, payoutId);
273273

274-
PoolController pool = getPoolContract();
275-
pool.processPayout(processId, netPayoutAmount + feeAmount);
274+
// move funds
275+
TreasuryModule treasury = getTreasuryContract();
276+
(feeAmount, netPayoutAmount) = treasury.processPayout(processId, payoutId);
276277
}
277278

278279
function request(

contracts/modules/PoolController.sol

Lines changed: 23 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -62,10 +62,20 @@ contract PoolController is
6262
_;
6363
}
6464

65-
modifier onlyTreasury() {
65+
modifier onlyActivePool(uint256 riskpoolId) {
6666
require(
67-
_msgSender() == _getContractAddress("Treasury"),
68-
"ERROR:POL-003:NOT_TREASURY"
67+
_component.getComponentState(riskpoolId) == IComponent.ComponentState.Active,
68+
"ERROR:POL-003:RISKPOOL_NOT_ACTIVE"
69+
);
70+
_;
71+
}
72+
73+
modifier onlyActivePoolForProcess(bytes32 processId) {
74+
IPolicy.Metadata memory metadata = _policy.getMetadata(processId);
75+
uint256 riskpoolId = _riskpoolIdForProductId[metadata.productId];
76+
require(
77+
_component.getComponentState(riskpoolId) == IComponent.ComponentState.Active,
78+
"ERROR:POL-004:RISKPOOL_NOT_ACTIVE"
6979
);
7080
_;
7181
}
@@ -91,12 +101,12 @@ contract PoolController is
91101
_riskpoolIds.push(riskpoolId);
92102
_maxmimumNumberOfActiveBundlesForRiskpoolId[riskpoolId] = DEFAULT_MAX_NUMBER_OF_ACTIVE_BUNDLES;
93103

94-
require(pool.createdAt == 0, "ERROR:POL-004:RISKPOOL_ALREADY_REGISTERED");
104+
require(pool.createdAt == 0, "ERROR:POL-005:RISKPOOL_ALREADY_REGISTERED");
95105

96-
require(wallet != address(0), "ERROR:POL-005:WALLET_ADDRESS_ZERO");
97-
require(erc20Token != address(0), "ERROR:POL-006:ERC20_ADDRESS_ZERO");
98-
require(collateralizationLevel <= COLLATERALIZATION_LEVEL_CAP, "ERROR:POL-007:COLLATERALIZATION_lEVEl_TOO_HIGH");
99-
require(sumOfSumInsuredCap > 0, "ERROR:POL-008:SUM_OF_SUM_INSURED_CAP_ZERO");
106+
require(wallet != address(0), "ERROR:POL-006:WALLET_ADDRESS_ZERO");
107+
require(erc20Token != address(0), "ERROR:POL-007:ERC20_ADDRESS_ZERO");
108+
require(collateralizationLevel <= COLLATERALIZATION_LEVEL_CAP, "ERROR:POL-008:COLLATERALIZATION_lEVEl_TOO_HIGH");
109+
require(sumOfSumInsuredCap > 0, "ERROR:POL-009:SUM_OF_SUM_INSURED_CAP_ZERO");
100110

101111
pool.id = riskpoolId;
102112
pool.wallet = wallet;
@@ -129,6 +139,7 @@ contract PoolController is
129139
function fund(uint256 riskpoolId, uint256 amount)
130140
external
131141
onlyRiskpoolService
142+
onlyActivePool(riskpoolId)
132143
{
133144
IPool.Pool storage pool = _riskpools[riskpoolId];
134145
pool.capital += amount;
@@ -139,6 +150,7 @@ contract PoolController is
139150
function defund(uint256 riskpoolId, uint256 amount)
140151
external
141152
onlyRiskpoolService
153+
onlyActivePool(riskpoolId)
142154
{
143155
IPool.Pool storage pool = _riskpools[riskpoolId];
144156

@@ -152,6 +164,7 @@ contract PoolController is
152164
function underwrite(bytes32 processId)
153165
external override
154166
onlyPolicyFlow("Pool")
167+
onlyActivePoolForProcess(processId)
155168
returns(bool success)
156169
{
157170
// check that application is in applied state
@@ -164,10 +177,6 @@ contract PoolController is
164177
// determine riskpool responsible for application
165178
IPolicy.Metadata memory metadata = _policy.getMetadata(processId);
166179
uint256 riskpoolId = _riskpoolIdForProductId[metadata.productId];
167-
require(
168-
_component.getComponentState(riskpoolId) == IComponent.ComponentState.Active,
169-
"ERROR:POL-021:RISKPOOL_NOT_ACTIVE"
170-
);
171180

172181
// calculate required collateral amount
173182
uint256 sumInsuredAmount = application.sumInsuredAmount;
@@ -223,6 +232,7 @@ contract PoolController is
223232
function processPremium(bytes32 processId, uint256 amount)
224233
external override
225234
onlyPolicyFlow("Pool")
235+
onlyActivePoolForProcess(processId)
226236
{
227237
IPolicy.Metadata memory metadata = _policy.getMetadata(processId);
228238
IRiskpool riskpool = _getRiskpoolComponent(metadata);
@@ -238,6 +248,7 @@ contract PoolController is
238248
function processPayout(bytes32 processId, uint256 amount)
239249
external override
240250
onlyPolicyFlow("Pool")
251+
onlyActivePoolForProcess(processId)
241252
{
242253
IPolicy.Metadata memory metadata = _policy.getMetadata(processId);
243254
uint256 riskpoolId = _riskpoolIdForProductId[metadata.productId];

contracts/modules/TreasuryModule.sol

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -315,16 +315,11 @@ contract TreasuryModule is
315315
uint256 netPayoutAmount
316316
)
317317
{
318-
IPolicy.Payout memory payout = _policy.getPayout(processId, payoutId);
319-
require(
320-
payout.state == IPolicy.PayoutState.Expected,
321-
"ERROR:TRS-040:PAYOUT_ALREADY_PROCESSED"
322-
);
323-
324318
IPolicy.Metadata memory metadata = _policy.getMetadata(processId);
325319
IERC20 token = getComponentToken(metadata.productId);
326320
(uint256 riskpoolId, address riskpoolWalletAddress) = _getRiskpoolWallet(processId);
327321

322+
IPolicy.Payout memory payout = _policy.getPayout(processId, payoutId);
328323
require(
329324
token.balanceOf(riskpoolWalletAddress) >= payout.amount,
330325
"ERROR:TRS-042:RISKPOOL_WALLET_BALANCE_TOO_SMALL"

tests/test_apply_underwrite_expire.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -492,7 +492,7 @@ def test_riskpool_inactive(
492492
applicationData = s2b32('application')
493493

494494
# check that inactive product does not lead to policy creation
495-
with brownie.reverts('ERROR:POL-021:RISKPOOL_NOT_ACTIVE'):
495+
with brownie.reverts('ERROR:POL-004:RISKPOOL_NOT_ACTIVE'):
496496
apply_tx = product.applyForPolicy(
497497
premium,
498498
sumInsured,

tests/test_riskpool_lifecycle.py

Lines changed: 192 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
from scripts.setup import (
1818
fund_riskpool,
19+
fund_customer,
1920
apply_for_policy,
2021
)
2122

@@ -131,7 +132,7 @@ def test_pause_unpause(
131132
riskpool.burnBundle(bundleId, {'from':bundleOwner})
132133

133134
# ensure underwriting new policies is not possible for paused riskpool
134-
with brownie.reverts("ERROR:POL-021:RISKPOOL_NOT_ACTIVE"):
135+
with brownie.reverts("ERROR:POL-004:RISKPOOL_NOT_ACTIVE"):
135136
policyId2 = apply_for_policy(instance, owner, product, customer, testCoin, 100, 1000)
136137

137138
# ensure existing policies may be closed while riskpool is paused
@@ -290,6 +291,7 @@ def test_suspend_resume(
290291

291292
assert bundle2Id == bundleId + 1
292293

294+
293295
def test_suspend_archive(
294296
instance: GifInstance,
295297
testCoin,
@@ -688,4 +690,192 @@ def test_propose_decline(
688690
def _getBundle(instance, riskpool, bundleIdx):
689691
instanceService = instance.getInstanceService()
690692
bundleId = riskpool.getBundleId(bundleIdx)
691-
return instanceService.getBundle(bundleId)
693+
return instanceService.getBundle(bundleId)
694+
695+
def test_policy_application_with_inactive_riskpool(
696+
instance: GifInstance,
697+
testCoin,
698+
gifTestProduct: GifTestProduct,
699+
instanceOperator: Account,
700+
productOwner: Account,
701+
riskpoolKeeper: Account,
702+
customer: Account,
703+
capitalOwner: Account
704+
):
705+
# prepare funded riskpool
706+
riskpool = gifTestProduct.getRiskpool().getContract()
707+
riskpoolId = riskpool.getId()
708+
initialFunding = 10000
709+
fund_riskpool(instance, instanceOperator, capitalOwner, riskpool, riskpoolKeeper, testCoin, initialFunding)
710+
711+
# pause riskpool
712+
componentOwnerService = instance.getComponentOwnerService()
713+
componentOwnerService.pause(riskpoolId, {'from': riskpoolKeeper})
714+
assert riskpool.getState() == 4
715+
716+
# attempt to create policy
717+
product = gifTestProduct.getContract()
718+
premium = 300
719+
sumInsured = 2000
720+
721+
with brownie.reverts('ERROR:POL-004:RISKPOOL_NOT_ACTIVE'):
722+
product.applyForPolicy(premium, sumInsured, bytes(0), bytes(0), {'from': customer})
723+
724+
assert product.policies() == 0
725+
726+
# unpuase riskpool again
727+
componentOwnerService.unpause(riskpoolId, {'from': riskpoolKeeper})
728+
assert riskpool.getState() == 3
729+
730+
# verify that policy creation works
731+
tx = product.applyForPolicy(premium, sumInsured, bytes(0), bytes(0), {'from': customer})
732+
processId = tx.return_value
733+
734+
assert product.policies() == 1
735+
736+
737+
def test_collect_premium_with_inactive_riskpool(
738+
instance: GifInstance,
739+
testCoin,
740+
gifTestProduct: GifTestProduct,
741+
instanceOperator: Account,
742+
productOwner: Account,
743+
riskpoolKeeper: Account,
744+
customer: Account,
745+
capitalOwner: Account
746+
):
747+
# prepare funded riskpool
748+
riskpool = gifTestProduct.getRiskpool().getContract()
749+
riskpoolId = riskpool.getId()
750+
initialFunding = 10000
751+
fund_riskpool(instance, instanceOperator, capitalOwner, riskpool, riskpoolKeeper, testCoin, initialFunding)
752+
753+
# create policy
754+
product = gifTestProduct.getContract()
755+
premium = 300
756+
sumInsured = 2000
757+
tx = product.applyForPolicy(premium, sumInsured, bytes(0), bytes(0), {'from': customer})
758+
processId = tx.return_value
759+
760+
# ComponentState {Created,Proposed,Declined,Active,Paused,Suspended}
761+
assert product.policies() == 1
762+
assert riskpool.getState() == 3
763+
764+
# pause riskpool
765+
componentOwnerService = instance.getComponentOwnerService()
766+
componentOwnerService.pause(riskpoolId, {'from': riskpoolKeeper})
767+
768+
assert riskpool.getState() == 4
769+
# fund customer to pay premium now
770+
fund_customer(instance, instanceOperator, customer, testCoin, premium)
771+
assert testCoin.balanceOf(customer) == premium
772+
assert testCoin.allowance(customer, instance.getTreasury()) == premium
773+
774+
# attempt to collect premium with paused riskpool
775+
with brownie.reverts('ERROR:POL-004:RISKPOOL_NOT_ACTIVE'):
776+
product.collectPremium(processId, premium, {'from': productOwner})
777+
778+
assert testCoin.balanceOf(customer) == premium
779+
780+
# unpuase riskpool
781+
componentOwnerService.unpause(riskpoolId, {'from': riskpoolKeeper})
782+
assert riskpool.getState() == 3
783+
784+
# ensure that collecting premiums works again
785+
tx = product.collectPremium(processId, premium, {'from': productOwner})
786+
(success, fee, netPremium) = tx.return_value
787+
788+
assert success
789+
assert testCoin.balanceOf(customer) == 0
790+
791+
792+
793+
794+
def test_payout_processing_with_inactive_riskpool(
795+
instance: GifInstance,
796+
testCoin,
797+
gifTestProduct: GifTestProduct,
798+
instanceOperator: Account,
799+
productOwner: Account,
800+
riskpoolKeeper: Account,
801+
customer: Account,
802+
capitalOwner: Account
803+
):
804+
instanceService = instance.getInstanceService()
805+
806+
# prepare funded riskpool
807+
riskpool = gifTestProduct.getRiskpool().getContract()
808+
riskpoolId = riskpool.getId()
809+
initialFunding = 10000
810+
fund_riskpool(instance, instanceOperator, capitalOwner, riskpool, riskpoolKeeper, testCoin, initialFunding)
811+
812+
# fund customer to pay premium now
813+
premium = 300
814+
fund_customer(instance, instanceOperator, customer, testCoin, premium)
815+
assert testCoin.balanceOf(customer) == premium
816+
assert testCoin.allowance(customer, instance.getTreasury()) == premium
817+
818+
# create policy
819+
product = gifTestProduct.getContract()
820+
sumInsured = 2000
821+
tx = product.applyForPolicy(premium, sumInsured, bytes(0), bytes(0), {'from': customer})
822+
processId = tx.return_value
823+
824+
# ComponentState {Created,Proposed,Declined,Active,Paused,Suspended}
825+
assert instanceService.processIds() == 1
826+
assert instanceService.claims(processId) == 0
827+
assert product.policies() == 1
828+
assert riskpool.getState() == 3
829+
830+
# pause riskpool
831+
componentOwnerService = instance.getComponentOwnerService()
832+
componentOwnerService.pause(riskpoolId, {'from': riskpoolKeeper})
833+
834+
assert riskpool.getState() == 4
835+
836+
claimAmount = 3 * premium
837+
(claimId, payoutId) = create_claim_no_oracle(product, customer, productOwner, processId, claimAmount)
838+
839+
assert instanceService.claims(processId) == 1
840+
assert instanceService.payouts(processId) == 1
841+
assert testCoin.balanceOf(customer) == 0
842+
843+
# attempt to process payout
844+
with brownie.reverts('ERROR:POL-004:RISKPOOL_NOT_ACTIVE'):
845+
product.processPayout(processId, payoutId, {'from':productOwner})
846+
847+
assert instanceService.claims(processId) == 1
848+
assert instanceService.payouts(processId) == 1
849+
assert testCoin.balanceOf(customer) == 0
850+
851+
# unpause riskpool
852+
componentOwnerService = instance.getComponentOwnerService()
853+
componentOwnerService.unpause(riskpoolId, {'from': riskpoolKeeper})
854+
855+
assert riskpool.getState() == 3
856+
857+
# enwure process payout works again
858+
product.processPayout(processId, payoutId, {'from':productOwner})
859+
860+
assert instanceService.claims(processId) == 1
861+
assert instanceService.payouts(processId) == 1
862+
assert testCoin.balanceOf(customer) == claimAmount
863+
864+
# ensure it's not possible to process same payout a 2nd time
865+
with brownie.reverts('ERROR:POC-091:POLICY_WITHOUT_OPEN_CLAIMS'):
866+
product.processPayout(processId, payoutId, {'from':productOwner})
867+
868+
assert instanceService.claims(processId) == 1
869+
assert instanceService.payouts(processId) == 1
870+
assert testCoin.balanceOf(customer) == claimAmount
871+
872+
873+
def create_claim_no_oracle(product, customer, productOwner, processId, claimAmount):
874+
tx = product.submitClaimNoOracle(processId, claimAmount, {'from':customer})
875+
claimId = tx.return_value
876+
product.confirmClaim(processId, claimId, claimAmount, {'from':productOwner})
877+
878+
tx = product.newPayout(processId, claimId, claimAmount, {'from':productOwner})
879+
payoutId = tx.return_value
880+
881+
return (claimId, payoutId)

tests/test_treasury_suspend_resume.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,11 @@ def test_process_payout(
218218
# resume treasury
219219
instanceOperatorService.resumeTreasury(ioDict)
220220

221+
# check that no funds have moved
222+
assert customerBalanceBeforePayout == erc20Token.balanceOf(customer)
223+
assert riskpoolBalanceBeforePayout == erc20Token.balanceOf(riskpoolWallet)
224+
assert riskpool.getBalance() == erc20Token.balanceOf(riskpoolWallet)
225+
221226
# trigger payout
222227
product.createPayout(policyId, claimId, claimAmount, {'from': productOwner})
223228

0 commit comments

Comments
 (0)