Skip to content

Commit b7addc0

Browse files
committed
feat: check risk strategies
1 parent 8a4f601 commit b7addc0

7 files changed

Lines changed: 174 additions & 6 deletions

File tree

config.example.toml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,14 @@ request-timeout = "5s"
236236
# Env: MBV_CHAINLINK__RISK__RISK_SCORE_THRESHOLD
237237
# risk-score-threshold = 5
238238

239+
# Which post-delegation action signers to risk check.
240+
# "all-signers": check every signer of every action (conservative).
241+
# "relevant-programs": only check when an action involves the SPL Token,
242+
# ephemeral SPL (eATA/ESPL), or Magic program.
243+
# Default: "relevant-programs"
244+
# Env: MBV_CHAINLINK__RISK__CHECK_STRATEGY
245+
check-strategy = "relevant-programs"
246+
239247
# ==============================================================================
240248
# Leader Administration (Optional)
241249
# ==============================================================================

magicblock-aml/src/lib.rs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ use std::{
66
};
77

88
use futures_util::future::{BoxFuture, FutureExt, Shared, try_join_all};
9-
use magicblock_config::config::RiskConfig;
9+
use magicblock_config::config::{AmlCheckStrategy, RiskConfig};
1010
use reqwest::Client;
1111
use rusqlite::{Connection, params};
1212
use serde_json::Value;
@@ -68,6 +68,7 @@ pub struct RiskService {
6868
api_key: String,
6969
cache_ttl: Duration,
7070
risk_score_threshold: u64,
71+
check_strategy: AmlCheckStrategy,
7172
}
7273

7374
impl RiskService {
@@ -120,9 +121,16 @@ impl RiskService {
120121
api_key,
121122
cache_ttl: config.cache_ttl,
122123
risk_score_threshold: config.risk_score_threshold,
124+
check_strategy: config.check_strategy,
123125
}))
124126
}
125127

128+
/// The configured strategy for deciding which post-delegation action
129+
/// signers to risk check.
130+
pub fn check_strategy(&self) -> AmlCheckStrategy {
131+
self.check_strategy
132+
}
133+
126134
pub async fn check_addresses(
127135
&self,
128136
addresses: Vec<String>,
@@ -450,6 +458,7 @@ mod tests {
450458
cache_ttl: Duration::from_secs(60),
451459
request_timeout: Duration::from_secs(2),
452460
risk_score_threshold: 7,
461+
check_strategy: AmlCheckStrategy::AllSigners,
453462
}
454463
}
455464

magicblock-chainlink/src/chainlink/fetch_cloner/mod.rs

Lines changed: 46 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,10 @@ use engine::Engine;
2222
use keeper::MissingAccount;
2323
use lru::LruCache;
2424
use magicblock_aml::RiskService;
25-
use magicblock_config::config::AllowedProgram;
25+
use magicblock_config::config::{AllowedProgram, AmlCheckStrategy};
2626
use magicblock_core::token_programs::{
27-
ASSOCIATED_TOKEN_PROGRAM_ID, EATA_PROGRAM_ID, TOKEN_PROGRAM_ID, is_ata,
28-
normalize_native_token_account_for_local_clone,
27+
ASSOCIATED_TOKEN_PROGRAM_ID, EATA_PROGRAM_ID, TOKEN_2022_PROGRAM_ID,
28+
TOKEN_PROGRAM_ID, is_ata, normalize_native_token_account_for_local_clone,
2929
};
3030
use magicblock_metrics::metrics::{
3131
self, AccountFetchContext, AccountFetchReason, BankPrecheckOutcome,
@@ -333,6 +333,42 @@ fn log_companion_fetch_failure<E: std::fmt::Display + ?Sized>(
333333
);
334334
}
335335

