Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 78 additions & 0 deletions docs/bridge-safety.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# Cross-Chain Bridge and Message Verification Safety

ChainProof's bridge analysis engine (`CP-BRG-001` through `CP-BRG-016`) models cross-chain bridges and message verification contracts for structural security vulnerabilities.

## Threat model

The analyzer assumes:

- Relayers, bridges, or transport layers may redeliver messages
- Validator signatures may be duplicated, unsorted, or include zero addresses
- Message payloads can be attacker-controlled
- Source chain reorgs and optimistic fraud are in scope

The analyzer does **not** prove transport-layer authenticity, oracle correctness, or live-network finality.

## Rules

| Rule | Category | Description |
|------|----------|-------------|
| CP-BRG-001 | domain-separation | Missing source chain binding on inbound messages |
| CP-BRG-002 | domain-separation | Missing destination binding on outbound messages |
| CP-BRG-003 | replay-protection | Replayable messages without nonce/ID consumption |
| CP-BRG-004 | replay-protection | Weak nonce management |
| CP-BRG-005 | validator-governance | Unsafe validator/threshold updates |
| CP-BRG-006 | verification | Proof/signature verification bypass |
| CP-BRG-007 | verification | Duplicate validators in proof loop |
| CP-BRG-008 | verification | Unsorted validator set |
| CP-BRG-009 | verification | Zero-address validator not rejected |
| CP-BRG-010 | verification | Stale Merkle/state root acceptance |
| CP-BRG-011 | validator-governance | Unsafe quorum arithmetic |
| CP-BRG-012 | payload-execution | Unvalidated payload arbitrary execution |
| CP-BRG-013 | token-bridge | Mint without verified lock |
| CP-BRG-014 | token-bridge | Release without verified burn |
| CP-BRG-015 | finality | Missing finality/challenge window |
| CP-BRG-016 | operational-safety | Missing pause/rate-limit mitigations |

## Usage

### CLI

```bash
chainproof bridge contracts/bridge/ --format markdown
chainproof bridge contracts/ --include-rule CP-BRG-003 --fail-on critical
```

### API

```typescript
import { analyzeBridgeSource, analyzeBridgeFiles } from '@chainproof/core';

const report = analyzeBridgeSource(source, 'Bridge.sol');
const files = analyzeBridgeFiles(['contracts/bridge/'], { includeModels: true });
```

## Configuration

Versioned configuration schema (`schemaVersion: 1`):

```json
{
"schemaVersion": 1,
"limits": { "maxFindings": 512 },
"includeRules": ["CP-BRG-001", "CP-BRG-003"],
"excludeRules": ["CP-BRG-016"]
}
```

## Limitations

