Skip to content

Merge v1.0 audit fixes to v3#157

Merged
EdNoepel merged 2 commits intov3from
audit-fix
Dec 2, 2025
Merged

Merge v1.0 audit fixes to v3#157
EdNoepel merged 2 commits intov3from
audit-fix

Conversation

@EdNoepel
Copy link
Contributor

No description provided.

@github-actions
Copy link

Unit Test Coverage Report

Coverage after merging audit-fix 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%

@github-actions
Copy link

Slither report

Static Analysis Report**THIS CHECKLIST IS NOT COMPLETE**. Use `--show-ignored-findings` to show all the results. Summary - [locked-ether](#locked-ether) (2 results) (Medium) - [reentrancy-no-eth](#reentrancy-no-eth) (2 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) ## locked-ether Impact: Medium Confidence: High - [ ] ID-0 Contract locking ether found: Contract [Mutator](https://github.com/equilibria-xyz/root/blob/c1ab099968cc4a8434a3eb8db942ac8c250477b6/src/mutability/Mutator.sol#L13-L63) has payable functions: - [IMutator.upgrade(IImplementation,bytes)](https://github.com/equilibria-xyz/root/blob/c1ab099968cc4a8434a3eb8db942ac8c250477b6/src/mutability/interfaces/IMutator.sol#L16) - [Mutator.upgrade(IImplementation,bytes)](https://github.com/equilibria-xyz/root/blob/c1ab099968cc4a8434a3eb8db942ac8c250477b6/src/mutability/Mutator.sol#L48-L52) But does not have a function to withdraw the ether

contract Mutator is IMutator, Derived, Pausable {
using EnumerableSet for EnumerableSet.AddressSet;
EnumerableSet.AddressSet private _mutables;
mapping(ShortString => IMutable) private _nameToMutable;
constructor() {
__Pausable__constructor();
__Ownable__constructor();
}
/// @dev The list of all mutables.
function mutables() public view returns (address[] memory) {
return _mutables.values();
}
/// @notice Creates a new mutable with the given name
/// @dev Initializes the mutable with the given implementation and data
/// @param implementation The implementation of the mutable
/// @param data The calldata to invoke the instance's initializer
/// @return newMutable The new mutable
function create(
IImplementation implementation,
bytes calldata data
) public onlyOwner returns (IMutableTransparent newMutable) {
_mutables.add(address(newMutable = new Mutable(implementation, data)));
_nameToMutable[ShortStrings.toShortString(implementation.name())] = IMutable(address(newMutable));
// ensure state of new mutable is consistent with mutator
if (paused()) IMutable(address(newMutable)).pause();
}
/// @notice Upgrades the implementation of a proxy and optionally calls its initializer
/// @param implementation New version of contract to be proxied
/// @param data Calldata to invoke the instance's initializer
function upgrade(IImplementation implementation, bytes memory data) public payable onlyOwner {
ShortString name = ShortStrings.toShortString(implementation.name());
if (_nameToMutable[name] == IMutable(address(0))) revert MutatorInvalidMutable();
_nameToMutable[name].upgrade(implementation, data);
}
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();
}
}

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

reentrancy-no-eth

Impact: Medium
Confidence: Medium

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 create(
IImplementation implementation,
bytes calldata data
) public onlyOwner returns (IMutableTransparent newMutable) {
_mutables.add(address(newMutable = new Mutable(implementation, data)));
_nameToMutable[ShortStrings.toShortString(implementation.name())] = IMutable(address(newMutable));
// ensure state of new mutable is consistent with mutator
if (paused()) IMutable(address(newMutable)).pause();
}

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

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 _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();
}

reentrancy-benign

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();
}

function create(
IImplementation implementation,
bytes calldata data
) public onlyOwner returns (IMutableTransparent newMutable) {
_mutables.add(address(newMutable = new Mutable(implementation, data)));
_nameToMutable[ShortStrings.toShortString(implementation.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() internal override {
for (uint256 i = 0; i < _mutables.length(); i++) IMutable(_mutables.at(i)).pause();
super._pause();
}

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();
}

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/c1ab099968cc4a8434a3eb8db942ac8c250477b6/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);
}
}


@EdNoepel EdNoepel changed the base branch from master to v3 November 26, 2025 21:43
@EdNoepel EdNoepel changed the title Merge v1.0 audit fixes to master Merge v1.0 audit fixes to v3 Nov 26, 2025
@EdNoepel EdNoepel merged commit d20f0d6 into v3 Dec 2, 2025
3 checks passed
@EdNoepel EdNoepel deleted the audit-fix branch December 2, 2025 13:20
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.

3 participants