Summary

Type: DeFi
Timeline: From 2026-02-23 → To 2026-02-24
Languages: Cairo

Findings
Total issues: 6 (1 resolved)
Critical: 0 (0 resolved) · High: 0 (0 resolved) · Medium: 0 (0 resolved) · Low: 2 (1 resolved)

Notes & Additional Information
4 notes raised (0 resolved)

Scope

OpenZeppelin audited the argentlabs/yearly-approval-contract repository at commit 9a0b710.

In scope were the following files:

 src
└── contract
    └── yearly_allowance.cairo

System Overview

This security audit evaluated the yearly_allowance contract, a Starknet Cairo module that enforces recurring ERC-20 spending limits over fixed 365-day periods. The contract is designed for recurring payment flows where users pre-authorize spending while retaining an explicit on-chain cap per interval, even when token-level approvals are broad.

The audited contract file contains two logical flows: allowance lifecycle management and operator-driven spend execution. These mechanics are expected to be reused in subscription-like payment systems, making correctness of interval accounting, access control, and transfer behavior essential for downstream integrations.

Allowance Lifecycle

Each user has a configuration with max_usage_per_interval, interval_start_timestamp, and spent_amount. The lifecycle is driven by setup_allowance and disable_allowance.

The flow operates as follows:

  1. The user grants ERC-20 approval to the yearly_allowance contract.
  2. The user configures a per-interval cap and a first interval start timestamp.
  3. Spending usage is tracked against the current effective interval and is reset when a new 365-day interval begins.
  4. The user may disable the on-contract configuration, which clears the stored allowance parameters.

The current implementation intentionally keeps an initial payment outside the contract flow. If a user wants an immediate first payment, it must be performed through a separate ERC-20 transfer, which is not capped by the allowance logic and is not counted in spent_amount.

Spend Execution and Administration

This flow governs how the configured allowance is consumed and where collected funds are sent. The spend_from_allowance function is restricted to the configured operator, validates interval status and remaining quota, and then performs an ERC-20 transfer_from from the user to the collector_address.

The yearly_allowance contract records the interval start used for the spend, enabling off-chain accounting of period-based usage. Administrative controls are handled by an owner that can update the operator and collector_address. The implementation emits dedicated events for user setup/disable, operator updates, collector updates, and allowance spends.

Security Model and Trust Assumptions

The yearly_allowance contract is a policy-enforcement layer over ERC-20 approvals, not a custody system. Its security depends on strict role separation, correct interval arithmetic, and trustworthy behavior from both privileged actors and the external token contract.

The model assumes that the owner and operator keys are managed by trusted entities with appropriate operational controls. The operator is constrained by user-defined yearly limits, but the owner can reconfigure operator and collector_address, so a compromise of the owner role can redirect future collections. The model also assumes token compatibility with transfer_from semantics and standard success/revert behavior.

Time logic relies on block timestamps. As with most timestamp-based systems, minor sequencer-controlled drift is an accepted assumption and should be considered in monitoring tolerances.

Finally, on-contract allowance state is independent of ERC-20 allowance state. If a user revokes token approval without calling disable_allowance, the contract configuration remains active and later spend attempts fail at token transfer execution rather than at configuration checks. Integrators should account for this behavior in UX and monitoring pipelines. In addition, any optional standalone ERC-20 transfer used as an initial payment is out of scope for on-contract yearly limit enforcement.

The model further assumes that deployed tokens are ERC-20 compliant and expose transfer_from. Users understand the operational implications of the disable flow and maintain balances sufficient to satisfy operator-initiated spends permitted by their configured limits. Additionally, the users are assumed to understand that reconfiguration via setup_allowance resets the tracked spent_amount to 0.

The yearly_allowance contract uses fixed 365-day years and does not account for leap years.

Privileged Roles

