HongCoin Refund Recovery: Payload Analysis of the 2016 ICO Unlock

Technical scope: This is a case reconstruction of an authorized HongCoin recovery path. The payload analysis explains verified calldata and integer overflow behavior, but the management function was gated by the HongCoin management wallet. This is not a public execution path for ordinary users.

In early June 2026, a white-hat recovery claim around the old HongCoin ICO contract drew attention because it was not a standard wallet rescue, phishing cleanup, or private-key recovery. The claim was sharper: 1,003.62 ETH, reportedly trapped for nine years in a 2016 ICO contract, became claimable again for the original investors after a controlled on-chain recovery path.

The interesting part is the mechanism. The relevant HONG contract was verified on Etherscan, compiled with Solidity v0.3.5, and still exposed the original ICO refund and management functions. The recovery did not rely on changing the contract code. It used an authorized management path to modify each affected holder's token balance in a way that made the old refund check pass.

This is a technical reconstruction of what can be verified from the contract source, ABI, and transactions. It complements our broader explanation of what smart contract recovery can and cannot do: recovery is possible only when a real on-chain path, permissions, and state conditions line up.

The Addresses Involved

The main HONG ICO contract is:

0x9fa8fa61a10ff892e4ebceb7f4e0fc684c2ce0a9

The visible unlock operator address is:

0x1212ce5652b20c0a8ce493458a5c99db24ed2925

The management wallet configured in the HONG contract constructor and public state is:

0xb79ab5993cef2e0b714a66f3eda73b55de812d31

That management address is not just a random EOA. Its verified source is a classic Parity/Gav Wood style Wallet contract with execute(address _to, uint _value, bytes _data). The visible unlock transactions from 0x1212... first call this wallet, and the wallet then calls the HONG contract. That distinction matters because the HONG contract checks msg.sender for management-only functions.

Why the Refund Path Was Stuck

The HONG contract contains a normal-looking ICO refund function:

function refundMyIcoInvestment() noEther notLocked onlyTokenHolders {
    if (weiGiven[msg.sender] == 0) {
        doThrow("noWeiGiven");
        return;
    }
    if (balances[msg.sender] > tokensCreated) {
        doThrow("invalidTokenCount");
        return;
     }

    var tmpWeiGiven = weiGiven[msg.sender];
    var tmpTaxPaidBySender = taxPaid[msg.sender];
    var tmpSenderBalance = balances[msg.sender];

    balances[msg.sender] = 0;
    weiGiven[msg.sender] = 0;
    taxPaid[msg.sender] = 0;
    tokensCreated -= tmpSenderBalance;

    extraBalanceWallet.returnAmountToMainAccount(tmpTaxPaidBySender);

    if (!msg.sender.send(tmpWeiGiven)) {
        evRefund(msg.sender, msg.value, msg.sender, tmpWeiGiven, false);
        doThrow("refund:SendFailed");
        return;
    }

    evRefund(msg.sender, msg.value, msg.sender, tmpWeiGiven, true);
}

Three preconditions explain why this function could become inaccessible for some investors:

CheckWhat it means
notLockedThe fund must not have entered the locked state.
onlyTokenHoldersbalanceOf(msg.sender) must be greater than zero.
balances[msg.sender] <= tokensCreatedThe caller's local token balance must not exceed the global created-token count.

The public claim says the ICO did not reach its funding goal, but the contract held the investors' ETH and was supposed to auto-refund them. The verified source supports the existence of a refund route, but it also shows that refund eligibility was not based only on weiGiven[msg.sender]. The caller also had to remain a token holder and pass the balances[msg.sender] > tokensCreated sanity check.

That is the narrow place where funds can become technically stuck: the ETH accounting can still exist, while the token-side precondition blocks the refund.

The Management Function That Made Recovery Possible

The recovery path centers on mgmtIssueBountyToken(address,uint256):

function mgmtIssueBountyToken(
    address _recipientAddress,
    uint _amount
) noEther onlyManagementBody onlyCanIssueBountyToken(_amount) returns (bool){
    balances[_recipientAddress] += _amount;
    bountyTokensCreated += _amount;

    evMgmtIssueBountyToken(msg.sender, msg.value, _recipientAddress, _amount, true);
}

