Refunds on Campaign Failure
When an LGE does not reach its target before the deadline, the campaign flips to a Failed state and no liquidity pool is created. Instead of stranding contributor ICP, the platform enables a claimable refund: contributors press the “GET REFUND” button on the LGE page and are repaid on-chain, atomically, from funds held in custody for that campaign. The refund window is one week (7 days) after the campaign’s expiry.
When a campaign becomes Failed
Section titled “When a campaign becomes Failed”A campaign is marked Failed in one of two ways:
| Trigger | Mechanism |
|---|---|
| The campaign expires without selling out | Housekeeping (check_expired_icos / check_and_update_campaign_status) sets the state to Failed once expiry_timestamp has passed |
| Finalization runs on a campaign that did not reach the token target | The backend marks it Failed and does not create a pool (e.g. finalize_ico is invoked by the authorized caller — the backend’s own finalize flow or an authorized canister — on a non–sold-out campaign) |
The Failed flip is status-only — it does not move any funds. Money moves only when a contributor (or the authorized-caller bulk path) initiates a refund.
The claimable “GET REFUND” flow
Section titled “The claimable “GET REFUND” flow”Refunds are user-initiated: there is no automatic payout. A contributor on the LGE page presses “GET REFUND”, which calls the backend method request_refund(campaign_id). The backend validates that the campaign is Failed, that the deadline has not passed, and that the caller has contributions, then marks those contributions refunded in dao_storage atomically before transferring the ICP from custody.
- Deadline. The refund must be claimed within 7 days of
campaign.expiry_timestamp. After that window,request_refundreturns an error. - Concurrency. The backend uses per-campaign and per-user locks, so only one refund is in progress per user per campaign at a time.
- Bulk refund path.
refund_campaign(campaign_id), invoked by the authorized caller, processes all pending refunds for a Failed campaign in one go. It routes each user through the same atomic-mark + subaccount-transfer path, so it is idempotent and a laterrequest_refundfor an already-refunded user cannot double-pay.
See the full flow in Core Workflows.
Per-party refund percentages
Section titled “Per-party refund percentages”| Party | Refund | Notes |
|---|---|---|
| Contributors | 90% of contributed ICP (legacy) / 95% for deferred-referral campaigns | The 95% is 90% net + the held 5% referral band, returned tier-independently |
| Creator | ~70% of ICP spent on their own token contributions | Same contributor↔creator ratio across campaign types |
| OHSHII | Non-refundable | The OHSHII platform share is not returned |
The user-facing summary is the simple 90% contributors / 70% creator / OHSHII non-refundable model. The 95% figure applies specifically to deferred-referral campaigns, where the full 5% referral band was held in custody rather than paid out at purchase time and is therefore available to return on failure. For pre-escrow / legacy campaigns the referral subaccount is empty and the entire amount is drawn from the campaign subaccount.
How the percentage is computed (gross vs net)
Section titled “How the percentage is computed (gross vs net)”The backend computes the exact refund and the pool_manager transfers it exactly — it does not re-apply a 90% factor (that double-discount would over-draw a net-funded subaccount). The campaign subaccount holds the net that was forwarded into it:
| Campaign type | Contributor refund basis | Creator |
|---|---|---|
Pre-escrow / legacy (e.g. dao-0000000011) | 90% of contributed ICP | 70% |
| New (escrow) | 100% of the net recorded for the contributions — ≈ the same e8s as the legacy 90%-of-gross, since fees were already taken at purchase | Same contributor↔creator ratio |
Either way the refund draws only from the net actually held in the subaccount, so it can never over-draw.
Migrated legacy campaigns: no practical difference
Section titled “Migrated legacy campaigns: no practical difference”A migrated legacy campaign — a pre-escrow campaign that now carries the current settings (referral_deferred = Some(true), curve_input_net = Some(false)), such as the long-running showcase LGE dao-0000000011 — behaves exactly like a freshly created escrow campaign in every user-visible respect. There is no migration step a participant or integrator needs to be aware of, and no fund relocation is required:
- Same fund location. Its contributed ICP sits in the standard per-campaign subaccount on
pool_manager(derived from the campaign id) — the identical account a brand-new campaign uses. Nothing is commingled with the treasury. - Same model for new contributions. Any contribution made after migration flows through the per-contribution escrow and the deferred-referral hold, exactly like any other current campaign.
- Same ~95% refund — not 90%. Because the campaign carries
referral_deferred = Some(true), contributors recover 90% net + the held 5% referral band = ~95%, tier-independent — the same figure a new campaign returns, not the 90% that non-migrated pre-escrow campaigns give. The only internal difference is the source of that 5%: for the campaign’s original pre-escrow contributions the per-campaign referral subaccount is empty, sosplit_refund_legs_e8sfolds the 5% into the campaign-subaccount leg — fully covered, because those contributions over-funded the campaign subaccount to ≈ the gross (not just the net). For contributions made after migration the 5% is held in, and returned from, the referral subaccount. Either way the contributor receives ~95%, drawn only from the campaign’s own held subaccounts — never the treasury — and the held funds are sufficient to cover every contributor’s full refund plus the creator’s ~70%.
The pre-escrow vs escrow distinction is therefore an internal accounting detail, not a behavioural one: a migrated legacy campaign and a freshly created one are indistinguishable to participants.
Per-contribution escrow custody
Section titled “Per-contribution escrow custody”Refunds work because contributor ICP is held in custody subaccounts tied to the campaign, never commingled with the platform treasury. Each purchase moves ICP through a per-contribution escrow before it reaches the campaign pool; on a Failed campaign the funds sit in custody and the refund draws them back out.
- Campaign subaccount. Holds the net ICP that funds the LGE.
process_single_refundtransfers the contributor’s campaign leg from here. Because the subaccount holds net, the refund and the (never-reached) finalization use the same net basis. - Per-campaign referral subaccount (deferred-referral campaigns). Holds the full 5% referral band. On failure the held band is returned to the contributor as a separate
refund_referral_legfrom this subaccount — this is the difference between the 90% and 95% figures. On legacy campaigns this subaccount is empty and the full amount comes from the campaign subaccount. - Held funds only. The backend’s
split_refund_legs_e8ssplits the refund across the held subaccounts only — never the treasury. Each leg is net of its own ~0.0001 ICP ledger fee, which the participant bears.
Guest fee refund (OHSHII)
Section titled “Guest fee refund (OHSHII)”If a contributor paid the one-time guest fee (charged only to Guest-tier participants on their first contribution), both request_refund and the bulk refund_campaign also return that OHSHII to them on a Failed campaign, alongside the ICP. The exact amount is snapshotted at contribution time, so the refund is independent of any later fee-config change. It is idempotent (recorded per campaign + user, mark-before-pay), and eligibility is read from the recorded payment, not the user’s current tier (tiers can change over time). The refund returns NotOwed if the user is still a participant.
On-chain, atomic, and self-healing
Section titled “On-chain, atomic, and self-healing”- On-chain and atomic. The backend marks contributions refunded in
dao_storageatomically before thepool_managertransfers ICP from custody. There is no off-chain reconciler — on-chain custody state is the source of truth. - No double-pay. Both the user path and the authorized-caller bulk path go through the same atomic mark + subaccount transfer, so a contribution can be refunded at most once.
- 7-day residual sweep. Once a campaign’s 7-day refund window has fully closed, a periodic housekeeping task sweeps any un-refunded residual ICP to the OhShii ecosystem treasury — NOT to the failed campaign’s own SONS DAO (the campaign failed, so it does not retain these funds): the campaign subaccount residual goes to the OhShii platform treasury (the backend canister) and the per-campaign referral subaccount residual to the ONS governance canister — fully on-chain, balance-driven, and at-most-once per campaign. Un-refunded OHSHII guest fees in the per-campaign guest-fee subaccount are forwarded to the treasury by the OHSHII leg of the same sweep. Both treasuries are ONS-controlled, so recovering any swept residual requires an ONS governance withdraw proposal — it is not lost, but it can only be released by a community vote.
For the end-to-end sequence — including the finalization success path that this failure path mirrors — see the full flow in Core Workflows.