Skip to content

feat: schedule automatic undelegation on AML check failure#1300

Open
Dodecahedr0x wants to merge 7 commits into
masterfrom
dode/automatic-undelegation
Open

feat: schedule automatic undelegation on AML check failure#1300
Dodecahedr0x wants to merge 7 commits into
masterfrom
dode/automatic-undelegation

Conversation

@Dodecahedr0x

@Dodecahedr0x Dodecahedr0x commented Jun 5, 2026

Copy link
Copy Markdown
Contributor

Summary

Automatically schedules the undelegation of AML-flagged accounts. Currently, if an account fails the AML check, it is just not cloned, meaning it stays owned by the DLP on mainnet but can't be used in the ER. This PR automatically undelegates it.

Breaking Changes

  • None

Summary by CodeRabbit

  • New Features
    • Added AML risk evaluation for delegated account cloning: high-risk signers now trigger automatic undelegation scheduling instead of clone failures.
    • Updated cloning flow to conditionally emit undelegation scheduling when needed.
  • Bug Fixes
    • Improved post-delegation instruction handling/ordering to ensure undelegation is processed correctly.
    • Added clearer errors when undelegation scheduling fails.
  • Tests
    • Added AML integration test suites with mocked risk services and new runner/devnet targets/configs.

@coderabbitai

coderabbitai Bot commented Jun 5, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The PR adds an AML-triggered undelegation scheduling path. A new needs_undelegation: bool field is added to AccountCloneRequest, and a ScheduleUndelegation variant is added to PostDelegationActionExecutorInstruction. Shared validation utilities (validate_last_after_clone, validate_current_top_level_instruction) are extracted into a new utils/validation module and reused by the refactored process_execute_post_delegation_actions. A new on-chain handler process_schedule_cloned_account_undelegation validates signers, prior clone instructions, and sysvar accounts, then schedules a CommitAndUndelegate intent in MagicContext. The account cloner transaction builders branch on needs_undelegation to emit either a schedule-undelegation instruction or the existing post-delegation action executor. FetchCloner intercepts RangeRisk(HighRiskAddresses(_)) errors from dependency checks and sets needs_undelegation = true instead of aborting. Test contexts are updated to accept optional RiskService instances. A new test-aml crate with MockRangeServer, validator setup utilities, and delegation record polling helpers is added, along with aml.devnet.toml config, Makefile targets, and two integration test suites validating the full risk-gating flow.

