Skip to content

Commit

Permalink
Bridge handshake protocol (#82)
Browse files Browse the repository at this point in the history
Adds a handshake protocol that ensures no funds are lost when
depositing in the canonical bridge.

---------

Co-authored-by: K1-R1 <77465250+K1-R1@users.noreply.github.com>
  • Loading branch information
DefiCake and K1-R1 authored Oct 20, 2023
1 parent 7a38691 commit ae19f30
Show file tree
Hide file tree
Showing 19 changed files with 541 additions and 18 deletions.
7 changes: 7 additions & 0 deletions .changeset/popular-feet-walk.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@fuel-bridge/solidity-contracts': minor
'@fuel-bridge/fungible-token': minor
'@fuel-bridge/test-utils': minor
---

Introduces a handshake protocol to avoid loss of funds while bridging assets from L1 to L2
7 changes: 6 additions & 1 deletion docker/l1-chain/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,12 @@ WORKDIR /l1chain/fuel-v2-contracts

# build the ethereum contracts and environment
RUN pnpm install

# Take advantage of cache by putting a placeholder and downloading the compiler
ADD ./docker/l1-chain/Placeholder.sol /l1chain/fuel-v2-contracts/contracts/Placeholder.sol
COPY ./docker/l1-chain/hardhat/hardhat.config.ts .
RUN pnpm compile

# replace the fuel chain consts values and change contract code
ADD ./docker/l1-chain/.fuelChainConsts.env /l1chain/fuel-v2-contracts/.fuelChainConsts.env
ADD ./packages/solidity-contracts/scripts/replaceFuelChainConsts.ts /l1chain/fuel-v2-contracts/scripts/replaceFuelChainConsts.ts
Expand All @@ -27,7 +33,6 @@ RUN pnpm ts-node scripts/replaceFuelChainConsts.ts

# remove build dependencies
# RUN pnpm prune --prod
COPY ./docker/l1-chain/hardhat/hardhat.config.ts .
RUN pnpm compile

# Create deployments dir
Expand Down
6 changes: 6 additions & 0 deletions docker/l1-chain/Placeholder.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.0;

/// @dev The only purpose of this contract is to be copied during the Dockerfile build
/// so that hardhat downloads the compiler and it gets cached
contract Placeholder {}
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,8 @@ abi Bridge {
// Recovers the sub_id used to generate an asset_id (= sha256(contract_id, sub_id))
#[storage(read)]
fn asset_to_sub_id(asset_id: b256) -> b256;

/// Sends a message to the L1 gateway to inform of capabilities to receive funds
#[storage(read)]
fn register_bridge();
}
12 changes: 11 additions & 1 deletion packages/fungible-token/bridge-fungible-token/src/main.sw
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,12 @@ use std::{
},
u256::U256,
};
use utils::{adjust_deposit_decimals, adjust_withdrawal_decimals, encode_data};
use utils::{
adjust_deposit_decimals,
adjust_withdrawal_decimals,
encode_data,
encode_register_calldata,
};

