From dc22ef09f313827d12c454912a504bd15497c2dd Mon Sep 17 00:00:00 2001 From: Spablob Date: Fri, 13 Jun 2025 18:48:57 +0100 Subject: [PATCH 1/2] unwrap bond --- contracts/interfaces/IWIP.sol | 7 ++++ .../policies/UMA/ArbitrationPolicyUMA.sol | 42 +++++++++++++++++-- script/foundry/utils/DeployHelper.sol | 4 +- .../policies/UMA/ArbitrationPolicyUMA.t.sol | 32 +++++++------- 4 files changed, 64 insertions(+), 21 deletions(-) create mode 100644 contracts/interfaces/IWIP.sol diff --git a/contracts/interfaces/IWIP.sol b/contracts/interfaces/IWIP.sol new file mode 100644 index 00000000..21a5bf9a --- /dev/null +++ b/contracts/interfaces/IWIP.sol @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.26; + +/// @title IWIP Interface +interface IWIP { + function withdraw(uint256 amount) external; +} \ No newline at end of file diff --git a/contracts/modules/dispute/policies/UMA/ArbitrationPolicyUMA.sol b/contracts/modules/dispute/policies/UMA/ArbitrationPolicyUMA.sol index 455b4f8f..f9bb1021 100644 --- a/contracts/modules/dispute/policies/UMA/ArbitrationPolicyUMA.sol +++ b/contracts/modules/dispute/policies/UMA/ArbitrationPolicyUMA.sol @@ -5,11 +5,13 @@ import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; import { ReentrancyGuardUpgradeable } from "@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol"; +import { Address } from "@openzeppelin/contracts/utils/Address.sol"; import { IDisputeModule } from "../../../../interfaces/modules/dispute/IDisputeModule.sol"; import { IRoyaltyModule } from "../../../../interfaces/modules/royalty/IRoyaltyModule.sol"; import { IArbitrationPolicyUMA } from "../../../../interfaces/modules/dispute/policies/UMA/IArbitrationPolicyUMA.sol"; import { IOOV3 } from "../../../../interfaces/modules/dispute/policies/UMA/IOOV3.sol"; +import { IWIP } from "../../../../interfaces/IWIP.sol"; import { ProtocolPausableUpgradeable } from "../../../../pause/ProtocolPausableUpgradeable.sol"; import { BytesConversion } from "../../../../lib/BytesConversion.sol"; import { Errors } from "../../../../lib/Errors.sol"; @@ -43,6 +45,7 @@ contract ArbitrationPolicyUMA is /// @param assertionIdToDisputeId The mapping of assertion id to dispute id /// @param counterEvidenceHashes The mapping of assertion id to counter evidence hash /// @param ipOwnerTimePercents The mapping of dispute id to ip owner time percent of the dispute + /// @param disputers The mapping of dispute id to disputer /// @custom:storage-location erc7201:story-protocol.ArbitrationPolicyUMA struct ArbitrationPolicyUMAStorage { uint64 minLiveness; @@ -54,12 +57,15 @@ contract ArbitrationPolicyUMA is mapping(bytes32 assertionId => uint256 disputeId) assertionIdToDisputeId; mapping(bytes32 assertionId => bytes32 counterEvidenceHash) counterEvidenceHashes; mapping(uint256 disputeId => uint32 ipOwnerTimePercent) ipOwnerTimePercents; + mapping(uint256 disputeId => address disputer) disputers; } // keccak256(abi.encode(uint256(keccak256("story-protocol.ArbitrationPolicyUMA")) - 1)) & ~bytes32(uint256(0xff)); bytes32 private constant ArbitrationPolicyUMAStorageLocation = 0xbd39630b628d883a3167c4982acf741cbddb24bae6947600210f8eb1db515300; + address public constant WIP = 0x1514000000000000000000000000000000000000; + /// @dev Restricts the calls to the dispute module modifier onlyDisputeModule() { if (msg.sender != address(DISPUTE_MODULE)) revert Errors.ArbitrationPolicyUMA__NotDisputeModule(); @@ -165,7 +171,7 @@ contract ArbitrationPolicyUMA is bytes32 assertionId = oov3.assertTruth( _constructClaim(targetIpId, targetTag, disputeEvidenceHash, disputeId), - disputeInitiator, // asserter + address(this), // asserter address(this), // callbackRecipient address(0), // escalationManager liveness, @@ -236,27 +242,53 @@ contract ArbitrationPolicyUMA is ); $.counterEvidenceHashes[assertionId] = counterEvidenceHash; + $.disputers[disputeId] = msg.sender; IERC20 currencyToken = IERC20(assertion.currency); IOOV3 oov3 = $.oov3; currencyToken.safeTransferFrom(msg.sender, address(this), assertion.bond); currencyToken.safeIncreaseAllowance(address(oov3), assertion.bond); - oov3.disputeAssertion(assertionId, msg.sender); + oov3.disputeAssertion(assertionId, address(this)); emit AssertionDisputed(disputeId, assertionId, counterEvidenceHash); } - /// @notice OOV3 callback function forwhen an assertion is resolved + /// @notice OOV3 callback function for when an assertion is resolved /// @param assertionId The resolved assertion identifier /// @param assertedTruthfully Indicates if the assertion was resolved as truthful or not function assertionResolvedCallback(bytes32 assertionId, bool assertedTruthfully) external nonReentrant { ArbitrationPolicyUMAStorage storage $ = _getArbitrationPolicyUMAStorage(); - if (msg.sender != address($.oov3)) revert Errors.ArbitrationPolicyUMA__NotOOV3(); + IOOV3 oov3 = $.oov3; + if (msg.sender != address(oov3)) revert Errors.ArbitrationPolicyUMA__NotOOV3(); uint256 disputeId = $.assertionIdToDisputeId[assertionId]; + (, address disputeInitiator, , , , , , ) = DISPUTE_MODULE.disputes(disputeId); + IOOV3.Assertion memory assertion = oov3.getAssertion(assertionId); + + // determine the amount to transfer and the recipient of the transfer + uint256 amountToTransfer; + address bondRecipient; + if (assertion.disputer == address(0)) { + // if the assertion was not disputed, the bond is transferred to the dispute initiator + amountToTransfer = assertion.bond; + bondRecipient = disputeInitiator; + } else { + uint256 oracleFee = (oov3.burnedBondPercentage() * assertion.bond) / 1e18; + amountToTransfer = assertion.bond * 2 - oracleFee; + bondRecipient = assertedTruthfully ? disputeInitiator : $.disputers[disputeId]; + } + // set the dispute judgement in the dispute module DISPUTE_MODULE.setDisputeJudgement(disputeId, assertedTruthfully, ""); + + // transfer the amount to the recipient + if (address(assertion.currency) == WIP && bondRecipient.code.length == 0) { + IWIP(WIP).withdraw(amountToTransfer); + Address.sendValue(payable(bondRecipient), amountToTransfer); + } else { + IERC20(assertion.currency).safeTransfer(bondRecipient, amountToTransfer); + } } /// @notice OOV3 callback function for when an assertion is disputed @@ -311,6 +343,8 @@ contract ArbitrationPolicyUMA is return _getArbitrationPolicyUMAStorage().assertionIdToDisputeId[assertionId]; } + receive() external payable {} + /// @notice Constructs the claim for a given dispute /// @param targetIpId The ipId that is the target of the dispute /// @param disputeEvidenceHash The hash pointing to the dispute evidence diff --git a/script/foundry/utils/DeployHelper.sol b/script/foundry/utils/DeployHelper.sol index 45b4c2df..62fe8799 100644 --- a/script/foundry/utils/DeployHelper.sol +++ b/script/foundry/utils/DeployHelper.sol @@ -582,14 +582,14 @@ contract DeployHelper is Script, BroadcastManager, JsonDeploymentHandler, Storag _predeploy("ArbitrationPolicyUMA"); impl = address(new ArbitrationPolicyUMA(address(disputeModule), address(royaltyModule))); - arbitrationPolicyUMA = ArbitrationPolicyUMA( + arbitrationPolicyUMA = ArbitrationPolicyUMA(payable( TestProxyHelper.deployUUPSProxy( create3Deployer, _getSalt(type(ArbitrationPolicyUMA).name), impl, abi.encodeCall(ArbitrationPolicyUMA.initialize, address(protocolAccessManager)) ) - ); + )); require( _getDeployedAddress(type(ArbitrationPolicyUMA).name) == address(arbitrationPolicyUMA), "Deploy: Arbitration Policy Address Mismatch" diff --git a/test/foundry/modules/dispute/policies/UMA/ArbitrationPolicyUMA.t.sol b/test/foundry/modules/dispute/policies/UMA/ArbitrationPolicyUMA.t.sol index c8294219..e695f246 100644 --- a/test/foundry/modules/dispute/policies/UMA/ArbitrationPolicyUMA.t.sol +++ b/test/foundry/modules/dispute/policies/UMA/ArbitrationPolicyUMA.t.sol @@ -51,7 +51,7 @@ contract ArbitrationPolicyUMATest is BaseTest { wip = 0x1514000000000000000000000000000000000000; // WIP address disputeModule = DisputeModule(0x9b7A9c70AFF961C799110954fc06F3093aeb94C5); royaltyModule = RoyaltyModule(0xD2f60c40fEbccf6311f8B47c4f2Ec6b040400086); - arbitrationPolicyUMA = ArbitrationPolicyUMA(0xfFD98c3877B8789124f02C7E8239A4b0Ef11E936); + arbitrationPolicyUMA = ArbitrationPolicyUMA(payable(0xfFD98c3877B8789124f02C7E8239A4b0Ef11E936)); protocolAdmin = 0x623Cb5A594dAD5cc1Ea1bDb0b084bf8F1fE4B2e4; protocolPauseAdmin = 0xdd661f55128A80437A0c0BDA6E13F214A3B2EB24; address upgrader = 0x4C30baDa479D0e13300b31b1696A5E570848bbEe; @@ -447,13 +447,13 @@ contract ArbitrationPolicyUMATest is BaseTest { (, , , , , , bytes32 currentTagBefore, ) = disputeModule.disputes(disputeId); - uint256 disputerBalBefore = currency.balanceOf(disputeInitiator); + uint256 disputerBalBefore = address(disputeInitiator).balance; // settle the assertion bytes32 assertionId = arbitrationPolicyUMA.disputeIdToAssertionId(disputeId); IOOV3(newOOV3).settleAssertion(assertionId); - uint256 disputerBalAfter = currency.balanceOf(disputeInitiator); + uint256 disputerBalAfter = address(disputeInitiator).balance; (, , , , , , bytes32 currentTagAfter, ) = disputeModule.disputes(disputeId); @@ -490,15 +490,15 @@ contract ArbitrationPolicyUMATest is BaseTest { (, , , , , , bytes32 currentTagBefore, ) = disputeModule.disputes(disputeId); - uint256 disputerBalBefore = currency.balanceOf(disputeInitiator); - uint256 callerBalBefore = currency.balanceOf(caller); + uint256 disputerBalBefore = address(disputeInitiator).balance; + uint256 callerBalBefore = address(caller).balance; // settle the assertion bytes32 assertionId = arbitrationPolicyUMA.disputeIdToAssertionId(disputeId); IOOV3(newOOV3).settleAssertion(assertionId); - uint256 disputerBalAfter = currency.balanceOf(disputeInitiator); - uint256 callerBalAfter = currency.balanceOf(caller); + uint256 disputerBalAfter = address(disputeInitiator).balance; + uint256 callerBalAfter = address(caller).balance; (, , , , , , bytes32 currentTagAfter, ) = disputeModule.disputes(disputeId); assertEq(currentTagBefore, bytes32("IN_DISPUTE")); @@ -796,15 +796,15 @@ contract ArbitrationPolicyUMATest is BaseTest { (, , , , , , bytes32 currentTagBefore, ) = disputeModule.disputes(disputeId); - uint256 disputeInitiatorBalBefore = currency.balanceOf(disputeInitiator); - uint256 defenderIpIdOwnerBalBefore = currency.balanceOf(randomIpId); + uint256 disputeInitiatorBalBefore = address(disputeInitiator).balance; + uint256 defenderIpIdOwnerBalBefore = address(randomIpId).balance; oov3.settleAssertion(assertionId); (, , , , , , bytes32 currentTagAfter, ) = disputeModule.disputes(disputeId); - uint256 disputeInitiatorBalAfter = currency.balanceOf(disputeInitiator); - uint256 defenderIpIdOwnerBalAfter = currency.balanceOf(randomIpId); + uint256 disputeInitiatorBalAfter = address(disputeInitiator).balance; + uint256 defenderIpIdOwnerBalAfter = address(randomIpId).balance; uint256 oracleFee = (oov3.burnedBondPercentage() * assertion.bond) / 1e18; uint256 bondRecipientAmount = assertion.bond * 2 - oracleFee; @@ -813,6 +813,8 @@ contract ArbitrationPolicyUMATest is BaseTest { assertEq(currentTagAfter, bytes32("IMPROPER_REGISTRATION")); assertEq(disputeInitiatorBalAfter - disputeInitiatorBalBefore, bondRecipientAmount); assertEq(defenderIpIdOwnerBalAfter - defenderIpIdOwnerBalBefore, 0); + assertEq(address(arbitrationPolicyUMA).balance, 0); + assertEq(IERC20(wip).balanceOf(address(arbitrationPolicyUMA)), 0); } function test_ArbitrationPolicyUMA_disputeAssertion_WithBondAndIpNotTagged() public { @@ -861,15 +863,15 @@ contract ArbitrationPolicyUMATest is BaseTest { (, , , , , , bytes32 currentTagBefore, ) = disputeModule.disputes(disputeId); - uint256 disputeInitiatorBalBefore = currency.balanceOf(disputeInitiator); - uint256 defenderIpIdOwnerBalBefore = currency.balanceOf(randomIpId); + uint256 disputeInitiatorBalBefore = address(disputeInitiator).balance; + uint256 defenderIpIdOwnerBalBefore = address(randomIpId).balance; oov3.settleAssertion(assertionId); (, , , , , , bytes32 currentTagAfter, ) = disputeModule.disputes(disputeId); - uint256 disputeInitiatorBalAfter = currency.balanceOf(disputeInitiator); - uint256 defenderIpIdOwnerBalAfter = currency.balanceOf(randomIpId); + uint256 disputeInitiatorBalAfter = address(disputeInitiator).balance; + uint256 defenderIpIdOwnerBalAfter = address(randomIpId).balance; uint256 oracleFee = (oov3.burnedBondPercentage() * assertion.bond) / 1e18; uint256 bondRecipientAmount = assertion.bond * 2 - oracleFee; From 395d70943537c5af4af46d00dcd9d194ba606799 Mon Sep 17 00:00:00 2001 From: Spablob Date: Fri, 13 Jun 2025 18:50:17 +0100 Subject: [PATCH 2/2] fix lint --- contracts/interfaces/IWIP.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/interfaces/IWIP.sol b/contracts/interfaces/IWIP.sol index 21a5bf9a..5489a6e3 100644 --- a/contracts/interfaces/IWIP.sol +++ b/contracts/interfaces/IWIP.sol @@ -4,4 +4,4 @@ pragma solidity 0.8.26; /// @title IWIP Interface interface IWIP { function withdraw(uint256 amount) external; -} \ No newline at end of file +}