Auditing Quantus Network's Threshold ML-DSA

Hernan
Research Engineer
Abstract
Quantus Network engaged HashCloak Inc. to audit their implementation of ML-DSA-87, developed using Claude, in two versions: standalone and threshold. Here, I will present a case study of the audit process, the main findings, my rationale throughout the audit, and lessons learned for other cryptography developers. This was our first client providing an AI-assisted implementation of a cryptographic scheme, which makes this the first of its kind for our company. This case study will reveal the capabilities of AI for cryptographic and security-sensitive implementations.
The Context
Recently, advances in quantum computing have posed a new threat on the horizon: the possibility that some widely used cryptographic schemes based on the integer factorization problem, the discrete logarithm problem (DL), and elliptic curves (EC) may be broken. This possibility may affect a large number of real-world systems, rendering them insecure and compromising their users' privacy. Some cryptographic schemes that are at risk in the presence of large quantum computers are ECDSA, a signature scheme based on elliptic curves, and RSA, an encryption and signature scheme based on integer factorization; these schemes are used in multiple privacy-sensitive systems, such as messaging applications, banking, blockchain infrastructure, web infrastructure, and online markets.
Considering the upcoming quantum advances and potential threats, the National Institute of Standards and Technology (NIST) is leading global efforts to standardize new cryptographic schemes believed to be secure even in the presence of large quantum computers. On August 13, 2024, NIST released FIPS-204, a standard that specifies ML-DSA, a set of algorithms for generating and verifying digital signatures. ML-DSA is a construction based on the Module Learning With Errors (MLWE) problem, a computational problem believed to be hard to solve.
With the emergence of FIPS-204, several teams and developers have been developing implementations of ML-DSA, in line with the standardized specification and proposed security parameters. Although ML-DSA is a theoretically sound scheme that aims to protect users from the quantum threat, this does not guarantee that implementations are secure. Developers are humans, and humans make mistakes. Hence, there is a high risk of implementations that still contain attack vectors that compromise real-world system deployments.
Quantus Network implemented ML-DSA-87, an ML-DSA signature scheme with specific security parameters. To audit this code, Quantus Network engaged HashCloak Inc., and I was assigned the task of finding vulnerabilities and security issues in this implementation. The source code includes two separate implementations: a threshold signature scheme based on ML-DSA-87 and the actual implementation of ML-DSA-87, following the FIPS-204 standard. I was provided with the source code and both FIPS-204 and the reference paper for the threshold ML-DSA-87. What makes this implementation interesting is that the client used Claude to implement the threshold signature scheme from scratch. This makes this audit somewhat special for exploring and assessing the capabilities of AI agents to implement cryptography and security-sensitive software.
An Overview of ML-DSA
ML-DSA (also known as Dilithium) is a post-quantum secure signature scheme based on the Module Learning With Errors problem. ML-DSA is believed to be secure against adversaries with access to large-scale quantum computers. It is also believed to be strongly unforgeable, which implies that the signature scheme can be used to detect whether some piece of information has unauthorized modifications, and non-repudiable, meaning that the signature can be used to claim to a third party that it was generated by the claimed signatory.
ML-DSA is a Schnorr-like signature scheme that uses the Fiat-Shamir heuristic to transform an interactive protocol into a non-interactive one. At a high level, the interactive version of ML-DSA consists of the following steps:
Commitment: The prover generates 𝐲 ∈ (ℤ/qℤ)ᴸ with small coefficients and commits to its value by sending 𝐰[Approx] = 𝐀𝐲 + 𝐲₂ to the verifier, where 𝐲₂ ∈ (ℤ/qℤ)ᴷ is a vector with small coefficients.
Challenge: The verifier sends a vector 𝐜 ∈ (ℤ/qℤ)ⁿ with small coefficients to the prover.
Response: The prover returns 𝐳 = 𝐲 + 𝐒₁𝐜, and the verifier checks that 𝐳 has small coefficients and that 𝐀𝐳 − 𝐓𝐜 ≈ 𝐰[Approx].
To reduce signature and public key sizes while providing additional security guarantees, the ML-DSA scheme introduces tweaks to the basic approach, such as module-structured matrices, hint computation, and pseudo-random matrix generation. I refer the reader to the NIST standard for more details.
Key Generation
Key generation (Algorithms 1 and 6 in FIPS-204) is deterministic once a random seed is fixed. It proceeds as follows:
Seed sampling: The algorithm samples a fresh 32-byte random seed ξ using an approved random bit generator (RBG).
Seed expansion: The seed ξ is expanded with the hash function H (SHAKE256) into a public seed ρ, a private seed ρ′, and a private key K that is used later for signing.
Matrix and secrets: The public matrix 𝐀 with k rows and ℓ columns over R(q) is derived pseudo-randomly from ρ, and the secret vectors 𝐬₁ ∈ R(q)ˡ and 𝐬₂ ∈ R(q)ᵏ, with coefficients in [−η, η], are derived from ρ′.
Public value: The algorithm computes 𝐭 = 𝐀𝐬₁ + 𝐬₂ and splits it with Power2Round into its high bits 𝐭₁ and its d low bits 𝐭₀.
Output: The public key is pk = (ρ, 𝐭₁), and the secret key is sk = (ρ, K, tr, 𝐬₁, 𝐬₂, 𝐭₀), where tr = H(pk) is a 64-byte hash of the public key.
Here, R(q) = (ℤ/qℤ)[X]/(X²⁵⁶ + 1), and for ML-DSA-87, q = 8380417, (k, ℓ) = (8, 7), η = 2, and d = 13. Notice that, since 𝐀 is generated from ρ, the public key only stores the 32-byte seed ρ instead of the whole matrix.
Signing
Signing (Algorithms 2 and 7 in FIPS-204) follows the Fiat-Shamir with aborts paradigm: the signer repeats the commitment, challenge, and response steps described above until the response does not leak information about the secret key. Given the secret key, a message M, and an optional context string, the signer proceeds as follows:
Message representative: The message is formatted together with the context string and hashed with tr into a 64-byte representative μ.
Private randomness: The signer computes a seed ρ″ = H(K ∥ rnd ∥ μ), where rnd is a fresh 32-byte random value in the default "hedged" variant, or 32 zero bytes in the deterministic variant.
Commitment: In each iteration of the loop, the signer derives a mask 𝐲 ∈ R(q)ˡ with coefficients in [−γ₁ + 1, γ₁] from ρ″ and a counter, computes 𝐰 = 𝐀𝐲, and keeps its high bits 𝐰₁.
Challenge: The signer hashes μ and 𝐰₁ into a commitment hash c̃, from which it samples the challenge polynomial c with exactly τ coefficients in {−1, 1} and all other coefficients equal to zero.
Response: The signer computes 𝐳 = 𝐲 + c𝐬₁.
Rejection: The signer discards the attempt and restarts the loop if ‖𝐳‖∞ ≥ γ₁ − β or if the low bits of 𝐰 − c𝐬₂ satisfy ‖·‖∞ ≥ γ₂ − β. Releasing such a response would leak information about the secret key.
Hint: Otherwise, the signer computes a hint 𝐡 that allows the verifier to recover 𝐰₁ using the compressed public key 𝐭₁. The attempt is also rejected if ‖c𝐭₀‖∞ ≥ γ₂ or if 𝐡 has more than ω ones.
Output: The signature is σ = (c̃, 𝐳, 𝐡).
For ML-DSA-87, γ₁ = 2¹⁹, γ₂ = (q − 1)/32, τ = 60, β = τ · η = 120, and ω = 75. With these parameters, FIPS-204 estimates an average of 3.85 repetitions of the loop per signature.
Threshold ML-DSA-87
The threshold signing protocol is a three-round scheme that protects against rushing adversaries in the choice of 𝐰. I present a high-level overview of the rounds:
First round: Parties sample randomness 𝐫ᵢ ← χᵣ and compute the commitment 𝐰ᵢ = [𝐀 𝐈] · 𝐫ᵢ. They publish a hash of the commitment H[cmt](vk, i, 𝐰ᵢ).
Second round: Parties reveal the values of (𝐰ᵢ)[i ∈ act].
Third round: Each party computes the response 𝐳ᵢ = c · 𝐬ᵢᵖᵃʳᵗ + 𝐫ᵢ, where c is derived from the aggregated commitment 𝐰 = ∑[i ∈ act] 𝐰ᵢ after proper rejection sampling.
After the three rounds, the Combine procedure computes the hint 𝐡 that allows verifiers to recover the higher bits of 𝐰 from a value used as an approximation of 𝐰.
Distributed Key Generation (DKG)
The audited code includes a protocol that generates shares of the secret key and the corresponding public key among the participants. However, after a careful analysis of the implemented protocol, I found that it performs the following steps in four rounds:
Round 1: Each party samples a session ID contribution that will be aggregated with the other parties' contributions with the intention of generating a global session ID to protect against replay attacks.
Round 2: Each party samples skᵢ = {s[I] ∈ R(q) : |I| = N − T + 1 ∧ I ⊆ [N]}, where each s[I] has small coefficients, and broadcasts the commitments cmtᵢ = H(skᵢ).
Round 3: Each party opens the commitments by broadcasting skᵢ. Once a party receives a commitment opening, it checks that the hash of the received value matches the commitment received in Round 2.
Round 4: Each party aggregates the final contributions by adding the contributions together, then computes the public key on the aggregated values, and finally returns the public key and the aggregated secret key.
In the audit process, we found a critical issue: this method was insecure, given that each party sends its own secret key share to everyone else; hence, all the parties can reconstruct the private key, which is clearly a privacy concern. As a result of the audit process, we reported this issue, and Quantus Network's team implemented the DKG protocol from Appendix D of "Efficient Threshold ML-DSA", which is the most direct solution.
The Audit Process
Scope of the Audit
The audit includes the AI-assisted implementation of a standalone ML-DSA following the NIST standard, a threshold ML-DSA implementation based on the paper "Efficient Threshold ML-DSA" by Celi et al., and its corresponding distributed key generation (DKG) protocol.
Timeline
The audit was conducted by one consultant (myself) in two phases:
February 16 to March 26, 2026: Initial assessment of the standalone and threshold implementations, which resulted in the first version of the report (v1.0).
March 26 to May 27, 2026: Post-audit review, in which Quantus Network sent the fixes for the reported issues and we reviewed them, including the new implementation of the DKG protocol.
Preparation
The first step I take for an audit is to familiarize myself with both theoretical specifications, in this case, FIPS-204 and the paper for the threshold signature scheme. Given that the task ahead is an audit, I review the specifications not only to understand the theoretical details of the schemes but also to highlight the requirements the implementation must meet to guarantee the schemes' security. For example, FIPS-204 states that:
Algorithm 1, implementing key generation for ML-DSA, uses an RBG to generate the 256-bit random seed ξ. The seed ξ shall be a fresh (i.e., not previously used) random value generated using an approved RBG, as prescribed in SP 800-90A, SP 800-90B, and SP 800-90C.
Here, FIPS-204 is showing me something I must check. I must verify that the random seed ξ is fresh and that the code uses distinct random seeds across executions; otherwise, I may have an attack vector. As a second example, the threshold ML-DSA-87 reference paper states in Figure 5, procedure ShareSign₃, Step 4, that the code should assert that:
For every party j in the set of signing parties, the value of the commitment cmtⱼ must be equal to H[cmt](vk, j, 𝐰ⱼ).
In this case, I must check that this assertion is performed as the paper states, at the moment the paper states, for the parties the paper states, with the values the paper states. Everything about this particular statement must be considered as closely as possible to the paper's protocol specification.
As I read the specifications and become familiar with them, I highlight all the potential checks I need to make in the implementation. It is important to note that highlighting these requirements from a standard like FIPS-204 is easier than from a research paper. Moreover, the standards use "standard" keywords such as shall and should to express requirements and recommendations, respectively. Hence, in the majority of cases, this requirement extraction is easier from standards because they have already done this task for you. In contrast, papers often focus on theoretical correctness (that the scheme fulfills a certain theoretical definition) rather than practical correctness. Therefore, you must extract these checking requirements for yourself.
First Look at the Code
Once I familiarize myself with the specifications, I take a first look at the code, trying to map the specification steps to the code itself. The goal of this task is not to conduct a full audit or identify any issues; rather, I am doing this to properly position myself for a deeper assessment of the code in the future. The idea of this step is to read the documentation inside the code and to learn where the key algorithms are implemented, which algorithms are being implemented, which files are involved in each algorithm, and so on. Here, I also get an idea of the quality of the code and the documentation.
Observing the code and documentation quality is (in my personal opinion) a key part of the audit too. I always think that one person does not maintain code for years; instead, the developers who fix, add features, and modify the code change over time. This makes documentation and code quality key. Also, I think that good documentation and code quality indirectly improve the security of the code itself. If the code has poor quality or is poorly documented, a developer who is new to the code may make, for example, wrong mathematical assumptions or wrong data manipulations, which may lead to future issues.
In-Depth Look
Now that I have a global understanding of the code, I can start with a detailed assessment. First, I focus on the standalone version of ML-DSA, since the threshold version shares many functionalities with it. In this step, I check the code line by line, comparing it with the specification and confirming that everything not covered by the specification is also compliant with widely accepted good practices.
In this step, I start with the primitives and work up to the high-level code. I first check the mathematical functions, such as the number-theoretic transform (NTT), polynomial implementation and operations, FIPS-202 implementation, and rounding. Then, I move to higher-level code, which is the actual implementation of ML-DSA-87, where the developers put together all the primitives to realize the signature scheme.
ML-DSA is particularly heavy on math concepts such as lattices, rings, polynomials, and rounding. Hence, the auditor needs to be conscious of the gap between mathematical theory and practice. For example, the auditor needs to be aware of how to compute modular reductions correctly, considering the possible edge cases, or of the proper use of signed and unsigned integers in ML-DSA implementations.
In the midst of this deep review, I check that some portions of code are behaving correctly. I tend to choose the more obscure portions in which the quality of the code, the complexity of the protocol, or the lack of documentation makes the assessment non-trivial. To evaluate correctness, I write tests myself to confirm that the code is correct. Also, this is valuable for the client because the tests can be considered an additional product of the audit, helping to assure the correctness of the implementation.
Post-Audit Review
Once we sent the first version of the report (v1.0), we received new code that fixed the audit findings. For this new code, we conducted an additional assessment to make sure that the corrections did not add new issues. The most significant portion that was evaluated in the post-audit was the DKG, where we found six additional findings that were reported back to the client for resolution.
It is worth highlighting that the AI or the prompt used by the client did not consider the findings and their implications when correcting the issues themselves. For example, one of the findings was about the zeroization of sensitive information, where the developer must make sure that sensitive information is zeroized when the protocol finishes its execution or aborts midway (you will find more details about this later). This issue was found in the signing protocol, and later in some additional code for the DKG protocol. Chronologically, the issue was reported before the addition of fixes and new code in the DKG. However, we found that the DKG protocol suffered from the same issue of zeroization. It is true that we do not know how the client provided the context to the AI agent, its configuration, or the exact model and its capabilities. However, this at least reveals the importance of context in AI-assisted development.
Notable Findings
Now, I will present the findings at a high level. The following table summarizes the findings.
Severity | No. of issues |
|---|---|
Critical | 8 |
High | 4 |
Medium | 7 |
Low | 2 |
Informational | 9 |
The most critical issue I found was in threshold ML-DSA, where the commitments of 𝐰 were not checked upon opening. In the threshold ML-DSA, in the third round, fourth step, the specification requires that the commitments are checked, that is, each party needs to check that ∀ j ∈ act, it holds that cmtⱼ = H[cmt](vk, j, 𝐰ⱼ). The lack of this check leaves the door open to so-called rushing adversaries. A rushing adversary waits for the honest parties to send their 𝐰ⱼ to all the other parties (including the parties controlled by the adversary). Then, the adversary analyzes the 𝐰ⱼ values from the honest parties and sends their adaptively corrupted versions of 𝐰ⱼ, where j ranges over the set of corrupted parties. The problem here is the adaptive nature of the corrupted parties' contributions, because these corrupted parties can modify their contribution according to the honest parties' contributions to bias the final value of 𝐰 = ∑ 𝐰ⱼ. This is critical because it alters the distribution of 𝐰, thereby affecting the scheme's security.
The new DKG protocol that replaced the original one also had critical issues. One of the most critical issues was a potentially wrongly generated key due to wrong management of the subsets S that hold the key contributions. During the DKG protocol, the parties are divided into subsets, and in this issue, the protocol did not check that every subset provided a contribution to the final key, so a corrupted leader could omit its partial public key and lead to a wrong key. Another issue that we found in the DKG allowed corrupted parties to replace honest contributions with wrong ones. The source of this issue was that the protocol sets ∂ᵢ ← {(S, 𝐭[S]) | (S, 𝐭[S]) ∈ pm₄ʲ, j = min(S)}, but the code did not check that j = min(S), allowing the replacement. This issue in particular reveals that omitting any subtle check or the most minimal instruction from a protocol specification can enable considerably harmful attacks.
A critical issue that we found in the audit is the lack of session IDs (SID) in the new DKG, signing, and resharing protocols. After analyzing those protocols, we found that the hashes inside the protocols are computed without inputs that prevent replay attacks. For example, h_commit is computed on the domain-separation tag, the party ID, and the data; the function h_commit_pk is even weaker, as it does not bind the hash to the party ID. This issue is similar to CVE-2022-47930, reported for IO FinNet's tss-lib before version 2.0.0. Common attacks related to this vulnerability are message replays and spoofing. In a message replay, an attacker observes or takes part in a session of a protocol and, given that sessions do not have an identifier, injects a message from that session into another one. Some protocols are severely vulnerable to these kinds of attacks, and an attacker can obtain secret information or bypass protocol security measures. For example, an eavesdropper can spy on a session of a protocol and extract a successful ZK proof for a certain witness. Given that the protocol has no knowledge of the session, the eavesdropper can send the same message to a future session using the same proof, even if the eavesdropper does not know the witness, which will make the proof pass. The message spoofing attack is quite similar, but in this case, the attacker takes a successful message from another party and makes it pass as its own. The solution for this kind of issue is to compute a session ID as
sid = H(“dilithium-threshold-sid-v1” ∥ PublicKey ∥ ParticipantList ∥ (t, n) ∥ AttemptNonce).
This SID must be attached to each challenge message, commitment hash, transcript, and signed message. SIDs are often ignored given that the protocol does not do any meaningful computation on them; however, they are there as an important protective measure.
Now, there is a series of findings regarding zeroization of sensitive values. Zeroization means that once a sensitive value has been used, you should zeroize that variable so that the sensitive information is not stored temporarily or permanently in the hardware. Leaving this information stored in memory or on disk allows an attacker to retrieve sensitive information during or after the execution of the protocol.
Many people think that when a variable goes out of scope, the memory is "deleted" and the information stored there is no longer accessible, but this is not true. When a variable goes out of scope, it is no longer accessible by your program, but in this case, the allocator does not "delete" the data. Instead, the allocator marks this memory fragment as free for the process to use later. But the key here is: it only marks the memory as free; it does not delete it. The same situation occurs in programming languages that use garbage collectors. There, the runtime may preserve sensitive information in memory, later mark it as free, and even then, this reclamation does not mean overwriting. In more severe cases, sensitive information used during the protocol may be written to disk, and it may remain there after the protocol ends. In those situations, an adversary with privileged access (e.g., root access to the system) may perform a memory dump (a purely software-oriented access), obtain physical access to the memory to access that information, or even read disk sectors during memory swapping.
I found another critical error related to state cleaning and the reuse of randomness across signing attempts. The code had the function DilithiumSignProtocol::reset defined as follows:
Notice that this function does not set the variable round1_seed to a fresh value. This variable contains the seed for the randomness in Round 1. Hence, a subsequent signing attempt using the same instance would use the same seed. This part is critical because Threshold ML-DSA assumes that the sampling of 𝐫ᵢ is done using a specific distribution; however, fixing the seed will cause subsequent values of 𝐫ᵢ to be the same. This value is used to compute 𝐳ᵢ = c · 𝐬ᵢᵖᵃʳᵗ + 𝐫ᵢ. If a malicious party observes two responses from the same party computed with the same 𝐫ᵢ under different challenges, it can compute
𝐳ᵢ⁽¹⁾ − 𝐳ᵢ⁽²⁾ = (c₁ − c₂) · 𝐬ᵢᵖᵃʳᵗ,
and get information about the secret share. To solve this error, the recommendation is to provide a new seed when reset is called, or to return an error when reset is called after the first call to poke in the protocol (the poke function advances the protocol state machine by one step). Quantus Network's team opted to remove reset entirely.
Another issue I considered with high severity is the potential misuse of the API by the end user, even though this can be prevented by the API design itself. In the audited code, I found that the secret key is defined as follows:
Notice that SecretKey derives ZeroizeOnDrop, which zeroizes the memory once a secret key variable is dropped. This is completely fine and necessary because it is a mechanism to zeroize sensitive values automatically and avoid the issues that we discussed before. However, notice that bytes is a public field in the struct. Hence, a user of the library can write something like:
This is possible as [u8; SECRETKEYBYTES] implements the Copy trait. However, the problem is that sk_bytes is of type [u8; SECRETKEYBYTES], which does not implement ZeroizeOnDrop. Hence, the variable sk_bytes is vulnerable to non-zeroization issues. Now the question is: can we prevent the API user from creating a non-zeroized copy of the secret key bytes? The answer is no. Even if we make bytes a private field, we need to expose a function that gives us access to the bytes, because at some point, we need to read them to create a signature. At that point, a reckless user can clone every element in bytes and create their own array. What is the solution here? The alternative is to make the end user struggle a little bit if they really want to clone the bytes array, instead of allowing them to make an easy copy. For example, one may consider creating a struct called SensitiveBytes that enforces move semantics and automatic zeroization, as follows:
In this way, the API user has no direct access to the inner (non-zeroized) array. If the bytes are needed, the user can use the expose_bytes function. It is important to note that this approach protects against accidental leakage but not against intentional copying of the inner bytes.
Quantus Network fixed this issue by making the bytes field private, so a line such as let sk_bytes = sk.bytes; no longer compiles for users of the library:
The bytes are still accessible through the function SecretKey::to_bytes, which returns a copy of the inner array with type [u8; SECRETKEYBYTES]. Hence, as discussed above, zeroizing that copy remains the responsibility of the caller.
Other informational issues include misleading comments, lack of documentation, magic numbers, debug comments, dead code, and multiple implementations of the same functionality. As we mentioned before, high-quality code and documentation can improve system security by helping developers gain clarity about the code. Moreover, good documentation and code quality improve the audit. I found myself struggling with certain magic numbers and misleading documentation during the audit. In the most severe cases, this may undermine the understanding of the code and draw attention to false positives or false negatives.
Challenges During the Audit
The main challenges of this audit were the quality of implicit and explicit information given to a code reviewer, which includes documentation of the code and code style. I identified the following challenges during the audit:
Challenges specific to the provided code:
Lack of documentation for some constants: In the parameter specification file (
dilithium/src/params.rs), some of the constants are not documented, and in this case, the auditor needs to map them to the specification.Misleading documentation: Some functions like
rej_uniformanddecomposedon't have the correct variable names in the comments. This adds noise to the audit process, as the auditor needs to determine the right information for the function. Some may argue that this is also part of the audit, but a good audit is also a result of high-quality documentation provided by the client.Re-implementation of widely used and known schemes like SHAKE256: This Rust code has SHAKE256 and SHAKE128 implementations from scratch, following FIPS-202. In this case, it would be better to use a battle-tested implementation like the one in the
sha3crate.Redefinition of constants and parameters: Both the standalone and the threshold ML-DSA implementations define their own parameters with the same values. As an auditor, you need to double-check that both sets of parameters are correct, which strains the audit process. Moreover, both implementations have two different types for the same variable (for example,
Nhas typesusizeandi32).Multiple implementations of NTT: The repository contains two implementations of NTT with different representations, one in the standalone ML-DSA implementation and one in the threshold implementation, and the threshold implementation uses both of them. Hence, the auditor needs to verify them separately.
Challenges from the theoretical perspective: Lattice-based cryptography is an area with complex math and intricate concepts. This adds an additional layer of difficulty to the audit, as the auditor needs to be knowledgeable about the context and aware of the gaps between theory and practice in implementation.
Takeaways
When you are implementing a cryptographic scheme, follow the specification as closely as possible.
If there are checks or instructions confirming some condition in the specification, don't ignore them; include them carefully. These checks may be critical for security.
High-quality documentation and code improve the security of your code indirectly by instructing future maintainers and developers to avoid wrong assumptions and keep good programming practices.
Zeroization is not often mentioned in papers, but it's an important security measure against adversaries snooping on memory during or after the protocol execution.
If your code is an API for third-party consumption, use the programming language as a guardrail for misuse. In Rust, the type system, the privacy of struct fields, and the traits can help you with this.
If you’re deploying products using advanced cryptography such as post quantum cryptography and multiparty computation, our team reviews these exact systems, so get in touch!
