Skip to content

Commit ad3006d

Browse files
unknownunknown1jaybuidl
authored andcommitted
fix: safeTransfer check M-03
1 parent b8af085 commit ad3006d

File tree

3 files changed

+74
-4
lines changed

3 files changed

+74
-4
lines changed

contracts/src/arbitration/KlerosCore.sol

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -627,7 +627,7 @@ contract KlerosCore is IArbitratorV2, Initializable, UUPSProxiable {
627627
function transferBySortitionModule(address _account, uint256 _amount) external {
628628
if (msg.sender != address(sortitionModule)) revert SortitionModuleOnly();
629629
// Note eligibility is checked in SortitionModule.
630-
pinakion.safeTransfer(_account, _amount);
630+
if (!pinakion.safeTransfer(_account, _amount)) revert TransferFailed();
631631
}
632632

633633
/// @inheritdoc IArbitratorV2

contracts/src/libraries/SafeERC20.sol

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,12 @@ import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
1515
/// To use this library you can add a `using SafeERC20 for IERC20;` statement to your contract,
1616
/// which allows you to call the safe operations as `token.safeTransfer(...)`, etc.
1717
library SafeERC20 {
18+
/// @notice Emits when safeTransfer fails.
19+
/// @param _token Token to transfer.
20+
/// @param _to Recipient address.
21+
/// @param _value Amount transferred.
22+
event SafeTransferFailed(IERC20 _token, address _to, uint256 _value);
23+
1824
/// @notice Increases the allowance granted to `spender` by the caller.
1925
/// @param _token Token to transfer.
2026
/// @param _spender The address which will spend the funds.
@@ -31,7 +37,9 @@ library SafeERC20 {
3137
/// @return Whether transfer succeeded or not.
3238
function safeTransfer(IERC20 _token, address _to, uint256 _value) internal returns (bool) {
3339
(bool success, bytes memory data) = address(_token).call(abi.encodeCall(IERC20.transfer, (_to, _value)));
34-
return (success && (data.length == 0 || abi.decode(data, (bool))));
40+
bool ok = success && (data.length == 0 || abi.decode(data, (bool)));
41+
if (!ok) emit SafeTransferFailed(_token, _to, _value);
42+
return ok;
3543
}
3644

3745
/// @notice Calls transferFrom() without reverting.

contracts/test/foundry/KlerosCore_Execution.t.sol

Lines changed: 64 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
pragma solidity ^0.8.24;
33

44
import {KlerosCore_TestBase} from "./KlerosCore_TestBase.sol";
5-
import {KlerosCore} from "../../src/arbitration/KlerosCore.sol";
5+
import {KlerosCore, SafeERC20} from "../../src/arbitration/KlerosCore.sol";
66
import {SortitionModule} from "../../src/arbitration/SortitionModule.sol";
77
import {DisputeKitClassicBase} from "../../src/arbitration/dispute-kits/DisputeKitClassicBase.sol";
88
import {IArbitratorV2, IArbitrableV2} from "../../src/arbitration/KlerosCore.sol";
@@ -487,6 +487,15 @@ contract KlerosCore_ExecutionTest is KlerosCore_TestBase {
487487
assertEq(pinakion.balanceOf(address(core)), 1000, "Wrong token balance of the core");
488488
assertEq(sortitionModule.totalStaked(), 1000, "Wrong totalStaked before withdrawal");
489489

490+
vm.prank(address(core));
491+
pinakion.transfer(staker2, 1000); // Manually send the balance to trigger the revert
492+
493+
vm.expectRevert(KlerosCore.TransferFailed.selector);
494+
sortitionModule.withdrawLeftoverPNK(staker1);
495+
496+
vm.prank(staker2);
497+
pinakion.transfer(address(core), 1000); // Transfer the tokens back to execute withdrawal
498+
490499
vm.expectEmit(true, true, true, true);
491500
emit SortitionModule.LeftoverPNKWithdrawn(staker1, 1000);
492501
sortitionModule.withdrawLeftoverPNK(staker1);
@@ -534,7 +543,7 @@ contract KlerosCore_ExecutionTest is KlerosCore_TestBase {
534543
voteIDs[1] = 1;
535544
voteIDs[2] = 2;
536545
vm.prank(staker1);
537-
disputeKit.castVote(disputeID, voteIDs, 1, 0, "XYZ"); // Staker1 only got 1 vote because of low stake
546+
disputeKit.castVote(disputeID, voteIDs, 1, 0, "XYZ");
538547

539548
core.passPeriod(disputeID); // Appeal
540549

@@ -554,6 +563,59 @@ contract KlerosCore_ExecutionTest is KlerosCore_TestBase {
554563
assertEq(feeToken.balanceOf(disputer), 0.82 ether, "Wrong fee token balance of disputer");
555564
}
556565

566+
function test_execute_feeToken_failedTransfer() public {
567+
uint256 disputeID = 0;
568+
569+
feeToken.transfer(disputer, 1 ether);
570+
vm.prank(disputer);
571+
feeToken.approve(address(arbitrable), 1 ether);
572+
573+
vm.prank(owner);
574+
core.changeAcceptedFeeTokens(feeToken, true);
575+
vm.prank(owner);
576+
core.changeCurrencyRates(feeToken, 500, 3);
577+
578+
vm.prank(disputer);
579+
arbitrable.createDispute("Action", 0.18 ether);
580+
581+
vm.prank(staker1);
582+
core.setStake(GENERAL_COURT, 20000);
583+
vm.warp(block.timestamp + minStakingTime);
584+
sortitionModule.passPhase(); // Generating
585+
vm.warp(block.timestamp + rngLookahead);
586+
sortitionModule.passPhase(); // Drawing phase
587+
588+
core.draw(disputeID, DEFAULT_NB_OF_JURORS);
589+
590+
vm.warp(block.timestamp + timesPerPeriod[0]);
591+
core.passPeriod(disputeID); // Vote
592+
593+
uint256[] memory voteIDs = new uint256[](3);
594+
voteIDs[0] = 0;
595+
voteIDs[1] = 1;
596+
voteIDs[2] = 2;
597+
vm.prank(staker1);
598+
disputeKit.castVote(disputeID, voteIDs, 1, 0, "XYZ");
599+
600+
core.passPeriod(disputeID); // Appeal
601+
602+
vm.warp(block.timestamp + timesPerPeriod[3]);
603+
core.passPeriod(disputeID); // Execution
604+
605+
vm.prank(address(core));
606+
feeToken.transfer(disputer, 0.18 ether); // Manually send all balance to make rewards fail
607+
assertEq(feeToken.balanceOf(staker1), 0, "Wrong fee token balance of staker1");
608+
assertEq(feeToken.balanceOf(disputer), 1 ether, "Wrong fee token balance of disputer");
609+
610+
vm.expectEmit(true, true, true, true);
611+
emit SafeERC20.SafeTransferFailed(feeToken, staker1, 0.06 ether); // One failed iteration has 0.06 eth
612+
core.execute(disputeID, 0, 6);
613+
614+
KlerosCore.Round memory round = core.getRoundInfo(disputeID, 0);
615+
assertEq(round.repartitions, 6, "Wrong repartitions");
616+
assertEq(feeToken.balanceOf(staker1), 0, "Staker1 still has no balance");
617+
}
618+
557619
function test_execute_NoCoherence_feeToken() public {
558620
uint256 disputeID = 0;
559621

0 commit comments

Comments
 (0)