Privileged behavior in this system is role-driven and enforced on-chain through function-level checks:

  • Owner can call update_operator and update_collector_address, and controls ownership transitions through the inherited ownable flow.
  • Operator can call spend_from_allowance to trigger transfers within each user's configured interval limit.
  • Collector receives transfers triggered by the operator through spend_from_allowance.
  • User can call setup_allowance and disable_allowance for their own allowance configuration at any time, and controls whether token pulls can succeed via ERC-20 approval settings.

Low Severity

Missing Validation of token_address in Constructor

The constructor of the yearly_allowance.cairo contract accepts the token_address and assigns it directly to storage without performing any sanity checks or validation.

The contract is designed to manage annual allowances by interacting with an external ERC-20 token. However, if the address provided during deployment does not correspond to a valid contract, or refers to a contract that does not implement the transfer_from function, subsequent attempts to spend the allowance will fail. While the transaction would revert at execution time, failing to validate this parameter during deployment can lead to the permanent misconfiguration of the contract, necessitating a redeployment.

Consider implementing a validation check within the constructor to ensure that the token_address is a valid contract and, where possible, supports the required interface. It is highly recommended to verify that the target address is not the zero address and behaves as an ERC-20 token to prevent unnecessary deployment of unusable contracts.

Update: Acknowledged, not resolved. The team stated:

We are accepting this risk, since the worst-case outcome is a contract redeployment due to misconfiguration. We have integration tests and QA checks in place before shipping. This does not affect the safety of users setting up yearly approvals.

Allowance Race Condition

If a user attempts to modify an existing allowance, an operator can monitor the mempool and execute a transfer_from transaction right before the update is processed. By doing so, the operator can spend the initial allowance and subsequently spend the new allowance once the update transaction is confirmed. This results in the operator spending more tokens than the user intended at any single point in time.

Therefore, any transition of the allowance from a non-zero value to another non-zero value is vulnerable to front-running. This behavior is also present in some ERC-20 implementations and can lead to an unexpected displacement of funds.

Consider implementing a way to increase or decrease the allowance when the user wants to set the approval from a non-zero value to an other one. Furthermore, consider documenting this risk to ensure that users are aware of it, and encouraging users to first set the allowance to zero via disable_allowance before setting it to a new value if they want to be protected from this behavior.

Update: Resolved in pull request #3 at commit b3202ae. The team stated:

We have updated the documentation to explicitly call out this risk and raise awareness of the allowance update behavior.

Notes & Additional Information

Missing Documentation

Throughout the codebase, multiple instances of missing documentation were identified:

Consider addressing the above instances of missing documentation to improve code readability and maintainability.

Update: Acknowledged, not resolved. The team stated:

We are not planning code or documentation changes. The referenced functions are simple/trivial (straightforward getters and parameter names are self-explanatory), so we do not believe additional inline documentation is necessary from a security or correctness standpoint.

Function Visibility Overly Permissive

The panic_with function with public visibility could be limited to private.

To better convey the intended use of the function, consider changing its function's visibility to be only as permissive as required.

Update: Acknowledged, not resolved. The team stated:

We are not planning a code change because panic_with is an internal implementation detail and is not exposed in the contract ABI, so it is not externally callable in practice.

Shadowing of Interval Start Timestamp Variable

In the get_interval_start function, the variable returned by the match statements StartedCurrentInterval and StartedFreshInterval is named interval_start_timestamp.

This identifier is identical to the storage member config.interval_start_timestamp used as the input for the allowance_config.entry(user).interval_start_timestamp.read() function call. While Cairo allows for variable shadowing, using the same name for a derived value and a raw storage value within the same scope can lead to confusion. Furthermore, this practice increases the risk of developer error during future upgrades or refactoring of the internal logic. Therefore, it is advisable to rename the variables returned by the matching clauses to distinguish them from the underlying storage elements.

Consider using a more descriptive name to improve code clarity and maintainability.

Update: Acknowledged, not resolved. The team stated:

