Skip to content

Fix missing emission Jacobian in VRGDA decay integral#166

Open
kbrizzle wants to merge 4 commits intomasterfrom
britz-fix-vrgda-emission
Open

Fix missing emission Jacobian in VRGDA decay integral#166
kbrizzle wants to merge 4 commits intomasterfrom
britz-fix-vrgda-emission

Conversation

@kbrizzle
Copy link
Copy Markdown
Contributor

@kbrizzle kbrizzle commented Apr 8, 2026

Summary

  • exponentialDecay integrates the VRGDA price curve over auction-time but was missing the Jacobian factor (emission) from the change of variables out of token-space (dn = emission * dt). This caused the price parameter to behave as price-per-unit-of-auction-time instead of price-per-token.
  • Added emission parameter to exponentialDecay() and exponentialDecayI() in VRGDADecayMath.sol, applying * emission in the forward formula and / emission in the inverse.
  • Updated LinearExponentialVRGDA.toCost() and toAmount() to thread emission through.
  • All test expected values updated for emission = 200. Both fuzz tests pass.

Why

The canonical VRGDA defines targetPrice as the per-token price. When extending to a continuous integral and changing variables from token-amount to auction-time, a Jacobian factor of emission is required. The existing tests didn't catch it because they only verified self-consistency (roundtrip, monotonicity), not absolute pricing at neutral time.


Note

Medium Risk
Changes core VRGDA pricing/inversion math and function signatures, which will materially change auction prices and could impact any integrators relying on previous (incorrect) behavior.

Overview
Fixes a pricing bug in VRGDADecayMath by adding an emission parameter to exponentialDecay/exponentialDecayI and applying the missing emission Jacobian so price is interpreted as per-token rather than per unit of auction-time.

Threads emission through LinearExponentialVRGDA.toCost/toAmount, updates all affected test vectors, and adds a new neutral-time test to assert marginal cost ≈ price * amount and that toAmount(toCost(x)) round-trips near schedule.

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

Summary by CodeRabbit

  • Bug Fixes

    • Decay-based pricing and inverse calculations now correctly incorporate emission adjustments, restoring consistent cost/amount conversions.
  • Tests

    • Added a neutral-time unit test validating on-schedule pricing/amount recovery.
    • Updated test expectations across pricing and conversion scenarios to reflect the corrected decay behavior.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 8, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: d61964ee-5593-43d7-ae5b-162aa819fcc3

📥 Commits

Reviewing files that changed from the base of the PR and between 07e4dc1 and f537683.

📒 Files selected for processing (1)
  • src/vrgda/VRGDADecayMath.sol

📝 Walkthrough

Walkthrough

Added an emission parameter to VRGDA decay math (forward and inverse), propagated it into the LinearExponentialVRGDA library calls, and updated tests (including a new neutral-time test and adjusted expected values) to reflect emission-corrected forward and inverse computations.

Changes

Cohort / File(s) Summary
Core VRGDA Decay Math
src/vrgda/VRGDADecayMath.sol
Added emission parameter to exponentialDecay(...) and exponentialDecayI(...). Forward computation multiplies the integral term by emission; inverse uses sEmission in the logarithm to align with the emission-corrected forward formula. Doc comments updated.
LinearExponentialVRGDA Integration
src/vrgda/types/LinearExponentialVRGDA.sol
Passed self.emission into VRGDADecayMath.exponentialDecay(...) and ...exponentialDecayI(...) calls to match new signatures; no other logic changes.
LinearExponentialVRGDA Tests
test/vrgda/LinearExponentialVRGDA.t.sol
Added test_neutralTimePriceEqualsTarget. Updated expected results in multiple pricing/conversion tests to reflect the emission-adjusted math; test control flow and assertions preserved.
VRGDAMath Tests
test/vrgda/VRGDAMath.t.sol
Imported/used UFixed18Lib and inserted UFixed18Lib.from(200) as the new emission argument in all exponentialDecay/exponentialDecayI calls. Updated deterministic expected outputs and tightened fuzz price upper bound.
🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: adding the missing emission Jacobian to the VRGDA decay integral calculations in exponentialDecay and exponentialDecayI functions.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch britz-fix-vrgda-emission

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions
Copy link
Copy Markdown

