Skip to content

Commit

Permalink
feat: let proposers release its bond if the response is won and not u…
Browse files Browse the repository at this point in the history
…sed (#23)
  • Loading branch information
0xmoebius authored Dec 15, 2023
1 parent 480dba7 commit b54ab5c
Show file tree
Hide file tree
Showing 3 changed files with 185 additions and 0 deletions.
28 changes: 28 additions & 0 deletions solidity/contracts/modules/response/BondedResponseModule.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
21 changes: 21 additions & 0 deletions solidity/interfaces/modules/response/IBondedResponseModule.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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
//////////////////////////////////////////////////////////////*/
Expand All @@ -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
//////////////////////////////////////////////////////////////*/
Expand Down Expand Up @@ -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;
}
136 changes: 136 additions & 0 deletions solidity/test/unit/modules/response/BondedResponseModule.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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);
}
}
}

0 comments on commit b54ab5c

Please sign in to comment.