PoolTogether V5: Part Deux - 0xStalin's results

A protocol for no-loss prize savings.

General Information

Platform: Code4rena

Start Date: 02/08/2023

Pot Size: $42,000 USDC

Total HM: 13

Participants: 45

Period: 5 days

Judge: hickuphh3

Total Solo HM: 5

Id: 271

League: ETH

PoolTogether

Findings Distribution

Researcher Performance

Rank: 7/45

Findings: 2

Award: $1,121.02

🌟 Selected for report: 1

🚀 Solo Findings: 0

Findings Information

Awards

89.6296 USDC - $89.63

Labels

bug
3 (High Risk)
satisfactory
duplicate-82

External Links

Lines of code

https://github.com/GenerationSoftware/pt-v5-draw-auction/blob/f1c6d14a1772d6609de1870f8713fb79977d51c1/src/RngRelayAuction.sol#L125-L176

Vulnerability details

Impact

Proof of Concept

  • The RngRelayAuction::rngComplete() is on charge of closing the draw in the PrizePool as well as withdrawing the reserve and send it to the rewardRecipient.

  • All the data that should be used to execute the rngComplete() function should be computed by the relayer, this function forwards the computed data from the relayer onto the PrizePool.

  • The problem is that this function has no access control, there is any check to validate if the caller is the relayer, which means that anybody can call this function and set the parameters at their convenience, which will lead to close draws using values that could benefit the caller and all the rest of users will be impacted because the randomNumber is now manipulated at the convenience of whoever calls first the rngComplete().

Tools Used

Manual Audit

function rngComplete(
  ...
) external returns (bytes32) {
+ require(msg.sender == rngAuctionRelayer, "Caller is not the authorized relayer");

  ...
  ...
  ...
}

Assessed type

Invalid Validation

#0 - c4-pre-sort

2023-08-08T01:37:44Z

raymondfam marked the issue as low quality report

#1 - raymondfam

2023-08-08T01:38:09Z

The function deals with random numbers that had already been generated.

#2 - c4-pre-sort

2023-08-08T02:29:48Z

raymondfam marked the issue as remove high or low quality report

#3 - c4-pre-sort

2023-08-08T02:30:03Z

raymondfam marked the issue as duplicate of #82

#4 - c4-judge

2023-08-14T02:44:55Z

HickupHH3 marked the issue as satisfactory

Findings Information

🌟 Selected for report: 0xStalin

Also found by: MohammedRizwan

Labels

bug
2 (Med Risk)
high quality report
primary issue
selected for report
sponsor confirmed
M-07

Awards

1031.3943 USDC - $1,031.39

External Links

Lines of code

https://github.com/GenerationSoftware/pt-v5-cgda-liquidator/blob/7f95bcacd4a566c2becb98d55c1886cadbaa8897/src/LiquidationPairFactory.sol#L65-L108

Vulnerability details

Impact

  • Users and Bots can be tricked into operating with LiquidationPairs that were deployed using the LiquidationPairFactory but they configured the LiquidationSource as a fake malicious contract that will allow the Liquidator's creator to steal all the POOL tokens that were meant to be used to liquidate the Vault's Yield

Proof of Concept

Liquidation Pair Protocol's Documentation
  • So, if a LiquidationPair is created by the LiquidationPairFactory may allow malicious users to trick users who want to liquidate the Vault's Yield to operate with a LiquidationPair who'll end up stealing their POOL tokens (tokenIn) when swapping tokens.

  • Practical Example of how the LiquidationPair would steal the user's assets, (Keep in mind that source is not the address of a Vault, but an arbitrary contract)

    • The fake source contract would look like this:
contract FakeSource {

  function targetOf(address _token) external view returns (address) {
    return <ContractCreatorAddress>;
  }

  function liquidate(
    address _account,
    address _tokenIn,
    uint256 _amountIn,
    address _tokenOut,
    uint256 _amountOut
  ) public virtual override returns (bool) {
    return true;
  }

}
  1. Calling LiquidationPair.target() will return source.targetOf(tokenIn), and the source contract can return any address when the targetOf() is called, so, let's say that will return the address of its creator.
  • so, LiquidationPair.target() will return the address of its creator, instead of returning the expected address of the PrizePool contract
  1. Calling LiquidationPair::swapExactAmount() will do some computations prior to call source.liquidate(), and the source.liquidate() can just return true not to cause the tx to be reverted.
  • So, LiquidationPair::swapExactAmount() will basically do nothing.

  • With the above points in mind, let's see what would be the result of swapping using the LiquidationRouter contract

