From fba3c30a6bb96e6dce1c04d404031683deb14dcb Mon Sep 17 00:00:00 2001 From: GuidoDipietro Date: Wed, 25 Mar 2026 15:28:58 -0300 Subject: [PATCH 01/19] Scaffold execute_proposal body --- packages/svm/programs/settler/src/errors.rs | 3 +++ .../src/instructions/execute_proposal.rs | 8 +++----- .../settler/src/utils/execution/misc.rs | 17 +++++++++++++++++ .../settler/src/utils/execution/mod.rs | 5 +++++ .../settler/src/utils/execution/transfer.rs | 18 ++++++++++++++++++ packages/svm/programs/settler/src/utils/mod.rs | 2 ++ 6 files changed, 48 insertions(+), 5 deletions(-) create mode 100644 packages/svm/programs/settler/src/utils/execution/misc.rs create mode 100644 packages/svm/programs/settler/src/utils/execution/mod.rs create mode 100644 packages/svm/programs/settler/src/utils/execution/transfer.rs diff --git a/packages/svm/programs/settler/src/errors.rs b/packages/svm/programs/settler/src/errors.rs index 8e00031..24c99e5 100644 --- a/packages/svm/programs/settler/src/errors.rs +++ b/packages/svm/programs/settler/src/errors.rs @@ -86,6 +86,9 @@ pub enum SettlerError { #[msg("Fee amount exceeds max fee")] FeeAmountExceedsMaxFee, + #[msg("Unsupported intent op")] + UnsupportedIntentOp, + #[msg("Math Error")] MathError, } diff --git a/packages/svm/programs/settler/src/instructions/execute_proposal.rs b/packages/svm/programs/settler/src/instructions/execute_proposal.rs index 1e141f5..1f03d82 100644 --- a/packages/svm/programs/settler/src/instructions/execute_proposal.rs +++ b/packages/svm/programs/settler/src/instructions/execute_proposal.rs @@ -5,6 +5,7 @@ use crate::{ errors::SettlerError, state::{FulfilledIntent, Intent, Proposal}, types::IntentEvent, + utils::{handle_intent_execution, pay_solver_fees}, }; #[derive(Accounts)] @@ -59,18 +60,15 @@ pub struct ExecuteProposal<'info> { pub fn execute_proposal(ctx: Context) -> Result<()> { let intent = &ctx.accounts.intent; - // TODO: Execute proposal + handle_intent_execution(&intent.op)?; - // TODO: Validate execution - - // TODO: Emit events intent.events.iter().for_each(|event| { emit!(IntentEventEvent { event: event.clone() }) }); - // TODO: Pay fees to Solver + pay_solver_fees()?; Ok(()) } diff --git a/packages/svm/programs/settler/src/utils/execution/misc.rs b/packages/svm/programs/settler/src/utils/execution/misc.rs new file mode 100644 index 0000000..042c91d --- /dev/null +++ b/packages/svm/programs/settler/src/utils/execution/misc.rs @@ -0,0 +1,17 @@ +use anchor_lang::prelude::*; + +use crate::{errors::SettlerError, types::OpType, utils::handle_transfer}; + +pub fn handle_intent_execution(op: &OpType) -> Result<()> { + match op { + OpType::Swap => err!(SettlerError::UnsupportedIntentOp), + OpType::Transfer => handle_transfer(), + OpType::EvmCall => err!(SettlerError::UnsupportedIntentOp), + OpType::SvmCall => err!(SettlerError::UnsupportedIntentOp), + } +} + +pub fn pay_solver_fees() -> Result<()> { + // TODO + Ok(()) +} diff --git a/packages/svm/programs/settler/src/utils/execution/mod.rs b/packages/svm/programs/settler/src/utils/execution/mod.rs new file mode 100644 index 0000000..4a03391 --- /dev/null +++ b/packages/svm/programs/settler/src/utils/execution/mod.rs @@ -0,0 +1,5 @@ +pub mod misc; +pub mod transfer; + +pub use misc::*; +pub use transfer::*; diff --git a/packages/svm/programs/settler/src/utils/execution/transfer.rs b/packages/svm/programs/settler/src/utils/execution/transfer.rs new file mode 100644 index 0000000..432e041 --- /dev/null +++ b/packages/svm/programs/settler/src/utils/execution/transfer.rs @@ -0,0 +1,18 @@ +use anchor_lang::prelude::*; + +pub fn handle_transfer() -> Result<()> { + execute_transfer()?; + validate_transfer()?; + + Ok(()) +} + +fn execute_transfer() -> Result<()> { + // TODO + Ok(()) +} + +fn validate_transfer() -> Result<()> { + // TODO + Ok(()) +} diff --git a/packages/svm/programs/settler/src/utils/mod.rs b/packages/svm/programs/settler/src/utils/mod.rs index 497b515..122fa2d 100644 --- a/packages/svm/programs/settler/src/utils/mod.rs +++ b/packages/svm/programs/settler/src/utils/mod.rs @@ -1,5 +1,7 @@ +pub mod execution; pub mod math; pub mod signatures; +pub use execution::*; pub use math::*; pub use signatures::*; From 142117e32c1e444c14b4301dcb1eca8da5383c64 Mon Sep 17 00:00:00 2001 From: GuidoDipietro Date: Thu, 26 Mar 2026 12:52:38 -0300 Subject: [PATCH 02/19] Implement execute_proposal (transfer intent) --- packages/svm/Cargo.lock | 1503 +++++++++++++++-- packages/svm/programs/settler/Cargo.toml | 3 +- packages/svm/programs/settler/src/errors.rs | 9 + .../src/instructions/execute_proposal.rs | 16 +- packages/svm/programs/settler/src/lib.rs | 4 +- .../settler/src/types/intent_data/mod.rs | 3 + .../settler/src/types/intent_data/transfer.rs | 14 + .../svm/programs/settler/src/types/mod.rs | 2 + .../settler/src/utils/execution/misc.rs | 25 +- .../settler/src/utils/execution/transfer.rs | 100 +- 10 files changed, 1544 insertions(+), 135 deletions(-) create mode 100644 packages/svm/programs/settler/src/types/intent_data/mod.rs create mode 100644 packages/svm/programs/settler/src/types/intent_data/transfer.rs diff --git a/packages/svm/Cargo.lock b/packages/svm/Cargo.lock index 40c262e..457260d 100644 --- a/packages/svm/Cargo.lock +++ b/packages/svm/Cargo.lock @@ -2,6 +2,42 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common", + "generic-array", +] + +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "aes-gcm-siv" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae0784134ba9375416d469ec31e7c5f9fa94405049cf08c5ce5b4698be673e0d" +dependencies = [ + "aead", + "aes", + "cipher", + "ctr", + "polyval", + "subtle", + "zeroize", +] + [[package]] name = "ahash" version = "0.8.12" @@ -23,6 +59,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + [[package]] name = "alloy-json-abi" version = "1.5.6" @@ -281,7 +323,7 @@ dependencies = [ "solana-instruction", "solana-instructions-sysvar", "solana-invoke", - "solana-loader-v3-interface", + "solana-loader-v3-interface 3.0.0", "solana-msg", "solana-program-entrypoint", "solana-program-error", @@ -293,7 +335,7 @@ dependencies = [ "solana-system-interface", "solana-sysvar", "solana-sysvar-id", - "thiserror", + "thiserror 1.0.69", ] [[package]] @@ -308,7 +350,7 @@ dependencies = [ "regex", "serde", "serde_json", - "sha2", + "sha2 0.10.9", ] [[package]] @@ -321,6 +363,21 @@ dependencies = [ "serde", ] +[[package]] +name = "anchor-spl" +version = "0.32.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3397ab3fc5b198bbfe55d827ff58bd69f2a8d3f9f71c3732c23c2093fec4d3ef" +dependencies = [ + "anchor-lang", + "spl-associated-token-account", + "spl-pod", + "spl-token", + "spl-token-2022", + "spl-token-group-interface", + "spl-token-metadata-interface", +] + [[package]] name = "anchor-syn" version = "0.32.1" @@ -335,9 +392,9 @@ dependencies = [ "quote", "serde", "serde_json", - "sha2", + "sha2 0.10.9", "syn 1.0.109", - "thiserror", + "thiserror 1.0.69", ] [[package]] @@ -535,6 +592,12 @@ dependencies = [ "rand 0.8.5", ] +[[package]] +name = "arrayref" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" + [[package]] name = "arrayvec" version = "0.7.6" @@ -564,6 +627,12 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" +[[package]] +name = "base64" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3441f0f7b02788e948e47f457ca01f1d7e6d92c693bc132c22b087d3141c03ff" + [[package]] name = "base64" version = "0.21.7" @@ -624,6 +693,30 @@ dependencies = [ "wyz", ] +[[package]] +name = "blake3" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2468ef7d57b3fb7e16b576e8377cdbde2320c60e1491e961d11da40fc4f02a2d" +dependencies = [ + "arrayref", + "arrayvec", + "cc", + "cfg-if", + "constant_time_eq", + "cpufeatures", + "digest 0.10.7", +] + +[[package]] +name = "block-buffer" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" +dependencies = [ + "generic-array", +] + [[package]] name = "block-buffer" version = "0.10.4" @@ -799,6 +892,36 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + +[[package]] +name = "console_error_panic_hook" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc" +dependencies = [ + "cfg-if", + "wasm-bindgen", +] + +[[package]] +name = "console_log" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e89f72f65e8501878b8a004d5a1afb780987e2ce2b4532c562e367a72c57499f" +dependencies = [ + "log", + "web-sys", +] + [[package]] name = "const-hex" version = "1.17.0" @@ -837,6 +960,12 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "constant_time_eq" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b" + [[package]] name = "controller" version = "0.1.0" @@ -887,9 +1016,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ "generic-array", + "rand_core 0.6.4", "typenum", ] +[[package]] +name = "ctr" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" +dependencies = [ + "cipher", +] + [[package]] name = "curve25519-dalek" version = "4.1.3" @@ -903,6 +1042,7 @@ dependencies = [ "fiat-crypto", "rand_core 0.6.4", "rustc_version 0.4.1", + "serde", "subtle", "zeroize", ] @@ -928,6 +1068,12 @@ dependencies = [ "zeroize", ] +[[package]] +name = "derivation-path" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e5c37193a1db1d8ed868c03ec7b152175f26160a5b740e5e484143877e0adf0" + [[package]] name = "derivative" version = "2.2.0" @@ -977,7 +1123,7 @@ version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ - "block-buffer", + "block-buffer 0.10.4", "const-oid", "crypto-common", "subtle", @@ -1203,6 +1349,17 @@ dependencies = [ "zeroize", ] +[[package]] +name = "getrandom" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.9.0+wasi-snapshot-preview1", +] + [[package]] name = "getrandom" version = "0.2.16" @@ -1212,7 +1369,7 @@ dependencies = [ "cfg-if", "js-sys", "libc", - "wasi", + "wasi 0.11.1+wasi-snapshot-preview1", "wasm-bindgen", ] @@ -1263,10 +1420,12 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.15.5" +version = "0.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" dependencies = [ + "allocator-api2", + "equivalent", "foldhash 0.1.5", ] @@ -1344,10 +1503,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "206a8042aec68fa4a62e8d3f7aa4ceb508177d9324faf261e1959e495b7a1921" dependencies = [ "equivalent", - "hashbrown 0.15.5", + "hashbrown 0.15.2", "serde", ] +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "generic-array", +] + [[package]] name = "itertools" version = "0.10.5" @@ -1357,6 +1525,15 @@ dependencies = [ "either", ] +[[package]] +name = "itertools" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +dependencies = [ + "either", +] + [[package]] name = "itertools" version = "0.13.0" @@ -1392,7 +1569,7 @@ dependencies = [ "ecdsa", "elliptic-curve", "once_cell", - "sha2", + "sha2 0.10.9", ] [[package]] @@ -1438,6 +1615,52 @@ version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" +[[package]] +name = "libsecp256k1" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9d220bc1feda2ac231cb78c3d26f27676b8cf82c96971f7aeef3d0cf2797c73" +dependencies = [ + "arrayref", + "base64 0.12.3", + "digest 0.9.0", + "libsecp256k1-core", + "libsecp256k1-gen-ecmult", + "libsecp256k1-gen-genmult", + "rand 0.7.3", + "serde", + "sha2 0.9.9", +] + +[[package]] +name = "libsecp256k1-core" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0f6ab710cec28cef759c5f18671a27dae2a5f952cdaaee1d8e2908cb2478a80" +dependencies = [ + "crunchy", + "digest 0.9.0", + "subtle", +] + +[[package]] +name = "libsecp256k1-gen-ecmult" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccab96b584d38fac86a83f07e659f0deafd0253dc096dab5a36d53efe653c5c3" +dependencies = [ + "libsecp256k1-core", +] + +[[package]] +name = "libsecp256k1-gen-genmult" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67abfe149395e3aa1c48a2beb32b068e2334402df8181f818d3aee2b304c4f5d" +dependencies = [ + "libsecp256k1-core", +] + [[package]] name = "linux-raw-sys" version = "0.11.0" @@ -1476,6 +1699,27 @@ version = "2.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "merlin" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58c38e2799fc0978b65dfff8023ec7843e2330bb462f19198840b34b6582397d" +dependencies = [ + "byteorder", + "keccak", + "rand_core 0.6.4", + "zeroize", +] + [[package]] name = "num-bigint" version = "0.4.6" @@ -1486,6 +1730,17 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.108", +] + [[package]] name = "num-integer" version = "0.1.46" @@ -1505,12 +1760,40 @@ dependencies = [ "libm", ] +[[package]] +name = "num_enum" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26" +dependencies = [ + "num_enum_derive", + "rustversion", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" +dependencies = [ + "proc-macro-crate 3.4.0", + "proc-macro2", + "quote", + "syn 2.0.108", +] + [[package]] name = "once_cell" version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + [[package]] name = "parity-scale-codec" version = "3.7.5" @@ -1568,6 +1851,21 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" +[[package]] +name = "pbkdf2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83a0692ec44e4cf1ef28ca317f14f8f07da2d95ec3fa01f86e4467b725e60917" +dependencies = [ + "digest 0.10.7", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + [[package]] name = "pest" version = "2.8.6" @@ -1588,6 +1886,18 @@ dependencies = [ "spki", ] +[[package]] +name = "polyval" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" +dependencies = [ + "cfg-if", + "cpufeatures", + "opaque-debug", + "universal-hash", +] + [[package]] name = "ppv-lite86" version = "0.2.21" @@ -1686,6 +1996,15 @@ dependencies = [ "unarray", ] +[[package]] +name = "qstring" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d464fae65fff2680baf48019211ce37aaec0c78e9264c84a3e484717f965104e" +dependencies = [ + "percent-encoding", +] + [[package]] name = "quick-error" version = "1.2.3" @@ -1713,6 +2032,19 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" +[[package]] +name = "rand" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" +dependencies = [ + "getrandom 0.1.16", + "libc", + "rand_chacha 0.2.2", + "rand_core 0.5.1", + "rand_hc", +] + [[package]] name = "rand" version = "0.8.5" @@ -1735,6 +2067,16 @@ dependencies = [ "serde", ] +[[package]] +name = "rand_chacha" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" +dependencies = [ + "ppv-lite86", + "rand_core 0.5.1", +] + [[package]] name = "rand_chacha" version = "0.3.1" @@ -1755,6 +2097,15 @@ dependencies = [ "rand_core 0.9.5", ] +[[package]] +name = "rand_core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" +dependencies = [ + "getrandom 0.1.16", +] + [[package]] name = "rand_core" version = "0.6.4" @@ -1774,6 +2125,15 @@ dependencies = [ "serde", ] +[[package]] +name = "rand_hc" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" +dependencies = [ + "rand_core 0.5.1", +] + [[package]] name = "rand_xorshift" version = "0.4.0" @@ -2064,6 +2424,20 @@ dependencies = [ "alloy-primitives", "alloy-sol-types", "anchor-lang", + "anchor-spl", +] + +[[package]] +name = "sha2" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800" +dependencies = [ + "block-buffer 0.9.0", + "cfg-if", + "cpufeatures", + "digest 0.9.0", + "opaque-debug", ] [[package]] @@ -2120,16 +2494,48 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" [[package]] -name = "solana-account-info" -version = "2.3.0" +name = "solana-account" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f949fe4edaeaea78c844023bfc1c898e0b1f5a100f8a8d2d0f85d0a7b090258" +dependencies = [ + "solana-account-info", + "solana-clock", + "solana-instruction", + "solana-pubkey", + "solana-sdk-ids", +] + +[[package]] +name = "solana-account-info" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8f5152a288ef1912300fc6efa6c2d1f9bb55d9398eb6c72326360b8063987da" dependencies = [ + "bincode", + "serde", "solana-program-error", "solana-program-memory", "solana-pubkey", ] +[[package]] +name = "solana-address-lookup-table-interface" +version = "2.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1673f67efe870b64a65cb39e6194be5b26527691ce5922909939961a6e6b395" +dependencies = [ + "bincode", + "bytemuck", + "serde", + "serde_derive", + "solana-clock", + "solana-instruction", + "solana-pubkey", + "solana-sdk-ids", + "solana-slot-hashes", +] + [[package]] name = "solana-atomic-u64" version = "2.2.1" @@ -2139,6 +2545,50 @@ dependencies = [ "parking_lot", ] +[[package]] +name = "solana-big-mod-exp" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75db7f2bbac3e62cfd139065d15bcda9e2428883ba61fc8d27ccb251081e7567" +dependencies = [ + "num-bigint", + "num-traits", + "solana-define-syscall", +] + +[[package]] +name = "solana-bincode" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19a3787b8cf9c9fe3dd360800e8b70982b9e5a8af9e11c354b6665dd4a003adc" +dependencies = [ + "bincode", + "serde", + "solana-instruction", +] + +[[package]] +name = "solana-blake3-hasher" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1a0801e25a1b31a14494fc80882a036be0ffd290efc4c2d640bfcca120a4672" +dependencies = [ + "blake3", + "solana-define-syscall", + "solana-hash", + "solana-sanitize", +] + +[[package]] +name = "solana-borsh" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "718333bcd0a1a7aed6655aa66bef8d7fb047944922b2d3a18f49cbc13e73d004" +dependencies = [ + "borsh 0.10.4", + "borsh 1.5.7", +] + [[package]] name = "solana-clock" version = "2.2.2" @@ -2166,6 +2616,20 @@ dependencies = [ "solana-stable-layout", ] +[[package]] +name = "solana-curve25519" +version = "2.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eae4261b9a8613d10e77ac831a8fa60b6fa52b9b103df46d641deff9f9812a23" +dependencies = [ + "bytemuck", + "bytemuck_derive", + "curve25519-dalek", + "solana-define-syscall", + "subtle", + "thiserror 2.0.18", +] + [[package]] name = "solana-decode-error" version = "2.3.0" @@ -2181,6 +2645,17 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2ae3e2abcf541c8122eafe9a625d4d194b4023c20adde1e251f94e056bb1aee2" +[[package]] +name = "solana-derivation-path" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "939756d798b25c5ec3cca10e06212bdca3b1443cb9bb740a38124f58b258737b" +dependencies = [ + "derivation-path", + "qstring", + "uriparse", +] + [[package]] name = "solana-epoch-rewards" version = "2.2.1" @@ -2208,14 +2683,44 @@ dependencies = [ "solana-sysvar-id", ] +[[package]] +name = "solana-example-mocks" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84461d56cbb8bb8d539347151e0525b53910102e4bced875d49d5139708e39d3" +dependencies = [ + "serde", + "serde_derive", + "solana-address-lookup-table-interface", + "solana-clock", + "solana-hash", + "solana-instruction", + "solana-keccak-hasher", + "solana-message", + "solana-nonce", + "solana-pubkey", + "solana-sdk-ids", + "solana-system-interface", + "thiserror 2.0.18", +] + [[package]] name = "solana-feature-gate-interface" version = "2.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "43f5c5382b449e8e4e3016fb05e418c53d57782d8b5c30aa372fc265654b956d" dependencies = [ + "bincode", + "serde", + "serde_derive", + "solana-account", + "solana-account-info", + "solana-instruction", + "solana-program-error", "solana-pubkey", + "solana-rent", "solana-sdk-ids", + "solana-system-interface", ] [[package]] @@ -2235,6 +2740,7 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5b96e9f0300fa287b545613f007dfe20043d7812bee255f418c1eb649c93b63" dependencies = [ + "borsh 1.5.7", "bytemuck", "bytemuck_derive", "five8", @@ -2253,6 +2759,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47298e2ce82876b64f71e9d13a46bc4b9056194e7f9937ad3084385befa50885" dependencies = [ "bincode", + "borsh 1.5.7", "getrandom 0.2.16", "js-sys", "num-traits", @@ -2293,6 +2800,18 @@ dependencies = [ "solana-stable-layout", ] +[[package]] +name = "solana-keccak-hasher" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7aeb957fbd42a451b99235df4942d96db7ef678e8d5061ef34c9b34cae12f79" +dependencies = [ + "sha3", + "solana-define-syscall", + "solana-hash", + "solana-sanitize", +] + [[package]] name = "solana-last-restart-slot" version = "2.2.1" @@ -2306,6 +2825,20 @@ dependencies = [ "solana-sysvar-id", ] +[[package]] +name = "solana-loader-v2-interface" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8ab08006dad78ae7cd30df8eea0539e207d08d91eaefb3e1d49a446e1c49654" +dependencies = [ + "serde", + "serde_bytes", + "serde_derive", + "solana-instruction", + "solana-pubkey", + "solana-sdk-ids", +] + [[package]] name = "solana-loader-v3-interface" version = "3.0.0" @@ -2321,6 +2854,59 @@ dependencies = [ "solana-system-interface", ] +[[package]] +name = "solana-loader-v3-interface" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f7162a05b8b0773156b443bccd674ea78bb9aa406325b467ea78c06c99a63a2" +dependencies = [ + "serde", + "serde_bytes", + "serde_derive", + "solana-instruction", + "solana-pubkey", + "solana-sdk-ids", + "solana-system-interface", +] + +[[package]] +name = "solana-loader-v4-interface" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "706a777242f1f39a83e2a96a2a6cb034cb41169c6ecbee2cf09cb873d9659e7e" +dependencies = [ + "serde", + "serde_bytes", + "serde_derive", + "solana-instruction", + "solana-pubkey", + "solana-sdk-ids", + "solana-system-interface", +] + +[[package]] +name = "solana-message" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1796aabce376ff74bf89b78d268fa5e683d7d7a96a0a4e4813ec34de49d5314b" +dependencies = [ + "bincode", + "blake3", + "lazy_static", + "serde", + "serde_derive", + "solana-bincode", + "solana-hash", + "solana-instruction", + "solana-pubkey", + "solana-sanitize", + "solana-sdk-ids", + "solana-short-vec", + "solana-system-interface", + "solana-transaction-error", + "wasm-bindgen", +] + [[package]] name = "solana-msg" version = "2.2.1" @@ -2330,6 +2916,106 @@ dependencies = [ "solana-define-syscall", ] +[[package]] +name = "solana-native-token" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61515b880c36974053dd499c0510066783f0cc6ac17def0c7ef2a244874cf4a9" + +[[package]] +name = "solana-nonce" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "703e22eb185537e06204a5bd9d509b948f0066f2d1d814a6f475dafb3ddf1325" +dependencies = [ + "serde", + "serde_derive", + "solana-fee-calculator", + "solana-hash", + "solana-pubkey", + "solana-sha256-hasher", +] + +[[package]] +name = "solana-program" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98eca145bd3545e2fbb07166e895370576e47a00a7d824e325390d33bf467210" +dependencies = [ + "bincode", + "blake3", + "borsh 0.10.4", + "borsh 1.5.7", + "bs58", + "bytemuck", + "console_error_panic_hook", + "console_log", + "getrandom 0.2.16", + "lazy_static", + "log", + "memoffset", + "num-bigint", + "num-derive", + "num-traits", + "rand 0.8.5", + "serde", + "serde_bytes", + "serde_derive", + "solana-account-info", + "solana-address-lookup-table-interface", + "solana-atomic-u64", + "solana-big-mod-exp", + "solana-bincode", + "solana-blake3-hasher", + "solana-borsh", + "solana-clock", + "solana-cpi", + "solana-decode-error", + "solana-define-syscall", + "solana-epoch-rewards", + "solana-epoch-schedule", + "solana-example-mocks", + "solana-feature-gate-interface", + "solana-fee-calculator", + "solana-hash", + "solana-instruction", + "solana-instructions-sysvar", + "solana-keccak-hasher", + "solana-last-restart-slot", + "solana-loader-v2-interface", + "solana-loader-v3-interface 5.0.0", + "solana-loader-v4-interface", + "solana-message", + "solana-msg", + "solana-native-token", + "solana-nonce", + "solana-program-entrypoint", + "solana-program-error", + "solana-program-memory", + "solana-program-option", + "solana-program-pack", + "solana-pubkey", + "solana-rent", + "solana-sanitize", + "solana-sdk-ids", + "solana-sdk-macro", + "solana-secp256k1-recover", + "solana-serde-varint", + "solana-serialize-utils", + "solana-sha256-hasher", + "solana-short-vec", + "solana-slot-hashes", + "solana-slot-history", + "solana-stable-layout", + "solana-stake-interface", + "solana-system-interface", + "solana-sysvar", + "solana-sysvar-id", + "solana-vote-interface", + "thiserror 2.0.18", + "wasm-bindgen", +] + [[package]] name = "solana-program-entrypoint" version = "2.3.0" @@ -2350,6 +3036,8 @@ checksum = "9ee2e0217d642e2ea4bee237f37bd61bb02aec60da3647c48ff88f6556ade775" dependencies = [ "borsh 1.5.7", "num-traits", + "serde", + "serde_derive", "solana-decode-error", "solana-instruction", "solana-msg", @@ -2407,191 +3095,710 @@ dependencies = [ ] [[package]] -name = "solana-rent" -version = "2.2.1" +name = "solana-rent" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1aea8fdea9de98ca6e8c2da5827707fb3842833521b528a713810ca685d2480" +dependencies = [ + "serde", + "serde_derive", + "solana-sdk-ids", + "solana-sdk-macro", + "solana-sysvar-id", +] + +[[package]] +name = "solana-sanitize" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61f1bc1357b8188d9c4a3af3fc55276e56987265eb7ad073ae6f8180ee54cecf" + +[[package]] +name = "solana-sdk-ids" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c5d8b9cc68d5c88b062a33e23a6466722467dde0035152d8fb1afbcdf350a5f" +dependencies = [ + "solana-pubkey", +] + +[[package]] +name = "solana-sdk-macro" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86280da8b99d03560f6ab5aca9de2e38805681df34e0bb8f238e69b29433b9df" +dependencies = [ + "bs58", + "proc-macro2", + "quote", + "syn 2.0.108", +] + +[[package]] +name = "solana-secp256k1-recover" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baa3120b6cdaa270f39444f5093a90a7b03d296d362878f7a6991d6de3bbe496" +dependencies = [ + "libsecp256k1", + "solana-define-syscall", + "thiserror 2.0.18", +] + +[[package]] +name = "solana-security-txt" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "156bb61a96c605fa124e052d630dba2f6fb57e08c7d15b757e1e958b3ed7b3fe" +dependencies = [ + "hashbrown 0.15.2", +] + +[[package]] +name = "solana-seed-derivable" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3beb82b5adb266c6ea90e5cf3967235644848eac476c5a1f2f9283a143b7c97f" +dependencies = [ + "solana-derivation-path", +] + +[[package]] +name = "solana-seed-phrase" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36187af2324f079f65a675ec22b31c24919cb4ac22c79472e85d819db9bbbc15" +dependencies = [ + "hmac", + "pbkdf2", + "sha2 0.10.9", +] + +[[package]] +name = "solana-serde-varint" +version = "2.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a7e155eba458ecfb0107b98236088c3764a09ddf0201ec29e52a0be40857113" +dependencies = [ + "serde", +] + +[[package]] +name = "solana-serialize-utils" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "817a284b63197d2b27afdba829c5ab34231da4a9b4e763466a003c40ca4f535e" +dependencies = [ + "solana-instruction", + "solana-pubkey", + "solana-sanitize", +] + +[[package]] +name = "solana-sha256-hasher" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aa3feb32c28765f6aa1ce8f3feac30936f16c5c3f7eb73d63a5b8f6f8ecdc44" +dependencies = [ + "sha2 0.10.9", + "solana-define-syscall", + "solana-hash", +] + +[[package]] +name = "solana-short-vec" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c54c66f19b9766a56fa0057d060de8378676cb64987533fa088861858fc5a69" +dependencies = [ + "serde", +] + +[[package]] +name = "solana-signature" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64c8ec8e657aecfc187522fc67495142c12f35e55ddeca8698edbb738b8dbd8c" +dependencies = [ + "five8", + "solana-sanitize", +] + +[[package]] +name = "solana-signer" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c41991508a4b02f021c1342ba00bcfa098630b213726ceadc7cb032e051975b" +dependencies = [ + "solana-pubkey", + "solana-signature", + "solana-transaction-error", +] + +[[package]] +name = "solana-slot-hashes" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c8691982114513763e88d04094c9caa0376b867a29577939011331134c301ce" +dependencies = [ + "serde", + "serde_derive", + "solana-hash", + "solana-sdk-ids", + "solana-sysvar-id", +] + +[[package]] +name = "solana-slot-history" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97ccc1b2067ca22754d5283afb2b0126d61eae734fc616d23871b0943b0d935e" +dependencies = [ + "bv", + "serde", + "serde_derive", + "solana-sdk-ids", + "solana-sysvar-id", +] + +[[package]] +name = "solana-stable-layout" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f14f7d02af8f2bc1b5efeeae71bc1c2b7f0f65cd75bcc7d8180f2c762a57f54" +dependencies = [ + "solana-instruction", + "solana-pubkey", +] + +[[package]] +name = "solana-stake-interface" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5269e89fde216b4d7e1d1739cf5303f8398a1ff372a81232abbee80e554a838c" +dependencies = [ + "borsh 0.10.4", + "borsh 1.5.7", + "num-traits", + "serde", + "serde_derive", + "solana-clock", + "solana-cpi", + "solana-decode-error", + "solana-instruction", + "solana-program-error", + "solana-pubkey", + "solana-system-interface", + "solana-sysvar-id", +] + +[[package]] +name = "solana-system-interface" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94d7c18cb1a91c6be5f5a8ac9276a1d7c737e39a21beba9ea710ab4b9c63bc90" +dependencies = [ + "js-sys", + "num-traits", + "serde", + "serde_derive", + "solana-decode-error", + "solana-instruction", + "solana-pubkey", + "wasm-bindgen", +] + +[[package]] +name = "solana-sysvar" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8c3595f95069f3d90f275bb9bd235a1973c4d059028b0a7f81baca2703815db" +dependencies = [ + "base64 0.22.1", + "bincode", + "bytemuck", + "bytemuck_derive", + "lazy_static", + "serde", + "serde_derive", + "solana-account-info", + "solana-clock", + "solana-define-syscall", + "solana-epoch-rewards", + "solana-epoch-schedule", + "solana-fee-calculator", + "solana-hash", + "solana-instruction", + "solana-instructions-sysvar", + "solana-last-restart-slot", + "solana-program-entrypoint", + "solana-program-error", + "solana-program-memory", + "solana-pubkey", + "solana-rent", + "solana-sanitize", + "solana-sdk-ids", + "solana-sdk-macro", + "solana-slot-hashes", + "solana-slot-history", + "solana-stake-interface", + "solana-sysvar-id", +] + +[[package]] +name = "solana-sysvar-id" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5762b273d3325b047cfda250787f8d796d781746860d5d0a746ee29f3e8812c1" +dependencies = [ + "solana-pubkey", + "solana-sdk-ids", +] + +[[package]] +name = "solana-transaction-error" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "222a9dc8fdb61c6088baab34fc3a8b8473a03a7a5fd404ed8dd502fa79b67cb1" +dependencies = [ + "solana-instruction", + "solana-sanitize", +] + +[[package]] +name = "solana-vote-interface" +version = "2.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b80d57478d6599d30acc31cc5ae7f93ec2361a06aefe8ea79bc81739a08af4c3" +dependencies = [ + "bincode", + "num-derive", + "num-traits", + "serde", + "serde_derive", + "solana-clock", + "solana-decode-error", + "solana-hash", + "solana-instruction", + "solana-pubkey", + "solana-rent", + "solana-sdk-ids", + "solana-serde-varint", + "solana-serialize-utils", + "solana-short-vec", + "solana-system-interface", +] + +[[package]] +name = "solana-zk-sdk" +version = "2.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97b9fc6ec37d16d0dccff708ed1dd6ea9ba61796700c3bb7c3b401973f10f63b" +dependencies = [ + "aes-gcm-siv", + "base64 0.22.1", + "bincode", + "bytemuck", + "bytemuck_derive", + "curve25519-dalek", + "itertools 0.12.1", + "js-sys", + "merlin", + "num-derive", + "num-traits", + "rand 0.8.5", + "serde", + "serde_derive", + "serde_json", + "sha3", + "solana-derivation-path", + "solana-instruction", + "solana-pubkey", + "solana-sdk-ids", + "solana-seed-derivable", + "solana-seed-phrase", + "solana-signature", + "solana-signer", + "subtle", + "thiserror 2.0.18", + "wasm-bindgen", + "zeroize", +] + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "spl-associated-token-account" +version = "7.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae179d4a26b3c7a20c839898e6aed84cb4477adf108a366c95532f058aea041b" +dependencies = [ + "borsh 1.5.7", + "num-derive", + "num-traits", + "solana-program", + "spl-associated-token-account-client", + "spl-token", + "spl-token-2022", + "thiserror 2.0.18", +] + +[[package]] +name = "spl-associated-token-account-client" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f8349dbcbe575f354f9a533a21f272f3eb3808a49e2fdc1c34393b88ba76cb" +dependencies = [ + "solana-instruction", + "solana-pubkey", +] + +[[package]] +name = "spl-discriminator" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7398da23554a31660f17718164e31d31900956054f54f52d5ec1be51cb4f4b3" +dependencies = [ + "bytemuck", + "solana-program-error", + "solana-sha256-hasher", + "spl-discriminator-derive", +] + +[[package]] +name = "spl-discriminator-derive" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9e8418ea6269dcfb01c712f0444d2c75542c04448b480e87de59d2865edc750" +dependencies = [ + "quote", + "spl-discriminator-syn", + "syn 2.0.108", +] + +[[package]] +name = "spl-discriminator-syn" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d1dbc82ab91422345b6df40a79e2b78c7bce1ebb366da323572dd60b7076b67" +dependencies = [ + "proc-macro2", + "quote", + "sha2 0.10.9", + "syn 2.0.108", + "thiserror 1.0.69", +] + +[[package]] +name = "spl-elgamal-registry" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65edfeed09cd4231e595616aa96022214f9c9d2be02dea62c2b30d5695a6833a" +dependencies = [ + "bytemuck", + "solana-account-info", + "solana-cpi", + "solana-instruction", + "solana-msg", + "solana-program-entrypoint", + "solana-program-error", + "solana-pubkey", + "solana-rent", + "solana-sdk-ids", + "solana-system-interface", + "solana-sysvar", + "solana-zk-sdk", + "spl-pod", + "spl-token-confidential-transfer-proof-extraction", +] + +[[package]] +name = "spl-memo" +version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1aea8fdea9de98ca6e8c2da5827707fb3842833521b528a713810ca685d2480" +checksum = "9f09647c0974e33366efeb83b8e2daebb329f0420149e74d3a4bd2c08cf9f7cb" dependencies = [ - "serde", - "serde_derive", - "solana-sdk-ids", - "solana-sdk-macro", - "solana-sysvar-id", + "solana-account-info", + "solana-instruction", + "solana-msg", + "solana-program-entrypoint", + "solana-program-error", + "solana-pubkey", ] [[package]] -name = "solana-sanitize" -version = "2.2.1" +name = "spl-pod" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61f1bc1357b8188d9c4a3af3fc55276e56987265eb7ad073ae6f8180ee54cecf" +checksum = "d994afaf86b779104b4a95ba9ca75b8ced3fdb17ee934e38cb69e72afbe17799" +dependencies = [ + "borsh 1.5.7", + "bytemuck", + "bytemuck_derive", + "num-derive", + "num-traits", + "solana-decode-error", + "solana-msg", + "solana-program-error", + "solana-program-option", + "solana-pubkey", + "solana-zk-sdk", + "thiserror 2.0.18", +] [[package]] -name = "solana-sdk-ids" -version = "2.2.1" +name = "spl-program-error" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c5d8b9cc68d5c88b062a33e23a6466722467dde0035152d8fb1afbcdf350a5f" +checksum = "9cdebc8b42553070b75aa5106f071fef2eb798c64a7ec63375da4b1f058688c6" dependencies = [ - "solana-pubkey", + "num-derive", + "num-traits", + "solana-decode-error", + "solana-msg", + "solana-program-error", + "spl-program-error-derive", + "thiserror 2.0.18", ] [[package]] -name = "solana-sdk-macro" -version = "2.2.1" +name = "spl-program-error-derive" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86280da8b99d03560f6ab5aca9de2e38805681df34e0bb8f238e69b29433b9df" +checksum = "2a2539e259c66910d78593475540e8072f0b10f0f61d7607bbf7593899ed52d0" dependencies = [ - "bs58", "proc-macro2", "quote", + "sha2 0.10.9", "syn 2.0.108", ] [[package]] -name = "solana-serialize-utils" -version = "2.2.1" +name = "spl-tlv-account-resolution" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "817a284b63197d2b27afdba829c5ab34231da4a9b4e763466a003c40ca4f535e" +checksum = "1408e961215688715d5a1063cbdcf982de225c45f99c82b4f7d7e1dd22b998d7" dependencies = [ + "bytemuck", + "num-derive", + "num-traits", + "solana-account-info", + "solana-decode-error", "solana-instruction", + "solana-msg", + "solana-program-error", "solana-pubkey", - "solana-sanitize", + "spl-discriminator", + "spl-pod", + "spl-program-error", + "spl-type-length-value", + "thiserror 2.0.18", ] [[package]] -name = "solana-sha256-hasher" -version = "2.3.0" +name = "spl-token" +version = "8.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5aa3feb32c28765f6aa1ce8f3feac30936f16c5c3f7eb73d63a5b8f6f8ecdc44" +checksum = "053067c6a82c705004f91dae058b11b4780407e9ccd6799dc9e7d0fab5f242da" dependencies = [ - "sha2", - "solana-define-syscall", - "solana-hash", + "arrayref", + "bytemuck", + "num-derive", + "num-traits", + "num_enum", + "solana-account-info", + "solana-cpi", + "solana-decode-error", + "solana-instruction", + "solana-msg", + "solana-program-entrypoint", + "solana-program-error", + "solana-program-memory", + "solana-program-option", + "solana-program-pack", + "solana-pubkey", + "solana-rent", + "solana-sdk-ids", + "solana-sysvar", + "thiserror 2.0.18", ] [[package]] -name = "solana-slot-hashes" -version = "2.2.1" +name = "spl-token-2022" +version = "8.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c8691982114513763e88d04094c9caa0376b867a29577939011331134c301ce" +checksum = "31f0dfbb079eebaee55e793e92ca5f433744f4b71ee04880bfd6beefba5973e5" dependencies = [ - "serde", - "serde_derive", - "solana-hash", + "arrayref", + "bytemuck", + "num-derive", + "num-traits", + "num_enum", + "solana-account-info", + "solana-clock", + "solana-cpi", + "solana-decode-error", + "solana-instruction", + "solana-msg", + "solana-native-token", + "solana-program-entrypoint", + "solana-program-error", + "solana-program-memory", + "solana-program-option", + "solana-program-pack", + "solana-pubkey", + "solana-rent", "solana-sdk-ids", - "solana-sysvar-id", + "solana-security-txt", + "solana-system-interface", + "solana-sysvar", + "solana-zk-sdk", + "spl-elgamal-registry", + "spl-memo", + "spl-pod", + "spl-token", + "spl-token-confidential-transfer-ciphertext-arithmetic", + "spl-token-confidential-transfer-proof-extraction", + "spl-token-confidential-transfer-proof-generation", + "spl-token-group-interface", + "spl-token-metadata-interface", + "spl-transfer-hook-interface", + "spl-type-length-value", + "thiserror 2.0.18", +] + +[[package]] +name = "spl-token-confidential-transfer-ciphertext-arithmetic" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cddd52bfc0f1c677b41493dafa3f2dbbb4b47cf0990f08905429e19dc8289b35" +dependencies = [ + "base64 0.22.1", + "bytemuck", + "solana-curve25519", + "solana-zk-sdk", ] [[package]] -name = "solana-slot-history" -version = "2.2.1" +name = "spl-token-confidential-transfer-proof-extraction" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97ccc1b2067ca22754d5283afb2b0126d61eae734fc616d23871b0943b0d935e" +checksum = "fe2629860ff04c17bafa9ba4bed8850a404ecac81074113e1f840dbd0ebb7bd6" dependencies = [ - "bv", - "serde", - "serde_derive", + "bytemuck", + "solana-account-info", + "solana-curve25519", + "solana-instruction", + "solana-instructions-sysvar", + "solana-msg", + "solana-program-error", + "solana-pubkey", "solana-sdk-ids", - "solana-sysvar-id", + "solana-zk-sdk", + "spl-pod", + "thiserror 2.0.18", ] [[package]] -name = "solana-stable-layout" -version = "2.2.1" +name = "spl-token-confidential-transfer-proof-generation" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f14f7d02af8f2bc1b5efeeae71bc1c2b7f0f65cd75bcc7d8180f2c762a57f54" +checksum = "fa27b9174bea869a7ebf31e0be6890bce90b1a4288bc2bbf24bd413f80ae3fde" dependencies = [ - "solana-instruction", - "solana-pubkey", + "curve25519-dalek", + "solana-zk-sdk", + "thiserror 2.0.18", ] [[package]] -name = "solana-stake-interface" -version = "1.2.1" +name = "spl-token-group-interface" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5269e89fde216b4d7e1d1739cf5303f8398a1ff372a81232abbee80e554a838c" +checksum = "5597b4cd76f85ce7cd206045b7dc22da8c25516573d42d267c8d1fd128db5129" dependencies = [ + "bytemuck", + "num-derive", "num-traits", - "serde", - "serde_derive", - "solana-clock", - "solana-cpi", "solana-decode-error", "solana-instruction", + "solana-msg", "solana-program-error", "solana-pubkey", - "solana-system-interface", - "solana-sysvar-id", + "spl-discriminator", + "spl-pod", + "thiserror 2.0.18", ] [[package]] -name = "solana-system-interface" -version = "1.0.0" +name = "spl-token-metadata-interface" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94d7c18cb1a91c6be5f5a8ac9276a1d7c737e39a21beba9ea710ab4b9c63bc90" +checksum = "304d6e06f0de0c13a621464b1fd5d4b1bebf60d15ca71a44d3839958e0da16ee" dependencies = [ - "js-sys", + "borsh 1.5.7", + "num-derive", "num-traits", - "serde", - "serde_derive", + "solana-borsh", "solana-decode-error", "solana-instruction", + "solana-msg", + "solana-program-error", "solana-pubkey", - "wasm-bindgen", + "spl-discriminator", + "spl-pod", + "spl-type-length-value", + "thiserror 2.0.18", ] [[package]] -name = "solana-sysvar" -version = "2.3.0" +name = "spl-transfer-hook-interface" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8c3595f95069f3d90f275bb9bd235a1973c4d059028b0a7f81baca2703815db" +checksum = "a7e905b849b6aba63bde8c4badac944ebb6c8e6e14817029cbe1bc16829133bd" dependencies = [ - "base64 0.22.1", - "bincode", - "lazy_static", - "serde", - "serde_derive", + "arrayref", + "bytemuck", + "num-derive", + "num-traits", "solana-account-info", - "solana-clock", - "solana-define-syscall", - "solana-epoch-rewards", - "solana-epoch-schedule", - "solana-fee-calculator", - "solana-hash", + "solana-cpi", + "solana-decode-error", "solana-instruction", - "solana-instructions-sysvar", - "solana-last-restart-slot", - "solana-program-entrypoint", + "solana-msg", "solana-program-error", - "solana-program-memory", - "solana-pubkey", - "solana-rent", - "solana-sanitize", - "solana-sdk-ids", - "solana-sdk-macro", - "solana-slot-hashes", - "solana-slot-history", - "solana-stake-interface", - "solana-sysvar-id", -] - -[[package]] -name = "solana-sysvar-id" -version = "2.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5762b273d3325b047cfda250787f8d796d781746860d5d0a746ee29f3e8812c1" -dependencies = [ "solana-pubkey", - "solana-sdk-ids", + "spl-discriminator", + "spl-pod", + "spl-program-error", + "spl-tlv-account-resolution", + "spl-type-length-value", + "thiserror 2.0.18", ] [[package]] -name = "spki" -version = "0.7.3" +name = "spl-type-length-value" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +checksum = "d417eb548214fa822d93f84444024b4e57c13ed6719d4dcc68eec24fb481e9f5" dependencies = [ - "base64ct", - "der", + "bytemuck", + "num-derive", + "num-traits", + "solana-account-info", + "solana-decode-error", + "solana-msg", + "solana-program-error", + "spl-discriminator", + "spl-pod", + "thiserror 2.0.18", ] [[package]] @@ -2665,7 +3872,16 @@ version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" dependencies = [ - "thiserror-impl", + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl 2.0.18", ] [[package]] @@ -2679,6 +3895,17 @@ dependencies = [ "syn 2.0.108", ] +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.108", +] + [[package]] name = "tinyvec" version = "1.10.0" @@ -2822,6 +4049,26 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" +[[package]] +name = "universal-hash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common", + "subtle", +] + +[[package]] +name = "uriparse" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0200d0fc04d809396c2ad43f3c95da3582a2556eba8d453c1087f4120ee352ff" +dependencies = [ + "fnv", + "lazy_static", +] + [[package]] name = "valuable" version = "0.1.1" @@ -2843,6 +4090,12 @@ dependencies = [ "libc", ] +[[package]] +name = "wasi" +version = "0.9.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" + [[package]] name = "wasi" version = "0.11.1+wasi-snapshot-preview1" @@ -2941,11 +4194,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" dependencies = [ "bitflags", - "hashbrown 0.15.5", + "hashbrown 0.15.2", "indexmap", "semver 1.0.27", ] +[[package]] +name = "web-sys" +version = "0.3.82" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a1f95c0d03a47f4ae1f7a64643a6bb97465d9b740f0fa8f90ea33915c99a9a1" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "windows-link" version = "0.2.1" diff --git a/packages/svm/programs/settler/Cargo.toml b/packages/svm/programs/settler/Cargo.toml index d7602f1..2c0fcae 100644 --- a/packages/svm/programs/settler/Cargo.toml +++ b/packages/svm/programs/settler/Cargo.toml @@ -15,13 +15,14 @@ cpi = ["no-entrypoint"] no-entrypoint = [] no-idl = [] no-log-ix-name = [] -idl-build = ["anchor-lang/idl-build"] +idl-build = ["anchor-lang/idl-build", "anchor-spl/idl-build"] anchor-debug = [] custom-heap = [] custom-panic = [] [dependencies] anchor-lang = { version = "0.32.1", features = [ "init-if-needed" ] } +anchor-spl = { version = "0.32.1" } alloy-sol-types = "1.5.4" alloy-primitives = "1.5.4" diff --git a/packages/svm/programs/settler/src/errors.rs b/packages/svm/programs/settler/src/errors.rs index 24c99e5..e30479d 100644 --- a/packages/svm/programs/settler/src/errors.rs +++ b/packages/svm/programs/settler/src/errors.rs @@ -89,6 +89,15 @@ pub enum SettlerError { #[msg("Unsupported intent op")] UnsupportedIntentOp, + #[msg("Incorrect intent chain id")] + IncorrectChainId, + + #[msg("Invalid transfer recipient")] + InvalidTransferRecipient, + + #[msg("Invalid transfer token")] + InvalidTransferToken, + #[msg("Math Error")] MathError, } diff --git a/packages/svm/programs/settler/src/instructions/execute_proposal.rs b/packages/svm/programs/settler/src/instructions/execute_proposal.rs index 1f03d82..37aa68f 100644 --- a/packages/svm/programs/settler/src/instructions/execute_proposal.rs +++ b/packages/svm/programs/settler/src/instructions/execute_proposal.rs @@ -54,13 +54,25 @@ pub struct ExecuteProposal<'info> { )] pub fulfilled_intent: Box>, + #[account(seeds = [b"delegate", intent.user.key().as_ref()], bump)] + pub delegate: SystemAccount<'info>, + pub system_program: Program<'info, System>, } -pub fn execute_proposal(ctx: Context) -> Result<()> { +pub fn execute_proposal<'info>( + ctx: Context<'_, '_, '_, 'info, ExecuteProposal<'info>>, +) -> Result<()> { let intent = &ctx.accounts.intent; + let proposal = &ctx.accounts.proposal; - handle_intent_execution(&intent.op)?; + handle_intent_execution( + &intent, + &proposal, + &ctx.accounts.delegate.clone(), + ctx.remaining_accounts, + ctx.bumps.delegate, + )?; intent.events.iter().for_each(|event| { emit!(IntentEventEvent { diff --git a/packages/svm/programs/settler/src/lib.rs b/packages/svm/programs/settler/src/lib.rs index d0df235..c2229d5 100644 --- a/packages/svm/programs/settler/src/lib.rs +++ b/packages/svm/programs/settler/src/lib.rs @@ -80,7 +80,9 @@ pub mod settler { instructions::create_proposal(ctx, instructions, fees, deadline, is_final) } - pub fn execute_proposal(ctx: Context) -> Result<()> { + pub fn execute_proposal<'info>( + ctx: Context<'_, '_, '_, 'info, ExecuteProposal<'info>>, + ) -> Result<()> { instructions::execute_proposal(ctx) } diff --git a/packages/svm/programs/settler/src/types/intent_data/mod.rs b/packages/svm/programs/settler/src/types/intent_data/mod.rs new file mode 100644 index 0000000..858d1d0 --- /dev/null +++ b/packages/svm/programs/settler/src/types/intent_data/mod.rs @@ -0,0 +1,3 @@ +pub mod transfer; + +pub use transfer::*; diff --git a/packages/svm/programs/settler/src/types/intent_data/transfer.rs b/packages/svm/programs/settler/src/types/intent_data/transfer.rs new file mode 100644 index 0000000..06d2521 --- /dev/null +++ b/packages/svm/programs/settler/src/types/intent_data/transfer.rs @@ -0,0 +1,14 @@ +use anchor_lang::prelude::{borsh::BorshDeserialize, *}; + +#[derive(BorshDeserialize)] +pub struct SvmTransfer { + pub token: Vec, + pub amount: u64, + pub recipient: Vec, +} + +#[derive(BorshDeserialize)] +pub struct SvmTransferIntentData { + pub chain_id: u32, + pub transfers: Vec, +} diff --git a/packages/svm/programs/settler/src/types/mod.rs b/packages/svm/programs/settler/src/types/mod.rs index ee137f9..1b8b438 100644 --- a/packages/svm/programs/settler/src/types/mod.rs +++ b/packages/svm/programs/settler/src/types/mod.rs @@ -1,9 +1,11 @@ pub mod eip712_domain; +pub mod intent_data; pub mod intent_event; pub mod op_type; pub mod token_fee; pub use eip712_domain::*; +pub use intent_data::*; pub use intent_event::*; pub use op_type::*; pub use token_fee::*; diff --git a/packages/svm/programs/settler/src/utils/execution/misc.rs b/packages/svm/programs/settler/src/utils/execution/misc.rs index 042c91d..68d0d6b 100644 --- a/packages/svm/programs/settler/src/utils/execution/misc.rs +++ b/packages/svm/programs/settler/src/utils/execution/misc.rs @@ -1,11 +1,28 @@ use anchor_lang::prelude::*; -use crate::{errors::SettlerError, types::OpType, utils::handle_transfer}; +use crate::{ + errors::SettlerError, + state::{Intent, Proposal}, + types::OpType, + utils::handle_transfer, +}; -pub fn handle_intent_execution(op: &OpType) -> Result<()> { - match op { +pub fn handle_intent_execution<'info>( + intent: &Intent, + proposal: &Proposal, + delegate: &AccountInfo<'info>, + remaining_accounts: &[AccountInfo<'info>], + delegate_bump: u8, +) -> Result<()> { + match intent.op { OpType::Swap => err!(SettlerError::UnsupportedIntentOp), - OpType::Transfer => handle_transfer(), + OpType::Transfer => handle_transfer( + intent, + proposal, + delegate, + remaining_accounts, + delegate_bump, + ), OpType::EvmCall => err!(SettlerError::UnsupportedIntentOp), OpType::SvmCall => err!(SettlerError::UnsupportedIntentOp), } diff --git a/packages/svm/programs/settler/src/utils/execution/transfer.rs b/packages/svm/programs/settler/src/utils/execution/transfer.rs index 432e041..25c990f 100644 --- a/packages/svm/programs/settler/src/utils/execution/transfer.rs +++ b/packages/svm/programs/settler/src/utils/execution/transfer.rs @@ -1,18 +1,104 @@ use anchor_lang::prelude::*; +use anchor_spl::{ + token_2022::TransferChecked, + token_interface::{self, Mint as IMint, TokenAccount as ITokenAccount}, +}; -pub fn handle_transfer() -> Result<()> { - execute_transfer()?; - validate_transfer()?; +use crate::{ + errors::SettlerError, + state::{Intent, Proposal}, + types::SvmTransferIntentData, +}; + +pub fn handle_transfer<'info>( + intent: &Intent, + proposal: &Proposal, + delegate: &AccountInfo<'info>, + remaining_accounts: &[AccountInfo<'info>], + delegate_bump: u8, +) -> Result<()> { + let decoded_intent_data = SvmTransferIntentData::try_from_slice(&intent.data)?; + + validate_transfer(proposal, &decoded_intent_data)?; + + let delegate_seeds: &[&[u8]] = &[b"delegate", intent.user.as_ref(), &[delegate_bump]]; + execute_transfer( + &intent.user, + delegate, + remaining_accounts, + &decoded_intent_data, + &[delegate_seeds], + )?; Ok(()) } -fn execute_transfer() -> Result<()> { - // TODO +fn execute_transfer<'info>( + user: &Pubkey, + delegate: &AccountInfo<'info>, + remaining_accounts: &[AccountInfo<'info>], + intent_data: &SvmTransferIntentData, + delegate_seeds: &[&[&[u8]]], +) -> Result<()> { + let mut remaining_accounts_iter = remaining_accounts.iter(); + + let token_program = next_account_info(&mut remaining_accounts_iter)?; + let token_2022_program = next_account_info(&mut remaining_accounts_iter)?; + + require_keys_eq!(token_program.key(), anchor_spl::token::ID); + require_keys_eq!(token_2022_program.key(), anchor_spl::token_2022::ID); + + for transfer in &intent_data.transfers { + let token_account_info = next_account_info(&mut remaining_accounts_iter)?; + let recipient_account_info = next_account_info(&mut remaining_accounts_iter)?; + let recipient_ta_account_info = next_account_info(&mut remaining_accounts_iter)?; + let user_ta_account_info = next_account_info(&mut remaining_accounts_iter)?; + + let mut token_data: &[u8] = &token_account_info.try_borrow_data()?; + let token_mint = IMint::try_deserialize(&mut token_data)?; + + let mut recipient_ta_data: &[u8] = &recipient_ta_account_info.try_borrow_data()?; + let recipient_ta = ITokenAccount::try_deserialize(&mut recipient_ta_data)?; + + let mut user_ta_data: &[u8] = &user_ta_account_info.try_borrow_data()?; + let user_ta = ITokenAccount::try_deserialize(&mut user_ta_data)?; + + let transfer_recipient = Pubkey::try_from(transfer.recipient.as_slice()) + .map_err(|_| error!(SettlerError::InvalidTransferRecipient))?; + + let transfer_token = Pubkey::try_from(transfer.token.as_slice()) + .map_err(|_| error!(SettlerError::InvalidTransferToken))?; + + require_keys_eq!(transfer_recipient, recipient_account_info.key()); + require_keys_eq!(transfer_token, token_account_info.key()); + require_keys_eq!(recipient_ta.owner, recipient_account_info.key()); + require_keys_eq!(recipient_ta.mint, token_account_info.key()); + require_keys_eq!(user_ta.owner, *user); + require_keys_eq!(user_ta.mint, token_account_info.key()); + + let cpi_accounts = TransferChecked { + authority: delegate.clone(), + from: user_ta_account_info.clone(), + mint: token_account_info.clone(), + to: recipient_ta_account_info.clone(), + }; + + let cpi_program = if *token_account_info.owner == anchor_spl::token::ID { + token_program.clone() + } else { + token_2022_program.clone() + }; + + let cpi_ctx = CpiContext::new_with_signer(cpi_program, cpi_accounts, delegate_seeds); + token_interface::transfer_checked(cpi_ctx, transfer.amount, token_mint.decimals)?; + } + Ok(()) } -fn validate_transfer() -> Result<()> { - // TODO +fn validate_transfer(proposal: &Proposal, intent_data: &SvmTransferIntentData) -> Result<()> { + require_eq!(intent_data.chain_id, 507424, SettlerError::IncorrectChainId); + require_eq!(proposal.instructions.len(), 0); + Ok(()) } From 35a65450fa6e2c8860cee38f099a3d276e503b3c Mon Sep 17 00:00:00 2001 From: GuidoDipietro Date: Tue, 31 Mar 2026 12:22:59 -0300 Subject: [PATCH 03/19] Abstract execute_transfer out to function --- .../settler/src/utils/execution/transfer.rs | 114 +++++++++++------- 1 file changed, 68 insertions(+), 46 deletions(-) diff --git a/packages/svm/programs/settler/src/utils/execution/transfer.rs b/packages/svm/programs/settler/src/utils/execution/transfer.rs index 25c990f..1e3297f 100644 --- a/packages/svm/programs/settler/src/utils/execution/transfer.rs +++ b/packages/svm/programs/settler/src/utils/execution/transfer.rs @@ -4,10 +4,12 @@ use anchor_spl::{ token_interface::{self, Mint as IMint, TokenAccount as ITokenAccount}, }; +use core::slice::Iter; + use crate::{ errors::SettlerError, state::{Intent, Proposal}, - types::SvmTransferIntentData, + types::{SvmTransfer, SvmTransferIntentData}, }; pub fn handle_transfer<'info>( @@ -22,8 +24,8 @@ pub fn handle_transfer<'info>( validate_transfer(proposal, &decoded_intent_data)?; let delegate_seeds: &[&[u8]] = &[b"delegate", intent.user.as_ref(), &[delegate_bump]]; - execute_transfer( - &intent.user, + execute_transfers( + intent.user, delegate, remaining_accounts, &decoded_intent_data, @@ -34,7 +36,60 @@ pub fn handle_transfer<'info>( } fn execute_transfer<'info>( - user: &Pubkey, + transfer: &SvmTransfer, + delegate: &AccountInfo<'info>, + remaining_accounts_iter: &mut Iter<'_, AccountInfo<'info>>, + delegate_seeds: &[&[&[u8]]], + user: Pubkey, + token_program: &AccountInfo<'info>, + token_2022_program: &AccountInfo<'info>, +) -> Result<()> { + let token_account_info = next_account_info(remaining_accounts_iter)?; + let recipient_account_info = next_account_info(remaining_accounts_iter)?; + let recipient_ta_account_info = next_account_info(remaining_accounts_iter)?; + let user_ta_account_info = next_account_info(remaining_accounts_iter)?; + + let mut token_data: &[u8] = &token_account_info.try_borrow_data()?; + let token_mint = IMint::try_deserialize(&mut token_data)?; + + let mut recipient_ta_data: &[u8] = &recipient_ta_account_info.try_borrow_data()?; + let recipient_ta = ITokenAccount::try_deserialize(&mut recipient_ta_data)?; + + let mut user_ta_data: &[u8] = &user_ta_account_info.try_borrow_data()?; + let user_ta = ITokenAccount::try_deserialize(&mut user_ta_data)?; + + let transfer_recipient = Pubkey::try_from(transfer.recipient.as_slice()) + .map_err(|_| error!(SettlerError::InvalidTransferRecipient))?; + + let transfer_token = Pubkey::try_from(transfer.token.as_slice()) + .map_err(|_| error!(SettlerError::InvalidTransferToken))?; + + require_keys_eq!(transfer_recipient, recipient_account_info.key()); + require_keys_eq!(transfer_token, token_account_info.key()); + require_keys_eq!(recipient_ta.owner, recipient_account_info.key()); + require_keys_eq!(recipient_ta.mint, token_account_info.key()); + require_keys_eq!(user_ta.owner, user); + require_keys_eq!(user_ta.mint, token_account_info.key()); + + let cpi_accounts = TransferChecked { + authority: delegate.clone(), + from: user_ta_account_info.clone(), + mint: token_account_info.clone(), + to: recipient_ta_account_info.clone(), + }; + + let cpi_program = if *token_account_info.owner == anchor_spl::token::ID { + token_program.clone() + } else { + token_2022_program.clone() + }; + + let cpi_ctx = CpiContext::new_with_signer(cpi_program, cpi_accounts, delegate_seeds); + token_interface::transfer_checked(cpi_ctx, transfer.amount, token_mint.decimals) +} + +fn execute_transfers<'info>( + user: Pubkey, delegate: &AccountInfo<'info>, remaining_accounts: &[AccountInfo<'info>], intent_data: &SvmTransferIntentData, @@ -49,48 +104,15 @@ fn execute_transfer<'info>( require_keys_eq!(token_2022_program.key(), anchor_spl::token_2022::ID); for transfer in &intent_data.transfers { - let token_account_info = next_account_info(&mut remaining_accounts_iter)?; - let recipient_account_info = next_account_info(&mut remaining_accounts_iter)?; - let recipient_ta_account_info = next_account_info(&mut remaining_accounts_iter)?; - let user_ta_account_info = next_account_info(&mut remaining_accounts_iter)?; - - let mut token_data: &[u8] = &token_account_info.try_borrow_data()?; - let token_mint = IMint::try_deserialize(&mut token_data)?; - - let mut recipient_ta_data: &[u8] = &recipient_ta_account_info.try_borrow_data()?; - let recipient_ta = ITokenAccount::try_deserialize(&mut recipient_ta_data)?; - - let mut user_ta_data: &[u8] = &user_ta_account_info.try_borrow_data()?; - let user_ta = ITokenAccount::try_deserialize(&mut user_ta_data)?; - - let transfer_recipient = Pubkey::try_from(transfer.recipient.as_slice()) - .map_err(|_| error!(SettlerError::InvalidTransferRecipient))?; - - let transfer_token = Pubkey::try_from(transfer.token.as_slice()) - .map_err(|_| error!(SettlerError::InvalidTransferToken))?; - - require_keys_eq!(transfer_recipient, recipient_account_info.key()); - require_keys_eq!(transfer_token, token_account_info.key()); - require_keys_eq!(recipient_ta.owner, recipient_account_info.key()); - require_keys_eq!(recipient_ta.mint, token_account_info.key()); - require_keys_eq!(user_ta.owner, *user); - require_keys_eq!(user_ta.mint, token_account_info.key()); - - let cpi_accounts = TransferChecked { - authority: delegate.clone(), - from: user_ta_account_info.clone(), - mint: token_account_info.clone(), - to: recipient_ta_account_info.clone(), - }; - - let cpi_program = if *token_account_info.owner == anchor_spl::token::ID { - token_program.clone() - } else { - token_2022_program.clone() - }; - - let cpi_ctx = CpiContext::new_with_signer(cpi_program, cpi_accounts, delegate_seeds); - token_interface::transfer_checked(cpi_ctx, transfer.amount, token_mint.decimals)?; + execute_transfer( + transfer, + delegate, + &mut remaining_accounts_iter, + delegate_seeds, + user, + token_program, + token_2022_program, + )?; } Ok(()) From fabea4e8b8038900a2bfe1cfc5496c51d3369340 Mon Sep 17 00:00:00 2001 From: GuidoDipietro Date: Tue, 31 Mar 2026 12:56:01 -0300 Subject: [PATCH 04/19] Documents execute_transfer remaining_accounts transfer branch --- .../settler/src/utils/execution/transfer.rs | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/packages/svm/programs/settler/src/utils/execution/transfer.rs b/packages/svm/programs/settler/src/utils/execution/transfer.rs index 1e3297f..6aa2892 100644 --- a/packages/svm/programs/settler/src/utils/execution/transfer.rs +++ b/packages/svm/programs/settler/src/utils/execution/transfer.rs @@ -35,6 +35,32 @@ pub fn handle_transfer<'info>( Ok(()) } +/// Deserializes and checks the following remaining_accounts: +/// +/// #[account( +/// address = transfer.token, +/// )] +/// pub token: Account<'info, Mint>, +/// +/// #[account( +/// address = transfer.recipient, +/// )] +/// pub recipient: AccountInfo<'info>, +/// +/// #[account( +/// mut, +/// token::authority = recipient, +/// token::mint = token, +/// )] +/// pub recipient_token_account: Account<'info, TokenAccount>, +/// +/// #[account( +/// mut, +/// token::authority = user, +/// token::mint = token, +/// )] +/// pub user_token_account: Account<'info, TokenAccount>, +/// fn execute_transfer<'info>( transfer: &SvmTransfer, delegate: &AccountInfo<'info>, From 0e018a18cadc82a2e734a2fc918accf4963cbbf9 Mon Sep 17 00:00:00 2001 From: GuidoDipietro Date: Tue, 31 Mar 2026 13:39:24 -0300 Subject: [PATCH 05/19] execute_proposal WIP --- .../settler/src/utils/execution/transfer.rs | 75 ++++++---- packages/svm/tests/settler.test.ts | 132 ++++++++++++++++++ 2 files changed, 177 insertions(+), 30 deletions(-) diff --git a/packages/svm/programs/settler/src/utils/execution/transfer.rs b/packages/svm/programs/settler/src/utils/execution/transfer.rs index 6aa2892..17aae71 100644 --- a/packages/svm/programs/settler/src/utils/execution/transfer.rs +++ b/packages/svm/programs/settler/src/utils/execution/transfer.rs @@ -35,6 +35,42 @@ pub fn handle_transfer<'info>( Ok(()) } +/// Deserializes and checks the following remaining_accounts: +/// +/// pub token_program: Program<'info, Token>, +/// +/// pub token_2022_program: Program<'info, Token2022>, +/// +fn execute_transfers<'info>( + user: Pubkey, + delegate: &AccountInfo<'info>, + remaining_accounts: &[AccountInfo<'info>], + intent_data: &SvmTransferIntentData, + delegate_seeds: &[&[&[u8]]], +) -> Result<()> { + let mut remaining_accounts_iter = remaining_accounts.iter(); + + let token_program = next_account_info(&mut remaining_accounts_iter)?; + let token_2022_program = next_account_info(&mut remaining_accounts_iter)?; + + require_keys_eq!(token_program.key(), anchor_spl::token::ID); + require_keys_eq!(token_2022_program.key(), anchor_spl::token_2022::ID); + + for transfer in &intent_data.transfers { + execute_transfer( + transfer, + delegate, + &mut remaining_accounts_iter, + delegate_seeds, + user, + token_program, + token_2022_program, + )?; + } + + Ok(()) +} + /// Deserializes and checks the following remaining_accounts: /// /// #[account( @@ -59,6 +95,7 @@ pub fn handle_transfer<'info>( /// token::authority = user, /// token::mint = token, /// )] +/// NOTE: must have PDA delegate approved and amount at least transfer.amount /// pub user_token_account: Account<'info, TokenAccount>, /// fn execute_transfer<'info>( @@ -75,6 +112,11 @@ fn execute_transfer<'info>( let recipient_ta_account_info = next_account_info(remaining_accounts_iter)?; let user_ta_account_info = next_account_info(remaining_accounts_iter)?; + // TODO: validate account ownership + // token_account_info.owner = token or token2022 + // recipient_ta_account_info = token or token2022 + // user_ta_account_info = token or token2022 + let mut token_data: &[u8] = &token_account_info.try_borrow_data()?; let token_mint = IMint::try_deserialize(&mut token_data)?; @@ -107,6 +149,7 @@ fn execute_transfer<'info>( let cpi_program = if *token_account_info.owner == anchor_spl::token::ID { token_program.clone() } else { + // TODO: validate that it is token2022, otherwise err token_2022_program.clone() }; @@ -114,39 +157,11 @@ fn execute_transfer<'info>( token_interface::transfer_checked(cpi_ctx, transfer.amount, token_mint.decimals) } -fn execute_transfers<'info>( - user: Pubkey, - delegate: &AccountInfo<'info>, - remaining_accounts: &[AccountInfo<'info>], - intent_data: &SvmTransferIntentData, - delegate_seeds: &[&[&[u8]]], -) -> Result<()> { - let mut remaining_accounts_iter = remaining_accounts.iter(); - - let token_program = next_account_info(&mut remaining_accounts_iter)?; - let token_2022_program = next_account_info(&mut remaining_accounts_iter)?; - - require_keys_eq!(token_program.key(), anchor_spl::token::ID); - require_keys_eq!(token_2022_program.key(), anchor_spl::token_2022::ID); - - for transfer in &intent_data.transfers { - execute_transfer( - transfer, - delegate, - &mut remaining_accounts_iter, - delegate_seeds, - user, - token_program, - token_2022_program, - )?; - } - - Ok(()) -} - fn validate_transfer(proposal: &Proposal, intent_data: &SvmTransferIntentData) -> Result<()> { require_eq!(intent_data.chain_id, 507424, SettlerError::IncorrectChainId); require_eq!(proposal.instructions.len(), 0); Ok(()) } + +// TODO: better error names for requires diff --git a/packages/svm/tests/settler.test.ts b/packages/svm/tests/settler.test.ts index 0244ee0..3643050 100644 --- a/packages/svm/tests/settler.test.ts +++ b/packages/svm/tests/settler.test.ts @@ -2292,4 +2292,136 @@ describe('Settler', () => { itThrowsAnError('AnchorError caused by account: solver_registry. Error Code: AccountNotInitialized') }) }) + + describe('execute_proposal', () => { + context('when intent is transfer', () => { + const itThrowsAnError = () => { + it('throws an error', async () => { + // TODO + }) + } + + const itWorksAsExpected = () => { + context('when remaining accounts are correct', () => { + context('when transfer/s is/are valid', () => { + context('when protocol has approval', () => { + context('when user has sufficient funds', () => { + it('executes transfer', async () => {}) + }) + + context('when user does not have sufficient funds', () => { + itThrowsAnError() + }) + }) + + context('when protocol does not have approval', () => { + itThrowsAnError() + }) + }) + + context('when proposal is not valid', () => { + context('when proposal intent is not for chain Solana', () => { + itThrowsAnError() + }) + + context('when proposal has data/instructions', () => { + itThrowsAnError() + }) + }) + }) + + context('when remaining accounts are not correct', () => { + context('when remaining accounts number is correct', () => { + context('when token programs are passed correctly', () => { + context('when token is incorrect', () => { + itThrowsAnError() + }) + + context('when recipient is incorrect', () => { + itThrowsAnError() + }) + + context('when recipient token account is incorrect', () => { + context('when authority is incorrect', () => { + itThrowsAnError() + }) + + context('when token mint is incorrect', () => { + itThrowsAnError() + }) + }) + + context('when user token account is incorrect', () => { + context('when authority is incorrect', () => { + itThrowsAnError() + }) + + context('when token mint is incorrect', () => { + itThrowsAnError() + }) + }) + }) + + context('when token programs are not passed correctly', () => { + itThrowsAnError() + }) + }) + + context('when remaining accounts number is not correct', () => { + itThrowsAnError() + }) + }) + } + + context('when caller is allowlisted solver', () => { + context('when intent exists', () => { + context('when intent is correct', () => { + context('when proposal exists', () => { + context('when proposal is correct', () => { + context('when intent has one transfer', () => { + itWorksAsExpected() + }) + + context('when intent has more than one transfer', () => { + itWorksAsExpected() + }) + }) + + context('when proposal is not correct', () => { + context('when proposal is for another intent', () => { + itThrowsAnError() + }) + + context('when proposal is from another solver', () => { + itThrowsAnError() + }) + + context('when proposal is not signed', () => { + itThrowsAnError() + }) + + context('when proposal is expired', () => { + itThrowsAnError() + }) + }) + }) + }) + + context('when intent is not correct', () => { + context('when intent_creator is not correct', () => { + itThrowsAnError() + }) + }) + }) + }) + + context('when caller is not allowlisted solver', () => { + it('throws an error', async () => {}) + }) + }) + + context('when intent is not transfer', () => { + it('throws an error', async () => {}) + }) + }) }) From 53b273780fd40ed71a6a1af1023fe9e3f4598121 Mon Sep 17 00:00:00 2001 From: GuidoDipietro Date: Tue, 31 Mar 2026 15:03:01 -0300 Subject: [PATCH 06/19] Improvements in execute_proposal --- packages/svm/programs/settler/src/errors.rs | 25 +++++- .../settler/src/utils/execution/misc.rs | 9 ++ .../settler/src/utils/execution/transfer.rs | 84 ++++++++++++------- 3 files changed, 88 insertions(+), 30 deletions(-) diff --git a/packages/svm/programs/settler/src/errors.rs b/packages/svm/programs/settler/src/errors.rs index e30479d..b8e3ca6 100644 --- a/packages/svm/programs/settler/src/errors.rs +++ b/packages/svm/programs/settler/src/errors.rs @@ -59,6 +59,9 @@ pub enum SettlerError { #[msg("Proposal deadline can't be after the Intent's deadline")] ProposalDeadlineExceedsIntentDeadline, + #[msg("Incorrect proposal data")] + IncorrectProposalData, + #[msg("Intent has insufficient validations")] InsufficientIntentValidations, @@ -92,12 +95,30 @@ pub enum SettlerError { #[msg("Incorrect intent chain id")] IncorrectChainId, - #[msg("Invalid transfer recipient")] + #[msg("Invalid transfer recipient: malformed pubkey")] InvalidTransferRecipient, - #[msg("Invalid transfer token")] + #[msg("Incorrect transfer recipient account")] + IncorrectTransferRecipient, + + #[msg("Invalid transfer token: malformed pubkey")] InvalidTransferToken, + #[msg("Incorrect transfer token mint account")] + IncorrectTransferToken, + + #[msg("Account not owned by TokenKeg or Token2022 programs")] + AccountNotOwnedByTokenProgram, + + #[msg("Incorrect recipient token account: mint or authority do not match expected")] + IncorrectRecipientTokenAccount, + + #[msg("Incorrect user token account: mint or authority do not match expected")] + IncorrectUserTokenAccount, + + #[msg("Incorrect token program account provided")] + IncorrectTokenProgram, + #[msg("Math Error")] MathError, } diff --git a/packages/svm/programs/settler/src/utils/execution/misc.rs b/packages/svm/programs/settler/src/utils/execution/misc.rs index 68d0d6b..ef09ff9 100644 --- a/packages/svm/programs/settler/src/utils/execution/misc.rs +++ b/packages/svm/programs/settler/src/utils/execution/misc.rs @@ -1,4 +1,5 @@ use anchor_lang::prelude::*; +use anchor_spl::{token, token_2022}; use crate::{ errors::SettlerError, @@ -32,3 +33,11 @@ pub fn pay_solver_fees() -> Result<()> { // TODO Ok(()) } + +pub fn check_owner_is_token_program<'info>(account_info: &AccountInfo<'info>) -> Result<()> { + if *account_info.owner != token::ID && *account_info.owner != token_2022::ID { + err!(SettlerError::AccountNotOwnedByTokenProgram)?; + } + + Ok(()) +} diff --git a/packages/svm/programs/settler/src/utils/execution/transfer.rs b/packages/svm/programs/settler/src/utils/execution/transfer.rs index 17aae71..cff1099 100644 --- a/packages/svm/programs/settler/src/utils/execution/transfer.rs +++ b/packages/svm/programs/settler/src/utils/execution/transfer.rs @@ -10,6 +10,7 @@ use crate::{ errors::SettlerError, state::{Intent, Proposal}, types::{SvmTransfer, SvmTransferIntentData}, + utils::check_owner_is_token_program, }; pub fn handle_transfer<'info>( @@ -36,11 +37,11 @@ pub fn handle_transfer<'info>( } /// Deserializes and checks the following remaining_accounts: -/// +/// /// pub token_program: Program<'info, Token>, -/// +/// /// pub token_2022_program: Program<'info, Token2022>, -/// +/// fn execute_transfers<'info>( user: Pubkey, delegate: &AccountInfo<'info>, @@ -53,8 +54,16 @@ fn execute_transfers<'info>( let token_program = next_account_info(&mut remaining_accounts_iter)?; let token_2022_program = next_account_info(&mut remaining_accounts_iter)?; - require_keys_eq!(token_program.key(), anchor_spl::token::ID); - require_keys_eq!(token_2022_program.key(), anchor_spl::token_2022::ID); + require_keys_eq!( + token_program.key(), + anchor_spl::token::ID, + SettlerError::IncorrectTokenProgram + ); + require_keys_eq!( + token_2022_program.key(), + anchor_spl::token_2022::ID, + SettlerError::IncorrectTokenProgram + ); for transfer in &intent_data.transfers { execute_transfer( @@ -72,24 +81,24 @@ fn execute_transfers<'info>( } /// Deserializes and checks the following remaining_accounts: -/// +/// /// #[account( /// address = transfer.token, /// )] /// pub token: Account<'info, Mint>, -/// +/// /// #[account( /// address = transfer.recipient, /// )] /// pub recipient: AccountInfo<'info>, -/// +/// /// #[account( /// mut, /// token::authority = recipient, /// token::mint = token, /// )] /// pub recipient_token_account: Account<'info, TokenAccount>, -/// +/// /// #[account( /// mut, /// token::authority = user, @@ -97,7 +106,7 @@ fn execute_transfers<'info>( /// )] /// NOTE: must have PDA delegate approved and amount at least transfer.amount /// pub user_token_account: Account<'info, TokenAccount>, -/// +/// fn execute_transfer<'info>( transfer: &SvmTransfer, delegate: &AccountInfo<'info>, @@ -112,10 +121,8 @@ fn execute_transfer<'info>( let recipient_ta_account_info = next_account_info(remaining_accounts_iter)?; let user_ta_account_info = next_account_info(remaining_accounts_iter)?; - // TODO: validate account ownership - // token_account_info.owner = token or token2022 - // recipient_ta_account_info = token or token2022 - // user_ta_account_info = token or token2022 + check_owner_is_token_program(recipient_ta_account_info)?; + check_owner_is_token_program(user_ta_account_info)?; let mut token_data: &[u8] = &token_account_info.try_borrow_data()?; let token_mint = IMint::try_deserialize(&mut token_data)?; @@ -132,12 +139,32 @@ fn execute_transfer<'info>( let transfer_token = Pubkey::try_from(transfer.token.as_slice()) .map_err(|_| error!(SettlerError::InvalidTransferToken))?; - require_keys_eq!(transfer_recipient, recipient_account_info.key()); - require_keys_eq!(transfer_token, token_account_info.key()); - require_keys_eq!(recipient_ta.owner, recipient_account_info.key()); - require_keys_eq!(recipient_ta.mint, token_account_info.key()); - require_keys_eq!(user_ta.owner, user); - require_keys_eq!(user_ta.mint, token_account_info.key()); + require_keys_eq!( + transfer_recipient, + recipient_account_info.key(), + SettlerError::IncorrectTransferRecipient + ); + require_keys_eq!( + transfer_token, + token_account_info.key(), + SettlerError::IncorrectTransferToken + ); + require_keys_eq!( + recipient_ta.owner, + recipient_account_info.key(), + SettlerError::IncorrectRecipientTokenAccount + ); + require_keys_eq!( + recipient_ta.mint, + token_account_info.key(), + SettlerError::IncorrectRecipientTokenAccount + ); + require_keys_eq!(user_ta.owner, user, SettlerError::IncorrectUserTokenAccount); + require_keys_eq!( + user_ta.mint, + token_account_info.key(), + SettlerError::IncorrectUserTokenAccount + ); let cpi_accounts = TransferChecked { authority: delegate.clone(), @@ -146,11 +173,10 @@ fn execute_transfer<'info>( to: recipient_ta_account_info.clone(), }; - let cpi_program = if *token_account_info.owner == anchor_spl::token::ID { - token_program.clone() - } else { - // TODO: validate that it is token2022, otherwise err - token_2022_program.clone() + let cpi_program = match *token_account_info.owner { + anchor_spl::token::ID => token_program.clone(), + anchor_spl::token_2022::ID => token_2022_program.clone(), + _ => err!(SettlerError::AccountNotOwnedByTokenProgram)?, }; let cpi_ctx = CpiContext::new_with_signer(cpi_program, cpi_accounts, delegate_seeds); @@ -159,9 +185,11 @@ fn execute_transfer<'info>( fn validate_transfer(proposal: &Proposal, intent_data: &SvmTransferIntentData) -> Result<()> { require_eq!(intent_data.chain_id, 507424, SettlerError::IncorrectChainId); - require_eq!(proposal.instructions.len(), 0); + require_eq!( + proposal.instructions.len(), + 0, + SettlerError::IncorrectProposalData + ); Ok(()) } - -// TODO: better error names for requires From 1b7824c110d9a7680f64213c09085373c747d1ed Mon Sep 17 00:00:00 2001 From: GuidoDipietro Date: Tue, 31 Mar 2026 17:29:19 -0300 Subject: [PATCH 07/19] WIP tests --- packages/svm/tests/settler.test.ts | 232 ++++++++++++++++++++++++++--- 1 file changed, 208 insertions(+), 24 deletions(-) diff --git a/packages/svm/tests/settler.test.ts b/packages/svm/tests/settler.test.ts index 3643050..da75f59 100644 --- a/packages/svm/tests/settler.test.ts +++ b/packages/svm/tests/settler.test.ts @@ -1,7 +1,7 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-non-null-assertion */ -import { Address, Program, translateAddress, Wallet } from '@coral-xyz/anchor' +import { Address, Program, translateAddress, Wallet, web3 } from '@coral-xyz/anchor' import { bytesToHex, Chains, @@ -10,7 +10,9 @@ import { EthersSigner, ExtendIntentParams, hexToBytes, + Intent, OpType, + Proposal, ProposalInstruction, ProposalSigner, randomHex, @@ -18,9 +20,20 @@ import { SolanaEip712Domain, SvmController, SvmSettler, + TransferIntentData, ValidatorSigner, } from '@mimicprotocol/sdk' +import { svmEncodeTransferIntent } from '@mimicprotocol/sdk/dist/shared/codec/chains/svm' import { + createAssociatedTokenAccountInstruction, + createInitializeMintInstruction, + createMintToInstruction, + getAssociatedTokenAddressSync, + TOKEN_2022_PROGRAM_ID, + TOKEN_PROGRAM_ID, +} from '@solana/spl-token' +import { + AccountMeta, CreateSecp256k1InstructionWithEthAddressParams, Keypair, PublicKey, @@ -2294,11 +2307,153 @@ describe('Settler', () => { }) describe('execute_proposal', () => { - context('when intent is transfer', () => { - const itThrowsAnError = () => { - it('throws an error', async () => { - // TODO + let ix: TransactionInstruction + let intentHash: string + let intent: Intent + let proposal: Proposal + let remainingAccounts: AccountMeta[] + + const user = randomKeypair() + const usdc = randomKeypair() + const recipient = randomPubkey() + + let userAta: web3.PublicKey + let recipientAta: web3.PublicKey + + const validator = ethers.Wallet.createRandom() + const axia = ethers.Wallet.createRandom() + + const createIx = async ( + sdk: SvmSettler, + accountsPartial: Partial<{ + solver: web3.PublicKey + proposalCreator: web3.PublicKey + proposal: web3.PublicKey + intentCreator: web3.PublicKey + intent: web3.PublicKey + delegate: web3.PublicKey + }> = {} + ) => { + const ix = await settler.methods + .executeProposal() + .accountsPartial({ + solver: sdk.getSignerKey(), + solverRegistry: sdk.getEntityRegistryKey(EntityType.Solver, sdk.getSignerKey()), + proposalCreator: translateAddress(proposal.solver), + proposal: sdk.getProposalKey(intentHash, proposal.solver), + intentCreator: proposal.solver, + intent: sdk.getIntentKey(intentHash), + fulfilledIntent: sdk.getFulfilledIntentKey(intentHash), + delegate: sdk.getDelegateKey(intent.user), + ...accountsPartial, }) + .remainingAccounts(remainingAccounts) + .instruction() + return ix + } + + const itThrowsAnError = async (error: string) => { + it('throws an error', async () => { + const res = await makeTxSignAndSend(solverProvider, ix) + expectTransactionError(res.toString(), error) + }) + } + + const createTestIntent = (data: string): Intent => ({ + configSig: randomHex(32), + data, + deadline: (Date.now() + 1000).toString(), + events: [{ topic: randomHex(32), data: randomHex(50) }], + maxFees: [{ token: usdc.publicKey.toString(), amount: '100' }], + minValidations: 1, + nonce: randomHex(32), + op: 1, + settler: settler.programId.toString(), + user: user.publicKey.toString(), + }) + + const createTestProposal = ( + intent: Intent, + data = '0x', + solver: PublicKey = solverSdk.getSignerKey() + ): Proposal => ({ + data, + deadline: intent.deadline, + fees: intent.maxFees.map((mf) => mf.amount), + solver: solver.toString(), + }) + + const prepareIntentAndProposal = async (sdk: SvmSettler = solverSdk) => { + await makeTxSignAndSend(solverProvider, await sdk.createIntentIx(intentHash, intent, true)) + + const validatorSig = await new ValidatorSigner(EthersSigner.fromPrivateKey(validator.privateKey)).signIntentHash({ + hash: intentHash, + settler: settler.programId.toString(), + chainId: Chains.Solana, + }) + + await makeTxSignAndSend( + solverProvider, + ...(await sdk.addValidatorSigIxs(intentHash, validator.address, validatorSig)) + ) + + await makeTxSignAndSend( + solverProvider, + await sdk.createProposalIx(intentHash, { + instructions: [], + ...proposal, + isFinal: true, + }) + ) + + const axiaSig = await new ProposalSigner(EthersSigner.fromPrivateKey(axia.privateKey)).signProposal( + { ...proposal, intent: intentHash }, + { + address: settler.programId.toString(), + chainId: Chains.Solana, + } + ) + + await makeTxSignAndSend(solverProvider, ...(await sdk.addAxiaSigIxs(intentHash, proposal, axia.address, axiaSig))) + } + + before('Create validator, Axia, USDC and fund user', async () => { + await createAllowlistedEntity(controllerSdk, adminProvider, EntityType.Validator, hexToBytes(validator.address)) + await createAllowlistedEntity(controllerSdk, adminProvider, EntityType.Axia, hexToBytes(axia.address)) + + adminProvider.client.airdrop(admin.publicKey, toLamports(100)) + userAta = getAssociatedTokenAddressSync(usdc.publicKey, user.publicKey) + recipientAta = getAssociatedTokenAddressSync(usdc.publicKey, recipient) + + const createUsdcIx = createInitializeMintInstruction(usdc.publicKey, 9, admin.publicKey, null) + const createAtaIxs = [ + createAssociatedTokenAccountInstruction(admin.publicKey, userAta, user.publicKey, usdc.publicKey), + createAssociatedTokenAccountInstruction(admin.publicKey, recipientAta, recipient, usdc.publicKey), + ] + const mintTokensIx = createMintToInstruction(usdc.publicKey, userAta, admin.publicKey, 100_000_000_000) + + // TODO: fix this part too + await makeTxSignAndSend(adminProvider, createUsdcIx, ...createAtaIxs, mintTokensIx) + }) + + context('when intent is transfer', () => { + const createTestIntentData = (transfers?: TransferIntentData['transfers']): TransferIntentData => ({ + chainId: Chains.Solana, + transfers: transfers ?? [], + }) + + const getRemainingAccounts = (transfers: TransferIntentData['transfers']): AccountMeta[] => { + const tokenProgram: AccountMeta = { pubkey: TOKEN_PROGRAM_ID, isSigner: false, isWritable: false } + const token2022Program: AccountMeta = { pubkey: TOKEN_2022_PROGRAM_ID, isSigner: false, isWritable: false } + + const transferAccounts = transfers.flatMap((transfer) => [ + { pubkey: translateAddress(transfer.token), isSigner: false, isWritable: false }, + { pubkey: translateAddress(transfer.recipient), isSigner: false, isWritable: false }, + { pubkey: recipientAta, isSigner: false, isWritable: true }, + { pubkey: userAta, isSigner: false, isWritable: true }, + ]) + + return [tokenProgram, token2022Program, ...transferAccounts] } const itWorksAsExpected = () => { @@ -2306,26 +2461,55 @@ describe('Settler', () => { context('when transfer/s is/are valid', () => { context('when protocol has approval', () => { context('when user has sufficient funds', () => { - it('executes transfer', async () => {}) + const transfers = [ + { + amount: '1000000000', + token: usdc.publicKey.toString(), + recipient: recipient.toString(), + }, + ] + + const testIntentData = createTestIntentData(transfers) + + beforeEach(async () => { + intentHash = randomHex(32) + intent = createTestIntent(svmEncodeTransferIntent(testIntentData)) + proposal = createTestProposal(intent) + remainingAccounts = getRemainingAccounts(transfers) + + await prepareIntentAndProposal() + + ix = await createIx(solverSdk) + }) + + it('executes transfer', async () => { + const res = await makeTxSignAndSend(solverProvider, ix) + console.log(res.toString()) + + // Check intent is closed + // Check proposal is closed + // Check fulfilled intent is initialized + // Check tokens were sent correctly + }) }) context('when user does not have sufficient funds', () => { - itThrowsAnError() + itThrowsAnError('Insufficient funds') }) }) context('when protocol does not have approval', () => { - itThrowsAnError() + itThrowsAnError('Delegate mismatch') }) }) context('when proposal is not valid', () => { context('when proposal intent is not for chain Solana', () => { - itThrowsAnError() + itThrowsAnError('Incorrect chain id') }) context('when proposal has data/instructions', () => { - itThrowsAnError() + itThrowsAnError('Incorrect proposal data') }) }) }) @@ -2334,41 +2518,41 @@ describe('Settler', () => { context('when remaining accounts number is correct', () => { context('when token programs are passed correctly', () => { context('when token is incorrect', () => { - itThrowsAnError() + itThrowsAnError('Incorrect token mint') }) context('when recipient is incorrect', () => { - itThrowsAnError() + itThrowsAnError('Incorrect recipient') }) context('when recipient token account is incorrect', () => { context('when authority is incorrect', () => { - itThrowsAnError() + itThrowsAnError('Incorrect recipient token account authority') }) context('when token mint is incorrect', () => { - itThrowsAnError() + itThrowsAnError('Incorrect recipient token account mint') }) }) context('when user token account is incorrect', () => { context('when authority is incorrect', () => { - itThrowsAnError() + itThrowsAnError('Incorrect user token account authority') }) context('when token mint is incorrect', () => { - itThrowsAnError() + itThrowsAnError('Incorrect user token account mint') }) }) }) context('when token programs are not passed correctly', () => { - itThrowsAnError() + itThrowsAnError('Incorrect token program account') }) }) context('when remaining accounts number is not correct', () => { - itThrowsAnError() + itThrowsAnError('ProgramError') }) }) } @@ -2389,19 +2573,19 @@ describe('Settler', () => { context('when proposal is not correct', () => { context('when proposal is for another intent', () => { - itThrowsAnError() + itThrowsAnError('Incorrect intent for proposal') }) - context('when proposal is from another solver', () => { - itThrowsAnError() + context('when proposal is from another proposal creator', () => { + itThrowsAnError('Incorrect proposal creator') }) context('when proposal is not signed', () => { - itThrowsAnError() + itThrowsAnError('Proposal is not signed') }) context('when proposal is expired', () => { - itThrowsAnError() + itThrowsAnError('Proposal is expired') }) }) }) @@ -2409,7 +2593,7 @@ describe('Settler', () => { context('when intent is not correct', () => { context('when intent_creator is not correct', () => { - itThrowsAnError() + itThrowsAnError('Incorrect intent creator') }) }) }) From a0ed7e216e2abf330be56ea03e4874a822971b81 Mon Sep 17 00:00:00 2001 From: GuidoDipietro Date: Tue, 7 Apr 2026 16:30:27 -0300 Subject: [PATCH 08/19] Fix double borrow issue in Rust --- .../settler/src/utils/execution/transfer.rs | 21 +++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/packages/svm/programs/settler/src/utils/execution/transfer.rs b/packages/svm/programs/settler/src/utils/execution/transfer.rs index cff1099..b084f07 100644 --- a/packages/svm/programs/settler/src/utils/execution/transfer.rs +++ b/packages/svm/programs/settler/src/utils/execution/transfer.rs @@ -124,14 +124,23 @@ fn execute_transfer<'info>( check_owner_is_token_program(recipient_ta_account_info)?; check_owner_is_token_program(user_ta_account_info)?; - let mut token_data: &[u8] = &token_account_info.try_borrow_data()?; - let token_mint = IMint::try_deserialize(&mut token_data)?; + let token_mint = { + let token_data: &[u8] = &token_account_info.try_borrow_data()?; + let mut token_data_ref: &[u8] = &token_data; + IMint::try_deserialize(&mut token_data_ref)? + }; - let mut recipient_ta_data: &[u8] = &recipient_ta_account_info.try_borrow_data()?; - let recipient_ta = ITokenAccount::try_deserialize(&mut recipient_ta_data)?; + let recipient_ta = { + let recipient_ta_data = recipient_ta_account_info.try_borrow_data()?; + let mut recipient_ta_data_ref: &[u8] = &recipient_ta_data; + ITokenAccount::try_deserialize(&mut recipient_ta_data_ref)? + }; - let mut user_ta_data: &[u8] = &user_ta_account_info.try_borrow_data()?; - let user_ta = ITokenAccount::try_deserialize(&mut user_ta_data)?; + let user_ta = { + let user_ta_data = user_ta_account_info.try_borrow_data()?; + let mut user_ta_data_ref: &[u8] = &user_ta_data; + ITokenAccount::try_deserialize(&mut user_ta_data_ref)? + }; let transfer_recipient = Pubkey::try_from(transfer.recipient.as_slice()) .map_err(|_| error!(SettlerError::InvalidTransferRecipient))?; From 9ab7b69c4574e026e4866db1c7cae7fa3acfa535 Mon Sep 17 00:00:00 2001 From: GuidoDipietro Date: Tue, 7 Apr 2026 16:32:11 -0300 Subject: [PATCH 09/19] Fix execute_proposal tests scaffold --- packages/svm/tests/helpers/index.ts | 1 + packages/svm/tests/helpers/spl.ts | 105 ++++++++++++++++++++++++++++ packages/svm/tests/settler.test.ts | 100 ++++++++++++++------------ 3 files changed, 163 insertions(+), 43 deletions(-) create mode 100644 packages/svm/tests/helpers/spl.ts diff --git a/packages/svm/tests/helpers/index.ts b/packages/svm/tests/helpers/index.ts index adddc2d..f33fef4 100644 --- a/packages/svm/tests/helpers/index.ts +++ b/packages/svm/tests/helpers/index.ts @@ -4,3 +4,4 @@ export * from './intents' export * from './misc' export * from './proposals' export * from './signatures' +export * from './spl' diff --git a/packages/svm/tests/helpers/spl.ts b/packages/svm/tests/helpers/spl.ts new file mode 100644 index 0000000..8ea3901 --- /dev/null +++ b/packages/svm/tests/helpers/spl.ts @@ -0,0 +1,105 @@ +import { Address, translateAddress, web3 } from '@coral-xyz/anchor' +import { + createApproveInstruction, + createAssociatedTokenAccountInstruction, + createInitializeMint2Instruction, + createMintToInstruction, + getAssociatedTokenAddressSync, + MINT_SIZE, + TOKEN_PROGRAM_ID, +} from '@solana/spl-token' +import { LiteSVMProvider } from 'anchor-litesvm' +import { FailedTransactionMetadata, LiteSVM, TransactionMetadata } from 'litesvm' + +import { makeTx, makeTxSignAndSend } from '../utils' +import { randomKeypair } from './misc' + +export type CreateMintParams = { + decimals: number + freezeAuthority: Address | null + programId?: Address +} + +export type CreateMintResult = { + mint: web3.PublicKey + res: TransactionMetadata | FailedTransactionMetadata +} + +const DEFAULT_CREATE_MINT_PARAMS: CreateMintParams = { + decimals: 9, + freezeAuthority: null, +} + +export function createMint( + client: LiteSVM, + mintAuthority: web3.Keypair, + params: Partial +): CreateMintResult { + const mint = randomKeypair() + const { decimals, freezeAuthority, programId } = { ...DEFAULT_CREATE_MINT_PARAMS, ...params } + const tokenProgramId = translateAddress(programId ?? TOKEN_PROGRAM_ID) + + const createMintAccountIx = web3.SystemProgram.createAccount({ + fromPubkey: mintAuthority.publicKey, + newAccountPubkey: mint.publicKey, + space: MINT_SIZE, + lamports: Number(client.minimumBalanceForRentExemption(BigInt(MINT_SIZE))), + programId: tokenProgramId, + }) + + const initializeMintIx = createInitializeMint2Instruction( + mint.publicKey, + decimals, + mintAuthority.publicKey, + freezeAuthority ? translateAddress(freezeAuthority) : null, + tokenProgramId + ) + + const tx = makeTx(createMintAccountIx, initializeMintIx) + tx.recentBlockhash = client.latestBlockhash() + tx.feePayer = mintAuthority.publicKey + tx.sign(mintAuthority, mint) + + return { mint: mint.publicKey, res: client.sendTransaction(tx) } +} + +export type CreatFundedAtaResult = { + ata: web3.PublicKey + res: TransactionMetadata | FailedTransactionMetadata +} + +export async function createFundedAta( + provider: LiteSVMProvider, + admin: web3.Keypair, + wallet: Address, + mint: Address, + amount: number, + programId?: Address +): Promise { + const mintKey = translateAddress(mint) + const walletKey = translateAddress(wallet) + const ata = getAssociatedTokenAddressSync( + mintKey, + walletKey, + true, + programId ? translateAddress(programId) : undefined + ) + + const ixs = [createAssociatedTokenAccountInstruction(admin.publicKey, ata, walletKey, mintKey)] + if (amount > 0) ixs.push(createMintToInstruction(mintKey, ata, admin.publicKey, amount)) + + const res = await makeTxSignAndSend(provider, ...ixs) + + return { ata, res } +} + +export async function approveDelegate( + provider: LiteSVMProvider, + ata: Address, + delegate: Address, + owner: web3.Keypair, + amount: number +): Promise { + const ix = createApproveInstruction(translateAddress(ata), translateAddress(delegate), owner.publicKey, amount) + return makeTxSignAndSend(provider, ix) +} diff --git a/packages/svm/tests/settler.test.ts b/packages/svm/tests/settler.test.ts index da75f59..2bcbdde 100644 --- a/packages/svm/tests/settler.test.ts +++ b/packages/svm/tests/settler.test.ts @@ -24,14 +24,7 @@ import { ValidatorSigner, } from '@mimicprotocol/sdk' import { svmEncodeTransferIntent } from '@mimicprotocol/sdk/dist/shared/codec/chains/svm' -import { - createAssociatedTokenAccountInstruction, - createInitializeMintInstruction, - createMintToInstruction, - getAssociatedTokenAddressSync, - TOKEN_2022_PROGRAM_ID, - TOKEN_PROGRAM_ID, -} from '@solana/spl-token' +import { TOKEN_2022_PROGRAM_ID, TOKEN_PROGRAM_ID } from '@solana/spl-token' import { AccountMeta, CreateSecp256k1InstructionWithEthAddressParams, @@ -94,6 +87,7 @@ import { WARP_TIME_LONG, WARP_TIME_SHORT, } from './helpers' +import { approveDelegate, createFundedAta, createMint } from './helpers/spl' import { makeTxSignAndSend, warpSeconds } from './utils' describe('Settler', () => { @@ -185,7 +179,7 @@ describe('Settler', () => { }) }) - describe('update_eip712_domain', () => { + describe.skip('update_eip712_domain', () => { context('when caller is controller admin', () => { context('when domain is valid', () => { const itUpdatesDomainCorrectly = (testCase: string, domain: SolanaEip712Domain) => { @@ -246,7 +240,7 @@ describe('Settler', () => { }) }) - describe('create_intent', () => { + describe.skip('create_intent', () => { let intentHash: string let intentOptions: CreateIntentOptions = {} @@ -524,7 +518,7 @@ describe('Settler', () => { }) }) - describe('extend_intent', () => { + describe.skip('extend_intent', () => { let intentHash: string let intentKey: PublicKey let extendParams: ExtendIntentParams = {} @@ -827,7 +821,7 @@ describe('Settler', () => { }) }) - describe('claim_stale_intent', () => { + describe.skip('claim_stale_intent', () => { let intentHash: string context('when caller is intent creator', () => { @@ -973,7 +967,7 @@ describe('Settler', () => { }) }) - describe('create_proposal', () => { + describe.skip('create_proposal', () => { let params: CreateProposalParams & { intentHash: string } const createProposalFromParams = async () => { @@ -1316,7 +1310,7 @@ describe('Settler', () => { }) }) - describe('add_instructions_to_proposal', () => { + describe.skip('add_instructions_to_proposal', () => { const createTestProposal = async (options?: CreateProposalOptions): Promise => { const params = await createProposalParams(solverSdk, solverProvider, client, options) const ix = await solverSdk.createProposalIx(params.intentHash, params) @@ -1541,7 +1535,7 @@ describe('Settler', () => { }) }) - describe('claim_stale_proposal', () => { + describe.skip('claim_stale_proposal', () => { const createTestProposal = async (options?: CreateProposalOptions): Promise => { const params = await createProposalParams(solverSdk, solverProvider, client, options) const ix = await solverSdk.createProposalIx(params.intentHash, params) @@ -1662,7 +1656,7 @@ describe('Settler', () => { }) }) - describe('add_validator_sigs', () => { + describe.skip('add_validator_sigs', () => { const createAllowlistedValidator = async () => { const validator = ethers.Wallet.createRandom() await createAllowlistedEntity(controllerSdk, adminProvider, EntityType.Validator, hexToBytes(validator.address)) @@ -1996,7 +1990,7 @@ describe('Settler', () => { }) }) - describe('add_axia_sig', () => { + describe.skip('add_axia_sig', () => { const createAllowlistedAxia = async () => { const axia = ethers.Wallet.createRandom() await createAllowlistedEntity(controllerSdk, adminProvider, EntityType.Axia, hexToBytes(axia.address)) @@ -2313,9 +2307,12 @@ describe('Settler', () => { let proposal: Proposal let remainingAccounts: AccountMeta[] - const user = randomKeypair() - const usdc = randomKeypair() - const recipient = randomPubkey() + let usdc: web3.PublicKey + + let user: Keypair + let recipient: web3.PublicKey + + let userProvider: LiteSVMProvider let userAta: web3.PublicKey let recipientAta: web3.PublicKey @@ -2364,7 +2361,7 @@ describe('Settler', () => { data, deadline: (Date.now() + 1000).toString(), events: [{ topic: randomHex(32), data: randomHex(50) }], - maxFees: [{ token: usdc.publicKey.toString(), amount: '100' }], + maxFees: [{ token: usdc.toString(), amount: '100' }], minValidations: 1, nonce: randomHex(32), op: 1, @@ -2388,7 +2385,7 @@ describe('Settler', () => { const validatorSig = await new ValidatorSigner(EthersSigner.fromPrivateKey(validator.privateKey)).signIntentHash({ hash: intentHash, - settler: settler.programId.toString(), + settler: '', chainId: Chains.Solana, }) @@ -2417,23 +2414,29 @@ describe('Settler', () => { await makeTxSignAndSend(solverProvider, ...(await sdk.addAxiaSigIxs(intentHash, proposal, axia.address, axiaSig))) } + before('set correct domain', async () => { + client.expireBlockhash() + const ix = await adminSdk.updateEip712DomainIx({ + ...SETTLER_EIP712_DOMAIN, + chainId: Chains.Solana, + }) + await makeTxSignAndSend(adminProvider, ix) + }) + before('Create validator, Axia, USDC and fund user', async () => { + user = randomKeypair() + recipient = randomPubkey() + userProvider = new LiteSVMProvider(client, new Wallet(user)) + + adminProvider.client.airdrop(user.publicKey, toLamports(100)) + await createAllowlistedEntity(controllerSdk, adminProvider, EntityType.Validator, hexToBytes(validator.address)) await createAllowlistedEntity(controllerSdk, adminProvider, EntityType.Axia, hexToBytes(axia.address)) - adminProvider.client.airdrop(admin.publicKey, toLamports(100)) - userAta = getAssociatedTokenAddressSync(usdc.publicKey, user.publicKey) - recipientAta = getAssociatedTokenAddressSync(usdc.publicKey, recipient) + usdc = createMint(client, admin, { decimals: 9, freezeAuthority: null }).mint - const createUsdcIx = createInitializeMintInstruction(usdc.publicKey, 9, admin.publicKey, null) - const createAtaIxs = [ - createAssociatedTokenAccountInstruction(admin.publicKey, userAta, user.publicKey, usdc.publicKey), - createAssociatedTokenAccountInstruction(admin.publicKey, recipientAta, recipient, usdc.publicKey), - ] - const mintTokensIx = createMintToInstruction(usdc.publicKey, userAta, admin.publicKey, 100_000_000_000) - - // TODO: fix this part too - await makeTxSignAndSend(adminProvider, createUsdcIx, ...createAtaIxs, mintTokensIx) + userAta = (await createFundedAta(adminProvider, admin, user.publicKey, usdc, 100_000_000_000)).ata + recipientAta = (await createFundedAta(adminProvider, admin, recipient, usdc, 0)).ata }) context('when intent is transfer', () => { @@ -2461,17 +2464,28 @@ describe('Settler', () => { context('when transfer/s is/are valid', () => { context('when protocol has approval', () => { context('when user has sufficient funds', () => { - const transfers = [ - { - amount: '1000000000', - token: usdc.publicKey.toString(), - recipient: recipient.toString(), - }, - ] + let transfers + let testIntentData + + beforeEach('Create data and approve delegate', async () => { + transfers = [ + { + amount: '1000000000', + token: usdc.toString(), + recipient: recipient.toString(), + }, + ] + + await approveDelegate( + userProvider, + userAta, + solverSdk.getDelegateKey(user.publicKey), + user, + Number(transfers[0].amount) + ) - const testIntentData = createTestIntentData(transfers) + testIntentData = createTestIntentData(transfers) - beforeEach(async () => { intentHash = randomHex(32) intent = createTestIntent(svmEncodeTransferIntent(testIntentData)) proposal = createTestProposal(intent) From 4d9ea5c23dc3ee639a59652321243c5660b73712 Mon Sep 17 00:00:00 2001 From: GuidoDipietro Date: Tue, 7 Apr 2026 17:59:56 -0300 Subject: [PATCH 10/19] Implement execute_proposal happy path! --- packages/svm/tests/helpers/spl.ts | 12 ++++++ packages/svm/tests/settler.test.ts | 60 +++++++++++++++++++++++++----- 2 files changed, 63 insertions(+), 9 deletions(-) diff --git a/packages/svm/tests/helpers/spl.ts b/packages/svm/tests/helpers/spl.ts index 8ea3901..80547fb 100644 --- a/packages/svm/tests/helpers/spl.ts +++ b/packages/svm/tests/helpers/spl.ts @@ -7,6 +7,7 @@ import { getAssociatedTokenAddressSync, MINT_SIZE, TOKEN_PROGRAM_ID, + unpackAccount, } from '@solana/spl-token' import { LiteSVMProvider } from 'anchor-litesvm' import { FailedTransactionMetadata, LiteSVM, TransactionMetadata } from 'litesvm' @@ -103,3 +104,14 @@ export async function approveDelegate( const ix = createApproveInstruction(translateAddress(ata), translateAddress(delegate), owner.publicKey, amount) return makeTxSignAndSend(provider, ix) } + +export function getAtaBalance(client: LiteSVM, ata: Address, programId: Address = TOKEN_PROGRAM_ID): number { + const ataKey = translateAddress(ata) + const ataAccount = client.getAccount(ataKey) + + if (!ataAccount) return 0 + + return Number( + unpackAccount(ataKey, { ...ataAccount, data: Buffer.from(ataAccount.data) }, translateAddress(programId)).amount + ) +} diff --git a/packages/svm/tests/settler.test.ts b/packages/svm/tests/settler.test.ts index 2bcbdde..6def906 100644 --- a/packages/svm/tests/settler.test.ts +++ b/packages/svm/tests/settler.test.ts @@ -87,7 +87,7 @@ import { WARP_TIME_LONG, WARP_TIME_SHORT, } from './helpers' -import { approveDelegate, createFundedAta, createMint } from './helpers/spl' +import { approveDelegate, createFundedAta, createMint, getAtaBalance } from './helpers/spl' import { makeTxSignAndSend, warpSeconds } from './utils' describe('Settler', () => { @@ -2464,8 +2464,8 @@ describe('Settler', () => { context('when transfer/s is/are valid', () => { context('when protocol has approval', () => { context('when user has sufficient funds', () => { - let transfers - let testIntentData + let transfers: TransferIntentData['transfers'] + let testIntentData: TransferIntentData beforeEach('Create data and approve delegate', async () => { transfers = [ @@ -2497,13 +2497,55 @@ describe('Settler', () => { }) it('executes transfer', async () => { - const res = await makeTxSignAndSend(solverProvider, ix) - console.log(res.toString()) + const proposalKey = sdk.getProposalKey(intentHash, proposal.solver) + const intentKey = sdk.getIntentKey(intentHash) + const fulfilledIntentKey = sdk.getFulfilledIntentKey(intentHash) - // Check intent is closed - // Check proposal is closed - // Check fulfilled intent is initialized - // Check tokens were sent correctly + const proposalBalanceBefore = Number(adminProvider.client.getBalance(proposalKey)) || 0 + const intentBalanceBefore = Number(adminProvider.client.getBalance(intentKey)) || 0 + const solverBalanceBefore = + Number(adminProvider.client.getBalance(translateAddress(proposal.solver))) || 0 + + const recipientBalanceBefore = getAtaBalance(client, recipientAta) + const userBalanceBefore = getAtaBalance(client, userAta) + + await makeTxSignAndSend(solverProvider, ix) + + const recipientBalanceAfter = getAtaBalance(client, recipientAta) + const userBalanceAfter = getAtaBalance(client, userAta) + + const proposalBalanceAfter = Number(adminProvider.client.getBalance(proposalKey)) || 0 + const intentBalanceAfter = Number(adminProvider.client.getBalance(intentKey)) || 0 + const solverBalanceAfter = + Number(adminProvider.client.getBalance(translateAddress(proposal.solver))) || 0 + const fulfilledIntentBalanceAfter = Number(adminProvider.client.getBalance(fulfilledIntentKey)) || 0 + + try { + await settler.account.intent.fetch(intentKey) + expect.fail('Intent account should be closed') + } catch (error: any) { + expect(error.message).to.include('Account does not exist') + } + + try { + await settler.account.proposal.fetch(proposalKey) + expect.fail('Proposal account should be closed') + } catch (error: any) { + expect(error.message).to.include('Account does not exist') + } + + expect(client.getAccount(fulfilledIntentKey)?.owner.toString()).to.be.eq(settler.programId.toString()) + expect(recipientBalanceAfter).to.be.eq(recipientBalanceBefore + Number(transfers[0].amount)) + expect(userBalanceAfter).to.be.eq(userBalanceBefore - Number(transfers[0].amount)) + expect(solverBalanceAfter).to.be.eq( + solverBalanceBefore + + intentBalanceBefore + + proposalBalanceBefore - + fulfilledIntentBalanceAfter - + 5000 + ) + expect(proposalBalanceAfter).to.be.eq(0) + expect(intentBalanceAfter).to.be.eq(0) }) }) From 7b1a75dc2b3dc51e0df4ed15fa17dcb1fc966b7f Mon Sep 17 00:00:00 2001 From: GuidoDipietro Date: Tue, 7 Apr 2026 18:19:22 -0300 Subject: [PATCH 11/19] Implement some unhappy paths --- packages/svm/tests/helpers/spl.ts | 10 +++ packages/svm/tests/settler.test.ts | 126 +++++++++++++++++++++++++---- 2 files changed, 121 insertions(+), 15 deletions(-) diff --git a/packages/svm/tests/helpers/spl.ts b/packages/svm/tests/helpers/spl.ts index 80547fb..264f7f6 100644 --- a/packages/svm/tests/helpers/spl.ts +++ b/packages/svm/tests/helpers/spl.ts @@ -4,6 +4,7 @@ import { createAssociatedTokenAccountInstruction, createInitializeMint2Instruction, createMintToInstruction, + createRevokeInstruction, getAssociatedTokenAddressSync, MINT_SIZE, TOKEN_PROGRAM_ID, @@ -105,6 +106,15 @@ export async function approveDelegate( return makeTxSignAndSend(provider, ix) } +export async function revokeDelegate( + provider: LiteSVMProvider, + ata: Address, + owner: web3.Keypair +): Promise { + const ix = createRevokeInstruction(translateAddress(ata), owner.publicKey) + return makeTxSignAndSend(provider, ix) +} + export function getAtaBalance(client: LiteSVM, ata: Address, programId: Address = TOKEN_PROGRAM_ID): number { const ataKey = translateAddress(ata) const ataAccount = client.getAccount(ataKey) diff --git a/packages/svm/tests/settler.test.ts b/packages/svm/tests/settler.test.ts index 6def906..3947e09 100644 --- a/packages/svm/tests/settler.test.ts +++ b/packages/svm/tests/settler.test.ts @@ -87,7 +87,7 @@ import { WARP_TIME_LONG, WARP_TIME_SHORT, } from './helpers' -import { approveDelegate, createFundedAta, createMint, getAtaBalance } from './helpers/spl' +import { approveDelegate, createFundedAta, createMint, getAtaBalance, revokeDelegate } from './helpers/spl' import { makeTxSignAndSend, warpSeconds } from './utils' describe('Settler', () => { @@ -2352,6 +2352,7 @@ describe('Settler', () => { const itThrowsAnError = async (error: string) => { it('throws an error', async () => { const res = await makeTxSignAndSend(solverProvider, ix) + console.log(res.toString()) expectTransactionError(res.toString(), error) }) } @@ -2460,21 +2461,23 @@ describe('Settler', () => { } const itWorksAsExpected = () => { + let transfers: TransferIntentData['transfers'] + let testIntentData: TransferIntentData + + const testTransfers = () => [ + { + amount: '1000000000', + token: usdc.toString(), + recipient: recipient.toString(), + }, + ] + context('when remaining accounts are correct', () => { context('when transfer/s is/are valid', () => { context('when protocol has approval', () => { context('when user has sufficient funds', () => { - let transfers: TransferIntentData['transfers'] - let testIntentData: TransferIntentData - beforeEach('Create data and approve delegate', async () => { - transfers = [ - { - amount: '1000000000', - token: usdc.toString(), - recipient: recipient.toString(), - }, - ] + transfers = testTransfers() await approveDelegate( userProvider, @@ -2485,7 +2488,6 @@ describe('Settler', () => { ) testIntentData = createTestIntentData(transfers) - intentHash = randomHex(32) intent = createTestIntent(svmEncodeTransferIntent(testIntentData)) proposal = createTestProposal(intent) @@ -2550,21 +2552,115 @@ describe('Settler', () => { }) context('when user does not have sufficient funds', () => { - itThrowsAnError('Insufficient funds') + beforeEach('Create data and approve delegate', async () => { + transfers = [ + { + amount: '1000000000000', + token: usdc.toString(), + recipient: recipient.toString(), + }, + ] + + await approveDelegate( + userProvider, + userAta, + solverSdk.getDelegateKey(user.publicKey), + user, + Number(transfers[0].amount) + ) + + testIntentData = createTestIntentData(transfers) + intentHash = randomHex(32) + intent = createTestIntent(svmEncodeTransferIntent(testIntentData)) + proposal = createTestProposal(intent) + remainingAccounts = getRemainingAccounts(transfers) + + await prepareIntentAndProposal() + + ix = await createIx(solverSdk) + }) + + itThrowsAnError('insufficient funds') }) }) context('when protocol does not have approval', () => { - itThrowsAnError('Delegate mismatch') + beforeEach('Create data and remove delegate', async () => { + await revokeDelegate(userProvider, userAta, user) + + transfers = testTransfers() + testIntentData = createTestIntentData(transfers) + intentHash = randomHex(32) + intent = createTestIntent(svmEncodeTransferIntent(testIntentData)) + proposal = createTestProposal(intent) + remainingAccounts = getRemainingAccounts(transfers) + + await prepareIntentAndProposal() + + ix = await createIx(solverSdk) + }) + + itThrowsAnError('owner does not match') }) }) context('when proposal is not valid', () => { context('when proposal intent is not for chain Solana', () => { - itThrowsAnError('Incorrect chain id') + beforeEach('Create data for Optimism', async () => { + transfers = testTransfers() + testIntentData = { ...createTestIntentData(transfers), chainId: Chains.Optimism } + intentHash = randomHex(32) + intent = createTestIntent(svmEncodeTransferIntent(testIntentData)) + proposal = createTestProposal(intent) + remainingAccounts = getRemainingAccounts(transfers) + + await prepareIntentAndProposal() + + ix = await createIx(solverSdk) + }) + + itThrowsAnError('Incorrect intent chain id') }) context('when proposal has data/instructions', () => { + const editProposal = async () => { + const proposalKey = sdk.getProposalKey(intentHash, proposal.solver) + const proposalAccount = await settler.account.proposal.fetch(proposalKey) + const proposalInfo = client.getAccount(proposalKey)! + + const modifiedProposal = { + ...proposalAccount, + instructions: [ + { + programId: randomPubkey(), + accounts: [], + data: Buffer.from('deadbeef', 'hex'), + }, + ], + } + + const serializedProposal = await settler.coder.accounts.encode('proposal', modifiedProposal) + + client.setAccount(proposalKey, { + ...proposalInfo, + data: serializedProposal, + }) + } + + beforeEach('Create Proposal and manually edit bytes to add data on-chain', async () => { + transfers = testTransfers() + testIntentData = createTestIntentData(transfers) + intentHash = randomHex(32) + intent = createTestIntent(svmEncodeTransferIntent(testIntentData)) + proposal = createTestProposal(intent) + remainingAccounts = getRemainingAccounts(transfers) + + await prepareIntentAndProposal() + editProposal() + + ix = await createIx(solverSdk) + }) + itThrowsAnError('Incorrect proposal data') }) }) From e7c406d62bd3bdfcf942cdb370b20b612f35692a Mon Sep 17 00:00:00 2001 From: GuidoDipietro Date: Tue, 7 Apr 2026 18:39:29 -0300 Subject: [PATCH 12/19] Implement more unhappy paths --- packages/svm/tests/helpers/spl.ts | 2 +- packages/svm/tests/settler.test.ts | 127 +++++++++++++++++++++++++++-- 2 files changed, 119 insertions(+), 10 deletions(-) diff --git a/packages/svm/tests/helpers/spl.ts b/packages/svm/tests/helpers/spl.ts index 264f7f6..90448a0 100644 --- a/packages/svm/tests/helpers/spl.ts +++ b/packages/svm/tests/helpers/spl.ts @@ -35,7 +35,7 @@ const DEFAULT_CREATE_MINT_PARAMS: CreateMintParams = { export function createMint( client: LiteSVM, mintAuthority: web3.Keypair, - params: Partial + params: Partial = {} ): CreateMintResult { const mint = randomKeypair() const { decimals, freezeAuthority, programId } = { ...DEFAULT_CREATE_MINT_PARAMS, ...params } diff --git a/packages/svm/tests/settler.test.ts b/packages/svm/tests/settler.test.ts index 3947e09..cf82453 100644 --- a/packages/svm/tests/settler.test.ts +++ b/packages/svm/tests/settler.test.ts @@ -2352,7 +2352,6 @@ describe('Settler', () => { const itThrowsAnError = async (error: string) => { it('throws an error', async () => { const res = await makeTxSignAndSend(solverProvider, ix) - console.log(res.toString()) expectTransactionError(res.toString(), error) }) } @@ -2667,44 +2666,154 @@ describe('Settler', () => { }) context('when remaining accounts are not correct', () => { + beforeEach('Set up base data', async () => { + transfers = testTransfers() + testIntentData = createTestIntentData(transfers) + intentHash = randomHex(32) + intent = createTestIntent(svmEncodeTransferIntent(testIntentData)) + proposal = createTestProposal(intent) + remainingAccounts = getRemainingAccounts(transfers) + }) + context('when remaining accounts number is correct', () => { context('when token programs are passed correctly', () => { context('when token is incorrect', () => { - itThrowsAnError('Incorrect token mint') + beforeEach(async () => { + remainingAccounts[2].pubkey = createMint(client, admin).mint + await prepareIntentAndProposal() + ix = await createIx(solverSdk) + }) + + itThrowsAnError('Incorrect transfer token mint account') }) context('when recipient is incorrect', () => { - itThrowsAnError('Incorrect recipient') + beforeEach(async () => { + remainingAccounts[3].pubkey = randomPubkey() + await prepareIntentAndProposal() + ix = await createIx(solverSdk) + }) + + itThrowsAnError('Incorrect transfer recipient account') }) context('when recipient token account is incorrect', () => { context('when authority is incorrect', () => { - itThrowsAnError('Incorrect recipient token account authority') + beforeEach(async () => { + remainingAccounts[4].pubkey = userAta + await prepareIntentAndProposal() + ix = await createIx(solverSdk) + }) + + itThrowsAnError('Incorrect recipient token account: mint or authority do not match expected') }) context('when token mint is incorrect', () => { - itThrowsAnError('Incorrect recipient token account mint') + beforeEach(async () => { + remainingAccounts[4].pubkey = ( + await createFundedAta(adminProvider, admin, recipient, createMint(client, admin).mint, 0) + ).ata + + await prepareIntentAndProposal() + ix = await createIx(solverSdk) + }) + + itThrowsAnError('Incorrect recipient token account: mint or authority do not match expected') }) }) context('when user token account is incorrect', () => { context('when authority is incorrect', () => { - itThrowsAnError('Incorrect user token account authority') + beforeEach(async () => { + remainingAccounts[5].pubkey = recipientAta + await prepareIntentAndProposal() + ix = await createIx(solverSdk) + }) + + itThrowsAnError('Incorrect user token account: mint or authority do not match expected') }) context('when token mint is incorrect', () => { - itThrowsAnError('Incorrect user token account mint') + beforeEach(async () => { + remainingAccounts[5].pubkey = ( + await createFundedAta(adminProvider, admin, user.publicKey, createMint(client, admin).mint, 0) + ).ata + + await prepareIntentAndProposal() + ix = await createIx(solverSdk) + }) + + itThrowsAnError('Incorrect user token account: mint or authority do not match expected') }) }) }) context('when token programs are not passed correctly', () => { - itThrowsAnError('Incorrect token program account') + context('when first program is wrong', () => { + beforeEach(async () => { + remainingAccounts[0].pubkey = randomPubkey() + await prepareIntentAndProposal() + ix = await createIx(solverSdk) + }) + + itThrowsAnError('Incorrect token program account') + }) + + context('when second program is wrong', () => { + beforeEach(async () => { + remainingAccounts[1].pubkey = randomPubkey() + await prepareIntentAndProposal() + ix = await createIx(solverSdk) + }) + + itThrowsAnError('Incorrect token program account') + }) + + context('when both programs are wrong', () => { + beforeEach(async () => { + remainingAccounts[0].pubkey = randomPubkey() + remainingAccounts[1].pubkey = randomPubkey() + await prepareIntentAndProposal() + ix = await createIx(solverSdk) + }) + + itThrowsAnError('Incorrect token program account') + }) }) }) context('when remaining accounts number is not correct', () => { - itThrowsAnError('ProgramError') + context('when there are less remaining accounts than expected', () => { + beforeEach(async () => { + remainingAccounts.pop() + await prepareIntentAndProposal() + ix = await createIx(solverSdk) + }) + + itThrowsAnError('ProgramError') + }) + + context('when there are more remaining accounts than expected', () => { + beforeEach(async () => { + // Re-approve Delegate for test + await approveDelegate( + userProvider, + userAta, + solverSdk.getDelegateKey(user.publicKey), + user, + Number(transfers[0].amount) + ) + + remainingAccounts.push({ pubkey: randomPubkey(), isWritable: true, isSigner: false }) + await prepareIntentAndProposal() + ix = await createIx(solverSdk) + }) + + it('works normally', async () => { + const res = await makeTxSignAndSend(solverProvider, ix) + expect(res.toString()).not.to.include('FailedTransactionMetadata') + }) + }) }) }) } From d1230dee54d8cf910cedc69fd18a3903dd96b0e6 Mon Sep 17 00:00:00 2001 From: GuidoDipietro Date: Tue, 7 Apr 2026 19:16:34 -0300 Subject: [PATCH 13/19] Finish execute_proposal tests (first iteration) --- packages/svm/programs/settler/src/errors.rs | 4 +- packages/svm/tests/settler.test.ts | 190 ++++++++++++++------ 2 files changed, 140 insertions(+), 54 deletions(-) diff --git a/packages/svm/programs/settler/src/errors.rs b/packages/svm/programs/settler/src/errors.rs index b8e3ca6..d225a19 100644 --- a/packages/svm/programs/settler/src/errors.rs +++ b/packages/svm/programs/settler/src/errors.rs @@ -23,10 +23,10 @@ pub enum SettlerError { #[msg("Validator is not allowlisted")] ValidatorNotAllowlisted, - #[msg("Signer must be intent creator")] + #[msg("Incorrect intent creator")] IncorrectIntentCreator, - #[msg("Signer must be proposal creator")] + #[msg("Incorrect proposal creator")] IncorrectProposalCreator, #[msg("Intent is already final")] diff --git a/packages/svm/tests/settler.test.ts b/packages/svm/tests/settler.test.ts index cf82453..8971945 100644 --- a/packages/svm/tests/settler.test.ts +++ b/packages/svm/tests/settler.test.ts @@ -74,6 +74,7 @@ import { LONG_DEADLINE, MEDIUM_DEADLINE, PROPOSAL_DEADLINE_OFFSET, + ProposalAccount, randomKeypair, randomPubkey, removeEntityFromAllowlist, @@ -179,7 +180,7 @@ describe('Settler', () => { }) }) - describe.skip('update_eip712_domain', () => { + describe('update_eip712_domain', () => { context('when caller is controller admin', () => { context('when domain is valid', () => { const itUpdatesDomainCorrectly = (testCase: string, domain: SolanaEip712Domain) => { @@ -240,7 +241,7 @@ describe('Settler', () => { }) }) - describe.skip('create_intent', () => { + describe('create_intent', () => { let intentHash: string let intentOptions: CreateIntentOptions = {} @@ -518,7 +519,7 @@ describe('Settler', () => { }) }) - describe.skip('extend_intent', () => { + describe('extend_intent', () => { let intentHash: string let intentKey: PublicKey let extendParams: ExtendIntentParams = {} @@ -816,12 +817,12 @@ describe('Settler', () => { const ix = await maliciousSdk.extendIntentIx(intentHash, extendParams, false) const res = await makeTxSignAndSend(maliciousProvider, ix) - expectTransactionError(res, `Signer must be intent creator`) + expectTransactionError(res, `Incorrect intent creator`) }) }) }) - describe.skip('claim_stale_intent', () => { + describe('claim_stale_intent', () => { let intentHash: string context('when caller is intent creator', () => { @@ -962,12 +963,12 @@ describe('Settler', () => { it('throws an error', async () => { const ix = await maliciousSdk.claimStaleIntentIx(intentHash) const res = await makeTxSignAndSend(maliciousProvider, ix) - expectTransactionError(res, `Signer must be intent creator`) + expectTransactionError(res, `Incorrect intent creator`) }) }) }) - describe.skip('create_proposal', () => { + describe('create_proposal', () => { let params: CreateProposalParams & { intentHash: string } const createProposalFromParams = async () => { @@ -1310,7 +1311,7 @@ describe('Settler', () => { }) }) - describe.skip('add_instructions_to_proposal', () => { + describe('add_instructions_to_proposal', () => { const createTestProposal = async (options?: CreateProposalOptions): Promise => { const params = await createProposalParams(solverSdk, solverProvider, client, options) const ix = await solverSdk.createProposalIx(params.intentHash, params) @@ -1530,12 +1531,12 @@ describe('Settler', () => { .instruction() const res = await makeTxSignAndSend(maliciousProvider, ix) - expectTransactionError(res, `Signer must be proposal creator`) + expectTransactionError(res, `Incorrect proposal creator`) }) }) }) - describe.skip('claim_stale_proposal', () => { + describe('claim_stale_proposal', () => { const createTestProposal = async (options?: CreateProposalOptions): Promise => { const params = await createProposalParams(solverSdk, solverProvider, client, options) const ix = await solverSdk.createProposalIx(params.intentHash, params) @@ -1651,12 +1652,12 @@ describe('Settler', () => { .instruction() const res = await makeTxSignAndSend(maliciousProvider, ix) - expectTransactionError(res, `Signer must be proposal creator`) + expectTransactionError(res, `Incorrect proposal creator`) }) }) }) - describe.skip('add_validator_sigs', () => { + describe('add_validator_sigs', () => { const createAllowlistedValidator = async () => { const validator = ethers.Wallet.createRandom() await createAllowlistedEntity(controllerSdk, adminProvider, EntityType.Validator, hexToBytes(validator.address)) @@ -1990,7 +1991,7 @@ describe('Settler', () => { }) }) - describe.skip('add_axia_sig', () => { + describe('add_axia_sig', () => { const createAllowlistedAxia = async () => { const axia = ethers.Wallet.createRandom() await createAllowlistedEntity(controllerSdk, adminProvider, EntityType.Axia, hexToBytes(axia.address)) @@ -2328,6 +2329,7 @@ describe('Settler', () => { proposal: web3.PublicKey intentCreator: web3.PublicKey intent: web3.PublicKey + fulfilledIntent: web3.PublicKey delegate: web3.PublicKey }> = {} ) => { @@ -2359,7 +2361,7 @@ describe('Settler', () => { const createTestIntent = (data: string): Intent => ({ configSig: randomHex(32), data, - deadline: (Date.now() + 1000).toString(), + deadline: (Number(client.getClock().unixTimestamp) + 1000).toString(), events: [{ topic: randomHex(32), data: randomHex(50) }], maxFees: [{ token: usdc.toString(), amount: '100' }], minValidations: 1, @@ -2440,6 +2442,17 @@ describe('Settler', () => { }) context('when intent is transfer', () => { + let transfers: TransferIntentData['transfers'] + let testIntentData: TransferIntentData + + const testTransfers = () => [ + { + amount: '1000000000', + token: usdc.toString(), + recipient: recipient.toString(), + }, + ] + const createTestIntentData = (transfers?: TransferIntentData['transfers']): TransferIntentData => ({ chainId: Chains.Solana, transfers: transfers ?? [], @@ -2459,18 +2472,24 @@ describe('Settler', () => { return [tokenProgram, token2022Program, ...transferAccounts] } - const itWorksAsExpected = () => { - let transfers: TransferIntentData['transfers'] - let testIntentData: TransferIntentData + const editProposal = async (proposalKey: web3.PublicKey, editedProposal: Partial) => { + const proposalAccount = await settler.account.proposal.fetch(proposalKey) + const proposalInfo = client.getAccount(proposalKey)! + + const modifiedProposal = { + ...proposalAccount, + ...editedProposal, + } - const testTransfers = () => [ - { - amount: '1000000000', - token: usdc.toString(), - recipient: recipient.toString(), - }, - ] + const serializedProposal = await settler.coder.accounts.encode('proposal', modifiedProposal) + client.setAccount(proposalKey, { + ...proposalInfo, + data: serializedProposal, + }) + } + + const itWorksAsExpected = () => { context('when remaining accounts are correct', () => { context('when transfer/s is/are valid', () => { context('when protocol has approval', () => { @@ -2622,30 +2641,6 @@ describe('Settler', () => { }) context('when proposal has data/instructions', () => { - const editProposal = async () => { - const proposalKey = sdk.getProposalKey(intentHash, proposal.solver) - const proposalAccount = await settler.account.proposal.fetch(proposalKey) - const proposalInfo = client.getAccount(proposalKey)! - - const modifiedProposal = { - ...proposalAccount, - instructions: [ - { - programId: randomPubkey(), - accounts: [], - data: Buffer.from('deadbeef', 'hex'), - }, - ], - } - - const serializedProposal = await settler.coder.accounts.encode('proposal', modifiedProposal) - - client.setAccount(proposalKey, { - ...proposalInfo, - data: serializedProposal, - }) - } - beforeEach('Create Proposal and manually edit bytes to add data on-chain', async () => { transfers = testTransfers() testIntentData = createTestIntentData(transfers) @@ -2655,7 +2650,15 @@ describe('Settler', () => { remainingAccounts = getRemainingAccounts(transfers) await prepareIntentAndProposal() - editProposal() + editProposal(solverSdk.getProposalKey(intentHash, proposal.solver), { + instructions: [ + { + programId: randomPubkey(), + accounts: [], + data: Buffer.from('deadbeef', 'hex'), + }, + ], + }) ix = await createIx(solverSdk) }) @@ -2833,20 +2836,62 @@ describe('Settler', () => { }) context('when proposal is not correct', () => { + beforeEach('Setup base data', async () => { + transfers = testTransfers() + testIntentData = createTestIntentData(transfers) + intentHash = randomHex(32) + intent = createTestIntent(svmEncodeTransferIntent(testIntentData)) + proposal = createTestProposal(intent) + remainingAccounts = getRemainingAccounts(transfers) + }) + context('when proposal is for another intent', () => { + beforeEach(async () => { + const otherIntentHash = randomHex(32) + const otherIntent = createTestIntent(svmEncodeTransferIntent(testIntentData)) + const otherIntentKey = solverSdk.getIntentKey(otherIntentHash) + const otherFulfilledIntentKey = solverSdk.getFulfilledIntentKey(otherIntentHash) + await makeTxSignAndSend( + solverProvider, + await solverSdk.createIntentIx(otherIntentHash, otherIntent, true) + ) + + await prepareIntentAndProposal() + ix = await createIx(solverSdk, { intent: otherIntentKey, fulfilledIntent: otherFulfilledIntentKey }) + }) + itThrowsAnError('Incorrect intent for proposal') }) context('when proposal is from another proposal creator', () => { + beforeEach(async () => { + await prepareIntentAndProposal() + ix = await createIx(solverSdk, { proposalCreator: randomPubkey() }) + }) + itThrowsAnError('Incorrect proposal creator') }) context('when proposal is not signed', () => { + beforeEach(async () => { + await prepareIntentAndProposal() + editProposal(solverSdk.getProposalKey(intentHash, proposal.solver), { isSigned: false }) + ix = await createIx(solverSdk) + }) + itThrowsAnError('Proposal is not signed') }) context('when proposal is expired', () => { - itThrowsAnError('Proposal is expired') + beforeEach(async () => { + await prepareIntentAndProposal() + ix = await createIx(solverSdk) + + const delta = Number(proposal.deadline) - Number(client.getClock().unixTimestamp) + warpSeconds(solverProvider, delta * 2) + }) + + itThrowsAnError('Proposal has already expired') }) }) }) @@ -2854,6 +2899,18 @@ describe('Settler', () => { context('when intent is not correct', () => { context('when intent_creator is not correct', () => { + beforeEach(async () => { + transfers = testTransfers() + testIntentData = createTestIntentData(transfers) + intentHash = randomHex(32) + intent = createTestIntent(svmEncodeTransferIntent(testIntentData)) + proposal = createTestProposal(intent) + remainingAccounts = getRemainingAccounts(transfers) + + await prepareIntentAndProposal() + ix = await createIx(solverSdk, { intentCreator: randomPubkey() }) + }) + itThrowsAnError('Incorrect intent creator') }) }) @@ -2861,12 +2918,41 @@ describe('Settler', () => { }) context('when caller is not allowlisted solver', () => { - it('throws an error', async () => {}) + beforeEach(async () => { + transfers = testTransfers() + testIntentData = createTestIntentData(transfers) + intentHash = randomHex(32) + intent = createTestIntent(svmEncodeTransferIntent(testIntentData)) + proposal = createTestProposal(intent) + remainingAccounts = getRemainingAccounts(transfers) + + await prepareIntentAndProposal() + ix = await createIx(maliciousSdk) + }) + + it('throws an error', async () => { + const res = await makeTxSignAndSend(maliciousProvider, ix) + expect(res.toString()).to.include( + 'AnchorError caused by account: solver_registry. Error Code: AccountNotInitialized' + ) + }) }) }) context('when intent is not transfer', () => { - it('throws an error', async () => {}) + beforeEach(async () => { + intentHash = randomHex(32) + intent = { ...createTestIntent('0xdeadbeef'), op: 2 } + proposal = createTestProposal(intent) + + await prepareIntentAndProposal() + ix = await createIx(solverSdk) + }) + + it('throws an error', async () => { + const res = await makeTxSignAndSend(solverProvider, ix) + expect(res.toString()).to.include('Unsupported intent op') + }) }) }) }) From 4468a9d354a83a998f1d4434806444ac1ff94be4 Mon Sep 17 00:00:00 2001 From: GuidoDipietro Date: Wed, 8 Apr 2026 12:23:01 -0300 Subject: [PATCH 14/19] Fix clippy --- .../src/instructions/resize_settings.rs | 2 +- .../controller/src/state/entity_registry.rs | 2 +- .../settler/src/instructions/create_intent.rs | 2 +- .../src/instructions/execute_proposal.rs | 4 +- .../programs/settler/src/state/proposal.rs | 6 +- .../settler/src/utils/execution/transfer.rs | 98 ++++++++++++------- .../settler/src/utils/signatures/secp256k1.rs | 14 ++- 7 files changed, 78 insertions(+), 50 deletions(-) diff --git a/packages/svm/programs/controller/src/instructions/resize_settings.rs b/packages/svm/programs/controller/src/instructions/resize_settings.rs index 28eb9f6..957755b 100644 --- a/packages/svm/programs/controller/src/instructions/resize_settings.rs +++ b/packages/svm/programs/controller/src/instructions/resize_settings.rs @@ -21,7 +21,7 @@ pub struct ResizeSettings<'info> { } fn check_settings_data(data: &[u8], expected_admin: Pubkey) -> Result<()> { - if !data.starts_with(&ControllerSettings::DISCRIMINATOR) { + if !data.starts_with(ControllerSettings::DISCRIMINATOR) { return Err(ProgramError::InvalidAccountData.into()); } diff --git a/packages/svm/programs/controller/src/state/entity_registry.rs b/packages/svm/programs/controller/src/state/entity_registry.rs index 100b2c6..cf9d6d9 100644 --- a/packages/svm/programs/controller/src/state/entity_registry.rs +++ b/packages/svm/programs/controller/src/state/entity_registry.rs @@ -10,7 +10,7 @@ pub struct EntityRegistry { } impl EntityRegistry { - pub fn size(entity_address: &Vec) -> usize { + pub fn size(entity_address: &[u8]) -> usize { EntityType::INIT_SPACE + VEC_LEN_SIZE + entity_address.len() + 1 } } diff --git a/packages/svm/programs/settler/src/instructions/create_intent.rs b/packages/svm/programs/settler/src/instructions/create_intent.rs index 57a3020..ba004c4 100644 --- a/packages/svm/programs/settler/src/instructions/create_intent.rs +++ b/packages/svm/programs/settler/src/instructions/create_intent.rs @@ -68,7 +68,7 @@ pub fn create_intent( ) -> Result<()> { let now = Clock::get()?.unix_timestamp as u64; require!(deadline > now, SettlerError::DeadlineIsInThePast); - require!(max_fees.len() > 0, SettlerError::NoMaxFees); + require!(!max_fees.is_empty(), SettlerError::NoMaxFees); // TODO: check hash diff --git a/packages/svm/programs/settler/src/instructions/execute_proposal.rs b/packages/svm/programs/settler/src/instructions/execute_proposal.rs index 37aa68f..4cab5e4 100644 --- a/packages/svm/programs/settler/src/instructions/execute_proposal.rs +++ b/packages/svm/programs/settler/src/instructions/execute_proposal.rs @@ -67,8 +67,8 @@ pub fn execute_proposal<'info>( let proposal = &ctx.accounts.proposal; handle_intent_execution( - &intent, - &proposal, + intent, + proposal, &ctx.accounts.delegate.clone(), ctx.remaining_accounts, ctx.bumps.delegate, diff --git a/packages/svm/programs/settler/src/state/proposal.rs b/packages/svm/programs/settler/src/state/proposal.rs index 83ce873..bcff6ad 100644 --- a/packages/svm/programs/settler/src/state/proposal.rs +++ b/packages/svm/programs/settler/src/state/proposal.rs @@ -25,14 +25,14 @@ impl Proposal { 1 // bump ; - pub fn total_size(instructions: &Vec, fees_len: usize) -> Result { + pub fn total_size(instructions: &[ProposalInstruction], fees_len: usize) -> Result { let size = add(8, Proposal::BASE_LEN)?; let size = add(size, Proposal::instructions_size(instructions)?)?; let size = add(size, Proposal::fees_size(fees_len)?)?; Ok(size) } - pub fn instructions_size(instructions: &Vec) -> Result { + pub fn instructions_size(instructions: &[ProposalInstruction]) -> Result { let sum = instructions .iter() .try_fold(0usize, |acc, ix| add(acc, ix.size()))?; @@ -45,7 +45,7 @@ impl Proposal { pub fn extended_size( size: usize, - more_instructions: &Vec, + more_instructions: &[ProposalInstruction], ) -> Result { sub( add(size, Proposal::instructions_size(more_instructions)?)?, diff --git a/packages/svm/programs/settler/src/utils/execution/transfer.rs b/packages/svm/programs/settler/src/utils/execution/transfer.rs index b084f07..0c87a1c 100644 --- a/packages/svm/programs/settler/src/utils/execution/transfer.rs +++ b/packages/svm/programs/settler/src/utils/execution/transfer.rs @@ -116,80 +116,110 @@ fn execute_transfer<'info>( token_program: &AccountInfo<'info>, token_2022_program: &AccountInfo<'info>, ) -> Result<()> { + // Read remaining accounts let token_account_info = next_account_info(remaining_accounts_iter)?; let recipient_account_info = next_account_info(remaining_accounts_iter)?; let recipient_ta_account_info = next_account_info(remaining_accounts_iter)?; let user_ta_account_info = next_account_info(remaining_accounts_iter)?; + // Check account ownership check_owner_is_token_program(recipient_ta_account_info)?; check_owner_is_token_program(user_ta_account_info)?; + // Check account layout let token_mint = { - let token_data: &[u8] = &token_account_info.try_borrow_data()?; - let mut token_data_ref: &[u8] = &token_data; - IMint::try_deserialize(&mut token_data_ref)? + let mut token_data: &[u8] = &token_account_info.try_borrow_data()?; + IMint::try_deserialize(&mut token_data)? }; let recipient_ta = { - let recipient_ta_data = recipient_ta_account_info.try_borrow_data()?; - let mut recipient_ta_data_ref: &[u8] = &recipient_ta_data; - ITokenAccount::try_deserialize(&mut recipient_ta_data_ref)? + let mut recipient_ta_data: &[u8] = &recipient_ta_account_info.try_borrow_data()?; + ITokenAccount::try_deserialize(&mut recipient_ta_data)? }; let user_ta = { - let user_ta_data = user_ta_account_info.try_borrow_data()?; - let mut user_ta_data_ref: &[u8] = &user_ta_data; - ITokenAccount::try_deserialize(&mut user_ta_data_ref)? + let mut user_ta_data: &[u8] = &user_ta_account_info.try_borrow_data()?; + ITokenAccount::try_deserialize(&mut user_ta_data)? }; - let transfer_recipient = Pubkey::try_from(transfer.recipient.as_slice()) + // Check logical constraints + check_token_accounts( + recipient_account_info.key(), + token_account_info.key(), + user, + &recipient_ta, + &user_ta, + &transfer.recipient, + &transfer.token, + )?; + + // Construct transfer_checked CPI + let cpi_accounts = TransferChecked { + authority: delegate.clone(), + from: user_ta_account_info.clone(), + mint: token_account_info.clone(), + to: recipient_ta_account_info.clone(), + }; + + let cpi_program = match *token_account_info.owner { + anchor_spl::token::ID => token_program.clone(), + anchor_spl::token_2022::ID => token_2022_program.clone(), + _ => err!(SettlerError::AccountNotOwnedByTokenProgram)?, + }; + + let cpi_ctx = CpiContext::new_with_signer(cpi_program, cpi_accounts, delegate_seeds); + token_interface::transfer_checked(cpi_ctx, transfer.amount, token_mint.decimals) +} + +/// Checks token accounts have correct owner and mint, and that they equal expected values per transfer struct +fn check_token_accounts( + recipient: Pubkey, + token: Pubkey, + user: Pubkey, + recipient_ta: &ITokenAccount, + user_ta: &ITokenAccount, + expected_recipient: &[u8], + expected_token: &[u8], +) -> Result<()> { + let expected_recipient_pubkey = Pubkey::try_from(expected_recipient) .map_err(|_| error!(SettlerError::InvalidTransferRecipient))?; - let transfer_token = Pubkey::try_from(transfer.token.as_slice()) - .map_err(|_| error!(SettlerError::InvalidTransferToken))?; + let expected_token_pubkey = + Pubkey::try_from(expected_token).map_err(|_| error!(SettlerError::InvalidTransferToken))?; require_keys_eq!( - transfer_recipient, - recipient_account_info.key(), + recipient, + expected_recipient_pubkey, SettlerError::IncorrectTransferRecipient ); + require_keys_eq!( - transfer_token, - token_account_info.key(), + token, + expected_token_pubkey, SettlerError::IncorrectTransferToken ); + require_keys_eq!( recipient_ta.owner, - recipient_account_info.key(), + expected_recipient_pubkey, SettlerError::IncorrectRecipientTokenAccount ); + require_keys_eq!( recipient_ta.mint, - token_account_info.key(), + expected_token_pubkey, SettlerError::IncorrectRecipientTokenAccount ); + require_keys_eq!(user_ta.owner, user, SettlerError::IncorrectUserTokenAccount); + require_keys_eq!( user_ta.mint, - token_account_info.key(), + expected_token_pubkey, SettlerError::IncorrectUserTokenAccount ); - let cpi_accounts = TransferChecked { - authority: delegate.clone(), - from: user_ta_account_info.clone(), - mint: token_account_info.clone(), - to: recipient_ta_account_info.clone(), - }; - - let cpi_program = match *token_account_info.owner { - anchor_spl::token::ID => token_program.clone(), - anchor_spl::token_2022::ID => token_2022_program.clone(), - _ => err!(SettlerError::AccountNotOwnedByTokenProgram)?, - }; - - let cpi_ctx = CpiContext::new_with_signer(cpi_program, cpi_accounts, delegate_seeds); - token_interface::transfer_checked(cpi_ctx, transfer.amount, token_mint.decimals) + Ok(()) } fn validate_transfer(proposal: &Proposal, intent_data: &SvmTransferIntentData) -> Result<()> { diff --git a/packages/svm/programs/settler/src/utils/signatures/secp256k1.rs b/packages/svm/programs/settler/src/utils/signatures/secp256k1.rs index 3b1d778..22d7106 100644 --- a/packages/svm/programs/settler/src/utils/signatures/secp256k1.rs +++ b/packages/svm/programs/settler/src/utils/signatures/secp256k1.rs @@ -5,7 +5,7 @@ use crate::errors::SettlerError; const SECP256K1_ID: Pubkey = pubkey!("KeccakSecp256k11111111111111111111111111111"); pub fn check_secp256k1_ix(ix: &Instruction) -> Result<()> { - if ix.program_id != SECP256K1_ID || ix.accounts.len() != 0 { + if ix.program_id != SECP256K1_ID || !ix.accounts.is_empty() { return err!(SettlerError::SigVerificationFailedInvalidPreinstruction); } @@ -58,12 +58,12 @@ pub fn get_args_from_secp256k1_ix_data(data: &[u8]) -> Result> // Header check if num_signatures != &exp_num_signatures.to_le_bytes() - || signature_offset != &exp_signature_offset.to_le_bytes() + || signature_offset != exp_signature_offset.to_le_bytes() || signature_instruction_index != &[0] - || eth_address_offset != &exp_eth_address_offset.to_le_bytes() + || eth_address_offset != exp_eth_address_offset.to_le_bytes() || eth_address_instruction_index != &[0] - || message_data_offset != &exp_message_data_offset.to_le_bytes() - || message_data_size != &msg_len.to_le_bytes() + || message_data_offset != exp_message_data_offset.to_le_bytes() + || message_data_size != msg_len.to_le_bytes() || message_instruction_index != &[0] { return err!(SettlerError::SigVerificationFailedInvalidPreinstruction); @@ -73,9 +73,7 @@ pub fn get_args_from_secp256k1_ix_data(data: &[u8]) -> Result> eth_address: eth_address .try_into() .map_err(|_| SettlerError::SigVerificationFailedInvalidPreinstruction)?, - msg: msg - .try_into() - .map_err(|_| SettlerError::SigVerificationFailedInvalidPreinstruction)?, + msg, sig: sig .try_into() .map_err(|_| SettlerError::SigVerificationFailedInvalidPreinstruction)?, From 8fa027290b452d0dbbd1d0d80acf3b14e5d66dd5 Mon Sep 17 00:00:00 2001 From: GuidoDipietro Date: Wed, 8 Apr 2026 12:27:06 -0300 Subject: [PATCH 15/19] Cargo fmt --- packages/svm/programs/settler/src/state/proposal.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/svm/programs/settler/src/state/proposal.rs b/packages/svm/programs/settler/src/state/proposal.rs index bcff6ad..781dc18 100644 --- a/packages/svm/programs/settler/src/state/proposal.rs +++ b/packages/svm/programs/settler/src/state/proposal.rs @@ -43,10 +43,7 @@ impl Proposal { add(4, mul(8, len)?) } - pub fn extended_size( - size: usize, - more_instructions: &[ProposalInstruction], - ) -> Result { + pub fn extended_size(size: usize, more_instructions: &[ProposalInstruction]) -> Result { sub( add(size, Proposal::instructions_size(more_instructions)?)?, 4, From 5b0be6d9a4926847d226cf6a36e8474f047c0b85 Mon Sep 17 00:00:00 2001 From: GuidoDipietro Date: Wed, 8 Apr 2026 13:01:43 -0300 Subject: [PATCH 16/19] Add pay_solver_fees logic --- .../src/instructions/execute_proposal.rs | 29 +++++- .../settler/src/utils/execution/misc.rs | 98 ++++++++++++++++++- .../settler/src/utils/execution/transfer.rs | 31 +++--- 3 files changed, 131 insertions(+), 27 deletions(-) diff --git a/packages/svm/programs/settler/src/instructions/execute_proposal.rs b/packages/svm/programs/settler/src/instructions/execute_proposal.rs index 4cab5e4..5987740 100644 --- a/packages/svm/programs/settler/src/instructions/execute_proposal.rs +++ b/packages/svm/programs/settler/src/instructions/execute_proposal.rs @@ -66,11 +66,28 @@ pub fn execute_proposal<'info>( let intent = &ctx.accounts.intent; let proposal = &ctx.accounts.proposal; + let mut remaining_accounts_iter = ctx.remaining_accounts.iter(); + let token_program = next_account_info(&mut remaining_accounts_iter)?; + let token_2022_program = next_account_info(&mut remaining_accounts_iter)?; + + require_keys_eq!( + token_program.key(), + anchor_spl::token::ID, + SettlerError::IncorrectTokenProgram + ); + require_keys_eq!( + token_2022_program.key(), + anchor_spl::token_2022::ID, + SettlerError::IncorrectTokenProgram + ); + handle_intent_execution( intent, proposal, &ctx.accounts.delegate.clone(), - ctx.remaining_accounts, + &mut remaining_accounts_iter, + token_program, + token_2022_program, ctx.bumps.delegate, )?; @@ -80,7 +97,15 @@ pub fn execute_proposal<'info>( }) }); - pay_solver_fees()?; + pay_solver_fees( + &mut remaining_accounts_iter, + intent, + proposal, + token_program, + token_2022_program, + &ctx.accounts.delegate.clone(), + ctx.bumps.delegate, + )?; Ok(()) } diff --git a/packages/svm/programs/settler/src/utils/execution/misc.rs b/packages/svm/programs/settler/src/utils/execution/misc.rs index ef09ff9..4edf4ad 100644 --- a/packages/svm/programs/settler/src/utils/execution/misc.rs +++ b/packages/svm/programs/settler/src/utils/execution/misc.rs @@ -1,5 +1,11 @@ use anchor_lang::prelude::*; -use anchor_spl::{token, token_2022}; +use anchor_spl::{ + token, + token_2022::{self, TransferChecked}, + token_interface::{self, Mint as IMint, TokenAccount as ITokenAccount}, +}; + +use core::slice::Iter; use crate::{ errors::SettlerError, @@ -12,7 +18,9 @@ pub fn handle_intent_execution<'info>( intent: &Intent, proposal: &Proposal, delegate: &AccountInfo<'info>, - remaining_accounts: &[AccountInfo<'info>], + remaining_accounts_iter: &mut Iter<'_, AccountInfo<'info>>, + token_program: &AccountInfo<'info>, + token_2022_program: &AccountInfo<'info>, delegate_bump: u8, ) -> Result<()> { match intent.op { @@ -21,7 +29,9 @@ pub fn handle_intent_execution<'info>( intent, proposal, delegate, - remaining_accounts, + remaining_accounts_iter, + token_program, + token_2022_program, delegate_bump, ), OpType::EvmCall => err!(SettlerError::UnsupportedIntentOp), @@ -29,8 +39,86 @@ pub fn handle_intent_execution<'info>( } } -pub fn pay_solver_fees() -> Result<()> { - // TODO +/// Deserializes and checks the following remaining_accounts: +/// +/// For each fee_token: +/// +/// #[account(mut)] +/// pub fee_token: Account<'info, IMint>, +/// +/// #[account( +/// mut, +/// token::owner = user, +/// token::mint = fee_token, +/// )] +/// pub user_ta: Account<'info, ITokenAccount>, +/// +/// #[account( +/// mut, +/// token::owner = solver, +/// token::mint = fee_token +/// )] +/// pub solver_ta: Account<'info, ITokenAccount>, +/// +pub fn pay_solver_fees<'info>( + remaining_accounts_iter: &mut Iter<'_, AccountInfo<'info>>, + intent: &Intent, + proposal: &Proposal, + token_program: &AccountInfo<'info>, + token_2022_program: &AccountInfo<'info>, + delegate: &AccountInfo<'info>, + delegate_bump: u8, +) -> Result<()> { + let delegate_seeds: &[&[u8]] = &[b"delegate", intent.user.as_ref(), &[delegate_bump]]; + let signer_seeds = [delegate_seeds]; + + for (fee, max_fee) in proposal.fees.iter().zip(&intent.max_fees) { + let token_account_info = next_account_info(remaining_accounts_iter)?; + let user_ta_account_info = next_account_info(remaining_accounts_iter)?; + let solver_ta_account_info = next_account_info(remaining_accounts_iter)?; + + check_owner_is_token_program(token_account_info)?; + check_owner_is_token_program(user_ta_account_info)?; + check_owner_is_token_program(solver_ta_account_info)?; + + let token_mint = { + let mut token_mint_data: &[u8] = &token_account_info.try_borrow_data()?; + IMint::try_deserialize(&mut token_mint_data)? + }; + + let user_ta = { + let mut user_ta_data: &[u8] = &user_ta_account_info.try_borrow_data()?; + ITokenAccount::try_deserialize(&mut user_ta_data)? + }; + + let solver_ta = { + let mut solver_ta_data: &[u8] = &solver_ta_account_info.try_borrow_data()?; + ITokenAccount::try_deserialize(&mut solver_ta_data)? + }; + + require_keys_eq!(user_ta.owner, intent.user); + require_keys_eq!(user_ta.mint, max_fee.token); + require_keys_eq!(solver_ta.owner, proposal.creator); + require_keys_eq!(solver_ta.mint, max_fee.token); + + // Construct transfer_checked CPI + let cpi_accounts = TransferChecked { + authority: delegate.clone(), + from: user_ta_account_info.clone(), + mint: token_account_info.clone(), + to: solver_ta_account_info.clone(), + }; + + let cpi_program = match *token_account_info.owner { + anchor_spl::token::ID => token_program.clone(), + anchor_spl::token_2022::ID => token_2022_program.clone(), + _ => err!(SettlerError::AccountNotOwnedByTokenProgram)?, + }; + + let cpi_ctx = CpiContext::new_with_signer(cpi_program, cpi_accounts, &signer_seeds); + token_interface::transfer_checked(cpi_ctx, *fee, token_mint.decimals)?; + } + Ok(()) } diff --git a/packages/svm/programs/settler/src/utils/execution/transfer.rs b/packages/svm/programs/settler/src/utils/execution/transfer.rs index 0c87a1c..0d5cadc 100644 --- a/packages/svm/programs/settler/src/utils/execution/transfer.rs +++ b/packages/svm/programs/settler/src/utils/execution/transfer.rs @@ -17,7 +17,9 @@ pub fn handle_transfer<'info>( intent: &Intent, proposal: &Proposal, delegate: &AccountInfo<'info>, - remaining_accounts: &[AccountInfo<'info>], + remaining_accounts_iter: &mut Iter<'_, AccountInfo<'info>>, + token_program: &AccountInfo<'info>, + token_2022_program: &AccountInfo<'info>, delegate_bump: u8, ) -> Result<()> { let decoded_intent_data = SvmTransferIntentData::try_from_slice(&intent.data)?; @@ -28,8 +30,10 @@ pub fn handle_transfer<'info>( execute_transfers( intent.user, delegate, - remaining_accounts, + remaining_accounts_iter, &decoded_intent_data, + token_program, + token_2022_program, &[delegate_seeds], )?; @@ -45,31 +49,17 @@ pub fn handle_transfer<'info>( fn execute_transfers<'info>( user: Pubkey, delegate: &AccountInfo<'info>, - remaining_accounts: &[AccountInfo<'info>], + remaining_accounts_iter: &mut Iter<'_, AccountInfo<'info>>, intent_data: &SvmTransferIntentData, + token_program: &AccountInfo<'info>, + token_2022_program: &AccountInfo<'info>, delegate_seeds: &[&[&[u8]]], ) -> Result<()> { - let mut remaining_accounts_iter = remaining_accounts.iter(); - - let token_program = next_account_info(&mut remaining_accounts_iter)?; - let token_2022_program = next_account_info(&mut remaining_accounts_iter)?; - - require_keys_eq!( - token_program.key(), - anchor_spl::token::ID, - SettlerError::IncorrectTokenProgram - ); - require_keys_eq!( - token_2022_program.key(), - anchor_spl::token_2022::ID, - SettlerError::IncorrectTokenProgram - ); - for transfer in &intent_data.transfers { execute_transfer( transfer, delegate, - &mut remaining_accounts_iter, + remaining_accounts_iter, delegate_seeds, user, token_program, @@ -123,6 +113,7 @@ fn execute_transfer<'info>( let user_ta_account_info = next_account_info(remaining_accounts_iter)?; // Check account ownership + check_owner_is_token_program(token_account_info)?; check_owner_is_token_program(recipient_ta_account_info)?; check_owner_is_token_program(user_ta_account_info)?; From 41c785c450a0e71ac71a2ea314f9dc98435e6b6b Mon Sep 17 00:00:00 2001 From: GuidoDipietro Date: Wed, 8 Apr 2026 13:12:28 -0300 Subject: [PATCH 17/19] Scaffold tests for pay_solver_fees --- .../settler/src/utils/execution/misc.rs | 16 +- packages/svm/tests/settler.test.ts | 207 ++++++++++-------- 2 files changed, 128 insertions(+), 95 deletions(-) diff --git a/packages/svm/programs/settler/src/utils/execution/misc.rs b/packages/svm/programs/settler/src/utils/execution/misc.rs index 4edf4ad..ce2985e 100644 --- a/packages/svm/programs/settler/src/utils/execution/misc.rs +++ b/packages/svm/programs/settler/src/utils/execution/misc.rs @@ -48,17 +48,17 @@ pub fn handle_intent_execution<'info>( /// /// #[account( /// mut, -/// token::owner = user, -/// token::mint = fee_token, -/// )] -/// pub user_ta: Account<'info, ITokenAccount>, -/// -/// #[account( -/// mut, /// token::owner = solver, /// token::mint = fee_token /// )] /// pub solver_ta: Account<'info, ITokenAccount>, +/// +/// #[account( +/// mut, +/// token::owner = user, +/// token::mint = fee_token, +/// )] +/// pub user_ta: Account<'info, ITokenAccount>, /// pub fn pay_solver_fees<'info>( remaining_accounts_iter: &mut Iter<'_, AccountInfo<'info>>, @@ -74,8 +74,8 @@ pub fn pay_solver_fees<'info>( for (fee, max_fee) in proposal.fees.iter().zip(&intent.max_fees) { let token_account_info = next_account_info(remaining_accounts_iter)?; - let user_ta_account_info = next_account_info(remaining_accounts_iter)?; let solver_ta_account_info = next_account_info(remaining_accounts_iter)?; + let user_ta_account_info = next_account_info(remaining_accounts_iter)?; check_owner_is_token_program(token_account_info)?; check_owner_is_token_program(user_ta_account_info)?; diff --git a/packages/svm/tests/settler.test.ts b/packages/svm/tests/settler.test.ts index 8971945..b8c3129 100644 --- a/packages/svm/tests/settler.test.ts +++ b/packages/svm/tests/settler.test.ts @@ -180,7 +180,7 @@ describe('Settler', () => { }) }) - describe('update_eip712_domain', () => { + describe.skip('update_eip712_domain', () => { context('when caller is controller admin', () => { context('when domain is valid', () => { const itUpdatesDomainCorrectly = (testCase: string, domain: SolanaEip712Domain) => { @@ -241,7 +241,7 @@ describe('Settler', () => { }) }) - describe('create_intent', () => { + describe.skip('create_intent', () => { let intentHash: string let intentOptions: CreateIntentOptions = {} @@ -519,7 +519,7 @@ describe('Settler', () => { }) }) - describe('extend_intent', () => { + describe.skip('extend_intent', () => { let intentHash: string let intentKey: PublicKey let extendParams: ExtendIntentParams = {} @@ -822,7 +822,7 @@ describe('Settler', () => { }) }) - describe('claim_stale_intent', () => { + describe.skip('claim_stale_intent', () => { let intentHash: string context('when caller is intent creator', () => { @@ -968,7 +968,7 @@ describe('Settler', () => { }) }) - describe('create_proposal', () => { + describe.skip('create_proposal', () => { let params: CreateProposalParams & { intentHash: string } const createProposalFromParams = async () => { @@ -1311,7 +1311,7 @@ describe('Settler', () => { }) }) - describe('add_instructions_to_proposal', () => { + describe.skip('add_instructions_to_proposal', () => { const createTestProposal = async (options?: CreateProposalOptions): Promise => { const params = await createProposalParams(solverSdk, solverProvider, client, options) const ix = await solverSdk.createProposalIx(params.intentHash, params) @@ -1536,7 +1536,7 @@ describe('Settler', () => { }) }) - describe('claim_stale_proposal', () => { + describe.skip('claim_stale_proposal', () => { const createTestProposal = async (options?: CreateProposalOptions): Promise => { const params = await createProposalParams(solverSdk, solverProvider, client, options) const ix = await solverSdk.createProposalIx(params.intentHash, params) @@ -1657,7 +1657,7 @@ describe('Settler', () => { }) }) - describe('add_validator_sigs', () => { + describe.skip('add_validator_sigs', () => { const createAllowlistedValidator = async () => { const validator = ethers.Wallet.createRandom() await createAllowlistedEntity(controllerSdk, adminProvider, EntityType.Validator, hexToBytes(validator.address)) @@ -1991,7 +1991,7 @@ describe('Settler', () => { }) }) - describe('add_axia_sig', () => { + describe.skip('add_axia_sig', () => { const createAllowlistedAxia = async () => { const axia = ethers.Wallet.createRandom() await createAllowlistedEntity(controllerSdk, adminProvider, EntityType.Axia, hexToBytes(axia.address)) @@ -2467,6 +2467,7 @@ describe('Settler', () => { { pubkey: translateAddress(transfer.recipient), isSigner: false, isWritable: false }, { pubkey: recipientAta, isSigner: false, isWritable: true }, { pubkey: userAta, isSigner: false, isWritable: true }, + // TODO: add pay_solver_fees accounts ]) return [tokenProgram, token2022Program, ...transferAccounts] @@ -2566,27 +2567,56 @@ describe('Settler', () => { ) expect(proposalBalanceAfter).to.be.eq(0) expect(intentBalanceAfter).to.be.eq(0) + + // TODO: check solver fees are paid }) }) context('when user does not have sufficient funds', () => { - beforeEach('Create data and approve delegate', async () => { - transfers = [ - { - amount: '1000000000000', - token: usdc.toString(), - recipient: recipient.toString(), - }, - ] + context('when user does not have transfer token sufficient funds', () => { + beforeEach('Create data and approve delegate', async () => { + transfers = [ + { + amount: '1000000000000', + token: usdc.toString(), + recipient: recipient.toString(), + }, + ] + + await approveDelegate( + userProvider, + userAta, + solverSdk.getDelegateKey(user.publicKey), + user, + Number(transfers[0].amount) + ) - await approveDelegate( - userProvider, - userAta, - solverSdk.getDelegateKey(user.publicKey), - user, - Number(transfers[0].amount) - ) + testIntentData = createTestIntentData(transfers) + intentHash = randomHex(32) + intent = createTestIntent(svmEncodeTransferIntent(testIntentData)) + proposal = createTestProposal(intent) + remainingAccounts = getRemainingAccounts(transfers) + + await prepareIntentAndProposal() + ix = await createIx(solverSdk) + }) + + itThrowsAnError('insufficient funds') + }) + + context('when user does not have fee token/s sufficient funds', () => { + // TODO: implement + }) + }) + }) + + context('when protocol does not have approval', () => { + context('when protocol does not have transfer token approval', () => { + beforeEach('Create data and remove delegate', async () => { + await revokeDelegate(userProvider, userAta, user) + + transfers = testTransfers() testIntentData = createTestIntentData(transfers) intentHash = randomHex(32) intent = createTestIntent(svmEncodeTransferIntent(testIntentData)) @@ -2598,27 +2628,12 @@ describe('Settler', () => { ix = await createIx(solverSdk) }) - itThrowsAnError('insufficient funds') + itThrowsAnError('owner does not match') }) - }) - - context('when protocol does not have approval', () => { - beforeEach('Create data and remove delegate', async () => { - await revokeDelegate(userProvider, userAta, user) - - transfers = testTransfers() - testIntentData = createTestIntentData(transfers) - intentHash = randomHex(32) - intent = createTestIntent(svmEncodeTransferIntent(testIntentData)) - proposal = createTestProposal(intent) - remainingAccounts = getRemainingAccounts(transfers) - - await prepareIntentAndProposal() - ix = await createIx(solverSdk) + context('when protocol does not have fee token/s approval', () => { + // TODO: implement }) - - itThrowsAnError('owner does not match') }) }) @@ -2680,73 +2695,91 @@ describe('Settler', () => { context('when remaining accounts number is correct', () => { context('when token programs are passed correctly', () => { - context('when token is incorrect', () => { - beforeEach(async () => { - remainingAccounts[2].pubkey = createMint(client, admin).mint - await prepareIntentAndProposal() - ix = await createIx(solverSdk) - }) - - itThrowsAnError('Incorrect transfer token mint account') - }) + context('when transfer accounts are incorrect', () => { + context('when token is incorrect', () => { + beforeEach(async () => { + remainingAccounts[2].pubkey = createMint(client, admin).mint + await prepareIntentAndProposal() + ix = await createIx(solverSdk) + }) - context('when recipient is incorrect', () => { - beforeEach(async () => { - remainingAccounts[3].pubkey = randomPubkey() - await prepareIntentAndProposal() - ix = await createIx(solverSdk) + itThrowsAnError('Incorrect transfer token mint account') }) - itThrowsAnError('Incorrect transfer recipient account') - }) - - context('when recipient token account is incorrect', () => { - context('when authority is incorrect', () => { + context('when recipient is incorrect', () => { beforeEach(async () => { - remainingAccounts[4].pubkey = userAta + remainingAccounts[3].pubkey = randomPubkey() await prepareIntentAndProposal() ix = await createIx(solverSdk) }) - itThrowsAnError('Incorrect recipient token account: mint or authority do not match expected') + itThrowsAnError('Incorrect transfer recipient account') }) - context('when token mint is incorrect', () => { - beforeEach(async () => { - remainingAccounts[4].pubkey = ( - await createFundedAta(adminProvider, admin, recipient, createMint(client, admin).mint, 0) - ).ata + context('when recipient token account is incorrect', () => { + context('when authority is incorrect', () => { + beforeEach(async () => { + remainingAccounts[4].pubkey = userAta + await prepareIntentAndProposal() + ix = await createIx(solverSdk) + }) - await prepareIntentAndProposal() - ix = await createIx(solverSdk) + itThrowsAnError('Incorrect recipient token account: mint or authority do not match expected') }) - itThrowsAnError('Incorrect recipient token account: mint or authority do not match expected') + context('when token mint is incorrect', () => { + beforeEach(async () => { + remainingAccounts[4].pubkey = ( + await createFundedAta(adminProvider, admin, recipient, createMint(client, admin).mint, 0) + ).ata + + await prepareIntentAndProposal() + ix = await createIx(solverSdk) + }) + + itThrowsAnError('Incorrect recipient token account: mint or authority do not match expected') + }) }) - }) - context('when user token account is incorrect', () => { - context('when authority is incorrect', () => { - beforeEach(async () => { - remainingAccounts[5].pubkey = recipientAta - await prepareIntentAndProposal() - ix = await createIx(solverSdk) + context('when user token account is incorrect', () => { + context('when authority is incorrect', () => { + beforeEach(async () => { + remainingAccounts[5].pubkey = recipientAta + await prepareIntentAndProposal() + ix = await createIx(solverSdk) + }) + + itThrowsAnError('Incorrect user token account: mint or authority do not match expected') }) - itThrowsAnError('Incorrect user token account: mint or authority do not match expected') + context('when token mint is incorrect', () => { + beforeEach(async () => { + remainingAccounts[5].pubkey = ( + await createFundedAta(adminProvider, admin, user.publicKey, createMint(client, admin).mint, 0) + ).ata + + await prepareIntentAndProposal() + ix = await createIx(solverSdk) + }) + + itThrowsAnError('Incorrect user token account: mint or authority do not match expected') + }) }) + }) - context('when token mint is incorrect', () => { - beforeEach(async () => { - remainingAccounts[5].pubkey = ( - await createFundedAta(adminProvider, admin, user.publicKey, createMint(client, admin).mint, 0) - ).ata + context('when transfer accounts are correct', () => { + context('when fee accounts are incorrect', () => { + context('when token is incorrect', () => { + // TODO: implement + }) - await prepareIntentAndProposal() - ix = await createIx(solverSdk) + context('when solver token account is incorrect', () => { + // TODO: implement }) - itThrowsAnError('Incorrect user token account: mint or authority do not match expected') + context('when user token account is incorrect', () => { + // TODO: implement + }) }) }) }) From 8fa39bfdb147c351880845c21e33f107e7cff2aa Mon Sep 17 00:00:00 2001 From: GuidoDipietro Date: Wed, 8 Apr 2026 16:58:11 -0300 Subject: [PATCH 18/19] Implement happy path with pay_solver_fees --- .../settler/src/utils/execution/misc.rs | 2 +- packages/svm/tests/settler.test.ts | 104 ++++++++++-------- 2 files changed, 62 insertions(+), 44 deletions(-) diff --git a/packages/svm/programs/settler/src/utils/execution/misc.rs b/packages/svm/programs/settler/src/utils/execution/misc.rs index ce2985e..d0f4188 100644 --- a/packages/svm/programs/settler/src/utils/execution/misc.rs +++ b/packages/svm/programs/settler/src/utils/execution/misc.rs @@ -43,7 +43,6 @@ pub fn handle_intent_execution<'info>( /// /// For each fee_token: /// -/// #[account(mut)] /// pub fee_token: Account<'info, IMint>, /// /// #[account( @@ -96,6 +95,7 @@ pub fn pay_solver_fees<'info>( ITokenAccount::try_deserialize(&mut solver_ta_data)? }; + require_keys_eq!(token_account_info.key(), max_fee.token); require_keys_eq!(user_ta.owner, intent.user); require_keys_eq!(user_ta.mint, max_fee.token); require_keys_eq!(solver_ta.owner, proposal.creator); diff --git a/packages/svm/tests/settler.test.ts b/packages/svm/tests/settler.test.ts index b8c3129..d396cea 100644 --- a/packages/svm/tests/settler.test.ts +++ b/packages/svm/tests/settler.test.ts @@ -23,8 +23,8 @@ import { TransferIntentData, ValidatorSigner, } from '@mimicprotocol/sdk' -import { svmEncodeTransferIntent } from '@mimicprotocol/sdk/dist/shared/codec/chains/svm' -import { TOKEN_2022_PROGRAM_ID, TOKEN_PROGRAM_ID } from '@solana/spl-token' +import { svmDecodeTransferIntent, svmEncodeTransferIntent } from '@mimicprotocol/sdk/dist/shared/codec/chains/svm' +import { getAssociatedTokenAddressSync, TOKEN_2022_PROGRAM_ID, TOKEN_PROGRAM_ID } from '@solana/spl-token' import { AccountMeta, CreateSecp256k1InstructionWithEthAddressParams, @@ -2363,7 +2363,7 @@ describe('Settler', () => { data, deadline: (Number(client.getClock().unixTimestamp) + 1000).toString(), events: [{ topic: randomHex(32), data: randomHex(50) }], - maxFees: [{ token: usdc.toString(), amount: '100' }], + maxFees: [{ token: usdc.toString(), amount: '10000000' }], minValidations: 1, nonce: randomHex(32), op: 1, @@ -2439,13 +2439,14 @@ describe('Settler', () => { userAta = (await createFundedAta(adminProvider, admin, user.publicKey, usdc, 100_000_000_000)).ata recipientAta = (await createFundedAta(adminProvider, admin, recipient, usdc, 0)).ata + await createFundedAta(adminProvider, admin, solver.publicKey, usdc, 0) }) context('when intent is transfer', () => { let transfers: TransferIntentData['transfers'] let testIntentData: TransferIntentData - const testTransfers = () => [ + const createTestTransfers = () => [ { amount: '1000000000', token: usdc.toString(), @@ -2458,7 +2459,10 @@ describe('Settler', () => { transfers: transfers ?? [], }) - const getRemainingAccounts = (transfers: TransferIntentData['transfers']): AccountMeta[] => { + const getRemainingAccounts = (intent: Intent, proposal: Proposal): AccountMeta[] => { + const decodedIntent = svmDecodeTransferIntent(intent) + const { transfers } = decodedIntent + const tokenProgram: AccountMeta = { pubkey: TOKEN_PROGRAM_ID, isSigner: false, isWritable: false } const token2022Program: AccountMeta = { pubkey: TOKEN_2022_PROGRAM_ID, isSigner: false, isWritable: false } @@ -2467,10 +2471,21 @@ describe('Settler', () => { { pubkey: translateAddress(transfer.recipient), isSigner: false, isWritable: false }, { pubkey: recipientAta, isSigner: false, isWritable: true }, { pubkey: userAta, isSigner: false, isWritable: true }, - // TODO: add pay_solver_fees accounts ]) - return [tokenProgram, token2022Program, ...transferAccounts] + const solverFeeAccounts = intent.maxFees.flatMap((maxFee) => { + const feeToken = translateAddress(maxFee.token) + const solverAta = getAssociatedTokenAddressSync(feeToken, translateAddress(proposal.solver)) + const userAta = getAssociatedTokenAddressSync(feeToken, translateAddress(intent.user)) + + return [ + { pubkey: feeToken, isSigner: false, isWritable: false }, + { pubkey: solverAta, isSigner: false, isWritable: true }, + { pubkey: userAta, isSigner: false, isWritable: true }, + ] + }) + + return [tokenProgram, token2022Program, ...transferAccounts, ...solverFeeAccounts] } const editProposal = async (proposalKey: web3.PublicKey, editedProposal: Partial) => { @@ -2496,28 +2511,28 @@ describe('Settler', () => { context('when protocol has approval', () => { context('when user has sufficient funds', () => { beforeEach('Create data and approve delegate', async () => { - transfers = testTransfers() + transfers = createTestTransfers() + testIntentData = createTestIntentData(transfers) + intentHash = randomHex(32) + intent = createTestIntent(svmEncodeTransferIntent(testIntentData)) + proposal = createTestProposal(intent) + remainingAccounts = getRemainingAccounts(intent, proposal) await approveDelegate( userProvider, userAta, solverSdk.getDelegateKey(user.publicKey), user, - Number(transfers[0].amount) + Number(transfers[0].amount) + Number(proposal.fees[0]) ) - testIntentData = createTestIntentData(transfers) - intentHash = randomHex(32) - intent = createTestIntent(svmEncodeTransferIntent(testIntentData)) - proposal = createTestProposal(intent) - remainingAccounts = getRemainingAccounts(transfers) - await prepareIntentAndProposal() ix = await createIx(solverSdk) }) it('executes transfer', async () => { + const solverAta = getAssociatedTokenAddressSync(usdc, solver.publicKey) const proposalKey = sdk.getProposalKey(intentHash, proposal.solver) const intentKey = sdk.getIntentKey(intentHash) const fulfilledIntentKey = sdk.getFulfilledIntentKey(intentHash) @@ -2529,11 +2544,13 @@ describe('Settler', () => { const recipientBalanceBefore = getAtaBalance(client, recipientAta) const userBalanceBefore = getAtaBalance(client, userAta) + const solverAtaBalanceBefore = getAtaBalance(client, solverAta) await makeTxSignAndSend(solverProvider, ix) const recipientBalanceAfter = getAtaBalance(client, recipientAta) const userBalanceAfter = getAtaBalance(client, userAta) + const solverAtaBalanceAfter = getAtaBalance(client, solverAta) const proposalBalanceAfter = Number(adminProvider.client.getBalance(proposalKey)) || 0 const intentBalanceAfter = Number(adminProvider.client.getBalance(intentKey)) || 0 @@ -2557,7 +2574,9 @@ describe('Settler', () => { expect(client.getAccount(fulfilledIntentKey)?.owner.toString()).to.be.eq(settler.programId.toString()) expect(recipientBalanceAfter).to.be.eq(recipientBalanceBefore + Number(transfers[0].amount)) - expect(userBalanceAfter).to.be.eq(userBalanceBefore - Number(transfers[0].amount)) + expect(userBalanceAfter).to.be.eq( + userBalanceBefore - Number(transfers[0].amount) - Number(proposal.fees[0]) + ) expect(solverBalanceAfter).to.be.eq( solverBalanceBefore + intentBalanceBefore + @@ -2567,8 +2586,7 @@ describe('Settler', () => { ) expect(proposalBalanceAfter).to.be.eq(0) expect(intentBalanceAfter).to.be.eq(0) - - // TODO: check solver fees are paid + expect(solverAtaBalanceAfter).to.be.eq(solverAtaBalanceBefore + Number(proposal.fees[0])) }) }) @@ -2595,7 +2613,7 @@ describe('Settler', () => { intentHash = randomHex(32) intent = createTestIntent(svmEncodeTransferIntent(testIntentData)) proposal = createTestProposal(intent) - remainingAccounts = getRemainingAccounts(transfers) + remainingAccounts = getRemainingAccounts(intent, proposal) await prepareIntentAndProposal() @@ -2616,12 +2634,12 @@ describe('Settler', () => { beforeEach('Create data and remove delegate', async () => { await revokeDelegate(userProvider, userAta, user) - transfers = testTransfers() + transfers = createTestTransfers() testIntentData = createTestIntentData(transfers) intentHash = randomHex(32) intent = createTestIntent(svmEncodeTransferIntent(testIntentData)) proposal = createTestProposal(intent) - remainingAccounts = getRemainingAccounts(transfers) + remainingAccounts = getRemainingAccounts(intent, proposal) await prepareIntentAndProposal() @@ -2640,12 +2658,12 @@ describe('Settler', () => { context('when proposal is not valid', () => { context('when proposal intent is not for chain Solana', () => { beforeEach('Create data for Optimism', async () => { - transfers = testTransfers() + transfers = createTestTransfers() testIntentData = { ...createTestIntentData(transfers), chainId: Chains.Optimism } intentHash = randomHex(32) intent = createTestIntent(svmEncodeTransferIntent(testIntentData)) proposal = createTestProposal(intent) - remainingAccounts = getRemainingAccounts(transfers) + remainingAccounts = getRemainingAccounts(intent, proposal) await prepareIntentAndProposal() @@ -2657,12 +2675,12 @@ describe('Settler', () => { context('when proposal has data/instructions', () => { beforeEach('Create Proposal and manually edit bytes to add data on-chain', async () => { - transfers = testTransfers() + transfers = createTestTransfers() testIntentData = createTestIntentData(transfers) intentHash = randomHex(32) intent = createTestIntent(svmEncodeTransferIntent(testIntentData)) proposal = createTestProposal(intent) - remainingAccounts = getRemainingAccounts(transfers) + remainingAccounts = getRemainingAccounts(intent, proposal) await prepareIntentAndProposal() editProposal(solverSdk.getProposalKey(intentHash, proposal.solver), { @@ -2684,13 +2702,22 @@ describe('Settler', () => { }) context('when remaining accounts are not correct', () => { - beforeEach('Set up base data', async () => { - transfers = testTransfers() + beforeEach('Set up base data and re-approve', async () => { + transfers = createTestTransfers() testIntentData = createTestIntentData(transfers) intentHash = randomHex(32) intent = createTestIntent(svmEncodeTransferIntent(testIntentData)) proposal = createTestProposal(intent) - remainingAccounts = getRemainingAccounts(transfers) + remainingAccounts = getRemainingAccounts(intent, proposal) + + // Re-approve Delegate for test + await approveDelegate( + userProvider, + userAta, + solverSdk.getDelegateKey(user.publicKey), + user, + Number(transfers[0].amount) + Number(proposal.fees[0]) + ) }) context('when remaining accounts number is correct', () => { @@ -2831,15 +2858,6 @@ describe('Settler', () => { context('when there are more remaining accounts than expected', () => { beforeEach(async () => { - // Re-approve Delegate for test - await approveDelegate( - userProvider, - userAta, - solverSdk.getDelegateKey(user.publicKey), - user, - Number(transfers[0].amount) - ) - remainingAccounts.push({ pubkey: randomPubkey(), isWritable: true, isSigner: false }) await prepareIntentAndProposal() ix = await createIx(solverSdk) @@ -2870,12 +2888,12 @@ describe('Settler', () => { context('when proposal is not correct', () => { beforeEach('Setup base data', async () => { - transfers = testTransfers() + transfers = createTestTransfers() testIntentData = createTestIntentData(transfers) intentHash = randomHex(32) intent = createTestIntent(svmEncodeTransferIntent(testIntentData)) proposal = createTestProposal(intent) - remainingAccounts = getRemainingAccounts(transfers) + remainingAccounts = getRemainingAccounts(intent, proposal) }) context('when proposal is for another intent', () => { @@ -2933,12 +2951,12 @@ describe('Settler', () => { context('when intent is not correct', () => { context('when intent_creator is not correct', () => { beforeEach(async () => { - transfers = testTransfers() + transfers = createTestTransfers() testIntentData = createTestIntentData(transfers) intentHash = randomHex(32) intent = createTestIntent(svmEncodeTransferIntent(testIntentData)) proposal = createTestProposal(intent) - remainingAccounts = getRemainingAccounts(transfers) + remainingAccounts = getRemainingAccounts(intent, proposal) await prepareIntentAndProposal() ix = await createIx(solverSdk, { intentCreator: randomPubkey() }) @@ -2952,12 +2970,12 @@ describe('Settler', () => { context('when caller is not allowlisted solver', () => { beforeEach(async () => { - transfers = testTransfers() + transfers = createTestTransfers() testIntentData = createTestIntentData(transfers) intentHash = randomHex(32) intent = createTestIntent(svmEncodeTransferIntent(testIntentData)) proposal = createTestProposal(intent) - remainingAccounts = getRemainingAccounts(transfers) + remainingAccounts = getRemainingAccounts(intent, proposal) await prepareIntentAndProposal() ix = await createIx(maliciousSdk) From b3902ab0831bda680f23adda1dd4ef217b6b1cdc Mon Sep 17 00:00:00 2001 From: GuidoDipietro Date: Wed, 8 Apr 2026 17:00:40 -0300 Subject: [PATCH 19/19] Add custom errors for pay_solver_fees --- packages/svm/programs/settler/src/errors.rs | 6 ++++++ .../svm/programs/settler/src/utils/execution/misc.rs | 10 +++++----- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/packages/svm/programs/settler/src/errors.rs b/packages/svm/programs/settler/src/errors.rs index d225a19..ea1e3a7 100644 --- a/packages/svm/programs/settler/src/errors.rs +++ b/packages/svm/programs/settler/src/errors.rs @@ -107,6 +107,9 @@ pub enum SettlerError { #[msg("Incorrect transfer token mint account")] IncorrectTransferToken, + #[msg("Incorrect fee token mint account")] + IncorrectFeeToken, + #[msg("Account not owned by TokenKeg or Token2022 programs")] AccountNotOwnedByTokenProgram, @@ -116,6 +119,9 @@ pub enum SettlerError { #[msg("Incorrect user token account: mint or authority do not match expected")] IncorrectUserTokenAccount, + #[msg("Incorrect solver token account: mint or authority do not match expected")] + IncorrectSolverTokenAccount, + #[msg("Incorrect token program account provided")] IncorrectTokenProgram, diff --git a/packages/svm/programs/settler/src/utils/execution/misc.rs b/packages/svm/programs/settler/src/utils/execution/misc.rs index d0f4188..2fa30a2 100644 --- a/packages/svm/programs/settler/src/utils/execution/misc.rs +++ b/packages/svm/programs/settler/src/utils/execution/misc.rs @@ -95,11 +95,11 @@ pub fn pay_solver_fees<'info>( ITokenAccount::try_deserialize(&mut solver_ta_data)? }; - require_keys_eq!(token_account_info.key(), max_fee.token); - require_keys_eq!(user_ta.owner, intent.user); - require_keys_eq!(user_ta.mint, max_fee.token); - require_keys_eq!(solver_ta.owner, proposal.creator); - require_keys_eq!(solver_ta.mint, max_fee.token); + require_keys_eq!(token_account_info.key(), max_fee.token, SettlerError::IncorrectFeeToken); + require_keys_eq!(user_ta.owner, intent.user, SettlerError::IncorrectUserTokenAccount); + require_keys_eq!(user_ta.mint, max_fee.token, SettlerError::IncorrectUserTokenAccount); + require_keys_eq!(solver_ta.owner, proposal.creator, SettlerError::IncorrectSolverTokenAccount); + require_keys_eq!(solver_ta.mint, max_fee.token, SettlerError::IncorrectSolverTokenAccount); // Construct transfer_checked CPI let cpi_accounts = TransferChecked {