diff --git a/contracts/DepegDistribution.sol b/contracts/DepegDistribution.sol new file mode 100644 index 0000000..52ee00c --- /dev/null +++ b/contracts/DepegDistribution.sol @@ -0,0 +1,273 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity 0.8.2; + +import "@etherisc/gif-interface/contracts/modules/IRegistry.sol"; +import "@etherisc/gif-interface/contracts/services/IInstanceService.sol"; + +import {IERC20Metadata} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; +import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; +import {DepegProduct} from "./DepegProduct.sol"; +import {DepegRiskpool} from "./DepegRiskpool.sol"; + +contract DepegDistribution is + Ownable +{ + struct DistributorInfo { + uint256 commissionRate; + uint256 commissionBalance; + uint256 policiesSold; + uint256 createdAt; + uint256 updatedAt; + } + + event LogDepegPolicySold(address distributor, bytes32 processId, uint256 premiumTotalAmount, address protectedWallet, uint256 protectedBalance); + event LogDistributionInfoUpdated(address distributor, uint256 commissionAmount, uint256 commissionBalance, uint256 totalPoliciesSold); + + uint8 public constant DECIMALS = 18; + uint256 public constant COMMISSION_RATE_DEFAULT = 5 * 10 ** (DECIMALS - 2); + uint256 public constant COMMISSION_RATE_MAX = 33 * 10 ** (DECIMALS - 2); + + DepegProduct private _depegProduct; + DepegRiskpool private _depegRiskpool; + IERC20Metadata private _token; + address private _treasury; + + mapping(address => DistributorInfo) private _distributor; + address [] private _distributors; + + + modifier onlyDistributor() { + require( + isDistributor(msg.sender), + "ERROR:DST-001:NOT_DISTRIBUTOR" + ); + _; + } + + constructor( + address depegProduct, + uint256 productId + ) + Ownable() + { + _depegProduct = DepegProduct(depegProduct); + require(_depegProduct.getId() == productId, "ERROR:DST-010:PRODUCT_ID_MISMATCH"); + + IRegistry registry = IRegistry(_depegProduct.getRegistry()); + IInstanceService instanceService = IInstanceService(registry.getContract("InstanceService")); + + _depegRiskpool = DepegRiskpool( + address(instanceService.getComponent( + _depegProduct.getRiskpoolId()))); + + _token = IERC20Metadata(_depegProduct.getToken()); + _treasury = instanceService.getTreasuryAddress(); + } + + function createDistributor(address distributor) + external + onlyOwner() + returns (DistributorInfo memory) + { + require(!isDistributor(distributor), "ERROR:DST-020:DISTRIBUTOR_ALREADY_EXISTS"); + + _distributor[distributor] = DistributorInfo( + COMMISSION_RATE_DEFAULT, + 0, // commissionAmount, + 0, // policiesSold + block.timestamp, // createdAt + block.timestamp // updatedAt + ); + + _distributors.push(distributor); + + return _distributor[distributor]; + } + + function setCommissionRate( + address distributor, + uint256 commissionRate + ) + external + onlyOwner() + { + require(isDistributor(distributor), "ERROR:DST-030:NOT_DISTRIBUTOR"); + require(commissionRate <= COMMISSION_RATE_MAX, "ERROR:DST-031:COMMISSION_RATE_TOO_HIGH"); + + DistributorInfo storage info = _distributor[distributor]; + info.commissionRate = commissionRate; + info.updatedAt = block.timestamp; + } + + + /// @dev lets a distributor create a policy for the specified wallet address + // the policy holder is this contract, the beneficiary is the specified wallet address + function createPolicy( + address buyer, + address protectedWallet, + uint256 protectedBalance, + uint256 duration, + uint256 bundleId + ) + external + onlyDistributor() + returns(bytes32 processId) + { + // collect premium and commission from buyer to this contract + ( + uint256 premiumTotalAmount, + uint256 premiumNetAmount, + ) = _collectTokenAndUpdateCommission( + buyer, + protectedBalance, + duration, + bundleId); + + // create allowance for net premium + _token.approve(_treasury, premiumNetAmount); + + // create policy + // this will transfer premium amount from this contract to depeg (and keep the commission in this contract) + processId = _depegProduct.applyForPolicyWithBundle( + protectedWallet, + protectedBalance, + duration, + bundleId); + + emit LogDepegPolicySold(msg.sender, processId, premiumTotalAmount, protectedWallet, protectedBalance); + } + + function _collectTokenAndUpdateCommission( + address buyer, + uint256 protectedBalance, + uint256 duration, + uint256 bundleId + ) + internal + returns ( + uint256 premiumTotalAmount, + uint256 premiumNetAmount, + uint256 commissionAmount + ) + { + address distributor = msg.sender; + + // calculate amounts + ( + premiumTotalAmount, + commissionAmount + ) = calculatePrice(distributor, protectedBalance, duration, bundleId); + + premiumNetAmount = premiumTotalAmount - commissionAmount; + + // update distributor book keeping record + DistributorInfo storage info = _distributor[distributor]; + info.commissionBalance += commissionAmount; + info.policiesSold += 1; + info.updatedAt = block.timestamp; + + // collect total premium amount + _token.transferFrom(buyer, address(this), premiumTotalAmount); + + emit LogDistributionInfoUpdated(distributor, commissionAmount, info.commissionBalance, info.policiesSold); + } + + + function calculatePrice( + address distributor, + uint256 protectedBalance, + uint256 duration, + uint256 bundleId + ) + public + view + returns ( + uint256 premiumTotalAmount, + uint256 commissionAmount + ) + { + // fetch policy price + uint256 sumInsured = _depegRiskpool.calculateSumInsured(protectedBalance); + uint256 netPremium = _depegProduct.calculateNetPremium( + sumInsured, + duration, + bundleId); + + uint256 depegPremium = _depegProduct.calculatePremium(netPremium); + + // calculate commission and total premium + commissionAmount = calculateCommission(distributor, depegPremium); + premiumTotalAmount = depegPremium + commissionAmount; + } + + function calculateCommission(address distributor, uint256 netPremiumAmount) + public + view + returns(uint256 commissionAmount) + { + uint256 rate = _distributor[distributor].commissionRate; + if(rate == 0) { + return 0; + } + + return (netPremiumAmount * rate) / (10**DECIMALS - rate); + } + + /// @dev distribution owner "override" to potentially collect commissions that + /// that is not collected by + function withdraw(uint256 amount) + external + onlyOwner() + { + require(_token.balanceOf(address(this)) >= amount, "ERROR:DST-040:BALANCE_INSUFFICIENT"); + require(_token.transfer(owner(), amount), "ERROR:DST-041:WITHDRAWAL_FAILED"); + } + + function withdrawCommission(uint256 amount) + external + onlyDistributor() + { + address distributor = msg.sender; + require(getCommissionBalance(distributor) >= amount, "ERROR:DST-050:AMOUNT_TOO_LARGE"); + require(_token.balanceOf(address(this)) >= amount, "ERROR:DST-051:BALANCE_INSUFFICIENT"); + + // update distributor book keeping record + DistributorInfo storage info = _distributor[distributor]; + info.commissionBalance -= amount; + info.updatedAt = block.timestamp; + + require(_token.transfer(distributor, amount), "ERROR:DST-052:WITHDRAWAL_FAILED"); + } + + function getToken() external view returns (address token) { + return address(_token); + } + + function distributors() external view returns(uint256) { + return _distributors.length; + } + + function getDistributor(uint256 idx) external view returns(address) { + return _distributors[idx]; + } + + function isDistributor(address distributor) public view returns (bool) { + return _distributor[distributor].createdAt > 0; + } + + function getPoliciesSold(address distributor) external view returns (uint256 policies) { + return _distributor[distributor].policiesSold; + } + + function getCommissionBalance(address distributor) public view returns (uint256 commissionAmount) { + return _distributor[distributor].commissionBalance; + } + + function getCommissionRate(address distributor) external view returns (uint256 commissionRate) { + return _distributor[distributor].commissionRate; + } + + function getDistributorInfo(address distributor) external view returns (DistributorInfo memory) { + return _distributor[distributor]; + } +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index f791b2e..bda23e8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@etherisc/depeg-contracts", - "version": "1.1.2", + "version": "1.2.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@etherisc/depeg-contracts", - "version": "1.1.2", + "version": "1.2.0", "license": "Apache-2.0", "dependencies": { "@chainlink/contracts": "0.4.1", diff --git a/package.json b/package.json index 31fc78f..ec9d47d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@etherisc/depeg-contracts", - "version": "1.1.2", + "version": "1.2.0", "description": "Etherisc's smart contracts for a depeg insurance for stable coins.", "repository": { "type": "git", diff --git a/tests/conftest.py b/tests/conftest.py index 13b4727..6a27bbd 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -115,6 +115,10 @@ def protectedWallet2(accounts) -> Account: def registryOwner(accounts) -> Account: return get_filled_account(accounts, 13, "1 ether") +@pytest.fixture(scope="module") +def distributor(accounts) -> Account: + return get_filled_account(accounts, 14, "1 ether") + @pytest.fixture(scope="module") def theOutsider(accounts) -> Account: return get_filled_account(accounts, 19, "1 ether") diff --git a/tests/test_distribution.py b/tests/test_distribution.py new file mode 100644 index 0000000..ce313e7 --- /dev/null +++ b/tests/test_distribution.py @@ -0,0 +1,624 @@ +import brownie +import pytest + +from brownie.network.account import Account +from brownie import ( + chain, + history, + interface, + UsdcPriceDataProvider, + USD1, + USD2, + DIP, + DepegDistribution +) + +from scripts.util import ( + b2s, + contract_from_address +) +from scripts.depeg_product import ( + GifDepegProduct, + GifDepegRiskpool, +) + +from scripts.deploy_depeg import get_setup + +from scripts.price_data import ( + STATE_PRODUCT, + PERFECT_PRICE, + TRIGGER_PRICE, + # RECOVERY_PRICE, + inject_and_process_data, + generate_next_data, +) + +from scripts.setup import ( + create_bundle, + apply_for_policy_with_bundle, +) + +# enforce function isolation for tests below +@pytest.fixture(autouse=True) +def isolation(fn_isolation): + pass + +COMMISSION_RATE_DEFAULT = 0.05 +COMMISSION_RATE_MAX = 0.33 +COMMISSION_TOLERANCE = 10 ** -9 + +def test_deploy_distribution( + product20, + riskpool20, + productOwner, + distributor, + theOutsider, + usd1: USD1, + usd2: USD2, +): + distribution = _deploy_distribution(product20, productOwner) + + assert distribution.owner() == productOwner + assert distribution.getToken() == product20.getToken() + assert distribution.getToken() == usd2 + + assert distribution.COMMISSION_RATE_DEFAULT() / 10**distribution.DECIMALS() == COMMISSION_RATE_DEFAULT + assert distribution.COMMISSION_RATE_MAX() / 10 ** distribution.DECIMALS() == COMMISSION_RATE_MAX + + assert distribution.distributors() == 0 + assert not distribution.isDistributor(distributor) + assert distribution.getCommissionRate(distributor) == 0 + assert distribution.getCommissionBalance(distributor) == 0 + assert distribution.getPoliciesSold(distributor) == 0 + + assert not distribution.isDistributor(theOutsider) + + +def test_create_distributor_happy_case( + product20, + riskpool20, + productOwner, + distributor, + theOutsider, + usd1: USD1, + usd2: USD2, +): + distribution = _deploy_distribution(product20, productOwner) + distribution.createDistributor(distributor, {'from': productOwner}) + + assert distribution.distributors() == 1 + assert distribution.isDistributor(distributor) + assert distribution.getCommissionRate(distributor) > 0 + assert distribution.getCommissionRate(distributor) == distribution.COMMISSION_RATE_DEFAULT() + assert distribution.getCommissionBalance(distributor) == 0 + assert distribution.getPoliciesSold(distributor) == 0 + + net_premium_100 = 100 * 10 ** usd2.decimals() + commission = distribution.calculateCommission(distributor, net_premium_100) + full_premium = net_premium_100 + commission + + commission_rate = distribution.getCommissionRate(distributor) + assert commission == full_premium * commission_rate / 10 ** distribution.DECIMALS() + + assert not distribution.isDistributor(theOutsider) + + +def test_set_commission_rate_happy_case( + product20, + productOwner, + distributor, +): + distribution = _deploy_distribution(product20, productOwner) + distribution.createDistributor(distributor, {'from': productOwner}) + + # check initial setting + assert distribution.getCommissionRate(distributor) == distribution.COMMISSION_RATE_DEFAULT() + + # set to higher rate + commission_rate_new = 12 * 10 ** (distribution.DECIMALS() - 2); + distribution.setCommissionRate(distributor, commission_rate_new, {'from': productOwner}) + + assert commission_rate_new > distribution.COMMISSION_RATE_DEFAULT() + assert distribution.getCommissionRate(distributor) == commission_rate_new + + # set to max rate + distribution.setCommissionRate(distributor, distribution.COMMISSION_RATE_MAX(), {'from': productOwner}) + + assert distribution.getCommissionRate(distributor) == distribution.COMMISSION_RATE_MAX() + + # set rate to zero + commission_rate_zero = 0; + distribution.setCommissionRate(distributor, commission_rate_zero, {'from': productOwner}) + + assert distribution.getCommissionRate(distributor) == commission_rate_zero + + +def test_set_commission_rate_too_high( + product20, + productOwner, + distributor, +): + distribution = _deploy_distribution(product20, productOwner) + distribution.createDistributor(distributor, {'from': productOwner}) + + # check initial setting + assert distribution.getCommissionRate(distributor) == distribution.COMMISSION_RATE_DEFAULT() + + # set to higher rate + commission_rate_too_high = distribution.COMMISSION_RATE_MAX() + 1 + with brownie.reverts("ERROR:DST-031:COMMISSION_RATE_TOO_HIGH"): + distribution.setCommissionRate(distributor, commission_rate_too_high, {'from': productOwner}) + + +def test_set_commission_rate_authz( + product20, + productOwner, + distributor, + theOutsider +): + distribution = _deploy_distribution(product20, productOwner) + distribution.createDistributor(distributor, {'from': productOwner}) + + # set to new rate + new_rate = 12 * 10 ** (distribution.DECIMALS() - 2); + + # attempt to set rate by distributor itself + with brownie.reverts("Ownable: caller is not the owner"): + distribution.setCommissionRate(distributor, new_rate, {'from': distributor}) + + # attempt to set rate by outsider + with brownie.reverts("Ownable: caller is not the owner"): + distribution.setCommissionRate(distributor, new_rate, {'from': theOutsider}) + + +def test_create_distributor_authz( + product20, + riskpool20, + productOwner, + distributor, + theOutsider, + usd1: USD1, + usd2: USD2, +): + distribution = _deploy_distribution(product20, productOwner) + + # attempt to self create distributor + with brownie.reverts('Ownable: caller is not the owner'): + distribution.createDistributor(distributor, {'from': distributor}) + + +def test_sell_policy_trough_distributor( + instance, + instanceService, + instanceOperator, + instanceWallet, + productOwner, + distributor, + investor, + customer, + protectedWallet, + product20, + riskpool20, + riskpoolWallet, + usd1: USD1, + usd2: USD2, +): + distribution = _deploy_distribution(product20, productOwner) + distribution.createDistributor(distributor, {'from': productOwner}) + + tf = 10**usd2.decimals() + max_protected_balance = 10000 + bundle_funding = (max_protected_balance * 2) / 5 + bundle_id = create_bundle( + instance, + instanceOperator, + investor, + riskpool20, + maxProtectedBalance = max_protected_balance, + funding = bundle_funding) + + # setup up wallet to protect with some coins + protected_balance = 5000 * tf + usd1.transfer(protectedWallet, protected_balance, {'from': instanceOperator}) + + # buy policy for wallet to be protected + duration_days = 60 + max_premium = 100 + duration_seconds = duration_days * 24 * 3600 + + ( + total_premium, + commission + ) = distribution.calculatePrice( + distributor, + protected_balance, + duration_seconds, + bundle_id + ) + + # fund customer + usd2.transfer(customer, total_premium, {'from': instanceOperator}) + usd2.approve(distribution, total_premium, {'from': customer}) + + assert usd2.balanceOf(customer) == total_premium + assert usd2.balanceOf(distribution) == 0 + + # check distributor book keeping (before policy sale) + assert distribution.getCommissionBalance(distributor) == 0 + assert distribution.getPoliciesSold(distributor) == 0 + + tx = distribution.createPolicy( + customer, + protectedWallet, + protected_balance, + duration_seconds, + bundle_id, + {'from': distributor}) + + process_id = tx.events['LogApplicationCreated']['processId'] + + assert usd2.balanceOf(customer) == 0 + assert usd2.balanceOf(distribution) == commission + + # check owner of policy is distribution contract + # customer from above is only used to pull premium + meta_data = instanceService.getMetadata(process_id).dict() + assert meta_data['owner'] != customer + assert meta_data['owner'] == distribution + + # check distributor book keeping (after policy sale) + assert distribution.getCommissionBalance(distributor) == commission + assert distribution.getPoliciesSold(distributor) == 1 + + # check that all other policy properties match the direct sale setup + # see test_product_20.py::test_product_20_create_policy + protected_amount = protected_balance + sum_insured_amount = protected_amount / 5 + net_premium_amount = product20.calculateNetPremium(sum_insured_amount, duration_days * 24 * 3600, bundle_id) + premium_amount = product20.calculatePremium(net_premium_amount) + + # check application event data + events = history[-1].events + app_evt = dict(events['LogDepegApplicationCreated']) + assert app_evt['protectedBalance'] == protected_amount + assert app_evt['sumInsuredAmount'] == sum_insured_amount + assert app_evt['premiumAmount'] == premium_amount + + # check application data + application = instanceService.getApplication(process_id).dict() + application_data = riskpool20.decodeApplicationParameterFromData(application['data']).dict() + + assert application['sumInsuredAmount'] == riskpool20.calculateSumInsured(protected_amount) + assert application['sumInsuredAmount'] == sum_insured_amount + assert application_data['protectedBalance'] == protected_amount + + # check policy + policy = instanceService.getPolicy(process_id).dict() + + assert policy['premiumExpectedAmount'] == premium_amount + assert policy['premiumPaidAmount'] == premium_amount + assert policy['payoutMaxAmount'] == sum_insured_amount + assert policy['payoutAmount'] == 0 + + # check bundle data + bundle = instanceService.getBundle(bundle_id).dict() + funding_amount = bundle_funding * tf + + assert bundle['balance'] == funding_amount + net_premium_amount + assert bundle['capital'] == funding_amount + assert bundle['lockedCapital'] == sum_insured_amount + + # check riskpool numbers + assert riskpool20.getBalance() == bundle['balance'] + assert riskpool20.getTotalValueLocked() == sum_insured_amount + assert riskpool20.getCapacity() == funding_amount - sum_insured_amount + + # check riskpool wallet + assert usd2.balanceOf(riskpoolWallet) == riskpool20.getBalance() + + +def test_withdrawal_distributor_happy_case( + instance, + instanceService, + instanceOperator, + instanceWallet, + productOwner, + distributor, + investor, + customer, + protectedWallet, + product20, + riskpool20, + riskpoolWallet, + usd1: USD1, + usd2: USD2, +): + ( + distribution, + commission + ) = _createCommisssionSetup( + instance, + instanceService, + instanceOperator, + instanceWallet, + productOwner, + distributor, + investor, + customer, + protectedWallet, + product20, + riskpool20, + riskpoolWallet, + usd1, + usd2 + ) + + assert usd2.balanceOf(distributor) == 0 + assert usd2.balanceOf(distribution) == commission + + withdrawal_amount = 100000 + remaining_commission = commission - withdrawal_amount + tx = distribution.withdrawCommission(withdrawal_amount, {'from': distributor}) + + # check updated book keeping + assert distribution.getCommissionBalance(distributor) == remaining_commission + + # check actual token balances + assert usd2.balanceOf(distributor) == withdrawal_amount + assert usd2.balanceOf(distribution) == remaining_commission + + +def test_withdrawal_distributor_amount_too_big( + instance, + instanceService, + instanceOperator, + instanceWallet, + productOwner, + distributor, + investor, + customer, + protectedWallet, + product20, + riskpool20, + riskpoolWallet, + usd1: USD1, + usd2: USD2, +): + ( + distribution, + commission + ) = _createCommisssionSetup( + instance, + instanceService, + instanceOperator, + instanceWallet, + productOwner, + distributor, + investor, + customer, + protectedWallet, + product20, + riskpool20, + riskpoolWallet, + usd1, + usd2 + ) + + assert usd2.balanceOf(distributor) == 0 + assert usd2.balanceOf(distribution) == commission + + # amount larger than accumulated commission + with brownie.reverts("ERROR:DST-050:AMOUNT_TOO_LARGE"): + distribution.withdrawCommission(commission + 1, {'from': distributor}) + + # reduce commission balance of distribution contract + distribution.withdraw(commission - 1000, {'from': productOwner}) + + # amount smaller accumulated commission + with brownie.reverts("ERROR:DST-051:BALANCE_INSUFFICIENT"): + distribution.withdrawCommission(commission - 1, {'from': distributor}) + + +def test_withdrawal_not_distributor( + instance, + instanceService, + instanceOperator, + instanceWallet, + productOwner, + distributor, + investor, + customer, + theOutsider, + protectedWallet, + product20, + riskpool20, + riskpoolWallet, + usd1: USD1, + usd2: USD2, +): + ( + distribution, + commission + ) = _createCommisssionSetup( + instance, + instanceService, + instanceOperator, + instanceWallet, + productOwner, + distributor, + investor, + customer, + protectedWallet, + product20, + riskpool20, + riskpoolWallet, + usd1, + usd2 + ) + + assert usd2.balanceOf(distributor) == 0 + assert usd2.balanceOf(distribution) == commission + + with brownie.reverts("ERROR:DST-001:NOT_DISTRIBUTOR"): + distribution.withdrawCommission(commission + 1, {'from': productOwner}) + + with brownie.reverts("ERROR:DST-001:NOT_DISTRIBUTOR"): + distribution.withdrawCommission(commission + 1, {'from': theOutsider}) + + # now, make the outsider to distributor - but not the one that owns the one with the commission + distribution.createDistributor(theOutsider, {'from': productOwner}) + + # amount larger than accumulated commission + with brownie.reverts("ERROR:DST-050:AMOUNT_TOO_LARGE"): + distribution.withdrawCommission(commission + 1, {'from': theOutsider}) + + +def test_withdrawal_owner_happy_case( + productOwner, + instanceOperator, + product20, + usd2: USD2, +): + distribution = _deploy_distribution(product20, productOwner) + + some_amount = 1000 * 10 ** usd2.decimals() + usd2.transfer(distribution, some_amount, {'from': instanceOperator}) + + # check balances before withdrawal + assert usd2.balanceOf(distribution) == some_amount + assert usd2.balanceOf(productOwner) == 0 + + other_amount = 200 * 10 ** usd2.decimals() + distribution.withdraw(other_amount, {'from': productOwner}) + + # check balances after withdrawal + assert usd2.balanceOf(distribution) == some_amount - other_amount + assert usd2.balanceOf(productOwner) == other_amount + + +def test_withdrawal_non_owner( + productOwner, + instanceOperator, + distributor, + theOutsider, + product20, + usd2: USD2, +): + distribution = _deploy_distribution(product20, productOwner) + distribution.createDistributor(distributor, {'from': productOwner}) + + some_amount = 1000 * 10 ** usd2.decimals() + usd2.transfer(distribution, some_amount, {'from': instanceOperator}) + + # check balances before withdrawal + assert usd2.balanceOf(distribution) == some_amount + assert usd2.balanceOf(productOwner) == 0 + + other_amount = 200 * 10 ** usd2.decimals() + + # attempt withdrawal by outsider + with brownie.reverts("Ownable: caller is not the owner"): + distribution.withdraw(other_amount, {'from': theOutsider}) + + # attempt withdrawal by distributor + with brownie.reverts("Ownable: caller is not the owner"): + distribution.withdraw(other_amount, {'from': distributor}) + + +def test_withdrawal_amount_too_big( + productOwner, + instanceOperator, + product20, + usd2: USD2, +): + distribution = _deploy_distribution(product20, productOwner) + + some_amount = 1000 * 10 ** usd2.decimals() + usd2.transfer(distribution, some_amount, {'from': instanceOperator}) + + # check balances before withdrawal + assert usd2.balanceOf(distribution) == some_amount + assert usd2.balanceOf(productOwner) == 0 + + # amount larger than balance + other_amount = some_amount + 1 + + # attempt withdrawal by too large amount + with brownie.reverts("ERROR:DST-040:BALANCE_INSUFFICIENT"): + distribution.withdraw(other_amount, {'from': productOwner}) + + +def _createCommisssionSetup( + instance, + instanceService, + instanceOperator, + instanceWallet, + productOwner, + distributor, + investor, + customer, + protectedWallet, + product20, + riskpool20, + riskpoolWallet, + usd1: USD1, + usd2: USD2, +): + distribution = _deploy_distribution(product20, productOwner) + distribution.createDistributor(distributor, {'from': productOwner}) + + tf = 10**usd2.decimals() + max_protected_balance = 10000 + bundle_funding = (max_protected_balance * 2) / 5 + bundle_id = create_bundle( + instance, + instanceOperator, + investor, + riskpool20, + maxProtectedBalance = max_protected_balance, + funding = bundle_funding) + + # setup up wallet to protect with some coins + protected_balance = 5000 * tf + usd1.transfer(protectedWallet, protected_balance, {'from': instanceOperator}) + + # buy policy for wallet to be protected + duration_days = 60 + duration_seconds = duration_days * 24 * 3600 + + ( + total_premium, + commission + ) = distribution.calculatePrice( + distributor, + protected_balance, + duration_seconds, + bundle_id + ) + + # fund customer + usd2.transfer(customer, total_premium, {'from': instanceOperator}) + usd2.approve(distribution, total_premium, {'from': customer}) + + tx = distribution.createPolicy( + customer, + protectedWallet, + protected_balance, + duration_seconds, + bundle_id, + {'from': distributor}) + + return ( + distribution, + commission + ) + + +def _deploy_distribution( + product, + productOwner, +): + return DepegDistribution.deploy( + product, + product.getId(), + {'from': productOwner})