Suggested reviewers

  • snawaz
  • GabrielePicco
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch dode/automatic-undelegation

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@Dodecahedr0x Dodecahedr0x marked this pull request as ready for review June 5, 2026 15:19

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@magicblock-api/src/magic_validator.rs`:
- Around line 115-120: Wrap the await on self.0.schedule_undelegation(pubkey,
request.account).await in a tokio timeout (e.g.,
tokio::time::timeout(Duration::from_secs(...), ...)) so the call cannot hang
indefinitely; after awaiting the timeout, map a timeout error to the same
chainlink/committor error variant currently produced (the "committor response
channel closed" mapping) so both channel-closure and timeout produce the same
error path, and then continue to .and_then(|result| ...) as before; update
imports if necessary to include tokio::time::timeout and Duration and ensure the
timeout mapping converts into a String consistent with the existing
.map_err(|message| ...) flow.

In `@magicblock-chainlink/src/chainlink/fetch_cloner/tests.rs`:
- Around line 251-255: The mock server's blocking accept loop inside the
tokio::task::spawn_blocking worker (which calls listener.accept() in a for
0..expected_calls loop) can hang CI if fewer connections arrive; modify the
worker to enforce a hard timeout (e.g., 5s) for each accept or for the entire
loop: set the TcpListener to non-blocking (listener.set_nonblocking(true)) or
use Instant::now()/elapsed to track time, repeatedly try accept() with a short
sleep, and if the deadline is exceeded before receiving all expected_calls,
break and return an error (or panic) so the test fails fast instead of hanging
while the test awaits the worker join. Ensure the change is applied to the same
worker block that spawns the mock server and to the other similar instance
mentioned (around the other accept loop).

In `@magicblock-chainlink/tests/10_aml_undelegation.rs`:
- Around line 70-74: The mock risk-server worker can block forever on
listener.accept() inside the spawned closure (the worker) causing join() to
hang; change the worker loop to avoid blocking by calling
listener.set_nonblocking(true) (or use try_accept if available) and perform a
bounded retry loop with a short sleep, counting successful accepts until
expected_calls or until a configurable timeout elapses, then break and return an
error/result so the task completes; update the code that awaits the worker (the
join() site) to handle the early-return error case. Ensure you reference the
spawned closure (worker), listener.accept, expected_calls and the join()
awaiting the worker when making the change.

In `@magicblock-committor-service/src/committor_processor.rs`:
- Around line 302-309: The current intent_id() uses wall-clock nanoseconds which
can collide; replace it with a collision-free generator such as a monotonic
atomic counter or a UUID-based ID instead of SystemTime::now(). Specifically,
update the intent_id() function to read from a shared AtomicU64 (e.g.,
NEXT_INTENT_ID.fetch_add(1, Ordering::SeqCst)) or generate a UUID and
convert/encode it for ScheduledIntentBundle.id so each undelegation gets a
unique, monotonic or globally-unique ID; ensure the chosen generator is
initialized and accessible where intent_id() is called and that persisted
keys/status lookups use the same ID format.

In `@magicblock-committor-service/src/service.rs`:
- Around line 68-72: The ScheduleUndelegation variant embeds AccountSharedData
which causes CommittorService::try_send() to log full account bytes on
TrySendError::{Full,Closed}; change the logging to avoid dumping the account
payload by either redacting ScheduleUndelegation's Debug output or logging only
the pubkey and message type. Concretely, update the CommittorService::try_send()
error paths that currently log the CommittorMessage to instead pattern-match the
message and log a concise string for ScheduleUndelegation (e.g.,
"ScheduleUndelegation { pubkey: <pubkey> }") or implement a custom Debug/Display
for that enum variant that omits AccountSharedData; ensure references to
ScheduleUndelegation, CommittorMessage, AccountSharedData, Pubkey, and
TrySendError::Full/Closed are used to locate and change the relevant code.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: dd15f542-18ab-4785-a422-c9f5e2e1d1a1

📥 Commits

Reviewing files that changed from the base of the PR and between 2407d9e and 54e0905.

⛔ Files ignored due to path filters (2)
  • Cargo.lock is excluded by !**/*.lock
  • test-integration/Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (15)
  • magicblock-api/Cargo.toml
  • magicblock-api/src/magic_validator.rs
  • magicblock-chainlink/Cargo.toml
  • magicblock-chainlink/src/chainlink/errors.rs
  • magicblock-chainlink/src/chainlink/fetch_cloner/mod.rs
  • magicblock-chainlink/src/chainlink/fetch_cloner/tests.rs
  • magicblock-chainlink/src/chainlink/mod.rs
  • magicblock-chainlink/tests/10_aml_undelegation.rs
  • magicblock-chainlink/tests/utils/test_context.rs
  • magicblock-committor-service/src/committor_processor.rs
  • magicblock-committor-service/src/service.rs
  • magicblock-committor-service/src/service_ext.rs
  • magicblock-committor-service/src/stubs/changeset_committor_stub.rs
  • test-integration/test-chainlink/src/ixtest_context.rs
  • test-integration/test-chainlink/src/test_context.rs

Comment thread magicblock-api/src/magic_validator.rs Outdated
Comment thread magicblock-chainlink/src/chainlink/fetch_cloner/tests.rs Outdated
Comment thread test-integration/test-chainlink/tests/ix_aml_undelegation.rs
Comment thread magicblock-committor-service/src/committor_processor.rs Outdated
Comment thread magicblock-committor-service/src/service.rs Outdated
@snawaz

snawaz commented Jun 6, 2026

Copy link
Copy Markdown
Contributor

@Dodecahedr0x : can you please add some description as to when/why undelegation should be scheduled and automated.

@taco-paco taco-paco left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this PR digresses from current committor flow, and imo there's no need in this deviation.
Delegation is an account state property, therefore it has to go through builtin program and modify delegated flags.

This makes flow similar to regular Intent scheduling one, in fact you do schedule an Intent youself.

  1. Validator detect account is malicious
  2. Validator sends TX in magic-program(new entrypoint, or an old one with commit)
  3. magic-program mutates account states correctly and schedules undelegation(mark_account_as_undelegated )
  4. CommittorService picks it up and executes as in current flow

This gets rid of new schedule_undelegation functionality completely as it will seamlessly integrates into current Committor logic. All is needed is new entrypoint in magic-program and TX sender

Current state bypasses all of that flow to which it belongs. This additionally lead to an amigious state where in AccountsDB account is delegated but on Base it is not. Maybe cloner will fix this(which I'm not sure about) after some updates, but this leaves window for an attack where in meantime it could be used by user.

Comment thread magicblock-api/src/magic_validator.rs Outdated
struct CommittorUndelegationScheduler(Arc<CommittorService>);

#[async_trait::async_trait]
impl UndelegationScheduler for CommittorUndelegationScheduler {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

magic_validator.rs is already massive, let's extract this into separate file

}
}

pub(crate) fn undelegation_intent_bundle(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This has to be created by magic-program which additionally would call mark_account_as_undelegated

}
}

fn intent_id() -> u64 {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Intent ids are assigned by magic-program

@snawaz snawaz changed the title feat: schedule automatic undelegation feat: schedule automatic undelegation on AML check failure Jun 10, 2026
@Dodecahedr0x

Copy link
Copy Markdown
Contributor Author

@taco-paco I completely reworked the flow to use the regular commit path. However, it now clones and undelegates the account in a single transaction.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
programs/magicblock/src/utils/instruction_utils.rs (1)

82-84: ⚠️ Potential issue | 🟠 Major

Mark accounts at index 2..n as non-signers, not signers.

Line 83 marks each PDA as writable and signer with AccountMeta::new(*pubkey, true), but the instruction spec (magicblock-magic-program-api/src/instruction.rs, line 57) documents accounts 2..n as [] (read-only, non-signer). Since the builder signs with only the payer, non-payer accounts marked as signers will fail transaction sanitization in production.

Suggested fix
         for pubkey in &pdas {
-            account_metas.push(AccountMeta::new(*pubkey, true));
+            account_metas.push(AccountMeta::new_readonly(*pubkey, false));
         }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@programs/magicblock/src/utils/instruction_utils.rs` around lines 82 - 84, The
