diff --git a/.solcover.js b/.solcover.js index 55a0be5102..5b80072dbb 100644 --- a/.solcover.js +++ b/.solcover.js @@ -48,6 +48,7 @@ async function onCompileComplete() { } module.exports = { + mocha: { parallel: false }, onCompileComplete, skipFiles: [ 'abstract/', @@ -55,8 +56,6 @@ module.exports = { 'interfaces/', 'libraries/', 'mocks/', - 'modules/assessment/AssessmentViewer.sol', - 'modules/viewer/NexusViewer.sol', 'modules/cover/CoverViewer.sol', 'modules/governance/external', 'modules/legacy', @@ -65,6 +64,6 @@ module.exports = { 'utils/', ], providerOptions: { - default_balance_ether: 100000000, + default_balance_ether: 1000000000, }, }; diff --git a/contracts/abstract/EIP712.sol b/contracts/abstract/EIP712.sol index 1a04696baa..70ab88cc91 100644 --- a/contracts/abstract/EIP712.sol +++ b/contracts/abstract/EIP712.sol @@ -21,7 +21,7 @@ abstract contract EIP712 { function recoverSigner( bytes memory message, bytes memory signature - ) internal view virtual returns (address signer){ + ) internal view virtual returns (address signer) { bytes32 digest = ECDSA.toTypedDataHash(DOMAIN_SEPARATOR, keccak256(message)); return ECDSA.recover(digest, signature); } diff --git a/contracts/abstract/RegistryAware.sol b/contracts/abstract/RegistryAware.sol index 22d6c7a218..c0c9fbb392 100644 --- a/contracts/abstract/RegistryAware.sol +++ b/contracts/abstract/RegistryAware.sol @@ -30,6 +30,7 @@ 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 = 1 << 5; // 32 +uint constant PAUSE_COVER = 1 << 6; // 64 contract RegistryAware { @@ -60,7 +61,6 @@ contract RegistryAware { _; } - // 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/IAssessmentViewer.sol b/contracts/interfaces/IAssessmentViewer.sol deleted file mode 100644 index 3b189f610f..0000000000 --- a/contracts/interfaces/IAssessmentViewer.sol +++ /dev/null @@ -1,21 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-only - -pragma solidity ^0.8.18; - -interface IAssessmentViewer { - - struct AssessmentRewards { - uint totalPendingAmountInNXM; - uint withdrawableAmountInNXM; - uint withdrawableUntilIndex; - } - - struct AssessmentStakeLockedState { - bool isStakeLocked; - uint stakeLockupExpiry; - } - - function getRewards(address user) external view returns (AssessmentRewards memory); - - function getStakeLocked(address member) external view returns (AssessmentStakeLockedState memory); -} diff --git a/contracts/interfaces/ICover.sol b/contracts/interfaces/ICover.sol index 1b0290071d..4b7b9335a3 100644 --- a/contracts/interfaces/ICover.sol +++ b/contracts/interfaces/ICover.sol @@ -4,6 +4,7 @@ pragma solidity >=0.5.0; import "./ICoverNFT.sol"; import "./IStakingNFT.sol"; +import "./IStakingPoolBeacon.sol"; /* io structs */ @@ -35,21 +36,6 @@ struct PoolAllocation { uint24 allocationId; } -struct LegacyCoverData { - uint24 productId; - uint8 coverAsset; - uint96 amountPaidOut; -} - -struct LegacyCoverSegment { - uint96 amount; - uint32 start; - uint32 period; // seconds - uint32 gracePeriod; // seconds - uint24 globalRewardsRatio; - uint24 globalCapacityRatio; -} - struct CoverData { uint24 productId; uint8 coverAsset; @@ -66,7 +52,25 @@ struct CoverReference { uint32 latestCoverId; // used only in the original cover (set to 0 in original cover if never edited) } -interface ICover { +// reinsurance info +struct Ri { + uint24 providerId; + uint96 amount; +} + +struct RiConfig { + uint24 nextNonce; + address premiumDestination; +} + +struct RiRequest { + uint providerId; + uint amount; + uint premium; + bytes signature; +} + +interface ICover is IStakingPoolBeacon { /* ========== DATA STRUCTURES ========== */ @@ -92,14 +96,18 @@ interface ICover { function getCoverData(uint coverId) external view returns (CoverData memory); - function getPoolAllocations(uint coverId) external view returns (PoolAllocation[] memory); + function getCoverRi(uint coverId) external view returns (Ri memory); - function getCoverDataCount() external view returns (uint); + function getCoverDataWithRi(uint coverId) external view returns (CoverData memory, Ri memory); function getCoverReference(uint coverId) external view returns(CoverReference memory); function getCoverDataWithReference(uint coverId) external view returns (CoverData memory, CoverReference memory); + function getCoverDataCount() external view returns (uint); + + function getPoolAllocations(uint coverId) external view returns (PoolAllocation[] memory); + function getLatestEditCoverData(uint coverId) external view returns (CoverData memory); function recalculateActiveCoverInAsset(uint coverAsset) external; @@ -132,7 +140,13 @@ interface ICover { address buyer ) external payable returns (uint coverId); - function burnStake(uint coverId, uint amount) external returns (address coverOwner); + function buyCoverWithRi( + BuyCoverParams calldata params, + PoolAllocationRequest[] calldata coverChunkRequests, + RiRequest calldata riRequest + ) external payable returns (uint coverId); + + function burnStake(uint coverId, uint amount) external; function coverNFT() external returns (ICoverNFT); @@ -188,9 +202,7 @@ interface ICover { // ETH transfers error InsufficientEthSent(); - error SendingEthToPoolFailed(); - error SendingEthToCommissionDestinationFailed(); - error ReturningEthRemainderToSenderFailed(); + error ETHTransferFailed(address to, uint amount); // Misc error ExpiredCoversCannotBeEdited(); @@ -198,4 +210,11 @@ interface ICover { error InsufficientCoverAmountAllocated(); error UnexpectedPoolId(); error AlreadyMigratedCoverData(uint coverId); + + // Ri + error InvalidSignature(); + error WrongCoverEditEntrypoint(); + error RiAmountIsZero(); + error InvalidRiConfig(); + error UnexpectedRiPremium(); } diff --git a/contracts/mocks/common/TokenControllerMock.sol b/contracts/mocks/common/TokenControllerMock.sol index 54ea136441..1441f2e71b 100644 --- a/contracts/mocks/common/TokenControllerMock.sol +++ b/contracts/mocks/common/TokenControllerMock.sol @@ -1,16 +1,11 @@ // SPDX-License-Identifier: GPL-3.0-only -pragma solidity ^0.8.18; +pragma solidity ^0.8.28; -import "../../abstract/MasterAwareV2.sol"; import "../../interfaces/INXMToken.sol"; import "../generic/TokenControllerGeneric.sol"; -interface ICover { - function stakingPool(uint poolId) external view returns (address); -} - -contract TokenControllerMock is TokenControllerGeneric, MasterAwareV2 { +contract TokenControllerMock is TokenControllerGeneric { address public addToWhitelistLastCalledWith; address public removeFromWhitelistLastCalledWith; @@ -29,37 +24,25 @@ contract TokenControllerMock is TokenControllerGeneric, MasterAwareV2 { token = INXMToken(_tokenAddress); } - function mint(address _member, uint256 _amount) public override onlyInternal { + function mint(address _member, uint256 _amount) public override { token.mint(_member, _amount); } - function burnFrom(address _of, uint amount) public override onlyInternal returns (bool) { + function burnFrom(address _of, uint amount) public override returns (bool) { return token.burnFrom(_of, amount); } - function addToWhitelist(address _member) public override onlyInternal { + function addToWhitelist(address _member) public override { addToWhitelistLastCalledWith = _member; } - function removeFromWhitelist(address _member) public override onlyInternal { + function removeFromWhitelist(address _member) public override { removeFromWhitelistLastCalledWith = _member; } /* ========== DEPENDENCIES ========== */ - function cover() internal view returns (ICover) { - return ICover(internalContracts[uint(ID.CO)]); - } - - function changeDependentContractAddress() public { - internalContracts[uint(ID.CO)] = master.getLatestAddress("CO"); - } - - function operatorTransfer(address _from, address _to, uint _value) onlyInternal external override returns (bool) { - require( - msg.sender == master.getLatestAddress("PS") || msg.sender == master.getLatestAddress("CO"), - "Call is only allowed from PooledStaking or Cover address" - ); + function operatorTransfer(address _from, address _to, uint _value) external override returns (bool) { require(token.operatorTransfer(_from, _value), "Operator transfer failed"); require(token.transfer(_to, _value), "Internal transfer failed"); return true; @@ -91,11 +74,6 @@ contract TokenControllerMock is TokenControllerGeneric, MasterAwareV2 { token.burn(amount); } - function setContractAddresses(address payable coverAddr, address payable tokenAddr) public { - internalContracts[uint(ID.CO)] = coverAddr; - token = INXMToken(tokenAddr); - } - function setStakingPoolManager(uint poolId, address manager) external { stakingPoolManagers[poolId] = manager; } @@ -143,14 +121,4 @@ contract TokenControllerMock is TokenControllerGeneric, MasterAwareV2 { return _isStakingPoolManager[manager]; } - /* unused functions */ - - modifier unused { - require(false, "Unexpected TokenControllerMock call"); - _; - } - - function burnLockedTokens(address, bytes32, uint256) unused external {} - - function releaseLockedTokens(address _of, bytes32 _reason, uint256 _amount) unused external {} } diff --git a/contracts/mocks/generic/CoverGeneric.sol b/contracts/mocks/generic/CoverGeneric.sol index 18ad304eff..8ff3cbfab5 100644 --- a/contracts/mocks/generic/CoverGeneric.sol +++ b/contracts/mocks/generic/CoverGeneric.sol @@ -50,6 +50,14 @@ contract CoverGeneric is ICover { revert("Unsupported"); } + function getCoverRi(uint) external virtual view returns (Ri memory) { + revert("Unsupported"); + } + + function getCoverDataWithRi(uint) external virtual view returns (CoverData memory, Ri memory) { + revert("Unsupported"); + } + function getPoolAllocations(uint) external virtual view returns (PoolAllocation[] memory) { revert("Unsupported"); } @@ -84,16 +92,24 @@ contract CoverGeneric is ICover { function executeCoverBuy( BuyCoverParams calldata /* params */, - PoolAllocationRequest[] calldata, /* coverChunkRequests */ + PoolAllocationRequest[] calldata /* coverChunkRequests */, address /*buyer*/ ) external virtual payable returns (uint) { revert("Unsupported"); } + function buyCoverWithRi( + BuyCoverParams calldata /* params */, + PoolAllocationRequest[] calldata /* coverChunkRequests */, + RiRequest calldata /* riRequest */ + ) external virtual payable returns (uint) { + revert("Unsupported"); + } + function burnStake( uint /* coverId */, uint /* amount */ - ) external virtual returns (address /* coverOwner */) { + ) external virtual { revert("Unsupported"); } @@ -116,4 +132,8 @@ contract CoverGeneric is ICover { function stakingPoolFactory() external virtual view returns (address) { revert("Unsupported"); } + + function stakingPoolImplementation() external virtual view returns (address) { + revert("Unsupported"); + } } diff --git a/contracts/mocks/generic/TokenControllerGeneric.sol b/contracts/mocks/generic/TokenControllerGeneric.sol index 398b07c48a..3366bed0d6 100644 --- a/contracts/mocks/generic/TokenControllerGeneric.sol +++ b/contracts/mocks/generic/TokenControllerGeneric.sol @@ -6,7 +6,7 @@ import "../../interfaces/ITokenController.sol"; contract TokenControllerGeneric is ITokenController { - INXMToken public token; + INXMToken public immutable token; function changeOperator(address) external pure { revert("changeOperator unsupported"); diff --git a/contracts/mocks/modules/Claims/CLMockCover.sol b/contracts/mocks/modules/Claims/CLMockCover.sol index a93bc5d5e4..f2ec0ca9d1 100644 --- a/contracts/mocks/modules/Claims/CLMockCover.sol +++ b/contracts/mocks/modules/Claims/CLMockCover.sol @@ -1,12 +1,11 @@ // SPDX-License-Identifier: GPL-3.0-only -pragma solidity ^0.8.18; +pragma solidity ^0.8.28; import "../../../interfaces/ICover.sol"; import "../../../interfaces/ICoverNFT.sol"; import "../../generic/CoverGeneric.sol"; - contract CLMockCover is CoverGeneric { ICoverNFT public immutable _coverNFT; @@ -16,47 +15,12 @@ contract CLMockCover is CoverGeneric { uint amount; } - struct MigrateCoverFromCalledWith { - uint coverId; - address from; - address newOwner; - } - BurnStakeCalledWith public burnStakeCalledWith; - MigrateCoverFromCalledWith public migrateCoverFromCalledWith; mapping(uint => CoverData) public _coverData; - mapping(uint => LegacyCoverSegment[]) _coverSegments; - mapping(uint => PoolAllocation[]) stakingPoolsForCover; - - mapping(uint => uint96) public activeCoverAmountInNXM; - - mapping(uint => uint) capacityFactors; - - mapping(uint => uint) initialPrices; - - /* - (productId, poolAddress) => lastPrice - Last base prices at which a cover was sold by a pool for a particular product. - */ - mapping(uint => mapping(address => uint)) lastPrices; - - /* - (productId, poolAddress) => lastPriceUpdate - Last base price update time. - */ - mapping(uint => mapping(address => uint)) lastPriceUpdate; - /* === CONSTANTS ==== */ - uint public REWARD_BPS = 5000; - uint public constant PERCENTAGE_CHANGE_PER_DAY_BPS = 100; - uint public constant BASIS_PRECISION = 10000; - uint public constant STAKE_SPEED_UNIT = 100000e18; - uint public constant PRICE_CURVE_EXPONENT = 7; - uint public constant MAX_PRICE_PERCENTAGE = 1e20; - constructor(address coverNFTAddress) { _coverNFT = ICoverNFT(coverNFTAddress); } @@ -93,19 +57,8 @@ contract CLMockCover is CoverGeneric { ); } - function burnStake(uint coverId, uint amount) external override returns (address) { + function burnStake(uint coverId, uint amount) external override { burnStakeCalledWith = BurnStakeCalledWith(coverId, amount); - return _coverNFT.ownerOf(coverId); - } - - function migrateCoverFrom( - uint coverId, - address from, - address newOwner - ) external returns (address) { - migrateCoverFromCalledWith = MigrateCoverFromCalledWith(coverId, from, newOwner); - // silence compiler warning: - return address(0); } function coverNFT() external override view returns (ICoverNFT) { diff --git a/contracts/mocks/modules/StakingProducts/SPMockCover.sol b/contracts/mocks/modules/StakingProducts/SPMockCover.sol index 2df79899cd..fcd854c76c 100644 --- a/contracts/mocks/modules/StakingProducts/SPMockCover.sol +++ b/contracts/mocks/modules/StakingProducts/SPMockCover.sol @@ -118,7 +118,7 @@ contract SPMockCover is CoverGeneric { emit RequestAllocationReturned(premium, allocationId); } - function stakingPoolImplementation() public view returns (address) { + function stakingPoolImplementation() public override view returns (address) { return _stakingPoolImplementation; } } diff --git a/contracts/modules/cover/Cover.sol b/contracts/modules/cover/Cover.sol index b19f5174d6..de73e88743 100644 --- a/contracts/modules/cover/Cover.sol +++ b/contracts/modules/cover/Cover.sol @@ -2,50 +2,45 @@ pragma solidity ^0.8.28; -import "@openzeppelin/contracts-v4/security/ReentrancyGuard.sol"; import "@openzeppelin/contracts-v4/token/ERC20/IERC20.sol"; import "@openzeppelin/contracts-v4/token/ERC20/utils/SafeERC20.sol"; -import "../../abstract/MasterAwareV2.sol"; +import "../../abstract/EIP712.sol"; import "../../abstract/Multicall.sol"; +import "../../abstract/ReentrancyGuard.sol"; +import "../../abstract/RegistryAware.sol"; import "../../interfaces/ICover.sol"; import "../../interfaces/ICoverNFT.sol"; import "../../interfaces/ICoverProducts.sol"; import "../../interfaces/IPool.sol"; import "../../interfaces/IStakingNFT.sol"; import "../../interfaces/IStakingPool.sol"; -import "../../interfaces/IStakingPoolBeacon.sol"; import "../../interfaces/ITokenController.sol"; import "../../libraries/Math.sol"; import "../../libraries/SafeUintCast.sol"; import "../../libraries/StakingPoolLibrary.sol"; -contract Cover is ICover, MasterAwareV2, IStakingPoolBeacon, ReentrancyGuard, Multicall { +contract Cover is ICover, EIP712, RegistryAware, ReentrancyGuard, Multicall { using SafeERC20 for IERC20; using SafeUintCast for uint; /* ========== STATE VARIABLES ========== */ - uint private __unused_0; // was Product[] products - uint private __unused_1; // was ProductType[] productTypes - - mapping(uint coverId => LegacyCoverData) private _legacyCoverData; - mapping(uint coverId => mapping(uint segmentId => PoolAllocation[])) private _legacyCoverSegmentAllocations; - - uint private __unused_4; // was mapping(uint => uint[]) allowedPools - - mapping(uint coverId => LegacyCoverSegment[]) private _legacyCoverSegments; + uint[9] private __unused_0; // slots 0 - 8 mapping(uint assetId => ActiveCover) public activeCover; mapping(uint assetId => mapping(uint bucketId => uint amount)) internal activeCoverExpirationBuckets; - uint private __unused_8; // was mapping(uint => string) _productNames - uint private __unused_9; // was mapping(uint => string) _productTypeNames + uint[2] private __unused_11; // slots 11 - 12 mapping(uint coverId => CoverData) private _coverData; mapping(uint coverId => PoolAllocation[]) private _poolAllocations; mapping(uint coverId => CoverReference) private _coverReference; + mapping(uint coverId => Ri) private _coverRi; + mapping(uint providerId => RiConfig) private _riProviderConfigs; + address public riSigner; + /* ========== CONSTANTS ========== */ uint private constant GLOBAL_CAPACITY_RATIO = 20000; // 2 @@ -75,28 +70,38 @@ contract Cover is ICover, MasterAwareV2, IStakingPoolBeacon, ReentrancyGuard, Mu // smallest unit we can allocate is 1e18 / 100 = 1e16 = 0.01 NXM uint public constant NXM_PER_ALLOCATION_UNIT = ONE_NXM / ALLOCATION_UNITS_PER_NXM; + IPool public immutable pool; + ITokenController public immutable tokenController; + ICoverProducts public immutable coverProducts; ICoverNFT public immutable override coverNFT; IStakingNFT public immutable override stakingNFT; address public immutable override stakingPoolFactory; address public immutable override stakingPoolImplementation; + address public immutable claims; /* ========== CONSTRUCTOR ========== */ constructor( - ICoverNFT _coverNFT, - IStakingNFT _stakingNFT, - address _stakingPoolFactory, - address _stakingPoolImplementation - ) { - // in constructor we only initialize immutable fields - coverNFT = _coverNFT; - stakingNFT = _stakingNFT; - stakingPoolFactory = _stakingPoolFactory; + address _registry, + address _stakingPoolImplementation, + address _verifyingAddress + ) RegistryAware(_registry) EIP712("NexusMutualCover", "1.0.0", _verifyingAddress) { + + // fetch deps + coverNFT = ICoverNFT(fetch(C_COVER_NFT)); + coverProducts = ICoverProducts(fetch(C_COVER_PRODUCTS)); + pool = IPool(fetch(C_POOL)); + stakingNFT = IStakingNFT(fetch(C_STAKING_NFT)); + stakingPoolFactory = fetch(C_STAKING_POOL_FACTORY); + tokenController = ITokenController(fetch(C_TOKEN_CONTROLLER)); + + // store staking pool implementation stakingPoolImplementation = _stakingPoolImplementation; } /* === MUTATIVE FUNCTIONS ==== */ + /// @dev Entrypoint for users buying a cover by interacting with this contract directly function buyCover( BuyCoverParams memory params, PoolAllocationRequest[] memory poolAllocationRequests @@ -104,9 +109,15 @@ contract Cover is ICover, MasterAwareV2, IStakingPoolBeacon, ReentrancyGuard, Mu if (params.coverId != 0) { require(coverNFT.isApprovedOrOwner(msg.sender, params.coverId), OnlyOwnerOrApproved()); + require(_coverRi[params.coverId].amount == 0, WrongCoverEditEntrypoint()); } - coverId = _buyCover(params, poolAllocationRequests); + coverId = _buyCover( + params, + poolAllocationRequests, + 0, // no riPremium + address(0) // no riPremiumDestination + ); emit CoverBought( coverId, @@ -119,17 +130,24 @@ contract Cover is ICover, MasterAwareV2, IStakingPoolBeacon, ReentrancyGuard, Mu return coverId; } + /// @dev Entrypoint for LimitOrders contract function executeCoverBuy( BuyCoverParams memory params, PoolAllocationRequest[] memory poolAllocationRequests, address buyer - ) external payable onlyInternal returns (uint coverId) { + ) external payable onlyContracts(C_LIMIT_ORDERS) returns (uint coverId) { if (params.coverId != 0) { require(coverNFT.isApprovedOrOwner(buyer, params.coverId), OnlyOwnerOrApproved()); + require(_coverRi[params.coverId].amount == 0, WrongCoverEditEntrypoint()); } - coverId = _buyCover(params, poolAllocationRequests); + coverId = _buyCover( + params, + poolAllocationRequests, + 0, // no riPremium + address(0) // no riPremiumDestination + ); emit CoverBought( coverId, @@ -142,21 +160,115 @@ contract Cover is ICover, MasterAwareV2, IStakingPoolBeacon, ReentrancyGuard, Mu return coverId; } + /// @dev Entrypoint for Users buying cover with ri + function buyCoverWithRi( + BuyCoverParams memory params, + PoolAllocationRequest[] memory poolAllocationRequests, + RiRequest memory riRequest + ) external payable onlyMember returns (uint coverId) { + + if (params.coverId != 0) { + require(coverNFT.isApprovedOrOwner(msg.sender, params.coverId), OnlyOwnerOrApproved()); + } else { + // ri amount should be non-zero on first cover buy, but allowed to be zero on edits + require(riRequest.amount > 0, RiAmountIsZero()); + } + + RiConfig storage riConfig = _riProviderConfigs[riRequest.providerId]; + address riPremiumDestination = riConfig.premiumDestination; + uint nonce = riConfig.nextNonce++; // SLOAD + SSTORE + + require(riPremiumDestination != address(0), InvalidRiConfig()); + + bytes memory message = abi.encode( + keccak256( + abi.encodePacked( + "RiQuote(", + "uint256 coverId,", + "uint24 productId,", + "uint256 providerId,", + "uint256 amount,", + "uint256 premium,", + "uint32 period,", + "uint8 coverAsset,", + "uint256 nonce)" + ) + ), + params.coverId, + params.productId, + riRequest.providerId, + riRequest.amount, + riRequest.premium, + params.period, + params.coverAsset, + nonce + ); + + require(recoverSigner(message, riRequest.signature) == riSigner, InvalidSignature()); + require(params.paymentAsset == params.coverAsset, InvalidPaymentAsset()); + + coverId = _buyCover(params, poolAllocationRequests, riRequest.premium, riPremiumDestination); + + _coverRi[coverId].providerId = riRequest.providerId.toUint24(); + _coverRi[coverId].amount = riRequest.amount.toUint96(); + + emit CoverBought( + coverId, + params.coverId != 0 ? params.coverId : coverId, + params.productId, + msg.sender, + params.ipfsData + ); + + return coverId; + } + function _buyCover( BuyCoverParams memory params, - PoolAllocationRequest[] memory poolAllocationRequests - ) internal nonReentrant whenNotPaused returns (uint coverId) { + PoolAllocationRequest[] memory poolAllocationRequests, + uint riPremiumInPaymentAsset, + address riPremiumDestination + ) internal nonReentrant whenNotPaused(PAUSE_COVER) returns (uint coverId) { require(params.period >= MIN_COVER_PERIOD, CoverPeriodTooShort()); require(params.period <= MAX_COVER_PERIOD, CoverPeriodTooLong()); require(params.commissionRatio <= MAX_COMMISSION_RATIO, CommissionRateTooHigh()); - require(params.amount != 0, CoverAmountIsZero()); + + // using riPremium as a proxy for the riAmount + require(params.amount != 0 || riPremiumInPaymentAsset != 0, CoverAmountIsZero()); + // can pay with cover asset or nxm only require(params.paymentAsset == params.coverAsset || params.paymentAsset == NXM_ASSET_ID, InvalidPaymentAsset()); // new cover coverId = coverNFT.mint(params.owner); + uint premiumInPaymentAsset; + + { + uint nxmPriceInCoverAsset = pool.getInternalTokenPriceInAssetAndUpdateTwap(params.coverAsset); + uint amountDueInNXM = _createCover(params, poolAllocationRequests, coverId, nxmPriceInCoverAsset); + premiumInPaymentAsset = nxmPriceInCoverAsset * amountDueInNXM / ONE_NXM; + } + + _retrievePayment( + params.paymentAsset, + premiumInPaymentAsset, + riPremiumInPaymentAsset, + params.maxPremiumInAsset + riPremiumInPaymentAsset, + riPremiumDestination, + params.commissionRatio, + params.commissionDestination + ); + } + + function _createCover( + BuyCoverParams memory params, + PoolAllocationRequest[] memory poolAllocationRequests, + uint coverId, + uint nxmPriceInCoverAsset + ) internal returns (uint amountDueInNXM) { + uint previousCoverAmount; uint previousCoverExpiration; uint refundedPremium; @@ -190,8 +302,6 @@ contract Cover is ICover, MasterAwareV2, IStakingPoolBeacon, ReentrancyGuard, Mu AllocationRequest memory allocationRequest; { - ICoverProducts coverProducts = _coverProducts(); - require(params.productId < coverProducts.getProductCount(), ProductNotFound()); ( @@ -215,9 +325,7 @@ contract Cover is ICover, MasterAwareV2, IStakingPoolBeacon, ReentrancyGuard, Mu ); } - uint nxmPriceInCoverAsset = _pool().getInternalTokenPriceInAssetAndUpdateTwap(params.coverAsset); - - (uint coverAmountInCoverAsset, uint amountDueInNXM) = _requestAllocation( + (uint coverAmountInCoverAsset, uint totalPremiumInNxm) = _requestAllocation( allocationRequest, poolAllocationRequests, nxmPriceInCoverAsset @@ -244,14 +352,10 @@ contract Cover is ICover, MasterAwareV2, IStakingPoolBeacon, ReentrancyGuard, Mu previousCoverExpiration ); - _retrievePayment( - refundedPremium >= amountDueInNXM ? 0 : amountDueInNXM - refundedPremium, - params.paymentAsset, - nxmPriceInCoverAsset, - params.maxPremiumInAsset, - params.commissionRatio, - params.commissionDestination - ); + // cap refund at new cover premium + return totalPremiumInNxm > refundedPremium + ? totalPremiumInNxm - refundedPremium + : 0; } function expireCover(uint coverId) external { @@ -295,7 +399,7 @@ contract Cover is ICover, MasterAwareV2, IStakingPoolBeacon, ReentrancyGuard, Mu uint nxmPriceInCoverAsset ) internal returns ( uint totalCoverAmountInCoverAsset, - uint totalAmountDueInNXM + uint totalPremiumInNXM ) { uint totalCoverAmountInNXM; @@ -322,13 +426,13 @@ contract Cover is ICover, MasterAwareV2, IStakingPoolBeacon, ReentrancyGuard, Mu ) ); - totalAmountDueInNXM += premiumInNXM; + totalPremiumInNXM += premiumInNXM; totalCoverAmountInNXM += coverAmountInNXM; } totalCoverAmountInCoverAsset = totalCoverAmountInNXM * nxmPriceInCoverAsset / ONE_NXM; - return (totalCoverAmountInCoverAsset, totalAmountDueInNXM); + return (totalCoverAmountInCoverAsset, totalPremiumInNXM); } function _requestDeallocation( @@ -364,67 +468,68 @@ contract Cover is ICover, MasterAwareV2, IStakingPoolBeacon, ReentrancyGuard, Mu } function _retrievePayment( - uint premiumInNxm, uint paymentAsset, - uint nxmPriceInCoverAsset, - uint maxPremiumInAsset, + uint premium, + uint riPremium, + uint maxAmountInAsset, + address riPremiumDestination, uint commissionRatio, address commissionDestination ) internal { + uint totalPremium = premium + riPremium; + uint totalPremiumWithCommission = totalPremium * COMMISSION_DENOMINATOR / (COMMISSION_DENOMINATOR - commissionRatio); + uint commission = totalPremiumWithCommission - totalPremium; + + require(totalPremiumWithCommission <= maxAmountInAsset, PriceExceedsMaxPremiumInAsset()); require(msg.value == 0 || paymentAsset == ETH_ASSET_ID, UnexpectedEthSent()); // NXM payment if (paymentAsset == NXM_ASSET_ID) { - uint commissionInNxm; - - if (commissionRatio > 0) { - commissionInNxm = (premiumInNxm * COMMISSION_DENOMINATOR / (COMMISSION_DENOMINATOR - commissionRatio)) - premiumInNxm; - } - require(premiumInNxm + commissionInNxm <= maxPremiumInAsset, PriceExceedsMaxPremiumInAsset()); + // no ri premium when paying with nxm + require(riPremium == 0, UnexpectedRiPremium()); - ITokenController tokenController = _tokenController(); - tokenController.burnFrom(msg.sender, premiumInNxm); + tokenController.burnFrom(msg.sender, premium); - if (commissionInNxm > 0) { + if (commission > 0) { // commission transfer reverts if the commissionDestination is not a member - tokenController.operatorTransfer(msg.sender, commissionDestination, commissionInNxm); + tokenController.operatorTransfer(msg.sender, commissionDestination, commission); } return; } - IPool pool = _pool(); - uint premiumInPaymentAsset = nxmPriceInCoverAsset * premiumInNxm / ONE_NXM; - uint commission = (premiumInPaymentAsset * COMMISSION_DENOMINATOR / (COMMISSION_DENOMINATOR - commissionRatio)) - premiumInPaymentAsset; - uint premiumWithCommission = premiumInPaymentAsset + commission; - - require(premiumWithCommission <= maxPremiumInAsset, PriceExceedsMaxPremiumInAsset()); - // ETH payment if (paymentAsset == ETH_ASSET_ID) { - require(msg.value >= premiumWithCommission, InsufficientEthSent()); - uint remainder = msg.value - premiumWithCommission; + require(msg.value >= totalPremiumWithCommission, InsufficientEthSent()); - { + uint remainder = msg.value - totalPremiumWithCommission; + + if (premium > 0) { // send premium in eth to the pool // solhint-disable-next-line avoid-low-level-calls - (bool ok, /* data */) = address(pool).call{value: premiumInPaymentAsset}(""); - require(ok, SendingEthToPoolFailed()); + (bool ok, /* data */) = address(pool).call{value: premium}(""); + require(ok, ETHTransferFailed(address(pool), premium)); + } + + if (riPremium > 0) { + // solhint-disable-next-line avoid-low-level-calls + (bool ok, /* data */) = address(riPremiumDestination).call{value: riPremium}(""); + require(ok, ETHTransferFailed(riPremiumDestination, riPremium)); } // send commission if (commission > 0) { (bool ok, /* data */) = address(commissionDestination).call{value: commission}(""); - require(ok, SendingEthToCommissionDestinationFailed()); + require(ok, ETHTransferFailed(commissionDestination, commission)); } if (remainder > 0) { // solhint-disable-next-line avoid-low-level-calls (bool ok, /* data */) = address(msg.sender).call{value: remainder}(""); - require(ok, ReturningEthRemainderToSenderFailed()); + require(ok, ETHTransferFailed(msg.sender, remainder)); } return; @@ -432,7 +537,14 @@ contract Cover is ICover, MasterAwareV2, IStakingPoolBeacon, ReentrancyGuard, Mu address coverAsset = pool.getAsset(paymentAsset).assetAddress; IERC20 token = IERC20(coverAsset); - token.safeTransferFrom(msg.sender, address(pool), premiumInPaymentAsset); + + if (premium > 0) { + token.safeTransferFrom(msg.sender, address(pool), premium); + } + + if (riPremium > 0) { + token.safeTransferFrom(msg.sender, riPremiumDestination, riPremium); + } if (commission > 0) { token.safeTransferFrom(msg.sender, commissionDestination, commission); @@ -487,11 +599,7 @@ contract Cover is ICover, MasterAwareV2, IStakingPoolBeacon, ReentrancyGuard, Mu activeCover[coverAsset] = _activeCover; } - // TODO: remove return address - function burnStake( - uint coverId, - uint payoutAmountInAsset - ) external onlyInternal override returns (address /* coverOwner */) { + function burnStake(uint coverId, uint payoutAmountInAsset) external onlyContracts(C_CLAIMS) override { CoverData memory cover = _coverData[coverId]; @@ -531,22 +639,21 @@ contract Cover is ICover, MasterAwareV2, IStakingPoolBeacon, ReentrancyGuard, Mu // update && sstore cover.amount -= payoutAmountInAsset.toUint96(); _coverData[coverId] = cover; - - return coverNFT.ownerOf(coverId); } /* ========== VIEWS ========== */ + // TODO: patch the descriptor to display the amount including ri function getCoverData(uint coverId) external override view returns (CoverData memory) { return _coverData[coverId]; } - function getPoolAllocations(uint coverId) external override view returns (PoolAllocation[] memory) { - return _poolAllocations[coverId]; + function getCoverRi(uint coverId) external override view returns (Ri memory) { + return _coverRi[coverId]; } - function getCoverDataCount() external override view returns (uint) { - return coverNFT.totalSupply(); + function getCoverDataWithRi(uint coverId) external override view returns (CoverData memory, Ri memory) { + return (_coverData[coverId], _coverRi[coverId]); } function getCoverReference(uint coverId) public override view returns(CoverReference memory coverReference) { @@ -559,6 +666,14 @@ contract Cover is ICover, MasterAwareV2, IStakingPoolBeacon, ReentrancyGuard, Mu return (_coverData[coverId], getCoverReference(coverId)); } + function getPoolAllocations(uint coverId) external override view returns (PoolAllocation[] memory) { + return _poolAllocations[coverId]; + } + + function getCoverDataCount() external override view returns (uint) { + return coverNFT.totalSupply(); + } + function getLatestEditCoverData(uint coverId) external override view returns (CoverData memory) { CoverReference memory coverReference = getCoverReference(coverId); return _coverData[coverReference.latestCoverId]; @@ -614,7 +729,7 @@ contract Cover is ICover, MasterAwareV2, IStakingPoolBeacon, ReentrancyGuard, Mu return false; } - Asset memory asset = _pool().getAsset(assetId); + Asset memory asset = pool.getAsset(assetId); return asset.isCoverAsset && !asset.isAbandoned; } @@ -625,68 +740,20 @@ contract Cover is ICover, MasterAwareV2, IStakingPoolBeacon, ReentrancyGuard, Mu ); } - function changeCoverNFTDescriptor(address _coverNFTDescriptor) external onlyAdvisoryBoard { - coverNFT.changeNFTDescriptor(_coverNFTDescriptor); - } - - function changeStakingNFTDescriptor(address _stakingNFTDescriptor) external onlyAdvisoryBoard { - stakingNFT.changeNFTDescriptor(_stakingNFTDescriptor); - } - - /* ========== DEPENDENCIES ========== */ - - function _pool() internal view returns (IPool) { - return IPool(internalContracts[uint(ID.P1)]); - } - - function _tokenController() internal view returns (ITokenController) { - return ITokenController(internalContracts[uint(ID.TC)]); + function setRiSigner(address _riSigner) external onlyContracts(C_GOVERNOR) { + riSigner = _riSigner; } - function _memberRoles() internal view returns (IMemberRoles) { - return IMemberRoles(internalContracts[uint(ID.MR)]); + function setRiConfig(uint providerId, address premiumDestination) external onlyContracts(C_GOVERNOR) { + _riProviderConfigs[providerId].premiumDestination = premiumDestination; } - function _coverProducts() internal view returns (ICoverProducts) { - return ICoverProducts(internalContracts[uint(ID.CP)]); + function changeCoverNFTDescriptor(address _coverNFTDescriptor) external onlyContracts(C_GOVERNOR) { + coverNFT.changeNFTDescriptor(_coverNFTDescriptor); } - function changeDependentContractAddress() external override { - internalContracts[uint(ID.P1)] = master.getLatestAddress("P1"); - internalContracts[uint(ID.TC)] = master.getLatestAddress("TC"); - internalContracts[uint(ID.MR)] = master.getLatestAddress("MR"); - internalContracts[uint(ID.CP)] = master.getLatestAddress("CP"); + function changeStakingNFTDescriptor(address _stakingNFTDescriptor) external onlyContracts(C_GOVERNOR) { + stakingNFT.changeNFTDescriptor(_stakingNFTDescriptor); } - /* ========== MIGRATION ========== */ - - function migrateCoverDataAndPoolAllocations(uint[] calldata coverIds) external { - uint length = coverIds.length; - for(uint i=0; i 0, AlreadyMigratedCoverData(coverId)); - - LegacyCoverData memory legacyCoverData = _legacyCoverData[coverId]; - - _coverData[coverId] = CoverData({ - productId: legacyCoverData.productId, - coverAsset: legacyCoverData.coverAsset, - amount: legacyCoverSegment.amount, - start: legacyCoverSegment.start, - period: legacyCoverSegment.period, - gracePeriod: legacyCoverSegment.gracePeriod, - rewardsRatio: uint(legacyCoverSegment.globalRewardsRatio).toUint16(), - capacityRatio: uint(legacyCoverSegment.globalCapacityRatio).toUint16() - }); - - _poolAllocations[coverId] = _legacyCoverSegmentAllocations[coverId][0]; - - delete _legacyCoverSegments[coverId][0]; - delete _legacyCoverData[coverId]; - delete _legacyCoverSegmentAllocations[coverId][0]; - } - } } diff --git a/contracts/modules/cover/CoverNFTDescriptor.sol b/contracts/modules/cover/CoverNFTDescriptor.sol index aec27d3f65..61fd3bbf48 100644 --- a/contracts/modules/cover/CoverNFTDescriptor.sol +++ b/contracts/modules/cover/CoverNFTDescriptor.sol @@ -68,6 +68,9 @@ contract CoverNFTDescriptor is ICoverNFTDescriptor { // Get cover data CoverData memory coverData = cover.getCoverData(tokenId); + Ri memory coverRi = cover.getCoverRi(tokenId); + uint amount = coverData.amount + coverRi.amount; + string memory productName = coverProducts.getProductName(coverData.productId); // Check if cover has already expired @@ -91,7 +94,7 @@ contract CoverNFTDescriptor is ICoverNFTDescriptor { descriptionString = string( abi.encodePacked( "This NFT represents a cover purchase made for: ", productName, - " \\nAmount Covered: ", FloatingPoint.toFloat(uint(coverData.amount), getAssetDecimals(coverData.coverAsset))," ", getAssetSymbol(coverData.coverAsset), + " \\nAmount Covered: ", FloatingPoint.toFloat(uint(amount), getAssetDecimals(coverData.coverAsset))," ", getAssetSymbol(coverData.coverAsset), " \\nExpiry Date: ", expiry, " \\n", expiryMessage ) @@ -101,7 +104,7 @@ contract CoverNFTDescriptor is ICoverNFTDescriptor { productName, getAssetSymbol(coverData.coverAsset), expiry, - coverData.amount, + amount, tokenId, getAssetDecimals(coverData.coverAsset) ); diff --git a/docs/contracts/TokenController.md b/docs/contracts/TokenController.md index 39df4b4f6a..d0e07e31d8 100644 --- a/docs/contracts/TokenController.md +++ b/docs/contracts/TokenController.md @@ -13,9 +13,9 @@ This contract enables: **Designed for Internal Use Only** -- 🚫 TokenController is NOT meant for direct integration by users or external contracts. -- ✅ Only protocol-approved contracts (e.g., Governance, StakingPool, Assessment, Pool) can interact with it. -- ✅ Functions are restricted using access control mechanisms such as onlyInternal and onlyGovernance. +- NOT meant for direct integration by users or external contracts. +- Only protocol-approved contracts (e.g., Governance, StakingPool, Assessment, Pool) can interact with it. +- Functions are restricted using access control mechanisms such as onlyInternal and onlyGovernance. This design ensures that all NXM token movements remain securely controlled within the protocol. @@ -61,7 +61,7 @@ Tokens can be locked for various reasons, restricting transfers until the condit | **Claim Assessment Lock** | Ensures assessors cannot withdraw NXM mid-vote. | Unlocks after the claim is resolved. | | **Staking Lock** | Ensures liquidity remains available for covers. | Unlocks after the staking period expires. | -**Important:** +**Important:** If NXM is locked for **multiple reasons**, **all** unlock conditions must be met before withdrawal is allowed. --- diff --git a/hardhat-config/index.js b/hardhat-config/index.js index ca34d8731a..7e4c97543a 100644 --- a/hardhat-config/index.js +++ b/hardhat-config/index.js @@ -35,11 +35,12 @@ const config = { }, mocha: { - exit: true, bail: false, - timeout: 0, + exit: true, + jobs: Number(process.env.MOCHA_JOBS) || undefined, + parallel: true, slow: 5000, - jobs: Number(process.env.MOCHA_JOBS) || 3, + timeout: 0, }, networks: require('./networks'), diff --git a/hardhat-config/tasks.js b/hardhat-config/tasks.js index 42dc5e9c76..24e78a6e3b 100644 --- a/hardhat-config/tasks.js +++ b/hardhat-config/tasks.js @@ -1,21 +1,16 @@ const fs = require('node:fs'); const path = require('node:path'); -const { extendEnvironment, task } = require('hardhat/config'); +const { extendConfig, extendEnvironment, task } = require('hardhat/config'); +const { TASK_TEST } = require('hardhat/builtin-tasks/task-names'); const { TASK_TYPECHAIN } = require('@typechain/hardhat/dist/constants'); -const { TASK_COMPILE, TASK_TEST } = require('hardhat/builtin-tasks/task-names'); extendEnvironment(hre => { hre.nexus = require('../lib'); }); -task(TASK_TYPECHAIN, async (args, hre, runSuper) => { - hre.config.typechain.dontOverrideCompile = false; - await runSuper(); -}); - -task(TASK_COMPILE).setAction(async function (_, hre, runSuper) { - const { compilers, overrides } = hre.config.solidity; +extendConfig(config => { + const { compilers, overrides } = config.solidity; // add storageLayout to compilers if missing for (const compiler of compilers) { @@ -32,7 +27,10 @@ task(TASK_COMPILE).setAction(async function (_, hre, runSuper) { output.push('storageLayout'); } } +}); +task(TASK_TYPECHAIN, async (args, hre, runSuper) => { + hre.config.typechain.dontOverrideCompile = false; await runSuper(); }); diff --git a/lib/constants.js b/lib/constants.js index 885613b86a..15b254d2ca 100644 --- a/lib/constants.js +++ b/lib/constants.js @@ -27,6 +27,7 @@ const PauseTypes = { PAUSE_MEMBERSHIP: 1n << 3n, PAUSE_ASSESSMENTS: 1n << 4n, PAUSE_CLAIMS: 1n << 5n, + PAUSE_COVER: 1n << 6n, }; const Choice = { diff --git a/lib/index.js b/lib/index.js index 4c95e6e560..43bde4babd 100644 --- a/lib/index.js +++ b/lib/index.js @@ -1,17 +1,17 @@ const constants = require('./constants'); const helpers = require('./helpers'); -const membership = require('./membership'); const multicall = require('./multicall'); const protocol = require('./protocol'); const pool = require('./pool'); +const signing = require('./signing'); const nexus = { constants, helpers, - membership, multicall, pool, protocol, + signing, }; module.exports = nexus; diff --git a/lib/membership.js b/lib/membership.js deleted file mode 100644 index 93d19158be..0000000000 --- a/lib/membership.js +++ /dev/null @@ -1,46 +0,0 @@ -const { ethers } = require('ethers'); - -/** - * @typedef {import('ethers').Addressable} Addressable - * @typedef {import('ethers').AddressLike} AddressLike - */ - -/** - * @param {Addressable|AddressLike} addressable - * @returns {Promise} - */ -const getAddress = async addressable => { - return ethers.isAddress(addressable) ? addressable : await addressable.getAddress(); -}; - -/** - * @param {import('ethers').Signer} signer - * @param {Addressable|AddressLike} member - * @param {Addressable|AddressLike} verifyingContract - * @param {{ name: string, version: string, chainId: number }} [options] - * @returns {Promise} - */ -const signJoinMessage = async (signer, member, verifyingContract, options = {}) => { - const defaults = { name: 'NexusMutualRegistry', version: '1.0.0' }; - const config = { ...defaults, ...options }; - - if (config.chainId === undefined) { - config.chainId = (await signer.provider.getNetwork()).chainId; - } - - const memberAddress = await getAddress(member); - const verifier = await getAddress(verifyingContract); - - const name = config.name; - const version = config.version; - const chainId = config.chainId; - - const domain = { name, version, chainId, verifyingContract: verifier }; - - const types = { Join: [{ name: 'member', type: 'address' }] }; - const value = { member: memberAddress }; - - return signer.signTypedData(domain, types, value); -}; - -module.exports = { signJoinMessage }; diff --git a/lib/signing.js b/lib/signing.js new file mode 100644 index 0000000000..b159646877 --- /dev/null +++ b/lib/signing.js @@ -0,0 +1,102 @@ +const { ethers } = require('ethers'); + +/** + * @typedef {import('ethers').AddressLike} AddressLike + */ + +/** + * @param {AddressLike} addresslike + * @returns {Promise} + */ +const getAddress = async addresslike => { + return ethers.isAddress(addresslike) ? addresslike : await addresslike.getAddress(); +}; + +/** + * @param {import('ethers').Signer} signer + * @param {AddressLike} member + * @param {AddressLike} verifyingContract + * @param {{ name: string, version: string, chainId: number }} [options] + * @returns {Promise} + */ +const signJoinMessage = async (signer, member, verifyingContract, options = {}) => { + const defaults = { name: 'NexusMutualRegistry', version: '1.0.0' }; + const config = { ...defaults, ...options }; + + if (config.chainId === undefined) { + config.chainId = (await signer.provider.getNetwork()).chainId; + } + + const memberAddress = await getAddress(member); + const verifier = await getAddress(verifyingContract); + + const name = config.name; + const version = config.version; + const chainId = config.chainId; + + const domain = { name, version, chainId, verifyingContract: verifier }; + + const types = { Join: [{ name: 'member', type: 'address' }] }; + const value = { member: memberAddress }; + + return signer.signTypedData(domain, types, value); +}; + +/** + * @param {import('ethers').Signer} signer + * @param {AddressLike} verifyingContract + * @param {object} quote + * @param {bigint|number} quote.coverId + * @param {bigint|number} quote.productId + * @param {bigint|number} quote.providerId + * @param {bigint|number} quote.amount + * @param {bigint|number} quote.premium + * @param {bigint|number} quote.period + * @param {bigint|number} quote.coverAsset + * @param {bigint|number} quote.nonce + * @param {{ name?: string, version?: string, chainId?: number }} [options] + * @returns {Promise} + */ +const signRiQuote = async (signer, verifyingContract, quote, options = {}) => { + const defaults = { name: 'NexusMutualCover', version: '1.0.0' }; + const config = { ...defaults, ...options }; + + if (config.chainId === undefined) { + config.chainId = (await signer.provider.getNetwork()).chainId; + } + + const name = config.name; + const version = config.version; + const chainId = config.chainId; + + const verifier = verifyingContract.target || verifyingContract; + const domain = { name, version, chainId, verifyingContract: verifier }; + + const types = { + RiQuote: [ + { name: 'coverId', type: 'uint256' }, + { name: 'productId', type: 'uint24' }, + { name: 'providerId', type: 'uint256' }, + { name: 'amount', type: 'uint256' }, + { name: 'premium', type: 'uint256' }, + { name: 'period', type: 'uint32' }, + { name: 'coverAsset', type: 'uint8' }, + { name: 'nonce', type: 'uint256' }, + ], + }; + + const values = { + coverId: quote.coverId ?? 0, + productId: quote.productId, + providerId: quote.providerId, + amount: quote.amount, + premium: quote.premium, + period: quote.period, + coverAsset: quote.coverAsset, + nonce: quote.nonce, + }; + + return signer.signTypedData(domain, types, values); +}; + +module.exports = { signJoinMessage, signRiQuote }; diff --git a/package-lock.json b/package-lock.json index 748111eb93..b85d8cd9a7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -47,7 +47,7 @@ "hardhat": "^2.26.3", "hardhat-contract-sizer": "^2.8.0", "hardhat-gas-reporter": "^2.3.0", - "hardhat-tracer": "^3.2.1", + "hardhat-tracer": "^3.4.0", "husky": "^9.1.6", "ipfs-http-client": "^47.0.1", "nodemon": "^3.1.9", @@ -9600,9 +9600,9 @@ } }, "node_modules/hardhat-tracer": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/hardhat-tracer/-/hardhat-tracer-3.2.1.tgz", - "integrity": "sha512-nCRCi8zQYDkNw9xDngokbmBmBDNuX9BadgvoRNVgpVpFkriRirdBVinYfBc7P+vufZhNNOjxyIDOPQs+ViRN9Q==", + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/hardhat-tracer/-/hardhat-tracer-3.4.0.tgz", + "integrity": "sha512-Szro71artobfytPVvv6SMLlboXoe/Z3opjuij3A0fEOCgQs1YKZhlvkKK+KAY7kLRgqQse0jO/dF2Sq3xIAJwg==", "dev": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index c2f86b102b..c4dd5fc0ab 100644 --- a/package.json +++ b/package.json @@ -58,7 +58,7 @@ "hardhat": "^2.26.3", "hardhat-contract-sizer": "^2.8.0", "hardhat-gas-reporter": "^2.3.0", - "hardhat-tracer": "^3.2.1", + "hardhat-tracer": "^3.4.0", "husky": "^9.1.6", "ipfs-http-client": "^47.0.1", "nodemon": "^3.1.9", diff --git a/test/fork/basic-functionality-tests.js b/test/fork/basic-functionality-tests.js index 07280ce818..53b6ea88b2 100644 --- a/test/fork/basic-functionality-tests.js +++ b/test/fork/basic-functionality-tests.js @@ -106,7 +106,7 @@ describe('basic functionality tests', function () { const { chainId } = await ethers.provider.getNetwork(); for (const member of this.members) { - const signature = await nexus.membership.signJoinMessage(this.kycAuthSigner, member, this.registry, { chainId }); + const signature = await nexus.signing.signJoinMessage(this.kycAuthSigner, member, this.registry, { chainId }); await this.registry.join(member, signature, { value: JOINING_FEE }); expect(await this.registry.isMember(member.address)).to.be.true; } diff --git a/test/init.js b/test/init.js new file mode 100644 index 0000000000..b4427f271c --- /dev/null +++ b/test/init.js @@ -0,0 +1,14 @@ +const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); + +// This is a fixture's sole purpose is to take a snapshot of a clean state before ANY test runs. +// It's supposed be inherited by all setup fixtures that need a clean state. + +const init = async () => { + // noop +}; + +const mochaHooks = { + beforeAll: () => loadFixture(init), +}; + +module.exports = { mochaHooks, init }; diff --git a/test/integration/setup.js b/test/integration/setup.js index fb7e1729f7..69b4df7dd8 100644 --- a/test/integration/setup.js +++ b/test/integration/setup.js @@ -1,5 +1,7 @@ const { ethers, nexus } = require('hardhat'); -const { setBalance, impersonateAccount } = require('@nomicfoundation/hardhat-network-helpers'); +const { impersonateAccount, loadFixture, setBalance } = require('@nomicfoundation/hardhat-network-helpers'); + +const { init } = require('../init'); const { parseEther, parseUnits, ZeroAddress, MaxUint256 } = ethers; const { ContractIndexes, ClaimMethod, AggregatorType, Assets } = nexus.constants; @@ -16,6 +18,7 @@ const assignRoles = accounts => ({ }); async function setup() { + await loadFixture(init); const accounts = assignRoles(await ethers.getSigners()); const { defaultSender, members, advisoryBoardMembers, stakingPoolManagers, emergencyAdmins } = accounts; const [abMember] = advisoryBoardMembers; @@ -188,10 +191,9 @@ async function setup() { ]); const coverImplementation = await ethers.deployContract('Cover', [ - coverNFT, - stakingNFT, - stakingPoolFactory, + registry, stakingPoolImplementation, + await registry.getContractAddressByIndex(ContractIndexes.C_COVER), // verifying contract ]); const coverProductsImplementation = await ethers.deployContract('CoverProducts', []); @@ -299,11 +301,7 @@ async function setup() { ); } - const masterAwareContracts = [ - ContractIndexes.C_COVER, - ContractIndexes.C_COVER_PRODUCTS, - ContractIndexes.C_STAKING_PRODUCTS, - ]; + const masterAwareContracts = [ContractIndexes.C_COVER_PRODUCTS, ContractIndexes.C_STAKING_PRODUCTS]; for (const contract of masterAwareContracts) { const contractAddress = await registry.getContractAddressByIndex(contract); @@ -312,8 +310,8 @@ async function setup() { await masterAwareContract.changeDependentContractAddress(); } - const coverBroker = await ethers.deployContract('CoverBroker', [registry.target, defaultSender.address]); - await registry.addMembers([coverBroker.target]); + const coverBroker = await ethers.deployContract('CoverBroker', [registry, defaultSender.address]); + await registry.addMembers([coverBroker]); // work done, switch to the real Governor, registry and Master contracts await registry.replaceGovernor(numberToBytes32(1337), governorImplementation); diff --git a/test/layout/slots.js b/test/layout/slots.js index 98867568f9..1c409540e0 100644 --- a/test/layout/slots.js +++ b/test/layout/slots.js @@ -1,8 +1,9 @@ +const fs = require('node:fs'); +const os = require('node:os'); +const path = require('node:path'); + const { config } = require('hardhat'); const { expect } = require('chai'); -const fs = require('fs'); -const os = require('os'); -const path = require('path'); const extractStorageLayout = require(path.join(config.paths.root, 'scripts/extract-storage-layout')); @@ -64,35 +65,40 @@ describe('Storage layout', function () { // } const exceptions = { Cover: { - _unused_products: { + master: { label: '__unused_0', - type: ['t_array(t_struct(Product)_storage)dyn_storage', 't_uint256'], + size: [20, 288], // uses 9 slots now, 32 * 9 = 288 bytes + type: ['t_address', 't_array(t_uint256)_storage'], }, - _unused_productTypes: { - label: '__unused_1', - type: ['t_array(t_struct(ProductType)_storage)dyn_storage', 't_uint256'], + internalContracts: { + deleted: true, }, - coverSegmentAllocations: { - label: '_legacyCoverSegmentAllocations', + _status: { + deleted: true, }, - _unused_allowedPools: { - label: '__unused_4', - type: ['t_mapping(t_uint256,t_array(t_uint256)dyn_storage)', 't_uint256'], + __unused_0: { + deleted: true, }, - _coverSegments: { - label: '_legacyCoverSegments', - type: [ - 't_mapping(t_uint256,t_array(t_struct(CoverSegment)_storage)dyn_storage)', - 't_mapping(t_uint256,t_array(t_struct(LegacyCoverSegment)_storage)dyn_storage)', - ], + __unused_1: { + deleted: true, }, - _unused_productNames: { - label: '__unused_8', - type: ['t_mapping(t_uint256,t_string_storage)', 't_uint256'], + _legacyCoverData: { + deleted: true, }, - _unused_productTypeNames: { - label: '__unused_9', - type: ['t_mapping(t_uint256,t_string_storage)', 't_uint256'], + _legacyCoverSegmentAllocations: { + deleted: true, + }, + __unused_4: { + deleted: true, + }, + _legacyCoverSegments: { + deleted: true, + }, + __unused_8: { + deleted: true, + }, + __unused_9: { + deleted: true, }, }, TokenController: { diff --git a/test/unit/Assessment/setup.js b/test/unit/Assessment/setup.js index c7782e00fc..c698fa9b44 100644 --- a/test/unit/Assessment/setup.js +++ b/test/unit/Assessment/setup.js @@ -1,6 +1,8 @@ const { ethers, nexus } = require('hardhat'); +const { loadFixture, setBalance } = require('@nomicfoundation/hardhat-network-helpers'); + const { getAccounts } = require('../../utils/accounts'); -const { setEtherBalance } = require('../../utils/evm'); +const { init } = require('../../init'); const { ContractIndexes } = nexus.constants; const ONE_DAY = BigInt(24 * 60 * 60); @@ -10,6 +12,7 @@ const CLAIM_ID = 1; const IPFS_HASH = ethers.solidityPackedKeccak256(['string'], ['standard-ipfs-hash']); async function setup() { + await loadFixture(init); const accounts = await getAccounts(); // Deploy contracts @@ -47,13 +50,13 @@ async function setup() { // Use a member account to submit the claim const [coverOwner] = accounts.members; - await setEtherBalance(coverOwner.address, ethers.parseEther('10')); + await setBalance(coverOwner.address, ethers.parseEther('10')); // 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')); + await setBalance(claims.target, ethers.parseEther('10')); return { accounts, diff --git a/test/unit/Claims/redeemClaimPayout.js b/test/unit/Claims/redeemClaimPayout.js index 4e6d9972f0..755530d40b 100644 --- a/test/unit/Claims/redeemClaimPayout.js +++ b/test/unit/Claims/redeemClaimPayout.js @@ -146,7 +146,7 @@ describe('redeemClaimPayout', function () { it('triggers twap update when fetching the token price', async function () { const fixture = await loadFixture(setup); - const { claims, cover, assessment, pool } = fixture.contracts; + const { claims, cover, assessment, ramm } = fixture.contracts; const [coverOwner] = fixture.accounts.members; await createMockCover(cover, { owner: coverOwner.address }); @@ -154,7 +154,7 @@ describe('redeemClaimPayout', function () { await submitClaim(fixture)({ coverId: 1, sender: coverOwner }); await assessment.setAssessmentForOutcome(claimId, AssessmentOutcome.Accepted); - expect(await claims.connect(coverOwner).redeemClaimPayout(claimId)).to.emit(pool, 'TwapUpdateTriggered'); + await expect(claims.connect(coverOwner).redeemClaimPayout(claimId)).to.emit(ramm, 'TwapUpdateTriggered'); }); it('sends the payout amount in ETH and the assessment deposit to the cover owner', async function () { diff --git a/test/unit/Claims/setup.js b/test/unit/Claims/setup.js index 1c492cabf7..799a08753f 100644 --- a/test/unit/Claims/setup.js +++ b/test/unit/Claims/setup.js @@ -1,13 +1,16 @@ const { ethers, nexus } = require('hardhat'); +const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); -const { Assets, ContractIndexes, ClaimMethod, PoolAsset } = nexus.constants; const { getAccounts } = require('../../utils/accounts'); +const { init } = require('../../init'); +const { Assets, ContractIndexes, ClaimMethod, PoolAsset } = nexus.constants; const { parseEther } = ethers; const ONE_DAY = 24 * 60 * 60; async function setup() { + await loadFixture(init); const accounts = await getAccounts(); const nxm = await ethers.deployContract('NXMTokenMock'); @@ -70,16 +73,13 @@ async function setup() { await coverProducts.addProduct({ ...productTemplate, productType: '1' }); // productId 1 -> productType 1 await coverProducts.addProduct({ ...productTemplate, productType: '2' }); // productId 2 -> productType 2 - const tokenControllerAddress = tokenController.target; for (const member of accounts.members) { - await Promise.all([ - registry.join(member, ethers.toBeHex(0, 32)), - nxm.mint(member.address, parseEther('10000')), - nxm.connect(member).approve(tokenControllerAddress, parseEther('10000')), - ]); + await registry.join(member, ethers.toBeHex(0, 32)); + await nxm.mint(member.address, parseEther('10000')); + await nxm.connect(member).approve(tokenController, parseEther('10000')); } - accounts.defaultSender.sendTransaction({ to: pool.target, value: parseEther('200') }); + await accounts.defaultSender.sendTransaction({ to: pool.target, value: parseEther('200') }); await dai.mint(pool.target, parseEther('200')); const config = { @@ -88,6 +88,7 @@ async function setup() { const contracts = { pool, + ramm, nxm, dai, claims, diff --git a/test/unit/Cover/burnStake.js b/test/unit/Cover/burnStake.js index ecc9ae75c7..c1dfb3754c 100644 --- a/test/unit/Cover/burnStake.js +++ b/test/unit/Cover/burnStake.js @@ -1,21 +1,25 @@ -const { ethers } = require('hardhat'); +const { ethers, nexus } = require('hardhat'); const { expect } = require('chai'); const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); -const { parseEther } = ethers; const { setup } = require('./setup'); +const { parseEther } = ethers; +const { ContractIndexes } = nexus.constants; + const gracePeriod = 120 * 24 * 3600; // 120 days const GLOBAL_CAPACITY_DENOMINATOR = 10000n; async function burnStakeFixture() { const fixture = await loadFixture(setup); - const { cover, accounts } = fixture; - const coverBuyer1 = accounts.members[0]; - const coverBuyer2 = accounts.members[1]; + const { accounts, cover, registry } = fixture; + const [coverBuyer1, coverBuyer2] = accounts.members; const { COVER_BUY_FIXTURE } = fixture.constants; const { amount, targetPriceRatio, period, priceDenominator, productId, coverAsset } = COVER_BUY_FIXTURE; + const [claims] = accounts.internalContracts; + await registry.addContract(ContractIndexes.C_CLAIMS, claims, true); + const expectedPremium = (amount * targetPriceRatio * period) / (priceDenominator * 3600n * 24n * 365n); // buyCover on 1 pool const singlePoolAllocationRequest = [{ poolId: 1, coverAmountInAsset: amount }]; @@ -76,7 +80,7 @@ describe('burnStake', function () { const fixture = await loadFixture(burnStakeFixture); const { cover, stakingProducts, accounts, constants, singlePoolCoverId } = fixture; const { COVER_BUY_FIXTURE } = constants; - const [internal] = accounts.internalContracts; + const [claims] = accounts.internalContracts; const { productId, coverAsset, period, amount } = COVER_BUY_FIXTURE; const payoutAmountInAsset = amount / 2n; @@ -88,7 +92,7 @@ describe('burnStake', function () { const payoutAmountInNXM = (poolAllocation.coverAmountInNXM * payoutAmountInAsset) / coverData.amount; const expectedBurnAmount = (payoutAmountInNXM * GLOBAL_CAPACITY_DENOMINATOR) / coverData.capacityRatio; - await cover.connect(internal).burnStake(singlePoolCoverId, payoutAmountInAsset); + await cover.connect(claims).burnStake(singlePoolCoverId, payoutAmountInAsset); const storedCoverData = await cover.getCoverData(singlePoolCoverId); expect(storedCoverData.productId).to.equal(productId); @@ -111,16 +115,16 @@ describe('burnStake', function () { const burnAmountDivisor = 2n; const burnAmount = amount / burnAmountDivisor; - await expect(cover.connect(member).burnStake(singlePoolCoverId, burnAmount)).to.be.revertedWith( - 'Caller is not an internal contract', - ); + await expect(cover.connect(member).burnStake(singlePoolCoverId, burnAmount)) + .to.be.revertedWithCustomError(cover, 'Unauthorized') + .withArgs(member.address, 0, 1 << 15); }); it('updates segment allocation cover amount in nxm', async function () { const fixture = await loadFixture(burnStakeFixture); const { cover, singlePoolCoverId } = fixture; const { amount } = fixture.constants.COVER_BUY_FIXTURE; - const [internal] = fixture.accounts.internalContracts; + const [claims] = fixture.accounts.internalContracts; const burnAmountDivisor = 2n; const burnAmount = amount / burnAmountDivisor; @@ -128,7 +132,7 @@ describe('burnStake', function () { const [poolAllocationBefore] = await cover.getPoolAllocations(singlePoolCoverId); const payoutAmountInNXM = poolAllocationBefore.coverAmountInNXM / burnAmountDivisor; - await cover.connect(internal).burnStake(singlePoolCoverId, burnAmount); + await cover.connect(claims).burnStake(singlePoolCoverId, burnAmount); const [poolAllocationAfter] = await cover.getPoolAllocations(singlePoolCoverId); @@ -139,7 +143,7 @@ describe('burnStake', function () { const fixture = await loadFixture(burnStakeFixture); const { cover, stakingProducts, constants, doublePoolCoverId } = fixture; const { COVER_BUY_FIXTURE } = constants; - const [internal] = fixture.accounts.internalContracts; + const [claims] = fixture.accounts.internalContracts; const { productId, coverAsset, period, amount } = COVER_BUY_FIXTURE; const amountOfPools = 2n; @@ -156,7 +160,7 @@ describe('burnStake', function () { return (payoutInNXM * GLOBAL_CAPACITY_DENOMINATOR) / coverData.capacityRatio; }); - await cover.connect(internal).burnStake(doublePoolCoverId, payoutAmountInAsset); + await cover.connect(claims).burnStake(doublePoolCoverId, payoutAmountInAsset); const storedCoverData = await cover.getCoverData(doublePoolCoverId); expect(storedCoverData.productId).to.equal(productId); @@ -184,7 +188,7 @@ describe('burnStake', function () { it('should perform a burn with globalCapacityRatio when the cover was bought', async function () { const fixture = await loadFixture(burnStakeFixture); const { cover, stakingProducts, constants, singlePoolCoverId } = fixture; - const [internal] = fixture.accounts.internalContracts; + const [claims] = fixture.accounts.internalContracts; const { productId, coverAsset, period, amount } = constants.COVER_BUY_FIXTURE; const payoutAmountInAsset = amount / 2n; @@ -196,7 +200,7 @@ describe('burnStake', function () { const payoutAmountInNXM = (poolAllocation.coverAmountInNXM * payoutAmountInAsset) / coverData.amount; const expectedBurnAmount = (payoutAmountInNXM * GLOBAL_CAPACITY_DENOMINATOR) / coverData.capacityRatio; - await cover.connect(internal).burnStake(singlePoolCoverId, payoutAmountInAsset); + await cover.connect(claims).burnStake(singlePoolCoverId, payoutAmountInAsset); const storedCoverData = await cover.getCoverData(singlePoolCoverId); expect(storedCoverData.productId).to.equal(productId); @@ -213,7 +217,7 @@ describe('burnStake', function () { it('updates segment allocation premium in nxm', async function () { const fixture = await loadFixture(burnStakeFixture); const { cover, singlePoolCoverId } = fixture; - const [internal] = fixture.accounts.internalContracts; + const [claims] = fixture.accounts.internalContracts; const { amount } = fixture.constants.COVER_BUY_FIXTURE; const burnAmountDivisor = 2n; @@ -221,7 +225,7 @@ describe('burnStake', function () { const [poolAllocationBefore] = await cover.getPoolAllocations(singlePoolCoverId); - await cover.connect(internal).burnStake(singlePoolCoverId, burnAmount); + await cover.connect(claims).burnStake(singlePoolCoverId, burnAmount); const payoutAmountInNXM = poolAllocationBefore.premiumInNXM / burnAmountDivisor; const [poolAllocationAfter] = await cover.getPoolAllocations(singlePoolCoverId); diff --git a/test/unit/Cover/buyCover.js b/test/unit/Cover/buyCover.js index a8204adc99..8997acff61 100644 --- a/test/unit/Cover/buyCover.js +++ b/test/unit/Cover/buyCover.js @@ -1,11 +1,11 @@ const { expect } = require('chai'); const { ethers, nexus } = require('hardhat'); -const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); -const { parseEther, MaxUint256, ZeroAddress } = ethers; -const { PoolAsset } = nexus.constants; +const { loadFixture, setBalance } = require('@nomicfoundation/hardhat-network-helpers'); const { setup } = require('./setup'); -const { setEtherBalance } = require('../../utils/evm'); + +const { parseEther, MaxUint256, ZeroAddress } = ethers; +const { PoolAsset, PauseTypes } = nexus.constants; const gracePeriod = 120 * 24 * 3600; // 120 days const NXM_ASSET_ID = 255; @@ -628,31 +628,35 @@ describe('buyCover', function () { it('reverts if system is paused', async function () { const fixture = await loadFixture(setup); - const { cover, master } = fixture; + const { cover, registry } = fixture; const [coverBuyer] = fixture.accounts.members; const { amount, productId, coverAsset, period, expectedPremium } = buyCoverFixture; - await master.setEmergencyPause(true); + const buyCoverParams = { + coverId: 0, + owner: coverBuyer.address, + productId, + coverAsset, + amount, + period, + maxPremiumInAsset: expectedPremium, + paymentAsset: coverAsset, + commissionRatio: parseEther('0'), + commissionDestination: ZeroAddress, + ipfsData: '', + }; - await expect( - cover.connect(coverBuyer).buyCover( - { - coverId: 0, - owner: coverBuyer.address, - productId, - coverAsset, - amount, - period, - maxPremiumInAsset: expectedPremium, - paymentAsset: coverAsset, - commissionRatio: parseEther('0'), - commissionDestination: ZeroAddress, - ipfsData: '', - }, - poolAllocationRequest, - { value: expectedPremium }, - ), - ).to.be.revertedWith('System is paused'); + await registry.confirmPauseConfig(PauseTypes.PAUSE_COVER); + + await expect(cover.connect(coverBuyer).buyCover(buyCoverParams, poolAllocationRequest)) + .to.be.revertedWithCustomError(cover, 'Paused') + .withArgs(PauseTypes.PAUSE_COVER, PauseTypes.PAUSE_COVER); + + await registry.confirmPauseConfig(PauseTypes.PAUSE_GLOBAL); + + await expect(cover.connect(coverBuyer).buyCover(buyCoverParams, poolAllocationRequest)) + .to.be.revertedWithCustomError(cover, 'Paused') + .withArgs(PauseTypes.PAUSE_GLOBAL, PauseTypes.PAUSE_COVER); }); it('reverts if caller is not member', async function () { @@ -683,7 +687,7 @@ describe('buyCover', function () { poolAllocationRequest, { value: expectedPremium }, ), - ).to.be.revertedWith('Caller is not a member'); + ).to.be.revertedWithCustomError(cover, 'OnlyMember'); }); it('reverts if owner is address zero', async function () { @@ -1220,12 +1224,12 @@ describe('buyCover', function () { { value: expectedPremiumWithCommission }, ); - await setEtherBalance(reentrantExploiter.target, expectedPremium * 2n); + await setBalance(reentrantExploiter.target, expectedPremium * 2n); await reentrantExploiter.setReentrancyParams(cover, expectedPremium, txData.data); // The test uses the payment to the commission destination to trigger reentrancy for the buyCover call. // The nonReentrant protection will make the new call revert, making the payment to the commission address to fail. - // The expected custom error is 'SendingEthToCommissionDestinationFailed' + // The expected custom error is 'ETHTransferFailed' // because the commission payment fails thanks to the nonReentrant guard. // Even if we can't verify that the transaction reverts with the "ReentrancyGuard: reentrant call" message // if the nonReentrant guard is removed from the buyCover() method this test will fail because the following @@ -1242,13 +1246,13 @@ describe('buyCover', function () { maxPremiumInAsset: expectedPremiumWithCommission, paymentAsset: coverAsset, commissionRatio, - commissionDestination: reentrantExploiter.target, + commissionDestination: reentrantExploiter, ipfsData: '', }, poolAllocationRequest, { value: expectedPremiumWithCommission }, ), - ).to.be.revertedWithCustomError(cover, 'SendingEthToCommissionDestinationFailed'); + ).to.be.revertedWithCustomError(cover, 'ETHTransferFailed'); }); it('correctly store cover and allocation data for the second cover buyer', async function () { diff --git a/test/unit/Cover/buyCoverWithRi.js b/test/unit/Cover/buyCoverWithRi.js new file mode 100644 index 0000000000..4a03fa1e1d --- /dev/null +++ b/test/unit/Cover/buyCoverWithRi.js @@ -0,0 +1,100 @@ +const { ethers, nexus } = require('hardhat'); +const { expect } = require('chai'); +const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); + +const { setup } = require('./setup'); + +const { signRiQuote } = nexus.signing; +const { parseEther, ZeroAddress } = ethers; + +const coverFixture = { + productId: 0n, + coverAsset: 0n, // ETH + poolId: 1n, + segmentId: 0n, + period: 3600n * 24n * 30n, // 30 days + amount: parseEther('1000'), + targetPriceRatio: 260n, + priceDenominator: 10000n, + activeCover: parseEther('8000'), + capacity: parseEther('10000'), + gracePeriod: 120 * 24 * 3600, // 120 days +}; + +const riCoverFixture = { + riAmount: parseEther('2000'), + riPremium: parseEther('20'), // 1% +}; + +const poolAllocationRequest = [{ poolId: 1, coverAmountInAsset: coverFixture.amount }]; + +describe('buyCoverWithRi', function () { + it('should purchase new cover with ri', async function () { + const fixture = await loadFixture(setup); + const { cover, pool, riSigner, riPremiumDst, riProviderId } = fixture; + const [coverBuyer] = fixture.accounts.members; + + const { productId, coverAsset, amount, period, targetPriceRatio, priceDenominator, gracePeriod } = coverFixture; + const { riAmount, riPremium } = riCoverFixture; + + const nativeCoverPremium = (amount * targetPriceRatio * period) / (priceDenominator * 365n * 24n * 3600n); + const totalPremium = nativeCoverPremium + riPremium; + + const coverParams = { + coverId: 0, + owner: coverBuyer, + productId, + coverAsset, + amount, + period, + maxPremiumInAsset: totalPremium, + paymentAsset: coverAsset, + commissionRatio: parseEther('0'), + commissionDestination: ZeroAddress, + ipfsData: '', + }; + + const riQuote = { + coverId: 0, + productId, + providerId: riProviderId, + amount: riAmount, + premium: riPremium, + period, + coverAsset, + nonce: 0, + }; + + const riRequest = { + providerId: riProviderId, + amount: riAmount, + premium: riPremium, + signature: await signRiQuote(riSigner, cover, riQuote), + }; + + const coverContractBalanceBefore = await ethers.provider.getBalance(cover.target); + const poolEthBalanceBefore = await ethers.provider.getBalance(pool.target); + const riPremiumDstEthBalanceBefore = await ethers.provider.getBalance(riPremiumDst.address); + + await cover + .connect(coverBuyer) + .buyCoverWithRi(coverParams, poolAllocationRequest, riRequest, { value: totalPremium }); + + expect(await ethers.provider.getBalance(cover.target)).to.be.equal(coverContractBalanceBefore); + expect(await ethers.provider.getBalance(pool.target)).to.equal(poolEthBalanceBefore + nativeCoverPremium); + expect(await ethers.provider.getBalance(riPremiumDst.address)).to.equal(riPremiumDstEthBalanceBefore + riPremium); + + const coverId = await cover.getCoverDataCount(); + + const storedCoverData = await cover.getCoverData(coverId); + expect(storedCoverData.productId).to.equal(productId); + expect(storedCoverData.coverAsset).to.equal(coverAsset); + expect(storedCoverData.gracePeriod).to.equal(gracePeriod); + expect(storedCoverData.period).to.equal(period); + expect(storedCoverData.amount).to.equal(amount); + + const riData = await cover.getCoverRi(coverId); + expect(riData.providerId).to.equal(riProviderId); + expect(riData.amount).to.equal(riAmount); + }); +}); diff --git a/test/unit/Cover/constructor.js b/test/unit/Cover/constructor.js index ad677cc25a..ef4b83d4fb 100644 --- a/test/unit/Cover/constructor.js +++ b/test/unit/Cover/constructor.js @@ -1,19 +1,46 @@ const { expect } = require('chai'); -const { ethers } = require('hardhat'); +const { ethers, nexus } = require('hardhat'); + +const { ContractIndexes } = nexus.constants; +const { keccak256, toUtf8Bytes, concat, zeroPadValue } = ethers; describe('constructor', function () { it('should set variables correctly', async function () { const coverNFT = '0x0000000000000000000000000000000000000001'; - const stakingNFT = '0x0000000000000000000000000000000000000002'; - const stakingPoolFactory = '0x0000000000000000000000000000000000000003'; - const stakingPoolImplementation = '0x0000000000000000000000000000000000000004'; + const pool = '0x0000000000000000000000000000000000000002'; + const stakingNFT = '0x0000000000000000000000000000000000000003'; + const stakingPoolFactory = '0x0000000000000000000000000000000000000004'; + const stakingPoolImplementation = '0x0000000000000000000000000000000000000005'; + const tokenController = '0x0000000000000000000000000000000000000006'; + const verifyingContract = '0x0000000000000000000000000000000000000007'; + + const registry = await ethers.deployContract('RegistryMock'); + await registry.addContract(ContractIndexes.C_COVER_NFT, coverNFT, true); + await registry.addContract(ContractIndexes.C_POOL, pool, true); + await registry.addContract(ContractIndexes.C_STAKING_NFT, stakingNFT, true); + await registry.addContract(ContractIndexes.C_STAKING_POOL_FACTORY, stakingPoolFactory, true); + await registry.addContract(ContractIndexes.C_TOKEN_CONTROLLER, tokenController, true); const Cover = await ethers.getContractFactory('Cover'); - const cover = await Cover.deploy(coverNFT, stakingNFT, stakingPoolFactory, stakingPoolImplementation); + const cover = await Cover.deploy(registry, stakingPoolImplementation, verifyingContract); expect(await cover.coverNFT()).to.equal(coverNFT); + expect(await cover.pool()).to.equal(pool); expect(await cover.stakingNFT()).to.equal(stakingNFT); expect(await cover.stakingPoolFactory()).to.equal(stakingPoolFactory); expect(await cover.stakingPoolImplementation()).to.equal(stakingPoolImplementation); + expect(await cover.tokenController()).to.equal(tokenController); + + const chainId = await ethers.provider.send('eth_chainId'); + const hashedName = keccak256(toUtf8Bytes('NexusMutualCover')); + const hashedVersion = keccak256(toUtf8Bytes('1.0.0')); + const typeHash = keccak256( + toUtf8Bytes('EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)'), + ); + const domainSeparator = keccak256( + concat([typeHash, hashedName, hashedVersion, zeroPadValue(chainId, 32), zeroPadValue(verifyingContract, 32)]), + ); + + expect(await cover.DOMAIN_SEPARATOR()).to.equal(domainSeparator); }); }); diff --git a/test/unit/Cover/setup.js b/test/unit/Cover/setup.js index 0e339d97d6..09960a80d0 100644 --- a/test/unit/Cover/setup.js +++ b/test/unit/Cover/setup.js @@ -1,10 +1,11 @@ const { ethers, nexus } = require('hardhat'); -const { setBalance } = require('@nomicfoundation/hardhat-network-helpers'); -const { expect } = require('chai'); +const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); + const { getAccounts } = require('../../utils/accounts'); +const { init } = require('../../init'); -const { parseEther, deployContract, getCreateAddress, MaxUint256 } = ethers; -const { PoolAsset, Role } = nexus.constants; +const { MaxUint256, deployContract, parseEther } = ethers; +const { ContractIndexes, PoolAsset, Role } = nexus.constants; const { hex } = nexus.helpers; const ASSETS = { @@ -12,6 +13,7 @@ const ASSETS = { USDC: 1, cbBTC: 2, }; + const DEFAULT_POOL_FEE = '5'; const DEFAULT_PRODUCTS = [ { @@ -93,92 +95,124 @@ const COVER_BUY_FIXTURE = { capacityFactor: '10000', }; -const getDeployAddressAfter = async (account, txCount) => { - const from = account.address; - const nonce = (await account.getNonce()) + txCount; - return getCreateAddress({ from, nonce }); -}; - async function setup() { + await loadFixture(init); const accounts = await getAccounts(); - const master = await deployContract('MasterMock'); - const memberRoles = await deployContract('MemberRolesMock'); - const nxm = await deployContract('NXMTokenMock'); - const tokenController = await deployContract('TokenControllerMock', [nxm]); - - await nxm.setOperator(tokenController.target); - - const stakingPoolImplementation = await deployContract('COMockStakingPool'); + const [governor] = accounts.governanceContracts; + const [riSigner, riPremiumDst] = accounts.generalPurpose; + + // deploy proxy contracts + const coverProxy = await deployContract('UpgradeableProxy'); + const coverProductsProxy = await deployContract('UpgradeableProxy'); + const stakingProductsProxy = await deployContract('UpgradeableProxy'); + const tokenControllerProxy = await deployContract('UpgradeableProxy'); + const poolProxy = await deployContract('UpgradeableProxy'); + + // deploy immutable contracts + const stakingPoolFactory = await deployContract('StakingPoolFactory', [stakingProductsProxy]); // SP is the operator const coverNFT = await deployContract('COMockCoverNFT'); const stakingNFT = await deployContract('COMockStakingNFT'); - - const { defaultSender } = accounts; - const expectedStakingProductsAddress = await getDeployAddressAfter(defaultSender, 3); - - const stakingPoolFactory = await deployContract('StakingPoolFactory', [expectedStakingProductsAddress]); - - const cover = await deployContract('Cover', [coverNFT, stakingNFT, stakingPoolFactory, stakingPoolImplementation]); - - const coverProducts = await ethers.deployContract('CoverProducts'); - - const stakingProducts = await ethers.deployContract('COMockStakingProducts', [ - cover, + const nxm = await deployContract('NXMTokenMock'); + await nxm.setOperator(tokenControllerProxy); + + const registry = await deployContract('RegistryMock'); + await registry.addContract(ContractIndexes.C_REGISTRY, registry, false); + await registry.addContract(ContractIndexes.C_GOVERNOR, governor, false); + + // add immutables + await registry.addContract(ContractIndexes.C_STAKING_POOL_FACTORY, stakingPoolFactory, false); + await registry.addContract(ContractIndexes.C_COVER_NFT, coverNFT, false); + await registry.addContract(ContractIndexes.C_STAKING_NFT, stakingNFT, false); + await registry.addContract(ContractIndexes.C_TOKEN, nxm, false); + + // add proxies + await registry.addContract(ContractIndexes.C_COVER, coverProxy, true); + await registry.addContract(ContractIndexes.C_COVER_PRODUCTS, coverProductsProxy, true); + await registry.addContract(ContractIndexes.C_STAKING_PRODUCTS, stakingProductsProxy, true); + await registry.addContract(ContractIndexes.C_TOKEN_CONTROLLER, tokenControllerProxy, true); + await registry.addContract(ContractIndexes.C_POOL, poolProxy, true); + + // deploy implementations + const stakingPoolImplementation = await deployContract('COMockStakingPool'); + const coverImplementation = await deployContract('Cover', [registry, stakingPoolImplementation, coverProxy]); + const tokenControllerImplementation = await deployContract('TokenControllerMock', [nxm]); + const coverProductsImplementation = await ethers.deployContract('CoverProducts'); + const stakingProductsImplementation = await ethers.deployContract('COMockStakingProducts', [ + coverProxy, stakingPoolFactory, - tokenController, - coverProducts, + tokenControllerProxy, + coverProductsProxy, ]); - expect(expectedStakingProductsAddress).to.equal(stakingProducts.target); + const poolImplementation = await deployContract('PoolMock'); + + // upgrade proxies + await coverProxy.upgradeTo(coverImplementation); + await tokenControllerProxy.upgradeTo(tokenControllerImplementation); + await coverProductsProxy.upgradeTo(coverProductsImplementation); + await stakingProductsProxy.upgradeTo(stakingProductsImplementation); + await poolProxy.upgradeTo(poolImplementation); + + // get contract instances + const cover = await ethers.getContractAt('Cover', coverProxy); + const tokenController = await ethers.getContractAt('TokenControllerMock', tokenControllerProxy); + const coverProducts = await ethers.getContractAt('CoverProducts', coverProductsProxy); + const stakingProducts = await ethers.getContractAt('COMockStakingProducts', stakingProductsProxy); + const pool = await ethers.getContractAt('PoolMock', poolProxy); const usdc = await deployContract('ERC20CustomDecimalsMock', [6]); const cbBTC = await deployContract('ERC20CustomDecimalsMock', [8]); - const pool = await deployContract('PoolMock'); + const riProviderId = 0n; + await cover.connect(governor).setRiSigner(riSigner); + await cover.connect(governor).setRiConfig(riProviderId, riPremiumDst); + await pool.setAssets([ - { assetAddress: cbBTC.target, isCoverAsset: true, isAbandoned: false }, + { assetAddress: '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee', isCoverAsset: true, isAbandoned: false }, { assetAddress: usdc.target, isCoverAsset: true, isAbandoned: false }, + { assetAddress: cbBTC.target, isCoverAsset: true, isAbandoned: false }, ]); await pool.setTokenPrice('0', parseEther('1')); await pool.setTokenPrice('1', parseEther('1')); await pool.setTokenPrice('2', parseEther('1')); + // legacy contracts + const memberRoles = await deployContract('MemberRolesMock'); + const master = await deployContract('MasterMock'); + await master.setTokenAddress(nxm); await master.setLatestAddress(hex('P1'), pool); - await master.setLatestAddress(hex('MR'), memberRoles); await master.setLatestAddress(hex('CO'), cover); await master.setLatestAddress(hex('TC'), tokenController); await master.setLatestAddress(hex('SP'), stakingProducts); await master.setLatestAddress(hex('CP'), coverProducts); - - const pooledStakingSigner = accounts.members[4]; - await master.setLatestAddress(hex('PS'), pooledStakingSigner); + await master.setLatestAddress(hex('MR'), memberRoles); for (const member of accounts.members) { - await master.enrollMember(member.address, Role.Member); - await memberRoles.setRole(member.address, Role.Member); - await setBalance(member.address, parseEther('100')); - await usdc.mint(member.address, parseEther('100000')); + await master.enrollMember(member, Role.Member); + await memberRoles.setRole(member, Role.Member); + await registry.join(member, '0x'); + await usdc.mint(member, parseEther('100000')); await usdc.connect(member).approve(cover, parseEther('100000')); - await cbBTC.mint(member.address, parseEther('100000')); + await cbBTC.mint(member, parseEther('100000')); await cbBTC.connect(member).approve(cover, parseEther('100000')); } for (const advisoryBoardMember of accounts.advisoryBoardMembers) { - await master.enrollMember(advisoryBoardMember.address, Role.AdvisoryBoard); - await memberRoles.setRole(advisoryBoardMember.address, Role.AdvisoryBoard); + await master.enrollMember(advisoryBoardMember, Role.AdvisoryBoard); + await memberRoles.setRole(advisoryBoardMember, Role.AdvisoryBoard); + await registry.join(advisoryBoardMember, '0x'); } for (const internalContract of accounts.internalContracts) { - await master.enrollInternal(internalContract.address); + await master.enrollInternal(internalContract); } - for (const contract of [cover, coverProducts, tokenController]) { - await contract.changeMasterAddress(master); - await contract.changeDependentContractAddress(); - await master.enrollInternal(contract); - } + await coverProducts.changeMasterAddress(master); + await coverProducts.changeDependentContractAddress(); - await master.setEmergencyAdmin(await accounts.emergencyAdmin.getAddress()); + const emergencyAdmin = accounts.emergencyAdmins[0]; + await master.setEmergencyAdmin(emergencyAdmin); // can only set one await coverProducts.connect(accounts.advisoryBoardMembers[0]).setProductTypes([ { @@ -254,6 +288,7 @@ async function setup() { return { accounts, + registry, master, pool, usdc, @@ -271,14 +306,16 @@ async function setup() { stakingPool1, stakingPool2, config: { DEFAULT_MIN_PRICE_RATIO, BUCKET_SIZE, MAX_COMMISSION_RATIO }, + riSigner, + riPremiumDst, + riProviderId, constants: { + PoolAsset, ASSETS, DEFAULT_POOL_FEE, DEFAULT_PRODUCTS, COVER_BUY_FIXTURE, }, - PoolAsset, - pooledStakingSigner, }; } diff --git a/test/unit/Cover/totalActiveCoverInAsset.js b/test/unit/Cover/totalActiveCoverInAsset.js index ecafb96115..a82e191a03 100644 --- a/test/unit/Cover/totalActiveCoverInAsset.js +++ b/test/unit/Cover/totalActiveCoverInAsset.js @@ -1,16 +1,22 @@ -const { ethers } = require('hardhat'); +const { ethers, nexus } = require('hardhat'); const { expect } = require('chai'); -const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); -const { parseEther, ZeroAddress } = ethers; +const { loadFixture, time } = require('@nomicfoundation/hardhat-network-helpers'); -const { increaseTime } = require('../../utils/evm'); const { setup } = require('./setup'); +const { parseEther, ZeroAddress } = ethers; +const { ContractIndexes } = nexus.constants; + async function setupTotalActiveCoverInAsset() { const fixture = await loadFixture(setup); - const { cover, accounts } = fixture; - const coverBuyer = accounts.members[0]; + const { accounts, cover, registry } = fixture; + const { COVER_BUY_FIXTURE } = fixture.constants; + const [coverBuyer] = accounts.members; + const [claims] = accounts.internalContracts; + + await registry.addContract(ContractIndexes.C_CLAIMS, claims, true); + const { amount, targetPriceRatio, period, priceDenominator, productId, coverAsset } = COVER_BUY_FIXTURE; const expectedPremium = (amount * targetPriceRatio * period) / (priceDenominator * 3600n * 24n * 365n); @@ -103,7 +109,7 @@ describe('totalActiveCoverInAsset', function () { const { coverAsset, amount } = COVER_BUY_FIXTURE; const { totalActiveCoverInAsset: totalActiveCoverInAssetBefore } = await cover.activeCover(coverAsset); - await increaseTime(Number(BUCKET_SIZE + COVER_BUY_FIXTURE.period)); + await time.increase(Number(BUCKET_SIZE + COVER_BUY_FIXTURE.period)); await cover.expireCover(coverId); const { timestamp } = await ethers.provider.getBlock('latest'); @@ -123,7 +129,7 @@ describe('totalActiveCoverInAsset', function () { const { amount, period, coverAsset, productId } = COVER_BUY_FIXTURE; // Move forward 1 bucket - await increaseTime(Number(BUCKET_SIZE)); + await time.increase(Number(BUCKET_SIZE)); // Edit cover await cover.connect(member).buyCover( @@ -153,7 +159,7 @@ describe('totalActiveCoverInAsset', function () { { // Move many blocks until next cover is expired - await increaseTime(500 * 24 * 60 * 60); + await time.increase(500 * 24 * 60 * 60); const amount = parseEther('5'); await cover.connect(member).buyCover( @@ -188,24 +194,24 @@ describe('totalActiveCoverInAsset', function () { it('should be able to burn all active cover', async function () { const fixture = await loadFixture(setupTotalActiveCoverInAsset); const { cover, coverId } = fixture; - const [internalContract] = fixture.accounts.internalContracts; + const [claims] = fixture.accounts.internalContracts; const { COVER_BUY_FIXTURE } = fixture.constants; const { coverAsset, amount } = COVER_BUY_FIXTURE; - await cover.connect(internalContract).burnStake(coverId, amount); + await cover.connect(claims).burnStake(coverId, amount); expect(await cover.totalActiveCoverInAsset(coverAsset)).to.be.equal(0); }); it('should decrease active cover by 1 WEI, and not cause rounding issues', async function () { const fixture = await loadFixture(setupTotalActiveCoverInAsset); const { cover, coverId } = fixture; - const [internalContract] = fixture.accounts.internalContracts; + const [claims] = fixture.accounts.internalContracts; const { COVER_BUY_FIXTURE } = fixture.constants; const { coverAsset, amount } = COVER_BUY_FIXTURE; - await cover.connect(internalContract).burnStake(coverId, 1); + await cover.connect(claims).burnStake(coverId, 1); expect(await cover.totalActiveCoverInAsset(coverAsset)).to.be.equal(amount - 1n); }); @@ -215,20 +221,20 @@ describe('totalActiveCoverInAsset', function () { const { BUCKET_SIZE } = fixture.config; const { COVER_BUY_FIXTURE } = fixture.constants; - const [internalContract] = fixture.accounts.internalContracts; + const [claims] = fixture.accounts.internalContracts; const members = fixture.accounts.members; const coverBuyer = members[0]; const { coverAsset, amount, productId, period } = COVER_BUY_FIXTURE; - await cover.connect(internalContract).burnStake(coverId, amount); + await cover.connect(claims).burnStake(coverId, amount); const timeBetweenPurchases = 2 * 24 * 60 * 60; expect(members.length * timeBetweenPurchases < COVER_BUY_FIXTURE.period); // purchase cover, then burn half of the cover and move forward 2 days each iteration for (let i = 1; i < members.length; i++) { - await increaseTime(2 * 24 * 60 * 60); + await time.increase(2 * 24 * 60 * 60); const expectedActiveCover = (amount * BigInt(i)) / 2n; const member = members[i]; @@ -250,12 +256,12 @@ describe('totalActiveCoverInAsset', function () { { value: expectedPremium }, ); // Burn first segment of coverId == i - await cover.connect(internalContract).burnStake(i + 1, amount / 2n); + await cover.connect(claims).burnStake(i + 1, amount / 2n); expect(await cover.totalActiveCoverInAsset(coverAsset)).to.be.equal(expectedActiveCover); } // Move forward cover period + 1 bucket to expire all covers - await increaseTime(Number(COVER_BUY_FIXTURE.period + BUCKET_SIZE)); + await time.increase(Number(COVER_BUY_FIXTURE.period + BUCKET_SIZE)); // New purchase should be the only active cover await cover.connect(coverBuyer).buyCover( diff --git a/test/unit/EnumerableSet/setup.js b/test/unit/EnumerableSet/setup.js index 2017c1766e..9d6f65a5d8 100644 --- a/test/unit/EnumerableSet/setup.js +++ b/test/unit/EnumerableSet/setup.js @@ -1,6 +1,10 @@ const { ethers } = require('hardhat'); +const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); + +const { init } = require('../../init'); async function setup() { + await loadFixture(init); const EnumerableSetMock = await ethers.getContractFactory('EnumerableSetMock'); const enumerableSetMock = await EnumerableSetMock.deploy(); return enumerableSetMock; diff --git a/test/unit/Governor/setup.js b/test/unit/Governor/setup.js index 6ea620792c..645203560e 100644 --- a/test/unit/Governor/setup.js +++ b/test/unit/Governor/setup.js @@ -1,4 +1,7 @@ const { ethers, nexus } = require('hardhat'); +const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); + +const { init } = require('../../init'); const { ContractIndexes } = nexus.constants; const { parseEther } = ethers; @@ -14,6 +17,7 @@ const assignRoles = accounts => ({ }); async function setup() { + await loadFixture(init); const accounts = assignRoles(await ethers.getSigners()); const registry = await ethers.deployContract('GVMockRegistry', []); const tokenController = await ethers.deployContract('GVMockTokenController', []); diff --git a/test/unit/Pool/setup.js b/test/unit/Pool/setup.js index e29853c3eb..6753358ea9 100644 --- a/test/unit/Pool/setup.js +++ b/test/unit/Pool/setup.js @@ -1,7 +1,10 @@ const { ethers, nexus } = require('hardhat'); -const { ContractIndexes } = nexus.constants; +const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); + +const { init } = require('../../init'); const { parseEther } = ethers; +const { ContractIndexes } = nexus.constants; const { ETH } = nexus.constants.Assets; const assignRoles = accounts => ({ @@ -17,6 +20,7 @@ const assignRoles = accounts => ({ }); async function setup() { + await loadFixture(init); const accounts = assignRoles(await ethers.getSigners()); const [governor] = accounts.governor; const [claims] = accounts.claims; diff --git a/test/unit/Registry/advisoryBoard.js b/test/unit/Registry/advisoryBoard.js index 087787cba5..d9da45bd98 100644 --- a/test/unit/Registry/advisoryBoard.js +++ b/test/unit/Registry/advisoryBoard.js @@ -8,7 +8,7 @@ const { const { setup } = require('./setup'); -const { signJoinMessage } = nexus.membership; +const { signJoinMessage } = nexus.signing; const { toBytes2 } = nexus.helpers; const { ZeroAddress } = ethers; const JOINING_FEE = ethers.parseEther('0.002'); diff --git a/test/unit/Registry/join.js b/test/unit/Registry/join.js index e4d779d930..53c50007f7 100644 --- a/test/unit/Registry/join.js +++ b/test/unit/Registry/join.js @@ -4,7 +4,7 @@ const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); const { setup } = require('./setup'); -const { signJoinMessage } = nexus.membership; +const { signJoinMessage } = nexus.signing; const { PauseTypes, ContractIndexes } = nexus.constants; const { ZeroAddress } = ethers; diff --git a/test/unit/Registry/leave.js b/test/unit/Registry/leave.js index c9560676a7..48a3858d97 100644 --- a/test/unit/Registry/leave.js +++ b/test/unit/Registry/leave.js @@ -8,7 +8,7 @@ const { const { setup } = require('./setup'); -const { signJoinMessage } = nexus.membership; +const { signJoinMessage } = nexus.signing; const { PauseTypes } = nexus.constants; const { toBytes2 } = nexus.helpers; const { ZeroAddress } = ethers; diff --git a/test/unit/Registry/setup.js b/test/unit/Registry/setup.js index 2fbea724f0..8d5321c4a0 100644 --- a/test/unit/Registry/setup.js +++ b/test/unit/Registry/setup.js @@ -1,11 +1,18 @@ const { ethers, nexus } = require('hardhat'); -const { impersonateAccount, setNextBlockBaseFeePerGas } = require('@nomicfoundation/hardhat-network-helpers'); +const { + impersonateAccount, + loadFixture, + setNextBlockBaseFeePerGas, +} = require('@nomicfoundation/hardhat-network-helpers'); + +const { init } = require('../../init'); const { ZeroAddress } = ethers; const { toBytes2, numberToBytes32 } = nexus.helpers; const { ContractIndexes } = nexus.constants; const setup = async () => { + await loadFixture(init); // note: kycAuth is the same as defaultSender const signers = await ethers.getSigners(); const [kycAuth, alice, bob, charlie, mallory, ea1, ea2, governor] = signers; diff --git a/test/unit/Registry/switch.js b/test/unit/Registry/switch.js index 63d3b785ee..a2b2d12ef5 100644 --- a/test/unit/Registry/switch.js +++ b/test/unit/Registry/switch.js @@ -8,7 +8,7 @@ const { const { setup } = require('./setup'); -const { signJoinMessage } = nexus.membership; +const { signJoinMessage } = nexus.signing; const { PauseTypes } = nexus.constants; const { toBytes2 } = nexus.helpers; const JOINING_FEE = ethers.parseEther('0.002'); diff --git a/test/unit/SwapOperator/setup.js b/test/unit/SwapOperator/setup.js index 16bd2869d9..33a790f877 100644 --- a/test/unit/SwapOperator/setup.js +++ b/test/unit/SwapOperator/setup.js @@ -1,5 +1,7 @@ const { ethers, nexus } = require('hardhat'); -const { setBalance } = require('@nomicfoundation/hardhat-network-helpers'); +const { loadFixture, setBalance } = require('@nomicfoundation/hardhat-network-helpers'); + +const { init } = require('../../init'); const { parseEther } = ethers; const { Assets, ContractIndexes } = nexus.constants; @@ -11,6 +13,7 @@ const deployERC20Mock = async (name, symbol, decimals) => { }; async function setup() { + await loadFixture(init); const [defaultSender, governor, alice, bob, mallory, swapController /*, safe */] = await ethers.getSigners(); // deploy weth and erc20 mocks diff --git a/test/unit/TokenController/setup.js b/test/unit/TokenController/setup.js index e7638851ad..25916bf96f 100644 --- a/test/unit/TokenController/setup.js +++ b/test/unit/TokenController/setup.js @@ -1,6 +1,9 @@ const { ethers, nexus } = require('hardhat'); +const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); const { parseEther } = ethers; +const { init } = require('../../init'); + const { ContractIndexes } = nexus.constants; const assignRoles = accounts => ({ @@ -18,6 +21,8 @@ const assignRoles = accounts => ({ }); async function setup() { + await loadFixture(init); + const accounts = assignRoles(await ethers.getSigners()); const [governor] = accounts.governor; const [cover] = accounts.cover; diff --git a/test/utils/accounts.js b/test/utils/accounts.js index 0ec8e268b4..2eec85cf7f 100644 --- a/test/utils/accounts.js +++ b/test/utils/accounts.js @@ -9,9 +9,9 @@ const assignRoles = accounts => ({ nonInternalContracts: accounts.slice(20, 25), governanceContracts: accounts.slice(25, 30), stakingPoolManagers: accounts.slice(30, 40), - emergencyAdmin: accounts[40], - generalPurpose: accounts.slice(41), - assessors: accounts.slice(41, 46), + assessors: accounts.slice(40, 45), + emergencyAdmins: accounts.slice(45, 50), + generalPurpose: accounts.slice(50), }); const getAccounts = async () => {