# Audit of Demox Labs - Token Disbursement Program

- **Client**: Demox Labs
- **Date**: December 2, 2024
- **Tags**: Aleo, Vault

## Introduction

On December 2, 2024, Demox Labs tasked zkSecurity with auditing its Token Disbursement program. The specific code to review was shared via GitHub as a public repository (https://github.com/demox-labs/aleo-standard-programs at commit `a5642c6f7f6150fc0d29dc30732894d31a9a3eeb`). The audit lasted 3 workdays with 1 consultant.

The program was found to be clear, accompanied with thorough tests. A few findings have been reported to the demox-labs team, which are detailed in the following sections.

Note that security audits are a valuable tool for identifying and mitigating security risks, but they are not a guarantee of perfect security. Security is a continuous process, and organizations should always be working to improve their security posture.

### Scope

The scope of the audit is the `token_disbursement.aleo` program.

<div style="page-break-after: always;"></div>

## Overview of Token Disbursement Program

The `token_disbursement.aleo` program is a vault to hold pAleo token and disburse a specific amount of tokens to the recipients after the one-year lock. If a disbursement is not claimed for too long (about one year after the unlock), the disbursement can be canceled, and the token will be transferred to a cold address. As pAleo is a liquid staking token of Aleo, the intrinsic value will continuously increase with the staking rewards. The recipients are able to withdraw the staking rewards before token unlock.

Below is the typical lifecycle of a disbursement (called `Claim` in the program): 

1. **Create**: The `Claim` is created by specifying `claim_id`, the amount of pAleo to lock and recipient address. In the same transaction, the caller will transfer the required pAleo token to the program.
2. **Withdraw Rewards**: The recipient can withdraw the staking reward before unlock time. The receipt calls the `withdraw_rewards` function specifying `claim_id` and the amount of pAleo to withdraw. The program will check that after this withdrawal, the value of remaining pAleo is no less than the initially locked value. The recipient can withdraw many times, but only before the unlock time.
3. **Withdraw Principal**: After the unlock time (about 1 year since mainnet genesis), the recipient can withdraw all the locked pAleo token in the `Claim`.
4. **Cancel**: If a `Claim` is not withdrawn within 1 year after the unlock time, it can be canceled and the pAleo token will be transferred to a fixed cold address.

## Findings

### The Cancel Time Is Not Fixed

- **Severity**: Low
- **Location**: token_disbursement.aleo

**Description**. If a `Claim` is not withdrawn after the cancel time, it can be canceled and the pAleo token will be transferred to a fixed cold address. The cancel time in the program is marked by block height. However, on Aleo the block time is not constant, which can be impacted by the network conditions or future upgrades. This means that the cancel time may fluctuate greatly.

```rust
    // The unlock timestamp for all of the distributions, 9/4/2025
    const UNLOCK_TIMESTAMP: u64 = 1_757_015_686u64;
    // Minimum cancel block height, approximately 2 years after genesis
    const MIN_CANCEL_HEIGHT: u32 = 22_500_000u32;
```

Furthermore, the unlock time is marked by timestamp. In extreme cases, if the block time decreases a lot, the cancel time might become smaller than the unlock time. This will lead to a `Claim` being canceled before withdrawal.

**Impact**. The cancel time is not fixed timestamp. In extreme cases the `Claim` can be canceled before withdrawal.

**Recommendation**. We recommend to use a fixed timestamp as the threshold for cancel time as program has access to the current timestamp via the time oracle.

### The `claim_id` Can Be Reused

- **Severity**: Informational
- **Location**: token_disbursement.aleo

**Description**. A `claim_id` is an `u64` integer specifying the ID of a `Claim`. This ID is intended to be unique and should not have any duplicates. The program stores all the `Claim` in a map with ID as the key. When creating a new `Claim`, it will check if there is the same `claim_id` in the map. However, when a `Claim` is canceled, the program will entirely remove it from the map. Afterward, others can create a new `Claim` using the same `claim_id`.

```rust
    async function finalize_cancel(
      public f0: Future,
      public claim_id: u64,
      public paleo_amount: u128
    ) {
      // Await the transfer completing
      f0.await();

      // Assert that the current timestamp is after the minimum cancel height
      assert(block.height > MIN_CANCEL_HEIGHT);

      // Get the claim
      let claim: Claim = claims.get(claim_id);
      // Assert that the transfer amount is the remaining pAleo
      assert_eq(claim.paleo_amount, paleo_amount);

      // Remove the claim
      claims.remove(claim_id);
    }
```

**Impact**. As `claim_id` is supposed to be a unique representation of a `Claim`. If there are duplicates, it can cause significant confusion for off-chain actors.

**Recommendation**. To prevent duplicated `claim_id` values, it is recommended to mark the Claim as empty within the `cancel` function instead of deleting it. Alternatively, we can prohibit the creation of new `Claim` instances after the unlock time.

---

This report was published on the [zkSecurity Audit Reports](https://reports.zksecurity.xyz) site by [ZK Security](https://www.zksecurity.xyz), a leading security firm specialized in zero-knowledge proofs, MPC, FHE, and advanced cryptography. For the full list of audit reports, see [llms.txt](https://reports.zksecurity.xyz/llms.txt).
