Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions contracts/mocks/MasterMock.sol
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,15 @@ contract MasterMock {
bool paused;
address public tokenAddress;

address public emergencyAdmin;

/* utils */


function setEmergencyAdmin(address _emergencyAdmin) external {
emergencyAdmin = _emergencyAdmin;
}

function enrollGovernance(address newGov) public {
governanceAddresses[newGov] = true;
}
Expand Down
49 changes: 9 additions & 40 deletions contracts/modules/cover/Cover.sol
Original file line number Diff line number Diff line change
Expand Up @@ -474,44 +474,7 @@ contract Cover is ICover, MasterAwareV2, IStakingPoolBeacon, ReentrancyGuard {
address commissionDestination
) internal {

// add commission
uint commission = premium * commissionRatio / COMMISSION_DENOMINATOR;

if (paymentAsset == 0) {

uint premiumWithCommission = premium + commission;
require(msg.value >= premiumWithCommission, "Cover: Insufficient ETH sent");

uint remainder = msg.value - premiumWithCommission;

if (remainder > 0) {
// solhint-disable-next-line avoid-low-level-calls
(bool ok, /* data */) = address(msg.sender).call{value: remainder}("");
require(ok, "Cover: Returning ETH remainder to sender failed.");
}

// send commission
if (commission > 0) {
(bool ok, /* data */) = address(commissionDestination).call{value: commission}("");
require(ok, "Cover: Sending ETH to commission destination failed.");
}

return;
}

IPool _pool = pool();

(
address payoutAsset,
/*uint8 decimals*/
) = _pool.coverAssets(paymentAsset);

IERC20 token = IERC20(payoutAsset);
token.safeTransferFrom(msg.sender, address(_pool), premium);

if (commission > 0) {
token.safeTransferFrom(msg.sender, commissionDestination, commission);
}
CoverUtilsLib.retrievePayment(premium, paymentAsset, commissionRatio, commissionDestination, pool());
}

function retrieveNXMPayment(uint price, uint commissionRatio, address commissionDestination) internal {
Expand Down Expand Up @@ -578,11 +541,17 @@ contract Cover is ICover, MasterAwareV2, IStakingPoolBeacon, ReentrancyGuard {
uint burnAmount
) external onlyInternal override returns (address /* owner */) {

return CoverUtilsLib.performStakeBurn(
CoverData storage cover =_coverData[coverId];

if (coverAmountTrackingEnabled) {
totalActiveCoverInAsset[cover.payoutAsset] -= burnAmount;
}

return CoverUtilsLib.performStakeBurn(
coverId,
burnAmount,
ICoverNFT(coverNFT),
_coverData[coverId],
cover,
coverSegments(coverId, segmentId),
coverSegmentAllocations[coverId][segmentId],
stakingPoolProxyCodeHash,
Expand Down
52 changes: 52 additions & 0 deletions contracts/modules/cover/CoverUtilsLib.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,24 @@

pragma solidity ^0.8.9;

import "@openzeppelin/contracts-v4/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts-v4/token/ERC20/utils/SafeERC20.sol";

import "../../interfaces/ICover.sol";
import "../../interfaces/ICoverNFT.sol";
import "../../interfaces/IProductsV1.sol";
import "../../interfaces/IQuotationData.sol";
import "../../interfaces/ITokenController.sol";
import "../../libraries/SafeUintCast.sol";
import "../../interfaces/IPool.sol";
import "./MinimalBeaconProxy.sol";


library CoverUtilsLib {
using SafeERC20 for IERC20;

uint private constant GLOBAL_CAPACITY_DENOMINATOR = 10_000;
uint private constant COMMISSION_DENOMINATOR = 10_000;

struct MigrateParams {
uint coverId;
Expand Down Expand Up @@ -194,4 +200,50 @@ library CoverUtilsLib {

return coverNFT.ownerOf(coverId);
}

function retrievePayment(
uint premium,
uint8 paymentAsset,
uint16 commissionRatio,
address commissionDestination,
IPool _pool
) external {

// add commission
uint commission = premium * commissionRatio / COMMISSION_DENOMINATOR;

if (paymentAsset == 0) {

uint premiumWithCommission = premium + commission;
require(msg.value >= premiumWithCommission, "Cover: Insufficient ETH sent");

uint remainder = msg.value - premiumWithCommission;

if (remainder > 0) {
// solhint-disable-next-line avoid-low-level-calls
(bool ok, /* data */) = address(msg.sender).call{value: remainder}("");
require(ok, "Cover: Returning ETH remainder to sender failed.");
}

// send commission
if (commission > 0) {
(bool ok, /* data */) = address(commissionDestination).call{value: commission}("");
require(ok, "Cover: Sending ETH to commission destination failed.");
}

return;
}

(
address payoutAsset,
/*uint8 decimals*/
) = _pool.coverAssets(paymentAsset);

IERC20 token = IERC20(payoutAsset);
token.safeTransferFrom(msg.sender, address(_pool), premium);

if (commission > 0) {
token.safeTransferFrom(msg.sender, commissionDestination, commission);
}
}
}
6 changes: 1 addition & 5 deletions test/unit/Cover/buyCover.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,11 +60,7 @@ describe('buyCover', function () {
value: expectedPremium,
},
);
const receipt = await tx.wait();

console.log({
gasUsed: receipt.gasUsed.toString(),
});
await tx.wait();

const expectedCoverId = '0';

Expand Down
2 changes: 1 addition & 1 deletion test/unit/Cover/createStakingPool.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ const {

describe('createStakingPool', function () {
it('should create new pool', async function () {
const { cover, nxm, memberRoles } = this;
const { cover } = this;

const {
members: [stakingPoolCreator, stakingPoolManager],
Expand Down
30 changes: 5 additions & 25 deletions test/unit/Cover/editCover.js
Original file line number Diff line number Diff line change
Expand Up @@ -65,11 +65,7 @@ describe('editCover', function () {
value: extraPremium.add(1),
},
);
const receipt = await tx.wait();

console.log({
gasUsed: receipt.gasUsed.toString(),
});
await tx.wait();

await assertCoverFields(cover, expectedCoverId,
{ productId, payoutAsset, period: period, amount: increasedAmount, targetPriceRatio, segmentId: '1' },
Expand Down Expand Up @@ -122,11 +118,7 @@ describe('editCover', function () {
},
);

const receipt = await tx.wait();

console.log({
gasUsed: receipt.gasUsed.toString(),
});
await tx.wait();

await assertCoverFields(cover, expectedCoverId,
{ productId, payoutAsset, period: increasedPeriod, amount: amount, targetPriceRatio, segmentId: '1' },
Expand Down Expand Up @@ -182,11 +174,7 @@ describe('editCover', function () {
},
);

const receipt = await tx.wait();

console.log({
gasUsed: receipt.gasUsed.toString(),
});
await tx.wait();

await assertCoverFields(cover, expectedCoverId,
{ productId, payoutAsset, period: increasedPeriod, amount: increasedAmount, targetPriceRatio, segmentId: '1' },
Expand Down Expand Up @@ -242,11 +230,7 @@ describe('editCover', function () {
},
);

const receipt = await tx.wait();

console.log({
gasUsed: receipt.gasUsed.toString(),
});
await tx.wait();

await assertCoverFields(cover, expectedCoverId,
{ productId, payoutAsset, period: increasedPeriod, amount: decreasedAmount, targetPriceRatio, segmentId: '1' },
Expand Down Expand Up @@ -299,11 +283,7 @@ describe('editCover', function () {
},
);

const receipt = await tx.wait();

console.log({
gasUsed: receipt.gasUsed.toString(),
});
await tx.wait();

await assertCoverFields(cover, expectedCoverId,
{ productId, payoutAsset, period: period, amount: increasedAmount, targetPriceRatio, segmentId: '1' },
Expand Down
106 changes: 106 additions & 0 deletions test/unit/Cover/expireCover.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
const { assert, expect } = require('chai');
const {
ethers: {
utils: { parseEther },
},
} = require('hardhat');
const { buyCoverOnOnePool } = require('./helpers');
const { bnEqual } = require('../utils').helpers;
const { time } = require('@openzeppelin/test-helpers');

describe('expireCover', function () {

const ethCoverBuyFixture = {
productId: 0,
payoutAsset: 0, // ETH
period: 3600 * 24 * 30, // 30 days

amount: parseEther('1000'),

targetPriceRatio: '260',
priceDenominator: '10000',
activeCover: parseEther('8000'),
capacity: parseEther('10000'),
capacityFactor: '10000',
};

it('expires cover and reduces active cover amount', async function () {
const { cover } = this;

const {
emergencyAdmin,
} = this.accounts;

const {
payoutAsset,
} = ethCoverBuyFixture;

await cover.connect(emergencyAdmin).enableActiveCoverAmountTracking([], []);

await cover.connect(emergencyAdmin).commitActiveCoverAmounts();

await buyCoverOnOnePool.call(this, ethCoverBuyFixture);

await time.increase(ethCoverBuyFixture.period + 1);

const coverId = 0;

await cover.expireCover(0);

const activeCoverAmountAfterExpiry = await cover.totalActiveCoverInAsset(payoutAsset);
bnEqual(activeCoverAmountAfterExpiry, parseEther('0'));

const segmentId = '0';
const segment = await cover.coverSegments(coverId, segmentId);
assert(segment.expired, true);
});

it('reverts when attempting to expire twice', async function () {
const { cover } = this;

const {
emergencyAdmin,
} = this.accounts;

await cover.connect(emergencyAdmin).enableActiveCoverAmountTracking([], []);

await cover.connect(emergencyAdmin).commitActiveCoverAmounts();

await buyCoverOnOnePool.call(this, ethCoverBuyFixture);

await time.increase(ethCoverBuyFixture.period + 1);

await cover.expireCover(0);

await expect(cover.expireCover(0)).to.be.revertedWith('Cover: Cover is already expired.');
});

it('reverts when cover expiry is not enabled', async function () {
const { cover } = this;

await buyCoverOnOnePool.call(this, ethCoverBuyFixture);

await time.increase(ethCoverBuyFixture.period);

await expect(cover.expireCover(0)).to.be.revertedWith('Cover: Cover expiring not enabled');
});

it('reverts when cover is not due to expire', async function () {
const { cover } = this;

const {
emergencyAdmin,
} = this.accounts;

await cover.connect(emergencyAdmin).enableActiveCoverAmountTracking([], []);

await cover.connect(emergencyAdmin).commitActiveCoverAmounts();

await buyCoverOnOnePool.call(this, ethCoverBuyFixture);

await time.increase((ethCoverBuyFixture.period - 3600));

await expect(cover.expireCover(0)).to.be.revertedWith('Cover: Cover is not due to expire yet');
});

});
1 change: 1 addition & 0 deletions test/unit/Cover/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,5 @@ describe('Cover unit tests', function () {
require('./createStakingPool');
require('./totalActiveCoverInAsset');
require('./performStakeBurn');
require('./expireCover');
});
13 changes: 8 additions & 5 deletions test/unit/Cover/performStakeBurn.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ describe('performStakeBurn', function () {
const { cover } = this;

const {
internalContracts: [internal1]
internalContracts: [internal1],
emergencyAdmin
} = this.accounts;

const {
Expand All @@ -37,6 +38,9 @@ describe('performStakeBurn', function () {
targetPriceRatio
} = coverBuyFixture;

await cover.connect(emergencyAdmin).enableActiveCoverAmountTracking([], []);
await cover.connect(emergencyAdmin).commitActiveCoverAmounts();

const { segmentId, coverId: expectedCoverId } = await buyCoverOnOnePool.call(this, coverBuyFixture);


Expand All @@ -47,12 +51,8 @@ describe('performStakeBurn', function () {

const segmentAllocation = await cover.coverSegmentAllocations(expectedCoverId, segmentId, '0');

console.log({
coverAmountInNXM: segmentAllocation.coverAmountInNXM.toString()
})
const expectedBurnAmount = segmentAllocation.coverAmountInNXM.div(burnAmountDivisor);


await cover.connect(internal1).performStakeBurn(
expectedCoverId,
segmentId,
Expand Down Expand Up @@ -80,5 +80,8 @@ describe('performStakeBurn', function () {
bnEqual(burnStakeCalledWith.productId, productId);
bnEqual(burnStakeCalledWith.period, period);
bnEqual(burnStakeCalledWith.amount, expectedBurnAmount);

const activeCoverAmount = await cover.totalActiveCoverInAsset(payoutAsset);
bnEqual(activeCoverAmount, amount.sub(burnAmount));
});
});
Loading