diff --git a/solidity/contracts/modules/response/BondedResponseModule.sol b/solidity/contracts/modules/response/BondedResponseModule.sol index 827ec06a..dfefff65 100644 --- a/solidity/contracts/modules/response/BondedResponseModule.sol +++ b/solidity/contracts/modules/response/BondedResponseModule.sol @@ -7,6 +7,9 @@ import {IOracle} from '@defi-wonderland/prophet-core/solidity/interfaces/IOracle import {IBondedResponseModule} from '../../../interfaces/modules/response/IBondedResponseModule.sol'; contract BondedResponseModule is Module, IBondedResponseModule { + /// @inheritDoc IBondedResponseModule + mapping(bytes32 _responseId => bool _released) public responseReleased; + constructor(IOracle _oracle) Module(_oracle) {} /// @inheritdoc IModule @@ -102,6 +105,8 @@ contract BondedResponseModule is Module, IBondedResponseModule { /// @inheritdoc IBondedResponseModule function releaseUnutilizedResponse(IOracle.Request calldata _request, IOracle.Response calldata _response) external { bytes32 _responseId = _validateResponse(_request, _response); + if (responseReleased[_responseId]) revert BondedResponseModule_ResponseAlreadyReleased(); + bytes32 _disputeId = ORACLE.disputeOf(_responseId); if (_disputeId > 0) { @@ -116,6 +121,8 @@ contract BondedResponseModule is Module, IBondedResponseModule { revert BondedResponseModule_InvalidReleaseParameters(); } + responseReleased[_responseId] = true; + RequestParameters memory _params = decodeRequestData(_request.responseModuleData); _params.accountingExtension.release({ _bonder: _response.proposer, diff --git a/solidity/interfaces/modules/response/IBondedResponseModule.sol b/solidity/interfaces/modules/response/IBondedResponseModule.sol index bacb55d0..41d68ed4 100644 --- a/solidity/interfaces/modules/response/IBondedResponseModule.sol +++ b/solidity/interfaces/modules/response/IBondedResponseModule.sol @@ -55,6 +55,11 @@ interface IBondedResponseModule is IResponseModule { */ error BondedResponseModule_InvalidReleaseParameters(); + /** + * @notice Thrown when trying to release an already released response + */ + error BondedResponseModule_ResponseAlreadyReleased(); + /*/////////////////////////////////////////////////////////////// STRUCTS //////////////////////////////////////////////////////////////*/ @@ -76,6 +81,16 @@ interface IBondedResponseModule is IResponseModule { uint256 disputeWindow; } + /*/////////////////////////////////////////////////////////////// + VARIABLES + //////////////////////////////////////////////////////////////*/ + /** + * @notice Returns true if the response was already released because it was unutilized + * @param _responseId The response id to check + * @return _released true if the response was already released + */ + function responseReleased(bytes32 _responseId) external view returns (bool _released); + /*/////////////////////////////////////////////////////////////// LOGIC //////////////////////////////////////////////////////////////*/ diff --git a/solidity/test/unit/modules/response/BondedResponseModule.t.sol b/solidity/test/unit/modules/response/BondedResponseModule.t.sol index 8a9eb121..374d924f 100644 --- a/solidity/test/unit/modules/response/BondedResponseModule.t.sol +++ b/solidity/test/unit/modules/response/BondedResponseModule.t.sol @@ -561,6 +561,65 @@ contract BondedResponseModule_Unit_ReleaseUnutilizedResponse is BaseTest { bondedResponseModule.releaseUnutilizedResponse(mockRequest, mockResponse); } + /** + * @notice Finalized request, undisputed response, the bond should be released + */ + function test_releaseUnutilizedResponse_revertsIfCalledTwice( + IERC20 _token, + uint256 _bondSize, + uint256 _deadline, + bytes32 _finalizedResponseId, + uint256 _finalizedAt + ) 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); + vm.assume(_finalizedAt > 0); + + // 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) + ); + + _mockAndExpect(address(oracle), abi.encodeCall(IOracle.finalizedAt, (_requestId)), abi.encode(_finalizedAt)); + + _mockAndExpect( + address(oracle), abi.encodeCall(IOracle.responseCreatedAt, (_responseId)), abi.encode(block.timestamp) + ); + + // Mock and expect IAccountingExtension.release to be called + _mockAndExpect( + address(accounting), + abi.encodeCall(IAccountingExtension.release, (proposer, _getId(mockRequest), _token, _bondSize)), + abi.encode(true) + ); + + // Before releasing the response should be marked as not released + vm.assertFalse(bondedResponseModule.responseReleased(_getId(mockResponse))); + + // Release the response bond + bondedResponseModule.releaseUnutilizedResponse(mockRequest, mockResponse); + + // Saves locally that the response was already released + vm.assertTrue(bondedResponseModule.responseReleased(_getId(mockResponse))); + + // Should revert if we call it again + vm.expectRevert(IBondedResponseModule.BondedResponseModule_ResponseAlreadyReleased.selector); + bondedResponseModule.releaseUnutilizedResponse(mockRequest, mockResponse); + } + /** * @notice Non-finalized request, undisputed response, the call should revert */