The function is protected by onlyManagementBody:

modifier onlyManagementBody {
    if(msg.sender != address(managementBodyAddress)) {doThrow("onlyManagementBody");} else {_}
}

So an arbitrary address cannot simply call it on the HONG contract. It has to be called by the configured management body address, which is the management wallet.

The quota check is also visible:

modifier onlyCanIssueBountyToken(uint _amount) {
    if (bountyTokensCreated + _amount > maxBountyTokens){
        doThrow("hitMaxBounty");
    }
    else {_}
}

In Solidity 0.3.5, unsigned integer arithmetic does not automatically revert on overflow. If bountyTokensCreated + _amount wraps around, the comparison can be made against the wrapped value rather than the mathematical sum. The function then adds _amount to the recipient's balance. If _amount is chosen so the recipient's existing balance wraps into a small positive value, the holder can become eligible for onlyTokenHolders while no longer tripping the refund guard.

The Payload Mechanics

The unlock payload is the technical core of the recovery. Each unlock transaction is not a direct call from the operator to mgmtIssueBountyToken(...). It is a nested call through the management wallet:

Wallet.execute(
  _to    = HONG contract,
  _value = 0,
  _data  = HONG.mgmtIssueBountyToken(holder, amount)
)

That outer call uses the wallet selector:

execute(address,uint256,bytes) -> 0xb61d27f6

The inner HONG call uses the management function selector:

mgmtIssueBountyToken(address,uint256) -> 0xfea5d7b1

One visible unlock transaction from 0x1212... decodes to this structure:

outer target:  0xb79ab5993cef2e0b714a66f3eda73b55de812d31  // management wallet
outer method:  execute(address,uint256,bytes)

execute._to:    0x9fa8fa61a10ff892e4ebceb7f4e0fc684c2ce0a9  // HONG
execute._value: 0
execute._data:  0xfea5d7b1
                0000000000000000000000001212ce5652b20c0a8ce493458a5c99db24ed2925
                fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe792f

The inner _data is exactly 68 bytes before ABI padding:

4 bytes   function selector: 0xfea5d7b1
32 bytes  recipient address: 0x1212ce5652b20c0a8ce493458a5c99db24ed2925
32 bytes  amount:            0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffe792f

That amount is not a normal bounty amount. It is a uint256 value close to 2^256:

0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffe792f
= 2^256 - 100049

In a checked-arithmetic language, adding this value would revert or require explicit wrapping. In Solidity 0.3.5, uint256 addition wraps modulo 2^256.

newBalance = (oldBalance + amount) mod 2^256
amount = 2^256 - delta
newBalance = oldBalance - delta

For the decoded example above, delta = 100049. If the holder's old balance was 100050, the wrapped addition would leave a balance of 1. That small positive result is important. The refund path does not only need the original ETH contribution in weiGiven[msg.sender]; it also needs onlyTokenHolders to pass. A balance of 0 would fail. A balance greater than tokensCreated would fail. A balance of 1 can satisfy both sides of the refund gate if the rest of the account state is consistent.

The same modular arithmetic also affects bountyTokensCreated:

bountyTokensCreated += _amount;

That means the payload cannot be chosen from the recipient balance alone. It must also pass the quota modifier under the current global bounty-token state. The 41 unlock transactions are not interchangeable decorative calls. Their order, holder address, existing holder balance, and current bountyTokensCreated state all matter.

An analysis-only encoder for the nested calldata looks like this:

const HONG = "0x9fa8fa61a10ff892e4ebceb7f4e0fc684c2ce0a9";

const hong = new Interface([
  "function mgmtIssueBountyToken(address recipient, uint256 amount)"
]);

const wallet = new Interface([
  "function execute(address to, uint256 value, bytes data)"
]);

const amount = (1n << 256n) - delta;
const inner = hong.encodeFunctionData("mgmtIssueBountyToken", [holder, amount]);
const outer = wallet.encodeFunctionData("execute", [HONG, 0n, inner]);

