Skip to content

OraclePriceAdjuster: scale the oracle answer to the pair's token decimals - #197

Closed
zexoverz wants to merge 1 commit into
1inch:mainfrom
zexoverz:oracle-adjuster-token-decimals
Closed

zexoverz wants to merge 1 commit into
1inch:mainfrom
zexoverz:oracle-adjuster-token-decimals

Conversation

@zexoverz

@zexoverz zexoverz commented Sep 10, 2026

Copy link
Copy Markdown

Follow-up to #31, which reported the scale mismatch and was closed as out of focus. What that report
did not say is that it pays out rather than just misbehaving, so here is the fix in case it is worth
having.

oraclePrice is scaled to 1e18, currentPrice is amountOut * 1e18 / amountIn in raw token
amounts. Those agree only on an 18/18 pair. On 18/6 the swap price is 1e12 smaller, so
oraclePrice > currentPrice is true on every fill and min(priceRatio, 2e18 - maxPriceDecay)
returns the cap. With maxPriceDecay = 0 that is 2x. There is no safe setting, since
maxPriceDecay < ONE is required at build.

The added test shows it. Reverting just the scaling line and rerunning gives:

1) test_ScaleAnswerMatchesTheRawUnitConvention()   3000000000000000000000 != 3000000000
2) test_OracleAdjusterIgnoresWorseFeed()           6000000000 != 3000000000
3) test_OracleAdjusterDoesNotAdjustWhenFeedMatchesCurve()  6000000000 != 3000000000
4) test_OracleAdjusterAppliesFeedPriceWhenBetter() 6000000000 !~= 3150000000

The fix scales to 10 ** (18 + tokenOutDecimals - tokenInDecimals) instead. On an 18/18 pair that
exponent is 18, so nothing changes there. Decimals are declared in the program rather than read from
the tokens to keep two external calls off the hot path.

This changes the encoding, so it is breaking for anyone building the instruction today. Happy to do
it another way if you would rather not.

803 passing locally, up from 797. .gas-snapshot not regenerated; per AGENTS.md new functions are
informational there.


Note

High Risk
Changes swap payout logic tied to oracle comparison and uses a breaking instruction encoding; incorrect decimals would mis-price adjustments, though the fix addresses a known overpayment on common decimal pairs.

Overview
Fixes OraclePriceAdjuster comparing Chainlink answers at a fixed 1e18 scale while currentPrice uses raw amountOut / amountIn, which diverges on mixed-decimal pairs (e.g. 18/6) and could treat every fill as oracle-favorable—capping at up to output when maxPriceDecay is minimal.

The instruction encoding now includes tokenInDecimals and tokenOutDecimals, and scaleAnswer rescales the feed to 10 ** (18 + tokenOutDecimals - tokenInDecimals) so it matches the swap price units (unchanged for 18/18 pairs). build / parse / sizeOf are updated accordingly—breaking for existing bytecode builders.

Adds PriceOracleMock, integration tests on a WETH/USDC-style pair (match feed, better/worse feed, cap), and unit tests for scaleAnswer.

Reviewed by Cursor Bugbot for commit 490941b. Bugbot is set up for automated code reviews on this repo. Configure here.

…mals

The instruction scaled the Chainlink answer to 1e18 and compared it against
a swap price computed from raw token amounts. Those match only when both
tokens have 18 decimals. On an 18/6 pair the swap price is 1e12 smaller, so
the oracle is found better on every fill and min(priceRatio, 2e18 -
maxPriceDecay) returns the cap.

Takes tokenInDecimals and tokenOutDecimals in the encoding and scales to
10 ** (18 + tokenOutDecimals - tokenInDecimals). On an 18/18 pair that
exponent is 18, so behaviour there is unchanged.

Refs 1inch#31

@SteMak SteMak left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PR proposes to modify OraclePriceAdjuster opcode implementing support for non-18 decimals tokens while keeping the opcode itself non-functional.

The OraclePriceAdjuster is not a production-ready opcode and is not part of deployed code. The code requires deep refactoring due to precision issues, inability to proceed reversed feeds (using USD/ETH feed for both ETH -> USD and USD -> ETH swaps), price-based instead of amount-based math.
Though, it's hard to validate the solution itself does not have precision issues.

I'd reject this PR as out-of-focus. There is no need to fix minors in demo code that requires complete rewrite.

/// @dev Encoding: [uint64 maxPriceDecay, uint16 maxStaleness, uint8 oracleDecimals, uint8 tokenInDecimals, uint8 tokenOutDecimals, address oracleAddress]
/// maxStaleness = 0 skips the staleness check, oracleDecimals = 0 fetches decimals from the oracle
/// @dev Supports only single direction swaps, adjustment is applied only if favorable for the taker
/// @dev tokenInDecimals and tokenOutDecimals describe the swap direction the instruction runs on.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Decimals are just decimals, they do not describe the swap direction - technically invalid claim


/// @notice Scales an oracle answer to the units the swap price is computed in
/// @dev A single net exponent, so the answer's low digits survive a scale-down
function scaleAnswer(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Helper functions should be placed below exec

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Function per 2-3 LoC is a poor style

} else if (oracleDecimals > DECIMALS) {
oraclePrice = oraclePrice / 10 ** (oracleDecimals - DECIMALS);
}
// Convert oracle price to the scale currentPrice below is computed in, which is 1e18 only

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

currentPrice is not a literal

@SteMak

SteMak commented Sep 15, 2026

Copy link
Copy Markdown
Contributor

Out of development focus. The opcode is demo and requires complete rewrite or needs to be removed.

@SteMak SteMak closed this Sep 15, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants