Skip to content

Commit 4db1def

Browse files
committed
Expand test coverage for dex competitions, governance privacy, insurance triggers and staking penalties
- dex: cover the trading-competition lifecycle (creation, validation, scoring via swaps, pro-rata reward claims, double-claim rejection, activate/deactivate toggles), the cross-chain trade flow (create, attach bridge request, finalize, authorization and error branches) and liquidity-mining campaign configuration (#997) - governance: test the private voting feature end to end - commit, start reveal phase, reveal with correct/wrong salt, reveal before phase start, double reveal, plus vote_with_signature with and without an approval and register_public_key signer gating (#998) - insurance: cover claim-trigger registration and deactivation, the admin/oracle authorization boundary of report_oracle_event, one-shot firing, threshold met/unmet boundaries and the auto-paid claim path (#999) - staking: pin the unbonding period boundary (claim exactly at and one block before/after), fix a mangled attribute that silently disabled two tests, and assert exact early-withdrawal penalty math on non-round amounts for default and custom bps (#1000) Closes #997, closes #998, closes #999, closes #1000
1 parent aeecc2d commit 4db1def

4 files changed

Lines changed: 937 additions & 5 deletions

File tree

contracts/dex/src/tests.rs

Lines changed: 338 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -764,4 +764,342 @@ mod tests {
764764
let actual_output = result.expect("swap succeeds");
765765
assert!(actual_output >= min_output, "Output should meet minimum");
766766
}
767+
768+
// =========================================================================
769+
// Trading competitions / cross-chain trades / liquidity mining (Issue #997)
770+
// =========================================================================
771+
772+
#[ink::test]
773+
fn trading_competition_scores_swaps_and_pays_proportional_rewards() {
774+
let mut dex = setup_dex();
775+
let pair_id = create_pool(&mut dex);
776+
let accounts = test::default_accounts::<DefaultEnvironment>();
777+
778+
test::set_block_number::<DefaultEnvironment>(100);
779+
780+
// Admin creates a competition on pair 1 running blocks 100..110.
781+
let competition_id = dex
782+
.create_trading_competition(
783+
Some(pair_id),
784+
String::from("Weekly Volume Race"),
785+
10_000,
786+
100,
787+
110,
788+
1,
789+
10,
790+
String::from("PCG"),
791+
)
792+
.expect("competition creation should work");
793+
794+
let competition = dex.get_trading_competition(competition_id).unwrap();
795+
assert!(competition.active);
796+
assert_eq!(
797+
dex.get_active_competitions().len(),
798+
1,
799+
"competition must be listed as active inside its window"
800+
);
801+
802+
// Claiming before the competition ends is rejected even with score.
803+
test::set_caller::<DefaultEnvironment>(accounts.bob);
804+
assert_eq!(
805+
dex.claim_competition_reward(competition_id),
806+
Err(Error::InvalidRequest)
807+
);
808+
809+
// Bob and Charlie trade inside the window; their quote output is the
810+
// scored volume.
811+
let _ = dex.swap_exact_base_for_quote(pair_id, 1_000, 1).unwrap();
812+
test::set_caller::<DefaultEnvironment>(accounts.charlie);
813+
let _ = dex.swap_exact_base_for_quote(pair_id, 2_000, 1).unwrap();
814+
815+
let bob_score = dex.get_competition_score(competition_id, accounts.bob);
816+
let charlie_score = dex.get_competition_score(competition_id, accounts.charlie);
817+
assert!(bob_score > 0, "bob must accrue score from his swap");
818+
assert!(charlie_score > bob_score, "larger trade -> larger score");
819+
820+
assert_eq!(
821+
dex.list_competition_participants(competition_id),
822+
vec![accounts.bob, accounts.charlie]
823+
);
824+
let leaderboard = dex.get_competition_leaderboard(competition_id);
825+
assert_eq!(leaderboard.len(), 2);
826+
assert!(leaderboard.contains(&(accounts.bob, bob_score)));
827+
assert!(leaderboard.contains(&(accounts.charlie, charlie_score)));
828+
829+
// After the window closes rewards can be claimed pro rata.
830+
test::set_block_number::<DefaultEnvironment>(111);
831+
let expected = 10_000u128 * bob_score / (bob_score + charlie_score);
832+
assert!(expected > 0);
833+
test::set_caller::<DefaultEnvironment>(accounts.bob);
834+
let balance_before = dex.get_governance_balance(accounts.bob);
835+
let reward = dex.claim_competition_reward(competition_id).unwrap();
836+
assert_eq!(reward, expected);
837+
assert_eq!(
838+
dex.get_governance_balance(accounts.bob),
839+
balance_before + reward
840+
);
841+
842+
// Double claims are rejected...
843+
assert_eq!(
844+
dex.claim_competition_reward(competition_id),
845+
Err(Error::InvalidRequest)
846+
);
847+
848+
// ...as are claims from accounts that never traded.
849+
test::set_caller::<DefaultEnvironment>(accounts.django);
850+
assert_eq!(
851+
dex.claim_competition_reward(competition_id),
852+
Err(Error::InvalidRequest)
853+
);
854+
}
855+
856+
#[ink::test]
857+
fn trading_competition_admin_controls_and_validation() {
858+
let mut dex = setup_dex();
859+
let accounts = test::default_accounts::<DefaultEnvironment>();
860+
861+
test::set_block_number::<DefaultEnvironment>(100);
862+
863+
// Only the admin may create competitions.
864+
test::set_caller::<DefaultEnvironment>(accounts.bob);
865+
assert_eq!(
866+
dex.create_trading_competition(
867+
None,
868+
String::from("Bob's Race"),
869+
5_000,
870+
100,
871+
110,
872+
1,
873+
10,
874+
String::from("PCG")
875+
),
876+
Err(Error::Unauthorized)
877+
);
878+
879+
// Validation of title, window and reward amount.
880+
test::set_caller::<DefaultEnvironment>(accounts.alice);
881+
assert_eq!(
882+
dex.create_trading_competition(
883+
None,
884+
String::new(),
885+
5_000,
886+
100,
887+
110,
888+
1,
889+
10,
890+
String::from("PCG")
891+
),
892+
Err(Error::InvalidRequest)
893+
);
894+
assert_eq!(
895+
dex.create_trading_competition(
896+
None,
897+
String::from("Backwards"),
898+
5_000,
899+
110,
900+
100,
901+
1,
902+
10,
903+
String::from("PCG")
904+
),
905+
Err(Error::InvalidRequest)
906+
);
907+
assert_eq!(
908+
dex.create_trading_competition(
909+
None,
910+
String::from("No Reward"),
911+
0,
912+
100,
913+
110,
914+
1,
915+
10,
916+
String::from("PCG")
917+
),
918+
Err(Error::InvalidRequest)
919+
);
920+
921+
let competition_id = dex
922+
.create_trading_competition(
923+
None,
924+
String::from("Open Race"),
925+
5_000,
926+
100,
927+
110,
928+
1,
929+
10,
930+
String::from("PCG"),
931+
)
932+
.unwrap();
933+
934+
// Deactivation stops score accrual immediately...
935+
dex.deactivate_competition(competition_id).unwrap();
936+
assert!(!dex.get_trading_competition(competition_id).unwrap().active);
937+
assert!(dex.get_active_competitions().is_empty());
938+
939+
test::set_caller::<DefaultEnvironment>(accounts.bob);
940+
let other_pool = create_pool(&mut dex);
941+
let _ = dex.swap_exact_base_for_quote(other_pool, 1_000, 1);
942+
assert_eq!(
943+
dex.get_competition_score(competition_id, accounts.bob),
944+
0,
945+
"inactive competitions must not accrue score"
946+
);
947+
test::set_caller::<DefaultEnvironment>(accounts.alice);
948+
949+
// ...and re-activation restores it.
950+
dex.activate_competition(competition_id).unwrap();
951+
assert!(dex.get_trading_competition(competition_id).unwrap().active);
952+
953+
// Authorization and unknown ids on both toggles.
954+
test::set_caller::<DefaultEnvironment>(accounts.bob);
955+
assert_eq!(
956+
dex.deactivate_competition(competition_id),
957+
Err(Error::Unauthorized)
958+
);
959+
assert_eq!(
960+
dex.activate_competition(999),
961+
Err(Error::Unauthorized)
962+
);
963+
test::set_caller::<DefaultEnvironment>(accounts.alice);
964+
assert_eq!(
965+
dex.deactivate_competition(999),
966+
Err(Error::InvalidRequest)
967+
);
968+
assert_eq!(
969+
dex.activate_competition(999),
970+
Err(Error::InvalidRequest)
971+
);
972+
}
973+
974+
#[ink::test]
975+
fn cross_chain_trade_lifecycle_create_attach_finalize() {
976+
let mut dex = setup_dex();
977+
let pair_id = create_pool(&mut dex);
978+
let accounts = test::default_accounts::<DefaultEnvironment>();
979+
980+
// Bob opens an intent to move value to chain 2.
981+
test::set_caller::<DefaultEnvironment>(accounts.bob);
982+
let trade_id = dex
983+
.create_cross_chain_trade(
984+
pair_id,
985+
None,
986+
2,
987+
accounts.charlie,
988+
500,
989+
1,
990+
)
991+
.expect("cross-chain trade creation should work");
992+
assert_eq!(
993+
dex.get_portfolio_snapshot(accounts.bob)
994+
.cross_chain_positions,
995+
1,
996+
"a pending intent counts as an open cross-chain position"
997+
);
998+
999+
// Only the trader or admin may attach a bridge request.
1000+
test::set_caller::<DefaultEnvironment>(accounts.django);
1001+
assert_eq!(
1002+
dex.attach_bridge_request(trade_id, 7),
1003+
Err(Error::Unauthorized)
1004+
);
1005+
1006+
test::set_caller::<DefaultEnvironment>(accounts.bob);
1007+
dex.attach_bridge_request(trade_id, 7).unwrap();
1008+
assert_eq!(
1009+
dex.get_portfolio_snapshot(accounts.bob)
1010+
.cross_chain_positions,
1011+
1,
1012+
"bridge-requested trades are still open positions"
1013+
);
1014+
1015+
// Finalization is admin-only...
1016+
test::set_caller::<DefaultEnvironment>(accounts.bob);
1017+
assert_eq!(
1018+
dex.finalize_cross_chain_trade(trade_id),
1019+
Err(Error::Unauthorized)
1020+
);
1021+
1022+
// ...and settles the position.
1023+
test::set_caller::<DefaultEnvironment>(accounts.alice);
1024+
dex.finalize_cross_chain_trade(trade_id).unwrap();
1025+
assert_eq!(
1026+
dex.get_portfolio_snapshot(accounts.bob)
1027+
.cross_chain_positions,
1028+
0,
1029+
"settled intents no longer count as open positions"
1030+
);
1031+
}
1032+
1033+
#[ink::test]
1034+
fn cross_chain_trade_rejects_unknown_pair_and_unconfigured_route() {
1035+
let mut dex = setup_dex();
1036+
let pair_id = create_pool(&mut dex);
1037+
let accounts = test::default_accounts::<DefaultEnvironment>();
1038+
1039+
test::set_caller::<DefaultEnvironment>(accounts.bob);
1040+
1041+
// Unknown pool id.
1042+
assert_eq!(
1043+
dex.create_cross_chain_trade(999, None, 2, accounts.charlie, 500, 1),
1044+
Err(Error::PoolNotFound)
1045+
);
1046+
1047+
// Chain 3 has no configured bridge route/fee quote.
1048+
assert_eq!(
1049+
dex.quote_cross_chain_trade(3),
1050+
Err(Error::InvalidBridgeRoute)
1051+
);
1052+
assert_eq!(
1053+
dex.create_cross_chain_trade(pair_id, None, 3, accounts.charlie, 500, 1),
1054+
Err(Error::InvalidBridgeRoute)
1055+
);
1056+
1057+
// Attaching or finalizing unknown trade ids fails cleanly.
1058+
assert_eq!(
1059+
dex.attach_bridge_request(42, 1),
1060+
Err(Error::CrossChainTradeNotFound)
1061+
);
1062+
test::set_caller::<DefaultEnvironment>(accounts.alice);
1063+
assert_eq!(
1064+
dex.finalize_cross_chain_trade(42),
1065+
Err(Error::CrossChainTradeNotFound)
1066+
);
1067+
}
1068+
1069+
#[ink::test]
1070+
fn liquidity_mining_campaign_is_admin_only_and_validated() {
1071+
let mut dex = setup_dex();
1072+
let accounts = test::default_accounts::<DefaultEnvironment>();
1073+
1074+
// Non-admins cannot configure campaigns.
1075+
test::set_caller::<DefaultEnvironment>(accounts.bob);
1076+
assert_eq!(
1077+
dex.set_liquidity_mining_campaign(50, 10, 20, String::from("PCG")),
1078+
Err(Error::Unauthorized)
1079+
);
1080+
1081+
// Validation of emission rate, window and token symbol.
1082+
test::set_caller::<DefaultEnvironment>(accounts.alice);
1083+
assert_eq!(
1084+
dex.set_liquidity_mining_campaign(0, 10, 20, String::from("PCG")),
1085+
Err(Error::InvalidRequest)
1086+
);
1087+
assert_eq!(
1088+
dex.set_liquidity_mining_campaign(50, 20, 20, String::from("PCG")),
1089+
Err(Error::InvalidRequest)
1090+
);
1091+
assert_eq!(
1092+
dex.set_liquidity_mining_campaign(50, 10, 20, String::new()),
1093+
Err(Error::InvalidRequest)
1094+
);
1095+
1096+
// A valid campaign is stored and readable.
1097+
dex.set_liquidity_mining_campaign(50, 10, 20, String::from("PCG"))
1098+
.expect("valid campaign config should work");
1099+
let campaign = dex.get_liquidity_mining_campaign();
1100+
assert_eq!(campaign.emission_rate, 50);
1101+
assert_eq!(campaign.start_block, 10);
1102+
assert_eq!(campaign.end_block, 20);
1103+
assert_eq!(campaign.reward_token_symbol, String::from("PCG"));
1104+
}
7671105
}

0 commit comments

Comments
 (0)