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.

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)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 a recipient that is neither the zero address nor the vault itself;
  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 ≤ backing();
  5. draws the payout, units − fee, from the release allowance;
  6. marks redeemId used, adds the fee to feesAccrued, and transfers (units − fee) × scale to the recipient;
  7. emits Released(redeemId, recipient, units, fee, payout).

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

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, or a zero or vault recipient Never paid: marked unpayable, and its kUSDT is lost. Wallets never build one
Payout above what the allowance holds now, or a paused vault Reverts; the relayer retries it with its own backoff, without holding up other burns
Payout 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)