Farseen0/scar-pairs huggingface dataset:
MEDIUM severity: `Goldigovernor._getProposalState()` shouldn't use `totalSupply`. **Severity:** Medium

**Description:** In `_getProposalState()`, it uses `Goldiswap(goldiswap).totalSupply()` during the comparison.

As `totalSupply` is increasing in real time, a `Queued` proposal might be changed to `Defeated` one unexpectedly due to the increased supply.

**Impact:** A...

Vulnerable code:
function _getProposalState(uint256 proposalId) internal view returns (ProposalState) {
    Proposal storage proposal = proposals[proposalId];
    if (proposal.cancelled) return ProposalState.Canceled;
    else if (block.number <= proposal.startBlock) return ProposalState.Pending;
    else if (block.number <= proposal.endBlock) return ProposalState.Active;
    else if (proposal.eta == 0) return ProposalState.Succeeded;
    else if (proposal.executed) return ProposalState.Executed;
    else if (proposal.forVotes <= proposal.againstVotes || proposal.forVotes < Goldiswap(goldiswap).totalSupply() / 20) { //@audit shouldn't use totalSupply
      return ProposalState.Defeated;
    }
    else if (block.timestamp >= proposal.eta + Timelock(timelock).GRACE_PERIOD()) {
      return ProposalState.Expired;
    }
    else {
      return ProposalState.Queued;
    }
  }
MEDIUM severity: In `Goldivault.redeemYield()`, users can redeem more yield tokens using reentrancy. **Severity:** Medium

**Description:** Possible reentrancy in `Goldivault.redeemYield()` if `yieldToken` has a `beforeTokenTransfer` hook.

- Let's assume `yt.totalSupply = 100, yieldToken.balance = 100` and the user has 20 yt.
- The user calls `redeemYield()` with 10 yt.
- Then `yt.totalSupply`...

Vulnerable code:
function redeemYield(uint256 amount) external {
    if(amount == 0) revert InvalidRedemption();
    if(block.timestamp < concludeTime + delay || !concluded) revert NotConcluded();
    uint256 yieldShare = FixedPointMathLib.divWad(amount, ERC20(yt).totalSupply());
    YieldToken(yt).burnYT(msg.sender, amount);
    uint256 yieldTokensLength = yieldTokens.length;
    for(uint8 i; i < yieldTokensLength; ++i) {
      uint256 finalYield;
      if(yieldTokens[i] == depositToken) {
        finalYield = ERC20(yieldTokens[i]).balanceOf(address(this)) - depositTokenAmount;
      }
      else {
        finalYield = ERC20(yieldTokens[i]).balanceOf(address(this));
      }
      uint256 claimable = FixedPointMathLib.mulWad(finalYield, yieldShare);
      SafeTransferLib.safeTransfer(yieldTokens[i], msg.sender, claimable);
    }
    emit YieldTokenRedemption(msg.sender, amount);
  }
MEDIUM severity: Wrong validation in `Goldigovernor.cancel()`. **Severity:** Medium

**Description:** In `Goldigovernor.cancel()`, the proposer should have fewer votes than `proposalThreshold` to cancel his proposal.

**Impact:** A proposer can't cancel his proposal unless he decreases his voting power.

**Recommended Mitigation:** It should be modified like...

Vulnerable code:
function cancel(uint256 proposalId) external {
    if(_getProposalState(proposalId) == ProposalState.Executed) revert InvalidProposalState();
    Proposal storage proposal = proposals[proposalId];
    if(msg.sender != proposal.proposer) revert NotProposer();
    if(GovLocks(govlocks).getPriorVotes(proposal.proposer, block.number - 1) > proposalThreshold) revert AboveThreshold(); //@audit incorrect
    proposal.cancelled = true;
    uint256 targetsLength = proposal.targets.length;
    for (uint256 i = 0; i < targetsLength; i++) {
      Timelock(timelock).cancelTransaction(proposal.targets[i], proposal.eta, proposal.values[i], proposal.calldatas[i], proposal.signatures[i]);
    }
    emit ProposalCanceled(proposalId);
  }

if(msg.sender != proposal.proposer && GovLocks(govlocks).getPriorVotes(proposal.proposer, block.number - 1) > proposalThreshold) revert Error;
MEDIUM severity: Users wouldn't cancel their proposals due to the increased `proposalThreshold`.. **Severity:** Medium

**Description:** When users call `cancel()`, it validates the caller's voting power with `proposalThreshold` which can be changed using `setProposalThreshold()`.

Here is a possible scenario.
- Let's assume `proposalThreshold = 100` and a user has 100 voting power.
- The user.....

Vulnerable code:
function setProposalThreshold(uint256 newProposalThreshold) external {
    if(msg.sender != multisig) revert NotMultisig();
    if(newProposalThreshold < MIN_PROPOSAL_THRESHOLD || newProposalThreshold > MAX_PROPOSAL_THRESHOLD) revert InvalidVotingParameter();
    uint256 oldProposalThreshold = proposalThreshold;
    proposalThreshold = newProposalThreshold;
    emit ProposalThresholdSet(oldProposalThreshold, proposalThreshold);
  }
MEDIUM severity: `Goldilend.liquidate()` might revert due to underflow. **Severity:** Medium

**Description:** In `repay()`, there would be a rounding during the `interest` calculation.

Here is a possible scenario.
- There are 2 borrowers of `borrowedAmount = 100, interest = 10`. And `outstandingDebt = 2 * (100 - 10) = 180`.
- The first borrower calls `repay()` with...

Vulnerable code:
function repay(uint256 repayAmount, uint256 _userLoanId) external {
      Loan memory userLoan = loans[msg.sender][_userLoanId];
      if(userLoan.borrowedAmount < repayAmount) revert ExcessiveRepay();
      if(block.timestamp > userLoan.endDate) revert LoanExpired();
      uint256 interestLoanRatio = FixedPointMathLib.divWad(userLoan.interest, userLoan.borrowedAmount);
L425  uint256 interest = FixedPointMathLib.mulWadUp(repayAmount, interestLoanRatio); //@audit rounding issue
      outstandingDebt -= repayAmount - interest > outstandingDebt ? outstandingDebt : repayAmount - interest;
      ...
  }
...
  function liquidate(address user, uint256 _userLoanId) external {
      Loan memory userLoan = loans[msg.sender][_userLoanId];
      if(block.timestamp < userLoan.endDate || userLoan.liquidated || userLoan.borrowedAmount == 0) revert Unliquidatable();
      loans[user][_userLoanId].liquidated = true;
      loans[user][_userLoanId].borrowedAmount = 0;
L448  outstandingDebt -= userLoan.borrowedAmount - userLoan.interest;
      ...
  }

/// @inheritdoc IGoldilend
  function liquidate(address user, uint256 _userLoanId) external {
    Loan memory userLoan = loans[msg.sender][_userLoanId];
    if(block.timestamp < userLoan.endDate || userLoan.liquidated || userLoan.borrowedAmount == 0) revert Unliquidatable();
    loans[user][_userLoanId].liquidated = true;
    loans[user][_userLoanId].borrowedAmount = 0;
+  uint256 debtToRepay = userLoan.borrowedAmount - userLoan.interest;
+  outstandingDebt -= debtToRepay > outstandingDebt ? outstandingDebt : debtToRepay;
   ...
  }
MEDIUM severity: In `Goldigovernor`, wrong assumption of block time. **Severity:** Medium

**Description:** In `Goldigovernor.sol`, voting period/delay limits are set with 15s block time.

