Xcosfc
Bitcoin

Examining the Risks in Relay Protocol’s Bitcoin Settlement Mechanism

Published Aug 27, 2026 Reads 619 By Christopher Johnson

Relay Protocol's simplistic Bitcoin transaction method hides complexities that may expose users to manipulation risks. Understand its settlement flaws.

**Understanding Relay Protocol’s Settlement Mechanism** The Relay Protocol employs a unique but potentially risky approach to manage Bitcoin transactions. At first glance, its Settlement Protocol appears straightforward, ostensibly relying on a simple mechanism to transfer Bitcoin deposits from user-specific addresses into Relay's holdings. This simplicity—characterized by a single input and output, a straightforward OP_RETURN identifier, and just one multi-party computation (MPC) signature—can mislead observers into underestimating the complexities involved. What’s more concerning lies beneath this surface-level simplicity. The protocol relies on an older Bitcoin signing methodology known as legacy Pay-to-Public-Key-Hash (P2PKH). This method features a significant flaw: the
SIGHASH_ALL signature does not include the amount of the Bitcoin being spent. In situations where the signer's identity and context regarding the transaction are obvious, this could be manageable. However, issues arise when the signer is an agnostic NEAR Chain Signatures contract, which only engages with a 32-byte hash. This design opens the proverbial doors to manipulation. The permissionless nature of the sweep() function, which directly uses a UTXO value extracted from user-supplied calldata, creates a clear pathway for would-be attackers. They could initiate a transaction that reports a far lower value for a confirmed deposit than its actual worth. In effect, they would coax the MPC to sign for this misleadingly smaller hash value and broadcast it. Bitcoin’s network would validate the transaction as legitimate, approving a tiny output while chalking up the remaining difference as a miner's fee. The crux of the problem, then, isn't that Relay is mistakenly signing erroneous transactions; rather, it’s effectively signing exactly what it’s asked to. The absence of an amount field leads to a fundamental misalignment between what Relay presupposes the transaction entails and what the Bitcoin network recognizes as its true context.

The Technical Underpinnings

If you’re already familiar with Bitcoin's script types and have a handle on BIP-143 specifications, you might want to skip ahead to the [Root Cause](https://www.zellic.io/blog/signing-blind#the-eight-bytes-that-arent-there) for a deeper dive.

Bitcoin UTXO Explained

- **Unspent Transaction Outputs (UTXO)**: In Bitcoin, there's no concept of accounts. Instead, your balance is effectively the sum of various unspent transaction outputs, each linked to a specific script. The UTXO set is publicly accessible and can be queried from any full node, including those managing deposits within this protocol. - **Signature Types**: The incident hinges on a critical question: Does the signature commit to the input’s amount? The difference between P2PKH, Pay-to-Witness-Public-Key-Hash (P2WPKH), and Pay-to-Taproot (P2TR) makes a significant impact on this aspect. | Script | Year | Signature | Commits to input amount? | |---------|-------|-----------|--------------------------| | P2PKH | ~2009 | ECDSA | **No** | | P2WPKH | 2017 | ECDSA | Yes, via [BIP-143](https://github.com/bitcoin/bips/blob/master/bip-0143.mediawiki) | | P2TR | 2021 | Schnorr | Yes, via BIP-341 | - **Aurora and Chain Signatures**: Aurora serves as NEAR's execution layer compatible with Ethereum Virtual Machine (EVM), where its native Chain Signatures contracts operate. This framework supports an MPC-based signing oracle, which generates valid ECDSA signatures based on a given 32-byte hash and a derivation path.

Permissionless by Nature

The structure of Relay relies on a specific deposit address each time a new Bitcoin transaction is settled via NEAR’s MPC. Users direct BTC to this unique address, and once the deposit is confirmed, any entity can invoke the sweep() function with the relevant UTXO and associated fees. The resulting transaction is crafted to funnel the deposit—minus transaction fees—into Relay’s main depository while logging the corresponding order ID in an OP_RETURN output. The NEAR MPC then signs off on the transaction, ready for broadcasting by the caller. However, it’s essential to highlight that Aurora lacks visibility into the Bitcoin UTXO set, necessitating that the sweep be executed off-chain. The caller must read the UTXO specific to the designated deposit address and relay that information to the contract as calldata. Subsequently, the contract assembles the transaction, computes the hash, and solicits a signature from NEAR MPC, which the caller ultimately broadcasts. For a clearer picture of how the payload is constructed, let’s look at the corresponding entry function:

When examining the verification mechanism in play, three key checks take center stage. The contract compares the feeRate against a ceiling defined by maxFeeRate, ensures that the declared UTXO value meets the fee requirements (InsufficientUTXOValue), and confirms that the amount swept exceeds a certain dust threshold (SweepAmountBelowDust). But here's the catch: there's a critical verification missing.

The fundamental flaw is the lack of a check to confirm whether utxo.value accurately reflects the real UTXO value linked to utxo.txid and utxo.index. Aurora operates without any visibility into Bitcoin’s complete UTXO database, and any data received comes directly from potentially untrustworthy inputs.

How Bitcoin Handles Validation​

On the Bitcoin network, the validation process is far more thorough. For each UTXO being accessed, a node retrieves the outpoint (prev_txid, prev_index) from its own UTXO database. This allows it to validate the locking script required for spending and determine the actual value of the output in satoshis.

