PoolTogether micro contest #1 - hickuphh3's results

A protocol for no loss prize savings on Ethereum

General Information

Platform: Code4rena

Start Date: 29/07/2021

Pot Size: $20,000 USDC

Total HM: 8

Participants: 12

Period: 3 days

Judge: LSDan

Total Solo HM: 2

Id: 24

League: ETH

PoolTogether

Findings Distribution

Researcher Performance

Rank: 3/12

Findings: 5

Award: $3,249.19

🌟 Selected for report: 8

🚀 Solo Findings: 0

Findings Information

🌟 Selected for report: cmichel

Also found by: hickuphh3, jonah1005, pauliax

Labels

bug
duplicate
G (Gas Optimization)
SwappableYieldSource
sponsor confirmed

Awards

565.8571 USDC - $565.86

External Links

Handle

hickuphh3

Vulnerability details

Impact

_depositToken.safeTransferFrom(address(this), msg.sender, redeemableBalance); can be optimized to use the safeTransfer method instead, since it bypasses allowance checks and effects.

Update to _depositToken.safeTransfer(msg.sender, redeemableBalance);

#1 - 0xean

2021-08-24T16:37:52Z

duplicate of #61 , which clearly states the severity.

Findings Information

🌟 Selected for report: hickuphh3

Also found by: 0xRajeev

Labels

bug
3 (High Risk)
SwappableYieldSource
sponsor confirmed

Awards

1397.178 USDC - $1,397.18

External Links

Handle

hickuphh3

Vulnerability details

Impact

transferFunds() will transfer funds from a specified yield source _yieldSource to the current yield source set in the contract _currentYieldSource. However, it fails to check that the deposit tokens are the same. If the specified yield source's assets are of a higher valuation, then a malicious owner or asset manager will be able to exploit and pocket the difference.

Proof of Concept