for loop in instruction_utils.rs that processes PDAs (line 82-84) marks each
account as both writable and signer using AccountMeta::new with true as the
second parameter. However, according to the instruction specification in
magicblock-magic-program-api/src/instruction.rs at line 57, accounts 2..n should
be read-only, non-signers. Change the AccountMeta::new call to use an
appropriate constructor method that marks these PDA accounts as read-only and
non-signers instead, since only the payer is expected to sign the transaction
and marking non-payer accounts as signers will cause transaction sanitization to
fail in production.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@test-integration/Makefile`:
- Around line 57-64: The test-aml target bypasses required program preparation
because the shared test target only runs chainlink-prep-programs when RUN_TESTS
is empty or contains chainlink or cloning. Since aml.devnet.toml requires
artifacts from ../target/deploy/miniv3/program_mini.so, ensure
chainlink-prep-programs is invoked before running aml tests. Either add aml to
the condition that triggers chainlink-prep-programs in the shared test target,
or explicitly invoke chainlink-prep-programs as a prerequisite step in the
test-aml target (similarly to how other test configurations handle program
preparation).

---

Outside diff comments:
In `@programs/magicblock/src/utils/instruction_utils.rs`:
- Around line 82-84: The for loop in instruction_utils.rs that processes PDAs
(line 82-84) marks each account as both writable and signer using
AccountMeta::new with true as the second parameter. However, according to the
instruction specification in magicblock-magic-program-api/src/instruction.rs at
line 57, accounts 2..n should be read-only, non-signers. Change the
AccountMeta::new call to use an appropriate constructor method that marks these
PDA accounts as read-only and non-signers instead, since only the payer is
expected to sign the transaction and marking non-payer accounts as signers will
cause transaction sanitization to fail in production.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: b5b1a290-f756-4c8a-a7f2-c08af943c5a5

📥 Commits

Reviewing files that changed from the base of the PR and between 54e0905 and 9876d63.

⛔ Files ignored due to path filters (1)
  • test-integration/Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (29)
  • magicblock-account-cloner/src/lib.rs
  • magicblock-chainlink/src/chainlink/fetch_cloner/ata_projection.rs
  • magicblock-chainlink/src/chainlink/fetch_cloner/mod.rs
  • magicblock-chainlink/src/chainlink/fetch_cloner/pipeline.rs
  • magicblock-chainlink/src/chainlink/fetch_cloner/tests.rs
  • magicblock-chainlink/src/chainlink/mod.rs
  • magicblock-chainlink/src/cloner/errors.rs
  • magicblock-chainlink/src/cloner/mod.rs
  • magicblock-chainlink/tests/utils/test_context.rs
  • magicblock-magic-program-api/src/instruction.rs
  • programs/magicblock/src/clone_account/process_post_delegation_actions.rs
  • programs/magicblock/src/magicblock_processor.rs
  • programs/magicblock/src/schedule_transactions/mod.rs
  • programs/magicblock/src/schedule_transactions/process_schedule_cloned_undelegation.rs
  • programs/magicblock/src/utils/instruction_utils.rs
  • programs/magicblock/src/utils/mod.rs
  • programs/magicblock/src/utils/validation.rs
  • test-integration/Cargo.toml
  • test-integration/Makefile
  • test-integration/configs/aml.devnet.toml
  • test-integration/test-aml/Cargo.toml
  • test-integration/test-aml/src/lib.rs
  • test-integration/test-aml/tests/range_mock.rs
  • test-integration/test-chainlink/Cargo.toml
  • test-integration/test-chainlink/src/ixtest_context.rs
  • test-integration/test-chainlink/src/test_context.rs
  • test-integration/test-chainlink/tests/ix_aml_undelegation.rs
  • test-integration/test-runner/Cargo.toml
  • test-integration/test-runner/bin/run_tests.rs

Comment thread test-integration/Makefile
Comment on lines +57 to +64
test-aml:
RUN_TESTS=aml \
$(MAKE) test
setup-aml-devnet:
RUST_LOG_STYLE=none \
RUN_TESTS=aml \
SETUP_ONLY=devnet \
$(MAKE) test

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

test-aml currently bypasses required program prep and can fail on clean runs

test-aml sets RUN_TESTS=aml, but the shared test target only invokes chainlink-prep-programs for empty RUN_TESTS or chainlink|cloning. Since aml.devnet.toml loads artifacts like ../target/deploy/miniv3/program_mini.so, make test-aml can fail before running tests when those artifacts are absent.

Suggested fix
 test: $(PROGRAMS_SO)
-	if [ -z "$(RUN_TESTS)" ] || printf ',%s,' "$(RUN_TESTS)" | grep -Eq ',(chainlink|cloning),'; then \
+	if [ -z "$(RUN_TESTS)" ] || printf ',%s,' "$(RUN_TESTS)" | grep -Eq ',(chainlink|cloning|aml),'; then \
 		$(MAKE) chainlink-prep-programs -C ./test-chainlink; \
 	fi && \
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test-integration/Makefile` around lines 57 - 64, The test-aml target bypasses
required program preparation because the shared test target only runs
chainlink-prep-programs when RUN_TESTS is empty or contains chainlink or
cloning. Since aml.devnet.toml requires artifacts from
../target/deploy/miniv3/program_mini.so, ensure chainlink-prep-programs is
invoked before running aml tests. Either add aml to the condition that triggers
chainlink-prep-programs in the shared test target, or explicitly invoke
chainlink-prep-programs as a prerequisite step in the test-aml target (similarly
to how other test configurations handle program preparation).

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@magicblock-account-cloner/src/lib.rs`:
- Around line 873-878: The panic messages in the match arms for
PostDelegationActionExecutorInstruction::ScheduleUndelegation are inverted and
misleading. These arms trigger when ScheduleUndelegation is unexpectedly
received, but the panic message says "expected schedule undelegation
instruction" which is backwards. Locate both occurrences of the
ScheduleUndelegation match arm and update each panic message to accurately
reflect that ScheduleUndelegation was received unexpectedly, and state what
instruction was actually expected instead (which should be Execute based on the
test expectations).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: a7fc8003-2a37-466a-96e1-35961d05bf41

📥 Commits

Reviewing files that changed from the base of the PR and between 9876d63 and ba22762.

⛔ Files ignored due to path filters (1)
  • test-integration/Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (4)
  • magicblock-account-cloner/src/lib.rs
  • magicblock-chainlink/src/chainlink/fetch_cloner/mod.rs
  • magicblock-chainlink/src/chainlink/fetch_cloner/tests.rs
  • magicblock-chainlink/src/chainlink/mod.rs

Comment on lines +873 to 878
PostDelegationActionExecutorInstruction::ScheduleUndelegation {
..
} => {
panic!("expected schedule undelegation instruction")
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Panic messages are inverted and misleading.

These match arms trigger when ScheduleUndelegation is unexpectedly received, but the message says "expected schedule undelegation instruction". The tests actually expect Execute, not ScheduleUndelegation.

🐛 Proposed fix
             PostDelegationActionExecutorInstruction::ScheduleUndelegation {
                 ..
             } => {
-                panic!("expected schedule undelegation instruction")
+                panic!("unexpected ScheduleUndelegation; expected Execute")
             }

Apply the same fix at both locations (lines 876 and 927).

Also applies to: 924-929

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@magicblock-account-cloner/src/lib.rs` around lines 873 - 878, The panic
messages in the match arms for
PostDelegationActionExecutorInstruction::ScheduleUndelegation are inverted and
misleading. These arms trigger when ScheduleUndelegation is unexpectedly
received, but the panic message says "expected schedule undelegation
instruction" which is backwards. Locate both occurrences of the
ScheduleUndelegation match arm and update each panic message to accurately
reflect that ScheduleUndelegation was received unexpectedly, and state what
instruction was actually expected instead (which should be Execute based on the
test expectations).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants