multisig 0.1.0

Created with Anchor
Documentation
use multisig::{
    Asset, FractionalThreshold, Group, GroupMember, Permissions, ProposalAsset,
    ProposalAssetThresholdState,
};
use solana_sdk::pubkey::Pubkey;

fn assert_anchor_error<T>(
    result: Result<T, anchor_lang::error::Error>,
    expected_name: &str,
    expected_code: u32,
    expected_msg: &str,
) {
    match result.err().expect("expected an error") {
        anchor_lang::error::Error::AnchorError(error) => {
            assert_eq!(error.error_name, expected_name);
            assert_eq!(error.error_code_number, expected_code);
            assert_eq!(error.error_msg, expected_msg);
        }
        other => panic!("expected AnchorError, got {other:?}"),
    }
}

#[test]
fn fractional_threshold_compares_without_division() {
    let threshold = FractionalThreshold::new_from_values(2, 3).unwrap();

    assert!(threshold.less_than_or_equal(4, 6).unwrap());
    assert!(threshold.less_than_or_equal(5, 6).unwrap());
    assert!(!threshold.less_than_or_equal(3, 6).unwrap());
    assert_eq!(threshold.numerator, 2);
    assert_eq!(threshold.denominator, 3);
}

#[test]
fn fractional_threshold_rejects_overlapping_pairs_and_allows_unanimity() {
    let pass = FractionalThreshold::new_from_values(1, 2).unwrap();
    let overlapping_fail = FractionalThreshold::new_from_values(1, 3).unwrap();
    assert_anchor_error(
        FractionalThreshold::validate_non_overlapping_pair(pass, overlapping_fail),
        "InvalidThreshold",
        6036,
        "Invalid threshold configuration",
    );

    let non_overlapping_fail = FractionalThreshold::new_from_values(2, 3).unwrap();
    assert!(FractionalThreshold::validate_non_overlapping_pair(pass, non_overlapping_fail).is_ok());

    let unanimity = FractionalThreshold::new_from_values(1, 1).unwrap();
    assert!(FractionalThreshold::validate_non_overlapping_pair(unanimity, pass).is_ok());
}

#[test]
fn permissions_reject_unknown_bits() {
    assert!(Permissions::try_from(0b0000_0011).is_ok());
    assert_anchor_error(
        Permissions::try_from(0b0000_0100),
        "InvalidPermissions",
        6051,
        "Invalid permissions",
    );
}

#[test]
fn member_constructors_reject_zero_and_oversized_weight() {
    let user = Pubkey::new_unique();
    let group = Pubkey::new_unique();
    let permissions = Permissions::try_from(0b0000_0011).unwrap();

    assert_anchor_error(
        GroupMember::new(user, group, permissions, 0, 255, 100),
        "InvalidMemberWeight",
        6046,
        "Invalid member weight",
    );
    assert_anchor_error(
        GroupMember::new(user, group, permissions, 101, 255, 100),
        "InvalidMemberWeight",
        6046,
        "Invalid member weight",
    );

    let asset = Pubkey::new_unique();
    assert_anchor_error(
        multisig::AssetMember::new(user, group, asset, permissions, 0, 255, 100),
        "InvalidMemberWeight",
        6046,
        "Invalid member weight",
    );
    assert_anchor_error(
        multisig::AssetMember::new(user, group, asset, permissions, 101, 255, 100),
        "InvalidMemberWeight",
        6046,
        "Invalid member weight",
    );
}

#[test]
fn group_and_asset_quorum_counts_are_positive_and_can_equal_member_count() {
    let pass = FractionalThreshold::new_from_values(1, 2).unwrap();
    let fail = FractionalThreshold::new_from_values(2, 3).unwrap();

    assert_anchor_error(
        Group::new(
            Pubkey::new_unique(),
            Pubkey::new_unique(),
            pass,
            fail,
            pass,
            fail,
            pass,
            fail,
            1,
            0,
            100,
            0,
            5,
            255,
        ),
        "InvalidMemberCount",
        6045,
        "Invalid member count",
    );

    let mut group = Group::new(
        Pubkey::new_unique(),
        Pubkey::new_unique(),
        pass,
        fail,
        pass,
        fail,
        pass,
        fail,
        5,
        5,
        100,
        0,
        5,
        255,
    )
    .unwrap();
    assert_anchor_error(
        group.set_minimum_vote_count(0),
        "InvalidMemberCount",
        6045,
        "Invalid member count",
    );
    assert!(group.set_minimum_vote_count(5).is_ok());

    let mut asset = Asset::new(
        Pubkey::new_unique(),
        pass,
        fail,
        pass,
        fail,
        pass,
        fail,
        pass,
        fail,
        3,
        3,
        3,
        254,
        253,
    )
    .unwrap();
    assert_anchor_error(
        asset.set_minimum_vote_count(1),
        "InvalidThreshold",
        6036,
        "Invalid threshold configuration",
    );
    assert!(asset.set_minimum_vote_count(3).is_ok());
}

#[test]
fn proposal_asset_vote_state_is_monotonic_after_threshold() {
    let asset = Pubkey::new_unique();
    let mut proposal_asset = ProposalAsset::new(0, 2, 255, asset);

    proposal_asset.increment_vote_count().unwrap();
    proposal_asset.add_use_vote_weight(10);

    assert_eq!(proposal_asset.vote_count, 1);
    assert_eq!(proposal_asset.use_vote_weight, 10);

    proposal_asset
        .set_threshold_state(ProposalAssetThresholdState::UseThresholdReached)
        .unwrap();

    assert_anchor_error(
        proposal_asset.set_threshold_state(ProposalAssetThresholdState::NotUseThresholdReached),
        "StateAlreadyFinalized",
        6047,
        "State already finalized",
    );
}

#[test]
fn proposal_asset_rejects_noop_threshold_transition() {
    let asset = Pubkey::new_unique();
    let mut proposal_asset = ProposalAsset::new(0, 0, 255, asset);

    assert_anchor_error(
        proposal_asset.set_threshold_state(ProposalAssetThresholdState::NoThresholdReached),
        "InvalidStateTransition",
        6048,
        "Invalid state transition",
    );
}