msc-smart-contract-auditing/vulnerable-functions-base huggingface dataset:
Vulnerability: isValidRefinance checks both conditions instead of one, leading to rejection of valid refinances
Severity: high

Description:
`isValidRefinance()` is intended to check whether either (a) the loan interest rate decreased sufficiently or (b) the loan duration increased sufficiently. Instead, it requires both of these to be true, leading to the rejection of valid refinances.\\nWhen trying to buy out a lien from `LienToken.sol:buyoutLien()`, the function calls `AstariaRouter.sol:isValidRefinance()` to check whether the refi terms are valid.\\n```\\nif (!ASTARIA_ROUTER.isValidRefinance(lienData[lienId], ld)) {\\n  revert InvalidRefinance();\\n}\\n```\\n\\nOne of the roles of this function is to check whether the rate decreased by more than 0.5%. From the docs:\\nAn improvement in terms is considered if either of these conditions is met:\\nThe loan interest rate decrease by more than 0.5%.\\nThe loan duration increases by more than 14 days.\\nThe currently implementation of the code requires both of these conditions to be met:\\n```\\nreturn (\\n    newLien.rate >= minNewRate &&\\n    ((block.timestamp + newLien.duration - lien.start - lien.duration) >= minDurationIncrease)\\n);\\n```\\n

Impact:
Valid refinances that meet one of the two criteria will be rejected.

Recommended mitigation:
Change the AND in the return statement to an OR:\\n```\\nreturn (\\n    newLien.rate >= minNewRate ||\\n    ((block.timestamp + newLien.duration - lien.start - lien.duration) >= minDurationIncrease)\\n);\\n```\\n
Vulnerability: isValidRefinance will approve invalid refinances and reject valid refinances due to buggy math
Severity: high

Description:
The math in `isValidRefinance()` checks whether the rate increased rather than decreased, resulting in invalid refinances being approved and valid refinances being rejected.\\nWhen trying to buy out a lien from `LienToken.sol:buyoutLien()`, the function calls `AstariaRouter.sol:isValidRefinance()` to check whether the refi terms are valid.\\n```\\nif (!ASTARIA_ROUTER.isValidRefinance(lienData[lienId], ld)) {\\n  revert InvalidRefinance();\\n}\\n```\\n\\nOne of the roles of this function is to check whether the rate decreased by more than 0.5%. From the docs:\\nAn improvement in terms is considered if either of these conditions is met:\\nThe loan interest rate decrease by more than 0.5%.\\nThe loan duration increases by more than 14 days.\\nThe current implementation of the function does the opposite. It calculates a `minNewRate` (which should be maxNewRate) and then checks whether the new rate is greater than that value.\\n```\\nuint256 minNewRate = uint256(lien.rate) - minInterestBPS;\\nreturn (newLien.rate >= minNewRate // rest of code\\n```\\n\\nThe result is that if the new rate has increased (or decreased by less than 0.5%), it will be considered valid, but if it has decreased by more than 0.5% (the ideal behavior) it will be rejected as invalid.

Impact:
Users can perform invalid refinances with the wrong parameters.\\nUsers who should be able to perform refinances at better rates will not be able to.

Recommended mitigation:
Flip the logic used to check the rate to the following:\\n```\\nuint256 maxNewRate = uint256(lien.rate) - minInterestBPS;\\nreturn (newLien.rate <= maxNewRate// rest of code\\n```\\n
Vulnerability: new loans "max duration" is not restricted
Severity: medium

