From 6536c248d722fdec96da1f11e874f57e77f574b6 Mon Sep 17 00:00:00 2001 From: Rocky Murdoch Date: Thu, 31 Jul 2025 06:55:30 +0300 Subject: [PATCH 01/18] feat: add payoutRedemptionPeriod to AssessmentData --- contracts/interfaces/IAssessment.sol | 7 ++++-- contracts/interfaces/IClaims.sol | 3 +-- contracts/mocks/generic/AssessmentGeneric.sol | 8 ++++-- .../mocks/modules/Claims/CLMockAssessment.sol | 12 +++++---- contracts/modules/assessment/Assessment.sol | 18 +++++++++---- contracts/modules/assessment/Claims.sol | 25 ++++++++----------- 6 files changed, 42 insertions(+), 31 deletions(-) diff --git a/contracts/interfaces/IAssessment.sol b/contracts/interfaces/IAssessment.sol index 322bfbfaf1..fbfdceb952 100644 --- a/contracts/interfaces/IAssessment.sol +++ b/contracts/interfaces/IAssessment.sol @@ -6,6 +6,7 @@ interface IAssessment { struct AssessmentData { uint16 assessingGroupId; uint32 cooldownPeriod; + uint32 payoutRedemptionPeriod; } struct AssessmentGroupView { @@ -22,6 +23,7 @@ interface IAssessment { struct Assessment { uint16 assessingGroupId; uint32 cooldownPeriod; + uint32 payoutRedemptionPeriod; uint32 start; uint32 votingEnd; uint8 acceptVotes; @@ -49,6 +51,7 @@ interface IAssessment { function setAssessmentDataForProductTypes( uint[] calldata productTypeIds, uint cooldownPeriod, + uint redemptionPeriod, uint groupId ) external; @@ -82,7 +85,7 @@ interface IAssessment { function payoutCooldown(uint productTypeId) external view returns (uint); - function getAssessmentResult(uint claimId) external view returns(uint cooldownEnd, AssessmentStatus status); + function getAssessmentResult(uint claimId) external view returns(AssessmentStatus status, uint payoutRedemptionPeriod, uint cooldownEnd); function ballotOf(uint claimId, uint assessorMemberId) external view returns (Ballot memory); @@ -92,7 +95,7 @@ interface IAssessment { /* ========= EVENTS ========== */ - event AssessmentDataForProductTypesSet(uint[] productTypeIds, uint cooldownPeriod, uint groupId); + event AssessmentDataForProductTypesSet(uint[] productTypeIds, uint cooldownPeriod, uint payoutRedemptionPeriod, uint groupId); event AssessorAddedToGroup(uint indexed groupId, uint assessorMemberId); event AssessorRemovedFromGroup(uint indexed groupId, uint assessorMemberId); event GroupMetadataSet(uint indexed groupId, bytes32 ipfsMetadata); diff --git a/contracts/interfaces/IClaims.sol b/contracts/interfaces/IClaims.sol index 5c0bd33959..6bf123d36f 100644 --- a/contracts/interfaces/IClaims.sol +++ b/contracts/interfaces/IClaims.sol @@ -34,6 +34,7 @@ interface IClaims { uint assessmentVotingEnd; uint assessmentCooldownEnd; uint assessmentStatus; + uint payoutRedemptionEnd; bool payoutRedeemed; } @@ -41,8 +42,6 @@ interface IClaims { function getClaimInfo(uint claimId) external view returns (Claim memory); - function getPayoutRedemptionPeriod() external view returns (uint); - function getClaimsCount() external view returns (uint); /* === MUTATIVE FUNCTIONS ==== */ diff --git a/contracts/mocks/generic/AssessmentGeneric.sol b/contracts/mocks/generic/AssessmentGeneric.sol index c46d9cde54..bffe7ccdd2 100644 --- a/contracts/mocks/generic/AssessmentGeneric.sol +++ b/contracts/mocks/generic/AssessmentGeneric.sol @@ -21,7 +21,7 @@ contract AssessmentGeneric is IAssessment { revert("Unsupported"); } - function setAssessmentDataForProductTypes(uint[] calldata, uint, uint) external virtual { + function setAssessmentDataForProductTypes(uint[] calldata, uint, uint, uint) external virtual { revert("Unsupported"); } @@ -99,7 +99,11 @@ contract AssessmentGeneric is IAssessment { revert("Unsupported"); } - function getAssessmentResult(uint) external virtual view returns (uint, AssessmentStatus) { + function getAssessmentDataForProductType(uint) external virtual view returns (AssessmentData memory) { + revert("Unsupported"); + } + + function getAssessmentResult(uint) external virtual view returns (AssessmentStatus, uint, uint) { revert("Unsupported"); } diff --git a/contracts/mocks/modules/Claims/CLMockAssessment.sol b/contracts/mocks/modules/Claims/CLMockAssessment.sol index 9a8d5f7a78..1295898d20 100644 --- a/contracts/mocks/modules/Claims/CLMockAssessment.sol +++ b/contracts/mocks/modules/Claims/CLMockAssessment.sol @@ -7,21 +7,23 @@ import "../../generic/AssessmentGeneric.sol"; contract CLMockAssessment is AssessmentGeneric { - mapping(uint claimId => uint) public _cooldown; mapping(uint claimId => AssessmentStatus) public _status; + mapping(uint claimId => uint) public _payoutRedemptionEnd; + mapping(uint claimId => uint) public _cooldownEnd; mapping(uint claimId => uint) public _productTypeForClaimId; function startAssessment(uint claimId, uint productTypeId) external override { _productTypeForClaimId[claimId] = productTypeId; } - function getAssessmentResult(uint claimId) external override view returns (uint, AssessmentStatus) { - return(_cooldown[claimId], _status[claimId]); + function getAssessmentResult(uint claimId) external override view returns (AssessmentStatus, uint, uint) { + return(_status[claimId], _payoutRedemptionEnd[claimId], _cooldownEnd[claimId]); } - function setAssessmentResult(uint claimId, uint cooldown, AssessmentStatus status) external { - _cooldown[claimId] = cooldown; + function setAssessmentResult(uint claimId, AssessmentStatus status, uint payoutRedemptionEnd, uint cooldownEnd) external { _status[claimId] = status; + _payoutRedemptionEnd[claimId] = payoutRedemptionEnd; + _cooldownEnd[claimId] = cooldownEnd; } } diff --git a/contracts/modules/assessment/Assessment.sol b/contracts/modules/assessment/Assessment.sol index ec42eed6e5..eda484fff7 100644 --- a/contracts/modules/assessment/Assessment.sol +++ b/contracts/modules/assessment/Assessment.sol @@ -170,19 +170,22 @@ contract Assessment is IAssessment, RegistryAware, Multicall { function setAssessmentDataForProductTypes( uint[] calldata productTypeIds, uint cooldownPeriod, + uint payoutRedemptionPeriod, uint groupId ) override external onlyContracts(C_GOVERNOR) { + require(groupId > 0 && groupId <= _groupCount, InvalidGroupId()); uint length = productTypeIds.length; for (uint i = 0; i < length; i++) { _assessmentData[productTypeIds[i]] = AssessmentData({ assessingGroupId: groupId.toUint16(), - cooldownPeriod: cooldownPeriod.toUint32() + cooldownPeriod: cooldownPeriod.toUint32(), + payoutRedemptionPeriod: payoutRedemptionPeriod.toUint32() }); } - emit AssessmentDataForProductTypesSet(productTypeIds, cooldownPeriod, groupId); + emit AssessmentDataForProductTypesSet(productTypeIds, cooldownPeriod, payoutRedemptionPeriod, groupId); } /// @notice Undoes votes cast by an assessor on multiple claims @@ -244,14 +247,18 @@ contract Assessment is IAssessment, RegistryAware, Multicall { /// @notice Returns the assessment result and cooldown end time for a claim /// @param claimId The ID of the claim to query - /// @return cooldownEnd Timestamp when the cooldown period ends /// @return status Current status of the assessment (VOTING, COOLDOWN, ACCEPTED, DENIED, DRAW) - function getAssessmentResult(uint claimId) override external view returns(uint cooldownEnd, AssessmentStatus status) { + /// @return payoutRedemptionEnd Timestamp when the payout redemption period ends + /// @return cooldownEnd Timestamp when the cooldown period ends + function getAssessmentResult(uint claimId) override external view returns(AssessmentStatus status, uint payoutRedemptionEnd, uint cooldownEnd) { + Assessment memory assessment = _assessments[claimId]; require(assessment.start != 0, InvalidClaimId()); cooldownEnd = assessment.votingEnd + assessment.cooldownPeriod; - return (cooldownEnd, _getAssessmentStatus(assessment)); + payoutRedemptionEnd = cooldownEnd + assessment.payoutRedemptionPeriod; + + return (_getAssessmentStatus(assessment), payoutRedemptionEnd, cooldownEnd); } /// @notice Determines the current status of an assessment based on timing and votes @@ -316,6 +323,7 @@ contract Assessment is IAssessment, RegistryAware, Multicall { _assessments[claimId] = Assessment({ assessingGroupId: assessmentData.assessingGroupId, cooldownPeriod: assessmentData.cooldownPeriod, + payoutRedemptionPeriod: assessmentData.payoutRedemptionPeriod, start: startTime, votingEnd: votingEndTime, acceptVotes: 0, diff --git a/contracts/modules/assessment/Claims.sol b/contracts/modules/assessment/Claims.sol index ba41c1f7dd..40438eb8c2 100644 --- a/contracts/modules/assessment/Claims.sol +++ b/contracts/modules/assessment/Claims.sol @@ -39,8 +39,6 @@ contract Claims is IClaims, RegistryAware { /* =========== CONSTANTS =========== */ - uint constant public PAYOUT_REDEMPTION_PERIOD = 30 days; - // NOTE: when updating the deposit value, make sure there are no open claims during the upgrade uint constant public CLAIM_DEPOSIT_IN_ETH = 0.05 ether; // TODO: confirm if claim deposit is to be dropped @@ -70,10 +68,6 @@ contract Claims is IClaims, RegistryAware { return _claims[claimId]; } - function getPayoutRedemptionPeriod() external override pure returns (uint) { - return PAYOUT_REDEMPTION_PERIOD; - } - /// Returns a Claim aggregated in a human-friendly format. /// /// @dev This view is meant to be used in user interfaces to get a claim in a format suitable for @@ -83,7 +77,7 @@ contract Claims is IClaims, RegistryAware { function getClaimDisplay(uint claimId) internal view returns (ClaimDisplay memory) { Claim memory claim = _claims[claimId]; - (uint cooldownEnd, IAssessment.AssessmentStatus assessmentStatus) = assessment.getAssessmentResult(claimId); + (IAssessment.AssessmentStatus assessmentStatus, uint payoutRedemptionEnd, uint cooldownEnd) = assessment.getAssessmentResult(claimId); (IAssessment.Assessment memory claimAssessment) = assessment.getAssessment(claimId); CoverData memory coverData = cover.getCoverData(claim.coverId); @@ -116,6 +110,7 @@ contract Claims is IClaims, RegistryAware { claimAssessment.votingEnd, cooldownEnd, uint(assessmentStatus), + payoutRedemptionEnd, claim.payoutRedeemed ); } @@ -162,7 +157,7 @@ contract Claims is IClaims, RegistryAware { uint previousSubmission = lastClaimSubmissionOnCover[coverId]; if (previousSubmission > 0) { - (uint cooldownEnd, IAssessment.AssessmentStatus status) = assessment.getAssessmentResult(previousSubmission); + (IAssessment.AssessmentStatus status, uint payoutRedemptionEnd, /* cooldownEnd */) = assessment.getAssessmentResult(previousSubmission); require( status != IAssessment.AssessmentStatus.VOTING && @@ -180,7 +175,7 @@ contract Claims is IClaims, RegistryAware { // if payout not yet redeemed and still within redemption period require( - block.timestamp >= cooldownEnd + PAYOUT_REDEMPTION_PERIOD, + block.timestamp >= payoutRedemptionEnd, PayoutCanStillBeRedeemed() ); } @@ -239,9 +234,9 @@ contract Claims is IClaims, RegistryAware { /// /// @param claimId Claim identifier function redeemClaimPayout(uint claimId) external override whenNotPaused(PAUSE_CLAIMS_PAYOUT) { - (Claim memory claim, uint cooldownEnd) = _validateClaimStatus(claimId, IAssessment.AssessmentStatus.ACCEPTED); + (Claim memory claim, uint payoutRedemptionEnd) = _validateClaimStatus(claimId, IAssessment.AssessmentStatus.ACCEPTED); - require(block.timestamp < cooldownEnd + PAYOUT_REDEMPTION_PERIOD, RedemptionPeriodExpired()); + require(block.timestamp < payoutRedemptionEnd, RedemptionPeriodExpired()); require(!claim.payoutRedeemed, PayoutAlreadyRedeemed()); _claims[claimId].payoutRedeemed = true; @@ -266,7 +261,7 @@ contract Claims is IClaims, RegistryAware { /// /// @param claimId The unique identifier of the claim for which the deposit is being retrieved. function retrieveDeposit(uint claimId) external override whenNotPaused(PAUSE_CLAIMS_PAYOUT) { - (Claim memory claim, /* cooldownEnd */) = _validateClaimStatus(claimId, IAssessment.AssessmentStatus.DRAW); + (Claim memory claim, /* payoutRedemptionEnd */) = _validateClaimStatus(claimId, IAssessment.AssessmentStatus.DRAW); require(!claim.depositRetrieved, DepositAlreadyRetrieved()); @@ -282,14 +277,14 @@ contract Claims is IClaims, RegistryAware { function _validateClaimStatus( uint claimId, IAssessment.AssessmentStatus expectedStatus - ) internal view returns (Claim memory claim, uint cooldownEnd) { + ) internal view returns (Claim memory claim, uint payoutRedemptionEnd) { claim = _claims[claimId]; require(claim.amount > 0, InvalidClaimId()); IAssessment.AssessmentStatus status; - (cooldownEnd, status) = assessment.getAssessmentResult(claimId); + (status, payoutRedemptionEnd, /* cooldownEnd */) = assessment.getAssessmentResult(claimId); require(status == expectedStatus, InvalidAssessmentStatus()); - return (claim, cooldownEnd); + return (claim, payoutRedemptionEnd); } } From a6e01ff43bf9ea32a258d3941c903173f7eb1fc5 Mon Sep 17 00:00:00 2001 From: Rocky Murdoch Date: Fri, 1 Aug 2025 04:15:03 +0300 Subject: [PATCH 02/18] feat: add onlyMember modifier to RegistryAware * use onlyMember modifier on submitClaim and redeemClaimPayout --- contracts/abstract/RegistryAware.sol | 6 ++++++ contracts/interfaces/IAssessment.sol | 1 - contracts/interfaces/IClaims.sol | 2 +- contracts/interfaces/IGovernor.sol | 1 - contracts/interfaces/IMemberRolesErrors.sol | 1 - contracts/modules/assessment/Claims.sol | 6 +++--- 6 files changed, 10 insertions(+), 7 deletions(-) diff --git a/contracts/abstract/RegistryAware.sol b/contracts/abstract/RegistryAware.sol index eb84620627..d6e9ebc2b9 100644 --- a/contracts/abstract/RegistryAware.sol +++ b/contracts/abstract/RegistryAware.sol @@ -38,6 +38,7 @@ contract RegistryAware { error Paused(uint currentState, uint checks); error Unauthorized(address caller, uint callerIndex, uint authorizedBitmap); + error OnlyMember(); modifier whenNotPaused(uint mask) { uint config = registry.getPauseConfig(); @@ -55,6 +56,11 @@ contract RegistryAware { _; } + modifier onlyMember() { + require(registry.isMember(msg.sender), OnlyMember()); + _; + } + // TODO: find a better short name for this function function fetch(uint index) internal view returns (address) { return registry.getContractAddressByIndex(index); diff --git a/contracts/interfaces/IAssessment.sol b/contracts/interfaces/IAssessment.sol index fbfdceb952..08d5fd8d42 100644 --- a/contracts/interfaces/IAssessment.sol +++ b/contracts/interfaces/IAssessment.sol @@ -130,7 +130,6 @@ interface IAssessment { error InvalidGroupId(); error InvalidMemberId(); error InvalidProductType(); - error OnlyMember(); error VotingPeriodEnded(); error AssessmentCooldownPassed(uint claimId); error HasNotVoted(uint claimId); diff --git a/contracts/interfaces/IClaims.sol b/contracts/interfaces/IClaims.sol index 6bf123d36f..1a29dcb63c 100644 --- a/contracts/interfaces/IClaims.sol +++ b/contracts/interfaces/IClaims.sol @@ -68,6 +68,7 @@ interface IClaims { error ClaimIsBeingAssessed(); error PayoutCanStillBeRedeemed(); error ClaimAlreadyPaidOut(); + error OnlyOwnerCanSubmitClaim(); error OnlyOwnerOrApprovedCanSubmitClaim(); error InvalidClaimMethod(); error CoveredAmountExceeded(); @@ -79,7 +80,6 @@ interface IClaims { error RedemptionPeriodExpired(); error PayoutAlreadyRedeemed(); error DepositAlreadyRetrieved(); - error OnlyMember(); error InvalidClaimId(); error AlreadyInitialized(); } diff --git a/contracts/interfaces/IGovernor.sol b/contracts/interfaces/IGovernor.sol index 6f2252413b..f00e9d4058 100644 --- a/contracts/interfaces/IGovernor.sol +++ b/contracts/interfaces/IGovernor.sol @@ -66,7 +66,6 @@ interface IGovernor { error AlreadyAdvisoryBoardMember(); error OnlyAdvisoryBoardMember(); error OnlyGovernor(); - error OnlyMember(); error NotMember(); error NotAuthorizedToVote(); diff --git a/contracts/interfaces/IMemberRolesErrors.sol b/contracts/interfaces/IMemberRolesErrors.sol index 4315e9fda2..dcefda949c 100644 --- a/contracts/interfaces/IMemberRolesErrors.sol +++ b/contracts/interfaces/IMemberRolesErrors.sol @@ -12,7 +12,6 @@ interface IMemberRolesErrors { error SignatureAlreadyUsed(); error InvalidSignature(); error TransferToPoolFailed(); - error OnlyMember(); error LockedForVoting(); error CantBeStakingPoolManager(); error HasNXMStakedInClaimAssessmentV1(); diff --git a/contracts/modules/assessment/Claims.sol b/contracts/modules/assessment/Claims.sol index 40438eb8c2..ba45c0af3c 100644 --- a/contracts/modules/assessment/Claims.sol +++ b/contracts/modules/assessment/Claims.sol @@ -147,8 +147,7 @@ contract Claims is IClaims, RegistryAware { uint32 coverId, uint96 requestedAmount, bytes32 ipfsMetadata - ) external payable override returns (Claim memory claim) { - require(registry.isMember(msg.sender), OnlyMember()); + ) external payable override onlyMember returns (Claim memory claim) { require(coverNFT.isApprovedOrOwner(msg.sender, coverId), OnlyOwnerOrApprovedCanSubmitClaim()); uint claimId = _nextClaimId++; @@ -233,7 +232,8 @@ contract Claims is IClaims, RegistryAware { /// @dev Can be called by anyone, but the payout and deposit are always transferred to the current cover NFT owner. /// /// @param claimId Claim identifier - function redeemClaimPayout(uint claimId) external override whenNotPaused(PAUSE_CLAIMS_PAYOUT) { + function redeemClaimPayout(uint claimId) external override onlyMember whenNotPaused(PAUSE_CLAIMS_PAYOUT) { + (Claim memory claim, uint payoutRedemptionEnd) = _validateClaimStatus(claimId, IAssessment.AssessmentStatus.ACCEPTED); require(block.timestamp < payoutRedemptionEnd, RedemptionPeriodExpired()); From 0b1ac921655fae83c4d6863cec515c9a508338b3 Mon Sep 17 00:00:00 2001 From: Rocky Murdoch Date: Fri, 1 Aug 2025 04:26:11 +0300 Subject: [PATCH 03/18] feat: add getAssessmentDataForProductType --- contracts/interfaces/IAssessment.sol | 2 ++ contracts/modules/assessment/Assessment.sol | 7 +++++++ 2 files changed, 9 insertions(+) diff --git a/contracts/interfaces/IAssessment.sol b/contracts/interfaces/IAssessment.sol index 08d5fd8d42..14f3455015 100644 --- a/contracts/interfaces/IAssessment.sol +++ b/contracts/interfaces/IAssessment.sol @@ -85,6 +85,8 @@ interface IAssessment { function payoutCooldown(uint productTypeId) external view returns (uint); + function getAssessmentDataForProductType(uint productTypeId) external view returns (AssessmentData memory assessmentData); + function getAssessmentResult(uint claimId) external view returns(AssessmentStatus status, uint payoutRedemptionPeriod, uint cooldownEnd); function ballotOf(uint claimId, uint assessorMemberId) external view returns (Ballot memory); diff --git a/contracts/modules/assessment/Assessment.sol b/contracts/modules/assessment/Assessment.sol index eda484fff7..8caff7f3ca 100644 --- a/contracts/modules/assessment/Assessment.sol +++ b/contracts/modules/assessment/Assessment.sol @@ -232,6 +232,13 @@ contract Assessment is IAssessment, RegistryAware, Multicall { return assessmentData.cooldownPeriod; } + /// @dev Returns assessment data for a given product type + /// @param productTypeId The product type identifier + /// @return assessmentData The assessment data including assessing group ID, cooldown period, and redemption period + function getAssessmentDataForProductType(uint productTypeId) external view override returns (AssessmentData memory assessmentData) { + return _assessmentData[productTypeId]; + } + /// @notice Returns the full assessment data for a claim /// @param claimId The ID of the claim to query /// @return assessment The complete assessment data including votes and timing From cbc7c1a634ef401b157b3b617cd899de00c87d58 Mon Sep 17 00:00:00 2001 From: Rocky Murdoch Date: Fri, 1 Aug 2025 04:41:13 +0300 Subject: [PATCH 04/18] feat: only coverNFT owner can call submitClaim/redeemClaimPayout --- contracts/modules/assessment/Claims.sol | 2 ++ 1 file changed, 2 insertions(+) diff --git a/contracts/modules/assessment/Claims.sol b/contracts/modules/assessment/Claims.sol index ba45c0af3c..c8a436c170 100644 --- a/contracts/modules/assessment/Claims.sol +++ b/contracts/modules/assessment/Claims.sol @@ -149,6 +149,7 @@ contract Claims is IClaims, RegistryAware { bytes32 ipfsMetadata ) external payable override onlyMember returns (Claim memory claim) { require(coverNFT.isApprovedOrOwner(msg.sender, coverId), OnlyOwnerOrApprovedCanSubmitClaim()); + require(coverNFT.ownerOf(coverId) == msg.sender, OnlyOwnerCanSubmitClaim()); uint claimId = _nextClaimId++; @@ -236,6 +237,7 @@ contract Claims is IClaims, RegistryAware { (Claim memory claim, uint payoutRedemptionEnd) = _validateClaimStatus(claimId, IAssessment.AssessmentStatus.ACCEPTED); + require(coverNFT.ownerOf(claim.coverId) == msg.sender, OnlyOwnerCanSubmitClaim()); require(block.timestamp < payoutRedemptionEnd, RedemptionPeriodExpired()); require(!claim.payoutRedeemed, PayoutAlreadyRedeemed()); From 0d3fdf0dabe34f51eddd87b7f74cd88985edb30f Mon Sep 17 00:00:00 2001 From: Rocky Murdoch Date: Fri, 1 Aug 2025 04:42:58 +0300 Subject: [PATCH 05/18] style: fix newlines, natspec and clean up comments --- contracts/modules/assessment/Assessment.sol | 51 +++++++++++++-------- contracts/modules/assessment/Claims.sol | 10 +++- 2 files changed, 39 insertions(+), 22 deletions(-) diff --git a/contracts/modules/assessment/Assessment.sol b/contracts/modules/assessment/Assessment.sol index 8caff7f3ca..5611a1f6b2 100644 --- a/contracts/modules/assessment/Assessment.sol +++ b/contracts/modules/assessment/Assessment.sol @@ -73,9 +73,9 @@ contract Assessment is IAssessment, RegistryAware, Multicall { groupIds = _groupsForAssessor[assessorMemberId].values(); } - /// @notice Checks if a given member ID belongs to at least one assessor group. - /// @param assessorMemberId The ID of the member to check. - /// @return True if the member is an assessor, false otherwise. + /// @notice Checks if a given member ID belongs to at least one assessor group + /// @param assessorMemberId The ID of the member to check + /// @return True if the member is an assessor, false otherwise function isAssessor(uint assessorMemberId) override external view returns (bool) { return _groupsForAssessor[assessorMemberId].length() > 0; } @@ -84,6 +84,7 @@ contract Assessment is IAssessment, RegistryAware, Multicall { /// @param groupIds Array of group IDs to query /// @return groups Array of group data including metadata and assessors function getGroupsData(uint[] calldata groupIds) override external view returns (AssessmentGroupView[] memory groups) { + uint length = groupIds.length; groups = new AssessmentGroupView[](length); @@ -106,6 +107,7 @@ contract Assessment is IAssessment, RegistryAware, Multicall { /// @param groupId Target group ID (0 creates new group) /// @dev Only callable by governor contract function addAssessorsToGroup(uint[] calldata assessorMemberIds, uint groupId) override external onlyContracts(C_GOVERNOR) { + // make new group id if (groupId == 0) { groupId = ++_groupCount; @@ -137,11 +139,13 @@ contract Assessment is IAssessment, RegistryAware, Multicall { /// @param groupId The ID of the group to remove from /// @dev Only callable by governor contract function removeAssessorFromGroup(uint assessorMemberId, uint groupId) override external onlyContracts(C_GOVERNOR) { - require(groupId > 0 && groupId <= _groupCount, InvalidGroupId()); + require(groupId > 0 && groupId <= _groupCount, InvalidGroupId()); require(assessorMemberId != 0, InvalidMemberId()); + _groups[groupId].remove(assessorMemberId); _groupsForAssessor[assessorMemberId].remove(groupId); + emit AssessorRemovedFromGroup(groupId, assessorMemberId); } @@ -149,10 +153,12 @@ contract Assessment is IAssessment, RegistryAware, Multicall { /// @param assessorMemberId The member ID of the assessor to remove /// @dev Only callable by governor contract function removeAssessorFromAllGroups(uint assessorMemberId) override external onlyContracts(C_GOVERNOR) { + require(assessorMemberId != 0, InvalidMemberId()); uint[] memory assessorsGroups = _groupsForAssessor[assessorMemberId].values(); uint assessorsGroupsLength = assessorsGroups.length; + for (uint groupIndex = 0; groupIndex < assessorsGroupsLength; groupIndex++) { uint groupId = assessorsGroups[groupIndex]; _groups[groupId].remove(assessorMemberId); @@ -193,6 +199,7 @@ contract Assessment is IAssessment, RegistryAware, Multicall { /// @param claimIds Array of claim IDs to undo votes for /// @dev Only callable by governor contract, must be within cooldown period function undoVotes(uint assessorMemberId, uint[] calldata claimIds) override external onlyContracts(C_GOVERNOR) { + uint len = claimIds.length; for (uint i = 0; i < len; i++) { uint claimId = claimIds[i]; @@ -273,6 +280,7 @@ contract Assessment is IAssessment, RegistryAware, Multicall { /// @return status The current assessment status /// @dev Internal helper function for status calculation function _getAssessmentStatus(Assessment memory assessment) internal view returns(AssessmentStatus status) { + if (block.timestamp < assessment.votingEnd) { return AssessmentStatus.VOTING; } @@ -318,9 +326,9 @@ contract Assessment is IAssessment, RegistryAware, Multicall { /// @dev Only callable by internal contracts /// @dev Reverts if an assessment already exists for the given claimId function startAssessment(uint claimId, uint productTypeId) override external onlyContracts(C_CLAIMS) { + require(_assessments[claimId].start == 0, AssessmentAlreadyExists()); - // validate that assessment data exists for the product type AssessmentData memory assessmentData = _assessmentData[productTypeId]; require(assessmentData.assessingGroupId != 0, InvalidProductType()); @@ -340,24 +348,25 @@ contract Assessment is IAssessment, RegistryAware, Multicall { emit AssessmentStarted(claimId, assessmentData.assessingGroupId, startTime, votingEndTime); } - /// @notice Allows an assessor to cast a vote on a claim. - /// @dev Requires the caller to be a valid assessor for the claim's assigned group. - /// Reverts if the voting period has ended or if the assessor has already voted. - /// @param claimId The unique identifier for the claim to vote on. - /// @param voteSupport The assessor's vote; `true` to accept the claim, `false` to deny it. - /// @param ipfsHash An IPFS hash containing off-chain metadata or reasoning for the vote. + /// @notice Allows an assessor to cast a vote on a claim + /// @dev Requires the caller to be a valid assessor for the claim's assigned group + /// Reverts if the voting period has ended or if the assessor has already voted + /// @param claimId The unique identifier for the claim to vote on + /// @param voteSupport The assessor's vote; `true` to accept the claim, `false` to deny it + /// @param ipfsHash An IPFS hash containing off-chain metadata or reasoning for the vote function castVote(uint claimId, bool voteSupport, bytes32 ipfsHash) override external whenNotPaused(PAUSE_ASSESSMENTS) { - // Validate caller is a member and get their member ID + uint assessorMemberId = registry.getMemberId(msg.sender); require(assessorMemberId > 0, OnlyMember()); - // Get assessment data and validate claim exists + // validate claim exists Assessment memory assessment = _assessments[claimId]; require(assessment.start != 0, InvalidClaimId()); - // Validate assessor is in the correct group for this claim + // validate voter is a valid assessor for this claim require(_groups[assessment.assessingGroupId].contains(assessorMemberId), InvalidAssessor()); + // validate voting period is still open and assessor has not voted yet require(block.timestamp < assessment.votingEnd, VotingPeriodEnded()); require(_ballots[assessorMemberId][claimId].timestamp == 0, AlreadyVoted()); @@ -378,11 +387,12 @@ contract Assessment is IAssessment, RegistryAware, Multicall { emit VoteCast(claimId, msg.sender, assessorMemberId, voteSupport, ipfsHash); } - /// @notice Allows for the early closing of a claim's voting period. - /// @dev Can only be called if all assigned assessors have cast their votes. - /// Sets the assessment's `votingEnd` to the current block timestamp. + /// @notice Allows for the early closing of a claim's voting period + /// @dev Can only be called if all assigned assessors have cast their votes + /// Sets the assessment's `votingEnd` to the current block timestamp /// @param claimId The unique identifier for the claim. function closeVotingEarly(uint claimId) override external { + Assessment memory assessment = _assessments[claimId]; require(assessment.start != 0, InvalidClaimId()); require(block.timestamp < assessment.votingEnd, VotingAlreadyClosed()); @@ -403,10 +413,11 @@ contract Assessment is IAssessment, RegistryAware, Multicall { emit VotingEndChanged(claimId, assessment.votingEnd); } - /// @notice Extends the voting period for a claim, starting a new full voting window. - /// @dev Can only be called by the Governor contract. Reverts if the assessment's cooldown period has already passed. - /// @param claimId The unique identifier for the claim. + /// @notice Extends the voting period for a claim, starting a new full voting window + /// @dev Can only be called by the Governor contract. Reverts if the assessment's cooldown period has already passed + /// @param claimId The unique identifier for the claim function extendVotingPeriod(uint claimId) override external onlyContracts(C_GOVERNOR) { + Assessment memory assessment = _assessments[claimId]; require(assessment.start != 0, InvalidClaimId()); diff --git a/contracts/modules/assessment/Claims.sol b/contracts/modules/assessment/Claims.sol index c8a436c170..49a78ff132 100644 --- a/contracts/modules/assessment/Claims.sol +++ b/contracts/modules/assessment/Claims.sol @@ -75,6 +75,7 @@ contract Claims is IClaims, RegistryAware { /// /// @param claimId Claim identifier for which the ClaimDisplay is returned function getClaimDisplay(uint claimId) internal view returns (ClaimDisplay memory) { + Claim memory claim = _claims[claimId]; (IAssessment.AssessmentStatus assessmentStatus, uint payoutRedemptionEnd, uint cooldownEnd) = assessment.getAssessmentResult(claimId); @@ -123,11 +124,14 @@ contract Claims is IClaims, RegistryAware { /// /// @param ids Array of Claim ids which are returned as ClaimDisplay function getClaimsToDisplay (uint[] calldata ids) external view returns (ClaimDisplay[] memory) { + ClaimDisplay[] memory claimDisplays = new ClaimDisplay[](ids.length); + for (uint i = 0; i < ids.length; i++) { uint id = ids[i]; claimDisplays[i] = getClaimDisplay(id); } + return claimDisplays; } @@ -136,7 +140,7 @@ contract Claims is IClaims, RegistryAware { /// Submits a claim for assessment for a specific cover /// /// @dev Requires a claim deposit fee. See: CLAIM_DEPOSIT_IN_ETH - /// @dev Requires the sender to be a member and the owner or approved operator of the cover NFT. + /// @dev Requires the sender to be a member and the owner or approved operator of the cover NFT /// /// @param coverId Cover identifier /// @param requestedAmount The amount expected to be received at payout @@ -230,7 +234,7 @@ contract Claims is IClaims, RegistryAware { /// Redeems payouts and sends assessment deposit back for accepted claims /// - /// @dev Can be called by anyone, but the payout and deposit are always transferred to the current cover NFT owner. + /// @dev Must be the cover NFT owner for the claim and a member can call this function /// /// @param claimId Claim identifier function redeemClaimPayout(uint claimId) external override onlyMember whenNotPaused(PAUSE_CLAIMS_PAYOUT) { @@ -263,6 +267,7 @@ contract Claims is IClaims, RegistryAware { /// /// @param claimId The unique identifier of the claim for which the deposit is being retrieved. function retrieveDeposit(uint claimId) external override whenNotPaused(PAUSE_CLAIMS_PAYOUT) { + (Claim memory claim, /* payoutRedemptionEnd */) = _validateClaimStatus(claimId, IAssessment.AssessmentStatus.DRAW); require(!claim.depositRetrieved, DepositAlreadyRetrieved()); @@ -280,6 +285,7 @@ contract Claims is IClaims, RegistryAware { uint claimId, IAssessment.AssessmentStatus expectedStatus ) internal view returns (Claim memory claim, uint payoutRedemptionEnd) { + claim = _claims[claimId]; require(claim.amount > 0, InvalidClaimId()); From 5a64c4190836ba4aa3db7b19a8595bcf225f4db7 Mon Sep 17 00:00:00 2001 From: Rocky Murdoch Date: Fri, 1 Aug 2025 04:44:57 +0300 Subject: [PATCH 06/18] test: add payoutRedemptionPeriod/End tests --- test/unit/Assessment/closeVotingEarly.js | 9 +- test/unit/Assessment/getAssessmentResult.js | 157 +++++++++++------- .../setAssessmentDataForProductTypes.js | 156 ++++++++++++++--- 3 files changed, 236 insertions(+), 86 deletions(-) diff --git a/test/unit/Assessment/closeVotingEarly.js b/test/unit/Assessment/closeVotingEarly.js index df73ae9d2a..2cb8087007 100644 --- a/test/unit/Assessment/closeVotingEarly.js +++ b/test/unit/Assessment/closeVotingEarly.js @@ -154,16 +154,19 @@ describe('closeVotingEarly', function () { expect(assessmentData.denyVotes).to.equal(0); // Advance time past the cooldown period to see DRAW status - const cooldownPeriod = assessmentData.cooldownPeriod; + const { cooldownPeriod, payoutRedemptionPeriod } = assessmentData; await setTime(Number(BigInt(closeVotingTimestamp) + BigInt(cooldownPeriod) + 1n)); // Check assessment result after cooldown period - const [cooldownEnd, status] = await assessment.getAssessmentResult(CLAIM_ID); + const [status, payoutRedemptionEnd, cooldownEnd] = await assessment.getAssessmentResult(CLAIM_ID); - // Verify cooldown end calculation + // Verify cooldown end and payout redemption end calculation const expectedCooldownEnd = BigInt(closeVotingTimestamp) + BigInt(cooldownPeriod); expect(cooldownEnd).to.equal(expectedCooldownEnd); + const expectedPayoutRedemptionEnd = expectedCooldownEnd + BigInt(payoutRedemptionPeriod); + expect(payoutRedemptionEnd).to.equal(expectedPayoutRedemptionEnd); + // Verify status is DRAW (0 accept == 0 deny votes) const AssessmentStatus = { VOTING: 0, diff --git a/test/unit/Assessment/getAssessmentResult.js b/test/unit/Assessment/getAssessmentResult.js index 99ee0a0c31..60de1a9744 100644 --- a/test/unit/Assessment/getAssessmentResult.js +++ b/test/unit/Assessment/getAssessmentResult.js @@ -1,8 +1,11 @@ +const { ethers } = require('hardhat'); const { expect } = require('chai'); const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); const { setup } = require('./setup'); const { setTime } = require('./helpers'); +const ONE_DAY = 24 * 60 * 60; + const AssessmentStatus = { VOTING: 0, COOLDOWN: 1, @@ -28,11 +31,13 @@ describe('getAssessmentResult', function () { const { assessment } = contracts; const { CLAIM_ID } = constants; - const [cooldownEnd, status] = await assessment.getAssessmentResult(CLAIM_ID); + const [status, payoutRedemptionEnd, cooldownEnd] = await assessment.getAssessmentResult(CLAIM_ID); const assessmentData = await assessment.getAssessment(CLAIM_ID); const expectedCooldownEnd = BigInt(assessmentData.votingEnd) + BigInt(assessmentData.cooldownPeriod); + const expectedPayoutRedemptionEnd = expectedCooldownEnd + BigInt(assessmentData.payoutRedemptionPeriod); expect(cooldownEnd).to.equal(expectedCooldownEnd); + expect(payoutRedemptionEnd).to.equal(expectedPayoutRedemptionEnd); expect(status).to.equal(AssessmentStatus.VOTING); }); @@ -49,15 +54,17 @@ describe('getAssessmentResult', function () { assessment.connect(assessor3).castVote(CLAIM_ID, false, IPFS_HASH), ]); - const assessmentData = await assessment.getAssessment(CLAIM_ID); - const expectedCooldownEndTime = BigInt(assessmentData.votingEnd) + BigInt(assessmentData.cooldownPeriod); + const { votingEnd, cooldownPeriod, payoutRedemptionPeriod } = await assessment.getAssessment(CLAIM_ID); + const expectedCooldownEnd = BigInt(votingEnd) + BigInt(cooldownPeriod); + const expectedPayoutRedemptionEnd = expectedCooldownEnd + BigInt(payoutRedemptionPeriod); // Set time to just after voting ends but before cooldown passes - await setTime(assessmentData.votingEnd + 1n); + await setTime(votingEnd + 1n); - const [cooldownEnd, status] = await assessment.getAssessmentResult(CLAIM_ID); + const [status, payoutRedemptionEnd, cooldownEnd] = await assessment.getAssessmentResult(CLAIM_ID); - expect(cooldownEnd).to.equal(expectedCooldownEndTime); + expect(cooldownEnd).to.equal(expectedCooldownEnd); + expect(payoutRedemptionEnd).to.equal(expectedPayoutRedemptionEnd); expect(status).to.equal(AssessmentStatus.COOLDOWN); }); @@ -74,14 +81,16 @@ describe('getAssessmentResult', function () { assessment.connect(assessor3).castVote(CLAIM_ID, false, IPFS_HASH), ]); - const assessmentData = await assessment.getAssessment(CLAIM_ID); - const cooldownEndTime = BigInt(assessmentData.votingEnd) + BigInt(assessmentData.cooldownPeriod); + const { votingEnd, cooldownPeriod, payoutRedemptionPeriod } = await assessment.getAssessment(CLAIM_ID); + const expectedCooldownEnd = BigInt(votingEnd) + BigInt(cooldownPeriod); + const expectedPayoutRedemptionEnd = expectedCooldownEnd + BigInt(payoutRedemptionPeriod); // Set time past cooldown period - await setTime(cooldownEndTime + 1n); + await setTime(expectedCooldownEnd + 1n); - const [cooldownEnd, status] = await assessment.getAssessmentResult(CLAIM_ID); - expect(cooldownEnd).to.equal(cooldownEndTime); + const [status, payoutRedemptionEnd, cooldownEnd] = await assessment.getAssessmentResult(CLAIM_ID); + expect(cooldownEnd).to.equal(expectedCooldownEnd); + expect(payoutRedemptionEnd).to.equal(expectedPayoutRedemptionEnd); expect(status).to.equal(AssessmentStatus.ACCEPTED); }); @@ -98,14 +107,16 @@ describe('getAssessmentResult', function () { assessment.connect(assessor3).castVote(CLAIM_ID, true, IPFS_HASH), ]); - const assessmentData = await assessment.getAssessment(CLAIM_ID); - const cooldownEndTime = BigInt(assessmentData.votingEnd) + BigInt(assessmentData.cooldownPeriod); + const { votingEnd, cooldownPeriod, payoutRedemptionPeriod } = await assessment.getAssessment(CLAIM_ID); + const cooldownEndTime = BigInt(votingEnd) + BigInt(cooldownPeriod); + const expectedPayoutRedemptionEnd = cooldownEndTime + BigInt(payoutRedemptionPeriod); // Set time past cooldown period await setTime(cooldownEndTime + 1n); - const [cooldownEnd, status] = await assessment.getAssessmentResult(CLAIM_ID); + const [status, payoutRedemptionEnd, cooldownEnd] = await assessment.getAssessmentResult(CLAIM_ID); expect(cooldownEnd).to.equal(cooldownEndTime); + expect(payoutRedemptionEnd).to.equal(expectedPayoutRedemptionEnd); expect(status).to.equal(AssessmentStatus.DENIED); }); @@ -119,15 +130,17 @@ describe('getAssessmentResult', function () { await assessment.connect(assessor1).castVote(CLAIM_ID, true, IPFS_HASH); await assessment.connect(assessor2).castVote(CLAIM_ID, false, IPFS_HASH); - const assessmentData = await assessment.getAssessment(CLAIM_ID); - const cooldownEndTime = BigInt(assessmentData.votingEnd) + BigInt(assessmentData.cooldownPeriod); + const { votingEnd, cooldownPeriod, payoutRedemptionPeriod } = await assessment.getAssessment(CLAIM_ID); + const expectedCooldownEnd = BigInt(votingEnd) + BigInt(cooldownPeriod); + const expectedPayoutRedemptionEnd = expectedCooldownEnd + BigInt(payoutRedemptionPeriod); // Set time past cooldown period - await setTime(cooldownEndTime + 1n); + await setTime(expectedCooldownEnd + 1n); - const [cooldownEnd, status] = await assessment.getAssessmentResult(CLAIM_ID); + const [status, payoutRedemptionEnd, cooldownEnd] = await assessment.getAssessmentResult(CLAIM_ID); - expect(cooldownEnd).to.equal(cooldownEndTime); + expect(cooldownEnd).to.equal(expectedCooldownEnd); + expect(payoutRedemptionEnd).to.equal(expectedPayoutRedemptionEnd); expect(status).to.equal(AssessmentStatus.DRAW); }); @@ -138,64 +151,84 @@ describe('getAssessmentResult', function () { // No votes cast (both accept and deny votes = 0) - const assessmentData = await assessment.getAssessment(CLAIM_ID); - const cooldownEndTime = BigInt(assessmentData.votingEnd) + BigInt(assessmentData.cooldownPeriod); + const { votingEnd, cooldownPeriod, payoutRedemptionPeriod } = await assessment.getAssessment(CLAIM_ID); + const expectedCooldownEnd = BigInt(votingEnd) + BigInt(cooldownPeriod); + const expectedPayoutRedemptionEnd = expectedCooldownEnd + BigInt(payoutRedemptionPeriod); // Set time past cooldown period - await setTime(cooldownEndTime + 1n); + await setTime(expectedCooldownEnd + 1n); - const [cooldownEnd, status] = await assessment.getAssessmentResult(CLAIM_ID); + const [status, payoutRedemptionEnd, cooldownEnd] = await assessment.getAssessmentResult(CLAIM_ID); - expect(cooldownEnd).to.equal(cooldownEndTime); + expect(cooldownEnd).to.equal(expectedCooldownEnd); + expect(payoutRedemptionEnd).to.equal(expectedPayoutRedemptionEnd); expect(status).to.equal(AssessmentStatus.DRAW); }); - it('should calculate cooldownEnd correctly with different cooldown periods', async function () { - const { contracts, constants } = await loadFixture(setup); - const { assessment } = contracts; - const { CLAIM_ID, PRODUCT_TYPE_ID } = constants; + it('should return different cooldown and payout redemption periods for different product types', async function () { + const { contracts, accounts, constants } = await loadFixture(setup); + const { assessment, claims } = contracts; + const { IPFS_HASH } = constants; + const [governanceAccount] = accounts.governanceContracts; + const [coverOwner] = accounts.members; - // Get the assessment data for the existing claim - const assessmentData = await assessment.getAssessment(CLAIM_ID); - const [cooldownEnd] = await assessment.getAssessmentResult(CLAIM_ID); + const ASSESSOR_GROUP_ID = await assessment.getGroupsCount(); - // Verify that cooldownEnd is calculated as votingEnd + cooldownPeriod - const expectedCooldownEnd = BigInt(assessmentData.votingEnd) + BigInt(assessmentData.cooldownPeriod); - expect(cooldownEnd).to.equal(expectedCooldownEnd); + const productType2CooldownPeriod = 2 * ONE_DAY; + const productType2PayoutRedemptionPeriod = 20 * ONE_DAY; - // Verify the cooldown period matches the expected value from payoutCooldown - const expectedCooldownPeriod = await assessment.payoutCooldown(PRODUCT_TYPE_ID); - expect(assessmentData.cooldownPeriod).to.equal(expectedCooldownPeriod); - }); + const productType3CooldownPeriod = 3 * ONE_DAY; + const productType3PayoutRedemptionPeriod = 30 * ONE_DAY; - it('should handle assessment status transitions correctly over time', async function () { - const { contracts, accounts, constants } = await loadFixture(setup); - const { assessment } = contracts; - const { CLAIM_ID, IPFS_HASH } = constants; - const [assessor1] = accounts.assessors; + // Set assessment data for two different product types with different periods + await assessment.connect(governanceAccount).setAssessmentDataForProductTypes( + [2], // product type 2 + productType2CooldownPeriod, + productType2PayoutRedemptionPeriod, + ASSESSOR_GROUP_ID, + ); - const assessmentData = await assessment.getAssessment(CLAIM_ID); - const expectedCooldownEnd = BigInt(assessmentData.votingEnd) + BigInt(assessmentData.cooldownPeriod); + await assessment.connect(governanceAccount).setAssessmentDataForProductTypes( + [3], // product type 3 + productType3CooldownPeriod, + productType3PayoutRedemptionPeriod, + ASSESSOR_GROUP_ID, + ); - // Initially should be VOTING - let [cooldownEnd, status] = await assessment.getAssessmentResult(CLAIM_ID); - expect(cooldownEnd).to.equal(expectedCooldownEnd); - expect(status).to.equal(AssessmentStatus.VOTING); + // Create a second claim with product type 2 (mock submitClaim sets product type to coverId) + const COVER_ID_2 = 2; + const CLAIM_ID_2 = 2; + await claims.connect(coverOwner).submitClaim(COVER_ID_2, ethers.parseEther('1'), IPFS_HASH); - // Cast a vote to create a non-draw scenario - await assessment.connect(assessor1).castVote(CLAIM_ID, true, IPFS_HASH); + const COVER_ID_3 = 3; + const CLAIM_ID_3 = 3; + await claims.connect(coverOwner).submitClaim(COVER_ID_3, ethers.parseEther('1'), IPFS_HASH); - // Set time to just after voting ends but before cooldown passes - should be COOLDOWN - await setTime(assessmentData.votingEnd + 1n); - [cooldownEnd, status] = await assessment.getAssessmentResult(CLAIM_ID); - expect(cooldownEnd).to.equal(expectedCooldownEnd); - expect(status).to.equal(AssessmentStatus.COOLDOWN); + // Should have different values for claims with different product types + const [status2, payoutRedemptionEnd2, cooldownEnd2] = await assessment.getAssessmentResult(CLAIM_ID_2); + const [status3, payoutRedemptionEnd3, cooldownEnd3] = await assessment.getAssessmentResult(CLAIM_ID_3); - // Set time past cooldown - should be ACCEPTED - const cooldownEndTime = BigInt(assessmentData.votingEnd) + BigInt(assessmentData.cooldownPeriod); - await setTime(cooldownEndTime + 1n); - [cooldownEnd, status] = await assessment.getAssessmentResult(CLAIM_ID); - expect(cooldownEnd).to.equal(expectedCooldownEnd); - expect(status).to.equal(AssessmentStatus.ACCEPTED); + expect(status2).to.equal(AssessmentStatus.VOTING); + expect(status3).to.equal(AssessmentStatus.VOTING); + + // Get assessment data for calculations + const claimAssessmentData2 = await assessment.getAssessment(CLAIM_ID_2); + const claimAssessmentData3 = await assessment.getAssessment(CLAIM_ID_3); + + // Verify getAssessmentResult calculations are correct for each claim + const expectedCooldownEnd2 = BigInt(claimAssessmentData2.votingEnd) + BigInt(productType2CooldownPeriod); + const expectedPayoutRedemptionEnd2 = expectedCooldownEnd2 + BigInt(productType2PayoutRedemptionPeriod); + + const expectedCooldownEnd3 = BigInt(claimAssessmentData3.votingEnd) + BigInt(productType3CooldownPeriod); + const expectedPayoutRedemptionEnd3 = expectedCooldownEnd3 + BigInt(productType3PayoutRedemptionPeriod); + + expect(cooldownEnd2).to.equal(expectedCooldownEnd2); + expect(payoutRedemptionEnd2).to.equal(expectedPayoutRedemptionEnd2); + + expect(cooldownEnd3).to.equal(expectedCooldownEnd3); + expect(payoutRedemptionEnd3).to.equal(expectedPayoutRedemptionEnd3); + + expect(cooldownEnd2).to.not.equal(cooldownEnd3); + expect(payoutRedemptionEnd2).to.not.equal(payoutRedemptionEnd3); }); }); diff --git a/test/unit/Assessment/setAssessmentDataForProductTypes.js b/test/unit/Assessment/setAssessmentDataForProductTypes.js index cf2cfd1e41..637e256ffa 100644 --- a/test/unit/Assessment/setAssessmentDataForProductTypes.js +++ b/test/unit/Assessment/setAssessmentDataForProductTypes.js @@ -2,6 +2,8 @@ const { expect } = require('chai'); const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); const { setup } = require('./setup'); +const ONE_DAY = 24 * 60 * 60; + describe('setAssessmentDataForProductTypes', function () { it('should revert if not called by governor', async function () { const { contracts, accounts, constants } = await loadFixture(setup); @@ -10,11 +12,12 @@ describe('setAssessmentDataForProductTypes', function () { const [nonGovernor] = accounts.nonMembers; const productTypeIds = [1, 2, 3]; - const cooldownPeriod = 24 * 60 * 60; // 1 day + const cooldownPeriod = ONE_DAY; + const payoutRedemptionPeriod = 7 * ONE_DAY; const setAssessmentDataForProductTypes = assessment .connect(nonGovernor) - .setAssessmentDataForProductTypes(productTypeIds, cooldownPeriod, ASSESSOR_GROUP_ID); + .setAssessmentDataForProductTypes(productTypeIds, cooldownPeriod, payoutRedemptionPeriod, ASSESSOR_GROUP_ID); await expect(setAssessmentDataForProductTypes).to.be.revertedWithCustomError(assessment, 'Unauthorized'); }); @@ -26,14 +29,20 @@ describe('setAssessmentDataForProductTypes', function () { const [governanceAccount] = accounts.governanceContracts; const productTypeIds = [10, 11, 12, 13]; - const cooldownPeriod = 72 * 60 * 60; // 3 days + const cooldownPeriod = 3 * ONE_DAY; + const payoutRedemptionPeriod = 14 * ONE_DAY; const tx = await assessment .connect(governanceAccount) - .setAssessmentDataForProductTypes(productTypeIds, cooldownPeriod, ASSESSOR_GROUP_ID); + .setAssessmentDataForProductTypes(productTypeIds, cooldownPeriod, payoutRedemptionPeriod, ASSESSOR_GROUP_ID); // Verify assessment data is set for all product types for (const productTypeId of productTypeIds) { + const assessmentData = await assessment.getAssessmentDataForProductType(productTypeId); + expect(assessmentData.cooldownPeriod).to.equal(cooldownPeriod); + expect(assessmentData.payoutRedemptionPeriod).to.equal(payoutRedemptionPeriod); + expect(assessmentData.assessingGroupId).to.equal(ASSESSOR_GROUP_ID); + const assessmentDataCooldown = await assessment.payoutCooldown(productTypeId); expect(assessmentDataCooldown).to.equal(cooldownPeriod); } @@ -41,10 +50,10 @@ describe('setAssessmentDataForProductTypes', function () { // Verify event emission await expect(tx) .to.emit(assessment, 'AssessmentDataForProductTypesSet') - .withArgs(productTypeIds, cooldownPeriod, ASSESSOR_GROUP_ID); + .withArgs(productTypeIds, cooldownPeriod, payoutRedemptionPeriod, ASSESSOR_GROUP_ID); }); - it('should handle zero cooldown period', async function () { + it('should handle zero cooldown and payout redemption periods', async function () { const { contracts, accounts, constants } = await loadFixture(setup); const { assessment } = contracts; const { ASSESSOR_GROUP_ID } = constants; @@ -52,10 +61,11 @@ describe('setAssessmentDataForProductTypes', function () { const productTypeIds = [30]; const cooldownPeriod = 0; + const payoutRedemptionPeriod = 0; const tx = await assessment .connect(governanceAccount) - .setAssessmentDataForProductTypes(productTypeIds, cooldownPeriod, ASSESSOR_GROUP_ID); + .setAssessmentDataForProductTypes(productTypeIds, cooldownPeriod, payoutRedemptionPeriod, ASSESSOR_GROUP_ID); // Verify assessment data is set with zero cooldown const assessmentDataCooldown = await assessment.payoutCooldown(productTypeIds[0]); @@ -64,10 +74,10 @@ describe('setAssessmentDataForProductTypes', function () { // Verify event emission await expect(tx) .to.emit(assessment, 'AssessmentDataForProductTypesSet') - .withArgs(productTypeIds, cooldownPeriod, ASSESSOR_GROUP_ID); + .withArgs(productTypeIds, cooldownPeriod, payoutRedemptionPeriod, ASSESSOR_GROUP_ID); }); - it('should handle maximum cooldown period', async function () { + it('should handle maximum cooldown period and payout redemption period', async function () { const { contracts, accounts, constants } = await loadFixture(setup); const { assessment } = contracts; const { ASSESSOR_GROUP_ID } = constants; @@ -75,10 +85,11 @@ describe('setAssessmentDataForProductTypes', function () { const productTypeIds = [31]; const maxCooldownPeriod = 2n ** 32n - 1n; // Max uint32 + const payoutRedemptionPeriod = 2n ** 32n - 1n; // Max uint32 const tx = await assessment .connect(governanceAccount) - .setAssessmentDataForProductTypes(productTypeIds, maxCooldownPeriod, ASSESSOR_GROUP_ID); + .setAssessmentDataForProductTypes(productTypeIds, maxCooldownPeriod, payoutRedemptionPeriod, ASSESSOR_GROUP_ID); // Verify assessment data is set with max cooldown const assessmentDataCooldown = await assessment.payoutCooldown(productTypeIds[0]); @@ -87,7 +98,7 @@ describe('setAssessmentDataForProductTypes', function () { // Verify event emission await expect(tx) .to.emit(assessment, 'AssessmentDataForProductTypesSet') - .withArgs(productTypeIds, maxCooldownPeriod, ASSESSOR_GROUP_ID); + .withArgs(productTypeIds, maxCooldownPeriod, payoutRedemptionPeriod, ASSESSOR_GROUP_ID); }); it('should handle duplicate product type IDs in same call', async function () { @@ -97,11 +108,12 @@ describe('setAssessmentDataForProductTypes', function () { const [governanceAccount] = accounts.governanceContracts; const productTypeIds = [40, 41, 40, 42, 41]; // Duplicates: 40, 41 - const cooldownPeriod = 24 * 60 * 60; + const cooldownPeriod = 3 * ONE_DAY; + const payoutRedemptionPeriod = 30 * ONE_DAY; const tx = await assessment .connect(governanceAccount) - .setAssessmentDataForProductTypes(productTypeIds, cooldownPeriod, ASSESSOR_GROUP_ID); + .setAssessmentDataForProductTypes(productTypeIds, cooldownPeriod, payoutRedemptionPeriod, ASSESSOR_GROUP_ID); // Verify assessment data is set for all IDs (including duplicates) for (const productTypeId of productTypeIds) { @@ -112,7 +124,7 @@ describe('setAssessmentDataForProductTypes', function () { // Verify event emission (with original array including duplicates) await expect(tx) .to.emit(assessment, 'AssessmentDataForProductTypesSet') - .withArgs(productTypeIds, cooldownPeriod, ASSESSOR_GROUP_ID); + .withArgs(productTypeIds, cooldownPeriod, payoutRedemptionPeriod, ASSESSOR_GROUP_ID); }); it('should handle sequential updates to same product types', async function () { @@ -122,16 +134,28 @@ describe('setAssessmentDataForProductTypes', function () { const [governanceAccount] = accounts.governanceContracts; const productTypeIds = [60, 61, 62]; - const initialCooldown = 24 * 60 * 60; // 1 day - const updatedCooldown = 72 * 60 * 60; // 3 days + const initialCooldown = ONE_DAY; + const updatedCooldown = 3 * ONE_DAY; + const initialPayoutRedemptionPeriod = 7 * ONE_DAY; + const updatedPayoutRedemptionPeriod = 14 * ONE_DAY; // Set initial data await assessment .connect(governanceAccount) - .setAssessmentDataForProductTypes(productTypeIds, initialCooldown, ASSESSOR_GROUP_ID); + .setAssessmentDataForProductTypes( + productTypeIds, + initialCooldown, + initialPayoutRedemptionPeriod, + ASSESSOR_GROUP_ID, + ); // Verify initial data for (const productTypeId of productTypeIds) { + const assessmentData = await assessment.getAssessmentDataForProductType(productTypeId); + expect(assessmentData.assessingGroupId).to.equal(ASSESSOR_GROUP_ID); + expect(assessmentData.cooldownPeriod).to.equal(initialCooldown); + expect(assessmentData.payoutRedemptionPeriod).to.equal(initialPayoutRedemptionPeriod); + const assessmentDataCooldown = await assessment.payoutCooldown(productTypeId); expect(assessmentDataCooldown).to.equal(initialCooldown); } @@ -139,10 +163,20 @@ describe('setAssessmentDataForProductTypes', function () { // Update data const tx = await assessment .connect(governanceAccount) - .setAssessmentDataForProductTypes(productTypeIds, updatedCooldown, ASSESSOR_GROUP_ID); + .setAssessmentDataForProductTypes( + productTypeIds, + updatedCooldown, + updatedPayoutRedemptionPeriod, + ASSESSOR_GROUP_ID, + ); // Verify updated data for (const productTypeId of productTypeIds) { + const assessmentData = await assessment.getAssessmentDataForProductType(productTypeId); + expect(assessmentData.cooldownPeriod).to.equal(updatedCooldown); + expect(assessmentData.payoutRedemptionPeriod).to.equal(updatedPayoutRedemptionPeriod); + expect(assessmentData.assessingGroupId).to.equal(ASSESSOR_GROUP_ID); + const assessmentDataCooldown = await assessment.payoutCooldown(productTypeId); expect(assessmentDataCooldown).to.equal(updatedCooldown); } @@ -150,7 +184,7 @@ describe('setAssessmentDataForProductTypes', function () { // Verify event emission for update await expect(tx) .to.emit(assessment, 'AssessmentDataForProductTypesSet') - .withArgs(productTypeIds, updatedCooldown, ASSESSOR_GROUP_ID); + .withArgs(productTypeIds, updatedCooldown, updatedPayoutRedemptionPeriod, ASSESSOR_GROUP_ID); }); it('should revert when groupId is invalid', async function () { @@ -159,7 +193,8 @@ describe('setAssessmentDataForProductTypes', function () { const [governanceAccount] = accounts.governanceContracts; const productTypeIds = [70, 71]; - const cooldownPeriod = 24 * 60 * 60; // 1 day + const cooldownPeriod = 3 * ONE_DAY; + const payoutRedemptionPeriod = 30 * ONE_DAY; // Get current group count and create array of invalid IDs const currentGroupCount = await assessment.getGroupsCount(); @@ -168,9 +203,88 @@ describe('setAssessmentDataForProductTypes', function () { for (const invalidGroupId of invalidGroupIds) { const setAssessmentDataForProductTypes = assessment .connect(governanceAccount) - .setAssessmentDataForProductTypes(productTypeIds, cooldownPeriod, invalidGroupId); + .setAssessmentDataForProductTypes(productTypeIds, cooldownPeriod, payoutRedemptionPeriod, invalidGroupId); await expect(setAssessmentDataForProductTypes).to.be.revertedWithCustomError(assessment, 'InvalidGroupId'); } }); + + it('should set different cooldown and payout redemption periods for different product types', async function () { + const { contracts, accounts, constants } = await loadFixture(setup); + const { assessment } = contracts; + const { ASSESSOR_GROUP_ID } = constants; + const [governanceAccount] = accounts.governanceContracts; + + // Define different periods for different product types + const productType0CooldownPeriod = ONE_DAY; + const productType0PayoutRedemptionPeriod = 10 * ONE_DAY; + + const productType1CooldownPeriod = 2 * ONE_DAY; + const productType1PayoutRedemptionPeriod = 20 * ONE_DAY; + + const productType2CooldownPeriod = 3 * ONE_DAY; + const productType2PayoutRedemptionPeriod = 30 * ONE_DAY; + + // Set assessment data for different product types + await assessment + .connect(governanceAccount) + .setAssessmentDataForProductTypes( + [0], + productType0CooldownPeriod, + productType0PayoutRedemptionPeriod, + ASSESSOR_GROUP_ID, + ); + + await assessment + .connect(governanceAccount) + .setAssessmentDataForProductTypes( + [1], + productType1CooldownPeriod, + productType1PayoutRedemptionPeriod, + ASSESSOR_GROUP_ID, + ); + + await assessment + .connect(governanceAccount) + .setAssessmentDataForProductTypes( + [2], + productType2CooldownPeriod, + productType2PayoutRedemptionPeriod, + ASSESSOR_GROUP_ID, + ); + + // Verify that different product types have different assessment data stored + const [assessmentData0, assessmentData1, assessmentData2] = await Promise.all([ + assessment.getAssessmentDataForProductType(0), + assessment.getAssessmentDataForProductType(1), + assessment.getAssessmentDataForProductType(2), + ]); + + // Verify each product type has correct data + expect(assessmentData0.cooldownPeriod).to.equal(productType0CooldownPeriod); + expect(assessmentData0.payoutRedemptionPeriod).to.equal(productType0PayoutRedemptionPeriod); + expect(assessmentData0.assessingGroupId).to.equal(ASSESSOR_GROUP_ID); + + expect(assessmentData1.cooldownPeriod).to.equal(productType1CooldownPeriod); + expect(assessmentData1.payoutRedemptionPeriod).to.equal(productType1PayoutRedemptionPeriod); + expect(assessmentData1.assessingGroupId).to.equal(ASSESSOR_GROUP_ID); + + expect(assessmentData2.cooldownPeriod).to.equal(productType2CooldownPeriod); + expect(assessmentData2.payoutRedemptionPeriod).to.equal(productType2PayoutRedemptionPeriod); + expect(assessmentData2.assessingGroupId).to.equal(ASSESSOR_GROUP_ID); + + // Verify all values are different from each other + expect(assessmentData0.cooldownPeriod).to.not.equal(assessmentData1.cooldownPeriod); + expect(assessmentData1.cooldownPeriod).to.not.equal(assessmentData2.cooldownPeriod); + expect(assessmentData0.cooldownPeriod).to.not.equal(assessmentData2.cooldownPeriod); + + expect(assessmentData0.payoutRedemptionPeriod).to.not.equal(assessmentData1.payoutRedemptionPeriod); + expect(assessmentData1.payoutRedemptionPeriod).to.not.equal(assessmentData2.payoutRedemptionPeriod); + expect(assessmentData0.payoutRedemptionPeriod).to.not.equal(assessmentData2.payoutRedemptionPeriod); + + // Verify payoutCooldown function returns correct values + expect(await assessment.payoutCooldown(0)).to.equal(productType0CooldownPeriod); + expect(await assessment.payoutCooldown(1)).to.equal(productType1CooldownPeriod); + expect(await assessment.payoutCooldown(2)).to.equal(productType2CooldownPeriod); + }); }); From f2d7820c85a1370016be2c09d38f4c93ad01e784 Mon Sep 17 00:00:00 2001 From: Rocky Murdoch Date: Fri, 1 Aug 2025 04:45:41 +0300 Subject: [PATCH 07/18] test: fix Assessment setup and unit tests --- .../mocks/modules/Assessment/ASMockClaims.sol | 9 +++------ test/unit/Assessment/getBallotsMetadata.js | 2 +- test/unit/Assessment/helpers.js | 3 --- test/unit/Assessment/setup.js | 15 ++++++++------- test/unit/Assessment/undoVotes.js | 4 ++-- 5 files changed, 14 insertions(+), 19 deletions(-) diff --git a/contracts/mocks/modules/Assessment/ASMockClaims.sol b/contracts/mocks/modules/Assessment/ASMockClaims.sol index c5892c18fa..840df150e8 100644 --- a/contracts/mocks/modules/Assessment/ASMockClaims.sol +++ b/contracts/mocks/modules/Assessment/ASMockClaims.sol @@ -45,14 +45,11 @@ contract ASMockClaims is IClaims, RegistryAware { return _claims[claimId]; } - function getPayoutRedemptionPeriod() external override pure returns (uint) { - return PAYOUT_REDEMPTION_PERIOD; - } - /* ========== MUTATIVE FUNCTIONS ========== */ /// @notice Simplified submit claim for testing /// @dev Calls assessment.startAssessment and stores the claim + /// @dev productTypeId is set to coverId for testing function submitClaim( uint32 coverId, uint96 requestedAmount, @@ -62,8 +59,8 @@ contract ASMockClaims is IClaims, RegistryAware { uint claimId = _nextClaimId++; lastClaimSubmissionOnCover[coverId] = claimId; - // For testing, use a simple product type ID - uint16 productTypeId = 1; + // For testing, set product type id to coverId + uint16 productTypeId = uint16(coverId); // Start the assessment _assessment().startAssessment(claimId, productTypeId); diff --git a/test/unit/Assessment/getBallotsMetadata.js b/test/unit/Assessment/getBallotsMetadata.js index 1eb8c1ee57..9ee7fa24dc 100644 --- a/test/unit/Assessment/getBallotsMetadata.js +++ b/test/unit/Assessment/getBallotsMetadata.js @@ -72,7 +72,7 @@ describe('getBallotsMetadata', function () { ]); const expectedClaimId1 = 2; const expectedClaimId2 = 3; - const coverIds = [300, 301]; + const coverIds = [1, 2]; await setEtherBalance(memberAddress, ethers.parseEther('10')); for (const coverId of coverIds) { diff --git a/test/unit/Assessment/helpers.js b/test/unit/Assessment/helpers.js index 9eb9f33509..85fb5283e9 100644 --- a/test/unit/Assessment/helpers.js +++ b/test/unit/Assessment/helpers.js @@ -1,7 +1,5 @@ const evm = require('../../utils/evm'); -const daysToSeconds = days => days * 24 * 60 * 60; - /** * Sets the blockchain time to a specific timestamp * @param {number | bigint} timestamp - The timestamp to set @@ -12,6 +10,5 @@ async function setTime(timestamp) { } module.exports = { - daysToSeconds, setTime, }; diff --git a/test/unit/Assessment/setup.js b/test/unit/Assessment/setup.js index dc29fb76ab..cd173f61c1 100644 --- a/test/unit/Assessment/setup.js +++ b/test/unit/Assessment/setup.js @@ -13,7 +13,7 @@ const IPFS_HASH = ethers.solidityPackedKeccak256(['string'], ['standard-ipfs-has async function setup() { const accounts = await getAccounts(); - // Deploy Registry Mock + // Deploy contracts const registry = await ethers.deployContract('RegistryMock', []); const assessment = await ethers.deployContract('Assessment', [registry]); const claims = await ethers.deployContract('ASMockClaims', [registry]); @@ -36,18 +36,19 @@ async function setup() { const assessorMemberIds = await Promise.all(accounts.assessors.map(a => registry.getMemberId(a.address))); await assessment.connect(governanceAccount).addAssessorsToGroup(assessorMemberIds, newGroupId); - // Set assessment data for product type + // Set assessment data for each product type + // NOTE: mock claims sets product type to coverId, so we need to set for each coverId that is used in tests const ASSESSOR_GROUP_ID = await assessment.getGroupsCount(); await assessment .connect(governanceAccount) - .setAssessmentDataForProductTypes([PRODUCT_TYPE_ID], ONE_DAY, ASSESSOR_GROUP_ID); + .setAssessmentDataForProductTypes([PRODUCT_TYPE_ID, 2, 3, 4], ONE_DAY, 7n * ONE_DAY, ASSESSOR_GROUP_ID); // Use a member account to submit the claim - const [memberAccount] = accounts.members; - await setEtherBalance(memberAccount.address, ethers.parseEther('10')); + const [coverOwner] = accounts.members; + await setEtherBalance(coverOwner.address, ethers.parseEther('10')); - // Submit a claim via the member account (this will call startAssessment internally) - await claims.connect(memberAccount).submitClaim(CLAIM_ID, ethers.parseEther('1'), IPFS_HASH); + // Submit a claim using the cover owner account + await claims.connect(coverOwner).submitClaim(CLAIM_ID, ethers.parseEther('1'), IPFS_HASH); // Give Claims contract ETH balance for tests that need to impersonate it await setEtherBalance(claims.target, ethers.parseEther('10')); diff --git a/test/unit/Assessment/undoVotes.js b/test/unit/Assessment/undoVotes.js index 703168c596..e7b381c011 100644 --- a/test/unit/Assessment/undoVotes.js +++ b/test/unit/Assessment/undoVotes.js @@ -116,7 +116,7 @@ describe('undoVotes', function () { // Create additional claims using member account const claimIds = [2, 3, 4]; - const coverIds = [100, 101, 102]; + const coverIds = [2, 3, 4]; const [memberAccount] = accounts.members; const claimAmount = ethers.parseEther('1'); @@ -232,7 +232,7 @@ describe('undoVotes', function () { // Create additional claim using member account const newClaimId = 2; - const coverId = 200; + const coverId = 2; const [memberAccount] = accounts.members; await claims.connect(memberAccount).submitClaim(coverId, ethers.parseEther('1'), IPFS_HASH); From 2b69ba760fe4c3a993f7a9342e7be323b6db3510 Mon Sep 17 00:00:00 2001 From: Rocky Murdoch Date: Fri, 1 Aug 2025 04:54:43 +0300 Subject: [PATCH 08/18] test: fix retrieveDeposit unit tests --- test/unit/Claims/retrieveDeposit.js | 88 ++++++++++++++++++----------- test/unit/Claims/setup.js | 1 - 2 files changed, 56 insertions(+), 33 deletions(-) diff --git a/test/unit/Claims/retrieveDeposit.js b/test/unit/Claims/retrieveDeposit.js index 291f65cbd7..64eba5add5 100644 --- a/test/unit/Claims/retrieveDeposit.js +++ b/test/unit/Claims/retrieveDeposit.js @@ -4,11 +4,9 @@ const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); const { setNextBlockBaseFee } = require('../../utils/evm'); const { PAUSE_CLAIMS_PAYOUT } = require('../../utils/registry'); -const { ASSET, ASSESSMENT_STATUS, createMockCover, submitClaim } = require('./helpers'); +const { ASSET, ASSESSMENT_STATUS, createMockCover, submitClaim, daysToSeconds } = require('./helpers'); const { setup } = require('./setup'); -const daysToSeconds = days => days * 24 * 60 * 60; - describe('retrieveDeposit', function () { it('reverts if the claim does not exist', async function () { const fixture = await loadFixture(setup); @@ -30,19 +28,20 @@ describe('retrieveDeposit', function () { const claimId = (await claims.getClaimsCount()) + 1n; await submitClaim(fixture)({ coverId: 1, sender: coverOwner }); - const { timestamp } = await ethers.provider.getBlock('latest'); + const { timestamp: cooldownEnd } = await ethers.provider.getBlock('latest'); + const payoutRedemptionEnd = cooldownEnd + daysToSeconds(30); // Test all non-DRAW statuses - await assessment.setAssessmentResult(claimId, timestamp, ASSESSMENT_STATUS.VOTING); + await assessment.setAssessmentResult(claimId, ASSESSMENT_STATUS.VOTING, payoutRedemptionEnd, cooldownEnd); await expect(claims.retrieveDeposit(claimId)).to.be.revertedWithCustomError(claims, 'InvalidAssessmentStatus'); - await assessment.setAssessmentResult(claimId, timestamp, ASSESSMENT_STATUS.COOLDOWN); + await assessment.setAssessmentResult(claimId, ASSESSMENT_STATUS.COOLDOWN, payoutRedemptionEnd, cooldownEnd); await expect(claims.retrieveDeposit(claimId)).to.be.revertedWithCustomError(claims, 'InvalidAssessmentStatus'); - await assessment.setAssessmentResult(claimId, timestamp, ASSESSMENT_STATUS.ACCEPTED); + await assessment.setAssessmentResult(claimId, ASSESSMENT_STATUS.ACCEPTED, payoutRedemptionEnd, cooldownEnd); await expect(claims.retrieveDeposit(claimId)).to.be.revertedWithCustomError(claims, 'InvalidAssessmentStatus'); - await assessment.setAssessmentResult(claimId, timestamp, ASSESSMENT_STATUS.DENIED); + await assessment.setAssessmentResult(claimId, ASSESSMENT_STATUS.DENIED, payoutRedemptionEnd, cooldownEnd); await expect(claims.retrieveDeposit(claimId)).to.be.revertedWithCustomError(claims, 'InvalidAssessmentStatus'); }); @@ -55,8 +54,9 @@ describe('retrieveDeposit', function () { const claimId = (await claims.getClaimsCount()) + 1n; await submitClaim(fixture)({ coverId: 1, sender: coverOwner }); - const { timestamp } = await ethers.provider.getBlock('latest'); - await assessment.setAssessmentResult(claimId, timestamp, ASSESSMENT_STATUS.DRAW); + const { timestamp: cooldownEnd } = await ethers.provider.getBlock('latest'); + const payoutRedemptionEnd = cooldownEnd + daysToSeconds(30); + await assessment.setAssessmentResult(claimId, ASSESSMENT_STATUS.DRAW, payoutRedemptionEnd, cooldownEnd); await expect(claims.retrieveDeposit(claimId)).not.to.be.reverted; await expect(claims.retrieveDeposit(claimId)).to.be.revertedWithCustomError(claims, 'DepositAlreadyRetrieved'); @@ -71,8 +71,9 @@ describe('retrieveDeposit', function () { const claimId = (await claims.getClaimsCount()) + 1n; await submitClaim(fixture)({ coverId: 1, sender: coverOwner }); - const { timestamp } = await ethers.provider.getBlock('latest'); - await assessment.setAssessmentResult(claimId, timestamp, ASSESSMENT_STATUS.DRAW); + const { timestamp: cooldownEnd } = await ethers.provider.getBlock('latest'); + const payoutRedemptionEnd = cooldownEnd + daysToSeconds(30); + await assessment.setAssessmentResult(claimId, ASSESSMENT_STATUS.DRAW, payoutRedemptionEnd, cooldownEnd); await registry.confirmPauseConfig(PAUSE_CLAIMS_PAYOUT); @@ -82,16 +83,31 @@ describe('retrieveDeposit', function () { it('successfully retrieves deposit when claim status is DRAW', async function () { const fixture = await loadFixture(setup); const { claims, cover, assessment } = fixture.contracts; + const { claimDepositInETH: deposit } = fixture.config; const [coverOwner] = fixture.accounts.members; await createMockCover(cover, { owner: coverOwner.address }); + const coverId = 1; + const coverData = await cover.getCoverData(coverId); + const claimId = (await claims.getClaimsCount()) + 1n; - await submitClaim(fixture)({ coverId: 1, sender: coverOwner }); - const { timestamp } = await ethers.provider.getBlock('latest'); - await assessment.setAssessmentResult(claimId, timestamp, ASSESSMENT_STATUS.DRAW); + const ipfsHash = ethers.solidityPackedKeccak256(['string'], ['ipfs-hash']); - await expect(claims.retrieveDeposit(claimId)).not.to.be.reverted; + await setNextBlockBaseFee('0'); + await claims.connect(coverOwner).submitClaim(coverId, coverData.amount, ipfsHash, { value: deposit, gasPrice: 0 }); + + const ethBalanceAfterSubmittingClaim = await ethers.provider.getBalance(coverOwner.address); + + const { timestamp: cooldownEnd } = await ethers.provider.getBlock('latest'); + const payoutRedemptionEnd = cooldownEnd + daysToSeconds(30); + await assessment.setAssessmentResult(claimId, ASSESSMENT_STATUS.DRAW, payoutRedemptionEnd, cooldownEnd); + + await setNextBlockBaseFee('0'); + await claims.connect(coverOwner).retrieveDeposit(claimId, { gasPrice: 0 }); + const ethBalanceAfter = await ethers.provider.getBalance(coverOwner.address); + + expect(ethBalanceAfter).to.be.equal(ethBalanceAfterSubmittingClaim + deposit); }); it('emits ClaimDepositRetrieved event with correct parameters', async function () { @@ -103,8 +119,9 @@ describe('retrieveDeposit', function () { const claimId = (await claims.getClaimsCount()) + 1n; await submitClaim(fixture)({ coverId: 1, sender: coverOwner }); - const { timestamp } = await ethers.provider.getBlock('latest'); - await assessment.setAssessmentResult(claimId, timestamp, ASSESSMENT_STATUS.DRAW); + const { timestamp: cooldownEnd } = await ethers.provider.getBlock('latest'); + const payoutRedemptionEnd = cooldownEnd + daysToSeconds(30); + await assessment.setAssessmentResult(claimId, ASSESSMENT_STATUS.DRAW, payoutRedemptionEnd, cooldownEnd); await expect(claims.retrieveDeposit(claimId)) .to.emit(claims, 'ClaimDepositRetrieved') @@ -120,8 +137,9 @@ describe('retrieveDeposit', function () { const claimId = (await claims.getClaimsCount()) + 1n; await submitClaim(fixture)({ coverId: 1, sender: coverOwner }); - const { timestamp } = await ethers.provider.getBlock('latest'); - await assessment.setAssessmentResult(claimId, timestamp, ASSESSMENT_STATUS.DRAW); + const { timestamp: cooldownEnd } = await ethers.provider.getBlock('latest'); + const payoutRedemptionEnd = cooldownEnd + daysToSeconds(30); + await assessment.setAssessmentResult(claimId, ASSESSMENT_STATUS.DRAW, payoutRedemptionEnd, cooldownEnd); const claimBefore = await claims.getClaimInfo(claimId); expect(claimBefore.depositRetrieved).to.be.false; @@ -143,8 +161,9 @@ describe('retrieveDeposit', function () { const coverId = 1; const claimId = (await claims.getClaimsCount()) + 1n; await submitClaim(fixture)({ coverId, sender: originalOwner }); - const { timestamp } = await ethers.provider.getBlock('latest'); - await assessment.setAssessmentResult(claimId, timestamp, ASSESSMENT_STATUS.DRAW); + const { timestamp: cooldownEnd } = await ethers.provider.getBlock('latest'); + const payoutRedemptionEnd = cooldownEnd + daysToSeconds(30); + await assessment.setAssessmentResult(claimId, ASSESSMENT_STATUS.DRAW, payoutRedemptionEnd, cooldownEnd); // Transfer NFT to new owner before retrieving deposit await coverNFT.connect(originalOwner).transferFrom(originalOwner.address, newOwner.address, coverId); @@ -173,8 +192,9 @@ describe('retrieveDeposit', function () { const claimId = (await claims.getClaimsCount()) + 1n; await submitClaim(fixture)({ coverId: 1, sender: coverOwner }); - const { timestamp } = await ethers.provider.getBlock('latest'); - await assessment.setAssessmentResult(claimId, timestamp, ASSESSMENT_STATUS.DRAW); + const { timestamp: cooldownEnd } = await ethers.provider.getBlock('latest'); + const payoutRedemptionEnd = cooldownEnd + daysToSeconds(30); + await assessment.setAssessmentResult(claimId, ASSESSMENT_STATUS.DRAW, payoutRedemptionEnd, cooldownEnd); const coverOwnerBalanceBefore = await ethers.provider.getBalance(coverOwner.address); const otherMemberBalanceBefore = await ethers.provider.getBalance(otherMember.address); @@ -200,14 +220,16 @@ describe('retrieveDeposit', function () { // Submit first claim and set to DRAW const firstClaimId = (await claims.getClaimsCount()) + 1n; await submitClaim(fixture)({ coverId: 1, sender: coverOwner }); - let timestamp = (await ethers.provider.getBlock('latest')).timestamp; - await assessment.setAssessmentResult(firstClaimId, timestamp, ASSESSMENT_STATUS.DRAW); + let { timestamp: cooldownEnd } = await ethers.provider.getBlock('latest'); + let payoutRedemptionEnd = cooldownEnd + daysToSeconds(30); + await assessment.setAssessmentResult(firstClaimId, ASSESSMENT_STATUS.DRAW, payoutRedemptionEnd, cooldownEnd); // Submit second claim and set to DRAW const secondClaimId = (await claims.getClaimsCount()) + 1n; await submitClaim(fixture)({ coverId: 1, sender: coverOwner }); - timestamp = (await ethers.provider.getBlock('latest')).timestamp; - await assessment.setAssessmentResult(secondClaimId, timestamp, ASSESSMENT_STATUS.DRAW); + ({ timestamp: cooldownEnd } = await ethers.provider.getBlock('latest')); + payoutRedemptionEnd = cooldownEnd + daysToSeconds(30); + await assessment.setAssessmentResult(secondClaimId, ASSESSMENT_STATUS.DRAW, payoutRedemptionEnd, cooldownEnd); // Retrieve deposit from first claim await claims.retrieveDeposit(firstClaimId); @@ -235,8 +257,9 @@ describe('retrieveDeposit', function () { const claimId = (await claims.getClaimsCount()) + 1n; await submitClaim(fixture)({ coverId: 1, sender: coverOwner }); - const { timestamp } = await ethers.provider.getBlock('latest'); - await assessment.setAssessmentResult(claimId, timestamp, ASSESSMENT_STATUS.DRAW); + const { timestamp: cooldownEnd } = await ethers.provider.getBlock('latest'); + const payoutRedemptionEnd = cooldownEnd + daysToSeconds(30); + await assessment.setAssessmentResult(claimId, ASSESSMENT_STATUS.DRAW, payoutRedemptionEnd, cooldownEnd); const poolBalanceBefore = await ethers.provider.getBalance(await pool.getAddress()); @@ -258,8 +281,9 @@ describe('retrieveDeposit', function () { const claimId = (await claims.getClaimsCount()) + 1n; await submitClaim(fixture)({ coverId: 1, sender: coverOwner }); - const { timestamp } = await ethers.provider.getBlock('latest'); - await assessment.setAssessmentResult(claimId, timestamp, ASSESSMENT_STATUS.DRAW); + const { timestamp: cooldownEnd } = await ethers.provider.getBlock('latest'); + const payoutRedemptionEnd = cooldownEnd + daysToSeconds(30); + await assessment.setAssessmentResult(claimId, ASSESSMENT_STATUS.DRAW, payoutRedemptionEnd, cooldownEnd); const balanceBefore = await ethers.provider.getBalance(coverOwner.address); diff --git a/test/unit/Claims/setup.js b/test/unit/Claims/setup.js index 3107927d5d..24c0869045 100644 --- a/test/unit/Claims/setup.js +++ b/test/unit/Claims/setup.js @@ -80,7 +80,6 @@ async function setup() { const config = { claimDepositInETH: await claims.CLAIM_DEPOSIT_IN_ETH(), - payoutRedemptionPeriod: Number(await claims.getPayoutRedemptionPeriod()), }; const contracts = { From 3be3a163f158391ce106728055b0468ca5c02741 Mon Sep 17 00:00:00 2001 From: Rocky Murdoch Date: Fri, 1 Aug 2025 04:55:58 +0300 Subject: [PATCH 09/18] test: submitClaim/redeemClaimPayout add must be member/coverNFT owner test cases --- test/unit/Claims/helpers.js | 7 +- test/unit/Claims/redeemClaimPayout.js | 279 +++++++++++--------------- test/unit/Claims/submitClaim.js | 203 ++++++++----------- 3 files changed, 210 insertions(+), 279 deletions(-) diff --git a/test/unit/Claims/helpers.js b/test/unit/Claims/helpers.js index c21ff2c7bd..81bb27df61 100644 --- a/test/unit/Claims/helpers.js +++ b/test/unit/Claims/helpers.js @@ -44,13 +44,16 @@ const submitClaim = ({ accounts, contracts, config }) => async ({ coverId = 0, amount = parseEther('1'), ipfsMetadata = toBeHex(0, 32), sender, value }) => { return await contracts.claims - .connect(sender || accounts[0]) - .submitClaim(coverId, amount, ipfsMetadata, { value: value || config.claimDepositInETH }); + .connect(sender || accounts.members[0]) + .submitClaim(coverId, amount, ipfsMetadata, { value: value ?? config.claimDepositInETH }); }; +const daysToSeconds = days => days * 24 * 60 * 60; + module.exports = { ASSET, ASSESSMENT_STATUS, createMockCover, submitClaim, + daysToSeconds, }; diff --git a/test/unit/Claims/redeemClaimPayout.js b/test/unit/Claims/redeemClaimPayout.js index 5b74f32127..c21c8368b0 100644 --- a/test/unit/Claims/redeemClaimPayout.js +++ b/test/unit/Claims/redeemClaimPayout.js @@ -1,21 +1,56 @@ const { ethers } = require('hardhat'); const { expect } = require('chai'); const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); -const { parseEther, toBeHex } = ethers; +const { parseEther } = ethers; const { mineNextBlock, setNextBlockTime, setNextBlockBaseFee } = require('../../utils/evm'); const { PAUSE_CLAIMS_PAYOUT } = require('../../utils/registry'); -const { ASSET, ASSESSMENT_STATUS, createMockCover, submitClaim } = require('./helpers'); +const { ASSET, ASSESSMENT_STATUS, createMockCover, submitClaim, daysToSeconds } = require('./helpers'); const { setup } = require('./setup'); -const daysToSeconds = days => days * 24 * 60 * 60; - const setTime = async timestamp => { await setNextBlockTime(timestamp); await mineNextBlock(); }; describe('redeemClaimPayout', function () { + const ipfsHash = ethers.solidityPackedKeccak256(['string'], ['ipfs-hash']); + + it('reverts if the caller is not a member', async function () { + const fixture = await loadFixture(setup); + const { claims, cover, assessment } = fixture.contracts; + const [coverOwner] = fixture.accounts.members; + const [nonMember] = fixture.accounts.nonMembers; + + await createMockCover(cover, { owner: coverOwner.address }); + + const claimId = (await claims.getClaimsCount()) + 1n; + await submitClaim(fixture)({ coverId: 1, sender: coverOwner }); + const { timestamp: cooldownEnd } = await ethers.provider.getBlock('latest'); + const payoutRedemptionEnd = cooldownEnd + daysToSeconds(30); + await assessment.setAssessmentResult(claimId, ASSESSMENT_STATUS.ACCEPTED, payoutRedemptionEnd, cooldownEnd); + + const redeemClaimPayout = claims.connect(nonMember).redeemClaimPayout(claimId); + await expect(redeemClaimPayout).to.be.revertedWithCustomError(claims, 'OnlyMember'); + }); + + it('reverts if the caller is not the cover NFT owner', async function () { + const fixture = await loadFixture(setup); + const { claims, cover, assessment } = fixture.contracts; + const [coverOwner, otherMember] = fixture.accounts.members; + + await createMockCover(cover, { owner: coverOwner.address }); + + const claimId = (await claims.getClaimsCount()) + 1n; + await submitClaim(fixture)({ coverId: 1, sender: coverOwner }); + const { timestamp: cooldownEnd } = await ethers.provider.getBlock('latest'); + const payoutRedemptionEnd = cooldownEnd + daysToSeconds(30); + await assessment.setAssessmentResult(claimId, ASSESSMENT_STATUS.ACCEPTED, payoutRedemptionEnd, cooldownEnd); + + const redeemClaimPayout = claims.connect(otherMember).redeemClaimPayout(claimId); + await expect(redeemClaimPayout).to.be.revertedWithCustomError(claims, 'OnlyOwnerCanSubmitClaim'); + }); + it('reverts if the claim is not accepted', async function () { const fixture = await loadFixture(setup); const { claims, cover, assessment } = fixture.contracts; @@ -28,15 +63,18 @@ describe('redeemClaimPayout', function () { const claimId = (await claims.getClaimsCount()) + 1n; await submitClaim(fixture)({ coverId: 1, sender: coverOwner }); - const { timestamp } = await ethers.provider.getBlock('latest'); + const { timestamp: cooldownEnd } = await ethers.provider.getBlock('latest'); + const payoutRedemptionEnd = cooldownEnd + daysToSeconds(30); // denied - await assessment.setAssessmentResult(claimId, timestamp, ASSESSMENT_STATUS.DENIED); - await expect(claims.redeemClaimPayout(claimId)).to.be.revertedWithCustomError(claims, 'InvalidAssessmentStatus'); + await assessment.setAssessmentResult(claimId, ASSESSMENT_STATUS.DENIED, payoutRedemptionEnd, cooldownEnd); + const redeemClaimPayoutTx1 = claims.connect(coverOwner).redeemClaimPayout(claimId); + await expect(redeemClaimPayoutTx1).to.be.revertedWithCustomError(claims, 'InvalidAssessmentStatus'); // draw - await assessment.setAssessmentResult(claimId, timestamp, ASSESSMENT_STATUS.DRAW); - await expect(claims.redeemClaimPayout(claimId)).to.be.revertedWithCustomError(claims, 'InvalidAssessmentStatus'); + await assessment.setAssessmentResult(claimId, ASSESSMENT_STATUS.DRAW, payoutRedemptionEnd, cooldownEnd); + const redeemClaimPayoutTx2 = claims.connect(coverOwner).redeemClaimPayout(claimId); + await expect(redeemClaimPayoutTx2).to.be.revertedWithCustomError(claims, 'InvalidAssessmentStatus'); }); it('reverts while the claim is being assessed or in cooldown period', async function () { @@ -51,34 +89,38 @@ describe('redeemClaimPayout', function () { const claimId = (await claims.getClaimsCount()) + 1n; await submitClaim(fixture)({ coverId: 1, sender: coverOwner }); - const { timestamp } = await ethers.provider.getBlock('latest'); + const { timestamp: cooldownEnd } = await ethers.provider.getBlock('latest'); + const payoutRedemptionEnd = cooldownEnd + daysToSeconds(30); // still voting - await assessment.setAssessmentResult(claimId, timestamp, ASSESSMENT_STATUS.VOTING); - await expect(claims.redeemClaimPayout(claimId)).to.be.revertedWithCustomError(claims, 'InvalidAssessmentStatus'); + await assessment.setAssessmentResult(claimId, ASSESSMENT_STATUS.VOTING, payoutRedemptionEnd, cooldownEnd); + const redeemClaimPayoutTx1 = claims.connect(coverOwner).redeemClaimPayout(claimId); + await expect(redeemClaimPayoutTx1).to.be.revertedWithCustomError(claims, 'InvalidAssessmentStatus'); // cooldown - await assessment.setAssessmentResult(claimId, timestamp, ASSESSMENT_STATUS.COOLDOWN); - await expect(claims.redeemClaimPayout(claimId)).to.be.revertedWithCustomError(claims, 'InvalidAssessmentStatus'); + await assessment.setAssessmentResult(claimId, ASSESSMENT_STATUS.COOLDOWN, payoutRedemptionEnd, cooldownEnd); + const redeemClaimPayoutTx2 = claims.connect(coverOwner).redeemClaimPayout(claimId); + await expect(redeemClaimPayoutTx2).to.be.revertedWithCustomError(claims, 'InvalidAssessmentStatus'); }); it('reverts if the redemption period expired', async function () { const fixture = await loadFixture(setup); const { claims, cover, assessment } = fixture.contracts; const [coverOwner] = fixture.accounts.members; - const { payoutRedemptionPeriod } = fixture.config; await createMockCover(cover, { owner: coverOwner.address }); const claimId = (await claims.getClaimsCount()) + 1n; await submitClaim(fixture)({ coverId: 1, sender: coverOwner }); - const { timestamp } = await ethers.provider.getBlock('latest'); - await assessment.setAssessmentResult(claimId, timestamp, ASSESSMENT_STATUS.ACCEPTED); + const { timestamp: cooldownEnd } = await ethers.provider.getBlock('latest'); + const payoutRedemptionEnd = cooldownEnd + daysToSeconds(30); + await assessment.setAssessmentResult(claimId, ASSESSMENT_STATUS.ACCEPTED, payoutRedemptionEnd, cooldownEnd); - await expect(claims.redeemClaimPayout(claimId)).not.to.be.reverted; + await expect(claims.connect(coverOwner).redeemClaimPayout(claimId)).not.to.be.reverted; - await setTime(timestamp + payoutRedemptionPeriod); - await expect(claims.redeemClaimPayout(claimId)).to.be.revertedWithCustomError(claims, 'RedemptionPeriodExpired'); + await setTime(payoutRedemptionEnd); + const redeemClaimPayout = claims.connect(coverOwner).redeemClaimPayout(claimId); + await expect(redeemClaimPayout).to.be.revertedWithCustomError(claims, 'RedemptionPeriodExpired'); }); it('reverts if a payout has already been redeemed', async function () { @@ -90,11 +132,13 @@ describe('redeemClaimPayout', function () { const claimId = (await claims.getClaimsCount()) + 1n; await submitClaim(fixture)({ coverId: 1, sender: coverOwner }); - const { timestamp } = await ethers.provider.getBlock('latest'); - await assessment.setAssessmentResult(claimId, timestamp, ASSESSMENT_STATUS.ACCEPTED); + const { timestamp: cooldownEnd } = await ethers.provider.getBlock('latest'); + const payoutRedemptionEnd = cooldownEnd + daysToSeconds(30); + await assessment.setAssessmentResult(claimId, ASSESSMENT_STATUS.ACCEPTED, payoutRedemptionEnd, cooldownEnd); - await expect(claims.redeemClaimPayout(claimId)).not.to.be.reverted; - await expect(claims.redeemClaimPayout(claimId)).to.be.revertedWithCustomError(claims, 'PayoutAlreadyRedeemed'); + await expect(claims.connect(coverOwner).redeemClaimPayout(claimId)).not.to.be.reverted; + const redeemClaimPayout = claims.connect(coverOwner).redeemClaimPayout(claimId); + await expect(redeemClaimPayout).to.be.revertedWithCustomError(claims, 'PayoutAlreadyRedeemed'); }); it('Should emit ClaimPayoutRedeemed event', async function () { @@ -106,8 +150,9 @@ describe('redeemClaimPayout', function () { const claimId = (await claims.getClaimsCount()) + 1n; await submitClaim(fixture)({ coverId: 1, sender: coverOwner }); - const { timestamp } = await ethers.provider.getBlock('latest'); - await assessment.setAssessmentResult(claimId, timestamp, ASSESSMENT_STATUS.ACCEPTED); + const { timestamp: cooldownEnd } = await ethers.provider.getBlock('latest'); + const payoutRedemptionEnd = cooldownEnd + daysToSeconds(30); + await assessment.setAssessmentResult(claimId, ASSESSMENT_STATUS.ACCEPTED, payoutRedemptionEnd, cooldownEnd); await expect(claims.connect(coverOwner).redeemClaimPayout(claimId)) .to.emit(claims, 'ClaimPayoutRedeemed') @@ -122,18 +167,18 @@ describe('redeemClaimPayout', function () { await createMockCover(cover, { owner: coverOwner.address }); const claimId = (await claims.getClaimsCount()) + 1n; await submitClaim(fixture)({ coverId: 1, sender: coverOwner }); - const { timestamp } = await ethers.provider.getBlock('latest'); - await assessment.setAssessmentResult(claimId, timestamp, ASSESSMENT_STATUS.ACCEPTED); + const { timestamp: cooldownEnd } = await ethers.provider.getBlock('latest'); + const payoutRedemptionEnd = cooldownEnd + daysToSeconds(30); + await assessment.setAssessmentResult(claimId, ASSESSMENT_STATUS.ACCEPTED, payoutRedemptionEnd, cooldownEnd); - const redeemTx = await claims.redeemClaimPayout(claimId); - expect(redeemTx).to.emit(pool, 'TwapUpdateTriggered'); + expect(await claims.connect(coverOwner).redeemClaimPayout(claimId)).to.emit(pool, 'TwapUpdateTriggered'); }); it('sends the payout amount in ETH and the assessment deposit to the cover owner', async function () { const fixture = await loadFixture(setup); const { claims, cover, coverNFT, assessment } = fixture.contracts; const { claimDepositInETH: deposit } = fixture.config; - const [originalOwner, newOwner, otherMember] = fixture.accounts.members; + const [originalOwner, newOwner] = fixture.accounts.members; await createMockCover(cover, { owner: originalOwner.address }); @@ -145,10 +190,11 @@ describe('redeemClaimPayout', function () { await setNextBlockBaseFee('0'); await claims .connect(originalOwner) - .submitClaim(coverId, coverData.amount, toBeHex(0, 32), { value: deposit, gasPrice: 0 }); + .submitClaim(coverId, coverData.amount, ipfsHash, { value: deposit, gasPrice: 0 }); - const { timestamp } = await ethers.provider.getBlock('latest'); - await assessment.setAssessmentResult(claimId, timestamp, ASSESSMENT_STATUS.ACCEPTED); + const { timestamp: cooldownEnd } = await ethers.provider.getBlock('latest'); + const payoutRedemptionEnd = cooldownEnd + daysToSeconds(30); + await assessment.setAssessmentResult(claimId, ASSESSMENT_STATUS.ACCEPTED, payoutRedemptionEnd, cooldownEnd); await setNextBlockBaseFee('0'); await claims.connect(originalOwner).redeemClaimPayout(claimId, { gasPrice: 0 }); @@ -169,16 +215,20 @@ describe('redeemClaimPayout', function () { const newCoverData = await cover.getCoverData(coverId); const claimId = (await claims.getClaimsCount()) + 1n; - await claims.connect(originalOwner).submitClaim(coverId, newCoverData.amount, toBeHex(0, 32), { value: deposit }); + await claims.connect(originalOwner).submitClaim(coverId, newCoverData.amount, ipfsHash, { value: deposit }); - await assessment.setAssessmentResult(claimId, timestamp, ASSESSMENT_STATUS.ACCEPTED); + const { timestamp: cooldownEnd } = await ethers.provider.getBlock('latest'); + const payoutRedemptionEnd = cooldownEnd + daysToSeconds(30); + await assessment.setAssessmentResult(claimId, ASSESSMENT_STATUS.ACCEPTED, payoutRedemptionEnd, cooldownEnd); + const newOwnerBalanceBefore = await ethers.provider.getBalance(newOwner.address); await coverNFT.connect(originalOwner).transferFrom(originalOwner.address, newOwner.address, coverId); - const ethBalanceBefore = await ethers.provider.getBalance(newOwner.address); - await claims.connect(otherMember).redeemClaimPayout(claimId); // anyone can poke this - const ethBalanceAfter = await ethers.provider.getBalance(newOwner.address); - expect(ethBalanceAfter).to.be.equal(ethBalanceBefore + newCoverData.amount + deposit); + await setNextBlockBaseFee('0'); + await claims.connect(newOwner).redeemClaimPayout(claimId, { gasPrice: 0 }); // only NFT owner can redeem + const newOwnerBalanceAfter = await ethers.provider.getBalance(newOwner.address); + + expect(newOwnerBalanceAfter).to.be.equal(newOwnerBalanceBefore + newCoverData.amount + deposit); } }); @@ -186,7 +236,7 @@ describe('redeemClaimPayout', function () { const fixture = await loadFixture(setup); const { claims, cover, coverNFT, assessment, dai } = fixture.contracts; const { claimDepositInETH: deposit } = fixture.config; - const [originalOwner, newOwner, otherMember] = fixture.accounts.members; + const [originalOwner, newOwner] = fixture.accounts.members; await createMockCover(cover, { owner: originalOwner.address, coverAsset: ASSET.DAI }); const coverId = 1; @@ -199,10 +249,11 @@ describe('redeemClaimPayout', function () { const claimId = (await claims.getClaimsCount()) + 1n; await claims .connect(originalOwner) - .submitClaim(coverId, coverData.amount, toBeHex(0, 32), { value: deposit, gasPrice: 0 }); + .submitClaim(coverId, coverData.amount, ipfsHash, { value: deposit, gasPrice: 0 }); - const { timestamp } = await ethers.provider.getBlock('latest'); - await assessment.setAssessmentResult(claimId, timestamp, ASSESSMENT_STATUS.ACCEPTED); + const { timestamp: cooldownEnd } = await ethers.provider.getBlock('latest'); + const payoutRedemptionEnd = cooldownEnd + daysToSeconds(30); + await assessment.setAssessmentResult(claimId, ASSESSMENT_STATUS.ACCEPTED, payoutRedemptionEnd, cooldownEnd); await setNextBlockBaseFee('0'); await claims.connect(originalOwner).redeemClaimPayout(claimId, { gasPrice: 0 }); @@ -217,25 +268,28 @@ describe('redeemClaimPayout', function () { const coverId = 2; const coverData = await cover.getCoverData(coverId); - const ethBalanceBefore = await ethers.provider.getBalance(newOwner.address); - const daiBalanceBefore = await dai.balanceOf(newOwner.address); + const newOwnerEthBalanceBefore = await ethers.provider.getBalance(newOwner.address); + const newOwnerDaiBalanceBefore = await dai.balanceOf(newOwner.address); const claimId = (await claims.getClaimsCount()) + 1n; await setNextBlockBaseFee('0'); await claims .connect(originalOwner) - .submitClaim(coverId, coverData.amount, toBeHex(0, 32), { value: deposit, gasPrice: 0 }); + .submitClaim(coverId, coverData.amount, ipfsHash, { value: deposit, gasPrice: 0 }); - const { timestamp } = await ethers.provider.getBlock('latest'); - await assessment.setAssessmentResult(claimId, timestamp, ASSESSMENT_STATUS.ACCEPTED); + const { timestamp: cooldownEnd } = await ethers.provider.getBlock('latest'); + const payoutRedemptionEnd = cooldownEnd + daysToSeconds(30); + await assessment.setAssessmentResult(claimId, ASSESSMENT_STATUS.ACCEPTED, payoutRedemptionEnd, cooldownEnd); await coverNFT.connect(originalOwner).transferFrom(originalOwner.address, newOwner.address, coverId); - await claims.connect(otherMember).redeemClaimPayout(claimId); // anyone can poke this - const ethBalanceAfter = await ethers.provider.getBalance(newOwner.address); - const daiBalanceAfter = await dai.balanceOf(newOwner.address); - expect(ethBalanceAfter).to.be.equal(ethBalanceBefore + deposit); - expect(daiBalanceAfter).to.be.equal(daiBalanceBefore + coverData.amount); + await setNextBlockBaseFee('0'); + await claims.connect(newOwner).redeemClaimPayout(claimId, { gasPrice: 0 }); // onlyNFT owner can redeem + const newOwnerEthBalanceAfter = await ethers.provider.getBalance(newOwner.address); + const newOwnerDaiBalanceAfter = await dai.balanceOf(newOwner.address); + + expect(newOwnerEthBalanceAfter).to.be.equal(newOwnerEthBalanceBefore + deposit); + expect(newOwnerDaiBalanceAfter).to.be.equal(newOwnerDaiBalanceBefore + coverData.amount); } }); @@ -251,19 +305,18 @@ describe('redeemClaimPayout', function () { { const coverId = 3; - const coverData = await cover.getCoverData(coverId); + const { amount: coverAmount } = await cover.getCoverData(coverId); await setNextBlockBaseFee('0'); const claimId = (await claims.getClaimsCount()) + 1n; - await claims - .connect(coverOwner) - .submitClaim(coverId, coverData.amount, toBeHex(0, 32), { value: deposit, gasPrice: 0 }); + await claims.connect(coverOwner).submitClaim(coverId, coverAmount, ipfsHash, { value: deposit, gasPrice: 0 }); - const { timestamp } = await ethers.provider.getBlock('latest'); - await assessment.setAssessmentResult(claimId, timestamp, ASSESSMENT_STATUS.ACCEPTED); + const { timestamp: cooldownEnd } = await ethers.provider.getBlock('latest'); + const payoutRedemptionEnd = cooldownEnd + daysToSeconds(30); + await assessment.setAssessmentResult(claimId, ASSESSMENT_STATUS.ACCEPTED, payoutRedemptionEnd, cooldownEnd); await claims.connect(coverOwner).redeemClaimPayout(claimId, { gasPrice: 0 }); const burnStakeCalledWith = await cover.burnStakeCalledWith(); - expect(burnStakeCalledWith.amount).to.be.equal(coverData.amount); + expect(burnStakeCalledWith.amount).to.be.equal(coverAmount); } { @@ -273,12 +326,11 @@ describe('redeemClaimPayout', function () { await setNextBlockBaseFee('0'); const claimId = (await claims.getClaimsCount()) + 1n; - await claims - .connect(coverOwner) - .submitClaim(coverId, claimAmount, toBeHex(0, 32), { value: deposit, gasPrice: 0 }); + await claims.connect(coverOwner).submitClaim(coverId, claimAmount, ipfsHash, { value: deposit, gasPrice: 0 }); - const { timestamp } = await ethers.provider.getBlock('latest'); - await assessment.setAssessmentResult(claimId, timestamp, ASSESSMENT_STATUS.ACCEPTED); + const { timestamp: cooldownEnd } = await ethers.provider.getBlock('latest'); + const payoutRedemptionEnd = cooldownEnd + daysToSeconds(30); + await assessment.setAssessmentResult(claimId, ASSESSMENT_STATUS.ACCEPTED, payoutRedemptionEnd, cooldownEnd); await setNextBlockBaseFee('0'); await claims.connect(coverOwner).redeemClaimPayout(claimId, { gasPrice: 0 }); @@ -297,101 +349,12 @@ describe('redeemClaimPayout', function () { const claimId = (await claims.getClaimsCount()) + 1n; await submitClaim(fixture)({ coverId: 1, sender: coverOwner }); - const { timestamp } = await ethers.provider.getBlock('latest'); - await assessment.setAssessmentResult(claimId, timestamp, ASSESSMENT_STATUS.ACCEPTED); + const { timestamp: cooldownEnd } = await ethers.provider.getBlock('latest'); + const payoutRedemptionEnd = cooldownEnd + daysToSeconds(30); + await assessment.setAssessmentResult(claimId, ASSESSMENT_STATUS.ACCEPTED, payoutRedemptionEnd, cooldownEnd); await registry.confirmPauseConfig(PAUSE_CLAIMS_PAYOUT); await expect(claims.connect(coverOwner).redeemClaimPayout(claimId)).to.be.revertedWithCustomError(claims, 'Paused'); }); - - it('should retrive deposit in case of draw', async function () { - const fixture = await loadFixture(setup); - const { claims, cover, assessment } = fixture.contracts; - const { claimDepositInETH: deposit } = fixture.config; - const [originalOwner] = fixture.accounts.members; - - await createMockCover(cover, { owner: originalOwner.address }); - - const coverId = 1; - const coverData = await cover.getCoverData(coverId); - - const claimId = (await claims.getClaimsCount()) + 1n; - await setNextBlockBaseFee('0'); - await claims - .connect(originalOwner) - .submitClaim(coverId, coverData.amount, toBeHex(0, 32), { value: deposit, gasPrice: 0 }); - - const ethBalanceAfterSubmittingClaim = await ethers.provider.getBalance(originalOwner.address); - - const { timestamp } = await ethers.provider.getBlock('latest'); - await assessment.setAssessmentResult(claimId, timestamp, ASSESSMENT_STATUS.DRAW); - - await setNextBlockBaseFee('0'); - await claims.connect(originalOwner).retrieveDeposit(claimId, { gasPrice: 0 }); - const ethBalanceAfter = await ethers.provider.getBalance(originalOwner.address); - - expect(ethBalanceAfter).to.be.equal(ethBalanceAfterSubmittingClaim + deposit); - }); - - it('should not be able to call retrive deposit of not a draw', async function () { - const fixture = await loadFixture(setup); - const { claims, cover, assessment } = fixture.contracts; - const { claimDepositInETH: deposit } = fixture.config; - const [originalOwner] = fixture.accounts.members; - - await createMockCover(cover, { owner: originalOwner.address }); - - const coverId = 1; - const coverData = await cover.getCoverData(coverId); - - const claimId = (await claims.getClaimsCount()) + 1n; - await setNextBlockBaseFee('0'); - await claims - .connect(originalOwner) - .submitClaim(coverId, coverData.amount, toBeHex(0, 32), { value: deposit, gasPrice: 0 }); - - const { timestamp } = await ethers.provider.getBlock('latest'); - - await assessment.setAssessmentResult(claimId, timestamp, ASSESSMENT_STATUS.ACCEPTED); - await expect(claims.connect(originalOwner).retrieveDeposit(claimId, { gasPrice: 0 })).to.be.revertedWithCustomError( - claims, - 'InvalidAssessmentStatus', - ); - - await assessment.setAssessmentResult(claimId, timestamp, ASSESSMENT_STATUS.DENIED); - await expect(claims.connect(originalOwner).retrieveDeposit(claimId, { gasPrice: 0 })).to.be.revertedWithCustomError( - claims, - 'InvalidAssessmentStatus', - ); - - await assessment.setAssessmentResult(claimId, timestamp, ASSESSMENT_STATUS.VOTING); - await expect(claims.connect(originalOwner).retrieveDeposit(claimId, { gasPrice: 0 })).to.be.revertedWithCustomError( - claims, - 'InvalidAssessmentStatus', - ); - - await assessment.setAssessmentResult(claimId, timestamp, ASSESSMENT_STATUS.COOLDOWN); - await expect(claims.connect(originalOwner).retrieveDeposit(claimId, { gasPrice: 0 })).to.be.revertedWithCustomError( - claims, - 'InvalidAssessmentStatus', - ); - }); - - it('retrieve deposit should revert if payout is paused', async function () { - const fixture = await loadFixture(setup); - const { claims, cover, assessment, registry } = fixture.contracts; - const [coverOwner] = fixture.accounts.members; - - await createMockCover(cover, { owner: coverOwner.address }); - - const claimId = (await claims.getClaimsCount()) + 1n; - await submitClaim(fixture)({ coverId: 1, sender: coverOwner }); - const { timestamp } = await ethers.provider.getBlock('latest'); - await assessment.setAssessmentResult(claimId, timestamp, ASSESSMENT_STATUS.DRAW); - - await registry.confirmPauseConfig(PAUSE_CLAIMS_PAYOUT); - - await expect(claims.connect(coverOwner).retrieveDeposit(claimId)).to.be.revertedWithCustomError(claims, 'Paused'); - }); }); diff --git a/test/unit/Claims/submitClaim.js b/test/unit/Claims/submitClaim.js index 43d666c7d1..569dafb828 100644 --- a/test/unit/Claims/submitClaim.js +++ b/test/unit/Claims/submitClaim.js @@ -4,7 +4,7 @@ const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); const { parseEther, toBeHex } = ethers; const { mineNextBlock, setNextBlockTime } = require('../../utils/evm'); -const { ASSET, ASSESSMENT_STATUS, createMockCover, submitClaim } = require('./helpers'); +const { ASSET, ASSESSMENT_STATUS, createMockCover, submitClaim, daysToSeconds } = require('./helpers'); const { setup } = require('./setup'); const { ContractIndexes } = nexus.constants; @@ -15,53 +15,39 @@ const setTime = async timestamp => { }; describe('submitClaim', function () { + const ipfsHash = ethers.solidityPackedKeccak256(['string'], ['ipfs-hash']); + it('reverts if sender is not a member', async function () { const fixture = await loadFixture(setup); const { claims } = fixture.contracts; const [nonMember] = fixture.accounts.nonMembers; - await expect( - claims.connect(nonMember).submitClaim(1, parseEther('100'), toBeHex(0, 32)), - ).to.be.revertedWithCustomError(claims, 'OnlyMember'); + const submitClaim = claims.connect(nonMember).submitClaim(1, parseEther('100'), ipfsHash); + await expect(submitClaim).to.be.revertedWithCustomError(claims, 'OnlyMember'); }); - it('reverts if sender is not a cover owner or an approved address', async function () { + it('reverts if sender is not the cover owner', async function () { const fixture = await loadFixture(setup); const { cover, coverNFT, claims } = fixture.contracts; - const [coverOwner, otherMember] = fixture.accounts.members; + const [coverOwner, otherMember, approvedMember] = fixture.accounts.members; await createMockCover(cover, { owner: coverOwner.address }); const coverId = 1; - await expect(submitClaim(fixture)({ coverId, sender: otherMember })).to.be.revertedWithCustomError( - claims, - 'OnlyOwnerOrApprovedCanSubmitClaim', - ); - await expect(submitClaim(fixture)({ coverId, sender: coverOwner })).not.to.be.revertedWithCustomError( - claims, - 'OnlyOwnerOrApprovedCanSubmitClaim', - ); + // Not owner fails + const notOwnerSubmitClaim = submitClaim(fixture)({ coverId, sender: otherMember }); + await expect(notOwnerSubmitClaim).to.be.revertedWithCustomError(claims, 'OnlyOwnerCanSubmitClaim'); + // Owner succeeds + const ownerSubmitClaim = submitClaim(fixture)({ coverId, sender: coverOwner }); + await expect(ownerSubmitClaim).not.to.be.revertedWithCustomError(claims, 'OnlyOwnerCanSubmitClaim'); const { timestamp } = await ethers.provider.getBlock('latest'); await createMockCover(cover, { owner: coverOwner.address, start: timestamp + 1 }); const coverId2 = 2; - await coverNFT.connect(coverOwner).approve(otherMember.address, coverId2); - await expect(submitClaim(fixture)({ coverId: coverId2, sender: otherMember })).not.to.be.revertedWithCustomError( - claims, - 'OnlyOwnerOrApprovedCanSubmitClaim', - ); - }); - - it('reverts if sender is not a cover owner', async function () { - const fixture = await loadFixture(setup); - const { claims, cover } = fixture.contracts; - const [coverOwner, notOwner] = fixture.accounts.members; - - await createMockCover(cover, { owner: coverOwner.address }); - const coverId = 1; - await expect( - claims.connect(notOwner).submitClaim(coverId, parseEther('100'), toBeHex(0, 32)), - ).to.be.revertedWithCustomError(claims, 'OnlyOwnerOrApprovedCanSubmitClaim'); + // Approved also fails + await coverNFT.connect(coverOwner).approve(approvedMember.address, coverId2); + const approvedSubmitClaim = submitClaim(fixture)({ coverId: coverId2, sender: approvedMember }); + await expect(approvedSubmitClaim).to.be.revertedWithCustomError(claims, 'OnlyOwnerCanSubmitClaim'); }); it('reverts if a claim on the same cover is already being assessed', async function () { @@ -74,15 +60,16 @@ describe('submitClaim', function () { await submitClaim(fixture)({ coverId: 1, sender: coverOwner }); const claimId = await claims.getClaimsCount(); - const { timestamp } = await ethers.provider.getBlock('latest'); + const { timestamp: cooldownEnd } = await ethers.provider.getBlock('latest'); + const payoutRedemptionEnd = cooldownEnd + daysToSeconds(30); - await assessment.setAssessmentResult(claimId, timestamp, ASSESSMENT_STATUS.VOTING); + await assessment.setAssessmentResult(claimId, ASSESSMENT_STATUS.VOTING, payoutRedemptionEnd, cooldownEnd); await expect(submitClaim(fixture)({ coverId: 1, sender: coverOwner })).to.be.revertedWithCustomError( claims, 'ClaimIsBeingAssessed', ); - await assessment.setAssessmentResult(claimId, timestamp, ASSESSMENT_STATUS.COOLDOWN); + await assessment.setAssessmentResult(claimId, ASSESSMENT_STATUS.COOLDOWN, payoutRedemptionEnd, cooldownEnd); await expect(submitClaim(fixture)({ coverId: 1, sender: coverOwner })).to.be.revertedWithCustomError( claims, 'ClaimIsBeingAssessed', @@ -98,20 +85,19 @@ describe('submitClaim', function () { await createMockCover(cover, { owner: coverOwner.address }); const coverId = 1; + const amount = parseEther('100'); + // not sending deposit - await expect( - claims.connect(coverOwner).submitClaim(coverId, parseEther('100'), toBeHex(0, 32)), - ).to.be.revertedWithCustomError(claims, 'AssessmentDepositNotExact'); + const submitClaimNoDeposit = submitClaim(fixture)({ coverId, amount, sender: coverOwner, value: 0n }); + await expect(submitClaimNoDeposit).to.be.revertedWithCustomError(claims, 'AssessmentDepositNotExact'); // sending less than expected - await expect( - claims.connect(coverOwner).submitClaim(coverId, parseEther('100'), toBeHex(0, 32), { value: deposit - 1n }), - ).to.be.revertedWithCustomError(claims, 'AssessmentDepositNotExact'); + const submitClaimLess = submitClaim(fixture)({ coverId, amount, sender: coverOwner, value: deposit - 1n }); + await expect(submitClaimLess).to.be.revertedWithCustomError(claims, 'AssessmentDepositNotExact'); // sending more than expected - await expect( - claims.connect(coverOwner).submitClaim(coverId, parseEther('100'), toBeHex(0, 32), { value: deposit + 1n }), - ).to.be.revertedWithCustomError(claims, 'AssessmentDepositNotExact'); + const submitClaimMore = submitClaim(fixture)({ coverId, amount, sender: coverOwner, value: deposit + 1n }); + await expect(submitClaimMore).to.be.revertedWithCustomError(claims, 'AssessmentDepositNotExact'); }); it('reverts if the requested amount exceeds cover amount', async function () { @@ -125,9 +111,9 @@ describe('submitClaim', function () { const coverId = 1; const coverData = await cover.getCoverData(coverId); - await expect( - claims.connect(coverOwner).submitClaim(coverId, coverData.amount + 1n, toBeHex(0, 32), { value: deposit }), - ).to.be.revertedWithCustomError(claims, 'CoveredAmountExceeded'); + const amountExceeded = coverData.amount + 1n; + const submitClaimExceeded = submitClaim(fixture)({ coverId, amount: amountExceeded, value: deposit }); + await expect(submitClaimExceeded).to.be.revertedWithCustomError(claims, 'CoveredAmountExceeded'); }); it('reverts if the claim submission is made in the same block as the cover purchase', async function () { @@ -142,12 +128,11 @@ describe('submitClaim', function () { await createMockCover(cover, { owner: coverOwner.address, start }); const coverId = 1; - const coverData = await cover.getCoverData(coverId); + const { amount } = await cover.getCoverData(coverId); await setNextBlockTime(start); - await expect( - claims.connect(coverOwner).submitClaim(coverId, coverData.amount, toBeHex(0, 32), { value: deposit }), - ).to.be.revertedWithCustomError(claims, 'CantBuyCoverAndClaimInTheSameBlock'); + const submitClaimSameBlock = submitClaim(fixture)({ coverId, amount, value: deposit, sender: coverOwner }); + await expect(submitClaimSameBlock).to.be.revertedWithCustomError(claims, 'CantBuyCoverAndClaimInTheSameBlock'); }); it('reverts if the cover is outside the grace period', async function () { @@ -166,10 +151,7 @@ describe('submitClaim', function () { // Advance time to grace period expiry await setTime(timestamp + Number(coverData.start) + Number(coverData.period) + Number(gracePeriod)); - await expect(submitClaim(fixture)({ coverId, sender: coverOwner })).to.be.revertedWithCustomError( - claims, - 'GracePeriodPassed', - ); + await expect(submitClaim(fixture)({ coverId })).to.be.revertedWithCustomError(claims, 'GracePeriodPassed'); }); it('Assessment should use cover grace period and not product.gracePeriod', async function () { @@ -192,10 +174,7 @@ describe('submitClaim', function () { // Advance time to grace period expiry await setTime(timestamp + Number(coverData.start) + Number(coverData.period) + Number(gracePeriod)); - await expect(submitClaim(fixture)({ coverId, sender: coverOwner })).to.be.revertedWithCustomError( - claims, - 'GracePeriodPassed', - ); + await expect(submitClaim(fixture)({ coverId })).to.be.revertedWithCustomError(claims, 'GracePeriodPassed'); }); it('calls startAssessment with the right productTypeId', async function () { @@ -216,14 +195,16 @@ describe('submitClaim', function () { it('emits MetadataSubmitted event with the provided ipfsMetadata when it is not an empty bytes', async function () { const fixture = await loadFixture(setup); const { claims, cover } = fixture.contracts; - const ipfsMetadata = toBeHex(5, 32); + const ipfsMetadata = ipfsHash; const [coverOwner] = fixture.accounts.members; await createMockCover(cover, { owner: coverOwner.address }); - await expect(submitClaim(fixture)({ coverId: 1, ipfsMetadata, sender: coverOwner })) - .to.emit(claims, 'MetadataSubmitted') - .withArgs(1, ipfsMetadata); + const coverId = 1; + const submitClaimTx = submitClaim(fixture)({ coverId, ipfsMetadata, sender: coverOwner }); + + const claimId = (await claims.getClaimsCount()) + 1n; + await expect(submitClaimTx).to.emit(claims, 'MetadataSubmitted').withArgs(claimId, ipfsHash); await createMockCover(cover, { owner: coverOwner.address }); @@ -253,8 +234,9 @@ describe('submitClaim', function () { expect(await claims.lastClaimSubmissionOnCover(2)).to.equal(2n); // Set cover 1 claim to DRAW to allow retry - const { timestamp } = await ethers.provider.getBlock('latest'); - await assessment.setAssessmentResult(1, timestamp, ASSESSMENT_STATUS.DRAW); + const { timestamp: cooldownEnd } = await ethers.provider.getBlock('latest'); + const payoutRedemptionEnd = cooldownEnd + daysToSeconds(30); + await assessment.setAssessmentResult(1, ASSESSMENT_STATUS.DRAW, payoutRedemptionEnd, cooldownEnd); // Second claim on cover 1 (retry after DRAW) await submitClaim(fixture)({ coverId: 1, sender: coverOwner }); @@ -288,9 +270,8 @@ describe('submitClaim', function () { const coverId = 1; - await expect(submitClaim(fixture)({ coverId, sender: coverOwner })) - .to.emit(claims, 'ClaimSubmitted') - .withArgs(coverOwner.address, 1, coverId, 2); + const submitClaimTx = submitClaim(fixture)({ coverId, sender: coverOwner }); + await expect(submitClaimTx).to.emit(claims, 'ClaimSubmitted').withArgs(coverOwner.address, 1, coverId, 2); }); it('creates claim struct with correct initial values', async function () { @@ -332,8 +313,9 @@ describe('submitClaim', function () { expect(claim.depositRetrieved).to.be.false; // Accept the claim - const { timestamp } = await ethers.provider.getBlock('latest'); - await assessment.setAssessmentResult(claimId, timestamp, ASSESSMENT_STATUS.ACCEPTED); + const { timestamp: cooldownEnd } = await ethers.provider.getBlock('latest'); + const payoutRedemptionEnd = cooldownEnd + daysToSeconds(30); + await assessment.setAssessmentResult(claimId, ASSESSMENT_STATUS.ACCEPTED, payoutRedemptionEnd, cooldownEnd); // State should still be false until payout is redeemed claim = await claims.getClaimInfo(claimId); @@ -341,7 +323,7 @@ describe('submitClaim', function () { expect(claim.depositRetrieved).to.be.false; // Redeem payout - await claims.redeemClaimPayout(claimId); + await claims.connect(coverOwner).redeemClaimPayout(claimId); // Both fields should become true claim = await claims.getClaimInfo(claimId); @@ -379,7 +361,6 @@ describe('submitClaim', function () { it('reverts if a payout on the same cover can be redeemed', async function () { const fixture = await loadFixture(setup); const { cover, assessment, claims } = fixture.contracts; - const { payoutRedemptionPeriod } = fixture.config; const [coverOwner] = fixture.accounts.members; await createMockCover(cover, { owner: coverOwner.address }); @@ -387,16 +368,17 @@ describe('submitClaim', function () { await submitClaim(fixture)({ coverId: 1, sender: coverOwner }); const claimId = await claims.getClaimsCount(); - const { timestamp } = await ethers.provider.getBlock('latest'); - await assessment.setAssessmentResult(claimId, timestamp, ASSESSMENT_STATUS.ACCEPTED); + const { timestamp: cooldownEnd } = await ethers.provider.getBlock('latest'); + const payoutRedemptionEnd = cooldownEnd + daysToSeconds(30); + await assessment.setAssessmentResult(claimId, ASSESSMENT_STATUS.ACCEPTED, payoutRedemptionEnd, cooldownEnd); // Should block new claim during redemption period - await setTime(timestamp + Math.floor(payoutRedemptionPeriod / 2)); + await setTime(payoutRedemptionEnd - daysToSeconds(1)); const submitClaimTx = submitClaim(fixture)({ coverId: 1, sender: coverOwner }); await expect(submitClaimTx).to.be.revertedWithCustomError(claims, 'PayoutCanStillBeRedeemed'); // Should allow new claim after redemption period expires - await setTime(timestamp + payoutRedemptionPeriod + 1); + await setTime(payoutRedemptionEnd); await expect(submitClaim(fixture)({ coverId: 1, sender: coverOwner })).not.to.be.reverted; }); @@ -415,21 +397,24 @@ describe('submitClaim', function () { await submitClaim(fixture)({ coverId: 1, sender: coverOwner }); const firstClaimId = await claims.getClaimsCount(); - let { timestamp } = await ethers.provider.getBlock('latest'); - await assessment.setAssessmentResult(firstClaimId, timestamp, ASSESSMENT_STATUS.ACCEPTED); - await claims.redeemClaimPayout(firstClaimId); + let { timestamp: cooldownEnd } = await ethers.provider.getBlock('latest'); + let payoutRedemptionEnd = cooldownEnd + daysToSeconds(30); + await assessment.setAssessmentResult(firstClaimId, ASSESSMENT_STATUS.ACCEPTED, payoutRedemptionEnd, cooldownEnd); + await claims.connect(coverOwner).redeemClaimPayout(firstClaimId); // Second claim on cover 2: set to DRAW await submitClaim(fixture)({ coverId: 2, sender: coverOwner }); const secondClaimId = await claims.getClaimsCount(); - ({ timestamp } = await ethers.provider.getBlock('latest')); - await assessment.setAssessmentResult(secondClaimId, timestamp, ASSESSMENT_STATUS.DRAW); + ({ timestamp: cooldownEnd } = await ethers.provider.getBlock('latest')); + payoutRedemptionEnd = cooldownEnd + daysToSeconds(30); + await assessment.setAssessmentResult(secondClaimId, ASSESSMENT_STATUS.DRAW, payoutRedemptionEnd, cooldownEnd); // Third claim on cover 2 again (retry for clear decision) await submitClaim(fixture)({ coverId: 2, sender: coverOwner }); const thirdClaimId = await claims.getClaimsCount(); - ({ timestamp } = await ethers.provider.getBlock('latest')); - await assessment.setAssessmentResult(thirdClaimId, timestamp, ASSESSMENT_STATUS.DENIED); + ({ timestamp: cooldownEnd } = await ethers.provider.getBlock('latest')); + payoutRedemptionEnd = cooldownEnd + daysToSeconds(30); + await assessment.setAssessmentResult(thirdClaimId, ASSESSMENT_STATUS.DENIED, payoutRedemptionEnd, cooldownEnd); // Fourth claim on cover 3 (new cover) await expect(submitClaim(fixture)({ coverId: 3, sender: coverOwner })).not.to.be.reverted; @@ -447,40 +432,17 @@ describe('submitClaim', function () { await submitClaim(fixture)({ coverId: 1, sender: coverOwner }); const firstClaimId = await claims.getClaimsCount(); - const { timestamp } = await ethers.provider.getBlock('latest'); - await assessment.setAssessmentResult(firstClaimId, timestamp, ASSESSMENT_STATUS.ACCEPTED); + const { timestamp: cooldownEnd } = await ethers.provider.getBlock('latest'); + const payoutRedemptionEnd = cooldownEnd + daysToSeconds(30); + await assessment.setAssessmentResult(firstClaimId, ASSESSMENT_STATUS.ACCEPTED, payoutRedemptionEnd, cooldownEnd); // Redeem the payout - await claims.redeemClaimPayout(firstClaimId); + await claims.connect(coverOwner).redeemClaimPayout(firstClaimId); // Should prevent re-submission after payout redeemed const submitClaimTx = submitClaim(fixture)({ coverId: 1, sender: coverOwner }); await expect(submitClaimTx).to.be.revertedWithCustomError(claims, 'ClaimAlreadyPaidOut'); }); - - it('reverts if a payout on the same cover can be redeemed', async function () { - const fixture = await loadFixture(setup); - const { cover, assessment, claims } = fixture.contracts; - const { payoutRedemptionPeriod } = fixture.config; - const [coverOwner] = fixture.accounts.members; - - await createMockCover(cover, { owner: coverOwner.address }); - - await submitClaim(fixture)({ coverId: 1, sender: coverOwner }); - const claimId = await claims.getClaimsCount(); - - const { timestamp } = await ethers.provider.getBlock('latest'); - await assessment.setAssessmentResult(claimId, timestamp, ASSESSMENT_STATUS.ACCEPTED); - - // Should block new claim during redemption period - await setTime(timestamp + Math.floor(payoutRedemptionPeriod / 2)); - const submitClaimTx = submitClaim(fixture)({ coverId: 1, sender: coverOwner }); - await expect(submitClaimTx).to.be.revertedWithCustomError(claims, 'PayoutCanStillBeRedeemed'); - - // Should allow new claim after redemption period expires - await setTime(timestamp + payoutRedemptionPeriod + 1); - await expect(submitClaim(fixture)({ coverId: 1, sender: coverOwner })).not.to.be.reverted; - }); }); describe('Allowed re-submission scenarios within grace period', function () { @@ -495,8 +457,9 @@ describe('submitClaim', function () { await submitClaim(fixture)({ coverId: 1, sender: coverOwner }); const firstClaimId = await claims.getClaimsCount(); - const { timestamp } = await ethers.provider.getBlock('latest'); - await assessment.setAssessmentResult(firstClaimId, timestamp, ASSESSMENT_STATUS.DENIED); + const { timestamp: cooldownEnd } = await ethers.provider.getBlock('latest'); + const payoutRedemptionEnd = cooldownEnd + daysToSeconds(30); + await assessment.setAssessmentResult(firstClaimId, ASSESSMENT_STATUS.DENIED, payoutRedemptionEnd, cooldownEnd); // Should allow re-submission after DENIED (edge case - maybe new evidence) await expect(submitClaim(fixture)({ coverId: 1, sender: coverOwner })).not.to.be.reverted; @@ -514,8 +477,9 @@ describe('submitClaim', function () { const firstClaimId = await claims.getClaimsCount(); // Set first claim result to DRAW - const { timestamp } = await ethers.provider.getBlock('latest'); - await assessment.setAssessmentResult(firstClaimId, timestamp, ASSESSMENT_STATUS.DRAW); + const { timestamp: cooldownEnd } = await ethers.provider.getBlock('latest'); + const payoutRedemptionEnd = cooldownEnd + daysToSeconds(30); + await assessment.setAssessmentResult(firstClaimId, ASSESSMENT_STATUS.DRAW, payoutRedemptionEnd, cooldownEnd); // Should allow submitting a new claim before deposit retrieval await expect(submitClaim(fixture)({ coverId: 1, sender: coverOwner })).not.to.be.reverted; @@ -536,8 +500,9 @@ describe('submitClaim', function () { const firstClaimId = await claims.getClaimsCount(); // Set first claim result to DRAW - const { timestamp } = await ethers.provider.getBlock('latest'); - await assessment.setAssessmentResult(firstClaimId, timestamp, ASSESSMENT_STATUS.DRAW); + const { timestamp: cooldownEnd } = await ethers.provider.getBlock('latest'); + const payoutRedemptionEnd = cooldownEnd + daysToSeconds(30); + await assessment.setAssessmentResult(firstClaimId, ASSESSMENT_STATUS.DRAW, payoutRedemptionEnd, cooldownEnd); // Retrieve deposit from DRAW claim await claims.retrieveDeposit(firstClaimId); @@ -549,7 +514,6 @@ describe('submitClaim', function () { it('allows re-submission when user missed redeeming the payout', async function () { const fixture = await loadFixture(setup); const { cover, assessment, claims } = fixture.contracts; - const { payoutRedemptionPeriod } = fixture.config; const [coverOwner] = fixture.accounts.members; await createMockCover(cover, { owner: coverOwner.address }); @@ -558,11 +522,12 @@ describe('submitClaim', function () { await submitClaim(fixture)({ coverId: 1, sender: coverOwner }); const firstClaimId = await claims.getClaimsCount(); - const { timestamp } = await ethers.provider.getBlock('latest'); - await assessment.setAssessmentResult(firstClaimId, timestamp, ASSESSMENT_STATUS.ACCEPTED); + const { timestamp: cooldownEnd } = await ethers.provider.getBlock('latest'); + const payoutRedemptionEnd = cooldownEnd + daysToSeconds(30); + await assessment.setAssessmentResult(firstClaimId, ASSESSMENT_STATUS.ACCEPTED, payoutRedemptionEnd, cooldownEnd); // Move time past redemption period - await setTime(timestamp + payoutRedemptionPeriod + 1); + await setTime(payoutRedemptionEnd); // Should allow re-submission after redemption period expires (user missed deadline) await expect(submitClaim(fixture)({ coverId: 1, sender: coverOwner })).not.to.be.reverted; From ced2c20464cdbe7e3c5d21f1a90a783f99e7594d Mon Sep 17 00:00:00 2001 From: Rocky Murdoch Date: Wed, 6 Aug 2025 17:28:04 +0300 Subject: [PATCH 10/18] feat: add whenNotPaused modifier on submitClaim + rename PAUSE_CLAIMS_PAYOUTS --- contracts/abstract/RegistryAware.sol | 2 +- contracts/modules/assessment/Claims.sol | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/contracts/abstract/RegistryAware.sol b/contracts/abstract/RegistryAware.sol index d6e9ebc2b9..5791020b5e 100644 --- a/contracts/abstract/RegistryAware.sol +++ b/contracts/abstract/RegistryAware.sol @@ -30,7 +30,7 @@ uint constant PAUSE_RAMM = 1 << 1; // 2 uint constant PAUSE_SWAPS = 1 << 2; // 4 uint constant PAUSE_MEMBERSHIP = 1 << 3; // 8 uint constant PAUSE_ASSESSMENTS = 1 << 4; // 16 -uint constant PAUSE_CLAIMS_PAYOUT = 1 << 5; // 32 +uint constant PAUSE_CLAIMS_PAYOUTS = 1 << 5; // 32 contract RegistryAware { diff --git a/contracts/modules/assessment/Claims.sol b/contracts/modules/assessment/Claims.sol index 49a78ff132..38b5fb7008 100644 --- a/contracts/modules/assessment/Claims.sol +++ b/contracts/modules/assessment/Claims.sol @@ -151,7 +151,7 @@ contract Claims is IClaims, RegistryAware { uint32 coverId, uint96 requestedAmount, bytes32 ipfsMetadata - ) external payable override onlyMember returns (Claim memory claim) { + ) external payable override onlyMember whenNotPaused(PAUSE_CLAIMS_PAYOUTS) returns (Claim memory claim) { require(coverNFT.isApprovedOrOwner(msg.sender, coverId), OnlyOwnerOrApprovedCanSubmitClaim()); require(coverNFT.ownerOf(coverId) == msg.sender, OnlyOwnerCanSubmitClaim()); @@ -237,7 +237,7 @@ contract Claims is IClaims, RegistryAware { /// @dev Must be the cover NFT owner for the claim and a member can call this function /// /// @param claimId Claim identifier - function redeemClaimPayout(uint claimId) external override onlyMember whenNotPaused(PAUSE_CLAIMS_PAYOUT) { + function redeemClaimPayout(uint claimId) external override onlyMember whenNotPaused(PAUSE_CLAIMS_PAYOUTS) { (Claim memory claim, uint payoutRedemptionEnd) = _validateClaimStatus(claimId, IAssessment.AssessmentStatus.ACCEPTED); @@ -266,7 +266,7 @@ contract Claims is IClaims, RegistryAware { /// @dev Can be called by anyone, but the claim deposit is always transferred to the current cover NFT owner. /// /// @param claimId The unique identifier of the claim for which the deposit is being retrieved. - function retrieveDeposit(uint claimId) external override whenNotPaused(PAUSE_CLAIMS_PAYOUT) { + function retrieveDeposit(uint claimId) external override whenNotPaused(PAUSE_CLAIMS_PAYOUTS) { (Claim memory claim, /* payoutRedemptionEnd */) = _validateClaimStatus(claimId, IAssessment.AssessmentStatus.DRAW); From 0b19a42ceb34b6990d53de8f7335672df40028df Mon Sep 17 00:00:00 2001 From: Rocky Murdoch Date: Wed, 6 Aug 2025 17:28:50 +0300 Subject: [PATCH 11/18] refactor: rename error to NotCoverOwner --- contracts/interfaces/IClaims.sol | 2 +- contracts/modules/assessment/Claims.sol | 4 ++-- test/unit/Claims/redeemClaimPayout.js | 2 +- test/unit/Claims/submitClaim.js | 8 ++++---- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/contracts/interfaces/IClaims.sol b/contracts/interfaces/IClaims.sol index 1a29dcb63c..7abb517b9c 100644 --- a/contracts/interfaces/IClaims.sol +++ b/contracts/interfaces/IClaims.sol @@ -68,7 +68,7 @@ interface IClaims { error ClaimIsBeingAssessed(); error PayoutCanStillBeRedeemed(); error ClaimAlreadyPaidOut(); - error OnlyOwnerCanSubmitClaim(); + error NotCoverOwner(); error OnlyOwnerOrApprovedCanSubmitClaim(); error InvalidClaimMethod(); error CoveredAmountExceeded(); diff --git a/contracts/modules/assessment/Claims.sol b/contracts/modules/assessment/Claims.sol index 38b5fb7008..62e7cf5498 100644 --- a/contracts/modules/assessment/Claims.sol +++ b/contracts/modules/assessment/Claims.sol @@ -153,7 +153,7 @@ contract Claims is IClaims, RegistryAware { bytes32 ipfsMetadata ) external payable override onlyMember whenNotPaused(PAUSE_CLAIMS_PAYOUTS) returns (Claim memory claim) { require(coverNFT.isApprovedOrOwner(msg.sender, coverId), OnlyOwnerOrApprovedCanSubmitClaim()); - require(coverNFT.ownerOf(coverId) == msg.sender, OnlyOwnerCanSubmitClaim()); + require(coverNFT.ownerOf(coverId) == msg.sender, NotCoverOwner()); uint claimId = _nextClaimId++; @@ -241,7 +241,7 @@ contract Claims is IClaims, RegistryAware { (Claim memory claim, uint payoutRedemptionEnd) = _validateClaimStatus(claimId, IAssessment.AssessmentStatus.ACCEPTED); - require(coverNFT.ownerOf(claim.coverId) == msg.sender, OnlyOwnerCanSubmitClaim()); + require(coverNFT.ownerOf(claim.coverId) == msg.sender, NotCoverOwner()); require(block.timestamp < payoutRedemptionEnd, RedemptionPeriodExpired()); require(!claim.payoutRedeemed, PayoutAlreadyRedeemed()); diff --git a/test/unit/Claims/redeemClaimPayout.js b/test/unit/Claims/redeemClaimPayout.js index c21c8368b0..acc4a3039e 100644 --- a/test/unit/Claims/redeemClaimPayout.js +++ b/test/unit/Claims/redeemClaimPayout.js @@ -48,7 +48,7 @@ describe('redeemClaimPayout', function () { await assessment.setAssessmentResult(claimId, ASSESSMENT_STATUS.ACCEPTED, payoutRedemptionEnd, cooldownEnd); const redeemClaimPayout = claims.connect(otherMember).redeemClaimPayout(claimId); - await expect(redeemClaimPayout).to.be.revertedWithCustomError(claims, 'OnlyOwnerCanSubmitClaim'); + await expect(redeemClaimPayout).to.be.revertedWithCustomError(claims, 'NotCoverOwner'); }); it('reverts if the claim is not accepted', async function () { diff --git a/test/unit/Claims/submitClaim.js b/test/unit/Claims/submitClaim.js index 569dafb828..a87410954e 100644 --- a/test/unit/Claims/submitClaim.js +++ b/test/unit/Claims/submitClaim.js @@ -1,7 +1,7 @@ const { ethers, nexus } = require('hardhat'); const { expect } = require('chai'); const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); -const { parseEther, toBeHex } = ethers; +const { parseEther } = ethers; const { mineNextBlock, setNextBlockTime } = require('../../utils/evm'); const { ASSET, ASSESSMENT_STATUS, createMockCover, submitClaim, daysToSeconds } = require('./helpers'); @@ -35,10 +35,10 @@ describe('submitClaim', function () { const coverId = 1; // Not owner fails const notOwnerSubmitClaim = submitClaim(fixture)({ coverId, sender: otherMember }); - await expect(notOwnerSubmitClaim).to.be.revertedWithCustomError(claims, 'OnlyOwnerCanSubmitClaim'); + await expect(notOwnerSubmitClaim).to.be.revertedWithCustomError(claims, 'NotCoverOwner'); // Owner succeeds const ownerSubmitClaim = submitClaim(fixture)({ coverId, sender: coverOwner }); - await expect(ownerSubmitClaim).not.to.be.revertedWithCustomError(claims, 'OnlyOwnerCanSubmitClaim'); + await expect(ownerSubmitClaim).not.to.be.revertedWithCustomError(claims, 'NotCoverOwner'); const { timestamp } = await ethers.provider.getBlock('latest'); await createMockCover(cover, { owner: coverOwner.address, start: timestamp + 1 }); @@ -47,7 +47,7 @@ describe('submitClaim', function () { // Approved also fails await coverNFT.connect(coverOwner).approve(approvedMember.address, coverId2); const approvedSubmitClaim = submitClaim(fixture)({ coverId: coverId2, sender: approvedMember }); - await expect(approvedSubmitClaim).to.be.revertedWithCustomError(claims, 'OnlyOwnerCanSubmitClaim'); + await expect(approvedSubmitClaim).to.be.revertedWithCustomError(claims, 'NotCoverOwner'); }); it('reverts if a claim on the same cover is already being assessed', async function () { From 5e1ce16454f43e095d30537d7b8c139502ca1eb1 Mon Sep 17 00:00:00 2001 From: Rocky Murdoch Date: Wed, 6 Aug 2025 17:29:32 +0300 Subject: [PATCH 12/18] fix: fix AssessmentDataForProductTypeSet event emission --- contracts/interfaces/IAssessment.sol | 7 +- contracts/modules/assessment/Assessment.sol | 6 +- .../setAssessmentDataForProductTypes.js | 137 ++++++++++++------ 3 files changed, 99 insertions(+), 51 deletions(-) diff --git a/contracts/interfaces/IAssessment.sol b/contracts/interfaces/IAssessment.sol index 14f3455015..c851a65d69 100644 --- a/contracts/interfaces/IAssessment.sol +++ b/contracts/interfaces/IAssessment.sol @@ -97,7 +97,12 @@ interface IAssessment { /* ========= EVENTS ========== */ - event AssessmentDataForProductTypesSet(uint[] productTypeIds, uint cooldownPeriod, uint payoutRedemptionPeriod, uint groupId); + event AssessmentDataForProductTypeSet( + uint indexed productTypeId, + uint indexed groupId, + uint cooldownPeriod, + uint payoutRedemptionPeriod + ); event AssessorAddedToGroup(uint indexed groupId, uint assessorMemberId); event AssessorRemovedFromGroup(uint indexed groupId, uint assessorMemberId); event GroupMetadataSet(uint indexed groupId, bytes32 ipfsMetadata); diff --git a/contracts/modules/assessment/Assessment.sol b/contracts/modules/assessment/Assessment.sol index 5611a1f6b2..33e7c0c9f0 100644 --- a/contracts/modules/assessment/Assessment.sol +++ b/contracts/modules/assessment/Assessment.sol @@ -184,14 +184,14 @@ contract Assessment is IAssessment, RegistryAware, Multicall { uint length = productTypeIds.length; for (uint i = 0; i < length; i++) { - _assessmentData[productTypeIds[i]] = AssessmentData({ + uint productTypeId = productTypeIds[i]; + _assessmentData[productTypeId] = AssessmentData({ assessingGroupId: groupId.toUint16(), cooldownPeriod: cooldownPeriod.toUint32(), payoutRedemptionPeriod: payoutRedemptionPeriod.toUint32() }); + emit AssessmentDataForProductTypeSet(productTypeId, groupId, cooldownPeriod, payoutRedemptionPeriod); } - - emit AssessmentDataForProductTypesSet(productTypeIds, cooldownPeriod, payoutRedemptionPeriod, groupId); } /// @notice Undoes votes cast by an assessor on multiple claims diff --git a/test/unit/Assessment/setAssessmentDataForProductTypes.js b/test/unit/Assessment/setAssessmentDataForProductTypes.js index 637e256ffa..b64737ba91 100644 --- a/test/unit/Assessment/setAssessmentDataForProductTypes.js +++ b/test/unit/Assessment/setAssessmentDataForProductTypes.js @@ -4,6 +4,40 @@ const { setup } = require('./setup'); const ONE_DAY = 24 * 60 * 60; +/** + * Helper function to verify AssessmentDataForProductTypeSet events + */ +async function verifyAssessmentDataEvents( + tx, + assessment, + productTypeIds, + cooldownPeriod, + payoutRedemptionPeriod, + groupId, +) { + const { logs } = await tx.wait(); + + const events = logs.filter(log => { + try { + const { name } = assessment.interface.parseLog(log); + return name === 'AssessmentDataForProductTypeSet'; + } catch { + return false; + } + }); + + expect(events).to.have.length(productTypeIds.length); + + // Verify each productType has its own event + productTypeIds.forEach((productTypeId, i) => { + const parsedEvent = assessment.interface.parseLog(events[i]); + expect(parsedEvent.args[0]).to.equal(productTypeId); + expect(parsedEvent.args[1]).to.equal(groupId); + expect(parsedEvent.args[2]).to.equal(cooldownPeriod); + expect(parsedEvent.args[3]).to.equal(payoutRedemptionPeriod); + }); +} + describe('setAssessmentDataForProductTypes', function () { it('should revert if not called by governor', async function () { const { contracts, accounts, constants } = await loadFixture(setup); @@ -42,15 +76,17 @@ describe('setAssessmentDataForProductTypes', function () { expect(assessmentData.cooldownPeriod).to.equal(cooldownPeriod); expect(assessmentData.payoutRedemptionPeriod).to.equal(payoutRedemptionPeriod); expect(assessmentData.assessingGroupId).to.equal(ASSESSOR_GROUP_ID); - - const assessmentDataCooldown = await assessment.payoutCooldown(productTypeId); - expect(assessmentDataCooldown).to.equal(cooldownPeriod); } // Verify event emission - await expect(tx) - .to.emit(assessment, 'AssessmentDataForProductTypesSet') - .withArgs(productTypeIds, cooldownPeriod, payoutRedemptionPeriod, ASSESSOR_GROUP_ID); + await verifyAssessmentDataEvents( + tx, + assessment, + productTypeIds, + cooldownPeriod, + payoutRedemptionPeriod, + ASSESSOR_GROUP_ID, + ); }); it('should handle zero cooldown and payout redemption periods', async function () { @@ -68,13 +104,20 @@ describe('setAssessmentDataForProductTypes', function () { .setAssessmentDataForProductTypes(productTypeIds, cooldownPeriod, payoutRedemptionPeriod, ASSESSOR_GROUP_ID); // Verify assessment data is set with zero cooldown - const assessmentDataCooldown = await assessment.payoutCooldown(productTypeIds[0]); - expect(assessmentDataCooldown).to.equal(cooldownPeriod); + const assessmentData = await assessment.getAssessmentDataForProductType(productTypeIds[0]); + expect(assessmentData.cooldownPeriod).to.equal(cooldownPeriod); + expect(assessmentData.payoutRedemptionPeriod).to.equal(payoutRedemptionPeriod); + expect(assessmentData.assessingGroupId).to.equal(ASSESSOR_GROUP_ID); // Verify event emission - await expect(tx) - .to.emit(assessment, 'AssessmentDataForProductTypesSet') - .withArgs(productTypeIds, cooldownPeriod, payoutRedemptionPeriod, ASSESSOR_GROUP_ID); + await verifyAssessmentDataEvents( + tx, + assessment, + productTypeIds, + cooldownPeriod, + payoutRedemptionPeriod, + ASSESSOR_GROUP_ID, + ); }); it('should handle maximum cooldown period and payout redemption period', async function () { @@ -92,13 +135,20 @@ describe('setAssessmentDataForProductTypes', function () { .setAssessmentDataForProductTypes(productTypeIds, maxCooldownPeriod, payoutRedemptionPeriod, ASSESSOR_GROUP_ID); // Verify assessment data is set with max cooldown - const assessmentDataCooldown = await assessment.payoutCooldown(productTypeIds[0]); - expect(assessmentDataCooldown).to.equal(maxCooldownPeriod); + const assessmentData = await assessment.getAssessmentDataForProductType(productTypeIds[0]); + expect(assessmentData.cooldownPeriod).to.equal(maxCooldownPeriod); + expect(assessmentData.payoutRedemptionPeriod).to.equal(payoutRedemptionPeriod); + expect(assessmentData.assessingGroupId).to.equal(ASSESSOR_GROUP_ID); // Verify event emission - await expect(tx) - .to.emit(assessment, 'AssessmentDataForProductTypesSet') - .withArgs(productTypeIds, maxCooldownPeriod, payoutRedemptionPeriod, ASSESSOR_GROUP_ID); + await verifyAssessmentDataEvents( + tx, + assessment, + productTypeIds, + maxCooldownPeriod, + payoutRedemptionPeriod, + ASSESSOR_GROUP_ID, + ); }); it('should handle duplicate product type IDs in same call', async function () { @@ -117,14 +167,21 @@ describe('setAssessmentDataForProductTypes', function () { // Verify assessment data is set for all IDs (including duplicates) for (const productTypeId of productTypeIds) { - const assessmentDataCooldown = await assessment.payoutCooldown(productTypeId); - expect(assessmentDataCooldown).to.equal(cooldownPeriod); + const assessmentData = await assessment.getAssessmentDataForProductType(productTypeId); + expect(assessmentData.cooldownPeriod).to.equal(cooldownPeriod); + expect(assessmentData.payoutRedemptionPeriod).to.equal(payoutRedemptionPeriod); + expect(assessmentData.assessingGroupId).to.equal(ASSESSOR_GROUP_ID); } - // Verify event emission (with original array including duplicates) - await expect(tx) - .to.emit(assessment, 'AssessmentDataForProductTypesSet') - .withArgs(productTypeIds, cooldownPeriod, payoutRedemptionPeriod, ASSESSOR_GROUP_ID); + // Verify event emission + await verifyAssessmentDataEvents( + tx, + assessment, + productTypeIds, + cooldownPeriod, + payoutRedemptionPeriod, + ASSESSOR_GROUP_ID, + ); }); it('should handle sequential updates to same product types', async function () { @@ -155,9 +212,6 @@ describe('setAssessmentDataForProductTypes', function () { expect(assessmentData.assessingGroupId).to.equal(ASSESSOR_GROUP_ID); expect(assessmentData.cooldownPeriod).to.equal(initialCooldown); expect(assessmentData.payoutRedemptionPeriod).to.equal(initialPayoutRedemptionPeriod); - - const assessmentDataCooldown = await assessment.payoutCooldown(productTypeId); - expect(assessmentDataCooldown).to.equal(initialCooldown); } // Update data @@ -176,15 +230,17 @@ describe('setAssessmentDataForProductTypes', function () { expect(assessmentData.cooldownPeriod).to.equal(updatedCooldown); expect(assessmentData.payoutRedemptionPeriod).to.equal(updatedPayoutRedemptionPeriod); expect(assessmentData.assessingGroupId).to.equal(ASSESSOR_GROUP_ID); - - const assessmentDataCooldown = await assessment.payoutCooldown(productTypeId); - expect(assessmentDataCooldown).to.equal(updatedCooldown); } // Verify event emission for update - await expect(tx) - .to.emit(assessment, 'AssessmentDataForProductTypesSet') - .withArgs(productTypeIds, updatedCooldown, updatedPayoutRedemptionPeriod, ASSESSOR_GROUP_ID); + await verifyAssessmentDataEvents( + tx, + assessment, + productTypeIds, + updatedCooldown, + updatedPayoutRedemptionPeriod, + ASSESSOR_GROUP_ID, + ); }); it('should revert when groupId is invalid', async function () { @@ -253,35 +309,22 @@ describe('setAssessmentDataForProductTypes', function () { ASSESSOR_GROUP_ID, ); - // Verify that different product types have different assessment data stored - const [assessmentData0, assessmentData1, assessmentData2] = await Promise.all([ - assessment.getAssessmentDataForProductType(0), - assessment.getAssessmentDataForProductType(1), - assessment.getAssessmentDataForProductType(2), - ]); - // Verify each product type has correct data + const assessmentData0 = await assessment.getAssessmentDataForProductType(0); expect(assessmentData0.cooldownPeriod).to.equal(productType0CooldownPeriod); expect(assessmentData0.payoutRedemptionPeriod).to.equal(productType0PayoutRedemptionPeriod); expect(assessmentData0.assessingGroupId).to.equal(ASSESSOR_GROUP_ID); + const assessmentData1 = await assessment.getAssessmentDataForProductType(1); expect(assessmentData1.cooldownPeriod).to.equal(productType1CooldownPeriod); expect(assessmentData1.payoutRedemptionPeriod).to.equal(productType1PayoutRedemptionPeriod); expect(assessmentData1.assessingGroupId).to.equal(ASSESSOR_GROUP_ID); + const assessmentData2 = await assessment.getAssessmentDataForProductType(2); expect(assessmentData2.cooldownPeriod).to.equal(productType2CooldownPeriod); expect(assessmentData2.payoutRedemptionPeriod).to.equal(productType2PayoutRedemptionPeriod); expect(assessmentData2.assessingGroupId).to.equal(ASSESSOR_GROUP_ID); - // Verify all values are different from each other - expect(assessmentData0.cooldownPeriod).to.not.equal(assessmentData1.cooldownPeriod); - expect(assessmentData1.cooldownPeriod).to.not.equal(assessmentData2.cooldownPeriod); - expect(assessmentData0.cooldownPeriod).to.not.equal(assessmentData2.cooldownPeriod); - - expect(assessmentData0.payoutRedemptionPeriod).to.not.equal(assessmentData1.payoutRedemptionPeriod); - expect(assessmentData1.payoutRedemptionPeriod).to.not.equal(assessmentData2.payoutRedemptionPeriod); - expect(assessmentData0.payoutRedemptionPeriod).to.not.equal(assessmentData2.payoutRedemptionPeriod); - // Verify payoutCooldown function returns correct values expect(await assessment.payoutCooldown(0)).to.equal(productType0CooldownPeriod); expect(await assessment.payoutCooldown(1)).to.equal(productType1CooldownPeriod); From 7327471c0517c5941c7ad40a4722f696e5940935 Mon Sep 17 00:00:00 2001 From: Rocky Murdoch Date: Wed, 6 Aug 2025 17:29:52 +0300 Subject: [PATCH 13/18] test: fix getGroupAssessors unit tests --- test/unit/Assessment/getGroupAssessors.js | 20 ++++++-------------- 1 file changed, 6 insertions(+), 14 deletions(-) diff --git a/test/unit/Assessment/getGroupAssessors.js b/test/unit/Assessment/getGroupAssessors.js index a659e0d5f2..a18f863e44 100644 --- a/test/unit/Assessment/getGroupAssessors.js +++ b/test/unit/Assessment/getGroupAssessors.js @@ -161,24 +161,16 @@ describe('getGroupAssessors', function () { const group1Result = await assessment.getGroupAssessors(group1Id); const group1Set = new Set(group1Result); - expect(group1Result.length).to.equal(group1Assessors.length + 1); - await Promise.all( - group1Assessors.map(async assessorId => { - expect(group1Set.has(assessorId)).to.be.true; - }), - ); - expect(group1Set.has(sharedAssessor)).to.be.true; + const group1WithSharedAssessor = [...group1Assessors, sharedAssessor]; + expect(group1Result).to.have.lengthOf(group1WithSharedAssessor.length); + group1WithSharedAssessor.forEach(assessorId => expect(group1Set.has(assessorId)).to.be.true); // Verify group 2 assessors const group2Result = await assessment.getGroupAssessors(group2Id); const group2Set = new Set(group2Result); - expect(group2Result.length).to.equal(group2Assessors.length + 1); - await Promise.all( - group2Assessors.map(async assessorId => { - expect(group2Set.has(assessorId)).to.be.true; - }), - ); - expect(group2Set.has(sharedAssessor)).to.be.true; + const group2WithSharedAssessor = [...group2Assessors, sharedAssessor]; + expect(group2Result).to.have.lengthOf(group2WithSharedAssessor.length); + group2WithSharedAssessor.forEach(assessorId => expect(group2Set.has(assessorId)).to.be.true); }); }); From 03402454ba4af7fa4de4a68ddc658f93d408aadc Mon Sep 17 00:00:00 2001 From: Rocky Murdoch Date: Wed, 6 Aug 2025 17:30:38 +0300 Subject: [PATCH 14/18] docs: add burnStake comment to remove return address --- contracts/modules/assessment/Claims.sol | 1 + contracts/modules/cover/Cover.sol | 1 + 2 files changed, 2 insertions(+) diff --git a/contracts/modules/assessment/Claims.sol b/contracts/modules/assessment/Claims.sol index 62e7cf5498..e44ae1efbe 100644 --- a/contracts/modules/assessment/Claims.sol +++ b/contracts/modules/assessment/Claims.sol @@ -250,6 +250,7 @@ contract Claims is IClaims, RegistryAware { ramm.updateTwap(); + // TODO: remove return address in burnStake and fetch cover owner from coverNFT address payable coverOwner = payable(cover.burnStake( claim.coverId, claim.amount diff --git a/contracts/modules/cover/Cover.sol b/contracts/modules/cover/Cover.sol index 50e058167e..3ac9b944eb 100644 --- a/contracts/modules/cover/Cover.sol +++ b/contracts/modules/cover/Cover.sol @@ -491,6 +491,7 @@ contract Cover is ICover, MasterAwareV2, IStakingPoolBeacon, ReentrancyGuard, Mu activeCover[coverAsset] = _activeCover; } + // TODO: remove return address function burnStake( uint coverId, uint payoutAmountInAsset From 20a8775aa37bceedfcd899c95b4f0d2c4129ca0d Mon Sep 17 00:00:00 2001 From: Rocky Murdoch Date: Wed, 6 Aug 2025 20:30:03 +0300 Subject: [PATCH 15/18] refactor: drop coverNFT approved validation for submitClaim --- contracts/interfaces/IClaims.sol | 1 - contracts/interfaces/IIndividualClaims.sol | 3 +-- contracts/modules/assessment/Claims.sol | 1 - 3 files changed, 1 insertion(+), 4 deletions(-) diff --git a/contracts/interfaces/IClaims.sol b/contracts/interfaces/IClaims.sol index 7abb517b9c..a90543adb5 100644 --- a/contracts/interfaces/IClaims.sol +++ b/contracts/interfaces/IClaims.sol @@ -69,7 +69,6 @@ interface IClaims { error PayoutCanStillBeRedeemed(); error ClaimAlreadyPaidOut(); error NotCoverOwner(); - error OnlyOwnerOrApprovedCanSubmitClaim(); error InvalidClaimMethod(); error CoveredAmountExceeded(); error CantBuyCoverAndClaimInTheSameBlock(); diff --git a/contracts/interfaces/IIndividualClaims.sol b/contracts/interfaces/IIndividualClaims.sol index eb26644653..a89452282f 100644 --- a/contracts/interfaces/IIndividualClaims.sol +++ b/contracts/interfaces/IIndividualClaims.sol @@ -82,10 +82,9 @@ interface IIndividualClaims { event ClaimPayoutRedeemed(address indexed user, uint amount, uint claimId, uint coverId); /* ========== ERRORS ========== */ - + error ClaimIsBeingAssessed(); error PayoutCanStillBeRedeemed(); - error OnlyOwnerOrApprovedCanSubmitClaim(); error InvalidClaimMethod(); error CoveredAmountExceeded(); error CantBuyCoverAndClaimInTheSameBlock(); diff --git a/contracts/modules/assessment/Claims.sol b/contracts/modules/assessment/Claims.sol index e44ae1efbe..781f0c7e91 100644 --- a/contracts/modules/assessment/Claims.sol +++ b/contracts/modules/assessment/Claims.sol @@ -152,7 +152,6 @@ contract Claims is IClaims, RegistryAware { uint96 requestedAmount, bytes32 ipfsMetadata ) external payable override onlyMember whenNotPaused(PAUSE_CLAIMS_PAYOUTS) returns (Claim memory claim) { - require(coverNFT.isApprovedOrOwner(msg.sender, coverId), OnlyOwnerOrApprovedCanSubmitClaim()); require(coverNFT.ownerOf(coverId) == msg.sender, NotCoverOwner()); uint claimId = _nextClaimId++; From 6a6d726cb8b22b648072ed3624654897a40fe765 Mon Sep 17 00:00:00 2001 From: shark0der Date: Thu, 7 Aug 2025 15:08:10 +0300 Subject: [PATCH 16/18] chore: fix claim payouts pause constant name --- contracts/abstract/RegistryAware.sol | 2 +- contracts/modules/assessment/Claims.sol | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/contracts/abstract/RegistryAware.sol b/contracts/abstract/RegistryAware.sol index 5791020b5e..5dbc100042 100644 --- a/contracts/abstract/RegistryAware.sol +++ b/contracts/abstract/RegistryAware.sol @@ -30,7 +30,7 @@ uint constant PAUSE_RAMM = 1 << 1; // 2 uint constant PAUSE_SWAPS = 1 << 2; // 4 uint constant PAUSE_MEMBERSHIP = 1 << 3; // 8 uint constant PAUSE_ASSESSMENTS = 1 << 4; // 16 -uint constant PAUSE_CLAIMS_PAYOUTS = 1 << 5; // 32 +uint constant PAUSE_CLAIM_PAYOUTS = 1 << 5; // 32 contract RegistryAware { diff --git a/contracts/modules/assessment/Claims.sol b/contracts/modules/assessment/Claims.sol index 781f0c7e91..8591d82fdc 100644 --- a/contracts/modules/assessment/Claims.sol +++ b/contracts/modules/assessment/Claims.sol @@ -151,7 +151,7 @@ contract Claims is IClaims, RegistryAware { uint32 coverId, uint96 requestedAmount, bytes32 ipfsMetadata - ) external payable override onlyMember whenNotPaused(PAUSE_CLAIMS_PAYOUTS) returns (Claim memory claim) { + ) external payable override onlyMember whenNotPaused(PAUSE_CLAIM_PAYOUTS) returns (Claim memory claim) { require(coverNFT.ownerOf(coverId) == msg.sender, NotCoverOwner()); uint claimId = _nextClaimId++; @@ -236,7 +236,7 @@ contract Claims is IClaims, RegistryAware { /// @dev Must be the cover NFT owner for the claim and a member can call this function /// /// @param claimId Claim identifier - function redeemClaimPayout(uint claimId) external override onlyMember whenNotPaused(PAUSE_CLAIMS_PAYOUTS) { + function redeemClaimPayout(uint claimId) external override onlyMember whenNotPaused(PAUSE_CLAIM_PAYOUTS) { (Claim memory claim, uint payoutRedemptionEnd) = _validateClaimStatus(claimId, IAssessment.AssessmentStatus.ACCEPTED); @@ -266,7 +266,7 @@ contract Claims is IClaims, RegistryAware { /// @dev Can be called by anyone, but the claim deposit is always transferred to the current cover NFT owner. /// /// @param claimId The unique identifier of the claim for which the deposit is being retrieved. - function retrieveDeposit(uint claimId) external override whenNotPaused(PAUSE_CLAIMS_PAYOUTS) { + function retrieveDeposit(uint claimId) external override whenNotPaused(PAUSE_CLAIM_PAYOUTS) { (Claim memory claim, /* payoutRedemptionEnd */) = _validateClaimStatus(claimId, IAssessment.AssessmentStatus.DRAW); From c12317774a7412d95b4d41a27f9a219d7a9effbe Mon Sep 17 00:00:00 2001 From: shark0der Date: Thu, 7 Aug 2025 15:10:59 +0300 Subject: [PATCH 17/18] chore: remove IIndividualClaims and IndividualClaimsGeneric interfaces --- contracts/interfaces/IIndividualClaims.sol | 100 ------------------ .../mocks/generic/IndividualClaimsGeneric.sol | 37 ------- 2 files changed, 137 deletions(-) delete mode 100644 contracts/interfaces/IIndividualClaims.sol delete mode 100644 contracts/mocks/generic/IndividualClaimsGeneric.sol diff --git a/contracts/interfaces/IIndividualClaims.sol b/contracts/interfaces/IIndividualClaims.sol deleted file mode 100644 index a89452282f..0000000000 --- a/contracts/interfaces/IIndividualClaims.sol +++ /dev/null @@ -1,100 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-only - -pragma solidity >=0.5.0; - -interface IIndividualClaims { - - enum ClaimStatus { PENDING, ACCEPTED, DENIED } - - enum PayoutStatus { PENDING, COMPLETE, UNCLAIMED, DENIED } - - struct Claim { - uint80 assessmentId; - uint32 coverId; - uint16 segmentId; // unused - uint96 amount; - uint8 coverAsset; // asset id in the Pool contract - bool payoutRedeemed; - } - - struct ClaimSubmission { - uint80 claimId; - // True when a previous submission exists - bool exists; - } - - // Claim structure but in a human-friendly format. - // - // Contains aggregated values that give an overall view about the claim and other relevant - // pieces of information such as cover period, asset symbol etc. This structure is not used in - // any storage variables. - struct ClaimDisplay { - uint id; - uint productId; - uint coverId; - uint assessmentId; - uint amount; - string assetSymbol; - uint assetIndex; - uint coverStart; - uint coverEnd; - uint pollStart; - uint pollEnd; - uint claimStatus; - uint payoutStatus; - } - - /* ========== VIEWS ========== */ - - function claims(uint id) external view returns ( - uint80 assessmentId, - uint32 coverId, - uint16 segmentId, - uint96 amount, - uint8 coverAsset, - bool payoutRedeemed - ); - - function getPayoutRedemptionPeriod() external view returns (uint); - - function getMinAssessmentDepositRatio() external view returns (uint); - - function getMaxRewardInNxm() external view returns (uint); - - function getRewardRatio() external view returns (uint); - - function getClaimsCount() external view returns (uint); - - /* === MUTATIVE FUNCTIONS ==== */ - - function submitClaim( - uint32 coverId, - uint96 requestedAmount, - string calldata ipfsMetadata - ) external payable returns (Claim memory); - - function redeemClaimPayout(uint104 id) external; - - /* ========== EVENTS ========== */ - - event ClaimSubmitted(address indexed user, uint claimId, uint indexed coverId, uint productId); - event MetadataSubmitted(uint indexed claimId, string ipfsMetadata); - event ClaimPayoutRedeemed(address indexed user, uint amount, uint claimId, uint coverId); - - /* ========== ERRORS ========== */ - - error ClaimIsBeingAssessed(); - error PayoutCanStillBeRedeemed(); - error InvalidClaimMethod(); - error CoveredAmountExceeded(); - error CantBuyCoverAndClaimInTheSameBlock(); - error GracePeriodPassed(); - error AssessmentDepositInsufficient(); - error AssessmentDepositTrasnferRefundFailed(); - error AssessmentDepositTransferToPoolFailed(); - error ClaimAssessmentNotFinished(); - error ClaimNotAccepted(); - error CooldownPeriodNotPassed(); - error RedemptionPeriodExpired(); - error PayoutAlreadyRedeemed(); -} diff --git a/contracts/mocks/generic/IndividualClaimsGeneric.sol b/contracts/mocks/generic/IndividualClaimsGeneric.sol deleted file mode 100644 index b0c247ac9d..0000000000 --- a/contracts/mocks/generic/IndividualClaimsGeneric.sol +++ /dev/null @@ -1,37 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-only - -pragma solidity ^0.8.18; - -import "../../interfaces/IIndividualClaims.sol"; - -contract IndividualClaimsGeneric is IIndividualClaims { - Claim[] public claims; - - function getClaimsCount() external pure returns (uint) { - revert("Unsupported"); - } - - function getPayoutRedemptionPeriod() external pure override virtual returns (uint) { - revert("Unsupported"); - } - - function getMinAssessmentDepositRatio() external pure override virtual returns (uint) { - revert("Unsupported"); - } - - function getMaxRewardInNxm() external pure override virtual returns (uint) { - revert("Unsupported"); - } - - function getRewardRatio() external pure override virtual returns (uint) { - revert("Unsupported"); - } - - function submitClaim(uint32, uint96, string calldata) external payable virtual returns (Claim memory) { - revert("Unsupported"); - } - - function redeemClaimPayout(uint104) external pure { - revert("Unsupported"); - } -} From c3d2afb33743c42ed027e0f1839627b4c1507692 Mon Sep 17 00:00:00 2001 From: shark0der Date: Thu, 7 Aug 2025 15:24:03 +0300 Subject: [PATCH 18/18] test: fix integration tests failing after IndividualClaims removal --- test/integration/setup.js | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/test/integration/setup.js b/test/integration/setup.js index 632df34fbd..b174e26571 100644 --- a/test/integration/setup.js +++ b/test/integration/setup.js @@ -224,9 +224,9 @@ async function setup() { defaultSender, // swap controller ]); - const assessmentImplementation = await ethers.deployContract('Assessment', [token]); + const assessmentImplementation = await ethers.deployContract('Assessment', [registry]); - const claimsImplementation = await ethers.deployContract('IndividualClaims', [coverNFT]); + const claimsImplementation = await ethers.deployContract('Claims', [registry]); // upgrade proxies @@ -258,7 +258,7 @@ async function setup() { const limitOrders = await getContract(ContractIndexes.C_LIMIT_ORDERS, 'LimitOrders'); const swapOperator = await getContract(ContractIndexes.C_SWAP_OPERATOR, 'SwapOperator'); const assessment = await getContract(ContractIndexes.C_ASSESSMENT, 'Assessment'); - const claims = await getContract(ContractIndexes.C_CLAIMS, 'IndividualClaims'); + const claims = await getContract(ContractIndexes.C_CLAIMS, 'Claims'); const assets = [ { asset: Assets.ETH, isCoverAsset: true, oracle: chainlinkEthUsd, type: AggregatorType.USD }, @@ -290,9 +290,6 @@ async function setup() { ContractIndexes.C_COVER_PRODUCTS, ContractIndexes.C_STAKING_PRODUCTS, ContractIndexes.C_LIMIT_ORDERS, - // TODO: remove Assessment and Claims from here once we've merged the new assessment - ContractIndexes.C_ASSESSMENT, - ContractIndexes.C_CLAIMS, ]; for (const contract of masterAwareContracts) {