o2-tools 0.4.7-rc5

Reusable tooling for trade account and order book contract interactions on Fuel
//! Regression coverage for preserving oracle identity across deployment builds.
use super::*;
use crate::trade_account_registry::{
    TradeAccountRegistryConfigurables,
    TradeAccountRegistryDeployConfig,
    TradeAccountRegistryManager,
};

fn resume(fixture: &PropFixture) -> PropDeployConfig {
    let mut config = PropDeployConfig::new(fixture.collateral_asset);
    config.existing_registry = Some(ExistingRegistry {
        registry_id: fixture.deployment.registry_id,
        trade_account_oracle_id: ContractId::zeroed(),
        trial_trade_account_oracle_id: ContractId::zeroed(),
    });
    config.max_offchain_age_seconds = Some(TEST_MAX_OFFCHAIN_AGE_SECONDS);
    config
}

#[tokio::test]
async fn a_pinned_oracle_preserves_owner_cosigner_pool_and_funded_session() {
    let fixture = PropFixture::new().await;
    let original_session = fixture.session().await;
    let oracle = &fixture.deployment.oracle;
    oracle
        .methods()
        .set_cosigner(fixture.outsider.address())
        .call()
        .await
        .unwrap();
    oracle
        .methods()
        .transfer_ownership(Identity::Address(fixture.outsider.address()))
        .call()
        .await
        .unwrap();

    let mut config = resume(&fixture);
    config.salt = Salt::new([0x5a; 32]);
    // Reproduce the bad deploy: a different derived oracle, same funded pool.
    config.existing_registry = None;
    config.existing_pool = Some(fixture.deployment.pool_id);
    config.existing_price_feed = Some(fixture.deployment.price_feed_id);
    let wrong = PropDeployment::deploy(&fixture.deployer, &config)
        .await
        .unwrap();
    assert_ne!(wrong.oracle_id, fixture.deployment.oracle_id);
    let manager = TradeAccountRegistryManager::new(
        fixture.deployer.clone(),
        fixture.deployment.registry_id,
    );
    let wrong_wiring = manager
        .live_prop_config(TradeAccountRegistryConfigurables::default())
        .await
        .unwrap()
        .with_PROP_ACCOUNT_ORACLE_CONTRACT_ID(wrong.oracle_id)
        .unwrap();
    manager
        .upgrade(
            ContractId::zeroed(),
            ContractId::zeroed(),
            &TradeAccountRegistryDeployConfig {
                registry_config: wrong_wiring,
                ..Default::default()
            },
        )
        .await
        .unwrap();

    // The explicit oracle must beat the registry's wrong value; neither pool
    // nor feed is supplied, so both must be recovered from the original oracle.
    config = resume(&fixture);
    config.salt = Salt::new([0x6b; 32]);
    config.existing_oracle = Some(fixture.deployment.oracle_id);
    let repaired = PropDeployment::deploy(&fixture.deployer, &config)
        .await
        .unwrap();
    assert_eq!(repaired.oracle_id, fixture.deployment.oracle_id);
    assert_eq!(repaired.pool_id, fixture.deployment.pool_id);
    assert_eq!(repaired.price_feed_id, fixture.deployment.price_feed_id);
    assert_eq!(
        oracle
            .methods()
            .owner()
            .simulate(Execution::state_read_only())
            .await
            .unwrap()
            .value,
        State::Initialized(Identity::Address(fixture.outsider.address()))
    );
    assert_eq!(
        oracle
            .methods()
            .get_cosigner()
            .simulate(Execution::state_read_only())
            .await
            .unwrap()
            .value,
        Some(fixture.outsider.address())
    );
    assert_eq!(
        repaired
            .registry
            .methods()
            .get_prop_oracle_id()
            .simulate(Execution::state_read_only())
            .await
            .unwrap()
            .value,
        repaired.oracle_id
    );
    assert_eq!(fixture.session().await, original_session);
    fixture.close_session().await;

    let provider = fixture.deployer.try_provider().unwrap();
    let before = provider.latest_block_height().await.unwrap();
    // With no explicit oracle, the repaired registry is also sufficient.
    config.existing_oracle = None;
    let again = PropDeployment::deploy(&fixture.deployer, &config)
        .await
        .unwrap();
    assert_eq!(again.oracle_id, repaired.oracle_id);
    assert_eq!(provider.latest_block_height().await.unwrap(), before);
}

