# KasExitBridge Developer Guide

Bridge iKAS (Igra) back to KAS (Kaspa).

## Contract

* **Proxy:** [`0x4bb88C213d3eD9dc4bae694f1bc1bF745903b2d0`](https://explorer.igralabs.com/address/0x4bb88C213d3eD9dc4bae694f1bc1bF745903b2d0)
* **Implementation:** [`0x00d39e05a20b2c4f6d0d6cfc3c5718066b861334`](https://explorer.igralabs.com/address/0x00d39e05a20b2c4f6d0d6cfc3c5718066b861334)
* **Chain:** Igra Mainnet (38833)
* **ABI:** [Blockscout](https://explorer.igralabs.com/address/0x4bb88C213d3eD9dc4bae694f1bc1bF745903b2d0?tab=contract)
* **Spec:** [KasExitBridge](https://igra-labs.gitbook.io/igralabs-docs/for-developers/architecture/specifications/kasexitbridge)

## Current Limits

| Parameter            | Value                               |
| -------------------- | ----------------------------------- |
| Min exit             | 1,000 KAS (= 100,000,000,000 sompi) |
| Max exit             | 50,000 KAS                          |
| Max exits per window | 20                                  |
| Max KAS per window   | 200,000 KAS                         |
| Window size          | 86,400 blocks (\~25 hours)          |
| Fee                  | 0 (no fee policy set)               |

## How to Exit

### 1. Check availability

```solidity
// Check remaining capacity in current window
(uint32 windowIndex, uint32 windowEndsAtBlock, uint32 remainingExits, uint64 remainingUnlockAmountSompi)
    = kasExitBridge.throttleStatus();
```

### 2. Quote fee (currently 0)

```solidity
uint64 fee = kasExitBridge.quoteFee(msg.sender, unlockAmountSompi);
```

### 3. Submit exit

```solidity
(uint32 requestId, bytes32 messageId) = kasExitBridge.requestExit{value: totalWei}(
    kasPayoutAddress,   // e.g. "kaspa:qr..."
    unlockAmountSompi   // amount in sompi (1 KAS = 1e8 sompi)
);
```

**`msg.value`** must equal exactly `(unlockAmountSompi + feeAmountSompi) * 1e10` wei.

The `1e10` factor (SOMPI\_SCALE) converts sompi to wei: 1 KAS = 1e8 sompi = 1e18 wei.

### 4. What happens on success

* iKAS is burned (removed from circulation via selfdestruct-to-self)
* Exit record is written to an on-chain ring buffer
* Hyperlane `Mailbox.dispatch()` is called, emitting `Dispatch` event
* Kaspa-side release actors observe the event and unlock KAS on L1
* `ExitRequested(requestId, messageId, feeAmountSompi)` is emitted

All three (burn, record, dispatch) are atomic — if any fails, the entire tx reverts.

### 5. Track your exit

```solidity
(bool exists, uint32 recordedAtBlock, uint64 feeAmountSompi, uint64 unlockAmountSompi, bytes32 messageId)
    = kasExitBridge.getExitRequest(requestId);
```

Ring buffer stores the last 8,191 requests. Older records are overwritten, but `messageId` is permanently anchored in Hyperlane's MerkleTreeHook.

## JavaScript Example (ethers v6)

```javascript
const { ethers } = require("ethers");

const provider = new ethers.JsonRpcProvider("https://rpc.igralabs.com:8545");
const wallet = new ethers.Wallet(PRIVATE_KEY, provider);

const bridge = new ethers.Contract(
    "0x4bb88C213d3eD9dc4bae694f1bc1bF745903b2d0",
    [
        "function requestExit(string kasPayoutAddress, uint64 unlockAmountSompi) payable returns (uint32 requestId, bytes32 messageId)",
        "function quoteFee(address originBurner, uint64 unlockAmountSompi) view returns (uint64 feeAmountSompi)",
        "function throttleStatus() view returns (uint32 windowIndex, uint32 windowEndsAtBlock, uint32 remainingExits, uint64 remainingUnlockAmountSompi)",
        "function getExitRequest(uint32 requestId) view returns (bool exists, uint32 recordedAtBlock, uint64 feeAmountSompi, uint64 unlockAmountSompi, bytes32 messageId)",
        "function getConfig() view returns (bytes32, address, address, uint32, uint32, uint64, uint64, uint64)",
    ],
    wallet
);

const SOMPI_SCALE = 10000000000n; // 1e10
const kasPayoutAddress = "kaspa:qr...your-address...";
const unlockAmountSompi = 1000_00000000n; // 1,000 KAS in sompi

// Check capacity
const status = await bridge.throttleStatus();
console.log("Remaining exits:", status.remainingExits);
console.log("Remaining KAS:", Number(status.remainingUnlockAmountSompi) / 1e8);

// Quote fee
const fee = await bridge.quoteFee(wallet.address, unlockAmountSompi);

// Calculate msg.value
const totalWei = (unlockAmountSompi + fee) * SOMPI_SCALE;

// Submit exit
const tx = await bridge.requestExit(kasPayoutAddress, unlockAmountSompi, {
    value: totalWei,
    gasPrice: 1100000000000n, // 1100 gwei — Igra minimum
});
const receipt = await tx.wait();
console.log("Exit submitted:", receipt.hash);
```

## Kaspa Address Requirements

* Must start with `kaspa:`
* At least 1 character after the prefix
* Max 67 bytes total
* Only bech32 characters after prefix: `qpzry9x8gf2tvdw0s3jn54khce6mua7l`
* No bech32 checksum validation (v1) — double-check your address

## Gotchas

1. **`msg.value` must be exact.** Not "at least" — exactly `(unlockAmountSompi + fee) * 1e10`. Too much or too little reverts with `InvalidMsgValue`.
2. **Amounts are in sompi, not KAS.** 1 KAS = 1e8 sompi. The `unlockAmountSompi` param is `uint64`.
3. **Min 1,000 KAS per exit.** Smaller amounts revert with `ExitAmountBelowMinimum`.
4. **Gas price.** Use `gasPrice: 1100000000000` (1100 gwei). Igra minimum is 1000 gwei; EIP-1559 fields from RPC are unreliable.
5. **No bech32 checksum validation.** The contract only checks prefix + charset. A typo in your Kaspa address will pass validation but your KAS will be unrecoverable.
6. **Throttle is global, not per-user.** 20 exits per window shared across all users. Check `throttleStatus()` before submitting.
7. **KAS release is not instant.** After your tx confirms on Igra, Kaspa-side release actors must observe the Hyperlane event and satisfy their finality policy before unlocking KAS. Expect delays.
8. **Ring buffer overwrites.** After 8,191 exits, `getExitRequest()` for your `requestId` will return `exists = false`. Save your `messageId` from the tx receipt if you need a permanent reference.
9. **Ownership transferring to DAO.** The bridge is being transferred to the Governance contract (`0xB3300fcC2F3EF3DeCdF8B1f710c21666f33Cbf18`). Future parameter changes will require governance proposals.

## View Functions

| Function                    | Returns                           |
| --------------------------- | --------------------------------- |
| `getConfig()`               | All bridge parameters             |
| `throttleStatus()`          | Current window capacity           |
| `quoteFee(burner, amount)`  | Fee for a given exit              |
| `getExitRequest(requestId)` | Stored exit record                |
| `nextExitRequestId()`       | Next requestId to be assigned     |
| `totalBurned()`             | Cumulative iKAS burned (wei)      |
| `totalFeeCharged()`         | Cumulative fees collected (wei)   |
| `owner()`                   | Current owner                     |
| `pendingOwner()`            | Pending ownership transfer target |

## Error Reference

| Error                          | Cause                                     |
| ------------------------------ | ----------------------------------------- |
| `InvalidMsgValue`              | `msg.value != (unlock + fee) * 1e10`      |
| `ExitAmountBelowMinimum`       | `unlockAmountSompi < minExitSompi`        |
| `ExitAmountAboveMaximum`       | `unlockAmountSompi > maxExitSompi`        |
| `InvalidExitAmount`            | `unlockAmountSompi == 0`                  |
| `InvalidKasPayoutAddress`      | Bad prefix, charset, length, or empty     |
| `ThrottleExitCountExceeded`    | Window exit count exhausted               |
| `ThrottleUnlockAmountExceeded` | Window KAS cap exhausted                  |
| `ExitRequestCounterExhausted`  | `uint32` requestId space full (4B+ exits) |
| `FeeBalanceExceeded`           | Claiming more fees than available         |
| `UnauthorizedFeeWithdrawal`    | Not owner or feeClaimer                   |
