From 80f18d007e1bad19acea560dca38554fda3e94a0 Mon Sep 17 00:00:00 2001 From: Matthias Zimmermann Date: Wed, 18 Dec 2024 23:21:48 +0000 Subject: [PATCH 1/7] initial crop example setup (some tests green) --- contracts/examples/crop/AccountingToken.sol | 26 + contracts/examples/crop/CropPool.sol | 89 +++ .../examples/crop/CropPoolAuthorization.sol | 37 ++ contracts/examples/crop/CropProduct.sol | 595 ++++++++++++++++++ .../crop/CropProductAuthorization.sol | 68 ++ contracts/examples/crop/Location.sol | 50 ++ test/examples/crop/CropBase.t.sol | 254 ++++++++ test/examples/crop/CropProduct.t.sol | 234 +++++++ test/type/Location.t.sol | 67 ++ 9 files changed, 1420 insertions(+) create mode 100644 contracts/examples/crop/AccountingToken.sol create mode 100644 contracts/examples/crop/CropPool.sol create mode 100644 contracts/examples/crop/CropPoolAuthorization.sol create mode 100644 contracts/examples/crop/CropProduct.sol create mode 100644 contracts/examples/crop/CropProductAuthorization.sol create mode 100644 contracts/examples/crop/Location.sol create mode 100644 test/examples/crop/CropBase.t.sol create mode 100644 test/examples/crop/CropProduct.t.sol create mode 100644 test/type/Location.t.sol diff --git a/contracts/examples/crop/AccountingToken.sol b/contracts/examples/crop/AccountingToken.sol new file mode 100644 index 000000000..0db577f35 --- /dev/null +++ b/contracts/examples/crop/AccountingToken.sol @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.20; + +import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; + +/// @dev FireUSD is a stablecoin with 6 decimals and an initial supply of 1 Billion tokens. +contract AccountingToken is ERC20 { + + string public constant NAME = "Local Currency (Accounting Token)"; + string public constant SYMBOL = "LCA"; + uint8 public constant DECIMALS = 6; + uint256 public constant INITIAL_SUPPLY = 10**12 * 10**DECIMALS; // 1'000'000'000'000 + + constructor() + ERC20(NAME, SYMBOL) + { + _mint( + _msgSender(), + INITIAL_SUPPLY + ); + } + + function decimals() public pure override returns(uint8) { + return DECIMALS; + } +} diff --git a/contracts/examples/crop/CropPool.sol b/contracts/examples/crop/CropPool.sol new file mode 100644 index 000000000..d156aa260 --- /dev/null +++ b/contracts/examples/crop/CropPool.sol @@ -0,0 +1,89 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.20; + +import {IERC20Metadata} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; + +import {IAuthorization} from "../../authorization/IAuthorization.sol"; +import {IComponents} from "../../instance/module/IComponents.sol"; + +import {Amount, AmountLib} from "../../type/Amount.sol"; +import {BasicPool} from "../../pool/BasicPool.sol"; +import {FeeLib} from "../../type/Fee.sol"; +import {NftId} from "../../type/NftId.sol"; +import {SecondsLib} from "../../type/Seconds.sol"; +import {UFixedLib} from "../../type/UFixed.sol"; + + +/// @dev CropPool implements the pool for the crop product. +/// Only the pool owner is allowed to create and manage bundles. +contract CropPool is + BasicPool +{ + constructor( + address registry, + NftId productNftId, + string memory componentName, + IAuthorization authorization + ) + { + address initialOwner = msg.sender; + _intialize( + registry, + productNftId, + componentName, + IComponents.PoolInfo({ + maxBalanceAmount: AmountLib.max(), + isInterceptingBundleTransfers: false, + isProcessingConfirmedClaims: false, + isExternallyManaged: false, + isVerifyingApplications: false, + collateralizationLevel: UFixedLib.one(), + retentionLevel: UFixedLib.one() + }), + authorization, + initialOwner); + } + + function _intialize( + address registry, + NftId productNftId, + string memory componentName, + IComponents.PoolInfo memory poolInfo, + IAuthorization authorization, + address initialOwner + ) + internal + initializer + { + _initializeBasicPool( + registry, + productNftId, + componentName, + poolInfo, + authorization, + initialOwner); + } + + function createBundle( + Amount initialAmount + ) + external + virtual + restricted() + onlyOwner() + returns(NftId bundleNftId) + { + address owner = msg.sender; + bundleNftId = _createBundle( + owner, + FeeLib.zero(), + SecondsLib.fromDays(90), + "" // filter + ); + + _stake(bundleNftId, initialAmount); + } + + function approveTokenHandler(IERC20Metadata token, Amount amount) external restricted() onlyOwner() { _approveTokenHandler(token, amount); } + function setWallet(address newWallet) external restricted() onlyOwner() { _setWallet(newWallet); } +} \ No newline at end of file diff --git a/contracts/examples/crop/CropPoolAuthorization.sol b/contracts/examples/crop/CropPoolAuthorization.sol new file mode 100644 index 000000000..e58d4f6fa --- /dev/null +++ b/contracts/examples/crop/CropPoolAuthorization.sol @@ -0,0 +1,37 @@ + + +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.20; + +import {IAccess} from "../../../contracts/authorization/IAccess.sol"; + +import {BasicPoolAuthorization} from "../../pool/BasicPoolAuthorization.sol"; +import {CropPool} from "./CropPool.sol"; +import {PUBLIC_ROLE} from "../../../contracts/type/RoleId.sol"; + +contract CropPoolAuthorization + is BasicPoolAuthorization +{ + + constructor(string memory poolName) + BasicPoolAuthorization(poolName) + {} + + + function _setupTargetAuthorizations() + internal + virtual override + { + super._setupTargetAuthorizations(); + IAccess.FunctionInfo[] storage functions; + + // authorize public role (also protected by onlyOwner) + functions = _authorizeForTarget(getMainTargetName(), PUBLIC_ROLE()); + + // only owner + _authorize(functions, CropPool.createBundle.selector, "createBundle"); + _authorize(functions, CropPool.approveTokenHandler.selector, "approveTokenHandler"); + _authorize(functions, CropPool.setWallet.selector, "setWallet"); + } +} + diff --git a/contracts/examples/crop/CropProduct.sol b/contracts/examples/crop/CropProduct.sol new file mode 100644 index 000000000..751d4e0d0 --- /dev/null +++ b/contracts/examples/crop/CropProduct.sol @@ -0,0 +1,595 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.20; + +import {IERC20Metadata} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; + +import {IAuthorization} from "../../authorization/IAuthorization.sol"; +import {IComponents} from "../../instance/module/IComponents.sol"; + +import {Amount, AmountLib} from "../../type/Amount.sol"; +import {ClaimId} from "../../type/ClaimId.sol"; +import {FeeLib} from "../../type/Fee.sol"; +import {InstanceReader} from "../../instance/InstanceReader.sol"; +import {Location, LocationLib} from "./Location.sol"; +import {NftId, NftIdLib} from "../../type/NftId.sol"; +import {PayoutId} from "../../type/PayoutId.sol"; +import {Product} from "../../product/Product.sol"; +import {ReferralLib} from "../../type/Referral.sol"; +import {RiskId} from "../../type/RiskId.sol"; +import {RequestId} from "../../type/RequestId.sol"; +import {Seconds, SecondsLib} from "../../type/Seconds.sol"; +import {Str} from "../../type/String.sol"; +import {Timestamp, TimestampLib} from "../../type/Timestamp.sol"; +import {UFixed, UFixedLib} from "../../type/UFixed.sol"; + + +/// @dev CropProduct implements the crop insurance product. +contract CropProduct is + Product +{ + + // Custom errors + error ErrorInvalidId(string id); + error ErrorRecordAlreadyExists(string id); + + error ErrorInvalidYear(uint16 year); + error ErrorInvalidSeasonStart(string seasonStart); + error ErrorInvalidSeasonEnd(string seasonEnd); + error ErrorInvalidSeasonDays(uint16 seasonDays); + + error ErrorInvalidSeasonEndAt(Timestamp seasonEndAt); + + error ErrorInvalidSeasonId(string seasonId); + error ErrorInvalidLocation(string locationId); + error ErrorInvalidCrop(string crop); + + error ErrorInvalidPolicyHolder(); + error ErrorInvalidRiskId(RiskId riskId); + error ErrorInvalidActivateAt(Timestamp activateAt); + error ErrorInvalidSumInsured(Amount sumInsuredAmount); + error ErrorInvalidPremium(Amount premiumAmount); + + // solhint-disable var-name-mixedcase + Amount public MIN_PREMIUM; + Amount public MAX_PREMIUM; + Amount public MIN_SUM_INSURED; + Amount public MAX_SUM_INSURED; + uint8 public MAX_POLICIES_TO_PROCESS = 1; + // solhint-enable var-name-mixedcase + + // Crop insurance specifics + uint16 public constant GRACE_PERIOD_DAYS = 60; + + struct Season { + uint16 year; + Str name; + Str seasonStart; // ISO 8601 date + Str seasonEnd; // ISO 8601 date + uint16 seasonDays; + } + + struct CropRisk { + Str seasonId; + Str locationId; + Str crop; + Timestamp seasonEndAt; + UFixed payoutFactor; + } + + // Seasons + mapping (Str seasonId => Season season) internal _season; + Str [] internal _seasons; + + // Locations + mapping(Str locationId => Location location) internal _location; + + // Crop names + mapping(Str cropName => bool isValid) internal _validCrop; + Str [] internal _crops; + + mapping(Str id => RiskId riskId) internal _riskId; + mapping(RiskId riskId => RequestId requestId) internal _requests; + + // GIF V3 specifics + NftId internal _defaultBundleNftId; + NftId internal _oracleNftId; + + + constructor( + address registry, + NftId instanceNftId, + string memory componentName, + IAuthorization authorization + ) + { + address initialOwner = msg.sender; + + _initialize( + registry, + instanceNftId, + componentName, + authorization, + initialOwner); + } + + + //--- external risk relatee functions -----------------------------------// + + function createSeason( + Str seasonId, + uint16 year, + Str name, + Str seasonStart, // ISO 8601 date, eg "2025-02-18" + Str seasonEnd, + uint16 seasonDays + ) + external + restricted() + { + // validate input + if (seasonId.length() == 0) { revert ErrorInvalidId(seasonId.toString()); } + if (_season[seasonId].year > 0) { revert ErrorRecordAlreadyExists(seasonId.toString()); } + if (year < 2023 || year > 2035 ) { revert ErrorInvalidYear(year); } + if (seasonStart.length() != 10 ) { revert ErrorInvalidSeasonStart(seasonStart.toString()); } + if (seasonEnd.length() != 10 ) { revert ErrorInvalidSeasonEnd(seasonEnd.toString()); } + if (seasonDays < 50 || seasonDays > 250 ) { revert ErrorInvalidSeasonDays(seasonDays); } + + _seasons.push(seasonId); + _season[seasonId] = Season({ + year: year, + name: name, + seasonStart: seasonStart, + seasonEnd: seasonEnd, + seasonDays: seasonDays + }); + } + + function createLocation( + Str locationId, + int32 latitude, + int32 longitude + ) + external + restricted() + returns (Location location) + { + // validate input + if (locationId.length() == 0) { revert ErrorInvalidId(locationId.toString()); } + // TODO add function location.isUndefined() + // if (!_location[locationId].isUndefined()) { revert ErrorRecordAlreadyExists(locationId.toString()); } + + location = LocationLib.toLocation(latitude, longitude); + _location[locationId] = location; + } + + function createCrop(Str cropName) external restricted() { + if (cropName.length() == 0) { revert ErrorInvalidId(cropName.toString()); } + if (_validCrop[cropName]) { revert ErrorRecordAlreadyExists(cropName.toString()); } + + _crops.push(cropName); + _validCrop[cropName] = true; + } + + function createRisk( + Str id, + Str seasonId, + Str locationId, + Str crop, + Timestamp seasonEndAt + ) + external + restricted() + returns (RiskId riskId) + { + // validate input + if (id.length() == 0) { revert ErrorInvalidId(id.toString()); } + if (_season[seasonId].year == 0) { revert ErrorInvalidSeasonId(seasonId.toString()); } + // TODO add function location.isUndefined() + // if (_locations[locationId].isUndefined()) { revert ErrorInvalidLocation(locationId.toString()); } + if (!_validCrop[crop]) { revert ErrorInvalidCrop(crop.toString()); } + if (seasonEndAt < TimestampLib.current()) { revert ErrorInvalidSeasonEndAt(seasonEndAt); } + + // create risk, if new + bytes32 riskKey = keccak256(abi.encode(id)); + CropRisk memory cropRisk = CropRisk({ + seasonId: seasonId, + locationId: locationId, + crop: crop, + seasonEndAt: seasonEndAt, + payoutFactor: UFixedLib.zero() + }); + + riskId = _createRisk(riskKey, abi.encode(cropRisk)); + _riskId[id] = riskId; + } + + + function updatePayoutFactor( + RiskId riskId, + UFixed payoutFactor + ) + external + restricted() + { + (bool exists, CropRisk memory cropRisk) = getRisk(riskId); + if (!exists) { revert ErrorInvalidRiskId(riskId); } + + cropRisk.payoutFactor = payoutFactor; + _updateRisk(riskId, abi.encode(cropRisk)); + } + + //--- external policy related functions ---------------------------------// + + /// @dev Creates a policy. + function createPolicy( + address policyHolder, + RiskId riskId, + Timestamp activateAt, + Amount sumInsuredAmount, + Amount premiumAmount + ) + external + virtual + restricted() + returns (NftId policyNftId) + { + // validate input + if (policyHolder == address(0)) { revert ErrorInvalidPolicyHolder(); } + + (bool exists, CropRisk memory cropRisk) = getRisk(riskId); + if (!exists) { revert ErrorInvalidRiskId(riskId); } + + if (activateAt < TimestampLib.current()) { revert ErrorInvalidActivateAt(activateAt); } + if (activateAt > cropRisk.seasonEndAt) { revert ErrorInvalidActivateAt(activateAt); } + if (sumInsuredAmount < MIN_SUM_INSURED || sumInsuredAmount > MAX_SUM_INSURED) { revert ErrorInvalidSumInsured(sumInsuredAmount); } + if (premiumAmount < MIN_PREMIUM || premiumAmount > MAX_PREMIUM) { revert ErrorInvalidPremium(premiumAmount); } + + // calculate policy lifetime + uint96 seasonDays = _season[cropRisk.seasonId].seasonDays; + Seconds lifetime = SecondsLib.toSeconds((seasonDays + GRACE_PERIOD_DAYS) * 24 * 3600); + + // create application + policyNftId = _createApplication( + policyHolder, + riskId, + sumInsuredAmount, + premiumAmount, + lifetime, + _defaultBundleNftId, + ReferralLib.zero(), + ""); // application data + + // underwrite and activate policy + _createPolicy( + policyNftId, + activateAt, + premiumAmount); // max premium amount + + _collectPremium( + policyNftId, + TimestampLib.zero()); // keep activation timestamp + } + + + + /// @dev Manual fallback function for product owner. + function processPayoutsAndClosePolicies( + RiskId riskId, + uint8 maxPoliciesToProcess + ) + external + virtual + restricted() + onlyOwner() + { + _processPayoutsAndClosePolicies( + riskId, + maxPoliciesToProcess); + } + + + //--- owner functions ---------------------------------------------------// + + + function resendResponse(RequestId requestId) + external + virtual + restricted() + { + _resendResponse(requestId); + } + + /// @dev Call after product registration with the instance + /// when the product token/tokenhandler is available + function setConstants( + Amount minPremium, + Amount maxPremium, + Amount minSumInsured, + Amount maxSumInsured, + uint8 maxPoliciesToProcess + ) + external + virtual + restricted() + onlyOwner() + { + MIN_PREMIUM = minPremium; + MAX_PREMIUM = maxPremium; + MIN_SUM_INSURED = minSumInsured; + MAX_SUM_INSURED = maxSumInsured; + MAX_POLICIES_TO_PROCESS = maxPoliciesToProcess; + } + + function setDefaultBundle(NftId bundleNftId) external restricted() onlyOwner() { _defaultBundleNftId = bundleNftId; } + + function approveTokenHandler(IERC20Metadata token, Amount amount) external restricted() onlyOwner() { _approveTokenHandler(token, amount); } + function setLocked(bool locked) external onlyOwner() { _setLocked(locked); } + function setWallet(address newWallet) external restricted() onlyOwner() { _setWallet(newWallet); } + + //--- unpermissioned functions ------------------------------------------// + + function setOracleNftId() + external + { + _oracleNftId = _getInstanceReader().getProductInfo( + getNftId()).oracleNftId[0]; + } + + //--- view functions ----------------------------------------------------// + + function getSeason(Str seasonId) public view returns (Season memory season) { return _season[seasonId]; } + function getLocation(Str locationId) public view returns (Location location) { return _location[locationId]; } + + function seasons() public view returns (Str [] memory) { return _seasons; } + function crops() public view returns (Str [] memory) { return _crops; } + + function getRisk(RiskId riskId) + public + view + returns ( + bool exists, + CropRisk memory cropRisk + ) + { + // check if risk exists + InstanceReader reader = _getInstanceReader(); + exists = reader.isProductRisk(getNftId(), riskId); + + // get risk data if risk exists + if (exists) { + cropRisk = abi.decode( + reader.getRiskInfo(riskId).data, + (CropRisk)); + } + } + + function calculateNetPremium( + Amount, // sumInsuredAmount: not used in this product + RiskId, // riskId: not used in this product + Seconds, // lifetime: not used in this product, a flight is a one time risk + bytes memory applicationData // holds the premium amount the customer is willing to pay + ) + external + virtual override + view + returns (Amount netPremiumAmount) + { + (netPremiumAmount, ) = abi.decode(applicationData, (Amount, Amount[5])); + } + + + function getOracleNftId() public view returns (NftId oracleNftId) { return _oracleNftId; } + + function getRequestForRisk(RiskId riskId) public view returns (RequestId requestId) { return _requests[riskId]; } + + //--- internal functions ------------------------------------------------// + + + // TODO cleanup + // function createPolicy( + // address policyHolder, + // Str flightData, + // Timestamp departureTime, + // string memory departureTimeLocal, + // Timestamp arrivalTime, + // string memory arrivalTimeLocal, + // Amount premiumAmount, + // uint256[6] memory statistics + // ) + // internal + // virtual + // returns ( + // RiskId riskId, + // NftId policyNftId + // ) + // { + + // (riskId, policyNftId) = _prepareApplication( + // policyHolder, + // flightData, + // departureTime, + // departureTimeLocal, + // arrivalTime, + // arrivalTimeLocal, + // premiumAmount, + // statistics); + + // _createPolicy( + // policyNftId, + // TimestampLib.zero(), // do not ativate yet + // premiumAmount); // max premium amount + + // // interactions (token transfer + callback to token holder, if contract) + // _collectPremium( + // policyNftId, + // departureTime); // activate at scheduled departure time of flight + + // // send oracle request for for new risk + // // if (_requests[riskId].eqz()) { + // // _requests[riskId] = _sendRequest( + // // _oracleNftId, + // // abi.encode( + // // FlightOracle.FlightStatusRequest( + // // riskId, + // // flightData, + // // departureTime)), + // // // allow up to 30 days to process the claim + // // arrivalTime.addSeconds(SecondsLib.fromDays(30)), + // // "flightStatusCallback"); + // // } + // } + + + // function _prepareApplication( + // address policyHolder, + // Str flightData, + // Timestamp departureTime, + // string memory departureTimeLocal, + // Timestamp arrivalTime, + // string memory arrivalTimeLocal, + // Amount premiumAmount, + // uint256[6] memory statistics + // ) + // internal + // virtual + // returns ( + // RiskId riskId, + // NftId policyNftId + // ) + // { + // Amount[5] memory payoutAmounts; + // Amount sumInsuredAmount; + + // ( + // riskId, + // payoutAmounts, + // sumInsuredAmount + // ) = _createRiskAndPayoutAmounts( + // flightData, + // departureTime, + // departureTimeLocal, + // arrivalTime, + // arrivalTimeLocal, + // premiumAmount, + // statistics); + + // policyNftId = _createApplication( + // policyHolder, + // riskId, + // sumInsuredAmount, + // premiumAmount, + // SecondsLib.toSeconds(180 * 24 * 3600), // 30 days + // _defaultBundleNftId, + // ReferralLib.zero(), + // abi.encode( + // premiumAmount, + // payoutAmounts)); // application data + // } + + + function _processPayoutsAndClosePolicies( + RiskId riskId, + uint8 maxPoliciesToProcess + ) + internal + virtual + returns ( + bool riskExists, + bool statusAvailable, + uint8 payoutOption + ) + { + // determine numbers of policies to process + InstanceReader reader = _getInstanceReader(); + // (riskExists, statusAvailable, payoutOption) = FlightLib.getPayoutOption(reader, getNftId(), riskId); + + // return with default values if risk does not exist or status is not yet available + if (!riskExists || !statusAvailable) { + return (riskExists, statusAvailable, payoutOption); + } + + uint256 policiesToProcess = reader.policiesForRisk(riskId); + uint256 policiesProcessed = policiesToProcess < maxPoliciesToProcess ? policiesToProcess : maxPoliciesToProcess; + + // assemble array with policies to process + NftId [] memory policies = new NftId[](policiesProcessed); + for (uint256 i = 0; i < policiesProcessed; i++) { + policies[i] = reader.getPolicyForRisk(riskId, i); + } + + // go through policies + for (uint256 i = 0; i < policiesProcessed; i++) { + NftId policyNftId = policies[i]; + Amount payoutAmount = AmountLib.zero(); + + // create claim/payout (if applicable) + _resolvePayout( + policyNftId, + payoutAmount); + + // expire and close policy + _expire(policyNftId, TimestampLib.current()); + _close(policyNftId); + } + } + + + function _resolvePayout( + NftId policyNftId, + Amount payoutAmount + ) + internal + virtual + { + // no action if no payout + if (payoutAmount.eqz()) { + return; + } + + // create confirmed claim + ClaimId claimId = _submitClaim(policyNftId, payoutAmount, ""); + _confirmClaim(policyNftId, claimId, payoutAmount, ""); + + // create and execute payout + PayoutId payoutId = _createPayout(policyNftId, claimId, payoutAmount, ""); + _processPayout(policyNftId, payoutId); + } + + + function _initialize( + address registry, + NftId instanceNftId, + string memory componentName, + IAuthorization authorization, + address initialOwner + ) + internal + initializer() + { + __Product_init( + registry, + instanceNftId, + componentName, + IComponents.ProductInfo({ + isProcessingFundedClaims: false, + isInterceptingPolicyTransfers: false, + hasDistribution: false, + expectedNumberOfOracles: 0, + numberOfOracles: 0, + poolNftId: NftIdLib.zero(), + distributionNftId: NftIdLib.zero(), + oracleNftId: new NftId[](0) + }), + IComponents.FeeInfo({ + productFee: FeeLib.zero(), + processingFee: FeeLib.zero(), + distributionFee: FeeLib.zero(), + minDistributionOwnerFee: FeeLib.zero(), + poolFee: FeeLib.zero(), + stakingFee: FeeLib.zero(), + performanceFee: FeeLib.zero() + }), + authorization, + initialOwner); // number of oracles + } +} \ No newline at end of file diff --git a/contracts/examples/crop/CropProductAuthorization.sol b/contracts/examples/crop/CropProductAuthorization.sol new file mode 100644 index 000000000..4da1c2293 --- /dev/null +++ b/contracts/examples/crop/CropProductAuthorization.sol @@ -0,0 +1,68 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.20; + +import {IAccess} from "../../../contracts/authorization/IAccess.sol"; + +import {AccessAdminLib} from "../../authorization/AccessAdminLib.sol"; +import {BasicProductAuthorization} from "../../product/BasicProductAuthorization.sol"; +import {CropProduct} from "./CropProduct.sol"; +import {RoleId, ADMIN_ROLE, PUBLIC_ROLE} from "../../../contracts/type/RoleId.sol"; + + +contract CropProductAuthorization + is BasicProductAuthorization +{ + + uint64 public constant PRODUCT_OPERATOR_ROLE_IDX = 1; // 1st custom role for flight product + string public constant PRODUCT_OPERATOR_ROLE_NAME = "ProductOperatorRole"; + // solhint-disable-next-line var-name-mixedcase + RoleId public PRODUCT_OPERATOR_ROLE; + + + constructor(string memory productName) + BasicProductAuthorization(productName) + { } + + + function _setupRoles() + internal + override + { + PRODUCT_OPERATOR_ROLE = AccessAdminLib.getCustomRoleId(PRODUCT_OPERATOR_ROLE_IDX); + + _addRole( + PRODUCT_OPERATOR_ROLE, + AccessAdminLib.roleInfo( + ADMIN_ROLE(), + TargetType.Custom, + 1, // max member count special case: instance nft owner is sole role owner + PRODUCT_OPERATOR_ROLE_NAME)); + } + + + function _setupTargetAuthorizations() + internal + virtual override + { + super._setupTargetAuthorizations(); + IAccess.FunctionInfo[] storage functions; + + // authorize product operator role + functions = _authorizeForTarget(getMainTargetName(), PRODUCT_OPERATOR_ROLE); + _authorize(functions, CropProduct.createSeason.selector, "createSeason"); + _authorize(functions, CropProduct.createLocation.selector, "createLocation"); + _authorize(functions, CropProduct.createCrop.selector, "createCrop"); + _authorize(functions, CropProduct.createRisk.selector, "createRisk"); + _authorize(functions, CropProduct.createPolicy.selector, "createPolicy"); + + // authorize public role (additional authz via onlyOwner) + functions = _authorizeForTarget(getMainTargetName(), PUBLIC_ROLE()); + _authorize(functions, CropProduct.processPayoutsAndClosePolicies.selector, "processPayoutsAndClosePolicies"); + _authorize(functions, CropProduct.setDefaultBundle.selector, "setDefaultBundle"); + _authorize(functions, CropProduct.setConstants.selector, "setConstants"); + _authorize(functions, CropProduct.approveTokenHandler.selector, "approveTokenHandler"); + _authorize(functions, CropProduct.setLocked.selector, "setLocked"); + _authorize(functions, CropProduct.setWallet.selector, "setWallet"); + } +} + diff --git a/contracts/examples/crop/Location.sol b/contracts/examples/crop/Location.sol new file mode 100644 index 000000000..8e3d88efe --- /dev/null +++ b/contracts/examples/crop/Location.sol @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.20; + +type Location is uint64; + +using { + LocationLib.latitude, + LocationLib.longitude +} for Location global; + +library LocationLib { + + error ErrorLatitudeInvalid(int32 amount); + error ErrorLongitudeInvalid(int32 amount); + + int32 public constant LATITUDE_MIN = -90000000; + int32 public constant LATITUDE_MAX = 90000000; + int32 public constant LONGITUDE_MIN = -180000000; + int32 public constant LONGITUDE_MAX = 180000000; + + // Constants for bit manipulation + int64 public constant LATITUDE_MASK = int64(0x7FFFFFF); + int64 public constant LONGITUDE_MASK = int64(0xFFFFFFF); + + function zero() public pure returns (Location) { + return Location.wrap(0); + } + + function toLocation(int32 latitude, int32 longitude) public pure returns (Location) { + if (latitude < LATITUDE_MIN || latitude > LATITUDE_MAX) revert ErrorLatitudeInvalid(latitude); + if (longitude < LONGITUDE_MIN || longitude > LONGITUDE_MAX) revert ErrorLongitudeInvalid(longitude); + + uint32 latPacked = uint32(latitude); + uint32 longPacked = uint32(longitude); + // uint64 packed = (uint64(latPacked) << 32) | uint64(longPacked); + return Location.wrap((uint64(latPacked) << 32) | uint64(longPacked)); + } + + function latitude(Location location) public pure returns (int32) { + return int32(uint32(Location.unwrap(location) >> 32)); + } + + function longitude(Location location) public pure returns (int32) { + return int32(uint32(Location.unwrap(location) & 0xFFFFFFFF)); + } + + function decimals() public pure returns (uint8) { + return uint8(6); + } +} \ No newline at end of file diff --git a/test/examples/crop/CropBase.t.sol b/test/examples/crop/CropBase.t.sol new file mode 100644 index 000000000..3e0b78025 --- /dev/null +++ b/test/examples/crop/CropBase.t.sol @@ -0,0 +1,254 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.20; + +import {console} from "../../../lib/forge-std/src/Test.sol"; + +import {IOracle} from "../../../contracts/oracle/IOracle.sol"; +import {IPolicy} from "../../../contracts/instance/module/IPolicy.sol"; + +import {AccountingToken} from "../../../contracts/examples/crop/AccountingToken.sol"; +import {Amount, AmountLib} from "../../../contracts/type/Amount.sol"; +import {FlightOracle} from "../../../contracts/examples/flight/FlightOracle.sol"; +import {FlightOracleAuthorization} from "../../../contracts/examples/flight/FlightOracleAuthorization.sol"; +import {CropPool} from "../../../contracts/examples/crop/CropPool.sol"; +import {CropPoolAuthorization} from "../../../contracts/examples/crop/CropPoolAuthorization.sol"; +import {CropProduct} from "../../../contracts/examples/crop/CropProduct.sol"; +import {CropProductAuthorization} from "../../../contracts/examples/crop/CropProductAuthorization.sol"; +import {GifTest} from "../../base/GifTest.sol"; +import {NftId} from "../../../contracts/type/NftId.sol"; +import {Location} from "../../../contracts/examples/crop/Location.sol"; +import {RequestId} from "../../../contracts/type/RequestId.sol"; +import {RiskId} from "../../../contracts/type/RiskId.sol"; +import {RoleId} from "../../../contracts/type/RoleId.sol"; +import {Seconds, SecondsLib} from "../../../contracts/type/Seconds.sol"; +import {Str, StrLib} from "../../../contracts/type/String.sol"; +import {Timestamp, TimestampLib} from "../../../contracts/type/Timestamp.sol"; +import {UFixedLib} from "../../../contracts/type/UFixed.sol"; +import {VersionPartLib} from "../../../contracts/type/Version.sol"; + + +contract CropBaseTest is GifTest { + + address public cropOwner = makeAddr("cropOwner"); + address public productOperator = makeAddr("productOperator"); + + uint8 public constant MAX_POLICIES_IN_ONE_GO = 2; + + AccountingToken public accountingToken; + FlightOracle public flightOracle; + CropPool public cropPool; + + CropProduct public cropProduct; + + NftId public flightOracleNftId; + NftId public cropPoolNftId; + NftId public cropProductNftId; + + function setUp() public virtual override { + customer = makeAddr("farmer"); + + super.setUp(); + + _deployAccountingToken(); + _deployCropProduct(); + _deployCropPool(); + + // do some initial funding + _initialFundAccounts(); + } + + + function _deployAccountingToken() internal { + // deploy fire token + vm.startPrank(cropOwner); + accountingToken = new AccountingToken(); + vm.stopPrank(); + + // whitelist fire token and make it active for release 3 + vm.startPrank(registryOwner); + tokenRegistry.registerToken(address(accountingToken)); + tokenRegistry.setActiveForVersion( + currentChainId, + address(accountingToken), + VersionPartLib.toVersionPart(3), + true); + vm.stopPrank(); + } + + + function _deployCropProduct() internal { + + vm.startPrank(cropOwner); + CropProductAuthorization productAuthz = new CropProductAuthorization("CropProduct"); + cropProduct = new CropProduct( + address(registry), + instanceNftId, + "CropProduct", + productAuthz + ); + vm.stopPrank(); + + // instance owner registeres fire product with instance (and registry) + vm.startPrank(instanceOwner); + cropProductNftId = instance.registerProduct( + address(cropProduct), + address(accountingToken)); + + // grant statistics provider role to statistics provider + (RoleId productOperatorRoleId, bool exists) = instanceReader.getRoleForName( + productAuthz.PRODUCT_OPERATOR_ROLE_NAME()); + + assertTrue(exists, "role PRODUCT_OPERATOR_ROLE_NAME missing"); + instance.grantRole(productOperatorRoleId, productOperator); + vm.stopPrank(); + + // complete setup + vm.startPrank(cropOwner); + cropProduct.setConstants( + AmountLib.toAmount(10 * 10 ** accountingToken.decimals()), // min premium + AmountLib.toAmount(99 * 10 ** accountingToken.decimals()), // max premium + AmountLib.toAmount(200 * 10 ** accountingToken.decimals()), // min sum insured + AmountLib.toAmount(1000 * 10 ** accountingToken.decimals()), // max sum insured + 1 // max policies to process + ); + vm.stopPrank(); + } + + + function _deployCropPool() internal { + vm.startPrank(cropOwner); + CropPoolAuthorization poolAuthz = new CropPoolAuthorization("CropPool"); + cropPool = new CropPool( + address(registry), + cropProductNftId, + "CropPool", + poolAuthz + ); + vm.stopPrank(); + + cropPoolNftId = _registerComponent( + cropOwner, + cropProduct, + address(cropPool), + "cropPool"); + } + + + function _deployFlightOracle() internal { + // vm.startPrank(cropOwner); + // FlightOracleAuthorization oracleAuthz = new FlightOracleAuthorization("FlightOracle", COMMIT_HASH); + // flightOracle = new FlightOracle( + // address(registry), + // cropProductNftId, + // "FlightOracle", + // oracleAuthz + // ); + // vm.stopPrank(); + + // flightOracleNftId = _registerComponent( + // cropOwner, + // cropProduct, + // address(flightOracle), + // "FlightOracle"); + + // // grant status provider role to status provider + // (RoleId statusProviderRoleId, bool exists) = instanceReader.getRoleForName( + // oracleAuthz.STATUS_PROVIDER_ROLE_NAME()); + + // vm.startPrank(instanceOwner); + // instance.grantRole(statusProviderRoleId, statusProvider); + // vm.stopPrank(); + } + + + function _createInitialBundle() internal returns (NftId bundleNftId) { + vm.startPrank(cropOwner); + Amount investAmount = AmountLib.toAmount(10000000 * 10 ** 6); + accountingToken.approve( + address(cropPool.getTokenHandler()), + investAmount.toInt()); + bundleNftId = cropPool.createBundle(investAmount); + vm.stopPrank(); + } + + + function _initialFundAccounts() internal { + _fundAccount(cropOwner, 100000 * 10 ** accountingToken.decimals()); + _fundAccount(productOperator, 100000 * 10 ** accountingToken.decimals()); + _fundAccount(customer, 10000 * 10 ** accountingToken.decimals()); + } + + + function _fundAccount(address account, uint256 amount) internal { + vm.startPrank(cropOwner); + accountingToken.transfer(account, amount); + vm.stopPrank(); + } + + + // function _printStatusRequest(FlightOracle.FlightStatusRequest memory statusRequest) internal { + // // solhint-disable + // console.log("FlightStatusRequest (requestData)", statusRequest.riskId.toInt()); + // console.log("- riskId", statusRequest.riskId.toInt()); + // console.log("- flightData", statusRequest.flightData.toString()); + // console.log("- departureTime", statusRequest.departureTime.toInt()); + // // solhint-enable + // } + + + // function _printRequest(RequestId requestId, IOracle.RequestInfo memory requestInfo) internal { + // // solhint-disable + // console.log("requestId", requestId.toInt()); + // console.log("- state", instanceReader.getRequestState(requestId).toInt()); + // console.log("- requesterNftId", requestInfo.requesterNftId.toInt()); + // console.log("- oracleNftId", requestInfo.oracleNftId.toInt()); + // console.log("- isCancelled", requestInfo.isCancelled); + // console.log("- respondedAt", requestInfo.respondedAt.toInt()); + // console.log("- expiredAt", requestInfo.expiredAt.toInt()); + // console.log("- callbackMethodName", requestInfo.callbackMethodName); + // console.log("- requestData.length", requestInfo.requestData.length); + // console.log("- responseData.length", requestInfo.responseData.length); + // // solhint-enable + // } + + + function _printRisk(RiskId riskId, CropProduct.CropRisk memory cropRisk) internal { + // solhint-disable + console.log("riskId", riskId.toInt()); + console.log("- seasonId", cropRisk.seasonId.toString()); + console.log("- locationId", cropRisk.locationId.toString()); + console.log("- crop", cropRisk.crop.toString()); + console.log("- seasonEndAt", cropRisk.seasonEndAt.toInt()); + console.log("- payoutFactor (x100)", (cropRisk.payoutFactor * UFixedLib.toUFixed(100)).toInt()); + // solhint-enable + } + + + function _printPolicy(NftId policyNftId, IPolicy.PolicyInfo memory policyInfo) internal { + // solhint-disable + console.log("policy", policyNftId.toInt()); + console.log("- productNftId", policyInfo.productNftId.toInt()); + console.log("- bundleNftId", policyInfo.bundleNftId.toInt()); + console.log("- riskId referralId", policyInfo.riskId.toInt(), policyInfo.referralId.toInt()); + console.log("- activatedAt lifetime", policyInfo.activatedAt.toInt(), policyInfo.lifetime.toInt()); + console.log("- expiredAt closedAt", policyInfo.expiredAt.toInt(), policyInfo.closedAt.toInt()); + console.log("- sumInsuredAmount", policyInfo.sumInsuredAmount.toInt()); + console.log("- premiumAmount", policyInfo.premiumAmount.toInt()); + console.log("- claimsCount claimAmount", policyInfo.claimsCount, policyInfo.claimAmount.toInt()); + console.log("- payoutAmount", policyInfo.payoutAmount.toInt()); + // solhint-enable + } + + + function _printPremium(NftId policyNftId, IPolicy.PremiumInfo memory premiumInfo) internal { + // solhint-disable + console.log("premium", policyNftId.toInt()); + console.log("- premiumAmount", premiumInfo.premiumAmount.toInt()); + console.log("- netPremiumAmount", premiumInfo.netPremiumAmount.toInt()); + console.log("- productFeeAmount", premiumInfo.productFeeAmount.toInt()); + console.log("- distributionFeeAndCommissionAmount", premiumInfo.distributionFeeAndCommissionAmount.toInt()); + console.log("- poolPremiumAndFeeAmount", premiumInfo.poolPremiumAndFeeAmount.toInt()); + console.log("- discountAmount", premiumInfo.discountAmount.toInt()); + // solhint-enable + } +} \ No newline at end of file diff --git a/test/examples/crop/CropProduct.t.sol b/test/examples/crop/CropProduct.t.sol new file mode 100644 index 000000000..62796cbbb --- /dev/null +++ b/test/examples/crop/CropProduct.t.sol @@ -0,0 +1,234 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.20; + +import {console} from "../../../lib/forge-std/src/Test.sol"; + +import {IAccess} from "../../../contracts/authorization/IAccess.sol"; +import {IOracle} from "../../../contracts/oracle/IOracle.sol"; +import {IPolicy} from "../../../contracts/instance/module/IPolicy.sol"; + +import {Amount, AmountLib} from "../../../contracts/type/Amount.sol"; +import {COLLATERALIZED, PAID} from "../../../contracts/type/StateId.sol"; +import {CropBaseTest} from "./CropBase.t.sol"; +import {CropProduct} from "../../../contracts/examples/crop/CropProduct.sol"; +import {Location} from "../../../contracts/examples/crop/Location.sol"; +import {NftId} from "../../../contracts/type/NftId.sol"; +import {RiskId} from "../../../contracts/type/RiskId.sol"; +import {RoleId} from "../../../contracts/type/RoleId.sol"; +import {Seconds, SecondsLib} from "../../../contracts/type/Seconds.sol"; +import {StateId, ACTIVE, FAILED, FULFILLED} from "../../../contracts/type/StateId.sol"; +import {Str, StrLib} from "../../../contracts/type/String.sol"; +import {Timestamp, TimestampLib} from "../../../contracts/type/Timestamp.sol"; + +// solhint-disable func-name-mixedcase +contract CropProductTest is CropBaseTest { + + function setUp() public override { + super.setUp(); + + // create and set bundle for flight product + bundleNftId = _createInitialBundle(); + + vm.prank(cropOwner); + cropProduct.setDefaultBundle(bundleNftId); + } + + + function approveProductTokenHandler() public { + // approve flight product to buy policies + vm.startPrank(customer); + accountingToken.approve( + address(cropProduct.getTokenHandler()), + accountingToken.balanceOf(customer)); + vm.stopPrank(); + } + + + function test_cropProductSetup() public { + // GIVEN - setp from flight base test + approveProductTokenHandler(); + + _printAuthz(instance.getInstanceAdmin(), "instance"); + + // solhint-disable + console.log(""); + console.log("crop product", cropProductNftId.toInt(), address(cropProduct)); + console.log("crop product wallet", cropProduct.getWallet()); + console.log("crop pool token handler", address(cropProduct.getTokenHandler())); + console.log("crop owner", cropOwner); + console.log("customer", customer); + console.log("customer balance [$]", accountingToken.balanceOf(customer) / 10 ** accountingToken.decimals()); + console.log("customer allowance [$] (token handler)", accountingToken.allowance(customer, address(cropProduct.getTokenHandler())) / 10 ** accountingToken.decimals()); + // solhint-enable + + // THEN + assertTrue(accountingToken.allowance(customer, address(cropProduct.getTokenHandler())) > 0, "product allowance zero"); + assertEq(registry.getNftIdForAddress(address(cropProduct)).toInt(), cropProductNftId.toInt(), "unexpected pool nft id"); + assertEq(registry.ownerOf(cropProductNftId), cropOwner, "unexpected product owner"); + assertEq(cropProduct.getWallet(), address(cropProduct.getTokenHandler()), "unexpected product wallet address"); + } + + function test_cropProductCreateSeason() public { + // GIVEN + string memory nanoId = "7Zv4TZoBLxUi"; + Str seasonId = StrLib.toStr(nanoId); + uint16 year = 2025; + Str name = StrLib.toStr("MainSeasons 2025"); + Str seasonStart = StrLib.toStr("2025-03-25"); + Str seasonEnd = StrLib.toStr("2025-06-30"); + uint16 seasonDays = 110; + + assertEq(cropProduct.seasons().length, 0, "season count not zero"); + + // WHEN + vm.startPrank(productOperator); + cropProduct.createSeason(seasonId, year, name, seasonStart, seasonEnd, seasonDays); + vm.stopPrank(); + + // THEN + assertEq(cropProduct.seasons().length, 1, "unexpected season count"); + assertEq(cropProduct.seasons()[0].toString(), nanoId, "unexpected season id"); + + CropProduct.Season memory season = cropProduct.getSeason(seasonId); + assertEq(season.year, year, "unexpected season year"); + assertEq(season.name.toString(), "MainSeasons 2025", "unexpected season name"); + assertEq(season.seasonStart.toString(), "2025-03-25", "unexpected season begin"); + assertEq(season.seasonEnd.toString(), "2025-06-30", "unexpected season end"); + assertEq(season.seasonDays, seasonDays, "unexpected season days"); + } + + function test_cropProductCreateLocation() public { + // GIVEN + string memory nanoId = "kDho7606IRdr"; + Str locationId = StrLib.toStr(nanoId); + int32 latitude = -436500; + int32 longitude = 31678000; + + // WHEN + vm.startPrank(productOperator); + cropProduct.createLocation(locationId, latitude, longitude); + vm.stopPrank(); + + // THEN + Location location = cropProduct.getLocation(locationId); + assertEq(locationId.toString(), nanoId, "unexpected location id"); + assertEq(location.latitude(), latitude, "unexpected location latitude"); + assertEq(location.longitude(), longitude, "unexpected location longitude"); + } + + function test_cropProductCreateCrop() public { + // GIVEN + string memory cropName = "maize"; + + assertEq(cropProduct.crops().length, 0, "crop count not zero"); + + // WHEN + vm.startPrank(productOperator); + cropProduct.createCrop(StrLib.toStr(cropName)); + vm.stopPrank(); + + // THEN + assertEq(cropProduct.crops().length, 1, "unexpected crop count"); + assertEq(cropProduct.crops()[0].toString(), cropName, "unexpected crop name"); + } + + function test_cropProductCreateRisk() public { + // GIVEN + string memory nanoId = "kDho7606IRdr"; + Str riskIdStr = StrLib.toStr(nanoId); + Str seasonId = _createSeason("7Zv4TZoBLxUi"); + Str locationId = _createLocation("kDho7606IRdr"); + Str crop = _createCrop("maize"); + Timestamp seasonEndAt = TimestampLib.current().addSeconds(SecondsLib.toSeconds(120 days)); + + assertEq(instanceReader.risks(cropProductNftId), 0, "risk count not zero"); + + // WHEN + vm.startPrank(productOperator); + RiskId riskId = cropProduct.createRisk(riskIdStr, seasonId, locationId, crop, seasonEndAt); + vm.stopPrank(); + + // THEN + assertEq(instanceReader.risks(cropProductNftId), 1, "unexpected risk count"); + assertTrue(instanceReader.getRiskId(cropProductNftId, 0) == riskId, "unexpected risk id"); + + ( + bool exists, + CropProduct.CropRisk memory risk + ) = cropProduct.getRisk(riskId); + + assertTrue(exists, "risk not found"); + assertEq(risk.seasonId.toString(), seasonId.toString(), "unexpected sesaon id"); + assertEq(risk.locationId.toString(), locationId.toString(), "unexpected location id"); + assertEq(risk.crop.toString(), "maize", "unexpected crop"); + assertTrue(risk.seasonEndAt.gtz(), "unexpected season is 0"); + assertTrue(risk.payoutFactor.eqz(), "payout factor is not 0"); + } + + function test_cropProductCreatePolicy() public { + // GIVEN + string memory nanoId = "kDho7606IRdr"; + RiskId riskId = _createRisk(nanoId); + Timestamp activateAt = TimestampLib.current(); + Amount sumInsuredAmount = AmountLib.toAmount(400 * 10 ** accountingToken.decimals()); + Amount premiumAmount = AmountLib.toAmount(25 * 10 ** accountingToken.decimals()); + + assertEq(instanceReader.policiesForRisk(riskId), 0, "policy count not zero"); + + // WHEN + vm.startPrank(productOperator); + NftId policyNftId = cropProduct.createPolicy( + customer, + riskId, + activateAt, + sumInsuredAmount, + premiumAmount); + vm.stopPrank(); + + // THEN + assertEq(instanceReader.policiesForRisk(riskId), 1, "unexpected policy count"); + assertEq(instanceReader.getPolicyForRisk(riskId, 0).toInt(), policyNftId.toInt(), "unexpected policy nft id"); + } + + function _createSeason(string memory nanoId) internal returns (Str seasonId) { + seasonId = StrLib.toStr(nanoId); + uint16 year = 2025; + Str name = StrLib.toStr("MainSeasons 2025"); + Str seasonStart = StrLib.toStr("2025-03-25"); + Str seasonEnd = StrLib.toStr("2025-06-30"); + uint16 seasonDays = 110; + + vm.startPrank(productOperator); + cropProduct.createSeason(seasonId, year, name, seasonStart, seasonEnd, seasonDays); + vm.stopPrank(); + } + + function _createLocation(string memory nanoId) internal returns (Str locationId) { + locationId = StrLib.toStr(nanoId); + int32 latitude = -436500; + int32 longitude = 31678000; + + vm.startPrank(productOperator); + cropProduct.createLocation(locationId, latitude, longitude); + vm.stopPrank(); + } + + function _createCrop(string memory cropName) internal returns (Str crop) { + crop = StrLib.toStr(cropName); + vm.startPrank(productOperator); + cropProduct.createCrop(crop); + vm.stopPrank(); + } + + function _createRisk(string memory nanoId) internal returns (RiskId riskId) { + Str riskIdStr = StrLib.toStr(nanoId); + Str seasonId = _createSeason("7Zv4TZoBLxUi"); + Str locationId = _createLocation("kDho7606IRdr"); + Str crop = _createCrop("maize"); + Timestamp seasonEndAt = TimestampLib.current().addSeconds(SecondsLib.toSeconds(120 days)); + + vm.startPrank(productOperator); + riskId = cropProduct.createRisk(riskIdStr, seasonId, locationId, crop, seasonEndAt); + vm.stopPrank(); + } +} \ No newline at end of file diff --git a/test/type/Location.t.sol b/test/type/Location.t.sol new file mode 100644 index 000000000..8641ef5d5 --- /dev/null +++ b/test/type/Location.t.sol @@ -0,0 +1,67 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.20; + +import {Test, console} from "../../lib/forge-std/src/Test.sol"; + +import {Location, LocationLib} from "../../contracts/examples/crop/Location.sol"; + +contract LocationTest is Test { + + function test_days() public { + assertEq(uint256(1 minutes), 60, "unexpected seconds for minute"); + assertEq(uint256(1 hours), 3600, "unexpected seconds for hour"); + assertEq(uint256(1 days), 24 * 3600, "unexpected seconds for day"); + } + + function test_locationToZurich() public { + // GIVEN + int32 latitude = 47367394; + int32 longitude = 8542192; + + // WHEN + Location location = LocationLib.toLocation(latitude, longitude); + + // THEN + assertEq(LocationLib.latitude(location), latitude, "unexpected latitude"); + assertEq(LocationLib.longitude(location), longitude, "unexpected longitude"); + } + + function test_locationLatLongMinMax() public { + assertEq(LocationLib.LATITUDE_MIN, -90000000, "unexpected LATITUDE_MIN"); + assertEq(LocationLib.LATITUDE_MAX, 90000000, "unexpected LATITUDE_MAX"); + assertEq(LocationLib.LONGITUDE_MIN, -180000000, "unexpected LONGITUDE_MIN"); + assertEq(LocationLib.LONGITUDE_MAX, 180000000, "unexpected LONGITUDE_MAX"); + + assertEq(LocationLib.LATITUDE_MIN / int32(uint32(10**LocationLib.decimals())), -90, "unexpected LATITUDE_MIN (rounded)"); + assertEq(LocationLib.LATITUDE_MAX / int32(uint32(10**LocationLib.decimals())), 90, "unexpected LATITUDE_MAX (rounded)"); + assertEq(LocationLib.LONGITUDE_MIN / int32(uint32(10**LocationLib.decimals())), -180, "unexpected LONGITUDE_MIN (rounded)"); + assertEq(LocationLib.LONGITUDE_MAX / int32(uint32(10**LocationLib.decimals())), 180, "unexpected LONGITUDE_MAX (rounded)"); + } + + function test_locationZero() public { + Location locationZero = LocationLib.toLocation(0, 0); + assertEq(LocationLib.latitude(locationZero), 0, "unexpected latitude"); + assertEq(LocationLib.longitude(locationZero), 0, "unexpected longitude"); + + assertEq(LocationLib.latitude(LocationLib.zero()), 0, "unexpected latitude (zero)"); + assertEq(LocationLib.longitude(LocationLib.zero()), 0, "unexpected longitude (zero)"); + } + + function test_locationToLatLongMaxMin() public { + // GIVEN + int32 latitudeMin = LocationLib.LATITUDE_MIN; + int32 latitudeMax = LocationLib.LATITUDE_MAX; + int32 longitudeMin = LocationLib.LONGITUDE_MIN; + int32 longitudeMax = LocationLib.LONGITUDE_MAX; + + // WHEN + Location locationMin = LocationLib.toLocation(latitudeMin, longitudeMin); + Location locationMax = LocationLib.toLocation(latitudeMax, longitudeMax); + + // THEN + assertEq(LocationLib.latitude(locationMin), latitudeMin, "unexpected latitude min"); + assertEq(LocationLib.longitude(locationMin), longitudeMin, "unexpected longitude min"); + assertEq(LocationLib.latitude(locationMax), latitudeMax, "unexpected latitude max"); + assertEq(LocationLib.longitude(locationMax), longitudeMax, "unexpected longitude max"); + } +} \ No newline at end of file From 326cd5635b914ebed297e76ad6dd59ace50c42a0 Mon Sep 17 00:00:00 2001 From: Matthias Zimmermann Date: Thu, 19 Dec 2024 17:29:16 +0000 Subject: [PATCH 2/7] add policy creation and payout testing --- contracts/examples/crop/CropProduct.sol | 203 ++++++------------ .../crop/CropProductAuthorization.sol | 3 +- test/examples/crop/CropBase.t.sol | 34 ++- test/examples/crop/CropProduct.t.sol | 130 ++++++++++- 4 files changed, 221 insertions(+), 149 deletions(-) diff --git a/contracts/examples/crop/CropProduct.sol b/contracts/examples/crop/CropProduct.sol index 751d4e0d0..a95a19f43 100644 --- a/contracts/examples/crop/CropProduct.sol +++ b/contracts/examples/crop/CropProduct.sol @@ -5,6 +5,7 @@ import {IERC20Metadata} from "@openzeppelin/contracts/token/ERC20/extensions/IER import {IAuthorization} from "../../authorization/IAuthorization.sol"; import {IComponents} from "../../instance/module/IComponents.sol"; +import {IPolicy} from "../../instance/module/IPolicy.sol"; import {Amount, AmountLib} from "../../type/Amount.sol"; import {ClaimId} from "../../type/ClaimId.sol"; @@ -49,6 +50,8 @@ contract CropProduct is error ErrorInvalidSumInsured(Amount sumInsuredAmount); error ErrorInvalidPremium(Amount premiumAmount); + error ErrorUndefinedRiskPayout(RiskId riskId); + // solhint-disable var-name-mixedcase Amount public MIN_PREMIUM; Amount public MAX_PREMIUM; @@ -74,6 +77,7 @@ contract CropProduct is Str crop; Timestamp seasonEndAt; UFixed payoutFactor; + bool payoutDefined; } // Seasons @@ -196,7 +200,8 @@ contract CropProduct is locationId: locationId, crop: crop, seasonEndAt: seasonEndAt, - payoutFactor: UFixedLib.zero() + payoutFactor: UFixedLib.zero(), + payoutDefined: false }); riskId = _createRisk(riskKey, abi.encode(cropRisk)); @@ -215,6 +220,8 @@ contract CropProduct is if (!exists) { revert ErrorInvalidRiskId(riskId); } cropRisk.payoutFactor = payoutFactor; + cropRisk.payoutDefined = true; + _updateRisk(riskId, abi.encode(cropRisk)); } @@ -257,7 +264,7 @@ contract CropProduct is lifetime, _defaultBundleNftId, ReferralLib.zero(), - ""); // application data + abi.encode(premiumAmount)); // application data: premium amount, see calculateNetPremium // underwrite and activate policy _createPolicy( @@ -271,33 +278,24 @@ contract CropProduct is } - - /// @dev Manual fallback function for product owner. - function processPayoutsAndClosePolicies( - RiskId riskId, - uint8 maxPoliciesToProcess - ) - external - virtual - restricted() - onlyOwner() + function processPolicy(NftId policyNftId) + external + restricted() { - _processPayoutsAndClosePolicies( - riskId, - maxPoliciesToProcess); + _processAndClosePolicy(policyNftId); } //--- owner functions ---------------------------------------------------// - - function resendResponse(RequestId requestId) - external - virtual - restricted() - { - _resendResponse(requestId); - } + // TODO cleanup + // function resendResponse(RequestId requestId) + // external + // virtual + // restricted() + // { + // _resendResponse(requestId); + // } /// @dev Call after product registration with the instance /// when the product token/tokenhandler is available @@ -374,117 +372,34 @@ contract CropProduct is view returns (Amount netPremiumAmount) { - (netPremiumAmount, ) = abi.decode(applicationData, (Amount, Amount[5])); + (netPremiumAmount) = abi.decode(applicationData, (Amount)); } - function getOracleNftId() public view returns (NftId oracleNftId) { return _oracleNftId; } - - function getRequestForRisk(RiskId riskId) public view returns (RequestId requestId) { return _requests[riskId]; } + function calculatePayout( + IPolicy.PolicyInfo memory info, + CropRisk memory cropRisk + ) + public + pure + returns (Amount payoutAmount) + { + Amount sumInsuredAmount = info.sumInsuredAmount; + UFixed payoutFactor = cropRisk.payoutFactor; - //--- internal functions ------------------------------------------------// + if (payoutFactor.eqz()) { + return AmountLib.zero(); + } + return sumInsuredAmount.multiplyWith(payoutFactor); + } - // TODO cleanup - // function createPolicy( - // address policyHolder, - // Str flightData, - // Timestamp departureTime, - // string memory departureTimeLocal, - // Timestamp arrivalTime, - // string memory arrivalTimeLocal, - // Amount premiumAmount, - // uint256[6] memory statistics - // ) - // internal - // virtual - // returns ( - // RiskId riskId, - // NftId policyNftId - // ) - // { - // (riskId, policyNftId) = _prepareApplication( - // policyHolder, - // flightData, - // departureTime, - // departureTimeLocal, - // arrivalTime, - // arrivalTimeLocal, - // premiumAmount, - // statistics); - - // _createPolicy( - // policyNftId, - // TimestampLib.zero(), // do not ativate yet - // premiumAmount); // max premium amount - - // // interactions (token transfer + callback to token holder, if contract) - // _collectPremium( - // policyNftId, - // departureTime); // activate at scheduled departure time of flight - - // // send oracle request for for new risk - // // if (_requests[riskId].eqz()) { - // // _requests[riskId] = _sendRequest( - // // _oracleNftId, - // // abi.encode( - // // FlightOracle.FlightStatusRequest( - // // riskId, - // // flightData, - // // departureTime)), - // // // allow up to 30 days to process the claim - // // arrivalTime.addSeconds(SecondsLib.fromDays(30)), - // // "flightStatusCallback"); - // // } - // } + function getOracleNftId() public view returns (NftId oracleNftId) { return _oracleNftId; } + function getRequestForRisk(RiskId riskId) public view returns (RequestId requestId) { return _requests[riskId]; } - // function _prepareApplication( - // address policyHolder, - // Str flightData, - // Timestamp departureTime, - // string memory departureTimeLocal, - // Timestamp arrivalTime, - // string memory arrivalTimeLocal, - // Amount premiumAmount, - // uint256[6] memory statistics - // ) - // internal - // virtual - // returns ( - // RiskId riskId, - // NftId policyNftId - // ) - // { - // Amount[5] memory payoutAmounts; - // Amount sumInsuredAmount; - - // ( - // riskId, - // payoutAmounts, - // sumInsuredAmount - // ) = _createRiskAndPayoutAmounts( - // flightData, - // departureTime, - // departureTimeLocal, - // arrivalTime, - // arrivalTimeLocal, - // premiumAmount, - // statistics); - - // policyNftId = _createApplication( - // policyHolder, - // riskId, - // sumInsuredAmount, - // premiumAmount, - // SecondsLib.toSeconds(180 * 24 * 3600), // 30 days - // _defaultBundleNftId, - // ReferralLib.zero(), - // abi.encode( - // premiumAmount, - // payoutAmounts)); // application data - // } + //--- internal functions ------------------------------------------------// function _processPayoutsAndClosePolicies( @@ -501,7 +416,8 @@ contract CropProduct is { // determine numbers of policies to process InstanceReader reader = _getInstanceReader(); - // (riskExists, statusAvailable, payoutOption) = FlightLib.getPayoutOption(reader, getNftId(), riskId); + CropRisk memory cropRisk; + (riskExists, cropRisk) = getRisk(riskId); // return with default values if risk does not exist or status is not yet available if (!riskExists || !statusAvailable) { @@ -520,28 +436,37 @@ contract CropProduct is // go through policies for (uint256 i = 0; i < policiesProcessed; i++) { NftId policyNftId = policies[i]; - Amount payoutAmount = AmountLib.zero(); + _processAndClosePolicy(policyNftId); + } + } - // create claim/payout (if applicable) - _resolvePayout( - policyNftId, - payoutAmount); - // expire and close policy - _expire(policyNftId, TimestampLib.current()); - _close(policyNftId); - } + function _processAndClosePolicy(NftId policyNftId) + internal + virtual + { + IPolicy.PolicyInfo memory info = _getInstanceReader().getPolicyInfo(policyNftId); + (bool exists, CropRisk memory cropRisk) = getRisk(info.riskId); + if (!exists) { revert ErrorInvalidRiskId(info.riskId); } + if (!cropRisk.payoutDefined) { revert ErrorUndefinedRiskPayout(info.riskId); } + + // create claim/payout (if applicable), then expire and close policy + _handlePayout(policyNftId, info, cropRisk); + _expire(policyNftId, TimestampLib.current()); + _close(policyNftId); } - function _resolvePayout( + function _handlePayout( NftId policyNftId, - Amount payoutAmount + IPolicy.PolicyInfo memory info, + CropRisk memory cropRisk ) internal - virtual { - // no action if no payout + Amount payoutAmount = calculatePayout(info, cropRisk); + + // if payout amount > 0: create and process claim and payout if (payoutAmount.eqz()) { return; } diff --git a/contracts/examples/crop/CropProductAuthorization.sol b/contracts/examples/crop/CropProductAuthorization.sol index 4da1c2293..6950362cc 100644 --- a/contracts/examples/crop/CropProductAuthorization.sol +++ b/contracts/examples/crop/CropProductAuthorization.sol @@ -54,10 +54,11 @@ contract CropProductAuthorization _authorize(functions, CropProduct.createCrop.selector, "createCrop"); _authorize(functions, CropProduct.createRisk.selector, "createRisk"); _authorize(functions, CropProduct.createPolicy.selector, "createPolicy"); + _authorize(functions, CropProduct.updatePayoutFactor.selector, "updatePayoutFactor"); + _authorize(functions, CropProduct.processPolicy.selector, "processPolicy"); // authorize public role (additional authz via onlyOwner) functions = _authorizeForTarget(getMainTargetName(), PUBLIC_ROLE()); - _authorize(functions, CropProduct.processPayoutsAndClosePolicies.selector, "processPayoutsAndClosePolicies"); _authorize(functions, CropProduct.setDefaultBundle.selector, "setDefaultBundle"); _authorize(functions, CropProduct.setConstants.selector, "setConstants"); _authorize(functions, CropProduct.approveTokenHandler.selector, "approveTokenHandler"); diff --git a/test/examples/crop/CropBase.t.sol b/test/examples/crop/CropBase.t.sol index 3e0b78025..4067256e7 100644 --- a/test/examples/crop/CropBase.t.sol +++ b/test/examples/crop/CropBase.t.sol @@ -3,7 +3,7 @@ pragma solidity ^0.8.20; import {console} from "../../../lib/forge-std/src/Test.sol"; -import {IOracle} from "../../../contracts/oracle/IOracle.sol"; +import {IBaseStore} from "../../../contracts/instance/IBaseStore.sol"; import {IPolicy} from "../../../contracts/instance/module/IPolicy.sol"; import {AccountingToken} from "../../../contracts/examples/crop/AccountingToken.sol"; @@ -176,6 +176,12 @@ contract CropBaseTest is GifTest { _fundAccount(cropOwner, 100000 * 10 ** accountingToken.decimals()); _fundAccount(productOperator, 100000 * 10 ** accountingToken.decimals()); _fundAccount(customer, 10000 * 10 ** accountingToken.decimals()); + + vm.startPrank(customer); + accountingToken.approve( + address(cropProduct.getTokenHandler()), + accountingToken.balanceOf(customer)); + vm.stopPrank(); } @@ -219,7 +225,20 @@ contract CropBaseTest is GifTest { console.log("- locationId", cropRisk.locationId.toString()); console.log("- crop", cropRisk.crop.toString()); console.log("- seasonEndAt", cropRisk.seasonEndAt.toInt()); - console.log("- payoutFactor (x100)", (cropRisk.payoutFactor * UFixedLib.toUFixed(100)).toInt()); + console.log("- payoutFactor (%)", (cropRisk.payoutFactor * UFixedLib.toUFixed(100)).toInt()); + console.log("- payoutDefined", cropRisk.payoutDefined); + + CropProduct.Season memory season = cropProduct.getSeason(cropRisk.seasonId); + console.log("- season", season.year); + console.log(" - season name", season.name.toString()); + console.log(" - season start", season.seasonStart.toString()); + console.log(" - season end", season.seasonEnd.toString()); + console.log(" - season days", season.seasonDays); + + Location location = cropProduct.getLocation(cropRisk.locationId); + console.log("- location"); + console.log(" - latitude", location.latitude()); + console.log(" - longitude", location.longitude()); // solhint-enable } @@ -230,6 +249,7 @@ contract CropBaseTest is GifTest { console.log("- productNftId", policyInfo.productNftId.toInt()); console.log("- bundleNftId", policyInfo.bundleNftId.toInt()); console.log("- riskId referralId", policyInfo.riskId.toInt(), policyInfo.referralId.toInt()); + console.log("- state", instanceReader.getPolicyState(policyNftId).toInt()); console.log("- activatedAt lifetime", policyInfo.activatedAt.toInt(), policyInfo.lifetime.toInt()); console.log("- expiredAt closedAt", policyInfo.expiredAt.toInt(), policyInfo.closedAt.toInt()); console.log("- sumInsuredAmount", policyInfo.sumInsuredAmount.toInt()); @@ -240,6 +260,16 @@ contract CropBaseTest is GifTest { } + function _printMetadata(IBaseStore.Metadata memory metadata) internal { + // solhint-disable + console.log("metadata"); + console.log("- objectType", metadata.objectType.toInt()); + console.log("- state", metadata.state.toInt()); + console.log("- updatedIn", metadata.updatedIn.toInt()); + // solhint-enable + } + + function _printPremium(NftId policyNftId, IPolicy.PremiumInfo memory premiumInfo) internal { // solhint-disable console.log("premium", policyNftId.toInt()); diff --git a/test/examples/crop/CropProduct.t.sol b/test/examples/crop/CropProduct.t.sol index 62796cbbb..89908e440 100644 --- a/test/examples/crop/CropProduct.t.sol +++ b/test/examples/crop/CropProduct.t.sol @@ -1,24 +1,26 @@ // SPDX-License-Identifier: Apache-2.0 pragma solidity ^0.8.20; -import {console} from "../../../lib/forge-std/src/Test.sol"; +import {console, console2} from "../../../lib/forge-std/src/Test.sol"; -import {IAccess} from "../../../contracts/authorization/IAccess.sol"; -import {IOracle} from "../../../contracts/oracle/IOracle.sol"; +import {IBaseStore} from "../../../contracts/instance/IBaseStore.sol"; import {IPolicy} from "../../../contracts/instance/module/IPolicy.sol"; import {Amount, AmountLib} from "../../../contracts/type/Amount.sol"; -import {COLLATERALIZED, PAID} from "../../../contracts/type/StateId.sol"; +import {ClaimId} from "../../../contracts/type/ClaimId.sol"; import {CropBaseTest} from "./CropBase.t.sol"; import {CropProduct} from "../../../contracts/examples/crop/CropProduct.sol"; +import {Key32} from "../../../contracts/type/Key32.sol"; import {Location} from "../../../contracts/examples/crop/Location.sol"; import {NftId} from "../../../contracts/type/NftId.sol"; +import {PayoutId} from "../../../contracts/type/PayoutId.sol"; +import {POLICY} from "../../../contracts/type/ObjectType.sol"; import {RiskId} from "../../../contracts/type/RiskId.sol"; -import {RoleId} from "../../../contracts/type/RoleId.sol"; import {Seconds, SecondsLib} from "../../../contracts/type/Seconds.sol"; -import {StateId, ACTIVE, FAILED, FULFILLED} from "../../../contracts/type/StateId.sol"; +import {StateId, ACTIVE, COLLATERALIZED, PAID, CLOSED} from "../../../contracts/type/StateId.sol"; import {Str, StrLib} from "../../../contracts/type/String.sol"; import {Timestamp, TimestampLib} from "../../../contracts/type/Timestamp.sol"; +import {UFixed, UFixedLib} from "../../../contracts/type/UFixed.sol"; // solhint-disable func-name-mixedcase contract CropProductTest is CropBaseTest { @@ -68,6 +70,7 @@ contract CropProductTest is CropBaseTest { assertEq(cropProduct.getWallet(), address(cropProduct.getTokenHandler()), "unexpected product wallet address"); } + function test_cropProductCreateSeason() public { // GIVEN string memory nanoId = "7Zv4TZoBLxUi"; @@ -97,6 +100,7 @@ contract CropProductTest is CropBaseTest { assertEq(season.seasonDays, seasonDays, "unexpected season days"); } + function test_cropProductCreateLocation() public { // GIVEN string memory nanoId = "kDho7606IRdr"; @@ -116,6 +120,7 @@ contract CropProductTest is CropBaseTest { assertEq(location.longitude(), longitude, "unexpected location longitude"); } + function test_cropProductCreateCrop() public { // GIVEN string memory cropName = "maize"; @@ -132,6 +137,7 @@ contract CropProductTest is CropBaseTest { assertEq(cropProduct.crops()[0].toString(), cropName, "unexpected crop name"); } + function test_cropProductCreateRisk() public { // GIVEN string memory nanoId = "kDho7606IRdr"; @@ -165,7 +171,28 @@ contract CropProductTest is CropBaseTest { assertTrue(risk.payoutFactor.eqz(), "payout factor is not 0"); } - function test_cropProductCreatePolicy() public { + + function test_cropProductRiskPayoutFactorUpdate() public { + // GIVEN + string memory nanoId = "kDho7606IRdr"; + RiskId riskId = _createRisk(nanoId); + UFixed payoutFactor = UFixedLib.toUFixed(35, -2); // 35% + + (bool exists, CropProduct.CropRisk memory riskBefore) = cropProduct.getRisk(riskId); + assertTrue(exists, "risk not found"); + assertEq(riskBefore.payoutFactor.toInt(), 0, "unexpected payout factor"); + + // WHEN + vm.startPrank(productOperator); + cropProduct.updatePayoutFactor(riskId, payoutFactor); + vm.stopPrank(); + + (, CropProduct.CropRisk memory riskAfter) = cropProduct.getRisk(riskId); + _printRisk(riskId, riskAfter); + } + + + function test_cropProductPolicyCreate() public { // GIVEN string memory nanoId = "kDho7606IRdr"; RiskId riskId = _createRisk(nanoId); @@ -188,8 +215,75 @@ contract CropProductTest is CropBaseTest { // THEN assertEq(instanceReader.policiesForRisk(riskId), 1, "unexpected policy count"); assertEq(instanceReader.getPolicyForRisk(riskId, 0).toInt(), policyNftId.toInt(), "unexpected policy nft id"); + + (, CropProduct.CropRisk memory cropRisk) = cropProduct.getRisk(riskId); + _printRisk(riskId, cropRisk); + _printPolicy(policyNftId, instanceReader.getPolicyInfo(policyNftId)); + + // TODO add creation checks for this specific policy + assertEq(instanceReader.getPolicyState(policyNftId).toInt(), COLLATERALIZED().toInt(), "unexpected policy state (not underwritten)"); + IPolicy.PolicyInfo memory info = instanceReader.getPolicyInfo(policyNftId); + assertEq(info.productNftId.toInt(), cropProductNftId.toInt(), "unexpected product nft id"); + assertEq(info.riskId.toInt(), riskId.toInt(), "unexpected policy risk id"); + assertTrue(info.expiredAt.gtz(), "unexpected policy expired at (is 0)"); + } + + + function test_cropProductPolicyClose() public { + // GIVEN + NftId policyNftId = _createPolicy(customer); + + IPolicy.PolicyInfo memory info = instanceReader.getPolicyInfo(policyNftId); + RiskId riskId = info.riskId; + assertEq(instanceReader.policiesForRisk(riskId), 1, "unexpected policy count"); + assertEq(instanceReader.getPolicyForRisk(riskId, 0).toInt(), policyNftId.toInt(), "unexpected policy nft id"); + assertEq(instanceReader.claims(policyNftId), 0, "unexpected claim count (before)"); + + _printPolicy(policyNftId, info); + + uint256 expiryAt = info.expiredAt.toInt(); + vm.warp(expiryAt - 1); + + (, CropProduct.CropRisk memory riskBefore) = cropProduct.getRisk(riskId); + UFixed payoutFactor = UFixedLib.toUFixed(35, -2); // 35% + uint256 customerBalanceBefore = accountingToken.balanceOf(customer); + + // WHEN + vm.startPrank(productOperator); + cropProduct.updatePayoutFactor(riskId, payoutFactor); + cropProduct.processPolicy(policyNftId); + vm.stopPrank(); + + // THEN + info = instanceReader.getPolicyInfo(policyNftId); + _printPolicy(policyNftId, info); + + assertEq(instanceReader.getPolicyState(policyNftId).toInt(), CLOSED().toInt(), "unexpected policy state (not closed)"); + assertEq(instanceReader.policiesForRisk(riskId), 0, "unexpected policy count"); + + // check claim + assertEq(instanceReader.claims(policyNftId), 1, "unexpected claim count (after)"); + ClaimId claimId = instanceReader.getClaimId(0); + assertEq(instanceReader.getClaimState(policyNftId, claimId).toInt(), CLOSED().toInt(), "unexpected claim state (not closed)"); + IPolicy.ClaimInfo memory claimInfo = instanceReader.getClaimInfo(policyNftId, claimId); + assertEq(claimInfo.payoutsCount, 1, "unexpected payouts count"); + assertEq(claimInfo.claimAmount.toInt(), claimInfo.paidAmount.toInt(), "claim and paid amount mismatch"); + Amount payoutAmount = claimInfo.paidAmount; + + // check payout + assertEq(instanceReader.payouts(policyNftId, claimId), 1, "unexpected payout count"); + PayoutId payoutId = instanceReader.getPayoutId(claimId, 0); + assertEq(instanceReader.getPayoutState(policyNftId, payoutId).toInt(), PAID().toInt(), "unexpected payout state (not paid)"); + IPolicy.PayoutInfo memory payoutInfo = instanceReader.getPayoutInfo(policyNftId, payoutId); + assertEq(payoutInfo.claimId.toInt(), claimId.toInt(), "unexpected claim id for payout"); + assertEq(payoutInfo.amount.toInt(), payoutAmount.toInt(), "unexpected payout amount"); + assertTrue(payoutAmount.gtz(), "payout amount is 0"); + + // check token balance after payout + assertEq(accountingToken.balanceOf(customer), customerBalanceBefore + payoutAmount.toInt(), "unexpected customer balance after payout"); } + function _createSeason(string memory nanoId) internal returns (Str seasonId) { seasonId = StrLib.toStr(nanoId); uint16 year = 2025; @@ -203,6 +297,7 @@ contract CropProductTest is CropBaseTest { vm.stopPrank(); } + function _createLocation(string memory nanoId) internal returns (Str locationId) { locationId = StrLib.toStr(nanoId); int32 latitude = -436500; @@ -213,6 +308,7 @@ contract CropProductTest is CropBaseTest { vm.stopPrank(); } + function _createCrop(string memory cropName) internal returns (Str crop) { crop = StrLib.toStr(cropName); vm.startPrank(productOperator); @@ -220,6 +316,7 @@ contract CropProductTest is CropBaseTest { vm.stopPrank(); } + function _createRisk(string memory nanoId) internal returns (RiskId riskId) { Str riskIdStr = StrLib.toStr(nanoId); Str seasonId = _createSeason("7Zv4TZoBLxUi"); @@ -231,4 +328,23 @@ contract CropProductTest is CropBaseTest { riskId = cropProduct.createRisk(riskIdStr, seasonId, locationId, crop, seasonEndAt); vm.stopPrank(); } + + + function _createPolicy(address policyHolder) internal returns (NftId policyNftId) { + string memory nanoId = "kDho7606IRdr"; + RiskId riskId = _createRisk(nanoId); + Timestamp activateAt = TimestampLib.current(); + Amount sumInsuredAmount = AmountLib.toAmount(400 * 10 ** accountingToken.decimals()); + Amount premiumAmount = AmountLib.toAmount(25 * 10 ** accountingToken.decimals()); + + // WHEN + vm.startPrank(productOperator); + policyNftId = cropProduct.createPolicy( + policyHolder, + riskId, + activateAt, + sumInsuredAmount, + premiumAmount); + vm.stopPrank(); + } } \ No newline at end of file From 5938b30c0a01456856abf27b7c1c8e5551084a87 Mon Sep 17 00:00:00 2001 From: Matthias Zimmermann Date: Fri, 20 Dec 2024 09:52:46 +0000 Subject: [PATCH 3/7] add processPoliciesForRisk --- contracts/examples/crop/AccountingToken.sol | 2 +- contracts/examples/crop/CropProduct.sol | 103 +++++++----------- .../crop/CropProductAuthorization.sol | 2 +- test/examples/crop/CropBase.t.sol | 29 +---- 4 files changed, 42 insertions(+), 94 deletions(-) diff --git a/contracts/examples/crop/AccountingToken.sol b/contracts/examples/crop/AccountingToken.sol index 0db577f35..5f056f5ff 100644 --- a/contracts/examples/crop/AccountingToken.sol +++ b/contracts/examples/crop/AccountingToken.sol @@ -9,7 +9,7 @@ contract AccountingToken is ERC20 { string public constant NAME = "Local Currency (Accounting Token)"; string public constant SYMBOL = "LCA"; uint8 public constant DECIMALS = 6; - uint256 public constant INITIAL_SUPPLY = 10**12 * 10**DECIMALS; // 1'000'000'000'000 + uint256 public constant INITIAL_SUPPLY = 10**12 * 10**DECIMALS; constructor() ERC20(NAME, SYMBOL) diff --git a/contracts/examples/crop/CropProduct.sol b/contracts/examples/crop/CropProduct.sol index a95a19f43..22c71b401 100644 --- a/contracts/examples/crop/CropProduct.sol +++ b/contracts/examples/crop/CropProduct.sol @@ -96,7 +96,6 @@ contract CropProduct is // GIF V3 specifics NftId internal _defaultBundleNftId; - NftId internal _oracleNftId; constructor( @@ -279,23 +278,48 @@ contract CropProduct is function processPolicy(NftId policyNftId) - external - restricted() + external + restricted() { - _processAndClosePolicy(policyNftId); + _processPolicy(policyNftId); } + function processPoliciesForRisk( + RiskId riskId, + uint8 maxPoliciesToProcess + ) + internal + virtual + returns ( + bool success, + bool riskExists, + bool payoutDefined, + uint256 policiesProcessed + ) + { + // determine numbers of policies to process + CropRisk memory cropRisk; + (riskExists, cropRisk) = getRisk(riskId); - //--- owner functions ---------------------------------------------------// + // return if risk does not exist or payout is not defined yet + if (!riskExists) { return (false, false, false, 0); } + if (!cropRisk.payoutDefined) { return (false, true, false, 0); } - // TODO cleanup - // function resendResponse(RequestId requestId) - // external - // virtual - // restricted() - // { - // _resendResponse(requestId); - // } + InstanceReader reader = _getInstanceReader(); + uint256 policiesToProcess = reader.policiesForRisk(riskId); + policiesProcessed = policiesToProcess < maxPoliciesToProcess ? policiesToProcess : maxPoliciesToProcess; + + // go through policies + for (uint256 i = 0; i < policiesProcessed; i++) { + NftId policyNftId = reader.getPolicyForRisk(riskId, i); + _processPolicy(policyNftId); + } + + return (true, true, true, policiesProcessed); + } + + + //--- owner functions ---------------------------------------------------// /// @dev Call after product registration with the instance /// when the product token/tokenhandler is available @@ -326,13 +350,6 @@ contract CropProduct is //--- unpermissioned functions ------------------------------------------// - function setOracleNftId() - external - { - _oracleNftId = _getInstanceReader().getProductInfo( - getNftId()).oracleNftId[0]; - } - //--- view functions ----------------------------------------------------// function getSeason(Str seasonId) public view returns (Season memory season) { return _season[seasonId]; } @@ -394,54 +411,12 @@ contract CropProduct is return sumInsuredAmount.multiplyWith(payoutFactor); } - - function getOracleNftId() public view returns (NftId oracleNftId) { return _oracleNftId; } - function getRequestForRisk(RiskId riskId) public view returns (RequestId requestId) { return _requests[riskId]; } //--- internal functions ------------------------------------------------// - function _processPayoutsAndClosePolicies( - RiskId riskId, - uint8 maxPoliciesToProcess - ) - internal - virtual - returns ( - bool riskExists, - bool statusAvailable, - uint8 payoutOption - ) - { - // determine numbers of policies to process - InstanceReader reader = _getInstanceReader(); - CropRisk memory cropRisk; - (riskExists, cropRisk) = getRisk(riskId); - - // return with default values if risk does not exist or status is not yet available - if (!riskExists || !statusAvailable) { - return (riskExists, statusAvailable, payoutOption); - } - - uint256 policiesToProcess = reader.policiesForRisk(riskId); - uint256 policiesProcessed = policiesToProcess < maxPoliciesToProcess ? policiesToProcess : maxPoliciesToProcess; - - // assemble array with policies to process - NftId [] memory policies = new NftId[](policiesProcessed); - for (uint256 i = 0; i < policiesProcessed; i++) { - policies[i] = reader.getPolicyForRisk(riskId, i); - } - - // go through policies - for (uint256 i = 0; i < policiesProcessed; i++) { - NftId policyNftId = policies[i]; - _processAndClosePolicy(policyNftId); - } - } - - - function _processAndClosePolicy(NftId policyNftId) + function _processPolicy(NftId policyNftId) internal virtual { @@ -515,6 +490,6 @@ contract CropProduct is performanceFee: FeeLib.zero() }), authorization, - initialOwner); // number of oracles + initialOwner); } } \ No newline at end of file diff --git a/contracts/examples/crop/CropProductAuthorization.sol b/contracts/examples/crop/CropProductAuthorization.sol index 6950362cc..c9e38e65b 100644 --- a/contracts/examples/crop/CropProductAuthorization.sol +++ b/contracts/examples/crop/CropProductAuthorization.sol @@ -35,7 +35,7 @@ contract CropProductAuthorization AccessAdminLib.roleInfo( ADMIN_ROLE(), TargetType.Custom, - 1, // max member count special case: instance nft owner is sole role owner + 100, // max member count PRODUCT_OPERATOR_ROLE_NAME)); } diff --git a/test/examples/crop/CropBase.t.sol b/test/examples/crop/CropBase.t.sol index 4067256e7..e3ef1dcb8 100644 --- a/test/examples/crop/CropBase.t.sol +++ b/test/examples/crop/CropBase.t.sol @@ -109,7 +109,7 @@ contract CropBaseTest is GifTest { AmountLib.toAmount(99 * 10 ** accountingToken.decimals()), // max premium AmountLib.toAmount(200 * 10 ** accountingToken.decimals()), // min sum insured AmountLib.toAmount(1000 * 10 ** accountingToken.decimals()), // max sum insured - 1 // max policies to process + 5 // max policies to process ); vm.stopPrank(); } @@ -134,33 +134,6 @@ contract CropBaseTest is GifTest { } - function _deployFlightOracle() internal { - // vm.startPrank(cropOwner); - // FlightOracleAuthorization oracleAuthz = new FlightOracleAuthorization("FlightOracle", COMMIT_HASH); - // flightOracle = new FlightOracle( - // address(registry), - // cropProductNftId, - // "FlightOracle", - // oracleAuthz - // ); - // vm.stopPrank(); - - // flightOracleNftId = _registerComponent( - // cropOwner, - // cropProduct, - // address(flightOracle), - // "FlightOracle"); - - // // grant status provider role to status provider - // (RoleId statusProviderRoleId, bool exists) = instanceReader.getRoleForName( - // oracleAuthz.STATUS_PROVIDER_ROLE_NAME()); - - // vm.startPrank(instanceOwner); - // instance.grantRole(statusProviderRoleId, statusProvider); - // vm.stopPrank(); - } - - function _createInitialBundle() internal returns (NftId bundleNftId) { vm.startPrank(cropOwner); Amount investAmount = AmountLib.toAmount(10000000 * 10 ** 6); From 889663d7d6c1584e4989b866d35f7392dcf7cc9f Mon Sep 17 00:00:00 2001 From: Matthias Zimmermann Date: Fri, 20 Dec 2024 11:44:31 +0000 Subject: [PATCH 4/7] add test for processPoliciesForRisk --- contracts/examples/crop/CropProduct.sol | 4 +- test/examples/crop/CropProduct.t.sol | 53 ++++++++++++++++++++++++- 2 files changed, 54 insertions(+), 3 deletions(-) diff --git a/contracts/examples/crop/CropProduct.sol b/contracts/examples/crop/CropProduct.sol index 22c71b401..d1f10292d 100644 --- a/contracts/examples/crop/CropProduct.sol +++ b/contracts/examples/crop/CropProduct.sol @@ -286,9 +286,9 @@ contract CropProduct is function processPoliciesForRisk( RiskId riskId, - uint8 maxPoliciesToProcess + uint256 maxPoliciesToProcess ) - internal + external virtual returns ( bool success, diff --git a/test/examples/crop/CropProduct.t.sol b/test/examples/crop/CropProduct.t.sol index 89908e440..05113c8f0 100644 --- a/test/examples/crop/CropProduct.t.sol +++ b/test/examples/crop/CropProduct.t.sol @@ -229,7 +229,7 @@ contract CropProductTest is CropBaseTest { } - function test_cropProductPolicyClose() public { + function test_cropProductPolicyProcess() public { // GIVEN NftId policyNftId = _createPolicy(customer); @@ -284,6 +284,51 @@ contract CropProductTest is CropBaseTest { } + function test_cropProductRiskProcessPolicies() public { + // GIVEN + RiskId riskId = _createRisk("kDho7606IRdr"); + NftId policyNftId1 = _createPolicy(customer, riskId); + NftId policyNftId2 = _createPolicy(customer, riskId); + NftId policyNftId3 = _createPolicy(customer, riskId); + + UFixed payoutFactor = UFixedLib.toUFixed(35, -2); // 35% + uint256 maxPoliciesToProcess = 2; + + // WHEN + vm.startPrank(productOperator); + cropProduct.updatePayoutFactor(riskId, payoutFactor); + (bool success, bool riskExists, bool payoutDefined, uint256 processedPolicies) = cropProduct.processPoliciesForRisk(riskId, maxPoliciesToProcess); + vm.stopPrank(); + + // THEN + assertTrue(success, "policy processing failed"); + assertTrue(riskExists, "risk not found"); + assertTrue(payoutDefined, "payout not defined"); + assertEq(processedPolicies, 2, "unexpected processed policies count"); + assertEq(instanceReader.policiesForRisk(riskId), 1, "unexpected policy count"); + + assertEq(instanceReader.getPolicyState(policyNftId1).toInt(), CLOSED().toInt(), "unexpected policy state 1 (1)"); + assertEq(instanceReader.getPolicyState(policyNftId2).toInt(), CLOSED().toInt(), "unexpected policy state 2 (1)"); + assertEq(instanceReader.getPolicyState(policyNftId3).toInt(), COLLATERALIZED().toInt(), "unexpected policy state 3 (1)"); + + // WHEN (2) + vm.startPrank(productOperator); + (success, riskExists, payoutDefined, processedPolicies) = cropProduct.processPoliciesForRisk(riskId, maxPoliciesToProcess); + vm.stopPrank(); + + // THEN (2) + assertTrue(success, "policy processing failed (2)"); + assertTrue(riskExists, "risk not found (2)"); + assertTrue(payoutDefined, "payout not defined (2)"); + assertEq(processedPolicies, 1, "unexpected processed policies count (2)"); + assertEq(instanceReader.policiesForRisk(riskId), 0, "unexpected policy count (2)"); + + assertEq(instanceReader.getPolicyState(policyNftId1).toInt(), CLOSED().toInt(), "unexpected policy state 1 (2)"); + assertEq(instanceReader.getPolicyState(policyNftId2).toInt(), CLOSED().toInt(), "unexpected policy state 2 (2)"); + assertEq(instanceReader.getPolicyState(policyNftId3).toInt(), CLOSED().toInt(), "unexpected policy state 3 (2)"); + } + + function _createSeason(string memory nanoId) internal returns (Str seasonId) { seasonId = StrLib.toStr(nanoId); uint16 year = 2025; @@ -333,6 +378,12 @@ contract CropProductTest is CropBaseTest { function _createPolicy(address policyHolder) internal returns (NftId policyNftId) { string memory nanoId = "kDho7606IRdr"; RiskId riskId = _createRisk(nanoId); + + return _createPolicy(policyHolder, riskId); + } + + + function _createPolicy(address policyHolder, RiskId riskId) internal returns (NftId policyNftId) { Timestamp activateAt = TimestampLib.current(); Amount sumInsuredAmount = AmountLib.toAmount(400 * 10 ** accountingToken.decimals()); Amount premiumAmount = AmountLib.toAmount(25 * 10 ** accountingToken.decimals()); From f60b1c99ba7220606940881eac766e118c12771d Mon Sep 17 00:00:00 2001 From: Marc Doerflinger Date: Fri, 20 Dec 2024 12:09:36 +0000 Subject: [PATCH 5/7] deploy crop components --- .devcontainer/docker-compose.yaml | 4 +- scripts/deploy_all.ts | 6 +- scripts/deploy_crop_components.ts | 366 ++++++++++++++++++++++++++++++ scripts/libs/accounts.ts | 4 +- 4 files changed, 376 insertions(+), 4 deletions(-) create mode 100644 scripts/deploy_crop_components.ts diff --git a/.devcontainer/docker-compose.yaml b/.devcontainer/docker-compose.yaml index d79850340..74e5b9e3c 100644 --- a/.devcontainer/docker-compose.yaml +++ b/.devcontainer/docker-compose.yaml @@ -7,8 +7,8 @@ services: dockerfile: .devcontainer/Dockerfile.anvil volumes: - anvil-state:/anvil - # ports: - # - "7545:7545" + ports: + - "7545:7545" contracts: # See https://aka.ms/vscode-remote/containers/non-root for details. user: node diff --git a/scripts/deploy_all.ts b/scripts/deploy_all.ts index 512850cbc..fa8913afb 100644 --- a/scripts/deploy_all.ts +++ b/scripts/deploy_all.ts @@ -1,3 +1,4 @@ +import { deployCropComponentContracts } from "./deploy_crop_components"; import { deployFireComponentContracts } from "./deploy_fire_components"; import { deployFlightDelayComponentContracts } from "./deploy_flightdelay_components"; import { deployGifContracts } from "./deploy_gif"; @@ -11,9 +12,10 @@ import { logger } from "./logger"; * - Deploys all contracts for fire * - Creates a fire bundle and policy * - Deploys all contracts for flight delay + * - Deploys all contracts for crop */ async function main() { - const { protocolOwner, masterInstanceOwner, instanceOwner, productOwner: fireOwner, investor, customer } = await getNamedAccounts(); + const { protocolOwner, instanceOwner, productOwner: fireOwner, investor, customer, productOperator } = await getNamedAccounts(); loadVerificationQueueState(); const {services, libraries } = await deployGifContracts(protocolOwner, instanceOwner); @@ -22,6 +24,8 @@ async function main() { await createFireBundleAndPolicy(fireOwner, investor, customer, fireUsd, fireProduct, firePool); await deployFlightDelayComponentContracts(libraries, services, fireOwner, protocolOwner); + + await deployCropComponentContracts(libraries, services, fireOwner, productOperator, protocolOwner); } if (require.main === module) { diff --git a/scripts/deploy_crop_components.ts b/scripts/deploy_crop_components.ts new file mode 100644 index 000000000..0b1f0b164 --- /dev/null +++ b/scripts/deploy_crop_components.ts @@ -0,0 +1,366 @@ +import { AddressLike, resolveAddress, Signer } from "ethers"; +import { IInstance__factory, IInstanceService__factory, IRegistry__factory, TokenRegistry__factory, FlightProduct, FlightProduct__factory, FlightPool, FlightOracle, IInstance, CropProduct, InstanceReader__factory, AccountingToken, AccountingToken__factory, CropProduct__factory } from "../typechain-types"; +import { getNamedAccounts } from "./libs/accounts"; +import { deployContract } from "./libs/deployment"; +import { LibraryAddresses } from "./libs/libraries"; +import { ServiceAddresses } from "./libs/services"; +import { executeTx, getFieldFromLogs, getTxOpts } from "./libs/transaction"; +import { loadVerificationQueueState } from './libs/verification_queue'; +import { logger } from "./logger"; +import simpleGit from "simple-git"; +import { printBalances, printGasSpent, resetBalances, resetGasSpent, setBalanceAfter } from "./libs/gas_and_balance_tracker"; +import { ethers } from "hardhat"; +import { instance } from "../typechain-types/contracts"; +import { crop } from "../typechain-types/contracts/examples"; + +async function main() { + loadVerificationQueueState(); + + const { protocolOwner, productOwner: cropOwner, instanceOwner, productOperator } = await getNamedAccounts(); + + await deployCropComponentContracts( + { + accessAdminLibAddress: process.env.ACCESSADMINLIB_ADDRESS!, + amountLibAddress: process.env.AMOUNTLIB_ADDRESS!, + blockNumberLibAddress: process.env.BLOCKNUMBERLIB_ADDRESS!, + contractLibAddress: process.env.CONTRACTLIB_ADDRESS!, + feeLibAddress: process.env.FEELIB_ADDRESS!, + libRequestIdSetAddress: process.env.LIBREQUESTIDSET_ADDRESS!, + nftIdLibAddress: process.env.NFTIDLIB_ADDRESS!, + objectTypeLibAddress: process.env.OBJECTTYPELIB_ADDRESS!, + referralLibAddress: process.env.REFERRALLIB_ADDRESS!, + requestIdLibAddress: process.env.REQUESTIDLIB_ADDRESS!, + riskIdLibAddress: process.env.RISKIDLIB_ADDRESS!, + roleIdLibAddress: process.env.ROLEIDLIB_ADDRESS!, + secondsLibAddress: process.env.SECONDSLIB_ADDRESS!, + selectorLibAddress: process.env.SELECTORLIB_ADDRESS!, + strLibAddress: process.env.STRLIB_ADDRESS!, + timestampLibAddress: process.env.TIMESTAMPLIB_ADDRESS!, + uFixedLibAddress: process.env.UFIXEDLIB_ADDRESS!, + versionLibAddress: process.env.VERSIONLIB_ADDRESS!, + versionPartLibAddress: process.env.VERSIONPARTLIB_ADDRESS!, + } as LibraryAddresses, + { + instanceServiceAddress: process.env.INSTANCE_SERVICE_ADDRESS!, + } as ServiceAddresses, + cropOwner, + productOperator, + protocolOwner, + ); +} + + +export async function deployCropComponentContracts( + libraries: LibraryAddresses, services: ServiceAddresses, cropOwner: Signer, productOperator: Signer, registryOwner: Signer) { + resetBalances(); + resetGasSpent(); + + logger.info("===== deploying crop insurance components on a new instance ..."); + + const accessAdminLibAddress = libraries.accessAdminLibAddress; + const amountLibAddress = libraries.amountLibAddress; + const blocknumberLibAddress = libraries.blockNumberLibAddress; + const contractLibAddress = libraries.contractLibAddress; + const feeLibAddress = libraries.feeLibAddress; + const nftIdLibAddress = libraries.nftIdLibAddress; + const objectTypeLibAddress = libraries.objectTypeLibAddress; + const referralLibAddress = libraries.referralLibAddress; + const requestIdLibAddress = libraries.requestIdLibAddress; + const riskIdLibAddress = libraries.riskIdLibAddress; + const roleIdLibAddress = libraries.roleIdLibAddress; + const secondsLibAddress = libraries.secondsLibAddress; + const selectorLibAddress = libraries.selectorLibAddress; + const strLibAddress = libraries.strLibAddress; + const timestampLibAddress = libraries.timestampLibAddress; + const ufixedLibAddress = libraries.uFixedLibAddress; + const versionLibAddress = libraries.versionLibAddress; + const versionPartLibAddress = libraries.versionPartLibAddress; + + const instanceServiceAddress = services.instanceServiceAddress; + + let instanceAddress: string; + let instanceNftId: string; + let instance: IInstance; + + if (process.env.INSTANCE_ADDRESS !== undefined && process.env.INSTANCE_ADDRESS !== '') { + logger.info(`===== using existing instance @ ${process.env.INSTANCE_ADDRESS}`); + instanceAddress = process.env.INSTANCE_ADDRESS!; + instance = IInstance__factory.connect(instanceAddress, cropOwner); + instanceNftId = (await instance.getNftId()).toString(); + } else { + logger.debug(`instanceServiceAddress: ${instanceServiceAddress}`); + const instanceService = IInstanceService__factory.connect(await resolveAddress(instanceServiceAddress), cropOwner); + + logger.info("===== create new instance"); + const instanceCreateTx = await executeTx(async () => + await instanceService.createInstance(getTxOpts()), + "crop - createInstance", + [IInstanceService__factory.createInterface()] + ); + + instanceAddress = getFieldFromLogs(instanceCreateTx.logs, instanceService.interface, "LogInstanceServiceInstanceCreated", "instance") as string; + instanceNftId = getFieldFromLogs(instanceCreateTx.logs, instanceService.interface, "LogInstanceServiceInstanceCreated", "instanceNftId") as string; + logger.info(`Instance created at ${instanceAddress} with NFT ID ${instanceNftId}`); + instance = IInstance__factory.connect(instanceAddress, cropOwner); + } + + logger.info(`----- AccountingToken -----`); + let accountingTokenAddress: AddressLike; + let accountingToken: AccountingToken; + if (process.env.ACCOUNTING_TOKEN_ADDRESS !== undefined && process.env.ACCOUNTING_TOKEN_ADDRESS !== '') { + logger.info(`using existing Token at ${process.env.ACCOUNTING_TOKEN_ADDRESS}`); + accountingTokenAddress = process.env.ACCOUNTING_TOKEN_ADDRESS; + accountingToken = AccountingToken__factory.connect(accountingTokenAddress, registryOwner); + } else { + const { address: deployedAccountingTokenAddress, contract: accountingTokenBaseContract } = await deployContract( + "AccountingToken", + registryOwner); + accountingTokenAddress = deployedAccountingTokenAddress; + accountingToken = accountingTokenBaseContract as AccountingToken; + logger.info(`registering AccountingToken on TokenRegistry`); + + const registry = IRegistry__factory.connect(await instance.getRegistry(), registryOwner); + const tokenRegistry = TokenRegistry__factory.connect(await registry.getTokenRegistryAddress(), registryOwner); + await executeTx(async () => + await tokenRegistry.registerToken(accountingTokenAddress, getTxOpts()), + "crop - registerToken", + [TokenRegistry__factory.createInterface()] + ); + await executeTx(async () => + await tokenRegistry.setActiveForVersion( + (await tokenRegistry.runner?.provider?.getNetwork())?.chainId || 1, + accountingTokenAddress, + 3, + true, + getTxOpts()), + "crop - setActiveForVersion", + [TokenRegistry__factory.createInterface()] + ); + } + + logger.info(`===== deploying crop contracts`); + + logger.info(`----- LocationLib -----`); + const { address: locationLibAddress } = await deployContract( + "LocationLib", + cropOwner, + [], + { + libraries: { + + } + }); + + logger.info(`----- CropProduct -----`); + const deploymentId = Math.random().toString(16).substring(7); + const productName = "CropProduct_" + deploymentId; + const { address: cropProductAuthAddress } = await deployContract( + "CropProductAuthorization", + cropOwner, + [productName], + { + libraries: { + AccessAdminLib: accessAdminLibAddress, + BlocknumberLib: blocknumberLibAddress, + ObjectTypeLib: objectTypeLibAddress, + RoleIdLib: roleIdLibAddress, + SelectorLib: selectorLibAddress, + StrLib: strLibAddress, + TimestampLib: timestampLibAddress, + VersionPartLib: versionPartLibAddress, + } + }, + "contracts/examples/crop/CropProductAuthorization.sol:CropProductAuthorization"); + + const { address: cropProductAddress, contract: cropProductBaseContract } = await deployContract( + "CropProduct", + cropOwner, + [ + await instance.getRegistry(), + instanceNftId, + productName, + cropProductAuthAddress, + ], + { + libraries: { + LocationLib: locationLibAddress, + AmountLib: amountLibAddress, + ContractLib: contractLibAddress, + FeeLib: feeLibAddress, + NftIdLib: nftIdLibAddress, + ObjectTypeLib: objectTypeLibAddress, + ReferralLib: referralLibAddress, + SecondsLib: secondsLibAddress, + StrLib: strLibAddress, + TimestampLib: timestampLibAddress, + UFixedLib: ufixedLibAddress, + VersionLib: versionLibAddress, + } + }); + const cropProduct = cropProductBaseContract as CropProduct; + + logger.info(`registering CropProduct on Instance`); + await executeTx(async () => + await instance.registerProduct(cropProductAddress, accountingTokenAddress, getTxOpts()), + "crop - registerProduct", + [IInstance__factory.createInterface()] + ); + const cropProductNftId = await cropProduct.getNftId(); + + const instanceReaderAddress = await instance.getInstanceReader(); + const instanceReader = InstanceReader__factory.connect(instanceReaderAddress, cropOwner); + const { roleId } = await instanceReader.getRoleForName("ProductOperatorRole"); + + await executeTx(async () => + await instance.grantRole( + roleId, + await productOperator.getAddress(), + getTxOpts()), + "crop - grantProductOperator", + [IInstance__factory.createInterface()] + ); + + const accountingTokenDecimals = await accountingToken.decimals(); + + await executeTx(async () => + await cropProduct.setConstants( + 10 * Math.pow(10, Number(accountingTokenDecimals)), // 10 min premium + 99 * Math.pow(10, Number(accountingTokenDecimals)), // 99 max premium + 200 * Math.pow(10, Number(accountingTokenDecimals)), // 200 min sum insured + 1000 * Math.pow(10, Number(accountingTokenDecimals)), // 1000 max sum insured + 5, + getTxOpts() + ), + "crop - setConstants", + [CropProduct__factory.createInterface()] + ); + + // logger.info(`----- FlightPool -----`); + // const poolName = "FDPool_" + deploymentId; + // const { address: flightPoolAuthAddress } = await deployContract( + // "FlightPoolAuthorization", + // flightOwner, + // [poolName], + // { + // libraries: { + // AccessAdminLib: accessAdminLibAddress, + // BlocknumberLib: blocknumberLibAddress, + // ObjectTypeLib: objectTypeLibAddress, + // RoleIdLib: roleIdLibAddress, + // SelectorLib: selectorLibAddress, + // StrLib: strLibAddress, + // TimestampLib: timestampLibAddress, + // VersionPartLib: versionPartLibAddress, + // } + // }, + // "contracts/examples/flight/FlightPoolAuthorization.sol:FlightPoolAuthorization"); + + // const { address: flightPoolAddress, contract: flightPoolBaseContract } = await deployContract( + // "FlightPool", + // flightOwner, + // [ + // await instance.getRegistry(), + // flightProductNftId, + // poolName, + // flightPoolAuthAddress, + // ], + // { + // libraries: { + // AmountLib: amountLibAddress, + // ContractLib: contractLibAddress, + // FeeLib: feeLibAddress, + // NftIdLib: nftIdLibAddress, + // ObjectTypeLib: objectTypeLibAddress, + // SecondsLib: secondsLibAddress, + // UFixedLib: ufixedLibAddress, + // VersionLib: versionLibAddress, + // } + // }); + // const flightPool = flightPoolBaseContract as FlightPool; + + // logger.info(`registering FlightPool on FlightProduct`); + // await executeTx(async () => + // await flightProduct.registerComponent(flightPoolAddress, getTxOpts()), + // "fd - registerComponent pool", + // [FlightProduct__factory.createInterface()] + // ); + // const flightPoolNftId = await flightPool.getNftId(); + + // logger.info(`----- FlightOracle -----`); + // const oracleName = "FDOracle_" + deploymentId; + // const commitHash = await simpleGit().revparse(["HEAD"]); + + // const { address: flightOracleAuthAddress } = await deployContract( + // "FlightOracleAuthorization", + // flightOwner, + // [ + // oracleName, + // commitHash, + // ], + // { + // libraries: { + // AccessAdminLib: accessAdminLibAddress, + // BlocknumberLib: blocknumberLibAddress, + // ObjectTypeLib: objectTypeLibAddress, + // RoleIdLib: roleIdLibAddress, + // SelectorLib: selectorLibAddress, + // StrLib: strLibAddress, + // TimestampLib: timestampLibAddress, + // VersionPartLib: versionPartLibAddress, + // } + // }, + // "contracts/examples/flight/FlightOracleAuthorization.sol:FlightOracleAuthorization"); + + // const { address: flightOracleAddress, contract: flightOracleBaseContract } = await deployContract( + // "FlightOracle", + // flightOwner, + // [ + // await instance.getRegistry(), + // flightProductNftId, + // oracleName, + // flightOracleAuthAddress, + // ], + // { + // libraries: { + // ContractLib: contractLibAddress, + // NftIdLib: nftIdLibAddress, + // StrLib: strLibAddress, + // TimestampLib: timestampLibAddress, + // VersionLib: versionLibAddress, + // } + // }); + // const flightOracle = flightOracleBaseContract as FlightOracle; + + // logger.info(`registering FlightOracle on FlightProduct`); + // await executeTx(async () => + // await flightProduct.registerComponent(flightOracleAddress, getTxOpts()), + // "fd - registerComponent oracle", + // [FlightProduct__factory.createInterface()] + // ); + // const flightOracleNftId = await flightOracle.getNftId(); + + // await executeTx(async () => + // await flightProduct.setOracleNftId(getTxOpts()), + // "fd - setOracleNftId", + // [FlightProduct__factory.createInterface()] + // ); + + setBalanceAfter(await resolveAddress(registryOwner), await ethers.provider.getBalance(registryOwner)); + setBalanceAfter(await resolveAddress(cropOwner), await ethers.provider.getBalance(cropOwner)); + printBalances(); + printGasSpent(); + + logger.info(`===== Instance created. address: ${instanceAddress}, NFT ID: ${instanceNftId}`); + logger.info(`===== AccountingToken deployed at ${accountingTokenAddress}`); + logger.info(`===== LocationLib deployed at ${locationLibAddress}`); + logger.info(`===== CropProduct deployed at ${cropProductAddress} and registered with NFT ID ${cropProductNftId}`); + // logger.info(`===== FlightPool deployed at ${flightPoolAddress} and registered with NFT ID ${flightPoolNftId}`); +} + +if (require.main === module) { + main().catch((error) => { + logger.error(error.stack); + logger.error(error.data); + process.exit(1); + }); +} diff --git a/scripts/libs/accounts.ts b/scripts/libs/accounts.ts index c63eaaacc..0d4ec3cba 100644 --- a/scripts/libs/accounts.ts +++ b/scripts/libs/accounts.ts @@ -15,6 +15,7 @@ export async function getNamedAccounts(): Promise<{ instanceOwner: HardhatEthersSigner; customer: HardhatEthersSigner; investor: HardhatEthersSigner; + productOperator: HardhatEthersSigner; }> { const signers = await ethers.getSigners(); const protocolOwner = signers[0]; @@ -26,6 +27,7 @@ export async function getNamedAccounts(): Promise<{ const customer = signers[6]; const investor = signers[7]; const instanceOwner = signers[10]; + const productOperator = signers[11]; await printBalance( ["protocolOwner", protocolOwner] , // ["masterInstanceOwner", masterInstanceOwner] , @@ -40,7 +42,7 @@ export async function getNamedAccounts(): Promise<{ setBalanceBefore(await resolveAddress(productOwner), await ethers.provider.getBalance(productOwner)); setBalanceBefore(await resolveAddress(instanceOwner), await ethers.provider.getBalance(instanceOwner)); - return { protocolOwner, masterInstanceOwner, productOwner, poolOwner, distributionOwner, instanceServiceOwner, instanceOwner, customer, investor }; + return { protocolOwner, masterInstanceOwner, productOwner, poolOwner, distributionOwner, instanceServiceOwner, instanceOwner, customer, investor, productOperator }; } export async function printBalance(...signers: [string,HardhatEthersSigner][]) { From 59949db38e3722a628a10d5f754399b99dc5c8e3 Mon Sep 17 00:00:00 2001 From: Marc Doerflinger Date: Fri, 20 Dec 2024 12:19:11 +0000 Subject: [PATCH 6/7] deploy crop pool --- scripts/deploy_crop_components.ts | 168 ++++++++++-------------------- 1 file changed, 53 insertions(+), 115 deletions(-) diff --git a/scripts/deploy_crop_components.ts b/scripts/deploy_crop_components.ts index 0b1f0b164..7f9ce33b6 100644 --- a/scripts/deploy_crop_components.ts +++ b/scripts/deploy_crop_components.ts @@ -1,22 +1,19 @@ import { AddressLike, resolveAddress, Signer } from "ethers"; -import { IInstance__factory, IInstanceService__factory, IRegistry__factory, TokenRegistry__factory, FlightProduct, FlightProduct__factory, FlightPool, FlightOracle, IInstance, CropProduct, InstanceReader__factory, AccountingToken, AccountingToken__factory, CropProduct__factory } from "../typechain-types"; +import { ethers } from "hardhat"; +import { AccountingToken, AccountingToken__factory, CropPool, CropProduct, CropProduct__factory, IInstance, IInstance__factory, IInstanceService__factory, InstanceReader__factory, IRegistry__factory, TokenRegistry__factory } from "../typechain-types"; import { getNamedAccounts } from "./libs/accounts"; import { deployContract } from "./libs/deployment"; +import { printBalances, printGasSpent, resetBalances, resetGasSpent, setBalanceAfter } from "./libs/gas_and_balance_tracker"; import { LibraryAddresses } from "./libs/libraries"; import { ServiceAddresses } from "./libs/services"; import { executeTx, getFieldFromLogs, getTxOpts } from "./libs/transaction"; import { loadVerificationQueueState } from './libs/verification_queue'; import { logger } from "./logger"; -import simpleGit from "simple-git"; -import { printBalances, printGasSpent, resetBalances, resetGasSpent, setBalanceAfter } from "./libs/gas_and_balance_tracker"; -import { ethers } from "hardhat"; -import { instance } from "../typechain-types/contracts"; -import { crop } from "../typechain-types/contracts/examples"; async function main() { loadVerificationQueueState(); - const { protocolOwner, productOwner: cropOwner, instanceOwner, productOperator } = await getNamedAccounts(); + const { protocolOwner, productOwner: cropOwner, productOperator } = await getNamedAccounts(); await deployCropComponentContracts( { @@ -235,115 +232,56 @@ export async function deployCropComponentContracts( [CropProduct__factory.createInterface()] ); - // logger.info(`----- FlightPool -----`); - // const poolName = "FDPool_" + deploymentId; - // const { address: flightPoolAuthAddress } = await deployContract( - // "FlightPoolAuthorization", - // flightOwner, - // [poolName], - // { - // libraries: { - // AccessAdminLib: accessAdminLibAddress, - // BlocknumberLib: blocknumberLibAddress, - // ObjectTypeLib: objectTypeLibAddress, - // RoleIdLib: roleIdLibAddress, - // SelectorLib: selectorLibAddress, - // StrLib: strLibAddress, - // TimestampLib: timestampLibAddress, - // VersionPartLib: versionPartLibAddress, - // } - // }, - // "contracts/examples/flight/FlightPoolAuthorization.sol:FlightPoolAuthorization"); - - // const { address: flightPoolAddress, contract: flightPoolBaseContract } = await deployContract( - // "FlightPool", - // flightOwner, - // [ - // await instance.getRegistry(), - // flightProductNftId, - // poolName, - // flightPoolAuthAddress, - // ], - // { - // libraries: { - // AmountLib: amountLibAddress, - // ContractLib: contractLibAddress, - // FeeLib: feeLibAddress, - // NftIdLib: nftIdLibAddress, - // ObjectTypeLib: objectTypeLibAddress, - // SecondsLib: secondsLibAddress, - // UFixedLib: ufixedLibAddress, - // VersionLib: versionLibAddress, - // } - // }); - // const flightPool = flightPoolBaseContract as FlightPool; - - // logger.info(`registering FlightPool on FlightProduct`); - // await executeTx(async () => - // await flightProduct.registerComponent(flightPoolAddress, getTxOpts()), - // "fd - registerComponent pool", - // [FlightProduct__factory.createInterface()] - // ); - // const flightPoolNftId = await flightPool.getNftId(); - - // logger.info(`----- FlightOracle -----`); - // const oracleName = "FDOracle_" + deploymentId; - // const commitHash = await simpleGit().revparse(["HEAD"]); - - // const { address: flightOracleAuthAddress } = await deployContract( - // "FlightOracleAuthorization", - // flightOwner, - // [ - // oracleName, - // commitHash, - // ], - // { - // libraries: { - // AccessAdminLib: accessAdminLibAddress, - // BlocknumberLib: blocknumberLibAddress, - // ObjectTypeLib: objectTypeLibAddress, - // RoleIdLib: roleIdLibAddress, - // SelectorLib: selectorLibAddress, - // StrLib: strLibAddress, - // TimestampLib: timestampLibAddress, - // VersionPartLib: versionPartLibAddress, - // } - // }, - // "contracts/examples/flight/FlightOracleAuthorization.sol:FlightOracleAuthorization"); + logger.info(`----- CropPool -----`); + const poolName = "CropPool_" + deploymentId; + const { address: cropPoolAuthAddress } = await deployContract( + "CropPoolAuthorization", + cropOwner, + [poolName], + { + libraries: { + AccessAdminLib: accessAdminLibAddress, + BlocknumberLib: blocknumberLibAddress, + ObjectTypeLib: objectTypeLibAddress, + RoleIdLib: roleIdLibAddress, + SelectorLib: selectorLibAddress, + StrLib: strLibAddress, + TimestampLib: timestampLibAddress, + VersionPartLib: versionPartLibAddress, + } + }, + "contracts/examples/crop/CropPoolAuthorization.sol:CropPoolAuthorization"); - // const { address: flightOracleAddress, contract: flightOracleBaseContract } = await deployContract( - // "FlightOracle", - // flightOwner, - // [ - // await instance.getRegistry(), - // flightProductNftId, - // oracleName, - // flightOracleAuthAddress, - // ], - // { - // libraries: { - // ContractLib: contractLibAddress, - // NftIdLib: nftIdLibAddress, - // StrLib: strLibAddress, - // TimestampLib: timestampLibAddress, - // VersionLib: versionLibAddress, - // } - // }); - // const flightOracle = flightOracleBaseContract as FlightOracle; + const { address: cropPoolAddress, contract: cropPoolBaseContract } = await deployContract( + "CropPool", + cropOwner, + [ + await instance.getRegistry(), + cropProductNftId, + poolName, + cropPoolAuthAddress, + ], + { + libraries: { + AmountLib: amountLibAddress, + ContractLib: contractLibAddress, + FeeLib: feeLibAddress, + NftIdLib: nftIdLibAddress, + ObjectTypeLib: objectTypeLibAddress, + SecondsLib: secondsLibAddress, + UFixedLib: ufixedLibAddress, + VersionLib: versionLibAddress, + } + }); + const cropPool = cropPoolBaseContract as CropPool; - // logger.info(`registering FlightOracle on FlightProduct`); - // await executeTx(async () => - // await flightProduct.registerComponent(flightOracleAddress, getTxOpts()), - // "fd - registerComponent oracle", - // [FlightProduct__factory.createInterface()] - // ); - // const flightOracleNftId = await flightOracle.getNftId(); - - // await executeTx(async () => - // await flightProduct.setOracleNftId(getTxOpts()), - // "fd - setOracleNftId", - // [FlightProduct__factory.createInterface()] - // ); + logger.info(`registering CropPool on CropProduct`); + await executeTx(async () => + await cropProduct.registerComponent(cropPoolAddress, getTxOpts()), + "crop - registerComponent pool", + [CropProduct__factory.createInterface()] + ); + const cropPoolNftId = await cropPool.getNftId(); setBalanceAfter(await resolveAddress(registryOwner), await ethers.provider.getBalance(registryOwner)); setBalanceAfter(await resolveAddress(cropOwner), await ethers.provider.getBalance(cropOwner)); @@ -354,7 +292,7 @@ export async function deployCropComponentContracts( logger.info(`===== AccountingToken deployed at ${accountingTokenAddress}`); logger.info(`===== LocationLib deployed at ${locationLibAddress}`); logger.info(`===== CropProduct deployed at ${cropProductAddress} and registered with NFT ID ${cropProductNftId}`); - // logger.info(`===== FlightPool deployed at ${flightPoolAddress} and registered with NFT ID ${flightPoolNftId}`); + logger.info(`===== CropPool deployed at ${cropPoolAddress} and registered with NFT ID ${cropPoolNftId}`); } if (require.main === module) { From 861ef0a7a96eb15cbff51888150d491fec96d40a Mon Sep 17 00:00:00 2001 From: Marc Doerflinger Date: Fri, 20 Dec 2024 12:21:39 +0000 Subject: [PATCH 7/7] cleanup --- scripts/deploy_crop_components.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/scripts/deploy_crop_components.ts b/scripts/deploy_crop_components.ts index 7f9ce33b6..74a25c1fd 100644 --- a/scripts/deploy_crop_components.ts +++ b/scripts/deploy_crop_components.ts @@ -52,7 +52,7 @@ export async function deployCropComponentContracts( resetBalances(); resetGasSpent(); - logger.info("===== deploying crop insurance components on a new instance ..."); + logger.info("===== deploying crop insurance components ..."); const accessAdminLibAddress = libraries.accessAdminLibAddress; const amountLibAddress = libraries.amountLibAddress; @@ -62,8 +62,6 @@ export async function deployCropComponentContracts( const nftIdLibAddress = libraries.nftIdLibAddress; const objectTypeLibAddress = libraries.objectTypeLibAddress; const referralLibAddress = libraries.referralLibAddress; - const requestIdLibAddress = libraries.requestIdLibAddress; - const riskIdLibAddress = libraries.riskIdLibAddress; const roleIdLibAddress = libraries.roleIdLibAddress; const secondsLibAddress = libraries.secondsLibAddress; const selectorLibAddress = libraries.selectorLibAddress;