Skip to content

Conversation

@AdnanHKx
Copy link

Overview

Introduces GoalBasedPaymentTreasury - a payment treasury with goal-based success conditions and time constraints.

Key Features

Time Constraints:

  • Create payments: Only allowed between launchTime and deadline
  • Confirm/Cancel payments: Allowed between launchTime and deadline + buffer
  • Withdraw/DisburseeFees: Allowed after deadline, requires goal to be met

Refund Behavior (claimRefund):

  • Before deadline: Always allowed
  • During buffer period (between deadline and deadline + buffer): Blocked if confirmed + pending >= goal
  • After buffer period: Blocked if confirmed >= goal

During the buffer period, the success condition uses the optimistic total (confirmed + pending). This prevents preemptive refunds while pending payments are still being processed. Once the buffer ends, payment processing halts - if pending payments failed to settle and the confirmed total falls short of the goal, users can claim refunds.

Withdraw/DisburseeFees:

  • Can be called as soon as the deadline is reached
  • Success condition is validated against confirmed amount only (confirmed >= goal)

Changelog

  • Added GoalBasedPaymentTreasury.sol
  • BasePaymentTreasury: Changed getExpectedAmount visibility from external to public as it is called inside GoalBasedPaymentTreasury.
  • Included relevant deployment scripts and tests for the new contract.

Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

ℹ️ 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".

Copy link
Collaborator

@mahabubAlahi mahabubAlahi left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good to me

rayedsikder pushed a commit that referenced this pull request Jan 2, 2026
* Fix incorrect description of the `claimFund` function (#6)

  (immunefi)(issue#06)
  - Updated the NatSpec comment for claimFund()

* Refactor `platformDataKey` parameter validation optimization (#7)

  (immunefi)(issue#09)
  - Move `platformDataKey` parameter validation to an earlier stage of the `createCampaign` flow

* Add configuring platform data during `updateSelectedPlatform` (#8)

  (immunefi)(issue#10)

* Add creator's non-zero validation during `createCampaign` (#9)

  (immunefi)(issue#16)

* Add zero validation for `platformDataValue` in `createCampaign` (#10)

  (immunefi)(issue#12)

* Add reward value zero validation in pledge (#11)

  (immunefi)(issue#14)

* Fix `updateDeadline` allowing past deadline that blocks `claimRefund` (#12)

  (immunefi)(issue#05)
  - Added check to ensure new deadline is after current block timestamp

* Fix blocking KeepWhatsRaised pledge functions via front-running (#13)

  (immunefi)(issue#04)
  - Add internal pledge ID generation using msg.sender and pledgeId

* Add fee configuration via configure treasury (#14)

  (immunefi)(issue#11)
  - Update configure treasury to support fee values
  - Add getter function for fee value

* Add campaign data validation in configure treasury (#15)

  (immunefi)(issue#20)
  - Update fee values js doc
  - Update custom error

* Fix Gateway fee bypass (#16)

  (immunefi)(issue#19)
  - When `setFeeAndPledge` is called, tokens are transferred from the admin's wallet (msg.sender)
  - When `pledgeWithoutAReward` or `pledgeForAReward` is called directly, tokens are transferred from the backer's wallet

* Add expected fee description in create campaign jsdoc (#17)

  (immunefi)(issue#03)

* Refactor withdrawal and pledge calculation (#19)

  (immunefi)(issue#15)
  (immunefi)(issue#18)

  - Restrict access to the withdrawal function so that only the campaign owner and platform administrator can use it.
  - Move the protocol fee calculation from the withdrawal process to the pledge stage.
  - For withdrawals:
    - Partial Withdrawals:
       - Conditions: amount > 0 and amount + fees ≤ available balance.
       - The creator must specify the exact withdrawal amount, and the system will ensure there are sufficient funds to cover both the withdrawal and the fees.

    - Final Withdrawals:
      - Conditions: available balance > 0 and fees ≤ available balance.
      - The creator can withdraw the entire remaining balance. The system will check if there are enough funds to cover the fees and will then provide the remainder to the creator.

---------

Co-authored-by: mahabubAlahi <mahabub@ccprotocol.xyz>
@lucian-calian
Copy link
Collaborator

lucian-calian commented Jan 28, 2026

We plan to use the GoalBasedPaymentTreasury in the standalone application. For fiat pledges, our intention is to charge cards only after the campaign has ended and has been deemed successful.

However, the current success condition is defined as getRaisedAmount >= INFO.goalAmount(). The getRaisedAmount value includes only confirmed fiat pledges. Since fiat pledges will only be confirmed after a campaign is successful (when cards are charged), they would not be included in getRaisedAmount during the success validation phase.

As a result, fiat pledges would not be taken into account when determining whether the campaign has met its success condition.

What would be the best way to handle the scenario of charging the cards and confirming the payments after the campaign has ended succesfully?

@adnanhq
Copy link
Contributor

adnanhq commented Jan 29, 2026

We plan to use the GoalBasedPaymentTreasury in the standalone application. For fiat pledges, our intention is to charge cards only after the campaign has ended and has been deemed successful.

However, the current success condition is defined as getRaisedAmount >= INFO.goalAmount(). The getRaisedAmount value includes only confirmed fiat pledges. Since fiat pledges will only be confirmed after a campaign is successful (when cards are charged), they would not be included in getRaisedAmount during the success validation phase.

As a result, fiat pledges would not be taken into account when determining whether the campaign has met its success condition.

What would be the best way to handle the scenario of charging the cards and confirming the payments after the campaign has ended succesfully?

I believe there is a slight misunderstanding regarding how the contract defines "success" for different operations.

The strict check you noticed (getRaisedAmount() >= goalAmount) is specifically used for withdraw() and disburseFees() only. This is a safety mechanism: the contract cannot allow the withdrawal of funds that haven't actually arrived yet.

However, for determining campaign success (blocking refunds), the contract uses an optimistic approach during the buffer period.

Here is the logic from _checkRefundAllowed():

function _checkRefundAllowed() internal view {
    if (block.timestamp >= INFO.getDeadline()) {
        uint256 progress = block.timestamp > INFO.getDeadline() + INFO.getBufferTime()
            ? getRaisedAmount()                          // After buffer: confirmed only
            : getRaisedAmount() + getExpectedAmount();   // During buffer: confirmed + pending
        if (progress >= INFO.getGoalAmount()) {
            revert GoalBasedPaymentTreasuryNotRefundable();
        }
    }
}

This ensures your "charge later" workflow is fully supported. Here is how the lifecycle works technically:

  1. During Campaign:
    Users pledge via createPayment(). Funds are tracked as pending (included in getExpectedAmount).
  2. At Deadline (Optimistic Success):
    The contract checks if confirmed + pending >= goal. If met, refunds are blocked, locking the campaign in a successful state to give you time to process payments.
  3. Up to End of Buffer Period:
    You charge cards and call confirmPayment(). This moves funds from pending to confirmed. (Note: You can confirm payments at any point from launch until the buffer ends).
  4. Withdrawal (Realized Success):
    You're allowed to withdraw as soon as the deadline has ended, provided that you have confirmed enough payments such that confirmed >= goal

Why the distinction?
If we allowed withdrawals based on the optimistic total as soon as the deadline ended, and for some reason some card charges subsequently failed (leaving the confirmed total below the goal), in that particular scenario the contract would be drained of funds needed to refund the users who did pay. By guarding withdraw() with the strict check, we ensure funds are only moved when they are securely in the contract, while the optimistic check protects the campaign from preemptive refunds while you process those charges.

@lucian-calian
Copy link
Collaborator

We plan to use the GoalBasedPaymentTreasury in the standalone application. For fiat pledges, our intention is to charge cards only after the campaign has ended and has been deemed successful.
However, the current success condition is defined as getRaisedAmount >= INFO.goalAmount(). The getRaisedAmount value includes only confirmed fiat pledges. Since fiat pledges will only be confirmed after a campaign is successful (when cards are charged), they would not be included in getRaisedAmount during the success validation phase.
As a result, fiat pledges would not be taken into account when determining whether the campaign has met its success condition.
What would be the best way to handle the scenario of charging the cards and confirming the payments after the campaign has ended succesfully?

I believe there is a slight misunderstanding regarding how the contract defines "success" for different operations.

The strict check you noticed (getRaisedAmount() >= goalAmount) is specifically used for withdraw() and disburseFees() only. This is a safety mechanism: the contract cannot allow the withdrawal of funds that haven't actually arrived yet.

However, for determining campaign success (blocking refunds), the contract uses an optimistic approach during the buffer period.

Here is the logic from _checkRefundAllowed():

function _checkRefundAllowed() internal view {
    if (block.timestamp >= INFO.getDeadline()) {
        uint256 progress = block.timestamp > INFO.getDeadline() + INFO.getBufferTime()
            ? getRaisedAmount()                          // After buffer: confirmed only
            : getRaisedAmount() + getExpectedAmount();   // During buffer: confirmed + pending
        if (progress >= INFO.getGoalAmount()) {
            revert GoalBasedPaymentTreasuryNotRefundable();
        }
    }
}

This ensures your "charge later" workflow is fully supported. Here is how the lifecycle works technically:

1. **During Campaign:**
   Users pledge via `createPayment()`. Funds are tracked as `pending` (included in `getExpectedAmount`).

2. **At Deadline (Optimistic Success):**
   The contract checks if `confirmed + pending >= goal`. If met, refunds are **blocked**, locking the campaign in a successful state to give you time to process payments.

3. **Up to End of Buffer Period:**
   You charge cards and call `confirmPayment()`. This moves funds from `pending` to `confirmed`. (Note: You can confirm payments at any point from launch until the buffer ends).

4. **Withdrawal (Realized Success):**
   You're allowed to withdraw as soon as the deadline has ended, provided that you have confirmed enough payments such that `confirmed >= goal`

Why the distinction? If we allowed withdrawals based on the optimistic total as soon as the deadline ended, and for some reason some card charges subsequently failed (leaving the confirmed total below the goal), in that particular scenario the contract would be drained of funds needed to refund the users who did pay. By guarding withdraw() with the strict check, we ensure funds are only moved when they are securely in the contract, while the optimistic check protects the campaign from preemptive refunds while you process those charges.

Thanks for your response. This clarifies a lot.

@AlinSF2024
Copy link
Collaborator

@AdnanHKx , @adnanhq how should the above implementation suppose to work from a UX point of view?

For instance, when we check to see if confirmed payments + unconfirmed payments > goal amount we tell the creator that his campaign is successful, but then if enough unconfirmed payments fail to make the confirmed payments < goal amount we tell the same creator his campaign is actually failed?

cc: @lucian-calian

@adnanhq
Copy link
Contributor

adnanhq commented Feb 2, 2026

@AdnanHKx , @adnanhq how should the above implementation suppose to work from a UX point of view?

For instance, when we check to see if confirmed payments + unconfirmed payments > goal amount we tell the creator that his campaign is successful, but then if enough unconfirmed payments fail to make the confirmed payments < goal amount we tell the same creator his campaign is actually failed?

cc: @lucian-calian

This is ultimately a product/business decision that needs to be defined by the platform. The expected UX depends on how much abstraction the platform wants to keep versus how explicitly it wants to surface intermediate states (e.g. unconfirmed vs confirmed payments) to creators.

From a development perspective, multiple approaches can be implemented, but the source of truth and messaging rules must be clearly specified by the platform to avoid contradictory states (such as marking a campaign successful and later failed).

For this PR, the discussion should remain focused on development and technical implementation only. Broader UX or product-level discussions should be moved to other channels (e.g. Discord or Confluence) and brought back here as concrete, agreed-upon code requirements if needed.

public
override
whenNotPaused
whenNotCancelled
Copy link
Collaborator

@lucian-calian lucian-calian Feb 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't the whenNotCancelled be removed? If a campaign is cancelled, the backers would not be able to reclaim funds. Same for TimeConstrainedPaymentTreasury.
See discussion: https://discord.com/channels/1150443607313092750/1434891866746392756/1464204507717046283

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're correct. Let me remove the constraint and redeploy the treasury implementation contract. Thanks for bringing it up.

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.

6 participants