Skip to content

[Bug] No exact-amount transfer verification permanently deadlocks a disputed assertion #164

Closed

Description

@collinsezedike

Summary

assert_outcome, dispute, and resolve (contracts/tholos/src/lib.rs:581-833) each call token::Client::transfer(...) and trust that the requested amount actually moved, without verifying the contract's real balance delta. Assertion.bond records the nominal bond_amount requested at deposit time, not whatever the contract actually received.

With a non-standard, fee-on-transfer token, the two incoming deposits (assert_outcome's and dispute's) each land short of the full bond_amount by whatever cut the token takes. resolve's payout, once a majority is reached, writes Status::Resolved to storage and then calls transfer(contract, winner, assertion.bond * 2), the full nominal amount, not the actual (short) balance. That transfer panics on insufficient balance, reverting the whole transaction, including the state write, so the assertion remains Disputed forever.

Impact: every future attempt to reach majority and pay out hits the identical panic, since the deficit is permanent and assertion.bond never reflects the real shortfall. Both bonds are permanently locked with no way to resolve or reclaim them, for any dispute funded with such a token.

Scope

  • Verify the contract's actual balance delta around each incoming transfer (assert_outcome, dispute) and either reject tokens that don't deliver the full requested amount, or record the actually-received amount instead of the nominal request.
  • Ensure resolve's payout can never request more than the contract's real, verified balance for that assertion's token.
  • Add a test using a mock fee-on-transfer token confirming a dispute funded with it either gets rejected up front or resolves correctly instead of deadlocking.

Proposed approach

The most direct fix is to check token_client.balance(&env.current_contract_address()) before and after each incoming transfer in assert_outcome/dispute, and store the actual received delta (not the requested bond_amount) as assertion.bond. This keeps the rest of the payout math unchanged, since it already operates purely on assertion.bond, while ensuring that value always reflects what the contract genuinely holds for that assertion.

Activity

  1. added
    bugSomething isn't working
    Maybe RewardedIssue may be eligible for a GrantFox reward
    GrantFox OSSIssue tracked in GrantFox OSS
    hardSecurity-sensitive or high-complexity change
    on Sep 2, 2026
  2. odiliflash-ops commented on Sep 3, 2026

    @odiliflash-ops

    I would love to work on this issue

  3. xtep103 commented on Sep 3, 2026

    @xtep103
    Contributor

    Hi there, I’ve carefully reviewed the issue details and I’m confident I can provide a clean, efficient, and maintainable solution.

  4. grantfox-oss commented on Sep 4, 2026

    @grantfox-oss

    🦊 GrantFox — @xtep103 has been assigned to this issue!

    Next steps:

    1. Open a Pull Request referencing this issue (e.g., Closes #164)
    2. Your PR will be reviewed by the drydocs maintainers

    Good luck! Track your progress on GrantFox.

  5. grantfox-oss commented on Sep 15, 2026

    @grantfox-oss

    🎉 This issue has been marked as completed on GrantFox!

    @xtep103's PR #216 was approved and merged by @collinsezedike.

    🏆 @xtep103: You earned 35 FoxPoints for this contribution! Your current tier: Explorer (140 total points). Track your full progress on GrantFox.

    👏 Great work, @xtep103! Keep contributing to drydocs.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

bugSomething isn't workinghardSecurity-sensitive or high-complexity change

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions