From b54ab5c58a94d7d984730c55ecaf1fe9711d60b6 Mon Sep 17 00:00:00 2001 From: moebius <132487952+0xmoebius@users.noreply.github.com> Date: Fri, 15 Dec 2023 10:03:56 -0300 Subject: [PATCH] feat: let proposers release its bond if the response is won and not used (#23) --- .../modules/response/BondedResponseModule.sol | 28 ++++ .../response/IBondedResponseModule.sol | 21 +++ .../response/BondedResponseModule.t.sol | 136 ++++++++++++++++++ 3 files changed, 185 insertions(+) diff --git a/solidity/contracts/modules/response/BondedResponseModule.sol b/solidity/contracts/modules/response/BondedResponseModule.sol index fd0ffa52..1b6c60d7 100644 --- a/solidity/contracts/modules/response/BondedResponseModule.sol +++ b/solidity/contracts/modules/response/BondedResponseModule.sol @@ -88,6 +88,34 @@ contract BondedResponseModule is Module, IBondedResponseModule { emit RequestFinalized(_response.requestId, _response, _finalizer); } + /// @inheritdoc IBondedResponseModule + function releaseUnutilizedResponse(IOracle.Request calldata _request, IOracle.Response calldata _response) external { + bytes32 _responseId = _validateResponse(_request, _response); + bytes32 _disputeId = ORACLE.disputeOf(_responseId); + + if (_disputeId > 0) { + IOracle.DisputeStatus _disputeStatus = ORACLE.disputeStatus(_disputeId); + if (_disputeStatus != IOracle.DisputeStatus.Lost && _disputeStatus != IOracle.DisputeStatus.NoResolution) { + revert BondedResponseModule_InvalidReleaseParameters(); + } + } + + bytes32 _finalizedResponseId = ORACLE.finalizedResponseId(_response.requestId); + if (_finalizedResponseId == _responseId || _finalizedResponseId == bytes32(0)) { + revert BondedResponseModule_InvalidReleaseParameters(); + } + + RequestParameters memory _params = decodeRequestData(_request.responseModuleData); + _params.accountingExtension.release({ + _bonder: _response.proposer, + _requestId: _response.requestId, + _token: _params.bondToken, + _amount: _params.bondSize + }); + + emit UnutilizedResponseReleased(_response.requestId, _responseId); + } + /// @inheritdoc IModule function validateParameters(bytes calldata _encodedParameters) external diff --git a/solidity/interfaces/modules/response/IBondedResponseModule.sol b/solidity/interfaces/modules/response/IBondedResponseModule.sol index 0c1f0cae..74073cd2 100644 --- a/solidity/interfaces/modules/response/IBondedResponseModule.sol +++ b/solidity/interfaces/modules/response/IBondedResponseModule.sol @@ -25,6 +25,14 @@ interface IBondedResponseModule is IResponseModule { */ event ResponseProposed(bytes32 indexed _requestId, IOracle.Response _response, uint256 indexed _blockNumber); + /** + * @notice Emitted when an uncalled response is released + * + * @param _requestId The ID of the request that the response was proposed to + * @param _responseId The ID of the response that was released + */ + event UnutilizedResponseReleased(bytes32 indexed _requestId, bytes32 indexed _responseId); + /*/////////////////////////////////////////////////////////////// ERRORS //////////////////////////////////////////////////////////////*/ @@ -44,6 +52,11 @@ interface IBondedResponseModule is IResponseModule { */ error BondedResponseModule_AlreadyResponded(); + /** + * @notice Thrown when trying to release an uncalled response with an invalid request, response or dispute + */ + error BondedResponseModule_InvalidReleaseParameters(); + /*/////////////////////////////////////////////////////////////// STRUCTS //////////////////////////////////////////////////////////////*/ @@ -99,4 +112,12 @@ interface IBondedResponseModule is IResponseModule { IOracle.Response calldata _response, address _finalizer ) external; + + /** + * @notice Releases the proposer fund if the response is valid and it has not been used to finalize the request + * + * @param _request The finalized request + * @param _response The unutilized response + */ + function releaseUnutilizedResponse(IOracle.Request calldata _request, IOracle.Response calldata _response) external; } diff --git a/solidity/test/unit/modules/response/BondedResponseModule.t.sol b/solidity/test/unit/modules/response/BondedResponseModule.t.sol index becc97b1..66ee6332 100644 --- a/solidity/test/unit/modules/response/BondedResponseModule.t.sol +++ b/solidity/test/unit/modules/response/BondedResponseModule.t.sol @@ -31,6 +31,7 @@ contract BaseTest is Test, Helpers { // Events event ResponseProposed(bytes32 indexed _requestId, IOracle.Response _response, uint256 indexed _blockNumber); + event UnutilizedResponseReleased(bytes32 indexed _requestId, bytes32 indexed _responseId); /** * @notice Deploy the target and mock oracle+accounting extension @@ -385,3 +386,138 @@ contract BondedResponseModule_Unit_FinalizeRequest is BaseTest { bondedResponseModule.finalizeRequest(mockRequest, mockResponse, _finalizer); } } + +contract BondedResponseModule_Unit_ReleaseUnutilizedResponse is BaseTest { + /** + * @notice Finalized request, undisputed response, the bond should be released + */ + function test_withUndisputedResponse_withFinalizedRequest_releasesBond( + IERC20 _token, + uint256 _bondSize, + uint256 _deadline, + address _proposer, + bytes32 _finalizedResponseId + ) public { + // Setting the response module data + mockRequest.responseModuleData = abi.encode(accounting, _token, _bondSize, _deadline, _baseDisputeWindow); + + // Updating IDs + bytes32 _requestId = _getId(mockRequest); + mockResponse.requestId = _requestId; + mockResponse.proposer = _proposer; + bytes32 _responseId = _getId(mockResponse); + + // Can't claim back the bond of the response that was finalized + vm.assume(_finalizedResponseId > 0); + vm.assume(_finalizedResponseId != _responseId); + + // Mock and expect IOracle.disputeOf to be called + _mockAndExpect(address(oracle), abi.encodeCall(IOracle.disputeOf, (_responseId)), abi.encode(bytes32(0))); + + // Mock and expect IOracle.finalizedResponseId to be called + _mockAndExpect( + address(oracle), abi.encodeCall(IOracle.finalizedResponseId, (_requestId)), abi.encode(_finalizedResponseId) + ); + + // Mock and expect IAccountingExtension.release to be called + _mockAndExpect( + address(accounting), + abi.encodeCall(IAccountingExtension.release, (_proposer, _getId(mockRequest), _token, _bondSize)), + abi.encode(true) + ); + + // Check: is the event emitted? + vm.expectEmit(true, true, true, true, address(bondedResponseModule)); + emit UnutilizedResponseReleased(_requestId, _responseId); + + // Test: does it release the bond? + bondedResponseModule.releaseUnutilizedResponse(mockRequest, mockResponse); + } + + /** + * @notice Non-finalized request, undisputed response, the call should revert + */ + function test_withUndisputedResponse_revertsIfRequestIsNotFinalized( + IERC20 _token, + uint256 _bondSize, + uint256 _deadline, + address _proposer + ) public { + // Setting the response module data + mockRequest.responseModuleData = abi.encode(accounting, _token, _bondSize, _deadline, _baseDisputeWindow); + + // Updating IDs + bytes32 _requestId = _getId(mockRequest); + mockResponse.requestId = _requestId; + mockResponse.proposer = _proposer; + bytes32 _responseId = _getId(mockResponse); + + // Mock and expect IOracle.disputeOf to be called + _mockAndExpect(address(oracle), abi.encodeCall(IOracle.disputeOf, (_responseId)), abi.encode(bytes32(0))); + + // Mock and expect IOracle.finalizedResponseId to be called + _mockAndExpect(address(oracle), abi.encodeCall(IOracle.finalizedResponseId, (_requestId)), abi.encode(0)); + + // Check: reverts? + vm.expectRevert(IBondedResponseModule.BondedResponseModule_InvalidReleaseParameters.selector); + + bondedResponseModule.releaseUnutilizedResponse(mockRequest, mockResponse); + } + + /** + * @notice Finalized request, disputed response, the call should revert if the dispute status is not Lost nor NoResolution + */ + function test_withDisputedResponse( + IERC20 _token, + uint256 _bondSize, + uint256 _deadline, + address _proposer, + bytes32 _finalizedResponseId, + bytes32 _disputeId + ) public { + // Setting the response module data + mockRequest.responseModuleData = abi.encode(accounting, _token, _bondSize, _deadline, _baseDisputeWindow); + + // Updating IDs + bytes32 _requestId = _getId(mockRequest); + mockResponse.requestId = _requestId; + mockResponse.proposer = _proposer; + bytes32 _responseId = _getId(mockResponse); + + // Make sure there is a dispute + vm.assume(_disputeId > 0); + + // Can't claim back the bond of the response that was finalized + vm.assume(_finalizedResponseId > 0); + vm.assume(_finalizedResponseId != _responseId); + + // Mock and expect IOracle.disputeOf to be called + _mockAndExpect(address(oracle), abi.encodeCall(IOracle.disputeOf, (_responseId)), abi.encode(_disputeId)); + + // Mock and expect IOracle.finalizedResponseId to be called + _mockAndExpect( + address(oracle), abi.encodeCall(IOracle.finalizedResponseId, (_requestId)), abi.encode(_finalizedResponseId) + ); + + // We're going to test all possible dispute statuses + for (uint256 _i = 0; _i < uint256(type(IOracle.DisputeStatus).max); _i++) { + IOracle.DisputeStatus _status = IOracle.DisputeStatus(_i); + + // Mock and expect IOracle.disputeOf to be called + _mockAndExpect(address(oracle), abi.encodeCall(IOracle.disputeStatus, (_disputeId)), abi.encode(_status)); + + if (_status == IOracle.DisputeStatus.Lost || _status == IOracle.DisputeStatus.NoResolution) { + // Mock and expect IAccountingExtension.release to be called + _mockAndExpect( + address(accounting), + abi.encodeCall(IAccountingExtension.release, (_proposer, _getId(mockRequest), _token, _bondSize)), + abi.encode(true) + ); + } else { + vm.expectRevert(IBondedResponseModule.BondedResponseModule_InvalidReleaseParameters.selector); + } + + bondedResponseModule.releaseUnutilizedResponse(mockRequest, mockResponse); + } + } +}