Skip to content

Commit

Permalink
Forkability improvements (#2335)
Browse files Browse the repository at this point in the history
* add fork detector contract

* forkability improvements

This PR introduces a new core utility contract, `ForkDetector`, which
provides a way for developer forks (such as Tenderly, Anvil) to
distinguish themselves from the canonical chain safely. This can be
useful as there are some functionalities which are difficult to operate
on a fork (ex. offchain prices or precompiled contracts), and the
behavior in those places can be selectively mocked to prevent
interruption of testing.

Design Considerations:
* The idea of these changes is to provide a one-line way for integrators
  or anyone trying to fork the synthetix system a way to make Synthetix
  fork-friendly setup. In the future, who knows, this functionality could be
  enabled by default.
* Original functionality of the chain can be restored by removing or
  modifying the code at `0x1234123412341234123412341234123412341234`, in
  case your tests require it

Specific changes:
* add `ForkDetector` library
* in op gas price oracle, replace calls to the OP precompiled contract
  with mock values or skip
* in arb gas price oracle, replace calls to the arbitrum precompiled
  contract with mock values or skip
* in pyth ERC7412 adapter proxy, use whatever the latest price available
  on-chain is regardless of its staleness. Additionally, prices can be
  overridden by calling the contract (note: it was previously possible
  to simply switch out the oracle node on the oracle manager as well,
  but sometimes this lever is much more convenient to pull)

Tests to be written tomorrow

* add tests

* set reasonable mock values

reccomendation from audit

* misc audit fixes

* add events for fork-only write operations
* misc lint/solidity style warnings
* move definition of contractCode variable

* fix abstract proxy is not effectively used by the pyth wrapper

audit note

* add flag

* undo storage changes

* fix yarn lock
  • Loading branch information
dbeal-eth authored Nov 17, 2024
1 parent f52c801 commit 2215a0e
Show file tree
Hide file tree
Showing 14 changed files with 275 additions and 19 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
/**/contracts/generated
/utils/hardhat-router/test
**/contracts/routers
**/lib

artifacts
cache
coverage
Expand Down
29 changes: 19 additions & 10 deletions auxiliary/ArbitrumGasPriceOracle/contracts/ArbGasPriceOracle.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
pragma solidity >=0.8.11 <0.9.0;

import {SafeCastU256} from "@synthetixio/core-contracts/contracts/utils/SafeCast.sol";
import {ForkDetector} from "@synthetixio/core-contracts/contracts/utils/ForkDetector.sol";
import {IExternalNode, NodeOutput, NodeDefinition} from "@synthetixio/oracle-manager/contracts/interfaces/external/IExternalNode.sol";
import {ArbGasInfo} from "./interfaces/ArbGasInfo.sol";

Expand Down Expand Up @@ -129,6 +130,10 @@ contract ArbGasPriceOracle is IExternalNode {
(address, uint256, uint256, uint256, uint256, uint256, uint256)
);

if (ForkDetector.isDevFork()) {
return true;
}

// verify the oracle can be properly called
try PRECOMPILE.getPricesInWei() {
// do nothing
Expand Down Expand Up @@ -168,16 +173,20 @@ contract ArbGasPriceOracle is IExternalNode {
function getCostOfExecutionEth(
RuntimeParams memory runtimeParams
) internal view returns (uint256 costOfExecutionGrossEth) {
// fetch & define L2 gas price
/// @dev perArbGasTotal is the best estimate of the L2 gas price "base fee" in wei
(, , , , , uint256 perArbGasTotal) = PRECOMPILE.getPricesInWei();

// fetch & define L1 gas base fee; incorporate overhead buffer
/// @dev if the estimate is too low or high at the time of the L1 batch submission,
/// the transaction will still be processed, but the arbitrum nitro mechanism will
/// amortize the deficit/surplus over subsequent users of the chain
/// (i.e. lowering/raising the L1 base fee for a period of time)
uint256 l1BaseFee = PRECOMPILE.getL1BaseFeeEstimate();
uint256 perArbGasTotal = 10000000;
uint256 l1BaseFee = 300000000;
if (!ForkDetector.isDevFork()) {
// fetch & define L2 gas price
/// @dev perArbGasTotal is the best estimate of the L2 gas price "base fee" in wei
(, , , , , perArbGasTotal) = PRECOMPILE.getPricesInWei();

// fetch & define L1 gas base fee; incorporate overhead buffer
/// @dev if the estimate is too low or high at the time of the L1 batch submission,
/// the transaction will still be processed, but the arbitrum nitro mechanism will
/// amortize the deficit/surplus over subsequent users of the chain
/// (i.e. lowering/raising the L1 base fee for a period of time)
l1BaseFee = PRECOMPILE.getL1BaseFeeEstimate();
}

// fetch & define gas units consumed on L1 and L2 for the given execution kind
(uint256 gasUnitsL1, uint256 gasUnitsL2) = getGasUnits(runtimeParams);
Expand Down
2 changes: 2 additions & 0 deletions auxiliary/ArbitrumGasPriceOracle/foundry.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[profile.default]
allow_paths = ["../../"]
2 changes: 2 additions & 0 deletions auxiliary/ArbitrumGasPriceOracle/remappings.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
@synthetixio/core-contracts=../../utils/core-contracts
@synthetixio/oracle-manager=../../protocol/oracle-manager
27 changes: 27 additions & 0 deletions auxiliary/ArbitrumGasPriceOracle/test/ArbGasPriceOracle.test.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.11 <0.9.0;

import "../contracts/ArbGasPriceOracle.sol";

import {Test} from "forge-std/Test.sol";

contract ArbGasPriceOracleTest is Test {
ArbGasPriceOracle private oracle;

function setUp() external {
vm.etch(0x1234123412341234123412341234123412341234, "FORK");
oracle = new ArbGasPriceOracle(address(0));
}

function testIsValidVerifies() external view {
bytes memory fakeParams = abi.encode(address(0), 0, 0, 0, 0, 0, 0);
oracle.isValid(
NodeDefinition.Data(NodeDefinition.NodeType.EXTERNAL, fakeParams, new bytes32[](0))
);
}

function testProcessProcesses() external view {
bytes memory fakeParams = abi.encode(address(0), 0, 0, 0, 0, 0, 0);
oracle.process(new NodeOutput.Data[](0), fakeParams, new bytes32[](0), new bytes32[](0));
}
}
12 changes: 12 additions & 0 deletions auxiliary/OpGasPriceOracle/contracts/OpGasPriceOracle.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
pragma solidity >=0.8.11 <0.9.0;

import {SafeCastU256} from "@synthetixio/core-contracts/contracts/utils/SafeCast.sol";
import {ForkDetector} from "@synthetixio/core-contracts/contracts/utils/ForkDetector.sol";
import {DecimalMath} from "@synthetixio/core-contracts/contracts/utils/DecimalMath.sol";
import {IExternalNode, NodeOutput, NodeDefinition} from "@synthetixio/oracle-manager/contracts/interfaces/external/IExternalNode.sol";
import "./interfaces/IOVM_GasPriceOracle_fjord.sol";
Expand All @@ -21,6 +22,7 @@ contract OpGasPriceOracle is IExternalNode {
uint256 public constant KIND_SETTLEMENT = 0;
uint256 public constant KIND_FLAG = 1;
uint256 public constant KIND_LIQUIDATE = 2;

struct RuntimeParams {
// Set up params
// Order execution
Expand Down Expand Up @@ -129,6 +131,11 @@ contract OpGasPriceOracle is IExternalNode {
function getCostOfExecutionEth(
RuntimeParams memory runtimeParams
) internal view returns (uint256 costOfExecutionGrossEth) {
if (ForkDetector.isDevFork()) {
// return some default value as this precompiled contract may misbehave outside of actual OP network
return 0.001 ether;
}

IOVM_GasPriceOracle ovmGasPriceOracle = IOVM_GasPriceOracle(ovmGasPriceOracleAddress);
OvmGasPriceOracleMode oracleMode;

Expand Down Expand Up @@ -234,6 +241,11 @@ contract OpGasPriceOracle is IExternalNode {
)
);

// skip the oracle call check if on a fork
if (ForkDetector.isDevFork()) {
return true;
}

// Must be able to call the oracle
IOVM_GasPriceOracle ovmGasPriceOracle = IOVM_GasPriceOracle(ovmGasPriceOracleAddress);

Expand Down
2 changes: 2 additions & 0 deletions auxiliary/OpGasPriceOracle/foundry.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[profile.default]
allow_paths = ["../../"]
2 changes: 2 additions & 0 deletions auxiliary/OpGasPriceOracle/remappings.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
@synthetixio/core-contracts=../../utils/core-contracts
@synthetixio/oracle-manager=../../protocol/oracle-manager
27 changes: 27 additions & 0 deletions auxiliary/OpGasPriceOracle/test/OpGasPriceOracle.test.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.11 <0.9.0;

import "../contracts/OpGasPriceOracle.sol";

import {Test} from "forge-std/Test.sol";

contract OpGasPriceOracleTest is Test {
OpGasPriceOracle private oracle;

function setUp() external {
vm.etch(0x1234123412341234123412341234123412341234, "FORK");
oracle = new OpGasPriceOracle(address(0));
}

function testIsValidVerifies() external view {
bytes memory fakeParams = abi.encode(address(0), 0, 0, 0, 0, 0, 0, 0, 0, 0);
oracle.isValid(
NodeDefinition.Data(NodeDefinition.NodeType.EXTERNAL, fakeParams, new bytes32[](0))
);
}

function testProcessProcesses() external view {
bytes memory fakeParams = abi.encode(address(0), 0, 0, 0, 0, 0, 0, 0, 0, 0);
oracle.process(new NodeOutput.Data[](0), fakeParams, new bytes32[](0), new bytes32[](0));
}
}
58 changes: 49 additions & 9 deletions auxiliary/PythERC7412Wrapper/contracts/PythERC7412Wrapper.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,33 +3,50 @@ pragma solidity >=0.8.11 <0.9.0;

import {DecimalMath} from "@synthetixio/core-contracts/contracts/utils/DecimalMath.sol";
import {SafeCastI256} from "@synthetixio/core-contracts/contracts/utils/SafeCast.sol";
import {AbstractProxy} from "@synthetixio/core-contracts/contracts/proxy/AbstractProxy.sol";
import {ForkDetector} from "@synthetixio/core-contracts/contracts/utils/ForkDetector.sol";
import {PythStructs, IPyth} from "@synthetixio/oracle-manager/contracts/interfaces/external/IPyth.sol";
import {IERC7412} from "./interfaces/IERC7412.sol";
import {Price} from "./storage/Price.sol";

contract PythERC7412Wrapper is IERC7412, AbstractProxy {
contract PythERC7412Wrapper is IERC7412 {
using DecimalMath for int64;
using SafeCastI256 for int256;

event ForkBenchmarkPriceSet(bytes32 priceId, uint64 requestedTime, int256 newPrice, int32 expo);
event ForkLatestPriceSet(bytes32 priceId, int256 newPrice);

int256 private constant PRECISION = 18;

error NotSupported(uint8 updateType);

address public immutable pythAddress;

// NOTE: this value is only settable on a fork
mapping(bytes32 => int256) overridePrices;

constructor(address _pythAddress) {
pythAddress = _pythAddress;
}

function _getImplementation() internal view override returns (address) {
return pythAddress;
}

function oracleId() external pure returns (bytes32) {
return bytes32("PYTH");
}

function setBenchmarkPrice(
bytes32 priceId,
uint64 requestedTime,
int256 newPrice,
int32 expo
) external {
ForkDetector.requireFork();

// solhint-disable-next-line numcast/safe-cast
Price.load(priceId).benchmarkPrices[requestedTime].price = int64(newPrice);
Price.load(priceId).benchmarkPrices[requestedTime].expo = expo;

emit ForkBenchmarkPriceSet(priceId, requestedTime, newPrice, expo);
}

function getBenchmarkPrice(
bytes32 priceId,
uint64 requestedTime
Expand All @@ -40,6 +57,14 @@ contract PythERC7412Wrapper is IERC7412, AbstractProxy {
return _getScaledPrice(priceData.price, priceData.expo);
}

if (ForkDetector.isDevFork() && priceData.price == 0) {
// Return whatever the latest available price is on chain to avoid difficult errors
// if price is set negative then oracle data required will still be returned
IPyth pyth = IPyth(pythAddress);
PythStructs.Price memory pythData = pyth.getPriceUnsafe(priceId);
return _getScaledPrice(pythData.price, pythData.expo);
}

revert OracleDataRequired(
// solhint-disable-next-line numcast/safe-cast
address(this),
Expand All @@ -53,14 +78,27 @@ contract PythERC7412Wrapper is IERC7412, AbstractProxy {
);
}

function setLatestPrice(bytes32 priceId, int256 newPrice) external {
ForkDetector.requireFork();

overridePrices[priceId] = newPrice;

emit ForkLatestPriceSet(priceId, newPrice);
}

function getLatestPrice(
bytes32 priceId,
uint256 stalenessTolerance
) external view returns (int256) {
bool isFork = ForkDetector.isDevFork();
if (isFork && overridePrices[priceId] != 0) {
return overridePrices[priceId];
}

IPyth pyth = IPyth(pythAddress);
PythStructs.Price memory pythData = pyth.getPriceUnsafe(priceId);

if (block.timestamp <= stalenessTolerance + pythData.publishTime) {
if (isFork || block.timestamp <= stalenessTolerance + pythData.publishTime) {
return _getScaledPrice(pythData.price, pythData.expo);
}

Expand All @@ -84,7 +122,8 @@ contract PythERC7412Wrapper is IERC7412, AbstractProxy {

if (updateType == 1) {
(
uint8 _updateType,
,
/* uint8 _updateType */
uint64 stalenessTolerance,
bytes32[] memory priceIds,
bytes[] memory updateData
Expand Down Expand Up @@ -117,7 +156,8 @@ contract PythERC7412Wrapper is IERC7412, AbstractProxy {
}
} else if (updateType == 2) {
(
uint8 _updateType,
,
/* uint8 _updateType */
uint64 timestamp,
bytes32[] memory priceIds,
bytes[] memory updateData
Expand Down
2 changes: 2 additions & 0 deletions auxiliary/PythERC7412Wrapper/foundry.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[profile.default]
allow_paths = ["../../"]
2 changes: 2 additions & 0 deletions auxiliary/PythERC7412Wrapper/remappings.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
@synthetixio/core-contracts=../../utils/core-contracts
@synthetixio/oracle-manager=../../protocol/oracle-manager
71 changes: 71 additions & 0 deletions auxiliary/PythERC7412Wrapper/test/PythERC7412Wrapper.test.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.11 <0.9.0;

import "../contracts/PythERC7412Wrapper.sol";

import {Test} from "forge-std/Test.sol";

contract MockPythApi {
function getPriceUnsafe(bytes32) external pure returns (PythStructs.Price memory price) {
return PythStructs.Price(100, 0, -17, 0);
}
}

contract PythERC7412WrapperTest is Test {
MockPythApi mockPyth;
PythERC7412Wrapper wrapper;
bytes32 testFeedId = keccak256("test");

function setUp() external {
vm.etch(0x1234123412341234123412341234123412341234, "FORK");
mockPyth = new MockPythApi();
wrapper = new PythERC7412Wrapper(address(mockPyth));
}

function testSetLatestPriceOutsideOfForkFails() external {
vm.etch(0x1234123412341234123412341234123412341234, "notf");

vm.expectRevert(ForkDetector.OnlyOnDevFork.selector);
wrapper.setLatestPrice(testFeedId, 500);
}

function testSetLatestPrice() external {
wrapper.setLatestPrice(testFeedId, 500);

assertEq(wrapper.getLatestPrice(testFeedId, 0), 500);
}

function testGetLatestPrice() external view {
assertEq(wrapper.getLatestPrice(testFeedId, 0), 1000);
}

function testSetBenchmarkPriceOutsideOfForkFails() external {
vm.etch(0x1234123412341234123412341234123412341234, "notf");

vm.expectRevert(ForkDetector.OnlyOnDevFork.selector);
wrapper.setLatestPrice(testFeedId, 500);
}

function testSetBenchmarkPrice() external {
wrapper.setBenchmarkPrice(testFeedId, 100, 1234, -18);

assertEq(wrapper.getBenchmarkPrice(testFeedId, 100), 1234);
}

function testGetBenchmarkPriceWithSkip() external {
wrapper.setBenchmarkPrice(testFeedId, 100, -1, -18);

vm.expectRevert(
abi.encodeWithSelector(
IERC7412.OracleDataRequired.selector,
address(wrapper),
hex"000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000649c22ff5f21f0b81b113e63f7db6da94fedef11b2119b4088b89664fb9a3cb658"
)
);
wrapper.getBenchmarkPrice(testFeedId, 100);
}

function testGetBenchmarkPrice() external view {
assertEq(wrapper.getBenchmarkPrice(testFeedId, 100), 1000);
}
}
Loading

0 comments on commit 2215a0e

Please sign in to comment.