msc-smart-contract-auditing/vulnerable-functions-base huggingface dataset:
Vulnerability: ChainlinkOracleWrapper - latestRoundData might return stale results
Severity: medium

Description:
The oracle wrapper calls out to a chainlink oracle receiving the `latestRoundData()`. It then checks freshness by verifying that the answer is indeed for the last known round. The returned `updatedAt` timestamp is not checked.\\nIf there is a problem with chainlink starting a new round and finding consensus on the new value for the oracle (e.g. chainlink nodes abandon the oracle, chain congestion, vulnerability/attacks on the chainlink system) consumers of this contract may continue using outdated stale data (if oracles are unable to submit no new round is started)\\n```\\n/// @notice read the oracle price\\n/// @return oracle price\\n/// @return true if price is valid\\nfunction read() external view override returns (Decimal.D256 memory, bool) {\\n    (uint80 roundId, int256 price,,, uint80 answeredInRound) = chainlinkOracle.latestRoundData();\\n    bool valid = !paused() && price > 0 && answeredInRound == roundId;\\n\\n    Decimal.D256 memory value = Decimal.from(uint256(price)).div(oracleDecimalsNormalizer);\\n    return (value, valid);\\n}\\n```\\n\\n```\\n/// @notice determine if read value is stale\\n/// @return true if read value is stale\\nfunction isOutdated() external view override returns (bool) {\\n    (uint80 roundId,,,, uint80 answeredInRound) = chainlinkOracle.latestRoundData();\\n    return answeredInRound != roundId;\\n}\\n```\\n

Impact:

Recommended mitigation:
Consider checking the oracle responses `updatedAt` value after calling out to `chainlinkOracle.latestRoundData()` verifying that the result is within an allowed margin of freshness.
Vulnerability: CollateralizationOracle - missing events and incomplete event information
Severity: low

Description:
The `CollateralizationOracle.setDepositExclusion` function is used to exclude and re-include deposits from collateralization calculations. Unlike the other state-changing functions in this contract, it doesn't emit an event to inform about the exclusion or re-inclusion.\\n```\\nfunction setDepositExclusion(address \\_deposit, bool \\_excluded) external onlyGuardianOrGovernor {\\n    excludedDeposits[\\_deposit] = \\_excluded;\\n}\\n```\\n\\nThe `DepositAdd` event emits not only the deposit address but also the deposit's token. Despite the symmetry, the `DepositRemove` event does not emit the token.\\n```\\nevent DepositAdd(address from, address indexed deposit, address indexed token);\\nevent DepositRemove(address from, address indexed deposit);\\n```\\n

Impact:

Recommended mitigation:
`setDepositInclusion` should emit an event that informs about the deposit and whether it was included or excluded.\\nFor symmetry reasons and because it is indeed useful information, the `DepositRemove` event could include the deposit's token.
Vulnerability: RateLimited - Contract starts with a full buffer at deployment
Severity: low

Description:
A contract that inherits from `RateLimited` starts out with a full buffer when it is deployed.\\n```\\n\\_bufferStored = \\_bufferCap;\\n```\\n\\nThat means the full `bufferCap` is immediately available after deployment; it doesn't have to be built up over time. This behavior might be unexpected.

Impact:

Recommended mitigation:
We recommend starting with an empty buffer, or - if there are valid reasons for the current implementation - at least document it clearly.
Vulnerability: BalancerLBPSwapper - tokenSpent and tokenReceived should be immutable
Severity: low

Description:
Acc. to the inline comment both `tokenSpent` and `tokenReceived` should be immutable but they are not declared as such.\\n```\\n// tokenSpent and tokenReceived are immutable\\ntokenSpent = \\_tokenSpent;\\ntokenReceived = \\_tokenReceived;\\n```\\n\\n```\\n/// @notice the token to be auctioned\\naddress public override tokenSpent;\\n\\n/// @notice the token to buy\\naddress public override tokenReceived;\\n```\\n

Impact:

Recommended mitigation:
Declare both variable `immutable`.
Vulnerability: CollateralizationOracle - potentially unsafe casts
Severity: low