336+
/// Programs whose presence in a post-delegation action triggers a risk check
337+
/// under [`AmlCheckStrategy::RelevantPrograms`]: SPL Token (legacy and 2022),
338+
/// the ephemeral SPL / eATA program (ESPL), and the Magic program.
339+
const RISK_RELEVANT_PROGRAMS: [Pubkey; 4] = [
340+
TOKEN_PROGRAM_ID,
341+
TOKEN_2022_PROGRAM_ID,
342+
EATA_PROGRAM_ID,
343+
magicblock_magic_program_api::ID,
344+
];
345+
346+
/// Decides whether the configured [`AmlCheckStrategy`] requires risk checking
347+
/// the signers of these post-delegation actions.
348+
fn delegation_actions_require_risk_check(
349+
strategy: AmlCheckStrategy,
350+
delegation_actions: &DelegationActions,
351+
) -> bool {
352+
match strategy {
353+
AmlCheckStrategy::AllSigners => true,
354+
AmlCheckStrategy::RelevantPrograms => delegation_actions
355+
.iter()
356+
.any(instruction_involves_risk_relevant_program),
357+
}
358+
}
359+
360+
/// Returns true when a risk-relevant program is invoked by the instruction or
361+
/// referenced by any of its accounts (e.g. as the target of a CPI).
362+
fn instruction_involves_risk_relevant_program(
363+
instruction: &solana_instruction::Instruction,
364+
) -> bool {
365+
RISK_RELEVANT_PROGRAMS.contains(&instruction.program_id)
366+
|| instruction
367+
.accounts
368+
.iter()
369+
.any(|meta| RISK_RELEVANT_PROGRAMS.contains(&meta.pubkey))
370+
}
371+
336372
impl<T, U> FetchCloner<T, U>
337373
where
338374
T: ChainRpcClient,
@@ -1538,6 +1574,13 @@ where
15381574
return Ok(());
15391575
};
15401576

1577+
if !delegation_actions_require_risk_check(
1578+
risk_service.check_strategy(),
1579+
delegation_actions,
1580+
) {
1581+
return Ok(());
1582+
}
1583+
15411584
let mut signers = delegation_actions
15421585
.iter()
15431586
.flat_map(|instruction| {

magicblock-chainlink/src/chainlink/fetch_cloner/tests.rs

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,3 +82,89 @@ fn clone_request_classification() {
8282
ChainlinkCloneIntent::ActionDependency
8383
);
8484
}
85+
86+
mod aml_check_strategy {
87+
use solana_instruction::AccountMeta;
88+
89+
use super::*;
90+
91+
fn action_for_program(program_id: Pubkey) -> Instruction {
92+
Instruction::new_with_bytes(program_id, &[], vec![])
93+
}
94+
95+
fn action_referencing(program_id: Pubkey, account: Pubkey) -> Instruction {
96+
Instruction::new_with_bytes(
97+
program_id,
98+
&[],
99+
vec![AccountMeta::new_readonly(account, false)],
100+
)
101+
}
102+
103+
#[test]
104+
fn all_signers_strategy_always_requires_check() {
105+
// An action that touches no risk-relevant program still gets checked.
106+
let actions: DelegationActions =
107+
vec![action_for_program(Pubkey::new_unique())].into();
108+
assert!(delegation_actions_require_risk_check(
109+
AmlCheckStrategy::AllSigners,
110+
&actions,
111+
));
112+
}
113+
114+
#[test]
115+
fn relevant_programs_strategy_skips_unrelated_actions() {
116+
let actions: DelegationActions = vec![
117+
action_for_program(Pubkey::new_unique()),
118+
action_for_program(Pubkey::new_unique()),
119+
]
120+
.into();
121+
assert!(!delegation_actions_require_risk_check(
122+
AmlCheckStrategy::RelevantPrograms,
123+
&actions,
124+
));
125+
}
126+
127+
#[test]
128+
fn relevant_programs_strategy_matches_each_relevant_program() {
129+
for program in [
130+
TOKEN_PROGRAM_ID,
131+
TOKEN_2022_PROGRAM_ID,
132+
EATA_PROGRAM_ID,
133+
magicblock_magic_program_api::ID,
134+
] {
135+
let actions: DelegationActions =
136+
vec![action_for_program(program)].into();
137+
assert!(
138+
delegation_actions_require_risk_check(
139+
AmlCheckStrategy::RelevantPrograms,
140+
&actions,
141+
),
142+
"program {program} invoked as program_id should require check",
143+
);
144+
145+
// Referenced as a CPI target account, not the invoked program.
146+
let actions: DelegationActions =
147+
vec![action_referencing(Pubkey::new_unique(), program)].into();
148+
assert!(
149+
delegation_actions_require_risk_check(
150+
AmlCheckStrategy::RelevantPrograms,
151+
&actions,
152+
),
153+
"program {program} referenced as account should require check",
154+
);
155+
}
156+
}
157+
158+
#[test]
159+
fn relevant_programs_strategy_matches_when_any_action_is_relevant() {
160+
let actions: DelegationActions = vec![
161+
action_for_program(Pubkey::new_unique()),
162+
action_for_program(EATA_PROGRAM_ID),
163+
]
164+
.into();
165+
assert!(delegation_actions_require_risk_check(
166+
AmlCheckStrategy::RelevantPrograms,
167+
&actions,
168+
));
169+
}
170+
}

magicblock-chainlink/tests/10_aml_undelegation.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ use dlp_api::{
1616
};
1717
use magicblock_aml::RiskService;
1818
use magicblock_chainlink::testing::{context::TestContext, init_logger};
19-
use magicblock_config::config::RiskConfig;
19+
use magicblock_config::config::{AmlCheckStrategy, RiskConfig};
2020
use solana_account::{Account, AccountMode};
2121
use solana_pubkey::Pubkey;
2222
use tokio::task::JoinHandle;
@@ -101,6 +101,7 @@ fn risk_config(base_url: String) -> RiskConfig {
101101
cache_ttl: Duration::from_secs(60),
102102
request_timeout: Duration::from_secs(2),
103103
risk_score_threshold: 7,
104+
check_strategy: AmlCheckStrategy::AllSigners,
104105
}
105106
}
106107

