Skip to content
Open
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
5 changes: 5 additions & 0 deletions contracts/interfaces/ICover.sol
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,9 @@ struct RiRequest {
uint amount;
uint premium;
bytes signature;
bytes data;
uint8 dataFormat;
uint32 deadline;
}

interface ICover is IStakingPoolBeacon {
Expand Down Expand Up @@ -167,6 +170,7 @@ interface ICover is IStakingPoolBeacon {
uint indexed buyerMemberId,
uint productId
);
event CoverRiAllocated(bytes data, uint8 dataFormatVersion);

// Auth
error OnlyOwnerOrApproved();
Expand Down Expand Up @@ -204,6 +208,7 @@ interface ICover is IStakingPoolBeacon {

// Ri
error InvalidSignature();
error SignatureExpired();
error WrongCoverEditEntrypoint();
error RiAmountIsZero();
error InvalidRiConfig();
Expand Down
9 changes: 9 additions & 0 deletions contracts/modules/cover/Cover.sol
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,8 @@ contract Cover is ICover, EIP712, RegistryAware, ReentrancyGuard, Multicall {
RiRequest memory riRequest
) external payable onlyMember returns (uint coverId) {

require(riRequest.deadline > block.timestamp, SignatureExpired());

if (params.coverId != 0) {
require(coverNFT.isApprovedOrOwner(msg.sender, params.coverId), OnlyOwnerOrApproved());
} else {
Expand All @@ -189,6 +191,9 @@ contract Cover is ICover, EIP712, RegistryAware, ReentrancyGuard, Multicall {
"uint256 premium,",
"uint32 period,",
"uint8 coverAsset,",
"bytes32 data,",
"uint8 dataFormat,",
"uint32 deadline,",
"uint256 nonce)"
)
),
Expand All @@ -199,6 +204,9 @@ contract Cover is ICover, EIP712, RegistryAware, ReentrancyGuard, Multicall {
riRequest.premium,
params.period,
params.coverAsset,
keccak256(riRequest.data),
riRequest.dataFormat,
riRequest.deadline,
nonce
);

Expand All @@ -216,6 +224,7 @@ contract Cover is ICover, EIP712, RegistryAware, ReentrancyGuard, Multicall {
registry.getMemberId(msg.sender),
params.productId
);
emit CoverRiAllocated(riRequest.data, riRequest.dataFormat);

return coverId;
}
Expand Down
11 changes: 7 additions & 4 deletions lib/signing.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
const { ethers } = require('ethers');

/**
* @typedef {import('ethers').AddressLike} AddressLike
*/
const { keccak256 } = ethers;

/**
* @param {AddressLike} addresslike
Expand Down Expand Up @@ -81,6 +78,9 @@ const signRiQuote = async (signer, verifyingContract, quote, options = {}) => {
{ name: 'premium', type: 'uint256' },
{ name: 'period', type: 'uint32' },
{ name: 'coverAsset', type: 'uint8' },
{ name: 'data', type: 'bytes32' },
{ name: 'dataFormat', type: 'uint8' },
{ name: 'deadline', type: 'uint32' },
{ name: 'nonce', type: 'uint256' },
],
};
Expand All @@ -94,6 +94,9 @@ const signRiQuote = async (signer, verifyingContract, quote, options = {}) => {
period: quote.period,
coverAsset: quote.coverAsset,
nonce: quote.nonce,
data: keccak256(quote.data),
dataFormat: quote.dataFormat,
deadline: quote.deadline,
};

return signer.signTypedData(domain, types, values);
Expand Down
205 changes: 201 additions & 4 deletions test/unit/Cover/buyCoverWithRi.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
const { ethers, nexus } = require('hardhat');
const { expect } = require('chai');
const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers');
const { loadFixture, time } = require('@nomicfoundation/hardhat-network-helpers');

const { setup } = require('./setup');

const { signRiQuote } = nexus.signing;
const { parseEther, ZeroAddress } = ethers;
const { parseEther, ZeroAddress, AbiCoder } = ethers;

const coverFixture = {
productId: 0n,
Expand All @@ -28,7 +28,9 @@

const poolAllocationRequest = [{ poolId: 1, coverAmountInAsset: coverFixture.amount }];

describe('buyCoverWithRi', function () {
const defaultAbiCoder = AbiCoder.defaultAbiCoder();

describe.only('buyCoverWithRi', function () {

Check failure on line 33 in test/unit/Cover/buyCoverWithRi.js

View workflow job for this annotation

GitHub Actions / lint

Unexpected exclusive mocha test
it('should purchase new cover with ri', async function () {
const fixture = await loadFixture(setup);
const { cover, pool, riSigner, riPremiumDst, riProviderId } = fixture;
Expand All @@ -54,6 +56,12 @@
ipfsData: '',
};

const timestamp = await time.latest();
const deadline = timestamp + 30 * 60;
const data = [{ amount: riAmount, riPoolId: 1, providerId: riProviderId }];
const dataFormat = 1;
const dataEncoded = defaultAbiCoder.encode(['tuple(uint256 amount,uint256 riPoolId,uint256 providerId)[]'], [data]);

const riQuote = {
coverId: 0,
productId,
Expand All @@ -62,23 +70,30 @@
premium: riPremium,
period,
coverAsset,
data: dataEncoded,
dataFormat,
deadline,
nonce: 0,
};

const riRequest = {
providerId: riProviderId,
amount: riAmount,
premium: riPremium,
deadline,
data: dataEncoded,
dataFormat,
signature: await signRiQuote(riSigner, cover, riQuote),
};

const coverContractBalanceBefore = await ethers.provider.getBalance(cover.target);
const poolEthBalanceBefore = await ethers.provider.getBalance(pool.target);
const riPremiumDstEthBalanceBefore = await ethers.provider.getBalance(riPremiumDst.address);

await cover
const tx = await cover
.connect(coverBuyer)
.buyCoverWithRi(coverParams, poolAllocationRequest, riRequest, { value: totalPremium });
await expect(tx).to.emit(cover, 'CoverRiAllocated').withArgs(dataEncoded, dataFormat);

expect(await ethers.provider.getBalance(cover.target)).to.be.equal(coverContractBalanceBefore);
expect(await ethers.provider.getBalance(pool.target)).to.equal(poolEthBalanceBefore + nativeCoverPremium);
Expand All @@ -97,4 +112,186 @@
expect(riData.providerId).to.equal(riProviderId);
expect(riData.amount).to.equal(riAmount);
});

it('should revert if the deadline expired', async function () {
const fixture = await loadFixture(setup);
const { cover, riSigner, riProviderId } = fixture;
const [coverBuyer] = fixture.accounts.members;

const { productId, coverAsset, amount, period, targetPriceRatio, priceDenominator } = coverFixture;
const { riAmount, riPremium } = riCoverFixture;

const nativeCoverPremium = (amount * targetPriceRatio * period) / (priceDenominator * 365n * 24n * 3600n);
const totalPremium = nativeCoverPremium + riPremium;

const coverParams = {
coverId: 0,
owner: coverBuyer,
productId,
coverAsset,
amount,
period,
maxPremiumInAsset: totalPremium,
paymentAsset: coverAsset,
commissionRatio: parseEther('0'),
commissionDestination: ZeroAddress,
ipfsData: '',
};

const timestamp = await time.latest();
const deadline = timestamp + 30 * 60;
const data = [{ providerId: riProviderId, riPoolId: 1, amount: riAmount }];
const dataFormat = 1;
const dataEncoded = defaultAbiCoder.encode(['tuple(uint256 amount,uint256 riPoolId,uint256 providerId)[]'], [data]);

const riQuote = {
coverId: 0,
productId,
providerId: riProviderId,
amount: riAmount,
premium: riPremium,
period,
coverAsset,
data: dataEncoded,
dataFormat,
deadline,
nonce: 0,
};

const riRequest = {
providerId: riProviderId,
amount: riAmount,
premium: riPremium,
deadline,
data: dataEncoded,
dataFormat,
signature: await signRiQuote(riSigner, cover, riQuote),
};

await time.increase(60 * 30);

await expect(
cover.connect(coverBuyer).buyCoverWithRi(coverParams, poolAllocationRequest, riRequest, { value: totalPremium }),
).to.be.revertedWithCustomError(cover, 'SignatureExpired');
});

it('should revert if the ri amount is 0', async function () {
const fixture = await loadFixture(setup);
const { cover, riSigner, riProviderId } = fixture;
const [coverBuyer] = fixture.accounts.members;

const { productId, coverAsset, amount, period, targetPriceRatio, priceDenominator } = coverFixture;
const { riAmount, riPremium } = riCoverFixture;

const nativeCoverPremium = (amount * targetPriceRatio * period) / (priceDenominator * 365n * 24n * 3600n);
const totalPremium = nativeCoverPremium + riPremium;

const coverParams = {
coverId: 0,
owner: coverBuyer,
productId,
coverAsset,
amount,
period,
maxPremiumInAsset: totalPremium,
paymentAsset: coverAsset,
commissionRatio: parseEther('0'),
commissionDestination: ZeroAddress,
ipfsData: '',
};

const timestamp = await time.latest();
const deadline = timestamp + 30 * 60;
const data = [{ providerId: riProviderId, riPoolId: 1, amount: 0 }];
const dataFormat = 1;
const dataEncoded = defaultAbiCoder.encode(['tuple(uint256 amount,uint256 riPoolId,uint256 providerId)[]'], [data]);

const riQuote = {
coverId: 0,
productId,
providerId: riProviderId,
amount: riAmount,
premium: riPremium,
period,
coverAsset,
data: dataEncoded,
dataFormat,
deadline,
nonce: 0,
};

const riRequest = {
providerId: riProviderId,
amount: 0,
premium: riPremium,
data: dataEncoded,
dataFormat,
deadline,
signature: await signRiQuote(riSigner, cover, riQuote),
};

await expect(
cover.connect(coverBuyer).buyCoverWithRi(coverParams, poolAllocationRequest, riRequest, { value: totalPremium }),
).to.be.revertedWithCustomError(cover, 'RiAmountIsZero');
});

it('should revert if the payment amount is not cover amount', async function () {
const fixture = await loadFixture(setup);
const { cover, riSigner, riProviderId } = fixture;
const [coverBuyer] = fixture.accounts.members;

const { productId, coverAsset, amount, period, targetPriceRatio, priceDenominator } = coverFixture;
const { riAmount, riPremium } = riCoverFixture;

const nativeCoverPremium = (amount * targetPriceRatio * period) / (priceDenominator * 365n * 24n * 3600n);
const totalPremium = nativeCoverPremium + riPremium;

const coverParams = {
coverId: 0,
owner: coverBuyer,
productId,
coverAsset,
amount,
period,
maxPremiumInAsset: totalPremium,
paymentAsset: coverAsset + 1n,
commissionRatio: parseEther('0'),
commissionDestination: ZeroAddress,
ipfsData: '',
};

const timestamp = await time.latest();
const deadline = timestamp + 30 * 60;
const data = [{ providerId: riProviderId, riPoolId: 1, amount: riAmount }];
const dataFormat = 1;
const dataEncoded = defaultAbiCoder.encode(['tuple(uint256 amount,uint256 riPoolId,uint256 providerId)[]'], [data]);

const riQuote = {
coverId: 0,
productId,
providerId: riProviderId,
amount: riAmount,
premium: riPremium,
period,
coverAsset,
data: dataEncoded,
dataFormat,
deadline,
nonce: 0,
};

const riRequest = {
providerId: riProviderId,
amount: riAmount,
premium: riPremium,
deadline,
data: dataEncoded,
dataFormat,
signature: await signRiQuote(riSigner, cover, riQuote),
};

await expect(
cover.connect(coverBuyer).buyCoverWithRi(coverParams, poolAllocationRequest, riRequest, { value: totalPremium }),
).to.be.revertedWithCustomError(cover, 'InvalidPaymentAsset');
});
});
Loading