o2-tools 0.4.3-rc

Reusable tooling for trade account and order book contract interactions on Fuel
Documentation
//! The account oracle: three pointers that every prop account in the system
//! reads on every call.
//!
//! It is the smallest contract in the family and the most dangerous one. The
//! implementation pointer is the upgrade authority for every account at once;
//! the cosigner is the platform's veto over every batch at once. Both are
//! owner-only and both are behind the pause.

use super::*;

/// The oracle's pointers are owner-only, refuse the zero value, and refuse an
/// implementation that is not a deployed blob - a typo there would brick every
/// account in the system simultaneously, since the proxy resolves its target
/// here on every single call.
#[tokio::test]
async fn the_oracle_refuses_a_pointer_that_would_brick_every_account() {
    let fixture = PropFixture::new().await;

    let oracle = &fixture.deployment.oracle;
    let intruder = oracle.clone().with_account(fixture.outsider.clone());
    let live_impl = oracle
        .methods()
        .get_prop_account_impl()
        .simulate(Execution::state_read_only())
        .await
        .unwrap()
        .value
        .unwrap();

    for (name, error) in [
        (
            "set_prop_account_impl",
            intruder
                .methods()
                .set_prop_account_impl(live_impl)
                .call()
                .await
                .unwrap_err(),
        ),
        (
            "set_cosigner",
            intruder
                .methods()
                .set_cosigner(fixture.outsider.address())
                .call()
                .await
                .unwrap_err(),
        ),
        (
            "pause",
            intruder.methods().pause().call().await.unwrap_err(),
        ),
    ] {
        assert!(
            error.to_string().contains("NotOwner"),
            "{name} is the owner's alone: {error:#}"
        );
    }

    let error = oracle
        .methods()
        .set_prop_account_impl(ContractId::zeroed())
        .call()
        .await
        .unwrap_err();
    assert!(
        error.to_string().contains("Implementation is zero"),
        "a zero target is not an upgrade: {error:#}"
    );

    // A well-formed id that is not a blob. Every account would resolve its
    // fallback to nothing and stop answering, so this is refused rather than
    // discovered later.
    //
    // Refused by the VM, not by the contract: `bsiz` PANICS with
    // `BlobNotFound` on an unknown blob rather than reporting a zero length,
    // so `validate_blob`'s own "Blob does not exist" message is unreachable.
    // The guard still holds - it is the panic that enforces it - but a reader
    // expecting the friendly error will never see it.
    let error = oracle
        .methods()
        .set_prop_account_impl(ContractId::new([0xa1; 32]))
        .call()
        .await
        .unwrap_err();
    assert!(
        error.to_string().contains("BlobNotFound")
            || error.to_string().contains("Blob does not exist"),
        "the target must be code that exists: {error:#}"
    );

    let error = oracle
        .methods()
        .set_cosigner(Address::zeroed())
        .call()
        .await
        .unwrap_err();
    assert!(
        error.to_string().contains("Cosigner is zero"),
        "a zero cosigner would make every recovered address match nothing: {error:#}"
    );

    // Paused, even the owner cannot move them - the pause is the "stop
    // touching it" switch during an incident.
    oracle.methods().pause().call().await.unwrap();
    let error = oracle
        .methods()
        .set_prop_account_impl(live_impl)
        .call()
        .await
        .unwrap_err();
    assert!(
        error.to_string().contains("Paused"),
        "no upgrades during an incident: {error:#}"
    );
    let error = oracle
        .methods()
        .set_cosigner(fixture.deployer.address())
        .call()
        .await
        .unwrap_err();
    assert!(
        error.to_string().contains("Paused"),
        "nor cosigner rotations: {error:#}"
    );

    // And the accounts keep working through it: the pause stops WRITES to the
    // oracle, not reads of it.
    oracle.methods().unpause().call().await.unwrap();
    assert_eq!(
        oracle
            .methods()
            .get_prop_account_impl()
            .simulate(Execution::state_read_only())
            .await
            .unwrap()
            .value,
        Some(live_impl)
    );
}

/// Rotating the cosigner is the platform's system-wide kill switch: every
/// batch already signed by the old key stops verifying, on every account, in
/// the same block. This is the property an incident response depends on, so
/// it is worth pinning that it is really one write.
#[tokio::test]
async fn rotating_the_cosigner_kills_every_signature_the_old_key_issued() {
    let mut fixture = PropFixture::new().await;

    // A batch signed and ready to go under the current cosigner.
    let draw = fixture.draw_call(100_000);
    let (user_signature, cosigner_signature, calls) =
        fixture.signed_calls(vec![draw]).await;

    fixture
        .deployment
        .oracle
        .methods()
        .set_cosigner(fixture.outsider.address())
        .call()
        .await
        .unwrap();

    let error = fixture
        .account
        .methods()
        .call_contracts(user_signature, cosigner_signature, calls)
        .with_contracts(&[
            &fixture.deployment.oracle,
            &fixture.deployment.pool,
            &fixture.deployment.price_feed,
            &fixture.deployment.registry,
            &fixture.order_book.order_book,
        ])
        .with_variable_output_policy(VariableOutputPolicy::Exactly(1))
        .call()
        .await
        .unwrap_err();
    assert!(
        error.to_string().contains("InvalidCosigner"),
        "one write retires every signature the old key ever made: {error:#}"
    );
    assert_eq!(fixture.session().await.drawn_quote, 0);

    // The owner route is deliberately NOT cosigned, so the parent can still
    // reach his own money while the platform is dark. That is the whole point
    // of splitting the two routes.
    fixture.close_session().await;
    assert!(
        fixture
            .deployment
            .pool
            .methods()
            .get_session(fixture.child_id)
            .simulate(Execution::state_read_only())
            .await
            .unwrap()
            .value
            .is_none(),
        "a dark cosigner must never trap a user inside a session"
    );

    // Rotating back restores service.
    fixture
        .deployment
        .oracle
        .methods()
        .set_cosigner(fixture.deployer.address())
        .call()
        .await
        .unwrap();
    assert_eq!(
        fixture
            .deployment
            .oracle
            .methods()
            .get_cosigner()
            .simulate(Execution::state_read_only())
            .await
            .unwrap()
            .value,
        Some(fixture.deployer.address())
    );
}