magicblock-config/src/config/chain.rs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,23 @@ impl Default for ChainLinkConfig {
7373
}
7474
}
7575

76+
/// Strategy for deciding which post-delegation action signers get AML/risk
77+
/// checked.
78+
#[derive(
79+
Deserialize, Serialize, Debug, Clone, Copy, PartialEq, Eq, Default,
80+
)]
81+
#[serde(rename_all = "kebab-case")]
82+
pub enum AmlCheckStrategy {
83+
/// Check every signer of every post-delegation action, regardless of which
84+
/// programs the action invokes.
85+
AllSigners,
86+
/// Only check signers when a post-delegation action involves the SPL Token,
87+
/// ephemeral SPL (eATA/ESPL), or Magic program. Actions touching none of
88+
/// these programs skip the risk check entirely.
89+
#[default]
90+
RelevantPrograms,
91+
}
92+
7693
/// Configuration for checking account risk with the Range API and a local sqlite cache.
7794
#[derive(Deserialize, Serialize, Debug, Clone)]
7895
#[serde(default, rename_all = "kebab-case", deny_unknown_fields)]
@@ -91,6 +108,8 @@ pub struct RiskConfig {
91108
pub request_timeout: Duration,
92109
/// Threshold on a scale of 0-10 for the risk score.
93110
pub risk_score_threshold: u64,
111+
/// Which post-delegation action signers to risk check.
112+
pub check_strategy: AmlCheckStrategy,
94113
}
95114

96115
impl Default for RiskConfig {
@@ -104,6 +123,7 @@ impl Default for RiskConfig {
104123
consts::DEFAULT_RISK_REQUEST_TIMEOUT_SEC,
105124
),
106125
risk_score_threshold: consts::DEFAULT_RISK_SCORE_THRESHOLD,
126+
check_strategy: AmlCheckStrategy::default(),
107127
}
108128
}
109129
}

magicblock-config/src/config/mod.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@ pub mod scheduler;
1111

1212
pub use aperture::ApertureConfig;
1313
pub use chain::{
14-
AdminConfig, AllowedProgram, ChainLinkConfig, CommittorConfig, RiskConfig,
14+
AdminConfig, AllowedProgram, AmlCheckStrategy, ChainLinkConfig,
15+
CommittorConfig, RiskConfig,
1516
};
1617
pub use engine::{EngineConfig, FollowerReplication, LeaderReplication};
1718
pub use grpc::GrpcConfig;

0 commit comments

Comments
 (0)