Crestal Network

nation-contracts

Cantina Security Report

Organization

@Crestal

Engagement Type

Cantina Reviews

Period

-

Repositories

N/A


Findings

Critical Risk

1 findings

1 fixed

0 acknowledged

High Risk

3 findings

3 fixed

0 acknowledged

Medium Risk

5 findings

3 fixed

2 acknowledged

Low Risk

4 findings

3 fixed

1 acknowledged

Informational

12 findings

8 fixed

4 acknowledged


Critical Risk1 finding

  1. Inconsistent Decimal Handling in Token Price Calculations

    Severity

    Severity: Critical

    Submitted by

    Cryptara


    Description

    The Agent contract performs multiple arithmetic operations for determining token purchase and sale amounts, specifically in the functions calculateAveragePrice, buyTokens, and calculateSellReturn. These calculations assume a uniform token decimal precision—implicitly expecting all payment tokens to use 18 decimals. This assumption breaks down when dealing with tokens like USDC (commonly 6 decimals on Ethereum), potentially leading to significant indiscrepancies in the computed values.

    For example, in calculateAveragePrice, the operation:

    uint256 tokenAmount = (inputAmount * PRECISION) / estimatedPrice;

    does not take into account the inputAmount or tokenAmount token's decimals. Similarly, in buyTokens:

    uint256 tokenAmount = (paymentAmount * PRECISION) / avgPrice;

    and in calculateSellReturn, the logic lacks decimal normalization for the involved ERC20 tokens.

    Without adjusting for varying token decimals, token valuations will be inaccurate, creating opportunities for exploitation (e.g., buying underpriced tokens or overpaying) or leading to unexpected contract behavior.

    Impact Explanation

    High, because incorrect decimal handling can lead to severe financial discrepancies. Buyers may receive fewer tokens than expected, and sellers may receive more or less than deserved. This not only affects user trust but could also be exploited for arbitrage or manipulation, especially with tokens of different decimal formats. Additionally, errors in fund calculation could result in long-term fund imbalances within the protocol's treasury or reserve pool.

    Likelihood Explanation

    High, since the contract is designed to support arbitrary ERC20 payment tokens and it is common for popular tokens like USDC or USDT to use 6 decimals. Without explicit decimal normalization, incorrect pricing and transfers are very likely during actual usage.

    Recommendation

    Introduce a decimal normalization mechanism that accounts for the decimals of the payment token and the token being bought or sold. This can be achieved by querying the decimals() function of each ERC20 token and applying an appropriate scaling factor during calculations. Alternatively, enforce that all supported payment tokens must conform to 18 decimals and reject any tokens with different configurations.

    A more flexible and robust design would involve explicitly scaling inputAmount and paymentAmount to a common 18-decimal standard during computation and scaling the final tokenAmount result back to the token’s actual decimal precision. This ensures mathematical correctness and prevents rounding or overflow errors due to mismatched assumptions.

    Crestal Network

    Fixed in 4967a410b10149064b8f0cf4908d87c8de5975c6.

    Cantina

    Verified.

