diff --git a/contracts/ExitHandler.sol b/contracts/ExitHandler.sol
index 69dc512..5ec90ea 100644
--- a/contracts/ExitHandler.sol
+++ b/contracts/ExitHandler.sol
@@ -29,6 +29,9 @@ contract ExitHandler is DepositHandler {
     uint256 amount
   );
 
+  event LimboExitStarted(bytes32 indexed exitId, uint256 color);
+  event LimboExitChallengePublished(bytes32 indexed exitId, address indexed _from, uint8 _challengeNumber, uint8 _inputNumber);
+
   struct Exit {
     uint256 amount;
     uint16 color;
@@ -36,16 +39,54 @@ contract ExitHandler is DepositHandler {
     bool finalized;
     uint32 priorityTimestamp;
     uint256 stake;
+    bool isLimbo;
+  }
+
+  struct LimboExit {
+    LimboIn[] input;
+    LimboOut[] output;
+    bool finalized;
+    uint256 stake;
+    address exitor;
+    bool isValid;
+    LimboChallenge[] challenge;
+  }
+
+  struct LimboIn {
+    address owner;
+    bool isPegged;
+    bool exitable;
+  }
+
+  struct LimboOut {
+    uint256 amount;
+    address owner;
+    bool isPegged;
+    uint256 color;
+    bool exitable;
+  }
+
+  struct LimboChallenge {
+    address owner;
+    uint8 inputNo;
+    bool resolved;
   }
 
   uint256 public exitDuration;
+  uint256 public limboPeriod;
+  uint256 public piggybackStake;
+  uint256 public challengeStake;
   uint256 public exitStake;
   uint256 public nftExitCounter;
 
+  uint256 public constant LimboJoinDelay = (12 seconds);
+
   /**
    * UTXO → Exit mapping. Contains exits for both NFT and ERC20 colors
    */
   mapping(bytes32 => Exit) public exits;
+  mapping(bytes32 => LimboExit) public limboExits;
+  mapping(bytes22 => bool) public succesfulLimboExits;
 
   function initializeWithExit(
     Bridge _bridge, 
@@ -65,6 +106,249 @@ contract ExitHandler is DepositHandler {
     exitDuration = _exitDuration;
   }
 
+  function startLimboExit(bytes memory inTxData) 
+  public payable returns (bytes32 utxoId) { 
+    require(msg.value >= exitStake, "Not enough ether sent to pay for exit stake");
+    TxLib.Tx memory transferTx = TxLib.parseTx(inTxData);
+
+    // assuming tx have one input and one output only
+    uint8 _outputIndex = 0;
+    uint8 _inputIndex = 0;
+
+    TxLib.Output memory out = transferTx.outs[_outputIndex];
+    
+    mapping(uint8 => LimboIn) public inputs;
+    mapping(uint8 => LimboOut) public outputs;
+
+    LimboOut memory output;
+    outputs[_outputIndex].owner = out.owner;
+    outputs[_outputIndex].color = out.color;
+    outputs[_outputIndex].amount = out.value;
+    outputs[_outputIndex].isPegged = false;
+    outputs[_outputIndex].exitable = true;
+
+    inputs[_inputIndex].isPegged = false;
+    inputs[_inputIndex].exitable = true;
+    
+    bytes32 inTxHash = keccak256(inTxData);
+
+    bytes32 utxoId = bytes32(uint256(_outputIndex) << 120 | uint120(uint256(inTxHash)));
+    uint256 priority;
+
+    if (isNft(out.color)) {
+      priority = (nftExitCounter << 128) | uint128(uint256(utxoId));
+      nftExitCounter++;
+    } else {      
+      priority = getERC20ExitPriority(*, utxoId, txPos);
+    }
+    limboExits[utxoId] = LimboExit({
+      output: outputs,
+      input: inputs,
+      finalized: false,
+      stake: exitStake,
+      exitor: msg.sender,
+      isValid: true,
+      challenges:{}
+    });
+
+    emit LimboExitStarted(
+      inTxHash, 
+      out.color
+    );
+    tokens[out.color].insert(priority);
+
+    return utxoId;
+  }
+
+  function joinLimboExit(bytes32 exitId, uint8 _index) public payable {
+    require(msg.value >= piggybackStake, "Not enough ether sent to join the exit");
+
+    address owner = msg.sender;
+    LimboExit memory limboExit = limboExits[exitId];
+
+    if (limboExit.input[_index].owner == owner){
+      // input is piggybacking
+      require(limboExit.input[_index].isPegged = false, "Already joined the exit");
+
+      limboExit.input[_index].isPegged = true;
+    } else if (limboExit.output[_index].owner == owner) {
+      // output is piggybacking
+      require(limboExit.output[_index].isPegged = false, "Already joined the exit");
+
+      limboExit.output[_index].isPegged = true;
+    }
+  }
+
+  function putChallengeOnLimboExitInput(
+        bytes32 exitId,
+        uint8 _inputIndex
+    ) public payable returns (bool success) {
+        require(msg.value >= challengeStake);
+        LimboExit memory exit = limboExits[exitId];
+        require(exit.isValid == true);
+        for (uint8 i = 0; i < exit.challenge.length; i++) {
+            require(_inputIndex != exit.challenge[i].inputNo);
+        }
+        LimboChallenge memory limboInputChallenge;
+        limboInputChallenge.from = msg.sender;
+        limboInputChallenge.inputNo = _inputIndex;
+        limboInputChallenge.resolved = false;
+        exit.challenge.push(limboInputChallenge);
+        emit LimboExitChallengePublished(exitId, msg.sender, uint8(exit.challenge.length-1), _inputIndex);
+        return true;
+  }
+
+  function challengeLimboExitByInclusionProof(
+    bytes32 exitId,
+    bytes inTxData, uint8 inputNo) 
+    public payable {
+    require(msg.value >= challengeStake, "Not enough ether sent to challenge exit");
+    LimboExit memory limboExit = limboExits[exitId];
+    bytes32 inTxHash = keccak256(inTxdata);
+    require(limboExit.txHash == inTxHash);
+    require(limboExit.isValid == true);
+
+    require(block.timestamp <= limboExit.timePublished + LimboChallangesDelay);
+    TxLib.Tx memory transferTx = Tx.parseTx(inTxData);
+
+    // check if this tx is included or not
+    // TxLib.Tx memory includedTx = checkForValidityAndInclusion(blockNumber, includedTxData, includedProof);
+
+    // not a valid tx because tx is included in the chain
+    // will block whole tx from exiitng
+    limboExit.isValid = false;
+    // payments?
+  }
+
+  function challengeLimboExitByInputSpend(
+    bytes32 exitId,
+    bytes inTxData, uint8 inInputNo,
+    bytes includedTxData, bytes includedProof, uint8 includedInputNo, uint32 blockNumber) 
+    public payable {
+    require(msg.value >= challengeStake, "Not enough ether sent to challenge exit");
+    LimboExit memory limboExit = limboExits[exitId];
+    bytes32 inTxHash = keccak256(inTxdata);
+
+    require(limboExit.txHash == inTxHash);
+    require(limboExit.isValid == true);
+    require(block.timestamp <= limboExit.timePublished + LimboChallangesDelay);
+
+    TxLib.Tx memory transferTx = Tx.parseTx(inTxData);
+    TxLib.Tx memory includedTx = checkForValidityAndInclusion(blockNumber, includedTxData, includedProof);
+
+    require(transferTx.sender == includedTx.sender);
+    TxLib.Input memory exitingInput = transferTx.inputs[inInputNo];
+    TxLib.Input memory includedInput = includedTx.inputs[includedInputNo];
+    require(exitingInput.blockNumber == includedInput.blockNumber);
+    require(exitingInput.amount == includedInput.amount);
+
+    // not a valid tx because canonical
+    // will block spent inputs from exiitng
+    limboExit.isValid = false;
+    // payments?
+  }
+
+  function challengeLimboExitByOutputSpend(
+    bytes32 exitId,
+    bytes inTxData, uint8 inOutputNo,
+    bytes includedTxData, bytes includedProof, uint8 includedInputNo, uint32 blockNumber) 
+    public payable {
+    require(msg.value >= challengeStake, "Not enough ether sent to challenge exit");
+    LimboExit memory limboExit = limboExits[exitId];
+    bytes32 inTxHash = keccak256(inTxdata);
+
+    require(limboExit.txHash == inTxHash);
+    require(limboExit.isValid == true);
+    require(block.timestamp <= limboExit.timePublished + LimboChallangesDelay);
+
+    TxLib.Tx memory transferTx = Tx.parseTx(inTxData);
+    TxLib.Tx memory includedTx = checkForValidityAndInclusion(blockNumber, includedTxData, includedProof);
+
+    require(transferTx.sender == includedTx.sender);
+
+    // which piggybacked output of exit
+    TxLib.Input memory exitingOutput = transferTx.outputs[inOutputNo];
+    TxLib.Input memory includedInput = includedTx.inputs[includedInputNo];
+    require(exitingInput.blockNumber == includedInput.blockNumber);
+    require(exitingOutput.amount == includedInput.amount);
+
+    // not a valid tx because not exitable
+    // will block spent outputs from exiitng
+    limboExit.isValid = false;
+    // payments?
+  }
+
+  function challengeLimboExitByNonCanonicalInput(
+    bytes32 exitId,
+    bytes inTxData, uint8 inInputNo,
+    bytes includedTxData, bytes includedProof, uint8 includedOutputNo, uint32 blockNumber) 
+    public payable {
+    require(msg.value >= challengeStake, "Not enough ether sent to challenge exit");
+    LimboExit memory limboExit = limboExits[exitId];
+    bytes32 inTxHash = keccak256(inTxdata);
+
+    require(limboExit.txHash == inTxHash);
+    require(limboExit.isValid == true);
+    require(block.timestamp <= limboExit.timePublished + LimboChallangesDelay);
+
+    TxLib.Tx memory transferTx = Tx.parseTx(inTxData);
+    TxLib.Tx memory includedTx = checkForValidityAndInclusion(blockNumber, includedTxData, includedProof);
+
+    require(transferTx.sender == includedTx.sender);
+
+    // which piggybacked input of exit
+    TxLib.Input memory exitingIntput = transferTx.inputs[inIntputNo];
+    TxLib.Output memory includedOutput = includedTx.outputs[includedOutputNo];
+    require(exitingInput.blockNumber == includedInput.blockNumber);
+    require(exitingOutput.amount == includedInput.amount);
+
+    // not a valid tx because input was not created by a canonical tx
+    // will block non canonical inputs from exiitng
+    limboExit.isValid = false;
+    // payments?
+  }
+
+  function resolveChallengeOnLimbo(
+    bytes32 exitId, bytes inTxData, uint256 challengeNo,
+    bytes includedTxData, bytes includedProof, uint8 includedOutputNo, uint32 blockNumber
+  ) public {
+
+    LimboExit memory limboExit = limboExits[exitId];
+    LimboChallenge memory challenge = limboExit.challenge[challengeNo];
+
+    bytes32 inTxHash = keccak256(inTxdata);
+    require(limboExit.isValid == true);
+
+    TxLib.Tx memory exitingTx = Tx.parseTx(inTxData);
+    TxLib.Input memory exitingInput = exitingTx.input[challenge.inputNo];
+
+    // check for validity and inclusion?
+    challenge.resolved = true;
+  }
+
+  function finalizeTopLimboExit(uint16 _color) public {
+    bytes32 utxoId;
+    uint256 exitableAt;
+    (utxoId, exitableAt) = getNextExit(_color);
+
+    require(exitableAt <= block.timestamp, "The top exit can not be exited yet");
+    require(tokens[_color].currentSize > 0, "The exit queue for color is empty");
+
+    LimboExit memory currentExit = limboExits[utxoId];
+    if (limboExit.isValid == true){
+      // assuming 1 output
+      LimboOut memory out = limboExit.output[0];
+      uint256 amount;
+      if (out.exitable){
+        amount = limboExit.stake + piggybackStake;
+        tokens[out.color].addr.transferFrom(address(this), out.owner, amount);
+      } else {
+        limboExit.isValid = false;
+      }
+    }
+    delete limboExits[utxoId];
+  }
+
   function startExit(
     bytes32[] memory _youngestInputProof, bytes32[] memory _proof,
     uint8 _outputIndex, uint8 _inputIndex
diff --git a/test/exitHandler.js b/test/exitHandler.js
index 8834742..e8d9cee 100644
--- a/test/exitHandler.js
+++ b/test/exitHandler.js
@@ -147,6 +147,111 @@ contract('ExitHandler', (accounts) => {
         assert(aliceBalanceBefore.add(new BN(50)).eq(aliceBalanceAfter));
       });
 
+      it('Should allow to challenge tx and prevent exit', async () => {
+        const period = await submitNewPeriod([depositTx, transferTx]);
+        const transferProof = period.proof(transferTx);
+
+        //bob spends utxo1 by sending it to charlie
+        const spendTx = Tx.transfer(
+          [new Input(new Outpoint(transferTx.hash(), 0))],
+          [new Output(50, charlie)]
+        ).sign([bobPriv]);
+
+        const exitStake = exitHandler.exitStake();
+        const piggybackStake = exitHandler.piggybackStake();
+        const challengeStake = exitHandler.challengeStake();
+
+        const inTxData = Tx.parseToParams(transferTx);
+        const spendTxData = Tx.parseToParams(spendTx);
+
+        // any user can start the exit for this transaction
+        exitId = await exitHandler.startLimboExit(inTxData, {from: bob, value: exitStake});
+
+        // Bob piggybacks and joins a Limo exit by its id
+        await exitHandler.joinLimboExit(exitId, 0, {from: bob, value: piggybackStake});
+
+        const bobBalanceBefore = await nativeToken.balanceOf(bob);
+        await exitHandler.challengeLimboExit(exitId, spendTxData, {from: pete, value: challengeStake});
+
+        const challengeTime = (await time.latest()) + (2 * time.duration.seconds(limboPeriod/2));
+        await time.increaseTo(challengeTime);
+
+        await exitHandler.finalizeTopLimboExit(nativeTokenColor);
+
+        // const responseTime = (await time.latest()) + (2 * time.duration.seconds(limboPeriod/2));
+        // await time.increaseTo(responseTime);
+
+        const bobBalanceAfter = await nativeToken.balanceOf(bob);
+        assert(bobBalanceAfter.eq(bobBalanceBefore));
+      });
+
+      it('Should allow user to challenge double spent inflight tx', async () => {
+        // const period = await submitNewPeriod([depositTx);
+
+        //alice double spends utxo1 by sending it to charlie
+        const spendTx = Tx.transfer(
+          [new Input(new Outpoint(depositTx.hash(), 0))],
+          [new Output(50, charlie)]
+        ).sign([alicePriv]);
+
+        const exitStake = exitHandler.exitStake();
+        const piggybackStake = exitHandler.piggybackStake();
+        const challengeStake = exitHandler.challengeStake();
+
+        const inTxData = Tx.parseToParams(transferTx);
+        const spendTxData = Tx.parseToParams(spendTx);
+
+        exitId = await exitHandler.startLimboExit(inTxData, {from: bob, value: exitStake});
+
+        // Bob piggybacks and joins a Limo exit by its id
+        await exitHandler.joinLimboExit(exitId, 0, {from: bob, value: piggybackStake});
+
+        const bobBalanceBefore = await nativeToken.balanceOf(bob);
+        // challenge to tx's canonicity
+        await exitHandler.challengeLimboExit(exitId, spendTxData, {from: pete, value: challengeStake});
+
+        const challengeTime = (await time.latest()) + (2 * time.duration.seconds(limboPeriod/2));
+        await time.increaseTo(challengeTime);
+
+        await exitHandler.finalizeTopLimboExit(nativeTokenColor);
+
+        // const responseTime = (await time.latest()) + (2 * time.duration.seconds(limboPeriod/2));
+        // await time.increaseTo(responseTime);
+
+        const bobBalanceAfter = await nativeToken.balanceOf(bob);
+        assert(bobBalanceAfter.eq(bobBalanceBefore));
+      });
+
+      it('Should resolve a challenge and exit', async () => {
+        const period = await submitNewPeriod([depositTx);
+        const depositProof = period.proof(depositTx);
+
+        const exitStake = exitHandler.exitStake();
+        const piggybackStake = exitHandler.piggybackStake();
+        const challengeStake = exitHandler.challengeStake();
+
+        const inTxData = Tx.parseToParams(transferTx);
+
+        exitId = await exitHandler.startLimboExit(inTxData, {from: bob, value: exitStake});
+
+        // Alice piggybacks and joins a Limo exit by its id
+        await exitHandler.joinLimboExit(exitId, 0, {from: alice, value: piggybackStake});
+
+        const aliceBalanceBefore = await nativeToken.balanceOf(alice);
+        await exitHandler.challengeLimboExit(exitId, randomTx, {from: pete, value: challengeStake});
+
+        const challengeTime = (await time.latest()) + (2 * time.duration.seconds(limboPeriod/2));
+        await time.increaseTo(challengeTime);
+
+        await exitHandler.resolveChallengeOnInput(exitId, inTxData, 0, depositData, depositProof, 0, blockNo);
+
+        const responseTime = (await time.latest()) + (2 * time.duration.seconds(limboPeriod/2));
+        await time.increaseTo(responseTime);
+
+        const aliceBalanceAfter = await nativeToken.balanceOf(alice);
+        assert(aliceBalanceAfter.eq(aliceBalanceBefore.add(new BN(50))));
+      });
+
       it('Should allow to exit deposit utxo', async () => {
         const period = await submitNewPeriod([depositTx]);