From 4982ff8f12ad7fb0cd560cf863523066ffd455d4 Mon Sep 17 00:00:00 2001 From: Luis Covarrubias Date: Fri, 13 Feb 2026 01:28:05 -0800 Subject: [PATCH] feat: enforce 5 SOL max on native transfers Add MAX_NATIVE_TRANSFER_LAMPORTS constant (5,000,000,000 lamports) and validate each recipient amount in build_payment. Throws an error if any single native transfer exceeds 5 SOL. Add Rust and TypeScript tests for the new validation. --- packages/wasm-solana/src/intent/build.rs | 70 ++++++++++++++++++++++ packages/wasm-solana/test/intentBuilder.ts | 42 +++++++++++++ 2 files changed, 112 insertions(+) diff --git a/packages/wasm-solana/src/intent/build.rs b/packages/wasm-solana/src/intent/build.rs index 6dfb15a..9ab0e89 100644 --- a/packages/wasm-solana/src/intent/build.rs +++ b/packages/wasm-solana/src/intent/build.rs @@ -34,6 +34,7 @@ const SYSTEM_PROGRAM_ID: &str = "11111111111111111111111111111111"; // Constants const STAKE_ACCOUNT_SPACE: u64 = 200; const STAKE_ACCOUNT_RENT: u64 = 2282880; // ~0.00228288 SOL +const MAX_NATIVE_TRANSFER_LAMPORTS: u64 = 5_000_000_000; // 5 SOL /// Build a transaction from a BitGo intent. /// @@ -180,6 +181,13 @@ fn build_payment( })?; let lamports: u64 = *amount; + if lamports > MAX_NATIVE_TRANSFER_LAMPORTS { + return Err(WasmSolanaError::new(&format!( + "Native transfer amount {} lamports exceeds maximum of {} lamports (5 SOL)", + lamports, MAX_NATIVE_TRANSFER_LAMPORTS + ))); + } + instructions.push(system_ix::transfer(&fee_payer, &to_pubkey, lamports)); } @@ -1163,6 +1171,68 @@ mod tests { ); } + #[test] + fn test_build_payment_rejects_amount_exceeding_max() { + // 5 SOL + 1 lamport should be rejected + let intent = serde_json::json!({ + "intentType": "payment", + "recipients": [{ + "address": { "address": "FKjSjCqByQRwSzZoMXA7bKnDbJe41YgJTHFFzBeC42bH" }, + "amount": { "value": "5000000001" } + }] + }); + + let result = build_from_intent(&intent, &test_params()); + assert!(result.is_err(), "Should fail for amount exceeding 5 SOL"); + assert!( + result.unwrap_err().to_string().contains("exceeds maximum"), + "Error should mention exceeds maximum" + ); + } + + #[test] + fn test_build_payment_allows_exactly_max_amount() { + // Exactly 5 SOL should succeed + let intent = serde_json::json!({ + "intentType": "payment", + "recipients": [{ + "address": { "address": "FKjSjCqByQRwSzZoMXA7bKnDbJe41YgJTHFFzBeC42bH" }, + "amount": { "value": "5000000000" } + }] + }); + + let result = build_from_intent(&intent, &test_params()); + assert!( + result.is_ok(), + "Should succeed for exactly 5 SOL: {:?}", + result + ); + } + + #[test] + fn test_build_payment_rejects_any_recipient_exceeding_max() { + // Multi-recipient where second exceeds max + let intent = serde_json::json!({ + "intentType": "payment", + "recipients": [ + { + "address": { "address": "FKjSjCqByQRwSzZoMXA7bKnDbJe41YgJTHFFzBeC42bH" }, + "amount": { "value": "1000000" } + }, + { + "address": { "address": "5ZWgXcyqrrNpQHCme5SdC5hCeYb2o3fEJhF7Gok3bTVN" }, + "amount": { "value": "6000000000" } + } + ] + }); + + let result = build_from_intent(&intent, &test_params()); + assert!( + result.is_err(), + "Should fail when any recipient exceeds 5 SOL" + ); + } + #[test] fn test_build_marinade_unstake_requires_recipients() { let intent = serde_json::json!({ diff --git a/packages/wasm-solana/test/intentBuilder.ts b/packages/wasm-solana/test/intentBuilder.ts index 519e3a6..ee1ae1a 100644 --- a/packages/wasm-solana/test/intentBuilder.ts +++ b/packages/wasm-solana/test/intentBuilder.ts @@ -65,6 +65,48 @@ describe("buildFromIntent", function () { assert.equal(transfers.length, 2, "Should have 2 transfer instructions"); }); + it("should reject native transfer exceeding 5 SOL", function () { + const intent = { + intentType: "payment", + recipients: [ + { + address: { address: "FKjSjCqByQRwSzZoMXA7bKnDbJe41YgJTHFFzBeC42bH" }, + amount: { value: 5000000001n }, // 5 SOL + 1 lamport + }, + ], + }; + + assert.throws( + () => { + buildFromIntent(intent, { + feePayer, + nonce: { type: "blockhash", value: blockhash }, + }); + }, + /exceeds maximum/, + "Should reject transfer exceeding 5 SOL", + ); + }); + + it("should allow native transfer of exactly 5 SOL", function () { + const intent = { + intentType: "payment", + recipients: [ + { + address: { address: "FKjSjCqByQRwSzZoMXA7bKnDbJe41YgJTHFFzBeC42bH" }, + amount: { value: 5000000000n }, // exactly 5 SOL + }, + ], + }; + + const result = buildFromIntent(intent, { + feePayer, + nonce: { type: "blockhash", value: blockhash }, + }); + + assert(result.transaction instanceof Transaction, "Should return Transaction object"); + }); + it("should include memo when provided", function () { const intent = { intentType: "payment",