Description:
`protocolControlledValue` is the cumulative USD token value of all tokens in the PCV. The USD value is determined using external chainlink oracles. To mitigate some effects of attacks on chainlink to propagate to this protocol it is recommended to implement a defensive approach to handling values derived from the external source. Arithm. overflows are checked by the compiler (0.8.4), however, it does not guarantee safe casting from unsigned to signed integer. The scenario of this happening might be rather unlikely, however, there is no guarantee that the external price-feed is not taken over by malicious actors and this is when every line of defense counts.\\n```\\n//solidity 0.8.7\\n »  int(uint(2\\*\\*255))\\n-57896044618658097711785492504343953926634992332820282019728792003956564819968\\n »  int(uint(2\\*\\*255-2))\\n57896044618658097711785492504343953926634992332820282019728792003956564819966\\n```\\n\\n```\\nprotocolEquity = int256(protocolControlledValue) - int256(userCirculatingFei);\\n```\\n\\n```\\nprotocolControlledValue += \\_oraclePrice.mul(\\_totalTokenBalance).asUint256();\\n```\\n

Impact:

Recommended mitigation:
Perform overflow checked SafeCast as another line of defense against oracle manipulation.
Vulnerability: FeiTimedMinter - constructor does not enforce the same boundaries as setter for frequency
Severity: low

Description:
The setter method for `frequency` enforced upper and lower bounds while the constructor does not. Users cannot trust that the `frequency` is actually set to be within bounds on deployment.\\n```\\nconstructor(\\n    address \\_core,\\n    address \\_target,\\n    uint256 \\_incentive,\\n    uint256 \\_frequency,\\n    uint256 \\_initialMintAmount\\n)\\n    CoreRef(\\_core)\\n    Timed(\\_frequency)\\n    Incentivized(\\_incentive)\\n    RateLimitedMinter((\\_initialMintAmount + \\_incentive) / \\_frequency, (\\_initialMintAmount + \\_incentive), true)\\n{\\n    \\_initTimed();\\n\\n    \\_setTarget(\\_target);\\n    \\_setMintAmount(\\_initialMintAmount);\\n}\\n```\\n\\n```\\nfunction setFrequency(uint256 newFrequency) external override onlyGovernorOrAdmin {\\n    require(newFrequency >= MIN\\_MINT\\_FREQUENCY, "FeiTimedMinter: frequency low");\\n    require(newFrequency <= MAX\\_MINT\\_FREQUENCY, "FeiTimedMinter: frequency high");\\n\\n    \\_setDuration(newFrequency);\\n}\\n```\\n

Impact:

Recommended mitigation:
Perform the same checks on `frequency` in the constructor as in the `setFrequency` method.\\nThis contract is also inherited by a range of contracts that might specify different boundaries to what is hardcoded in the `FeiTimedMinter`. A way to enforce bounds-checks could be to allow overriding the setter method and using the setter in the constructor as well ensuring that bounds are also checked on deployment.
Vulnerability: CollateralizationOracle - swapDeposit should call internal functions to remove/add deposits
Severity: low

Description:
Instead of calling `removeDeposit` and `addDeposit`, `swapDeposit` should call its internal sister functions `_removeDeposit` and `_addDeposit` to avoid running the `onlyGovernor` checks multiple times.\\n```\\n/// @notice Swap a PCVDeposit with a new one, for instance when a new version\\n/// of a deposit (holding the same token) is deployed.\\n/// @param \\_oldDeposit : the PCVDeposit to remove from the list.\\n/// @param \\_newDeposit : the PCVDeposit to add to the list.\\nfunction swapDeposit(address \\_oldDeposit, address \\_newDeposit) external onlyGovernor {\\n    removeDeposit(\\_oldDeposit);\\n    addDeposit(\\_newDeposit);\\n}\\n```\\n

Impact:

Recommended mitigation:
Call the internal functions instead. addDeposit's and removeDeposit's visibility can then be changed from `public` to `external`.
Vulnerability: CollateralizationOracle - misleading comments
Severity: low

