code-423n4 / 2023-08-pooltogether-mitigation-findings

0 stars 0 forks source link

M-02 - Malicious users can set their hooks to contracts that will always revert, causing Claimers to get their tx to claim the user's prizes to be reverted #69

Open code423n4 opened 1 year ago

code423n4 commented 1 year ago

Lines of code

https://github.com/GenerationSoftware/pt-v5-vault/blob/main/src/Vault.sol#L1318-L1357

Vulnerability details

Title

M-02 - Malicious users can set their hooks to contracts that will always revert, causing Claimers to get their tx to claim the user's prizes to be reverted

Original Issue

M-02 - Unintended or Malicious Use of Prize Winners' Hooks

Details

The previous implementation claimed the prizes for all the winners in one single transaction, each winner was allowed to set arbitrary hooks that would cause the Vault contract to perform arbitrary calls to the address of the user's hooks. As the original issue mentions, some consequences of allowing executions to arbitrary addresses are unauthorized side transactions with gas paid unbeknownst to the claimer, reentrant calls, or denial-of-service attacks on claiming transactions.

Mitigation

The mitigation implements a limit of gas that can be spent on each hook's call, and now the hook's call is made using a try-catch block.

The issue about causing DoS on other users is still present, when using a Claimer to claim a user's prizes in batches, if at least one of the hook's calls reverts, the whole tx claim the user's prizes will be reverted.

  function claimPrize(
    ...
  ) external onlyClaimer returns (uint256) {
    ...

    if (hooks.useBeforeClaimPrize) {
      try
        hooks.implementation.beforeClaimPrize{ gas: HOOK_GAS }(
          _winner,
          _tier,
          _prizeIndex,
          _fee,
          _feeRecipient
        )
      returns (address result) {
        recipient = result;
      } catch (bytes memory reason) {
        revert BeforeClaimPrizeFailed(reason);
      }
    } else {
      recipient = _winner;
    }

    ...

    if (hooks.useAfterClaimPrize) {
      try
        hooks.implementation.afterClaimPrize{ gas: HOOK_GAS }(
          _winner,
          _tier,
          _prizeIndex,
          prizeTotal,
          recipient
        )
      {} catch (bytes memory reason) {
        revert AfterClaimPrizeFailed(reason);
      }
    }

    return prizeTotal;
  }

Conclusion of the Mitigation and Proof of Concept of the New Bug

The mitigation solves most of the problems described in the original issue, but the problem of causing DoS to claim other user's prizes is still present.

  function claimPrize(
    ...
  ) external onlyClaimer returns (uint256) {
    ...

    if (hooks.useBeforeClaimPrize) {
      try
        hooks.implementation.beforeClaimPrize{ gas: HOOK_GAS }(
          _winner,
          _tier,
          _prizeIndex,
          _fee,
          _feeRecipient
        )
      returns (address result) {
        recipient = result;
      } catch (bytes memory reason) {
        //@audit-issue => Malicious hooks can force a revert which will cause the whole tx to be reverted
        revert BeforeClaimPrizeFailed(reason);
      }
    } else {
      recipient = _winner;
    }

    ...

    if (hooks.useAfterClaimPrize) {
      try
        hooks.implementation.afterClaimPrize{ gas: HOOK_GAS }(
          _winner,
          _tier,
          _prizeIndex,
          prizeTotal,
          recipient
        )
      {} catch (bytes memory reason) {
        //@audit-issue => Malicious hooks can force a revert which will cause the whole tx to be reverted
        revert AfterClaimPrizeFailed(reason);
      }
    }

    return prizeTotal;
  }

Flow to claim prizes when a Claimer is enabled:

Coded PoC

contract MaliciousHook {

function beforeClaimPrize( address winner, uint8 tier, uint32 prizeIndex ) external returns (address) { revert("Forcing to revert"); }

}


- Run the PoC, this is the expected result:
> forge test --match-test testClaimPrizeMaliciousHookPoC

Running 1 test for test/unit/Vault/Vault.t.sol:VaultTest [FAIL. Reason: BeforeClaimPrizeFailed(0x)] testClaimPrizeMaliciousHookPoC() (gas: 140886) Test result: FAILED. 0 passed; 1 failed; 0 skipped; finished in 7.24ms Ran 1 test suites: 0 tests passed, 1 failed, 0 skipped (1 total tests)

Failing tests: Encountered 1 failing test in test/unit/Vault/Vault.t.sol:VaultTest [FAIL. Reason: BeforeClaimPrizeFailed(0x)] testClaimPrizeMaliciousHookPoC() (gas: 140886)

Encountered a total of 1 failing tests, 0 tests succeeded


## Impact
Claimers can get their TX reverted if a malicious winner sets his hook to a malicious contract that will always revert, which will cause the whole tx to claim the user's prizes to be reverted.

## Recommended Mitigation Steps
- The mitigation for this issue would be to instead of reverting the tx, just emit an event and return a 0 (this indicates that 0 prizes were claimed for that winner), In this way, the tx to claim prizes will continue its execution and will be able to claim the prizes for the rest of winners.

```solidity
  function claimPrize(
    ...
  ) external onlyClaimer returns (uint256) {
    ...

    if (hooks.useBeforeClaimPrize) {
      try
        hooks.implementation.beforeClaimPrize{ gas: HOOK_GAS }(
          _winner,
          _tier,
          _prizeIndex,
          _fee,
          _feeRecipient
        )
      returns (address result) {
        recipient = result;
      } catch (bytes memory reason) {
-       revert BeforeClaimPrizeFailed(reason);
+       emit BeforeHookExecutionFailed(_winner,reason);
+       return 0;
      }
    } else {
      recipient = _winner;
    }

    ...

    if (hooks.useAfterClaimPrize) {
      try
        hooks.implementation.afterClaimPrize{ gas: HOOK_GAS }(
          _winner,
          _tier,
          _prizeIndex,
          prizeTotal,
          recipient
        )
      {} catch (bytes memory reason) {
-       revert AfterClaimPrizeFailed(reason);
+       emit AfterHookExecutionFailed(_winner,reason);
        //@audit-info => In case the prizes were claimed, so fees are charged!
+       uint claimed = prizeTotal ! 0 = prizeTotal : 0;
+       return claimed;
      }
    }

    return prizeTotal;
  }

Assessed type

Context

thebrittfactor commented 1 year ago

For transparency, warden requested to have labels removed and MR-New added.

thebrittfactor commented 1 year ago

Medium risk label added per warden request.

thebrittfactor commented 1 year ago

Labels updated per dev clarification of needs.

djb15 commented 1 year ago

This is the same as #24. This is not a new issue, the original issue is just unmitigated.

c4-sponsor commented 1 year ago

asselstine marked the issue as sponsor confirmed

asselstine commented 1 year ago

I believe we've mitigated this, as the claimer swallows the error and emits an event

asselstine commented 1 year ago

No; actually scratch that

asselstine commented 1 year ago

Fixed here https://github.com/GenerationSoftware/pt-v5-claimer/pull/13

stalinMacias commented 1 year ago

I think I agree with @djb15 's comments, the result of the new implementation was the same as the original.

c4-judge commented 1 year ago

Picodes marked the issue as unmitigated

c4-judge commented 1 year ago

Picodes marked the issue as satisfactory

thebrittfactor commented 12 months ago

Missing label added to correctly display.