- Static analysis only; no live-network monitoring
- Does not duplicate AI multi-contract analysis (#61)
- Framework adapters provide hints, not proofs of correctness

## Troubleshooting

- **No findings on obvious bridge**: Ensure the contract contains bridge signals (e.g. `receiveMessage`, `processedMessages`, `sourceChainId`)
- **False positives on trusted relayer paths**: Use `--exclude-rule` or document relayer authentication in code comments
- **Truncated output**: Increase `maxFindings` in configuration
47 changes: 47 additions & 0 deletions examples/contracts/bridge/SecureBurnReleaseBridge.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

/// @notice Secure burn-release bridge with proof verification and finality window.
contract SecureBurnReleaseBridge {
mapping(bytes32 => bool) public processedMessages;
mapping(bytes32 => uint256) public messageReceivedAt;
uint256 public totalBurned;
uint256 public totalReleased;
bytes32 public merkleRoot;
uint256 public rootUpdatedAt;
uint256 public finalityWindow = 86400;
address public token;

function receiveMessage(bytes32 messageId, bytes32 root, bytes calldata proof) external {
require(root == merkleRoot, "stale root");
require(block.timestamp >= rootUpdatedAt, "root not ready");
require(!processedMessages[messageId], "replay");
verifyProof(proof);
processedMessages[messageId] = true;
messageReceivedAt[messageId] = block.timestamp;
}

function releaseTokens(bytes32 messageId, address to, uint256 amount) external {
require(processedMessages[messageId], "not received");
require(block.timestamp >= messageReceivedAt[messageId] + finalityWindow, "finality");
require(amount <= totalBurned - totalReleased, "exceeds burn");
(bool ok,) = token.call(abi.encodeWithSignature("transfer(address,uint256)", to, amount));
require(ok);
totalReleased += amount;
}

function burnTokens(uint256 amount) external {
totalBurned += amount;
}

function verifyProof(bytes calldata proof) internal view {
require(proof.length > 0, "empty proof");
require(block.timestamp >= rootUpdatedAt, "root timestamp");
merkleRoot;
}

function updateRoot(bytes32 newRoot) external {
merkleRoot = newRoot;
rootUpdatedAt = block.timestamp;
}
}
64 changes: 64 additions & 0 deletions examples/contracts/bridge/SecureLockMintBridge.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

interface IMintable {
function mint(address to, uint256 amount) external;
}

/// @notice Secure lock-mint bridge with domain binding, replay protection, and lock verification.
contract SecureLockMintBridge {
mapping(bytes32 => bool) public processedMessages;
mapping(uint256 => uint256) public inboundNonce;
uint256 public totalLocked;
uint256 public totalMinted;
IMintable public wrappedToken;
address public trustedRelayer;
uint256 public sourceChainId;
uint256 public validatorThreshold;
bool public paused;

constructor(address token, address relayer, uint256 _sourceChain) {
wrappedToken = IMintable(token);
trustedRelayer = relayer;
sourceChainId = _sourceChain;
validatorThreshold = 3;
}

modifier whenNotPaused() {
require(!paused, "paused");
_;
}

function lockTokens(address, uint256 amount) external {
totalLocked += amount;
}

function mintTokens(address to, uint256 amount) external whenNotPaused {
require(amount <= totalLocked - totalMinted, "exceeds lock");
wrappedToken.mint(to, amount);
totalMinted += amount;
}

function receiveMessage(
bytes32 messageId,
uint256 originChain,
uint256 nonce,
address to,
uint256 amount
) external whenNotPaused {
require(msg.sender == trustedRelayer, "untrusted");
require(originChain == sourceChainId, "wrong source");
require(!processedMessages[messageId], "replay");
require(nonce == inboundNonce[originChain] + 1, "bad nonce");
require(amount <= totalLocked - totalMinted, "exceeds lock");

processedMessages[messageId] = true;
inboundNonce[originChain] = nonce;

mintTokens(to, amount);
}

function pause() external {
paused = true;
}
}
28 changes: 28 additions & 0 deletions examples/contracts/bridge/VulnerableBurnReleaseBridge.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

/// @notice Vulnerable burn-release bridge without burn verification and with weak validator loop.
contract VulnerableBurnReleaseBridge {
mapping(bytes32 => bool) public executedMessages;
uint256 public totalBurned;
uint256 public totalReleased;
address public token;

function releaseTokens(bytes32 messageId, address to, uint256 amount) external {
messageId;
(bool ok,) = token.call(abi.encodeWithSignature("transfer(address,uint256)", to, amount));
require(ok);
totalReleased += amount;
}

function verifyValidators(address[] calldata validators, bytes[] calldata sigs) external pure {
for (uint256 i = 0; i < sigs.length; i++) {
validators[i];
}
}

function sendMessage(uint256 destChainId, bytes calldata payload) external {
destChainId;
payload;
}
}
43 changes: 43 additions & 0 deletions examples/contracts/bridge/VulnerableLockMintBridge.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

interface IMintable {
function mint(address to, uint256 amount) external;
}

/// @notice Vulnerable lock-mint bridge lacking replay protection, source binding, and lock verification.
contract VulnerableLockMintBridge {
mapping(bytes32 => bool) public processedMessages;
uint256 public totalMinted;
IMintable public wrappedToken;
address public relayer;

constructor(address token, address _relayer) {
wrappedToken = IMintable(token);
relayer = _relayer;
}

function receiveMessage(bytes32 messageId, address to, uint256 amount, bytes calldata) external {
require(msg.sender == relayer, "untrusted");
wrappedToken.mint(to, amount);
totalMinted += amount;
}

function executeMessage(bytes32, address target, bytes calldata data) external {
(bool ok,) = target.call(data);
require(ok);
}

function updateThreshold(uint256 newThreshold) external {
// no bounds check, instant update
newThreshold;
}

function verifySignatures(address[] calldata signers, bytes[] calldata) external pure returns (bool) {
uint256 count;
for (uint256 i = 0; i < signers.length; i++) {
count++;
}
return count >= 1;
}
}
2 changes: 2 additions & 0 deletions packages/cli/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import { registerWatchCommand } from "./commands/watch";
import { registerInvariantsCommand } from "./commands/invariants";
import { registerStakingCommand } from "./commands/staking";
import { registerGovernanceCommand } from "./commands/governance";
import { registerBridgeCommand } from "./commands/bridge";

// ─── ASCII Banner ─────────────────────────────────────────────────────────────

Expand Down Expand Up @@ -631,5 +632,6 @@ registerWatchCommand(program, printBanner);
registerInvariantsCommand(program, printBanner);
registerStakingCommand(program);
registerGovernanceCommand(program, printBanner);
registerBridgeCommand(program, printBanner);

program.parse();
Loading