diff --git a/packages/fungible-token/bridge-fungible-token/proxy/src/proxy.sw b/packages/fungible-token/bridge-fungible-token/proxy/src/proxy.sw index a80ddb9b..ce4341e3 100644 --- a/packages/fungible-token/bridge-fungible-token/proxy/src/proxy.sw +++ b/packages/fungible-token/bridge-fungible-token/proxy/src/proxy.sw @@ -24,12 +24,20 @@ abi Proxy { #[storage(read, write)] fn _proxy_change_owner(new_owner: Identity); + + #[storage(read, write)] + fn _proxy_revoke_ownership(); +} + +configurable { + INITIAL_OWNER: State = State::Uninitialized, + INITIAL_TARGET: ContractId = ContractId::from(ZERO_B256) } #[namespace(SRC14)] storage { // target is at sha256("storage_SRC14_0") - target: ContractId = ContractId::zero(), + target: Option = None, // owner is at sha256("storage_SRC14_1") owner: State = State::Uninitialized, } @@ -39,7 +47,7 @@ impl SRC14 for Contract { fn set_proxy_target(new_target: ContractId) { only_owner(); require(new_target.bits() != ZERO_B256, ProxyErrors::IdentityZero); - storage.target.write(new_target); + storage.target.write(Some(new_target)); } } @@ -47,15 +55,19 @@ impl SRC14 for Contract { #[storage(read)] fn fallback() { // pass through any other method call to the target - run_external(storage.target.read()) + run_external(storage.target.read().unwrap_or(INITIAL_TARGET)) } #[storage(read)] fn only_owner() { + + let owner = match storage.owner.read() { + State::Uninitialized => INITIAL_OWNER, + state => state, + }; + require( - storage - .owner - .read() == State::Initialized(msg_sender().unwrap()), + owner == State::Initialized(msg_sender().unwrap()), AccessError::NotOwner, ); } @@ -64,12 +76,17 @@ impl Proxy for Contract { #[storage(read)] fn _proxy_owner() -> State { - storage.owner.read() + let owner = storage.owner.read(); + + match owner { + State::Uninitialized => INITIAL_OWNER, + _ => owner, + } } #[storage(read)] fn _proxy_target() -> ContractId { - storage.target.read() + storage.target.read().unwrap_or(INITIAL_TARGET) } #[storage(read, write)] @@ -78,4 +95,10 @@ impl Proxy for Contract { require(new_owner.bits() != ZERO_B256, ProxyErrors::IdentityZero); storage.owner.write(State::Initialized(new_owner)); } + + #[storage(read,write)] + fn _proxy_revoke_ownership() { + only_owner(); + storage.owner.write(State::Revoked); + } } \ No newline at end of file diff --git a/packages/fungible-token/bridge-fungible-token/tests/functions/proxy/mod.rs b/packages/fungible-token/bridge-fungible-token/tests/functions/proxy/mod.rs index 1408fbf8..80b73946 100644 --- a/packages/fungible-token/bridge-fungible-token/tests/functions/proxy/mod.rs +++ b/packages/fungible-token/bridge-fungible-token/tests/functions/proxy/mod.rs @@ -48,7 +48,8 @@ mod tests { State::Initialized(fuels::types::Identity::Address(address)) if address == wallet.address().clone().into() ), - "Ownership was not initialized or owner is not the expected address" + "Ownership was not initialized or owner is not the expected address. Value: {:?}", + owner ); let target = proxy @@ -110,12 +111,60 @@ mod tests { State::Initialized(fuels::types::Identity::Address(address)) if address == new_owner.address().clone().into() ), - "Ownership was not initialized or owner is not the expected address" + "Ownership was not initialized or owner is not the expected address. Value: {:?}", + owner ); Ok(()) } + #[tokio::test] + async fn proxy_revoke_ownership() -> anyhow::Result<()> { + let mut wallet = create_wallet(); + + let configurables: Option = None; + + let (proxy_id, _implementation_contract_id) = + get_contract_ids(&wallet, configurables.clone()); + + let wallet_funds = (DEFAULT_COIN_AMOUNT, AssetId::default()); + + let (_, bridge, _) = setup_environment( + &mut wallet, + vec![wallet_funds], + vec![], + None, + None, + configurables, + ) + .await; + + let proxy = BridgeProxy::new(bridge.contract_id().clone(), wallet.clone()); + + let _tx_id = proxy + .methods() + ._proxy_revoke_ownership() + .with_contract_ids(&[proxy_id.into()]) + .call() + .await? + .tx_id + .unwrap(); + + let owner = proxy + .methods() + ._proxy_owner() + .with_contract_ids(&[proxy_id.into()]) + .simulate() + .await? + .value; + + assert_eq!(owner, State::Revoked); + + Ok(()) + } + + + #[tokio::test] async fn proxy_change_owner_cannot_be_zero() -> anyhow::Result<()> { let mut wallet = create_wallet(); @@ -322,6 +371,4 @@ mod tests { Ok(()) } - - } diff --git a/packages/fungible-token/bridge-fungible-token/tests/utils/setup.rs b/packages/fungible-token/bridge-fungible-token/tests/utils/setup.rs index 7043ac8e..c77de576 100644 --- a/packages/fungible-token/bridge-fungible-token/tests/utils/setup.rs +++ b/packages/fungible-token/bridge-fungible-token/tests/utils/setup.rs @@ -8,11 +8,11 @@ use crate::utils::{ }; use ethers::abi::Token; use fuel_core_types::{ - fuel_crypto::{Hasher, SecretKey}, + fuel_crypto::SecretKey, fuel_tx::{Bytes32, Output, TxId, TxPointer, UtxoId}, - fuel_types::{canonical::Deserialize, Nonce, Word}, + fuel_types::{Nonce, Word}, }; -use fuels::core::{codec::ABIEncoder, traits::Tokenizable}; + use fuels::{ accounts::{predicate::Predicate, wallet::WalletUnlocked, ViewOnlyAccount}, @@ -20,9 +20,9 @@ use fuels::{ abigen, setup_custom_assets_coins, setup_test_provider, Address, AssetConfig, AssetId, Bech32ContractId, Contract, ContractId, LoadConfiguration, Provider, TxPolicies, }, - programs::contract::StorageConfiguration, + test_helpers::{setup_single_message, DEFAULT_COIN_AMOUNT}, - tx::StorageSlot, + types::{coin::Coin, input::Input, message::Message, tx_status::TxStatus, Bits256, U256}, }; use sha2::Digest; @@ -232,13 +232,13 @@ pub(crate) async fn setup_environment( .await .unwrap(); - let storage_configuration = get_proxy_storage_config( - (*implementation_contract_id.clone().hash).into(), - State::Initialized(wallet.address().clone().into()), - ); + let proxy_configurables = BridgeProxyConfigurables::default() + .with_INITIAL_OWNER(State::Initialized(wallet.address().into())).unwrap() + .with_INITIAL_TARGET(implementation_contract_id.clone().into()).unwrap(); let proxy_config = - LoadConfiguration::default().with_storage_configuration(storage_configuration); + LoadConfiguration::default().with_configurables(proxy_configurables); + let proxy_contract_id = Contract::load_from(BRIDGE_PROXY_BINARY, proxy_config) .unwrap() .deploy(&wallet.clone(), TxPolicies::default()) @@ -573,60 +573,15 @@ pub(crate) fn get_contract_ids( .unwrap() .contract_id(); - let storage_configuration = get_proxy_storage_config( - Bytes32::from_bytes(implementation_contract_id.as_slice()).unwrap(), - State::Initialized(proxy_owner.address().clone().into()), - ); + let proxy_configurables = BridgeProxyConfigurables::default() + .with_INITIAL_OWNER(State::Initialized(proxy_owner.address().clone().into())).unwrap() + .with_INITIAL_TARGET(implementation_contract_id.clone()).unwrap(); let proxy_config = - LoadConfiguration::default().with_storage_configuration(storage_configuration); + LoadConfiguration::default().with_configurables(proxy_configurables); let proxy_contract_id = Contract::load_from(BRIDGE_PROXY_BINARY, proxy_config) .unwrap() .contract_id(); (proxy_contract_id, implementation_contract_id) } - -pub(crate) fn get_proxy_storage_config( - target_contract_id: Bytes32, - owner: State, -) -> StorageConfiguration { - let target_key_hash = Hasher::hash("storage_SRC14_0"); - let slot_override_target = StorageSlot::new(target_key_hash, target_contract_id); - - let (owner_key_hash_1, owner_key_hash_2) = { - let owner_key_hash_1: Bytes32 = Hasher::hash("storage_SRC14_1"); - let mut key_hash_slice = owner_key_hash_1.clone().as_slice().to_vec(); - - let last_index = key_hash_slice.len() - 1; - key_hash_slice[last_index] = key_hash_slice[last_index].wrapping_add(1); - - let owner_key_hash_2 = Bytes32::from_bytes(&key_hash_slice).unwrap(); - - (owner_key_hash_1, owner_key_hash_2) - }; - - let (owner_value_1, owner_value_2) = { - let mut encoded_value = ABIEncoder::default().encode(&[owner.into_token()]).unwrap(); - - if encoded_value.len() < 64 { - encoded_value.resize(64, 0); - } - - let owner_value_1: Bytes32 = - Bytes32::from_bytes(&mut encoded_value[0..32]).expect("Could not decode owner_value_1"); - let owner_value_2: Bytes32 = Bytes32::from_bytes(&mut encoded_value[32..64]) - .expect("Could not decode owner_value_2"); - - (owner_value_1, owner_value_2) - }; - - let slot_override_owner_1 = StorageSlot::new(owner_key_hash_1, owner_value_1); - let slot_override_owner_2 = StorageSlot::new(owner_key_hash_2, owner_value_2); - - StorageConfiguration::default().add_slot_overrides([ - slot_override_target, - slot_override_owner_1, - slot_override_owner_2, - ]) -} diff --git a/packages/integration-tests/tests/bridge_proxy.ts b/packages/integration-tests/tests/bridge_proxy.ts index f535646c..99494906 100644 --- a/packages/integration-tests/tests/bridge_proxy.ts +++ b/packages/integration-tests/tests/bridge_proxy.ts @@ -145,4 +145,35 @@ describe('Proxy', async function () { expect(value.bits).to.be.equal(contractId); }); }); + + describe('_proxy_revoke_ownership()', () => { + const contractId = + '0x7296ff960b5eb86b5f79aa587d7ebe1bae147c7cac046a16d062fbd7f3a753ec'; + const contractIdentityInput = { bits: contractId.toString() }; + + it('revokes ownership', async () => { + fuel_proxy.account = env.fuel.deployer; + const tx = await fuel_proxy.functions._proxy_revoke_ownership().call(); + const result = await tx.transactionResponse.waitForResult(); + expect(result.status).to.equal('success'); + + const { value } = await fuel_proxy.functions._proxy_owner().dryRun(); + expect(value).to.have.property('Revoked'); + }); + + it('disallows proxy upgrades', async () => { + fuel_proxy.account = env.fuel.deployer; + const tx = fuel_proxy.functions + .set_proxy_target(contractIdentityInput) + .call(); + const [txResult] = await Promise.allSettled([tx]); + + if (txResult.status === 'fulfilled') { + throw new Error('Transaction did not revert'); + } + const { message } = txResult.reason as FuelError; + + expect(message).contains('NotOwner'); + }); + }); }); diff --git a/packages/test-utils/src/utils/fuels/getOrDeployL2Bridge.ts b/packages/test-utils/src/utils/fuels/getOrDeployL2Bridge.ts index f18fd20d..a1d39742 100644 --- a/packages/test-utils/src/utils/fuels/getOrDeployL2Bridge.ts +++ b/packages/test-utils/src/utils/fuels/getOrDeployL2Bridge.ts @@ -3,10 +3,11 @@ import { fungibleTokenABI, bridgeProxyBinary, bridgeProxyABI, + bridgeProxyStorageSlots, } from '@fuel-bridge/fungible-token'; import type { AddressLike } from 'ethers'; -import type { StorageSlot, TxParams } from 'fuels'; -import { ContractFactory, Contract, sha256, toUtf8Bytes, BN } from 'fuels'; +import type { TxParams } from 'fuels'; +import { ContractFactory, Contract } from 'fuels'; import { debug } from '../logs'; import { eth_address_to_b256 } from '../parsers'; @@ -73,14 +74,14 @@ export async function getOrDeployL2Bridge( env.fuel.deployer ); - const configurableConstants: any = { + const implConfigurables: any = { BRIDGED_TOKEN_GATEWAY: eth_address_to_b256(tokenGateway), }; - if (DECIMALS !== undefined) configurableConstants['DECIMALS'] = DECIMALS; + if (DECIMALS !== undefined) implConfigurables['DECIMALS'] = DECIMALS; // Set the token gateway and token address in the contract - implFactory.setConfigurableConstants(configurableConstants); + implFactory.setConfigurableConstants(implConfigurables); const { contractId: implContractId, @@ -112,49 +113,27 @@ export async function getOrDeployL2Bridge( debug('Creating proxy contract'); const proxyFactory = new ContractFactory( bridgeProxyBinary, - bridgeProxyABI as any, + bridgeProxyABI, env.fuel.deployer ); - const targetStorageSlot: StorageSlot = { - key: sha256(toUtf8Bytes('storage_SRC14_0')), - value: implContractId, - }; - - const ownerStorageSlotKey = new BN(sha256(toUtf8Bytes('storage_SRC14_1'))); - const ownerStorageSlotValue_1 = - '0x00000000000000010000000000000000' + - env.fuel.deployer.address.toHexString().substring(2).slice(0, 32); - const ownerStorageSlotValue_2 = - '0x' + - env.fuel.deployer.address - .toHexString() - .substring(2) - .slice(32) - .padEnd(64, '0'); - - const ownerStorageSlots: StorageSlot[] = [ - { - key: ownerStorageSlotKey.toHex(32), - value: ownerStorageSlotValue_1, - }, - { - key: ownerStorageSlotKey.add(1).toHex(32), - value: ownerStorageSlotValue_2, + const proxyConfigurables: any = { + INITIAL_TARGET: { bits: implContractId }, + INITIAL_OWNER: { + Initialized: { + Address: { bits: env.fuel.deployer.address.toHexString() }, + }, }, - ]; - - const storageSlots = [targetStorageSlot, ...ownerStorageSlots]; + }; - debug('STORAGE SLOTS: '); - debug(storageSlots); + proxyFactory.setConfigurableConstants(proxyConfigurables); const { contractId: proxyContractId, transactionRequest: proxyCreateTxRequest, } = proxyFactory.createTransactionRequest({ ...fuelTxParams, - storageSlots, + storageSlots: bridgeProxyStorageSlots, }); {