function swapExactAmountOut(
  LiquidationPair _liquidationPair,
  address _receiver,
  uint256 _amountOut,
  uint256 _amountInMax
) external onlyTrustedLiquidationPair(_liquidationPair) returns (uint256) {

  //@audit-issue => The `tokenIn` will be transferred to the address of the LiquidationPair's creator instead of the PrizePool contract (Point 1) <====== Point 1 ======>
  IERC20(_liquidationPair.tokenIn()).safeTransferFrom(
    msg.sender,
    _liquidationPair.target(),
    _liquidationPair.computeExactAmountIn(_amountOut)
  );

  //@audit-issue => This call will basically do nothing, just return a true not to cause the tx to be reverted (Point 2)  <====== Point 2 ======>
  uint256 amountIn = _liquidationPair.swapExactAmountOut(_receiver, _amountOut, _amountInMax);

  emit SwappedExactAmountOut(_liquidationPair, _receiver, _amountOut, _amountInMax, amountIn);

  return amountIn;
}

Tools Used

Manual Audit

  • Use the deployedVaults mapping of the VaultFactory contract to validate if the inputted address of the _source parameter is a valid vault supported by the Protocol.
    • Additionally, it could be a good idea to set the _tokenIn and _tokenOut by pulling the values that are already set up in the vault.
function createPair(
  ILiquidationSource _source,
- address _tokenIn,
- address _tokenOut,
  uint32 _periodLength,
  uint32 _periodOffset,
  uint32 _targetFirstSaleTime,
  SD59x18 _decayConstant,
  uint112 _initialAmountIn,
  uint112 _initialAmountOut,
  uint256 _minimumAuctionAmount
) external returns (LiquidationPair) {

+ require(VaultFactory.deployedVaults(address(_source)) == true, "_source address is not a supported Vault");
+ address _prizePool = _source.prizePool();
+ address _tokenIn = _prizePool.prizeToken();
+ address _tokenOut = address(_source);
  
  LiquidationPair _liquidationPair = new LiquidationPair(
    _source,
    _tokenIn,
    _tokenOut,
    _periodLength,
    _periodOffset,
    _targetFirstSaleTime,
    _decayConstant,
    _initialAmountIn,
    _initialAmountOut,
    _minimumAuctionAmount
  );

  allPairs.push(_liquidationPair);
  deployedPairs[_liquidationPair] = true;

  emit PairCreated(
    _liquidationPair,
    _source,
    _tokenIn,
    _tokenOut,
    _periodLength,
    _periodOffset,
    _targetFirstSaleTime,
    _decayConstant,
    _initialAmountIn,
    _initialAmountOut,
    _minimumAuctionAmount
  );

  return _liquidationPair;
}

Assessed type

Invalid Validation

#0 - c4-pre-sort

2023-08-08T02:56:33Z

raymondfam marked the issue as duplicate of #52

#1 - c4-pre-sort

2023-08-08T05:59:06Z

raymondfam marked the issue as high quality report

#2 - c4-pre-sort

2023-08-10T00:00:14Z

raymondfam marked the issue as not a duplicate

#3 - c4-pre-sort

2023-08-10T00:00:31Z

raymondfam marked the issue as primary issue

#4 - asselstine

2023-08-10T16:37:25Z

This is an interesting report.

There are two ways we expect liquidations to occur:

  • EOAs that interact with the LiquidationRouter. (users, off-chain bots, etc)
  • Smart Contracts that interact with the LiquidationPair directly

Smart contracts that interact with the Pair can easily assert that they received the expected tokens; so a malicious LiquidationSource would not be a problem.

However, EOAs that interact with a Pair via the LiquidationRouter would be vulnerable to this kind of attack.

This makes me think that the fix is actually to alter the LiquidationRouter so that:

  1. In swapExactAmountOut the router makes itself the recipient of the tokens.
  2. The router asserts that it has received the expected amount of tokens
  3. The router transfers to the tokens to the caller.

It costs more gas, but this change ensures that anyone who calls the router would receive the expected tokens.

#5 - c4-sponsor

2023-08-10T16:37:31Z

asselstine marked the issue as sponsor confirmed

#6 - c4-judge

2023-08-14T06:41:28Z

HickupHH3 marked the issue as selected for report

AuditHub

A portfolio for auditors, a security profile for protocols, a hub for web3 security.

Built bymalatrax © 2024

Auditors

Browse

Contests

Browse

Get in touch

ContactTwitter