Skip to content

Commit

Permalink
feat: handle NoResolution case in the BondEscalationModule (#40)
Browse files Browse the repository at this point in the history
* feat: handle `NoResolution` case in the `BondEscalationModule`

* fix: pay pledgers

* fix: wrong id

* fix: double comments

* feat: skip external calls if no one pledged

* test: fix bond escalation unit tests

* feat: upgrade `prophet-core`
  • Loading branch information
gas1cent authored Dec 18, 2023
1 parent ce0d37f commit ab7d538
Show file tree
Hide file tree
Showing 9 changed files with 147 additions and 121 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
"package.json": "sort-package-json"
},
"dependencies": {
"@defi-wonderland/prophet-core-contracts": "0.0.0-6ac0f50d",
"@defi-wonderland/prophet-core-contracts": "0.0.0-f88b32e2",
"@openzeppelin/contracts": "4.9.5",
"solmate": "https://github.com/transmissions11/solmate.git#bfc9c25865a274a7827fea5abf6e4fb64fc64e6c"
},
Expand Down
16 changes: 10 additions & 6 deletions solidity/contracts/extensions/BondEscalationAccounting.sol
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,6 @@ contract BondEscalationAccounting is AccountingExtension, IBondEscalationAccount
function onSettleBondEscalation(
bytes32 _requestId,
bytes32 _disputeId,
bool _forVotesWon,
IERC20 _token,
uint256 _amountPerPledger,
uint256 _winningPledgersLength
Expand All @@ -59,7 +58,6 @@ contract BondEscalationAccounting is AccountingExtension, IBondEscalationAccount

escalationResults[_disputeId] = EscalationResult({
requestId: _requestId,
forVotesWon: _forVotesWon,
token: _token,
amountPerPledger: _amountPerPledger,
bondEscalationModule: IBondEscalationModule(msg.sender)
Expand All @@ -68,7 +66,6 @@ contract BondEscalationAccounting is AccountingExtension, IBondEscalationAccount
emit BondEscalationSettled({
_requestId: _requestId,
_disputeId: _disputeId,
_forVotesWon: _forVotesWon,
_token: _token,
_amountPerPledger: _amountPerPledger,
_winningPledgersLength: _winningPledgersLength
Expand All @@ -82,10 +79,17 @@ contract BondEscalationAccounting is AccountingExtension, IBondEscalationAccount
bytes32 _requestId = _result.requestId;
if (pledgerClaimed[_requestId][_pledger]) revert BondEscalationAccounting_AlreadyClaimed();

IOracle.DisputeStatus _status = ORACLE.disputeStatus(_disputeId);
uint256 _amountPerPledger = _result.amountPerPledger;
uint256 _numberOfPledges = _result.forVotesWon
? _result.bondEscalationModule.pledgesForDispute(_requestId, _pledger)
: _result.bondEscalationModule.pledgesAgainstDispute(_requestId, _pledger);
uint256 _numberOfPledges;

if (_status == IOracle.DisputeStatus.NoResolution) {
_numberOfPledges = 1;
} else {
_numberOfPledges = _status == IOracle.DisputeStatus.Won
? _result.bondEscalationModule.pledgesForDispute(_requestId, _pledger)
: _result.bondEscalationModule.pledgesAgainstDispute(_requestId, _pledger);
}

IERC20 _token = _result.token;
uint256 _claimAmount = _amountPerPledger * _numberOfPledges;
Expand Down
154 changes: 97 additions & 57 deletions solidity/contracts/modules/dispute/BondEscalationModule.sol
Original file line number Diff line number Diff line change
Expand Up @@ -77,12 +77,15 @@ contract BondEscalationModule is Module, IBondEscalationModule {
IOracle.Dispute calldata _dispute
) external onlyOracle {
RequestParameters memory _params = decodeRequestData(_request.disputeModuleData);

BondEscalation storage _escalation = _escalations[_dispute.requestId];
IOracle.DisputeStatus _disputeStatus = ORACLE.disputeStatus(_disputeId);
BondEscalationStatus _newStatus;

if (_disputeStatus == IOracle.DisputeStatus.Escalated) {
if (_disputeId == _escalation.disputeId) {
if (_disputeId == _escalation.disputeId) {
// The bond escalated (first) dispute has been updated
if (_disputeStatus == IOracle.DisputeStatus.Escalated) {
// The dispute has been escalated to the Resolution module
// Make sure the bond escalation deadline has passed and update the status
if (block.timestamp <= _params.bondEscalationDeadline) revert BondEscalationModule_BondEscalationNotOver();

if (
Expand All @@ -92,77 +95,114 @@ contract BondEscalationModule is Module, IBondEscalationModule {
revert BondEscalationModule_NotEscalatable();
}

_escalation.status = BondEscalationStatus.Escalated;
emit BondEscalationStatusUpdated(_dispute.requestId, _disputeId, BondEscalationStatus.Escalated);
return;
} else {
emit DisputeStatusChanged({_disputeId: _disputeId, _dispute: _dispute, _status: IOracle.DisputeStatus.Escalated});
return;
}
}

bool _won = _disputeStatus == IOracle.DisputeStatus.Won;
_newStatus = BondEscalationStatus.Escalated;
} else if (_disputeStatus == IOracle.DisputeStatus.NoResolution) {
// The resolution module failed to reach a resolution
// Refund the disputer and all pledgers, the bond escalation escalation status stays Escalated
_newStatus = BondEscalationStatus.Escalated;

if (_escalation.amountOfPledgesForDispute + _escalation.amountOfPledgesAgainstDispute > 0) {
_params.accountingExtension.onSettleBondEscalation({
_requestId: _dispute.requestId,
_disputeId: _disputeId,
_token: _params.bondToken,
_amountPerPledger: _params.bondSize,
_winningPledgersLength: _escalation.amountOfPledgesForDispute + _escalation.amountOfPledgesAgainstDispute
});
}

_params.accountingExtension.pay({
_requestId: _dispute.requestId,
_payer: _won ? _dispute.proposer : _dispute.disputer,
_receiver: _won ? _dispute.disputer : _dispute.proposer,
_token: _params.bondToken,
_amount: _params.bondSize
});
_params.accountingExtension.release({
_requestId: _dispute.requestId,
_bonder: _dispute.disputer,
_token: _params.bondToken,
_amount: _params.bondSize
});
} else {
// One of the sides won
// Pay the winner (proposer/disputer) and the pledgers, the bond escalation status changes to DisputerWon/DisputerLost
bool _won = _disputeStatus == IOracle.DisputeStatus.Won;
_newStatus = _won ? BondEscalationStatus.DisputerWon : BondEscalationStatus.DisputerLost;

if (_won) {
_params.accountingExtension.release({
_requestId: _dispute.requestId,
_bonder: _dispute.disputer,
_token: _params.bondToken,
_amount: _params.bondSize
});
}
uint256 _pledgesForDispute = _escalation.amountOfPledgesForDispute;
uint256 _pledgesAgainstDispute = _escalation.amountOfPledgesAgainstDispute;

if (_disputeId == _escalation.disputeId) {
// The dispute has been escalated to the Resolution module
if (_escalation.status == BondEscalationStatus.Escalated) {
if (_escalation.amountOfPledgesAgainstDispute == 0) {
return;
if (_pledgesAgainstDispute > 0) {
uint256 _amountToPay = _won
? _params.bondSize
+ FixedPointMathLib.mulDivDown(_pledgesAgainstDispute, _params.bondSize, _pledgesForDispute)
: _params.bondSize
+ FixedPointMathLib.mulDivDown(_pledgesForDispute, _params.bondSize, _pledgesAgainstDispute);

_params.accountingExtension.onSettleBondEscalation({
_requestId: _dispute.requestId,
_disputeId: _escalation.disputeId,
_token: _params.bondToken,
_amountPerPledger: _amountToPay,
_winningPledgersLength: _won ? _pledgesForDispute : _pledgesAgainstDispute
});
}

BondEscalationStatus _newStatus = _won ? BondEscalationStatus.DisputerWon : BondEscalationStatus.DisputerLost;
_escalation.status = _newStatus;

emit BondEscalationStatusUpdated(_dispute.requestId, _disputeId, _newStatus);
_params.accountingExtension.pay({
_requestId: _dispute.requestId,
_payer: _won ? _dispute.proposer : _dispute.disputer,
_receiver: _won ? _dispute.disputer : _dispute.proposer,
_token: _params.bondToken,
_amount: _params.bondSize
});

_params.accountingExtension.onSettleBondEscalation({
if (_won) {
_params.accountingExtension.release({
_requestId: _dispute.requestId,
_bonder: _dispute.disputer,
_token: _params.bondToken,
_amount: _params.bondSize
});
}
}
} else {
// The non-bond escalated (second and subsequent) dispute has been updated
if (_disputeStatus == IOracle.DisputeStatus.Escalated) {
// The dispute has been escalated to the Resolution module
// Update the bond escalation status to Escalated
_newStatus = BondEscalationStatus.Escalated;
} else if (_disputeStatus == IOracle.DisputeStatus.NoResolution) {
// The resolution module failed to reach a resolution
// Refund the disputer, the bond escalation status stays Escalated
_newStatus = BondEscalationStatus.Escalated;
_params.accountingExtension.release({
_requestId: _dispute.requestId,
_disputeId: _disputeId,
_forVotesWon: _won,
_bonder: _dispute.disputer,
_token: _params.bondToken,
_amountPerPledger: _params.bondSize << 1,
_winningPledgersLength: _won ? _escalation.amountOfPledgesForDispute : _escalation.amountOfPledgesAgainstDispute
_amount: _params.bondSize
});
} else {
// The status has changed to Won or Lost
uint256 _pledgesForDispute = _escalation.amountOfPledgesForDispute;
uint256 _pledgesAgainstDispute = _escalation.amountOfPledgesAgainstDispute;
bool _disputersWon = _pledgesForDispute > _pledgesAgainstDispute;
// One of the sides won
// Pay the winner (proposer/disputer), the bond escalation status changes to DisputerWon/DisputerLost
bool _won = _disputeStatus == IOracle.DisputeStatus.Won;
_newStatus = _won ? BondEscalationStatus.DisputerWon : BondEscalationStatus.DisputerLost;

uint256 _amountToPay = _disputersWon
? _params.bondSize + FixedPointMathLib.mulDivDown(_pledgesAgainstDispute, _params.bondSize, _pledgesForDispute)
: _params.bondSize + FixedPointMathLib.mulDivDown(_pledgesForDispute, _params.bondSize, _pledgesAgainstDispute);

_params.accountingExtension.onSettleBondEscalation({
_params.accountingExtension.pay({
_requestId: _dispute.requestId,
_disputeId: _escalation.disputeId,
_forVotesWon: _disputersWon,
_payer: _won ? _dispute.proposer : _dispute.disputer,
_receiver: _won ? _dispute.disputer : _dispute.proposer,
_token: _params.bondToken,
_amountPerPledger: _amountToPay,
_winningPledgersLength: _disputersWon ? _pledgesForDispute : _pledgesAgainstDispute
_amount: _params.bondSize
});

if (_won) {
_params.accountingExtension.release({
_requestId: _dispute.requestId,
_bonder: _dispute.disputer,
_token: _params.bondToken,
_amount: _params.bondSize
});
}
}
}

IOracle.DisputeStatus _status = ORACLE.disputeStatus(_disputeId);
emit DisputeStatusChanged({_disputeId: _disputeId, _dispute: _dispute, _status: _status});
_escalation.status = _newStatus;
emit BondEscalationStatusUpdated(_dispute.requestId, _disputeId, _newStatus);
emit DisputeStatusChanged({_disputeId: _disputeId, _dispute: _dispute, _status: _disputeStatus});
}

////////////////////////////////////////////////////////////////////
Expand Down
21 changes: 2 additions & 19 deletions solidity/interfaces/extensions/IBondEscalationAccounting.sol
Original file line number Diff line number Diff line change
Expand Up @@ -49,18 +49,12 @@ interface IBondEscalationAccounting is IAccountingExtension {
*
* @param _requestId The ID of the bond-escalated request
* @param _disputeId The ID of the bond-escalated dispute
* @param _forVotesWon True if the winning side were the for votes
* @param _token The address of the token being paid out
* @param _amountPerPledger The amount of `_token` to be paid for each winning pledgers
* @param _winningPledgersLength The number of winning pledgers
*/
event BondEscalationSettled(
bytes32 _requestId,
bytes32 _disputeId,
bool _forVotesWon,
IERC20 _token,
uint256 _amountPerPledger,
uint256 _winningPledgersLength
bytes32 _requestId, bytes32 _disputeId, IERC20 _token, uint256 _amountPerPledger, uint256 _winningPledgersLength
);

/**
Expand Down Expand Up @@ -120,14 +114,12 @@ interface IBondEscalationAccounting is IAccountingExtension {
/**
* @notice Contains the data of the result of an escalation. Is used by users to claim their pledges
* @param requestId The ID of the bond-escalated request
* @param forVotesWon Whether the for votes won the dispute
* @param token The address of the token being paid out
* @param amountPerPledger The amount of token paid to each of the winning pledgers
* @param bondEscalationModule The address of the bond escalation module that was used
*/
struct EscalationResult {
bytes32 requestId;
bool forVotesWon;
IERC20 token;
uint256 amountPerPledger;
IBondEscalationModule bondEscalationModule;
Expand All @@ -151,20 +143,13 @@ interface IBondEscalationAccounting is IAccountingExtension {
*
* @param _disputeId The ID of the bond-escalated dispute
* @return _requestId The ID of the bond-escalated request
* @return _forVotesWon True if the for votes won the dispute
* @return _token Address of the token being paid as a reward for winning the bond escalation
* @return _amountPerPledger Amount of `_token` to be rewarded to each of the winning pledgers
* @return _bondEscalationModule The address of the bond escalation module that was used
*/
function escalationResults(bytes32 _disputeId)
external
returns (
bytes32 _requestId,
bool _forVotesWon,
IERC20 _token,
uint256 _amountPerPledger,
IBondEscalationModule _bondEscalationModule
);
returns (bytes32 _requestId, IERC20 _token, uint256 _amountPerPledger, IBondEscalationModule _bondEscalationModule);

/**
* @notice True if the given pledger has claimed their reward for the given dispute
Expand Down Expand Up @@ -198,15 +183,13 @@ interface IBondEscalationAccounting is IAccountingExtension {
*
* @param _requestId The ID of the bond-escalated request
* @param _disputeId The ID of the bond-escalated dispute
* @param _forVotesWon True if the for votes won the dispute
* @param _token Address of the token being paid as a reward for winning the bond escalation
* @param _amountPerPledger Amount of `_token` to be rewarded to each of the winning pledgers
* @param _winningPledgersLength Amount of pledges that won the dispute
*/
function onSettleBondEscalation(
bytes32 _requestId,
bytes32 _disputeId,
bool _forVotesWon,
IERC20 _token,
uint256 _amountPerPledger,
uint256 _winningPledgersLength
Expand Down
14 changes: 7 additions & 7 deletions solidity/test/integration/BondEscalation.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -101,24 +101,24 @@ contract Integration_BondEscalation is IntegrationBase {
}

function test_proposerWins() public {
// // Step 1: Proposer pledges against the dispute
// Step 1: Proposer pledges against the dispute
_deposit(_bondEscalationAccounting, proposer, usdc, _pledgeSize);
vm.prank(proposer);
_bondEscalationModule.pledgeAgainstDispute(mockRequest, mockDispute);

// // Step 2: Disputer doubles down
// Step 2: Disputer doubles down
_deposit(_bondEscalationAccounting, disputer, usdc, _pledgeSize);
vm.prank(disputer);
_bondEscalationModule.pledgeForDispute(mockRequest, mockDispute);

// // Step 3: Proposer doubles down
// Step 3: Proposer doubles down
_deposit(_bondEscalationAccounting, proposer, usdc, _pledgeSize);
vm.prank(proposer);
_bondEscalationModule.pledgeAgainstDispute(mockRequest, mockDispute);

// // Step 4: Disputer runs out of capital
// // Step 5: External parties see that Disputer's dispute was wrong so they don't join to escalate
// // Step 6: Proposer response's is deemed correct and final once the bond escalation window is over
// Step 4: Disputer runs out of capital
// Step 5: External parties see that Disputer's dispute was wrong so they don't join to escalate
// Step 6: Proposer response's is deemed correct and final once the bond escalation window is over
vm.warp(_expectedDeadline + _tyingBuffer + 1);
_bondEscalationModule.settleBondEscalation(mockRequest, mockResponse, mockDispute);

Expand Down Expand Up @@ -191,7 +191,7 @@ contract Integration_BondEscalation is IntegrationBase {
_bondEscalationAccounting.claimEscalationReward(_disputeId, requester);
assertEq(_bondEscalationAccounting.balanceOf(requester, usdc), 0, 'Mismatch: Requester balance');

// Test: The proposer has lost his pledge
// // Test: The proposer has lost his pledge
_bondEscalationAccounting.claimEscalationReward(_disputeId, proposer);
assertEq(_bondEscalationAccounting.balanceOf(proposer, usdc), 0, 'Mismatch: Proposer balance');

Expand Down
Loading

0 comments on commit ab7d538

Please sign in to comment.