[BUG]: Old prices stay in the feed after someone resubmits (decay is not multiplicative)

Open
#25 1 comment 0 reactions 0 assignees View on GitHub

@Deepak-negi11 is already working on this.

Since Nov 7, 2026.

  • #26 by @Deepak-negi11 — open

Assessment

Difficulty
5/5
Estimated time
Over a week
Newbie friendliness
35/100
Issue type
Bug
Clarity
Clearly specified
Activity status
Active
Tech stack
solidity

Research direction

Start with src/Oracle.sol:109-125 and src/DecayLib.sol:36-47, then run the reproduction with forge test --match-path test/DecayRepro.t.sol -vv. The failing assertions show that decay does not compose and old values remain after correction; done means the decay behavior is consistent across submission intervals and the reproduced feed returns 100 after Alice's correction. The issue asks whether to use exponential decay or weights at a fixed reference time, so the design choice needs resolution.

Written by the indexing model from the issue text.

Description

bug
Bug Description

When someone corrects their price part of their old price stays in the feed.

submitValue decays the total weight a little at every submission (src/Oracle.sol:109). Then it takes out the submitter's old weight decayed in one go from their last submit (src/Oracle.sol:116). That only works if small steps give the same result as one big step. DecayLib.decayFactor draws straight lines between powers of 2 (src/DecayLib.sol:36-47) so they don't match. Two half steps give 0.5625 but one full half life gives 0.5. So less weight gets taken out than is really there and the leftover keeps the old price.

The paper's decay does compose (decay_add in OrbOracle-Formalization Oracle.v). Posters submit every 60s by default so this happens all the time. It's also why #17 can't be fixed cleanly yet. I also hit a related underflow at src/Oracle.sol:125 with some stake values.

Steps to Reproduce
  1. Make an oracle with a 60s half life. Alice stakes 100 and Bob stakes 1.
  2. Alice submits 10000.
  3. Bob submits 100 once in between.
  4. After 60s Alice submits 100.
  5. readValue() should be 100 but it's 678. If Bob submits 59 times in between it's 1035.
Logs and Screenshots

Save the test below as test/DecayRepro.t.sol and run forge test --match-path test/DecayRepro.t.sol -vv. Both tests fail on main.

Exact equality isn't possible in fixed point so the asserts allow 1 wei and 100 wei of rounding.

Reproduction test
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import {Test, console2} from "lib/forge-std/src/Test.sol";
import {ERC20} from "lib/openzeppelin-contracts/contracts/token/ERC20/ERC20.sol";
import {Oracle} from "../src/Oracle.sol";
import {DecayLib} from "../src/DecayLib.sol";

contract MockToken is ERC20 {
    constructor() ERC20("Weight", "WGT") {}

    function mint(address to, uint256 amount) external {
        _mint(to, amount);
    }
}

contract DecayReproTest is Test {
    address alice = makeAddr("alice"); // stake 100
    address bob = makeAddr("bob"); // stake 1

    function _stake(Oracle o, MockToken t, address user, uint256 amount) internal {
        t.mint(user, amount);
        vm.startPrank(user);
        t.approve(address(o), amount);
        o.depositTokens(amount);
        vm.stopPrank();
    }

    // Alice says 10000, then corrects herself to 100 one half-life later.
    // Bob says 100 a few times in between. Everyone now says 100, so the feed should be 100.
    function _feedAfterCorrection(uint256 bobSubmissions) internal returns (uint256) {
        MockToken t = new MockToken();
        // halfLife 60s, q 50000, both locks 60s, rewardBps 100, gamma 0, sampleSize 5
        Oracle o = new Oracle(address(this), "test", "test", address(t), 60, 50_000, 60, 60, 100, 0, 5);
        _stake(o, t, alice, 100e18);
        _stake(o, t, bob, 1e18);

        uint256 start = vm.getBlockTimestamp() + 61;
        vm.warp(start);
        vm.prank(alice);
        o.submitValue(10_000e18);

        uint256 step = 60 / (bobSubmissions + 1);
        for (uint256 i = 1; i <= bobSubmissions; i++) {
            vm.warp(start + i * step);
            vm.prank(bob);
            o.submitValue(100e18);
        }

        vm.warp(start + 60);
        vm.prank(alice);
        o.submitValue(100e18);
        return o.readValue();
    }

    function test_decayIsNotMultiplicative() public pure {
        uint256 half = DecayLib.decayFactor(30, 60); // 0.5 half-life
        console2.log("decay(0.5 HL) * decay(0.5 HL):", half * half / 1e18);
        console2.log("decay(1 HL):                  ", DecayLib.decayFactor(60, 60));
        // 1 wei of slack for fixed point rounding
        assertApproxEqAbs(half * half / 1e18, DecayLib.decayFactor(60, 60), 1, "decay must compose");
    }

    function test_oldValueStaysInFeed() public {
        console2.log("bob submits 0 times in between -> feed:", _feedAfterCorrection(0));
        console2.log("bob submits 1 time  in between -> feed:", _feedAfterCorrection(1));
        console2.log("bob submits 59 times in between -> feed:", _feedAfterCorrection(59));
        // 100 wei of slack for fixed point rounding
        assertApproxEqAbs(_feedAfterCorrection(1), 100e18, 100, "old value remains in feed");
    }
}
Ran 2 tests for test/DecayRepro.t.sol:DecayReproTest
[FAIL: decay must compose: 562500000000000000 !~= 500000000000000000 (max delta: 1, real delta: 62500000000000000)] test_decayIsNotMultiplicative() (gas: 35082)
Logs:
  decay(0.5 HL) * decay(0.5 HL): 562500000000000000
  decay(1 HL):                   500000000000000000

[FAIL: old value remains in feed: 678271028037383177569 !~= 100000000000000000000 (max delta: 100, real delta: 578271028037383177569)] test_oldValueStaysInFeed() (gas: 23326813)
Logs:
  bob submits 0 times in between -> feed: 100000000000000000000
  bob submits 1 time  in between -> feed: 678271028037383177569
  bob submits 59 times in between -> feed: 1035439416692341479361

Suite result: FAILED. 0 passed; 2 failed; 0 skipped; finished in 29.44ms (23.91ms CPU time)

All submitted values are 100 after Alice's correction so the feed should go back to 100. It stays at about 678 with one submission in between and 1035 with 59.

I'd like to work on this. Would you prefer exponential decay or weights stored at a fixed reference time?

Environment Details

macOS with forge 1.8.7 (same as CI) and solc 0.8.33 with via_ir. Upstream main at c2cb5bd. Local test only with no fork. I used Claude Code to help write the test and this text and I checked the numbers myself.

Impact

High - Major feature is broken

Code of Conduct
  • I have joined the Discord server and will post updates there
  • I have searched existing issues to avoid duplicates
Dominant language
Solidity
Stars
3
Forks
3
Avg merge
15h 55m
Merged PRs (30d)
6

Getting set up

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from StabilityNexus/OrbOracle-Solidity

All issues in StabilityNexus/OrbOracle-Solidity

Similar issues

More Backend & API Design issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.