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

Add recommit protection #174

Merged
merged 8 commits into from
May 7, 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
5 changes: 5 additions & 0 deletions .changeset/hot-cows-bake.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@fuel-bridge/solidity-contracts': minor
---

Add recommit protection in FuelChainState
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ pub enum MessageData {
impl MessageData {
pub fn parse(msg_idx: u64) -> Self {
let message_type: u8 = input_message_data(msg_idx, OFFSET_MESSAGE_TYPE).get(31).unwrap(); // Get the last byte

match message_type {
DEPOSIT => MessageData::Deposit(DepositMessage::parse_deposit_to_address(msg_idx)),
CONTRACT_DEPOSIT => MessageData::Deposit(DepositMessage::parse_deposit_to_contract(msg_idx)),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,7 @@ mod success {
},
setup::{get_asset_id, ClaimRefundEvent, RefundRegisteredEvent},
};
use fuels::{
prelude::Address,
programs::contract::SettableContract,
tx::Receipt,
types::U256,
};
use fuels::{prelude::Address, programs::contract::SettableContract, tx::Receipt, types::U256};
use primitive_types::H160;
use std::str::FromStr;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,20 +34,24 @@ mod success {
#[tokio::test]
async fn deposit_to_wallet() {
let mut wallet = create_wallet();

let amount: u64 = 10;
let token_address = "0x000000000000000000000000fcF38f326CA709b0B04B2215Dbc969fC622775F7";
let token_id = BRIDGED_TOKEN_ID;
let from_address = "0x00000000000000000000000090F79bf6EB2c4f870365E785982E1f101E93b906";
let message_sender = "0x00000000000000000000000059F2f1fCfE2474fD5F0b9BA1E73ca90b143Eb8d0";
let recipient: Bytes32 = Bytes32::from_bytes(&hex::decode("92dffc873b56f219329ed03bb69bebe8c3d8b041088574882f7a6404f02e2f28").unwrap()).unwrap();
let recipient_bech32: Bech32Address = Bech32Address::new(FUEL_BECH32_HRP, recipient.clone());

let configurables: BridgeFungibleTokenContractConfigurables =
let recipient: Bytes32 = Bytes32::from_bytes(
&hex::decode("92dffc873b56f219329ed03bb69bebe8c3d8b041088574882f7a6404f02e2f28")
.unwrap(),
)
.unwrap();
let recipient_bech32: Bech32Address = Bech32Address::new(FUEL_BECH32_HRP, recipient);

let configurables: BridgeFungibleTokenContractConfigurables =
BridgeFungibleTokenContractConfigurables::default()
.with_BRIDGED_TOKEN_GATEWAY(Bits256::from_hex_str(message_sender).unwrap())
.unwrap();

let (message, coin, deposit_contract) = create_deposit_message(
token_address,
token_id,
Expand Down Expand Up @@ -83,12 +87,15 @@ mod success {

let tx_status = wallet.provider().unwrap().tx_status(&_tx_id).await.unwrap();
assert!(matches!(tx_status, TxStatus::Success { .. }));

let eth_balance =
contract_balance(provider, bridge.contract_id(), AssetId::default()).await;
let asset_id = get_asset_id(bridge.contract_id(), token_address);
let asset_balance = provider.get_asset_balance(&recipient_bech32, asset_id).await.unwrap();

let asset_balance = provider
.get_asset_balance(&recipient_bech32, asset_id)
.await
.unwrap();

// Verify the message value was received by the bridge
assert_eq!(eth_balance, MESSAGE_AMOUNT);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@ use fuel_core_types::fuel_tx::{Bytes32, Output};
use fuels::{
prelude::*,
types::{
coin::Coin, coin_type::CoinType, input::Input, transaction_builders::{ScriptTransactionBuilder, TransactionBuilder}
coin::Coin,
coin_type::CoinType,
input::Input,
transaction_builders::{ScriptTransactionBuilder, TransactionBuilder},
},
};

Expand Down Expand Up @@ -50,10 +53,11 @@ pub async fn build_contract_message_tx(
// Build a change output for the owner of the first provided coin input
if !gas_coins.is_empty() {
// Append provided inputs
let mut wtf =
gas_coins.to_vec()
let mut wtf = gas_coins
.to_vec()
.iter()
.map(|coin| Input::resource_signed(CoinType::Coin(coin.clone()))).collect();
.map(|coin| Input::resource_signed(CoinType::Coin(coin.clone())))
.collect();
tx_inputs.append(&mut wtf);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ use crate::utils::{
};
use ethers::abi::Token;
use fuel_core_types::{
fuel_crypto::SecretKey, fuel_tx::{Bytes32, Output, TxId, TxPointer, UtxoId}, fuel_types::{Nonce, Word}
fuel_crypto::SecretKey,
fuel_tx::{Bytes32, Output, TxId, TxPointer, UtxoId},
fuel_types::{Nonce, Word},
};

use fuels::{
Expand Down Expand Up @@ -278,13 +280,13 @@ pub(crate) async fn relay_message_to_contract(
let provider = wallet.provider().expect("Wallet has no provider");

let gas_price: u64 = 1; // NodeInfo.min_gas_price is no longer available

let tx_policies = TxPolicies::new(Some(gas_price), None, Some(0), None, Some(30_000));

let fetched_gas_coins: Vec<Coin> =
provider.get_coins(wallet.address(), Default::default())
.await
.unwrap();
let fetched_gas_coins: Vec<Coin> = provider
.get_coins(wallet.address(), Default::default())
.await
.unwrap();

let tx = builder::build_contract_message_tx(
message,
Expand Down Expand Up @@ -428,7 +430,6 @@ pub(crate) async fn create_deposit_message(
let message = (MESSAGE_AMOUNT, message_data);
let coin = (DEFAULT_COIN_AMOUNT, AssetId::default());


(message, coin, deposit_recipient)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,12 @@ use fuels::{
accounts::wallet::WalletUnlocked,
prelude::{ScriptTransaction, TxPolicies},
types::{
coin::Coin, coin_type::CoinType, input::Input, transaction_builders::{
coin::Coin,
coin_type::CoinType,
input::Input,
transaction_builders::{
BuildableTransaction, ScriptTransactionBuilder, TransactionBuilder,
}
},
},
};

Expand All @@ -40,12 +43,12 @@ pub async fn build_contract_message_tx(
.unwrap();

let funding_utx0 = fetched_gas_coins.first().unwrap().to_owned();

tx_inputs.push(Input::resource_signed(CoinType::Coin(funding_utx0.clone())));
tx_outputs.push(Output::Change {
to: wallet.address().into(),
amount: funding_utx0.amount,
asset_id: funding_utx0.asset_id
asset_id: funding_utx0.asset_id,
});

for input in inputs {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ contract FuelChainState is Initializable, PausableUpgradeable, AccessControlUpgr
// Time to fianlize in seconds
// TIME_TO_FINALIZE = target interval in minutes * 60
uint256 public constant TIME_TO_FINALIZE = 10800;
uint32 public constant COMMIT_COOLDOWN = uint32(TIME_TO_FINALIZE) * 8;

/// @dev The admin related contract roles
bytes32 public constant PAUSER_ROLE = keccak256("PAUSER_ROLE");
Expand All @@ -50,6 +51,7 @@ contract FuelChainState is Initializable, PausableUpgradeable, AccessControlUpgr
////////////

error UnknownBlock();
error CannotRecommit();

/////////////
// Storage //
Expand Down Expand Up @@ -95,16 +97,18 @@ contract FuelChainState is Initializable, PausableUpgradeable, AccessControlUpgr
}

/// @notice Commits a block header.
/// @dev Committing to the same commitHeight twice would have the effect of delaying
/// finality, as the timestamp of the committed slot is taken from the ETH block
/// timestamp. TODO: it might be reasonable to put a require here to disallow overwriting
/// In the future we will want this to be challenge-able, and rewriting the slot
/// could open for foul play during IVG
/// @param blockHash The hash of a block
/// @param commitHeight The height of the commit
function commit(bytes32 blockHash, uint256 commitHeight) external whenNotPaused onlyRole(COMMITTER_ROLE) {
uint256 slot = commitHeight % NUM_COMMIT_SLOTS;
Commit storage commitSlot = _commitSlots[slot];

unchecked {
if (commitSlot.timestamp + COMMIT_COOLDOWN > uint32(block.timestamp)) {
revert CannotRecommit();
}
}

commitSlot.blockHash = blockHash;
commitSlot.timestamp = uint32(block.timestamp);

Expand Down
37 changes: 37 additions & 0 deletions packages/solidity-contracts/test/fuelChainState.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { time } from '@nomicfoundation/hardhat-network-helpers';
import chai from 'chai';
import { keccak256, toBeHex, toUtf8Bytes } from 'ethers';
import { ethers } from 'hardhat';
Expand Down Expand Up @@ -285,6 +286,42 @@ describe('Fuel Chain State', async () => {
});
});

describe('Verify recommit cooldown', () => {
it('Should revert when trying to recommit to a warm slot', async () => {
const blockHash = randomBytes32();
const slot = 10;
await env.fuelChainState.connect(env.signers[1]).commit(blockHash, slot);

expect(await env.fuelChainState.blockHashAtCommit(slot)).to.equal(
blockHash
);

const cooldown = await env.fuelChainState.COMMIT_COOLDOWN();
const currentTime = await ethers.provider
.getBlock('latest')
.then((block) => block.timestamp);
await time.setNextBlockTimestamp(cooldown + BigInt(currentTime) - 1n);

const tx = env.fuelChainState
.connect(env.signers[1])
.commit(blockHash, slot);

await expect(tx).to.be.revertedWithCustomError(
env.fuelChainState,
'CannotRecommit'
);
await time.setNextBlockTimestamp(cooldown + BigInt(currentTime));

const newBlockHash = randomBytes32();
await env.fuelChainState
.connect(env.signers[1])
.commit(newBlockHash, slot);
expect(await env.fuelChainState.blockHashAtCommit(slot)).to.equal(
newBlockHash
);
});
});

describe('Verify pause and unpause', async () => {
const defaultAdminRole =
'0x0000000000000000000000000000000000000000000000000000000000000000';
Expand Down
Loading