Description:
document : " Epochs PublicVaults operate around a time-based epoch system. An epoch length is defined by the strategist that deploys the PublicVault. The duration of new loans is restricted to not exceed the end of the next epoch. For example, if a PublicVault is 15 days into a 30-day epoch, new loans must not be longer than 45 days. " but more than 2 epoch's duration can be added\\nthe max duration is not detected. add success when > next epoch\\n#AstariaTest#testBasicPublicVaultLoan\\n```\\n  function testBasicPublicVaultLoan() public {\\n\\n  IAstariaRouter.LienDetails memory standardLien2 =\\n    IAstariaRouter.LienDetails({\\n      maxAmount: 50 ether,\\n      rate: (uint256(1e16) * 150) / (365 days),\\n      duration: 50 days,  /****** more then 14 * 2 *******/\\n      maxPotentialDebt: 50 ether\\n    });    \\n\\n    _commitToLien({\\n      vault: publicVault,\\n      strategist: strategistOne,\\n      strategistPK: strategistOnePK,\\n      tokenContract: tokenContract,\\n      tokenId: tokenId,\\n      lienDetails: standardLien2, /**** use standardLien2 ****/\\n      amount: 10 ether,\\n      isFirstLien: true\\n    });\\n  }\\n```\\n

Impact:
Too long duration

Recommended mitigation:
PublicVault#_afterCommitToLien\\n```\\n  function _afterCommitToLien(uint256 lienId, uint256 amount)\\n    internal\\n    virtual\\n    override\\n  {\\n    // increment slope for the new lien\\n    unchecked {\\n      slope += LIEN_TOKEN().calculateSlope(lienId);\\n    }\\n\\n    ILienToken.Lien memory lien = LIEN_TOKEN().getLien(lienId);\\n\\n    uint256 epoch = Math.ceilDiv(\\n      lien.start + lien.duration - START(),\\n      EPOCH_LENGTH()\\n    ) - 1;\\n\\n+   require(epoch <= currentEpoch + 1,"epoch max <= currentEpoch + 1");\\n\\n    liensOpenForEpoch[epoch]++;\\n    emit LienOpen(lienId, epoch);\\n  }\\n```\\n
Vulnerability: LienToken._payment function increases users debt
Severity: medium