We agree this is a valid code clarity / maintainability concern and appreciate the note. We are not making changes at this time, but we will consider it in future work.

Redundant Match Arms in get_current_interval_spent_amount Function

In the get_current_interval_spent_amount function of the yearly_allowance.cairo contract, the match statement explicitly lists multiple patterns that result in a return value of 0. Specifically, the patterns StartedFreshInterval(_), Disabled, and NotStarted(_) all trigger the same logic as a default case. While explicit matching can sometimes provide clarity, in this instance, it leads to unnecessary verbosity and increased maintenance overhead if the underlying Enum is updated. Therefore, the code can be simplified by consolidating these branches.

Consider using a wildcard pattern (_ => 0) to handle all cases where the current interval has not yet started or is not properly set up. This approach improves code readability and ensures the function remains concise.

Update: Acknowledged, not resolved. The team stated:

The exhaustive match arms are intentional. Even if more verbose, we prefer being explicit here because it forces conscious handling of new enum variants if the enum is updated, rather than silently falling through a wildcard branch.

Conclusion

The yearly_allowance contract implements a 365-day recurring allowance model with clear core mechanics: user-configured limits, operator-triggered spending, and interval-based reset logic. The overall design is coherent, and the tested paths cover the main operational flows.

During the audit, no critical- or high-severity issues were identified. However, several opportunities to improve code clarity, consistency, and efficiency were noted. These do not break the core security model, but they do affect transparency, monitoring, and integration safety. Overall, the codebase was found to be well structured and clearly documented, which significantly enhances auditability and supports straightforward adoption and integration.

The Ready team is commended for their responsiveness and transparency throughout the review process. Their willingness to provide in-depth technical explanations, clarify architectural and design decisions, and share comprehensive documentation greatly facilitated the assessment and reflects a strong commitment to delivering secure infrastructure.

Appendix

Issue Classification

OpenZeppelin classifies smart contract vulnerabilities on a 5-level scale:

  • Critical
  • High
  • Medium
  • Low
  • Note/Information

Critical Severity

This classification is applied when the issue’s impact is catastrophic, threatening extensive damage to the client's reputation and/or causing severe financial loss to the client or users. The likelihood of exploitation can be high, warranting a swift response. Critical issues typically involve significant risks such as the permanent loss or locking of a large volume of users' sensitive assets or the failure of core system functionalities without viable mitigations. These issues demand immediate attention due to their potential to compromise system integrity or user trust significantly.

High Severity

These issues are characterized by the potential to substantially impact the client’s reputation and/or result in considerable financial losses. The likelihood of exploitation is significant, warranting a swift response. Such issues might include temporary loss or locking of a significant number of users' sensitive assets or disruptions to critical system functionalities, albeit with potential, yet limited, mitigations available. The emphasis is on the significant but not always catastrophic effects on system operation or asset security, necessitating prompt and effective remediation.

Medium Severity

Issues classified as being of medium severity can lead to a noticeable negative impact on the client's reputation and/or moderate financial losses. Such issues, if left unattended, have a moderate likelihood of being exploited or may cause unwanted side effects in the system. These issues are typically confined to a smaller subset of users' sensitive assets or might involve deviations from the specified system design that, while not directly financial in nature, compromise system integrity or user experience. The focus here is on issues that pose a real but contained risk, warranting timely attention to prevent escalation.

Low Severity

Low-severity issues are those that have a low impact on the client's operations and/or reputation. These issues may represent minor risks or inefficiencies to the client's specific business model. They are identified as areas for improvement that, while not urgent, could enhance the security and quality of the codebase if addressed.

Notes & Additional Information Severity

This category is reserved for issues that, despite having a minimal impact, are still important to resolve. Addressing these issues contributes to the overall security posture and code quality improvement but does not require immediate action. It reflects a commitment to maintaining high standards and continuous improvement, even in areas that do not pose immediate risks.