Description:
According to an inline comment in `isOvercollateralized`, the validity status of `pcvStats` is ignored, while it is actually being checked.\\nSimilarly, a comment in `pcvStats` mentions that the returned `protocolEquity` is 0 if there is less PCV than circulating FEI, while in reality, `pcvStats` always returns the difference between the former and the latter, even if it is negative.\\n```\\n/// Controlled Value) than the circulating (user-owned) FEI, i.e.\\n/// a positive Protocol Equity.\\n/// Note: the validity status is ignored in this function.\\nfunction isOvercollateralized() external override view whenNotPaused returns (bool) {\\n    (,, int256 \\_protocolEquity, bool \\_valid) = pcvStats();\\n    require(\\_valid, "CollateralizationOracle: reading is invalid");\\n    return \\_protocolEquity > 0;\\n}\\n```\\n\\n```\\n/// @return protocolEquity : the difference between PCV and user circulating FEI.\\n/// If there are more circulating FEI than $ in the PCV, equity is 0.\\n```\\n\\n```\\nprotocolEquity = int256(protocolControlledValue) - int256(userCirculatingFei);\\n```\\n

Impact:

Recommended mitigation:
Revise the comments.
Vulnerability: The withdrawUnstakedTokens may run out of gas
Severity: high

Description:
The `withdrawUnstakedTokens` is iterating over all batches of unstaked tokens. One user, if unstaked many times, could get their tokens stuck in the contract.\\n```\\nfunction withdrawUnstakedTokens(address staker)\\n public\\n virtual\\n override\\n whenNotPaused\\n{\\n require(staker == \\_msgSender(), "LQ20");\\n uint256 \\_withdrawBalance;\\n uint256 \\_unstakingExpirationLength = \\_unstakingExpiration[staker]\\n  .length;\\n uint256 \\_counter = \\_withdrawCounters[staker];\\n for (\\n  uint256 i = \\_counter;\\n  i < \\_unstakingExpirationLength;\\n  i = i.add(1)\\n ) {\\n  //get getUnstakeTime and compare it with current timestamp to check if 21 days + epoch difference has passed\\n  (uint256 \\_getUnstakeTime, , ) = getUnstakeTime(\\n   \\_unstakingExpiration[staker][i]\\n  );\\n  if (block.timestamp >= \\_getUnstakeTime) {\\n   //if 21 days + epoch difference has passed, then add the balance and then mint uTokens\\n   \\_withdrawBalance = \\_withdrawBalance.add(\\n    \\_unstakingAmount[staker][i]\\n   );\\n   \\_unstakingExpiration[staker][i] = 0;\\n   \\_unstakingAmount[staker][i] = 0;\\n   \\_withdrawCounters[staker] = \\_withdrawCounters[staker].add(1);\\n  }\\n }\\n\\n require(\\_withdrawBalance > 0, "LQ21");\\n emit WithdrawUnstakeTokens(staker, \\_withdrawBalance, block.timestamp);\\n \\_uTokens.mint(staker, \\_withdrawBalance);\\n}\\n```\\n

Impact:

Recommended mitigation:
Resolution\\nComment from pSTAKE Finance team:\\nHave implemented a batchingLimit variable which enforces a definite number of iterations during withdrawal of unstaked tokens, instead of indefinite iterations.\\nLimit the number of processed unstaked batches, and possibly add pagination.
Vulnerability: The _calculatePendingRewards can run out of gas
Severity: medium

Description:
The reward rate in STokens can be changed, and the history of these changes are stored in the contract:\\n```\\nfunction setRewardRate(uint256 rewardRate)\\n public\\n virtual\\n override\\n returns (bool success)\\n{\\n // range checks for rewardRate. Since rewardRate cannot be more than 100%, the max cap\\n // is \\_valueDivisor \\* 100, which then brings the fees to 100 (percentage)\\n require(rewardRate <= \\_valueDivisor.mul(100), "ST17");\\n require(hasRole(DEFAULT\\_ADMIN\\_ROLE, \\_msgSender()), "ST2");\\n \\_rewardRate.push(rewardRate);\\n \\_lastMovingRewardTimestamp.push(block.timestamp);\\n emit SetRewardRate(rewardRate);\\n\\n return true;\\n}\\n```\\n\\nWhen the reward is calculated `for` each user, all changes of the `_rewardRate` are considered. So there is a `for` loop that iterates over all changes since the last reward update. If the reward rate was changed many times, the `_calculatePendingRewards` function could run out of gas.

