Spot Price Oracle
Briefing
A small lending market values collateral using the reserves of a constant-product pool. Flash loans of the borrowable asset are available from other protocols.
// Quantix Security Research Network — training challenge.// Intentionally vulnerable. Written for education; do not deploy.// SPDX-License-Identifier: MITpragma solidity 0.8.24; interface IPair { function getReserves() external view returns (uint112 reserve0, uint112 reserve1, uint32 ts);} interface IERC20 { function transfer(address, uint256) external returns (bool); function transferFrom(address, address, uint256) external returns (bool);} contract SpotLending { IPair public immutable pair; // token0 = COLL, token1 = USD IERC20 public immutable coll; IERC20 public immutable usd; uint256 public constant LTV_BPS = 7_500; mapping(address => uint256) public collateral; mapping(address => uint256) public debt; constructor(IPair _pair, IERC20 _coll, IERC20 _usd) { pair = _pair; coll = _coll; usd = _usd; } /// USD per 1e18 COLL, from current pool reserves. function price() public view returns (uint256) { (uint112 r0, uint112 r1, ) = pair.getReserves(); return (uint256(r1) * 1e18) / r0; } function depositCollateral(uint256 amount) external { require(coll.transferFrom(msg.sender, address(this), amount), "transfer failed"); collateral[msg.sender] += amount; } function borrow(uint256 amount) external { uint256 value = (collateral[msg.sender] * price()) / 1e18; require(debt[msg.sender] + amount <= (value * LTV_BPS) / 10_000, "undercollateralised"); debt[msg.sender] += amount; require(usd.transfer(msg.sender, amount), "transfer failed"); }}Hints
Reveal one at a time. Each moves a step closer to the answer.
- H1
Hidden
- H2
Hidden
- H3
Hidden
Solution
Try writing your own explanation and fix before opening it.
In one transaction the attacker flash-borrows USD, swaps it into the pool for COLL, which raises `r1 / r0`, deposits a small amount of COLL as collateral, and borrows USD against the inflated price. They then swap back, repay the flash loan and keep the excess borrowed USD, leaving the market with under-collateralised debt.
The attack is profitable whenever the extra borrowing capacity exceeds the swap fees and price-impact losses of moving the pool. With thin liquidity this cost is small.
Fix: price collateral from a manipulation-resistant source (a time-weighted average over an appropriate window, or an external oracle network with staleness and deviation checks), bound borrowing against the lower of reference and spot prices, and cap per-block exposure for assets with thin liquidity.
- function price() public view returns (uint256) {- (uint112 r0, uint112 r1, ) = pair.getReserves();- return (uint256(r1) * 1e18) / r0;- }+ IPriceFeed public immutable feed; // aggregated external feed, 18 decimals+ uint256 public constant MAX_AGE = 1 hours;+ uint256 public constant MAX_DEVIATION_BPS = 200;++ function price() public view returns (uint256) {+ (uint256 ref, uint256 updatedAt) = feed.latest();+ require(block.timestamp - updatedAt <= MAX_AGE, "stale price");+ (uint112 r0, uint112 r1, ) = pair.getReserves();+ uint256 spot = (uint256(r1) * 1e18) / r0;+ uint256 diff = spot > ref ? spot - ref : ref - spot;+ require(diff * 10_000 <= ref * MAX_DEVIATION_BPS, "price deviation");+ return spot < ref ? spot : ref; // value collateral at the lower price+ }