configurable {
DECIMALS: u8 = 9u8,
Expand Down Expand Up @@ -126,6 +131,11 @@ impl MessageReceiver for Contract {
}

impl Bridge for Contract {
#[storage(read)]
fn register_bridge() {
send_message(BRIDGED_TOKEN_GATEWAY, encode_register_calldata(BRIDGED_TOKEN), 0);
}

#[storage(read, write)]
fn claim_refund(from: b256, token_address: b256, token_id: b256) {
let asset = sha256((token_address, token_id));
Expand Down
15 changes: 15 additions & 0 deletions packages/fungible-token/bridge-fungible-token/src/utils.sw
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,21 @@ pub fn encode_data(to: b256, amount: b256, bridged_token: b256, token_id: b256)
data
}

pub fn encode_register_calldata(bridged_token: b256) -> Bytes {
let mut data = Bytes::with_capacity(36);

// First 4 bytes are funcSig: aec97dc6 => registerAsReceiver(address)
data.push(0xaeu8);
data.push(0xc9u8);
data.push(0x7du8);
data.push(0xc6u8);

// Now the parameters
data.append(Bytes::from(bridged_token));

data
}

fn shift_decimals_left(bn: U256, decimals: u8) -> Result<U256, BridgeFungibleTokenError> {
let mut bn_clone = bn;
let mut decimals_to_shift = asm(r1: decimals) { r1: u64 };
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// SPDX-License-Identifier: MIT
// SPDX-License-Identifier: Apache-2.0
pragma solidity 0.8.9;

import {FuelMessagePortal} from "../fuelchain/FuelMessagePortal.sol";
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// SPDX-License-Identifier: MIT
// SPDX-License-Identifier: Apache-2.0
pragma solidity 0.8.9;

import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// SPDX-License-Identifier: MIT
// SPDX-License-Identifier: Apache-2.0
pragma solidity 0.8.9;

abstract contract FuelBridgeBase {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// SPDX-License-Identifier: MIT
// SPDX-License-Identifier: Apache-2.0
pragma solidity 0.8.9;

import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
Expand Down Expand Up @@ -54,7 +54,7 @@ contract FuelERC20Gateway is
/////////////

/// @notice Maps ERC20 tokens to Fuel tokens to balance of the ERC20 tokens deposited
mapping(address => mapping(bytes32 => uint256)) private _deposits;
mapping(address => mapping(bytes32 => uint256)) internal _deposits;

/////////////////////////////
// Constructor/Initializer //
Expand Down Expand Up @@ -101,7 +101,7 @@ contract FuelERC20Gateway is
/// @param tokenAddress ERC-20 token address
/// @param fuelContractId ID of the corresponding token on Fuel
/// @return amount of tokens deposited
function tokensDeposited(address tokenAddress, bytes32 fuelContractId) public view returns (uint256) {
function tokensDeposited(address tokenAddress, bytes32 fuelContractId) public view virtual returns (uint256) {
return _deposits[tokenAddress][fuelContractId];
}

Expand All @@ -116,7 +116,7 @@ contract FuelERC20Gateway is
address tokenAddress,
bytes32 fuelContractId,
uint256 amount
) external payable whenNotPaused {
) external virtual payable whenNotPaused {
bytes memory messageData = abi.encodePacked(
fuelContractId,
bytes32(uint256(uint160(tokenAddress))), // OFFSET_TOKEN_ADDRESS = 32
Expand All @@ -141,7 +141,7 @@ contract FuelERC20Gateway is
bytes32 fuelContractId,
uint256 amount,
bytes calldata data
) external payable whenNotPaused {
) external virtual payable whenNotPaused {
bytes memory messageData = abi.encodePacked(
fuelContractId,
bytes32(uint256(uint160(tokenAddress))), // OFFSET_TOKEN_ADDRESS = 32
Expand Down Expand Up @@ -181,7 +181,7 @@ contract FuelERC20Gateway is

/// @notice Allows the admin to rescue ETH sent to this contract by accident
/// @dev Made payable to reduce gas costs
function rescueETH() external payable onlyRole(DEFAULT_ADMIN_ROLE) {
function rescueETH() external virtual payable onlyRole(DEFAULT_ADMIN_ROLE) {
(bool success, ) = address(msg.sender).call{value: address(this).balance}("");
require(success);
}
Expand All @@ -195,7 +195,7 @@ contract FuelERC20Gateway is
/// @param fuelContractId ID of the contract on Fuel that manages the deposited tokens
/// @param amount Amount of tokens to deposit
/// @param messageData The data of the message to send for deposit
function _deposit(address tokenAddress, bytes32 fuelContractId, uint256 amount, bytes memory messageData) private {
function _deposit(address tokenAddress, bytes32 fuelContractId, uint256 amount, bytes memory messageData) internal virtual {
require(amount > 0, "Cannot deposit zero");

//transfer tokens to this contract and update deposit balance
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// SPDX-License-Identifier: MIT
// SPDX-License-Identifier: Apache-2.0
pragma solidity 0.8.9;

import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
Expand Down Expand Up @@ -58,7 +58,7 @@ contract FuelERC721Gateway is
/////////////

/// @notice Maps ERC721 tokens to its fuel bridge counterpart
mapping(address => mapping(uint256 => bytes32)) private _deposits;
mapping(address => mapping(uint256 => bytes32)) internal _deposits;

/////////////////////////////
// Constructor/Initializer //
Expand Down Expand Up @@ -190,7 +190,7 @@ contract FuelERC721Gateway is
/// @param fuelContractId ID of the contract on Fuel that manages the deposited tokens
/// @param tokenId tokenId to deposit
/// @param messageData The data of the message to send for deposit
function _deposit(address tokenAddress, bytes32 fuelContractId, uint256 tokenId, bytes memory messageData) private {
function _deposit(address tokenAddress, bytes32 fuelContractId, uint256 tokenId, bytes memory messageData) internal virtual {
// TODO: this check might be unnecessary. If the token is conformant to ERC721
// it should not be possible to deposit the same token again
require(_deposits[tokenAddress][tokenId] == 0, "tokenId is already owned by another fuel bridge");
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// SPDX-License-Identifier: Apache-2.0
pragma solidity 0.8.9;

abstract contract FuelBridgeBaseV2 {
error FuelContractIsNotBridge();

event ReceiverRegistered(
bytes32 indexed fuelContractId,
address indexed tokenAddress
);

mapping(bytes32 => mapping(address => bool)) public isBridge;

/// @notice Accepts a message from a fuel entity to acknowledge it can receive tokens
/// @param tokenAddress The token address that the fuel entity can receive
/// @dev Made payable to reduce gas costs
/// @dev funcSig: aec97dc6 => registerAsReceiver(address)
function registerAsReceiver(address tokenAddress) external payable virtual;

/**
* @dev This empty reserved space is put in place to allow future versions to add new
* variables without shifting down storage in the inheritance chain.
* See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps
*/
uint256[49] private __gap;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// SPDX-License-Identifier: Apache-2.0
pragma solidity 0.8.9;

import '../FuelERC20Gateway.sol';
import './FuelBridgeBaseV2.sol';

contract FuelERC20GatewayV2 is FuelERC20Gateway, FuelBridgeBaseV2 {
using SafeERC20Upgradeable for IERC20Upgradeable;

function registerAsReceiver(address tokenAddress) external payable virtual override onlyFromPortal() {
bytes32 sender = messageSender();

isBridge[sender][tokenAddress] = true;

emit ReceiverRegistered(sender, tokenAddress);
}

/// @notice Deposits the given tokens to an account or contract on Fuel
/// @param tokenAddress Address of the token being transferred to Fuel
/// @param fuelContractId ID of the contract on Fuel that manages the deposited tokens
/// @param amount Amount of tokens to deposit
/// @param messageData The data of the message to send for deposit
function _deposit(address tokenAddress, bytes32 fuelContractId, uint256 amount, bytes memory messageData) internal virtual override {
require(amount > 0, "Cannot deposit zero");
if (!isBridge[fuelContractId][tokenAddress]) revert FuelContractIsNotBridge();

//transfer tokens to this contract and update deposit balance
IERC20Upgradeable(tokenAddress).safeTransferFrom(msg.sender, address(this), amount);
_deposits[tokenAddress][fuelContractId] = _deposits[tokenAddress][fuelContractId] + amount;

//send message to gateway on Fuel to finalize the deposit
sendMessage(CommonPredicates.CONTRACT_MESSAGE_PREDICATE, messageData);

//emit event for successful token deposit
emit Deposit(bytes32(uint256(uint160(msg.sender))), tokenAddress, fuelContractId, amount);
}

/**
* @dev This empty reserved space is put in place to allow future versions to add new
* variables without shifting down storage in the inheritance chain.
* See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps
*/
uint256[49] private __gap;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
// SPDX-License-Identifier: Apache-2.0
pragma solidity 0.8.9;

import '../FuelERC721Gateway.sol';
import './FuelBridgeBaseV2.sol';

contract FuelERC721GatewayV2 is FuelERC721Gateway, FuelBridgeBaseV2 {
function registerAsReceiver(address tokenAddress) external payable virtual override onlyFromPortal() {
bytes32 sender = messageSender();

isBridge[sender][tokenAddress] = true;

emit ReceiverRegistered(sender, tokenAddress);
}

/// @notice Deposits the given tokens to an account or contract on Fuel
/// @param tokenAddress Address of the token being transferred to Fuel
/// @param fuelContractId ID of the contract on Fuel that manages the deposited tokens
/// @param tokenId tokenId to deposit
/// @param messageData The data of the message to send for deposit
function _deposit(
address tokenAddress,
bytes32 fuelContractId,
uint256 tokenId,
bytes memory messageData
) internal virtual override {
// TODO: this check might be unnecessary. If the token is conformant to ERC721
// it should not be possible to deposit the same token again
require(_deposits[tokenAddress][tokenId] == 0, "tokenId is already owned by another fuel bridge");
if (!isBridge[fuelContractId][tokenAddress]) revert FuelContractIsNotBridge();

_deposits[tokenAddress][tokenId] = fuelContractId;

//send message to gateway on Fuel to finalize the deposit
sendMessage(CommonPredicates.CONTRACT_MESSAGE_PREDICATE, messageData);

IERC721Upgradeable(tokenAddress).transferFrom(msg.sender, address(this), tokenId);
//emit event for successful token deposit
emit Deposit(bytes32(uint256(uint160(msg.sender))), tokenAddress, fuelContractId, tokenId);
}

/**
* @dev This empty reserved space is put in place to allow future versions to add new
* variables without shifting down storage in the inheritance chain.
* See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps
*/
uint256[49] private __gap;
}
4 changes: 2 additions & 2 deletions packages/solidity-contracts/protocol/harness.ts
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ export async function deployFuel(

// Deploy gateway contract for ERC20 bridging
const FuelERC20Gateway = await ethers.getContractFactory(
'FuelERC20Gateway',
'FuelERC20GatewayV2',
deployer
);
const fuelERC20Gateway = (await upgrades.deployProxy(
Expand All @@ -173,7 +173,7 @@ export async function deployFuel(

// Deploy gateway contract for ERC721 bridging
const FuelERC721Gateway = await ethers.getContractFactory(
'FuelERC721Gateway',
'FuelERC721GatewayV2',
deployer
);
const fuelERC721Gateway = (await upgrades.deployProxy(
Expand Down
Loading

0 comments on commit ae19f30

Please sign in to comment.