diff --git a/contracts/examples/flight/FlightLib.sol b/contracts/examples/flight/FlightLib.sol index 4828ad8c4..599f58891 100644 --- a/contracts/examples/flight/FlightLib.sol +++ b/contracts/examples/flight/FlightLib.sol @@ -11,6 +11,7 @@ import {InstanceReader} from "../../instance/InstanceReader.sol"; import {NftId} from "../../type/NftId.sol"; import {RequestId} from "../../type/RequestId.sol"; import {RiskId, RiskIdLib} from "../../type/RiskId.sol"; +import {Seconds} from "../../type/Seconds.sol"; import {StateId} from "../../type/StateId.sol"; import {Str} from "../../type/String.sol"; import {Timestamp, TimestampLib} from "../../type/Timestamp.sol"; @@ -18,6 +19,19 @@ import {Timestamp, TimestampLib} from "../../type/Timestamp.sol"; library FlightLib { + event LogFlightProductErrorUnprocessableStatus(RequestId requestId, RiskId riskId, bytes1 status); + event LogFlightProductErrorUnexpectedStatus(RequestId requestId, RiskId riskId, bytes1 status, int256 delayMinutes); + + error ErrorFlightProductRiskInvalid(RiskId riskId); + error ErrorFlightProductPremiumAmountTooSmall(Amount premiumAmount, Amount minPremium); + error ErrorFlightProductPremiumAmountTooLarge(Amount premiumAmount, Amount maxPremium); + error ErrorFlightProductArrivalBeforeDepartureTime(Timestamp departureTime, Timestamp arrivalTime); + error ErrorFlightProductArrivalAfterMaxFlightDuration(Timestamp arrivalTime, Timestamp maxArrivalTime, Seconds maxDuration); + error ErrorFlightProductDepartureBeforeMinTimeBeforeDeparture(Timestamp departureTime, Timestamp now, Seconds minTimeBeforeDeparture); + error ErrorFlightProductDepartureAfterMaxTimeBeforeDeparture(Timestamp departureTime, Timestamp now, Seconds maxTimeBeforeDeparture); + error ErrorFlightProductNotEnoughObservations(uint256 observations, uint256 minObservations); + error ErrorFlightProductClusterRisk(Amount totalSumInsured, Amount maxTotalPayout); + function checkApplicationData( FlightProduct flightProduct, Str flightData, @@ -45,32 +59,45 @@ library FlightLib { // solhint-disable if (premiumAmount < flightProduct.MIN_PREMIUM()) { - revert FlightProduct.ErrorFlightProductPremiumAmountTooSmall(premiumAmount, flightProduct.MIN_PREMIUM()); + revert ErrorFlightProductPremiumAmountTooSmall(premiumAmount, flightProduct.MIN_PREMIUM()); } if (premiumAmount > flightProduct.MAX_PREMIUM()) { - revert FlightProduct.ErrorFlightProductPremiumAmountTooLarge(premiumAmount, flightProduct.MAX_PREMIUM()); + revert ErrorFlightProductPremiumAmountTooLarge(premiumAmount, flightProduct.MAX_PREMIUM()); } if (arrivalTime <= departureTime) { - revert FlightProduct.ErrorFlightProductArrivalBeforeDepartureTime(departureTime, arrivalTime); + revert ErrorFlightProductArrivalBeforeDepartureTime(departureTime, arrivalTime); } - - // test mode allows the creation for policies that ore not time constrained - if (!testMode && arrivalTime > departureTime.addSeconds(flightProduct.MAX_FLIGHT_DURATION())) { - revert FlightProduct.ErrorFlightProductArrivalAfterMaxFlightDuration(arrivalTime, departureTime, flightProduct.MAX_FLIGHT_DURATION()); + if (arrivalTime > departureTime.addSeconds(flightProduct.MAX_FLIGHT_DURATION())) { + revert ErrorFlightProductArrivalAfterMaxFlightDuration(arrivalTime, departureTime, flightProduct.MAX_FLIGHT_DURATION()); } + + // test mode allows the creation for policies that are outside restricted policy creation times if (!testMode && departureTime < TimestampLib.current().addSeconds(flightProduct.MIN_TIME_BEFORE_DEPARTURE())) { - revert FlightProduct.ErrorFlightProductDepartureBeforeMinTimeBeforeDeparture(departureTime, TimestampLib.current(), flightProduct.MIN_TIME_BEFORE_DEPARTURE()); + revert ErrorFlightProductDepartureBeforeMinTimeBeforeDeparture(departureTime, TimestampLib.current(), flightProduct.MIN_TIME_BEFORE_DEPARTURE()); } if (!testMode && departureTime > TimestampLib.current().addSeconds(flightProduct.MAX_TIME_BEFORE_DEPARTURE())) { - revert FlightProduct.ErrorFlightProductDepartureAfterMaxTimeBeforeDeparture(departureTime, TimestampLib.current(), flightProduct.MAX_TIME_BEFORE_DEPARTURE()); + revert ErrorFlightProductDepartureAfterMaxTimeBeforeDeparture(departureTime, TimestampLib.current(), flightProduct.MAX_TIME_BEFORE_DEPARTURE()); } // solhint-enable } + function checkClusterRisk( + Amount sumOfSumInsuredAmounts, + Amount sumInsuredAmount, + Amount maxTotalPayout + ) + public + pure + { + if (sumOfSumInsuredAmounts + sumInsuredAmount > maxTotalPayout) { + revert ErrorFlightProductClusterRisk(sumOfSumInsuredAmounts + sumInsuredAmount, maxTotalPayout); + } + } + + /// @dev calculates payout option based on flight status and delay minutes. /// Is not a view function as it emits log evens in case of unexpected status. - // TODO decide if reverts instead of log events could work too (and convert the function into a view function) function checkAndGetPayoutOption( RequestId requestId, RiskId riskId, @@ -85,13 +112,13 @@ library FlightLib { // check status if (status != "L" && status != "A" && status != "C" && status != "D") { - emit FlightProduct.LogErrorUnprocessableStatus(requestId, riskId, status); + emit LogFlightProductErrorUnprocessableStatus(requestId, riskId, status); return payoutOption; } if (status == "A") { // todo: active, reschedule oracle call + 45 min - emit FlightProduct.LogErrorUnexpectedStatus(requestId, riskId, status, delayMinutes); + emit LogFlightProductErrorUnexpectedStatus(requestId, riskId, status, delayMinutes); return payoutOption; } @@ -114,7 +141,7 @@ library FlightLib { { // check we have enough observations if (statistics[0] < flightProduct.MIN_OBSERVATIONS()) { - revert FlightProduct.ErrorFlightProductNotEnoughObservations(statistics[0], flightProduct.MIN_OBSERVATIONS()); + revert ErrorFlightProductNotEnoughObservations(statistics[0], flightProduct.MIN_OBSERVATIONS()); } weight = 0; @@ -147,10 +174,10 @@ library FlightLib { ) { if (premium < flightProduct.MIN_PREMIUM()) { - revert FlightProduct.ErrorFlightProductPremiumAmountTooSmall(premium, flightProduct.MIN_PREMIUM()); + revert ErrorFlightProductPremiumAmountTooSmall(premium, flightProduct.MIN_PREMIUM()); } if (premium > flightProduct.MAX_PREMIUM()) { - revert FlightProduct.ErrorFlightProductPremiumAmountTooLarge(premium, flightProduct.MAX_PREMIUM()); + revert ErrorFlightProductPremiumAmountTooLarge(premium, flightProduct.MAX_PREMIUM()); } sumInsuredAmount = AmountLib.zero(); @@ -186,7 +213,8 @@ library FlightLib { (exists, flightRisk) = getFlightRisk( reader, productNftId, - riskId); + riskId, + false); statusAvailable = flightRisk.statusUpdatedAt.gtz(); payoutOption = flightRisk.payoutOption; @@ -201,6 +229,10 @@ library FlightLib { pure returns (Amount payoutAmount) { + if (payoutOption == type(uint8).max) { + return AmountLib.zero(); + } + // retrieve payout amounts from application data (, Amount[5] memory payoutAmounts) = abi.decode( applicationData, (Amount, Amount[5])); @@ -228,7 +260,7 @@ library FlightLib { ) { riskId = getRiskId(productNftId, flightData); - (exists, flightRisk) = getFlightRisk(reader, productNftId, riskId); + (exists, flightRisk) = getFlightRisk(reader, productNftId, riskId, false); // create new risk if not existing if (!exists) { @@ -250,7 +282,8 @@ library FlightLib { function getFlightRisk( InstanceReader reader, NftId productNftId, - RiskId riskId + RiskId riskId, + bool requireRiskExists ) public view @@ -262,6 +295,10 @@ library FlightLib { // check if risk exists exists = reader.isProductRisk(productNftId, riskId); + if (!exists && requireRiskExists) { + revert ErrorFlightProductRiskInvalid(riskId); + } + // get risk data if risk exists if (exists) { flightRisk = abi.decode( diff --git a/contracts/examples/flight/FlightOracle.sol b/contracts/examples/flight/FlightOracle.sol index 959c14e57..5ae45ea2d 100644 --- a/contracts/examples/flight/FlightOracle.sol +++ b/contracts/examples/flight/FlightOracle.sol @@ -3,14 +3,15 @@ pragma solidity ^0.8.20; import {IAuthorization} from "../../authorization/IAuthorization.sol"; -import {FULFILLED} from "../../type/StateId.sol"; +import {ACTIVE, FULFILLED, FAILED} from "../../type/StateId.sol"; import {NftId} from "../../type/NftId.sol"; import {BasicOracle} from "../../oracle/BasicOracle.sol"; import {RequestId} from "../../type/RequestId.sol"; import {LibRequestIdSet} from "../../type/RequestIdSet.sol"; import {RiskId} from "../../type/RiskId.sol"; +import {StateId} from "../../type/StateId.sol"; import {Str} from "../../type/String.sol"; -import {Timestamp} from "../../type/Timestamp.sol"; +import {Timestamp, TimestampLib} from "../../type/Timestamp.sol"; contract FlightOracle is BasicOracle @@ -110,6 +111,7 @@ contract FlightOracle is } } + //--- view functions ----------------------------------------------------// // TODO decide if the code below should be moved to GIF @@ -131,6 +133,30 @@ contract FlightOracle is return LibRequestIdSet.getElementAt(_activeRequests, idx); } + + function getRequestState(RequestId requestId) + external + view + returns ( + RiskId riskId, + string memory flightData, + StateId requestState, + bool readyForResponse, + bool waitingForResend + ) + { + bytes memory requestData = _getInstanceReader().getRequestInfo(requestId).requestData; + Str fltData; + Timestamp departureTime; + (riskId, fltData, departureTime) = abi.decode(requestData, (RiskId, Str, Timestamp)); + + flightData = fltData.toString(); + requestState = _getInstanceReader().getRequestState(requestId); + readyForResponse = requestState == ACTIVE() && TimestampLib.current() >= departureTime; + waitingForResend = requestState == FAILED(); + } + + function decodeFlightStatusRequestData(bytes memory data) external pure returns (FlightStatusRequest memory) { return abi.decode(data, (FlightStatusRequest)); } diff --git a/contracts/examples/flight/FlightProduct.sol b/contracts/examples/flight/FlightProduct.sol index 0a600ae3a..fc62bc224 100644 --- a/contracts/examples/flight/FlightProduct.sol +++ b/contracts/examples/flight/FlightProduct.sol @@ -6,9 +6,11 @@ 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"; +import {Component} from "../../shared/Component.sol"; import {FeeLib} from "../../type/Fee.sol"; import {FlightLib} from "./FlightLib.sol"; import {FlightMessageVerifier} from "./FlightMessageVerifier.sol"; @@ -30,68 +32,43 @@ contract FlightProduct is Product { - event LogRequestFlightRatings(uint256 requestId, bytes32 carrierFlightNumber, uint256 departureTime, uint256 arrivalTime, bytes32 riskId); - event LogRequestFlightStatus(uint256 requestId, uint256 arrivalTime, bytes32 carrierFlightNumber, bytes32 departureYearMonthDay); - event LogPayoutTransferred(bytes32 bpKey, uint256 claimId, uint256 payoutId, uint256 amount); + event LogFlightPolicyPurchased(NftId policyNftId, string flightData, Amount premiumAmount); + event LogFlightPolicyClosed(NftId policyNftId, Amount payoutAmount); + event LogFlightStatusProcessed(RequestId requestId, RiskId riskId, bytes1 status, int256 delayMinutes, uint8 payoutOption); event LogFlightPoliciesProcessed(RiskId riskId, uint8 payoutOption, uint256 policiesProcessed, uint256 policiesRemaining); - // TODO convert error logs to custom errors - // event LogError(string error, uint256 index, uint256 stored, uint256 calculated); - event LogPolicyExpired(bytes32 bpKey); - - event LogErrorRiskInvalid(RequestId requestId, RiskId riskId); - event LogErrorUnprocessableStatus(RequestId requestId, RiskId riskId, bytes1 status); - event LogErrorUnexpectedStatus(RequestId requestId, RiskId riskId, bytes1 status, int256 delayMinutes); - - error ErrorApplicationDataSignatureMismatch(address expectedSigner, address actualSigner); - error ErrorFlightProductClusterRisk(Amount totalSumInsured, Amount maxTotalPayout); - error ErrorFlightProductPremiumAmountTooSmall(Amount premiumAmount, Amount minPremium); - error ErrorFlightProductPremiumAmountTooLarge(Amount premiumAmount, Amount maxPremium); - error ErrorFlightProductArrivalBeforeDepartureTime(Timestamp departureTime, Timestamp arrivalTime); - error ErrorFlightProductArrivalAfterMaxFlightDuration(Timestamp arrivalTime, Timestamp maxArrivalTime, Seconds maxDuration); - error ErrorFlightProductDepartureBeforeMinTimeBeforeDeparture(Timestamp departureTime, Timestamp now, Seconds minTimeBeforeDeparture); - error ErrorFlightProductDepartureAfterMaxTimeBeforeDeparture(Timestamp departureTime, Timestamp now, Seconds maxTimeBeforeDeparture); - error ErrorFlightProductNotEnoughObservations(uint256 observations, uint256 minObservations); - // solhint-disable - // Minimum observations for valid prediction - uint256 public immutable MIN_OBSERVATIONS = 10; - // Minimum time before departure for applying - Seconds public MIN_TIME_BEFORE_DEPARTURE = SecondsLib.fromDays(14); - // Maximum time before departure for applying - Seconds public MAX_TIME_BEFORE_DEPARTURE = SecondsLib.fromDays(90); - // Maximum duration of flight - Seconds public MAX_FLIGHT_DURATION = SecondsLib.fromDays(2); - // Check for delay after .. minutes after scheduled arrival - Seconds public CHECK_OFFSET = SecondsLib.fromHours(1); - // Max time to process claims after departure - Seconds public LIFETIME = SecondsLib.fromDays(30); - - // uint256 public constant MIN_PREMIUM = 15 * 10 ** 18; // production - // All amounts in cent = multiplier is 10 ** 16! Amount public MIN_PREMIUM; Amount public MAX_PREMIUM; Amount public MAX_PAYOUT; - Amount public MAX_TOTAL_PAYOUT; // Maximum risk per flight is 3x max payout. - - // Maximum cumulated weighted premium per risk - uint256 public MARGIN_PERCENT = 30; + Amount public MAX_TOTAL_PAYOUT; // Maximum risk per flight/risk - // Maximum number of policies to process in one callback - uint8 public MAX_POLICIES_TO_PROCESS = 5; + // Minimum time before departure for applying + Seconds public MIN_TIME_BEFORE_DEPARTURE; + // Maximum time before departure for applying + Seconds public MAX_TIME_BEFORE_DEPARTURE; + // Maximum duration of flight + Seconds public MAX_FLIGHT_DURATION; + // Max time to process claims after departure + Seconds public LIFETIME; // ['observations','late15','late30','late45','cancelled','diverted'] // no payouts for delays of 30' or less - uint8[6] public WEIGHT_PATTERN = [0, 0, 0, 30, 50, 50]; - uint8 public constant MAX_WEIGHT = 50; + uint8[6] public WEIGHT_PATTERN; + // Minimum number of observations for valid prediction/premium calculation + uint256 public MIN_OBSERVATIONS; + // Maximum cumulated weighted premium per risk + uint256 public MARGIN_PERCENT; + // Maximum number of policies to process in one callback + uint8 public MAX_POLICIES_TO_PROCESS; + // solhint-enable bool internal _testMode; // GIF V3 specifics NftId internal _defaultBundleNftId; NftId internal _oracleNftId; - // solhint-enable struct FlightRisk { @@ -103,14 +80,13 @@ contract FlightProduct is // this field contains static data required by the frontend and is not directly used by the product string arrivalTimeLocal; // example "2024-10-14T10:10:00.000 Asia/Seoul" Amount sumOfSumInsuredAmounts; - // uint256 premiumMultiplier; // what is this? UFixed? - // uint256 weight; // what is this? UFixed? bytes1 status; // 'L'ate, 'C'ancelled, 'D'iverted, ... int256 delayMinutes; uint8 payoutOption; Timestamp statusUpdatedAt; } + struct ApplicationData { Str flightData; Timestamp departureTime; @@ -135,7 +111,7 @@ contract FlightProduct is constructor( address registry, - NftId instanceNftid, + NftId instanceNftId, string memory componentName, IAuthorization authorization ) @@ -144,12 +120,13 @@ contract FlightProduct is _initialize( registry, - instanceNftid, + instanceNftId, componentName, authorization, initialOwner); } + //--- external functions ------------------------------------------------// //--- unpermissioned functions ------------------------------------------// @@ -272,6 +249,8 @@ contract FlightProduct is // allow up to 30 days to process the claim arrivalTime.addSeconds(SecondsLib.fromDays(30)), "flightStatusCallback"); + + emit LogFlightPolicyPurchased(policyNftId, flightData.toString(), premiumAmount); } @@ -292,31 +271,17 @@ contract FlightProduct is requestId, response.riskId, response.status, - response.delayMinutes, - MAX_POLICIES_TO_PROCESS); + response.delayMinutes); } - /// @dev Manual fallback function for product owner. - function processFlightStatus( - RequestId requestId, - RiskId riskId, - bytes1 status, - int256 delayMinutes, - uint8 maxPoliciesToProcess - ) + function resendRequest(RequestId requestId) external virtual restricted() - onlyOwner() { - _processFlightStatus( - requestId, - riskId, - status, - delayMinutes, - maxPoliciesToProcess); - } + _resendRequest(requestId); + } /// @dev Manual fallback function for product owner. @@ -339,22 +304,30 @@ contract FlightProduct is /// @dev Call after product registration with the instance /// when the product token/tokenhandler is available - function completeSetup() + function setConstants( + Amount minPremium, + Amount maxPremium, + Amount maxPayout, + Amount maxTotalPayout, + Seconds minTimeBeforeDeparture, + Seconds maxTimeBeforeDeparture, + uint8 maxPoliciesToProcess + ) external virtual restricted() onlyOwner() { - IERC20Metadata token = IERC20Metadata(getToken()); - uint256 tokenMultiplier = 10 ** token.decimals(); - - MIN_PREMIUM = AmountLib.toAmount(15 * tokenMultiplier); - MAX_PREMIUM = AmountLib.toAmount(200 * tokenMultiplier); - MAX_PAYOUT = AmountLib.toAmount(500 * tokenMultiplier); - MAX_TOTAL_PAYOUT = AmountLib.toAmount(3 * MAX_PAYOUT.toInt()); + MIN_PREMIUM = minPremium; + MAX_PREMIUM = maxPremium; + MAX_PAYOUT = maxPayout; + MAX_TOTAL_PAYOUT = maxTotalPayout; + + MIN_TIME_BEFORE_DEPARTURE = minTimeBeforeDeparture; + MAX_TIME_BEFORE_DEPARTURE = maxTimeBeforeDeparture; + MAX_POLICIES_TO_PROCESS = maxPoliciesToProcess; } - function setDefaultBundle(NftId bundleNftId) external restricted() onlyOwner() { _defaultBundleNftId = bundleNftId; } function setTestMode(bool testMode) external restricted() onlyOwner() { _testMode = testMode; } @@ -524,10 +497,10 @@ contract FlightProduct is _createRisk(riskKey, abi.encode(flightRisk)); } - // check for cluster risk: additional sum insured amount must not exceed MAX_TOTAL_PAYOUT - if (flightRisk.sumOfSumInsuredAmounts + sumInsuredAmount > MAX_TOTAL_PAYOUT) { - revert ErrorFlightProductClusterRisk(flightRisk.sumOfSumInsuredAmounts + sumInsuredAmount, MAX_TOTAL_PAYOUT); - } + FlightLib.checkClusterRisk( + flightRisk.sumOfSumInsuredAmounts, + sumInsuredAmount, + MAX_TOTAL_PAYOUT); // update existing risk with additional sum insured amount flightRisk.sumOfSumInsuredAmounts = flightRisk.sumOfSumInsuredAmounts + sumInsuredAmount; @@ -539,8 +512,7 @@ contract FlightProduct is RequestId requestId, RiskId riskId, bytes1 status, - int256 delayMinutes, - uint8 maxPoliciesToProcess + int256 delayMinutes ) internal virtual @@ -550,29 +522,22 @@ contract FlightProduct is ( bool exists, FlightRisk memory flightRisk - ) = FlightLib.getFlightRisk(reader, getNftId(), riskId); + ) = FlightLib.getFlightRisk(reader, getNftId(), riskId, true); - if (!exists) { - // TODO decide to switch from log to error - emit LogErrorRiskInvalid(requestId, riskId); - return; - } else { - // update status, if not yet set - if (flightRisk.statusUpdatedAt.eqz()) { - flightRisk.status = status; - flightRisk.delayMinutes = delayMinutes; - flightRisk.payoutOption = FlightLib.checkAndGetPayoutOption( - requestId, riskId, status, delayMinutes); - flightRisk.statusUpdatedAt = TimestampLib.current(); - - _updateRisk(riskId, abi.encode(flightRisk)); - } - // TODO revert in else case? + // update status, if not yet set + if (flightRisk.statusUpdatedAt.eqz()) { + flightRisk.statusUpdatedAt = TimestampLib.current(); + flightRisk.status = status; + flightRisk.delayMinutes = delayMinutes; + flightRisk.payoutOption = FlightLib.checkAndGetPayoutOption( + requestId, riskId, status, delayMinutes); + + _updateRisk(riskId, abi.encode(flightRisk)); } (,, uint8 payoutOption) = _processPayoutsAndClosePolicies( riskId, - maxPoliciesToProcess); + MAX_POLICIES_TO_PROCESS); // logging emit LogFlightStatusProcessed(requestId, riskId, status, delayMinutes, payoutOption); @@ -606,22 +571,20 @@ contract FlightProduct is // go trough policies for (uint256 i = 0; i < policiesProcessed; i++) { NftId policyNftId = reader.getPolicyForRisk(riskId, i); + Amount payoutAmount = FlightLib.getPayoutAmount( + reader.getPolicyInfo(policyNftId).applicationData, + payoutOption); - // create payout (if any) - if (payoutOption < type(uint8).max) { - bytes memory applicationData = reader.getPolicyInfo( - policyNftId).applicationData; - - _resolvePayout( - policyNftId, - FlightLib.getPayoutAmount( - applicationData, - payoutOption)); - } + // create claim/payout (if applicable) + _resolvePayout( + policyNftId, + payoutAmount); // expire and close policy _expire(policyNftId, TimestampLib.current()); _close(policyNftId); + + emit LogFlightPolicyClosed(policyNftId, payoutAmount); } // logging @@ -636,6 +599,11 @@ contract FlightProduct is internal virtual { + // no action if no payout + if (payoutAmount.eqz()) { + return; + } + // create confirmed claim ClaimId claimId = _submitClaim(policyNftId, payoutAmount, ""); _confirmClaim(policyNftId, claimId, payoutAmount, ""); @@ -654,7 +622,7 @@ contract FlightProduct is address initialOwner ) internal - initializer + initializer() { __Product_init( registry, @@ -681,5 +649,11 @@ contract FlightProduct is }), authorization, initialOwner); // number of oracles + + MAX_FLIGHT_DURATION = SecondsLib.fromDays(2); + LIFETIME = SecondsLib.fromDays(30); + WEIGHT_PATTERN = [0, 0, 0, 30, 50, 50]; + MIN_OBSERVATIONS = 10; + MARGIN_PERCENT = 30; } } \ No newline at end of file diff --git a/contracts/examples/flight/FlightProductAuthorization.sol b/contracts/examples/flight/FlightProductAuthorization.sol index 556effada..650831b18 100644 --- a/contracts/examples/flight/FlightProductAuthorization.sol +++ b/contracts/examples/flight/FlightProductAuthorization.sol @@ -57,9 +57,10 @@ contract FlightProductAuthorization // authorize public role (additional authz via onlyOwner) functions = _authorizeForTarget(getMainTargetName(), PUBLIC_ROLE()); - _authorize(functions, FlightProduct.processFlightStatus.selector, "processFlightStatus"); + _authorize(functions, FlightProduct.resendRequest.selector, "resendRequest"); _authorize(functions, FlightProduct.processPayoutsAndClosePolicies.selector, "processPayoutsAndClosePolicies"); - _authorize(functions, FlightProduct.completeSetup.selector, "completeSetup"); + _authorize(functions, FlightProduct.setConstants.selector, "setConstants"); + _authorize(functions, FlightProduct.setTestMode.selector, "setTestMode"); _authorize(functions, FlightProduct.setDefaultBundle.selector, "setDefaultBundle"); _authorize(functions, FlightProduct.approveTokenHandler.selector, "approveTokenHandler"); _authorize(functions, FlightProduct.setLocked.selector, "setLocked"); diff --git a/contracts/examples/flight/FlightProductManager.sol b/contracts/examples/flight/FlightProductManager.sol new file mode 100644 index 000000000..eb602999a --- /dev/null +++ b/contracts/examples/flight/FlightProductManager.sol @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.20; + +import {IAuthorization} from "../../authorization/IAuthorization.sol"; +import {IVersionable} from "../../upgradeability/IVersionable.sol"; + +import {NftId} from "../../type/NftId.sol"; +import {ProxyManager} from "../../upgradeability/ProxyManager.sol"; +import {FlightProduct} from "./FlightProduct.sol"; + + +contract FlightProductManager is ProxyManager { + + FlightProduct private _flightProduct; + bytes32 private _salt = "0x1234"; + + /// @dev initializes proxy manager with flight product implementation + constructor( + address registry, + NftId instanceNftId, + string memory componentName, + IAuthorization authorization + ) + { + // FlightProduct prd = new FlightProduct{salt: _salt}(); + // bytes memory data = abi.encode( + // registry, + // instanceNftId, + // componentName, + // authorization); + + // IVersionable versionable = initialize( + // registry, + // address(prd), + // data, + // _salt); + + // _flightProduct = FlightProduct(address(versionable)); + } + + //--- view functions ----------------------------------------------------// + function getFlightProduct() + external + view + returns (FlightProduct flightProduct) + { + return _flightProduct; + } +} \ No newline at end of file diff --git a/contracts/instance/InstanceReader.sol b/contracts/instance/InstanceReader.sol index 22c027062..7a071ab28 100644 --- a/contracts/instance/InstanceReader.sol +++ b/contracts/instance/InstanceReader.sol @@ -34,6 +34,7 @@ import {RiskId} from "../type/RiskId.sol"; import {RiskSet} from "./RiskSet.sol"; import {RoleId, INSTANCE_OWNER_ROLE} from "../type/RoleId.sol"; import {StateId} from "../type/StateId.sol"; +import {Str, StrLib} from "../type/String.sol"; import {TokenHandler} from "../shared/TokenHandler.sol"; import {UFixed, UFixedLib} from "../type/UFixed.sol"; @@ -601,13 +602,18 @@ contract InstanceReader { } - function toUFixed(uint256 value, int8 exp) public pure returns (UFixed) { - return UFixedLib.toUFixed(value, exp); + function toInt(UFixed value) public pure returns (uint256) { + return UFixedLib.toInt(value); } - function toInt(UFixed value) public pure returns (uint256) { - return UFixedLib.toInt(value); + function toString(Str str) public pure returns (string memory) { + return StrLib.toString(str); + } + + + function toUFixed(uint256 value, int8 exp) public pure returns (UFixed) { + return UFixedLib.toUFixed(value, exp); } //--- internal functions ----------------------------------------------------// diff --git a/contracts/oracle/BasicOracle.sol b/contracts/oracle/BasicOracle.sol index 5a215a898..049ce8d8f 100644 --- a/contracts/oracle/BasicOracle.sol +++ b/contracts/oracle/BasicOracle.sol @@ -22,6 +22,7 @@ contract BasicOracle is _respond(requestId, responseData); } + function _initializeBasicOracle( address registry, NftId instanceNftId, diff --git a/contracts/oracle/Oracle.sol b/contracts/oracle/Oracle.sol index 45d5afaa0..96690d580 100644 --- a/contracts/oracle/Oracle.sol +++ b/contracts/oracle/Oracle.sol @@ -140,10 +140,10 @@ abstract contract Oracle is internal virtual { - _getOracleStorage()._oracleService.respond( - requestId, responseData); + _getOracleStorage()._oracleService.respond(requestId, responseData); } + function _getOracleStorage() private pure returns (OracleStorage storage $) { assembly { $.slot := ORACLE_STORAGE_LOCATION_V1 diff --git a/contracts/product/PolicyServiceLib.sol b/contracts/product/PolicyServiceLib.sol index 8eec347bc..7df054b94 100644 --- a/contracts/product/PolicyServiceLib.sol +++ b/contracts/product/PolicyServiceLib.sol @@ -95,13 +95,14 @@ library PolicyServiceLib { { if (policyState != COLLATERALIZED()) { revert IPolicyService.ErrorPolicyServicePolicyNotActive(policyNftId, policyState); - } - if (TimestampLib.current() < policyInfo.activatedAt) { + } + + if (policyInfo.activatedAt.eqz() || TimestampLib.current() < policyInfo.activatedAt) { revert IPolicyService.ErrorPolicyServicePolicyNotActive(policyNftId, policyState); } // check expiredAt represents a valid expiry time - if (newExpiredAt >= policyInfo.expiredAt) { + if (newExpiredAt > policyInfo.expiredAt) { revert IPolicyService.ErrorPolicyServicePolicyExpirationTooLate(policyNftId, policyInfo.expiredAt, newExpiredAt); } @@ -110,6 +111,7 @@ library PolicyServiceLib { } } + function policyIsCloseable(InstanceReader instanceReader, NftId policyNftId) external view diff --git a/scripts/deploy_flightdelay_components.ts b/scripts/deploy_flightdelay_components.ts index 2c40c10ce..5037baa60 100644 --- a/scripts/deploy_flightdelay_components.ts +++ b/scripts/deploy_flightdelay_components.ts @@ -196,6 +196,7 @@ export async function deployFlightDelayComponentContracts(libraries: LibraryAddr ObjectTypeLib: objectTypeLibAddress, ReferralLib: referralLibAddress, SecondsLib: secondsLibAddress, + StrLib: strLibAddress, TimestampLib: timestampLibAddress, VersionLib: versionLibAddress, } @@ -209,9 +210,31 @@ export async function deployFlightDelayComponentContracts(libraries: LibraryAddr [IInstance__factory.createInterface()] ); const flightProductNftId = await flightProduct.getNftId(); + + // // grant statistics provider role to statistics provider + // (RoleId statisticProviderRoleId, bool exists) = instanceReader.getRoleForName( + // productAuthz.STATISTICS_PROVIDER_ROLE_NAME()); + + // instance.grantRole(statisticProviderRoleId, statisticsProvider); + // vm.stopPrank(); + + // old function + // await executeTx(async () => + // await flightProduct.completeSetup(), + // "fd - completeSetup", + // [FlightProduct__factory.createInterface()] + // ); await executeTx(async () => - await flightProduct.completeSetup(), - "fd - completeSetup", + await flightProduct.setConstants( + 15 * 10 ** 6, // 15 USD min premium + 15 * 10 ** 6, // 15 USD max premium + 200 * 10 ** 6, // 15 USD max premium + 600 * 10 ** 6, // 15 USD max premium + 14 * 24 * 3600, // 14 days min time before departure + 90 * 24 * 3600, // 90 days max time before departure + 5, // max policies to process in one tx + ), + "fd - setConstants", [FlightProduct__factory.createInterface()] ); @@ -305,6 +328,8 @@ export async function deployFlightDelayComponentContracts(libraries: LibraryAddr ContractLib: contractLibAddress, NftIdLib: nftIdLibAddress, LibRequestIdSet: libRequestIdSetAddress, + StrLib: strLibAddress, + TimestampLib: timestampLibAddress, VersionLib: versionLibAddress, } }); diff --git a/scripts/libs/instance.ts b/scripts/libs/instance.ts index 093dadd7a..7081f52b7 100644 --- a/scripts/libs/instance.ts +++ b/scripts/libs/instance.ts @@ -178,6 +178,7 @@ export async function deployAndRegisterMasterInstance( RequestIdLib: libraries.requestIdLibAddress, RiskIdLib: libraries.riskIdLibAddress, RoleIdLib: libraries.roleIdLibAddress, + StrLib: libraries.strLibAddress, UFixedLib: libraries.uFixedLibAddress, } } diff --git a/test/component/product/ProductPolicy.t.sol b/test/component/product/ProductPolicy.t.sol index 853b298bb..a2f2d7ed5 100644 --- a/test/component/product/ProductPolicy.t.sol +++ b/test/component/product/ProductPolicy.t.sol @@ -1693,27 +1693,30 @@ contract ProductPolicyTest is GifTest { assertTrue(instanceReader.getPolicyState(policyNftId) == COLLATERALIZED(), "policy state not COLLATERALIZED"); uint256 expireAt = createdAt + 30; - Timestamp expireAtTs = TimestampLib.toTimestamp(expireAt); - + Timestamp expireAtOriginal = TimestampLib.toTimestamp(expireAt); + Timestamp expireAtNew = TimestampLib.toTimestamp(expireAt + 1); + // THEN - expect revert - vm.expectRevert(abi.encodeWithSelector( - IPolicyService.ErrorPolicyServicePolicyExpirationTooLate.selector, - policyNftId, - expireAtTs, - expireAtTs)); + vm.expectRevert( + abi.encodeWithSelector( + IPolicyService.ErrorPolicyServicePolicyExpirationTooLate.selector, + policyNftId, + expireAtOriginal, + expireAtNew)); // WHEN - product.expire(policyNftId, expireAtTs); + product.expire(policyNftId, expireAtNew); // THEN - expect revert uint256 expireAt2 = createdAt + 35; - Timestamp expireAtTs2 = TimestampLib.toTimestamp(expireAt); - - vm.expectRevert(abi.encodeWithSelector( - IPolicyService.ErrorPolicyServicePolicyExpirationTooLate.selector, - policyNftId, - expireAtTs, - expireAtTs2)); + Timestamp expireAtTs2 = TimestampLib.toTimestamp(expireAt2); + + vm.expectRevert( + abi.encodeWithSelector( + IPolicyService.ErrorPolicyServicePolicyExpirationTooLate.selector, + policyNftId, + expireAtOriginal, + expireAtTs2)); // WHEN product.expire(policyNftId, expireAtTs2); diff --git a/test/examples/flight/FlightBase.t.sol b/test/examples/flight/FlightBase.t.sol index 87a198787..b9af32bf9 100644 --- a/test/examples/flight/FlightBase.t.sol +++ b/test/examples/flight/FlightBase.t.sol @@ -20,6 +20,8 @@ import {NftId} from "../../../contracts/type/NftId.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 {SigUtils} from "./SigUtils.sol"; import {Str, StrLib} from "../../../contracts/type/String.sol"; import {Timestamp, TimestampLib} from "../../../contracts/type/Timestamp.sol"; import {VersionPartLib} from "../../../contracts/type/Version.sol"; @@ -27,11 +29,15 @@ import {VersionPartLib} from "../../../contracts/type/Version.sol"; contract FlightBaseTest is GifTest { + + SigUtils internal sigUtils; + address public flightOwner = makeAddr("flightOwner"); FlightUSD public flightUSD; FlightOracle public flightOracle; FlightPool public flightPool; + FlightProduct public flightProduct; NftId public flightOracleNftId; @@ -63,8 +69,91 @@ contract FlightBaseTest is GifTest { // do some initial funding _initialFundAccounts(); + + sigUtils = new SigUtils(flightUSD.DOMAIN_SEPARATOR()); + } + + + function _createPermitWithSignature( + address policyHolder, + Amount premiumAmount, + uint256 policyHolderPrivateKey, + uint256 nonce + ) + internal + view + returns (FlightProduct.PermitData memory permit) + { + SigUtils.Permit memory suPermit = SigUtils.Permit({ + owner: policyHolder, + spender: address(flightProduct.getTokenHandler()), + value: premiumAmount.toInt(), + nonce: nonce, + deadline: TimestampLib.current().toInt() + 3600 + }); + + bytes32 digest = sigUtils.getTypedDataHash(suPermit); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(policyHolderPrivateKey, digest); + + permit.owner = policyHolder; + permit.spender = address(flightProduct.getTokenHandler()); + permit.value = premiumAmount.toInt(); + permit.deadline = TimestampLib.current().toInt() + 3600; + permit.v = v; + permit.r = r; + permit.s = s; + } + + + function _createPolicySimple( + Str flightData, // example: "LX 180 ZRH BKK 20241104" + Timestamp departureTime, + Timestamp arrivalTime, + uint256 [6] memory statistics, + FlightProduct.PermitData memory permit + ) + internal + returns (NftId policyNftId) + { + return _createPolicy( + flightData, + departureTime, + "departure date time local timezone", + arrivalTime, + "arrival date time local timezome", + statistics, + permit + ); + } + + + function _createPolicy( + Str flightData, // example: "LX 180 ZRH BKK 20241104" + Timestamp departureTime, + string memory departureTimeLocal, // example "2024-10-14T10:10:00.000 Europe/Zurich" + Timestamp arrivalTime, + string memory arrivalTimeLocal, // example "2024-10-14T10:10:00.000 Asia/Seoul" + uint256 [6] memory statistics, + FlightProduct.PermitData memory permit + ) + internal + returns (NftId policyNftId) + { + (, policyNftId) = flightProduct.createPolicyWithPermit( + permit, + FlightProduct.ApplicationData({ + flightData: flightData, + departureTime: departureTime, + departureTimeLocal: departureTimeLocal, + arrivalTime: arrivalTime, + arrivalTimeLocal: arrivalTimeLocal, + premiumAmount: AmountLib.toAmount(permit.value), + statistics: statistics + }) + ); } + function _deployFlightUSD() internal { // deploy fire token vm.startPrank(flightOwner); @@ -100,6 +189,15 @@ contract FlightBaseTest is GifTest { "FlightProduct", productAuthz ); + + // flightProductManager = new FlightProductManager( + // address(registry), + // instanceNftId, + // "FlightProduct", + // productAuthz); + + // flightProduct = flightProductManager.getFlightProduct(); + vm.stopPrank(); // instance owner registeres fire product with instance (and registry) @@ -117,8 +215,17 @@ contract FlightBaseTest is GifTest { vm.stopPrank(); // complete setup - vm.prank(flightOwner); - flightProduct.completeSetup(); + vm.startPrank(flightOwner); + flightProduct.setConstants( + AmountLib.toAmount(15 * 10 ** flightUSD.decimals()), // 15 USD min premium + AmountLib.toAmount(15 * 10 ** flightUSD.decimals()), // 15 USD max premium + AmountLib.toAmount(200 * 10 ** flightUSD.decimals()), // 200 USD max payout + AmountLib.toAmount(600 * 10 ** flightUSD.decimals()), // 600 USD max total payout + SecondsLib.fromDays(14), // min time before departure + SecondsLib.fromDays(90), // max time before departure + 5 // max policies to process + ); + vm.stopPrank(); } @@ -194,6 +301,7 @@ contract FlightBaseTest is GifTest { (v, r, s) = vm.sign(signerPrivateKey, ratingsHash); } + function _createInitialBundle() internal returns (NftId bundleNftId) { vm.startPrank(flightOwner); Amount investAmount = AmountLib.toAmount(10000000 * 10 ** 6); diff --git a/test/examples/flight/FlightPricing.t.sol b/test/examples/flight/FlightPricing.t.sol index bdc64b91c..5ef44f5d1 100644 --- a/test/examples/flight/FlightPricing.t.sol +++ b/test/examples/flight/FlightPricing.t.sol @@ -116,7 +116,7 @@ contract FlightPricingTest is FlightBaseTest { function test_flightPricingCalculateSumInsuredHappyCase() public { // GIVEN - Amount premiumAmount = AmountLib.toAmount(100 * 10 ** flightUSD.decimals()); + Amount premiumAmount = AmountLib.toAmount(15 * 10 ** flightUSD.decimals()); // WHEN ( diff --git a/test/examples/flight/FlightProduct.t.sol b/test/examples/flight/FlightProduct.t.sol index 822de1eb1..366768fbb 100644 --- a/test/examples/flight/FlightProduct.t.sol +++ b/test/examples/flight/FlightProduct.t.sol @@ -3,6 +3,7 @@ pragma solidity ^0.8.20; import {console} from "../../../lib/forge-std/src/Test.sol"; +import {IAccess} from "../../../contracts/authorization/IAccess.sol"; import {INftOwnable} from "../../../contracts/shared/INftOwnable.sol"; import {IOracle} from "../../../contracts/oracle/IOracle.sol"; import {IPolicy} from "../../../contracts/instance/module/IPolicy.sol"; @@ -18,16 +19,15 @@ import {IBundle} from "../../../contracts/instance/module/IBundle.sol"; import {NftId} from "../../../contracts/type/NftId.sol"; import {RiskId} from "../../../contracts/type/RiskId.sol"; import {RequestId, RequestIdLib} from "../../../contracts/type/RequestId.sol"; -import {SecondsLib} from "../../../contracts/type/Seconds.sol"; -import {SigUtils} from "./SigUtils.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 FlightProductTest is FlightBaseTest { - SigUtils internal sigUtils; - // sample flight data Str public flightData = StrLib.toStr("LX 180 ZRH BKK 20241108"); Timestamp public departureTime = TimestampLib.toTimestamp(1731085200); @@ -45,8 +45,6 @@ contract FlightProductTest is FlightBaseTest { function setUp() public override { super.setUp(); - sigUtils = new SigUtils(flightUSD.DOMAIN_SEPARATOR()); - // set time to somewhere before devcon in bkk vm.warp(1726260993); @@ -97,155 +95,196 @@ contract FlightProductTest is FlightBaseTest { assertEq(flightProduct.getWallet(), address(flightProduct.getTokenHandler()), "unexpected product wallet address"); } - // TODO cleanup only createPolicyWithPermit is now public/external - // function test_flightProductCreatePolicyHappyCase() public { - // // GIVEN - setp from flight base test - // approveProductTokenHandler(); - - // uint256 customerBalanceBefore = flightUSD.balanceOf(customer); - // uint256 poolBalanceBefore = flightUSD.balanceOf(flightPool.getWallet()); - // uint256 productBalanceBefore = flightUSD.balanceOf(flightProduct.getWallet()); - // Amount premiumAmount = AmountLib.toAmount(30 * 10 ** flightUSD.decimals()); - - // assertEq(instanceReader.risks(flightProductNftId), 0, "unexpected number of risks (before)"); - // assertEq(instanceReader.activeRisks(flightProductNftId), 0, "unexpected number of active risks (before)"); - // assertEq(flightOracle.activeRequests(), 0, "unexpected number of active requests (before)"); - - // (uint8 v, bytes32 r, bytes32 s) = _getSignature( - // dataSignerPrivateKey, - // flightData, - // departureTime, - // arrivalTime, - // premiumAmount, - // statistics); - - // // WHEN - // vm.startPrank(statisticsProvider); - // (, NftId policyNftId) = flightProduct.createPolicy( - // customer, - // flightData, - // departureTime, - // "2024-11-08 Europe/Zurich", - // arrivalTime, - // "2024-11-08 Europe/Bangkok", - // premiumAmount, - // statistics); - // vm.stopPrank(); - - // // THEN - // // check risks - // assertEq(instanceReader.risks(flightProductNftId), 1, "unexpected number of risks (after)"); - // assertEq(instanceReader.activeRisks(flightProductNftId), 1, "unexpected number of active risks (after)"); - - // RiskId riskId = instanceReader.getRiskId(flightProductNftId, 0); - // (bool exists, FlightProduct.FlightRisk memory flightRisk) = FlightLib.getFlightRisk(instanceReader, flightProductNftId, riskId); - // _printRisk(riskId, flightRisk); - - // assertTrue(exists, "risk does not exist"); - // assertEq(instanceReader.policiesForRisk(riskId), 1, "unexpected number of policies for risk"); - // assertEq(instanceReader.getPolicyForRisk(riskId, 0).toInt(), policyNftId.toInt(), "unexpected 1st policy for risk"); - - // // check policy - // assertTrue(policyNftId.gtz(), "policy nft id zero"); - // assertEq(registry.ownerOf(policyNftId), customer, "unexpected policy holder"); - // assertEq(instanceReader.getPolicyState(policyNftId).toInt(), COLLATERALIZED().toInt(), "unexpected policy state"); - - // // check policy info - // IPolicy.PolicyInfo memory policyInfo = instanceReader.getPolicyInfo(policyNftId); - // _printPolicy(policyNftId, policyInfo); - - // // check policy data - // assertTrue(instanceReader.isProductRisk(flightProductNftId, policyInfo.riskId), "risk does not exist for product"); - // assertEq(policyInfo.productNftId.toInt(), flightProductNftId.toInt(), "unexpected product nft id"); - // assertEq(policyInfo.bundleNftId.toInt(), bundleNftId.toInt(), "unexpected bundle nft id"); - // assertEq(policyInfo.activatedAt.toInt(), departureTime.toInt(), "unexpected activate at timestamp"); - // assertEq(policyInfo.lifetime.toInt(), flightProduct.LIFETIME().toInt(), "unexpected lifetime"); - // assertTrue(policyInfo.sumInsuredAmount > premiumAmount, "sum insured <= premium amount"); - - // // check premium info - // IPolicy.PremiumInfo memory premiumInfo = instanceReader.getPremiumInfo(policyNftId); - // _printPremium(policyNftId, premiumInfo); - // assertEq(instanceReader.getPremiumState(policyNftId).toInt(), PAID().toInt(), "unexpected premium state"); - - // // check token balances - // assertEq(flightUSD.balanceOf(flightProduct.getWallet()), productBalanceBefore, "unexpected product balance"); - // assertEq(flightUSD.balanceOf(flightPool.getWallet()), poolBalanceBefore + premiumAmount.toInt(), "unexpected pool balance"); - // assertEq(flightUSD.balanceOf(customer), customerBalanceBefore - premiumAmount.toInt(), "unexpected customer balance"); - - // // check oracle request - // assertEq(flightOracle.activeRequests(), 1, "unexpected number of active requests (after policy creation)"); - - // RequestId requestId = flightOracle.getActiveRequest(0); - // assertTrue(requestId.gtz(), "request id zero"); - - // IOracle.RequestInfo memory requestInfo = instanceReader.getRequestInfo(requestId); - // _printRequest(requestId, requestInfo); - - // FlightOracle.FlightStatusRequest memory statusRequest = abi.decode(requestInfo.requestData, (FlightOracle.FlightStatusRequest)); - // _printStatusRequest(statusRequest); - - // assertEq(statusRequest.riskId.toInt(), riskId.toInt(), "unexpected risk id"); - // assertTrue(statusRequest.flightData == flightData, "unexpected flight data"); - // assertEq(statusRequest.departureTime.toInt(), departureTime.toInt(), "unexpected departure time"); - // } - - function _createPermitWithSignature( - address policyHolder, - Amount premiumAmount, - uint256 policyHolderPrivateKey, - uint256 nonce - ) - internal - view - returns (FlightProduct.PermitData memory permit) - { - SigUtils.Permit memory suPermit = SigUtils.Permit({ - owner: policyHolder, - spender: address(flightProduct.getTokenHandler()), - value: premiumAmount.toInt(), - nonce: nonce, - deadline: TimestampLib.current().toInt() + 3600 - }); - - bytes32 digest = sigUtils.getTypedDataHash(suPermit); - (uint8 v, bytes32 r, bytes32 s) = vm.sign(policyHolderPrivateKey, digest); - - permit.owner = policyHolder; - permit.spender = address(flightProduct.getTokenHandler()); - permit.value = premiumAmount.toInt(); - permit.deadline = TimestampLib.current().toInt() + 3600; - permit.v = v; - permit.r = r; - permit.s = s; + + function test_flightProductSetTestMode() public { + // GIVEN - setup from flight base test + + assertFalse(flightProduct.isTestMode(), "test mode already set"); + + (RoleId publicRoleId, ) = instanceReader.getRoleForName("PublicRole"); + IAccess.FunctionInfo memory setTestModeFunction = instanceReader.toFunction( + FlightProduct.setTestMode.selector, + "setTestMode"); + + IAccess.FunctionInfo[] memory functions = new IAccess.FunctionInfo[](1); + functions[0] = setTestModeFunction; + + vm.prank(instanceOwner); + instance.authorizeFunctions(address(flightProduct), publicRoleId, functions); + + console.log("setTestMode selector"); + console.logBytes4(FlightProduct.setTestMode.selector); + + // WHEN + vm.prank(flightOwner); + flightProduct.setTestMode(true); + + // THEN + assertTrue(flightProduct.isTestMode(), "test mode not set"); + + // assertTrue(false, "oops"); } - function _createPolicy( - Str flightData, // example: "LX 180 ZRH BKK 20241104" - Timestamp departureTime, - string memory departureTimeLocal, // example "2024-10-14T10:10:00.000 Europe/Zurich" - Timestamp arrivalTime, - string memory arrivalTimeLocal, // example "2024-10-14T10:10:00.000 Asia/Seoul" - uint256 [6] memory statistics, - FlightProduct.PermitData memory permit - ) - internal - returns (NftId policyNftId) - { - (, policyNftId) = flightProduct.createPolicyWithPermit( - permit, - FlightProduct.ApplicationData({ - flightData: flightData, - departureTime: departureTime, - departureTimeLocal: departureTimeLocal, - arrivalTime: arrivalTime, - arrivalTimeLocal: arrivalTimeLocal, - premiumAmount: AmountLib.toAmount(permit.value), - statistics: statistics - }) - ); + function test_flightProductCreatePolicyTestModeFlightSevenDayAgo() public { + // GIVEN - setup from flight base test + + vm.prank(flightOwner); + flightProduct.setTestMode(true); + + assertTrue(flightProduct.isTestMode(), "test mode already set"); + + Amount premiumAmount = flightProduct.MAX_PREMIUM(); // AmountLib.toAmount(30 * 10 ** flightUSD.decimals()); + (FlightProduct.PermitData memory permit) = _createPermitWithSignature( + customer, + premiumAmount, + customerPrivateKey, + 0); // nonce + + // WHEN + Timestamp departureTimeModified = TimestampLib.toTimestamp(block.timestamp - 7 * 24 * 3600); + Timestamp arrivalTimeModified = departureTimeModified.addSeconds( + SecondsLib.toSeconds(5 * 3600)); // 5h flight + + Timestamp currentTime = TimestampLib.current(); + Seconds minTimeBeforeDeparture = flightProduct.MIN_TIME_BEFORE_DEPARTURE(); + + vm.startPrank(statisticsProvider); + NftId policyNftId = _createPolicySimple( + flightData, + departureTimeModified, + arrivalTimeModified, + statistics, + permit); + vm.stopPrank(); + + // THEN + assertTrue(policyNftId.gtz(), "policy nft id zero"); + } + + + function test_flightProductCreatePolicyTestModeFlightInOneHour() public { + // GIVEN - setup from flight base test + + vm.prank(flightOwner); + flightProduct.setTestMode(true); + + assertTrue(flightProduct.isTestMode(), "test mode already set"); + + Amount premiumAmount = flightProduct.MAX_PREMIUM(); // AmountLib.toAmount(30 * 10 ** flightUSD.decimals()); + (FlightProduct.PermitData memory permit) = _createPermitWithSignature( + customer, + premiumAmount, + customerPrivateKey, + 0); // nonce + + // WHEN + Timestamp departureTimeModified = TimestampLib.current().addSeconds( + SecondsLib.toSeconds(3600)); + Timestamp arrivalTimeModified = departureTimeModified.addSeconds( + SecondsLib.toSeconds(5 * 3600)); // 5h flight + + Timestamp currentTime = TimestampLib.current(); + Seconds minTimeBeforeDeparture = flightProduct.MIN_TIME_BEFORE_DEPARTURE(); + + vm.startPrank(statisticsProvider); + NftId policyNftId = _createPolicySimple( + flightData, + departureTimeModified, + arrivalTimeModified, + statistics, + permit); + vm.stopPrank(); + + // THEN + assertTrue(policyNftId.gtz(), "policy nft id zero"); } + + function test_flightProductCreatePolicyTestModeFlightInOneYear() public { + // GIVEN - setup from flight base test + + vm.prank(flightOwner); + flightProduct.setTestMode(true); + + assertTrue(flightProduct.isTestMode(), "test mode already set"); + + Amount premiumAmount = flightProduct.MAX_PREMIUM(); // AmountLib.toAmount(30 * 10 ** flightUSD.decimals()); + (FlightProduct.PermitData memory permit) = _createPermitWithSignature( + customer, + premiumAmount, + customerPrivateKey, + 0); // nonce + + // WHEN + Timestamp departureTimeModified = TimestampLib.current().addSeconds( + SecondsLib.toSeconds(365 * 24 * 3600)); + Timestamp arrivalTimeModified = departureTimeModified.addSeconds( + SecondsLib.toSeconds(5 * 3600)); // 5h flight + + Timestamp currentTime = TimestampLib.current(); + Seconds minTimeBeforeDeparture = flightProduct.MIN_TIME_BEFORE_DEPARTURE(); + + vm.startPrank(statisticsProvider); + NftId policyNftId = _createPolicySimple( + flightData, + departureTimeModified, + arrivalTimeModified, + statistics, + permit); + vm.stopPrank(); + + // THEN + assertTrue(policyNftId.gtz(), "policy nft id zero"); + } + + + function test_flightProductCreateAndTriggerPolicyTestModeFlightSevenDaysAgo() public { + // GIVEN - setup from flight base test + + vm.prank(flightOwner); + flightProduct.setTestMode(true); + + assertTrue(flightProduct.isTestMode(), "test mode already set"); + + Amount premiumAmount = flightProduct.MAX_PREMIUM(); // AmountLib.toAmount(30 * 10 ** flightUSD.decimals()); + (FlightProduct.PermitData memory permit) = _createPermitWithSignature( + customer, + premiumAmount, + customerPrivateKey, + 0); // nonce + + // WHEN + Timestamp departureTimeModified = TimestampLib.toTimestamp(block.timestamp - 7 * 24 * 3600); + Timestamp arrivalTimeModified = departureTimeModified.addSeconds( + SecondsLib.toSeconds(5 * 3600)); // 5h flight + + Timestamp currentTime = TimestampLib.current(); + Seconds minTimeBeforeDeparture = flightProduct.MIN_TIME_BEFORE_DEPARTURE(); + + vm.startPrank(statisticsProvider); + NftId policyNftId = _createPolicySimple( + flightData, + departureTimeModified, + arrivalTimeModified, + statistics, + permit); + vm.stopPrank(); + + // THEN + assertTrue(policyNftId.gtz(), "policy nft id zero"); + assertEq(flightOracle.activeRequests(), 1, "unexpected number of active requests (before status callback)"); + RequestId requestId = flightOracle.getActiveRequest(0); + assertEq(requestId.toInt(), 1, "unexpected request id"); + + // WHEN + bytes1 status = "L"; + int256 delayMinutes = 16; + vm.startPrank(statusProvider); + flightOracle.respondWithFlightStatus(requestId, status, delayMinutes); + } + + function test_flightProductCreatePolicyWithPermitHappyCase() public { // GIVEN - setp from flight base test approveProductTokenHandler(); @@ -253,7 +292,7 @@ contract FlightProductTest is FlightBaseTest { uint256 customerBalanceBefore = flightUSD.balanceOf(customer); uint256 poolBalanceBefore = flightUSD.balanceOf(flightPool.getWallet()); uint256 productBalanceBefore = flightUSD.balanceOf(flightProduct.getWallet()); - Amount premiumAmount = AmountLib.toAmount(30 * 10 ** flightUSD.decimals()); + Amount premiumAmount = flightProduct.MAX_PREMIUM(); // AmountLib.toAmount(30 * 10 ** flightUSD.decimals()); assertEq(instanceReader.risks(flightProductNftId), 0, "unexpected number of risks (before)"); assertEq(instanceReader.activeRisks(flightProductNftId), 0, "unexpected number of active risks (before)"); @@ -263,19 +302,6 @@ contract FlightProductTest is FlightBaseTest { console.log("ts", block.timestamp); // solhint-enable - // TODO cleanup - // SigUtils.Permit memory permit = SigUtils.Permit({ - // owner: customer, - // spender: address(flightProduct.getTokenHandler()), - // value: premiumAmount.toInt(), - // nonce: 0, - // deadline: TimestampLib.current().toInt() + 3600 - // }); - - // // vm.startPrank(customer); - // bytes32 digest = sigUtils.getTypedDataHash(permit); - // (uint8 permitV, bytes32 permitR, bytes32 permitS) = vm.sign(customerPrivateKey, digest); - // // vm.stopPrank(); (FlightProduct.PermitData memory permit) = _createPermitWithSignature( customer, premiumAmount, @@ -292,27 +318,6 @@ contract FlightProductTest is FlightBaseTest { "2024-11-08 Asia/Bangkok", statistics, permit); - - // (, NftId policyNftId) = flightProduct.createPolicyWithPermit( - // FlightProduct.PermitData({ - // owner: customer, - // spender: address(flightProduct.getTokenHandler()), - // value: premiumAmount.toInt(), - // deadline: TimestampLib.current().toInt() + 3600, - // v: permit_v, - // r: permit_r, - // s: permit_s - // }), - // FlightProduct.ApplicationData({ - // flightData: flightData, - // departureTime: departureTime, - // departureTimeLocal: "2024-11-08 Europe/Zurich", - // arrivalTime: arrivalTime, - // arrivalTimeLocal: "2024-11-08 Asia/Bangkok", - // premiumAmount: premiumAmount, - // statistics: statistics - // }) - // ); vm.stopPrank(); // THEN @@ -322,7 +327,7 @@ contract FlightProductTest is FlightBaseTest { assertEq(instanceReader.activeRisks(flightProductNftId), 1, "unexpected number of active risks (after)"); RiskId riskId = instanceReader.getRiskId(flightProductNftId, 0); - (bool exists, FlightProduct.FlightRisk memory flightRisk) = FlightLib.getFlightRisk(instanceReader, flightProductNftId, riskId); + (bool exists, FlightProduct.FlightRisk memory flightRisk) = FlightLib.getFlightRisk(instanceReader, flightProductNftId, riskId, false); _printRisk(riskId, flightRisk); assertTrue(exists, "risk does not exist"); @@ -379,17 +384,149 @@ contract FlightProductTest is FlightBaseTest { } } - function test_flightCreatePolicyAndProcessFlightStatus() public { + + function test_flightProductCreatePolicyAndCheckRequest() public { + // GIVEN - setp from flight base test + approveProductTokenHandler(); + + uint256 customerBalanceBefore = flightUSD.balanceOf(customer); + uint256 poolBalanceBefore = flightUSD.balanceOf(flightPool.getWallet()); + uint256 productBalanceBefore = flightUSD.balanceOf(flightProduct.getWallet()); + Amount premiumAmount = flightProduct.MAX_PREMIUM(); // AmountLib.toAmount(30 * 10 ** flightUSD.decimals()); + + assertEq(instanceReader.risks(flightProductNftId), 0, "unexpected number of risks (before)"); + assertEq(instanceReader.activeRisks(flightProductNftId), 0, "unexpected number of active risks (before)"); + assertEq(flightOracle.activeRequests(), 0, "unexpected number of active requests (before)"); + + // solhint-disable + console.log("ts", block.timestamp); + // solhint-enable + + (FlightProduct.PermitData memory permit) = _createPermitWithSignature( + customer, + premiumAmount, + customerPrivateKey, + 0); // nonce + + // WHEN + vm.startPrank(statisticsProvider); + NftId policyNftId = _createPolicy( + flightData, + departureTime, + "2024-11-08 Europe/Zurich", + arrivalTime, + "2024-11-08 Asia/Bangkok", + statistics, + permit); + vm.stopPrank(); + + // THEN + RequestId requestId = flightOracle.getActiveRequest(0); + ( + RiskId riskId, + string memory flightData, + StateId requestState, + bool readyForResponse, + bool waitingForResend + ) = flightOracle.getRequestState(requestId); + + // solhint-disable + console.log("--- after policy creation (before departure) ---"); + console.log("request id", requestId.toInt(), "risk id"); + console.logBytes8(RiskId.unwrap(riskId)); + console.log("flight data", flightData, "request state", requestState.toInt()); + // console.log("request state", requestState.toInt()); + console.log("readyForResponse, waitingForResend", readyForResponse, waitingForResend); + // solhint-enable + + assertEq(requestState.toInt(), ACTIVE().toInt(), "unexpected request state (not active)"); + assertFalse(readyForResponse, "ready for response (1)"); + assertFalse(waitingForResend, "waiting for resend (1)"); + + // WHEN wait until after scheduled departure + vm.warp(departureTime.toInt() + 1); + + // THEN + ( + riskId, + flightData, + requestState, + readyForResponse, + waitingForResend + ) = flightOracle.getRequestState(requestId); + + assertTrue(readyForResponse, "ready for response (2)"); + assertFalse(waitingForResend, "waiting for resend (2)"); + + // WHEN send flight cancelled using insufficient gas + bytes1 status = "C"; + int256 delay = 0; + uint8 maxPoliciesToProcess = 1; + // gas amount experimentally determined to make the tx run out of gas while inside product callback + // this results in an updated request but in a failed callback + uint256 insufficientGas = 1300000; + + vm.startPrank(statusProvider); + flightOracle.respondWithFlightStatus{gas:insufficientGas}(requestId, status, delay); + vm.stopPrank(); + + // THEN + ( + riskId, + flightData, + requestState, + readyForResponse, + waitingForResend + ) = flightOracle.getRequestState(requestId); + + // solhint-disable + console.log("--- after response with insufficient gas ---"); + console.log("request id", requestId.toInt(), "risk id"); + console.logBytes8(RiskId.unwrap(riskId)); + console.log("flight data", flightData, "request state", requestState.toInt()); + // console.log("request state", requestState.toInt()); + console.log("readyForResponse, waitingForResend", readyForResponse, waitingForResend); + // solhint-enable + + assertEq(requestState.toInt(), FAILED().toInt(), "unexpected request state (not failed)"); + assertFalse(readyForResponse, "ready for response (3)"); + assertTrue(waitingForResend, "waiting for resend (3)"); + + // WHEN resend request (with sufficient gas) + vm.startPrank(flightOwner); + flightProduct.resendRequest(requestId); + vm.stopPrank(); + + // THEN + ( + riskId, + flightData, + requestState, + readyForResponse, + waitingForResend + ) = flightOracle.getRequestState(requestId); + + // solhint-disable + console.log("--- after resend ---"); + console.log("request id", requestId.toInt(), "risk id"); + console.logBytes8(RiskId.unwrap(riskId)); + console.log("flight data", flightData, "request state", requestState.toInt()); + // console.log("request state", requestState.toInt()); + console.log("readyForResponse, waitingForResend", readyForResponse, waitingForResend); + // solhint-enable + + assertEq(requestState.toInt(), FULFILLED().toInt(), "unexpected request state (not failed)"); + assertFalse(readyForResponse, "ready for response (4)"); + assertFalse(waitingForResend, "waiting for resend (4)"); + } + + + function test_flightCreatePolicyAndProcessFlightStatusWithPayout() public { // GIVEN - create policy approveProductTokenHandler(); - Amount premiumAmount = AmountLib.toAmount(30 * 10 ** flightUSD.decimals()); + Amount premiumAmount = flightProduct.MAX_PREMIUM(); // AmountLib.toAmount(30 * 10 ** flightUSD.decimals()); - // TODO cleanup - // vm.startPrank(customer); - // bytes32 digest = sigUtils.getTypedDataHash(permit); - // (uint8 permitV, bytes32 permitR, bytes32 permitS) = vm.sign(customerPrivateKey, digest); - // vm.stopPrank(); (FlightProduct.PermitData memory permit) = _createPermitWithSignature( customer, premiumAmount, @@ -406,15 +543,6 @@ contract FlightProductTest is FlightBaseTest { "2024-11-08 Asia/Bangkok", statistics, permit); - // (RiskId riskId, NftId policyNftId) = flightProduct.createPolicy( - // customer, - // flightData, - // departureTime, - // "2024-11-08 Europe/Zurich", - // arrivalTime, - // "2024-11-08 Europe/Bangkok", - // premiumAmount, - // statistics); vm.stopPrank(); assertEq(flightOracle.activeRequests(), 1, "unexpected number of active requests (before status callback)"); @@ -450,4 +578,63 @@ contract FlightProductTest is FlightBaseTest { // assertTrue(false, "oops"); } + + + function test_flightCreatePolicyAndProcessFlightStatusWithoutPayout() public { + // GIVEN - create policy + approveProductTokenHandler(); + + Amount premiumAmount = flightProduct.MAX_PREMIUM(); // AmountLib.toAmount(30 * 10 ** flightUSD.decimals()); + + (FlightProduct.PermitData memory permit) = _createPermitWithSignature( + customer, + premiumAmount, + customerPrivateKey, + 0); // nonce + + // WHEN + vm.startPrank(statisticsProvider); + NftId policyNftId = _createPolicy( + flightData, + departureTime, + "2024-11-08 Europe/Zurich", + arrivalTime, + "2024-11-08 Asia/Bangkok", + statistics, + permit); + vm.stopPrank(); + + assertEq(flightOracle.activeRequests(), 1, "unexpected number of active requests (before status callback)"); + RequestId requestId = flightOracle.getActiveRequest(0); + assertTrue(requestId.gtz(), "request id zero"); + + // create flight status data (90 min late) + bytes1 status = "L"; + int256 delay = 22; + uint8 maxPoliciesToProcess = 1; + + // print request before allback + IOracle.RequestInfo memory requestInfo = instanceReader.getRequestInfo(requestId); + _printRequest(requestId, requestInfo); + + // WHEN + // set cheking time 2h after scheduled arrival time + vm.warp(arrivalTime.toInt() + 2 * 3600); + + vm.startPrank(statusProvider); + flightOracle.respondWithFlightStatus(requestId, status, delay); + vm.stopPrank(); + + // THEN + requestInfo = instanceReader.getRequestInfo(requestId); + _printRequest(requestId, requestInfo); + + assertEq(flightOracle.activeRequests(), 0, "unexpected number of active requests (after status callback)"); + + _printPolicy( + policyNftId, + instanceReader.getPolicyInfo(policyNftId)); + + // assertTrue(false, "oops"); + } } \ No newline at end of file