Farseen0/scar-pairs huggingface dataset:
HIGH severity: `Silo_Module::silo_execute` will revert as approval is to the wrong contract. **Description:** D2 integrates with the lending protocol [Silo](https://www.silo.finance/) via the [`Silo_Module`](https://github.com/d2sd2s/d2-contracts/blob/c2fc257605ebc725525028a5c17f30c74202010b/contracts/modules/Silo.sol), which facilitates communication with the Silo contract.

One of the...

Vulnerable code:
function silo_execute(ISiloRouter.Action[] calldata actions) external onlyRole(EXECUTOR_ROLE) nonReentrant {
    // @audit approval should be to `address(router)` as it is the one facilitating the actions
    IERC20(actions[0].asset).approve(actions[0].silo, actions[0].amount);
    router.execute(actions);
}

function test_silo_execute() public {
    deal(WETH_ADDRESS,address(strategy),1e18);

    ISiloRouter.Action[] memory actions = new ISiloRouter.Action[](4);
    actions[0] = ISiloRouter.Action(ISiloRouter.ActionType.Deposit, GMX_SILO, WETH_ADDRESS, 1e18, false);
    actions[1] = ISiloRouter.Action(ISiloRouter.ActionType.Borrow, GMX_SILO, GMX_ADDRESS, 100e18, false);
    actions[2] = ISiloRouter.Action(ISiloRouter.ActionType.Repay, GMX_SILO, GMX_ADDRESS, 100e18, false);
    actions[3] = ISiloRouter.Action(ISiloRouter.ActionType.Withdraw, GMX_SILO, WETH_ADDRESS, 1e18-1, false);

    vm.prank(EXECUTOR);
    silo.silo_execute(actions);

    assertEq(gmxDebtToken.balanceOf(address(strategy)), 1);
    assertEq(gmxCollateralToken.balanceOf(address(strategy)), 0);
    assertEq(wethCollateralToken.balanceOf(address(strategy)), 0);
    assertEq(address(strategy).balance, 1e18-1);
}

- IERC20(actions[0].asset).approve(actions[0].silo, actions[0].amount);
+ IERC20(actions[0].asset).approve(address(router), actions[0].amount);
HIGH severity: Silo and Dolomite lack LTV limit increasing liquidation risk. **Description:** When borrowing from Aave, there is a check in [`Aave_Module::aave_borrow`](https://github.com/d2sd2s/d2-contracts/blob/c2fc257605ebc725525028a5c17f30c74202010b/contracts/modules/Aave.sol#L51-L54) to ensure that the loan-to-value (LTV) ratio remains below `MAX_LTV_FACTOR`...

Vulnerable code:
pool.borrow(asset, amount, interestRateMode, referralCode, onBehalfOf);
(uint256 totalCollateralBase, uint256 totalDebtBase, , , uint256 ltv, ) = pool.getUserAccountData(onBehalfOf);
uint256 maxDebtBase = (totalCollateralBase * ltv * MAX_LTV_FACTOR) / (BASIS_FACTOR * MANTISSA_FACTOR);
require(totalDebtBase <= maxDebtBase, "borrow amount exceeds max LTV");
HIGH severity: Unnecessary validation in `Bera_Module::bera_infrared_stake` should follow standard pattern. **Description:** In [`Bera_Module::bera_infrared_stake`](https://github.com/d2sd2s/d2-contracts/blob/c2fc257605ebc725525028a5c17f30c74202010b/contracts/modules/Bera.sol#L78-L82), there is a call to `validateToken(vault)`:

This is unusual because the validation is applied to `vault` rather than...

Vulnerable code:
function bera_infrared_stake(address vault, address token, uint256 amount) external onlyRole(EXECUTOR_ROLE) nonReentrant {
    validateToken(vault); // @audit unnecessary
    IERC20(token).approve(vault, amount);
    IInfraredVault(vault).stake(amount);
}

function bera_infrared_stake(address vault, address token, uint256 amount) external onlyRole(EXECUTOR_ROLE) nonReentrant {
-   validateToken(vault);
-   IERC20(token).approve(vault, amount);
    IInfraredVault(vault).stake(amount);
}
HIGH severity: Unused imports. **Description:** There are a couple of unused imports in the contracts:

These are imported in:
* [`Dolomite.sol#L6-L7`](https://github.com/d2sd2s/d2-contracts/blob/c2fc257605ebc725525028a5c17f30c74202010b/contracts/modules/Dolomite.sol#L6-L7)
 *...

Vulnerable code:
import "@solidstate/contracts/introspection/ERC165/base/ERC165Base.sol";
import "@solidstate/contracts/proxy/diamond/writable/DiamondWritableInternal.sol";

import "@openzeppelin/contracts/access/Ownable.sol";
HIGH severity: Aave `swapBorrowRateMode` is deprecated. **Description:** In the `Aave_Module` there is a call [`aave_swapBorrowRateMode`](https://github.com/d2sd2s/d2-contracts/blob/c2fc257605ebc725525028a5c17f30c74202010b/contracts/modules/Aave.sol#L67-L69):

The call `swapBorrowRateMode` is however...

Vulnerable code:
function aave_swapBorrowRateMode(address asset, uint256 interestRateMode) external onlyRole(EXECUTOR_ROLE) nonReentrant {
    pool.swapBorrowRateMode(asset, interestRateMode);
}
HIGH severity: Lack of events emitted on state changes. **Description:** The following functions should ideally emit an event to enhance transparency and traceability:

[`Vault::setDelegateRegistry`](https://github.com/stakedotlink/audit-2025-02-linkpool/blob/046c65a9c771315816bc59533183f52661af8e5e/contracts/linkStaking/base/Vault.sol#L180-L186) and...

Vulnerable code:
function setDelegateRegistry(address _delegateRegistry) external onlyOwner {
      delegateRegistry = _delegateRegistry;
+     emit SetDelegateRegistry(_delegateRegistry);
  }

function setNonLINKRewardReceiver(address _nonLINKRewardReceiver) external onlyOwner {
      nonLINKRewardReceiver = _nonLINKRewardReceiver;
+     emit SetNonLINKRewardReceiver(_nonLINKRewardReceiver);
  }

function withdrawTokenRewards(address[] calldata _vaults, address[] calldata _tokens) external {
      // ...
+     emit WithdrawTokenRewards(msg.sender, _vaults, _tokens);
  }
HIGH severity: Unnecessary token transfer when withdrawing reward tokens. **Description:** When claiming non-LINK reward tokens, the tokens are transferred `Vault -> FundFlowController -> nonLINKRewardReceiver`:

[`Vault::withdrawTokenRewards`](https://github.com/stakedotlink/audit-2025-02-linkpool/blob/046c65a9c771315816bc59533183f52661af8e5e/contracts/linkStaking/base/V...

Vulnerable code:
function withdrawTokenRewards(address[] calldata _tokens) external onlyFundFlowController {
    for (uint256 i = 0; i < _tokens.length; ++i) {
        IERC20Upgradeable rewardToken = IERC20Upgradeable(_tokens[i]);
        uint256 balance = rewardToken.balanceOf(address(this));
        if (balance != 0) rewardToken.safeTransfer(msg.sender, balance);
    }
}

function withdrawTokenRewards(address[] calldata _vaults, address[] calldata _tokens) external {
    for (uint256 i = 0; i < _vaults.length; ++i) {
        IVault(_vaults[i]).withdrawTokenRewards(_tokens);
    }

    for (uint256 i = 0; i < _tokens.length; ++i) {
        IERC20Upgradeable rewardToken = IERC20Upgradeable(_tokens[i]);
        if (address(rewardToken) == linkToken) revert InvalidToken();
        uint256 balance = rewardToken.balanceOf(address(this));
        if (balance != 0) rewardToken.safeTransfer(nonLINKRewardReceiver, balance);
    }
}

function withdrawTokenRewards(address[] calldata _vaults, address[] calldata _tokens) external {
    // cache linkToken
    address _linkToken = linkToken;

    // check for LINK token
    for (uint256 i = 0; i < _tokens.length; ) {
        if (_tokens[i] == _linkToken) revert InvalidToken();
        unchecked { ++i; }
    }

    for (uint256 i = 0; i < _vaults.length; ++i) {
        // add `nonLINKRewardReceiver` in the call to vault.withdrawTokenRewards
        IVault(_vaults[i]).withdrawTokenRewards(_tokens, nonLINKRewardReceiver);
    }
}
HIGH severity: Reward tokens can become inaccessible due to revert when `rewardPerTokenStored` is downcast and overflows. **Description:** The return value of `_newRewardPerToken()` that is assigned to the `rewardPerTokenStored` state variable present in all incentive logic contracts is not safe to be type casted to `uint96` because it can easily overflow. The computation in which the scaling up of precision by...

Vulnerable code:
function _newRewardPerToken(IncentivizedPoolId id, address token) internal view override returns (uint256) {
    uint256 _totalSupply = poolStates[id].totalLiquidity;
    RewardData memory _state = poolRewardData[id][token];
    if (_totalSupply == 0) return _state.rewardPerTokenStored;

    uint256 lastUpdateTimeApplicable = block.timestamp < _state.endTimestamp ? block.timestamp : _state.endTimestamp;

    // decimals analysis: (seconds * (rewardDecimals / seconds) * 18 decimals) / liquidityDecimals = rewardDecimals + 18 - liquidityDecimals
    return _state.rewardPerTokenStored
        + ((((lastUpdateTimeApplicable - _state.lastUpdateTime) * _state.ratePerSec) * UNIT) / _totalSupply);
}

function getLiquidityForAmounts(
    uint160 sqrtRatioX96,
    uint160 sqrtRatioAX96,
    uint160 sqrtRatioBX96,
    uint256 amount0,
    uint256 amount1
) internal pure returns (uint128 liquidity) {
    if (sqrtRatioAX96 > sqrtRatioBX96) (sqrtRatioAX96, sqrtRatioBX96) = (sqrtRatioBX96, sqrtRatioAX96);

    if (sqrtRatioX96 <= sqrtRatioAX96) {
        liquidity = getLiquidityForAmount0(sqrtRatioAX96, sqrtRatioBX96, amount0);
    } else if (sqrtRatioX96 < sqrtRatioBX96) {
        uint128 liquidity0 = getLiquidityForAmount0(sqrtRatioX96, sqrtRatioBX96, amount0);
        uint128 liquidity1 = getLiquidityForAmount1(sqrtRatioAX96, sqrtRatioX96, amount1);

        liquidity = liquidity0 < liquidity1 ? liquidity0 : liquidity1;
    } else {
        liquidity = getLiquidityForAmount1(sqrtRatioAX96, sqrtRatioBX96, amount1);
    }
}

function test_LessThan18Decimals() public {
    MockERC20 token0WithLessDecimals = new MockERC20("TEST1", "TEST1", 18);
    token0WithLessDecimals.mint(address(this), 1_000_000 * 10 ** 18);
    MockERC20 token1WithLessDecimals = new MockERC20("TEST2", "TEST2", 6);
    token1WithLessDecimals.mint(address(this), 1_000_000 * 10 ** 6);

    PoolKey memory newKey = createPoolKey(address(token0WithLessDecimals), address(token1WithLessDecimals), 3000, hookAddress);
    PoolId newId = newKey.toId();

    token0WithLessDecimals.approve(address(fullRange), type(uint256).max);
    token1WithLessDecimals.approve(address(fullRange), type(uint256).max);

    initPool(newKey.currency0, newKey.currency1, IHooks(hookAddress), 3000, SQRT_PRICE_1_1);

    uint128 liquidityMinted = fullRange.addLiquidity(
        FullRangeHook.AddLiquidityParams(
            newKey.currency0,
            newKey.currency1,
            3000,
            100 * 10 ** 18,
            100 * 10 ** 6,
            0,
            0,
            address(this),
            MAX_DEADLINE
        )
    );

    assertEq(liquidityMinted, 100 * 10 ** 6);
}
HIGH severity: `poolRewards` arrays can grow without bounds, resulting in DoS of core functionality. **Description:** The `poolRewards` mapping maintains an array of reward token addresses for a given `IncentivizedPoolId`. Given that the distribution of rewards is permissionless, anyone can invoke this functionality with arbitrary ERC-20 tokens to cause the array to grow without bounds. The...

Vulnerable code:
function _updateAllRewardState(IncentivizedPoolId id) internal virtual {
    address[] memory _tokens = poolRewards[id];
    uint256 _length = _tokens.length;
    for (uint256 i; i < _length; i++) {
        _updateRewardState(id, _tokens[i], address(0));
    }
}

function _updateAllUserState(IncentivizedPoolId id, address account) internal virtual {
    if(!userLiquiditySynced[id][account]) {
        _syncUserLiquidity(id, account);
        userLiquiditySynced[id][account] = true;
    }

    address[] memory _tokens = poolRewards[id];
    uint256 _length = _tokens.length;
    for (uint256 i; i < _length; i++) {
        _updateRewardState(id, _tokens[i], account);
    }
}

function earnedAll(IncentivizedPoolId id, address account) external view virtual returns (EarnedData[] memory _earnings) {
        address[] memory _tokens = poolRewards[id];
        uint256 _length = _tokens.length;
        _earnings = new EarnedData[](_length);
        for (uint256 i; i < _length; i++) {
            _earnings[i].token = _tokens[i];
            _earnings[i].amount = _earned(id, _tokens[i], account);
        }
    }
HIGH severity: Manipulation of price outside of the `FullRangeHook` liquidity range can result in DoS and financial loss to liquidity providers. **Description:** The minimum and maximum ticks used by the `FullRangeHook` differ from those defined in Uniswap V4 pools:

This means that there are two regions into which the price can move to become outside the range of the liquidity. This is against the intended behavior that the price of a...

Vulnerable code:
FullRangeHook.sol:
 /// @dev Min tick for full range with tick spacing of 60
int24 internal constant MIN_TICK = -887220;
/// @dev Max tick for full range with tick spacing of 60
int24 internal constant MAX_TICK = -MIN_TICK;

TickMath.sol:
/// @dev The minimum tick that may be passed to #getSqrtPriceAtTick computed from log base 1.0001 of 2**-128
/// @dev If ever MIN_TICK and MAX_TICK are not centered around 0, the absTick logic in getSqrtPriceAtTick cannot be used
int24 internal constant MIN_TICK = -887272;
/// @dev The maximum tick that may be passed to #getSqrtPriceAtTick computed from log base 1.0001 of 2**128
/// @dev If ever MIN_TICK and MAX_TICK are not centered around 0, the absTick logic in getSqrtPriceAtTick cannot be used
int24 internal constant MAX_TICK = 887272;

function getLiquidityForAmounts(
    uint160 sqrtRatioX96,
    uint160 sqrtRatioAX96,
    uint160 sqrtRatioBX96,
    uint256 amount0,
    uint256 amount1
) internal pure returns (uint128 liquidity) {
    if (sqrtRatioAX96 > sqrtRatioBX96) (sqrtRatioAX96, sqrtRatioBX96) = (sqrtRatioBX96, sqrtRatioAX96);

    if (sqrtRatioX96 <= sqrtRatioAX96) {
        liquidity = getLiquidityForAmount0(sqrtRatioAX96, sqrtRatioBX96, amount0);
    } else if (sqrtRatioX96 < sqrtRatioBX96) {
        uint128 liquidity0 = getLiquidityForAmount0(sqrtRatioX96, sqrtRatioBX96, amount0);
        uint128 liquidity1 = getLiquidityForAmount1(sqrtRatioAX96, sqrtRatioX96, amount1);

        liquidity = liquidity0 < liquidity1 ? liquidity0 : liquidity1;
    } else {
        liquidity = getLiquidityForAmount1(sqrtRatioAX96, sqrtRatioBX96, amount1);
    }
}

function test_LiquidityManipulation() public {
    uint160 sqrtMaxTickUniswap = TickMath.getSqrtPriceAtTick(887272);

    uint256 liquidity = LiquidityAmounts.getLiquidityForAmounts(
        SQRT_PRICE_1_1,
        TickMath.getSqrtPriceAtTick(MIN_TICK),
        TickMath.getSqrtPriceAtTick(MAX_TICK),
        100 * 10 ** 18,
        100 * 10 ** 18
    );

    uint256 manipulatedLiquidity = LiquidityAmounts.getLiquidityForAmounts(
        sqrtMaxTickUniswap,
        TickMath.getSqrtPriceAtTick(MIN_TICK),
        TickMath.getSqrtPriceAtTick(MAX_TICK),
        100 * 10 ** 18,
        100 * 10 ** 18
    );

    console.log(liquidity);
    console.log(manipulatedLiquidity);
}
HIGH severity: Reward distribution can be blocked by an initial distribution of long duration. **Description:** Given that distribution of rewards is permissionless, anyone can distribute any arbitrary amount of tokens with a duration greater than 1 week. If there is an existing distribution active for a given token and additional tokens are attempted to be distributed, the duration will be.....

Vulnerable code:
function depositRewards(IncentivizedPoolId id, address token, uint256 amount, uint256 duration)
    external
    override
    nonReentrant
{
    ...
    // Update the reward distribution parameters
    uint32 endTimestampCache = _state.endTimestamp;
    if (endTimestampCache < block.timestamp) {
        ...
    } else {
        // Calculates the remianing duration left for the current distribution
        uint256 remainingDuration = endTimestampCache - block.timestamp;
        if (remainingDuration + duration < MIN_DURATION) revert InvalidDuration();
        // And calculates the new duration
        uint256 newDuration = remainingDuration + duration;

        // Calculates the leftover rewards from the current distribution
        uint96 ratePerSecCache = _state.ratePerSec;
        uint256 leftoverRewards = ratePerSecCache * remainingDuration;

        // Calculates the new rate
        uint256 newRate = (amount + leftoverRewards) / newDuration;
        if (newRate < ratePerSecCache) revert CannotReduceRate();

        // Stores the new reward distribution parameters
        _state.ratePerSec = newRate.toUint96();
        _state.endTimestamp = (block.timestamp + newDuration).toUint32();
        _state.lastUpdateTime = (block.timestamp).toUint32();
    }
    ...
}

function test_DoSTokenIncentiveDistribution() public {
        PoolId poolKey;
        IncentivizedPoolId poolId;
        address lpToken = address(7);

        poolKey = createPoolKey(address(1), address(2), 3000).toId();
        poolId = IncentivizedPoolKey({ id: poolKey, lpToken: lpToken }).toId();

        manager.setListedPool(poolId, true);

        address[] memory systems = new address[](1);
        systems[0] = address(logic);

        manager.notifyAddLiquidty(systems, poolKey, lpToken, user1, int256(100 * 10 ** 18));

        uint256 amount = 1;
        uint256 duration = type(uint32).max - 1 - block.timestamp;

        logic.depositRewards(poolId, address(token0), amount, duration);

        skip(3 days);

        // If someone tries to deposit more tokens, the duration will overflow
        vm.expectRevert();
        logic.depositRewards(poolId, address(token0), 100 * 10 ** 18, 1 weeks);
    }
HIGH severity: Swaps performed in pools with zero liquidity can cause a complete DoS of `FullRangeHook`. **Description:** The `FullRangeHook` is characterized by rebalancing after every swap executed in the pool:

It first removes all the liquidity deposited to the pool for later price adjusting; however, this assumes that there is already liquidity in the position to be removed. Hence, if there is...

Vulnerable code:
function _rebalance(PoolKey memory key) public {
    PoolId poolId = key.toId();
    // Remove all the liquidity from the Pool
    (BalanceDelta balanceDelta,) = poolManager.modifyLiquidity(
        key,
        IPoolManager.ModifyLiquidityParams({
            tickLower: MIN_TICK,
            tickUpper: MAX_TICK,
            liquidityDelta: -(poolManager.getLiquidity(poolId).toInt256()),
            salt: 0
        }),
        ZERO_BYTES
    );
    ...
}

library Position {
    ...
    function update(
        State storage self,
        int128 liquidityDelta,
        uint256 feeGrowthInside0X128,
        uint256 feeGrowthInside1X128
    ) internal returns (uint256 feesOwed0, uint256 feesOwed1) {
        uint128 liquidity = self.liquidity;

        if (liquidityDelta == 0) {
            // disallow pokes for 0 liquidity positions
@>          if (liquidity == 0) CannotUpdateEmptyPosition.selector.revertWith();
        } else {
            self.liquidity = LiquidityMath.addDelta(liquidity, liquidityDelta);
        }
        ...
    }
}

function _unlockCallback(bytes calldata rawData) internal override returns (bytes memory) {
    ...
    // Rebalance the Pool if the Pool had swaps to adapt to new price
    if (pool.hasAccruedFees) {
        pool.hasAccruedFees = false;
        _rebalance(data.key);
    }
   ...
}
HIGH severity: Swaps are not possible on pools registered with Bunni due to incorrect access control in `ValkyrieHooklet::afterSwap`. **Description:** `ValkyrieHooklet::afterSwap` has the `onlyBunniHub` modifier applied; however, this hook is invoked from `BunniHook` (using the `BunniHookLogic` library) and not `BunniHub` (using the `BunniHubLogic` library). As such, this will result in a DoS of swap functionality for pools...

Vulnerable code:
function afterSwap(
    address,
    PoolKey calldata key,
    IPoolManager.SwapParams calldata,
    SwapReturnData calldata
) external override onlyBunniHub returns (bytes4 selector) {
    PoolId poolId = key.toId();
    if (address(incentiveManager) != address(0)) {
        incentiveManager.notifySwap(poolId, bunniTokens[poolId]);
    }
    return ValkyrieHooklet.afterSwap.selector;
}

function afterDeposit(
    address hooklet,
    address caller,
    IBunniHub.DepositParams calldata params,
    IHooklet.DepositReturnData calldata returnData
) external {
    ValkyrieHooklet(hooklet).afterDeposit(
        caller,
        params,
        returnData
    );
}
HIGH severity: Missing user liquidity sync in `TimeWeightedIncentiveLogic::_updateAllUserState` can result in rewards becoming locked. **Description:** Within the incentive logic contracts, liquidity synchronization is a necessary step that must be performed before executing any computation based on liquidity values in storage. In the case of total liquidity within `TimeWeightedIncentiveLogic`, an automatic sync is performed upon.....

Vulnerable code:
function _depositRewards(
    IncentivizedPoolId id,
    address token,
    uint256 amount,
    uint256 duration,
    uint256 requiredDuration,
    RewardType rewardType
) internal {
    ...
    // Sync pool total liquidity if not already done
    if(!poolSynced[id]) {
        _syncPoolLiquidity(id);
        poolSynced[id] = true;
    }

    emit RewardsDeposited(id, token, amount, duration);
}

function _updateAllUserState(IncentivizedPoolId id, address account) internal override {
    address[] memory _tokens = poolRewards[id];
    uint256 _length = _tokens.length;
    for (uint256 i; i < _length; i++) {
        _updateRewardState(id, _tokens[i], account);
    }
    _updateCheckpoints(id, account);
}

function test_MissingUserSync() public {
    skip(1 weeks);

    logic.updateDefaultFee(0);

    address[] memory emptySystems = new address[](0);
    IncentivizedPoolId incentivizedId = manager.getIncentivizedKey(pool1, lpToken1);

    manager.setListedPool(incentivizedId, true);

    // Register 100 tokens for the user1 for an empty logic
    // Manager.TotalLiquidity = 100
    // Manager.User1Liquidity = 100
    manager.notifyAddLiquidty(emptySystems, pool1, lpToken1, user1, int256(100 ether));

    address[] memory systems = new address[](1);
    systems[0] = address(logic);

    // 1000 tokens will be distributed during 1 week
    uint256 amount = 1000 ether;
    uint256 duration = 1 weeks;

    // Distributing rewards triggers total liquidity sync
    // Manager.TotalLiquidity = 200
    // Manager.User1Liquidity = 200
    // Logic.TotalLiquidity = 200
    // Logic.User1Liquidity = 0
    logic.depositRewards(incentivizedId, address(token0), amount, duration);

    // Register now 100 more tokens for the user1 once the new logic is already registered
    // Manager.TotalLiquidity = 200
    // Manager.User1Liquidity = 200
    // Logic.TotalLiquidity = 200
    // Logic.User1Liquidity = 100
    manager.notifyAddLiquidty(systems, pool1, lpToken1, user1, int256(100 ether));

    // Skip a lot of time to avoid the time weighted feature
    skip(10000000 weeks);

    uint256 tokensEarned = logic.claim(incentivizedId, address(token0), user1, user1);

    console.log(tokensEarned);
}