nation-contracts
Cantina Security Report
Organization
- @Crestal
Engagement Type
Cantina Reviews
Period
-
Repositories
N/A
Researchers
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
Inconsistent Decimal Handling in Token Price Calculations
State
Severity
- Severity: Critical
Submitted by
Cryptara
Description
The
Agent
contract performs multiple arithmetic operations for determining token purchase and sale amounts, specifically in the functionscalculateAveragePrice
,buyTokens
, andcalculateSellReturn
. 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
ortokenAmount
token's decimals. Similarly, inbuyTokens
: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
andpaymentAmount
to a common 18-decimal standard during computation and scaling the finaltokenAmount
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
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 theAgent
contract intends to create a Uniswap pair (agentToken
/paymentToken
) with an asset ratio (price) according to the specified amountslpTokenAmount
/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 amountslpTokenAmount
/lpPaymentAmount
are unlikely to be fully utilized, and the expected liquidity tokens are unlikely to be minted.Attack path:
- The adversary buys some agent tokens. Preferably shortly after deployment of the agent to secure a better entry price.
- 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.
- 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.
- The adversary can buy the severely underpriced agent tokens from the LP.
Impact Explanation
High:
- Any
paymentToken
not used byaddLiquidity
due to the asset ratio will be stuck/lost in theAgent
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 haspaymentToken
andagentToken
, which can be permissionlessly bought viabuyTokens
.Recommendation
It is recommended to restrict transfers of the
AgentToken
to/from the deterministic address of the Uniswap pair (agentToken
/paymentToken
) until activated in thecreateLPPosition
method.
This way, no one can preliminarily create the pair and set the price.Crestal Network
Fixed in fb43b57ac0d0c501c265e5b8420ab536835acf1d.
Cantina
Verified.
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 justPRECISION
for short) rather thanCURVE_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 toCURVE_DENOMINATOR * PRECISION
, then an attacker could reach the end, trigger mispricing logic (e.g., artificially lowendPrice
), 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 justPRECISION
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.
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 thecreateLPPosition
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 fromisMarketCapReached
) 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
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 thedistributeTokens
method by reaching the block gas limit in a for-loop over all contributors.Finding Description
The
distributeTokens
method of theAgent
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 thecontributors
storage array) is unbounded, the for-loop might exceed the block gas limit due to the costly transfers.Impact Explanation
High:
DoS of thedistributeTokens
method once the for-loop of agent token transfers exceeds the block gas limit if thecontributors
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 viabuyTokens
using different accounts to inflate thecontributors
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.
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
ofpaymentToken
to theAgent
contract, theisMarketCapReached
method can be manipulated to returntrue
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
andinitialPrice
are configured such that it is economically feasible to donateminMarketCapThreshold
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 usingbalanceOf
to avoid manipulation of theisMarketCapReached
method.Crestal Network
Won't fix.
Cantina
Acknowledged.
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
andsellTokens
(subsequentlycalculateSellReturn
) methods of theAgent
contract rely on the actual balance ofagentToken
in the contract usingbalanceOf
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 donatingagentToken
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 usingbalanceOf
to avoid manipulation of the balance used for price calculation.Crestal Network
Won't fix.
Cantina
Acknowledged.
Incompatible Token Standard Handling in ERC20 Operations
Severity
- Severity: Medium
Submitted by
Cryptara
Description
In the
Agent
contract, ERC20 operations such astransfer
,transferFrom
, andapprove
rely on the assumption that the underlying token strictly adheres to the ERC20 standard, specifically by returning a boolean value. The contract uses theIERC20Metadata
interface, which declares these functions with areturns (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 anyapprove
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 useIERC20
(without assuming thereturns (bool)
signature) and import thesafeTransfer
,safeTransferFrom
, andsafeApprove
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.
Mispricing at Bonding Curve Terminal Due to Incorrect Price Approximation
Severity
- Severity: Medium
Submitted by
Cryptara
Description
In the
P(x)=(aD−x−o+1)⋅P0P(x) = \left( \frac{a}{D - x} - o + 1 \right) \cdot P_0P(x)=(D−xa−o+1)⋅P0Agent
contract, the bonding curve implements a price function that asymptotically approaches infinity as token supply is depleted. Mathematically: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 asinitialPrice * 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)=(35,190,005,7301−32+1)⋅P0=35,190,005,699⋅P0P(D - 1) = \left( \frac{35,190,005,730}{1} - 32 + 1 \right) \cdot P_0 = 35,190,005,699 \cdot P_0P(D−1)=(135,190,005,730−32+1)⋅P0=35,190,005,699⋅P0P(D - 1)
) should be: -
But the contract may return:
P(0)=1000⋅P0P(0) = 1000 \cdot P_0P(0)=1000⋅P0
This results in a mispriced
endPrice
duringcalculateAveragePrice()
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+3⋅Pend4P_{\text{avg}} = \frac{P_{\text{start}} + 3 \cdot P_{\text{end}}}{4}Pavg=4Pstart+3⋅PendIf
P_end
is incorrectly capped, the resultingP_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 favoringendPrice
.Exploitation Scenario
- A buyer targets a moment when the remaining token supply is minimal.
tokenAmount
pushesendBalance = 0
.calculatePrice(0)
returnsinitialPrice * 1000
rather than the mathematically correct terminal value (~$35B).averagePrice
is vastly under-calculated.- The buyer receives tokens at a deep discount.
- 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 of1 * PRECISION
, failing to preventx → 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 likeinitialPrice * 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
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 theAgent
contract'sdistributeTokens
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 anERC1967Proxy
contract which refers to theAgentStaking
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.
Inadequate Access Control Safeguards for Admin Role
Severity
- Severity: Low
Submitted by
Cryptara
Description
In the
AgentFactory
contract, access control is implemented using OpenZeppelin'sAccessControl
. However, the contract does not useAccessControlDefaultAdminRules
, a newer and safer base contract that prevents accidental or malicious revocation of theDEFAULT_ADMIN_ROLE
. This presents a critical governance and upgradeability risk: if all holders of theDEFAULT_ADMIN_ROLE
are removed, no further roles — includingAgentRoles.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 ofAgentRoles.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 ofAccessControl
. This newer contract introduces safeguards that ensure at least one account always retains theDEFAULT_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 adoptAccessControlDefaultAdminRules
for immediate resilience against role mismanagement.Crestal Network
Fixed in 58a5cccab6ce720e0e1a9506397ebf594b3f8b3c.
Cantina
Verified.
Infinite Reward Accrual After Reward Pool Depletion
Severity
- Severity: Low
Submitted by
Cryptara
Description
In the
AgentStaking
contract, thegetPendingReward()
function continues to return a reward value for users even after the fullinitialRewardAmount
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 whenclaimReward()
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 againstinitialRewardAmount
. Alternatively, returnmin(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.
Ineffective role segregation in Agent contract
State
- Acknowledged
Severity
- Severity: Low
≈
Likelihood: Low×
Impact: Low Submitted by
Mario Poneder
Description
In the
initialize
method ofAgent
contract both theADMIN_ROLE
as well as theAGENT_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
Internal Function Naming Convention
Severity
- Severity: Informational
Submitted by
Cryptara
Description
In the
Agent
,AgentStaking
, andAgentFactory
contracts, internal functions such ascreateLPPosition
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.
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 of30
and a 6-decimal token like USDC. However, this is incorrect given the actualoffset
value in the code is32
, 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
, not30
. - 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
Precision Loss in Reward Accounting
State
- Acknowledged
Severity
- Severity: Informational
Submitted by
Cryptara
Description
In the
AgentStaking
contract, therewardPerToken()
andgetPendingReward()
calculations are affected by a mismatch in precision between therewardRate
and thetotalStaked
variable. Specifically,rewardRate
operates with high precision (e.g., scaled by1e18
or more), buttotalStaked
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 initialrewardPerToken()
call returns0
, and the computedgetPendingReward()
under-reports the expected value. This discrepancy is due to rounding during division caused by insufficient scaling intotalStaked
, 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
, anduserRewards
inPRECISION
units or higher — typically using a1e18
or even1e36
scaling factor. Update all relevant math inrewardPerToken()
,getPendingReward()
, and any state mutation logic to use the same precision base. Only convert down to token-decimals when performing the final reward payout inclaim()
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.
Incorrect Documentation of Bonding Curve Formula and Initial Price
Severity
- Severity: Informational
Submitted by
Cryptara
Description
In the
Agent
contract, the comment above therequire(_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)=(aD−x−o+1)⋅P0P(x) = \left( \frac{a}{D - x} - o + 1 \right) \cdot P_0P(x)=(D−xa−o+1)⋅P0Where:
P(x)
is the price at supplyx
a
is theCURVE_NUMERATOR
D
is theCURVE_DENOMINATOR
o
is theCURVE_OFFSET
P_0
is theinitialPrice
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 returnsinitialPrice
. 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
P(0)=(aD−o+1)⋅P0P(0) = \left( \frac{a}{D} - o + 1 \right) \cdot P_0P(0)=(Da−o+1)⋅P0x = 0
, then:Substituting actual constants from the contract:
P(0)=(35,190,005,7301,073,000,191−32+1)⋅P0≈(32.795899−32+1)⋅P0=1.795899⋅P0P(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,730−32+1)⋅P0≈(32.795899−32+1)⋅P0=1.795899⋅P0So the theoretical initial price is
1.795899 * initialPrice
, but in Solidity, due to fixed-point arithmetic and integer rounding, this is truncated to1 * 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's32
) - 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 to1 * 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 asinitialPrice
, 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.
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
andendPrice
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
P(x)=(aD−x−o+1)⋅P0P(x) = \left( \frac{a}{D - x} - o + 1 \right) \cdot P_0P(x)=(D−xa−o+1)⋅P0x₀
to the final token positionx₁
. The bonding curve implemented in the contract follows the function:To compute the total cost for a transaction, the accurate form is the definite integral:
paymentAmount=∫x0x1P(x) dx=P0⋅[a⋅ln(D−x0D−x1)−(o−1)(x1−x0)]\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⋅[a⋅ln(D−x1D−x0)−(o−1)(x1−x0)]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
tokensOut=x1−x0\text{tokensOut} = x_1 - x_0tokensOut=x1−x0x₁
such that the area matches the user's funds. This is a numerical inverse problem wherex₀
is known (tokens sold so far), andx₁
(tokens sold after the trade) is computed by solving the integral equation for a givenpaymentAmount
. The number of tokens received is then: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 andx
as token supply is also crucial. In this context,x
always refers to the amount of tokens sold. When integratingP(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 targetpaymentAmount
, using the known integral form of the curve. The number of tokens to issue can then be calculated asx₁ - 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 atx ≈ D - 1
whencalculatePrice(0)
is called, and avoid hardcoded multipliers likeinitialPrice * 1000
. Otherwise, users may be able to exploit average price underestimations near the terminal end of the curve.Crestal Network
Acknowledged
Cantina
Acknowledged
Incorrect endBalance Direction in Sell Price Calculation
Severity
- Severity: Informational
Submitted by
Cryptara
Description
In the
Agent
contract, the current implementation of thecalculateAveragePrice()
function calculates theendBalance
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.
Missing Directional Price Check in calculateAveragePrice
Severity
- Severity: Informational
Submitted by
Cryptara
Description
Within the
Agent
contract, thecalculateAveragePrice()
function estimates the average price of a token trade by calculatingstartPrice
andendPrice
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 tostartPrice
. Conversely, for sells, where tokens are added back to the contract (supply increases), price must decrease, andendPrice
should be less than or equal tostartPrice
.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
, incorrectendBalance
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.
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 base1e18
. 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.
Inconsistent currentPrice Tracking via Average Instead of Terminal Spot Price
Severity
- Severity: Informational
Submitted by
Cryptara
Description
In the
Agent
contract, thecurrentPrice
variable is updated at the end of bothbuyToken
andsellToken
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), thecurrentPrice
becomes discontinuous across opposite trade directions. That is, a buy followed by a sell of the same size does not restore the samecurrentPrice
, 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 oncurrentPrice
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 thecurrentPrice
which serves as the real-time market reference.Crestal Network
Fixed in d5f4514eca44507e094f3d95227581eed19439e2
Cantina
Verified.
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 thetreasury
address as well as the associatedtradingFeePercentage
. However, any changes only apply to new agents created viacreateAgent
and are not propagated to existing agents.Recommendation
It is recommended that the
Agent
contract always reads the currenttreasury
address and the associatedtradingFeePercentage
(if desired) from the factory contract.Crestal Network
Fixed in 1699d54d805bc71a3947e9555e526f5b123ca551.
Cantina
Verified.
Unnecessary payment token enabled check in withdrawFees method
Severity
- Severity: Informational
Submitted by
Mario Poneder
Description
The
withdrawFees
method of theAgentFactory
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.
Unused roles in Agent contract
State
- Acknowledged
Severity
- Severity: Informational
Submitted by
Mario Poneder
Description
The roles
AGENT_CREATOR_ROLE
andAGENT_CONTRACT_ROLE
are assigned in theinitialize
method of theAgent
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.