Impact:

Recommended mitigation:
Provide an option to partially update the reward, so the full update can be split in multiple transactions.
Vulnerability: The calculateRewards should not be callable by the whitelisted contract
Severity: medium

Description:
The `calculateRewards` function should only be called for non-whitelisted addresses:\\n```\\nfunction calculateRewards(address to)\\n public\\n virtual\\n override\\n whenNotPaused\\n returns (bool success)\\n{\\n require(to == \\_msgSender(), "ST5");\\n uint256 reward = \\_calculateRewards(to);\\n emit TriggeredCalculateRewards(to, reward, block.timestamp);\\n return true;\\n}\\n```\\n\\nFor all the whitelisted addresses, the `calculateHolderRewards` function is called. But if the `calculateRewards` function is called by the whitelisted address directly, the function will execute, and the rewards will be distributed to the caller instead of the intended recipients.

Impact:

Recommended mitigation:
Resolution\\nComment from pSTAKE Finance team:\\nHave created a require condition in Smart Contract code to disallow whitelisted contracts from calling the function\\nWhile this scenario is unlikely to happen, adding the additional check in the `calculateRewards` is a good option.
Vulnerability: Presence of testnet code
Severity: medium

Description:
Based on the discussions with pStake team and in-line comments, there are a few instances of code and commented code in the code base under audit that are not finalized for mainnet deployment.\\n```\\nfunction initialize(address pauserAddress) public virtual initializer {\\n \\_\\_ERC20\\_init("pSTAKE Token", "PSTAKE");\\n \\_\\_AccessControl\\_init();\\n \\_\\_Pausable\\_init();\\n \\_setupRole(DEFAULT\\_ADMIN\\_ROLE, \\_msgSender());\\n \\_setupRole(PAUSER\\_ROLE, pauserAddress);\\n // PSTAKE IS A SIMPLE ERC20 TOKEN HENCE 18 DECIMAL PLACES\\n \\_setupDecimals(18);\\n // pre-allocate some tokens to an admin address which will air drop PSTAKE tokens\\n // to each of holder contracts. This is only for testnet purpose. in Mainnet, we\\n // will use a vesting contract to allocate tokens to admin in a certain schedule\\n \\_mint(\\_msgSender(), 5000000000000000000000000);\\n}\\n```\\n\\nThe initialize function currently mints all the tokens to msg.sender, however the goal for mainnet is to use a vesting contract which is not present in the current code.

Impact:

Recommended mitigation:
It is recommended to fully test the final code before deployment to the mainnet.
Vulnerability: Sanity check on all important variables
Severity: low

Description:
Most of the functionalities have proper sanity checks when it comes to setting system-wide variables, such as whitelist addresses. However there are a few key setters that lack such sanity checks.\\nSanity check (!= address(0)) on all token contracts.\\n```\\nfunction setUTokensContract(address uAddress) public virtual override {\\n require(hasRole(DEFAULT\\_ADMIN\\_ROLE, \\_msgSender()), "LP9");\\n \\_uTokens = IUTokens(uAddress);\\n emit SetUTokensContract(uAddress);\\n}\\n\\n/\\*\\*\\n \\* @dev Set 'contract address', called from constructor\\n \\* @param sAddress: stoken contract address\\n \\*\\n \\* Emits a {SetSTokensContract} event with '\\_contract' set to the stoken contract address.\\n \\*\\n \\*/\\nfunction setSTokensContract(address sAddress) public virtual override {\\n require(hasRole(DEFAULT\\_ADMIN\\_ROLE, \\_msgSender()), "LP10");\\n \\_sTokens = ISTokens(sAddress);\\n emit SetSTokensContract(sAddress);\\n}\\n\\n/\\*\\*\\n \\* @dev Set 'contract address', called from constructor\\n \\* @param pstakeAddress: pStake contract address\\n \\*\\n \\* Emits a {SetPSTAKEContract} event with '\\_contract' set to the stoken contract address.\\n \\*\\n \\*/\\nfunction setPSTAKEContract(address pstakeAddress) public virtual override {\\n require(hasRole(DEFAULT\\_ADMIN\\_ROLE, \\_msgSender()), "LP11");\\n \\_pstakeTokens = IPSTAKE(pstakeAddress);\\n emit SetPSTAKEContract(pstakeAddress);\\n}\\n```\\n\\nSanity check on `unstakingLockTime` to be in the acceptable range (21 hours to 21 days)\\n```\\n/\\*\\*\\n \\* @dev Set 'unstake props', called from admin\\n \\* @param unstakingLockTime: varies from 21 hours to 21 days\\n \\*\\n \\* Emits a {SetUnstakeProps} event with 'fee' set to the stake and unstake.\\n \\*\\n \\*/\\nfunction setUnstakingLockTime(uint256 unstakingLockTime)\\n public\\n virtual\\n returns (bool success)\\n{\\n require(hasRole(DEFAULT\\_ADMIN\\_ROLE, \\_msgSender()), "LQ3");\\n \\_unstakingLockTime = unstakingLockTime;\\n emit SetUnstakingLockTime(unstakingLockTime);\\n return true;\\n}\\n```\\n

Impact:

Recommended mitigation:
Resolution\\nComment from pSTAKE Finance team:\\nPost the implementation of new emission logic there have been a rearrangement of some variables, but the rest have been sanity tested and corrected
Vulnerability: TransactionManager - Receiver-side check also on sending side Fix
Severity: high

Description:
The functions `prepare`, `cancel`, and `fulfill` in the `TransactionManager` all have a “common part” that is executed on both the sending and the receiving chain and side-specific parts that are only executed either on the sending or on the receiving side.\\nThe following lines occur in fulfill's common part, but this should only be checked on the receiving chain. In fact, on the sending chain, we might even compare amounts of different assets.\\n```\\n// Sanity check: fee <= amount. Allow `=` in case of only wanting to execute\\n// 0-value crosschain tx, so only providing the fee amount\\nrequire(relayerFee <= txData.amount, "#F:023");\\n```\\n\\nThis could prevent a legitimate `fulfill` on the sending chain, causing a loss of funds for the router.

Impact:

Recommended mitigation:
Resolution\\nThe Connext team claims to have fixed this in commit `4adbfd52703441ee5de655130fc2e0252eae4661`. We have not reviewed this commit or, generally, the codebase at this point.\\nMove these lines to the receiving-side part.\\nRemark\\nThe `callData` supplied to `fulfill` is not used at all on the sending chain, but the check whether its hash matches `txData.callDataHash` happens in the common part.\\n```\\n// Check provided callData matches stored hash\\nrequire(keccak256(callData) == txData.callDataHash, "#F:024");\\n```\\n\\nIn principle, this check could also be moved to the receiving-chain part, allowing the router to save some gas by calling sending-side `fulfill` with empty `callData` and skip the check. Note, however, that the `TransactionFulfilled` event will then also emit the “wrong” `callData` on the sending chain, so the off-chain code has to be able to deal with that if you want to employ this optimization.
Vulnerability: TransactionManager - Relayer may use user's cancel after expiry signature to steal user's funds by colluding with a router  Acknowledged
Severity: medium