Take, for example, a traditional P2PKH transaction. There are two significant checks that need to occur. Initially, the node reconstructs the legacy sighash preimage for the input and evaluates the ECDSA signature against its double-SHA-256 digest. The second vital step involves economics: the cumulative value of the consumed UTXOs—which the node reads from its own set—must equal or exceed the new output values. Any remainder from the total value is considered the fee.

However, there’s a disparity here that needs addressing. The term “fee” tends to be used interchangeably, yet it can refer to two distinct quantities:

  • Contract Sizing Fee: This is calculated as feeRate * SWEEP_TX_SIZE, purely used to determine the size of outputs[0]. This is confined within the limit set by maxFeeRate.
  • Realized Bitcoin Fee: This is the result of the formula sum(inputs) - sum(outputs), derived from the node’s own UTXO valuations, and is not constrained by anything in the contract.

This distinction is pivotal: while Relay bases output sizes purely on the caller's specified values, Bitcoin itself assesses the transaction against actual UTXO values. This difference could spell trouble.

The Missing Eight Bytes​

A closer look reveals that the fault lies not exclusively with either party, but rather at the intersection of their processes. The preimage generation adheres meticulously to legacy specifications:

function buildPreImageForInput(
BitcoinTransactionData memory txData,
uint256 whichInput
) internal pure returns (bytes memory) {
bytes memory versionLE = Utils.encodeUint32LE(1);
bytes memory inputCountLE = encodeVarInt(txData.inputs.length);

bytes memory allInputs;
for (uint256 i = 0; i < txData.inputs.length; i++) {
bytes memory prevTxidLe = txData.inputs[i].txid;
bytes memory prevIndexLe = txData.inputs[i].index;
// scriptSig is the prevout scriptPubKey for the input being signed, empty otherwise
// ...
bytes memory sequenceLE = Utils.encodeUint32LE(0xFFFFFFFD);
allInputs = bytes.concat(
allInputs,
prevTxidLe, // which outpoint
prevIndexLe, // which index
scriptSigLen,
scriptSigBytes,
sequenceLE // that's it: txData.inputs[i].value is never serialized
);
}

// ...outputs, locktime, hashType (0x01 = SIGHASH_ALL)...
}

Despite each BitcoinTransactionDataInput containing a .value attribute, the serializer fails to extract this information. It only includes components such as the outpoint, the scriptSig, and the sequence. The actual amount is not factored into the preimage.

The layers of the issue can be described as follows:

  1. A permissionless access point (sweep → buildSweepPayload) that accepts an externally supplied utxo.value without verifying it against the actual Bitcoin UTXO data.
  2. A legacy SIGHASH_ALL preimage that omits crucial input amounts, as per established specifications.
  3. A generic ECDSA oracle (ChainSignatures) that signs any provided double-SHA-256 hash, lacking any knowledge of Bitcoin’s context.

If the communication was only one-way, with trusted relayers calling the shots, the risk would be minimized. However, sweep() is designed to be permissionless by intent, as highlighted in the NatSpec comments:

// contracts/BitcoinDepositAddress.sol (sweep, L133-155)
/// @dev Permissionless — anyone can call. Re-callable after the pending signature
/// cooldown expires to prevent griefing via low gas settings.
function sweep(
bytes32 orderId,
bytes calldata data,
GasSettings calldata gasSettings
) external {

// ...builds the payload, hashes it, and requests the MPC signature...

}
The security implications of this mechanism are significant. Here’s the crux: while attackers can misrepresent the value of a UTXO, they can't dictate where the outputs will go. The outputs generated are predetermined by contract code, not by whatever deceitful values an attacker might feed in. This means no matter how clever an exploitation attempt is, the value doesn't go to a personal wallet—it effectively disappears. Why, then, would an attacker choose this path over directing funds to their own address? Simply put, that option isn’t available to them within the confines of the contract's logic. The two outputs defined in the contract make this happen: the first is dedicated to a depository script, while the second returns a zero value as OP_RETURN. The attacker’s last resort for profit involves manipulating the fee—by causing the deposit to dissipate as transaction fees. In this scenario, the only one who stands to gain is the miner, whether they’re a malicious actor themselves or simply in collusion with one. What's left for everyone else is effectively ash—no assets left behind. Now, let's pivot to a potential proof of concept. Interestingly, there’s an opportunity to demonstrate that Multi-Party Computation (MPC) can sign the same digest regardless of whether an input is claimed to hold 10,000 sats or 1,000,000. If both declarations yield equal digests, then a singular ECDSA signature suffices for both, revealing a critical flaw: the input value was never actually part of the contract promise. In a couple of Foundry tests, this flaw becomes even clearer. Both tests set out to create identical transactions but differ only in the values they claim for the inputs. This direct comparison not only validates the existence of this exploit but also raises inevitable questions about the robustness of the systems relying on these contracts. As we look ahead, the industry must grapple with the implications of these vulnerabilities. If contracts themselves can be sidestepped by clever input manipulation, it raises a larger question: how can we fortify trust in decentralized systems that are supposed to be self-governing? Addressing these loopholes isn't just about patching up code; it's about redefining trust in the blockchain space. If you're working with smart contracts, now's the time to re-evaluate their designs and your assumptions about security. The stakes are higher than they seem at first glance, and ensuring integrity is more vital than ever.
Source: Christopher Johnson · www.zellic.io

Discussion

Sign in to join the discussion.