This code explains the encoding shape. It is not sufficient to perform the recovery by itself. The outer call must be accepted by the management wallet, and the HONG contract will only accept the inner management call if msg.sender is the configured managementBodyAddress.

The On-Chain Sequence

The unlock address shows a repeated pattern. First, it sends a series of transactions to the management wallet:

0x1212... -> 0xb79a... execute(...)

Inside the calldata, the wallet targets the HONG contract and passes a call to mgmtIssueBountyToken(address _recipientAddress, uint256 _amount). The transaction list shows 41 such successful execute(...) calls from nonce 19 through nonce 59. Each one targets the HONG contract through the management wallet and carries a different recipient address and amount.

Then the same unlock address directly calls the refund function on the HONG contract:

0x1212... -> 0x9fa8... refundMyIcoInvestment()

For that transaction, the internal trace shows:

HONG contract -> 0x1212... : 0.5 ETH

That direct refund confirms the mechanism at a small scale: after the accounting adjustment, the HONG refund function could pass its own checks and send ETH back to the caller.

Other successful refunds are visible from the HONG contract as well. The internal traces include payouts such as 96 ETH, 25.04823308 ETH, 4 ETH, and 50 ETH to different addresses. Those traces match the broader claim that original investors could claim their funds after the unlock step, although this article does not independently verify the full 1,003.62 ETH total.

Why This Was a White-Hat Recovery, Not a Public Exploit Path

The most important boundary is authorization. The vulnerable arithmetic was inside a management-only function. The public could call refundMyIcoInvestment(), but the unlock step required mgmtIssueBountyToken(...), and that required msg.sender == managementBodyAddress.

authorized operator -> management wallet -> HONG management function -> investor refund path

That makes the recovery different from a free-for-all bug where any attacker can drain a contract. The bug mattered because it gave the legitimate management path a way to repair otherwise stuck holder accounting. Without the management wallet's participation, the same route should fail at onlyManagementBody.

What the Bug Teaches

This incident is a useful example of why old ICO contracts can remain recoverable even when they look abandoned or broken. It also shows why recovery analysis cannot stop at a single failed transaction.

LayerRecovery question
ICO accountingDoes weiGiven[address] still record the investor's original contribution?
Token balanceDoes balanceOf(address) satisfy onlyTokenHolders?
Global supplyDoes the holder balance pass balances[address] <= tokensCreated?
Management authorityCan the configured management body still execute authorized calls?
Arithmetic behaviorDoes the old compiler allow overflow instead of reverting?
Refund executionDoes send succeed with the recipient's address type and 2,300 gas stipend?

Several things should stay separate. The public claim says 1,003.62 ETH became recoverable; this on-chain review confirms the mechanism and several refund traces, but not the full total. The public claim also says 48 original investors can now claim; the contract can show holder and refund behavior, but the identities and off-chain coordination of all investors require additional evidence.

The HongCoin case is a strong example of technical recovery that is neither magic nor generic. The ETH was not recovered because someone found a private key or convinced a chain to reverse history. It became accessible because a very specific combination existed: verified source, intact refund records, an isolated token-accounting failure, a live management wallet, predictable old compiler behavior, and a team willing to execute a controlled recovery path on-chain.

For users, the lesson is the opposite of copy-paste experimentation. Do not try random function calls just because a contract exposes a refund-looking method. In the HONG contract, failed attempts to call transfer, mgmtDistribute, or refundMyIcoInvestment are visible on-chain. The successful route depended on understanding both the source code and the management authority. For evidence collection methods, compare this with how to use block explorers to track funds and how to read smart contract functions.

Sources Checked

Need a Contract Recovery Feasibility Review

Prepare the public contract address, wallet address, transaction hashes, chain, and a short timeline. Do not share seed phrases, private keys, wallet files, or remote access.

Request Case EvaluationReview Recovery Services

A review can identify callable paths and permission boundaries. It cannot guarantee that every old contract has a safe recovery route.


Related Resources

Last updated:

← Back to Blog