diff --git a/src/ReservoirPriceOracle.sol b/src/ReservoirPriceOracle.sol index 2562040..4ca2f47 100644 --- a/src/ReservoirPriceOracle.sol +++ b/src/ReservoirPriceOracle.sol @@ -5,11 +5,7 @@ import { IERC20 } from "forge-std/interfaces/IERC20.sol"; import { IERC4626 } from "forge-std/interfaces/IERC4626.sol"; import { OracleErrors } from "src/libraries/OracleErrors.sol"; -import { - IReservoirPriceOracle, - OracleAverageQuery, - OracleLatestQuery -} from "src/interfaces/IReservoirPriceOracle.sol"; +import { IReservoirPriceOracle, OracleAverageQuery, OracleLatestQuery } from "src/interfaces/IReservoirPriceOracle.sol"; import { IPriceOracle } from "src/interfaces/IPriceOracle.sol"; import { QueryProcessor, ReservoirPair, PriceType } from "src/libraries/QueryProcessor.sol"; import { Utils } from "src/libraries/Utils.sol"; @@ -241,12 +237,10 @@ contract ReservoirPriceOracle is IPriceOracle, IReservoirPriceOracle, Owned(msg. lPayoutAmt = block.basefee * rewardGasAmount; } - if (lPayoutAmt <= address(this).balance) { - // REVIEW: This can still revert, rather than balance checking we - // could just try/catch or use Yul to ignore reverts? - payable(aRecipient).transfer(lPayoutAmt); + // does not revert even if transfer fails + assembly ("memory-safe") { + let result := call(gas(), aRecipient, lPayoutAmt, codesize(), 0x00, codesize(), 0x00) } - // do nothing if lPayoutAmt is greater than the balance } /// @return rRoute The route to determine the price between aToken0 and aToken1 diff --git a/test/mock/GasBuster.sol b/test/mock/GasBuster.sol new file mode 100644 index 0000000..930c762 --- /dev/null +++ b/test/mock/GasBuster.sol @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.0; + +contract GasBuster { + // Allow the contract to receive ETH + receive() external payable { + while(true) { + // This loop will continue until all gas is consumed + } + } +} diff --git a/test/unit/ReservoirPriceOracle.t.sol b/test/unit/ReservoirPriceOracle.t.sol index 6082106..b333d06 100644 --- a/test/unit/ReservoirPriceOracle.t.sol +++ b/test/unit/ReservoirPriceOracle.t.sol @@ -20,6 +20,7 @@ import { EnumerableSetLib } from "lib/solady/src/utils/EnumerableSetLib.sol"; import { Constants } from "src/libraries/Constants.sol"; import { MockFallbackOracle } from "test/mock/MockFallbackOracle.sol"; import { StubERC4626 } from "test/mock/StubERC4626.sol"; +import { GasBuster } from "test/mock/GasBuster.sol"; contract ReservoirPriceOracleTest is BaseTest { using Utils for *; @@ -621,6 +622,31 @@ contract ReservoirPriceOracleTest is BaseTest { assertEq(lPriceAB, 0); // composite price is not stored in the cache } + function testUpdatePrice_RecipientOutOfGas() external { + // arrange + GasBuster lGasBuster = new GasBuster(); + + _writePriceCache(address(_tokenA), address(_tokenB), 5e18); + deal(address(_oracle), 1 ether); + + skip(1); + _pair.sync(); + skip(_oracle.twapPeriod() * 2); + _tokenA.mint(address(_pair), 2e18); + _pair.swap(2e18, true, address(this), ""); + + // act + _oracle.updatePrice(address(_tokenB), address(_tokenA), address(lGasBuster)); + + // assert + (uint256 lPrice,) = _oracle.priceCache(address(_tokenA), address(_tokenB)); + assertEq(lPrice, 98_918_868_099_219_913_512); + (lPrice,) = _oracle.priceCache(address(_tokenB), address(_tokenA)); + assertEq(lPrice, 0); + assertEq(address(this).balance, block.basefee * _oracle.rewardGasAmount()); + assertEq(address(_oracle).balance, 1 ether - block.basefee * _oracle.rewardGasAmount()); + } + function testSetRoute() public { // arrange address lToken0 = address(_tokenB);