Assumptions:

  • _yieldSource has a deposit token of WETH (18 decimals)
  • _currentYieldSource has a deposit token of DAI (18 decimals)
  • 1 WETH > 1 DAI (definitely true, I'd be really sad otherwise)

Attacker does the following:

  1. Deposit 100 DAI into the swappable yield source contract
  2. Call transferFunds(_yieldSource, 100 * 1e18)
    • _requireDifferentYieldSource() passes
    • _transferFunds(_yieldSource, 100 * 1e18) is called
      • _yieldSource.redeemToken(_amount); → This will transfer 100 WETH out of the _yieldSource into the contract
      • uint256 currentBalance = IERC20Upgradeable(_yieldSource.depositToken()).balanceOf(address(this)); → This will equate to ≥ 100 WETH.
      • require(_amount <= currentBalance, "SwappableYieldSource/transfer-amount-different"); is true since both are 100 * 1e18
      • _currentYieldSource.supplyTokenTo(currentBalance, address(this)); → This supplies the transferred 100 DAI from step 1 to the current yield source
    • We now have 100 WETH in the swappable yield source contract
  3. Call transferERC20(WETH, attackerAddress, 100 * 1e18) to withdraw 100 WETH out of the contract to the attacker's desired address.

_requireDifferentYieldSource() should also verify that the yield sources' deposit token addresses are the same.

function _requireDifferentYieldSource(IYieldSource _yieldSource) internal view {
    require(address(_yieldSource) != address(yieldSource), "SwappableYieldSource/same-yield-source");
		require(_newYieldSource.depositToken() == yieldSource.depositToken(), "SwappableYieldSource/different-deposit-token");
}

#0 - PierrickGT

2021-08-11T22:41:54Z

This exploit was indeed possible when we had the transferFunds function but now that we have removed it and funds can only be moved by swapYieldSource(), this exploit is no longer possible since we check for the same depositToken in _setYieldSource().

https://github.com/pooltogether/swappable-yield-source/pull/4

#1 - 0xean

2021-08-24T16:45:56Z

Upgrading to 3 considering the potential for loss of funds

Findings Information

🌟 Selected for report: tensors

Also found by: GalloDaSballo, cmichel, hickuphh3

Labels

bug
duplicate
0 (Non-critical)
SwappableYieldSource

Awards

169.7571 USDC - $169.76

External Links

Handle

hickuphh3

Vulnerability details

Impact

While not necessary, it would be good practice to zero out the allowance given to the current yield source when swapping out for a new one.

function _setYieldSource(IYieldSource _newYieldSource) internal {
	...
	IERC20Upgradeable(yieldSource.depositToken()).safeApprove(address(yieldSource), 0);
	yieldSource = _newYieldSource;
	IERC20Upgradeable(_newYieldSource.depositToken()).safeApprove(address(_newYieldSource), type(uint256).max);

	emit SwappableYieldSourceSet(_newYieldSource);
}

#0 - PierrickGT

2021-08-06T16:44:02Z

Findings Information

🌟 Selected for report: gpersoon

Also found by: hickuphh3, pauliax

Labels

bug
duplicate
0 (Non-critical)
SwappableYieldSource

Awards

83.8307 USDC - $83.83

External Links

Handle

hickuphh3

Vulnerability details

Impact

The logic of _requireYieldSource() is fine, but the variable isInvalidYieldSource within the function does the opposite of its intended purpose. The lines of interest are below.

isInvalidYieldSource = depositTokenAddress != address(0);
require(isInvalidYieldSource, "SwappableYieldSource/invalid-yield-source");

The intention is to check that depositTokenAddress is non-null, which makes the yield source valid (not invalid like the variable name suggests).

Change isInvalidYieldSource to isValidYieldSource.

#0 - PierrickGT

2021-08-06T15:51:51Z

Findings Information

🌟 Selected for report: hickuphh3

Also found by: shw

Labels

bug
1 (Low Risk)
SwappableYieldSource
sponsor confirmed

Awards

139.7178 USDC - $139.72

External Links

Handle

hickuphh3

Vulnerability details

Impact

The FundsTransferred() event in _transferFunds() will report a smaller amount than expected if currentBalance > _amount.

This would affect applications utilizing event logs like subgraphs.

Update the event emission to emit FundsTransferred(_yieldSource, currentBalance);

#0 - PierrickGT

2021-08-11T20:24:23Z

This issue has been fixed in the following PR: https://github.com/pooltogether/swappable-yield-source/pull/4

Findings Information

🌟 Selected for report: hickuphh3

Labels

bug
1 (Low Risk)
SwappableYieldSource
sponsor acknowledged

Awards

310.484 USDC - $310.48

External Links

Handle

hickuphh3

Vulnerability details

Impact

setYieldSource() changes the current yield source to a new yield source. It has similar functionality as swapYieldSource(), except that it doesn't transfer deposited funds from the current to the new one. However, it fails to check that it does not have any remaining deposited funds in the current yield source before the transfer.

It is highly recommended for this check to be in place so that funds aren't forgotten / unintentionally lost.

Add a require check

require(yieldSource.balanceOfToken(address(this)); == 0, "SwappableYieldSource/existing-funds-in-current-yield-source") **before calling _setYieldSource()

#0 - PierrickGT

2021-08-11T20:28:55Z

We have decided to remove setYieldSource and transferFunds functions. When using swapYieldSource to change of yield source, all funds from the old yield source will be moved to the new yield source in one transaction. https://github.com/pooltogether/swappable-yield-source/pull/4

Findings Information

🌟 Selected for report: hickuphh3

Also found by: 0xRajeev, GalloDaSballo, cmichel, pauliax

Labels

bug
G (Gas Optimization)
SwappableYieldSource
sponsor confirmed

Awards

26.3992 USDC - $26.40

External Links

Handle

hickuphh3

Vulnerability details

Impact

Assuming that depositToken of a yield source doesn't change, it would make sense to save its value as a storage variable in the contract as well, so that an external call to yieldSource to retrieve it can be avoided whenever it is needed.

Define address public override depositToken; or IERC20Upgradeable public depositToken; which gets initialized in the initialize() function. The nice thing is that it also doesn't need to be updated when swapping sources because a requirement is that the new yield source must have the same deposit token.

As an optimization, since the _requireYieldSource() function already retrieves the depositToken address, it can return it so that its value need not be externally retrieved again in the initialize() function.

The depositToken() function can be removed if the former suggestion is implemented (ie. address public override depositToken).

Then, yieldSource.depositToken() can be replaced with depositToken where applicable (with appropriate casting).

A part of the former implementation is provided below.

address public override depositToken;

function initialize(...) {
	address depositTokenAddress = _requireYieldSource(_yieldSource);
	yieldSource = _yieldSource;
  depositToken = depositTokenAddress;
	...
	IERC20Upgradeable(depositTokenAddress).safeApprove(address(_yieldSource), type(uint256).max);
}

function _requireYieldSource(IYieldSource _yieldSource) internal view returns (address depositTokenAddress) {
	...
	(depositTokenAddress) = abi.decode(depositTokenAddressData, (address));
}

// function depositToken() can be removed
// yieldSource.depositToken() can be replaced with depositToken in other functions
// Example: _setYieldSource
function _setYieldSource(IYieldSource _newYieldSource) internal {
	_requireDifferentYieldSource(_newYieldSource);
	// Commented out check below should be shifted to inside _requireDifferentYieldSource()
	// Optimization: it can also return depositToken to avoid another SLOAD
	// similar to _requireYieldSource() above
	// require(_newYieldSource.depositToken() == depositToken, "SwappableYieldSource/different-deposit-token");

  yieldSource = _newYieldSource;
  IERC20Upgradeable(depositToken).safeApprove(address(_newYieldSource), type(uint256).max);

  emit SwappableYieldSourceSet(_newYieldSource);
}

Findings Information

🌟 Selected for report: maplesyrup

Also found by: 0xRajeev, hickuphh3, hrkrshnn

Labels

bug
duplicate
G (Gas Optimization)
mStableYieldSource

Awards

26.3992 USDC - $26.40

External Links

Handle

hickuphh3

Vulnerability details

Impact

approveMax() and depositToken() are not called internally in the contract. Making these methods external would help reduce execution gas costs.

#0 - PierrickGT

2021-08-06T16:14:43Z

Findings Information

🌟 Selected for report: hickuphh3

Also found by: 0xRajeev, hrkrshnn, shw

Labels

bug
G (Gas Optimization)
mStableYieldSource
sponsor confirmed

Awards

36.6656 USDC - $36.67

External Links

Handle

hickuphh3

Vulnerability details

Impact

The immutable mAsset is assigned to the immutable savings contract. Hence, we can avoid an external function call to the savings contract in the approveMax function by replacing it with mAsset.

function approveMax() public {
	mAsset.safeApprove(address(savings), type(uint256).max);

	emit ApprovedMax(msg.sender);
}

Findings Information

🌟 Selected for report: hickuphh3

Labels

bug
G (Gas Optimization)
mStableYieldSource
SwappableYieldSource
sponsor disputed

Awards

201.183 USDC - $201.18

External Links

Handle

hickuphh3

Vulnerability details

Impact

The number of solc runs used for contract compilation is 200. This number can be bumped significantly to produce more gas efficient code (max value of 2**32-1).

More information can be found in the solidity docs.

In hardhat.config.ts, increase solc runs significantly. Contract sizes and thus deployment costs will increase, but functions will cost less gas to execute.

#0 - PierrickGT

2021-08-11T22:29:14Z

200 is the default value, not sure what would be the real gain of bumping it and since no value is proposed by the warden, this recommended mitigation isn't concrete and applicable.

#1 - 0xean

2021-08-24T17:31:45Z

Disagree, warden has provided exact instructions on how to increase the value.

#2 - asselstine

2021-08-30T19:18:20Z

This issue is really hand-wavy. Based on the warden's logic, this "issue" applies to any contract that isn't compiled with runs set to 2**32-1, which is absurd.

The number of runs is a balance between contract size and runtime efficiency. The warden has done zero analysis in this respect, and simply hand-waved "do more".

This isn't specific enough to be useful. Saying "do 2**32-1 runs" isn't helpful for us, and likely inaccurate.

Findings Information

🌟 Selected for report: hickuphh3

Labels

bug
G (Gas Optimization)
mStableYieldSource
sponsor disputed

Awards

201.183 USDC - $201.18

External Links

Handle

hickuphh3

Vulnerability details

Impact

Since mAsset is immutable and is initialized to a valid token address, a redundant extcodesize check can be avoided, just like how UniswapV3 does it.

function mAssetBalance() private view returns (uint256) {
	(bool success, bytes memory data) =
		mAsset.staticcall(abi.encodeWithSelector(IERC20.balanceOf.selector, address(this)));
		require(success && data.length >= 32);
		return abi.decode(data, (uint256));
}

// Replace lines mAsset.balanceOf(address(this)) with mAssetBalance()
function redeemToken(uint256 mAssetAmount)
  external
  override
  nonReentrant
  returns (uint256 mAssetsActual)
{   
	uint256 mAssetBalanceBefore = mAssetBalance();

  uint256 creditsBurned = savings.redeemUnderlying(mAssetAmount);

  imBalances[msg.sender] -= creditsBurned;
  uint256 mAssetBalanceAfter = mAssetBalance();
  mAssetsActual = mAssetBalanceAfter - mAssetBalanceBefore;

  mAsset.safeTransfer(msg.sender, mAssetsActual);

  emit Redeemed(msg.sender, mAssetAmount, mAssetsActual);
}

#0 - PierrickGT

2021-08-16T15:18:39Z

This optimization seems overkill. How much gas would we save by implementing this kind of function? It's a trade of between gas consumption and the ease of maintenance of our code, so I prefer to keep a code that will be easier to maintain in the future than introduce some complexity that may not bring much in term of gas saving.

Findings Information

🌟 Selected for report: hickuphh3

Also found by: hrkrshnn

Labels

bug
G (Gas Optimization)
SwappableYieldSource
sponsor disputed

Awards

90.5323 USDC - $90.53

External Links

Handle

hickuphh3

Vulnerability details

Impact

Revert messages tend to be long, like SwappableYieldSource/yield-source-token-transfer-not-allowed and SwappableYieldSource/yieldSource-not-zero-address. Reducing these messages to < 32 bytes will help save on deployment gas.

Consider shortening the messages. One suggestion is to use error codes, like PT_001, PT_002 etc (similar to hardhat error codes). Alternatively, find ways to abbreviate error messages such that they are suggestive enough to make debugging easy, but are short as well.

#0 - PierrickGT

2021-08-11T22:23:05Z

We prefer to use readable error message instead of error codes, gas cost in deployments being less important than gas cost to call functions in the contract.

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