Source: #24
KupiaSec, OpaBatyo
Adversaries can constantly reset the withdrawal delay of lender group members by performing 0-value transferFrom
transactions to invoke the afterTokenTransfer
hook.
Currently there is a delay on withdrawals to prevent sandwich attacks in lender group contracts. Members must invoke prepareSharesForBurn
by stating how many shares they want to burn and start an internal countdown. Afterwards, members invoke burnSharesToWithdrawEarnings
which checks whether the delay passed in burn
function burn(address _burner, uint256 _amount, uint256 withdrawDelayTimeSeconds) external onlyOwner {
//require prepared
require(poolSharesPreparedToWithdrawForLender[_burner] >= _amount,"Shares not prepared for withdraw");
@> require(poolSharesPreparedTimestamp[_burner] <= block.timestamp - withdrawDelayTimeSeconds,"Shares not prepared for withdraw");
//reset prepared
poolSharesPreparedToWithdrawForLender[_burner] = 0;
poolSharesPreparedTimestamp[_burner] = block.timestamp;
_burn(_burner, _amount);
}
This countdown is reset every time a member invokes a share transfer through the _afterTokenTransfer
hook presumably to prevent users preparing shares in advance by transferring it between one another.
function _afterTokenTransfer(
address from,
address to,
uint256 amount
) internal override {
//reset prepared
poolSharesPreparedToWithdrawForLender[from] = 0;
poolSharesPreparedTimestamp[from] = block.timestamp;
}
Adversaries can perform 0-value transferFrom
transactions which will always pass as there are no 0-value checks in OZ's version 4.8 ERC20.sol
used by the protocol. Users will have their countdown constantly reset thus being prevented from withdrawing forever or until a bribe is paid.
- OpenZeppelin's
ERC20.transferFrom
has no 0-value input validation LenderCommitmentGroupShares._afterTokenTransfer
does not perform 0-value input either.
Group members must have invoked prepareSharesForBurn
None
- Group member invokes
prepareSharesForBurn
starting the countdown - Adversary invokes
transferFrom(victim, to, 0)
minutes before the cooldown expires - Cooldown and shares are reset because
_afterTokenTransfer
was triggered - Group member is forced to re-prepare their shares
- Attacker repeats this continuously or until a bribe is paid
Lender commit group members will have their funds permanently locked in the contract
No response
Rewrite the _afterTokenTransfer
hook to be skipped in case of amount = 0
sherlock-admin2
The protocol team fixed this issue in the following PRs/commits: teller-protocol/teller-protocol-v2-audit-2024#76
Source: #39
OpaBatyo, hash
Insufficient validation for try/catch
address will disallow borrower's from repaying their loans
A malicious lender can bypass the try/catch
block covering the repayLoanCallback external call by selfdestructing loanRepaymentListener
if (loanRepaymentListener != address(0)) {
require(gasleft() >= 80000, "NR gas"); //fixes the 63/64 remaining issue
try
ILoanRepaymentListener(loanRepaymentListener).repayLoanCallback{
gas: 80000
}( //limit gas costs to prevent lender preventing repayments
_bidId,
_msgSenderForMarket(bid.marketplaceId),
_payment.principal,
_payment.interest
)
The try/catch
block will revert if the call is made to a non-contract address. To avoid this a check for codesize > 0
is kept inside the setRepaymentListenerForBid
function. But this can be bypassed by the lender selfdestructing
the _listener
in the same transaction which will delete the contract
function setRepaymentListenerForBid(uint256 _bidId, address _listener) external {
uint256 codeSize;
assembly {
codeSize := extcodesize(_listener)
}
require(codeSize > 0, "Not a contract");
address sender = _msgSenderForMarket(bids[_bidId].marketplaceId);
require(
sender == getLoanLender(_bidId),
"Not lender"
);
repaymentListenerForBid[_bidId] = _listener;
}
No response
No response
- Lender creates a contract which can
selfdestruct
itself - Lender sets this address as the repaymentListener
- In the same tx, the lender destroys the contract
- Now the borrower cannot repay because the try/catch block will always revert
Borrowers will not be able to repay the loan allowing the lender to steal the collateral after the loan will default
No response
Use .call instead of the try/catch
sherlock-admin2
The protocol team fixed this issue in the following PRs/commits: teller-protocol/teller-protocol-v2-audit-2024#79
Issue H-3: Using original principal amount as due amount inside liquidateDefaultedLoanWithIncentive
breaks contract accounting leading to lost assets/broken functionalities
Source: #43
0xeix, hash
Using original principal amount as due amount inside liquidateDefaultedLoanWithIncentive
breaks contract accounting leading to lost assets/broken functionalities
In liquidateDefaultedLoanWithIncentive
, the amount due is taken as the original principal amount of the bid rather than the remaining to be repaid principal which is incorrect as part of this principal could have already been paid back
function liquidateDefaultedLoanWithIncentive(
uint256 _bidId,
int256 _tokenAmountDifference
) external whenForwarderNotPaused whenNotPaused bidIsActiveForGroup(_bidId) nonReentrant onlyOracleApprovedAllowEOA {
//use original principal amount as amountDue
uint256 amountDue = _getAmountOwedForBid(_bidId);
function _getAmountOwedForBid(uint256 _bidId )
internal
view
virtual
returns (uint256 amountDue)
{
// @audit this is the entire principal amount which is incorrect
(,,,, amountDue, , , )
= ITellerV2(TELLER_V2).getLoanSummary(_bidId);
}
Parts of the original principal could have been repaid and accounted via this function leading to double counting of principal in totalPrincipalTokensRepaid
function repayLoanCallback(
uint256 _bidId,
address repayer,
uint256 principalAmount,
uint256 interestAmount
) external onlyTellerV2 whenForwarderNotPaused whenNotPaused bidIsActiveForGroup(_bidId) {
totalPrincipalTokensRepaid += principalAmount;
totalInterestCollected += interestAmount;
This leads to several problems:
- Underflow in
totalPrincipalTokensLended - totalPrincipalTokensRepaid
astotalPrincipalTokensRepaid
can double count repaid tokens causing bids to revert - Lost assets due to
tokenDifferenceFromLiquidations
calculating difference fromtotalPrincipal
without considering the repaid assets
No response
No response
- A loan is created with principal amount == 100 tokenBalanceOfContract == 0
totalPrincipalTokensCommitted == 100 totalPrincipalTokensWithdrawn == 0 totalPrincipalTokensLended == 100 totalPrincipalTokensRepaid == 0 tokenDifferenceFromLiquidations == 0
- Repayment of 80 principal occurs before the loan gets defaulted tokenBalanceOfContract == 80
totalPrincipalTokensCommitted == 100 totalPrincipalTokensWithdrawn == 0 totalPrincipalTokensLended == 100 totalPrincipalTokensRepaid == 80 tokenDifferenceFromLiquidations == 0
- Loan defaults and auction settles at price 50 (similarly problematic paths are lenders withdrawing 80 first or the auction settling at higher prices) tokenBalanceOfContract == 80 + 50 == 130
totalPrincipalTokensCommitted == 100 totalPrincipalTokensWithdrawn == 0 totalPrincipalTokensLended == 100 totalPrincipalTokensRepaid == 80 + 100 == 180 => incorrect tokenDifferenceFromLiquidations == (100 - 50 == -50) => incorrect
Now:
- available amount to withdraw will be calculated as (totalPrincipalTokensCommitted + tokenDifferenceFromLiquidations == 50) while there is actually 130 amount of assets available to withdraw causing loss for lenders
- getPrincipalAmountAvailableToBorrow will underflow because (totalPrincipalTokensLended - totalPrincipalTokensRepaid == -80) and no new bids can be accepted
There are more scenarios that arise from the same root cause such as estimated value becoming 0 incorrectly, which will cause division by 0 and hence revert on withdrawals, deposits will be lost or 0 shares will be minted etc.
Lost assets for users, broken functionalities
No response
Instead of the totalPrincipal consider the remaining principal ie. totalPrincipal - repaidPrincipal
sherlock-admin2
The protocol team fixed this issue in the following PRs/commits: teller-protocol/teller-protocol-v2-audit-2024#85
Source: #29
0xpetern, Flare, hash, mgf15
The function acceptFundsForAcceptBid handles accepting a loan bid, transferring funds to the borrower, and securing collateral. It approves the TELLER_V2 contract to transfer the loan's principal amount using "ERC20.approve".
However, some ERC20s on some chains don't return a value. The most popular example is USDT and USDC on the main net, and as the docs mention it should be compatible on any EVM chain and will support USDT:
Q: On what chains are the smart contracts going to be deployed? Ethereum Mainnet, Polygon PoS, Arbitrum One, Base
Q: If you are integrating tokens, are you allowing only whitelisted tokens to work with the codebase or any complying with the standard? Are they assumed to have certain properties, e.g. be non-reentrant? Are there any types of [weird tokens](https://github.com/d-xo/weird-erc20) you want to integrate? We absolutely do want to fully support all mainstream tokens like USDC, USDT, WETH, MOG, PEPE, etc. and we already took special consideration to make sure USDT works with our contracts.
Despite this claim, the current implementation of the approve function does not account for tokens like USDT, contradicting the protocol's intentions. Therefore acceptFundsForAcceptBid will never work on the EVM Chain or other related chain and tokens.
In LenderCommitmentGroup_Smart.soL: 556, approve function is used instead of safeApprove. USDT on the main net doesn't return a value, https://etherscan.io/token/0xdac17f958d2ee523a2206206994597c13d831ec7#code. This includes USDC which should work in the protocol. This behavior causes the approve function to revert when interacting with these tokens.
function acceptFundsForAcceptBid(
address _borrower,
uint256 _bidId,
uint256 _principalAmount,
uint256 _collateralAmount,
address _collateralTokenAddress,
uint256 _collateralTokenId,
uint32 _loanDuration,
uint16 _interestRate
) external onlySmartCommitmentForwarder whenForwarderNotPaused whenNotPaused {
require(
_collateralTokenAddress == address(collateralToken),
"Mismatching collateral token"
);
//the interest rate must be at least as high has the commitment demands. The borrower can use a higher interest rate although that would not be beneficial to the borrower.
require(_interestRate >= getMinInterestRate(_principalAmount), "Invalid interest rate");
//the loan duration must be less than the commitment max loan duration. The lender who made the commitment expects the money to be returned before this window.
require(_loanDuration <= maxLoanDuration, "Invalid loan max duration");
require(
getPrincipalAmountAvailableToBorrow() >= _principalAmount,
"Invalid loan max principal"
);
uint256 requiredCollateral = calculateCollateralRequiredToBorrowPrincipal(
_principalAmount
);
require(
_collateralAmount >=
requiredCollateral,
"Insufficient Borrower Collateral"
);
principalToken.approve(address(TELLER_V2), _principalAmount);
//do not have to override msg.sender as this contract is the lender !
_acceptBidWithRepaymentListener(_bidId);
totalPrincipalTokensLended += _principalAmount;
activeBids[_bidId] = true; //bool for now
emit BorrowerAcceptedFunds(
_borrower,
_bidId,
_principalAmount,
_collateralAmount,
_loanDuration,
_interestRate
);
}
The specific part of the function is highlighted her;
principalToken.approve(address(TELLER_V2), _principalAmount);
//do not have to override msg.sender as this contract is the lender !
- The protocol uses approve calls in functions like acceptFundsForAcceptBid.
- There is no consideration or error handling for approve calls when interacting with USDT, USDC.
- The protocol operates on EVM-compatible chains where these tokens are prevalent.
- Users or lenders supply USDT or USDC as the principalToken.
- Transactions that include approve calls revert, causing the protocol to fail in providing the intended functionality.
- The protocol becomes unusable with ERC20 tokens like USDT AND USDC, a widely used stablecoin.
- Breaks the intended functionality of the protocol.
- This breaks a criticial function in the protocol and causes the pool to fail.
No response
Use safeApprove
instead of approve
require(
_collateralAmount >=
requiredCollateral,
"Insufficient Borrower Collateral"
);
principalToken.safeApprove(address(TELLER_V2), _principalAmount);
sherlock-admin2
The protocol team fixed this issue in the following PRs/commits: teller-protocol/teller-protocol-v2-audit-2024#78
Source: #34
The protocol has acknowledged this issue.
KupiaSec
The LenderCommitmentGroup_Smart.getMinInterestRate()
function calculates the minimum APR for borrowing. However, this APR is determined based on the utilization ratio, which includes the newly borrowed amount. This allows borrowers to reduce the APR by dividing a loan into multiple smaller loans.
The LenderCommitmentGroup_Smart.getMinInterestRate()
function calculates the minimum APR for borrowing.
https://github.com/sherlock-audit/2024-11-teller-finance-update/blob/main/teller-protocol-v2-audit-2024/packages/contracts/contracts/LenderCommitmentForwarder/extensions/LenderCommitmentGroup/LenderCommitmentGroup_Smart.sol#L1017-1023
function getMinInterestRate(uint256 amountDelta) public view returns (uint16) {
return interestRateLowerBound +
uint16( uint256(interestRateUpperBound-interestRateLowerBound)
@> .percent(getPoolUtilizationRatio(amountDelta )
) );
}
However, the APR is calculated based on the utilization ratio, which includes the newly borrowed amount. This allows borrowers to reduce the APR by dividing a loan into multiple smaller loans, creating an unfair advantage for them over other users. https://github.com/sherlock-audit/2024-11-teller-finance-update/blob/main/teller-protocol-v2-audit-2024/packages/contracts/contracts/LenderCommitmentForwarder/extensions/LenderCommitmentGroup/LenderCommitmentGroup_Smart.sol#L1002-1015
function getPoolUtilizationRatio(uint256 activeLoansAmountDelta ) public view returns (uint16) {
if (getPoolTotalEstimatedValue() == 0) {
return 0;
}
return uint16( Math.min(
MathUpgradeable.mulDiv(
@> (getTotalPrincipalTokensOutstandingInActiveLoans() + activeLoansAmountDelta),
10000 ,
getPoolTotalEstimatedValue() ) ,
10000 ));
}
interestRateLowerBound = 0 interestRateLowerBound = 800 // 8% current total estimated value = $20000 current borrowed value = $5000
none
If Alice borrows $10000 USD at once, the APR is (5000 + 10000) / 20000 * 8% = 6%
However, Alice borrow $10000 as follows.
- Alice is going to borrow $10000.
- Alice borrows $1,000 repeatedly for a total of 10 times.
Then the APR for each 1000 USD is: (5000 + 1000) / 20000 * 8% = 2.4% (6000 + 1000) / 20000 * 8% = 2.8% (7000 + 1000) / 20000 * 8% = 3.2% (8000 + 1000) / 20000 * 8% = 3.6% (9000 + 1000) / 20000 * 8% = 4.0% (10000 + 1000) / 20000 * 8% = 4.4% (11000 + 1000) / 20000 * 8% = 4.8% (12000 + 1000) / 20000 * 8% = 5.2% (13000 + 1000) / 20000 * 8% = 5.6% (14000 + 1000) / 20000 * 8% = 6%
(2.4 + 2.8 + ... + 6) / 10 = 4.2
Therefore, the borrower will only pay an APR of 4.2% on the $10,000 loan. As a result, Alice lowers the interest rate from 6% to 4.2% by splitting a $10,000 loan into ten separate loans of $1,000 each.
Users can lower the interest rate by dividing a loan into multiple smaller loans.
none
Middle value should be used instead of end value.
function getPoolUtilizationRatio(uint256 activeLoansAmountDelta ) public view returns (uint16) {
if (getPoolTotalEstimatedValue() == 0) {
return 0;
}
return uint16( Math.min(
MathUpgradeable.mulDiv(
- (getTotalPrincipalTokensOutstandingInActiveLoans() + activeLoansAmountDelta),
+ (getTotalPrincipalTokensOutstandingInActiveLoans() + (activeLoansAmountDelta + 1) / 2),
10000 ,
getPoolTotalEstimatedValue() ) ,
10000 ));
}
Source: #37
PeterSR, hash
Lack of access control in revokeLender
allows an attacker to revoke any participant from a market
The delegation version of the revokeLender
function fails to perform any access control checks allowing any user to revoke any user
function _revokeStakeholderViaDelegation(
uint256 _marketId,
address _stakeholderAddress,
bool _isLender,
uint8 _v,
bytes32 _r,
bytes32 _s
) internal {
bytes32 uuid = _revokeStakeholderVerification(
_marketId,
_stakeholderAddress,
_isLender
);
// NOTE: Disabling the call to revoke the attestation on EAS contracts
// address attestor = markets[_marketId].owner;
// tellerAS.revokeByDelegation(uuid, attestor, _v, _r, _s);
}
Attestation should be enabled to observe the impact
No response
- Attacker calls
revokeLender
by passing in any address they wish to revoke from the market
Attacker can revoke any address they wish from any market making the market unuseable
No response
Perform access control checks
sherlock-admin2
The protocol team fixed this issue in the following PRs/commits: teller-protocol/teller-protocol-v2-audit-2024#77
Issue M-4: Not updating state before making custom external call can cause borrower's to loose assets due to re-entrancy
Source: #42
hash
Not updating state before making custom external call can cause borrower's to loose assets due to re-entrancy
The details of the repayment is updated only after the external call to the loanRepaymentListener
is made
function _repayLoan(
uint256 _bidId,
Payment memory _payment,
uint256 _owedAmount,
bool _shouldWithdrawCollateral
) internal virtual {
....
// @audit attacker can re-enter here. the repayment details are not yet updated
_sendOrEscrowFunds(_bidId, _payment); //send or escrow the funds
// update our mappings
bid.loanDetails.totalRepaid.principal += _payment.principal;
bid.loanDetails.totalRepaid.interest += _payment.interest;
bid.loanDetails.lastRepaidTimestamp = uint32(block.timestamp);
function _sendOrEscrowFunds(uint256 _bidId, Payment memory _payment)
internal virtual
{
....
address loanRepaymentListener = repaymentListenerForBid[_bidId];
// @audit re-enter in this call
if (loanRepaymentListener != address(0)) {
require(gasleft() >= 80000, "NR gas"); //fixes the 63/64 remaining issue
try
ILoanRepaymentListener(loanRepaymentListener).repayLoanCallback{
gas: 80000
}( //limit gas costs to prevent lender preventing repayments
_bidId,
_msgSenderForMarket(bid.marketplaceId),
_payment.principal,
_payment.interest
)
{} catch {}
This allows a malicious lender to reenter the TellerV2
contract and invoke lenderCloseLoan
seizing the collateral of the borrower as well if the loan is currently defaulted
- The repayment should be made after defaultTimestamp has passed
No response
- Defaulting timestmap of loan has passed
- Borrower does a repayment of 100 which is transferred to the lender. Following this
.repayLoanCallback
is called - Lender reenters via the
loanRepaymentListener
and invokes thelenderCloseLoan
function further seizing the collateral of the borrower - Borrower looses both the repayment amount and the collateral
Borrower will loose repayment amount and also the collateral
No response
Update the state before the loanRepaymentListener
call is made
sherlock-admin2
The protocol team fixed this issue in the following PRs/commits: teller-protocol/teller-protocol-v2-audit-2024#81
Issue M-5: Repayer can brick lending functionality of LenderCommitmentGroup_Smart
by repaying excess
Source: #46
OpaBatyo, hash
LenderCommitmentGroup_Smart
doesn't handle excess repayments making it possible to brick the lending functionality
The getTotalPrincipalTokensOutstandingInActiveLoans
function performs totalPrincipalTokensLended - totalPrincipalTokensRepaid
without handling the underflow scenario. This is problematic as excess amount can be repaid for loans which will cause an underflow here
function getTotalPrincipalTokensOutstandingInActiveLoans()
public
view
returns (uint256)
{
return totalPrincipalTokensLended - totalPrincipalTokensRepaid;
}
on excess repayments, totalPrincipalTokensRepaid
will become greater than totalPrincipalTokensLended
function repayLoanCallback(
uint256 _bidId,
address repayer,
uint256 principalAmount,
uint256 interestAmount
) external onlyTellerV2 whenForwarderNotPaused whenNotPaused bidIsActiveForGroup(_bidId) {
totalPrincipalTokensRepaid += principalAmount;
totalInterestCollected += interestAmount;
function in TellerV2.sol contract to repay excess amount. User can specify any amount greater than minimum amount
function repayLoan(uint256 _bidId, uint256 _amount)
external
acceptedLoan(_bidId, "rl")
{
_repayLoanAtleastMinimum(_bidId, _amount, true);
}
The function getTotalPrincipalTokensOutstandingInActiveLoans
is invoked before every lending. Hence an underflow here will cause the lending functionality to revert
Another quirk regarding excess repayments is that the lenders of the pool won't obtain the excess repaid amount since it is not accounted anywhere. But this cannot be considered an issue since the lenders are only guarenteed the original lending amount + interest
No response
No response
-
Attacker borrows 100 from the lender pool totalPrincipalTokensLended == 100 totalPrincipalTokensRepaid == 0
-
Attacker repays 101 totalPrincipalTokensLended == 100 totalPrincipalTokensRepaid == 101
Now getTotalPrincipalTokensOutstandingInActiveLoans
will always revert
Bricked lending functionality
No response
In case repaid principal is more, return 0 instead
sherlock-admin2
The protocol team fixed this issue in the following PRs/commits: teller-protocol/teller-protocol-v2-audit-2024#80
Source: #51
hash
Tokens that revert of zero value transfers can cause reverts on liquidation
In the readme the team has mentioned that they would like to know if any wierd token breaks their contract pools
In multiple places token amount which can become zero is transferred without checking the value is zero. This will cause these transactions to revert https://github.com/sherlock-audit/2024-11-teller-finance-update/blob/0c8535728f97d37a4052d2a25909d28db886a422/teller-protocol-v2-audit-2024/packages/contracts/contracts/LenderCommitmentForwarder/extensions/LenderCommitmentGroup/LenderCommitmentGroup_Smart.sol#L699-L727
IERC20(principalToken).safeTransferFrom(
msg.sender,
address(this),
amountDue + tokensToTakeFromSender - liquidationProtocolFee
);
address protocolFeeRecipient = ITellerV2(address(TELLER_V2)).getProtocolFeeRecipient();
IERC20(principalToken).safeTransferFrom(
msg.sender,
address(protocolFeeRecipient),
liquidationProtocolFee
);
totalPrincipalTokensRepaid += amountDue;
tokenDifferenceFromLiquidations += int256(tokensToTakeFromSender - liquidationProtocolFee );
} else {
uint256 tokensToGiveToSender = abs(minAmountDifference);
IERC20(principalToken).safeTransferFrom(
msg.sender,
address(this),
amountDue - tokensToGiveToSender
);
No response
No response
No response
In case liquidation reverts (due to tokensToGiveToSender == -amountDue), the tokenDifferenceFromLiquidations
won't be updated which will cause the value of the shares to be incorrectly high (because in reality the auction is settling at 0 price)
No response
Check if amount is non-zero before transferring
sherlock-admin2
The protocol team fixed this issue in the following PRs/commits: teller-protocol/teller-protocol-v2-audit-2024#82
Issue M-7: The totalPrincipalTokensRepaid
and totalInterestCollected
may not be updated even when funds are already transferred
Source: #54
KupiaSec
The LenderCommitmentGroup_Smart.repayLoanCallback()
function will be paused, causing the transaction to continue despite the revert. As a result, while the funds are transferred, the amounts will not be added to totalPrincipalTokensRepaid
and totalInterestCollected
. This discrepancy will lead to an incorrect calculation of the exchange rate, potentially resulting in a loss of funds for shareholders.
The LenderCommitmentGroup_Smart.repayLoanCallback()
function will revert due to being paused.
https://github.com/sherlock-audit/2024-11-teller-finance-update/blob/main/teller-protocol-v2-audit-2024/packages/contracts/contracts/LenderCommitmentForwarder/extensions/LenderCommitmentGroup/LenderCommitmentGroup_Smart.sol#L928-L945
function repayLoanCallback(
uint256 _bidId,
address repayer,
uint256 principalAmount,
uint256 interestAmount
@> ) external onlyTellerV2 whenForwarderNotPaused whenNotPaused bidIsActiveForGroup(_bidId) {
totalPrincipalTokensRepaid += principalAmount;
totalInterestCollected += interestAmount;
emit LoanRepaid(
_bidId,
repayer,
principalAmount,
interestAmount,
totalPrincipalTokensRepaid,
totalInterestCollected
);
}
However, the whole transaction will not be reverted because of the try/catch statement. https://github.com/sherlock-audit/2024-11-teller-finance-update/blob/main/teller-protocol-v2-audit-2024/packages/contracts/contracts/TellerV2.sol#L938-950
if (loanRepaymentListener != address(0)) {
require(gasleft() >= 80000, "NR gas"); //fixes the 63/64 remaining issue
@> try
ILoanRepaymentListener(loanRepaymentListener).repayLoanCallback{
gas: 80000
}( //limit gas costs to prevent lender preventing repayments
_bidId,
_msgSenderForMarket(bid.marketplaceId),
_payment.principal,
_payment.interest
)
@> {} catch {}
}
Borrowers can repay their loans even during a pause. This means that while the funds are transferred, the amounts will not be added to totalPrincipalTokensRepaid
and totalInterestCollected
. Consequently, the exchange rate will be calculated incorrectly, which could result in a loss of funds for shareholders.
none
none
none
Loss of fund to shareholders.
none
The LenderCommitmentGroup_Smart.repayLoanCallback()
function should not revert when paused.
sherlock-admin2
The protocol team fixed this issue in the following PRs/commits: teller-protocol/teller-protocol-v2-audit-2024#83
Source: #71
The protocol has acknowledged this issue.
hash
Taking min when calculating EMI repayment amount is flawed
The amount due in case of EMI repayment is calculated as: https://github.com/sherlock-audit/2024-11-teller-finance-update/blob/0c8535728f97d37a4052d2a25909d28db886a422/teller-protocol-v2-audit-2024/packages/contracts/contracts/libraries/V2Calculations.sol#L124-L138
} else {
// Default to PaymentType.EMI
// Max payable amount in a cycle
// NOTE: the last cycle could have less than the calculated payment amount
//the amount owed for the cycle should never exceed the current payment cycle amount so we use min here
uint256 owedAmountForCycle = Math.min( ((_bid.terms.paymentCycleAmount * owedTime) ) /
_paymentCycleDuration , _bid.terms.paymentCycleAmount+interest_ ) ;
uint256 owedAmount = isLastPaymentCycle
? owedPrincipal_ + interest_
: owedAmountForCycle ;
duePrincipal_ = Math.min(owedAmount - interest_, owedPrincipal_);
}
This is incorrect and leads to lowered payments since _bid.terms.paymentCycleAmount+interest_
will be taken instead of the ratio wise amount
Eg: Principal (P) = 100 Annual Rate (r) = 12% = 0.12 Number of Monthly Payments (n) = 12 monthly EMI = 8.84
But if the repayment occurs after 2 months, this formula calculates the amount due as 8.84 + 2 == 10.84 instead of 8.84 * 2
No response
No response
No response
Incorrectly lowered payments in case of EMI repayments
No response
Dont take the min. Instead use
((_bid.terms.paymentCycleAmount * owedTime) ) /
_paymentCycleDuration