Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Migrate bridge related contracts from Clementine repo to here #332

Merged
merged 7 commits into from
Apr 3, 2024
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
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
[submodule "crates/evm/src/evm/system_contracts/lib/forge-std"]
path = crates/evm/src/evm/system_contracts/lib/forge-std
url = https://github.com/foundry-rs/forge-std
[submodule "contracts/lib/bitcoin-spv"]
path = crates/evm/src/evm/system_contracts/lib/bitcoin-spv
url = https://github.com/keep-network/bitcoin-spv
1 change: 1 addition & 0 deletions crates/evm/src/evm/system_contracts/lib/bitcoin-spv
Submodule bitcoin-spv added at 856849
139 changes: 139 additions & 0 deletions crates/evm/src/evm/system_contracts/src/Bridge.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

import "bitcoin-spv/solidity/contracts/ValidateSPV.sol";
import "bitcoin-spv/solidity/contracts/BTCUtils.sol";

import "./MerkleTree.sol";
import "./L1BlockHashList.sol";

/// @title Bridge contract of Clementine
/// @author Citrea

contract Bridge is MerkleTree, Ownable {
// TODO: Update this to be the actual address of the L1BlockHashList contract
L1BlockHashList public constant BLOCK_HASH_LIST = L1BlockHashList(address(0xdeaDDeADDEaDdeaDdEAddEADDEAdDeadDEADDEaD));

bytes public DEPOSIT_TXOUT_0 = hex"c2ddf50500000000225120fc6eb6fa4fd4ed1e8519a7edfa171eddcedfbd0e0be49b5e531ef36e7e66eb05";
uint256 public constant DEPOSIT_AMOUNT = 1 ether;
address public operator;
mapping(bytes32 => bool) public blockHashes;
mapping(bytes32 => bool) public spentTxIds;

event Deposit(bytes32 txId, uint256 timestamp);
event Withdrawal(bytes32 bitcoin_address, uint32 indexed leafIndex, uint256 timestamp);
event DepositTxOutUpdate(bytes oldExpectedVout0, bytes newExpectedVout0);
event BlockHashAdded(bytes32 block_hash);
event OperatorUpdated(address oldOperator, address newOperator);

modifier onlyOperator() {
require(msg.sender == operator, "caller is not the operator");
_;
}

constructor(uint32 _levels) MerkleTree(_levels) {}

/// @notice Sets the expected first transaction output of a deposit transaction on Bitcoin, which signifies the multisig address on Bitcoin
/// @dev TxOut0 is derived from the multisig on Bitcoin so it stays constant as long as the multisig doesn't change
/// @param _depositTxOut0 The new expected first transaction output of a deposit transaction on Bitcoin
function setDepositTxOut0(bytes calldata _depositTxOut0) external onlyOwner {
bytes memory oldDepositTxOut0 = DEPOSIT_TXOUT_0;
DEPOSIT_TXOUT_0 = _depositTxOut0;
emit DepositTxOutUpdate(oldDepositTxOut0, DEPOSIT_TXOUT_0);
}

/// @notice Checks if funds 1 BTC is sent to the bridge multisig on Bitcoin, and if so, sends 1 cBTC to the receiver
/// @param version The version of the Bitcoin transaction
/// @param vin The transaction inputs
/// @param vout The transaction outputs
/// @param locktime Locktime of the Bitcoin transaction
/// @param intermediate_nodes -
/// @param block_header Block header of the Bitcoin block that the deposit transaction is in
/// @param index Index of the transaction in the block
function deposit(
bytes4 version,
bytes calldata vin,
bytes calldata vout,
bytes4 locktime,
bytes calldata intermediate_nodes,
bytes calldata block_header,
uint256 block_height,
uint index
) external onlyOperator {
bytes32 block_hash = BTCUtils.hash256(block_header);
require(BLOCK_HASH_LIST.getBlockHash(block_height) == block_hash, "Incorrect block hash");

bytes32 extracted_merkle_root = BTCUtils.extractMerkleRootLE(block_header);
bytes32 txId = ValidateSPV.calculateTxId(version, vin, vout, locktime);
require(!spentTxIds[txId], "txId already spent");
spentTxIds[txId] = true;

bool result = ValidateSPV.prove(txId, extracted_merkle_root, intermediate_nodes, index);
require(result, "SPV Verification failed.");

// First output is always the bridge utxo, so it should be constant
bytes memory output1 = BTCUtils.extractOutputAtIndex(vout, 0);
require(isBytesEqual(output1, DEPOSIT_TXOUT_0), "Incorrect Deposit TxOut");

// Second output is the receiver of tokens
bytes memory output2 = BTCUtils.extractOutputAtIndex(vout, 1);
bytes memory output2_ext = BTCUtils.extractOpReturnData(output2);
address receiver = address(bytes20(output2_ext));
require(receiver != address(0), "Invalid receiver address");

emit Deposit(txId, block.timestamp);
(bool success, ) = receiver.call{value: DEPOSIT_AMOUNT}("");
require(success, "Transfer failed");
}

/// @notice Accepts 1 cBTC from the sender and inserts this withdrawal request of 1 BTC on Bitcoin into the Merkle tree so that later on can be processed by the operator
/// @param bitcoin_address The Bitcoin address of the receiver
function withdraw(bytes32 bitcoin_address) external payable {
require(msg.value == DEPOSIT_AMOUNT, "Invalid withdraw amount");
insertWithdrawalTree(bitcoin_address);
emit Withdrawal(bitcoin_address, nextIndex, block.timestamp);
}

/// @notice Batch version of `withdraw` that can accept multiple cBTC
/// @dev Takes in multiple Bitcoin addresses as recipient addresses should be unique
/// @param bitcoin_addresses The Bitcoin addresses of the receivers
function batchWithdraw(bytes32[] calldata bitcoin_addresses) external payable {
require(msg.value == DEPOSIT_AMOUNT * bitcoin_addresses.length, "Invalid withdraw amount");
for (uint i = 0; i < bitcoin_addresses.length; i++) {
insertWithdrawalTree(bitcoin_addresses[i]);
emit Withdrawal(bitcoin_addresses[i], nextIndex, block.timestamp);
}
}

/// @notice Adds a block hash to the list of block hashes
/// @param block_hash The block hash to be added
function addBlockHash(bytes32 block_hash) external onlyOwner {
blockHashes[block_hash] = true;
emit BlockHashAdded(block_hash);
}

/// @notice Sets the operator address that can process user deposits
/// @param _operator Address of the privileged operator
function setOperator(address _operator) external onlyOwner {
operator = _operator;
emit OperatorUpdated(operator, _operator);
}

/// @notice Checks if two byte sequences are equal
/// @dev This is not efficient, and a better approach would be doing a hash based comparison but as this is ran in a zkEVM, hashing is inefficient
/// @param a First byte sequence
/// @param b Second byte sequence
function isBytesEqual(bytes memory a, bytes memory b) internal pure returns (bool result) {
require(a.length == b.length, "Lengths do not match");

// Cannot use keccak as its costly in ZK environment
uint length = a.length;
for (uint i = 0; i < length; i++) {
if (a[i] != b[i]) {
result = false;
return result;
}
}
result = true;
}
}
136 changes: 136 additions & 0 deletions crates/evm/src/evm/system_contracts/src/MerkleTree.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

contract MerkleTree {
bytes32 public constant ZERO_VALUE = 0xcb0c9f4264546b15be9801ecb11df7e43bfc6841609fc1e4e9de5b3a5973af38; // keccak256("CITREA")

uint32 public immutable levels;
mapping(uint256 => bytes32) filledSubtrees;
bytes32 root;
uint32 nextIndex;

constructor(uint32 _levels) {
levels = _levels;
initializeTree();
}

function initializeTree() internal {
for (uint32 i = 0; i < levels; i++) {
filledSubtrees[i] = zeros(i);
}
root = zeros(levels);
}

function hashLeftRight(bytes32 _left, bytes32 _right) public pure returns (bytes32 value) {
return sha256(abi.encodePacked(_left, _right));
}

function _insert(bytes32 _leaf) internal returns (uint32 index) {
uint32 _nextIndex = nextIndex;
require(_nextIndex != uint32(2) ** levels, "Merkle tree is full. No more leaves can be added");
uint32 currentIndex = _nextIndex;
bytes32 currentLevelHash = _leaf;
bytes32 left;
bytes32 right;

for (uint32 i = 0; i < levels; i++) {
if (currentIndex % 2 == 0) {
left = currentLevelHash;
right = zeros(i);
filledSubtrees[i] = currentLevelHash;
} else {
left = filledSubtrees[i];
right = currentLevelHash;
}
currentLevelHash = hashLeftRight(left, right);
currentIndex /= 2;
}

root = currentLevelHash;
nextIndex = _nextIndex + 1;
return _nextIndex;
}

// Insert function
function insertWithdrawalTree(bytes32 _leaf) public returns (uint32 index) {
return _insert(_leaf);
}

// Get root function
function getRootWithdrawalTree() public view returns (bytes32) {
return root;
}

/// @dev provides Zero (Empty) elements for a MiMC MerkleTree. Up to 32 levels
function zeros(uint256 i) public pure returns (bytes32) {
if (i == 0) {
return bytes32(0xcb0c9f4264546b15be9801ecb11df7e43bfc6841609fc1e4e9de5b3a5973af38);
} else if (i == 1) {
return bytes32(0x455b22fd2c80e024797f8d83d31a0b6b11aa024ecffb905b1d691d657434b90a);
} else if (i == 2) {
return bytes32(0x480c45bfe41828a16e4bf0bd7a1db34d2ec4d239101daf0b774821ed2dfca761);
} else if (i == 3) {
return bytes32(0x29f2f8c3c9d792bd9ebdc71486151113b73af357a205dd41760f941555e26146);
} else if (i == 4) {
return bytes32(0xd0fb88bcc6243bb6021b49d377e1655d1e61fb08a659b9511680f83824d64197);
} else if (i == 5) {
return bytes32(0x5c7fac0890e73fb2e0d8fb3063c6786a2c63da95187efe29da4bce99d621d0ba);
} else if (i == 6) {
return bytes32(0xcb08837032cc923c34faece314b3e13158b7fda962321262618d4bcc46217eb2);
} else if (i == 7) {
return bytes32(0x229c0a76b756fc3db3266dc0f8e571d113ca44e0cb7326bee03d35331748091f);
} else if (i == 8) {
return bytes32(0x8270abf4c13bfeb13ee8967544be4d343424fa3910ec7ad995873ca0d86daf69);
} else if (i == 9) {
return bytes32(0x2d338a30d2a6bb5169df7cc8d61b6578d35568f6e1f09eda43f5bd36ad68be67);
} else if (i == 10) {
return bytes32(0x6609db0b330090c86c349ad31f986c5f2a346461eb6f160a0e0a865d39260df6);
} else if (i == 11) {
return bytes32(0x81861227312c9b868eb34b8cf216893c0db60b6b5c5e05f549f3ab6811b9f3d6);
} else if (i == 12) {
return bytes32(0x1f7af2d9c7e25a85c9035abd0d8c2c7b993754a4af7250924bc55071770f75ef);
} else if (i == 13) {
return bytes32(0x45d98c88c1c60a5d35eabe7a4cf3b2ba3bee0d8a4617f7bf485a5bd93749d3f0);
} else if (i == 14) {
return bytes32(0x11b8807d04fe98c0bd60dec1890026baa1d83f51b37e365a45b5c87028aaf6c9);
} else if (i == 15) {
return bytes32(0x6a43477876ca14f6d714ab184a94b3a8d324e29085646fd2bc3663427c512332);
} else if (i == 16) {
return bytes32(0x2e2ca637ce5b8e8b33629f637c7fd77c7ac300ae409e64898d38b4c177280e43);
} else if (i == 17) {
return bytes32(0x212db654d1d3a4e31fd8b86d3545babbf23ce592753c50936b30b1eb34d6db2a);
} else if (i == 18) {
return bytes32(0x7818b4041e25a0bcd1bf0528728f20c6976f0923d5038facff21524ccd971d75);
} else if (i == 19) {
return bytes32(0x538977a76a1b4557dd0e98d557d9a24d88349d83220ed84feb13c79f8ae83c7c);
} else if (i == 20) {
return bytes32(0x9f3efb03c31cf43571e1e5395e2f71fcaabc491ca72631b44ec9cf99110821be);
} else if (i == 21) {
return bytes32(0xfb29f69c3f45b34a9e0b7a4007a953190aa2618deef63b15ea4dd10998785734);
} else if (i == 22) {
return bytes32(0x3d12f73b1f682ed24aa0b1a39df347240a74a64b5e15312466368dd943e769bc);
} else if (i == 23) {
return bytes32(0x5bce4a357cef84ee3edf0669ec908843281b2d19072f58eff8d6503468b0c8d6);
} else if (i == 24) {
return bytes32(0x069835ff0850f45ad28f01399d6af695a96d056589cc0ac9259e66beae87cd3c);
} else if (i == 25) {
return bytes32(0xdbdece04a4bafc538705847f0c28d441f57c7b6dbf8a803ca0e5b097857e0513);
} else if (i == 26) {
return bytes32(0x020d43e87ea83e8ad54ca1eb11d826a2cd75823b5f6a91a6568839b42551f727);
} else if (i == 27) {
return bytes32(0x19a652f1c22915bd595b81d87a7dd5cc01ba7d82ba78882949373b6220d3a504);
} else if (i == 28) {
return bytes32(0x4138c0097adba86ca3c19d2181a21b8e331c42c1fdb3ce8cfd953a4553279ef1);
} else if (i == 29) {
return bytes32(0xfdc8ebd533132c3178ab8434060ae1007fc3b672fcf270d30b57f8e12ca7fa27);
} else if (i == 30) {
return bytes32(0xf0b266c6a0adb776bf7b9fafe1f02c99f35ba89067bcedb8f8f267002d51bceb);
} else if (i == 31) {
return bytes32(0x2afd595f486a771bf9653b9333d78bf101fad1f5ddb0db960c5a1450200061db);
} else if (i == 32) {
return bytes32(0x35c59abafcc58285f02e048ba62334323f15a2d2ea0a033f8df2fbee3344902d);
} else {
revert("Index out of bounds");
}
}
}
Loading
Loading