solidity.security.balancer-readonly-reentrancy-getpooltokens.balancer-readonly-reentrancy-getpooltokens

profile photo of semgrepsemgrep
Author
unknown
Download Count*

$VAULT.getPoolTokens() call on a Balancer pool is not protected from the read-only reentrancy.

Run Locally

Run in CI

Defintion

rules:
  - id: balancer-readonly-reentrancy-getpooltokens
    message: $VAULT.getPoolTokens() call on a Balancer pool is not protected from
      the read-only reentrancy.
    metadata:
      category: security
      technology:
        - solidity
      cwe: "CWE-841: Improper Enforcement of Behavioral Workflow"
      confidence: HIGH
      likelihood: MEDIUM
      impact: HIGH
      subcategory:
        - vuln
      references:
        - https://quillaudits.medium.com/decoding-sentiment-protocols-1-million-exploit-quillaudits-f36bee77d376
        - https://hackmd.io/@sentimentxyz/SJCySo1z2
      license: Commons Clause License Condition v1.0[LGPL-2.1-only]
      vulnerability_class:
        - Other
    patterns:
      - pattern-either:
          - pattern: |
              function $F(...) {
                ...
                $RETURN = $VAULT.getPoolTokens(...);
                ...
              }
      - metavariable-pattern:
          metavariable: $RETURN
          pattern-regex: .*uint256\[].*
      - pattern-not-inside: |
          contract $C {
            ...
            function $CHECKFUNC(...) {
              ...
              VaultReentrancyLib.ensureNotInVaultContext(...);
              ...
            }
            ...
            function $F(...) {
              ...
              $CHECKFUNC(...);
              ...
              $RETURN = $VAULT.getPoolTokens(...);
              ...
            }
            ...
          }
      - pattern-not-inside: |
          contract $C {
            ...
            function $CHECKFUNC(...) {
              ...
              VaultReentrancyLib.ensureNotInVaultContext(...);
              ...
            }
            ...
            function $F(...) {
              ...
              $RETURN = $VAULT.getPoolTokens(...);
              ...
              $CHECKFUNC(...);
              ...
            }
            ...
          }
      - pattern-not-inside: |
          contract $C {
            ...
            function $CHECKFUNC(...) {
              ...
              $VAULT.manageUserBalance(...);
              ...
            }
            ...
            function $F(...) {
              ...
              $RETURN = $VAULT.getPoolTokens(...);
              ...
              $CHECKFUNC(...);
              ...
            }
            ...
          }
      - pattern-not-inside: |
          contract $C {
            ...
            function $CHECKFUNC(...) {
              ...
              $VAULT.manageUserBalance(...);
              ...
            }
            ...
            function $F(...) {
              ...
              $CHECKFUNC(...);
              ...
              $RETURN = $VAULT.getPoolTokens(...);
              ...
            }
            ...
          }
      - pattern-not: |
          function $F(...) {
            ...
            VaultReentrancyLib.ensureNotInVaultContext(...);
            ...
          }
      - pattern-not: |
          function $F(...) {
            ...
            $VAULT.manageUserBalance(...);
            ...
          }
      - pattern-not-inside: |
          contract LinearPool {
            ...
          }
      - pattern-not-inside: |
          contract ComposableStablePool {
            ...
          }
      - pattern-not-inside: |
          contract BalancerQueries {
            ...
          } 
      - pattern-not-inside: |
          contract ManagedPool {
            ...
          }
      - pattern-not-inside: |
          contract BaseWeightedPool {
            ...
          } 
      - pattern-not-inside: |
          contract ComposableStablePoolStorage {
            ...
          }
      - pattern-not-inside: |
          contract RecoveryModeHelper {
            ...
          }
      - focus-metavariable:
          - $VAULT
    languages:
      - solidity
    severity: ERROR

Examples

balancer-readonly-reentrancy-getpooltokens.sol

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract BALBBA3USDOracle is IOracle, IOracleValidate {

    function check() internal view {
        // ok: balancer-readonly-reentrancy-getpooltokens  
        (address[] memory poolTokens, , ) = getVault().getPoolTokens(getPoolId());
    }
    function test() view {
        // ruleid: balancer-readonly-reentrancy-getpooltokens  
        (, uint256[] memory balances, ) = IVault(VAULT_ADDRESS).getPoolTokens(poolId);
    }

    function getPrice(address token) external view returns (uint) {
        (
            address[] memory poolTokens,
            uint256[] memory balances,
        // ruleid: balancer-readonly-reentrancy-getpooltokens  
        ) = vault.getPoolTokens(IPool(token).getPoolId());

        uint256[] memory weights = IPool(token).getNormalizedWeights();

        uint length = weights.length;
        uint temp = 1e18;
        uint invariant = 1e18;
        for(uint i; i < length; i++) {
            temp = temp.mulDown(
                (oracleFacade.getPrice(poolTokens[i]).divDown(weights[i]))
                .powDown(weights[i])
            );
            invariant = invariant.mulDown(
                (balances[i] * 10 ** (18 - IERC20(poolTokens[i]).decimals()))
                .powDown(weights[i])
            );
        }
        return invariant
            .mulDown(temp)
            .divDown(IPool(token).totalSupply());
    }  
}

abstract contract LinearPool {
    function check() internal view {
        // ok: balancer-readonly-reentrancy-getpooltokens  
        (, uint256[] memory registeredBalances, ) = getVault().getPoolTokens(getPoolId());
    }
}

contract Sentiment {

    function checkReentrancy() internal {
        vault.manageUserBalance(new IVault.UserBalanceOp[](0));
    }

    function getPrice(address token) external returns (uint) {
        checkReentrancy();
        (
            address[] memory poolTokens,
            uint256[] memory balances,
        // ok: balancer-readonly-reentrancy-getpooltokens
        ) = vault.getPoolTokens(IPool(token).getPoolId());
        
        //...
    }  
}

contract Testing {

    function getPrice(address token) external returns (uint) {
        
        (
            address[] memory poolTokens,
            uint256[] memory balances,
        // ok: balancer-readonly-reentrancy-getpooltokens
        ) = vault.getPoolTokens(IPool(token).getPoolId());
        
        vault.manageUserBalance(new IVault.UserBalanceOp[](0));

        //...
    }
}

contract TestingSecondCase {

    function checkReentrancy() internal {
        VaultReentrancyLib.ensureNotInVaultContext(getVault());
    }

    function getPrice(address token) external returns (uint) {
        checkReentrancy();
        
        (
            address[] memory poolTokens,
            uint256[] memory balances,
        // ok: balancer-readonly-reentrancy-getpooltokens
        ) = vault.getPoolTokens(IPool(token).getPoolId());
        
        //...
    }  

    function getPrice2(address token) external returns (uint) {
        
        (
            address[] memory poolTokens,
            uint256[] memory balances,
        // ruleid: balancer-readonly-reentrancy-getpooltokens
        ) = vault.getPoolTokens(IPool(token).getPoolId());
        
        //...
    }  

    function getPrice3(address token) external returns (uint) {
        VaultReentrancyLib.ensureNotInVaultContext(getVault());
        (
            address[] memory poolTokens,
            uint256[] memory balances,
        // ok: balancer-readonly-reentrancy-getpooltokens
        ) = vault.getPoolTokens(IPool(token).getPoolId());
        
        //...
    }  
}