Skip to content

Resolve open issues: reentrancy tests, TimeLock Ownable + grace period, Staking invariant - #14

Merged
oliv3rdrt merged 5 commits into
mainfrom
resolve-open-issues
Jul 29, 2026
Merged

Resolve open issues: reentrancy tests, TimeLock Ownable + grace period, Staking invariant#14
oliv3rdrt merged 5 commits into
mainfrom
resolve-open-issues

Conversation

@oliv3rdrt

Copy link
Copy Markdown
Owner

Works through the four open issues, one commit each (plus a gas-snapshot refresh).

Add reentrancy tests for Vault and Staking (#10). New test/Reentrancy.t.sol with malicious receivers that re-enter withdraw/unstake from inside receive(). The attack unwinds because state is updated before the external call, and the tests assert the honest balances and totals are untouched. Turns the assumed checks-effects-interactions property into a tested one.

TimeLock: make admin transferable via the two-step Ownable mixin (#12). TimeLock now inherits the existing src/Ownable.sol, so the deployer becomes the admin and the role can be handed over with propose plus accept. The onlyAdmin guard and the constructor admin argument are replaced by Ownable's onlyOwner and owner, with tests for the full transfer flow.

TimeLock: expire stale queued operations with a grace period (#11). Adds an immutable gracePeriod and reverts execute once block.timestamp is past eta + gracePeriod, following Compound's Timelock. Covered with a vm.warp stale test, and the fuzz test now stays inside the window.

Add a dedicated Staking invariant handler like VaultInvariant (#13). Moves the light inline Staking invariant into test/StakingInvariant.t.sol and gives it the same shape as the Vault handler: a bounded actor set, ghost variables, and three invariants (per-actor sum, net flow, balance versus totalStaked).

Verified locally: forge build clean, forge test 59 passed (up from 51), forge fmt --check clean.

Closes #10
Closes #11
Closes #12
Closes #13

Vault.withdraw and Staking.unstake both update state before the external call,
so a re-entrant call sees a balance that is already spent. Add malicious
receivers that re-enter withdraw and unstake from inside their receive(), and
assert the attack unwinds and leaves the honest balances and totals untouched.
This turns the assumed checks-effects-interactions property into a tested one.

Closes #10
TimeLock fixed the admin in the constructor with no way to rotate it. Inherit
the existing two-step Ownable mixin instead, so the deployer becomes the admin
and the role can be handed over with propose plus accept. The onlyAdmin guard
and the constructor admin argument are gone, replaced by Ownable's onlyOwner and
its owner. Tests deploy as the admin and cover the full transfer flow.

Closes #12
Once eta passed, a queued operation stayed executable forever. Follow the
pattern Compound's Timelock uses: add an immutable gracePeriod and revert
execute once block.timestamp is past eta plus the grace period, so a stale
operation has to be queued again. The constructor takes the grace period, the
fuzz test now stays within the window, and a vm.warp test covers the stale case.

Closes #11
Staking had a light inline invariant that only checked totalStaked against the
balance. Move it into its own test/StakingInvariant.t.sol and give it the same
shape as the Vault handler: a bounded actor set, ghost variables, and three
invariants (per-actor sum, net flow, and balance versus totalStaked). Drop the
old inline version so the staking accounting is exercised the same way Vault is.

Closes #13
@oliv3rdrt
oliv3rdrt merged commit 063f00f into main Jul 29, 2026
1 check passed
@oliv3rdrt
oliv3rdrt deleted the resolve-open-issues branch July 29, 2026 09:38
@oliv3rdrt oliv3rdrt self-assigned this Jul 29, 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

1 participant