GithubHelp home page GithubHelp logo

2022-04-pooltogether-findings's Introduction

PoolTogether Aave v3 Contest

Unless otherwise discussed, this repo will be made public after contest completion, sponsor review, judging, and two-week issue mitigation window.


Contest findings are submitted to this repo

Typically most findings come in on the last day of the contest, so don't be alarmed at all if there's nothing here but crickets until the end of the contest.

As a sponsor, you have four critical tasks in the contest process:

  1. Handle duplicate issues.
  2. Weigh in on severity.
  3. Respond to issues.
  4. Share your mitigation of findings.

Let's walk through each of these.

High and Medium Risk Issues

Handle duplicates

Because the wardens are submitting issues without seeing each others' submissions, there will always be findings that are clear duplicates. Other findings may use different language that ultimately describes the same issue, but from different angles. Use your best judgment in identifying duplicates, and don't hesitate to reach out (in your private contest channel) to ask C4 for advice.

  1. For all issues labeled 3 (High Risk) or 2 (Medium Risk), determine the best and most thorough description of the finding among the set of duplicates. (At least a portion of the content of the most useful description will be used in the audit report.)
  2. Close the other duplicate issues and label them with duplicate
  3. Mention the primary issue # when closing the issue (using the format Duplicate of #issueNumber), so that duplicate issues get linked.

Weigh in on severity

Judges have the ultimate discretion in determining severity of issues, as well as whether/how issues are considered duplicates. However, sponsor input is a significant criteria.

For a detailed breakdown of severity criteria and how to estimate risk, please refer to the judging criteria in our documentation.

If you disagree with a finding's severity, leave the original severity label set by the warden and add the label disagree with severity, along with a comment indicating your opinion for the judges to review. It is possible for issues to be considered 0 (Non-critical).

Feel free to use the question label for anything you would like additional C4 input on.

Please don't change the severity labels; that's up to the judge's discretion.

Respond to issues

Label each High or Medium risk finding as one of these:

  • sponsor confirmed, meaning: "Yes, this is a problem and we intend to fix it."
  • sponsor disputed, meaning either: "We cannot duplicate this issue" or "We disagree that this is an issue at all."
  • sponsor acknowledged, meaning: "Yes, technically the issue is correct, but we are not going to resolve it for xyz reasons."

