Skip to content

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.

A campaign is marked Failed in one of two ways:

TriggerMechanism
The campaign expires without selling outHousekeeping (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 targetThe 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.

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.

"OHSHII ledger"ICPLedgerpool_managerdao_storageBackendUser"OHSHII ledger"ICPLedgerpool_managerdao_storageBackendUserCampaign is Failed (expiry or finalize on non-sold-out)alt[referral leg > 0 (deferred-referral campaign)]opt[Guest paid the one-time OHSHII guest fee]"request_refund(campaign_id)"get_campaignvalidate state Failed, deadline, has contributionsmark_contributions_refunded_atomic(user, campaign_id)Okcompute refund + split into (campaign leg, referral leg)process_single_refund(campaign_id, user, campaign_leg)transfer ICP from campaign subaccount to userrefund_referral_leg(campaign_id, user, referral_leg)transfer held referral ICP from referral subaccount to userrefund OHSHII guest fee to user"RefundResult::Ok"
  • Deadline. The refund must be claimed within 7 days of campaign.expiry_timestamp. After that window, request_refund returns 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 later request_refund for an already-refunded user cannot double-pay.

See the full flow in Core Workflows.

PartyRefundNotes
Contributors90% of contributed ICP (legacy) / 95% for deferred-referral campaignsThe 95% is 90% net + the held 5% referral band, returned tier-independently
Creator~70% of ICP spent on their own token contributionsSame contributor↔creator ratio across campaign types
OHSHIINon-refundableThe 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 typeContributor refund basisCreator
Pre-escrow / legacy (e.g. dao-0000000011)90% of contributed ICP70%
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 purchaseSame 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, so split_refund_legs_e8s folds 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.

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_refund transfers 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_leg from 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_e8s splits 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.

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 and atomic. The backend marks contributions refunded in dao_storage atomically before the pool_manager transfers 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.