Skip to content

Commit 3fa12fb

Browse files
authored
Merge pull request #3 from feat/add-secp256k1-signature
feat: add dual signature support (Ed25519 & Secp256k1)
2 parents 78f6261 + 369fdd3 commit 3fa12fb

File tree

4 files changed

+843
-118
lines changed

4 files changed

+843
-118
lines changed

tests/integration-tests/vault_integration_test.rs

Lines changed: 256 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,11 @@ impl VaultTestEnv {
222222
self.get_wbtc_token_client().balance(&self.treasurer)
223223
}
224224

225+
/// Get vault contract's WBTC balance
226+
fn get_vault_wbtc_balance(&self) -> i128 {
227+
self.get_wbtc_token_client().balance(&self.vault_addr)
228+
}
229+
225230
/// Get user's SolvBTC balance
226231
fn get_user_solvbtc_balance(&self) -> i128 {
227232
self.get_solvbtc_token_client().balance(&self.user)
@@ -371,8 +376,13 @@ impl VaultTestEnv {
371376
let eip712_message = self.create_eip712_signature_message(&message_hash_bytes);
372377
Self::debug_print_bytes("EIP712 message", &eip712_message);
373378

379+
// 3.5. Ed25519 requires an additional sha256 hash on the eip712_message
380+
let digest = self.env.crypto().sha256(&eip712_message);
381+
let digest_bytes: Bytes = digest.into();
382+
Self::debug_print_bytes("Final digest for Ed25519", &digest_bytes);
383+
374384
// 4. Convert message to signable format
375-
let message_vec = Self::bytes_to_vec_for_signing(&eip712_message);
385+
let message_vec = Self::bytes_to_vec_for_signing(&digest_bytes);
376386

377387
// 5. Get keypair and sign
378388
let (signing_key, verifying_key) = Self::create_real_keypair();
@@ -455,6 +465,8 @@ impl VaultTestEnv {
455465
&nav,
456466
&request_hash,
457467
&signature,
468+
&0u32,
469+
&0u32,
458470
)
459471
}
460472
}
@@ -741,7 +753,7 @@ fn test_complete_vault_withdraw_flow() {
741753
assert_eq!(domain_separator.len(), 32);
742754

743755
// Verify withdrawal settings
744-
let withdraw_verifier = vault_client.get_withdraw_verifier();
756+
let withdraw_verifier = vault_client.get_withdraw_verifier(&0u32);
745757
let withdraw_ratio = vault_client.get_withdraw_fee_ratio();
746758
let withdraw_currency = vault_client.get_withdraw_currency();
747759

@@ -758,7 +770,7 @@ fn test_complete_vault_withdraw_flow() {
758770
expected_verifier_bytes[0] = 0xDE;
759771
expected_verifier_bytes[1] = 0xAD;
760772
let expected_verifier = BytesN::from_array(&test_env.env, &expected_verifier_bytes);
761-
assert_eq!(withdraw_verifier, expected_verifier);
773+
assert_eq!(withdraw_verifier, Some(expected_verifier.into()));
762774
assert_eq!(withdraw_ratio, 100); // 1%
763775
assert!(withdraw_currency.is_some());
764776
assert_eq!(withdraw_currency.unwrap(), test_env.wbtc_token_addr);
@@ -818,15 +830,15 @@ fn test_withdraw_error_scenarios() {
818830
println!("=== Test 3: Contract initialization state ===");
819831

820832
let admin = vault_client.get_admin();
821-
let withdraw_verifier = vault_client.get_withdraw_verifier();
833+
let withdraw_verifier = vault_client.get_withdraw_verifier(&0u32);
822834
let withdraw_ratio = vault_client.get_withdraw_fee_ratio();
823835

824836
assert_eq!(admin, test_env.admin);
825837
let mut expected_verifier_bytes = [0u8; 32];
826838
expected_verifier_bytes[0] = 0xDE;
827839
expected_verifier_bytes[1] = 0xAD;
828840
let expected_verifier = BytesN::from_array(&test_env.env, &expected_verifier_bytes);
829-
assert_eq!(withdraw_verifier, expected_verifier);
841+
assert_eq!(withdraw_verifier, Some(expected_verifier.into()));
830842
assert_eq!(withdraw_ratio, 100);
831843

832844
println!("✓ Contract initialization state test passed");
@@ -887,7 +899,7 @@ fn test_withdraw_signature_validation_structure() {
887899

888900
// 6. Verify contract state
889901
let vault_client = test_env.get_vault_client();
890-
let withdraw_verifier = vault_client.get_withdraw_verifier();
902+
let withdraw_verifier = vault_client.get_withdraw_verifier(&0u32);
891903
let withdraw_currency = vault_client.get_withdraw_currency();
892904
let is_currency_supported = vault_client.is_currency_supported(&test_env.wbtc_token_addr);
893905

@@ -896,7 +908,8 @@ fn test_withdraw_signature_validation_structure() {
896908
expected_verifier_bytes[1] = 0xAD;
897909
let expected_verifier = BytesN::from_array(&test_env.env, &expected_verifier_bytes);
898910
assert_eq!(
899-
withdraw_verifier, expected_verifier,
911+
withdraw_verifier,
912+
Some(expected_verifier.into()),
900913
"Verifier public key should match"
901914
);
902915
assert!(
@@ -946,6 +959,137 @@ fn test_withdraw_signature_validation_structure() {
946959
println!(" ✓ Complete withdrawal process structure verification passed");
947960
}
948961

962+
#[test]
963+
#[should_panic]
964+
fn test_withdraw_secp256k1_invalid_signature_should_panic_integration() {
965+
// 1) Initialize environment and relationships
966+
let test_env = VaultTestEnv::new();
967+
test_env.setup_relationships();
968+
969+
// 2) Set secp256k1 verifier (65-byte uncompressed public key)
970+
let mut pubkey_bytes = [0u8; 65];
971+
pubkey_bytes[0] = 0x04;
972+
for i in 1..65 {
973+
pubkey_bytes[i] = i as u8;
974+
}
975+
let secp_pub = Bytes::from_slice(&test_env.env, &pubkey_bytes);
976+
test_env
977+
.get_vault_client()
978+
.set_withdraw_verifier_by_admin(&1u32, &secp_pub);
979+
980+
// 3) Mint shares to user
981+
let deposit_amount = 100_000_000i128;
982+
test_env.mint_wbtc_to_user(deposit_amount);
983+
test_env.approve_vault_for_wbtc(deposit_amount);
984+
test_env.set_nav_value(100_000_000i128);
985+
let minted = test_env.deposit(deposit_amount);
986+
987+
// 4) Create withdraw_request, ensure status is Pending
988+
let shares = minted / 2;
989+
let request_hash = test_env.create_request_hash(9);
990+
test_env
991+
.get_vault_client()
992+
.withdraw_request(&test_env.user, &shares, &request_hash);
993+
994+
// 5) Use invalid r||s and recovery_id=0 to trigger secp256k1 branch and expect panic
995+
let invalid_sig = BytesN::<64>::from_array(&test_env.env, &[0u8; 64]);
996+
test_env.get_vault_client().withdraw(
997+
&test_env.user,
998+
&shares,
999+
&100_000_000i128,
1000+
&request_hash,
1001+
&invalid_sig,
1002+
&1u32, // signature_type = secp256k1
1003+
&0u32, // recovery_id
1004+
);
1005+
}
1006+
1007+
#[test]
1008+
fn test_withdraw_ed25519_success_integration() {
1009+
// 1) Initialize environment and relationships
1010+
let test_env = VaultTestEnv::new();
1011+
test_env.setup_relationships();
1012+
1013+
// 2) Set ed25519 verifier = real public key used by signer
1014+
let ed_pub = test_env.get_real_public_key(); // Bytes(32)
1015+
test_env
1016+
.get_vault_client()
1017+
.set_withdraw_verifier_by_admin(&0u32, &ed_pub);
1018+
1019+
// 3) Prepare balances: user deposit to get shares; treasurer provides liquidity
1020+
let nav = 100_000_000i128; // 1.0
1021+
let deposit_amount = 200_000_000i128; // 2 WBTC
1022+
test_env.mint_wbtc_to_user(deposit_amount);
1023+
test_env.approve_vault_for_wbtc(deposit_amount);
1024+
test_env.set_nav_value(nav);
1025+
let minted = test_env.deposit(deposit_amount);
1026+
1027+
// Treasurer liquidity >= expected withdraw amount
1028+
let liq = 200_000_000i128; // 2 WBTC
1029+
test_env.mint_wbtc_to_treasurer(liq);
1030+
test_env.approve_vault_for_treasurer_wbtc(liq);
1031+
test_env.treasurer_deposit_wbtc(liq);
1032+
1033+
// 4) Create withdraw_request for shares
1034+
let shares = minted / 2; // withdraw half
1035+
let request_hash = test_env.create_request_hash(11);
1036+
test_env
1037+
.get_vault_client()
1038+
.withdraw_request(&test_env.user, &shares, &request_hash);
1039+
1040+
// 5) Sign EIP712 message with real ed25519 key
1041+
let signature = test_env.sign_vault_withdraw_message(
1042+
&test_env.user,
1043+
shares,
1044+
&test_env.wbtc_token_addr,
1045+
nav,
1046+
&request_hash,
1047+
);
1048+
1049+
// 6) Call withdraw and verify balances
1050+
let before_user_wbtc = test_env.get_user_wbtc_balance();
1051+
let before_treas_wbtc = test_env.get_treasurer_wbtc_balance();
1052+
let before_vault_wbtc = test_env.get_vault_wbtc_balance();
1053+
1054+
// Compute expected amount using on-chain formula
1055+
let withdraw_dec = test_env.get_wbtc_token_client().decimals();
1056+
let shares_dec = test_env.get_solvbtc_token_client().decimals();
1057+
let nav_dec = test_env.get_oracle_client().get_nav_decimals();
1058+
let pow10 = |n: u32| -> i128 {
1059+
let mut x = 1i128;
1060+
for _ in 0..n {
1061+
x *= 10;
1062+
}
1063+
x
1064+
};
1065+
let amount = (shares * nav * pow10(withdraw_dec)) / (pow10(nav_dec) * pow10(shares_dec));
1066+
let fee_bps = test_env.get_vault_client().get_withdraw_fee_ratio();
1067+
let expected_fee = (amount * fee_bps) / 10000;
1068+
let expected_after = amount - expected_fee;
1069+
1070+
let actual_after = test_env.get_vault_client().withdraw(
1071+
&test_env.user,
1072+
&shares,
1073+
&nav,
1074+
&request_hash,
1075+
&signature,
1076+
&0u32, // signature_type=ed25519
1077+
&0u32, // recovery ignored
1078+
);
1079+
1080+
let after_user_wbtc = test_env.get_user_wbtc_balance();
1081+
let after_treas_wbtc = test_env.get_treasurer_wbtc_balance();
1082+
let after_vault_wbtc = test_env.get_vault_wbtc_balance();
1083+
1084+
assert_eq!(actual_after, expected_after);
1085+
assert_eq!(after_user_wbtc - before_user_wbtc, expected_after);
1086+
// Treasurer balance should remain the same since funds are transferred from vault contract
1087+
// not from treasurer directly
1088+
assert_eq!(before_treas_wbtc, after_treas_wbtc);
1089+
// Vault contract balance should decrease by the total amount (including fee)
1090+
assert_eq!(before_vault_wbtc - after_vault_wbtc, amount);
1091+
}
1092+
9491093
#[test]
9501094
#[should_panic]
9511095
fn test_withdraw_with_invalid_signature_should_panic() {
@@ -981,6 +1125,105 @@ fn test_withdraw_with_invalid_signature_should_panic() {
9811125
test_env.withdraw(target_amount, nav_value, request_hash, invalid_signature);
9821126
}
9831127

1128+
#[test]
1129+
#[should_panic(expected = "Error(Contract, #313)")] // VaultError::Unauthorized = 313
1130+
fn test_withdraw_secp256k1_wrong_pubkey_should_panic() {
1131+
// This test covers the Unauthorized error at line 1086 in vault.rs
1132+
// when recovered public key doesn't match expected public key
1133+
1134+
let test_env = VaultTestEnv::new();
1135+
test_env.setup_relationships();
1136+
1137+
// 1. Set a specific secp256k1 public key in the contract
1138+
let mut expected_pubkey_bytes = [0u8; 65];
1139+
expected_pubkey_bytes[0] = 0x04; // uncompressed public key prefix
1140+
for i in 1..65 {
1141+
expected_pubkey_bytes[i] = (i * 2) as u8; // Some deterministic pattern
1142+
}
1143+
let expected_pub = Bytes::from_slice(&test_env.env, &expected_pubkey_bytes);
1144+
test_env
1145+
.get_vault_client()
1146+
.set_withdraw_verifier_by_admin(&1u32, &expected_pub);
1147+
1148+
// 2. Prepare: deposit to get shares
1149+
let deposit_amount = 100_000_000i128;
1150+
test_env.mint_wbtc_to_user(deposit_amount);
1151+
test_env.approve_vault_for_wbtc(deposit_amount);
1152+
test_env.set_nav_value(100_000_000i128);
1153+
let minted = test_env.deposit(deposit_amount);
1154+
1155+
// 3. Create withdraw request
1156+
let shares = minted / 2;
1157+
let request_hash = test_env.create_request_hash(42);
1158+
test_env
1159+
.get_vault_client()
1160+
.withdraw_request(&test_env.user, &shares, &request_hash);
1161+
1162+
// 4. Prepare liquidity for withdrawal
1163+
let liquidity = 200_000_000i128;
1164+
test_env.mint_wbtc_to_treasurer(liquidity);
1165+
test_env.approve_vault_for_treasurer_wbtc(liquidity);
1166+
test_env.treasurer_deposit_wbtc(liquidity);
1167+
1168+
// 5. Create a valid secp256k1 signature but with a DIFFERENT private key
1169+
// This will produce a valid signature that recovers to a different public key
1170+
1171+
// Create the withdraw message
1172+
let withdraw_message = test_env.create_vault_withdraw_message(
1173+
&test_env.user,
1174+
shares,
1175+
&test_env.wbtc_token_addr,
1176+
100_000_000i128,
1177+
&request_hash,
1178+
);
1179+
1180+
// Hash the message
1181+
let message_hash = test_env.env.crypto().sha256(&withdraw_message);
1182+
1183+
// Create EIP712 message
1184+
let domain_separator = Bytes::from_slice(
1185+
&test_env.env,
1186+
&[
1187+
0xa5, 0x93, 0x7d, 0xb5, 0x54, 0xc5, 0x13, 0x4b, 0xd1, 0xec, 0x9f, 0x43, 0xfb, 0x96,
1188+
0x12, 0xab, 0xda, 0x34, 0x02, 0xed, 0xd9, 0x7e, 0x43, 0x3d, 0x26, 0xd4, 0x63, 0xae,
1189+
0x26, 0x55, 0x18, 0x0a,
1190+
],
1191+
);
1192+
let prefix = Bytes::from_slice(&test_env.env, &[0x19, 0x01]);
1193+
let mut eip712_message = Bytes::new(&test_env.env);
1194+
eip712_message.append(&prefix);
1195+
eip712_message.append(&domain_separator);
1196+
eip712_message.append(&Bytes::from(message_hash));
1197+
1198+
// Create a valid secp256k1 signature that will recover to a different public key
1199+
// Using a known valid signature (r||s format, 64 bytes total)
1200+
// This signature is valid but will recover to a different public key than expected
1201+
let wrong_signature = BytesN::<64>::from_array(
1202+
&test_env.env,
1203+
&[
1204+
// r (32 bytes) - valid secp256k1 r value
1205+
0x8b, 0x9d, 0x7a, 0xe8, 0x56, 0x85, 0x37, 0x56, 0x14, 0x92, 0xf6, 0xd2, 0x18, 0x24,
1206+
0xf2, 0xb8, 0x48, 0x86, 0x89, 0xb4, 0xae, 0x11, 0x25, 0x01, 0x43, 0x4f, 0x8d, 0x8c,
1207+
0xc5, 0x11, 0x50, 0x66, // s (32 bytes) - valid secp256k1 s value
1208+
0x3f, 0xdd, 0x94, 0xc9, 0x7f, 0x25, 0xe4, 0x18, 0x28, 0xa3, 0x7d, 0xfd, 0x81, 0x9f,
1209+
0xc0, 0xad, 0x23, 0xc7, 0xea, 0x67, 0xe7, 0xb3, 0xe7, 0x02, 0xd9, 0xd3, 0xe2, 0xf9,
1210+
0x54, 0x37, 0x75, 0x8e,
1211+
],
1212+
);
1213+
1214+
// 6. This should panic with Unauthorized error because the recovered public key
1215+
// won't match the expected public key stored in the contract
1216+
test_env.get_vault_client().withdraw(
1217+
&test_env.user,
1218+
&shares,
1219+
&100_000_000i128,
1220+
&request_hash,
1221+
&wrong_signature,
1222+
&1u32, // signature_type = secp256k1
1223+
&0u32, // recovery_id
1224+
);
1225+
}
1226+
9841227
#[test]
9851228
fn test_withdraw_with_real_signature_success() {
9861229
println!("Starting test using real signature successful withdrawal process");
@@ -1000,7 +1243,7 @@ fn test_withdraw_with_real_signature_success() {
10001243
println!();
10011244

10021245
// Get verifier address set in contract
1003-
let withdraw_verifier = test_env.get_vault_client().get_withdraw_verifier();
1246+
let withdraw_verifier = test_env.get_vault_client().get_withdraw_verifier(&0u32);
10041247
println!("Verifier address set in contract: {:?}", withdraw_verifier);
10051248

10061249
// Prepare test data - first deposit
@@ -1504,7 +1747,7 @@ fn test_complete_withdraw_operation_flow() {
15041747

15051748
// Step 7: Verify withdrawal configuration
15061749
println!("=== Step 7: Verify withdrawal configuration ===");
1507-
let withdraw_verifier = vault_client.get_withdraw_verifier();
1750+
let withdraw_verifier = vault_client.get_withdraw_verifier(&0u32);
15081751
let withdraw_fee_ratio = vault_client.get_withdraw_fee_ratio();
15091752
let withdraw_fee_receiver = vault_client.get_withdraw_fee_receiver();
15101753

@@ -1854,7 +2097,10 @@ fn test_vault_initialization_with_config() {
18542097
assert_eq!(vault_client.get_admin(), admin);
18552098
assert_eq!(vault_client.get_oracle(), oracle);
18562099
assert_eq!(vault_client.get_treasurer(), treasurer);
1857-
assert_eq!(vault_client.get_withdraw_verifier(), withdraw_verifier);
2100+
assert_eq!(
2101+
vault_client.get_withdraw_verifier(&0u32),
2102+
Some(withdraw_verifier.clone().into())
2103+
);
18582104
assert_eq!(vault_client.get_withdraw_fee_ratio(), 150);
18592105
assert_eq!(
18602106
vault_client.get_eip712_domain_name(),

0 commit comments

Comments
 (0)