Redemptions and releases
The redemption
Section titled “The redemption”A redemption is a standard KCC-20 transfer that sends kUSDT to a burn owner. The burn owner commits to the payout chain and EVM address. Nobody can redirect the payout, and the tokens can never move again.
Rules for the burn output:
| Field | Required value |
|---|---|
owner_scheme |
0x03 (p2sh) |
owner |
BLAKE2b-256(R(chain, address)) |
borrow_scheme |
0x00, so the burn state cannot be borrowed into |
borrow_guard |
32 zero bytes |
extension_commitment |
32 zero bytes |
amount |
Above the release fee for that chain, or it is never paid |
Signers refuse a burn state whose borrow scheme, borrow guard or extension commitment is not zero: a borrowable
state could move to a new outpoint and be claimed twice. Such kUSDT is lost, so wallets never build one, and since
run 21 the core’s transfer builder refuses one, as it refuses a redemption naming a chain other than 1 or 2 or the
zero address (Daybreak [27]).
The transaction payload should name the burn outputs and their payout, so the bridge can find them.
The flow
Section titled “The flow”What signers check before signing
Section titled “What signers check before signing”Each signer reads the burn from its own Kaspa node. It signs a Release for a burn output once the burn is final
(see Finality) and its amount is above the release fee. Before signing it checks a rule
the vault cannot see: the release names the chain and the address that the burn owner commits to.
A signer also refuses a release to an address the token blocks. The deployment names the token’s blocklist view
for each chain (blocklist: isBlackListed for Ethereum USDT, none for BSC-USD). The signer asks every one of
its RPCs; if they disagree, it signs nothing. USDT’s blacklist stops only senders (transfer checks
msg.sender, transferFrom checks from), so the vault would pay a blacklisted address, and the USDT would be
frozen there for good. The signers stop that payout instead.
The release
Section titled “The release”function release(Release calldata r, bytes[] calldata signatures)struct Release { bytes32 redeemId; address recipient; uint256 units; }The vault:
- requires
redeemIdunused and not cancelled by the owner, and a recipient that is not the zero address, the vault itself or the token contract; - computes
fee = max(minReleaseFee, ceil(units × feeBps / 10⁴))and requiresunits > fee; - verifies at least 3 EIP-712 signatures by distinct current signers, given in strictly increasing order of signer address (the ordering proves they are distinct);
- requires
units ≤ unreservedBacking(), the backing no other queued payout has a claim on; - draws
unitsfrom the release allowance, the fee included, because the fee leaves the backing too; - marks
redeemIdused and queues the payout; - emits
ReleaseQueued(redeemId, recipient, units, fee, payout, readyAt).
Anyone may call release with valid signatures. The coordinator normally does.
The wait
Section titled “The wait”Nothing has left yet. The payout waits in the vault until readyAt: 10 minutes on mainnet, which the owner can
set from 5 minutes to 24 hours. Then anyone may call executeRelease(redeemId), the coordinator normally does, and
the vault adds the fee to feesAccrued, transfers (units − fee) × scale to the recipient and emits
Released(redeemId, recipient, units, fee, payout).
The wait is the guardian’s time to pause. Nothing is paid while a vault is paused, and an unpause starts a full wait again for everything queued, so if signer keys are stolen, a pause within the wait means no forged payout leaves. Every payout waits, however small: otherwise a thief would split the theft into small payouts. See the threat model.
A payout the owner cancels stays cancelled. The owner can take a queued payout out of the queue
(cancelRelease, cancelRefund): nothing is paid and its allowance comes back. From then on the vault refuses to
queue that redeemId or nonce again, whatever signatures come with it, until the owner reinstates it
(reinstateRelease, reinstateRefund; on mainnet that waits 48 hours). After that it must be queued again with
signatures of the current signers, and it waits again. Meanwhile the coordinator asks no signer for it, looks
again every ten minutes, and holds up no other payout. A release’s fee is always the flat fee: the
rising deposit fee does not apply to it.
The redeem ID
Section titled “The redeem ID”Each burn output has one redeem ID:
redeemId = keccak256(abi.encodePacked("kusdt/redeem/v1", bytes32 kaspaTxId, uint32 outputIndex))The vault exposes it as redeemIdOf(kaspaTxId, outputIndex). Because the vault stores used IDs, each burn
output is paid at most once (INV-3).
Limits that apply to one release
Section titled “Limits that apply to one release”| Condition | Consequence |
|---|---|
units ≤ fee |
Not paid: marked unpayable, looked at again about once a minute, and paid if the fee falls below units. Wallets never build one |
| A zero or vault recipient | Never paid: marked unpayable, and its kUSDT is lost. Wallets never build one |
units above what the allowance holds now, or a paused vault |
Reverts; the relayer retries it with its own backoff, without holding up other burns |
units above releaseCapacity (250 000 USDT by default) |
Cannot be paid until the owner raises the capacity (within its hard bound). Wallets refuse to build such a burn |
units above that vault’s backing |
Reverts; waits until deposits on that chain bring the backing. Wallets refuse to build such a burn |
| Recipient blocked by the token | Never signed. The burn is held as blocked, checked again every hour, and paid if the token unblocks the address. Its USDT stays in the vault as backing, and it holds up no other burn. Wallets refuse to build such a burn (see Blocked recipients) |
| The token blocks the vault itself | The vault can pay nothing out, while deposits still go in. The monitor raises vault-blocked and the guardian pauses everything (see The monitor) |