Skip to content

fix: close sponsored claim+burn bond extraction vulnerability#107

Merged
aliXsed merged 3 commits intomainfrom
aliX/tighten-paymaster
Apr 10, 2026
Merged

fix: close sponsored claim+burn bond extraction vulnerability#107
aliXsed merged 3 commits intomainfrom
aliX/tighten-paymaster

Conversation

@aliXsed
Copy link
Copy Markdown
Collaborator

@aliXsed aliXsed commented Apr 10, 2026

Problem

A whitelisted user could exploit claimUuidSponsored + burn to extract bond tokens from the treasury to their personal wallet:

  1. Call claimUuidSponsored(uuid, operator, treasury) — treasury pays the bond
  2. Call burn(tokenId) — bond refunds to uuidOwner (the user), not the treasury
  3. Repeat with fresh UUIDs (unlimited supply of bytes16 values)

The only throttle was the periodic QuotaControl, meaning the entire treasury was drainable over time.

Solution

Two complementary fixes (defense-in-depth):

1. FleetIdentityUpgradeable: refund bond to original payer (root cause fix)

  • New mapping: uuidOwnershipBondPayer tracks who paid the ownership bond
  • claimUuid: payer = msg.sender (self-funded)
  • claimUuidSponsored: payer = treasury
  • _register (first registration): payer = msg.sender
  • burn (owned-only): refunds to uuidOwnershipBondPayer with fallback to uuidOwner for pre-upgrade tokens
  • On ERC-721 transfer: bond payer follows only if the previous owner was the payer (self-funded); sponsored bonds stay with the treasury

Storage: +1 mapping before __gap (reduced from 50 to 49 slots). No reinitializer needed — address(0) falls back to uuidOwner.

2. BondTreasuryPaymaster: per-user NODL allowance (defense-in-depth)

  • New mapping: userBondAllowance — token-denominated lifetime cap per user
  • consumeSponsoredBond checks and decrements allowance before processing
  • setUserBondAllowance / increaseUserBondAllowance — managed by WHITELIST_ADMIN_ROLE
  • Amount-based (not claim-count) so the cap stays correct even if baseBond changes
  • Non-periodic: admin tops up explicitly

Test Coverage

Contract Lines Branches Functions
FleetIdentityUpgradeable 96.61% 95.56% 95.24%
BondTreasuryPaymaster 97.62% 97.92% 100%

787 tests pass across 26 suites (0 failures).

aliXsed added 3 commits April 10, 2026 12:56
Track who paid the ownership bond via uuidOwnershipBondPayer mapping.
For self-funded claims the payer is msg.sender; for sponsored claims
it is the treasury. On burn the bond refunds to the recorded payer,
closing a vulnerability where a whitelisted user could claim+burn
sponsored UUIDs to extract treasury funds to their own wallet.

Backward compatible: pre-upgrade tokens (bondPayer==address(0)) fall
back to uuidOwner. On token transfer, bondPayer follows only if the
previous owner was the payer (self-funded); sponsored bonds stay with
the treasury.

Storage: adds 1 mapping before __gap (reduced from 50 to 49 slots).
Defense-in-depth for the sponsored claim exploit: each whitelisted
user now has a token-denominated allowance that is decremented on
every consumeSponsoredBond call.  When the allowance hits zero the
user cannot consume any more sponsored bonds regardless of the global
periodic quota.

Amount-based (not claim-count-based) so the cap stays correct even if
baseBond parameters change.  Non-periodic: whitelist admin can top up
via increaseUserBondAllowance or overwrite via setUserBondAllowance.

New storage:
  mapping(address => uint256) public userBondAllowance

New functions (WHITELIST_ADMIN_ROLE):
  setUserBondAllowance(address user, uint256 allowance)
  increaseUserBondAllowance(address user, uint256 amount)

New error: UserBondAllowanceExceeded
New events: UserBondAllowanceSet, UserBondAllowanceIncreased
@github-actions
Copy link
Copy Markdown

LCOV of commit 1b235f2 during checks #643

Summary coverage rate:
  lines......: 32.2% (777 of 2412 lines)
  functions..: 28.7% (108 of 376 functions)
  branches...: 37.6% (140 of 372 branches)

Files changed coverage rate:
                                                  |Lines       |Functions  |Branches    
  Filename                                        |Rate     Num|Rate    Num|Rate     Num
  ======================================================================================
  src/paymasters/BondTreasuryPaymaster.sol        | 0.0%     42| 0.0%     5| 0.0%      8
  src/swarms/FleetIdentityUpgradeable.sol         |96.7%    454|95.2%    63|80.3%     76
  test/FleetIdentity.t.sol                        |81.2%     16|87.5%     8| 100%      1
  test/paymasters/BondTreasuryPaymaster.t.sol     | 0.0%     12| 0.0%     5|    -      0

@aliXsed aliXsed merged commit 49ea72e into main Apr 10, 2026
3 checks passed
@aliXsed aliXsed deleted the aliX/tighten-paymaster branch April 10, 2026 01:52
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