Description:
LienToken._payment function increases users debt by setting `lien.amount = _getOwed(lien)`\\n`LienToken._payment` is used by `LienToken.makePayment` function that allows borrower to repay part or all his debt.\\nAlso this function can be called by `AuctionHouse` when the lien is liquidated.\\n```\\n  function _payment(\\n    uint256 collateralId,\\n    uint8 position,\\n    uint256 paymentAmount,\\n    address payer\\n  ) internal returns (uint256) {\\n    if (paymentAmount == uint256(0)) {\\n      return uint256(0);\\n    }\\n\\n\\n    uint256 lienId = liens[collateralId][position];\\n    Lien storage lien = lienData[lienId];\\n    uint256 end = (lien.start + lien.duration);\\n    require(\\n      block.timestamp < end || address(msg.sender) == address(AUCTION_HOUSE),\\n      "cannot pay off an expired lien"\\n    );\\n\\n\\n    address lienOwner = ownerOf(lienId);\\n    bool isPublicVault = IPublicVault(lienOwner).supportsInterface(\\n      type(IPublicVault).interfaceId\\n    );\\n\\n\\n    lien.amount = _getOwed(lien);\\n\\n\\n    address payee = getPayee(lienId);\\n    if (isPublicVault) {\\n      IPublicVault(lienOwner).beforePayment(lienId, paymentAmount);\\n    }\\n    if (lien.amount > paymentAmount) {\\n      lien.amount -= paymentAmount;\\n      lien.last = block.timestamp.safeCastTo32();\\n      // slope does not need to be updated if paying off the rest, since we neutralize slope in beforePayment()\\n      if (isPublicVault) {\\n        IPublicVault(lienOwner).afterPayment(lienId);\\n      }\\n    } else {\\n      if (isPublicVault && !AUCTION_HOUSE.auctionExists(collateralId)) {\\n        // since the openLiens count is only positive when there are liens that haven't been paid off\\n        // that should be liquidated, this lien should not be counted anymore\\n        IPublicVault(lienOwner).decreaseEpochLienCount(\\n          IPublicVault(lienOwner).getLienEpoch(end)\\n        );\\n      }\\n      //delete liens\\n      _deleteLienPosition(collateralId, position);\\n      delete lienData[lienId]; //full delete\\n\\n\\n      _burn(lienId);\\n    }\\n\\n\\n    TRANSFER_PROXY.tokenTransferFrom(WETH, payer, payee, paymentAmount);\\n\\n\\n    emit Payment(lienId, paymentAmount);\\n    return paymentAmount;\\n  }\\n```\\n\\nHere lien.amount becomes lien.amount + accrued interests, because `_getOwed` do that calculation.\\n`lien.amount` is the amount that user borrowed. So actually that line has just increased user's debt. And in case if he didn't pay all amount of lien, then next time he will pay more interests.\\nExample. User borrows 1 eth. His `lien.amount` is 1eth. Then he wants to repay some part(let's say 0.5 eth). Now his `lien.amount` becomes `lien.amount + interests`. When he pays next time, he pays `(lien.amount + interests) + new interests`. So interests are acummulated on previous interests.

Impact:
User borrowed amount increases and leads to lose of funds.

Recommended mitigation:
Issue LienToken._payment function increases users debt\\nDo not update lien.amount to _getOwed(lien).
Vulnerability: _validateCommitment fails for approved operators
Severity: medium

Description:
If a collateral token owner approves another user as an operator for all their tokens (rather than just for a given token), the validation check in `_validateCommitment()` will fail.\\nThe collateral token is implemented as an ERC721, which has two ways to approve another user:\\nApprove them to take actions with a given token (approve())\\nApprove them as an "operator" for all your owned tokens (setApprovalForAll())\\nHowever, when the `_validateCommitment()` function checks that the token is owned or approved by `msg.sender`, it does not accept those who are set as operators.\\n```\\nif (msg.sender != holder) {\\n  require(msg.sender == operator, "invalid request");\\n}\\n```\\n

Impact:
Approved operators of collateral tokens will be rejected from taking actions with those tokens.

Recommended mitigation:
Include an additional check to confirm whether the `msg.sender` is approved as an operator on the token:\\n```\\n    address holder = ERC721(COLLATERAL_TOKEN()).ownerOf(collateralId);\\n    address approved = ERC721(COLLATERAL_TOKEN()).getApproved(collateralId);\\n    address operator = ERC721(COLLATERAL_TOKEN()).isApprovedForAll(holder);\\n\\n    if (msg.sender != holder) {\\n      require(msg.sender == operator || msg.sender == approved, "invalid request");\\n    }\\n```\\n
Vulnerability: timeToEpochEnd calculates backwards, breaking protocol math
Severity: medium

Description:
When a lien is liquidated, it calls `timeToEpochEnd()` to determine if a liquidation accountant should be deployed and we should adjust the protocol math to expect payment in a future epoch. Because of an error in the implementation, all liquidations that will pay out in the current epoch are set up as future epoch liquidations.\\nThe `liquidate()` function performs the following check to determine if it should set up the liquidation to be paid out in a future epoch:\\n```\\nif (PublicVault(owner).timeToEpochEnd() <= COLLATERAL_TOKEN.auctionWindow())\\n```\\n\\nThis check expects that `timeToEpochEnd()` will return the time until the epoch is over. However, the implementation gets this backwards:\\n```\\nfunction timeToEpochEnd() public view returns (uint256) {\\n  uint256 epochEnd = START() + ((currentEpoch + 1) * EPOCH_LENGTH());\\n\\n  if (epochEnd >= block.timestamp) {\\n    return uint256(0);\\n  }\\n\\n  return block.timestamp - epochEnd;\\n}\\n```\\n\\nIf `epochEnd >= block.timestamp`, that means that there IS remaining time in the epoch, and it should perform the calculation to return `epochEnd - block.timestamp`. In the opposite case, where `epochEnd <= block.timestamp`, it should return zero.\\nThe result is that the function returns 0 for any epoch that isn't over. Since `0 < COLLATERAL_TOKEN.auctionWindow())`, all liquidated liens will trigger a liquidation accountant and the rest of the accounting for future epoch withdrawals.

Impact:
Accounting for a future epoch withdrawal causes a number of inconsistencies in the protocol's math, the impact of which vary depending on the situation. As a few examples:\\nIt calls `decreaseEpochLienCount()`. This has the effect of artificially lowering the number of liens in the epoch, which will cause the final liens paid off in the epoch to revert (and will let us process the epoch earlier than intended).\\nIt sets the payee of the lien to the liquidation accountant, which will pay out according to the withdrawal ratio (whereas all funds should be staying in the vault).\\nIt calls `increaseLiquidationsExpectedAtBoundary()`, which can throw off the math when processing the epoch.

Recommended mitigation:
Fix the `timeToEpochEnd()` function so it calculates the remaining time properly:\\n```\\nfunction timeToEpochEnd() public view returns (uint256) {\\n  uint256 epochEnd = START() + ((currentEpoch + 1) * EPOCH_LENGTH());\\n\\n  if (epochEnd <= block.timestamp) {\\n    return uint256(0);\\n  }\\n\\n  return epochEnd - block.timestamp; //\\n}\\n```\\n
Vulnerability: _payment() function transfers full paymentAmount, overpaying first liens
Severity: medium

Description:
The `_payment()` function sends the full `paymentAmount` argument to the lien owner, which both (a) overpays lien owners if borrowers accidentally overpay and (b) sends the first lien owner all the funds for the entire loop of a borrower is intending to pay back multiple loans.\\nThere are two `makePayment()` functions in LienToken.sol. One that allows the user to specific a `position` (which specific lien they want to pay back, and another that iterates through their liens, paying each back.\\nIn both cases, the functions call out to `_payment()` with a `paymentAmount`, which is sent (in full) to the lien owner.\\n```\\nTRANSFER_PROXY.tokenTransferFrom(WETH, payer, payee, paymentAmount);\\n```\\n\\nThis behavior can cause problems in both cases.\\nThe first case is less severe: If the user is intending to pay off one lien, and they enter a `paymentAmount` greater than the amount owed, the function will send the full `paymentAmount` to the lien owner, rather than just sending the amount owed.\\nThe second case is much more severe: If the user is intending to pay towards all their loans, the `_makePayment()` function loops through open liens and performs the following:\\n```\\nuint256 paymentAmount = totalCapitalAvailable;\\nfor (uint256 i = 0; i < openLiens.length; ++i) {\\n  uint256 capitalSpent = _payment(\\n    collateralId,\\n    uint8(i),\\n    paymentAmount,\\n    address(msg.sender)\\n  );\\n  paymentAmount -= capitalSpent;\\n}\\n```\\n\\nThe `_payment()` function is called with the first lien with `paymentAmount` set to the full amount sent to the function. The result is that this full amount is sent to the first lien holder, which could greatly exceed the amount they are owed.

Impact:
A user who is intending to pay off all their loans will end up paying all the funds they offered, but only paying off their first lien, potentially losing a large amount of funds.

Recommended mitigation:
Issue _payment() function transfers full paymentAmount, overpaying first liens\\nIn `_payment()`, if `lien.amount < paymentAmount`, set `paymentAmount = lien.amount`.\\nThe result will be that, in this case, only `lien.amount` is transferred to the lien owner, and this value is also returned from the function to accurately represent the amount that was paid.
Vulnerability: _getInterest() function uses block.timestamp instead of the inputted timestamp
Severity: medium

Description:
The `_getInterest()` function takes a timestamp as input. However, in a crucial check in the function, it uses `block.timestamp` instead. The result is that other functions expecting accurate interest amounts will receive incorrect values.\\nThe `_getInterest()` function takes a lien and a timestamp as input. The intention is for it to calculate the amount of time that has passed in the lien (delta_t) and multiply this value by the rate and the amount to get the interest generated by this timestamp.\\nHowever, the function uses the following check regarding the timestamp:\\n```\\nif (block.timestamp >= lien.start + lien.duration) {\\n  delta_t = uint256(lien.start + lien.duration - lien.last);\\n} \\n```\\n\\nBecause this check uses `block.timestamp` before returning the maximum interest payment, the function will incorrectly determine which path to take, and return an incorrect interest value.

Impact:
There are two negative consequences that can come from this miscalculation:\\nif the function is called when the lien is over (block.timestamp >= lien.start + lien.duration) to check an interest amount from a timestamp during the lien, it will incorrectly return the maximum interest value\\nIf the function is called when the lien is active for a timestamp long after the lien is over, it will skip the check to return maximum value and return the value that would have been generated if interest kept accruing indefinitely (using delta_t = uint256(timestamp.safeCastTo32() - lien.last);)\\nThis `_getInterest()` function is used in many crucial protocol functions (_getOwed(), `calculateSlope()`, `changeInSlope()`, getTotalDebtForCollateralToken()), so these incorrect values can have surprising and unexpected negative impacts on the protocol.

Recommended mitigation:
Change `block.timestamp` to `timestamp` so that the if statement checks correctly.
Vulnerability: Vault Fee uses incorrect offset leading to wildly incorrect value, allowing strategists to steal all funds
Severity: medium

Description:
`VAULT_FEE()` uses an incorrect offset, returning a number ~1e16X greater than intended, providing strategists with unlimited access to drain all vault funds.\\nWhen using ClonesWithImmutableArgs, offset values are set so that functions representing variables can retrieve the correct values from storage.\\nIn the ERC4626-Cloned.sol implementation, `VAULT_TYPE()` is given an offset of 172. However, the value before it is a `uint8` at the offset 164. Since a `uint8` takes only 1 byte of space, `VAULT_TYPE()` should have an offset of 165.\\nI put together a POC to grab the value of `VAULT_FEE()` in the test setup:\\n```\\nfunction testVaultFeeIncorrectlySet() public {\\n  Dummy721 nft = new Dummy721();\\n  address tokenContract = address(nft);\\n  uint256 tokenId = uint256(1);\\n  address publicVault = _createPublicVault({\\n      strategist: strategistOne,\\n      delegate: strategistTwo,\\n      epochLength: 14 days\\n  });\\n  uint fee = PublicVault(publicVault).VAULT_FEE();\\n  console.log(fee)\\n  assert(fee == 5000); // 5000 is the value that was meant to be set\\n}\\n```\\n\\nIn this case, the value returned is > 3e20.

Impact:
This is a highly critical bug. `VAULT_FEE()` is used in `_handleStrategistInterestReward()` to determine the amount of tokens that should be allocated to `strategistUnclaimedShares`.\\n```\\nif (VAULT_FEE() != uint256(0)) {\\n    uint256 interestOwing = LIEN_TOKEN().getInterest(lienId);\\n    uint256 x = (amount > interestOwing) ? interestOwing : amount;\\n    uint256 fee = x.mulDivDown(VAULT_FEE(), 1000); //VAULT_FEE is a basis point\\n    strategistUnclaimedShares += convertToShares(fee);\\n  }\\n```\\n\\nThe result is that strategistUnclaimedShares will be billions of times higher than the total interest generated, essentially giving strategist access to withdraw all funds from their vaults at any time.

Recommended mitigation:
Set the offset for `VAULT_FEE()` to 165. I tested this value in the POC I created and it correctly returned the value of 5000.
Vulnerability: Bids cannot be created within timeBuffer of completion of a max duration auction
Severity: medium

Description:
The auction mechanism is intended to watch for bids within `timeBuffer` of the end of the auction, and automatically increase the remaining duration to `timeBuffer` if such a bid comes in.\\nThere is an error in the implementation that causes all bids within `timeBuffer` of the end of a max duration auction to revert, effectively ending the auction early and cutting off bidders who intended to wait until the end.\\nIn the `createBid()` function in AuctionHouse.sol, the function checks if a bid is within the final `timeBuffer` of the auction:\\n```\\nif (firstBidTime + duration - block.timestamp < timeBuffer)\\n```\\n\\nIf so, it sets `newDuration` to equal the amount that will extend the auction to `timeBuffer` from now:\\n```\\nuint64 newDuration = uint256( duration + (block.timestamp + timeBuffer - firstBidTime) ).safeCastTo64();\\n```\\n\\nIf this `newDuration` doesn't extend beyond the `maxDuration`, this works great. However, if it does extend beyond `maxDuration`, the following code is used to update duration:\\n```\\nauctions[tokenId].duration = auctions[tokenId].maxDuration - firstBidTime;\\n```\\n\\nThis code is incorrect. `maxDuration` will be a duration for the contest (currently set to 3 days), whereas `firstTimeBid` is a timestamp for the start of the auction (current timestamps are > 1 billion).\\nSubtracting `firstTimeBid` from `maxDuration` will underflow, which will revert the function.

Impact:
Bidders who expected to wait until the end of the auction to vote will be cut off from voting, as the auction will revert their bids.\\nVaults whose collateral is up for auction will earn less than they otherwise would have.

Recommended mitigation:
Change this assignment to simply assign `duration` to `maxDuration`, as follows:\\n```\\nauctions[tokenId].duration = auctions[tokenId].maxDuration\\n```\\n
Vulnerability: Loan can be written off by anybody before overdue delay expires
Severity: high

Description:
When a borrower takes a second loan after a loan that has been written off, this second loan can be written off instantly by any other member due to missing update of last repay block, leaving the staker at a loss.\\nA staker stakes and vouches a borrower\\nthe borrower borrows calling UToken:borrow: `accountBorrows[borrower].lastRepay` is updated with the current block number\\nthe staker writes off the entire debt of the borrower calling `UserManager:debtWriteOff`. In the internal call to `UToken:debtWriteOff` the principal is set to zero but `accountBorrows[borrower].lastRepay` is not updated\\n90 days pass and a staker vouches for the same borrower\\nthe borrower borrows calling UToken:borrow: `accountBorrows[borrower].lastRepay` is not set to the current block since non zero and stays to the previous value.\\n`accountBorrows[borrower].lastRepay` is now old enough to allow the check in `UserManager:debtWriteOff` at line 738 to pass. The debt is written off by any other member immediatly after the loan is given. The staker looses the staked amount immediatly.\\n```\\n        if (block.number <= lastRepay + overdueBlocks + maxOverdueBlocks) {\\n            if (staker != msg.sender) revert AuthFailed();\\n        }\\n```\\n\\nThe last repay block is still stale and a new loan can be taken and written off immediatly many times as long as stakers are trusting the borrower\\nNote that this can be exploited maliciously by the borrower, who can continously ask for loans and then write them off immediatly.

Impact:
The staker of the loan looses the staked amount well before the overdue delay is expired

Recommended mitigation:
Issue Loan can be written off by anybody before overdue delay expires\\nReset `lastRepay` for the borrower to 0 when the debt is written off completely\\n```\\n    function debtWriteOff(address borrower, uint256 amount) external override whenNotPaused onlyUserManager {\\n        uint256 oldPrincipal = getBorrowed(borrower);\\n        uint256 repayAmount = amount > oldPrincipal ? oldPrincipal : amount;\\n\\n// Add the line below\\n       if (oldPrincipal == repayAmount) accountBorrows[borrower].lastRepay = 0;\\n        accountBorrows[borrower].principal = oldPrincipal - repayAmount;\\n        totalBorrows -= repayAmount;\\n    }\\n```\\n