Add VoidablePledge module with unit tests#89
Conversation
This reverts commit b2c09ec.
Cover the three key behaviors when Config.forwardTipsImmediately is true: - claimTip() reverts with KeepWhatsRaisedTipsAlreadyForwarded - Permit2 pledge forwards tip to platform admin at pledge time - Admin setFeeAndPledge splits pledge vs tip accounting correctly Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 631015ff86
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| uint256 protocolFeeReversed = _min(v.protocolFee, s_protocolFeePerToken[v.pledgeToken]); | ||
| uint256 platformFeeReversed = _min(v.platformFee, s_platformFeePerToken[v.pledgeToken]); | ||
| s_protocolFeePerToken[v.pledgeToken] -= protocolFeeReversed; | ||
| s_platformFeePerToken[v.pledgeToken] -= platformFeeReversed; |
There was a problem hiding this comment.
Prevent voids from consuming newer pledges’ fee buckets
voidPledge reverses fees by taking min(storedFee, s_*FeePerToken[token]) from the aggregate per-token buckets, so a void for an older pledge can consume fees accrued by newer pledges after an earlier disburseFees(). Concretely: pledge A accrues fees, fees are disbursed (A’s fees leave), pledge B accrues new fees, then voiding A will subtract B’s fee bucket and transfer it to the platform admin, causing later disbursement to underpay protocol/platform for B. This breaks per-pledge fee accounting whenever fee disbursement and voiding are interleaved over time.
Useful? React with 👍 / 👎.
Problem
No mechanism existed to reverse a single pledge on-chain when an individual contribution was flagged as fraud or a dispute was lost. The COPM from a fraudulent pledge remained locked in the treasury, and the adjustment was handled entirely off-chain (excluded from fiat off-ramp calculation), creating a permanent on-chain/off-chain discrepancy. getRaisedAmount() on-chain would always be higher than real totals.
This was especially urgent for Stripe disputes, which can take up to 75 days to resolve — often after the creator has already withdrawn and the refund window has closed.
Solution
A composable VoidablePledge abstract module (src/utils/VoidablePledge.sol) that any treasury can opt into by inheritance. KeepWhatsRaised is the first treasury to integrate it.
Design decision — status flag, not forced burn: The NFT receipt is marked as voided via an on-chain flag rather than being burned. Forced burn via ERC721Burnable requires the token owner or an approved operator — in a fraud scenario the backer is adversarial or absent and will never grant approval. The flag approach avoids this dependency entirely while keeping the historical pledge visible on-chain but explicitly marked unusable.
Changes
src/utils/VoidablePledge.sol (new)
A self-contained abstract module providing:
src/treasuries/KeepWhatsRaised.sol (modified)
test/foundry/unit/VoidablePledge.t.sol (new)
36 unit tests covering all edge cases: