[Contract] Implement Auto-Refund Fallback Mechanism on Expired Campaigns - #562
[Contract] Implement Auto-Refund Fallback Mechanism on Expired Campaigns#562Skinny001 wants to merge 4 commits into
Conversation
…und triggers New Soroban smart contract for campaign funding with: - Campaign creation with goal amount and deadline - Permissionless contributions to active campaigns - Creator claim when goal met after deadline - Auto-refund for backers when deadline passes without meeting goal - Permissionless refund triggering (anyone can refund any backer) - 27 comprehensive unit tests covering success, failure, and edge cases - Storage TTL management following existing contract patterns
|
@Skinny001 Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits. You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀 |
📝 WalkthroughWalkthroughAdds the ChangesCampaign funding contract
Estimated code review effort: 4 (Complex) | ~60 minutes Sequence Diagram(s)sequenceDiagram
participant Contributor
participant CampaignFundingContract
participant TokenContract
Contributor->>CampaignFundingContract: refund(campaign_id, contributor)
CampaignFundingContract->>CampaignFundingContract: set status Expired
CampaignFundingContract->>TokenContract: transfer refund
CampaignFundingContract->>CampaignFundingContract: persist refunded contribution
Possibly related PRs
🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (3 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 5
🧹 Nitpick comments (2)
contracts/campaign-funding/src/test.rs (2)
606-625: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winLoosen-to-strengthen:
test_events_emittedonly checks event count, not content.
assert!(events.len() >= 2)doesn't verify which events were emitted (e.g., campaign-created vs contribution-made) or their payloads, so it wouldn't catch a regression that emits the wrong event type while preserving count.♻️ Suggested tightening
let events = env.events().all(); - assert!(events.len() >= 2); + assert!(events.len() >= 2, "expected at least a campaign-created and a contribution event"); + // Optionally assert on event topics/data to catch wrong-event-type regressions, + // e.g. matching on the contract address and topic symbol for each expected event.🤖 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 `@contracts/campaign-funding/src/test.rs` around lines 606 - 625, Strengthen test_events_emitted by asserting the emitted events’ topics and payloads, not only that at least two events exist. Verify the campaign-created and contribution-made events are present in the expected order and contain the relevant campaign ID, addresses, and contribution amount.
169-188: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick winClarify deadline error direction or split error codes.
Error(Contract,#8)is used for both past-deadline failures (test_contribute_after_deadline) and before-deadline failures (test_claim_before_deadline,test_refund_before_deadline). If this is a single sharedDeadlineErrorvariant, production failures won’t distinguish “deadline already passed” from “deadline not reached”, so use distinct codes/variants or document the intentionally shared semantics.🤖 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 `@contracts/campaign-funding/src/test.rs` around lines 169 - 188, The deadline validation errors are ambiguous because error code `#8` is used for both expired and not-yet-reached campaigns. Update the relevant contract error variants and checks used by test_contribute_after_deadline, test_claim_before_deadline, and test_refund_before_deadline to return distinct codes for each direction, or explicitly document the shared DeadlineError semantics if that behavior is intentional.
🤖 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.
Inline comments:
In `@contracts/campaign-funding/src/lib.rs`:
- Around line 92-323: Add concise Rust documentation comments (`///`)
immediately before every public entrypoint in `CampaignFundingContract`:
`initialize`, `create_campaign`, `contribute`, `claim`, `refund`,
`get_campaign`, `get_contribution`, and `get_admin`. Describe each method’s
purpose and key parameters/return value where applicable, without changing
behavior.
- Around line 24-30: Assign CampaignStatus::Expired when a campaign deadline has
passed without reaching its goal. Update the relevant refund/deadline handling
flow around refund() so it persists the status transition before allowing
withdrawals, while preserving the existing eligibility checks and Active/Success
behavior.
- Around line 86-88: The fixed LEDGER_THRESHOLD and LEDGER_BUMP values do not
guarantee that entries survive until the deadline set by create_campaign. Update
create_campaign and its Campaign/Contribution TTL initialization to derive the
extension from deadline minus the current ledger time, including the required
margin, or enforce and validate a maximum deadline within the configured TTL
window before creating entries.
- Around line 160-178: Add a distinct error variant representing an expired
campaign deadline, then update contribute so its current_time >=
campaign.deadline guard panics with that variant instead of
Error::DeadlineNotReached. Preserve DeadlineNotReached for the pre-deadline
checks in claim and refund.
- Around line 95-104: Update initialize and the state-using methods
create_campaign, contribute, claim, and refund to require persisted
initialization before proceeding: detect missing admin state and return
Error::NotInitialized instead of relying on fallback values. Enforce admin
authorization on the operations intended to be admin-protected by loading the
stored admin and rejecting non-admin callers with Error::Unauthorized, while
preserving existing authentication and state-update behavior after validation.
---
Nitpick comments:
In `@contracts/campaign-funding/src/test.rs`:
- Around line 606-625: Strengthen test_events_emitted by asserting the emitted
events’ topics and payloads, not only that at least two events exist. Verify the
campaign-created and contribution-made events are present in the expected order
and contain the relevant campaign ID, addresses, and contribution amount.
- Around line 169-188: The deadline validation errors are ambiguous because
error code `#8` is used for both expired and not-yet-reached campaigns. Update the
relevant contract error variants and checks used by
test_contribute_after_deadline, test_claim_before_deadline, and
test_refund_before_deadline to return distinct codes for each direction, or
explicitly document the shared DeadlineError semantics if that behavior is
intentional.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 5d902fad-64e1-4014-b5d5-281dee026894
📒 Files selected for processing (31)
contracts/Cargo.tomlcontracts/campaign-funding/Cargo.tomlcontracts/campaign-funding/src/lib.rscontracts/campaign-funding/src/test.rscontracts/campaign-funding/test_snapshots/test/test/test_claim_already_withdrawn.1.jsoncontracts/campaign-funding/test_snapshots/test/test/test_claim_before_deadline.1.jsoncontracts/campaign-funding/test_snapshots/test/test/test_claim_goal_not_met.1.jsoncontracts/campaign-funding/test_snapshots/test/test/test_claim_success.1.jsoncontracts/campaign-funding/test_snapshots/test/test/test_contribute.1.jsoncontracts/campaign-funding/test_snapshots/test/test/test_contribute_after_deadline.1.jsoncontracts/campaign-funding/test_snapshots/test/test/test_contribute_campaign_not_found.1.jsoncontracts/campaign-funding/test_snapshots/test/test/test_contribute_multiple_backers.1.jsoncontracts/campaign-funding/test_snapshots/test/test/test_contribute_multiple_from_same_backer.1.jsoncontracts/campaign-funding/test_snapshots/test/test/test_contribute_zero_amount.1.jsoncontracts/campaign-funding/test_snapshots/test/test/test_create_campaign.1.jsoncontracts/campaign-funding/test_snapshots/test/test/test_create_campaign_deadline_in_past.1.jsoncontracts/campaign-funding/test_snapshots/test/test/test_create_campaign_zero_goal.1.jsoncontracts/campaign-funding/test_snapshots/test/test/test_events_emitted.1.jsoncontracts/campaign-funding/test_snapshots/test/test/test_full_lifecycle_refund.1.jsoncontracts/campaign-funding/test_snapshots/test/test/test_full_lifecycle_success.1.jsoncontracts/campaign-funding/test_snapshots/test/test/test_get_campaign_not_found.1.jsoncontracts/campaign-funding/test_snapshots/test/test/test_get_contribution_none.1.jsoncontracts/campaign-funding/test_snapshots/test/test/test_initialize.1.jsoncontracts/campaign-funding/test_snapshots/test/test/test_re_initialize_fails.1.jsoncontracts/campaign-funding/test_snapshots/test/test/test_refund_already_refunded.1.jsoncontracts/campaign-funding/test_snapshots/test/test/test_refund_before_deadline.1.jsoncontracts/campaign-funding/test_snapshots/test/test/test_refund_goal_met.1.jsoncontracts/campaign-funding/test_snapshots/test/test/test_refund_multiple_backers.1.jsoncontracts/campaign-funding/test_snapshots/test/test/test_refund_no_contribution.1.jsoncontracts/campaign-funding/test_snapshots/test/test/test_refund_permissionless.1.jsoncontracts/campaign-funding/test_snapshots/test/test/test_refund_success.1.json
| pub fn initialize(env: Env, admin: Address) { | ||
| if env.storage().instance().has(&Symbol::new(&env, "admin")) { | ||
| panic_with_error!(&env, Error::AlreadyInitialized); | ||
| } | ||
| admin.require_auth(); | ||
|
|
||
| env.storage().instance().set(&Symbol::new(&env, "admin"), &admin); | ||
| env.storage().instance().set(&Symbol::new(&env, "campaign_counter"), &0u64); | ||
| env.storage().instance().extend_ttl(LEDGER_THRESHOLD, LEDGER_BUMP); | ||
| } |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "Files matching lib.rs under campaigns-campaign-funding:"
fd -a 'lib.rs' . | sed 's#^\./##' | rg 'campaign-funding|campaign' || true
echo
echo "git ls-files relevant:"
git ls-files | rg 'campaign-funding|campaign' || true
echo
echo "Outline:"
file="$(fd 'lib.rs' contracts/campaign-funding/src | head -n1 || true)"
if [ -n "${file:-}" ]; then
echo "file=$file"
wc -l "$file"
ast-grep outline "$file" || true
echo
echo "Lines 1-220:"
sed -n '1,220p' "$file" | nl -ba
fi
echo
echo "Search errors/usages:"
rg -n "NotInitialized|Unauthorized|initialize|campaign_counter|admin|require_auth|unwrap_or" contracts/campaign-funding/src || trueRepository: Fundable-Protocol/stellar_client_os
Length of output: 2074
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "pwd:"
pwd
echo
echo "git status:"
git status --short || true
echo
echo "top-level files:"
git ls-files | head -80 || true
echo
echo "Find campaign-funding:"
fd -a 'lib.rs' . 2>/dev/null | sed 's#^\./##' | rg '(^|/)campaign-funding($|/)|campaign' || true
echo
echo "Search local tracked files for symbols:"
rg -n "Error::NotInitialized|Error::Unauthorized|fn initialize|campaign_counter|unwrap_or|admin.require_auth|admin" . 2>/dev/null | head -200 || trueRepository: Fundable-Protocol/stellar_client_os
Length of output: 2074
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf 'PWD: %s\n' "$(pwd)"
printf '\nTracked candidates:\n'
git ls-files | rg '(^|/)contracts/campaign-funding/src/lib\.rs$|campaign-funding|campaign' || true
printf '\nLocal file direct check:\n'
p='contracts/campaign-funding/src/lib.rs'
if [ -f "$p" ]; then
echo "EXISTS $p"
wc -l "$p"
sed -n '1,190p' "$p" | nl -ba
else
echo "MISSING $p"
fi
printf '\nLocal symbol search:\n'
if [ -d contracts/campaign-funding/src ]; then
rg -n "Error::NotInitialized|Error::Unauthorized|fn initialize|campaign_counter|unwrap_or|admin.require_auth|admin|create_campaign|contribute|claim|refund" contracts/campaign-funding/src || true
else
echo "contracts/campaign-funding/src directory not present"
fiRepository: Fundable-Protocol/stellar_client_os
Length of output: 2074
🌐 Web query:
stellar_client_os contracts/campaign-funding/src/lib.rs initialize admin campaign_counter initialize
💡 Result:
The command you are referencing refers to initializing a Soroban smart contract, commonly found in crowdfunding DApp implementations on the Stellar network [1][2][3]. In the context of Soroban contracts (typically written in Rust), the initialize function is used to set the initial state of the contract, such as the admin address and various configuration parameters, upon deployment [2][3]. While there is no single universal codebase matching your exact string, the structure you described is common in Soroban crowdfunding projects [1][2][4]. Below is a breakdown of how these components are typically handled: 1. Initialization: The initialize function (often named init or initialize) is defined in the contract's main library file (e.g., lib.rs) and is invoked to persist critical data to the ledger [2][5][3]. 2. Storage Keys: Data like the admin account and campaign_counter are typically stored using an enum-based data key system to ensure type-safe access to contract storage [2]. 3. Campaign Counter: A campaign_counter is often used in factory-style contracts to generate deterministic IDs for new campaigns as they are deployed [5]. Example of typical initialization logic in a Soroban contract: #[contractimpl] impl CrowdfundingContract { pub fn initialize(env: Env, admin: Address, campaign_counter: u32) { // Logic to store the admin and counter in the contract's persistent storage env.storage.instance.set(&DataKey::Admin, &admin); env.storage.instance.set(&DataKey::CampaignCounter, &campaign_counter); } } To invoke this function via the Stellar CLI after deploying your contract, you would use a command similar to the following: stellar contract invoke \ --id <contract_id> \ --source <admin_key> \ --network \ -- initialize \ --admin <admin_address> \ --campaign_counter Please ensure you check the specific lib.rs file in your project or the relevant repository for the exact parameter names, as they can vary between different implementations [2][6].
Citations:
- 1: https://github.com/Crowdfunding-DApp/stellar-raise-contracts
- 2: https://github.com/0xDarioSanchez/NovaFund
- 3: https://github.com/ritik4ever/stellar-goal-vault
- 4: https://github.com/fundlr/fundlr-contracts/blob/main/README.md
- 5: https://dev.to/berkebey01/building-milestone-escrow-crowdfunding-on-soroban-5hc
- 6: https://libraries.io/npm/@rtu-bindings%2Fsoroban-groth16-verifier
🌐 Web query:
"Fundable-Protocol/stellar_client_os" "campaign-funding" "lib.rs" "campaign_counter" "unwrap_or"
💡 Result:
The repository Fundable-Protocol/stellar_client_os exists within the Fundable-Protocol organization on GitHub [1][2], but there is no widely documented or standardized campaign_funding module, lib.rs file, or campaign_counter function associated with that specific repository in public documentation. The Fundable-Protocol primarily develops decentralized finance solutions on StarkNet (using Cairo) [3] and maintains various client repositories [1]. It is common in Rust development to use the unwrap_or pattern when handling Option<T> types, such as some_variable.unwrap_or_default() or some_variable.unwrap_or(value), to provide fallback values [4][5]. If you are encountering a compiler error or logic issue related to campaign_counter and unwrap_or in your local environment, ensure that: 1. The variable campaign_counter is of type Option<T> [4][6]. 2. The argument passed to unwrap_or matches the type T of the inner value [4]. 3. You are importing the necessary modules if campaign_counter is defined in a different file within your project structure. If this refers to a private project or a specific commit, please double-check the repository path and the file structure of your local clone.
Citations:
- 1: https://github.com/Fundable-Protocol
- 2: Dashboard shows nothing when wallet disconnected - inconsistent with other pages #119
- 3: https://github.com/Fundable-Protocol/fundable
- 4: https://docs.rs/stellar-rpc-client/latest/src/stellar_rpc_client/lib.rs.html
- 5: https://docs.rs/wasi-stellar-rpc-client/latest/src/wasi_stellar_rpc_client/lib.rs.html
- 6: https://github.com/gemwalletcom/wallet/blob/f2d99035/core/crates/gem_stellar/src/rpc/client.rs
🌐 Web query:
site:github.com/Fundable-Protocol/stellar_client_os contracts/campaign-funding/src/lib.rs campaign_counter unwrap_or
💡 Result:
Based on the provided search results, there is no evidence of a file path specifically named contracts/campaign-funding/src/lib.rs containing a variable or function called campaign_counter used with the unwrap_or method. While the search results identify various crowdfunding-related projects and their directory structures—such as fundlr-contracts, substrate-recipes, and solfundme—none of these repositories match the specific file path or code pattern requested [1][2][3]. The method unwrap_or is a standard Rust idiom for handling Option types by providing a default value if the Option is None, but no specific implementation of this in relation to a campaign_counter in the specified file path was found [1][2][3].
Citations:
- 1: https://github.com/fundlr/fundlr-contracts/blob/main/README.md
- 2: https://github.com/JoshOrndorff/recipes/blob/master/pallets/simple-crowdfund/src/lib.rs
- 3: domluna/solfundme@6b99b89
Enforce admin/initialize() before using persisted state
admin and campaign_counter are only read with fallback handling, so create_campaign(), contribute(), claim(), and refund() can succeed even without initialize() and admin is never consulted for authorization. Make initialization required before using shared state, and have the admin gate the operations it’s meant to protect so Error::NotInitialized / Error::Unauthorized are not dead code.
🤖 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 `@contracts/campaign-funding/src/lib.rs` around lines 95 - 104, Update
initialize and the state-using methods create_campaign, contribute, claim, and
refund to require persisted initialization before proceeding: detect missing
admin state and return Error::NotInitialized instead of relying on fallback
values. Enforce admin authorization on the operations intended to be
admin-protected by loading the stored admin and rejecting non-admin callers with
Error::Unauthorized, while preserving existing authentication and state-update
behavior after validation.
| pub fn contribute(env: Env, campaign_id: u64, contributor: Address, amount: i128) { | ||
| contributor.require_auth(); | ||
|
|
||
| if amount <= 0 { | ||
| panic_with_error!(&env, Error::InvalidAmount); | ||
| } | ||
|
|
||
| let mut campaign: Campaign = env.storage().persistent() | ||
| .get(&campaign_id) | ||
| .unwrap_or_else(|| panic_with_error!(&env, Error::CampaignNotFound)); | ||
|
|
||
| if campaign.status != CampaignStatus::Active { | ||
| panic_with_error!(&env, Error::CampaignNotActive); | ||
| } | ||
|
|
||
| let current_time = env.ledger().timestamp(); | ||
| if current_time >= campaign.deadline { | ||
| panic_with_error!(&env, Error::DeadlineNotReached); | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Misleading error code when blocking contributions after the deadline.
At Line 176-178, current_time >= campaign.deadline (deadline has already passed) panics with Error::DeadlineNotReached — the exact opposite of what happened. Contrast with claim() (Line 226-229) and refund() (Line 265-268), where the same error is correctly used for current_time < campaign.deadline. Callers switching on error codes will get a misleading reason for the rejected contribution.
🐛 Proposed fix: add a distinct error for "deadline already passed"
GoalNotMet = 10,
AlreadyWithdrawn = 11,
AlreadyRefunded = 12,
ArithmeticOverflow = 13,
DeadlineInPast = 14,
+ DeadlinePassed = 15,
} let current_time = env.ledger().timestamp();
if current_time >= campaign.deadline {
- panic_with_error!(&env, Error::DeadlineNotReached);
+ panic_with_error!(&env, Error::DeadlinePassed);
}🤖 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 `@contracts/campaign-funding/src/lib.rs` around lines 160 - 178, Add a distinct
error variant representing an expired campaign deadline, then update contribute
so its current_time >= campaign.deadline guard panics with that variant instead
of Error::DeadlineNotReached. Preserve DeadlineNotReached for the pre-deadline
checks in claim and refund.
1. CampaignStatus::Expired now assigned — set in refund() when deadline passes without meeting goal; consumer-facing status is accurate. 2. TTL bump scaled to campaign deadline — campaign_ttl() computes (threshold, bump) proportional to deadline - now, ensuring entries survive until the campaign ends, capped at MAX_TTL (6312000). 3. Doc comments added to all public entrypoints (initialize, create_campaign, contribute, claim, refund, get_campaign, get_contribution, get_admin). 4. Initialization enforced — require_initialized() guard at the top of every state-mutating function; Error::NotInitialized is no longer dead code. 5. Wrong error in contribute() fixed — using CampaignNotActive (Fundable-Protocol#7) instead of DeadlineNotReached (Fundable-Protocol#8) when deadline has passed. Also adds 4 new tests: expired status after refund, contribution to expired campaign forbidden, and init enforcement for create + contribute.
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 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.
Inline comments:
In `@contracts/campaign-funding/src/lib.rs`:
- Around line 180-182: In the campaign creation flow using Self::campaign_ttl,
also extend instance storage with the same ttl and bump via
env.storage().instance().extend_ttl(ttl, bump). Keep the existing persistent
campaign write and persistent TTL extension unchanged.
- Around line 124-133: Update create_campaign and any related deadline
validation to reject future deadlines beyond the maximum retainable TTL horizon
before writing or extending the campaign entry. Reuse campaign_ttl’s
MAX_TTL/ledger timing semantics to calculate the allowed expiry boundary, while
preserving rejection of past deadlines and ensuring accepted campaigns remain
claimable or refundable until their deadline.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 755c5676-df73-4d89-bd2a-39dc0c77f139
📒 Files selected for processing (27)
contracts/campaign-funding/src/lib.rscontracts/campaign-funding/src/test.rscontracts/campaign-funding/test_snapshots/test/test/test_claim_already_withdrawn.1.jsoncontracts/campaign-funding/test_snapshots/test/test/test_claim_before_deadline.1.jsoncontracts/campaign-funding/test_snapshots/test/test/test_claim_goal_not_met.1.jsoncontracts/campaign-funding/test_snapshots/test/test/test_claim_success.1.jsoncontracts/campaign-funding/test_snapshots/test/test/test_contribute.1.jsoncontracts/campaign-funding/test_snapshots/test/test/test_contribute_after_deadline.1.jsoncontracts/campaign-funding/test_snapshots/test/test/test_contribute_multiple_backers.1.jsoncontracts/campaign-funding/test_snapshots/test/test/test_contribute_multiple_from_same_backer.1.jsoncontracts/campaign-funding/test_snapshots/test/test/test_contribute_to_expired_campaign_fails.1.jsoncontracts/campaign-funding/test_snapshots/test/test/test_contribute_without_init_fails.1.jsoncontracts/campaign-funding/test_snapshots/test/test/test_contribute_zero_amount.1.jsoncontracts/campaign-funding/test_snapshots/test/test/test_create_campaign.1.jsoncontracts/campaign-funding/test_snapshots/test/test/test_create_campaign_without_init_fails.1.jsoncontracts/campaign-funding/test_snapshots/test/test/test_events_emitted.1.jsoncontracts/campaign-funding/test_snapshots/test/test/test_full_lifecycle_refund.1.jsoncontracts/campaign-funding/test_snapshots/test/test/test_full_lifecycle_success.1.jsoncontracts/campaign-funding/test_snapshots/test/test/test_get_contribution_none.1.jsoncontracts/campaign-funding/test_snapshots/test/test/test_refund_already_refunded.1.jsoncontracts/campaign-funding/test_snapshots/test/test/test_refund_before_deadline.1.jsoncontracts/campaign-funding/test_snapshots/test/test/test_refund_goal_met.1.jsoncontracts/campaign-funding/test_snapshots/test/test/test_refund_multiple_backers.1.jsoncontracts/campaign-funding/test_snapshots/test/test/test_refund_no_contribution.1.jsoncontracts/campaign-funding/test_snapshots/test/test/test_refund_permissionless.1.jsoncontracts/campaign-funding/test_snapshots/test/test/test_refund_sets_campaign_status_to_expired.1.jsoncontracts/campaign-funding/test_snapshots/test/test/test_refund_success.1.json
🚧 Files skipped from review as they are similar to previous changes (19)
- contracts/campaign-funding/test_snapshots/test/test/test_claim_before_deadline.1.json
- contracts/campaign-funding/test_snapshots/test/test/test_contribute.1.json
- contracts/campaign-funding/test_snapshots/test/test/test_refund_already_refunded.1.json
- contracts/campaign-funding/test_snapshots/test/test/test_claim_already_withdrawn.1.json
- contracts/campaign-funding/test_snapshots/test/test/test_contribute_multiple_from_same_backer.1.json
- contracts/campaign-funding/test_snapshots/test/test/test_refund_goal_met.1.json
- contracts/campaign-funding/test_snapshots/test/test/test_create_campaign.1.json
- contracts/campaign-funding/test_snapshots/test/test/test_refund_permissionless.1.json
- contracts/campaign-funding/test_snapshots/test/test/test_full_lifecycle_success.1.json
- contracts/campaign-funding/test_snapshots/test/test/test_claim_success.1.json
- contracts/campaign-funding/test_snapshots/test/test/test_full_lifecycle_refund.1.json
- contracts/campaign-funding/test_snapshots/test/test/test_contribute_multiple_backers.1.json
- contracts/campaign-funding/test_snapshots/test/test/test_events_emitted.1.json
- contracts/campaign-funding/test_snapshots/test/test/test_claim_goal_not_met.1.json
- contracts/campaign-funding/test_snapshots/test/test/test_contribute_after_deadline.1.json
- contracts/campaign-funding/test_snapshots/test/test/test_refund_before_deadline.1.json
- contracts/campaign-funding/test_snapshots/test/test/test_refund_multiple_backers.1.json
- contracts/campaign-funding/test_snapshots/test/test/test_contribute_zero_amount.1.json
- contracts/campaign-funding/src/test.rs
…tainable deadlines - Instance storage (admin, campaign_counter) now gets the same campaign- specific (ttl, bump) from campaign_ttl() at creation time, ensuring the contract stays available for the full campaign lifecycle. - create_campaign rejects deadlines beyond MAX_DEADLINE_DELTA (~365 days) with Error::DeadlineTooFar (Fundable-Protocol#15), preventing campaigns that would be archived before their deadline. - Adds test_create_campaign_deadline_too_far test.
|
@Idrhas Done |
|
@Idrhas pls check and close the PR |
|
dont forget to offramp using https://stellar.fundable.finance/offramp its fast, free and p2p rates |
1 similar comment
|
dont forget to offramp using https://stellar.fundable.finance/offramp its fast, free and p2p rates |
you dont merge my work, why? @Idrhas |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
contracts/Cargo.toml (1)
9-11: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueRemove the duplicate
nft-streamworkspace member.
contracts/Cargo.tomllistsnft-streamtwice in[workspace.members], starting at line 7 and again at line 10. Remove the duplicate entry so each package has one canonical workspace member.🤖 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 `@contracts/Cargo.toml` around lines 9 - 11, Remove the duplicate "nft-stream" entry from the [workspace.members] list in contracts/Cargo.toml, keeping one canonical occurrence and preserving all other workspace members unchanged.
🤖 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.
Inline comments:
In `@contracts/campaign-funding/src/lib.rs`:
- Around line 98-100: Update MAX_DEADLINE_DELTA and the related deadline
validation and campaign_ttl bump cap to use the pinned SDK’s current maximum TTL
with a conservative fixed-ledger buffer, rather than deriving retention from
SECONDS_PER_LEDGER. Ensure accepted deadlines cannot exceed the TTL system’s
safe retention window. Add boundary tests that advance the ledger around the
accepted deadline and verify campaign reads/writes remain available.
---
Nitpick comments:
In `@contracts/Cargo.toml`:
- Around line 9-11: Remove the duplicate "nft-stream" entry from the
[workspace.members] list in contracts/Cargo.toml, keeping one canonical
occurrence and preserving all other workspace members unchanged.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: b1c726da-b704-46d2-8ba2-d4cccdf6ec9a
📒 Files selected for processing (4)
contracts/Cargo.tomlcontracts/campaign-funding/src/lib.rscontracts/campaign-funding/src/test.rscontracts/campaign-funding/test_snapshots/test/test/test_create_campaign_deadline_too_far.1.json
🚧 Files skipped from review as they are similar to previous changes (1)
- contracts/campaign-funding/src/test.rs
| /// Upper bound for (deadline - now) that the TTL system can retain. | ||
| /// ~365 days at 5 s/ledger. | ||
| const MAX_DEADLINE_DELTA: u64 = (MAX_TTL as u64) * SECONDS_PER_LEDGER; |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
rg -n -C 4 \
'MAX_TTL|MAX_DEADLINE_DELTA|SECONDS_PER_LEDGER|campaign_ttl|max_ttl\(' \
contracts/campaign-funding/src/lib.rs || true
rg -n -C 4 \
'max_entry_ttl|max_ttl|LedgerInfo|sequence_number|timestamp' \
contracts/campaign-funding/src/test.rs \
contracts/campaign-funding/test_snapshots || true
rg -n 'soroban-sdk' \
contracts/Cargo.toml \
contracts/campaign-funding/Cargo.toml || trueRepository: Fundable-Protocol/stellar_client_os
Length of output: 50391
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== relevant source =="
sed -n '1,220p' contracts/campaign-funding/src/lib.rs
echo
echo "== settlement/read/write paths =="
sed -n '200,380p' contracts/campaign-funding/src/lib.rs
echo
echo "== read only invariant probe: derive constants and campaign_ttl for fixed input =="
python3 - <<'PY'
MAX_TTL = 6312000
SECONDS_PER_LEDGER = 5
LEDGER_THRESHOLD = 518400
LEDGER_BUMP = 535680
MAX_DEADLINE_DELTA = (MAX_TTL as u64) * SECONDS_PER_LEDGER
def campaign_ttl(timestamp, deadline):
duration = max(0, deadline - timestamp)
deadline_ledgers = duration // SECONDS_PER_LEDGER
threshold = min(
max(LEDGER_THRESHOLD, deadline_ledgers),
MAX_TTL - LEDGER_BUMP
)
bump = min(threshold + LEDGER_BUMP, MAX_TTL)
return threshold, bump
timestamp = 0
deadline = timestamp + MAX_DEADLINE_DELTA
ttl, bump = campaign_ttl(timestamp, deadline)
print("MAX_DEADLINE_DELTA", MAX_DEADLINE_DELTA)
print("created deadline TTL", ttl, "bump", bump)
print("bump equals MAX_TTL?", bump == MAX_TTL)
print("threshold equals MAX_TTL?", threshold == MAX_TTL)
def simulated_close(current, close_dt):
return current + close_dt
close_dt = 10
current = simulated_close(timestamp, close_dt)
duration = max(0, deadline - current)
deadline_ledgers = duration // SECONDS_PER_LEDGER
threshold = max(LEDGER_THRESHOLD, deadline_ledgers)
bump = threshold + LEDGER_BUMP
print("after {} seconds current", current, "deadline ledgers", deadline_ledgers, "threshold", threshold, "bump", bump)
print("retention window above deadline after close?", bump > deadline_ledgers + LEDGER_THRESHOLD)
PYRepository: Fundable-Protocol/stellar_client_os
Length of output: 14080
🌐 Web query:
Soroban Storage max_ttl max_entry_ttl ledger timestamp is not fixed seconds per ledger documentation
💡 Result:
In Soroban, storage Time-to-Live (TTL) and ledger timestamps are governed by distinct mechanisms, and your observation that seconds per ledger is not a fixed constant is correct [1][2][3]. 1. Storage TTL is Ledger-Based: TTL in Soroban is measured in ledger sequences, not time [4][5][6]. Specifically, it represents the number of ledgers remaining until an entry is considered expired [4][5]. When an entry's TTL reaches zero, it is either archived (for Persistent or Instance storage) or permanently deleted (for Temporary storage) [4][6]. The max_ttl and max_entry_ttl values, accessible via the Soroban SDK, define the upper bounds for these ledger-based lifespans [7][8][9]. 2. Ledger Timestamp is Real-World Time: The ledger timestamp, accessed via env.ledger.timestamp, provides the Unix epoch time (in seconds) for when a specific ledger was closed [10][1]. Because ledger close times on the Stellar network can fluctuate based on network conditions and consensus, the number of seconds that pass between ledger sequences is variable [1][3]. 3. Relationship: While developers sometimes use an approximation (such as 5 seconds per ledger) for convenience when estimating time-based deadlines, this is an heuristic and not a protocol-level guarantee [2]. Relying on a fixed "seconds per ledger" value for critical logic is discouraged [1]. For time-sensitive logic, you should use the ledger timestamp; for logic dependent on ledger progression and state retention, you should use the ledger sequence and TTL mechanisms [1]. Top_results: [10][4][5][6][1][2]
Citations:
- 1: https://www.mintlify.com/stellar/rs-soroban-sdk/api/modules/ledger
- 2: Replace manual ledger-to-time comments with a shared conversion constant doc Fidelis900/soroban-starter-kit#694
- 3: contract: session expiry is checked against ledger timestamp but SESSION_TTL is in seconds Haroldwonder/AnchorKit#430
- 4: https://developers.stellar.org/docs/build/guides/storage/choosing-the-right-storage
- 5: https://developers.stellar.org/docs/learn/fundamentals/contract-development/storage/state-archival
- 6: https://rya-sge.github.io/access-denied/2026/04/15/soroban-state-archival/
- 7: https://docs.rs/soroban-sdk/latest/soroban_sdk/storage/struct.Storage.html
- 8: https://docs.rs/soroban-sdk/latest/src/soroban_sdk/storage.rs.html
- 9: https://docs.rs/soroban-ledger-snapshot/latest/soroban_ledger_snapshot/struct.LedgerSnapshot.html
- 10: https://docs.rs/soroban-sdk/latest/soroban_sdk/ledger/struct.Ledger.html
Do not derive campaign retention from a fixed ledger cadence.
MAX_DEADLINE_DELTA uses SECONDS_PER_LEDGER = 5, but Soroban TTL is ledger-sequence based and ledger timestamps are not guaranteed to advance five seconds per ledger. Accepting MAX_DEADLINE_DELTA allows campaigns whose TTL may be capped below the deadline in later ledgers; archived persistent entries can then prevent later reads/writes such as claim() / refund().
Use the pinned SDK’s current maximum TTL for MAX_TTL, then apply a conservative fixed-ledger buffer before the deadline for both deadline validation and the campaign_ttl bump cap. Add boundary tests that advance the ledger around the accepted deadline.
Also applies to: 165-168
🤖 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 `@contracts/campaign-funding/src/lib.rs` around lines 98 - 100, Update
MAX_DEADLINE_DELTA and the related deadline validation and campaign_ttl bump cap
to use the pinned SDK’s current maximum TTL with a conservative fixed-ledger
buffer, rather than deriving retention from SECONDS_PER_LEDGER. Ensure accepted
deadlines cannot exceed the TTL system’s safe retention window. Add boundary
tests that advance the ledger around the accepted deadline and verify campaign
reads/writes remain available.
This pull request introduces a new smart contract for campaign-based crowdfunding on Soroban, adds it to the workspace, and provides initial test coverage for key error conditions. The main focus is on creating, funding, claiming, and refunding campaigns, with robust error handling and event emission for transparency.
close #518
New Campaign Funding Contract
campaign-fundingcontract implementing crowdfunding logic, including campaign creation, contribution, claim, and refund functions, with comprehensive error handling and event emission. (contracts/campaign-funding/src/lib.rs)Cargo.tomlfor the new contract, specifying dependencies and build configuration. (contracts/campaign-funding/Cargo.toml)contracts/Cargo.toml)Testing and Snapshots
contracts/campaign-funding/test_snapshots/test/test/test_contribute_campaign_not_found.1.json,test_create_campaign_deadline_in_past.1.json,test_create_campaign_zero_goal.1.json) [1] [2] [3]…und triggersNew Soroban smart contract for campaign funding with:
Summary by CodeRabbit