But Berachain has 5s block time according to [its documentation](https://docs.berachain.com/faq/#how-well-does-berachain-perform).

So these limits will be set shorter than...

Vulnerable code:
/// @notice Minimum voting period
  uint32 public constant MIN_VOTING_PERIOD = 5760; // About 24 hours

  /// @notice Maximum voting period
  uint32 public constant MAX_VOTING_PERIOD = 80640; // About 2 weeks

  /// @notice Minimum voting delay
  uint32 public constant MIN_VOTING_DELAY = 1;

  /// @notice Maximum voting delay
  uint32 public constant MAX_VOTING_DELAY = 40320; // About 1 week

Berachain has the following properties:

- Block time: 5s
LOW severity: `Goldivault.changeProtocolParameters()` shouldn't update `endTime`.. **Description:** `Goldivault.changeProtocolParameters()` updates `endTime` only without changing `startTime`.

It's more appropriate not to update `endTime` as this function is just to change parameters.

**Client:** Fixed in [PR #11](https://github.com/0xgeeb/goldilocks-core/pull/11)

**Cyfrin:**.....

Vulnerable code:
function changeProtocolParameters(
    uint256 _earlyWithdrawalFee,
    uint256 _yieldFee,
    uint256 _delay,
    uint256 _duration
  ) external {
    if(msg.sender != timelock) revert NotTimelock();
    earlyWithdrawalFee = _earlyWithdrawalFee;
    yieldFee = _yieldFee;
    delay = _delay;
    duration = _duration;
    endTime = block.timestamp + _duration;
  }
LOW severity: `Goldilocked._lockedLocks()` should round up.. **Description:** Users might borrow 1 more wei as it rounds down.

**Client:** Fixed in [PR #15](https://github.com/0xgeeb/goldilocks-core/pull/15)

**Cyfrin:** Verified.

Vulnerable code:
function _lockedLocks(address user) internal view returns (uint256) {
    return FixedPointMathLib.divWad(borrowedHoney[user], IGoldiswap(goldiswap).floorPrice());//@audit round up
  }
LOW severity: Inconsistent comparison while checking `eta`. **Description:** In `Goldigovernor` and `Timelock`, inconsistent comparisons are used.

**Client:** Fixed in [PR #6](https://github.com/0xgeeb/goldilocks-core/pull/6)

**Cyfrin:** Verified.

Vulnerable code:
File: Goldigovernor.sol
386:     else if (block.timestamp >= proposal.eta + Timelock(timelock).GRACE_PERIOD()) {
387:       return ProposalState.Expired;
388:     }

File: Timelock.sol
138:     if(block.timestamp > eta + GRACE_PERIOD) revert TxStale();
LOW severity: Wrong comment. **Description:**

**Client:** Fixed in [PR #16](https://github.com/0xgeeb/goldilocks-core/pull/16)

**Cyfrin:** Verified.

Vulnerable code:
File: Timelock.sol
49:   /// @notice Delay for queueing a transaction in blocks //@audit seconds, not blocks
60:   /// @param _delay Delay the timelock will use, in blocks //@audit seconds, not blocks
103:   /// @param eta Duration of time until transaction can be executed, in blocks //@audit seconds, not blocks
123:   /// @param eta Duration of time until transaction can be executed, in blocks //@audit seconds, not blocks
154:   /// @param eta Duration of time until transaction can be executed, in blocks //@audit seconds, not blocks

File: Goldilend.sol
496:   /// @notice Calculates claimable Porridge per GiBGT //@audit claimable reward token
608:   /// @dev Claims existing vault rewards and updates poolSize //@audit this function doesn't update poolSize
LOW severity: Missing bracket in docs. **Description:** [The document](https://goldilocks-1.gitbook.io/goldidocs/locks/the-price-function) is missing a bracket.

**Client:** Acknowledged, this has been corrected.

**Cyfrin:** Acknowledged.

Vulnerable code:
- Market price = Floor Price + ((PSL/Supply)*(FSL+PSL/FSL)^6)
+ Market price = Floor Price + (PSL/Supply)*((FSL+PSL)/FSL)^6
LOW severity: Rounding loss of 1 wei. **Description:** In `Goldivault`, sum of 2 amounts might be less than `amount` due to the rounding loss.

**Client:** Fixed in [PR #17](https://github.com/0xgeeb/goldilocks-core/pull/17)

**Cyfrin:** Verified.

Vulnerable code:
File: Goldivault.sol
159:     if(remainingTime > 0) {
160:       SafeTransferLib.safeTransfer(depositToken, msg.sender, amount * (1000 - _fee) / 1000);
161:       SafeTransferLib.safeTransfer(depositToken, multisig, amount * _fee / 1000); //@audit rounding loss
162:       emit OwnershipTokenRedemption(msg.sender, amount * (1000 - _fee) / 1000);
163:     }

File: Goldilend.sol
430:     poolSize += userLoan.interest * (1000 - (multisigShare + apdaoShare)) / 1000;
431:     _updateInterestClaims(interest); //@audit rounding loss
...
694:   function _updateInterestClaims(uint256 interest) internal {
695:     multisigClaims += interest * multisigShare / 1000;
696:     apdaoClaims += interest * apdaoShare / 1000;
697:   }
HIGH severity: `InvestorBasedRateLimiter::setInvestorMintLimit` and `setInvestorRedemptionLimit` can make subsequent calls to `checkAndUpdateMintLimit` and `checkAndUpdateRedemptionLimit` revert due to underflow. **Description:** `InvestorBasedRateLimiter::_checkAndUpdateRateLimitState` [L211-213](https://github.com/ondoprotocol/rwa-internal/blob/6747ebada1c867a668a8da917aaaa7a0639a5b7a/contracts/ousg/InvestorBasedRateLimiter.sol#L211-L213) subtracts the current mint/redemption amount from the...

Vulnerable code:
if (amount > rateLimit.limit - rateLimit.currentAmount) {
  revert RateLimitExceeded();
}

function test_setInvestorMintLimit_underflow_DoS() public initDefault(alice) {
    // first perform a mint
    uint256 mintAmount = rateLimiter.defaultMintLimit();
    vm.prank(client);
    rateLimiter.checkAndUpdateMintLimit(alice, mintAmount);

    // admin now reduces the mint limit to be under the current
    // minted amount
    uint256 aliceInvestorId = 1;
    uint256 newMintLimit = mintAmount - 1;
    vm.prank(guardian);
    rateLimiter.setInvestorMintLimit(aliceInvestorId, newMintLimit);

    // subsequent calls to `checkAndUpdateMintLimit` revert due to underflow
    vm.prank(client);
    rateLimiter.checkAndUpdateMintLimit(alice, 1);

    // same issue affects `setInvestorRedemptionLimit`
}

Ran 1 test for forge-tests/ousg/InvestorBasedRateLimiter/setters.t.sol:Test_InvestorBasedRateLimiter_setters_ETH
[FAIL. Reason: panic: arithmetic underflow or overflow (0x11)] test_setInvestorMintLimit_underflow_DoS() (gas: 264384)
Suite result: FAILED. 0 passed; 1 failed; 0 skipped; finished in 1.09ms (116.74µs CPU time)
LOW severity: Prevent creating an investor record associated with the zero address. **Description:** `InvestorBasedRateLimiter::checkAndUpdateMintLimit` and `checkAndUpdateRedemptionLimit` can create a new investor record and associate it with the zero address.

**Impact:** Investor records can be created which are associated with the zero address. This breaks the following...

Vulnerable code:
function test_mint_zero_address() public {
    uint256 mintAmount = rateLimiter.defaultMintLimit();
    vm.prank(client);
    rateLimiter.checkAndUpdateMintLimit(address(0), mintAmount);

    // an investor has been created with a 0 address
    assertEq(1, rateLimiter.addressToInvestorId(address(0)));

    // same issue affects checkAndUpdateRedemptionLimit
}

function _setAddressToInvestorId(
    address investorAddress,
    uint256 newInvestorId
) internal {
    if(investorAddress == address(0)) revert NoZeroAddress();
HIGH severity: Prevent creating an investor record associated with no address. **Description:** `InvestorBasedRateLimiter::initializeInvestorStateDefault` is supposed to associate a newly created investor with one or more addresses but the `for` [loop](https://github.com/ondoprotocol/rwa-internal/blob/6747ebada1c867a668a8da917aaaa7a0639a5b7a/contracts/ousg/InvestorBasedRateLim...

Vulnerable code:
function initializeInvestorStateDefault(
    address[] memory addresses
    ) external onlyRole(CONFIGURER_ROLE) {
    _initializeInvestorState(
      addresses,
      defaultMintLimit,
      defaultRedemptionLimit,
      defaultMintLimitDuration,
      defaultRedemptionLimitDuration
    );
}

function _initializeInvestorState(
    address[] memory addresses,
    uint256 mintLimit,
    uint256 redemptionLimit,
    uint256 mintLimitDuration,
    uint256 redemptionLimitDuration
    ) internal {
    uint256 investorId = ++investorIdCounter;

    // @audit this `for` loop can by bypassed by calling
    // `initializeInvestorStateDefault` with an empty array
    for (uint256 i = 0; i < addresses.length; ++i) {
      // Safety check to ensure the address is not already associated with an investor
      // before associating it with a new investor
      if (addressToInvestorId[addresses[i]] != 0) {
        revert AddressAlreadyAssociated();
      }
      _setAddressToInvestorId(addresses[i], investorId);
    }

    investorIdToMintState[investorId] = RateLimit({
      currentAmount: 0,
      limit: mintLimit,
      lastResetTime: block.timestamp,
      limitDuration: mintLimitDuration
    });
    investorIdToRedemptionState[investorId] = RateLimit({
      currentAmount: 0,
      limit: redemptionLimit,
      lastResetTime: block.timestamp,
      limitDuration: redemptionLimitDuration
    });
}

function test_initializeInvestor_NoAddress() public {
    // no investor created
    assertEq(0, rateLimiter.investorIdCounter());

    // empty input array will bypass the `for` loop that is supposed
    // to associate addresses to the newly created investor
    address[] memory addresses;

    vm.prank(guardian);
    rateLimiter.initializeInvestorStateDefault(addresses);

    // one investor created
    assertEq(1, rateLimiter.investorIdCounter());

    // not associated with any addresses
    assertEq(0, rateLimiter.investorAddressCount(1));
}

uint256 addressesLength = addresses.length;

if(addressesLength == 0) revert EmptyAddressArray();
HIGH severity: `InstantMintTimeBasedRateLimiter::_setInstantMintLimit` and `_setInstantRedemptionLimit` can make subsequent calls to `_checkAndUpdateInstantMintLimit` and `_checkAndUpdateInstantRedemptionLimit` revert due to underflow. **Description:** `InstantMintTimeBasedRateLimiter::_checkAndUpdateInstantMintLimit` [L103-106](https://github.com/ondoprotocol/rwa-internal/blob/6747ebada1c867a668a8da917aaaa7a0639a5b7a/contracts/InstantMintTimeBasedRateLimiter.sol#L103-L106) subtracts the currently minted amount from the mint...

Vulnerable code:
require(
  amount <= instantMintLimit - currentInstantMintAmount,
  "RateLimit: Mint exceeds rate limit"
);

function _checkAndUpdateInstantMintLimit(uint256 amount) internal {
    require(
      instantMintLimit > currentInstantMintAmount && amount <= instantMintLimit - currentInstantMintAmount,
      "RateLimit: Mint exceeds rate limit"
    );
}

function _checkAndUpdateInstantRedemptionLimit(uint256 amount) internal {
    require(
      instantRedemptionLimit > currentInstantRedemptionAmount && amount <= instantRedemptionLimit - currentInstantRedemptionAmount,
      "RateLimit: Redemption exceeds rate limit"
    );
}
HIGH severity: Protocol may be short-changed by `BuidlRedeemer` during a USDC depeg event. **Description:** `OUSGInstantManager::_redeemBUIDL` assumes that 1 BUIDL = 1 USDC as it [enforces](https://github.com/ondoprotocol/rwa-internal/blob/6747ebada1c867a668a8da917aaaa7a0639a5b7a/contracts/ousg/ousgInstantManager.sol#L453-L459) receiving 1 USDC for every 1 BUIDL it redeems:

In the...

Vulnerable code:
uint256 usdcBalanceBefore = usdc.balanceOf(address(this));
buidl.approve(address(buidlRedeemer), buidlAmountToRedeem);
buidlRedeemer.redeem(buidlAmountToRedeem);
require(
  usdc.balanceOf(address(this)) == usdcBalanceBefore + buidlAmountToRedeem,
  "OUSGInstantManager::_redeemBUIDL: BUIDL:USDC not 1:1"
);
HIGH severity: Reduce approval before transferring tokens in `rOUSG::transferFrom`. **Description:** `rOUSG::transferFrom` [L286-289](https://github.com/ondoprotocol/rwa-internal/blob/6747ebada1c867a668a8da917aaaa7a0639a5b7a/contracts/ousg/rOUSG.sol#L286-L289) currently checks approvals, transfers the tokens then reduces the approvals:

A safer coding pattern is to reduce the...

Vulnerable code:
// verify approval
require(currentAllowance >= _amount, "TRANSFER_AMOUNT_EXCEEDS_ALLOWANCE");

// perform transfer
_transfer(_sender, _recipient, _amount);

// reduce approval
_approve(_sender, msg.sender, currentAllowance - _amount);
HIGH severity: Transfer tokens before minting shares in `rOUSG::wrap`. **Description:** `rOUSG::wrap` [L411-413](https://github.com/ondoprotocol/rwa-internal/blob/6747ebada1c867a668a8da917aaaa7a0639a5b7a/contracts/ousg/rOUSG.sol#L411-L413) currently mints shares before transferring tokens used to mint those shares:

A safer coding pattern is to transfer the tokens...

Vulnerable code:
// mint shares
uint256 ousgSharesAmount = _OUSGAmount * OUSG_TO_ROUSG_SHARES_MULTIPLIER;
_mintShares(msg.sender, ousgSharesAmount);

// transfer tokens used to mint the shares
ousg.transferFrom(msg.sender, address(this), _OUSGAmount);
LOW severity: Cache storage variables in stack when read multiple times without being changed. **Description:** Reading from storage is considerably more expensive than reading from the stack so cache storage variables when read multiple times without being changed:

**Ondo:**
Acknowledged.

Vulnerable code:
File: contracts/ousg/InvestorBasedRateLimiter.sol

// @audit cache these then use cache values when emitting event to save 2 storage reads
324:      --investorAddressCount[previousInvestorId];
335:      ++investorAddressCount[newInvestorId];

// @audit cache and use cached value for check in L470 to save 1 storage read
462:    if (mintState.lastResetTime == 0) {

// @audit cache and use cached value for check in L506 to save 1 storage read
498:    if (redemptionState.lastResetTime == 0) {
HIGH severity: Avoid unnecessary initialization to zero. **Description:** Avoid unnecessary initialization to zero:

**Ondo:**
Fixed in commit [a7dab64](https://github.com/ondoprotocol/rwa-internal/commit/a7dab64a2ad87b6ca051c3aeb5371c8f9f933350).

**Cyfrin:** Verified.

Vulnerable code:
File: contracts/ousg/InvestorBasedRateLimiter.sol

253:     for (uint256 i = 0; i < addresses.length; ++i) {

File: contracts/ousg/ousgInstantManager.sol

106:   uint256 public mintFee = 0;

109:   uint256 public redeemFee = 0;

881:     for (uint256 i = 0; i < exCallData.length; ++i) {
HIGH severity: Failure to add modified facets and facets with modified dependencies to `bips::bipSeedGauge` breaks the protocol. **Description:** At the time of a Diamond Proxy upgrade, modified facets are cut by their inclusion in the relevant function within [`bips.js`](https://github.com/BeanstalkFarms/Beanstalk/blob/dfb418d185cd93eef08168ccaffe9de86bc1f062/protocol/scripts/bips.js). are Currently, the [`bipSeedGauge`...

Vulnerable code:
git diff --stat 7606673..dfb418d -- ".sol" ":\!protocol/test/" ":\!protocol/contracts/mocks/*" | grep "Facet.sol"

protocol/contracts/beanstalk/barn/UnripeFacet.sol  | 360 +++++++++------
protocol/contracts/beanstalk/field/FieldFacet.sol  |   4 +-
protocol/contracts/beanstalk/silo/BDVFacet.sol     |  12 +-
protocol/contracts/beanstalk/silo/ConvertFacet.sol |   8 +-
.../contracts/beanstalk/silo/WhitelistFacet.sol    |  79 +++-
.../contracts/beanstalk/sun/GaugePointFacet.sol    |  39 ++
.../beanstalk/sun/SeasonFacet/SeasonFacet.sol      | 120 +++--
.../sun/SeasonFacet/SeasonGettersFacet.sol         | 248 ++++++++++

git diff --stat 7606673..dfb418d -- "*.sol" ":\!protocol/test/*" ":\!protocol/contracts/mocks/*" | grep "Lib.*\.sol"
HIGH severity: The previous milestone stem should be scaled for use with the new gauge point system which uses untruncated values moving forward. **Description:** Within the Beanstalk Silo, the milestone stem for a given token is the cumulative amount of grown stalk per BDV for this token at the last `stalkEarnedPerSeason` update. Previously, the milestone stem was stored in its truncated representation; however, the seed gauge system now...

Vulnerable code:
_stemTipForToken = s.ss[token].milestoneStem +
    int96(s.ss[token].stalkEarnedPerSeason).mul(
        int96(s.season.current).sub(int96(s.ss[token].milestoneSeason))
    );

function stemTipForToken(address token)
        internal
        view
        returns (int96 _stemTipForToken)
    {
        AppStorage storage s = LibAppStorage.diamondStorage();

        // SafeCast unnecessary because all casted variables are types smaller that int96.
        _stemTipForToken = s.ss[token].milestoneStem +
        int96(s.ss[token].stalkEarnedPerSeason).mul(
            int96(s.season.current).sub(int96(s.ss[token].milestoneSeason))
        ).div(1e6); //round here
    }

for (uint i = 0; i < siloTokens.length; i++) {
+   s.ss[siloTokens[i]].milestoneStem = int96(s.ss[siloTokens[i]].milestoneStem.mul(1e6));