#[tokio::test]
async fn bad_oracle_bindings_fail_before_deployment_transactions() {
    let fixture = PropFixture::new().await;
    let provider = fixture.deployer.try_provider().unwrap();
    let mut config = resume(&fixture);
    config.salt = Salt::new([0x6c; 32]);
    for oracle in [
        ContractId::zeroed(),
        ContractId::new([0xf1; 32]),
        fixture.deployment.pool_id,
    ] {
        config.existing_oracle = Some(oracle);
        let before = provider.latest_block_height().await.unwrap();
        assert!(
            PropDeployment::deploy(&fixture.deployer, &config)
                .await
                .is_err()
        );
        assert_eq!(provider.latest_block_height().await.unwrap(), before);
    }
    config.existing_oracle = Some(fixture.deployment.oracle_id);
    config.existing_pool = Some(ContractId::new([0xf2; 32]));
    let before = provider.latest_block_height().await.unwrap();
    let error = PropDeployment::deploy(&fixture.deployer, &config)
        .await
        .err()
        .unwrap();
    assert!(
        error.to_string().contains("pool cannot be changed"),
        "{error:#}"
    );
    assert_eq!(provider.latest_block_height().await.unwrap(), before);

    // No saved IDs: the default derivation finds the fixture's initialized
    // oracle. It must also reject a conflicting pool without sending anything.
    let mut derived = PropDeployConfig::new(fixture.collateral_asset);
    derived.existing_pool = config.existing_pool;
    let error = PropDeployment::deploy(&fixture.deployer, &derived)
        .await
        .err()
        .unwrap();
    assert!(
        error.to_string().contains("pool cannot be changed"),
        "{error:#}"
    );
    assert_eq!(provider.latest_block_height().await.unwrap(), before);

    // A failed registry read is also an error, never a fresh-deploy signal.
    config.existing_oracle = None;
    config.existing_registry.as_mut().unwrap().registry_id = ContractId::new([0xf3; 32]);
    assert!(
        PropDeployment::deploy(&fixture.deployer, &config)
            .await
            .is_err()
    );
    assert_eq!(provider.latest_block_height().await.unwrap(), before);
}

#[tokio::test]
async fn pinned_oracle_resumes_after_create_or_proxy_initialization() {
    let fixture = PropFixture::new().await;
    for initialized_proxy in [false, true] {
        let mut config = resume(&fixture);
        config.salt = Salt::new([if initialized_proxy { 0x72 } else { 0x71 }; 32]);
        config.existing_pool = Some(fixture.deployment.pool_id);
        config.existing_price_feed = Some(fixture.deployment.price_feed_id);
        config.cosigner = Some(fixture.outsider.address());
        let bootstrap = upload_implementation(
            &fixture.deployer,
            PROP_ACCOUNT_ORACLE_BYTECODE,
            PROP_ACCOUNT_ORACLE_STORAGE,
            Configurables::default(),
            &config,
        )
        .await
        .unwrap();
        let contract = regular_contract_with_implementation_storage(
            PROP_ACCOUNT_ORACLE_PROXY_BYTECODE,
            PROP_ACCOUNT_ORACLE_PROXY_STORAGE,
            PROP_ACCOUNT_ORACLE_STORAGE,
            config.salt,
        )
        .unwrap()
        .with_configurables(
            PropAccountOracleProxyContractConfigurables::default()
                .with_INITIAL_OWNER(State::Initialized(Identity::Address(
                    fixture.deployer.address(),
                )))
                .unwrap()
                .with_INITIAL_TARGET(bootstrap.into())
                .unwrap(),
        );
        let (oracle_id, _) = deploy_regular(&fixture.deployer, contract).await.unwrap();
        if initialized_proxy {
            PropAccountOracleProxyContract::new(oracle_id, fixture.deployer.clone())
                .methods()
                .initialize_proxy()
                .call()
                .await
                .unwrap();
        }
        config.existing_oracle = Some(oracle_id);
        let deployed = PropDeployment::deploy(&fixture.deployer, &config)
            .await
            .unwrap();
        assert_eq!(deployed.oracle_id, oracle_id);
        assert_eq!(
            deployed
                .oracle
                .methods()
                .get_cosigner()
                .simulate(Execution::state_read_only())
                .await
                .unwrap()
                .value,
            Some(fixture.outsider.address())
        );
        assert_eq!(
            deployed
                .oracle
                .methods()
                .get_prop_margin_pool()
                .simulate(Execution::state_read_only())
                .await
                .unwrap()
                .value,
            Some(fixture.deployment.pool_id)
        );
    }
}