Skip to content

Add a revocable vesting wallet extension#6525

Open
knoal wants to merge 1 commit into
OpenZeppelin:masterfrom
knoal:feat/vesting-wallet-revocable
Open

Add a revocable vesting wallet extension#6525
knoal wants to merge 1 commit into
OpenZeppelin:masterfrom
knoal:feat/vesting-wallet-revocable

Conversation

@knoal
Copy link
Copy Markdown

@knoal knoal commented May 16, 2026

Summary

  • add VestingWalletRevocable as a VestingWallet extension with per-asset revocation
  • freeze vesting at revocation time while keeping already vested funds claimable by the beneficiary
  • return the unvested portion of revoked Ether or ERC20 assets to a designated revoker
  • add README coverage and focused tests for Ether and ERC20 revocation flows

Context

Closes #6493.

Design

This keeps the beneficiary as the inherited owner() of VestingWallet, and introduces a separate immutable revoker account authorized to cancel vesting. On revocation, the historical allocation and revocation timestamp are snapshotted so vestedAmount continues to report the same vested total after the contract balance changes.

Testing

  • npm test -- test/finance/VestingWallet.test.js test/finance/VestingWalletCliff.test.js test/finance/VestingWalletRevocable.test.js

@knoal knoal requested a review from a team as a code owner May 16, 2026 04:40
@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented May 16, 2026

⚠️ No Changeset found

Latest commit: c7fdae0

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 16, 2026

Review Change Stack

Walkthrough

This PR introduces VestingWalletRevocable, a new contract that extends OpenZeppelin's VestingWallet with per-asset revocation functionality. The revoker can revoke vesting for the native asset and individual ERC-20 tokens, which transfers only unvested amounts back while allowing already-vested portions to remain claimable. After revocation, vestedAmount() calculations freeze at the revocation timestamp using historical allocation snapshots. The implementation includes custom errors for authorization and double-revocation prevention, alongside a comprehensive test suite validating constructor state, access control, revocation mechanics, and forbidden state transitions.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the primary change: adding a revocable vesting wallet extension, which matches the main changeset of adding VestingWalletRevocable contract, tests, and documentation.
Description check ✅ Passed The description is directly related to the changeset, covering the addition of VestingWalletRevocable, per-asset revocation, vesting freezing, fund returns, test coverage, and the design approach with adequate technical detail.
Linked Issues check ✅ Passed All objectives from issue #6493 are met: VestingWalletRevocable allows authorized revoker to cancel vesting while keeping vested amounts claimable [#6493], prevents double revocation by tracking state [#6493], returns only unvested portions to revoker [#6493], implements revoke() functions [#6493], overrides vestedAmount() using snapshots [#6493], and provides real-world design [#6493].
Out of Scope Changes check ✅ Passed All changes are in scope: VestingWalletRevocable implementation, README update documenting the contract, comprehensive test suite, and design supporting per-asset revocation with immutable revoker—all directly aligned with issue #6493 objectives.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

Copy link
Copy Markdown
Contributor

@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 (1)
test/finance/VestingWalletRevocable.test.js (1)

55-80: ⚡ Quick win

Consider verifying emitted events during revocation.

The test validates balance changes but doesn't assert that the expected events are emitted when revoke() is called. Verifying events ensures the contract's complete public API behavior is tested.

📋 Proposed enhancement to add event verification
     const refund = totalAllocation - (await this.mock.vestedAmount(halfway));

-    await expect(() => this.mock.connect(this.revoker).revoke()).to.changeEtherBalances(
+    await expect(this.mock.connect(this.revoker).revoke())
+      .to.emit(this.mock, 'EtherRevoked')
+      .withArgs(/* expected event arguments */);
+
+    await expect(() => this.mock.connect(this.revoker).revoke()).to.changeEtherBalances(
       [this.mock, this.revoker],
       [-refund, refund],
     );

Apply similar event verification to the ERC20 revocation tests as well.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/finance/VestingWalletRevocable.test.js` around lines 55 - 80, Add an
assertion that calling this.mock.connect(this.revoker).revoke() emits the
contract's revocation event: wrap the revoke call in an
expect(...).to.emit(this.mock, '<revocationEventName>').withArgs(this.revoker,
refund) (or the actual event argument types used by the contract such as
beneficiary and refund amount), and do the same pattern for the ERC20 revoke
test; locate the revoke invocation in the test
(this.mock.connect(this.revoker).revoke()) and add the expect-to.emit assertion
immediately around it to verify the event and its args.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@test/finance/VestingWalletRevocable.test.js`:
- Around line 55-80: Add an assertion that calling
this.mock.connect(this.revoker).revoke() emits the contract's revocation event:
wrap the revoke call in an expect(...).to.emit(this.mock,
'<revocationEventName>').withArgs(this.revoker, refund) (or the actual event
argument types used by the contract such as beneficiary and refund amount), and
do the same pattern for the ERC20 revoke test; locate the revoke invocation in
the test (this.mock.connect(this.revoker).revoke()) and add the expect-to.emit
assertion immediately around it to verify the event and its args.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: ee47b840-9301-4bd4-9ec9-5ba1d28a9944

📥 Commits

Reviewing files that changed from the base of the PR and between cd05883 and c7fdae0.

📒 Files selected for processing (3)
  • contracts/finance/README.adoc
  • contracts/finance/VestingWalletRevocable.sol
  • test/finance/VestingWalletRevocable.test.js

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.

Extending VestingWallet to have revocability

1 participant