High Risk3 findings

  1. Anyone can preliminarily create an agent's Uniswap pair leading to stuck payment tokens

    Severity

    Severity: High

    Likelihood: Medium

    ×

    Impact: High

    Submitted by

    Mario Poneder


    Summary

    Anyone can buy agent tokens to preliminarily create an agent's Uniswap pair, setting a custom asset ratio (price) and therefore tampering with the agent's intended pair creation and liquidity provision mechanism, leading to stuck payment tokens.

    Finding Description

    The createLPPosition method of the Agent contract intends to create a Uniswap pair (agentToken/paymentToken) with an asset ratio (price) according to the specified amounts lpTokenAmount/lpPaymentAmount.
    However, the method expects the Uniswap pair not to exist yet and subsequently expects the full amounts to be utilized, leading to a price that should approximately match the last valuation of the bonding curve.

    In case the Uniswap pair was externally created beforehand, Uniswap's addLiquidity method will maintain the present asset ratio (price). Consequently, the desired amounts lpTokenAmount/lpPaymentAmount are unlikely to be fully utilized, and the expected liquidity tokens are unlikely to be minted.

    Attack path:

    1. The adversary buys some agent tokens. Preferably shortly after deployment of the agent to secure a better entry price.
    2. The adversary creates the LP with a high agent to payment token ratio to set a low price for agent tokens. Preferably this is done shortly before the agent attempts to create the LP.
    3. The agent attempts to create the LP and add liquidity. Thereby, the agent to payment token ratio will be maintained leading to payment tokens being left over and stuck in the agent contract.
    4. The adversary can buy the severely underpriced agent tokens from the LP.

    Impact Explanation

    High:

    • Any paymentToken not used by addLiquidity due to the asset ratio will be stuck/lost in the Agent contract.
    • The Uniswap pair's price does not match the last valuation according to the bonding curve.
    • Severely underpriced payment tokens can be bought from the Uniswap pair.

    Likelihood Explanation

    Medium:
    Can be done by anyone who has paymentToken and agentToken, which can be permissionlessly bought via buyTokens.

    Recommendation

    It is recommended to restrict transfers of the AgentToken to/from the deterministic address of the Uniswap pair (agentToken/paymentToken) until activated in the createLPPosition method.
    This way, no one can preliminarily create the pair and set the price.

    Crestal Network

    Fixed in fb43b57ac0d0c501c265e5b8420ab536835acf1d.

    Cantina

    Verified.

  2. Incorrect Curve Cap Comparison May Allow Overflow Beyond Curve Terminal

    Severity

    Severity: High

    Submitted by

    Cryptara


    Description

    In the Agent contract, the following check is used to prevent trades from exceeding the maximum supply bound of the bonding curve:

    require(    scaledTokensSoldPercentage + scaledNewTokensPercentage < CURVE_DENOMINATOR * PRECISION,    "Purchase exceeds bonding curve limit");

    This logic is flawed. The bonding curve is designed to terminate at 100% token supply usage, which should be checked using 1 * PRECISION (or just PRECISION for short) rather than CURVE_DENOMINATOR * PRECISION. CURVE_DENOMINATOR is a scaling constant used in the pricing formula but does not represent a true 100% bound in normalized units. Using it in this context falsely enlarges the permitted trade window.

    As a result, the contract may allow purchases that exceed the full curve allocation, resulting in attempted overdraws from the contract's token balance. While such a purchase will often fail due to a transfer or balance check downstream, the check is not tight enough to enforce the curve’s terminal condition at the level of logic enforcement.

    An additional nuance is that the inequality used is <, which prohibits reaching exactly the end of the curve (100%). This inadvertently protects against the exploit scenario described in the bonding curve asymptotic mispricing issue. However, if the condition is ever changed to <= for design reasons, and the limit remains incorrectly set to CURVE_DENOMINATOR * PRECISION, then an attacker could reach the end, trigger mispricing logic (e.g., artificially low endPrice), and underpay for tokens.

    In cases where the contract receives unexpected token donations (e.g., via direct transfer bypassing logic), the balance would be artificially increased, allowing buys that exceed the curve supply constraint without triggering reverts, further exacerbating the issue.

    Impact Explanation

    Medium, because although token transfers will often fail when attempting to exceed the curve bounds, the invalid check opens a window for economic manipulation and logical inconsistencies. In some edge cases (e.g., external token donation or LP delay), attackers may exploit the mispricing near the curve end.

    Likelihood Explanation

    High, as this logic is used in a core buy path and will be executed frequently. The misuse of CURVE_DENOMINATOR as a percentage reference is a subtle but common error, and it introduces persistent incorrect behavior under normal operation.

    Recommendation

    Replace the comparison constant with an explicit cap of 1 * PRECISION (or just PRECISION for short) to accurately represent the full curve limit. This ensures no purchase is allowed once the full curve allocation is reached, enforcing proper economic constraints and protecting against overflow or rounding errors. The inequality direction (< vs <=) should also be reviewed to ensure it matches intended bonding curve semantics at the terminal supply.

    Crestal Network

    Fixed in 05131be89df71ed4a50c0ee59b8b66ba9617bb8c

    Cantina

    Verified.

  3. Insufficient Reserve Enforcement May Block LP Creation

    Severity

    Severity: High

    Submitted by

    Cryptara


    Description

    In the Agent contract, a portion of the token supply is reserved for liquidity pool initialization via the createLPPosition function. This reserve is defined by:

    agentToken.totalSupply() * lpInitialTokenPercentage

    However, the contract currently does not enforce a lower bound on trades to ensure that this amount remains available. As a result, users can continue to purchase tokens until the contract’s balance falls below the required LP reserve, effectively locking the system in an unrecoverable state.

    If this condition is met:

    • Calls to createLPPosition() will revert due to insufficient token balance, rendering LP formation impossible.
    • For agents with automaticLPCreation enabled, this will cause any flag-triggered LP creation (such as from isMarketCapReached) to fail.
    • The system will be permanently stuck without the ability to initialize liquidity, and any future buy will continue reducing the balance unless manually halted.

    This scenario doesn't require malicious intent — it can occur naturally if the last buyer is unaware of the LP requirement and the code allows a purchase that drops below the LP threshold.

    Impact Explanation

    High, because the inability to create the LP position halts the full lifecycle of the agent. Without LP creation, token trading mechanisms break down, and any downstream integrations relying on price discovery or secondary market liquidity will fail.

    Likelihood Explanation

    Medium, since the exact threshold depends on lpInitialTokenPercentage, which may vary by deployment. However, under moderate or high purchase activity, it is plausible that this condition is reached unless explicitly guarded.

    Recommendation

    Introduce a pre-check in the token purchase logic to ensure that no trade is allowed if the resulting token balance would fall below the LP reserve threshold.

    This preserves the LP reserve and guarantees that createLPPosition() can always succeed when triggered. As an added safeguard, consider emitting an event when this limit is near, to alert frontends or off-chain systems that LP initialization is approaching and must be handled with care.

    Crestal Network

    Fixed in a92a9d97c0170a8d034c1fcbe147c6fd57fad285

    Cantina

    Verified.

