use super::*;
use crate::{
prop::{
PropOrderBookCleanup,
SessionView,
},
prop_force_close::generate_pool_force_close_signing_payload,
};
use fuels::crypto::Message;
use sha2::{
Digest,
Sha256,
};
async fn armed_fixture(duration: u64) -> PropFixture {
let fixture = PropFixture::new().await;
let mut params = turbo_tier_params();
params.duration = duration;
publish_turbo_tier_with_params(&fixture, params, turbo_terms(DRAWDOWN_SCHEDULE))
.await;
open_turbo_session(&fixture).await;
fixture
}
async fn sign(signer: &Wallet, message: Message) -> Signature {
Signature::Secp256k1(Secp256k1 {
bits: *signer.signer().sign(message).await.unwrap(),
})
}
async fn state(fixture: &PropFixture) -> (Option<SessionView>, Vec<u128>) {
let mut balances = Vec::new();
for asset in [fixture.base_asset, fixture.collateral_asset] {
balances.push(fixture.pool_balance(asset).await as u128);
balances.push(fixture.account_balance(asset).await as u128);
for wallet in [&fixture.user, &fixture.deployer, &fixture.outsider] {
balances.push(wallet.get_asset_balance(&asset).await.unwrap());
}
}
(fixture.session_opt().await, balances)
}
#[tokio::test]
async fn unrelated_relayer_closes_a_healthy_perpetual_session_without_receiving_payouts()
{
let fixture = armed_fixture(u64::MAX).await;
let session = fixture.session().await;
let signature = fixture
.force_close_cosignature(session.session_id, &fixture.deployer)
.await;
for role in [0, ACCOUNT_CLOSER_ROLE] {
assert!(
!fixture
.deployment
.pool
.methods()
.has_role(role, Identity::Address(fixture.outsider.address()))
.simulate(Execution::state_read_only())
.await
.unwrap()
.value
);
}
fixture
.deployment
.pool
.methods()
.set_liquidator(Identity::Address(fixture.deployer.address()))
.call()
.await
.unwrap();
let gain = 500_000;
fixture
.user
.force_transfer_to_contract(
fixture.child_id,
gain,
fixture.collateral_asset,
TxPolicies::default(),
)
.await
.unwrap();
advance_eight_days(&fixture).await;
assert_eq!(fixture.session().await.expires_at, u64::MAX);
assert!(!fixture.is_liquidatable().await);
let parent = fixture
.user
.get_asset_balance(&fixture.collateral_asset)
.await
.unwrap();
let platform = fixture
.deployer
.get_asset_balance(&fixture.collateral_asset)
.await
.unwrap();
let relayer = fixture
.outsider
.get_asset_balance(&fixture.collateral_asset)
.await
.unwrap();
let relayer_base = fixture
.outsider
.get_asset_balance(&fixture.base_asset)
.await
.unwrap();
let response = fixture
.force_close_session_signed(
&fixture.outsider,
session.session_id,
vec![],
signature,
)
.await
.unwrap();
let closed = fixture.closed_event(&response);
assert_eq!(closed.reason, SettlementReason::PermissionedClosure);
assert_eq!(closed.session_id, session.session_id);
assert_eq!(closed.user_net, gain);
assert_eq!(closed.payout_parent, vec![(fixture.collateral_asset, gain)]);
assert_eq!(
fixture
.user
.get_asset_balance(&fixture.collateral_asset)
.await
.unwrap(),
parent + u128::from(gain)
);
assert_eq!(
fixture
.deployer
.get_asset_balance(&fixture.collateral_asset)
.await
.unwrap(),
platform + u128::from(TURBO_DEPOSIT)
);
assert_eq!(
fixture
.outsider
.get_asset_balance(&fixture.collateral_asset)
.await
.unwrap(),
relayer
);
assert_eq!(
fixture
.outsider
.get_asset_balance(&fixture.base_asset)
.await
.unwrap(),
relayer_base
);
let seized = response.decode_logs_with_type::<SessionSeized>().unwrap();
assert_eq!(seized.len(), 1);
assert_eq!(seized[0].reason, SettlementReason::PermissionedClosure);
assert_eq!(
seized[0].liquidator,
Identity::Address(fixture.deployer.address())
);
let tickets = response.decode_logs_with_type::<TicketSettled>().unwrap();
assert_eq!(tickets.len(), 1);
assert_eq!(tickets[0].to_platform, TURBO_DEPOSIT);
assert_eq!(tickets[0].absorbed_by_pool, 0);
assert!(
response
.decode_logs_with_type::<SessionRolledOver>()
.unwrap()
.is_empty()
);
assert!(fixture.session_opt().await.is_none());
}
#[tokio::test]
async fn missing_cosigner_wrong_key_and_malformed_signatures_never_authorize_closure() {
let fixture = PropFixture::new().await;
let session = fixture.session().await;
let valid = fixture
.force_close_cosignature(session.session_id, &fixture.deployer)
.await;
let before = state(&fixture).await;
let error = fixture
.force_close_session_signed(&fixture.outsider, session.session_id, vec![], valid)
.await
.unwrap_err();
assert!(
error.to_string().contains("ExpiryCosignerNotSet"),
"{error:#}"
);
assert_eq!(state(&fixture).await, before);
fixture
.deployment
.pool
.methods()
.set_expiry_cosigner(fixture.deployer.address())
.call()
.await
.unwrap();
let wrong = fixture
.force_close_cosignature(session.session_id, &fixture.user)
.await;
let malformed = Signature::Secp256k1(Secp256k1 { bits: [0; 64] });
for signature in [wrong, malformed] {
for caller in [&fixture.deployer, &fixture.outsider] {
let error = fixture
.force_close_session_signed(
caller,
session.session_id,
vec![],
signature.clone(),
)
.await
.unwrap_err();
assert!(
error.to_string().contains("InvalidForceCloseCosigner"),
"{error:#}"
);
assert_eq!(state(&fixture).await, before);
}
}
}
#[tokio::test]
async fn authorization_binds_chain_pool_account_session_and_action_domain() {
let fixture = armed_fixture(u64::MAX).await;
let chain = *fixture
.user
.provider()
.consensus_parameters()
.await
.unwrap()
.chain_id();
let pool = fixture.deployment.pool_id;
let account = fixture.child_id;
let session = fixture.session().await.session_id;
let before = state(&fixture).await;
for (chain, pool, account, id) in [
(chain + 1, pool, account, session),
(chain, ContractId::new([0x66; 32]), account, session),
(chain, pool, ContractId::new([0x77; 32]), session),
(chain, pool, account, session + 1),
] {
let signature = sign(
&fixture.deployer,
generate_pool_force_close_signing_payload(chain, pool, account, id),
)
.await;
let error = fixture
.force_close_session_signed(&fixture.outsider, session, vec![], signature)
.await
.unwrap_err();
assert!(
error.to_string().contains("InvalidForceCloseCosigner"),
"{error:#}"
);
assert_eq!(state(&fixture).await, before);
}
for tag in [0x02, 0x03, 0x04] {
let mut bytes = vec![tag];
bytes.extend_from_slice(&chain.to_be_bytes());
bytes.extend_from_slice(pool.as_slice());
bytes.extend_from_slice(account.as_slice());
bytes.extend_from_slice(&session.to_be_bytes());
let signature = sign(
&fixture.deployer,
Message::from_bytes(Sha256::digest(bytes).into()),
)
.await;
let error = fixture
.force_close_session_signed(&fixture.outsider, session, vec![], signature)
.await
.unwrap_err();
assert!(
error.to_string().contains("InvalidForceCloseCosigner"),
"{error:#}"
);
assert_eq!(state(&fixture).await, before);
}
}
#[tokio::test]
async fn retired_authorization_cannot_close_a_replacement_session() {
let fixture = armed_fixture(u64::MAX).await;
let session = fixture.session().await.session_id;
let signature = fixture
.force_close_cosignature(session, &fixture.deployer)
.await;
let before = state(&fixture).await;
let error = fixture
.force_close_session_signed(
&fixture.outsider,
session + 1,
vec![],
signature.clone(),
)
.await
.unwrap_err();
assert!(error.to_string().contains("SessionIdMismatch"), "{error:#}");
assert_eq!(state(&fixture).await, before);
fixture
.force_close_session_signed(&fixture.outsider, session, vec![], signature.clone())
.await
.unwrap();
let after_close = state(&fixture).await;
let error = fixture
.force_close_session_signed(&fixture.outsider, session, vec![], signature.clone())
.await
.unwrap_err();
assert!(error.to_string().contains("NoActiveSession"), "{error:#}");
assert_eq!(state(&fixture).await, after_close);
let reopened = start_turbo_session(&fixture).await;
assert!(reopened.session_id > session);
let before = state(&fixture).await;
let error = fixture
.force_close_session_signed(&fixture.outsider, session, vec![], signature.clone())
.await
.unwrap_err();
assert!(error.to_string().contains("SessionIdMismatch"), "{error:#}");
let error = fixture
.force_close_session_signed(
&fixture.outsider,
reopened.session_id,
vec![],
signature,
)
.await
.unwrap_err();
assert!(
error.to_string().contains("InvalidForceCloseCosigner"),
"{error:#}"
);
assert_eq!(state(&fixture).await, before);
}
#[tokio::test]
async fn expiry_and_forced_close_signatures_are_not_interchangeable() {
let fixture = armed_fixture(604_800).await;
advance_past_expiry(&fixture).await;
let session = fixture.session().await.session_id;
let expiry = expiry_cosignature(&fixture, 0, false, fixture.deployer.clone()).await;
let close = fixture
.force_close_cosignature(session, &fixture.deployer)
.await;
let before = state(&fixture).await;
let error = fixture
.force_close_session_signed(&fixture.outsider, session, vec![], expiry)
.await
.unwrap_err();
assert!(
error.to_string().contains("InvalidForceCloseCosigner"),
"{error:#}"
);
let error = settle_with(
&fixture,
0,
false,
close.clone(),
Some(fixture.outsider.clone()),
)
.await
.unwrap_err();
assert!(
error.to_string().contains("InvalidExpiryCosigner"),
"{error:#}"
);
assert_eq!(state(&fixture).await, before);
fixture
.force_close_session_signed(&fixture.outsider, session, vec![], close)
.await
.unwrap();
}
#[tokio::test]
async fn signer_rotation_rejects_the_previous_signer_and_accepts_the_new_one() {
let fixture = armed_fixture(u64::MAX).await;
let session = fixture.session().await.session_id;
let old = fixture
.force_close_cosignature(session, &fixture.deployer)
.await;
fixture
.deployment
.pool
.methods()
.set_expiry_cosigner(fixture.outsider.address())
.call()
.await
.unwrap();
let before = state(&fixture).await;
let error = fixture
.force_close_session_signed(&fixture.user, session, vec![], old)
.await
.unwrap_err();
assert!(
error.to_string().contains("InvalidForceCloseCosigner"),
"{error:#}"
);
assert_eq!(state(&fixture).await, before);
let current = fixture
.force_close_cosignature(session, &fixture.outsider)
.await;
fixture
.force_close_session_signed(&fixture.user, session, vec![], current)
.await
.unwrap();
assert!(fixture.session_opt().await.is_none());
}
#[tokio::test]
async fn authorization_precedes_pause_and_cleanup_and_a_paused_close_can_be_retried() {
let fixture = armed_fixture(u64::MAX).await;
let session = fixture.session().await.session_id;
let signature = fixture
.force_close_cosignature(session, &fixture.deployer)
.await;
fixture
.deployment
.pool
.methods()
.pause()
.call()
.await
.unwrap();
let before = state(&fixture).await;
let bad_cleanup = vec![PropOrderBookCleanup {
order_book: ContractId::new([0x77; 32]),
order_ids: vec![],
}];
let wrong = fixture
.force_close_cosignature(session, &fixture.user)
.await;
let error = fixture
.force_close_session_signed(
&fixture.outsider,
session,
bad_cleanup.clone(),
wrong,
)
.await
.unwrap_err();
assert!(
error.to_string().contains("InvalidForceCloseCosigner"),
"{error:#}"
);
let error = fixture
.force_close_session_signed(
&fixture.outsider,
session + 1,
bad_cleanup.clone(),
signature.clone(),
)
.await
.unwrap_err();
assert!(error.to_string().contains("SessionIdMismatch"), "{error:#}");
let error = fixture
.force_close_session_signed(
&fixture.outsider,
session,
bad_cleanup,
signature.clone(),
)
.await
.unwrap_err();
assert!(error.to_string().contains("Paused"), "{error:#}");
assert_eq!(state(&fixture).await, before);
fixture
.deployment
.pool
.methods()
.unpause()
.call()
.await
.unwrap();
fixture
.force_close_session_signed(&fixture.outsider, session, vec![], signature)
.await
.unwrap();
}
#[tokio::test]
async fn stale_prices_preserve_the_session_and_funds_and_allow_retry_with_the_same_signature()
{
let fixture = PropFixture::new().await;
let mut params = turbo_tier_params();
params.duration = u64::MAX;
params.max_price_age = 1_000_100;
publish_turbo_tier_with_params(&fixture, params, turbo_terms(DRAWDOWN_SCHEDULE))
.await;
open_turbo_session(&fixture).await;
fixture
.user
.force_transfer_to_contract(
fixture.child_id,
100_000_000,
fixture.base_asset,
TxPolicies::default(),
)
.await
.unwrap();
let session = fixture.session().await.session_id;
let signature = fixture
.force_close_cosignature(session, &fixture.deployer)
.await;
advance_eight_days(&fixture).await;
let before = state(&fixture).await;
let error = fixture
.force_close_session_signed(&fixture.outsider, session, vec![], signature.clone())
.await
.unwrap_err();
assert!(error.to_string().contains("PriceStale"), "{error:#}");
assert_eq!(state(&fixture).await, before);
let now = fixture
.user
.provider()
.latest_block_time()
.await
.unwrap()
.unwrap()
.timestamp() as u64;
fixture
.deployment
.price_feed
.methods()
.publish_prices(vec![PriceInput {
asset: fixture.base_asset,
bid: 2_000_000,
ask: 2_000_000,
timestamp: now,
}])
.call()
.await
.unwrap();
fixture
.force_close_session_signed(&fixture.outsider, session, vec![], signature)
.await
.unwrap();
assert!(fixture.session_opt().await.is_none());
}
#[tokio::test]
async fn a_rollover_does_not_invalidate_authorization_for_the_same_session() {
let fixture = armed_fixture(604_800).await;
let session = fixture.session().await;
let signature = fixture
.force_close_cosignature(session.session_id, &fixture.deployer)
.await;
fixture
.user
.force_transfer_to_contract(
fixture.child_id,
ROLLOVER_BAR,
fixture.collateral_asset,
TxPolicies::default(),
)
.await
.unwrap();
advance_past_expiry(&fixture).await;
try_roll(&fixture).await.unwrap();
let rolled = fixture.session().await;
assert_eq!(rolled.session_id, session.session_id);
assert!(rolled.expires_at > session.expires_at);
fixture
.force_close_session_signed(
&fixture.outsider,
session.session_id,
vec![],
signature,
)
.await
.unwrap();
}