Canonical page: <https://www.guvenkaya.co/reports/sweat-token>

[Work & reports](https://www.guvenkaya.co/work) Report

# Sweat Economy SWEAT NEP-141 Token Security Review

Guvenkaya reviewed the SWEAT NEP-141 token contract on NEAR, including deferred minting, storage management, migration, access control and pause behavior.

[View PDF on GitHub](https://github.com/Guvenkaya/public-reports/blob/master/The-Sweat-Foundation-Ltd-SWEAT-NEP-141-Token-Smart-Contract-Security-Review.pdf)

![Sweat Economy logo](https://www.guvenkaya.co/reports/assets/clients/sweat.svg)

Date of engagement: 29th May 2026 - 5th June 2026

- **Timur Güvenkaya** Principal advisor

## Engagement Dashboard

### Audit results

Guvenkaya conducted a security assessment of the SWEAT NEP-141 token contract on NEAR, including the deferred minting flow, storage management behavior, migration path, access control wiring, pause behavior, and Sweat-specific changes to the forked vendored fungible token implementation.

During this engagement, 11 findings were reported: 1 High, 1 Medium, 7 Low, and 2 Informational severity issues. The High finding concerns storage accounting undercharge risk in the custom **`LookupMapAdapter`**. The Medium finding concerns runtime governance coupling. The Low findings cover deferred minting hardening, pause controls, fee calculation, burn authorization hardening, and batch gas limits. The Informational findings cover missing event emissions for operational observability and an acknowledged deferred-batch overlap edge case.

### Engagement record

- Client

  The Sweat Foundation Ltd

- System

  SWEAT NEP-141 token contract on NEAR

- Review period

  29th May 2026 - 5th June 2026

- Reviewed baseline

  `9b84e71ae05d7faf46826a82fbaf453794f0ef14`

- Paths in scope

  10

- Principal advisor

  Timur Güvenkaya

### Timeline

1. Review start 29th May 2026
2. Draft report 5th June 2026
3. Final report 5th June 2026

### Findings by severity

- Critical **0**
- High **1**
- Medium **1**
- Low **7**
- Informational **2**

### Finding status at final report

**11** Raised

**10** Fixed

**1** Acknowledged

### Remediation complexity

**8** Simple

**3** Moderate

### Impact

**2** High

**7** Moderate

**2** Negligible

### Likelihood

**1** Possible

**10** Rare

## Advisory and Client

The advisory

![Guvenkaya](https://www.guvenkaya.co/logo.svg)

Guvenkaya specializes in security for digital assets and financial infrastructure, from smart contracts and protocols to applications, infrastructure, custody and cryptography.

Our team, including the sitting Heads of Security at Gauntlet and Agora, brings experience from Kraken, OpenZeppelin, Binance, ZKsync, Nillion, Invicti and EY. Across the team: 200+ security reviews, $30B+ in volume secured, 13 published cryptography research papers, and credentials including OSCP, OSWE and CISM.

- Every reviewer has a public bio and is named before you sign. We match expertise to your system, with a principal leading scoping and checking findings.

The client

![The Sweat Foundation Ltd](https://www.guvenkaya.co/reports/assets/clients/sweat.svg)

The Sweat Foundation is the organization behind Sweat Economy, an application that sits at the intersection of fitness and crypto. Users convert their steps into SWEAT Token, and for many of them the app is an accessible entry point into crypto.

SWEAT is a NEP-141 fungible token on NEAR. A backend oracle submits batches of accounts and step counts to the token contract, which prices each batch, credits users through a holding contract and mints the oracle fee.

- Step rewards mint SWEAT to a user for steps the Sweatcoin app has validated.
- Growth Jars let a holder deposit SWEAT to generate earnings and access new rewards.
- Sweat Wallet uses SWEAT as its gas token, so a user pays fees for actions on any supported chain in SWEAT.
- Open-market trading lets a holder sell SWEAT or exchange it for other crypto assets.

## Scope and Review Boundaries

### Review depths

- Deep

  Line-by-line review with explicit attack hypotheses, adversarial reasoning about state transitions, and reproduction attempts for each hypothesis that survived reading.

- Supporting

  The code was read for its effect on a reviewed flow. Behaviour outside that effect was not assessed.

- Targeted

  Entry points were exercised or triaged against the flow's security properties rather than read in full.

This is the code we read, and how closely. Ten source files are in scope: eight of the SWEAT NEP-141 token contract and two vendored files from the forked fungible token implementation. Each path links to the file at the reviewed commit.

| Path                                                                                                                                                                                                                                     | Review depth | Scope note                                               |
| ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------ | -------------------------------------------------------- |
| [`contract/src/api.rs`](https://github.com/Sweat-Foundation/sweat-token/blob/9b84e71ae05d7faf46826a82fbaf453794f0ef14/contract/src/api.rs)                                                                                               | Deep         | SWEAT NEP-141 token contract                             |
| [`contract/src/core.rs`](https://github.com/Sweat-Foundation/sweat-token/blob/9b84e71ae05d7faf46826a82fbaf453794f0ef14/contract/src/core.rs)                                                                                             | Deep         | SWEAT NEP-141 token contract                             |
| [`contract/src/defer.rs`](https://github.com/Sweat-Foundation/sweat-token/blob/9b84e71ae05d7faf46826a82fbaf453794f0ef14/contract/src/defer.rs)                                                                                           | Deep         | SWEAT NEP-141 token contract                             |
| [`contract/src/lib.rs`](https://github.com/Sweat-Foundation/sweat-token/blob/9b84e71ae05d7faf46826a82fbaf453794f0ef14/contract/src/lib.rs)                                                                                               | Deep         | SWEAT NEP-141 token contract                             |
| [`contract/src/math.rs`](https://github.com/Sweat-Foundation/sweat-token/blob/9b84e71ae05d7faf46826a82fbaf453794f0ef14/contract/src/math.rs)                                                                                             | Deep         | SWEAT NEP-141 token contract                             |
| [`contract/src/metadata.rs`](https://github.com/Sweat-Foundation/sweat-token/blob/9b84e71ae05d7faf46826a82fbaf453794f0ef14/contract/src/metadata.rs)                                                                                     | Deep         | SWEAT NEP-141 token contract                             |
| [`contract/src/migration.rs`](https://github.com/Sweat-Foundation/sweat-token/blob/9b84e71ae05d7faf46826a82fbaf453794f0ef14/contract/src/migration.rs)                                                                                   | Deep         | SWEAT NEP-141 token contract                             |
| [`contract/src/storage.rs`](https://github.com/Sweat-Foundation/sweat-token/blob/9b84e71ae05d7faf46826a82fbaf453794f0ef14/contract/src/storage.rs)                                                                                       | Deep         | SWEAT NEP-141 token contract                             |
| [`vendors/near-contract-standards/src/fungible_token/core_impl.rs`](https://github.com/Sweat-Foundation/sweat-token/blob/9b84e71ae05d7faf46826a82fbaf453794f0ef14/vendors/near-contract-standards/src/fungible_token/core_impl.rs)       | Deep         | Vendored, reviewed for Sweat-specific modifications only |
| [`vendors/near-contract-standards/src/fungible_token/storage_impl.rs`](https://github.com/Sweat-Foundation/sweat-token/blob/9b84e71ae05d7faf46826a82fbaf453794f0ef14/vendors/near-contract-standards/src/fungible_token/storage_impl.rs) | Deep         | Vendored, reviewed for Sweat-specific modifications only |

The two vendored files, `core_impl.rs` and `storage_impl.rs`, were reviewed only for the Sweat-specific modifications made in this fork, such as the custom **`LookupMapAdapter`**. The upstream common fungible token library was not reviewed in full.

### Out of scope

The audit focused on the code paths listed in scope and their smart contract security properties. It did not include a full review of off-chain services, oracle infrastructure beyond how oracle data is accepted by the contract, deployment operations, private keys, tests, or external dependencies. The upstream NEAR fungible token standard library was out of scope except for the Sweat-specific changes to the forked vendored files identified above.

## Assessment Framework

Guvenkaya rates findings by likelihood and impact so remediation can be prioritized by technical risk, business context, and the decision the engagement is meant to support.

### Rating chain applied to every finding

1. 01 **Likelihood** Likely, possible or rare
2. 02 **Impact** Severe, high, moderate or negligible
3. 03 **Severity** Derived from the matrix below
4. 04 **Remediation complexity** Simple, moderate or difficult
5. 05 **Status** Not fixed, fixed, acknowledged or scheduled

### Impact

| Impact     | What it means here                                                                                                                                                 |
| ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Severe     | Exploitation could result in critical loss or compromise, such as full system control, substantial financial loss, or severe reputational damage.                  |
| High       | Exploitation could materially affect governance, accounting, availability, or user funds, but does not imply full system compromise under the assessed conditions. |
| Moderate   | Exploitation may lead to limited data loss, partial compromise, moderate financial impact, or noticeable degradation of services.                                  |
| Negligible | Exploitation has minimal impact, such as minor data exposure without significant consequences or slight inconvenience without substantial disruption.              |

### Likelihood

| Likelihood | What it means here                                                                                 |
| ---------- | -------------------------------------------------------------------------------------------------- |
| Likely     | The vulnerability is easily discoverable and not overly complex to exploit.                        |
| Possible   | The vulnerability presents some challenges either in discovery or in the complexity of the attack. |
| Rare       | The vulnerability is either very difficult to discover or complex to exploit, or both.             |

This matrix provides a nuanced view, taking into account both the ease of discovering a vulnerability and the complexity involved in exploiting it.

| Attack Complexity \ Discovery Ease | Obvious  | Concealed | Hidden   |
| ---------------------------------- | -------- | --------- | -------- |
| Complex                            | Possible | Rare      | Rare     |
| Moderate                           | Likely   | Possible  | Rare     |
| Straightforward                    | Likely   | Possible  | Possible |

### Severity

| Severity      | What it means here                                                                                                                                                                                |
| ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Critical      | Assigned to vulnerabilities with severe impact and a likely likelihood of exploitation.                                                                                                           |
| High          | For vulnerabilities with either severe impact but only a possible likelihood, or high impact with a likely or possible likelihood.                                                                |
| Medium        | Used for vulnerabilities with severe impact but a rare likelihood, high impact with a rare likelihood, moderate impact with a possible likelihood, or negligible impact with a likely likelihood. |
| Low           | For vulnerabilities with moderate impact and rare likelihood, or negligible impact with a possible likelihood.                                                                                    |
| Informational | The lowest severity rating, typically for vulnerabilities with negligible impact and a rare likelihood of exploitation.                                                                           |

| Likelihood \ Impact | Severe   | High   | Moderate | Negligible    |
| ------------------- | -------- | ------ | -------- | ------------- |
| Likely              | Critical | High   | High     | Medium        |
| Possible            | High     | High   | Medium   | Low           |
| Rare                | Medium   | Medium | Low      | Informational |

### Remediation complexity

This measures how difficult it is to remediate a finding once it has been identified.

| Remediation complexity | What it means here                                                                                                 |
| ---------------------- | ------------------------------------------------------------------------------------------------------------------ |
| Simple                 | Patches or fixes are readily available and easily implemented.                                                     |
| Moderate               | Requires some time and resources to remediate, but well within the capabilities of most organizations.             |
| Difficult              | Remediation requires significant resources, specialized skills, or substantial changes to systems or architecture. |

### Finding status

This tracks the client's response to each finding and whether remediation has been completed, accepted, or scheduled.

| Status       | What it means here                                                                                                                                                                                                                                                              |
| ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Not Fixed    | Indicates that the vulnerability has been identified but no remedial action has been taken yet. This status is crucial for newly discovered vulnerabilities or those awaiting prioritization.                                                                                   |
| Fixed        | This status is applied when the vulnerability has been successfully remediated. It implies that appropriate measures, such as patching, configuration changes, or architectural modifications, have been implemented to resolve the issue.                                      |
| Acknowledged | This status is used for vulnerabilities that have been recognized, but for various reasons (such as risk acceptance, cost, or other business decisions), have not been fixed. It indicates that the risk posed by the vulnerability is known and has been consciously accepted. |
| Scheduled    | This status indicates that the vulnerability has been acknowledged and a plan is in place to fix it in the future. It signifies that while remediation hasn't yet occurred, the issue has been prioritized and is part of the planned development roadmap.                      |

## Findings Summary

**11** All findings

**0** Critical

**1** High

**1** Medium

**7** Low

**2** Informational

### Findings register

| Finding                                                                                         | Impact     | Likelihood | Severity      | Remediation complexity | Status       |
| ----------------------------------------------------------------------------------------------- | ---------- | ---------- | ------------- | ---------------------- | ------------ |
| [GUV-1: LookupMap adapter can undercharge storage for selected accounts](#guv-1)                | High       | Possible   | High          | Moderate               | Fixed        |
| [GUV-2: Runtime governance was coupled to contract self-call authority](#guv-2)                 | High       | Rare       | Medium        | Moderate               | Fixed        |
| [GUV-3: Denylisted users could still receive deferred mints](#guv-3)                            | Moderate   | Rare       | Low           | Simple                 | Fixed        |
| [GUV-4: Fee calculation could overflow before division](#guv-4)                                 | Moderate   | Rare       | Low           | Simple                 | Fixed        |
| [GUV-5: burn did not require one yoctoNEAR](#guv-5)                                             | Moderate   | Rare       | Low           | Simple                 | Fixed        |
| [GUV-6: Forced storage\_unregister was not covered by token pause](#guv-6)                      | Moderate   | Rare       | Low           | Simple                 | Fixed        |
| [GUV-7: Arbitrary pause keys can bloat storage through third-party pausable dependency](#guv-7) | Moderate   | Rare       | Low           | Moderate               | Fixed        |
| [GUV-8: Deferred minting accepted empty and zero-step batches](#guv-8)                          | Moderate   | Rare       | Low           | Simple                 | Fixed        |
| [GUV-9: Deferred minting had no explicit max batch size](#guv-9)                                | Moderate   | Rare       | Low           | Simple                 | Fixed        |
| [GUV-10: Missing event emissions reduced auditability](#guv-10)                                 | Negligible | Rare       | Informational | Simple                 | Fixed        |
| [GUV-11: Overlapping deferred batches can reuse step ranges after rollback](#guv-11)            | Negligible | Rare       | Informational | Simple                 | Acknowledged |

## Detailed Findings

### GUV-1: LookupMap adapter can undercharge storage for selected accounts

- Severity

  High

- Impact

  High

- Likelihood

  Possible

- Remediation Complexity

  Moderate

- Status

  Fixed

- Fix commit

  `afd398e6e092ab4fbff6d14162b9d0529bc2c6cf`

#### Description

The vendored fungible token implementation used a custom **`LookupMapAdapter`** that could store either **`sha256(AccountId)`** keys or unhashed account ID keys depending on a configured postfix. Accounts matching the unhashed path could consume different storage than the measured 64-byte probe account used for **`account_storage_usage`**.

Because NEP-145 storage deposits are charged from **`account_storage_usage`**, selected account names could be undercharged and storage costs could depend on account naming rather than a single stable key format.

Affected area: LookupMapAdapter

```
pub struct LookupMapAdapter {     inner: LookupMap<LookupMapKey, Balance>,     skip_hashing_postfix: Option<String>, }
```

#### Recommendation

Use a single account-key encoding for all users and measure storage against that exact encoding. Preserve legacy trie keys during migration so existing balances remain addressable.

#### Remediation

The adapter now stores balances under the hashed key variant, while migration reads the old layout and reconstructs the token with `from_prefix`.

Fixed code: hashed key variant

```
pub enum LookupMapKey {     Hash([u8; 32]), }   fn hash_key(account: &AccountId) -> LookupMapKey {     LookupMapKey::Hash(env::sha256_array(account.as_bytes())) }
```

### GUV-2: Runtime governance was coupled to contract self-call authority

- Severity

  Medium

- Impact

  High

- Likelihood

  Rare

- Remediation Complexity

  Moderate

- Status

  Fixed

- Fix commit

  `779e31c2951209c5c7777de160d0b937f9823775`

#### Description

The initial ACL bootstrap treated the contract account as the super-admin by default. This coupled runtime governance with contract self-call or upgrade authority and made it harder to assign operational roles to the intended accounts during initialization or migration.

The issue is medium severity because compromised or misconfigured governance authority can have high operational impact, while exploitation remains rare due to the required authority path.

Affected area: initializer authority

```
#[init] fn new(holding_account_id: AccountId) -> Self {     let mut contract = Self { /* ... */ };     contract.acl_init_super_admin(env::current_account_id());     contract }
```

#### Recommendation

Accept explicit governance accounts during initialization and migration, and initialize ACL roles through an internal unchecked grant path only during trusted setup flows.

#### Remediation

The initializer and migration now accept explicit super-admin, oracle, denylist manager, pause manager, and unpause manager accounts.

Fixed code: explicit ACL bootstrap

```
fn init_acl(     &mut self,     super_admin_account_id: AccountId,     oracle_account_ids: Vec<AccountId>,     denylist_manager_account_ids: Vec<AccountId>,     pause_manager_account_ids: Vec<AccountId>,     unpause_manager_account_ids: Vec<AccountId>, ) {     self.acl_init_super_admin(super_admin_account_id);       for (role, account_ids) in [         (Role::Oracle, oracle_account_ids),         (Role::DenylistManager, denylist_manager_account_ids),         (Role::PauseManager, pause_manager_account_ids),         (Role::UnpauseManager, unpause_manager_account_ids),     ] {         for account_id in account_ids {             self.acl_get_or_init().grant_role_unchecked(role, &account_id);         }     } }
```

### GUV-3: Denylisted users could still receive deferred mints

- Severity

  Low

- Impact

  Moderate

- Likelihood

  Rare

- Remediation Complexity

  Simple

- Status

  Fixed

- Fix commit

  `d51de9307ac4555ba87fa53d9b97d3cbd27d8b56`

#### Description

The deferred minting path computed rewards for every `(account_id, step_count)` entry in `defer_batch` and sent each user amount to the holding contract. A denylisted account could therefore still be credited through the deferred claim ledger even though direct transfer and burn paths rejected restricted users.

Affected area: defer\_batch loop

```
for (account_id, step_count) in steps_batch {     let (amount, fee) = self.calculate_tokens_amount(step_count);     self.steps_since_tge.0 += u64::from(step_count);     accounts_tokens.push((account_id, U128(amount))); }
```

#### Recommendation

Check each batch recipient against the denylist before calculating or recording any mint amount for that recipient.

#### Remediation

The loop now skips restricted accounts before payout calculation, step accounting, or holding-contract payload construction.

Fixed code: denylist skip before accounting

```
for (account_id, step_count) in steps_batch {     if self.is_restricted(&account_id) {         continue;     }       let (amount, fee) = self.calculate_tokens_amount(step_count);     self.steps_since_tge.0 += u64::from(step_count);     accounts_tokens.push((account_id, U128(amount))); }
```

### GUV-4: Fee calculation could overflow before division

- Severity

  Low

- Impact

  Moderate

- Likelihood

  Rare

- Remediation Complexity

  Simple

- Status

  Fixed

- Fix commit

  `d6d481cf5de26b52143dd4ed9d79e77cba24ba69`

#### Description

The payout split calculated the oracle fee as `(value * 5).div_ceil(100)`. For sufficiently large `u128` input, the multiplication could overflow before the division reduced the value.

The issuance formula is not expected to produce values near `u128::MAX` under normal operation, but fee calculation is consensus relevant and should avoid unnecessary overflow patterns.

Affected area: fee calculation

```
let fee = (value * 5).div_ceil(100);
```

#### Recommendation

Rewrite the fee calculation into an equivalent expression that avoids multiplication before division.

#### Remediation

The fee calculation was rewritten to `value.div_ceil(20)`, which is equivalent to a 5 percent rounded-up fee and avoids intermediate multiplication overflow.

Fixed code: overflow-safe fee calculation

```
let fee = value.div_ceil(20); // == value * 5 / 100
```

### GUV-5: burn did not require one yoctoNEAR

- Severity

  Low

- Impact

  Moderate

- Likelihood

  Rare

- Remediation Complexity

  Simple

- Status

  Fixed

- Fix commit

  `ce514f6d533ce35a63a5b2a539052057567d290f`

#### Description

The custom `burn` method mutates token balances but did not require exactly one yoctoNEAR. NEAR token-standard mutating calls typically require one yoctoNEAR to prevent accidental or restricted access-key invocation.

Because `burn` destroys user funds, it should follow the same intent-confirmation pattern used by `ft_transfer`, `ft_transfer_call`, and storage unregister.

Affected area: burn

```
fn burn(&mut self, amount: U128) {     self.assert_not_in_denylist(vec![&env::predecessor_account_id()]);     self.token.internal_withdraw(&env::predecessor_account_id(), amount.0); }
```

#### Recommendation

Mark `burn` as payable and require `assert_one_yocto()` before withdrawing the caller balance.

#### Remediation

The method is now `#[payable]` and calls `assert_one_yocto()` before denylist checks and balance withdrawal.

Fixed code: payable burn with one yoctoNEAR

```
#[payable] fn burn(&mut self, amount: U128) {     self.assert_feature_enabled(Feature::Token);     assert_one_yocto();     self.assert_not_in_denylist(vec![&env::predecessor_account_id()]);       self.token.internal_withdraw(&env::predecessor_account_id(), amount.0); }
```

### GUV-6: Forced storage\_unregister was not covered by token pause

- Severity

  Low

- Impact

  Moderate

- Likelihood

  Rare

- Remediation Complexity

  Simple

- Status

  Fixed

- Fix commit

  `319cb9408423a231069bf7fd300cd5b29c9e31d9`

#### Description

`storage_unregister(force = true)` can burn the caller's token balance while removing their storage registration. Before remediation, token transfers and burns were covered by the `token` pause feature, but forced unregister was not.

During a token pause, users could still execute a state-mutating path that burns balances and changes storage registration.

Affected area: storage\_unregister

```
#[payable] fn storage_unregister(&mut self, force: Option<bool>) -> bool {     self.assert_not_in_denylist(vec![&env::predecessor_account_id()]);     self.token.internal_storage_unregister(force).is_some() }
```

#### Recommendation

Apply the token pause guard to `storage_unregister` so the pause surface covers all user-facing token balance mutation paths.

#### Remediation

`storage_unregister` now calls `assert_feature_enabled(Feature::Token)`, which blocks forced unregister while the token feature is paused.

Fixed code: token pause guard

```
#[payable] fn storage_unregister(&mut self, force: Option<bool>) -> bool {     self.assert_feature_enabled(Feature::Token);     self.assert_not_in_denylist(vec![&env::predecessor_account_id()]);       self.token.internal_storage_unregister(force).is_some() }
```

### GUV-7: Arbitrary pause keys can bloat storage through third-party pausable dependency

- Severity

  Low

- Impact

  Moderate

- Likelihood

  Rare

- Remediation Complexity

  Moderate

- Status

  Fixed

- Fix commit

  `3a6a0bf77f34f4ed9a56d660fb98ee2c4f40d0bd`

#### Description

The third-party `near-plugins` pausable implementation stores paused feature keys as arbitrary strings. If a `PauseManager` role is compromised, the attacker can repeatedly pause large or junk keys and force the contract to persist them in the pause-key set.

The contract only needs a small fixed pause domain, such as token operations, minting, and all features. Allowing arbitrary pause-key storage increases dependency risk and gives a compromised role an avoidable storage-bloat primitive.

Affected dependency behavior

```
fn pa_pause_feature(&mut self, key: String) -> bool {     let mut paused_keys = self.pa_all_paused().unwrap_or_default();     let newly_paused = paused_keys.insert(key.clone());     env::storage_write(         self.pa_storage_key().as_ref(),         borsh::to_vec(&paused_keys).unwrap().as_ref(),     );     true }
```

#### Recommendation

Replace arbitrary-string pause storage with a small internal bitmap and reject unknown pause features.

#### Remediation

The remediation replaces arbitrary-key pause storage with fixed feature bits for token and minting operations while preserving the pause-manager and unpause-manager access model. Pause state is now stored as a `u32` bitmask on contract state, and public pause calls accept only the `Feature` enum values.

Fixed code: feature bitmask pause state

```
pub enum Feature {     Token,     Minting, }   impl Feature {     const fn bit(self) -> u32 {         match self {             Feature::Token => 1 << 0,             Feature::Minting => 1 << 1,         }     } }   fn set_features_paused(&mut self, features: Vec<Feature>, paused: bool) -> bool {     let before = self.paused_features;       for feature in features {         if paused {             self.paused_features |= feature.bit();         } else {             self.paused_features &= !feature.bit();         }     }       self.paused_features != before }
```

### GUV-8: Deferred minting accepted empty and zero-step batches

- Severity

  Low

- Impact

  Moderate

- Likelihood

  Rare

- Remediation Complexity

  Simple

- Status

  Fixed

- Fix commit

  `3921733e72ba2e9b8a9e3cdd5a15c4241724e357`

#### Description

`defer_batch` accepted an empty `steps_batch` and entries where `step_count == 0`. A zero-step entry does not mint value, but its presence indicates inconsistent backend input and can make the batch's accounting assumptions less trustworthy.

For a protocol-controlled oracle flow, a single invalid entry should be treated as a malformed batch rather than silently processed.

Fixed code: batch input validation

```
require!(!steps_batch.is_empty(), "Empty steps batch");   for (account_id, step_count) in steps_batch {     require!(step_count != 0, "Step count must not be zero");       if self.is_restricted(&account_id) {         continue;     } }
```

#### Recommendation

Require a non-empty batch and reject zero-step entries before applying denylist skips or accounting changes.

#### Remediation

The deferred minting path now rejects empty batches and zero-step entries before computing payouts or scheduling holding-contract callback work. Rejected zero-step batches do not advance `steps_since_tge`.

### GUV-9: Deferred minting had no explicit max batch size

- Severity

  Low

- Impact

  Moderate

- Likelihood

  Rare

- Remediation Complexity

  Simple

- Status

  Fixed

- Fix commit

  `b8f927f268af938563e429bfb17d02694875e5ea`

#### Description

`defer_batch` builds a cross-contract call payload and then reserves only a small fixed callback gas amount. Without an explicit `MAX_BATCH_SIZE`, an oversized batch can consume enough gas in the holding-contract call or callback path that the callback does not execute.

If the callback does not execute, tokens are not deposited to the claim contract and the oracle fee is not minted, while local pre-callback accounting can become inconsistent with the intended batch lifecycle.

Affected area: defer\_batch size

```
fn defer_batch(&mut self, steps_batch: Vec<(AccountId, u32)>) -> PromiseOrValue<()> {     // no explicit maximum length check before processing }
```

#### Recommendation

Introduce a protocol-level `MAX_BATCH_SIZE` based on gas testing, reject oversized batches before state mutation, and keep a safety margin for callback execution. Document the maximum as part of the backend contract for oracle submissions.

#### Remediation

The deferred minting entry point now rejects batches above the configured maximum before processing entries or scheduling cross-contract work. The configured maximum is `135` entries. Batches with `136` entries are rejected and leave `steps_since_tge` unchanged.

Fixed code: maximum batch size

```
const MAX_BATCH_SIZE: usize = 135;   require!(     steps_batch.len() <= MAX_BATCH_SIZE,     "Batch size exceeds the maximum allowed" );
```

### GUV-10: Missing event emissions reduced auditability

- Severity

  Informational

- Impact

  Negligible

- Likelihood

  Rare

- Remediation Complexity

  Simple

- Status

  Fixed

- Fix commits

  `73edfc4a7729ca1d4b5757b7a3a9fd30d7365429` `9255cb6f2b00d553ec50f3c5c8cfc6ec1f0a2613`

#### Description

Some state-changing operations did not emit the events that downstream monitors and indexers would expect. `set_restricted` changed the denylist without emitting a project-specific restriction event, and forced `storage_unregister` could burn a remaining token balance without emitting a NEP-141 `ft_burn` event from the wrapper method.

This did not directly break authorization or token accounting, but it reduced observability and made off-chain reconciliation harder.

Affected area: missing events

```
fn set_restricted(&mut self, account_id: &AccountId, is_restricted: bool) {     if is_restricted {         self.denylist.insert(account_id);     } else {         self.denylist.remove(account_id);     } }   fn storage_unregister(&mut self, force: Option<bool>) -> bool {     self.token.internal_storage_unregister(force).is_some() }
```

#### Recommendation

Emit a project-specific NEP-297 style event when restriction status changes, and emit NEP-141 burn events whenever forced unregister burns a token balance.

#### Remediation

The contract now emits `restriction_changed` events for denylist updates and emits `FtBurn` when forced `storage_unregister` burns a balance.

Fixed code: restriction and burn events

```
Event::RestrictionChanged {     account_id,     is_restricted, } .emit();   self.token     .internal_storage_unregister(force)     .inspect(|(account_id, balance)| {         FtBurn {             owner_id: account_id,             amount: (*balance).into(),             memo: None,         }         .emit();     })     .is_some()
```

### GUV-11: Overlapping deferred batches can reuse step ranges after rollback

- Severity

  Informational

- Impact

  Negligible

- Likelihood

  Rare

- Remediation Complexity

  Simple

- Status

  Acknowledged

#### Description

`defer_batch` updates `steps_since_tge` immediately, before the holding-contract promise finishes. Each batch therefore reserves and prices a step range first, then later confirms minting or rolls the range back in the callback.

If a second `defer_batch` is submitted before the previous batch's callback has resolved, an earlier failed holding call can roll back `steps_since_tge` behind a later successful batch. This can make the same step range available again even though the later successful batch already used that range.

Example: overlapping deferred batch rollback

```
Start: steps_since_tge = 0   Batch A reserves range 0..100 and remains pending Batch B reserves range 100..200 and later succeeds Batch A's holding call fails A's callback subtracts 100 steps, moving counter back to 100 Next batch starts from 100 and can reuse range 100..200
```

Since rewards decrease as `steps_since_tge` increases, reusing a range can calculate later rewards too generously compared to the intended monotonically increasing difficulty schedule.

#### Recommendation

If strict on-chain accounting is required, enforce one unresolved `defer_batch` at a time, make `steps_since_tge` monotonic by not rolling it back on failed holding calls, or track pending batch ranges independently so a failed callback cannot roll back behind later successful ranges.

#### Remediation

The issue is acknowledged as an edge case. The current design prioritizes deferred-minting throughput and relies on operational handling of overlapping batches rather than enforcing single-flight batch processing on-chain.

The web version and the [PDF](https://www.guvenkaya.co/reports/pdfs/sweat-token.pdf) (22 pages) are generated from the same source. The [report as first delivered](https://github.com/Guvenkaya/public-reports/blob/master/legacy-design/The-Sweat-Foundation-Ltd-SWEAT-NEP-141-Token-Smart-Contract-Security-Review.pdf) is kept in the public reports archive.

## Table of Contents

- [Engagement Dashboard](#engagement-dashboard)
- [Advisory and Client](#advisory-and-client)
- [Scope and Review Boundaries](#scope-and-review-boundaries)
- [Assessment Framework](#assessment-framework)
- [Findings Summary](#findings-summary)
- [Detailed Findings](#detailed-findings)

## Share

[Share on X](https://twitter.com/intent/tweet?url=https%3A%2F%2Fwww.guvenkaya.co%2Freports%2Fsweat-token\&text=Sweat%20Economy%3A%20SWEAT%20NEP-141%20Token%20Security%20Review) [Share on LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fwww.guvenkaya.co%2Freports%2Fsweat-token) [Share on Threads](https://www.threads.net/intent/post?text=Sweat%20Economy%3A%20SWEAT%20NEP-141%20Token%20Security%20Review%20https%3A%2F%2Fwww.guvenkaya.co%2Freports%2Fsweat-token)

[Discuss a security review](https://www.guvenkaya.co/contact?source=report) [View PDF on GitHub](https://github.com/Guvenkaya/public-reports/blob/master/The-Sweat-Foundation-Ltd-SWEAT-NEP-141-Token-Smart-Contract-Security-Review.pdf)

## Tell us what you need to secure.

Describe your system, main concern, and deadline. We’ll reply with scoping questions and a proposed next step.

[Discuss your scope](https://www.guvenkaya.co/contact?source=report)