Medium Risk5 findings

  1. Anyone can cause DoS of the distributeTokens method

    Severity

    Severity: Medium

    Likelihood: Low

    ×

    Impact: High

    Submitted by

    Mario Poneder


    Summary

    Anyone can inflate the agent's contributors array to cause DoS of the distributeTokens method by reaching the block gas limit in a for-loop over all contributors.

    Finding Description

    The distributeTokens method of the Agent contract transfers each share of the agent token distribution to the corresponding contributor using ERC-20 transfers in a for-loop.
    Since the maximum number of contributors (length of the contributors storage array) is unbounded, the for-loop might exceed the block gas limit due to the costly transfers.

    Impact Explanation

    High:
    DoS of the distributeTokens method once the for-loop of agent token transfers exceeds the block gas limit if the contributors array is too large. Consequently, the main use case of the protocol will be dysfunctional and agent tokens become stuck in the contract.

    Likelihood Explanation

    Low:
    Anyone can buy miniscule amounts of agent tokens permissionlessly via buyTokens using different accounts to inflate the contributors storage array.

    Recommendation

    It is recommended to implement a pull pattern where contributors have to manually call a method to claim their corresponding share of the agent token distribution instead of having it automatically transferred to them in the distributeTokens method.

    Crestal Network

    Fixed in 05457a083e2fcfb9f5ac2fadc1cb776854a4ef01.

    Cantina

    Verified.

  2. Donation attack using paymentToken affects intended protocol behavior

    State

    Acknowledged

    Severity

    Severity: Medium

    Likelihood: Low

    ×

    Impact: Medium

    Submitted by

    Mario Poneder


    Summary

    Donation attack using paymentToken leads to a devalued pool price and an inflated contributor distribution amount being transferred to the adversary.

    Finding Description

    By donation of the amount minMarketCapThreshold of paymentToken to the Agent contract, the isMarketCapReached method can be manipulated to return true before any agent tokens are sold:

    function isMarketCapReached() public view returns (bool) {    return paymentToken.balanceOf(address(this)) >= minMarketCapThreshold;}

    This in turn, allows an adversary to trigger LP creation and subsequently disable future buy-ins (be the only buyer) by performing only one small buy-in:

    function buyTokens(uint256 amount) external nonReentrant returns (uint256) {    require(amount > 0, "Amount must be greater than 0");    require(!isLPCreated, "Bonding curve phase ended");    ...    // Create LP if conditions are met    if (automaticLPCreation && !isLPCreated && isMarketCapReached()) {        createLPPosition();    }    ...}

    Afterwards, the remaining amount of agent tokens in the contract will be inflated (way higher than intended) due to cutting ahead of the bonding curve, i.e. not selling as many agent tokens as intended.

    As a consequence:

    • The lpTokenAmount of agent tokens at LP creation will be inflated, leading to a devalued pool price.
    • The contribAmount on token distribution will be inflated and is fully transferred to the adversary (if they manage to be the only buyer using this attack vector).

    Impact Explanation

    Medium:

    • Circumvents permissioned manualCreateLP method, cutting ahead of the bonding curve permissionlessly.
    • Remaining tokens for distribution and LP creation are inflated because only a small amount of agent tokens is sold before LP creation.
    • An adversary can get the whole inflated contributor distribution amount for just a small buy-in.
    • LP price is lower than intended due to inflated agent token amount at LP creation.
    • Generally tampers with expected and intended protocol behavior.

    Likelihood Explanation

    Low:

    • Requires automaticLPCreation to be enabled (default: disabled).
    • Requires that contributorPercentage and initialPrice are configured such that it is economically feasible to donate minMarketCapThreshold of payment tokens instead of regularly buying all agent tokens until the market cap is reached.

    Recommendation

    It is recommended to rely on internal accounting of the paymentToken balance instead of using balanceOf to avoid manipulation of the isMarketCapReached method.

    Crestal Network

    Won't fix.

    Cantina

    Acknowledged.

  3. Donation attack using agentToken shifts the bonding curve

    State

    Acknowledged

    Severity

    Severity: Medium

    Likelihood: Low

    ×

    Impact: Medium

    Submitted by

    Mario Poneder


    Summary

    The bonding curve can be shifted towards lower prices by donation of agentToken to the contract.

    Finding Description

    The buyTokens and sellTokens (subsequently calculateSellReturn) methods of the Agent contract rely on the actual balance of agentToken in the contract using balanceOf to determine the current buy/sell price according to the bonding curve. However, the actual balance and therefore the price calculation can be manipulated by donating agentToken to the contract.

    Impact Explanation

    Medium:
    The bonding curve is shifted, i.e. buy prices as well as sell prices become lower than expected and intended.

    Likelihood Explanation

    Low:
    This can be done by anyone, but it is economically infeasible.

    Recommendation

    It is recommended to rely on internal accounting of the agentToken balance instead of using balanceOf to avoid manipulation of the balance used for price calculation.

    Crestal Network

    Won't fix.

    Cantina

    Acknowledged.

  4. Incompatible Token Standard Handling in ERC20 Operations

    Severity

    Severity: Medium

    Submitted by

    Cryptara


    Description

    In the Agent contract, ERC20 operations such as transfer, transferFrom, and approve rely on the assumption that the underlying token strictly adheres to the ERC20 standard, specifically by returning a boolean value. The contract uses the IERC20Metadata interface, which declares these functions with a returns (bool) signature. However, several widely-used tokens — most notably USDT (Tether) — deviate from the ERC20 standard by not returning a value or by reverting on failure silently, making them incompatible with this implementation.

    Consequently, any interaction with such tokens (e.g., fee claiming, liquidity provisioning via createLPPosition, or any approve operation) would result in a revert. This prevents the creation of an Agent with such tokens and blocks further functionality, limiting interoperability and creating unnecessary friction for users attempting to use popular non-compliant tokens.

    Impact Explanation

    High, because this issue completely blocks the functionality of the contract with non-standard tokens like USDT. Users will be unable to create agents, stake, or claim fees using such tokens, leading to failed interactions and degraded protocol usability. This limitation could also impact protocol adoption if users cannot use tokens they expect to work seamlessly.

    Likelihood Explanation

    Low, as the issue only manifests with a subset of tokens that are known to deviate from the ERC20 standard. Furthermore, the affected tokens can be excluded through the whitelist mechanism, avoiding runtime errors. However, this still relies on proactive filtering and limits flexibility.

    Recommendation

    To improve compatibility and robustness, use OpenZeppelin’s SafeERC20 library for all ERC20 interactions. This library wraps token calls with checks that handle non-standard return behaviors safely. Additionally, refactor the contract interfaces to use IERC20 (without assuming the returns (bool) signature) and import the safeTransfer, safeTransferFrom, and safeApprove functions accordingly.

    This approach ensures safe interaction with both standard and non-standard tokens and minimizes the risk of revert due to non-compliance. Alternatively, implement a custom adapter layer or token wrapper that normalizes token behavior internally, though this adds complexity and is generally not needed if SafeERC20 is used correctly.

    Crestal Network

    Fixed in 73a786b9ccdc775d4546078e2ad95bb2a0bd562f and 7d90fa025d0de3ce905786a69289a9585ae16a2c

    Cantina

    Verified.

  5. Mispricing at Bonding Curve Terminal Due to Incorrect Price Approximation

    Severity

    Severity: Medium

    Submitted by

    Cryptara


    Description

    In the Agent contract, the bonding curve implements a price function that asymptotically approaches infinity as token supply is depleted. Mathematically:

    P(x)=(aDxo+1)P0P(x) = \left( \frac{a}{D - x} - o + 1 \right) \cdot P_0P(x)=(Dxao+1)P0

    Where:

    • x is the number of tokens sold (or the complement of current balance),
    • a = CURVE_NUMERATOR,
    • D = CURVE_DENOMINATOR,
    • o = CURVE_OFFSET,
    • P_0 = initialPrice.

    As x → D, the denominator of the curve approaches zero, and the price diverges toward infinity. However, in the current Solidity implementation, calculatePrice(0) — which represents the case where all tokens are sold (i.e., currentBalance == 0) — returns a capped value such as initialPrice * 1000.

    This approximation introduces significant inaccuracies during high-end curve interactions, especially for large buy operations nearing full supply depletion. While this is meant as a safety fallback to avoid division-by-zero, it results in the following issue:

    • The price at full supply (P(D - 1)) should be:

      P(D1)=(35,190,005,730132+1)P0=35,190,005,699P0P(D - 1) = \left( \frac{35,190,005,730}{1} - 32 + 1 \right) \cdot P_0 = 35,190,005,699 \cdot P_0P(D1)=(135,190,005,73032+1)P0=35,190,005,699P0
    • But the contract may return:

      P(0)=1000P0P(0) = 1000 \cdot P_0P(0)=1000P0

    This results in a mispriced endPrice during calculateAveragePrice() calls for buys close to terminal supply, severely undervaluing tokens and breaking the intended convex price dynamics of the bonding curve.

    Given that the average price formula for buys is:

    Pavg=Pstart+3Pend4P_{\text{avg}} = \frac{P_{\text{start}} + 3 \cdot P_{\text{end}}}{4}Pavg=4Pstart+3Pend

    If P_end is incorrectly capped, the resulting P_avg is significantly lower than expected, allowing users to purchase tokens below their intended cost. The issue is amplified due to the buy-weighted averaging favoring endPrice.

    Exploitation Scenario

    1. A buyer targets a moment when the remaining token supply is minimal.
    2. tokenAmount pushes endBalance = 0.
    3. calculatePrice(0) returns initialPrice * 1000 rather than the mathematically correct terminal value (~$35B).
    4. averagePrice is vastly under-calculated.
    5. The buyer receives tokens at a deep discount.
    6. If LP creation hasn't occurred yet (or is delayed), the user can resell those tokens through the curve or other mechanisms, profiting from the mispricing.

    Mitigations and Friction Points

    • The buyToken function checks:

      scaledTokensSoldPercentage + scaledNewTokensPercentage < CURVE_DENOMINATOR * PRECISION;

      This may not be a sufficient boundary, as it incorrectly uses CURVE_DENOMINATOR instead of 1 * PRECISION, failing to prevent x → D edge conditions.

    • The isMarketCapReached() function typically finalizes the curve by triggering LP creation before this scenario manifests, acting as a strong preventative layer.

    • Additionally, the sellPenalty mechanism discourages arbitrage unless the market deviates by 10% or more, providing an additional disincentive in many practical cases.

    Impact Explanation

    High, because if a user manages to purchase tokens at the terminal end of the bonding curve and receives them at a mispriced average (far below intended value), it could result in extractable economic value and undermine the curve’s integrity. This breaks the expected cost gradient of the curve and could be exploited to drain protocol value under precise timing or adversarial automation.

    Likelihood Explanation

    Low, because several protections are already in place:

    • isMarketCapReached() ensures LP is formed before full depletion.
    • sellPenalty discourages rapid sell-backs.
    • Only a very narrow window near the terminal supply would allow this, and front-running it would be difficult without significant capital and precision.

    Nonetheless, it remains exploitable under edge-case conditions, and the mispricing is systematic.

    Recommendation

    To fix the mispricing at the end of the bonding curve, the logic in calculatePrice(0) should be updated to accurately reflect the theoretical price when all tokens are sold, rather than returning a hardcoded value like initialPrice * 1000. This can be achieved in two ways.

    One option is to simulate the final step of the curve by substituting x = CURVE_DENOMINATOR - 1, which avoids division by zero but still provides a very close approximation to the true terminal price. This keeps the formula aligned with the actual curve shape and ensures accurate pricing:

    if (currentBalance == 0) {    uint256 simulatedX = CURVE_DENOMINATOR - 1;    uint256 rawPrice = (CURVE_NUMERATOR * PRECISION) / (CURVE_DENOMINATOR - simulatedX);    uint256 finalPrice = rawPrice > CURVE_OFFSET * PRECISION        ? rawPrice - (CURVE_OFFSET * PRECISION) + PRECISION        : PRECISION;    return (finalPrice * initialPrice) / PRECISION;}

    Alternatively, for gas optimization and simplicity, the correct multiplier can be precomputed and hardcoded. Since the terminal price converges to:

    P_terminal = (CURVE_NUMERATOR - (CURVE_OFFSET - 1)) * initialPrice           = 35_190_005_699 * initialPrice

    you can directly return:

    if (currentBalance == 0) {    return initialPrice * 35_190_005_699;}

    Both approaches resolve the underpricing issue while preserving the integrity of the bonding curve mechanics.

    Crestal Network

    Fixed in 3ba21b77c05dc7d128cfb52212d02d9e3afc1fce

    Cantina

    Verified.

Low Risk4 findings

  1. Upgradeable AgentStaking implementation contract is used directly without proxy

    Severity

    Severity: Low

    Likelihood: Low

    ×

    Impact: Low

    Submitted by

    Mario Poneder


    Description

    The AgentStaking contract is upgradeable, nevertheless it is directly deployed and initialized in the Agent contract's distributeTokens method.
    However, such implementation contracts are intended to be solely used through proxy contracts instead of being used directly.

    Recommendation

    It is recommended to create a new instance of the AgentStaking contract by deploying an ERC1967Proxy contract which refers to the AgentStaking implementation, instead of deploying and initializing the implementation contract itself.

    Furthermore, it is recommended to prevent the direct initialization of the AgentStaking contract by adding the following constructor:

    constructor() {    _disableInitializers();}

    Crestal Network

    Fixed in 31b916779084aa6655c0fbaa7a16b84e33a57ec7.

    Cantina

    Verified.

  2. Inadequate Access Control Safeguards for Admin Role

    Severity

    Severity: Low

    Submitted by

    Cryptara


    Description

    In the AgentFactory contract, access control is implemented using OpenZeppelin's AccessControl. However, the contract does not use AccessControlDefaultAdminRules, a newer and safer base contract that prevents accidental or malicious revocation of the DEFAULT_ADMIN_ROLE. This presents a critical governance and upgradeability risk: if all holders of the DEFAULT_ADMIN_ROLE are removed, no further roles — including AgentRoles.ADMIN_ROLE — can be granted, revoked, or managed.

    Since DEFAULT_ADMIN_ROLE is the root authority in the AccessControl hierarchy, its loss results in permanent loss of administrative control over all role-based functionality. In this contract, that includes the inability to assign administrative or privileged access to agents, effectively bricking the system's governance.

    Impact Explanation

    High, because revocation of the DEFAULT_ADMIN_ROLE results in total loss of control over access permissions. No new agents could be administered, upgraded, or deployed securely, and all subsequent management of AgentRoles.ADMIN_ROLE becomes impossible. This could lead to a catastrophic failure of the contract's governance framework, rendering it permanently inoperative or locked.

    Likelihood Explanation

    Low, as revoking a role generally requires explicit action, and such a mistake can be caught with proper operational procedures. However, since the role can be renounced or revoked manually or via a bug in permission logic, the risk is non-trivial in practice. This is especially relevant in environments where roles are frequently reassigned or managed by DAO votes or external governance layers.

    Recommendation

    It is recommended to inherit from OpenZeppelin’s AccessControlDefaultAdminRules instead of AccessControl. This newer contract introduces safeguards that ensure at least one account always retains the DEFAULT_ADMIN_ROLE, thereby protecting against scenarios in which no account remains capable of managing roles. Alternatively, implement custom role recovery mechanisms or establish a privileged multisig or timelock mechanism that retains the power to recover lost roles under extreme cases. However, the preferred and simpler option is to adopt AccessControlDefaultAdminRules for immediate resilience against role mismanagement.

    Crestal Network

    Fixed in 58a5cccab6ce720e0e1a9506397ebf594b3f8b3c.

    Cantina

    Verified.

  3. Infinite Reward Accrual After Reward Pool Depletion

    Severity

    Severity: Low

    Submitted by

    Cryptara


    Description

    In the AgentStaking contract, the getPendingReward() function continues to return a reward value for users even after the full initialRewardAmount has already been claimed. This occurs because the function computes rewards based solely on elapsed time and stake amount, without validating whether sufficient tokens remain in the contract.

    As the proof-of-concept shows, a single user can stake, wait until the maximum possible reward accrual, claim the reward (draining the pool), and still see getPendingReward() return a non-zero value indefinitely. Although the contract will revert when claimReward() is called due to insufficient token balance, it continues to report misleading reward data after depletion.

    This is primarily an accounting inconsistency rather than a direct vulnerability. Since the actual transfer of rewards will fail when no tokens remain, it does not pose an exploitation risk, but it does mislead users and could result in failed transactions or erroneous front-end behavior.

    POC

    function testSingleStakerClaimInfinite() public {
            uint256 stakeAmount = 10_000 * 1e18;
            vm.startPrank(user1);        token.approve(address(staking), stakeAmount);        staking.stake(stakeAmount);        vm.stopPrank();
            vm.warp(block.timestamp + 400 days);
            console.log("Pending reward: ", staking.getPendingReward(user1));
            vm.startPrank(user1);        staking.claimReward();        vm.stopPrank();
            // balance        uint256 balance = token.balanceOf(user1);        console.log("User1 balance after claim: ", balance);        console.log("Pending reward: ", staking.getPendingReward(user1));    }

    Output:

    Logs:  Pending reward:  450000000000000000000000  User1 balance:  490000000000000000000000  Pending reward:  450000000000000000000000

    If we now call claimReward the transfer will fail due to insufficient funds.

    Impact Explanation

    Low, because the issue does not enable unauthorized reward claims or incorrect token distribution. It merely causes the system to report stale or unclaimable reward data, which can lead to unnecessary reverts and poor user experience.

    Likelihood Explanation

    Medium, since the issue will naturally occur once the reward pool has been exhausted. It is deterministic and will affect any user attempting to interact with claimReward() post-depletion.

    Recommendation

    Modify getPendingReward() or the internal accounting logic to cap reported rewards based on the remaining token balance or track cumulative distributed rewards against initialRewardAmount. Alternatively, return min(calculatedReward, agentToken.balanceOf(address(this))) to avoid reporting unavailable amounts.

    This change would ensure consistency between reported and claimable values, improve user experience, and prevent unnecessary transaction failures after reward exhaustion.

    Crestal Network

    Fixed in 424d54cb279a5a4c7be88b1a2e2fa5460bdbf4c1

    Cantina

    Verified.

  4. Ineffective role segregation in Agent contract

    State

    Acknowledged

    Severity

    Severity: Low

    Likelihood: Low

    ×

    Impact: Low

    Submitted by

    Mario Poneder


    Description

    In the initialize method of Agent contract both the ADMIN_ROLE as well as the AGENT_WALLET_ROLE are assigned to the same _agentWallet account.
    Therefore, the segregation among these distinct roles is ineffective.

    Recommendation

    Although this role assignment can be changed later on, it is recommended to already assign these roles to different accounts at initialization.

    Crestal Network

    Won't fix.

    Cantina

    Acknowledged.

Informational12 findings

  1. Internal Function Naming Convention

    Severity

    Severity: Informational

    Submitted by

    Cryptara


    Description

    In the Agent, AgentStaking, and AgentFactory contracts, internal functions such as createLPPosition and others are currently named without a leading underscore. While this does not introduce a functional vulnerability, it diverges from widely accepted Solidity naming conventions. By convention, internal and private functions should be prefixed with an underscore (_) to clearly distinguish them from externally callable or public functions.

    This naming convention improves code readability and maintainability, especially in complex systems with deep inheritance or multiple contract interactions. Without consistent naming, the distinction between externally visible and internal-only logic becomes ambiguous, potentially leading to developer confusion or misuse during upgrades or extensions of the system.

    Recommendation

    It is recommended to rename all internal functions in Agent, AgentStaking, and related contracts by prefixing them with an underscore (e.g., _createLPPosition, _handleStake, etc.). This aligns the codebase with Solidity best practices, reinforces clear function visibility intentions, and improves auditability and maintainability across the system.

    Crestal Network

    Fixed in dd493f4f383089d6cc5e1bf0f28b3153d0432dee and c0af5e04e7d42186bf3079f11d6dfe1af5265567

    Cantina

    Verified.

  2. Incorrect Initial Token Price and Scaling Description in Documentation

    State

    Acknowledged

    Severity

    Severity: Informational

    Submitted by

    Cryptara


    Description

    The current documentation (https://docs.google.com/document/d/1G0CuVq_vrjKfQ0hZOvUBl-PNuLplCud3qk_PAQpbuOI/) inaccurately states that the initial token price is $0.0000038, and implies a fixed interpretation of value based on an offset of 30 and a 6-decimal token like USDC. However, this is incorrect given the actual offset value in the code is 32, which adjusts the price scaling. This makes the correct initial price approximately $0.0000017 per token unit — not $0.0000038.

    Moreover, the documentation inaccurately generalizes the price by assuming a static $0.000001 per token. In reality, the initial price must be interpreted as: "1 unit of the payment token (assumed to be 18 decimals) scaled by initialPrice, adjusted by the offset constant." This price will naturally vary depending on the actual decimals of the token involved and the way rounding or truncation occurs, especially for tokens with fewer decimals like USDC.

    Recommendation

    Update the documentation to reflect that:

    • The offset is 32, not 30.
    • The initial token price should not be fixed to $0.000001 or $0.0000038.
    • The correct phrasing is: "The initial price of a token corresponds to 1 unit of the payment token multiplied by initialPrice, normalized with the defined offset."
    • If USDC (6 decimals) were to be used hypothetically, then the visible price per token unit would appear truncated due to decimal mismatch, but this is not representative of how the system internally calculates price or value distribution.

    Ensure that both internal comments and external user-facing documentation reflect this logic accurately to avoid confusion or incorrect expectations from integrators and token holders.

    Crestal Network

    Acknowledged

    Cantina

    Acknowledged

  3. Precision Loss in Reward Accounting

    State

    Acknowledged

    Severity

    Severity: Informational

    Submitted by

    Cryptara


    Description

    In the AgentStaking contract, the rewardPerToken() and getPendingReward() calculations are affected by a mismatch in precision between the rewardRate and the totalStaked variable. Specifically, rewardRate operates with high precision (e.g., scaled by 1e18 or more), but totalStaked is stored without sufficient scaling to preserve precision when dividing.

    As shown in the provided proof-of-concept, even after a large time duration (300 days), the initial rewardPerToken() call returns 0, and the computed getPendingReward() under-reports the expected value. This discrepancy is due to rounding during division caused by insufficient scaling in totalStaked, which strips precision from the reward distribution.

    Since the staking reward distribution is heavily reliant on accurate per-token accounting, truncating this value at the rewardPerToken() level causes persistent under-rewarding of stakers and subtle inaccuracies over time, especially with large durations or high-value stakes.

    POC

    function testRewardPerToken() public {
            uint256 stakeAmount = 10_000 * 1e18;
            vm.startPrank(user1);        token.approve(address(staking), stakeAmount);
            vm.expectEmit(true, false, false, true);        emit Staked(user1, stakeAmount);        staking.stake(stakeAmount);        vm.stopPrank();
            assertEq(staking.totalStaked(), stakeAmount, "Total staked amount incorrect");        assertEq(staking.getStakedAmount(user1), stakeAmount, "User staked amount incorrect");        assertEq(token.balanceOf(user1), 40_000 * 1e18, "User balance after staking incorrect");
            console.log("Reward rate: ", staking.rewardRate());        console.log("Reward per token: ", staking.rewardPerToken());        console.log("Pending reward: ", staking.getPendingReward(user1));
            uint256 time = 300 days;
            // Advance time 1 days        vm.warp(block.timestamp + time);
            console.log("Reward rate: ", staking.rewardRate());        console.log("Reward per token: ", staking.rewardPerToken());        console.log("Reward per token calculated for 1 day: ", (staking.rewardRate() * time) / stakeAmount);        console.log("Reward per token calculated for 1 day (1e18): ", (staking.rewardRate() * 1e18 * time) / stakeAmount);
            // Pending rewards        console.log("Pending reward: ", staking.getPendingReward(user1));
            // Calculated pending rewards with 1e18 precision        uint256 calculatedPendingReward = (staking.rewardRate() * time * stakeAmount) / 1e18 / stakeAmount;        console.log("Calculated pending reward: ", calculatedPendingReward);    }

    Output:

    Logs:  Reward rate:  14269406392694063926940639269406392  Reward per token:  0  Pending reward:  0  Reward rate:  14269406392694063926940639269406392  Reward per token:  36986301369863013698  Reward per token calculated for 1 day:  36986301369863013698  Reward per token calculated for 1 day (1e18):  36986301369863013698630136986301368064  Pending reward:  369863013698630136980000  Calculated pending reward:  369863013698630136986301

    Notice the difference between "Pending reward" and "Calculated pending reward".

    Impact Explanation

    Low, because while this does not break functionality or cause incorrect fund distribution from a security standpoint, it introduces noticeable imprecision in reward calculations. Stakers may be underpaid in minor amounts, which could compound across many users or over long durations.

    Likelihood Explanation

    Medium, because the issue occurs under standard conditions when using highly precise reward rates with unscaled totalStaked.

    Recommendation

    Internally track both rewardPerTokenStored, totalStaked, and userRewards in PRECISION units or higher — typically using a 1e18 or even 1e36 scaling factor. Update all relevant math in rewardPerToken(), getPendingReward(), and any state mutation logic to use the same precision base. Only convert down to token-decimals when performing the final reward payout in claim() or similar functions.

    This ensures that fractional rewards are correctly accumulated and only truncated at the last moment, preserving fairness and mathematical accuracy across reward cycles.

    Crestal Network

    Crestal has stated that the issue may not look valid neither the POC. The have added the modified test to do the checks, and they pass. https://github.com/crestalnetwork/nation-contracts/pull/12/commits/f5a138ca81f1f2101541f705f248727293a755b9

    Cantina

    Acknowledged. The loss in precision is almost negligibly and in the end of the range.

  4. Incorrect Documentation of Bonding Curve Formula and Initial Price

    Severity

    Severity: Informational

    Submitted by

    Cryptara


    Description

    In the Agent contract, the comment above the require(_initialPrice > 0, "Initial price must be positive"); check contains an inaccurate explanation of the bonding curve logic and the initial token price behavior. Specifically, it misrepresents the curve formula, the expected starting price, and the interpretation of price units based on payment token decimals.

    The actual pricing function used in the bonding curve is:

    P(x)=(aDxo+1)P0P(x) = \left( \frac{a}{D - x} - o + 1 \right) \cdot P_0P(x)=(Dxao+1)P0

    Where:

    • P(x) is the price at supply x
    • a is the CURVE_NUMERATOR
    • D is the CURVE_DENOMINATOR
    • o is the CURVE_OFFSET
    • P_0 is the initialPrice

    The function getCurrentPrice() in the contract uses this curve, and includes a condition that, if no tokens have been sold (i.e., when the entire token supply remains in the contract), it simply returns initialPrice. While this is a practical approximation, it deviates slightly from the mathematical output of the curve.

    If we strictly apply the bonding curve formula when x = 0, then:

    P(0)=(aDo+1)P0P(0) = \left( \frac{a}{D} - o + 1 \right) \cdot P_0P(0)=(Dao+1)P0

    Substituting actual constants from the contract:

    P(0)=(35,190,005,7301,073,000,19132+1)P0(32.79589932+1)P0=1.795899P0P(0) = \left( \frac{35{,}190{,}005{,}730}{1{,}073{,}000{,}191} - 32 + 1 \right) \cdot P_0 \approx (32.795899 - 32 + 1) \cdot P_0 = 1.795899 \cdot P_0P(0)=(1,073,000,19135,190,005,73032+1)P0(32.79589932+1)P0=1.795899P0

    So the theoretical initial price is 1.795899 * initialPrice, but in Solidity, due to fixed-point arithmetic and integer rounding, this is truncated to 1 * initialPrice. This introduces a small but consistent discrepancy at the very beginning of the bonding curve.

    Furthermore, the original documentation claims a starting price of $0.0000038, which would only be accurate with:

    • An offset of 30 (which is not the case — it's 32)
    • A specific token like USDC with 6 decimals
    • A misinterpretation of how price scaling and units relate to decimals

    In reality, the system operates in full base units of the payment token, and the correct description is: "The initial token price is equal to initialPrice multiplied by ~1.795899 (adjusted for curve constants), truncated to 1 * initialPrice due to rounding."

    Recommendation

    Update the comment near the require(_initialPrice > 0) check to reflect the true behavior and formula, including:

    • The exact bonding curve formula:
      P(x) = (a / (D - x) - o + 1) * initialPrice

    • The fact that getCurrentPrice() approximates the starting price as initialPrice, but mathematically it would be ~1.795899 * initialPrice, truncated due to integer arithmetic.

    • Clarify that any perceived fiat equivalent (e.g., "$0.0000038") depends entirely on external price feeds and token decimals, and should not be hardcoded or assumed.

    • If relevant, mention that over the full curve range, a small number of tokens (documented as ~1.43) are effectively "lost" due to rounding — a detail that's useful for understanding curve supply dynamics.

    This documentation correction improves clarity for developers and integrators and ensures consistent interpretation of on-chain economic behavior.

    Crestal Network

    Fixed in d8e3edc592f68cafc457ddb47f9200150fe633ed

    Cantina

    Verified.

  5. Inexact Token Pricing via Weighted Average Instead of Curve Integration

    State

    Acknowledged

    Severity

    Severity: Informational

    Submitted by

    Cryptara


    Description

    The Agent contract currently calculates the cost of buying or selling tokens along its bonding curve using a weighted average of the start and end prices. Specifically, for buy operations, it uses the formula:

    averagePrice = (startPrice + endPrice * 3) / 4;

    and for sells:

    averagePrice = (startPrice * 3 + endPrice) / 4;

    While this approach avoids complex arithmetic and is gas-efficient, it only approximates the true area under the bonding curve, which represents the total cost to acquire tokens at variable pricing. This is especially inaccurate near the convex, non-linear regions of the curve — such as close to the start or at the asymptotic end — where price differentials between startPrice and endPrice are substantial.

    In reality, the exact and economically correct method to price variable-rate purchases is to integrate the curve from the initial token position x₀ to the final token position x₁. The bonding curve implemented in the contract follows the function:

    P(x)=(aDxo+1)P0P(x) = \left( \frac{a}{D - x} - o + 1 \right) \cdot P_0P(x)=(Dxao+1)P0

    To compute the total cost for a transaction, the accurate form is the definite integral:

    paymentAmount=x0x1P(x)dx=P0[aln(Dx0Dx1)(o1)(x1x0)]\text{paymentAmount} = \int_{x_0}^{x_1} P(x)\,dx = P_0 \cdot \left[ a \cdot \ln\left( \frac{D - x_0}{D - x_1} \right) - (o - 1)(x_1 - x_0) \right]paymentAmount=x0x1P(x)dx=P0[aln(Dx1Dx0)(o1)(x1x0)]

    When a user initiates a trade and provides a fixed payment value, this total cost corresponds to the area under the curve — and the task becomes finding the correct value of x₁ such that the area matches the user's funds. This is a numerical inverse problem where x₀ is known (tokens sold so far), and x₁ (tokens sold after the trade) is computed by solving the integral equation for a given paymentAmount. The number of tokens received is then:

    tokensOut=x1x0\text{tokensOut} = x_1 - x_0tokensOut=x1x0

    The use of a weighted average instead of this integral means the system is only approximating the value of tokens, which introduces measurable inaccuracies, particularly when the price function has high curvature. While this is tolerable in low-slope regions of the curve, it can significantly misprice trades in edge conditions.

    The issue becomes critical only when combined with incorrect edge behavior, such as the previously reported case of returning a capped value at calculatePrice(0). If the terminal price is underreported, then the weighted average can lead to users purchasing tokens at a much lower cost than economically expected.

    The distinction between x as a price input and x as token supply is also crucial. In this context, x always refers to the amount of tokens sold. When integrating P(x), you are summing up the cost of infinitesimal token slices across a changing price landscape. This is conceptually equivalent to accumulating the total cost of a commodity that becomes more expensive with every additional unit bought. Hence, P(x) provides the spot price at a given sold amount, and integrating it gives the total cost to reach a new supply level.

    Recommendation

    Consider replacing the weighted average approximation with the exact integrated cost formula to ensure accurate pricing across all regions of the bonding curve. This can be done off-chain where computation of logarithmic functions and inverse solving for x₁ can be performed efficiently, and then passed as a parameter to the smart contract with suitable verification.

    Alternatively, if on-chain calculation is preferred, implement a bounded binary search to solve for x₁ given a target paymentAmount, using the known integral form of the curve. The number of tokens to issue can then be calculated as x₁ - x₀. While this may be more computationally expensive, it ensures economically correct behavior, especially in the nonlinear regions near the start and end of the curve.

    If gas efficiency remains a priority and the approximation is to be retained, it is crucial to ensure the endPrice is never underestimated. Specifically, the system must accurately simulate the final curve value at x ≈ D - 1 when calculatePrice(0) is called, and avoid hardcoded multipliers like initialPrice * 1000. Otherwise, users may be able to exploit average price underestimations near the terminal end of the curve.

    Crestal Network

    Acknowledged

    Cantina

    Acknowledged

  6. Incorrect endBalance Direction in Sell Price Calculation

    Severity

    Severity: Informational

    Submitted by

    Cryptara


    Description

    In the Agent contract, the current implementation of the calculateAveragePrice() function calculates the endBalance incorrectly when performing a sell operation. Specifically, the logic:

    endBalance = (tradeAmount >= currentBalance) ? 0 : currentBalance - tradeAmount;

    assumes that token sales decrease the contract's token balance, when in reality, a sell operation increases the token balance, since tokens are being returned to the contract.

    This inversion leads to incorrect price calculations for the endPrice, and by extension, to incorrect averaging logic, especially in price-sensitive areas of the bonding curve. It also makes the weighted average asymmetric between buys and sells.

    The correct behavior should be:

    endBalance = currentBalance + tradeAmount;

    This adjustment correctly reflects the post-sell token balance in the bonding curve and aligns with the economic model where returning tokens lowers the price.

    As a result, the weighted average price formula for sells should also match the structure used for buys:

    return (startPrice + endPrice * 3) / 4;

    This harmonizes the pricing logic between buys and sells by always weighting toward the post-trade price (endPrice). For buys, where prices rise as supply shrinks, this biases toward higher prices. For sells, where prices drop as supply increases, it biases toward lower prices — which is economically intuitive and consistent.

    Updating the logic in this way reduces code duplication, clarifies intent, and ensures that the average price more accurately reflects the actual slope of the curve across the transaction range.

    Crestal Network

    Fixed in 44b0dfe19265b8e73ad241fed41a80e5b933d1ff.

    Cantina

    Verified.

  7. Missing Directional Price Check in calculateAveragePrice

    Severity

    Severity: Informational

    Submitted by

    Cryptara


    Description

    Within the Agent contract, the calculateAveragePrice() function estimates the average price of a token trade by calculating startPrice and endPrice based on token balances before and after a buy or sell operation. However, there is currently no validation to ensure that the direction of price change aligns with the expected behavior of the bonding curve.

    For buys, where tokens are removed from the contract (supply decreases), price must increase — meaning endPrice should always be greater than or equal to startPrice. Conversely, for sells, where tokens are added back to the contract (supply increases), price must decrease, and endPrice should be less than or equal to startPrice.

    Without this check, unexpected bugs or miscalculations — especially near curve edges or in edge-case conditions — could allow inverted price movement, which would break the curve’s economic guarantees and may lead to mispricing.

    For example:

    • During a buy, if endPrice < startPrice, the buyer is effectively "buying backward" on the curve.
    • During a sell, if endPrice > startPrice, the seller earns more per token the more they return — violating the principle of bonding curve deflation.

    These errors are often caused by mis-set startBalance, incorrect endBalance logic, or side-effect rounding issues near boundary values.

    Recommendation

    Introduce an explicit assertion or guard clause in calculateAveragePrice() to validate price directionality:

    • For buys: require that endPrice ≥ startPrice
    • For sells: require that endPrice ≤ startPrice

    If this invariant is violated, revert with a descriptive error indicating that the curve behavior is invalid for the transaction type. This adds a clear contract-level invariant and acts as a protective check against incorrect balance or price assumptions introduced through future code changes, upgrades, or rounding edge cases.

    Crestal Network

    Fixed in d148c2a246d552a73382d66595a0b351a6c231e2.

    Cantina

    Verified.

  8. Incorrect Comment on Sell Penalty Threshold

    Severity

    Severity: Informational

    Submitted by

    Cryptara


    Description

    In the Agent contract, the comment describing the sell penalty threshold incorrectly states a 15% deviation, while the actual threshold enforced in code is 10%, expressed in base 1e18. This mismatch may mislead developers or reviewers about the intended behavior of the slippage tolerance before penalties apply.

    Recommendation

    Update the comment to accurately reflect the implemented 10% threshold (1e17) to maintain consistency between code and documentation.

    Crestal Network

    Fixed in 97c25eb610d8b8bee3d5869d163984b66a26cc38

    Cantina

    Verified.

  9. Inconsistent currentPrice Tracking via Average Instead of Terminal Spot Price

    Severity

    Severity: Informational

    Submitted by

    Cryptara


    Description

    In the Agent contract, the currentPrice variable is updated at the end of both buyToken and sellToken functions. However, the update uses the average price of the trade, not the final price after the transaction completes:

    currentPrice = averagePrice;

    While this reflects the cost of the user’s trade, it does not accurately track the current market price on the bonding curve — which should be the spot price at the post-trade supply level (i.e., the price returned by calculatePrice(newBalance)).

    Additionally, because the weighted average is calculated differently for buys and sells (favoring endPrice more heavily on buys), the currentPrice becomes discontinuous across opposite trade directions. That is, a buy followed by a sell of the same size does not restore the same currentPrice, even if the supply is unchanged, leading to price flickering or misalignment between observed and actual curve prices.

    This behavior breaks the intuition that currentPrice should follow the bonding curve closely and smoothly. It also introduces inconsistencies when external components or UIs rely on currentPrice for spot value estimations or oracle anchoring.

    Recommendation

    Update the currentPrice to reflect the final price after a trade, rather than the average. Specifically, set it as:

    currentPrice = endPrice;

    This aligns currentPrice with the true state of the bonding curve and ensures that it remains continuous across buys and sells. If the average price is still needed for UI or reporting purposes, it can be emitted in events but should not overwrite the currentPrice which serves as the real-time market reference.

    Crestal Network

    Fixed in d5f4514eca44507e094f3d95227581eed19439e2

    Cantina

    Verified.

  10. Treasury and trading fee changes are not propagated to existing agents

    Severity

    Severity: Informational

    Submitted by

    Mario Poneder


    Description

    The AgentFactory contract allows the admin to update the treasury address as well as the associated tradingFeePercentage. However, any changes only apply to new agents created via createAgent and are not propagated to existing agents.

    Recommendation

    It is recommended that the Agent contract always reads the current treasury address and the associated tradingFeePercentage (if desired) from the factory contract.

    Crestal Network

    Fixed in 1699d54d805bc71a3947e9555e526f5b123ca551.

    Cantina

    Verified.

  11. Unnecessary payment token enabled check in withdrawFees method

    Severity

    Severity: Informational

    Submitted by

    Mario Poneder


    Description

    The withdrawFees method of the AgentFactory contract employs the following check:

    require(paymentTokens[token].enabled, "Token not enabled");

    However, this check is not necessary since withdrawFees can only be invoked by the admin who can enable/disable payment tokens anyway. Furthermore, it should also be possible to withdraw fees of previously enabled but currently disabled payment tokens.

    Recommendation

    It is recommended to remove the mentioned paymentTokens[token].enabled check.

    Crestal Network

    Fixed in cdff01485f8efdc052c75c725b006301f71dc75f.

    Cantina

    Verified.

  12. Unused roles in Agent contract

    State

    Acknowledged

    Severity

    Severity: Informational

    Submitted by

    Mario Poneder


    Description

    The roles AGENT_CREATOR_ROLE and AGENT_CONTRACT_ROLE are assigned in the initialize method of the Agent contract.
    However, these roles are never utilized for access control within the contract.

    Recommendation

    It is recommended to either remove these roles or add further documentation about their intended use cases.

    Crestal Network

    Won't fix.

    Cantina

    Acknowledged.