github-actions bot commented Apr 8, 2026

Slither report

Static Analysis Report**THIS CHECKLIST IS NOT COMPLETE**. Use `--show-ignored-findings` to show all the results. Summary - [locked-ether](#locked-ether) (1 results) (Medium) - [reentrancy-no-eth](#reentrancy-no-eth) (3 results) (Medium) - [unused-return](#unused-return) (9 results) (Medium) - [incorrect-modifier](#incorrect-modifier) (1 results) (Low) - [calls-loop](#calls-loop) (2 results) (Low) - [reentrancy-benign](#reentrancy-benign) (3 results) (Low) - [reentrancy-events](#reentrancy-events) (4 results) (Low) - [dead-code](#dead-code) (44 results) (Informational) - [solc-version](#solc-version) (3 results) (Informational) - [missing-inheritance](#missing-inheritance) (2 results) (Informational) - [naming-convention](#naming-convention) (13 results) (Informational) - [unimplemented-functions](#unimplemented-functions) (4 results) (Informational) - [unindexed-event-address](#unindexed-event-address) (1 results) (Informational) ## locked-ether Impact: Medium Confidence: High - [ ] ID-0 Contract locking ether found: Contract [MutablePauseTarget](https://github.com/equilibria-xyz/root/blob/1ca90e482d2f6341ab6f0256c6ced56cb451fca9/src/mutability/Mutable.sol#L138-L143) has payable functions: - [MutablePauseTarget.fallback()](https://github.com/equilibria-xyz/root/blob/1ca90e482d2f6341ab6f0256c6ced56cb451fca9/src/mutability/Mutable.sol#L139-L141) But does not have a function to withdraw the ether

https://github.com/equilibria-xyz/root/blob/1ca90e482d2f6341ab6f0256c6ced56cb451fca9/src/mutability/Mutable.sol#L138-L143

reentrancy-no-eth

Impact: Medium
Confidence: Medium

function create(
IImplementation implementation,
bytes calldata data
) public onlyOwner returns (IMutableTransparent newMutable) {
ShortString name = ShortStrings.toShortString(implementation.name());
if (_nameToMutable[name] != IMutable(address(0))) revert MutatorMutableAlreadyExists();
_mutables.add(address(newMutable = new Mutable(implementation, data)));
_nameToMutable[name] = IMutable(address(newMutable));
// ensure state of new mutable is consistent with mutator
if (paused()) IMutable(address(newMutable)).pause();
}

function _unpause() private whenPaused {
ERC1967Utils.upgradeToAndCall(Mutable$().paused, "");
Mutable$().paused = address(0);
emit Unpaused();
}

function _upgrade(IImplementation newImplementation, bytes memory data) private {
// validate the upgrade metadata of the new implementation
if (!Strings.equal(
(_implementation() == address(0) ? NULL_VERSION : IImplementation(_implementation()).version()),
newImplementation.predecessor()
)) revert MutablePredecessorMismatch();
if (Strings.equal(
newImplementation.version(),
ShortStrings.toString(Mutable$().version)
)) revert MutableVersionMismatch();
// update the implementation and call its constructor
ERC1967Utils.upgradeToAndCall(address(newImplementation), abi.encodeCall(IImplementation.construct, (data)));
// record the new implementation version
Mutable$().version = ShortStrings.toShortString(newImplementation.version());
}

unused-return

Impact: Medium
Confidence: Medium

function approve(Token6 self, address grantee) internal {
IERC20(Token6.unwrap(self)).approve(grantee, type(uint256).max);
}

function removeDistribution(bytes32 merkleRoot) external onlyOwner {
if (!_merkleRoots.contains(merkleRoot)) revert AirdropRootDoesNotExist();
_merkleRoots.remove(merkleRoot);
distributions[merkleRoot] = Token18.wrap(address(0));
emit DistributionRemoved(merkleRoot);
}

function approve(Token self, address grantee, uint256 amount) internal {
IERC20(Token.unwrap(self)).approve(grantee, amount);
}

function approve(Token18 self, address grantee, UFixed18 amount) internal {
IERC20(Token18.unwrap(self)).approve(grantee, UFixed18.unwrap(amount));
}

function approve(Token6 self, address grantee, UFixed6 amount) internal {
IERC20(Token6.unwrap(self)).approve(grantee, UFixed6.unwrap(amount));
}

function addDistributions(Token18 token, bytes32 merkleRoot) external override onlyOwner {
if (_merkleRoots.contains(merkleRoot)) revert AirdropDistributionAlreadyExists();
distributions[merkleRoot] = token;
_merkleRoots.add(merkleRoot);
emit DistributionAdded(token, merkleRoot);
}

function approve(Token self, address grantee) internal {
IERC20(Token.unwrap(self)).approve(grantee, type(uint256).max);
}

function create(
IImplementation implementation,
bytes calldata data
) public onlyOwner returns (IMutableTransparent newMutable) {
ShortString name = ShortStrings.toShortString(implementation.name());
if (_nameToMutable[name] != IMutable(address(0))) revert MutatorMutableAlreadyExists();
_mutables.add(address(newMutable = new Mutable(implementation, data)));
_nameToMutable[name] = IMutable(address(newMutable));
// ensure state of new mutable is consistent with mutator
if (paused()) IMutable(address(newMutable)).pause();
}

function approve(Token18 self, address grantee) internal {
IERC20(Token18.unwrap(self)).approve(grantee, type(uint256).max);
}

incorrect-modifier

Impact: Low
Confidence: High

modifier initializer(string memory attribute) {
if (!_constructing()) revert AttributeNotConstructing();
if (!Attribute$().attributes[attribute]) _;
Attribute$().attributes[attribute] = true;
}

calls-loop

Impact: Low
Confidence: Medium

function _unpause() internal override {
for (uint256 i = 0; i < _mutables.length(); i++) IMutable(_mutables.at(i)).unpause();
super._unpause();
}

function _pause() internal override {
for (uint256 i = 0; i < _mutables.length(); i++) IMutable(_mutables.at(i)).pause();
super._pause();
}

reentrancy-benign

Impact: Low
Confidence: Medium

function _pause() internal override {
for (uint256 i = 0; i < _mutables.length(); i++) IMutable(_mutables.at(i)).pause();
super._pause();
}

function _unpause() internal override {
for (uint256 i = 0; i < _mutables.length(); i++) IMutable(_mutables.at(i)).unpause();
super._unpause();
}

function create(
IImplementation implementation,
bytes calldata data
) public onlyOwner returns (IMutableTransparent newMutable) {
ShortString name = ShortStrings.toShortString(implementation.name());
if (_nameToMutable[name] != IMutable(address(0))) revert MutatorMutableAlreadyExists();
_mutables.add(address(newMutable = new Mutable(implementation, data)));
_nameToMutable[name] = IMutable(address(newMutable));
// ensure state of new mutable is consistent with mutator
if (paused()) IMutable(address(newMutable)).pause();
}

reentrancy-events

Impact: Low
Confidence: Medium

function _pause() private whenUnpaused {
Mutable$().paused = _implementation();
ERC1967Utils.upgradeToAndCall(_pauseTarget, "");
emit Paused();
}

function _unpause() internal override {
for (uint256 i = 0; i < _mutables.length(); i++) IMutable(_mutables.at(i)).unpause();
super._unpause();
}

function _unpause() private whenPaused {
ERC1967Utils.upgradeToAndCall(Mutable$().paused, "");
Mutable$().paused = address(0);
emit Unpaused();
}

function _pause() internal override {
for (uint256 i = 0; i < _mutables.length(); i++) IMutable(_mutables.at(i)).pause();
super._pause();
}

dead-code

Impact: Informational
Confidence: Medium

function eq(UFixed18 a, UFixed18 b) pure returns (bool) {
return UFixed18.unwrap(a) == UFixed18.unwrap(b);
}

function add(Fixed6 a, Fixed6 b) pure returns (Fixed6) {
return Fixed6.wrap(Fixed6.unwrap(a) + Fixed6.unwrap(b));
}

function eq(UFixed6 a, UFixed6 b) pure returns (bool) {
return UFixed6.unwrap(a) == UFixed6.unwrap(b);
}

  • ID-26
    neg(Fixed6) is never used and should be removed

function neg(Fixed6 a) pure returns (Fixed6) {
return Fixed6.wrap(-Fixed6.unwrap(a));
}

function _constructing() internal view override returns (bool) {
return Implementation$().constructing;
}

function add(Fixed18 a, Fixed18 b) pure returns (Fixed18) {
return Fixed18.wrap(Fixed18.unwrap(a) + Fixed18.unwrap(b));
}

function mul(Fixed6 a, Fixed6 b) pure returns (Fixed6) {
return Fixed6.wrap(Fixed6.unwrap(a) * Fixed6.unwrap(b) / Fixed6Lib.BASE);
}

function eq(Fixed6 a, Fixed6 b) pure returns (bool) {
return Fixed6.unwrap(a) == Fixed6.unwrap(b);
}

function gt(UFixed6 a, UFixed6 b) pure returns (bool) {
(uint256 au, uint256 bu) = (UFixed6.unwrap(a), UFixed6.unwrap(b));
return au > bu;
}

function neq(Fixed6 a, Fixed6 b) pure returns (bool) {
return Fixed6.unwrap(a) != Fixed6.unwrap(b);
}

function mul(Fixed18 a, Fixed18 b) pure returns (Fixed18) {
return Fixed18.wrap(Fixed18.unwrap(a) * Fixed18.unwrap(b) / Fixed18Lib.BASE);
}

function gt(Fixed6 a, Fixed6 b) pure returns (bool) {
(int256 au, int256 bu) = (Fixed6.unwrap(a), Fixed6.unwrap(b));
return au > bu;
}

function lt(UFixed18 a, UFixed18 b) pure returns (bool) {
(uint256 au, uint256 bu) = (UFixed18.unwrap(a), UFixed18.unwrap(b));
return au < bu;
}

function div(UFixed18 a, UFixed18 b) pure returns (UFixed18) {
return UFixed18.wrap(UFixed18.unwrap(a) * UFixed18Lib.BASE / UFixed18.unwrap(b));
}

function gte(UFixed6 a, UFixed6 b) pure returns (bool) {
return eq(a, b) || gt(a, b);
}

function neg(Fixed18 a) pure returns (Fixed18) {
return Fixed18.wrap(-Fixed18.unwrap(a));
}

function sub(Fixed6 a, Fixed6 b) pure returns (Fixed6) {
return Fixed6.wrap(Fixed6.unwrap(a) - Fixed6.unwrap(b));
}

function gte(Fixed6 a, Fixed6 b) pure returns (bool) {
return eq(a, b) || gt(a, b);
}

function mul(UFixed6 a, UFixed6 b) pure returns (UFixed6) {
return UFixed6.wrap(UFixed6.unwrap(a) * UFixed6.unwrap(b) / UFixed6Lib.BASE);
}

function lte(Fixed6 a, Fixed6 b) pure returns (bool) {
return eq(a, b) || lt(a, b);
}

function lte(UFixed18 a, UFixed18 b) pure returns (bool) {
return eq(a, b) || lt(a, b);
}

function mul(UFixed18 a, UFixed18 b) pure returns (UFixed18) {
return UFixed18.wrap(UFixed18.unwrap(a) * UFixed18.unwrap(b) / UFixed18Lib.BASE);
}

function lt(Fixed6 a, Fixed6 b) pure returns (bool) {
(int256 au, int256 bu) = (Fixed6.unwrap(a), Fixed6.unwrap(b));
return au < bu;
}

function div(Fixed6 a, Fixed6 b) pure returns (Fixed6) {
return Fixed6.wrap(Fixed6.unwrap(a) * Fixed6Lib.BASE / Fixed6.unwrap(b));
}

function neq(UFixed18 a, UFixed18 b) pure returns (bool) {
return UFixed18.unwrap(a) != UFixed18.unwrap(b);
}

function _deployer() internal view override returns (address) {
return IMutator(msg.sender).owner();
}

function div(Fixed18 a, Fixed18 b) pure returns (Fixed18) {
return Fixed18.wrap(Fixed18.unwrap(a) * Fixed18Lib.BASE / Fixed18.unwrap(b));
}

function lt(UFixed6 a, UFixed6 b) pure returns (bool) {
(uint256 au, uint256 bu) = (UFixed6.unwrap(a), UFixed6.unwrap(b));
return au < bu;
}

function lt(Fixed18 a, Fixed18 b) pure returns (bool) {
(int256 au, int256 bu) = (Fixed18.unwrap(a), Fixed18.unwrap(b));
return au < bu;
}

function gt(Fixed18 a, Fixed18 b) pure returns (bool) {
(int256 au, int256 bu) = (Fixed18.unwrap(a), Fixed18.unwrap(b));
return au > bu;
}

function lte(Fixed18 a, Fixed18 b) pure returns (bool) {
return eq(a, b) || lt(a, b);
}

function eq(Fixed18 a, Fixed18 b) pure returns (bool) {
return Fixed18.unwrap(a) == Fixed18.unwrap(b);
}

function sub(UFixed18 a, UFixed18 b) pure returns (UFixed18) {
return UFixed18.wrap(UFixed18.unwrap(a) - UFixed18.unwrap(b));
}

function neq(UFixed6 a, UFixed6 b) pure returns (bool) {
return UFixed6.unwrap(a) != UFixed6.unwrap(b);
}

function gt(UFixed18 a, UFixed18 b) pure returns (bool) {
(uint256 au, uint256 bu) = (UFixed18.unwrap(a), UFixed18.unwrap(b));
return au > bu;
}

function gte(UFixed18 a, UFixed18 b) pure returns (bool) {
return eq(a, b) || gt(a, b);
}

function sub(UFixed6 a, UFixed6 b) pure returns (UFixed6) {
return UFixed6.wrap(UFixed6.unwrap(a) - UFixed6.unwrap(b));
}

function add(UFixed6 a, UFixed6 b) pure returns (UFixed6) {
return UFixed6.wrap(UFixed6.unwrap(a) + UFixed6.unwrap(b));
}

function sub(Fixed18 a, Fixed18 b) pure returns (Fixed18) {
return Fixed18.wrap(Fixed18.unwrap(a) - Fixed18.unwrap(b));
}

function gte(Fixed18 a, Fixed18 b) pure returns (bool) {
return eq(a, b) || gt(a, b);
}

function lte(UFixed6 a, UFixed6 b) pure returns (bool) {
return eq(a, b) || lt(a, b);
}

function div(UFixed6 a, UFixed6 b) pure returns (UFixed6) {
return UFixed6.wrap(UFixed6.unwrap(a) * UFixed6Lib.BASE / UFixed6.unwrap(b));
}

function add(UFixed18 a, UFixed18 b) pure returns (UFixed18) {
return UFixed18.wrap(UFixed18.unwrap(a) + UFixed18.unwrap(b));
}

function neq(Fixed18 a, Fixed18 b) pure returns (bool) {
return Fixed18.unwrap(a) != Fixed18.unwrap(b);
}

solc-version

Impact: Informational
Confidence: High

pragma solidity ^0.8.13;

pragma solidity ^0.8.19;

pragma solidity >=0.8.20;

missing-inheritance

Impact: Informational
Confidence: High

contract OwnableStub {
/// @notice Accepts ownership of the contract
/// @dev Can only be called by the pending owner to ensure correctness.
function acceptOwner(address ownable) external {
Ownable(ownable).acceptOwner();
}
}

https://github.com/equilibria-xyz/root/blob/1ca90e482d2f6341ab6f0256c6ced56cb451fca9/src/mutability/Mutable.sol#L138-L143

naming-convention

Impact: Informational
Confidence: High

function __Pausable__constructor() internal initializer("Pausable") {
_updatePauser(_deployer());
}

function Mutable$() private pure returns (MutableStorage storage $) {
assembly {
$.slot := MutableStorageLocation
}
}

bytes32 private constant MutableStorageLocation = 0xb906736fa3fc696e6c19a856e0f8737e348fda5c7f33a32db99da3b92f19a800;

bytes32 private constant OwnableStorageLocation = 0x863176706c9b4c9b393005d0714f55de5425abea2a0b5dfac67fac0c9e2ffe00;

function __Ownable__constructor() internal initializer("Ownable") {
_updateOwner(_deployer());
}

bytes32 private constant ImplementationStorageLocation = 0x3c57b102c533ff058ebe9a7c745178ce4174563553bb3edde7874874c532c200;

function Ownable$() private pure returns (OwnableStorage storage $) {
assembly {
$.slot := OwnableStorageLocation
}
}

function Implementation$() private pure returns (ImplementationStorage storage $) {
assembly {
$.slot := ImplementationStorageLocation
}
}

bytes32 private constant PausableStorageLocation = 0x3f6e81f1674f7eaca7e8904fa6f14f10175d4d641e37fc18a3df849e00101900;

function Pausable$() private pure returns (PausableStorage storage $) {
assembly {
$.slot := PausableStorageLocation
}
}

function __constructor(bytes memory data) internal virtual returns (string memory);

function Attribute$() private pure returns (AttributeStorage storage $) {
assembly {
$.slot := AttributeStorageLocation
}
}

bytes32 private constant AttributeStorageLocation = 0x429797e2de2710eed6bc286312ff2c2286e5c3e13ca14d38e450727a132bfa00;

unimplemented-functions

Impact: Informational
Confidence: High

abstract contract Withdrawable is IWithdrawable, Attribute, Ownable {
/// @notice Withdraws all ERC20 tokens from the contract to the owner
/// @dev Can only be called by the owner
/// @param token Address of the ERC20 token
function withdraw(Token18 token) public virtual onlyOwner {
token.push(owner());
}
}

abstract contract Implementation is IImplementation, Contract {
/// @custom:storage-location erc7201:equilibria.root.Implementation
struct ImplementationStorage {
bool constructing;
}
/// @dev The erc7201 storage location of the mix-in
// solhint-disable-next-line const-name-snakecase
bytes32 private constant ImplementationStorageLocation = 0x3c57b102c533ff058ebe9a7c745178ce4174563553bb3edde7874874c532c200;
/// @dev The erc7201 storage of the mix-in
function Implementation$() private pure returns (ImplementationStorage storage $) {
assembly {
$.slot := ImplementationStorageLocation
}
}
/// @dev The version of this implementation.
ShortString private immutable _version;
/// @dev The version of the predecessor implementation.
ShortString private immutable _predecessor;
/// @dev Constructor for the implementation.
constructor(string memory version_, string memory predecessor_) {
_version = ShortStrings.toShortString(version_);
_predecessor = ShortStrings.toShortString(predecessor_);
}
/// @dev The name of the implementation.
function name() external view virtual returns (string memory);
/// @dev The version of this implementation.
function version() public view virtual returns (string memory) {
return ShortStrings.toString(_version);
}
/// @dev The version of the predecessor implementation.
function predecessor() external view virtual returns (string memory) {
return ShortStrings.toString(_predecessor);
}
/// @dev Called at upgrade time to initialize the contract with `data`.
function construct(bytes memory data) external {
Implementation$().constructing = true;
string memory constructorVersion = __constructor(data);
if (!Strings.equal(constructorVersion, version())) revert ImplementationConstructorVersionMismatch();
Implementation$().constructing = false;
}
/// @dev Whether the contract is initializing.
function _constructing() internal view override returns (bool) {
return Implementation$().constructing;
}
/// @dev The deployer of the contract.
function _deployer() internal view override returns (address) {
return IMutator(msg.sender).owner();
}
/// @dev Hook for inheriting contracts to construct the contract.
function __constructor(bytes memory data) internal virtual returns (string memory);
}

abstract contract Delegatable is IDelegatable, Attribute, Ownable {
/// @notice Delegates voting power for a specific token to an address
/// @dev Can only be called by the owner
/// @param token The IVotes-compatible token to delegate
/// @param delegatee The address to delegate voting power to
function delegate(IVotes token, address delegatee) public virtual onlyOwner {
token.delegate(delegatee);
}
}

abstract contract Executable is IExecutable, Attribute, Ownable {
/// @notice Executes a call to a target contract
/// @dev Can only be called by the owner
/// @param target Address of the target contract
/// @param data Calldata to be executed
/// @return result The result of the call
function execute(address target, bytes calldata data) public payable virtual onlyOwner returns (bytes memory) {
return Address.functionCallWithValue(target, data, msg.value);
}
}

unindexed-event-address

Impact: Informational
Confidence: High


Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
test/vrgda/VRGDAMath.t.sol (1)

186-194: Fuzz emission instead of pinning it to 200.

The changed behavior is now parameterized by emission, but Lines 192 and 194 still only exercise a single value. Varying emission over a nonzero range would cover the Jacobian scaling directly and make it much easier to keep a regression for the current CI counterexample.


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 5c6a1e32-8dc4-408b-a9a7-928a8911341b

📥 Commits

Reviewing files that changed from the base of the PR and between 4cd20c6 and 357d9fa.

📒 Files selected for processing (4)
  • src/vrgda/VRGDADecayMath.sol
  • src/vrgda/types/LinearExponentialVRGDA.sol
  • test/vrgda/LinearExponentialVRGDA.t.sol
  • test/vrgda/VRGDAMath.t.sol

Comment thread src/vrgda/VRGDADecayMath.sol
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (2)
test/vrgda/LinearExponentialVRGDA.t.sol (2)

33-37: Tighten the Jacobian regression tolerances.

Line 33 and Line 37 allow 10% relative error, which is quite loose for a targeted correctness guard. Consider reducing to ~1% (or tighter if stable) so scaling regressions are caught earlier.

♻️ Proposed refactor
-        assertApproxEqRel(UFixed18.unwrap(cost), 0.1e18, 0.1e18, "neutral time price should approximate price * amount");
+        assertApproxEqRel(UFixed18.unwrap(cost), 0.1e18, 0.01e18, "neutral time price should approximate price * amount");
...
-        assertApproxEqRel(UFixed18.unwrap(recovered), UFixed18.unwrap(smallAmount), 0.1e18, "neutral time toAmount(toCost(x)) should approximate x");
+        assertApproxEqRel(UFixed18.unwrap(recovered), UFixed18.unwrap(smallAmount), 0.01e18, "neutral time toAmount(toCost(x)) should approximate x");

24-27: Make neutral-time setup relative, not absolute.

Line 26 and line 27 hardcode both wall-clock time and issued amount. This makes the test fragile: it implicitly assumes block.timestamp at setUp time equals exactly 1 day (86400 seconds), which is not guaranteed by Foundry or RootTest. Use relative timing instead.

♻️ Proposed refactor
-        vm.warp(86400 + 86400); // time() = 2.0 days, timestamp = 1.0 day, elapsed = 1.0 day
-        issued = UFixed18Lib.from(200); // exactly on schedule
+        vm.warp(block.timestamp + 1 days);
+        issued = vrgda.emission; // exactly on schedule

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 8fe3e737-4499-4108-aa8c-bd34113c445f

📥 Commits

Reviewing files that changed from the base of the PR and between 46a483a and 07e4dc1.

📒 Files selected for processing (1)
  • test/vrgda/LinearExponentialVRGDA.t.sol

@github-actions
Copy link
Copy Markdown

github-actions bot commented Apr 8, 2026

Unit Test Coverage Report

Coverage after merging britz-fix-vrgda-emission into master will be
100.00%
Coverage Report
FileStmtsBranchesFuncsLinesUncovered Lines
src/attribute
   Attribute.sol90%50%100%100%30
   Delegatable.sol100%100%100%100%
   Executable.sol100%100%100%100%
   Ownable.sol100%100%100%100%
   Pausable.sol100%100%100%100%
   Withdrawable.sol100%100%100%100%
src/distribution
   Airdrop.sol100%100%100%100%
   Treasury.sol100%100%100%100%
src/mutability
   Derived.sol100%100%100%100%
   Implementation.sol100%100%100%100%
   Mutable.sol100%100%100%100%
   Mutator.sol100%100%100%100%
src/number
   NumberMath.sol100%100%100%100%
src/number/types
   Fixed18.sol100%100%100%100%
   Fixed6.sol100%100%100%100%
   UFixed18.sol100%100%100%100%
   UFixed6.sol100%100%100%100%
src/token/types
   Token.sol100%100%100%100%
   Token18.sol100%100%100%100%
   Token6.sol100%100%100%100%
src/utils
   OwnableStub.sol100%100%100%100%
   console.sol99.89%60%100%100%44, 65
src/vrgda
   VRGDADecayMath.sol100%100%100%100%
   VRGDAIssuanceMath.sol100%100%100%100%
src/vrgda/types
   LinearExponentialVRGDA.sol100%100%100%100%

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.

1 participant