From 9c8dce2e52e95c34340f90d03416338f0f588b76 Mon Sep 17 00:00:00 2001 From: 0xJabberwock <0xjabberwock@defi.sucks> Date: Thu, 28 Nov 2024 08:12:50 -0300 Subject: [PATCH] test(medusa): requester properties (#57) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # 🤖 Linear Closes GRT-XXX --------- Co-authored-by: drgorillamd <83670532+drgorillamd@users.noreply.github.com> Co-authored-by: Simon Something /DrGoNoGo <83670532+simon-something@users.noreply.github.com> --- .solhint.tests.json | 2 +- medusa.json | 4 +- test/invariants/FuzzTest.t.sol | 5 +- test/invariants/Setup.t.sol | 108 +++++++++++++- test/invariants/handlers/BaseHandler.t.sol | 66 +++++---- .../HandlerBondEscalationModule.t.sol | 22 ++- .../HandlerBondedResponseModule.t.sol | 7 +- .../handlers/HandlerCouncilArbitrator.t.sol | 6 +- .../handlers/HandlerEBOFinalityModule.t.sol | 6 +- .../handlers/HandlerEBORequestCreator.t.sol | 89 +++++------ .../HandlerHorizonAccountingExtension.t.sol | 50 ++++--- test/invariants/handlers/HandlerOracle.t.sol | 30 ++-- test/invariants/handlers/HandlerParent.t.sol | 1 - test/invariants/helpers/FuzzERC20.sol | 2 +- .../invariants/helpers/MockEpochManager.t.sol | 10 +- .../helpers/MockHorizonStaking.t.sol | 17 ++- test/invariants/helpers/Utils.t.sol | 120 ++++++++++++++- .../properties/PropertyDispute.t.sol | 138 ++++++++++++++++++ test/invariants/properties/PropertyEbo.t.sol | 16 -- .../properties/PropertyFinalize.t.sol | 29 ++++ .../properties/PropertyParent.t.sol | 12 +- .../properties/PropertyProposer.t.sol | 45 ++++++ .../properties/PropertyRequester.t.sol | 78 ++++++++++ .../properties/PropertySanityCheck.t.sol | 101 ------------- 24 files changed, 680 insertions(+), 284 deletions(-) create mode 100644 test/invariants/properties/PropertyDispute.t.sol delete mode 100644 test/invariants/properties/PropertyEbo.t.sol create mode 100644 test/invariants/properties/PropertyFinalize.t.sol create mode 100644 test/invariants/properties/PropertyProposer.t.sol create mode 100644 test/invariants/properties/PropertyRequester.t.sol delete mode 100644 test/invariants/properties/PropertySanityCheck.t.sol diff --git a/.solhint.tests.json b/.solhint.tests.json index fb26b4e..04950e4 100644 --- a/.solhint.tests.json +++ b/.solhint.tests.json @@ -10,7 +10,7 @@ "var-name-mixedcase": "off", "const-name-snakecase": "off", "no-inline-assembly": "off", - "no-empty-blocks": "error", + "no-empty-blocks": "off", "definition-name-capwords": "off", "named-parameters-function": "off", "no-global-import": "off", diff --git a/medusa.json b/medusa.json index 146d4a0..06d20b7 100644 --- a/medusa.json +++ b/medusa.json @@ -9,7 +9,7 @@ "corpusDirectory": "test/invariants/corpus", "coverageEnabled": true, "targetContracts": ["FuzzTest"], - "predeployedContracts": {}, + "predeployedContracts": { "ValidatorLib": "0xc0ffee" }, "targetContractsBalances": [], "constructorArgs": {}, "deployerAddress": "0x30000", @@ -76,7 +76,7 @@ "target": "test/invariants/FuzzTest.t.sol", "solcVersion": "", "exportDirectory": "", - "args": ["--compile-libraries=(ValidatorLib,0xf0)"] + "args": ["--compile-libraries=(ValidatorLib,0xc0ffee)"] } }, "logging": { diff --git a/test/invariants/FuzzTest.t.sol b/test/invariants/FuzzTest.t.sol index ce7a9b5..a2a24a9 100644 --- a/test/invariants/FuzzTest.t.sol +++ b/test/invariants/FuzzTest.t.sol @@ -4,7 +4,6 @@ pragma solidity 0.8.26; import {PropertyParent} from './properties/PropertyParent.t.sol'; contract FuzzTest is PropertyParent { - function test_debug() public { - this.property_sanityCheck(); - } + //solhint-disable no-empty-blocks + function test_debug() public {} } diff --git a/test/invariants/Setup.t.sol b/test/invariants/Setup.t.sol index 5d8a97d..f46eddf 100644 --- a/test/invariants/Setup.t.sol +++ b/test/invariants/Setup.t.sol @@ -71,7 +71,7 @@ contract Setup is Utils { IEpochManager internal epochManager; // Constants - uint256 internal constant START_EPOCH = 1000; + uint256 internal immutable START_EPOCH = block.timestamp; uint64 internal constant MIN_THAWING_PERIOD = 1 days; uint128 internal constant INITIAL_MAX_USERS_TO_CHECK = 10; uint32 internal constant MAX_VERIFIER_CUT = 1_000_000; @@ -88,10 +88,12 @@ contract Setup is Utils { uint256 internal constant TYING_BUFFER = 3 days; uint256 internal constant DISPUTE_DISPUTE_WINDOW = 2 weeks; + string[] internal INITIAL_CHAINS = ['mainnet', 'optimism', 'arbitrum']; + constructor() { // Deploy mock contracts GRT = IERC20(address(new FuzzERC20())); - epochManager = IEpochManager(address(new MockEpochManager(START_EPOCH))); + epochManager = IEpochManager(address(new MockEpochManager())); horizonStaking = IHorizonStaking(address(new MockHorizonStaking(GRT))); // Deploy core contracts @@ -157,12 +159,15 @@ contract Setup is Utils { horizonAccountingExtension.approveModule(address(bondEscalationModule)); // Set up initial chain IDs - eboRequestCreator.addChain('mainnet'); - eboRequestCreator.addChain('optimism'); - eboRequestCreator.addChain('arbitrum'); + for (uint256 i; i < INITIAL_CHAINS.length; i++) { + eboRequestCreator.addChain(INITIAL_CHAINS[i]); + } // Set up initial module parameters _setupModuleParameters(); + + // System health check + _sanityCheck(); } function _setupModuleParameters() internal { @@ -205,4 +210,97 @@ contract Setup is Utils { // Set finality module eboRequestCreator.setFinalityModuleData(address(eboFinalityModule)); } + + /// @custom:property-id 0 + /// @custom:property Check if eboRequestCreator has the correct properties, including the ones set after its initial deployment + function _sanityCheck() internal { + assertEq(address(eboRequestCreator.ORACLE()), address(oracle), 'prop-0: wrong oracle address'); + + assertEq(address(eboRequestCreator.ARBITRABLE()), address(arbitrable), 'prop-0: wrong arbitrable address'); + + assertEq(eboRequestCreator.START_EPOCH(), START_EPOCH, 'prop-0: wrong start epoch'); + + assertEq(address(eboRequestCreator.epochManager()), address(epochManager), 'prop-0: wrong epoch manager address'); + + IOracle.Request memory initialRequestStored = eboRequestCreator.getRequestData(); + + assertEq( + initialRequestStored.requestModule, address(eboRequestModule), 'prop-0: wrong request module in initialRequest' + ); + + assertEq( + initialRequestStored.responseModule, + address(bondedResponseModule), + 'prop-0: wrong response module in initialRequest' + ); + + assertEq( + initialRequestStored.disputeModule, address(bondEscalationModule), 'prop-0: wrong disputeModule in initialRequest' + ); + + assertEq( + initialRequestStored.resolutionModule, + address(arbitratorModule), + 'prop-0: wrong resolutionModule in initialRequest' + ); + + assertEq( + initialRequestStored.finalityModule, address(eboFinalityModule), 'prop-0: wrong finalityModule in initialRequest' + ); + + assertEq(initialRequestStored.requester, address(eboRequestCreator), 'prop-0: wrong requester in initialRequest'); + + assertEq( + initialRequestStored.requestModuleData, + abi.encode( + IEBORequestModule.RequestParameters({ + epoch: START_EPOCH, + chainId: 'mainnet', + accountingExtension: IAccountingExtension(address(horizonAccountingExtension)), + paymentAmount: PAYMENT_AMOUNT + }) + ), + 'prop-0: wrong requestModuleData in initialRequest' + ); + + assertEq( + initialRequestStored.responseModuleData, + abi.encode( + IBondedResponseModule.RequestParameters({ + accountingExtension: IAccountingExtension(address(horizonAccountingExtension)), + bondToken: GRT, + bondSize: RESPONSE_BOND_SIZE, + deadline: RESPONSE_DEADLINE, + disputeWindow: RESPONSE_DISPUTE_WINDOW + }) + ), + 'prop-0: wrong responseModuleData module data in initialRequest' + ); + + assertEq( + initialRequestStored.disputeModuleData, + abi.encode( + IBondEscalationModule.RequestParameters({ + accountingExtension: IBondEscalationAccounting(address(horizonAccountingExtension)), + bondToken: GRT, + bondSize: DISPUTE_BOND_SIZE, + maxNumberOfEscalations: MAX_NB_ESCALATION, + bondEscalationDeadline: DISPUTE_DEADLINE, + tyingBuffer: TYING_BUFFER, + disputeWindow: DISPUTE_DISPUTE_WINDOW + }) + ), + 'prop-0: wrong disputeModuleData in initialRequest' + ); + + assertEq( + initialRequestStored.resolutionModuleData, + abi.encode(IArbitratorModule.RequestParameters({arbitrator: address(councilArbitrator)})), + 'prop-0: wrong resolutionModuleData in initialRequest' + ); + + assertEq(initialRequestStored.finalityModuleData, bytes(''), 'prop-0: wrong finality module data in initialRequest'); + + assertEq(initialRequestStored.nonce, 0, 'prop-0: wrong nonce in initialRequest'); + } } diff --git a/test/invariants/handlers/BaseHandler.t.sol b/test/invariants/handlers/BaseHandler.t.sol index da1edae..a2ac9ef 100644 --- a/test/invariants/handlers/BaseHandler.t.sol +++ b/test/invariants/handlers/BaseHandler.t.sol @@ -8,6 +8,11 @@ import {Actors} from '../helpers/Actors.t.sol'; contract BaseHandler is Setup, Actors { // Track all created request IDs bytes32[] internal _ghost_requests; + // Track all created request IDs per epoch and chainId + mapping(uint256 _epoch => mapping(string _chainId => bytes32[] _requestIds)) internal _ghost_requestsPerEpochChainId; + + // Track the chainId->chain (reverse the hash) + mapping(bytes32 _hash => string _chain) internal _ghost_chainIdToChain; // Track request details mapping(bytes32 _requestId => IOracle.Request _data) internal _ghost_requestData; @@ -20,53 +25,64 @@ contract BaseHandler is Setup, Actors { // Track disputes mapping(bytes32 _requestId => bytes32[] _disputeIds) internal _ghost_disputes; // requestId => disputeIds mapping(bytes32 _disputeId => IOracle.Dispute _data) internal _ghost_disputeData; + mapping(bytes32 _disputeId => bool _escalated) internal _ghost_escalatedDisputes; // Track which requests came from EBORequestCreator mapping(bytes32 _requestId => bool _isFromRequestCreator) internal _ghost_validRequests; - // Track chain IDs used per epoch to prevent duplicates - mapping(uint256 _epoch => mapping(string _chainId => bool _isRequested)) internal _ghost_epochChainIds; - // Track bonds and pledges mapping(address _owner => mapping(bytes32 _requestId => uint256 _boundedAmount)) internal _ghost_bonds; mapping(address _pledger => mapping(bytes32 _disputeId => uint256 _pledgedAmount)) internal _ghost_pledgesFor; mapping(address _pledger => mapping(bytes32 _disputeId => uint256 _pledgedAmount)) internal _ghost_pledgesAgainst; // Helper functions - function _boundEpoch(uint256 _epoch) internal view returns (uint256) { - return bound(_epoch, START_EPOCH, START_EPOCH + 1000); - } - - function _boundAmount(uint256 _amount) internal pure returns (uint256) { - return bound(_amount, 1, 1_000_000e18); - } + function _getRandomChain(uint256 _seed) internal view returns (string memory) { + bytes32[] memory chainIds = eboRequestCreator.getAllowedChainIds(); + if (chainIds.length == 0) return ''; - function _boundBlockNumber(uint256 _blockNumber) internal view returns (uint256) { - return bound(_blockNumber, block.number - 1000, block.number); + return _ghost_chainIdToChain[chainIds[_seed % chainIds.length]]; } - function _generateChainId(uint256 _seed) internal pure returns (string memory) { - string[3] memory chains = ['mainnet', 'optimism', 'arbitrum']; - return chains[_seed % 3]; - } - - function _getRandomRequest(uint256 _seed) internal view returns (bytes32) { - if (_ghost_requests.length == 0) return bytes32(0); - return _ghost_requests[_seed % _ghost_requests.length]; + function _getRandomRequest(uint256 _seed) internal view returns (bytes32, IOracle.Request memory) { + if (_ghost_requests.length == 0) { + return ( + bytes32(0), + IOracle.Request( + 0, + address(0), + address(0), + address(0), + address(0), + address(0), + address(0), + bytes(''), + bytes(''), + bytes(''), + bytes(''), + bytes('') + ) + ); + } + bytes32 requestId = _ghost_requests[_seed % _ghost_requests.length]; + return (requestId, _ghost_requestData[requestId]); } - function _getRandomActiveResponse(bytes32 _requestId, uint256 _seed) internal view returns (IOracle.Response memory) { + function _getRandomActiveResponse( + bytes32 _requestId, + uint256 _seed + ) internal view returns (bytes32, IOracle.Response memory) { bytes32 responseId = _ghost_activeResponses[_requestId][_seed % _ghost_activeResponses[_requestId].length]; - return _ghost_responseData[responseId]; + + return (responseId, _ghost_responseData[responseId]); } - function _getRandomDispute(bytes32 _requestId, uint256 _seed) internal view returns (IOracle.Dispute memory) { + function _getRandomDispute(bytes32 _requestId, uint256 _seed) internal view returns (bytes32, IOracle.Dispute memory) { bytes32[] storage disputes = _ghost_disputes[_requestId]; if (disputes.length == 0) { - return IOracle.Dispute(address(0), address(0), bytes32(0), 0); + return (bytes32(0), IOracle.Dispute(address(0), address(0), bytes32(0), 0)); } bytes32 disputeId = disputes[_seed % disputes.length]; - return _ghost_disputeData[disputeId]; + return (disputeId, _ghost_disputeData[disputeId]); } // Events to track state changes diff --git a/test/invariants/handlers/HandlerBondEscalationModule.t.sol b/test/invariants/handlers/HandlerBondEscalationModule.t.sol index 3861603..00dbc0c 100644 --- a/test/invariants/handlers/HandlerBondEscalationModule.t.sol +++ b/test/invariants/handlers/HandlerBondEscalationModule.t.sol @@ -5,51 +5,47 @@ import {BaseHandler, IOracle} from './BaseHandler.t.sol'; contract HandlerBondEscalationModule is BaseHandler { function handlePledgeForDispute(uint256 _requestSeed, uint256 _disputeIndex, uint256 _actorSeed) external { - bytes32 requestId = _getRandomRequest(_requestSeed); + (bytes32 requestId, IOracle.Request memory request) = _getRandomRequest(_requestSeed); if (requestId == bytes32(0)) return; - IOracle.Dispute memory dispute = _getRandomDispute(requestId, _disputeIndex); + (bytes32 disputeId, IOracle.Dispute memory dispute) = _getRandomDispute(requestId, _disputeIndex); if (dispute.requestId == bytes32(0)) return; if (oracle.disputeStatus(keccak256(abi.encode(dispute))) != IOracle.DisputeStatus.Escalated) return; address pledger = _pickActor(_actorSeed); - bondEscalationModule.pledgeForDispute(_ghost_requestData[requestId], dispute); + bondEscalationModule.pledgeForDispute(request, dispute); - bytes32 disputeId = keccak256(abi.encode(dispute)); _ghost_pledgesFor[pledger][disputeId] += DISPUTE_BOND_SIZE; _ghost_bonds[pledger][requestId] += DISPUTE_BOND_SIZE; // Track bond amount } function handlePledgeAgainstDispute(uint256 _requestSeed, uint256 _disputeIndex, uint256 _actorSeed) external { - bytes32 requestId = _getRandomRequest(_requestSeed); + (bytes32 requestId, IOracle.Request memory request) = _getRandomRequest(_requestSeed); if (requestId == bytes32(0)) return; - IOracle.Dispute memory dispute = _getRandomDispute(requestId, _disputeIndex); + (bytes32 disputeId, IOracle.Dispute memory dispute) = _getRandomDispute(requestId, _disputeIndex); if (dispute.requestId == bytes32(0)) return; if (oracle.disputeStatus(keccak256(abi.encode(dispute))) != IOracle.DisputeStatus.Escalated) return; address pledger = _pickActor(_actorSeed); - bondEscalationModule.pledgeAgainstDispute(_ghost_requestData[requestId], dispute); + bondEscalationModule.pledgeAgainstDispute(request, dispute); - bytes32 disputeId = keccak256(abi.encode(dispute)); _ghost_pledgesAgainst[pledger][disputeId] += DISPUTE_BOND_SIZE; _ghost_bonds[pledger][requestId] += DISPUTE_BOND_SIZE; // Track bond amount } function handleSettleBondEscalation(uint256 _requestSeed, uint256 _disputeIndex) external { - bytes32 requestId = _getRandomRequest(_requestSeed); + (bytes32 requestId, IOracle.Request memory request) = _getRandomRequest(_requestSeed); if (requestId == bytes32(0)) return; - IOracle.Dispute memory dispute = _getRandomDispute(requestId, _disputeIndex); + (bytes32 disputeId, IOracle.Dispute memory dispute) = _getRandomDispute(requestId, _disputeIndex); if (dispute.requestId == bytes32(0)) return; IOracle.Response memory response = _ghost_responseData[dispute.responseId]; - bondEscalationModule.settleBondEscalation(_ghost_requestData[requestId], response, dispute); + bondEscalationModule.settleBondEscalation(request, response, dispute); - // Update ghost variables - bytes32 disputeId = keccak256(abi.encode(dispute)); // Clear dispute pledges as they're now settled delete _ghost_pledgesFor[dispute.disputer][disputeId]; delete _ghost_pledgesAgainst[dispute.disputer][disputeId]; diff --git a/test/invariants/handlers/HandlerBondedResponseModule.t.sol b/test/invariants/handlers/HandlerBondedResponseModule.t.sol index 7a1822b..f73719a 100644 --- a/test/invariants/handlers/HandlerBondedResponseModule.t.sol +++ b/test/invariants/handlers/HandlerBondedResponseModule.t.sol @@ -5,16 +5,15 @@ import {BaseHandler, IOracle} from './BaseHandler.t.sol'; contract HandlerBondedResponseModule is BaseHandler { function handleReleaseUnutilizedResponse(uint256 _requestSeed) external { - bytes32 requestId = _getRandomRequest(_requestSeed); + (bytes32 requestId, IOracle.Request memory request) = _getRandomRequest(_requestSeed); if (requestId == bytes32(0)) return; - IOracle.Response memory response = _getRandomActiveResponse(requestId, _requestSeed); + (bytes32 responseId, IOracle.Response memory response) = _getRandomActiveResponse(requestId, _requestSeed); if (response.requestId == bytes32(0)) return; - bondedResponseModule.releaseUnutilizedResponse(_ghost_requestData[requestId], response); + bondedResponseModule.releaseUnutilizedResponse(request, response); // Update ghost variables - clear response state - bytes32 responseId = keccak256(abi.encode(response)); delete _ghost_activeResponses[requestId]; delete _ghost_responseData[responseId]; delete _ghost_bonds[response.proposer][requestId]; diff --git a/test/invariants/handlers/HandlerCouncilArbitrator.t.sol b/test/invariants/handlers/HandlerCouncilArbitrator.t.sol index ed08aa2..92a4415 100644 --- a/test/invariants/handlers/HandlerCouncilArbitrator.t.sol +++ b/test/invariants/handlers/HandlerCouncilArbitrator.t.sol @@ -5,14 +5,12 @@ import {BaseHandler, IOracle} from './BaseHandler.t.sol'; contract HandlerCouncilArbitrator is BaseHandler { function handleArbitrateDispute(uint256 _requestSeed, uint256 _disputeIndex, uint256 _statusSeed) external { - bytes32 requestId = _getRandomRequest(_requestSeed); + (bytes32 requestId,) = _getRandomRequest(_requestSeed); if (requestId == bytes32(0)) return; - IOracle.Dispute memory dispute = _getRandomDispute(requestId, _disputeIndex); + (bytes32 disputeId, IOracle.Dispute memory dispute) = _getRandomDispute(requestId, _disputeIndex); if (dispute.requestId == bytes32(0)) return; - bytes32 disputeId = keccak256(abi.encode(dispute)); - // Generate a valid dispute status (NoResolution, Won, Lost) IOracle.DisputeStatus status = IOracle.DisputeStatus( bound(_statusSeed, uint8(IOracle.DisputeStatus.NoResolution), uint8(IOracle.DisputeStatus.Lost)) diff --git a/test/invariants/handlers/HandlerEBOFinalityModule.t.sol b/test/invariants/handlers/HandlerEBOFinalityModule.t.sol index 35eb801..27daab5 100644 --- a/test/invariants/handlers/HandlerEBOFinalityModule.t.sol +++ b/test/invariants/handlers/HandlerEBOFinalityModule.t.sol @@ -6,18 +6,18 @@ import {BaseHandler, IEBORequestCreator, IOracle} from './BaseHandler.t.sol'; contract HandlerEBOFinalityModule is BaseHandler { function handleAmendEpoch( uint256 _epoch, + uint256 _blockNumber, uint256[] calldata _chainIdSeeds, uint256[] calldata _blockNumbers ) external { if (_chainIdSeeds.length != _blockNumbers.length) return; - _epoch = _boundEpoch(_epoch); string[] memory chainIds = new string[](_chainIdSeeds.length); uint256[] memory blockNums = new uint256[](_blockNumbers.length); for (uint256 i = 0; i < _chainIdSeeds.length; i++) { - chainIds[i] = _generateChainId(_chainIdSeeds[i]); - blockNums[i] = _boundBlockNumber(_blockNumbers[i]); + chainIds[i] = _getRandomChain(_chainIdSeeds[i]); + blockNums[i] = _blockNumber; } eboFinalityModule.amendEpoch(_epoch, chainIds, blockNums); diff --git a/test/invariants/handlers/HandlerEBORequestCreator.t.sol b/test/invariants/handlers/HandlerEBORequestCreator.t.sol index 2104da1..c2c9a53 100644 --- a/test/invariants/handlers/HandlerEBORequestCreator.t.sol +++ b/test/invariants/handlers/HandlerEBORequestCreator.t.sol @@ -2,22 +2,36 @@ pragma solidity 0.8.26; import { + ArbitratorModule, BaseHandler, + BondEscalationModule, + BondedResponseModule, + EBOFinalityModule, + EBORequestModule, + IAccountingExtension, IArbitratorModule, + IBondEscalationAccounting, IBondEscalationModule, IBondedResponseModule, IEBORequestModule, IEpochManager, - IOracle + IOracle, + MockEpochManager } from './BaseHandler.t.sol'; contract HandlerEBORequestCreator is BaseHandler { + constructor() { + // Initialize the ghost mapping, allowing to reverse lookup the chain + for (uint256 i; i < INITIAL_CHAINS.length; i++) { + _ghost_chainIdToChain[keccak256(abi.encodePacked(INITIAL_CHAINS[i]))] = INITIAL_CHAINS[i]; + } + } + function handleCreateRequest(uint256 _epoch, uint256 _chainIdSeed) external { - _epoch = _boundEpoch(_epoch); - string memory chainId = _generateChainId(_chainIdSeed); + _epoch = bound(_epoch, START_EPOCH, block.timestamp); - // Prevent duplicate chainId for same epoch - if (_ghost_epochChainIds[_epoch][chainId]) return; + string memory chainId = _getRandomChain(_chainIdSeed); + if (bytes(chainId).length == 0) return; // Create request via EBORequestCreator eboRequestCreator.createRequest(_epoch, chainId); @@ -25,67 +39,34 @@ contract HandlerEBORequestCreator is BaseHandler { // Get current request data IOracle.Request memory requestData = eboRequestCreator.getRequestData(); + // Build request module parameters + IEBORequestModule.RequestParameters memory requestParams = + abi.decode(requestData.requestModuleData, (IEBORequestModule.RequestParameters)); + requestParams.epoch = _epoch; + requestParams.chainId = chainId; + requestData.requestModuleData = abi.encode(requestParams); + // Calculate request ID using same logic as Oracle bytes32 requestId = keccak256(abi.encode(requestData)); // Track the request _ghost_requests.push(requestId); + _ghost_requestsPerEpochChainId[_epoch][chainId].push(requestId); _ghost_requestData[requestId] = requestData; _ghost_validRequests[requestId] = true; - _ghost_epochChainIds[_epoch][chainId] = true; emit RequestCreated(requestId, _epoch, chainId); } - function handleAddChain(uint256 _chainIdSeed) external { - string memory chainId = _generateChainId(_chainIdSeed); - eboRequestCreator.addChain(chainId); - } - - function handleRemoveChain(uint256 _chainIdSeed) external { - string memory chainId = _generateChainId(_chainIdSeed); - eboRequestCreator.removeChain(chainId); - } - - function handleSetRequestModuleData( - uint256 _actorSeed, - IEBORequestModule.RequestParameters calldata _params - ) external { - address module = _pickActor(_actorSeed); - eboRequestCreator.setRequestModuleData(module, _params); - } + function handleAddChain(string memory _chainId) external { + eboRequestCreator.addChain(_chainId); - function handleSetResponseModuleData( - uint256 _actorSeed, - IBondedResponseModule.RequestParameters calldata _params - ) external { - address module = _pickActor(_actorSeed); - eboRequestCreator.setResponseModuleData(module, _params); + // Track the chain + _ghost_chainIdToChain[keccak256(abi.encodePacked(_chainId))] = _chainId; } - function handleSetDisputeModuleData( - uint256 _actorSeed, - IBondEscalationModule.RequestParameters calldata _params - ) external { - address module = _pickActor(_actorSeed); - eboRequestCreator.setDisputeModuleData(module, _params); - } - - function handleSetResolutionModuleData( - uint256 _actorSeed, - IArbitratorModule.RequestParameters calldata _params - ) external { - address module = _pickActor(_actorSeed); - eboRequestCreator.setResolutionModuleData(module, _params); - } - - function handleSetFinalityModuleData(uint256 _actorSeed) external { - address module = _pickActor(_actorSeed); - eboRequestCreator.setFinalityModuleData(module); - } - - function handleSetEpochManager(uint256 _actorSeed) external { - address manager = _pickActor(_actorSeed); - eboRequestCreator.setEpochManager(IEpochManager(manager)); - } + // function handleRemoveChain(uint256 _chainIdSeed) external { + // string memory chainId = _getRandomChainId(_chainIdSeed); + // eboRequestCreator.removeChain(chainId); + // } } diff --git a/test/invariants/handlers/HandlerHorizonAccountingExtension.t.sol b/test/invariants/handlers/HandlerHorizonAccountingExtension.t.sol index 5d1abde..b5abc2e 100644 --- a/test/invariants/handlers/HandlerHorizonAccountingExtension.t.sol +++ b/test/invariants/handlers/HandlerHorizonAccountingExtension.t.sol @@ -15,15 +15,15 @@ contract HandlerHorizonAccountingExtension is BaseHandler { } function handlePledge(uint256 _pledgerSeed, uint256 _requestSeed, uint256 _disputeIndex, uint256 _amount) external { - address pledger = _pickActor(_pledgerSeed); - bytes32 requestId = _getRandomRequest(_requestSeed); + (bytes32 requestId, IOracle.Request memory request) = _getRandomRequest(_requestSeed); if (requestId == bytes32(0)) return; - IOracle.Dispute memory dispute = _getRandomDispute(requestId, _disputeIndex); + (, IOracle.Dispute memory dispute) = _getRandomDispute(requestId, _disputeIndex); if (dispute.requestId == bytes32(0)) return; - _amount = _boundAmount(_amount); - horizonAccountingExtension.pledge(pledger, _ghost_requestData[requestId], dispute, GRT, _amount); + address pledger = _pickActor(_pledgerSeed); + + horizonAccountingExtension.pledge(pledger, request, dispute, GRT, _amount); } function handleOnSettleBondEscalation( @@ -32,27 +32,23 @@ contract HandlerHorizonAccountingExtension is BaseHandler { uint256 _amountPerPledger, uint256 _winningPledgersLength ) external { - bytes32 requestId = _getRandomRequest(_requestSeed); + (bytes32 requestId, IOracle.Request memory request) = _getRandomRequest(_requestSeed); if (requestId == bytes32(0)) return; - IOracle.Dispute memory dispute = _getRandomDispute(requestId, _disputeIndex); + (, IOracle.Dispute memory dispute) = _getRandomDispute(requestId, _disputeIndex); if (dispute.requestId == bytes32(0)) return; - _amountPerPledger = _boundAmount(_amountPerPledger); - horizonAccountingExtension.onSettleBondEscalation( - _ghost_requestData[requestId], dispute, GRT, _amountPerPledger, _winningPledgersLength - ); + horizonAccountingExtension.onSettleBondEscalation(request, dispute, GRT, _amountPerPledger, _winningPledgersLength); } function handleClaimEscalationReward(uint256 _requestSeed, uint256 _disputeIndex, uint256 _actorSeed) external { - bytes32 requestId = _getRandomRequest(_requestSeed); + (bytes32 requestId,) = _getRandomRequest(_requestSeed); if (requestId == bytes32(0)) return; - IOracle.Dispute memory dispute = _getRandomDispute(requestId, _disputeIndex); + (bytes32 disputeId, IOracle.Dispute memory dispute) = _getRandomDispute(requestId, _disputeIndex); if (dispute.requestId == bytes32(0)) return; address pledger = _pickActor(_actorSeed); - bytes32 disputeId = keccak256(abi.encode(dispute)); horizonAccountingExtension.claimEscalationReward(disputeId, pledger); @@ -68,13 +64,12 @@ contract HandlerHorizonAccountingExtension is BaseHandler { uint256 _usersToSlash, uint256 _maxUsersToCheck ) external { - bytes32 requestId = _getRandomRequest(_requestSeed); + (bytes32 requestId,) = _getRandomRequest(_requestSeed); if (requestId == bytes32(0)) return; - IOracle.Dispute memory dispute = _getRandomDispute(requestId, _disputeIndex); + (bytes32 disputeId, IOracle.Dispute memory dispute) = _getRandomDispute(requestId, _disputeIndex); if (dispute.requestId == bytes32(0)) return; - bytes32 disputeId = keccak256(abi.encode(dispute)); _usersToSlash = bound(_usersToSlash, 1, 10); _maxUsersToCheck = bound(_maxUsersToCheck, _usersToSlash, 20); @@ -88,4 +83,25 @@ contract HandlerHorizonAccountingExtension is BaseHandler { _maxUsers = bound(_maxUsers, 1, 100); horizonAccountingExtension.setMaxUsersToCheck(uint128(_maxUsers)); } + + function _stakeGRT(uint256 _amount) internal { + GRT.transfer(msg.sender, _amount); + + vm.prank(msg.sender); + GRT.approve(address(horizonStaking), _amount); + + vm.prank(msg.sender); + horizonStaking.stake(_amount); + } + + function _provisionGRT(uint256 _amount) internal { + vm.prank(msg.sender); + horizonStaking.provision( + msg.sender, + address(horizonAccountingExtension), + _amount, + horizonAccountingExtension.MAX_VERIFIER_CUT(), + horizonAccountingExtension.MIN_THAWING_PERIOD() + ); + } } diff --git a/test/invariants/handlers/HandlerOracle.t.sol b/test/invariants/handlers/HandlerOracle.t.sol index 0714b44..9bf8c05 100644 --- a/test/invariants/handlers/HandlerOracle.t.sol +++ b/test/invariants/handlers/HandlerOracle.t.sol @@ -5,10 +5,10 @@ import {BaseHandler, IOracle} from './BaseHandler.t.sol'; contract HandlerOracle is BaseHandler { function handleCreateRequest(uint256 _requestSeed, bytes32 _previousId) external returns (bytes32) { - bytes32 requestId = _getRandomRequest(_requestSeed); + (bytes32 requestId, IOracle.Request memory request) = _getRandomRequest(_requestSeed); if (requestId == bytes32(0)) return bytes32(0); - return oracle.createRequest(_ghost_requestData[requestId], _previousId); + return oracle.createRequest(request, _previousId); } function handleProposeResponse( @@ -16,15 +16,13 @@ contract HandlerOracle is BaseHandler { uint256 _blockNumber, uint256 _actorSeed ) external returns (bytes32) { - bytes32 requestId = _getRandomRequest(_requestSeed); + (bytes32 requestId, IOracle.Request memory request) = _getRandomRequest(_requestSeed); if (requestId == bytes32(0) || !_ghost_validRequests[requestId]) { return bytes32(0); } address proposer = _pickActor(_actorSeed); - _blockNumber = _boundBlockNumber(_blockNumber); - IOracle.Request memory request = _ghost_requestData[requestId]; IOracle.Response memory response = IOracle.Response({requestId: requestId, response: abi.encode(_blockNumber), proposer: proposer}); @@ -40,7 +38,7 @@ contract HandlerOracle is BaseHandler { } function handleDisputeResponseOracle(uint256 _requestSeed, uint256 _actorSeed) external returns (bytes32) { - bytes32 requestId = _getRandomRequest(_requestSeed); + (bytes32 requestId, IOracle.Request memory request) = _getRandomRequest(_requestSeed); if (requestId == bytes32(0) || !_ghost_validRequests[requestId]) { return bytes32(0); } @@ -52,12 +50,12 @@ contract HandlerOracle is BaseHandler { if (_ghost_finalizedResponses[responseId]) return bytes32(0); address disputer = _pickActor(_actorSeed); - uint256 bond = _boundAmount(1e18); + uint256 bond = DISPUTE_BOND_SIZE; IOracle.Dispute memory dispute = IOracle.Dispute({disputer: disputer, proposer: response.proposer, requestId: requestId, responseId: responseId}); - bytes32 disputeId = oracle.disputeResponse(_ghost_requestData[requestId], response, dispute); + bytes32 disputeId = oracle.disputeResponse(request, response, dispute); // Track dispute _ghost_disputes[requestId].push(disputeId); @@ -70,38 +68,38 @@ contract HandlerOracle is BaseHandler { } function handleEscalateDispute(uint256 _requestSeed, uint256 _disputeIndex) external { - bytes32 requestId = _getRandomRequest(_requestSeed); + (bytes32 requestId, IOracle.Request memory request) = _getRandomRequest(_requestSeed); if (requestId == bytes32(0)) return; - IOracle.Dispute memory dispute = _getRandomDispute(requestId, _disputeIndex); + (, IOracle.Dispute memory dispute) = _getRandomDispute(requestId, _disputeIndex); if (dispute.requestId == bytes32(0)) return; if (oracle.disputeStatus(keccak256(abi.encode(dispute))) != IOracle.DisputeStatus.Escalated) return; IOracle.Response memory response = _ghost_responseData[dispute.responseId]; - oracle.escalateDispute(_ghost_requestData[requestId], response, dispute); + oracle.escalateDispute(request, response, dispute); } function handleResolveDispute(uint256 _requestSeed, uint256 _disputeIndex) external { - bytes32 requestId = _getRandomRequest(_requestSeed); + (bytes32 requestId, IOracle.Request memory request) = _getRandomRequest(_requestSeed); if (requestId == bytes32(0)) return; - IOracle.Dispute memory dispute = _getRandomDispute(requestId, _disputeIndex); + (, IOracle.Dispute memory dispute) = _getRandomDispute(requestId, _disputeIndex); if (dispute.requestId == bytes32(0)) return; IOracle.Response memory response = _ghost_responseData[dispute.responseId]; - oracle.resolveDispute(_ghost_requestData[requestId], response, dispute); + oracle.resolveDispute(request, response, dispute); } function handleFinalize(uint256 _requestSeed) external { - bytes32 requestId = _getRandomRequest(_requestSeed); + (bytes32 requestId, IOracle.Request memory request) = _getRandomRequest(_requestSeed); if (requestId == bytes32(0)) return; if (_ghost_activeResponses[requestId].length == 0) return; bytes32 responseId = _ghost_activeResponses[requestId][0]; IOracle.Response memory response = _ghost_responseData[responseId]; - oracle.finalize(_ghost_requestData[requestId], response); + oracle.finalize(request, response); // Update ghost variables _ghost_finalizedResponses[responseId] = true; diff --git a/test/invariants/handlers/HandlerParent.t.sol b/test/invariants/handlers/HandlerParent.t.sol index 33b3297..aecc706 100644 --- a/test/invariants/handlers/HandlerParent.t.sol +++ b/test/invariants/handlers/HandlerParent.t.sol @@ -2,7 +2,6 @@ pragma solidity 0.8.26; import {HandlerArbitrable} from './HandlerArbitrable.t.sol'; - import {HandlerBondEscalationModule} from './HandlerBondEscalationModule.t.sol'; import {HandlerBondedResponseModule} from './HandlerBondedResponseModule.t.sol'; import {HandlerCouncilArbitrator} from './HandlerCouncilArbitrator.t.sol'; diff --git a/test/invariants/helpers/FuzzERC20.sol b/test/invariants/helpers/FuzzERC20.sol index ed42f9d..cdbd9f4 100644 --- a/test/invariants/helpers/FuzzERC20.sol +++ b/test/invariants/helpers/FuzzERC20.sol @@ -13,7 +13,7 @@ contract FuzzERC20 is ERC20 { function transfer(address to, uint256 amount) public override returns (bool) { address owner = _msgSender(); - // FuzzTest has an infinite mint + // FuzzTest contract has an infinite mint if (owner == testContract) { _mint(testContract, amount); } diff --git a/test/invariants/helpers/MockEpochManager.t.sol b/test/invariants/helpers/MockEpochManager.t.sol index ffd54ea..5e311d0 100644 --- a/test/invariants/helpers/MockEpochManager.t.sol +++ b/test/invariants/helpers/MockEpochManager.t.sol @@ -2,13 +2,7 @@ pragma solidity 0.8.26; contract MockEpochManager { - uint256 public currentEpoch; - - constructor(uint256 _startEpoch) { - currentEpoch = _startEpoch; - } - - function setEpoch(uint256 _epoch) external { - currentEpoch = _epoch; + function currentEpoch() external view returns (uint256) { + return block.timestamp; } } diff --git a/test/invariants/helpers/MockHorizonStaking.t.sol b/test/invariants/helpers/MockHorizonStaking.t.sol index 3ab32a3..0092f51 100644 --- a/test/invariants/helpers/MockHorizonStaking.t.sol +++ b/test/invariants/helpers/MockHorizonStaking.t.sol @@ -7,6 +7,7 @@ import {IHorizonStaking} from 'interfaces/external/IHorizonStaking.sol'; contract MockHorizonStaking { IERC20 public immutable GRT; mapping(address => mapping(address => IHorizonStaking.Provision)) public provisions; + mapping(address => uint256) public stakes; constructor(IERC20 _grt) { GRT = _grt; @@ -36,9 +37,21 @@ contract MockHorizonStaking { maxVerifierCutPending: 0, thawingPeriodPending: 0 }); + + stakes[_serviceProvider] -= _tokens; + } + + function thaw(address _serviceProvider, address _verifier, uint256 tokens) external { + provisions[_serviceProvider][_verifier].tokensThawing += tokens; + } + + function stake(uint256 _amount) external { + stakes[msg.sender] += _amount; + GRT.transferFrom(msg.sender, address(this), _amount); } - function slash(address, uint256, uint256 _tokensVerifier, address _verifierDestination) external { - GRT.transfer(_verifierDestination, _tokensVerifier); + function slash(address _serviceProvider, uint256 _tokens, uint256 _tokensVerifier, address _verifier) external { + provisions[_serviceProvider][_verifier].tokensThawing -= _tokens; + GRT.transfer(_verifier, _tokensVerifier); } } diff --git a/test/invariants/helpers/Utils.t.sol b/test/invariants/helpers/Utils.t.sol index b74fbd4..9470618 100644 --- a/test/invariants/helpers/Utils.t.sol +++ b/test/invariants/helpers/Utils.t.sol @@ -21,10 +21,9 @@ contract Utils { return genAddress; } - // same as forge-std + // same as forge-std, except for min/max validation (we swap) function bound(uint256 x, uint256 min, uint256 max) internal pure returns (uint256 result) { - //solhint-disable custom-errors - require(min <= max, 'StdUtils bound(uint256,uint256,uint256): Max is less than min.'); + if (min > max) (min, max) = (max, min); uint256 UINT256_MAX = 2 ** 256 - 1; @@ -68,6 +67,17 @@ contract Utils { } } + function assertNotEq(uint256 a, uint256 b) internal { + assertNotEq(a, b, 'assertNotEq: a == b'); + } + + function assertNotEq(uint256 a, uint256 b, string memory reason) internal { + if (a == b) { + emit TestFailure(reason); + assert(false); + } + } + function assertEq(address a, address b) internal { assertEq(a, b, 'assertEq: a != b'); } @@ -79,6 +89,39 @@ contract Utils { } } + function assertNotEq(address a, address b) internal { + assertNotEq(a, b, 'assertNotEq: a == b'); + } + + function assertNotEq(address a, address b, string memory reason) internal { + if (a == b) { + emit TestFailure(reason); + assert(false); + } + } + + function assertEq(bytes32 a, bytes32 b) internal { + assertEq(a, b, 'assertEq: a != b'); + } + + function assertEq(bytes32 a, bytes32 b, string memory reason) internal { + if (a != b) { + emit TestFailure(reason); + assert(false); + } + } + + function assertNotEq(bytes32 a, bytes32 b) internal { + assertNotEq(a, b, 'assertNotEq: a == b'); + } + + function assertNotEq(bytes32 a, bytes32 b, string memory reason) internal { + if (a == b) { + emit TestFailure(reason); + assert(false); + } + } + function assertEq(bytes memory a, bytes memory b) internal { assertEq(a, b, 'assertEq: a != b'); } @@ -96,6 +139,66 @@ contract Utils { } } + function assertNotEq(bytes memory a, bytes memory b) internal { + assertNotEq(a, b, 'assertNotEq: a == b'); + } + + function assertNotEq(bytes memory a, bytes memory b, string memory reason) internal { + if (a.length == b.length) { + for (uint256 i = 0; i < a.length; i++) { + if (a[i] != b[i]) { + return; + } + } + emit TestFailure(reason); + assert(false); + } + } + + function assertLt(uint256 a, uint256 b) internal { + assertLt(a, b, 'assertLt: a >= b'); + } + + function assertLt(uint256 a, uint256 b, string memory reason) internal { + if (a >= b) { + emit TestFailure(reason); + assert(false); + } + } + + function assertLte(uint256 a, uint256 b) internal { + assertLte(a, b, 'assertLte: a > b'); + } + + function assertLte(uint256 a, uint256 b, string memory reason) internal { + if (a > b) { + emit TestFailure(reason); + assert(false); + } + } + + function assertGt(uint256 a, uint256 b) internal { + assertGt(a, b, 'assertGt: a <= b'); + } + + function assertGt(uint256 a, uint256 b, string memory reason) internal { + if (a <= b) { + emit TestFailure(reason); + assert(false); + } + } + + function assertGte(uint256 a, uint256 b) internal { + assertGte(a, b, 'assertGte: a < b'); + } + + function assertGte(uint256 a, uint256 b, string memory reason) internal { + if (a < b) { + emit TestFailure(reason); + assert(false); + } + } + function assertTrue(bool a) internal { assertTrue(a, 'assertTrue: !a'); } @@ -106,4 +209,15 @@ contract Utils { assert(false); } } + + function assertFalse(bool a) internal { + assertFalse(a, 'assertFalse: a'); + } + + function assertFalse(bool a, string memory reason) internal { + if (a) { + emit TestFailure(reason); + assert(false); + } + } } diff --git a/test/invariants/properties/PropertyDispute.t.sol b/test/invariants/properties/PropertyDispute.t.sol new file mode 100644 index 0000000..ac698e2 --- /dev/null +++ b/test/invariants/properties/PropertyDispute.t.sol @@ -0,0 +1,138 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import {IEBORequestModule, IOracle} from '../Setup.t.sol'; +import {HandlerParent} from '../handlers/HandlerParent.t.sol'; + +contract PropertyDispute is HandlerParent { + /// @custom:property-id 6 + /// @custom:property Disputer can always dispute a response before the finalisation, if no previous dispute has been made. + /// @custom:property-id 13 + /// @custom:property Each disputeId and responseId can only be tied to one requestId + function property_disputerCanAlwaysCreateDispute(uint256 _requestIdSeed, uint256 _responseIdSeed) public { + // Pick random Response + (bytes32 _requestId, IOracle.Request memory _requestData) = _getRandomRequest(_requestIdSeed); + (bytes32 _responseId, IOracle.Response memory _responseData) = _getRandomActiveResponse(_requestId, _responseIdSeed); + + IOracle.Dispute memory _disputeData = IOracle.Dispute({ + disputer: msg.sender, + proposer: _responseData.proposer, + responseId: _responseId, + requestId: _requestId + }); + + // Stake some GRT in Horizon + _stakeGRT(DISPUTE_BOND_SIZE); + _provisionGRT(DISPUTE_BOND_SIZE); + + vm.prank(msg.sender); + try oracle.disputeResponse(_requestData, _responseData, _disputeData) returns (bytes32 _disputeId) { + // check if no previous dispute + assertEq(_ghost_disputes[_requestId].length, 0, 'property 6: new dispute duplicate'); + + // check the disputeId isn't tied to another requestId + // property 13 + assertEq(_ghost_disputeData[_disputeId].requestId, bytes32(0), 'Property 13: disputeId tied to another requestId'); + + // add to ghost disputes + _ghost_disputes[_responseId].push(_disputeId); + _ghost_disputeData[_disputeId] = _disputeData; + } catch { + // check that there was a previous dispute, or...? + assertTrue(_ghost_disputes[_responseId].length > 0, 'property 6: fails but no previous active dispute'); + } + } + + /// @custom:property-id 7 + /// @custom:property A disputer can only escalate the first disputed response + function prop_disputerEscalateFirstDisputedResponse( + uint256 _requestIdSeed, + uint256 _responseIdSeed, + uint256 _disputeSeed + ) public { + _stakeGRT(DISPUTE_BOND_SIZE); + + // Pick random Dispute + (bytes32 _requestId, IOracle.Request memory _requestData) = _getRandomRequest(_requestIdSeed); + (bytes32 _responseId, IOracle.Response memory _responseData) = _getRandomActiveResponse(_requestId, _responseIdSeed); + (bytes32 _disputeId, IOracle.Dispute memory _disputeData) = _getRandomDispute(_requestId, _disputeSeed); + + // Escalate it + vm.prank(msg.sender); + try oracle.escalateDispute(_requestData, _responseData, _disputeData) { + // check that the dispute is the first one + assertEq(_ghost_activeResponses[_requestId][0], _disputeData.requestId, 'property 7: not first dispute'); + } catch { + // not first dispute or + // not past the bond escalation deadline + // past deadline/tying buffer or + // already escalated or + // bond not tied or + // + } + } + + /// @custom:property-id 8a + /// @custom:property A pledger can only pledge for the correct side for an active dispure or resolution, during the tying buffer or before the deadline + function prop_pledgerCanPledgeFor(uint256 _requestIdSeed, uint256 _disputeSeed) public { + _stakeGRT(DISPUTE_BOND_SIZE); + + // Pick random dispute + (bytes32 _requestId, IOracle.Request memory _requestData) = _getRandomRequest(_requestIdSeed); + (bytes32 _disputeId, IOracle.Dispute memory _disputeData) = _getRandomDispute(_requestId, _disputeSeed); + + vm.prank(msg.sender); + try bondEscalationModule.pledgeForDispute(_requestData, _disputeData) { + assertTrue(_ghost_escalatedDisputes[_disputeId], 'property 8a: pledging for a dispute not escalated'); + } catch { + // !_ghost_escalatedDisputes or + // past deadline/tying buffer or + // already pledged this turn or + // + } + } + + /// @custom:property-id 8b + /// @custom:property A pledger can only pledge for the correct side for an active dispure or resolution, during the tying buffer or before the deadline + function prop_pledgerCanPledgeAgainst(uint256 _requestIdSeed, uint256 _disputeSeed) public { + _stakeGRT(DISPUTE_BOND_SIZE); + + // Pick random dispute + (bytes32 _requestId, IOracle.Request memory _requestData) = _getRandomRequest(_requestIdSeed); + (bytes32 _disputeId, IOracle.Dispute memory _disputeData) = _getRandomDispute(_requestId, _disputeSeed); + + vm.prank(msg.sender); + try bondEscalationModule.pledgeAgainstDispute(_requestData, _disputeData) { + assertTrue(_ghost_escalatedDisputes[_disputeId], 'property 8a: pledging for a dispute not escalated'); + } catch { + // !_ghost_escalatedDisputes or + // past deadline/tying buffer or + // already pledged this turn or + // + } + } + + /// @custom:property-id 9 + /// @custom:property An arbitrator can always settle a dispute if it has not been finalised yet + function prop_arbitratorCanSettle(uint256 _requestIdSeed, uint256 _disputeSeed, uint256 _whoWonSeed) public { + // Pick random dispute + (bytes32 _requestId,) = _getRandomRequest(_requestIdSeed); + (bytes32 _disputeId,) = _getRandomDispute(_requestId, _disputeSeed); + + // Pick random outcome (won, lost, noResolution) + IOracle.DisputeStatus _whoWon = IOracle.DisputeStatus( + bound(_whoWonSeed, uint256(IOracle.DisputeStatus.Won), uint256(IOracle.DisputeStatus.NoResolution)) + ); + + // This test contract is the default arbitrator + try councilArbitrator.arbitrateDispute(_disputeId, _whoWon) { + // Dispute is finalised, with _whoWon + // + // ghost updates + } catch { + // not arbitrator or + // + // + } + } +} diff --git a/test/invariants/properties/PropertyEbo.t.sol b/test/invariants/properties/PropertyEbo.t.sol deleted file mode 100644 index 4c2df8b..0000000 --- a/test/invariants/properties/PropertyEbo.t.sol +++ /dev/null @@ -1,16 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.26; - -import {HandlerParent} from '../handlers/HandlerParent.t.sol'; - -contract PropertyEbo is HandlerParent { - //solhint-disable no-empty-blocks - constructor() {} - - /// @custom:property-id 1 - /// @custom:property Requester can always create a request as long as the same chainId/epoch isn't requested yet - // function property_canAlwaysCreateRequest(uint256 _epoch, uint256 _chainId) external { - // _epoch = _boundEpoch(_epoch); - // string memory _chainId = _generateChainId(_chainId); - // } -} diff --git a/test/invariants/properties/PropertyFinalize.t.sol b/test/invariants/properties/PropertyFinalize.t.sol new file mode 100644 index 0000000..934614f --- /dev/null +++ b/test/invariants/properties/PropertyFinalize.t.sol @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import {IEBORequestModule, IOracle} from '../Setup.t.sol'; +import {HandlerParent} from '../handlers/HandlerParent.t.sol'; + +contract PropertyFinalize is HandlerParent { + /// @custom:property-id 10 + /// @custom:property An answer can only be finalized after the deadline + function property_finalizeAfterDeadline(uint256 _requestIdSeed, uint256 _responseIdSeed) public { + // Pick random Response + (bytes32 _requestId, IOracle.Request memory _requestData) = _getRandomRequest(_requestIdSeed); + (bytes32 _responseId, IOracle.Response memory _responseData) = _getRandomActiveResponse(_requestId, _responseIdSeed); + + vm.prank(msg.sender); + try oracle.finalize(_requestData, _responseData) { + // After deadline + // + // Update ghosts + } catch { + // Before response deadline or + // No response if there is one or + // Unresolved disputed or + // + } + } + + // TODO: consider adding property for releasing unfinazible ones? +} diff --git a/test/invariants/properties/PropertyParent.t.sol b/test/invariants/properties/PropertyParent.t.sol index 5e6eeda..2e3a558 100644 --- a/test/invariants/properties/PropertyParent.t.sol +++ b/test/invariants/properties/PropertyParent.t.sol @@ -1,10 +1,12 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.26; -import {PropertyEbo} from './PropertyEbo.t.sol'; -import {PropertySanityCheck} from './PropertySanityCheck.t.sol'; +import {PropertyDispute} from './PropertyDispute.t.sol'; -contract PropertyParent is PropertyEbo, PropertySanityCheck { - //solhint-disable no-empty-blocks - constructor() {} +import {PropertyFinalize} from './PropertyFinalize.t.sol'; +import {PropertyProposer} from './PropertyProposer.t.sol'; +import {PropertyRequester} from './PropertyRequester.t.sol'; + +contract PropertyParent is PropertyRequester, PropertyProposer, PropertyDispute, PropertyFinalize { +// | 11 | bonded token can never be used on behalf of someone else, unless allowed by the Horizon staking contract | | [ ] | } diff --git a/test/invariants/properties/PropertyProposer.t.sol b/test/invariants/properties/PropertyProposer.t.sol new file mode 100644 index 0000000..4f95ee2 --- /dev/null +++ b/test/invariants/properties/PropertyProposer.t.sol @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import {IOracle} from '../Setup.t.sol'; +import {HandlerParent} from '../handlers/HandlerParent.t.sol'; + +contract PropertyProposer is HandlerParent { + /// @custom:property-id 3 + /// @custom:property A proposer can always propose an answer before the deadline, if no response has been submitted + /// @custom:property-id 4 + /// @custom:property A proposer can always propose an answer before the deadline, if previous response is disputed and has staked + function property_proposeBeforeDeadlineAndNoAnswer(uint256 _requestIdSeed, bytes calldata _response) public { + // Pick random request + (bytes32 requestId, IOracle.Request memory requestData) = _getRandomRequest(_requestIdSeed); + if (requestId == bytes32(0) || !_ghost_validRequests[requestId]) return; + + // address proposer = _pickActor(_actorSeed); + + // Build response data + IOracle.Response memory responseData = IOracle.Response(msg.sender, requestId, _response); // abi.encode(_blockNumber)? + + // Calculate response ID using same logic as Oracle + bytes32 responseId = keccak256(abi.encode(responseData)); + + // Stake some GRT in Horizon + _stakeGRT(RESPONSE_BOND_SIZE); + // Provision some GRT in Horizon + _provisionGRT(RESPONSE_BOND_SIZE); + // Approve response module + vm.prank(msg.sender); + horizonAccountingExtension.approveModule(address(bondedResponseModule)); + + // Propose response + vm.prank(msg.sender); + try oracle.proposeResponse(requestData, responseData) { + // Track response + _ghost_activeResponses[requestId].push(responseId); + _ghost_responseData[responseId] = responseData; + + emit ResponseProposed(requestId, responseId); + } catch { + // assert(false); + } + } +} diff --git a/test/invariants/properties/PropertyRequester.t.sol b/test/invariants/properties/PropertyRequester.t.sol new file mode 100644 index 0000000..6a8e88b --- /dev/null +++ b/test/invariants/properties/PropertyRequester.t.sol @@ -0,0 +1,78 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import {IEBORequestModule, IOracle} from '../Setup.t.sol'; +import {HandlerParent} from '../handlers/HandlerParent.t.sol'; + +contract PropertyRequester is HandlerParent { + //solhint-disable no-empty-blocks + constructor() {} + + /// @custom:property-id 1 + /// @custom:property Requester can always create a request as long as the same chainId/epoch isn't already finalized with response + /// @custom:property-id 2 + /// @custom:property Requester can always create a request as long as there is no other active request per the same chainId/epoch + function property_canAlwaysCreateRequest(uint256 _epoch, uint256 _chainIdSeed) external { + _epoch = bound(_epoch, START_EPOCH, block.timestamp); + + string memory chainId = _getRandomChain(_chainIdSeed); + if (bytes(chainId).length == 0) return; + + uint256 requestsPerEpochChainId = _ghost_requestsPerEpochChainId[_epoch][chainId].length; + bytes32 requestId; + uint256 activeRequests; + bool isFinalizedWithResponse; + + // Create request via EBORequestCreator + try eboRequestCreator.createRequest(_epoch, chainId) { + // Check if there are active requests and a request finalized with response + for (uint256 i; i < requestsPerEpochChainId; ++i) { + requestId = _ghost_requestsPerEpochChainId[_epoch][chainId][i]; + if (oracle.finalizedAt(requestId) == 0) ++activeRequests; + if (oracle.finalizedResponseId(requestId) != 0) { + isFinalizedWithResponse = true; + break; + } + } + + // property 1 + assertFalse(isFinalizedWithResponse, 'prop-1: same chainId/epoch request finalized with response'); + // property 2 + assertEq(activeRequests, 0, 'prop-2: same chainId/epoch active request'); + + // Get current request data + IOracle.Request memory requestData = eboRequestCreator.getRequestData(); + + // Build request module parameters + IEBORequestModule.RequestParameters memory requestParams = + abi.decode(requestData.requestModuleData, (IEBORequestModule.RequestParameters)); + requestParams.epoch = _epoch; + requestParams.chainId = chainId; + requestData.requestModuleData = abi.encode(requestParams); + + // Calculate request ID using same logic as Oracle + bytes32 requestId = keccak256(abi.encode(requestData)); + + // Track the request + _ghost_requests.push(requestId); + _ghost_requestsPerEpochChainId[_epoch][chainId].push(requestId); + _ghost_requestData[requestId] = requestData; + _ghost_validRequests[requestId] = true; + + emit RequestCreated(requestId, _epoch, chainId); + } catch { + // Check if there are active requests and a request finalized with response + for (uint256 i; i < requestsPerEpochChainId; ++i) { + requestId = _ghost_requestsPerEpochChainId[_epoch][chainId][i]; + if (oracle.finalizedAt(requestId) == 0) ++activeRequests; + if (oracle.finalizedResponseId(requestId) != 0) { + isFinalizedWithResponse = true; + break; + } + } + + // property 1 and 2 + assertTrue(isFinalizedWithResponse || activeRequests > 0, 'prop-1-2: create request reverted'); + } + } +} diff --git a/test/invariants/properties/PropertySanityCheck.t.sol b/test/invariants/properties/PropertySanityCheck.t.sol deleted file mode 100644 index 4dc6d69..0000000 --- a/test/invariants/properties/PropertySanityCheck.t.sol +++ /dev/null @@ -1,101 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.26; - -import '../Setup.t.sol'; -import {HandlerParent} from '../handlers/HandlerParent.t.sol'; -import {IOracle} from '@defi-wonderland/prophet-core/solidity/contracts/Oracle.sol'; - -contract PropertySanityCheck is HandlerParent { - /// @custom:property-id 0 - /// @custom:property Check if eboRequestCreator has the correct properties, including the ones set after its initial deployment - function property_sanityCheck() external { - assertEq(address(eboRequestCreator.ORACLE()), address(oracle), 'prop-0: wrong oracle address'); - - assertEq(address(eboRequestCreator.ARBITRABLE()), address(arbitrable), 'prop-0: wrong arbitrable address'); - - assertEq(eboRequestCreator.START_EPOCH(), START_EPOCH, 'prop-0: wrong start epoch'); - - assertEq(address(eboRequestCreator.epochManager()), address(epochManager), 'prop-0: wrong epoch manager address'); - - IOracle.Request memory initialRequestStored = eboRequestCreator.getRequestData(); - - assertEq( - initialRequestStored.requestModule, address(eboRequestModule), 'prop-0: wrong request module in initialRequest' - ); - - assertEq( - initialRequestStored.responseModule, - address(bondedResponseModule), - 'prop-0: wrong response module in initialRequest' - ); - - assertEq( - initialRequestStored.disputeModule, address(bondEscalationModule), 'prop-0: wrong disputeModule in initialRequest' - ); - - assertEq( - initialRequestStored.resolutionModule, - address(arbitratorModule), - 'prop-0: wrong resolutionModule in initialRequest' - ); - - assertEq( - initialRequestStored.finalityModule, address(eboFinalityModule), 'prop-0: wrong finalityModule in initialRequest' - ); - - assertEq(initialRequestStored.requester, address(eboRequestCreator), 'prop-0: wrong requester in initialRequest'); - - assertEq( - initialRequestStored.requestModuleData, - abi.encode( - IEBORequestModule.RequestParameters({ - epoch: START_EPOCH, - chainId: 'mainnet', - accountingExtension: IAccountingExtension(address(horizonAccountingExtension)), - paymentAmount: PAYMENT_AMOUNT - }) - ), - 'prop-0: wrong requestModuleData in initialRequest' - ); - - assertEq( - initialRequestStored.responseModuleData, - abi.encode( - IBondedResponseModule.RequestParameters({ - accountingExtension: IAccountingExtension(address(horizonAccountingExtension)), - bondToken: GRT, - bondSize: RESPONSE_BOND_SIZE, - deadline: RESPONSE_DEADLINE, - disputeWindow: RESPONSE_DISPUTE_WINDOW - }) - ), - 'prop-0: wrong responseModuleData module data in initialRequest' - ); - - assertEq( - initialRequestStored.disputeModuleData, - abi.encode( - IBondEscalationModule.RequestParameters({ - accountingExtension: IBondEscalationAccounting(address(horizonAccountingExtension)), - bondToken: GRT, - bondSize: DISPUTE_BOND_SIZE, - maxNumberOfEscalations: MAX_NB_ESCALATION, - bondEscalationDeadline: DISPUTE_DEADLINE, - tyingBuffer: TYING_BUFFER, - disputeWindow: DISPUTE_DISPUTE_WINDOW - }) - ), - 'prop-0: wrong disputeModuleData in initialRequest' - ); - - assertEq( - initialRequestStored.resolutionModuleData, - abi.encode(IArbitratorModule.RequestParameters({arbitrator: address(councilArbitrator)})), - 'prop-0: wrong resolutionModuleData in initialRequest' - ); - - assertEq(initialRequestStored.finalityModuleData, bytes(''), 'prop-0: wrong finality module data in initialRequest'); - - assertEq(initialRequestStored.nonce, 0, 'prop-0: wrong nonce in initialRequest'); - } -}