Skip to content

Commit

Permalink
Merge pull request #583 from NexusMutual/fix/buy-cover-fund-pool
Browse files Browse the repository at this point in the history
Fix: 5.1.3 Premium never sent to Pool
  • Loading branch information
shark0der committed Jan 5, 2023
2 parents 2b4d436 + 92b5639 commit 444e626
Show file tree
Hide file tree
Showing 3 changed files with 109 additions and 50 deletions.
25 changes: 25 additions & 0 deletions contracts/mocks/Cover/CoverMockStakingPool.sol
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ contract CoverMockStakingPool is IStakingPool, ERC721Mock {
uint public constant MAX_PRICE_RATIO = 10_000;
uint constant REWARDS_DENOMINATOR = 10_000;
uint public constant GLOBAL_MIN_PRICE_RATIO = 100; // 1%
uint public constant ONE_NXM = 1 ether;
uint public constant ALLOCATION_UNITS_PER_NXM = 100;
uint public constant NXM_PER_ALLOCATION_UNIT = ONE_NXM / ALLOCATION_UNITS_PER_NXM;
uint public constant TARGET_PRICE_DENOMINATOR = 100_00;

uint public poolId;
// erc721 supply
Expand Down Expand Up @@ -69,9 +73,30 @@ contract CoverMockStakingPool is IStakingPool, ERC721Mock {
AllocationRequest calldata request
) external override returns (uint premium) {
usedCapacity[request.productId] += amount;
if (request.useFixedPrice) {
return calculateFixedPricePremium(amount, request.period, mockPrices[request.productId]);
}
return calculatePremium(mockPrices[request.productId], amount, request.period);
}


function calculateFixedPricePremium(
uint coverAmount,
uint period,
uint fixedPrice
) public pure returns (uint) {
// NOTE: the actual function takes coverAmount scaled down by NXM_PER_ALLOCATION_UNIT as an argument
coverAmount = Math.divCeil(coverAmount, NXM_PER_ALLOCATION_UNIT);

uint premiumPerYear =
coverAmount
* NXM_PER_ALLOCATION_UNIT
* fixedPrice
/ TARGET_PRICE_DENOMINATOR;

return premiumPerYear * period / 365 days;
}

function setProducts(StakedProductParam[] memory params) external {
totalSupply = totalSupply;
params;
Expand Down
15 changes: 9 additions & 6 deletions contracts/modules/cover/Cover.sol
Original file line number Diff line number Diff line change
Expand Up @@ -414,19 +414,22 @@ contract Cover is ICover, MasterAwareV2, IStakingPoolBeacon, ReentrancyGuard {

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 premium in eth to the pool
// solhint-disable-next-line avoid-low-level-calls
(bool ok, /* data */) = address(_pool).call{value: premiumInPaymentAsset}("");
require(ok, "Cover: Sending ETH to pool failed.");

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

// TODO: send eth to pool
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.");
}

return;
}
Expand Down
119 changes: 75 additions & 44 deletions test/unit/Cover/buyCover.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
const { expect } = require('chai');
const { ethers } = require('hardhat');
const { setEtherBalance } = require('../../utils/evm');
const { daysToSeconds } = require('../../../lib/helpers');

const { createStakingPool, assertCoverFields } = require('./helpers');

Expand Down Expand Up @@ -49,14 +50,15 @@ describe('buyCover', function () {
});

it('should purchase new cover using 1 staking pool', async function () {
const { cover } = this;
const { cover, pool } = this;

const {
members: [coverBuyer],
} = this.accounts;

const { amount, productId, coverAsset, period, expectedPremium } = buyCoverFixture;

const poolEthBalanceBefore = await ethers.provider.getBalance(pool.address);
const tx = await cover.connect(coverBuyer).buyCover(
{
coverId: MaxUint256,
Expand All @@ -78,6 +80,10 @@ describe('buyCover', function () {
);
await tx.wait();

// no eth should be left in the cover contract
expect(await ethers.provider.getBalance(cover.address)).to.be.equal(0);
const premium = expectedPremium.mul(period).div(daysToSeconds(365));
expect(await ethers.provider.getBalance(pool.address)).to.equal(poolEthBalanceBefore.add(premium));
const coverId = (await cover.coverDataCount()).sub(1);
await assertCoverFields(cover, coverId, {
productId,
Expand All @@ -89,7 +95,7 @@ describe('buyCover', function () {
});

it('should purchase new cover with fixed price using 1 staking pool', async function () {
const { cover } = this;
const { cover, pool } = this;

const {
members: [coverBuyer],
Expand All @@ -98,6 +104,10 @@ describe('buyCover', function () {
const { amount, targetPriceRatio, coverAsset, period, expectedPremium } = buyCoverFixture;

const productId = 1;
const stakingPool = await ethers.getContractAt('CoverMockStakingPool', await cover.stakingPool(0));
await stakingPool.setPrice(productId, targetPriceRatio);

const poolEthBalanceBefore = await ethers.provider.getBalance(pool.address);

const tx = await cover.connect(coverBuyer).buyCover(
{
Expand All @@ -120,6 +130,10 @@ describe('buyCover', function () {
);
await tx.wait();

// no eth should be left in the cover contract
expect(await ethers.provider.getBalance(cover.address)).to.be.equal(0);
const premium = expectedPremium.mul(period).div(daysToSeconds(365));
expect(await ethers.provider.getBalance(pool.address)).to.equal(poolEthBalanceBefore.add(premium));
const coverId = (await cover.coverDataCount()).sub(1);

await assertCoverFields(cover, coverId, {
Expand All @@ -133,7 +147,7 @@ describe('buyCover', function () {
});

it('should purchase new cover using 2 staking pools', async function () {
const { cover } = this;
const { cover, pool } = this;

const {
members: [coverBuyer, stakingPoolManager],
Expand Down Expand Up @@ -177,6 +191,9 @@ describe('buyCover', function () {
},
);

const expectedPremiumPerPool = expectedPremium.div(2).mul(period).div(daysToSeconds(365));
expect(await ethers.provider.getBalance(pool.address)).to.equal(expectedPremiumPerPool.mul(2));

const coverId = (await cover.coverDataCount()).sub(1);
await assertCoverFields(cover, coverId, {
productId,
Expand All @@ -189,7 +206,7 @@ describe('buyCover', function () {
});

it('should purchase new cover using NXM with commission', async function () {
const { cover, nxm, tokenController } = this;
const { cover, nxm, tokenController, pool } = this;

const [coverBuyer, stakingPoolManager] = this.accounts.members;

Expand All @@ -211,31 +228,38 @@ describe('buyCover', function () {
const nxmBalanceBefore = await nxm.balanceOf(coverBuyer.address);
const commissionNxmBalanceBefore = await nxm.balanceOf(stakingPoolManager.address);

await cover.connect(coverBuyer).buyCover(
{
coverId: MaxUint256,
owner: coverBuyer.address,
productId,
coverAsset,
amount,
period,
maxPremiumInAsset: expectedPremium,
paymentAsset: NXM_ASSET_ID,
payWithNXM: true,
commissionRatio,
commissionDestination: stakingPoolManager.address,
ipfsData: '',
},
[{ poolId: '0', coverAmountInAsset: amount }],
{ value: '0' },
);
await expect(
cover.connect(coverBuyer).buyCover(
{
coverId: MaxUint256,
owner: coverBuyer.address,
productId,
coverAsset,
amount,
period,
maxPremiumInAsset: expectedPremium,
paymentAsset: NXM_ASSET_ID,
payWithNXM: true,
commissionRatio,
commissionDestination: stakingPoolManager.address,
ipfsData: '',
},
[{ poolId: '0', coverAmountInAsset: amount }],
{ value: '0' },
),
)
.to.emit(nxm, 'Transfer')
.withArgs(coverBuyer.address, AddressZero, expectedBasePremium);

const nxmBalanceAfter = await nxm.balanceOf(coverBuyer.address);
const commissionNxmBalanceAfter = await nxm.balanceOf(stakingPoolManager.address);

const difference = nxmBalanceBefore.sub(nxmBalanceAfter);
expect(difference).to.be.equal(expectedPremium);

// nxm is burned
expect(await nxm.balanceOf(pool.address)).to.be.equal(0);

const commissionDifference = commissionNxmBalanceAfter.sub(commissionNxmBalanceBefore);
expect(commissionDifference).to.be.equal(expectedCommission);

Expand All @@ -251,7 +275,7 @@ describe('buyCover', function () {
});

it('should purchase new cover using DAI with commission', async function () {
const { cover, dai } = this;
const { cover, dai, pool } = this;

const {
members: [coverBuyer],
Expand All @@ -278,6 +302,7 @@ describe('buyCover', function () {

const daiBalanceBefore = await dai.balanceOf(coverBuyer.address);
const commissionDaiBalanceBefore = await dai.balanceOf(commissionReceiver.address);
expect(await dai.balanceOf(pool.address)).to.be.equal(0);

await cover.connect(coverBuyer).buyCover(
{
Expand All @@ -300,6 +325,8 @@ describe('buyCover', function () {
},
);

expect(await dai.balanceOf(pool.address)).to.equal(expectedBasePremium);

const daiBalanceAfter = await dai.balanceOf(coverBuyer.address);
const commissionDaiBalanceAfter = await dai.balanceOf(commissionReceiver.address);

Expand All @@ -321,7 +348,7 @@ describe('buyCover', function () {
});

it('should purchase new cover using USDC with commission', async function () {
const { cover, usdc } = this;
const { cover, usdc, pool } = this;

const {
members: [coverBuyer],
Expand All @@ -348,26 +375,30 @@ describe('buyCover', function () {
const daiBalanceBefore = await usdc.balanceOf(coverBuyer.address);
const commissionDaiBalanceBefore = await usdc.balanceOf(commissionReceiver.address);

await cover.connect(coverBuyer).buyCover(
{
coverId: MaxUint256,
owner: coverBuyer.address,
productId,
coverAsset,
amount,
period,
maxPremiumInAsset: expectedPremium,
paymentAsset: coverAsset,
payWithNXM: false,
commissionRatio,
commissionDestination: commissionReceiver.address,
ipfsData: '',
},
[{ poolId: '0', coverAmountInAsset: amount }],
{
value: '0',
},
);
await expect(
cover.connect(coverBuyer).buyCover(
{
coverId: MaxUint256,
owner: coverBuyer.address,
productId,
coverAsset,
amount,
period,
maxPremiumInAsset: expectedPremium,
paymentAsset: coverAsset,
payWithNXM: false,
commissionRatio,
commissionDestination: commissionReceiver.address,
ipfsData: '',
},
[{ poolId: '0', coverAmountInAsset: amount }],
{
value: '0',
},
),
)
.to.emit(usdc, 'Transfer') // Verify usdc is transferred to pool
.withArgs(coverBuyer.address, pool.address, expectedBasePremium);

const daiBalanceAfter = await usdc.balanceOf(coverBuyer.address);
const commissionDaiBalanceAfter = await usdc.balanceOf(commissionReceiver.address);
Expand Down

0 comments on commit 444e626

Please sign in to comment.