Skip to content

Redemptions and releases

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.

Bridge out: from a kUSDT burn on Kaspa to a USDT releaseThe user sends kUSDT with a standard KCC-20 transfer to a burn owner that names the payout chain and address. Once the burn is final on Kaspa, each signer reads it from its own node, checks the burn owner and amount, and signs an EIP-712 Release. The coordinator collects at least three signatures, sorted by signer address, and calls release on the vault, which checks them and pays units minus the fee to the named address.UserKaspa walletkUSDT familyKaspa L1Signers (5)each on its own nodesCoordinatoruntrusted relayKusdtVaultEthereum or BNB ChainKCC-20 transfer to the burn ownerpayload "KUSDT" 01 … names the payout1burn state: owner = BLAKE2b(R);it can never move again23Finality: the accepting block is at least 1 000 DAA below the virtual DAA score (about 100 s), deeper above 10 000 kUSDT (up to 36 000 DAA, about 1 h)reads the burnfrom the signer's own node4checks burn owner and amount;ECDSA-signs an EIP-712 Release53 or more signaturessorted by signer address6release(r, signatures)anyone may submit7checks quorum, redeemId,backing and release allowance8pays units − fee in USDT to the address named in Revent Released(redeemId, …)9
Bridge out. Numbers match the steps on this page. The payout address is fixed by the burn owner, so no party can redirect it.

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.

function release(Release calldata r, bytes[] calldata signatures)
struct Release { bytes32 redeemId; address recipient; uint256 units; }

The vault:

  1. requires redeemId unused and not cancelled by the owner, and a recipient that is not the zero address, the vault itself or the token contract;
  2. computes fee = max(minReleaseFee, ceil(units × feeBps / 10⁴)) and requires units > fee;
  3. 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);
  4. requires units ≤ unreservedBacking(), the backing no other queued payout has a claim on;
  5. draws units from the release allowance, the fee included, because the fee leaves the backing too;
  6. marks redeemId used and queues the payout;
  7. emits ReleaseQueued(redeemId, recipient, units, fee, payout, readyAt).

Anyone may call release with valid signatures. The coordinator normally does.

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.

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).

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)