Blackhan Software Disclosed Report

vault insolvency

Created date
May 09 2025

Target

https://github.com/blackhan-software/xpower-banq/tree/5f734cc14fd05afcf7ee221de0bb600f8ca2339c

Vulnerability Details

_settle function of the Pool contract that lets users reduce their debt by more than the actual assets they deposit into the Vault. this happens because the burn function uses amount instead of assets, and when the Vault has an entry fee, it causes a collateral shortage, bankrupting the vault. github : https://github.com/blackhan-software/xpower-banq/blob/5f734cc14fd05afcf7ee221de0bb600f8ca2339c/source/contract/Pool.sol#L382 code :

function _settle(
    address user,
    IERC20 token,
    uint256 amount
) private returns (uint256 assets) {
    IVault vault = _vault[token];
    token.safeTransferFrom(user, address(this), amount);
    assert(token.approve(address(vault), amount));
    uint256 shares = vault.deposit(amount, address(this));
    assets = vault.convertToAssets(shares);
    vault.borrow().burn(user, amount, false);  // bug: should use assets, not amount
    require(assets > 0, EmptySettle(user, token, amount));
}

line vault.borrow().burn(user, amount, false) is wrong. it burns amount (the tokens transferred) instead of assets (the actual value added to the Vault after fees). Vault can have an entry fee (handled by _entryFee()), so assets is less than amount. for example, if a user deposits 100 tokens with a 1% fee, assets is 99, but burn reduces the debt by 100. this over-reduces the user’s debt, leaving the Vault undercollateralized. over time, the Vault loses assets compared to recorded debts, leading to insolvency.

fix : update the _settle function to burn assets instead of amount

Validation steps

foundry:

function test_incorrect_debt_settlement() public {
    // user borrows 100 tokens
    uint256 borrowamount = 100 * 10**token.decimals();
    pool.borrow(token, borrowamount, false, IFlash(address(0)), "");
    
    // user settles with 100 tokens, vault has 1% entry fee
    uint256 settleamount = 100 * 10**token.decimals();
    uint256 assets = pool.settle(token, settleamount);
    
    // debt reduced by 100, but vault only got 99
    uint256 debt = vault.borrow().totalOf(user);
    assert(debt == 0);  // debt fully cleared, vault undercollateralized
}

Attachments

hidden
CommentsReport History
Comments on this report are hidden
Details
Statedisclosed
Severity
Medium
Bounty$500
Visibilitypartially
VulnerabilityOther
Participants (3)
company admin
company admin
triage team