#47009 [SC-Low] Any position can be closed (by repaying the debt) even after the maturity date has passed
Submitted on Jun 7th 2025 at 19:51:39 UTC by @zeroK for IOP | Term Structure Institutional
Report ID: #47009
Report Type: Smart Contract
Report severity: Low
Target: https://github.com/term-structure/tsi-contract/blob/main/src/Settlement.sol
Impacts:
Block stuffing
Description
Brief/Intro
Any loan created has a maturity date, which is important because it enforces a repayment deadline. If the borrower does not repay on time, the position should be liquidated to ensure that the lender and the protocol are compensated. However, currently there is no restriction preventing repayment after the maturity date. Borrowers can repay or modify their position at any time, even after maturity. This undermines the liquidation process because users can front-run any attempt at liquidation by repaying their debt after maturity, effectively blocking liquidation for that loan position.
position should be liquidated right when the maturity reached for position: https://docs.institutional.ts.finance/features/liquidation-and-physical-delivery#liquidation-ltv-lltv
Vulnerability Details
we can see when a loan created, the maturity time should be set as input parameter:
function createSettlement(
string memory _settlementId,
SettleInfo calldata _settleInfo,
string[] memory _loanIds,
LoanInfo[] calldata _loans,
bytes calldata _signature
) external nonReentrant {
.....
// storage
struct LoanInfo {
bytes32 settlementId; // ID of the settlement this loan belongs to
address maker; // who created the loan
address lender; // who provides the funds
address borrower; // who receives the funds(borrow)
address collateralTokenAddr; // address of the collateral token
address debtTokenAddr; // address of the debt token
DebtData debtData;
bool settled; // has the loan been settled
}
struct DebtData {
uint256 collateralAmt; // amount of the collateral
uint256 debtAmt; // debt amount
uint256 borrowedAmt; // how muhch borrowed
uint256 feeAmt; // fee to pay to the protocol
uint64 maturity; // when the loan matures
uint32 lltv; // loan to value liquidation ratio in basis point
uint32 mltv; // maximum loan to value
}
however, there is nothing prevent the borrower to invoke the repay function, even when the maturity passed, since there is not check to prevent invoking the function when timestamp > maturity:
function repay(string memory _loanId, uint256 repayAmt, uint256 removeCollateralAmt) external nonReentrant {
bytes32 loanId = _loanId.toBytes32();
LoanInfo memory loanInfo = loans[loanId];
loanInfo.repay(repayAmt);
if (removeCollateralAmt > 0) {
loanInfo.checkBorrower(loanId, msg.sender);
loanInfo.removeCollateral(removeCollateralAmt, _oracle);
}
IERC20(loanInfo.debtTokenAddr).safeTransferFrom(msg.sender, loanInfo.lender, repayAmt);
if (removeCollateralAmt > 0) {
IERC20(loanInfo.collateralTokenAddr).safeTransferFrom(
loanInfo.lender, loanInfo.borrower, removeCollateralAmt
);
}
if (loanInfo.debtData.debtAmt == 0 && loanInfo.debtData.collateralAmt == 0) {
delete loans[loanId];
} else {
loans[loanId] = loanInfo;
}
emit Repaid(loanId, repayAmt, removeCollateralAmt);
}
this can lead calls to liquidation for mature position to fail since the loan deleted:
function liquidate(string memory _loanId, uint256 liquidationAmt) external nonReentrant {
bytes32 loanId = _loanId.toBytes32();
LoanInfo memory loanInfo = loans[loanId]; //@audit not exist
(uint256 collateralToLiquidator, uint256 collateralToProtocol) =
loanInfo.liquidate(loanId, _oracle, _minimumDebtValue, liquidationAmt);
this can block the liquidation process invoked by the bot, it can affect badly when batch of liquidation executed in one transaction.
Impact Details
maturity of the loans not respected in repay function which lead to prevent liquidations.
Recommend
add check for maturity in repay function, prevent repaying when the position passed the maturity.
Proof of Concept
Proof of Concept
first thing create a settlement.
set maturity to block.timestamp + 10 days.
invoke the repay function, before that, wrap block.timestamp `wrap(block.timestamp +10 days)
we can see the loan repaid and deleted even it get matured.
Was this helpful?