Description:
Users that are willing to have a lower trust dependency on a relayer should have the ability to opt-in only for the service that allows the relayer to withdraw back users' funds from the sending chain after expiry. However, in practice, a user is forced to opt-in for the service that refunds the router before the expiry, since the same signature is used for both services (lines 795,817 use the same signature).\\nLet's consider the case of a user willing to call `fulfill` on his own, but to use the relayer only to withdraw back his funds from the sending chain after expiry. In this case, the relayer can collude with the router and use the user's `cancel` signature (meant for withdrawing his only after expiry) as a front-running transaction for a user call to `fulfill`. This way the router will be able to withdraw both his funds and the user's funds since the user's `fulfill` signature is now public data residing in the mem-pool.\\n```\\n      require(msg.sender == txData.user || recoverSignature(txData.transactionId, relayerFee, "cancel", signature) == txData.user, "#C:022");\\n\\n      Asset.transferAsset(txData.sendingAssetId, payable(msg.sender), relayerFee);\\n    }\\n\\n    // Get the amount to refund the user\\n    uint256 toRefund;\\n    unchecked {\\n      toRefund = amount - relayerFee;\\n    }\\n\\n    // Return locked funds to sending chain fallback\\n    if (toRefund > 0) {\\n      Asset.transferAsset(txData.sendingAssetId, payable(txData.sendingChainFallback), toRefund);\\n    }\\n  }\\n\\n} else {\\n  // Receiver side, router liquidity is returned\\n  if (txData.expiry >= block.timestamp) {\\n    // Timeout has not expired and tx may only be cancelled by user\\n    // Validate signature\\n    require(msg.sender == txData.user || recoverSignature(txData.transactionId, relayerFee, "cancel", signature) == txData.user, "#C:022");\\n```\\n

Impact:

Recommended mitigation:
The crucial point here is that the user must never sign a “cancel” that could be used on the receiving chain while fulfillment on the sending chain is still a possibility.\\nOr, to put it differently: A user may only sign a “cancel” that is valid on the receiving chain after sending-chain expiry or if they never have and won't ever sign a “fulfill” (or at least won't sign until sending-chain expiry — but it is pointless to sign a “fulfill” after that, so “never” is a reasonable simplification).\\nOr, finally, a more symmetric perspective on this requirement: If a user has signed “fulfill”, they must not sign a receiving-chain-valid “cancel” until sending-chain expiry, and if they have signed a receiving-chain-valid “cancel”, they must not sign a “fulfill” (until sending-chain expiry).\\nIn this sense, “cancel” signatures that are valid on the receiving chain are dangerous, while sending-side cancellations are not. So the principle stated in the previous paragraph might be easier to follow with different signatures for sending- and receiving-chain cancellations.
Vulnerability: ProposedOwnable - two-step ownership transfer should be confirmed by the new owner
Severity: medium

Description:
In order to avoid losing control of the contract, the two-step ownership transfer should be confirmed by the new owner's address instead of the current owner.\\n`acceptProposedOwner` is restricted to `onlyOwner` while ownership should be accepted by the newOwner\\n```\\n/\\*\\*\\n \\* @notice Transfers ownership of the contract to a new account (`newOwner`).\\n \\* Can only be called by the current owner.\\n \\*/\\nfunction acceptProposedOwner() public virtual onlyOwner {\\n  require((block.timestamp - \\_proposedTimestamp) > \\_delay, "#APO:030");\\n  \\_setOwner(\\_proposed);\\n}\\n```\\n\\nmove `renounced()` to `ProposedOwnable` as this is where it logically belongs to\\n```\\nfunction renounced() public view override returns (bool) {\\n  return owner() == address(0);\\n}\\n```\\n\\n`onlyOwner` can directly access state-var `_owner` instead of spending more gas on calling `owner()`\\n```\\nmodifier onlyOwner() {\\n    require(owner() == msg.sender, "#OO:029");\\n    \\_;\\n}\\n```\\n

Impact:

Recommended mitigation:
Resolution\\nAll recommendations given below have been implemented. In addition to that, the privilege to manage assets and the privilege to manage routers can now be renounced separately.\\n`onlyOwner` can directly access `_owner` (gas optimization)\\nadd a method to explicitly renounce ownership of the contract\\nmove `TransactionManager.renounced()` to `ProposedOwnable` as this is where it logically belongs to\\nchange the access control for `acceptProposedOwner` from `onlyOwner` to `require(msg.sender == _proposed)` (new owner).