diff --git a/.gitmodules b/.gitmodules index 8055ab5..b3326a6 100644 --- a/.gitmodules +++ b/.gitmodules @@ -10,3 +10,6 @@ [submodule "lib/filecoin-solidity"] path = lib/filecoin-solidity url = https://github.com/filecoin-project/filecoin-solidity +[submodule "lib/filecoin-pay"] + path = lib/filecoin-pay + url = https://github.com/FilOzone/filecoin-pay diff --git a/abis/Operator.json b/abis/Operator.json new file mode 100644 index 0000000..2ba6e68 --- /dev/null +++ b/abis/Operator.json @@ -0,0 +1,59 @@ +[ + { + "type": "function", + "name": "createRail", + "inputs": [ + { + "name": "token", + "type": "address", + "internalType": "contract IERC20" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "modifyRailPayment", + "inputs": [ + { + "name": "railId", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "terminateRail", + "inputs": [ + { + "name": "railId", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "updateLockupPeriod", + "inputs": [ + { + "name": "railId", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "newLockupPeriod", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + } +] diff --git a/abis/PoRepMarket.json b/abis/PoRepMarket.json index 98f21ef..ca56025 100644 --- a/abis/PoRepMarket.json +++ b/abis/PoRepMarket.json @@ -77,7 +77,7 @@ { "name": "completedDeals", "type": "tuple[]", - "internalType": "struct PoRepMarket.DealProposal[]", + "internalType": "struct PoRepTypes.DealProposal[]", "components": [ { "name": "dealId", @@ -151,7 +151,7 @@ { "name": "state", "type": "uint8", - "internalType": "enum PoRepMarket.DealState" + "internalType": "enum PoRepTypes.DealState" }, { "name": "railId", @@ -182,7 +182,7 @@ { "name": "", "type": "tuple", - "internalType": "struct PoRepMarket.DealProposal", + "internalType": "struct PoRepTypes.DealProposal", "components": [ { "name": "dealId", @@ -256,7 +256,7 @@ { "name": "state", "type": "uint8", - "internalType": "enum PoRepMarket.DealState" + "internalType": "enum PoRepTypes.DealState" }, { "name": "railId", @@ -532,6 +532,29 @@ ], "stateMutability": "view" }, + { + "type": "function", + "name": "terminateDeal", + "inputs": [ + { + "name": "dealId", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "terminator", + "type": "address", + "internalType": "address" + }, + { + "name": "endEpoch", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, { "type": "function", "name": "updateManifestLocation", @@ -717,6 +740,12 @@ "type": "string", "indexed": false, "internalType": "string" + }, + { + "name": "totalDealSize", + "type": "uint256", + "indexed": false, + "internalType": "uint256" } ], "anonymous": false @@ -740,6 +769,31 @@ ], "anonymous": false }, + { + "type": "event", + "name": "DealTerminated", + "inputs": [ + { + "name": "dealId", + "type": "uint256", + "indexed": true, + "internalType": "uint256" + }, + { + "name": "terminator", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "endEpoch", + "type": "uint256", + "indexed": true, + "internalType": "uint256" + } + ], + "anonymous": false + }, { "type": "event", "name": "Initialized", @@ -936,6 +990,22 @@ } ] }, + { + "type": "error", + "name": "CallerIsNotValidator", + "inputs": [ + { + "name": "dealId", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "caller", + "type": "address", + "internalType": "address" + } + ] + }, { "type": "error", "name": "DealDoesNotExist", @@ -953,12 +1023,12 @@ { "name": "currentState", "type": "uint8", - "internalType": "enum PoRepMarket.DealState" + "internalType": "enum PoRepTypes.DealState" }, { "name": "expectedState", "type": "uint8", - "internalType": "enum PoRepMarket.DealState" + "internalType": "enum PoRepTypes.DealState" } ] }, diff --git a/abis/Validator.json b/abis/Validator.json index e89c599..55f6799 100644 --- a/abis/Validator.json +++ b/abis/Validator.json @@ -17,6 +17,45 @@ ], "stateMutability": "view" }, + { + "type": "function", + "name": "POREP_SERVICE_ROLE", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "bytes32", + "internalType": "bytes32" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "createRail", + "inputs": [ + { + "name": "token", + "type": "address", + "internalType": "contract IERC20" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "disableFutureRailPayments", + "inputs": [ + { + "name": "railId", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, { "type": "function", "name": "getRoleAdmin", @@ -83,42 +122,78 @@ "name": "initialize", "inputs": [ { - "name": "", + "name": "_admin", "type": "address", "internalType": "address" }, { - "name": "", + "name": "_porepService", "type": "address", "internalType": "address" }, { - "name": "", + "name": "_filecoinPay", "type": "address", "internalType": "address" }, { - "name": "", + "name": "_SLIScorer", "type": "address", "internalType": "address" }, { - "name": "", + "name": "_clientSC", "type": "address", "internalType": "address" }, { - "name": "", + "name": "_poRepMarket", "type": "address", "internalType": "address" }, { - "name": "", + "name": "_SPRegistry", "type": "address", "internalType": "address" }, { - "name": "", + "name": "_dealId", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "modifyRailPayment", + "inputs": [ + { + "name": "railId", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "railTerminated", + "inputs": [ + { + "name": "railId", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "terminator", + "type": "address", + "internalType": "address" + }, + { + "name": "endEpoch", "type": "uint256", "internalType": "uint256" } @@ -162,6 +237,24 @@ "outputs": [], "stateMutability": "nonpayable" }, + { + "type": "function", + "name": "setDealEndEpoch", + "inputs": [ + { + "name": "dealId", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "endEpoch", + "type": "int64", + "internalType": "CommonTypes.ChainEpoch" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, { "type": "function", "name": "supportsInterface", @@ -181,6 +274,112 @@ ], "stateMutability": "view" }, + { + "type": "function", + "name": "terminateRail", + "inputs": [ + { + "name": "railId", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "updateLockupPeriod", + "inputs": [ + { + "name": "railId", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "newLockupPeriod", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "validatePayment", + "inputs": [ + { + "name": "railId", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "proposedAmount", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "fromEpoch", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "toEpoch", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "rate", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [ + { + "name": "result", + "type": "tuple", + "internalType": "struct IValidator.ValidationResult", + "components": [ + { + "name": "modifiedAmount", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "settleUpto", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "note", + "type": "string", + "internalType": "string" + } + ] + } + ], + "stateMutability": "nonpayable" + }, + { + "type": "event", + "name": "DealEndEpochUpdated", + "inputs": [ + { + "name": "dealId", + "type": "uint256", + "indexed": true, + "internalType": "uint256" + }, + { + "name": "endEpoch", + "type": "int64", + "indexed": false, + "internalType": "CommonTypes.ChainEpoch" + } + ], + "anonymous": false + }, { "type": "event", "name": "Initialized", @@ -194,6 +393,82 @@ ], "anonymous": false }, + { + "type": "event", + "name": "LockupPeriodUpdated", + "inputs": [ + { + "name": "railId", + "type": "uint256", + "indexed": true, + "internalType": "uint256" + }, + { + "name": "newLockupPeriod", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "RailDisabled", + "inputs": [ + { + "name": "railId", + "type": "uint256", + "indexed": true, + "internalType": "uint256" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "RailPaymentModified", + "inputs": [ + { + "name": "railId", + "type": "uint256", + "indexed": true, + "internalType": "uint256" + }, + { + "name": "newRate", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "RailTerminated", + "inputs": [ + { + "name": "railId", + "type": "uint256", + "indexed": true, + "internalType": "uint256" + }, + { + "name": "terminator", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "endEpoch", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + } + ], + "anonymous": false + }, { "type": "event", "name": "RoleAdminChanged", @@ -290,14 +565,146 @@ } ] }, + { + "type": "error", + "name": "CallerIsNotClient", + "inputs": [] + }, + { + "type": "error", + "name": "CallerIsNotClientSC", + "inputs": [] + }, + { + "type": "error", + "name": "CallerIsNotFilecoinPay", + "inputs": [] + }, + { + "type": "error", + "name": "DealNotCompleted", + "inputs": [ + { + "name": "dealId", + "type": "uint256", + "internalType": "uint256" + } + ] + }, + { + "type": "error", + "name": "InvalidAdminAddress", + "inputs": [] + }, + { + "type": "error", + "name": "InvalidClientSCAddress", + "inputs": [] + }, + { + "type": "error", + "name": "InvalidDealDuration", + "inputs": [] + }, + { + "type": "error", + "name": "InvalidDealId", + "inputs": [] + }, + { + "type": "error", + "name": "InvalidFilecoinPayAddress", + "inputs": [] + }, { "type": "error", "name": "InvalidInitialization", "inputs": [] }, + { + "type": "error", + "name": "InvalidLockupAllowance", + "inputs": [] + }, + { + "type": "error", + "name": "InvalidPoRepMarketAddress", + "inputs": [] + }, + { + "type": "error", + "name": "InvalidPoRepServiceAddress", + "inputs": [] + }, + { + "type": "error", + "name": "InvalidRailId", + "inputs": [ + { + "name": "expected", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "actual", + "type": "uint256", + "internalType": "uint256" + } + ] + }, + { + "type": "error", + "name": "InvalidRateAllowance", + "inputs": [] + }, + { + "type": "error", + "name": "InvalidSLIScorerAddress", + "inputs": [] + }, + { + "type": "error", + "name": "InvalidSPRegistryAddress", + "inputs": [] + }, + { + "type": "error", + "name": "InvalidSectorCount", + "inputs": [] + }, + { + "type": "error", + "name": "InvalidZeroAmount", + "inputs": [] + }, + { + "type": "error", + "name": "MaxLockupPeriodLessThanMinimum", + "inputs": [] + }, + { + "type": "error", + "name": "NegativeEndEpoch", + "inputs": [] + }, { "type": "error", "name": "NotInitializing", "inputs": [] + }, + { + "type": "error", + "name": "OperatorNotApproved", + "inputs": [] + }, + { + "type": "error", + "name": "RailAlreadyCreated", + "inputs": [] + }, + { + "type": "error", + "name": "UnauthorizedCaller", + "inputs": [] } ] diff --git a/foundry.lock b/foundry.lock index 765530b..27a9f07 100644 --- a/foundry.lock +++ b/foundry.lock @@ -1,4 +1,10 @@ { + "lib/filecoin-pay": { + "tag": { + "name": "v1.0.0", + "rev": "f0a40fe287ecb08c2c20b828bdbadd2437988bba" + } + }, "lib/filecoin-solidity": { "tag": { "name": "v1.1.2", diff --git a/lib/filecoin-pay b/lib/filecoin-pay new file mode 160000 index 0000000..f0a40fe --- /dev/null +++ b/lib/filecoin-pay @@ -0,0 +1 @@ +Subproject commit f0a40fe287ecb08c2c20b828bdbadd2437988bba diff --git a/remappings.txt b/remappings.txt index e2cbfdc..65a7f99 100644 --- a/remappings.txt +++ b/remappings.txt @@ -11,4 +11,8 @@ openzeppelin-contracts-upgradeable/=lib/openzeppelin-contracts-upgradeable/ openzeppelin-contracts/=lib/openzeppelin-contracts/ openzeppelin/=lib/filecoin-solidity/lib/openzeppelin-contracts-upgradeable/contracts/ solidity-BigNumber/=lib/filecoin-solidity/contracts/vendor/solidity-BigNumber/src/ -solidity-cborutils/=lib/filecoin-solidity/lib/solidity-cborutils/ \ No newline at end of file +solidity-cborutils/=lib/filecoin-solidity/lib/solidity-cborutils/ +@prb-math/=lib/filecoin-pay/lib/prb-math/src/ +filecoin-pay/=lib/filecoin-pay/src/ +fvm-solidity/=lib/filecoin-pay/lib/fvm-solidity/src/ +prb-math/=lib/filecoin-pay/lib/prb-math/src/ \ No newline at end of file diff --git a/src/Client.sol b/src/Client.sol index 983ecc4..a24c42a 100644 --- a/src/Client.sol +++ b/src/Client.sol @@ -14,6 +14,7 @@ import {UtilsHandlers} from "filecoin-solidity/v0.8/utils/UtilsHandlers.sol"; import {FilAddresses} from "filecoin-solidity/v0.8/utils/FilAddresses.sol"; import {AllocationResponseCbor} from "./lib/AllocationResponseCbor.sol"; import {PoRepMarket} from "./PoRepMarket.sol"; +import {PoRepTypes} from "./types/PoRepTypes.sol"; import {IValidator} from "./interfaces/Validator.sol"; import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; @@ -384,13 +385,13 @@ contract Client is Initializable, AccessControlUpgradeable, UUPSUpgradeable, Ree function _verifyAndRegisterDeal(uint256 dealId, bool dealCompleted) internal { ClientStorage storage $ = s(); - PoRepMarket.DealProposal memory proposal = $._poRepMarketContract.getDealProposal(dealId); + PoRepTypes.DealProposal memory proposal = $._poRepMarketContract.getDealProposal(dealId); if (proposal.client != msg.sender) { revert InvalidClient(); } - if (proposal.state != PoRepMarket.DealState.Accepted) { + if (proposal.state != PoRepTypes.DealState.Accepted) { revert InvalidDealStateForTransfer(); } diff --git a/src/PoRepMarket.sol b/src/PoRepMarket.sol index 3a00dcb..600b6b2 100644 --- a/src/PoRepMarket.sol +++ b/src/PoRepMarket.sol @@ -10,6 +10,7 @@ import {ISPRegistry} from "./interfaces/ISPRegistry.sol"; import {ValidatorFactory} from "./ValidatorFactory.sol"; import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; import {SLITypes} from "./types/SLITypes.sol"; +import {PoRepTypes} from "./types/PoRepTypes.sol"; import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; /** @@ -26,7 +27,7 @@ contract PoRepMarket is Initializable, AccessControlUpgradeable, UUPSUpgradeable /// @custom:storage-location erc7201:porepmarket.storage.DealProposalsStorage struct DealProposalsStorage { - mapping(uint256 dealId => DealProposal) _dealProposals; + mapping(uint256 dealId => PoRepTypes.DealProposal) _dealProposals; EnumerableSet.UintSet _dealIdsReadyForPayment; ISPRegistry _SPRegistryContract; ValidatorFactory _validatorFactoryContract; @@ -53,31 +54,6 @@ contract PoRepMarket is Initializable, AccessControlUpgradeable, UUPSUpgradeable return _getDealProposalsStorage(); } - /** - * @notice DealState enum - */ - enum DealState { - Proposed, - Accepted, - Completed, - Rejected - } - - /** - * @notice DealProposal struct - */ - struct DealProposal { - uint256 dealId; - address client; - CommonTypes.FilActorId provider; - SLITypes.SLIThresholds requirements; - SLITypes.DealTerms terms; - address validator; - DealState state; - uint256 railId; - string manifestLocation; - } - /** * @notice DealProposalCreated event * @param dealId The id of the deal proposal @@ -85,13 +61,15 @@ contract PoRepMarket is Initializable, AccessControlUpgradeable, UUPSUpgradeable * @param provider The address of the provider * @param requirements The SLI thresholds for the deal * @param manifestLocation The location of the manifest for the deal + * @param totalDealSize The total size of the deal in bytes */ event DealProposalCreated( uint256 indexed dealId, address indexed client, CommonTypes.FilActorId indexed provider, SLITypes.SLIThresholds requirements, - string manifestLocation + string manifestLocation, + uint256 totalDealSize ); /** @@ -126,6 +104,15 @@ contract PoRepMarket is Initializable, AccessControlUpgradeable, UUPSUpgradeable */ event DealCompleted(uint256 indexed dealId, address indexed client, CommonTypes.FilActorId indexed provider); + /** + * @notice DealTerminated event + * @dev DealTerminated event is emitted when a deal is terminated + * @param dealId The id of the deal proposal + * @param terminator The address that terminated the deal + * @param endEpoch The Filecoin epoch at which the deal was terminated + */ + event DealTerminated(uint256 indexed dealId, address indexed terminator, uint256 indexed endEpoch); + /** * @notice DealRejected event * @param dealId The id of the deal proposal @@ -153,7 +140,8 @@ contract PoRepMarket is Initializable, AccessControlUpgradeable, UUPSUpgradeable error NotTheDealValidator(uint256 dealId, address validator); error NotTheClientSmartContract(uint256 dealId, address clientSmartContract); error NotTheControllingAddress(uint256 dealId, address msgSender, CommonTypes.FilActorId provider); - error DealNotInExpectedState(uint256 dealId, DealState currentState, DealState expectedState); + error DealNotInExpectedState(uint256 dealId, PoRepTypes.DealState currentState, PoRepTypes.DealState expectedState); + error CallerIsNotValidator(uint256 dealId, address caller); error DealDoesNotExist(); error NotTheClientOrStorageProvider(uint256 dealId, address rejector); error NoProviderFoundForDeal(); @@ -236,9 +224,9 @@ contract PoRepMarket is Initializable, AccessControlUpgradeable, UUPSUpgradeable } uint256 dealId = ++$._dealIdCounter; - DealState initialState = autoApprove ? DealState.Accepted : DealState.Proposed; + PoRepTypes.DealState initialState = autoApprove ? PoRepTypes.DealState.Accepted : PoRepTypes.DealState.Proposed; - $._dealProposals[dealId] = DealProposal({ + $._dealProposals[dealId] = PoRepTypes.DealProposal({ dealId: dealId, client: msg.sender, provider: provider, @@ -250,7 +238,7 @@ contract PoRepMarket is Initializable, AccessControlUpgradeable, UUPSUpgradeable manifestLocation: manifestLocation }); - emit DealProposalCreated(dealId, msg.sender, provider, requirements, manifestLocation); + emit DealProposalCreated(dealId, msg.sender, provider, requirements, manifestLocation, terms.dealSizeBytes); if (autoApprove) { emit DealAccepted(dealId, msg.sender, provider); } @@ -262,10 +250,10 @@ contract PoRepMarket is Initializable, AccessControlUpgradeable, UUPSUpgradeable */ function updateValidator(uint256 dealId) external { DealProposalsStorage storage $ = s(); - DealProposal storage dp = $._dealProposals[dealId]; + PoRepTypes.DealProposal storage dp = $._dealProposals[dealId]; _ensureDealExists(dp); - _ensureDealCorrectState(dp, DealState.Accepted); + _ensureDealCorrectState(dp, PoRepTypes.DealState.Accepted); if (dp.validator != address(0)) { revert ValidatorAlreadySet(dealId); @@ -287,10 +275,10 @@ contract PoRepMarket is Initializable, AccessControlUpgradeable, UUPSUpgradeable */ function updateRailId(uint256 dealId, uint256 railId) external { DealProposalsStorage storage $ = s(); - DealProposal storage dp = $._dealProposals[dealId]; + PoRepTypes.DealProposal storage dp = $._dealProposals[dealId]; _ensureDealExists(dp); - _ensureDealCorrectState(dp, DealState.Accepted); + _ensureDealCorrectState(dp, PoRepTypes.DealState.Accepted); if (dp.railId != 0) { revert RailIdAlreadySet(); @@ -313,7 +301,7 @@ contract PoRepMarket is Initializable, AccessControlUpgradeable, UUPSUpgradeable * @param dealId The id of the deal proposal * @return DealProposal The deal proposal */ - function getDealProposal(uint256 dealId) external view returns (DealProposal memory) { + function getDealProposal(uint256 dealId) external view returns (PoRepTypes.DealProposal memory) { DealProposalsStorage storage $ = s(); return $._dealProposals[dealId]; } @@ -324,16 +312,16 @@ contract PoRepMarket is Initializable, AccessControlUpgradeable, UUPSUpgradeable */ function acceptDeal(uint256 dealId) external { DealProposalsStorage storage $ = s(); - DealProposal storage dp = $._dealProposals[dealId]; + PoRepTypes.DealProposal storage dp = $._dealProposals[dealId]; _ensureDealExists(dp); - _ensureDealCorrectState(dp, DealState.Proposed); + _ensureDealCorrectState(dp, PoRepTypes.DealState.Proposed); if (!$._SPRegistryContract.isAuthorizedForProvider(msg.sender, dp.provider)) { revert NotTheControllingAddress(dealId, msg.sender, dp.provider); } - dp.state = DealState.Accepted; + dp.state = PoRepTypes.DealState.Accepted; emit DealAccepted(dealId, msg.sender, dp.provider); } @@ -343,14 +331,14 @@ contract PoRepMarket is Initializable, AccessControlUpgradeable, UUPSUpgradeable */ function completeDeal(uint256 dealId) external { DealProposalsStorage storage $ = s(); - DealProposal storage dp = $._dealProposals[dealId]; + PoRepTypes.DealProposal storage dp = $._dealProposals[dealId]; _ensureDealExists(dp); - _ensureDealCorrectState(dp, DealState.Accepted); + _ensureDealCorrectState(dp, PoRepTypes.DealState.Accepted); if (msg.sender != $._clientSmartContract) revert NotTheClientSmartContract(dealId, msg.sender); - dp.state = DealState.Completed; + dp.state = PoRepTypes.DealState.Completed; $._dealIdsReadyForPayment.add(dealId); // TODO: actualSizeBytes should come from Client contract's allocation tracking // For now, use estimated size (no tolerance delta) @@ -359,22 +347,47 @@ contract PoRepMarket is Initializable, AccessControlUpgradeable, UUPSUpgradeable emit DealCompleted(dealId, msg.sender, dp.provider); } + /** + * @notice Terminate a deal + * @dev Terminates a deal by setting the deal state to terminated + * @param dealId The id of the deal proposal + * @param terminator The address that terminated the deal + * @param endEpoch The Filecoin epoch at which the deal was terminated + */ + function terminateDeal(uint256 dealId, address terminator, uint256 endEpoch) external { + DealProposalsStorage storage $ = _getDealProposalsStorage(); + PoRepTypes.DealProposal storage dp = $._dealProposals[dealId]; + + _ensureDealExists(dp); + _ensureDealCorrectState(dp, PoRepTypes.DealState.Completed); + + if (msg.sender != dp.validator || dp.validator == address(0)) { + revert CallerIsNotValidator(dealId, msg.sender); + } + + $._SPRegistryContract.releaseCapacity(dp.provider, dp.terms.dealSizeBytes); + $._dealIdsReadyForPayment.remove(dealId); + + dp.state = PoRepTypes.DealState.Terminated; + emit DealTerminated(dealId, terminator, endEpoch); + } + /** * @notice Rejects a deal * @param dealId The id of the deal proposal */ function rejectDeal(uint256 dealId) external { DealProposalsStorage storage $ = s(); - DealProposal storage dp = $._dealProposals[dealId]; + PoRepTypes.DealProposal storage dp = $._dealProposals[dealId]; _ensureDealExists(dp); - _ensureDealCorrectState(dp, DealState.Proposed); + _ensureDealCorrectState(dp, PoRepTypes.DealState.Proposed); if (msg.sender != dp.client && !$._SPRegistryContract.isAuthorizedForProvider(msg.sender, dp.provider)) { revert NotTheClientOrStorageProvider(dealId, msg.sender); } - dp.state = DealState.Rejected; + dp.state = PoRepTypes.DealState.Rejected; $._SPRegistryContract.releasePendingCapacity(dp.provider, dp.terms.dealSizeBytes); emit DealRejected(dealId, msg.sender); } @@ -383,15 +396,15 @@ contract PoRepMarket is Initializable, AccessControlUpgradeable, UUPSUpgradeable * @notice Gets all completed deals * @return completedDeals Array of completed deal proposals */ - function getCompletedDeals() external view returns (DealProposal[] memory completedDeals) { + function getCompletedDeals() external view returns (PoRepTypes.DealProposal[] memory completedDeals) { DealProposalsStorage storage $ = s(); uint256[] memory completedDealsIds = $._dealIdsReadyForPayment.values(); - completedDeals = new DealProposal[](completedDealsIds.length); + completedDeals = new PoRepTypes.DealProposal[](completedDealsIds.length); uint256 dealCounter = 0; for (uint256 i = 0; i < completedDealsIds.length; i++) { - DealProposal memory dp = $._dealProposals[completedDealsIds[i]]; - if (dp.state == DealState.Completed) { + PoRepTypes.DealProposal memory dp = $._dealProposals[completedDealsIds[i]]; + if (dp.state == PoRepTypes.DealState.Completed) { completedDeals[dealCounter] = dp; dealCounter++; } @@ -410,7 +423,7 @@ contract PoRepMarket is Initializable, AccessControlUpgradeable, UUPSUpgradeable */ function getManifestLocation(uint256 dealId) external view returns (string memory manifestLocation) { DealProposalsStorage storage $ = s(); - DealProposal storage dealProposal = $._dealProposals[dealId]; + PoRepTypes.DealProposal storage dealProposal = $._dealProposals[dealId]; _ensureDealExists(dealProposal); return dealProposal.manifestLocation; } @@ -422,7 +435,7 @@ contract PoRepMarket is Initializable, AccessControlUpgradeable, UUPSUpgradeable */ function updateManifestLocation(uint256 dealId, string calldata newManifestLocation) external { DealProposalsStorage storage $ = s(); - DealProposal storage dealProposal = $._dealProposals[dealId]; + PoRepTypes.DealProposal storage dealProposal = $._dealProposals[dealId]; _ensureDealExists(dealProposal); if (msg.sender != dealProposal.client) { revert UnauthorisedCaller(dealId, msg.sender, dealProposal.client); @@ -445,7 +458,7 @@ contract PoRepMarket is Initializable, AccessControlUpgradeable, UUPSUpgradeable * @notice Ensures a deal exists * @param dealProposal The id of the deal proposal */ - function _ensureDealExists(DealProposal memory dealProposal) internal pure { + function _ensureDealExists(PoRepTypes.DealProposal memory dealProposal) internal pure { if (dealProposal.dealId == 0) revert DealDoesNotExist(); } @@ -454,7 +467,10 @@ contract PoRepMarket is Initializable, AccessControlUpgradeable, UUPSUpgradeable * @param dp The deal proposal * @param expectedState The expected state */ - function _ensureDealCorrectState(DealProposal memory dp, DealState expectedState) internal pure { + function _ensureDealCorrectState(PoRepTypes.DealProposal memory dp, PoRepTypes.DealState expectedState) + internal + pure + { if (dp.state != expectedState) revert DealNotInExpectedState(dp.dealId, dp.state, expectedState); } diff --git a/src/SPRegistry.sol b/src/SPRegistry.sol index 0038e25..bda3244 100644 --- a/src/SPRegistry.sol +++ b/src/SPRegistry.sol @@ -10,7 +10,7 @@ import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet import {CommonTypes} from "filecoin-solidity/v0.8/types/CommonTypes.sol"; import {ISPRegistry} from "./interfaces/ISPRegistry.sol"; import {SLITypes} from "./types/SLITypes.sol"; -import {MinerUtils} from "./libs/MinerUtils.sol"; +import {MinerUtils} from "./lib/MinerUtils.sol"; /** * @title SPRegistry diff --git a/src/Validator.sol b/src/Validator.sol index 0ef64b1..73ad740 100644 --- a/src/Validator.sol +++ b/src/Validator.sol @@ -1,30 +1,611 @@ // SPDX-License-Identifier: MIT -// solhint-disable +// solhint-disable var-name-mixedcase pragma solidity =0.8.25; +import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; import {AccessControlUpgradeable} from "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol"; -import {CommonTypes} from "filecoin-solidity/v0.8/types/CommonTypes.sol"; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -contract Validator is AccessControlUpgradeable { +import {CommonTypes} from "filecoin-solidity/v0.8/types/CommonTypes.sol"; + +import {IFilecoinPayV1} from "./interfaces/IFilecoinPayV1.sol"; +import {IValidator} from "./interfaces/IValidator.sol"; +import {ISLIScorer} from "./interfaces/ISLIScorer.sol"; +import {IPoRepMarket} from "./interfaces/IPoRepMarket.sol"; +import {ISPRegistry} from "./interfaces/ISPRegistry.sol"; +import {Operator} from "./abstracts/Operator.sol"; +import {PoRepTypes} from "./types/PoRepTypes.sol"; +import {Client} from "./Client.sol"; + +/** + * @title Validator + * @dev Implements validator and operator logic for managing Filecoin Pay rails + * @notice Validator contract for Filecoin Pay + */ +contract Validator is Initializable, AccessControlUpgradeable, IValidator, Operator { + /** + * @notice Error indicating that the caller is not the FilecoinPay contract + */ + error CallerIsNotFilecoinPay(); + + /** + * @notice Error indicating that the caller is not the Client Smart Contract + */ + error CallerIsNotClientSC(); + + /** + * @notice Error indicating that the admin address provided during initialization is the zero address + */ + error InvalidAdminAddress(); + + /** + * @notice Error indicating that the FilecoinPay address provided during initialization is the zero address + */ + error InvalidFilecoinPayAddress(); + + /** + * @notice Error indicating that the SLIScorer address provided during initialization is the zero address + */ + error InvalidSLIScorerAddress(); + + /** + * @notice Error indicating that the client smart contract address provided during initialization is the zero address + */ + error InvalidClientSCAddress(); + + /** + * @notice Error indicating that the PoRepMarket address provided during initialization is the zero address + */ + error InvalidPoRepMarketAddress(); + + /** + * @notice Error indicating that the PoRep service bot address provided during initialization is the zero address + */ + error InvalidPoRepServiceAddress(); + + /** + * @notice Error indicating that the SPRegistry address provided during initialization is the zero address + */ + error InvalidSPRegistryAddress(); + + /** + * @notice Error indicating that the caller is not the client + */ + error CallerIsNotClient(); + + /** + * @notice Error indicating that a payment rail has already been created for this validator + */ + error RailAlreadyCreated(); + + /** + * @notice Error indicating that an invalid deal ID was provided + */ + error InvalidDealId(); + + /** + * @notice Error indicating that the operator is not approved + */ + error OperatorNotApproved(); + + /** + * @notice Error indicating that the maximum lockup period is less than the minimum required lockup period + */ + error MaxLockupPeriodLessThanMinimum(); + + /** + * @notice Error indicating that the lockup allowance is not set properly + */ + error InvalidLockupAllowance(); + + /** + * @notice Error indicating that the rate allowance is not set properly + */ + error InvalidRateAllowance(); + + /** + * @notice Error indicating that the number of sectors in the deal is zero, which is invalid + */ + error InvalidSectorCount(); + + /** + * @notice Error indicating that the duration of the deal is zero, which is invalid + */ + error InvalidDealDuration(); + + /** + * @notice Error indicating that the caller is not authorized to perform the action + */ + error UnauthorizedCaller(); + + /** + * @notice Error indicating that the provided end epoch is negative, which is invalid + */ + error NegativeEndEpoch(); + + /** + * @notice Error indicating that the calculated amount per epoch is zero, which is invalid + */ + error InvalidZeroAmount(); + + /** + * @notice Error indicating that the deal associated with this validator has not been completed yet + * @param dealId The ID of the deal that is not completed + */ + error DealNotCompleted(uint256 dealId); + + /** + * @notice Error indicating that an invalid rail ID was provided + * @dev We expect only one rail ID to be valid for per validator + * @param expected The expected rail ID + * @param actual The actual rail ID provided in the function call + */ + error InvalidRailId(uint256 expected, uint256 actual); + + // solhint-disable gas-indexed-events + /** + * @notice Event emitted when a payment rail is terminated + * @param railId The ID of the terminated rail + * @param terminator The address that initiated the termination + * @param endEpoch The Filecoin epoch at which the rail was terminated + */ + event RailTerminated(uint256 indexed railId, address indexed terminator, uint256 endEpoch); + + /** + * @notice Event emitted when the lockup period of a rail is updated + * @param railId The ID of the rail + * @param newLockupPeriod The new lockup period for the rail + */ + event LockupPeriodUpdated(uint256 indexed railId, uint256 newLockupPeriod); + + /** + * @notice Event emitted when the payment rate of a rail is modified + * @param railId The ID of the rail + * @param newRate The new payment rate for the rail + */ + event RailPaymentModified(uint256 indexed railId, uint256 newRate); + + /** + * @notice Event emitted when the deal end epoch is updated + * @param dealId The ID of the deal + * @param endEpoch The Filecoin epoch at which the deal ended + */ + event DealEndEpochUpdated(uint256 indexed dealId, CommonTypes.ChainEpoch endEpoch); + + /** + * @notice Event emitted when a rail is disabled for future payments + * @param railId The ID of the rail that has been disabled + */ + event RailDisabled(uint256 indexed railId); + + // solhint-enable gas-indexed-events + + /// @custom:storage-location erc7201:porepmarket.storage.ValidatorStorage + struct ValidatorStorage { + uint256 railId; + uint256 dealId; + address filecoinPay; + address SLIScorer; + address clientSC; + address poRepMarket; + address SPRegistry; + CommonTypes.FilActorId providerId; + CommonTypes.ChainEpoch dealEndEpoch; + uint256 amountPerEpoch; + uint256 earlyTerminatedEpoch; + } + + /** + * @notice Role for PoRep bot which is responsible for automating validator functions + */ + bytes32 public constant POREP_SERVICE_ROLE = keccak256("POREP_SERVICE_ROLE"); + + /** + * @notice Number of epochs in one month + * @dev 30 days * 24 hours/day * 60 minutes/hour * 2 epochs/minute = 86_400 epochs + */ + uint256 private constant EPOCHS_IN_MONTH = 86_400; + + /** + * @notice Number of epochs in one day + * @dev 24 hours/day * 60 minutes/hour * 2 epochs/minute = 2_880 epochs + */ + uint256 private constant EPOCHS_IN_DAY = 2_880; + + /** + * @notice Storage location for ValidatorStorage struct + * @dev keccak256(abi.encode(uint256(keccak256("porepmarket.storage.ValidatorStorage")) - 1)) & ~bytes32(uint256(0xff)) + */ + bytes32 private constant VALIDATOR_STORAGE_LOCATION = + 0xf51cddbeb47ca42a561371db80eaffa401732269b8af46b255e3f43a7c044000; + + /** + * @notice Modifier to check that the provided rail ID is valid before executing the function + * @param railId The rail ID to validate + */ + modifier isRailIdValid(uint256 railId) { + _checkRailIdValid(railId); + _; + } + + /** + * @notice Constructor + * @dev Constructor disables initializers + */ + constructor() { + _disableInitializers(); + } + + // solhint-disable func-param-name-mixedcase + /** + * @notice Initializes the contract + * @param _admin Address to be granted the default admin role + * @param _porepService Address of the PoRep service bot + * @param _filecoinPay Address of the FilecoinPay contract + * @param _SLIScorer Address of the SLIScorer contract + * @param _clientSC Address of the client smart contract + * @param _poRepMarket Address of the PoRepMarket contract + * @param _SPRegistry Address of the SPRegistry contract + * @param _dealId The ID of the deal for which this validator is being initialized + */ function initialize( - address, // admin - address, // _porepService - address, // _filecoinPay - address, // _SLIScorer - address, // _clientSC - address, // _poRepMarket - address, // _SPRegistry, - uint256 //_dealId - ) + address _admin, + address _porepService, + address _filecoinPay, + address _SLIScorer, + address _clientSC, + address _poRepMarket, + address _SPRegistry, + uint256 _dealId + ) external initializer { + _validateInitializeAddresses( + _admin, _porepService, _filecoinPay, _SLIScorer, _clientSC, _SPRegistry, _poRepMarket + ); + + __AccessControl_init(); + _grantRole(DEFAULT_ADMIN_ROLE, _admin); + _grantRole(POREP_SERVICE_ROLE, _porepService); + + ValidatorStorage storage $ = _getValidatorStorage(); + + PoRepTypes.DealProposal memory dealProposal = IPoRepMarket(_poRepMarket).getDealProposal(_dealId); + + $.providerId = dealProposal.provider; + $.filecoinPay = _filecoinPay; + $.SLIScorer = _SLIScorer; + $.clientSC = _clientSC; + $.poRepMarket = _poRepMarket; + $.SPRegistry = _SPRegistry; + $.dealId = _dealId; + + IPoRepMarket(_poRepMarket).updateValidator(_dealId); + } + + // solhint-enable func-param-name-mixedcase + + // solhint-disable function-max-lines, gas-strict-inequalities + /** + * @notice Validates a proposed payment amount for a payment rail + * @dev Only callable by the FilecoinPay contract + * @param railId ID of the payment rail + * @param proposedAmount Proposed payment amount to validate + * @param fromEpoch The epoch up to and including which the rail has already been settled + * @param toEpoch The epoch up to and including which validation is requested; payment will be validated for (toEpoch - fromEpoch) epochs + * @param rate Rate used for payment calculation + * @return result ValidationResult struct containing validation outcome + */ + function validatePayment(uint256 railId, uint256 proposedAmount, uint256 fromEpoch, uint256 toEpoch, uint256 rate) external - initializer + returns (IValidator.ValidationResult memory result) { - __AccessControl_init(); + ValidatorStorage storage $ = _getValidatorStorage(); + if (msg.sender != $.filecoinPay) { + revert CallerIsNotFilecoinPay(); + } + + _checkRailIdValid(railId); + + // forge-lint: disable-next-line(unsafe-typecast) + uint256 dealEndEpoch = uint256(uint64(CommonTypes.ChainEpoch.unwrap($.dealEndEpoch))); + + if (dealEndEpoch == 0) { + revert DealNotCompleted($.dealId); + } + + if ($.earlyTerminatedEpoch != 0 && $.earlyTerminatedEpoch < dealEndEpoch) { + dealEndEpoch = $.earlyTerminatedEpoch; + } + + if (toEpoch < fromEpoch + EPOCHS_IN_MONTH) { + result.settleUpto = fromEpoch; + result.note = "1mo payout period not reached"; + return result; + } + + PoRepTypes.DealProposal memory dealProposal = IPoRepMarket($.poRepMarket).getDealProposal($.dealId); + uint256 score = ISLIScorer($.SLIScorer).calculateScore($.providerId, dealProposal.requirements); + + bool scoreMatches = score == 100; + bool dataSizeMatches = Client($.clientSC).isDataSizeMatching($.dealId); + + if (!scoreMatches || !dataSizeMatches) { + result.settleUpto = toEpoch; + result.note = + !scoreMatches ? "score below required threshold" : "data size does not match the deal proposal"; + return result; + } + + if (fromEpoch >= dealEndEpoch) { + result.settleUpto = fromEpoch; + result.note = "deal ended"; + return result; + } + + if (toEpoch > dealEndEpoch) { + result.modifiedAmount = rate * (dealEndEpoch - fromEpoch); + result.note = "payment limited to deal endepoch"; + result.settleUpto = dealEndEpoch; + } else { + result.modifiedAmount = proposedAmount; + result.note = "payment validated successfully"; + result.settleUpto = toEpoch; + } } - constructor() { - _disableInitializers(); + // solhint-enable function-max-lines, gas-strict-inequalities + + /** + * @notice Creates a payment rail with the specified parameters and set initial lockup period + * @dev Only callable by the client + * @dev Sets railID in contract state and updates the PoRepMarket with the created rail ID + * @param token The ERC20 token to use for the payment rail + */ + function createRail(IERC20 token) external override { + ValidatorStorage storage $ = _getValidatorStorage(); + PoRepTypes.DealProposal memory dealProposal = IPoRepMarket($.poRepMarket).getDealProposal($.dealId); + + if (msg.sender != dealProposal.client) { + revert CallerIsNotClient(); + } + + if ($.railId != 0) { + revert RailAlreadyCreated(); + } + + (bool isApproved, uint256 rateAllowance, uint256 lockupAllowance,,, uint256 maxLockupPeriod) = + IFilecoinPayV1($.filecoinPay).operatorApprovals(token, dealProposal.client, address(this)); + + if (!isApproved) { + revert OperatorNotApproved(); + } + /// NOTE: to be discussed - might be shorter period than a month + if (maxLockupPeriod < EPOCHS_IN_MONTH) { + revert MaxLockupPeriodLessThanMinimum(); + } + + if (lockupAllowance == 0) { + revert InvalidLockupAllowance(); + } + + if (rateAllowance == 0) { + revert InvalidRateAllowance(); + } + + address payee = ISPRegistry($.SPRegistry).getPayee($.providerId); + + uint256 railId = _createRail(IFilecoinPayV1($.filecoinPay), token, dealProposal.client, payee, 0, address(0)); + $.railId = railId; + + IPoRepMarket($.poRepMarket).updateRailId($.dealId, railId); + _setInitialLockup(railId, EPOCHS_IN_MONTH); + } + + /** + * @notice Modifies the payment rate + * @dev Only callable by POREP_SERVICE bot + * @param railId The ID of the rail to modify + */ + function modifyRailPayment(uint256 railId) external override onlyRole(POREP_SERVICE_ROLE) isRailIdValid(railId) { + ValidatorStorage storage $ = _getValidatorStorage(); + + uint256 newRate = _calculateAmountPerEpoch(); + $.amountPerEpoch = newRate; + + _modifyRailPayment(IFilecoinPayV1($.filecoinPay), railId, newRate, 0); + emit RailPaymentModified(railId, newRate); + } + + /** + * @notice Disables future payments for a payment rail by terminating the rail + * @dev Only callable by POREP_SERVICE bot + * @dev After calling this method, the lockup period cannot be changed, and the rail's rate and fixed lockup may only be reduced + * @param railId The ID of the rail to terminate + */ + function disableFutureRailPayments(uint256 railId) external onlyRole(POREP_SERVICE_ROLE) isRailIdValid(railId) { + ValidatorStorage storage $ = _getValidatorStorage(); + $.earlyTerminatedEpoch = block.number; + _terminateRail(IFilecoinPayV1($.filecoinPay), railId); + emit RailDisabled(railId); + } + + /** + * @notice Updates the lockup period of a payment rail + * @dev Only callable by the admin + * @param railId The ID of the rail to modify + * @param newLockupPeriod New lockup period to set + */ + function updateLockupPeriod(uint256 railId, uint256 newLockupPeriod) + external + override + onlyRole(DEFAULT_ADMIN_ROLE) + isRailIdValid(railId) + { + ValidatorStorage storage $ = _getValidatorStorage(); + _updateLockupPeriod(IFilecoinPayV1($.filecoinPay), railId, newLockupPeriod, 0); + emit LockupPeriodUpdated(railId, newLockupPeriod); + } + + /** + * @notice Terminates a payment rail, preventing further payments after the rail's lockup period. After calling this method, the lockup period cannot be changed, and the rail's rate and fixed lockup may only be reduced. + * @param railId The ID of the rail to terminate. + */ + function terminateRail(uint256 railId) external override isRailIdValid(railId) { + ValidatorStorage storage $ = _getValidatorStorage(); + if (!hasRole(POREP_SERVICE_ROLE, msg.sender) && !hasRole(DEFAULT_ADMIN_ROLE, msg.sender)) { + revert UnauthorizedCaller(); + } + _terminateRail(IFilecoinPayV1($.filecoinPay), railId); + } + + /** + * @notice Invoked when a payment rail is terminated + * @dev Only callable by the FilecoinPay contract + * @param railId The ID of the terminated rail + * @param terminator Address that initiated the termination + * @param endEpoch Filecoin epoch at which the rail was terminated + */ + function railTerminated(uint256 railId, address terminator, uint256 endEpoch) + external + override + isRailIdValid(railId) + { + ValidatorStorage storage $ = _getValidatorStorage(); + if (msg.sender != $.filecoinPay) { + revert CallerIsNotFilecoinPay(); + } + + IPoRepMarket($.poRepMarket).terminateDeal($.dealId, terminator, endEpoch); + emit RailTerminated(railId, terminator, endEpoch); + } + + /** + * @notice Sets the end epoch for the deal associated with this validator + * @dev Only callable by POREP_SERVICE bot + * @param dealId The ID of the deal + * @param endEpoch The Filecoin epoch at which the deal ended + */ + function setDealEndEpoch(uint256 dealId, CommonTypes.ChainEpoch endEpoch) external onlyRole(POREP_SERVICE_ROLE) { + ValidatorStorage storage $ = _getValidatorStorage(); + + if (dealId != $.dealId) { + revert InvalidDealId(); + } + + int64 unwrappedEndEpoch = CommonTypes.ChainEpoch.unwrap(endEpoch); + if (unwrappedEndEpoch < 0) { + revert NegativeEndEpoch(); + } + + $.dealEndEpoch = endEpoch; + emit DealEndEpochUpdated(dealId, endEpoch); + } + + /** + * @notice Checks that the provided rail ID matches the expected rail ID stored in contract state + * @param railId The rail ID to validate + */ + function _checkRailIdValid(uint256 railId) internal view { + ValidatorStorage storage $ = _getValidatorStorage(); + if (railId != $.railId) { + revert InvalidRailId({expected: $.railId, actual: railId}); + } + } + + /** + * @notice Sets the initial lockup period for a payment rail + * @param railId The ID of the rail for which to set the initial lockup period + * @param lockupPeriod The lockup period to set + */ + function _setInitialLockup(uint256 railId, uint256 lockupPeriod) internal { + ValidatorStorage storage $ = _getValidatorStorage(); + _updateLockupPeriod(IFilecoinPayV1($.filecoinPay), railId, lockupPeriod, 0); + emit LockupPeriodUpdated(railId, lockupPeriod); + } + + /** + * @notice Calculates the amount to be paid per epoch for the deal + * @return Amount to be paid per epoch + */ + function _calculateAmountPerEpoch() internal view returns (uint256) { + ValidatorStorage storage $ = _getValidatorStorage(); + + PoRepTypes.DealProposal memory dealProposal = IPoRepMarket($.poRepMarket).getDealProposal($.dealId); + CommonTypes.FilActorId[] memory allocationIds = Client($.clientSC).getClientAllocationIdsPerDeal($.dealId); + + uint256 sectorCount = allocationIds.length; + uint256 pricePerSector = dealProposal.terms.pricePerSector; + uint32 durationDays = dealProposal.terms.durationDays; + + if (sectorCount == 0) { + revert InvalidSectorCount(); + } + + if (durationDays == 0) { + revert InvalidDealDuration(); + } + + uint256 totalEpochs = durationDays * EPOCHS_IN_DAY; + uint256 amount = (pricePerSector * sectorCount) / totalEpochs; + + if (amount == 0) { + revert InvalidZeroAmount(); + } + + return amount; + } + + /** + * @notice Validates that the provided addresses for initialization are not zero addresses + * @param _admin Address to be granted the default admin role + * @param _porepService Address of the PoRep service bot + * @param _filecoinPay Address of the FilecoinPay contract + * @param _SLIScorer Address of the SLIScorer contract + * @param _clientSC Address of the client smart contract + * @param _SPRegistry Address of the SPRegistry contract + * @param _poRepMarket Address of the PoRepMarket contract + */ + function _validateInitializeAddresses( + address _admin, + address _porepService, + address _filecoinPay, + address _SLIScorer, + address _clientSC, + address _SPRegistry, + address _poRepMarket + ) internal pure { + if (_admin == address(0)) { + revert InvalidAdminAddress(); + } + if (_porepService == address(0)) { + revert InvalidPoRepServiceAddress(); + } + if (_filecoinPay == address(0)) { + revert InvalidFilecoinPayAddress(); + } + if (_SLIScorer == address(0)) { + revert InvalidSLIScorerAddress(); + } + if (_clientSC == address(0)) { + revert InvalidClientSCAddress(); + } + if (_SPRegistry == address(0)) { + revert InvalidSPRegistryAddress(); + } + if (_poRepMarket == address(0)) { + revert InvalidPoRepMarketAddress(); + } + } + + // solhint-disable + /** + * @notice Retrieves the ValidatorStorage struct from the designated storage location + * @return $ Reference to the ValidatorStorage struct + */ + function _getValidatorStorage() private pure returns (ValidatorStorage storage $) { + assembly { + $.slot := VALIDATOR_STORAGE_LOCATION + } } + // solhint-enable } diff --git a/src/ValidatorFactory.sol b/src/ValidatorFactory.sol index 7ec69ce..06977f5 100644 --- a/src/ValidatorFactory.sol +++ b/src/ValidatorFactory.sol @@ -10,6 +10,7 @@ import {Validator} from "./Validator.sol"; import {Create2} from "@openzeppelin/contracts/utils/Create2.sol"; import {UUPSUpgradeable} from "@openzeppelin/contracts/proxy/utils/UUPSUpgradeable.sol"; import {PoRepMarket} from "./PoRepMarket.sol"; +import {PoRepTypes} from "./types/PoRepTypes.sol"; /** * @title ValidatorFactory @@ -135,7 +136,7 @@ contract ValidatorFactory is UUPSUpgradeable, AccessControlUpgradeable { ValidatorFactoryStorage storage $ = s(); if ($._instances[dealId] != address(0)) revert InstanceAlreadyExists(); - PoRepMarket.DealProposal memory dp = PoRepMarket($._poRepMarket).getDealProposal(dealId); + PoRepTypes.DealProposal memory dp = PoRepMarket($._poRepMarket).getDealProposal(dealId); if (msg.sender != dp.client) revert InvalidClientAddress(); bytes memory initCode = abi.encodePacked( @@ -157,7 +158,7 @@ contract ValidatorFactory is UUPSUpgradeable, AccessControlUpgradeable { ) ) ); - + // forge-lint: disable-next-line(asm-keccak256) bytes32 salt = keccak256(abi.encode(admin, dealId)); address proxy = Create2.computeAddress(salt, keccak256(initCode), address(this)); $._instances[dealId] = proxy; diff --git a/src/abstracts/Operator.sol b/src/abstracts/Operator.sol new file mode 100644 index 0000000..f779b21 --- /dev/null +++ b/src/abstracts/Operator.sol @@ -0,0 +1,96 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {IFilecoinPayV1} from "../interfaces/IFilecoinPayV1.sol"; + +/** + * @title Operator abstract contract + * @notice Abstract contract defining operator functions for creating and managing payment rails in the FilecoinPayV1 system. + * This contract provides internal helper functions for interacting with the FilecoinPayV1 interface, while leaving the implementation of the external functions to derived contracts. + */ +abstract contract Operator { + /** + * @notice Creates a payment rail + * @param token The ERC20 token to use for the payment rail + */ + function createRail(IERC20 token) external virtual; + + /** + * @notice Updates the lockup period of a payment rail + * @param railId ID of the payment rail + * @param newLockupPeriod New lockup period to set + */ + function updateLockupPeriod(uint256 railId, uint256 newLockupPeriod) external virtual; + + /** + * @notice Modifies the payment rate and optionally makes a one-time payment. + * @param railId The ID of the rail to modify. + */ + function modifyRailPayment(uint256 railId) external virtual; + + /** + * @notice Terminates a payment rail, preventing further payments after the rail's lockup period. After calling this method, the lockup period cannot be changed, and the rail's rate and fixed lockup may only be reduced. + * @param railId The ID of the rail to terminate. + */ + function terminateRail(uint256 railId) external virtual; + + /** + * @notice Internal function to create a payment rail + * @param filecoinPay The FilecoinPayV1 interface + * @param token The ERC20 token to use for the payment rail + * @param payer The address paying the tokens + * @param payee The address receiving the tokens + * @param commissionRateBps The commission rate in basis points for the payment rail + * @param serviceFeeRecipient The recipient of service fees for the payment rail + * @return railId ID of the created payment rail + */ + function _createRail( + IFilecoinPayV1 filecoinPay, + IERC20 token, + address payer, + address payee, + uint256 commissionRateBps, + address serviceFeeRecipient + ) internal returns (uint256 railId) { + railId = filecoinPay.createRail(token, payer, payee, address(this), commissionRateBps, serviceFeeRecipient); + } + + /** + * @notice Internal function to update the lockup period of a payment rail + * @param filecoinPay The FilecoinPayV1 interface + * @param railId ID of the payment rail + * @param newLockupPeriod New lockup period to set + * @param lockupFixed Fixed lockup amount + */ + function _updateLockupPeriod( + IFilecoinPayV1 filecoinPay, + uint256 railId, + uint256 newLockupPeriod, + uint256 lockupFixed + ) internal { + filecoinPay.modifyRailLockup(railId, newLockupPeriod, lockupFixed); + } + + /** + * @notice Internal function to modify the payment rate and optionally make a one-time payment. + * @param filecoinPay The FilecoinPayV1 interface + * @param railId The ID of the rail to modify. + * @param newRate The new payment rate (per epoch). This new rate applies starting the next epoch after the current one. + * @param oneTimePayment Optional one-time payment amount to transfer immediately, taken out of the rail's fixed lockup. + */ + function _modifyRailPayment(IFilecoinPayV1 filecoinPay, uint256 railId, uint256 newRate, uint256 oneTimePayment) + internal + { + filecoinPay.modifyRailPayment(railId, newRate, oneTimePayment); + } + + /** + * @notice Internal function to terminate a payment rail, preventing further payments after the rail's lockup period. After calling this method, the lockup period cannot be changed, and the rail's rate and fixed lockup may only be reduced. + * @param filecoinPay The FilecoinPayV1 interface + * @param railId The ID of the rail to terminate. + */ + function _terminateRail(IFilecoinPayV1 filecoinPay, uint256 railId) internal { + filecoinPay.terminateRail(railId); + } +} diff --git a/src/interfaces/IFilecoinPayV1.sol b/src/interfaces/IFilecoinPayV1.sol new file mode 100644 index 0000000..d7933fe --- /dev/null +++ b/src/interfaces/IFilecoinPayV1.sol @@ -0,0 +1,89 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +/** + * @title Interface for FilecoinPayV1 + * @notice Includes necessary functions from FilecoinPayV1 for operator interactions + */ +interface IFilecoinPayV1 { + /** + * @notice Creates a payment rail + * @param token The ERC20 token to use for the payment rail + * @param payer The address paying the tokens + * @param payee The address receiving the tokens + * @param operator The operator address for the payment rail + * @param commissionRateBps The commission rate in basis points for the payment rail + * @param serviceFeeRecipient The recipient of service fees for the payment rail + * @return railId ID of the created payment rail + * @custom:constraint Caller must be approved as an operator by the client (from address). + */ + function createRail( + IERC20 token, + address payer, + address payee, + address operator, + uint256 commissionRateBps, + address serviceFeeRecipient + ) external returns (uint256); + + /** + * @notice Custom getter for operator approvals + * @param token The ERC20 token address for which the approval is being set + * @param client The client address for which to check operator approval + * @param operator The operator address for which to check approval + * @return isApproved Whether the operator is approved by the client for the specified token + * @return rateAllowance The maximum payment rate the operator can set across all rails created by the operator on behalf of the message sender + * @return lockupAllowance The maximum amount of funds the operator can lock up on behalf of the message sender towards future payments + * @return rateUsage Track actual usage for rate + * @return lockupUsage Track actual usage for lockup + * @return maxLockupPeriod Maximum lockup period the operator can set for rails created on behalf of the client + */ + function operatorApprovals(IERC20 token, address client, address operator) + external + view + returns ( + bool isApproved, + uint256 rateAllowance, + uint256 lockupAllowance, + uint256 rateUsage, + uint256 lockupUsage, + uint256 maxLockupPeriod + ); + + /** + * @notice Modifies the fixed lockup and lockup period of a rail. + * @dev - If the rail has already been terminated, the lockup period may not be altered and the fixed lockup may only be reduced. + * @dev - If the rail is active, the lockup may only be modified if the payer's account is fully funded and will remain fully funded after the operation. + * @param railId The ID of the rail to modify. + * @param period The new lockup period (in epochs/blocks). + * @param lockupFixed The new fixed lockup amount. + * @custom:constraint Caller must be the rail operator. + * @custom:constraint Operator must have sufficient lockup allowance to cover any increases the lockup period or the fixed lockup. + */ + function modifyRailLockup(uint256 railId, uint256 period, uint256 lockupFixed) external; + + /** + * @notice Modifies the payment rate and optionally makes a one-time payment. + * @dev - If the rail has already been terminated, one-time payments can be made and the rate may always be decreased (but never increased) regardless of the status of the payer's account. + * @dev - If the payer's account isn't fully funded and the rail is active (not terminated), the rail's payment rate may not be changed at all (increased or decreased). + * @dev - Regardless of the payer's account status, one-time payments will always go through provided that the rail has sufficient fixed lockup to cover the payment. + * @param railId The ID of the rail to modify. + * @param newRate The new payment rate (per epoch). This new rate applies starting the next epoch after the current one. + * @param oneTimePayment Optional one-time payment amount to transfer immediately, taken out of the rail's fixed lockup. + * @custom:constraint Caller must be the rail operator. + * @custom:constraint Operator must have sufficient rate and lockup allowances for any increases. + */ + function modifyRailPayment(uint256 railId, uint256 newRate, uint256 oneTimePayment) external; + + /** + * @notice Terminates a payment rail, preventing further payments after the rail's lockup period. After calling this method, the lockup period cannot be changed, and the rail's rate and fixed lockup may only be reduced. + * @param railId The ID of the rail to terminate. + * @custom:constraint Caller must be a rail client or operator. + * @custom:constraint Rail must be active and not already terminated. + * @custom:constraint If called by the client, the payer's account must be fully funded. + * @custom:constraint If called by the operator, the payer's funding status isn't checked. + */ + function terminateRail(uint256 railId) external; +} diff --git a/src/interfaces/IPoRepMarket.sol b/src/interfaces/IPoRepMarket.sol new file mode 100644 index 0000000..0a99a09 --- /dev/null +++ b/src/interfaces/IPoRepMarket.sol @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: MIT + +import {PoRepTypes} from "../types/PoRepTypes.sol"; + +pragma solidity ^0.8.24; + +/** + * @title IPoRepMarket interface + * @notice IPoRepMarket interface for interacting with PoRepMarket contract + */ +interface IPoRepMarket { + /** + * @notice Updates the validator address for a given deal ID + * @param dealId The ID of the deal for which the validator address is to be updated + */ + function updateValidator(uint256 dealId) external; + + /** + * @notice Updates the rail ID for a given deal ID + * @param dealId The ID of the deal for which the rail ID is to be updated + * @param railId The new rail ID to be set for the deal + */ + function updateRailId(uint256 dealId, uint256 railId) external; + + /** + * @notice Terminate a deal + * @dev Terminates a deal by setting the deal state to terminated + * @param dealId The id of the deal proposal + * @param terminator The address that terminated the deal + * @param endEpoch The Filecoin epoch at which the deal was terminated + */ + function terminateDeal(uint256 dealId, address terminator, uint256 endEpoch) external; + + /** + * @notice Gets a deal proposal + * @dev Gets a deal proposal by deal id + * @param dealId The id of the deal proposal + * @return DealProposal The deal proposal + */ + function getDealProposal(uint256 dealId) external view returns (PoRepTypes.DealProposal memory); +} diff --git a/src/interfaces/IValidator.sol b/src/interfaces/IValidator.sol new file mode 100644 index 0000000..aa9164d --- /dev/null +++ b/src/interfaces/IValidator.sol @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +/** + * @title Interface for Validator + * @notice Defines the interface for payment validation in Filecoin Pay rails + */ +interface IValidator { + /** + * @notice Result structure for validation during rail settlement + * @param modifiedAmount The actual payment amount determined by the validator after validation of a rail during settlement + * @param settleUpto The epoch up to and including which settlement should occur + * @param note A placeholder note for any additional information the validator wants to send to the caller of `settleRail` + */ + struct ValidationResult { + // The actual payment amount determined by the validator after validation of a rail during settlement + uint256 modifiedAmount; + // The epoch up to and including which settlement should occur. + uint256 settleUpto; + // A placeholder note for any additional information the validator wants to send to the caller of `settleRail` + string note; + } + + /** + * @notice Validates a proposed payment amount for a payment rail + * @param railId ID of the payment rail + * @param proposedAmount Proposed payment amount to validate + * @param fromEpoch The epoch up to and including which the rail has already been settled + * @param toEpoch The epoch up to and including which validation is requested; payment will be validated for (toEpoch - fromEpoch) epochs + * @param rate Rate used for payment calculation + * @return result ValidationResult struct containing validation outcome + */ + function validatePayment( + uint256 railId, + uint256 proposedAmount, + // the epoch up to and including which the rail has already been settled + uint256 fromEpoch, + // the epoch up to and including which validation is requested; payment will be validated for (toEpoch - fromEpoch) epochs + uint256 toEpoch, + uint256 rate + ) external returns (ValidationResult memory result); + + /** + * @notice Invoked when a payment rail is terminated + * @param railId The ID of the terminated rail + * @param terminator Address that initiated the termination + * @param endEpoch Filecoin epoch at which the rail was terminated + */ + function railTerminated(uint256 railId, address terminator, uint256 endEpoch) external; +} diff --git a/src/libs/MinerUtils.sol b/src/lib/MinerUtils.sol similarity index 59% rename from src/libs/MinerUtils.sol rename to src/lib/MinerUtils.sol index fa819e2..bac67c4 100644 --- a/src/libs/MinerUtils.sol +++ b/src/lib/MinerUtils.sol @@ -29,4 +29,20 @@ library MinerUtils { } return controllingAddress; } + + /// NOTE: Temporary commented out for coverage purposes; it is not currently called anywhere + // /** + // * @notice Retrieves the owner information for a given miner actor ID. + // * @dev Wraps the numeric minerID into a FilActorId and calls MinerAPI.getOwner. + // * Reverts with ExitCodeError if the FVM call returns a non-zero exit code. + // * @param minerID The numeric Filecoin miner actor id. + // * @return ownerData The MinerTypes.GetOwnerReturn struct returned by the actor call. + // */ + // function getOwner(CommonTypes.FilActorId minerID) internal view returns (MinerTypes.GetOwnerReturn memory) { + // (int256 exitCode, MinerTypes.GetOwnerReturn memory ownerData) = MinerAPI.getOwner(minerID); + // if (exitCode != 0) { + // revert ExitCodeError(); + // } + // return ownerData; + // } } diff --git a/src/types/PoRepTypes.sol b/src/types/PoRepTypes.sol new file mode 100644 index 0000000..503bfbb --- /dev/null +++ b/src/types/PoRepTypes.sol @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: MIT + +pragma solidity =0.8.25; + +import {CommonTypes} from "filecoin-solidity/v0.8/types/CommonTypes.sol"; +import {SLITypes} from "./SLITypes.sol"; + +/** + * @title PoRepMarket Types + * @notice Shared types for PoRepMarket deal proposals and states + */ +library PoRepTypes { + /** + * @notice DealState enum + * @dev Represents the various states a deal can be + */ + enum DealState { + Proposed, + Accepted, + Completed, + Rejected, + Terminated + } + + /** + * @notice DealProposal struct + * @dev Represents a proposal for a PoRep deal, including all relevant details and terms + * dealId: Unique identifier for the deal + * client: Address of the client proposing the deal + * provider: FilActor ID of the storage provider + * requirements: SLI thresholds that the provider must meet for the deal + * terms: Commercial terms of the deal, such as size, price, and duration + * validator: Address of the validator responsible for validating the deal + * state: Current state of the deal (Proposed, Accepted, Completed, Rejected, Terminated) + * railId: ID of the payment rail associated with the deal + * manifestLocation: Location of the deal manifest + */ + struct DealProposal { + uint256 dealId; + address client; + CommonTypes.FilActorId provider; + SLITypes.SLIThresholds requirements; + SLITypes.DealTerms terms; + address validator; + DealState state; + uint256 railId; + string manifestLocation; + } +} diff --git a/test/Client.t.sol b/test/Client.t.sol index 2015c7b..63a28d6 100644 --- a/test/Client.t.sol +++ b/test/Client.t.sol @@ -4,7 +4,6 @@ pragma solidity =0.8.25; import {Test} from "forge-std/Test.sol"; import {Client} from "../src/Client.sol"; -import {PoRepMarket} from "../src/PoRepMarket.sol"; import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; import {CommonTypes} from "filecoin-solidity/v0.8/types/CommonTypes.sol"; @@ -28,6 +27,7 @@ import {ClientContractMock} from "./contracts/ClientContractMock.sol"; import {ReentrantValidatorMock} from "./contracts/ReentrantValidatorMock.sol"; import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; import {SLITypes} from "../src/types/SLITypes.sol"; +import {PoRepTypes} from "../src/types/PoRepTypes.sol"; // solhint-disable max-states-count contract ClientTest is Test { @@ -38,6 +38,7 @@ contract ClientTest is Test { address public terminationOracle; bytes public transferTo = abi.encodePacked(vm.addr(2)); uint256 public dealId; + uint256 public totalDealSize; CommonTypes.FilActorId public providerFilActorId; // solhint-disable var-name-mixedcase @@ -77,6 +78,7 @@ contract ClientTest is Test { poRepMarketMock = new PoRepMarketMock(); validatorMock = new ValidatorMock(); terminationOracle = vm.addr(3); + totalDealSize = 1024; client = Client(setupProxy(address(impl))); actorIdMock = new ActorIdMock(); failingMockInvalidTopLevelArray = new FailingMockInvalidTopLevelArray(); @@ -110,16 +112,16 @@ contract ClientTest is Test { dealId = 1; poRepMarketMock.setDealProposal( dealId, - PoRepMarket.DealProposal({ + PoRepTypes.DealProposal({ dealId: dealId, client: clientAddress, provider: SP1, requirements: SLITypes.SLIThresholds({ retrievabilityBps: 80, bandwidthMbps: 500, latencyMs: 200, indexingPct: 90 }), - terms: SLITypes.DealTerms({dealSizeBytes: 1000, pricePerSector: 100, durationDays: 365}), + terms: SLITypes.DealTerms({dealSizeBytes: 1024, pricePerSector: 100, durationDays: 365}), validator: address(validatorMock), - state: PoRepMarket.DealState.Accepted, + state: PoRepTypes.DealState.Accepted, railId: 0, manifestLocation: expectedManifestLocation }) @@ -255,16 +257,16 @@ contract ClientTest is Test { function testShouldRevertTransferWhenDealIsNotInCorrectState() public { poRepMarketMock.setDealProposal( dealId, - PoRepMarket.DealProposal({ + PoRepTypes.DealProposal({ dealId: dealId, client: clientAddress, provider: SP1, requirements: SLITypes.SLIThresholds({ retrievabilityBps: 80, bandwidthMbps: 500, latencyMs: 200, indexingPct: 90 }), - terms: SLITypes.DealTerms({dealSizeBytes: 1000, pricePerSector: 100, durationDays: 365}), + terms: SLITypes.DealTerms({dealSizeBytes: 1024, pricePerSector: 100, durationDays: 365}), validator: address(validatorMock), - state: PoRepMarket.DealState.Completed, + state: PoRepTypes.DealState.Completed, railId: 0, manifestLocation: expectedManifestLocation }) @@ -391,16 +393,16 @@ contract ClientTest is Test { poRepMarketMock.setDealProposal( dealId, - PoRepMarket.DealProposal({ + PoRepTypes.DealProposal({ dealId: 150, client: clientAddress, provider: SP2, requirements: SLITypes.SLIThresholds({ retrievabilityBps: 80, bandwidthMbps: 500, latencyMs: 200, indexingPct: 90 }), - terms: SLITypes.DealTerms({dealSizeBytes: 1000, pricePerSector: 100, durationDays: 365}), + terms: SLITypes.DealTerms({dealSizeBytes: 1024, pricePerSector: 100, durationDays: 365}), validator: address(validatorMock), - state: PoRepMarket.DealState.Accepted, + state: PoRepTypes.DealState.Accepted, railId: 0, manifestLocation: expectedManifestLocation }) @@ -487,16 +489,16 @@ contract ClientTest is Test { ReentrantValidatorMock reentrantValidatorMock = new ReentrantValidatorMock(); poRepMarketMock.setDealProposal( dealId, - PoRepMarket.DealProposal({ + PoRepTypes.DealProposal({ dealId: 150, client: clientAddress, provider: SP2, requirements: SLITypes.SLIThresholds({ retrievabilityBps: 80, bandwidthMbps: 500, latencyMs: 200, indexingPct: 90 }), - terms: SLITypes.DealTerms({dealSizeBytes: 1000, pricePerSector: 100, durationDays: 365}), + terms: SLITypes.DealTerms({dealSizeBytes: 1024, pricePerSector: 100, durationDays: 365}), validator: address(reentrantValidatorMock), - state: PoRepMarket.DealState.Accepted, + state: PoRepTypes.DealState.Accepted, railId: 0, manifestLocation: expectedManifestLocation }) @@ -517,16 +519,16 @@ contract ClientTest is Test { poRepMarketMock.setDealProposal( dealId, - PoRepMarket.DealProposal({ + PoRepTypes.DealProposal({ dealId: 150, client: clientAddress, provider: SP1, requirements: SLITypes.SLIThresholds({ retrievabilityBps: 80, bandwidthMbps: 500, latencyMs: 200, indexingPct: 90 }), - terms: SLITypes.DealTerms({dealSizeBytes: 1000, pricePerSector: 100, durationDays: 365}), + terms: SLITypes.DealTerms({dealSizeBytes: 1024, pricePerSector: 100, durationDays: 365}), validator: address(validatorMock), - state: PoRepMarket.DealState.Accepted, + state: PoRepTypes.DealState.Accepted, railId: 0, manifestLocation: expectedManifestLocation }) @@ -754,16 +756,16 @@ contract ClientTest is Test { poRepMarketMock.setDealProposal( dealId, - PoRepMarket.DealProposal({ + PoRepTypes.DealProposal({ dealId: dealId, client: clientAddress, provider: SP1, requirements: SLITypes.SLIThresholds({ retrievabilityBps: 80, bandwidthMbps: 500, latencyMs: 200, indexingPct: 90 }), - terms: SLITypes.DealTerms({dealSizeBytes: 1000, pricePerSector: 100, durationDays: 365}), + terms: SLITypes.DealTerms({dealSizeBytes: 1024, pricePerSector: 100, durationDays: 365}), validator: address(0), - state: PoRepMarket.DealState.Accepted, + state: PoRepTypes.DealState.Accepted, railId: 0, manifestLocation: expectedManifestLocation }) diff --git a/test/MinerUtils.t.sol b/test/MinerUtils.t.sol index a8e2d40..b521295 100644 --- a/test/MinerUtils.t.sol +++ b/test/MinerUtils.t.sol @@ -10,7 +10,7 @@ import {MockProxy} from "./contracts/MockProxy.sol"; import {ActorIdMock} from "./contracts/ActorIdMock.sol"; import {ActorIdFailingMock} from "./contracts/ActorIdFailingMock.sol"; import {ActorIdExitCodeErrorFailingMock} from "./contracts/ActorIdExitCodeErrorFailingMock.sol"; -import {MinerUtils} from "../src/libs/MinerUtils.sol"; +import {MinerUtils} from "../src/lib/MinerUtils.sol"; contract MinerUtilsTest is Test { MinerUtilsHarness public harness; diff --git a/test/PoRepMarket.t.sol b/test/PoRepMarket.t.sol index cf53c7b..7bc1de8 100644 --- a/test/PoRepMarket.t.sol +++ b/test/PoRepMarket.t.sol @@ -10,6 +10,7 @@ import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.s import {CommonTypes} from "filecoin-solidity/v0.8/types/CommonTypes.sol"; import {IAccessControl} from "@openzeppelin/contracts/access/IAccessControl.sol"; import {SLITypes} from "../src/types/SLITypes.sol"; +import {PoRepTypes} from "../src/types/PoRepTypes.sol"; import {PoRepMarketContractMock} from "./contracts/PoRepMarketContractMock.sol"; import {TestUtils} from "./utils/TestUtils.sol"; @@ -25,6 +26,7 @@ contract PoRepMarketTest is Test { address public adminAddress; uint256 public railId; uint256 public dealId; + uint256 public totalDealSize; CommonTypes.FilActorId public providerFilActorId; @@ -32,7 +34,7 @@ contract PoRepMarketTest is Test { SLITypes.SLIThresholds({retrievabilityBps: 80, bandwidthMbps: 500, latencyMs: 200, indexingPct: 90}); SLITypes.DealTerms internal defaultTerms = - SLITypes.DealTerms({dealSizeBytes: 1000, pricePerSector: 100, durationDays: 365}); + SLITypes.DealTerms({dealSizeBytes: 1024, pricePerSector: 100, durationDays: 365}); string public expectedManifestLocation = "https://example.com/manifest"; @@ -47,6 +49,7 @@ contract PoRepMarketTest is Test { adminAddress = vm.addr(0x006); dealId = 1; railId = 1; + totalDealSize = 1024; providerFilActorId = CommonTypes.FilActorId.wrap(1000); @@ -62,20 +65,20 @@ contract PoRepMarketTest is Test { validatorFactory.setValidator(validatorAddress, true); } - function createDealProposal(uint256 proposalDealId, PoRepMarket.DealState state) + function createDealProposal(uint256 proposalDealId, PoRepTypes.DealState state) public view - returns (PoRepMarket.DealProposal memory) + returns (PoRepTypes.DealProposal memory) { - return PoRepMarket.DealProposal({ + return PoRepTypes.DealProposal({ dealId: proposalDealId, client: clientAddress, provider: providerFilActorId, requirements: defaultRequirements, terms: defaultTerms, validator: validatorAddress, - state: state, railId: railId, + state: state, manifestLocation: expectedManifestLocation }); } @@ -84,7 +87,7 @@ contract PoRepMarketTest is Test { vm.prank(clientAddress); vm.expectEmit(true, true, true, true); emit PoRepMarket.DealProposalCreated( - dealId, clientAddress, providerFilActorId, defaultRequirements, expectedManifestLocation + dealId, clientAddress, providerFilActorId, defaultRequirements, expectedManifestLocation, totalDealSize ); poRepMarket.proposeDeal(defaultRequirements, defaultTerms, expectedManifestLocation); @@ -94,7 +97,7 @@ contract PoRepMarketTest is Test { vm.prank(clientAddress); poRepMarket.proposeDeal(defaultRequirements, defaultTerms, expectedManifestLocation); - PoRepMarket.DealProposal memory p = poRepMarket.getDealProposal(1); + PoRepTypes.DealProposal memory p = poRepMarket.getDealProposal(1); assertEq(p.dealId, 1); assertEq(p.client, clientAddress); assertEq(CommonTypes.FilActorId.unwrap(p.provider), CommonTypes.FilActorId.unwrap(providerFilActorId)); @@ -108,7 +111,7 @@ contract PoRepMarketTest is Test { assertEq(p.terms.durationDays, defaultTerms.durationDays); assertEq(p.validator, address(0)); assertEq(p.railId, 0); - assertTrue(p.state == PoRepMarket.DealState.Proposed); + assertTrue(p.state == PoRepTypes.DealState.Proposed); p = poRepMarket.getDealProposal(0); assertEq(p.dealId, 0); @@ -129,7 +132,7 @@ contract PoRepMarketTest is Test { function testShouldIncrementDealIdCounter() public { uint8 proposalsCount = 3; uint8 startingId = 1; - PoRepMarket.DealProposal memory p; + PoRepTypes.DealProposal memory p; // solhint-disable-next-line gas-strict-inequalities for (uint8 i = startingId; i <= proposalsCount; i++) { @@ -235,8 +238,8 @@ contract PoRepMarketTest is Test { abi.encodeWithSelector( PoRepMarket.DealNotInExpectedState.selector, dealId, - PoRepMarket.DealState.Proposed, - PoRepMarket.DealState.Accepted + PoRepTypes.DealState.Proposed, + PoRepTypes.DealState.Accepted ) ); vm.prank(validatorAddress); @@ -317,8 +320,8 @@ contract PoRepMarketTest is Test { abi.encodeWithSelector( PoRepMarket.DealNotInExpectedState.selector, dealId, - PoRepMarket.DealState.Rejected, - PoRepMarket.DealState.Proposed + PoRepTypes.DealState.Rejected, + PoRepTypes.DealState.Proposed ) ); vm.prank(clientAddress); @@ -388,8 +391,8 @@ contract PoRepMarketTest is Test { abi.encodeWithSelector( PoRepMarket.DealNotInExpectedState.selector, dealId, - PoRepMarket.DealState.Proposed, - PoRepMarket.DealState.Accepted + PoRepTypes.DealState.Proposed, + PoRepTypes.DealState.Accepted ) ); vm.prank(clientSmartContractAddress); @@ -408,8 +411,8 @@ contract PoRepMarketTest is Test { abi.encodeWithSelector( PoRepMarket.DealNotInExpectedState.selector, dealId, - PoRepMarket.DealState.Completed, - PoRepMarket.DealState.Accepted + PoRepTypes.DealState.Completed, + PoRepTypes.DealState.Accepted ) ); vm.prank(clientSmartContractAddress); @@ -488,8 +491,8 @@ contract PoRepMarketTest is Test { vm.prank(clientAddress); poRepMarket.proposeDeal(defaultRequirements, defaultTerms, expectedManifestLocation); - PoRepMarket.DealProposal memory p = poRepMarket.getDealProposal(dealId); - assertTrue(p.state == PoRepMarket.DealState.Accepted); + PoRepTypes.DealProposal memory p = poRepMarket.getDealProposal(dealId); + assertTrue(p.state == PoRepTypes.DealState.Accepted); } function testProposeDealAutoApproveEmitsBothEvents() public { @@ -498,7 +501,7 @@ contract PoRepMarketTest is Test { vm.prank(clientAddress); vm.expectEmit(true, true, true, true); emit PoRepMarket.DealProposalCreated( - dealId, clientAddress, providerFilActorId, defaultRequirements, expectedManifestLocation + dealId, clientAddress, providerFilActorId, defaultRequirements, expectedManifestLocation, totalDealSize ); vm.expectEmit(true, true, true, true); emit PoRepMarket.DealAccepted(dealId, clientAddress, providerFilActorId); @@ -511,8 +514,8 @@ contract PoRepMarketTest is Test { vm.prank(clientAddress); poRepMarket.proposeDeal(defaultRequirements, defaultTerms, expectedManifestLocation); - PoRepMarket.DealProposal memory p = poRepMarket.getDealProposal(dealId); - assertTrue(p.state == PoRepMarket.DealState.Proposed); + PoRepTypes.DealProposal memory p = poRepMarket.getDealProposal(dealId); + assertTrue(p.state == PoRepTypes.DealState.Proposed); } function testGetCompletedDeals() public { @@ -523,19 +526,19 @@ contract PoRepMarketTest is Test { ids[2] = 3; ids[3] = 4; ids[4] = 5; - porepMarekMock.setDealProposal(createDealProposal(ids[0], PoRepMarket.DealState.Completed)); - porepMarekMock.setDealProposal(createDealProposal(ids[1], PoRepMarket.DealState.Accepted)); - porepMarekMock.setDealProposal(createDealProposal(ids[2], PoRepMarket.DealState.Proposed)); - porepMarekMock.setDealProposal(createDealProposal(ids[3], PoRepMarket.DealState.Completed)); - porepMarekMock.setDealProposal(createDealProposal(ids[4], PoRepMarket.DealState.Rejected)); + porepMarekMock.setDealProposal(createDealProposal(ids[0], PoRepTypes.DealState.Completed)); + porepMarekMock.setDealProposal(createDealProposal(ids[1], PoRepTypes.DealState.Accepted)); + porepMarekMock.setDealProposal(createDealProposal(ids[2], PoRepTypes.DealState.Proposed)); + porepMarekMock.setDealProposal(createDealProposal(ids[3], PoRepTypes.DealState.Completed)); + porepMarekMock.setDealProposal(createDealProposal(ids[4], PoRepTypes.DealState.Rejected)); porepMarekMock.setDealIdsReadyForPayment(ids); - PoRepMarket.DealProposal[] memory dealProposal = porepMarekMock.getCompletedDeals(); + PoRepTypes.DealProposal[] memory dealProposal = porepMarekMock.getCompletedDeals(); assertEq(dealProposal.length, 2); assertEq(dealProposal[0].dealId, ids[0]); - assertTrue(dealProposal[0].state == PoRepMarket.DealState.Completed); + assertTrue(dealProposal[0].state == PoRepTypes.DealState.Completed); assertEq(dealProposal[1].dealId, ids[3]); - assertTrue(dealProposal[1].state == PoRepMarket.DealState.Completed); + assertTrue(dealProposal[1].state == PoRepTypes.DealState.Completed); } function testProposeDealRevertsEmptyManifestLocation() public { @@ -614,4 +617,88 @@ contract PoRepMarketTest is Test { vm.expectRevert(abi.encodeWithSelector(PoRepMarket.InvalidClientSmartContractAddress.selector)); poRepMarket.setClientSmartContract(address(0)); } + + function testTerminateDealEmitsEventAndSetsState() public { + address terminator = vm.addr(0x777); + uint256 endEpoch = 12345; + + vm.prank(clientAddress); + poRepMarket.proposeDeal(defaultRequirements, defaultTerms, expectedManifestLocation); + + vm.prank(providerOwnerAddress); + poRepMarket.acceptDeal(dealId); + + vm.startPrank(validatorAddress); + poRepMarket.updateValidator(dealId); + poRepMarket.updateRailId(dealId, railId); + vm.stopPrank(); + + vm.prank(clientSmartContractAddress); + poRepMarket.completeDeal(dealId); + + vm.expectEmit(true, true, true, true); + + emit PoRepMarket.DealTerminated(dealId, terminator, endEpoch); + vm.prank(validatorAddress); + poRepMarket.terminateDeal(dealId, terminator, endEpoch); + + PoRepTypes.DealProposal memory p = poRepMarket.getDealProposal(dealId); + assertTrue(p.state == PoRepTypes.DealState.Terminated); + } + + function testTerminateDealRevertsWhenDealDoesNotExist() public { + vm.expectRevert(abi.encodeWithSelector(PoRepMarket.DealDoesNotExist.selector)); + poRepMarket.terminateDeal(dealId, vm.addr(0x1), 1); + } + + function testTerminateDealRevertsWhenDealNotAccepted() public { + vm.prank(clientAddress); + poRepMarket.proposeDeal(defaultRequirements, defaultTerms, expectedManifestLocation); + + vm.expectRevert( + abi.encodeWithSelector( + PoRepMarket.DealNotInExpectedState.selector, + dealId, + PoRepTypes.DealState.Proposed, + PoRepTypes.DealState.Completed + ) + ); + vm.prank(validatorAddress); + poRepMarket.terminateDeal(dealId, vm.addr(0x2), 2); + } + + function testTerminateDealRevertsWhenValidatorNotSet() public { + vm.prank(clientAddress); + poRepMarket.proposeDeal(defaultRequirements, defaultTerms, expectedManifestLocation); + + vm.prank(providerOwnerAddress); + poRepMarket.acceptDeal(dealId); + + vm.prank(clientSmartContractAddress); + poRepMarket.completeDeal(dealId); + + address caller = vm.addr(0x999); + vm.expectRevert(abi.encodeWithSelector(PoRepMarket.CallerIsNotValidator.selector, dealId, caller)); + vm.prank(caller); + poRepMarket.terminateDeal(dealId, vm.addr(0x3), 3); + } + + function testTerminateDealRevertsWhenCallerIsNotValidator() public { + vm.prank(clientAddress); + poRepMarket.proposeDeal(defaultRequirements, defaultTerms, expectedManifestLocation); + + vm.prank(providerOwnerAddress); + poRepMarket.acceptDeal(dealId); + + vm.prank(validatorAddress); + poRepMarket.updateValidator(dealId); + + vm.prank(clientSmartContractAddress); + poRepMarket.completeDeal(dealId); + + address caller = vm.addr(0x999); + vm.expectRevert(abi.encodeWithSelector(PoRepMarket.CallerIsNotValidator.selector, dealId, caller)); + vm.prank(caller); + poRepMarket.terminateDeal(dealId, vm.addr(0x4), 4); + } } diff --git a/test/SPRegistry.t.sol b/test/SPRegistry.t.sol index 7ae3f61..0e9ea24 100644 --- a/test/SPRegistry.t.sol +++ b/test/SPRegistry.t.sol @@ -1441,4 +1441,9 @@ contract SPRegistryTest is Test { MockProxy proxy = new MockProxy(address(5555)); vm.etch(address(5555), address(proxy).code); } + + function testGetPayAddressForProviderReturnsZeroAddress() public view { + address payAddress = spRegistry.getPayee(provider1); + assertEq(payAddress, address(0)); + } } diff --git a/test/Validator.t.sol b/test/Validator.t.sol new file mode 100644 index 0000000..52f556a --- /dev/null +++ b/test/Validator.t.sol @@ -0,0 +1,768 @@ +// SPDX-License-Identifier: MIT +// solhint-disable +pragma solidity ^0.8.24; + +import {Test} from "lib/forge-std/src/Test.sol"; +import {Validator} from "../src/Validator.sol"; +import {SLIOracle} from "../src/SLIOracle.sol"; +import {SLIScorer} from "../src/SLIScorer.sol"; +import {IValidator} from "../src/interfaces/IValidator.sol"; +import {PoRepTypes} from "../src/types/PoRepTypes.sol"; +import {SLITypes} from "../src/types/SLITypes.sol"; +import {SPRegistry} from "../src/SPRegistry.sol"; + +import {FilecoinPayV1Mock} from "./contracts/FilecoinPayV1Mock.sol"; +import {ClientSCMock} from "./contracts/ClientSCMock.sol"; +import {PoRepMarketMock} from "./contracts/PoRepMarketMock.sol"; +import {SPRegistryMock} from "./contracts/SPRegistryMock.sol"; + +import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; +import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {IAccessControl} from "@openzeppelin/contracts/access/IAccessControl.sol"; + +import {CommonTypes} from "filecoin-solidity/v0.8/types/CommonTypes.sol"; + +contract ValidatorTest is Test { + Validator public validator; + FilecoinPayV1Mock public filecoinPayMock; + PoRepMarketMock public poRepMarketMock; + SPRegistryMock public spRegistryMock; + ClientSCMock public clientSCMock; + SLIOracle public sliOracle; + SLIScorer public sliScorer; + SPRegistry public spRegistry; + + address public admin; + address public porepService; + address public oracleUpdater; + address public clientSC; + IERC20 public token; + CommonTypes.FilActorId public providerFilActorId; + uint256 public dealId; + uint256 public railId; + string public expectedManifestLocation; + + SLITypes.SLIThresholds public defaultRequirements; + uint256 public constant BLOCK_TIMESTAMP = 1_772_000_000; + int64 public constant CHAIN_EPOCH = 5_800_000; + + function setUp() public { + filecoinPayMock = new FilecoinPayV1Mock(); + clientSCMock = new ClientSCMock(); + poRepMarketMock = new PoRepMarketMock(); + spRegistryMock = new SPRegistryMock(); + + admin = address(this); + porepService = vm.addr(0x123); + oracleUpdater = vm.addr(0xA11CE); + clientSC = address(clientSCMock); + token = IERC20(vm.addr(0x5)); + providerFilActorId = CommonTypes.FilActorId.wrap(20000); + dealId = 1; + railId = 1; + expectedManifestLocation = "https://example.com/manifest"; + + defaultRequirements = + SLITypes.SLIThresholds({retrievabilityBps: 8000, bandwidthMbps: 500, latencyMs: 200, indexingPct: 90}); + + poRepMarketMock.setDealProposal( + dealId, + PoRepTypes.DealProposal({ + dealId: dealId, + client: admin, + provider: providerFilActorId, + terms: SLITypes.DealTerms({dealSizeBytes: 1024, pricePerSector: 100, durationDays: 365}), + requirements: defaultRequirements, + validator: address(0), + state: PoRepTypes.DealState.Proposed, + railId: railId, + manifestLocation: expectedManifestLocation + }) + ); + + SLIOracle oracleImpl = new SLIOracle(); + ERC1967Proxy oracleProxy = new ERC1967Proxy(address(oracleImpl), ""); + sliOracle = SLIOracle(address(oracleProxy)); + sliOracle.initialize(admin, oracleUpdater); + + SLIScorer scorerImpl = new SLIScorer(); + ERC1967Proxy scorerProxy = new ERC1967Proxy(address(scorerImpl), ""); + sliScorer = SLIScorer(address(scorerProxy)); + sliScorer.initialize(admin, sliOracle); + + Validator impl = new Validator(); + ERC1967Proxy validatorProxy = new ERC1967Proxy(address(impl), ""); + validator = Validator(address(validatorProxy)); + + validator.initialize( + admin, + porepService, + address(filecoinPayMock), + address(sliScorer), + clientSC, + address(poRepMarketMock), + address(spRegistryMock), + dealId + ); + + filecoinPayMock.setOperatorApproval(token, admin, address(validator), true, 1_000_000, 1_000_000, 0, 0, 86_400); + + CommonTypes.FilActorId[] memory ids = new CommonTypes.FilActorId[](1); + ids[0] = CommonTypes.FilActorId.wrap(1); + clientSCMock.setAllocationIds(dealId, ids); + + vm.prank(admin); + validator.createRail(token); + + vm.prank(porepService); + validator.setDealEndEpoch(dealId, CommonTypes.ChainEpoch.wrap(int64(CHAIN_EPOCH))); + + vm.prank(oracleUpdater); + sliOracle.setSLI(providerFilActorId, defaultRequirements); + } + + function testIsAdminSet() public view { + bytes32 adminRole = validator.DEFAULT_ADMIN_ROLE(); + assertTrue(validator.hasRole(adminRole, admin)); + } + + function testEIP7201StorageSlotIsCorrect() public pure { + // solhint-disable-next-line gas-small-strings + bytes32 expected = keccak256(abi.encode(uint256(keccak256("porepmarket.storage.ValidatorStorage")) - 1)) + & ~bytes32(uint256(0xff)); + assertEq(expected, 0xf51cddbeb47ca42a561371db80eaffa401732269b8af46b255e3f43a7c044000); + } + + function testRailTerminatedCallerIsNotFilecoinPayRevert() public { + vm.expectRevert(Validator.CallerIsNotFilecoinPay.selector); + validator.railTerminated(1, address(this), 0); + } + + function testUpdateLockupPeriodUpdatesFilecoinPayRail() public { + uint256 newLockup = 123; + + vm.prank(admin); + validator.updateLockupPeriod(railId, newLockup); + + (uint256 lockupPeriod, uint256 lockupFixed) = filecoinPayMock.getRailLockup(railId); + assertEq(lockupPeriod, newLockup); + assertEq(lockupFixed, 0); + } + + function testImplementationContractCannotBeInitialized() public { + Validator impl = new Validator(); + vm.expectRevert(abi.encodeWithSelector(Initializable.InvalidInitialization.selector)); + impl.initialize( + admin, + porepService, + address(filecoinPayMock), + address(sliScorer), + clientSC, + address(poRepMarketMock), + address(spRegistryMock), + dealId + ); + } + + function testValidatorCannotBeReinitialized() public { + vm.expectRevert(abi.encodeWithSelector(Initializable.InvalidInitialization.selector)); + validator.initialize( + admin, + porepService, + address(filecoinPayMock), + address(sliScorer), + clientSC, + address(poRepMarketMock), + address(spRegistryMock), + dealId + ); + } + + function testValidatePaymentTooEarlyForNextPayout() public { + vm.prank(address(filecoinPayMock)); + IValidator.ValidationResult memory result = validator.validatePayment(1, 100, 0, 0, 1); + + assertEq(result.modifiedAmount, 0); + assertEq(result.settleUpto, 0); + assertEq(result.note, "1mo payout period not reached"); + } + + function testValidatePaymentDatacapMismatch() public { + vm.prank(address(filecoinPayMock)); + IValidator.ValidationResult memory result = validator.validatePayment(1, 100, 0, type(uint256).max, 1); + + assertEq(result.modifiedAmount, 0); + assertEq(result.settleUpto, type(uint256).max); + assertEq(result.note, "data size does not match the deal proposal"); + } + + function testValidatePaymentFullSlashWhenScoreZero() public { + clientSCMock.setDataSizeMatching(dealId, true); + + vm.prank(oracleUpdater); + sliOracle.setSLI( + providerFilActorId, + SLITypes.SLIThresholds({retrievabilityBps: 0, bandwidthMbps: 0, latencyMs: 0, indexingPct: 0}) + ); + + vm.prank(address(filecoinPayMock)); + IValidator.ValidationResult memory result = validator.validatePayment(1, 100, 0, type(uint256).max, 1); + + assertEq(result.modifiedAmount, 0); + assertEq(result.settleUpto, type(uint256).max); + assertEq(result.note, "score below required threshold"); + } + + function testValidatePaymentOkWhenScorePositiveAndDatacapMatches() public { + clientSCMock.setDataSizeMatching(dealId, true); + + vm.prank(oracleUpdater); + sliOracle.setSLI(providerFilActorId, defaultRequirements); + + vm.prank(address(filecoinPayMock)); + IValidator.ValidationResult memory result = validator.validatePayment(1, 100, 0, 86_400, 1); + + assertEq(result.modifiedAmount, 100); + assertEq(result.settleUpto, 86_400); + assertEq(result.note, "payment validated successfully"); + } + + function testValidatePaymentCallerIsNotFilecoinPayRevert() public { + vm.expectRevert(Validator.CallerIsNotFilecoinPay.selector); + validator.validatePayment(1, 100, 0, 0, 1); + } + + function testCreateRailCallerIsNotClientRevert() public { + address notClient = vm.addr(0xCAFE); + vm.expectRevert(Validator.CallerIsNotClient.selector); + vm.prank(notClient); + validator.createRail(token); + } + + function testValidatePaymentInvalidRailIdRevert() public { + uint256 wrongRailId = railId + 1; + + vm.expectRevert(abi.encodeWithSelector(Validator.InvalidRailId.selector, railId, wrongRailId)); + vm.prank(address(filecoinPayMock)); + validator.validatePayment(wrongRailId, 100, 0, type(uint256).max, 1); + } + + function testUpdateLockupPeriodInvalidRailIdRevert() public { + uint256 wrongRailId = railId + 1; + + vm.expectRevert(abi.encodeWithSelector(Validator.InvalidRailId.selector, railId, wrongRailId)); + vm.prank(admin); + validator.updateLockupPeriod(wrongRailId, 123); + } + + function testRailTerminatedInvalidRailIdRevert() public { + uint256 wrongRailId = railId + 1; + + vm.expectRevert(abi.encodeWithSelector(Validator.InvalidRailId.selector, railId, wrongRailId)); + vm.prank(address(filecoinPayMock)); + validator.railTerminated(wrongRailId, address(this), 10); + } + + function testInitializeRevertsWhenAdminIsZeroAddress() public { + Validator impl = new Validator(); + ERC1967Proxy proxy = new ERC1967Proxy(address(impl), ""); + Validator newValidator = Validator(address(proxy)); + + vm.expectRevert(Validator.InvalidAdminAddress.selector); + newValidator.initialize( + address(0), + porepService, + address(filecoinPayMock), + address(sliScorer), + clientSC, + address(poRepMarketMock), + address(spRegistryMock), + dealId + ); + } + + function testInitializeRevertsWhenPoRepServiceIsZeroAddress() public { + Validator impl = new Validator(); + ERC1967Proxy proxy = new ERC1967Proxy(address(impl), ""); + Validator newValidator = Validator(address(proxy)); + + vm.expectRevert(Validator.InvalidPoRepServiceAddress.selector); + newValidator.initialize( + admin, + address(0), + address(filecoinPayMock), + address(sliScorer), + clientSC, + address(poRepMarketMock), + address(spRegistryMock), + dealId + ); + } + + function testInitializeRevertsWhenFilecoinPayIsZeroAddress() public { + Validator impl = new Validator(); + ERC1967Proxy proxy = new ERC1967Proxy(address(impl), ""); + Validator newValidator = Validator(address(proxy)); + + vm.expectRevert(Validator.InvalidFilecoinPayAddress.selector); + newValidator.initialize( + admin, + porepService, + address(0), + address(sliScorer), + clientSC, + address(poRepMarketMock), + address(spRegistryMock), + dealId + ); + } + + function testInitializeRevertsWhenSLIScorerIsZeroAddress() public { + Validator impl = new Validator(); + ERC1967Proxy proxy = new ERC1967Proxy(address(impl), ""); + Validator newValidator = Validator(address(proxy)); + + vm.expectRevert(Validator.InvalidSLIScorerAddress.selector); + newValidator.initialize( + admin, + porepService, + address(filecoinPayMock), + address(0), + clientSC, + address(poRepMarketMock), + address(spRegistryMock), + dealId + ); + } + + function testInitializeRevertsWhenClientSCIsZeroAddress() public { + Validator impl = new Validator(); + ERC1967Proxy proxy = new ERC1967Proxy(address(impl), ""); + Validator newValidator = Validator(address(proxy)); + + vm.expectRevert(Validator.InvalidClientSCAddress.selector); + newValidator.initialize( + admin, + porepService, + address(filecoinPayMock), + address(sliScorer), + address(0), + address(poRepMarketMock), + address(spRegistryMock), + dealId + ); + } + + function testInitializeRevertsWhenPoRepMarketIsZeroAddress() public { + Validator impl = new Validator(); + ERC1967Proxy proxy = new ERC1967Proxy(address(impl), ""); + Validator newValidator = Validator(address(proxy)); + + vm.expectRevert(Validator.InvalidPoRepMarketAddress.selector); + newValidator.initialize( + admin, + porepService, + address(filecoinPayMock), + address(sliScorer), + clientSC, + address(0), + address(spRegistryMock), + dealId + ); + } + + function testInitializeRevertsWhenSpRegistryIsZeroAddress() public { + Validator impl = new Validator(); + ERC1967Proxy proxy = new ERC1967Proxy(address(impl), ""); + Validator newValidator = Validator(address(proxy)); + + vm.expectRevert(Validator.InvalidSPRegistryAddress.selector); + newValidator.initialize( + admin, + porepService, + address(filecoinPayMock), + address(sliScorer), + clientSC, + address(poRepMarketMock), + address(0), + dealId + ); + } + + function testModifyRailPaymentEmitsRailPaymentModified() public { + PoRepTypes.DealProposal memory dealProposal = poRepMarketMock.getDealProposal(dealId); + dealProposal.terms.pricePerSector = 2_000_000; + poRepMarketMock.setDealProposal(dealId, dealProposal); + + uint256 sectorCount = clientSCMock.getClientAllocationIdsPerDeal(dealId).length; + uint256 durationMonths = (uint256(dealProposal.terms.durationDays) + 29) / 30; + uint256 totalEpochs = durationMonths * 86_400; + uint256 expectedRate = (dealProposal.terms.pricePerSector * sectorCount) / totalEpochs; + + vm.expectEmit(true, false, false, true, address(validator)); + emit Validator.RailPaymentModified(railId, expectedRate); + + vm.prank(porepService); + validator.modifyRailPayment(railId); + } + + function testUpdateLockupPeriodEmitsLockupPeriodUpdated() public { + uint256 newLockupPeriod = 123; + + vm.expectEmit(true, false, false, true, address(validator)); + emit Validator.LockupPeriodUpdated(railId, newLockupPeriod); + + validator.updateLockupPeriod(railId, newLockupPeriod); + } + + function testRailTerminatedEmitsRailTerminated() public { + address terminator = address(0xBEEF); + uint256 endEpoch = 777; + + vm.expectEmit(true, true, false, true, address(validator)); + emit Validator.RailTerminated(railId, terminator, endEpoch); + + vm.prank(address(filecoinPayMock)); + validator.railTerminated(railId, terminator, endEpoch); + } + + function testCreateRailRevertsWhenRailAlreadyCreated() public { + vm.expectRevert(Validator.RailAlreadyCreated.selector); + vm.prank(admin); + validator.createRail(token); + } + + function testTerminateRailTerminatesFilecoinPayRailAsAnAdmin() public { + assertFalse(filecoinPayMock.terminated(railId)); + vm.prank(admin); + validator.terminateRail(railId); + assertTrue(filecoinPayMock.terminated(railId)); + } + + function testTerminateRailTerminatesFilecoinPayRailAsPoRepService() public { + assertFalse(filecoinPayMock.terminated(railId)); + vm.prank(porepService); + validator.terminateRail(railId); + assertTrue(filecoinPayMock.terminated(railId)); + } + + function testTerminateRailRevertsWhenCallerHasPoRepServiceRole() public { + vm.expectRevert(Validator.UnauthorizedCaller.selector); + vm.prank(address(123)); + validator.terminateRail(railId); + } + + function testTerminateRailRevertsWhenCallerHasAdminRole() public { + vm.expectRevert(Validator.UnauthorizedCaller.selector); + vm.prank(address(123)); + validator.terminateRail(railId); + } + + function testValidatePaymentReturnsDealEndedWhenFromEpochPastDealEndEpoch() public { + vm.prank(porepService); + validator.setDealEndEpoch(dealId, CommonTypes.ChainEpoch.wrap(int64(10))); + + clientSCMock.setDataSizeMatching(dealId, true); + + vm.prank(address(filecoinPayMock)); + IValidator.ValidationResult memory result = validator.validatePayment(railId, 100, 10, 86_410, 1); + + assertEq(result.modifiedAmount, 0); + assertEq(result.settleUpto, 10); + assertEq(result.note, "deal ended"); + } + + function testValidatePaymentCapsSettlementToDealEndEpoch() public { + clientSCMock.setDataSizeMatching(dealId, true); + + vm.prank(porepService); + validator.setDealEndEpoch(dealId, CommonTypes.ChainEpoch.wrap(int64(1000))); + + vm.prank(oracleUpdater); + sliOracle.setSLI(providerFilActorId, defaultRequirements); + + vm.prank(address(filecoinPayMock)); + IValidator.ValidationResult memory result = validator.validatePayment(railId, 10_000, 0, 86_400, 10); + + assertEq(result.modifiedAmount, 10 * 1000); + assertEq(result.settleUpto, 1000); + assertEq(result.note, "payment limited to deal endepoch"); + } + + function testSetDealEndEpochCallerIsNotPoRepServiceRevert() public { + address unauthorized = vm.addr(0x321); + bytes32 expectedRole = validator.POREP_SERVICE_ROLE(); + + vm.expectRevert( + abi.encodeWithSelector(IAccessControl.AccessControlUnauthorizedAccount.selector, unauthorized, expectedRole) + ); + vm.prank(unauthorized); + validator.setDealEndEpoch(dealId, CommonTypes.ChainEpoch.wrap(int64(1_000_000))); + } + + function testSetDealEndEpochInvalidDealIdRevert() public { + uint256 wrongDealId = dealId + 1; + + vm.expectRevert(Validator.InvalidDealId.selector); + vm.prank(porepService); + validator.setDealEndEpoch(wrongDealId, CommonTypes.ChainEpoch.wrap(int64(1_000_000))); + } + + function testCreateRailRevertsWhenOperatorNotApproved() public { + Validator impl = new Validator(); + ERC1967Proxy proxy = new ERC1967Proxy(address(impl), ""); + Validator newValidator = Validator(address(proxy)); + + newValidator.initialize( + admin, + porepService, + address(filecoinPayMock), + address(sliScorer), + clientSC, + address(poRepMarketMock), + address(spRegistryMock), + dealId + ); + + filecoinPayMock.setOperatorApproval( + token, admin, address(newValidator), false, 1_000_000, 1_000_000, 0, 0, 86_400 + ); + + vm.expectRevert(Validator.OperatorNotApproved.selector); + newValidator.createRail(token); + } + + function testCreateRailRevertsWhenMaxLockupPeriodLessThanMinimum() public { + Validator impl = new Validator(); + ERC1967Proxy proxy = new ERC1967Proxy(address(impl), ""); + Validator newValidator = Validator(address(proxy)); + + newValidator.initialize( + admin, + porepService, + address(filecoinPayMock), + address(sliScorer), + clientSC, + address(poRepMarketMock), + address(spRegistryMock), + dealId + ); + + filecoinPayMock.setOperatorApproval( + token, admin, address(newValidator), true, 1_000_000, 1_000_000, 0, 0, 86_399 + ); + + vm.expectRevert(Validator.MaxLockupPeriodLessThanMinimum.selector); + newValidator.createRail(token); + } + + function testCreateRailRevertsWhenLockupAllowanceIsZero() public { + Validator impl = new Validator(); + ERC1967Proxy proxy = new ERC1967Proxy(address(impl), ""); + Validator newValidator = Validator(address(proxy)); + + newValidator.initialize( + admin, + porepService, + address(filecoinPayMock), + address(sliScorer), + clientSC, + address(poRepMarketMock), + address(spRegistryMock), + dealId + ); + + filecoinPayMock.setOperatorApproval(token, admin, address(newValidator), true, 1_000_000, 0, 0, 0, 86_400); + + vm.expectRevert(Validator.InvalidLockupAllowance.selector); + newValidator.createRail(token); + } + + function testCreateRailRevertsWhenRateAllowanceIsZero() public { + Validator impl = new Validator(); + ERC1967Proxy proxy = new ERC1967Proxy(address(impl), ""); + Validator newValidator = Validator(address(proxy)); + + newValidator.initialize( + admin, + porepService, + address(filecoinPayMock), + address(sliScorer), + clientSC, + address(poRepMarketMock), + address(spRegistryMock), + dealId + ); + + filecoinPayMock.setOperatorApproval(token, admin, address(newValidator), true, 0, 1_000_000, 0, 0, 86_400); + + vm.expectRevert(Validator.InvalidRateAllowance.selector); + newValidator.createRail(token); + } + + function testModifyRailPaymentRevertsWhenSectorCountIsZero() public { + CommonTypes.FilActorId[] memory emptyIds = new CommonTypes.FilActorId[](0); + clientSCMock.setAllocationIds(dealId, emptyIds); + + vm.expectRevert(Validator.InvalidSectorCount.selector); + vm.prank(porepService); + validator.modifyRailPayment(railId); + } + + function testModifyRailPaymentRevertsWhenDealDurationIsZero() public { + CommonTypes.FilActorId[] memory ids = new CommonTypes.FilActorId[](1); + ids[0] = CommonTypes.FilActorId.wrap(1); + clientSCMock.setAllocationIds(dealId, ids); + + PoRepTypes.DealProposal memory dealProposal = poRepMarketMock.getDealProposal(dealId); + dealProposal.terms.durationDays = 0; + poRepMarketMock.setDealProposal(dealId, dealProposal); + + vm.expectRevert(Validator.InvalidDealDuration.selector); + vm.prank(porepService); + validator.modifyRailPayment(railId); + } + + function testValidatePaymentRevertsWhenDealNotCompleted() public { + vm.prank(porepService); + validator.setDealEndEpoch(dealId, CommonTypes.ChainEpoch.wrap(int64(0))); + + vm.expectRevert(abi.encodeWithSelector(Validator.DealNotCompleted.selector, dealId)); + vm.prank(address(filecoinPayMock)); + validator.validatePayment(railId, 100, 0, 86_400, 1); + } + + function testDisableFutureRailPaymentsInvalidRailIdRevert() public { + uint256 wrongRailId = railId + 1; + + vm.expectRevert(abi.encodeWithSelector(Validator.InvalidRailId.selector, railId, wrongRailId)); + vm.prank(porepService); + validator.disableFutureRailPayments(wrongRailId); + } + + function testModifyRailPaymentInvalidRailIdRevert() public { + uint256 wrongRailId = railId + 1; + + vm.expectRevert(abi.encodeWithSelector(Validator.InvalidRailId.selector, railId, wrongRailId)); + vm.prank(porepService); + validator.modifyRailPayment(wrongRailId); + } + + function testTerminateRailInvalidRailIdRevert() public { + uint256 wrongRailId = railId + 1; + + vm.expectRevert(abi.encodeWithSelector(Validator.InvalidRailId.selector, railId, wrongRailId)); + validator.terminateRail(wrongRailId); + } + + function testCreateRailEmitsInitialLockupPeriodUpdated() public { + Validator impl = new Validator(); + ERC1967Proxy proxy = new ERC1967Proxy(address(impl), ""); + Validator newValidator = Validator(address(proxy)); + + newValidator.initialize( + admin, + porepService, + address(filecoinPayMock), + address(sliScorer), + clientSC, + address(poRepMarketMock), + address(spRegistryMock), + dealId + ); + + filecoinPayMock.setOperatorApproval( + token, admin, address(newValidator), true, 1_000_000, 1_000_000, 0, 0, 86_400 + ); + + vm.expectEmit(true, false, false, true, address(newValidator)); + emit Validator.LockupPeriodUpdated(2, 86_400); + + newValidator.createRail(token); + } + + function testSetDealEndEpochEmitsDealEndEpochUpdated() public { + CommonTypes.ChainEpoch newEndEpoch = CommonTypes.ChainEpoch.wrap(int64(123_456)); + + vm.expectEmit(true, false, false, true, address(validator)); + emit Validator.DealEndEpochUpdated(dealId, newEndEpoch); + + vm.prank(porepService); + validator.setDealEndEpoch(dealId, newEndEpoch); + } + + function testDisableFutureRailPaymentsEmitsRailDisabled() public { + vm.expectEmit(true, false, false, true, address(validator)); + emit Validator.RailDisabled(railId); + + vm.prank(porepService); + validator.disableFutureRailPayments(railId); + } + + function testValidatePaymentCapsSettlementToEarlyTerminatedEpoch() public { + clientSCMock.setDataSizeMatching(dealId, true); + + vm.prank(oracleUpdater); + sliOracle.setSLI(providerFilActorId, defaultRequirements); + + vm.warp(BLOCK_TIMESTAMP); + + vm.prank(oracleUpdater); + sliOracle.setSLI(providerFilActorId, defaultRequirements); + + vm.prank(porepService); + validator.disableFutureRailPayments(railId); + + vm.prank(address(filecoinPayMock)); + IValidator.ValidationResult memory result = validator.validatePayment(railId, 2_000_000, 0, 200_000, 10); + + assertEq(result.modifiedAmount, 10); + assertEq(result.settleUpto, 1); + assertEq(result.note, "payment limited to deal endepoch"); + } + + function testValidatePaymentUsesEarlyTerminatedEpochWhenEarlierThanDealEndEpoch() public { + clientSCMock.setDataSizeMatching(dealId, true); + + // forge-lint: disable-next-line(unsafe-typecast) + uint256 chainEpochConversion = uint256(uint64(CHAIN_EPOCH)); + uint256 earlyTerminationEpoch = chainEpochConversion - 100_000; + vm.roll(earlyTerminationEpoch); + + vm.prank(oracleUpdater); + sliOracle.setSLI(providerFilActorId, defaultRequirements); + + vm.prank(porepService); + validator.disableFutureRailPayments(railId); + + vm.prank(address(filecoinPayMock)); + IValidator.ValidationResult memory result = + validator.validatePayment(railId, 50000000, 0, chainEpochConversion, 10); + + assertEq(result.modifiedAmount, 10 * earlyTerminationEpoch); + assertEq(result.settleUpto, earlyTerminationEpoch); + assertEq(result.note, "payment limited to deal endepoch"); + } + + function testModifyRailPaymentRevertsWhenCalculatedAmountIsZero() public { + CommonTypes.FilActorId[] memory ids = new CommonTypes.FilActorId[](1); + ids[0] = CommonTypes.FilActorId.wrap(1); + clientSCMock.setAllocationIds(dealId, ids); + + PoRepTypes.DealProposal memory dealProposal = poRepMarketMock.getDealProposal(dealId); + dealProposal.terms.pricePerSector = 1; + dealProposal.terms.durationDays = 3650; + poRepMarketMock.setDealProposal(dealId, dealProposal); + + vm.expectRevert(Validator.InvalidZeroAmount.selector); + vm.prank(porepService); + validator.modifyRailPayment(railId); + } + + function testSetDealEndEpochNegativeEndEpochRevert() public { + vm.expectRevert(Validator.NegativeEndEpoch.selector); + vm.prank(porepService); + validator.setDealEndEpoch(dealId, CommonTypes.ChainEpoch.wrap(int64(-1))); + } +} diff --git a/test/ValidatorFactory.t.sol b/test/ValidatorFactory.t.sol index a7ec0bc..509e621 100644 --- a/test/ValidatorFactory.t.sol +++ b/test/ValidatorFactory.t.sol @@ -13,8 +13,8 @@ import {ValidatorFactory} from "../src/ValidatorFactory.sol"; import {Validator} from "../src/Validator.sol"; import {IAccessControl} from "@openzeppelin/contracts/access/IAccessControl.sol"; import {PoRepMarketMock} from "./contracts/PoRepMarketMock.sol"; -import {PoRepMarket} from "../src/PoRepMarket.sol"; import {SLITypes} from "../src/types/SLITypes.sol"; +import {PoRepTypes} from "../src/types/PoRepTypes.sol"; contract ValidatorFactoryTest is Test { ValidatorFactory public factory; @@ -49,7 +49,7 @@ contract ValidatorFactoryTest is Test { factoryImpl = new ValidatorFactory(); poRepMarketMock.setDealProposal( dealId, - PoRepMarket.DealProposal({ + PoRepTypes.DealProposal({ dealId: dealId, client: client, provider: provider, @@ -58,7 +58,7 @@ contract ValidatorFactoryTest is Test { }), terms: SLITypes.DealTerms({dealSizeBytes: 1_000_000, pricePerSector: 100, durationDays: 365}), validator: vm.addr(10), - state: PoRepMarket.DealState.Accepted, + state: PoRepTypes.DealState.Accepted, railId: 200, manifestLocation: "https://example.com/manifest" }) diff --git a/test/contracts/ClientSCMock.sol b/test/contracts/ClientSCMock.sol new file mode 100644 index 0000000..886fcef --- /dev/null +++ b/test/contracts/ClientSCMock.sol @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: MIT +// solhint-disable + +pragma solidity ^0.8.24; + +import {CommonTypes} from "filecoin-solidity/v0.8/types/CommonTypes.sol"; +import {Client} from "../../src/Client.sol"; + +contract ClientSCMock { + mapping(CommonTypes.FilActorId provider => bool ok) public valid; + mapping(uint256 dealId => bool matches) public dataSizeMatches; + mapping(uint256 dealId => Client.Deal deal) public deals; + mapping(uint256 dealId => CommonTypes.FilActorId[] ids) internal allocationIds; + + function setValid(CommonTypes.FilActorId provider, bool ok) external { + valid[provider] = ok; + } + + function setDataSizeMatching(uint256 dealId, bool matches) external { + dataSizeMatches[dealId] = matches; + } + + function isDataSizeMatching(uint256 dealId) external view returns (bool) { + return dataSizeMatches[dealId]; + } + + function setLongestDealTerm(uint256 dealId, int64 longestDealTerm) external { + deals[dealId].longestDealTerm = CommonTypes.ChainEpoch.wrap(longestDealTerm); + } + + function getClientDealInfo(uint256 dealId) external view returns (Client.Deal memory) { + return deals[dealId]; + } + + function setAllocationIds(uint256 dealId, CommonTypes.FilActorId[] calldata ids_) external { + delete allocationIds[dealId]; + for (uint256 i = 0; i < ids_.length; i++) { + allocationIds[dealId].push(ids_[i]); + } + } + + function getClientAllocationIdsPerDeal(uint256 dealId) external view returns (CommonTypes.FilActorId[] memory) { + return allocationIds[dealId]; + } +} diff --git a/test/contracts/FilecoinPayV1Mock.sol b/test/contracts/FilecoinPayV1Mock.sol new file mode 100644 index 0000000..23e842b --- /dev/null +++ b/test/contracts/FilecoinPayV1Mock.sol @@ -0,0 +1,118 @@ +// SPDX-License-Identifier: MIT +// solhint-disable + +pragma solidity ^0.8.24; + +import {IFilecoinPayV1} from "../../src/interfaces/IFilecoinPayV1.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +contract FilecoinPayV1Mock is IFilecoinPayV1 { + uint256 public nextRailId = 1; + mapping(uint256 => bool) public terminated; + mapping(IERC20 => mapping(address => mapping(address => OperatorApproval))) internal _operatorApprovals; + + struct OperatorApproval { + bool isApproved; + uint256 rateAllowance; + uint256 lockupAllowance; + uint256 rateUsage; + uint256 lockupUsage; + uint256 maxLockupPeriod; + } + + function setOperatorApproval( + IERC20 token, + address client, + address operator, + bool isApproved, + uint256 rateAllowance, + uint256 lockupAllowance, + uint256 rateUsage, + uint256 lockupUsage, + uint256 maxLockupPeriod + ) external { + _operatorApprovals[token][client][operator] = OperatorApproval({ + isApproved: isApproved, + rateAllowance: rateAllowance, + lockupAllowance: lockupAllowance, + rateUsage: rateUsage, + lockupUsage: lockupUsage, + maxLockupPeriod: maxLockupPeriod + }); + } + + function operatorApprovals(IERC20 token, address client, address operator) + external + view + override + returns ( + bool isApproved, + uint256 rateAllowance, + uint256 lockupAllowance, + uint256 rateUsage, + uint256 lockupUsage, + uint256 maxLockupPeriod + ) + { + OperatorApproval storage approval = _operatorApprovals[token][client][operator]; + return ( + approval.isApproved, + approval.rateAllowance, + approval.lockupAllowance, + approval.rateUsage, + approval.lockupUsage, + approval.maxLockupPeriod + ); + } + + struct Rail { + IERC20 token; + address payer; + address payee; + address operator; + uint256 commissionRateBps; + address serviceFeeRecipient; + uint256 lockupPeriod; + uint256 lockupFixed; + } + + mapping(uint256 => Rail) public rails; + + function createRail( + IERC20 token, + address payer, + address payee, + address operator, + uint256 commissionRateBps, + address serviceFeeRecipient + ) external override returns (uint256 railId) { + railId = nextRailId++; + rails[railId] = Rail({ + token: token, + payer: payer, + payee: payee, + operator: operator, + commissionRateBps: commissionRateBps, + serviceFeeRecipient: serviceFeeRecipient, + lockupPeriod: 0, + lockupFixed: 0 + }); + } + + function modifyRailLockup(uint256 railId, uint256 newLockupPeriod, uint256 lockupFixed) external override { + Rail storage r = rails[railId]; + r.lockupPeriod = newLockupPeriod; + r.lockupFixed = lockupFixed; + } + + function modifyRailPayment(uint256 railId, uint256 newRate, uint256 oneTimePayment) external override {} + + function terminateRail(uint256 railId) external override { + terminated[railId] = true; + } + + function getRailLockup(uint256 railId) external view returns (uint256 lockupPeriod, uint256 lockupFixed) { + Rail storage r = rails[railId]; + return (r.lockupPeriod, r.lockupFixed); + } +} diff --git a/test/contracts/MinerUtilsHarness.sol b/test/contracts/MinerUtilsHarness.sol index 8098bc2..3084de4 100644 --- a/test/contracts/MinerUtilsHarness.sol +++ b/test/contracts/MinerUtilsHarness.sol @@ -4,7 +4,7 @@ pragma solidity =0.8.25; import {CommonTypes} from "filecoin-solidity/v0.8/types/CommonTypes.sol"; -import {MinerUtils} from "../../src/libs/MinerUtils.sol"; +import {MinerUtils} from "../../src/lib/MinerUtils.sol"; contract MinerUtilsHarness { function isControllingAddress(CommonTypes.FilActorId minerID, address addr) external view returns (bool) { diff --git a/test/contracts/PoRepMarketContractMock.sol b/test/contracts/PoRepMarketContractMock.sol index 662d19a..04f7a8d 100644 --- a/test/contracts/PoRepMarketContractMock.sol +++ b/test/contracts/PoRepMarketContractMock.sol @@ -4,6 +4,7 @@ pragma solidity =0.8.25; import {PoRepMarket} from "../../src/PoRepMarket.sol"; +import {PoRepTypes} from "../../src/types/PoRepTypes.sol"; import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; contract PoRepMarketContractMock is PoRepMarket { @@ -16,7 +17,7 @@ contract PoRepMarketContractMock is PoRepMarket { } } - function setDealProposal(PoRepMarket.DealProposal calldata dealProposal) external { + function setDealProposal(PoRepTypes.DealProposal calldata dealProposal) external { _getStorage()._dealProposals[++_getStorage()._dealIdCounter] = dealProposal; } diff --git a/test/contracts/PoRepMarketMock.sol b/test/contracts/PoRepMarketMock.sol index a399f82..6bc405c 100644 --- a/test/contracts/PoRepMarketMock.sol +++ b/test/contracts/PoRepMarketMock.sol @@ -3,16 +3,16 @@ pragma solidity =0.8.25; -import {PoRepMarket} from "../../src/PoRepMarket.sol"; +import {PoRepTypes} from "../../src/types/PoRepTypes.sol"; contract PoRepMarketMock { - mapping(uint256 dealId => PoRepMarket.DealProposal deal) public deals; + mapping(uint256 dealId => PoRepTypes.DealProposal deal) public deals; - function setDealProposal(uint256 dealId, PoRepMarket.DealProposal calldata dealProposal) external { + function setDealProposal(uint256 dealId, PoRepTypes.DealProposal calldata dealProposal) external { deals[dealId] = dealProposal; } - function getDealProposal(uint256 dealId) external view returns (PoRepMarket.DealProposal memory) { + function getDealProposal(uint256 dealId) external view returns (PoRepTypes.DealProposal memory) { return deals[dealId]; } @@ -20,5 +20,17 @@ contract PoRepMarketMock { function completeDeal(uint256) external { //noop } + + function updateValidator(uint256 dealId) external { + deals[dealId].validator = msg.sender; + } + + function updateRailId(uint256 dealId, uint256 newRailId) external { + deals[dealId].railId = newRailId; + } + + function terminateDeal(uint256 dealId, address, uint256) external { + deals[dealId].state = PoRepTypes.DealState.Terminated; + } } diff --git a/test/contracts/ResolveAddressPrecompileFailingMock.sol b/test/contracts/ResolveAddressPrecompileFailingMock.sol new file mode 100644 index 0000000..7155515 --- /dev/null +++ b/test/contracts/ResolveAddressPrecompileFailingMock.sol @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: MIT +// solhint-disable + +pragma solidity ^0.8.24; + +contract ResolveAddressPrecompileFailingMock { + fallback(bytes calldata data) external payable returns (bytes memory) { + return abi.encode(data); + } +} diff --git a/test/contracts/SPRegistryMock.sol b/test/contracts/SPRegistryMock.sol index 5ef4636..aed78cc 100644 --- a/test/contracts/SPRegistryMock.sol +++ b/test/contracts/SPRegistryMock.sol @@ -21,6 +21,7 @@ contract SPRegistryMock is ISPRegistry { function getProviderForDeal(SLITypes.SLIThresholds calldata, SLITypes.DealTerms calldata) external + view returns (CommonTypes.FilActorId, bool) { return (nextProvider, nextAutoApprove); @@ -99,4 +100,6 @@ contract SPRegistryMock is ISPRegistry { function getToleranceBps() external pure returns (uint256) { return 0; } + + function getPayAddressForProvider(CommonTypes.FilActorId provider) external view returns (address) {} }