(Note: please don't use sponsor disputed for a finding if you think it should be considered of lower or higher severity. Instead, use the label disagree with severity and add comments to recommend a different severity level -- and include your reasoning.)

Add any necessary comments explaining your rationale for your evaluation of the issue. Note that when the repo is public, after all issues are mitigated, wardens will read these comments.

QA and Gas Reports

For contests starting on or after February 3, 2022, C4 introduced a mechanism change for low and non-critical findings, as well as gas optimizations. All warden submissions in these three categories should now be submitted as bulk listings of issues and recommendations:

  • QA reports should include all low severity and non-critical findings, along with a summary statement.
  • Gas reports should include all gas optimization recommendations, along with a summary statement.

For QA and Gas reports, we ask that you:

  • Leave a comment for the judge on any reports you consider to be particularly high quality. (These reports will be awarded on a curve.)
  • Add the sponsor disputed label to any reports that you think should be completely disregarded by the judge, i.e. the report contains no valid findings at all.

Once de-duping and labelling is complete

When you have marked all duplicates and labelled all findings, drop the C4 team a note in your private Discord backroom channel and let us know you've completed the sponsor review process. At this point, we will pass the repo over to the judge, and they'll get to work while you work on mitigation.

Share your mitigation of findings

For each non-duplicate finding which you have confirmed, you will want to mitigate the issue before the contest report is made public.

As you undertake that process, we ask that you create a pull request in your original repo for each finding, and link to the PR in the issue the PR resolves. This will allow for complete transparency in showing the work of mitigating the issues found in the contest. Do not close the issue; simply label it as resolved. If the issue in question has duplicates, please link to your PR from the issue you selected as the best and most thoroughly articulated one.

2022-04-pooltogether-findings's People

Contributors

code423n4 avatar c4-staff avatar jeeberc4 avatar kartoonjoy avatar liveactionllama avatar itsmetechjay avatar

Watchers

Ashok avatar Justin avatar  avatar

2022-04-pooltogether-findings's Issues

Lack of require in redeemToken could produce token loses

Lines of code

https://github.com/pooltogether/aave-v3-yield-source/blob/e63d1b0e396a5bce89f093630c282ca1c6627e44/contracts/AaveV3YieldSource.sol#L255

Vulnerability details

Impact

Lack of require in redeemToken could produce token loses.

Proof of Concept

In the method redeemToken the user set the expected _redeemAmount, it will compute the expected shares to burn, and after it, it will transfer the amout according the _redeemAmount argument. Because of precision it's possible that a small _redeemAmount will be computed as 0 shares in _tokenToShares method, and if this happens, it will burn 0 shares and transfer tokens to the user.

Recommended Mitigation Steps

Add require(_shares > 0, "AaveV3YS/shares-gt-zero"); before burn.

Gas Optimizations

Consider joining functions to avoid extra function call

https://github.com/pooltogether/aave-v3-yield-source/blob/e63d1b0e396a5bce89f093630c282ca1c6627e44/contracts/AaveV3YieldSource.sol#L384-L401

  /**
   * @notice Retrieves Aave PoolAddressesProvider address.
   * @return A reference to PoolAddressesProvider interface.
   */
  function _poolProvider() internal view returns (IPoolAddressesProvider) {
    return
      IPoolAddressesProvider(
        poolAddressesProviderRegistry.getAddressesProvidersList()[ADDRESSES_PROVIDER_ID]
      );
  }

  /**
   * @notice Retrieves Aave Pool address.
   * @return A reference to Pool interface.
   */
  function _pool() internal view returns (IPool) {
    return IPool(_poolProvider().getPool());
  }

Change it for;

  /**
   * @notice Retrieves Aave Pool address.
   * @return A reference to Pool interface.
   */
  function _pool() internal view returns (IPool) {
    return IPool(
        IPoolAddressesProvider(
            poolAddressesProviderRegistry.getAddressesProvidersList()[ADDRESSES_PROVIDER_ID]
        ).getPool());
  }

Gas Optimizations

https://github.com/pooltogether/aave-v3-yield-source/blob/e63d1b0e396a5bce89f093630c282ca1c6627e44/contracts/AaveV3YieldSource.sol#L127
https://github.com/pooltogether/aave-v3-yield-source/blob/e63d1b0e396a5bce89f093630c282ca1c6627e44/contracts/AaveV3YieldSource.sol#L130
https://github.com/pooltogether/aave-v3-yield-source/blob/e63d1b0e396a5bce89f093630c282ca1c6627e44/contracts/AaveV3YieldSource.sol#L133

The contract variables aToken, rewardsController, poolAddressesProviderRegistry must be immutable to save gas

IAToken public immutable aToken;
IRewardsController public immutable rewardsController;
IPoolAddressesProviderRegistry public immutable poolAddressesProviderRegistry;

extend this report:
If change the aToken should also change the constructor https://github.com/pooltogether/aave-v3-yield-source/blob/e63d1b0e396a5bce89f093630c282ca1c6627e44/contracts/AaveV3YieldSource.sol#L183

- IERC20(_tokenAddress()).safeApprove(address(_pool()), type(uint256).max);
+ IERC20(_aToken.UNDERLYING_ASSET_ADDRESS()).safeApprove(address(_pool()), type(uint256).max); 

QA Report

LOW

L-01: No way to withdraw native assets

If native assets such as ETH or MATIC are for some reason deposited in the contract (via selfdestruct) they will be locked/lost as there is no way to withdraw them from the contract.

I suggest implementing a withdraw function to manage native assets that could have been mistakenly sent to the contract.

L-02: 1:1 Ratio can be manipulated by artifically inflating aToken balance of AaveV3YieldSource

If attacker directly sends aTokens to AaveV3YieldSource it will change the 1:1 ratio in _tokenToShares. Consequently users will not get 1:1 shares when depositing ERC20s. This might confuse users into thinking they wont get their fair share back when calling redeemToken, which could be true if they deposited before the attacker directly sent aTokens to AaveV3YieldSource, but not after.

I recommend implementing checks to ensure aTokens held in contract have a 1:1 ratio to total shares before redeeming asset tokens.
In addition implement a function that can rebalance amount of aToken held by contract if check above is false.

NON-CRITICAL

N-01: Function state mutability can be restricted to view

AaveV3YieldSource.sol#L203

Function state mutability can be restricted to view

function balanceOfToken(address _user) external override returns (uint256)

Function does not makes any state changes and therefore can be restricted to view.

QA Report

In the AaveV3YieldSource.test.ts.

describe('decimals()', () => {
it('should return the ERC30 token decimals number', async () => {
expect(await aaveV3YieldSource.decimals()).to.equal(DECIMALS);
});
});

Should replace ERC30 with ERC20

Gas Optimizations

https://github.com/pooltogether/aave-v3-yield-source/blob/e63d1b0e396a5bce89f093630c282ca1c6627e44/contracts/AaveV3YieldSource.sol#L127
https://github.com/pooltogether/aave-v3-yield-source/blob/e63d1b0e396a5bce89f093630c282ca1c6627e44/contracts/AaveV3YieldSource.sol#L130
https://github.com/pooltogether/aave-v3-yield-source/blob/e63d1b0e396a5bce89f093630c282ca1c6627e44/contracts/AaveV3YieldSource.sol#L133

The contract variables aToken, rewardsController, poolAddressesProviderRegistry must be immutable to save gas

IAToken public immutable aToken;
IRewardsController public immutable rewardsController;
IPoolAddressesProviderRegistry public immutable poolAddressesProviderRegistry;

extend this report:
If change the aToken should also change the constructor https://github.com/pooltogether/aave-v3-yield-source/blob/e63d1b0e396a5bce89f093630c282ca1c6627e44/contracts/AaveV3YieldSource.sol#L183

- IERC20(_tokenAddress()).safeApprove(address(_pool()), type(uint256).max);
+ IERC20(_aToken.UNDERLYING_ASSET_ADDRESS()).safeApprove(address(_pool()), type(uint256).max); 

extend 2:
https://github.com/pooltogether/aave-v3-yield-source/blob/e63d1b0e396a5bce89f093630c282ca1c6627e44/contracts/AaveV3YieldSource.sol#L183

-  IERC20(_tokenAddress()).safeApprove(address(_pool()), type(uint256).max);
+ IERC20(_aToken.UNDERLYING_ASSET_ADDRESS()).safeApprove(
      IPoolAddressesProvider(
        _poolAddressesProviderRegistry.getAddressesProvidersList()[ADDRESSES_PROVIDER_ID]
      ).getPool(),
      type(uint256).max
    );

User fund loss in supplyTokenTo() because of rounding

Lines of code

https://github.com/pooltogether/aave-v3-yield-source/blob/e63d1b0e396a5bce89f093630c282ca1c6627e44/contracts/AaveV3YieldSource.sol#L231-L242
https://github.com/pooltogether/aave-v3-yield-source/blob/e63d1b0e396a5bce89f093630c282ca1c6627e44/contracts/AaveV3YieldSource.sol#L357-L362

Vulnerability details

Impact

When user use supplyTokenTo() to deposit his tokens and get share in FeildSource because of rounding in division user gets lower amount of share. for example if token's _decimal was 1 and totalSupply() was 1000 and aToken.balanceOf(FieldSource.address) was 2100 (becasue of profits in Aave Pool balance is higher than supply), then if user deposit 4 token to the contract with supplyTokenTo(), contract is going to mint only 1 share for that user and if user calls YeildToken.balanceOf(user) the return value is going to be 2 and user already lost half of his deposit.
Of course if _precision was high this loss is going to be low enough to ignore but in case of low _precision and high price token and high balance / supply ratio this loss is going to be noticeable.

Proof of Concept

This is the code of supplyTokenTo():

  function supplyTokenTo(uint256 _depositAmount, address _to) external override nonReentrant {
    uint256 _shares = _tokenToShares(_depositAmount);
    require(_shares > 0, "AaveV3YS/shares-gt-zero");

    address _underlyingAssetAddress = _tokenAddress();
    IERC20(_underlyingAssetAddress).safeTransferFrom(msg.sender, address(this), _depositAmount);
    _pool().supply(_underlyingAssetAddress, _depositAmount, address(this), REFERRAL_CODE);

    _mint(_to, _shares);

    emit SuppliedTokenTo(msg.sender, _shares, _depositAmount, _to);
  }

which in line: _shares = _tokenToShares(_depositAmount) trying to calculated shares corresponding to the number of tokens supplied. and then transfer _depositAmount from user and mint shares amount for user.
the problem is that if user convert _shares to token, he is going to receive lower amount because in most cases:

_depositAmount > _sharesToToken(_tokenToShares(_depositAmount))

and that's because of rounding in division. Value of _shares is less than _depositAmount. so YeildSource should only take part of _depositAmount that equals to _sharesToToken(_tokenToShares(_depositAmount)) and mint _share for user.

Of course if _precision was high and aToken.balanceOf(FieldSource.address) / totalSupply() was low, then this amount will be insignificant, but for some cases it can be harmful for users. for example this conditions:

  • _perecision is low like 1 or 2.
  • token value is very high like BTC.
  • aToken.balanceOf(FieldSource.address) / totalSupply() is high due to manipulation or profit in Aave pool.

Tools Used

VIM

Recommended Mitigation Steps

To resolve this issue this can be done:

  function supplyTokenTo(uint256 _depositAmount, address _to) external override nonReentrant {
    uint256 _shares = _tokenToShares(_depositAmount);
    require(_shares > 0, "AaveV3YS/shares-gt-zero");
    
    _depositAmount = _sharesToToken(_shares); // added hero to only take correct amount of user tokens
    address _underlyingAssetAddress = _tokenAddress();
    IERC20(_underlyingAssetAddress).safeTransferFrom(msg.sender, address(this), _depositAmount);
    _pool().supply(_underlyingAssetAddress, _depositAmount, address(this), REFERRAL_CODE);

    _mint(_to, _shares);

    emit SuppliedTokenTo(msg.sender, _shares, _depositAmount, _to);
  }

Gas Optimizations

No need to use safeMath, also tou could do an unchecked operation

https://github.com/pooltogether/aave-v3-yield-source/blob/e63d1b0e396a5bce89f093630c282ca1c6627e44/contracts/AaveV3YieldSource.sol#L262-L263

Consider change it to;

    uint256 _balanceDiff = _afterBalance - _beforeBalance;
    _assetToken.safeTransfer(msg.sender, _balanceDiff);

Or:

    _assetToken.safeTransfer(msg.sender, _afterBalance - _beforeBalance);

Also since _beforeBalance is always equal or less than _afterBalance we could do;

    unckecked {
        _assetToken.safeTransfer(msg.sender, _afterBalance - _beforeBalance);
    }

Gas Optimizations

Consider caching the aToken.UNDERLYING_ASSET_ADDRESS(); since its a constant it could be cached in a immutable state variable.

Remove this lines;
https://github.com/pooltogether/aave-v3-yield-source/blob/e63d1b0e396a5bce89f093630c282ca1c6627e44/contracts/AaveV3YieldSource.sol#L376-L382

Add this state variable;

  address public immutable _tokenAddress;

Setup _tokenAddress;

  constructor(
    IAToken _aToken,
    IRewardsController _rewardsController,
    IPoolAddressesProviderRegistry _poolAddressesProviderRegistry,
    string memory _name,
    string memory _symbol,
    uint8 decimals_,
    address _owner
  ) Ownable(_owner) ERC20(_name, _symbol) ReentrancyGuard() {
    require(address(_aToken) != address(0), "AaveV3YS/aToken-not-zero-address");
    aToken = _aToken;

    _tokenAddress = aToken.UNDERLYING_ASSET_ADDRESS();

Then instead of calling _tokenAddress() use _tokenAddress

Fund theft in redeemToken() because of rounding in division

Lines of code

https://github.com/pooltogether/aave-v3-yield-source/blob/e63d1b0e396a5bce89f093630c282ca1c6627e44/contracts/AaveV3YieldSource.sol#L251-L267
https://github.com/pooltogether/aave-v3-yield-source/blob/e63d1b0e396a5bce89f093630c282ca1c6627e44/contracts/AaveV3YieldSource.sol#L357-L362

Vulnerability details

Impact

When user use redeemToken() to get his tokens and burn his shares in FeildSource because of rounding in division user's share balance decrease is going to be lower than corresponding withdraw amount in value. for example if token's _decimal was 1 and totalSupply() was 1000 and aToken.balanceOf(FieldSource.address) was 2100 (becasue of profits in Aave Pool balance is higher than supply), and user had 10 share in the YeildSource, Then if user withdraw 2 token from the contract with redeemToken(), contract is going to burn only 0 share for user and if user calls YeildToken.balanceOf(user) after withdrawing, the return value is going to be same, and user can repeatedly(max 50 times) withdraw 2 token and contract won't decrease his balance.
Of course if _precision was high this theft is going to be low enough and will not cover gas fee, but in case of low _precision and high price token and high balance / supply ratio this loss is going to be noticeable and hacker can use it to steal founds.

Proof of Concept

This is the code of redeemToken():

  function redeemToken(uint256 _redeemAmount) external override nonReentrant returns (uint256) {
    address _underlyingAssetAddress = _tokenAddress();
    IERC20 _assetToken = IERC20(_underlyingAssetAddress);

    uint256 _shares = _tokenToShares(_redeemAmount);
    _burn(msg.sender, _shares);

    uint256 _beforeBalance = _assetToken.balanceOf(address(this));
    _pool().withdraw(_underlyingAssetAddress, _redeemAmount, address(this));
    uint256 _afterBalance = _assetToken.balanceOf(address(this));

    uint256 _balanceDiff = _afterBalance.sub(_beforeBalance);
    _assetToken.safeTransfer(msg.sender, _balanceDiff);

    emit RedeemedToken(msg.sender, _shares, _redeemAmount);
    return _balanceDiff;
  }

which in line: _shares = _tokenToShares(_redeemAmount) trying to calculated shares corresponding to the number of tokens requested. and then transfer _redeemAmount from YeildSource to user and burn shares amount for user.
The problem is that if we calculate token amount equal to burned _shares amount, it is going to be lower than withdrawal amount because in most cases:

_redeemAmount > _sharesToToken(_tokenToShares(_redeemAmount))

and that's because of rounding in division. So in reality FieldSource is transfering others tokens for this user. Toekn value of _shares is less than _redeemAmount. so YeildSource should only transfer part of _redeemAmount that equals to _sharesToToken(_tokenToShares_redeemAmount)) and burn _share for user.

Of course if _precision was high and aToken.balanceOf(FieldSource.address) / totalSupply() was low, then this amount theft will be insignificant, but for some cases it can be harmful for users. for example this conditions:

  • _perecision is low like 1 or 2.
  • token price is very high like BTC.
  • aToken.balanceOf(FieldSource.address) / totalSupply() is high due to manipulation or profit in Aave pool.

Tools Used

VIM

Recommended Mitigation Steps

To resolve this issue this can be done:

  function redeemToken(uint256 _redeemAmount) external override nonReentrant returns (uint256) {
    address _underlyingAssetAddress = _tokenAddress();
    IERC20 _assetToken = IERC20(_underlyingAssetAddress);

    uint256 _shares = _tokenToShares(_redeemAmount);
    _burn(msg.sender, _shares);
    _redeemAmount= _sharesToToken(_shares); // added hero to only transfer correct amount of token to user

    uint256 _beforeBalance = _assetToken.balanceOf(address(this));
    _pool().withdraw(_underlyingAssetAddress, _redeemAmount, address(this));
    uint256 _afterBalance = _assetToken.balanceOf(address(this));

    uint256 _balanceDiff = _afterBalance.sub(_beforeBalance);
    _assetToken.safeTransfer(msg.sender, _balanceDiff);

    emit RedeemedToken(msg.sender, _shares, _redeemAmount);
    return _balanceDiff;
  }

Gas Optimizations

  1. Removing SafeMath usage minorly improves contract gas consumption (as shown for redeemToken and supplyTokenTo)

Removing using SafeMath for uint256; and making adjustments accordingly to the contract's arithmetic as:
L262: uint256 _balanceDiff = _afterBalance - _beforeBalance;
L361: return _supply == 0 ? _tokens : _tokens * _supply / aToken.balanceOf(address(this));
L373: return _supply == 0 ? _shares : _shares * aToken.balanceOf(address(this)) / _supply;

Results in lesser gas usage ! (shown below)

Reason: starting Solidity >= 0.8.0, arithmetic operations revert on underflow and overflow.
OZ's SafeMath v4.4.1 for Solidity >= 0.8.0 relies on compiler's checks for sub, mul and div functions anyway, making the use of the library completely redundant.

with SafeMath:

·-------------------------------------------------------|----------------------------|-------------|-----------------------------·
|                 Solc version: 0.8.10                  ·  Optimizer enabled: false  ·  Runs: 200  ·  Block limit: 30000000 gas  │
························································|····························|·············|······························
|  Methods                                              ·               100 gwei/gas               ·       2772.21 usd/eth       │
·····························|··························|··············|·············|·············|···············|··············
|  Contract                  ·  Method                  ·  Min         ·  Max        ·  Avg        ·  # calls      ·  usd (avg)  │
·····························|··························|··············|·············|·············|···············|··············
|  AaveV3YieldSourceHarness  ·  claimRewards            ·       57098  ·      59291  ·      58195  ·            4  ·      16.13  │
·····························|··························|··············|·············|·············|···············|··············
|  AaveV3YieldSourceHarness  ·  decreaseERC20Allowance  ·       40154  ·      42359  ·      41620  ·            3  ·      11.54  │
·····························|··························|··············|·············|·············|···············|··············
|  AaveV3YieldSourceHarness  ·  increaseERC20Allowance  ·       61935  ·      64452  ·      64041  ·            7  ·      17.75  │
·····························|··························|··············|·············|·············|···············|··············
|  AaveV3YieldSourceHarness  ·  mint                    ·       52265  ·      69377  ·      62788  ·           13  ·      17.41  │
·····························|··························|··············|·············|·············|···············|··············
|  AaveV3YieldSourceHarness  ·  redeemToken             ·           -  ·          -  ·     116178  ·            1  ·      32.21  │
·····························|··························|··············|·············|·············|···············|··············
|  AaveV3YieldSourceHarness  ·  setManager              ·           -  ·          -  ·      48335  ·            4  ·      13.40  │
·····························|··························|··············|·············|·············|···············|··············
|  AaveV3YieldSourceHarness  ·  supplyTokenTo           ·      115041  ·     157449  ·     146161  ·            6  ·      40.52  │
·····························|··························|··············|·············|·············|···············|··············
|  AaveV3YieldSourceHarness  ·  transferERC20           ·       60271  ·      62452  ·      61362  ·            2  ·      17.01  │
·····························|··························|··············|·············|·············|···············|··············
|  ERC20Mintable             ·  approve                 ·       27329  ·      47229  ·      43912  ·            6  ·      12.17  │
·····························|··························|··············|·············|·············|···············|··············
|  ERC20Mintable             ·  mint                    ·       52286  ·      69698  ·      66593  ·           12  ·      18.46  │
·····························|··························|··············|·············|·············|···············|··············
|  ERC20Mintable             ·  transferFrom            ·           -  ·          -  ·      50967  ·            1  ·      14.13  │
·····························|··························|··············|·············|·············|···············|··············
|  Deployments                                          ·                                          ·  % of limit   ·             │
························································|··············|·············|·············|···············|··············
|  AaveV3YieldSourceHarness                             ·     4670575  ·    4670587  ·    4670585  ·       15.6 %  ·    1294.78  │
························································|··············|·············|·············|···············|··············
|  ERC20Mintable                                        ·           -  ·          -  ·    2256052  ·        7.5 %  ·     625.42  │
·-------------------------------------------------------|--------------|-------------|-------------|---------------|-------------·

after SafeMath removed and adjusted:

·-------------------------------------------------------|----------------------------|-------------|-----------------------------·
|                 Solc version: 0.8.10                  ·  Optimizer enabled: false  ·  Runs: 200  ·  Block limit: 30000000 gas  │
························································|····························|·············|······························
|  Methods                                              ·               100 gwei/gas               ·       2787.95 usd/eth       │
·····························|··························|··············|·············|·············|···············|··············
|  Contract                  ·  Method                  ·  Min         ·  Max        ·  Avg        ·  # calls      ·  usd (avg)  │
·····························|··························|··············|·············|·············|···············|··············
|  AaveV3YieldSourceHarness  ·  claimRewards            ·       57098  ·      59291  ·      58195  ·            4  ·      16.22  │
·····························|··························|··············|·············|·············|···············|··············
|  AaveV3YieldSourceHarness  ·  decreaseERC20Allowance  ·       40154  ·      42359  ·      41620  ·            3  ·      11.60  │
·····························|··························|··············|·············|·············|···············|··············
|  AaveV3YieldSourceHarness  ·  increaseERC20Allowance  ·       61935  ·      64452  ·      64041  ·            7  ·      17.85  │
·····························|··························|··············|·············|·············|···············|··············
|  AaveV3YieldSourceHarness  ·  mint                    ·       52265  ·      69377  ·      62788  ·           13  ·      17.50  │
·····························|··························|··············|·············|·············|···············|··············
|  AaveV3YieldSourceHarness  ·  redeemToken             ·           -  ·          -  ·     115989  ·            1  ·      32.34  │
·····························|··························|··············|·············|·············|···············|··············
|  AaveV3YieldSourceHarness  ·  setManager              ·           -  ·          -  ·      48335  ·            4  ·      13.48  │
·····························|··························|··············|·············|·············|···············|··············
|  AaveV3YieldSourceHarness  ·  supplyTokenTo           ·      114915  ·     157449  ·     146119  ·            6  ·      40.74  │
·····························|··························|··············|·············|·············|···············|··············
|  AaveV3YieldSourceHarness  ·  transferERC20           ·       60271  ·      62452  ·      61362  ·            2  ·      17.11  │
·····························|··························|··············|·············|·············|···············|··············
|  ERC20Mintable             ·  approve                 ·       27329  ·      47229  ·      43912  ·            6  ·      12.24  │
·····························|··························|··············|·············|·············|···············|··············
|  ERC20Mintable             ·  mint                    ·       52286  ·      69698  ·      66593  ·           12  ·      18.57  │
·····························|··························|··············|·············|·············|···············|··············
|  ERC20Mintable             ·  transferFrom            ·           -  ·          -  ·      50967  ·            1  ·      14.21  │
·····························|··························|··············|·············|·············|···············|··············
|  Deployments                                          ·                                          ·  % of limit   ·             │
························································|··············|·············|·············|···············|··············
|  AaveV3YieldSourceHarness                             ·     4648841  ·    4648853  ·    4648851  ·       15.5 %  ·    1296.08  │
························································|··············|·············|·············|···············|··············
|  ERC20Mintable                                        ·           -  ·          -  ·    2256052  ·        7.5 %  ·     628.98  │
·-------------------------------------------------------|--------------|-------------|-------------|---------------|-------------·

Gas Optimizations

GAS

G-01: Unnecessary use of SafeMath

Using safeMath:
Contract · Method · Min · Max · Avg · # cal
| AaveV3YieldSourceHarness · redeemToken · - · - · 116178 ·
| AaveV3YieldSourceHarness · supplyTokenTo · 115041 · 157449 · 146161 ·

Not using SafeMath:
| AaveV3YieldSourceHarness · redeemToken · - · - · 115989 ·
| AaveV3YieldSourceHarness · supplyTokenTo · 114915 · 157449 · 146119 ·

Difference:
redeemToken = 116178 - 115989 = 189

supplyTokenTo (avg) = 146161 - 146119 = 42

supplyTokenTo allows users to bypass prize pool

Lines of code

https://github.com/pooltogether/aave-v3-yield-source/blob/e63d1b0e396a5bce89f093630c282ca1c6627e44/contracts/AaveV3YieldSource.sol#L231-L242

Vulnerability details

Impact

Loss of user rewards

Proof of Concept

Contract allows anyone to call the supplyTokenTo and redeemToken functions, allowing users to bypass prize pool contract and deposit/supply directly to the Yield Source. Underlying tokens would be safe but user's rewards would be distributed to other users or lost

Tools Used

Recommended Mitigation Steps

Require that only prize pool can make redeem or supply calls

Gas Optimizations

Gas1:

No need to use safemath library for solidity 0.8.4+ as the compiler itself now checks for overflow/underflow, hence results in lot of gas savings

Gas2:

prefer != instead of > for unsigned integer, saves gas
https://github.com/pooltogether/aave-v3-yield-source/blob/e63d1b0e396a5bce89f093630c282ca1c6627e44/contracts/AaveV3YieldSource.sol#L179
https://github.com/pooltogether/aave-v3-yield-source/blob/e63d1b0e396a5bce89f093630c282ca1c6627e44/contracts/AaveV3YieldSource.sol#L233

Gas3:

uint256 should be preferred, not against readability of code
https://github.com/pooltogether/aave-v3-yield-source/blob/e63d1b0e396a5bce89f093630c282ca1c6627e44/contracts/AaveV3YieldSource.sol#L145
https://github.com/pooltogether/aave-v3-yield-source/blob/e63d1b0e396a5bce89f093630c282ca1c6627e44/contracts/AaveV3YieldSource.sol#L136

Gas4:

no need to increase or decrease allowance if it's approved for max limit,results in lot of gas savings
Similar optimizations are used in ERC20 tokens like WETH, The Wrapped Ether (WETH) ERC-20 contract does not update the allowance if it is the max uint.

https://github.com/pooltogether/aave-v3-yield-source/blob/e63d1b0e396a5bce89f093630c282ca1c6627e44/contracts/AaveV3YieldSource.sol#L296
https://github.com/pooltogether/aave-v3-yield-source/blob/e63d1b0e396a5bce89f093630c282ca1c6627e44/contracts/AaveV3YieldSource.sol#L315

[WP-H1] A malicious early user/attacker can manipulate the vault's pricePerShare to take an unfair share of future users' deposits

Lines of code

https://github.com/pooltogether/aave-v3-yield-source/blob/e63d1b0e396a5bce89f093630c282ca1c6627e44/contracts/AaveV3YieldSource.sol#L352-L374

Vulnerability details

This is a well-known attack vector for new contracts that utilize pricePerShare for accounting.

https://github.com/pooltogether/aave-v3-yield-source/blob/e63d1b0e396a5bce89f093630c282ca1c6627e44/contracts/AaveV3YieldSource.sol#L352-L374

  /**
   * @notice Calculates the number of shares that should be minted or burnt when a user deposit or withdraw.
   * @param _tokens Amount of asset tokens
   * @return Number of shares.
   */
  function _tokenToShares(uint256 _tokens) internal view returns (uint256) {
    uint256 _supply = totalSupply();

    // shares = (tokens * totalShares) / yieldSourceATokenTotalSupply
    return _supply == 0 ? _tokens : _tokens.mul(_supply).div(aToken.balanceOf(address(this)));
  }

  /**
   * @notice Calculates the number of asset tokens a user has in the yield source.
   * @param _shares Amount of shares
   * @return Number of asset tokens.
   */
  function _sharesToToken(uint256 _shares) internal view returns (uint256) {
    uint256 _supply = totalSupply();

    // tokens = (shares * yieldSourceATokenTotalSupply) / totalShares
    return _supply == 0 ? _shares : _shares.mul(aToken.balanceOf(address(this))).div(_supply);
  }

A malicious early user can supplyTokenTo() with 1 wei of _underlyingAssetAddress token as the first depositor of the AaveV3YieldSource.sol, and get 1 wei of shares token.

Then the attacker can send 10000e18 - 1 of aToken and inflate the price per share from 1.0000 to an extreme value of 1.0000e22 ( from (1 + 10000e18 - 1) / 1) .

As a result, the future user who deposits 19999e18 will only receive 1 wei (from 19999e18 * 1 / 10000e18) of shares token.

They will immediately lose 9999e18 or half of their deposits if they redeemToken() right after the supplyTokenTo().

https://github.com/pooltogether/aave-v3-yield-source/blob/e63d1b0e396a5bce89f093630c282ca1c6627e44/contracts/AaveV3YieldSource.sol#L251-L256

  function redeemToken(uint256 _redeemAmount) external override nonReentrant returns (uint256) {
    address _underlyingAssetAddress = _tokenAddress();
    IERC20 _assetToken = IERC20(_underlyingAssetAddress);

    uint256 _shares = _tokenToShares(_redeemAmount);
    _burn(msg.sender, _shares);
    ...

Furthermore, after the PPS has been inflated to an extremely high value (10000e18), the attacker can also redeem tokens up to 9999e18 for free, (burn 0 shares) due to the precision loss.

Recommendation

Consider requiring a minimal amount of share tokens to be minted for the first minter, and send a port of the initial mints as a reserve to the DAO address so that the pricePerShare can be more resistant to manipulation.

Also, consder adding require(_shares > 0, "AaveV3YS/shares-gt-zero"); before _burn(msg.sender, _shares);.

An attacker can make users' funds get "locked" in the contract (the owner can get them out and transfer them back to the users)

Lines of code

https://github.com/pooltogether/aave-v3-yield-source/blob/e63d1b0e396a5bce89f093630c282ca1c6627e44/contracts/AaveV3YieldSource.sol#L357-L362

Vulnerability details

Impact

If a user manages to be the first user to deposit into the contract, he will be minted shares and he can steal all the other users' deposits.

Proof of Concept

  1. The attacker deposits 1 token into the contract and 1 share is minted to him (totalSupply and the aToken balance of the contract is zero).
  2. A user deposits y tokens to the contract (y > 0).
  3. The attacker front runs the user's transactions and transfers y tokens to the pool (so the pool aToken balance after the transfer will be y + 1) - this is a token transfer and not a regular deposit through the contract.
  4. The user deposit transaction is executed - y tokens are transferred from the user to the contract and 0 shares are minted to the user - the calculation of the share amount is y * 1 / (y + 1) which is equal to zero.
  5. The attacker can redeem the tokens that are in the contract by calling the redeemToken function which returns all the tokens to the attacker because he has all the shares.

The attacker can perform this attack for every user that tries to deposit to the contract. The attacker doesn't profit from this attack, but he steals all the users' funds (they are actually left in the contract).
The owner can get the funds from the contract using the transferERC20 function and transfer them back to the users, but this can be prevented from the first place.

function _tokenToShares(uint256 _tokens) internal view returns (uint256) {
    uint256 _supply = totalSupply();

    // shares = (tokens * totalShares) / yieldSourceATokenTotalSupply
    return _supply == 0 ? _tokens : _tokens.mul(_supply).div(aToken.balanceOf(address(this)));
}

Tools Used

VS Code & Remix

Recommended Mitigation Steps

Save the deposited balance as an updated variable and use it instead aToken.balanceOf(address(this)) in the _tokenToShares function.

Gas Optimizations

[2022-04-pooltogether] QA report

tags: c4, 2022-04-pooltogether

SafeApprove is deprecated

Unnecessary use of SafeMath

Gas Optimizations

Gas Report

Replace strict greater-than-zero operation (> 0) with does-not-equal-zero (!= 0) operation

When checking whether a value is equal to zero, using the construction var != 0 is costs less gas than using var > 0. Note that this is true only when the comparison occurs in a conditional context and the Solidity compiler is using the Optimizer.

For more information, please consult the following resources:

Twitter discussion detailing the gas costs of != 0 vs > 0 in require() calls

Solidity Compiler: Optimizer options

The following lines of code are affected:

code4rena/pooltogether/AaveV3YieldSource.sol:179:    require(decimals_ > 0, "AaveV3YS/decimals-gt-zero");
code4rena/pooltogether/AaveV3YieldSource.sol:233:    require(_shares > 0, "AaveV3YS/shares-gt-zero");

[WP-M1] `supplyTokenTo()` may fail when Aave Pool address changed

Lines of code

https://github.com/pooltogether/aave-v3-yield-source/blob/e63d1b0e396a5bce89f093630c282ca1c6627e44/contracts/AaveV3YieldSource.sol#L231-L242

Vulnerability details

https://github.com/pooltogether/aave-v3-yield-source/blob/e63d1b0e396a5bce89f093630c282ca1c6627e44/contracts/AaveV3YieldSource.sol#L231-L242

  function supplyTokenTo(uint256 _depositAmount, address _to) external override nonReentrant {
    uint256 _shares = _tokenToShares(_depositAmount);
    require(_shares > 0, "AaveV3YS/shares-gt-zero");

    address _underlyingAssetAddress = _tokenAddress();
    IERC20(_underlyingAssetAddress).safeTransferFrom(msg.sender, address(this), _depositAmount);
    _pool().supply(_underlyingAssetAddress, _depositAmount, address(this), REFERRAL_CODE);

    _mint(_to, _shares);

    emit SuppliedTokenTo(msg.sender, _shares, _depositAmount, _to);
  }

https://github.com/pooltogether/aave-v3-yield-source/blob/e63d1b0e396a5bce89f093630c282ca1c6627e44/contracts/AaveV3YieldSource.sol#L384-L401

  /**
   * @notice Retrieves Aave PoolAddressesProvider address.
   * @return A reference to PoolAddressesProvider interface.
   */
  function _poolProvider() internal view returns (IPoolAddressesProvider) {
    return
      IPoolAddressesProvider(
        poolAddressesProviderRegistry.getAddressesProvidersList()[ADDRESSES_PROVIDER_ID]
      );
  }

  /**
   * @notice Retrieves Aave Pool address.
   * @return A reference to Pool interface.
   */
  function _pool() internal view returns (IPool) {
    return IPool(_poolProvider().getPool());
  }

https://github.com/pooltogether/aave-v3-yield-source/blob/e63d1b0e396a5bce89f093630c282ca1c6627e44/contracts/AaveV3YieldSource.sol#L183

    // Approve once for max amount
    IERC20(_tokenAddress()).safeApprove(address(_pool()), type(uint256).max);

In the constructor(), _underlyingAssetAddress is approved once for max amount to _pool().

And when deposit, or supplyTokenTo(), _pool().supply() will be called without approve _underlyingAssetAddress to the _pool again.

However, _pool() is a dynamic address that can actually be changed by Aave.

See: https://github.com/aave/aave-v3-core/blob/d851792e56c4ca1e12617d237702045d956b0145/contracts/protocol/configuration/PoolAddressesProviderRegistry.sol

When that happens, supplyTokenTo() will always fail due to insufficient allowance.

Recommendation

Consider adding a new method to revoke allowance to the previous pool address and approve the new address.

Gas Optimizations

do not cache variable used only once

description

for variables only used once, changing it to inline saves gas

findings

258: uint256 _beforeBalance = _assetToken.balanceOf(address(this));
260: uint256 _afterBalance = _assetToken.balanceOf(address(this));

Gas Optimizations

Consider removing safeMath library.

Source:
OpenZeppelin/openzeppelin-contracts@24a0bc2#diff-f4b1737177aad965d94530b54ac4001a2e1f5fe6e4e34bafe023310cea599eca

Remove;
https://github.com/pooltogether/aave-v3-yield-source/blob/e63d1b0e396a5bce89f093630c282ca1c6627e44/contracts/AaveV3YieldSource.sol#L26

Change;
https://github.com/pooltogether/aave-v3-yield-source/blob/e63d1b0e396a5bce89f093630c282ca1c6627e44/contracts/AaveV3YieldSource.sol#L361

    return _supply == 0 ? _tokens : _tokens.mul(_supply).div(aToken.balanceOf(address(this)));

For:

    return _supply == 0 ? _tokens : (_tokens *_supply) / aToken.balanceOf(address(this));

Change:
https://github.com/pooltogether/aave-v3-yield-source/blob/e63d1b0e396a5bce89f093630c282ca1c6627e44/contracts/AaveV3YieldSource.sol#L373

    return _supply == 0 ? _shares : _shares.mul(aToken.balanceOf(address(this))).div(_supply);

For:

    return _supply == 0 ? _shares : (_shares * aToken.balanceOf(address(this))) / _supply;

Fee-on-transfer tokens will mess up the internal accounting of the contract

Lines of code

https://github.com/pooltogether/aave-v3-yield-source/blob/e63d1b0e396a5bce89f093630c282ca1c6627e44/contracts/AaveV3YieldSource.sol#L237

Vulnerability details

Impact

If the underlying token is a fee-on-transfer token, the amount of tokens that will be transferred to the contract isn't equal to the amount the supplyTokenTo tries to supply to the aave protocol, so the function will revert because the contract won't have enough balance of the token.

Proof of Concept

Let's assume that the underlying token is a n fee-on-transfer token (0 < n <= 1).
If a user deposits x tokens, x*(1-n) tokens are actually transferred to the contract, and the supplyTokenTo function will try to deposit x tokens to the aave pool, which will trigger the transfer of the tokens and revert because the contract won't have enough token balance.

Tools Used

VS Code & Remix

Recommended Mitigation Steps

calculate the deposited amount by the difference between the balance of the contract before and after the transfer

Early Depositor can DOS Deposits

Lines of code

https://github.com/pooltogether/aave-v3-yield-source/blob/e63d1b0e396a5bce89f093630c282ca1c6627e44/contracts/AaveV3YieldSource.sol#L232
https://github.com/pooltogether/aave-v3-yield-source/blob/e63d1b0e396a5bce89f093630c282ca1c6627e44/contracts/AaveV3YieldSource.sol#L361

Vulnerability details

Impact

A malicious, but generous, early depositor can DOS all future deposits. This is accomplished by directly sending aTokens to the AaveV3YieldSource.sol contract after making their first deposit. The amount of aTokens sent to the contract will manipulate the share distribution for future depositors. Future depositors will need to deposit at least the amount of aTokens sent directly to the contract to receive even a single share.

The griefer could be assumed to be motivated by a desire to undermine the success of PoolTogether by DOSing contract interactions.

Proof of Concept

Steps to exploit:

  • Griefer deposits 1 wei USDC
  • Griefer receives 1 wei share based on the following formula:

return _supply == 0 ? _tokens : _tokens.mul(_supply).div(aToken.balanceOf(address(this)));

Since supply==0, 1 wei shares is minted to the griefer.

  • Next, the griefer directly sends 10,000 ether of USDC to AaveV3YieldSource contract. This artificially inflates the denominator of the above function: aToken.balanceOf(address(this)
  • A prospective user attempts to deposit 5,000 ether USDC but the transaction will revert because of the following check: require(_shares > 0, "AaveV3YS/shares-gt-zero"); Calculation below without safeMath for ease of understanding:

shares = 5,000 ether * 1 wei / 10,000 ether = 0 shares due to precision loss

Therefore, any depositor that attempts to deposit less than 10,000 ether USDC will be unable. Please note that 10,000 USDC is arbitrary. The griefer can send any value.

Finally, it should be noted that the AaveV3YieldSource contract does not have any way to transfer out aTokens to correct the issue.

Tools Used

Manual Review

Recommended Mitigation Steps

Instead of relying on aToken.balanceOf(address(this), use internal accounting variables to keep track of the protocol's aToken balance.

In the transferERC20() function, the owner is not allowed to send aTokens. Removing this restriction would help out in this situation.

Gas Optimizations

AaveV3YieldSource.sol

  • L136-145 - _decimals which is uint8 would be better than this together with REFERRAL_CODE uint16 , so it takes up less storage space.

  • L168.171.174 - In the constructor, it is not necessary to validate if it is zero, since if an interface is set in the input parameters of the signature, it already includes the validation that it is different from zero.

  • L235.236.237 - _tokenAddress() is view therefore it does not generate a gas cost, instead the creation of the variable and its use does. It would be best to call _tokenAddress() directly.

  • L252.253.259 - _tokenAddress() is view therefore it does not generate a gas cost, instead the creation of the variable and its use does. It would be best to call _tokenAddress() directly.

  • L260.262 - It could be executed like this and it would save creating a variable (_assetToken.balanceOf(address(this))).sub(_beforeBalance);

  • L232.239.241 - _tokenToShares() is a view function, so it can be passed as a parameter without creating a local variable, this would reduce the gas by 2000.

  • L255.256.265 - _tokenToShares() is a view function, so it can be passed as a parameter without creating a local variable, this would reduce the gas by 2000.

When the return value of _pool () is changed, it can DOS supplyTokenTo().

Lines of code

https://github.com/pooltogether/aave-v3-yield-source/blob/e63d1b0e396a5bce89f093630c282ca1c6627e44/contracts/AaveV3YieldSource.sol#L183-L183

Vulnerability details

Impact

In the constructor of the AaveV3YieldSource contract, the contract calls the safeApprove function for the return value of _pool(), eliminating the need to call the safeApprove function in the supplyTokenTo function.
However, when the address is changed in PoolAddressesProviderRegistry or PoolAddressesProvider contract, the return value of the _pool() function may also change, resulting in the failure to execute the supplieTokenTo function due to not to call the safeApprove function for the new return value of the _pool().

Proof of Concept

https://github.com/pooltogether/aave-v3-yield-source/blob/e63d1b0e396a5bce89f093630c282ca1c6627e44/contracts/AaveV3YieldSource.sol#L183-L183
https://github.com/aave/aave-v3-core/blob/master/contracts/protocol/configuration/PoolAddressesProviderRegistry.sol#L82-L85
https://github.com/aave/aave-v3-core/blob/master/contracts/protocol/configuration/PoolAddressesProvider.sol#L57-L61

Tools Used

None

Recommended Mitigation Steps

  function supplyTokenTo(uint256 _depositAmount, address _to) external override nonReentrant {
    uint256 _shares = _tokenToShares(_depositAmount);
    require(_shares > 0, "AaveV3YS/shares-gt-zero");

    address _underlyingAssetAddress = _tokenAddress();
    IERC20(_underlyingAssetAddress).safeTransferFrom(msg.sender, address(this), _depositAmount);
+  IERC20(_tokenAddress()).safeApprove(address(_pool()), 0);
+  IERC20(_tokenAddress()).safeApprove(address(_pool()), _depositAmount);
    _pool().supply(_underlyingAssetAddress, _depositAmount, address(this), REFERRAL_CODE);

    _mint(_to, _shares);

    emit SuppliedTokenTo(msg.sender, _shares, _depositAmount, _to);
  }

Manager or owner can send rewards to any address

Lines of code

https://github.com/pooltogether/aave-v3-yield-source/blob/e63d1b0e396a5bce89f093630c282ca1c6627e44/contracts/AaveV3YieldSource.sol#L275-L286

Vulnerability details

Impact

In the claimRewards function, manager or owner can send rewards to any address.

  function claimRewards(address _to) external onlyManagerOrOwner returns (bool) {
    require(_to != address(0), "AaveV3YS/payee-not-zero-address");

    address[] memory _assets = new address[](1);
    _assets[0] = address(aToken);

    (address[] memory _rewardsList, uint256[] memory _claimedAmounts) = rewardsController
      .claimAllRewards(_assets, _to);

    emit Claimed(msg.sender, _to, _rewardsList, _claimedAmounts);
    return true;
  }

Proof of Concept

https://github.com/pooltogether/aave-v3-yield-source/blob/e63d1b0e396a5bce89f093630c282ca1c6627e44/contracts/AaveV3YieldSource.sol#L275-L286

Tools Used

None

Recommended Mitigation Steps

Consider the claimRewards function to send rewards to a fixed reward distribution contract

Gas Optimizations

Impact

As per 0.8.4 solidity version it supports new custom errors.
Custom errors are reducing 38 gas if condition is met and 22 gas otherwise.
Also reduces contract size and deployment costs.

Affected code: https://github.com/pooltogether/aave-v3-yield-source/tree/e63d1b0e396a5bce89f093630c282ca1c6627e44/contracts/AaveV3YieldSource.sol#L168

Proof of Concept

https://blog.soliditylang.org/2021/04/21/custom-errors/

Tools Used

Recommended Mitigation Steps

Recommended code:
error RC_NotZeroAddress();
..
if(address(_aToken) == address(0))
{
revert RC_NotZeroAddress();
}


Impact

As per 0.8.4 solidity version it supports new custom errors.
Custom errors are reducing 38 gas if condition is met and 22 gas otherwise.
Also reduces contract size and deployment costs.

Affected code: https://github.com/pooltogether/aave-v3-yield-source/tree/e63d1b0e396a5bce89f093630c282ca1c6627e44/contracts/AaveV3YieldSource.sol#L171

Proof of Concept

https://blog.soliditylang.org/2021/04/21/custom-errors/

Tools Used

Recommended Mitigation Steps

Recommended code:
error RC_NotZeroAddress();
..
if(address(_rewardsController) == address(0))
{
revert RC_NotZeroAddress();
}


Impact

As per 0.8.4 solidity version it supports new custom errors.
Custom errors are reducing 38 gas if condition is met and 22 gas otherwise.
Also reduces contract size and deployment costs.

Affected code: https://github.com/pooltogether/aave-v3-yield-source/tree/e63d1b0e396a5bce89f093630c282ca1c6627e44/contracts/AaveV3YieldSource.sol#L174

Proof of Concept

https://blog.soliditylang.org/2021/04/21/custom-errors/

Tools Used

Recommended Mitigation Steps

Recommended code:
error RC_NotZeroAddress();
..
if(address(_poolAddressesProviderRegistry) == address(0))
{
revert RC_NotZeroAddress();
}


Impact

As per 0.8.4 solidity version it supports new custom errors.
Custom errors are reducing 38 gas if condition is met and 22 gas otherwise.
Also reduces contract size and deployment costs.

Affected code: https://github.com/pooltogether/aave-v3-yield-source/tree/e63d1b0e396a5bce89f093630c282ca1c6627e44/contracts/AaveV3YieldSource.sol#L177

Proof of Concept

https://blog.soliditylang.org/2021/04/21/custom-errors/

Tools Used

Recommended Mitigation Steps

Recommended code:
error Owner_NotZeroAddress();
..
if(_owner == address(0))
{
revert Owner_NotZeroAddress();
}


Impact

As per 0.8.4 solidity version it supports new custom errors.
Custom errors are reducing 38 gas if condition is met and 22 gas otherwise.
Also reduces contract size and deployment costs.

Affected code: https://github.com/pooltogether/aave-v3-yield-source/tree/e63d1b0e396a5bce89f093630c282ca1c6627e44/contracts/AaveV3YieldSource.sol#L179

Proof of Concept

https://blog.soliditylang.org/2021/04/21/custom-errors/

Tools Used

Recommended Mitigation Steps

Recommended code:
error DecimalsGtZero();
..
if(decimals_ == 0)
{
revert DecimalsGtZero();
}


Impact

As per 0.8.4 solidity version it supports new custom errors.
Custom errors are reducing 38 gas if condition is met and 22 gas otherwise.
Also reduces contract size and deployment costs.

Affected code: https://github.com/pooltogether/aave-v3-yield-source/tree/e63d1b0e396a5bce89f093630c282ca1c6627e44/contracts/AaveV3YieldSource.sol#L233

Proof of Concept

https://blog.soliditylang.org/2021/04/21/custom-errors/

Tools Used

Recommended Mitigation Steps

Recommended code:
error SharesGtZero();
..
if(_shares == 0)
{
revert SharesGtZero();
}


Impact

As per 0.8.4 solidity version it supports new custom errors.
Custom errors are reducing 38 gas if condition is met and 22 gas otherwise.
Also reduces contract size and deployment costs.

Affected code: https://github.com/pooltogether/aave-v3-yield-source/tree/e63d1b0e396a5bce89f093630c282ca1c6627e44/contracts/AaveV3YieldSource.sol#L276

Proof of Concept

https://blog.soliditylang.org/2021/04/21/custom-errors/

Tools Used

Recommended Mitigation Steps

Recommended code:
error PayeeNotZeroAddress();
..
if(_to == address(0))
{
revert PayeeNotZeroAddress();
}


Impact

As per 0.8.4 solidity version it supports new custom errors.
Custom errors are reducing 38 gas if condition is met and 22 gas otherwise.
Also reduces contract size and deployment costs.

Affected code: https://github.com/pooltogether/aave-v3-yield-source/tree/e63d1b0e396a5bce89f093630c282ca1c6627e44/contracts/AaveV3YieldSource.sol#L337

Proof of Concept

https://blog.soliditylang.org/2021/04/21/custom-errors/

Tools Used

Recommended Mitigation Steps

Recommended code:
error Forbid_aToken_Transfer();
..
if(address(_token) == address(aToken))
{
revert Forbid_aToken_Transfer();
}


Impact

As per 0.8.4 solidity version it supports new custom errors.
Custom errors are reducing 38 gas if condition is met and 22 gas otherwise.
Also reduces contract size and deployment costs.

Affected code: https://github.com/pooltogether/aave-v3-yield-source/tree/e63d1b0e396a5bce89f093630c282ca1c6627e44/contracts/AaveV3YieldSource.sol#L349

Proof of Concept

https://blog.soliditylang.org/2021/04/21/custom-errors/

Tools Used

Recommended Mitigation Steps

Recommended code:
error Forbid_aToken_Allowance();
..
if(_token == address(aToken))
{
revert Forbid_aToken_Allowance();
}


QA Report

Low Critical

Use _balanceDiff instead of _redeemAmount in RedeemedToken event:

AaveV3YieldSource.sol#L265
redeemToken() send user _balanceDiff so it's better to emit RedeemedToken event with _balanceDiff instead of _redeemAmount.

Recommended Mitigation Steps:

change:

emit RedeemedToken(msg.sender, _shares, _redeemAmount);

to this:

emit RedeemedToken(msg.sender, _shares, _balanceDiff);

Non Critical

balanceOfToken() do not write, so it can be restricted to view for readability:

AaveV3YieldSource.sol#L203

Recommended Mitigation Steps:

change:

function balanceOfToken(address _user) external override returns (uint256) {

to this:

function balanceOfToken(address _user) external view override returns (uint256) {

typos in comments:

change inhereted to inherited:

AaveV3YieldSource.sol
  38,53:    * @param decimals Number of decimals the shares (inhereted ERC20) will ...
  156,54:    * @param decimals_ Number of decimals the shares (inhereted ERC20) will ...

Gas Optimizations

Table of Contents:

Some variables should be immutable

These variables are only set in the constructor and are never edited after that:

File: AaveV3YieldSource.sol
126:   /// @notice Yield-bearing Aave aToken address.
127:   IAToken public aToken;
128: 
129:   /// @notice Aave RewardsController address.
130:   IRewardsController public rewardsController;
131: 
132:   /// @notice Aave poolAddressesProviderRegistry address.
133:   IPoolAddressesProviderRegistry public poolAddressesProviderRegistry;
...
169:   aToken = _aToken;
172:   rewardsController = _rewardsController;
175:   poolAddressesProviderRegistry = _poolAddressesProviderRegistry;

Consider marking them as immutable.

> 0 is less efficient than != 0 for unsigned integers

!= 0 costs less gas compared to > 0 for unsigned integers in require statements with the optimizer enabled (6 gas)

Proof: While it may seem that > 0 is cheaper than !=, this is only true without the optimizer enabled and outside a require statement. If you enable the optimizer at 10k AND you're in a require statement, this will save gas. You can see this tweet for more proofs: https://twitter.com/gzeon/status/1485428085885640706

I suggest changing > 0 with != 0 here:

AaveV3YieldSource.sol:179:    require(decimals_ > 0, "AaveV3YS/decimals-gt-zero");
AaveV3YieldSource.sol:233:    require(_shares > 0, "AaveV3YS/shares-gt-zero");

Also, please enable the Optimizer.

Amounts should be checked for 0 before calling a transfer

Checking non-zero transfer values can avoid an expensive external call and save gas.

I suggest adding a non-zero-value check here:

AaveV3YieldSource.sol:338:    _token.safeTransfer(_to, _amount);

SafeMath is not needed when using Solidity version 0.8.+

Solidity version 0.8.+ already implements overflow and underflow checks by default.
Using the SafeMath library from OpenZeppelin (which is more gas expensive than the 0.8.* overflow checks) is therefore redundant.

Affected code:

14:     import { SafeMath } from "@openzeppelin/contracts/utils/math/SafeMath.sol";
26:     using SafeMath for uint256;
262:    uint256 _balanceDiff = _afterBalance.sub(_beforeBalance);
361:    return _supply == 0 ? _tokens : _tokens.mul(_supply).div(aToken.balanceOf(address(this)));
373:    return _supply == 0 ? _shares : _shares.mul(aToken.balanceOf(address(this))).div(_supply);

Use Custom Errors instead of Revert Strings to save Gas

Custom errors from Solidity 0.8.4 are cheaper than revert strings (cheaper deployment cost and runtime cost when the revert condition is met)

Source: https://blog.soliditylang.org/2021/04/21/custom-errors/:

Starting from Solidity v0.8.4, there is a convenient and gas-efficient way to explain to users why an operation failed through the use of custom errors. Until now, you could already use strings to give more information about failures (e.g., revert("Insufficient funds.");), but they are rather expensive, especially when it comes to deploy cost, and it is difficult to use dynamic information in them.

Custom errors are defined using the error statement, which can be used inside and outside of contracts (including interfaces and libraries).

Instances include:

AaveV3YieldSource.sol:168:    require(address(_aToken) != address(0), "AaveV3YS/aToken-not-zero-address");
AaveV3YieldSource.sol:171:    require(address(_rewardsController) != address(0), "AaveV3YS/RC-not-zero-address");
AaveV3YieldSource.sol:174:    require(address(_poolAddressesProviderRegistry) != address(0), "AaveV3YS/PR-not-zero-address");
AaveV3YieldSource.sol:177:    require(_owner != address(0), "AaveV3YS/owner-not-zero-address");
AaveV3YieldSource.sol:179:    require(decimals_ > 0, "AaveV3YS/decimals-gt-zero");
AaveV3YieldSource.sol:233:    require(_shares > 0, "AaveV3YS/shares-gt-zero");
AaveV3YieldSource.sol:276:    require(_to != address(0), "AaveV3YS/payee-not-zero-address");
AaveV3YieldSource.sol:337:    require(address(_token) != address(aToken), "AaveV3YS/forbid-aToken-transfer");
AaveV3YieldSource.sol:349:    require(_token != address(aToken), "AaveV3YS/forbid-aToken-allowance");

I suggest replacing revert strings with custom errors.

Gas Optimizations

Gas Optimizations

From solidity ^0.8.0 we do not need SafeMath:

As solidity ^0.8.0 Arithmetic operations revert on underflow and overflow.

  • save 21734 gas on Deployments
  • save 189 gas on redeemToken()
  • save 42 gas on supplyTokenTo()

Recommended Mitigation Steps:

Remove:

change:

AaveV3YieldSource.sol#L262:

uint256 _balanceDiff = _afterBalance.sub(_beforeBalance);

to this:

uint256 _balanceDiff = _afterBalance - _beforeBalance;

AaveV3YieldSource.sol#L361:

return _supply == 0 ? _tokens : _tokens.mul(_supply).div(aToken.balanceOf(address(this)));

to this:

return _supply == 0 ? _tokens : _tokens * _supply / aToken.balanceOf(address(this));

AaveV3YieldSource.sol#L373:

return _supply == 0 ? _shares : _shares.mul(aToken.balanceOf(address(this))).div(_supply);

to this:

_supply == 0 ? _shares : _shares * aToken.balanceOf(address(this)) / _supply;

Use Custom Errors to save Gas:

Custom errors from Solidity 0.8.4 are cheaper than require messages.
https://blog.soliditylang.org/2021/04/21/custom-errors/

Malicious competitor/attacker could make users receive less tokens per share than they should.

Lines of code

AaveV3YieldSource.sol#L231
AaveV3YieldSource.sol#L251

Vulnerability details

Impact

Pool/users of AaveV3YieldSource would only be able to redeem a portion of their deposits. Effectively losing the rest.

Proof of Concept

If someone had the intent to damage the protocols reputation or the balance of its users/pool it could artificially increase the aToken balance of AaveV3YieldSource by making a deposit to Aave and transferring the aTokens to AaveV3YieldSource.

This would decrease the ratio of asset tokens redeemed through redeemToken by increasing the denominator in _tokenToShares (yieldSourceATokenTotalSupply).

//shares = (tokens * totalShares) / yieldSourceATokenTotalSupply

Assume _supply == 0:

  • Imagine pool/user calls supplyTokenTo and deposits 10 ERC20, getting 10 shares in return (1:1 ratio).
  • Attacker aritifially increases yieldSourceATokenTotalSupply by directly transferring 10 aTokens to AaveV3YieldSource.
  • Same pool/user calls redeemToken for the 10 shares they own, but instead of receiving back 10 ERC20, _tokenToShares only gives them 5 ERC20 back.

Tools Used

Manual Review

Recommended Mitigation Steps

Implement checks to ensure aTokens held in contract have a 1:1 ratio to total shares before redeeming asset token.
Implement a function that can rebalance amount of aToken held by contract if check above is false.

Gas Optimizations

[S]: Suggested optimation, save a decent amount of gas without compromising readability;

[M]: Minor optimation, the amount of gas saved is minor, change when you see fit;

[N]: Non-preferred, the amount of gas saved is at cost of readability, only apply when gas saving is a top priority.

[S] Changing unnecessary storage variables to immutable can save gas

https://github.com/pooltogether/aave-v3-yield-source/blob/e63d1b0e396a5bce89f093630c282ca1c6627e44/contracts/AaveV3YieldSource.sol#L127

  IAToken public aToken;

Considering that aToken will never change, changing it to immutable variable instead of a storage variable can save gas.

[S] SafeMath is no longer needed

SafeMath is no longer needed starting with Solidity 0.8. The compiler now has built in overflow checking.

Removing SafeMath can save some gas.

[S] _tokenAddress() can be changed to immutable to save gas from unnecessary external calls

https://github.com/pooltogether/aave-v3-yield-source/blob/e63d1b0e396a5bce89f093630c282ca1c6627e44/contracts/AaveV3YieldSource.sol#L231-L235

  function supplyTokenTo(uint256 _depositAmount, address _to) external override nonReentrant {
    uint256 _shares = _tokenToShares(_depositAmount);
    require(_shares > 0, "AaveV3YS/shares-gt-zero");

    address _underlyingAssetAddress = _tokenAddress();
    ...

Since _underlyingAssetAddress will never change, it can be change to a immutable variable in the constructor() and save a decent of gas from the unnecessary external calls in every supplyTokenTo() and redeemToken() txs.

QA Report

Impact

uint256 is assigned to zero by default, additional reassignment to zero is unnecessary
Affected code: https://github.com/pooltogether/aave-v3-yield-source/blob/e63d1b0e396a5bce89f093630c282ca1c6627e44/contracts/AaveV3YieldSource.sol#L142

Proof of Concept

https://docs.soliditylang.org/en/v0.8.13/control-structures.html#default-value

Tools Used

Recommended Mitigation Steps

Recommended code:
uint256 private constant ADDRESSES_PROVIDER_ID;


Impact

Since solidity v0.8.0 SafeMath library is used by default in arithmetic operations. Using external SafeMath libraries is unnecessary.

Affected code: https://github.com/pooltogether/aave-v3-yield-source/blob/e63d1b0e396a5bce89f093630c282ca1c6627e44/contracts/AaveV3YieldSource.sol#L361

Proof of Concept

https://blog.soliditylang.org/2020/12/16/solidity-v0.8.0-release-announcement/#:~:text=the%20near%20future.-,Checked,-arithmetic%2C%20i.e

Tools Used

Recommended Mitigation Steps

Recommended code:
return _supply == 0 ? _tokens : (_tokens * _supply) / aToken.balanceOf(address(this));


Impact

Since solidity v0.8.0 SafeMath library is used by default in arithmetic operations. Using external SafeMath libraries is unnecessary.

Affected code: https://github.com/pooltogether/aave-v3-yield-source/blob/e63d1b0e396a5bce89f093630c282ca1c6627e44/contracts/AaveV3YieldSource.sol#L373

Proof of Concept

https://blog.soliditylang.org/2020/12/16/solidity-v0.8.0-release-announcement/#:~:text=the%20near%20future.-,Checked,-arithmetic%2C%20i.e

Tools Used

Recommended Mitigation Steps

Recommended code:
return _supply == 0 ? _shares : (_shares * aToken.balanceOf(address(this))) / _supply;


User fund lose and DOS attack when totalSupply() is zero

Lines of code

https://github.com/pooltogether/aave-v3-yield-source/blob/e63d1b0e396a5bce89f093630c282ca1c6627e44/contracts/AaveV3YieldSource.sol#L357-L362
https://github.com/pooltogether/aave-v3-yield-source/blob/e63d1b0e396a5bce89f093630c282ca1c6627e44/contracts/AaveV3YieldSource.sol#L231-L242
https://github.com/pooltogether/aave-v3-yield-source/blob/e63d1b0e396a5bce89f093630c282ca1c6627e44/contracts/AaveV3YieldSource.sol#L251-L267

Vulnerability details

Impact

Hacker can do this two action:

  • Perform a DOS attack and continuously deny users from supplying their tokens to YeildSource (deposits less than a high amount will be rejected and attacker can control this amount, for example set it to 10K and every deposit with less than 100K token will be rejected by contract)
  • Perform Sandwich Attack and continuously steal user supplied funds in every block.

When One of this conditions are met:

  • totalSupply() of YeildSource contract is still zero. (_decimal can be anything) (when contract created or when everybody redeemed their funds totalSupply() will be zero)
  • totalSupply() of YeildSource is low and _decimalcan is low too (for example _decimals<3 and totalSupply()<100)

Proof of Concept

YeildSource contract uses this code to convert token amount to share amount. if _supply>0 then to convert the amounts it multiply token amount to supply() and divide by aToken.balance().

  function _tokenToShares(uint256 _tokens) internal view returns (uint256) {
    uint256 _supply = totalSupply();

    // shares = (tokens * totalShares) / yieldSourceATokenTotalSupply
    return _supply == 0 ? _tokens : _tokens.mul(_supply).div(aToken.balanceOf(address(this)));
  }

and this function is for supplying tokens and minting share in YeildSource for user: (which will revert if user tries to mint _share==0)

  function supplyTokenTo(uint256 _depositAmount, address _to) external override nonReentrant {
    uint256 _shares = _tokenToShares(_depositAmount);
    require(_shares > 0, "AaveV3YS/shares-gt-zero");

    address _underlyingAssetAddress = _tokenAddress();
    IERC20(_underlyingAssetAddress).safeTransferFrom(msg.sender, address(this), _depositAmount);
    _pool().supply(_underlyingAssetAddress, _depositAmount, address(this), REFERRAL_CODE);

    _mint(_to, _shares);

    emit SuppliedTokenTo(msg.sender, _shares, _depositAmount, _to);
  }

let's assume totalSupply() == 0. To exploit this attacker will do this steps (attack can put this logic in a contract so they all execute in one transaction) (token has 18 _precision):

  1. first attacker will approves YeildSource address and supply 1 token to YeildSource with supplyTokenTo(). (not 10e18 token just 1 token).
  2. because totalSupply() is 0 so YeildSource will accept attacker supply and deposit attacker token to Aave Pool and mint 1 share for attacker.
  3. attacker will deposit 1000 * 10e18 token into Aave protocol and get 1000 * 10e18 aToken. (10e18 is 10 power 18)
  4. attacker will transfer 1000 * 10e18 - 1 aToken to YeildSource address. with this action totalSupply() will not change but aToken.balanceOf(YeildSource.address) will increase.
  5. right now totalSupply() of shares in YeildSource is 1 and aToken.balanceOf(YeildSource.address) is 1000 * 10e18. so it's obvious that _tokenToShares(uint256 _tokens) will return zero for any value less than 1000 * 10e18 so attacker was able to deny other from depositing less than 1K token and also 100% of the shares int the YeildSource belongs to attacker. Becasue of this limitation no one can supply to YeildSource with amount less than 1000 * 10e18 as long as attacker wants. (attacker can choose higher amount too)

Attacker can continue his attack to steal user's tokens too. to do this let's assume some users send transaction to supplyTokenTo of YeildScource to deposit and get share of pool. so this transactions happen:

  1. YeildSource.connect(user1).supplyTokenTo(1500 * 10e18). because right now totalSupply() of shares in YeildSource is 1 and aToken.balanceOf(YeildSource.address) is 1000 * 10e18 so _tokenToShares() will return 1 and user1 will get 1 share for his deposit.
  2. YeildSource.connect(user2).supplyTokenTo(2000 * 10e18). because right now totalSupply() of shares in YeildSource is 2 and aToken.balanceOf(YeildSource.address) is 2500 * 10e18 so _tokenToShares() will return 1 and user2 will get 1 share for his deposit.
  3. Right now totalSupply() of shares in YeildSource is 3 and aToken.balanceOf(YeildSource.address) is 4500 * 10e18 so for any amount lower than 1500 * 10e18 the return of _tokenToShares() will be 0.
  4. After users deposit their transactions, attacker will call YeildSource.redeemToken(1400 * 10e18) and will get 1400 * 10e18 token but his share amount(balanceOf(attacker)) will not change in the YeildSource becasue return value of _tokenToShares(1400 * 10e18) is 0. below is redeemToken() code which confirms this:
  function redeemToken(uint256 _redeemAmount) external override nonReentrant returns (uint256) {
    address _underlyingAssetAddress = _tokenAddress();
    IERC20 _assetToken = IERC20(_underlyingAssetAddress);

    uint256 _shares = _tokenToShares(_redeemAmount);
    _burn(msg.sender, _shares);

    uint256 _beforeBalance = _assetToken.balanceOf(address(this));
    _pool().withdraw(_underlyingAssetAddress, _redeemAmount, address(this));
    uint256 _afterBalance = _assetToken.balanceOf(address(this));

    uint256 _balanceDiff = _afterBalance.sub(_beforeBalance);
    _assetToken.safeTransfer(msg.sender, _balanceDiff);

    emit RedeemedToken(msg.sender, _shares, _redeemAmount);
    return _balanceDiff;
  }
  1. Right now totalSupply() of shares in YeildSource is 3 and aToken.balanceOf(YeildSource.address) is 3100 * 10e18 and attacker can repeat step #9 with lower amounts(999 * 10e18 then 700 * 10e18 then 450 * 10e18 and ....) multiple times and in each time his balance in YeildSource will not change and he can drain YeildSource's most of aTokens. (in each step amount needs to be smaller than aToken.balanceOf(YeildSource.address) / totalSupply() so return value of _tokenToShares() will be zero and attacker balance will not change)

Attacker can perform all steps of this attack in multiple ways he just need to be first one who deposits token to YeildSource to manipulate aToken.balanceOf(YeildSource.address) / totalSupply() ratio. he can do this with:

  • Sandwich Attacking first deposit. (to change the Ratio and get user deposited assets)
  • Sandiwitch Attacking contract creation. (to change the Ratio and wait for others suplly to steal them)
  • Front-running first deposit. (to change the Ratio and w8 for others deposit to steal them)
  • Sandwich Attacking in every block. (to continue stealing funds)

If totalSupply() is low and token's _decimals is low too, It is still possible to perform this attacks to some degree(the impact will be lower if _decimal get higher, but with _decimal=1 and totalSupply() < 1000 attacker can effectively steal users founds).

Tools Used

VIM

Recommended Mitigation Steps

To resolve this issue the logic of _tokenToShares() needs to be changed. for example something like this:

    return _supply == 0 ? _tokens * 10e18 : _tokens.mul(_supply).div(aToken.balanceOf(address(this)));

so attacker could not manipulate balance/supply ratio so easily.

Gas Optimizations

https://github.com/pooltogether/aave-v3-yield-source/blob/e63d1b0e396a5bce89f093630c282ca1c6627e44/contracts/AaveV3YieldSource.sol#L127
https://github.com/pooltogether/aave-v3-yield-source/blob/e63d1b0e396a5bce89f093630c282ca1c6627e44/contracts/AaveV3YieldSource.sol#L130
https://github.com/pooltogether/aave-v3-yield-source/blob/e63d1b0e396a5bce89f093630c282ca1c6627e44/contracts/AaveV3YieldSource.sol#L133

The contract variables aToken, rewardsController, poolAddressesProviderRegistry must be immutable to save gas

IAToken public immutable aToken;
IRewardsController public immutable rewardsController;
IPoolAddressesProviderRegistry public immutable poolAddressesProviderRegistry;

Manager or owner can send rewards to any address

Lines of code

https://github.com/pooltogether/aave-v3-yield-source/blob/e63d1b0e396a5bce89f093630c282ca1c6627e44/contracts/AaveV3YieldSource.sol#L275-L286

Vulnerability details

Impact

In the claimRewards function, manager or owner can send rewards to any address.

  function claimRewards(address _to) external onlyManagerOrOwner returns (bool) {
    require(_to != address(0), "AaveV3YS/payee-not-zero-address");

    address[] memory _assets = new address[](1);
    _assets[0] = address(aToken);

    (address[] memory _rewardsList, uint256[] memory _claimedAmounts) = rewardsController
      .claimAllRewards(_assets, _to);

    emit Claimed(msg.sender, _to, _rewardsList, _claimedAmounts);
    return true;
  }

Proof of Concept

https://github.com/pooltogether/aave-v3-yield-source/blob/e63d1b0e396a5bce89f093630c282ca1c6627e44/contracts/AaveV3YieldSource.sol#L275-L286

Tools Used

None

Recommended Mitigation Steps

Consider the claimRewards function to send rewards to a fixed reward distribution contract

Gas Optimizations

1. Use custom error for gas optimisation

require(address(_aToken) != address(0), "AaveV3YS/aToken-not-zero-address");
require(address(_rewardsController) != address(0), "AaveV3YS/RC-not-zero-address");
require(address(_poolAddressesProviderRegistry) != address(0), "AaveV3YS/PR-not-zero-address");
require(_owner != address(0), "AaveV3YS/owner-not-zero-address");
require(decimals_ > 0, "AaveV3YS/decimals-gt-zero");
require(_shares > 0, "AaveV3YS/shares-gt-zero");
.....

2. >0 is less gas efficient than != 0 for uints

3. Reduce gas using modifiers instead of internal function (requiretoken())

modifier requireNotAToken(address _token) {
  require(_token != address(aToken), "AaveV3YS/forbid-aToken-allowance");
  _;
}

Before:

·····························|··························|··············|·············|·············|···············|··············
|  AaveV3YieldSourceHarness  ·  decreaseERC20Allowance  ·       40154  ·      42359  ·      41620  ·            3  ·      11.59  │
·····························|··························|··············|·············|·············|···············|··············
|  AaveV3YieldSourceHarness  ·  increaseERC20Allowance  ·       61935  ·      64452  ·      64041  ·            7  ·      17.84  │
·····························|··························|··············|·············|·············|···············|··············

After:

·····························|··························|··············|·············|·············|···············|··············
|  AaveV3YieldSourceHarness  ·  decreaseERC20Allowance  ·       40130  ·      42335  ·      41596  ·            3  ·      11.58  │
·····························|··························|··············|·············|·············|···············|··············
|  AaveV3YieldSourceHarness  ·  increaseERC20Allowance  ·       61911  ·      64428  ·      64017  ·            7  ·      17.82  │
·····························|··························|··············|·············|·············|···············|··············

_depositAmount requires to be updated to contract balance increase

Lines of code

https://github.com/pooltogether/aave-v3-yield-source/blob/e63d1b0e396a5bce89f093630c282ca1c6627e44/contracts/AaveV3YieldSource.sol#L231-L242

Vulnerability details

Impact

Every time transferFrom or transfer function in ERC20 standard is called there is a possibility that underlying smart contract did not transfer the exact amount entered.
It is required to find out contract balance increase/decrease after the transfer.
This pattern also prevents from re-entrancy attack vector.

Proof of Concept

Tools Used

Recommended Mitigation Steps

Recommended code:
function supplyTokenTo(uint256 _depositAmount, address _to) external override nonReentrant {
uint256 _shares = _tokenToShares(_depositAmount);
require(_shares > 0, "AaveV3YS/shares-gt-zero");

address _underlyingAssetAddress = _tokenAddress();

uint256 balanceBefore = IERC20(_underlyingAssetAddress).balanceOf(address(this)); // remembering asset balance before the transfer
IERC20(_underlyingAssetAddress).safeTransferFrom(msg.sender, address(this), _depositAmount);
_depositAmount = IERC20(_underlyingAssetAddress).balanceOf(address(this)) - balanceBefore; // updating actual amount to the contract balance increase

_pool().supply(_underlyingAssetAddress, _depositAmount, address(this), REFERRAL_CODE);

_mint(_to, _shares);

emit SuppliedTokenTo(msg.sender, _shares, _depositAmount, _to);

}

Frontrun attack to steal first depositor money

Lines of code

https://github.com/pooltogether/aave-v3-yield-source/blob/e63d1b0e396a5bce89f093630c282ca1c6627e44/contracts/AaveV3YieldSource.sol#L361

Vulnerability details

Impact

Frontrun attack to steal first depositor money

Proof of Concept

The flow is as following:

  1. The first depositor wants to deposit X.
  2. We detect it and frontrun 2 operations:
    1. We deposit 1 of the underlying to the system. In exchange we receive 1 share. (Based on that formula return _supply == 0 ? _tokens : _tokens.mul(_supply).div(aToken.balanceOf(address(this))); because _supply = 0)
    2. We deposit X (through the aToken contract api and not from this system) on behalf of the AaveV3YieldSource.sol contract. From that the aToken.balanceOf(address(this)) would increase to x+1 but _supply stays on 1.
  3. Now the user deposits X to the system and receive 0 shares, because:
    return _supply == 0 ? _tokens : _tokens.mul(_supply).div(aToken.balanceOf(address(this)));
    so it will be 1.mul(x).div(x+1) = 0.
  4. Now we call redeemToken(X+1) and uint256 _shares = _tokenToShares(_redeemAmount);
    and it will be 1 because _shares = _supply == 0 ? _tokens : _tokens.mul(_supply).div(aToken.balanceOf(address(this))) which means (x+1).mul(1).div(x+1)=1 and we have 1 share so it will be burnt and we'll receive x+1 of the underlying.
    Final result:
    We stole the first depositor's deposit.

Tools Used

Recommended Mitigation Steps

Use local variable to track (aToken.balanceOf(address(this)).

Gas Optimizations

Use immutable in variables that you never update;

Lines:
https://github.com/pooltogether/aave-v3-yield-source/blob/e63d1b0e396a5bce89f093630c282ca1c6627e44/contracts/AaveV3YieldSource.sol#L127:L133

Current code

  /// @notice Yield-bearing Aave aToken address.
  IAToken public aToken;

  /// @notice Aave RewardsController address.
  IRewardsController public rewardsController;

  /// @notice Aave poolAddressesProviderRegistry address.
  IPoolAddressesProviderRegistry public poolAddressesProviderRegistry;

Recommendation:

  /// @notice Yield-bearing Aave aToken address.
  IAToken public immutable aToken;

  /// @notice Aave RewardsController address.
  IRewardsController public immutable rewardsController;

  /// @notice Aave poolAddressesProviderRegistry address.
  IPoolAddressesProviderRegistry public immutable poolAddressesProviderRegistry;

Unsupported fee-on-transfer tokens

[Low-01] Unsupported fee-on-transfer tokens

Impact

When _underlyingAssetAddress is fee-on-transfer tokens, in the supplyTokenTo function, the actual amount of tokens received by the contract will be less than the _depositAmount, so that the subsequent _pool().supply function will fail to execute.

Proof of Concept

https://github.com/pooltogether/aave-v3-yield-source/blob/e63d1b0e396a5bce89f093630c282ca1c6627e44/contracts/AaveV3YieldSource.sol#L231-L242

Tools Used

None

Recommended Mitigation Steps

Consider getting the received amount by calculating the difference of token balance (using balanceOf) before and after the safeTransferFrom.

Bug in calculation it should use aToken balance instead of totalSupply

Lines of code

https://github.com/pooltogether/aave-v3-yield-source/blob/e63d1b0e396a5bce89f093630c282ca1c6627e44/contracts/AaveV3YieldSource.sol#L357:L362

Vulnerability details

Impact

If aToken.balanceOf(address(this)) is 0 this function will always fail due a divition by zero.

Proof of Concept

If the contract balance of aToken is zero it will fail here;
https://github.com/pooltogether/aave-v3-yield-source/blob/e63d1b0e396a5bce89f093630c282ca1c6627e44/contracts/AaveV3YieldSource.sol#L232
https://github.com/pooltogether/aave-v3-yield-source/blob/e63d1b0e396a5bce89f093630c282ca1c6627e44/contracts/AaveV3YieldSource.sol#L255

Tools Used

Manual verification

Recommended Mitigation Steps

Change current function to:

  /**
   * @notice Calculates the number of shares that should be minted or burnt when a user deposit or withdraw.
   * @param _tokens Amount of asset tokens
   * @return Number of shares.
   */
  function _tokenToShares(uint256 _tokens) internal view returns (uint256) {
    uint256 _balance = aToken.balanceOf(address(this));

    // shares = (tokens * totalShares) / yieldSourceATokenTotalSupply
    return _balance == 0 ? _tokens : (_tokens * totalSupply()) / _balance;
  }

Incorrect _tokenToShares calculation (division by zero + wrong shares output if _supply is zero)

Lines of code

https://github.com/pooltogether/aave-v3-yield-source/blob/e63d1b0e396a5bce89f093630c282ca1c6627e44/contracts/AaveV3YieldSource.sol#L357-L362

Vulnerability details

Impact

According to this formula shares = (tokens * totalShares) / yieldSourceATokenTotalSupply the following rules must be applied:

  1. shares == 0, if totalShares == 0
  2. shares == 0, if yieldSourceATokenTotalSupply == 0 (prevent division by zero)

Proof of Concept

Tools Used

Recommended Mitigation Steps

Recommended code:
function _tokenToShares(uint256 _tokens) internal view returns (uint256) {
uint256 _supply = totalSupply();
uint256 _yieldSourceATokenTotalSupply = aToken.balanceOf(address(this));

// shares = (tokens * totalShares) / yieldSourceATokenTotalSupply
return _supply == 0 || _yieldSourceATokenTotalSupply == 0 ? 0 : 
    (_tokens * _supply) / _yieldSourceATokenTotalSupply;

}

An attacker can make users' funds get "locked" in the contract (the owner can get them out and transfer them back to the users)

Lines of code

https://github.com/pooltogether/aave-v3-yield-source/blob/e63d1b0e396a5bce89f093630c282ca1c6627e44/contracts/AaveV3YieldSource.sol#L357-L362

Vulnerability details

Impact

If a user manages to be the first user to deposit into the contract, he will be minted shares and he can steal all the other users' deposits.

Proof of Concept

  1. The attacker deposits 1 token into the contract and 1 share is minted to him (totalSupply and the aToken balance of the contract is zero).
  2. A user deposits y tokens to the contract (y > 0).
  3. The attacker front runs the user's transactions and transfers y tokens to the pool (so the pool aToken balance after the transfer will be y + 1) - this is a token transfer and not a regular deposit through the contract.
  4. The user deposit transaction is executed - y tokens are transferred from the user to the contract and 0 shares are minted to the user - the calculation of the share amount is y * 1 / (y + 1) which is equal to zero.
  5. The attacker can redeem the tokens that are in the contract by calling the redeemToken function which returns all the tokens to the attacker because he has all the shares.

The attacker can perform this attack for every user that tries to deposit to the contract. The attacker doesn't profit from this attack, but he steals all the users' funds (they are actually left in the contract).
The owner can get the funds from the contract using the transferERC20 function and transfer them back to the users, but this can be prevented from the first place.

function _tokenToShares(uint256 _tokens) internal view returns (uint256) {
    uint256 _supply = totalSupply();

    // shares = (tokens * totalShares) / yieldSourceATokenTotalSupply
    return _supply == 0 ? _tokens : _tokens.mul(_supply).div(aToken.balanceOf(address(this)));
}

Tools Used

VS Code & Remix

Recommended Mitigation Steps

Save the deposited balance as an updated variable and use it instead aToken.balanceOf(address(this)) in the _tokenToShares function.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.