Building on Avalanche? Ava Labs can cover up to 75% of your audit, and we take 10% off our quote. Avalanche audits: up to 75% covered. See how

Work & reportsReport

Sweat EconomyJars Refactor Security Review

Guvenkaya reviewed the refactored Sweat Jar smart contract, including claim, withdraw, restake, score and product functionality.

Sweat Economy logo

Date of engagement: 9th December 2024 - 18th December 2024

  • Timur GüvenkayaPrincipal advisor

Engagement Dashboard

Audit results

Guvenkaya conducted a security assessment of the Sweat Jars smart contract refactor. During this engagement, 6 findings were reported: 1 High, 2 Medium, 1 Low and 2 Informational severity. The Sweat Foundation team has fixed all the issues.

Engagement record

Client
The Sweat Foundation Ltd
System
Sweat Jar contract on NEAR
Review period
9th December 2024 - 18th December 2024
Reviewed baseline
ba110ce01f9a104e7d533b24bafe57956976f2a9
Paths in scope
17
Principal advisor
Timur Güvenkaya

Timeline

  1. Review start9th December 2024
  2. Draft report19th December 2024
  3. Final report10th January 2025

Findings by severity

  • Critical 0
  • High 1
  • Medium 2
  • Low 1
  • Informational 2

Finding status at final report

6Raised
6Fixed

Remediation complexity

5Simple
1Moderate

Impact

1Severe
2Moderate
3Negligible

Likelihood

4Possible
2Rare

Advisory and Client

The advisory

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 is an organization behind Sweat Economy, an innovative project at the intersection of fitness and crypto. It motivates users to stay active by converting their steps into SWEAT Token. This approach promotes health and fitness and works as an entry point to crypto for many users.

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. 17 paths are in scope. Each path links to the file at the reviewed commit.

PathReview depthScope note
contract/src/lib.rsDeepLib
contract/src/internal.rsDeepInternal
contract/src/ft_receiver.rsDeepFT Receiver
contract/src/ft_interface.rsDeepFT Interface
contract/src/event.rsDeepEvent
contract/src/assert.rsDeepAssert
contract/src/claimDeepClaim Functionality
contract/src/withdrawDeepWithdraw Functionality
contract/src/restakeDeepRestake Functionality
contract/src/scoreDeepScore Functionality
contract/src/productDeepProduct Functionality
contract/src/productDeepPenalty Functionality
contract/src/productDeepMigration Functionality
contract/src/productDeepJar Functionality
contract/src/productDeepFee Functionality
contract/src/productDeepCommon Functionality
model/srcDeepModel

Out of scope

The audit will include, but is not limited to, reviewing the code for security vulnerabilities, coding practices, and architecture. The audit does not include a review of the dependencies.

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, 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

ImpactWhat it means here
SevereExploitation could result in critical loss or compromise, such as full system control, substantial financial loss, or severe reputational damage.
ModerateExploitation may lead to limited data loss, partial compromise, moderate financial impact, or noticeable degradation of services.
NegligibleExploitation has minimal impact, such as minor data exposure without significant consequences or slight inconvenience without substantial disruption.

Likelihood

LikelihoodWhat it means here
LikelyThe vulnerability is easily discoverable and not overly complex to exploit.
PossibleThe vulnerability presents some challenges either in discovery or in the complexity of the attack.
RareThe 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 EaseObviousConcealedHidden
ComplexPossibleRareRare
ModerateLikelyPossibleRare
StraightforwardLikelyPossiblePossible

Severity

SeverityWhat it means here
CriticalAssigned to vulnerabilities with severe impact and a likely likelihood of exploitation.
HighFor vulnerabilities with either severe impact but only a possible likelihood, or moderate impact with a likely likelihood.
MediumUsed for vulnerabilities with severe impact but a rare likelihood, moderate impact with a possible likelihood, or negligible impact with a likely likelihood.
LowFor vulnerabilities with moderate impact and rare likelihood, or negligible impact with a possible likelihood.
InformationalThe lowest severity rating, typically for vulnerabilities with negligible impact and a rare likelihood of exploitation.
Likelihood \ ImpactSevereModerateNegligible
LikelyCriticalHighMedium
PossibleHighMediumLow
RareMediumLowInformational

Remediation complexity

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

Remediation complexityWhat it means here
SimplePatches or fixes are readily available and easily implemented.
ModerateRequires some time and resources to remediate, but well within the capabilities of most organizations.
DifficultRemediation 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.

StatusWhat it means here
Not FixedIndicates 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.
FixedThis 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.
AcknowledgedThis 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.
ScheduledThis 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

6All findings
0 Critical
1 High
2 Medium
1 Low
2 Informational

Findings register

FindingImpactLikelihoodSeverityRemediation complexityStatus
GUV-1: Potential DoS of Batch OperationsSeverePossibleHighModerateFixed
GUV-2: Missing Race Condition LockModeratePossibleMediumSimpleFixed
GUV-3: Missing Score Product Key Enforcement During CreationModeratePossibleMediumSimpleFixed
GUV-4: Suboptimal Rounding DirectionNegligiblePossibleLowSimpleFixed
GUV-5: Outdated Gas MeasurementsNegligibleRareInformationalSimpleFixed
GUV-6: Redundant State AccessNegligibleRareInformationalSimpleFixed

Detailed Findings

GUV-1: Potential DoS of Batch Operations

Severity
High
Impact
Severe
Likelihood
Possible
Remediation Complexity
Moderate
Status
Fixed

Description

Before a user can create a deposit to a jar, the amount is validated against minimum and maximum caps.

jars:assert_cap:contract/src/product/model/v1.rs
pub(crate) fn assert_cap(&self, amount: TokenAmount) {    if self.cap.min > amount || amount > self.cap.max {        env::panic_str(&format!(            "Total amount is out of product bounds: [{}..{}]",            self.cap.min, self.cap.max        ));    }}

After discussion with the team, we discovered that the minimum cap in production is 1 SWEAT. This minimum deposit amount is too low, allowing an attacker to create numerous deposits for any user. This can trigger a gas limit failure in methods like withdraw_all, claim_total, and restake_all, preventing users from performing these operations.

The attack requires approximately $30 in SWEAT and 4,500 deposits. The vulnerability affects all batch operations because the system iterates over all products-even if a user has other products with higher minimum deposits, operations like restake_all, claim_total, and withdraw_all will fail due to the product with excessive deposits.

Recommendation

Consider raising the minimum deposit amount to make the attack economically unviable.

Remediation

The Sweat Foundation team has fixed the issue by throttling requests on the backend side, setting keys for all products, add monitoring for jars' creation and stop issuing tickets for suspicious users

GUV-2: Missing Race Condition Lock

Severity
Medium
Impact
Moderate
Likelihood
Possible
Remediation Complexity
Simple
Status
Fixed
Fix commit
1b0c87287cd553c7c210acafbfc3c1d3bd63e131

Description

The restake method is missing an is_pending_withdraw lock on jars. This lock prevents race conditions before the callback arrives after a cross-contract call. When a jar has is_pending_withdraw set to true, the operation is either skipped or the smart contract returns an error.

jars:assert_not_locked:contract/src/assert.rs
pub(crate) fn assert_not_locked(jar: &Jar) {    require!(!jar.is_pending_withdraw, "Another operation on this Jar is in progress");}

This issue could lead to a scenario where a user's deposit is recorded with a timestamp from the past inside of the cache instead of the actual update time. As a result, interest may begin accumulating earlier than intended.

Possible Scenario

  • User deregisters their account on the token contract
  • Calls claim_total, which sets the cache to current timestamp
  • When the callback arrives, it fails due to the unregistered account and rolls back the cache to the previous timestamp while retaining the new deposit.

Recommendation

Consider implementing the is_pending_withdraw check in the restake method.

Remediation

The Sweat Foundation team has fixed the issue by adding the race condition lock in this commit: 1b0c87287cd553c7c210acafbfc3c1d3bd63e131

GUV-3: Missing Score Product Key Enforcement During Creation

Severity
Medium
Impact
Moderate
Likelihood
Possible
Remediation Complexity
Simple
Status
Fixed
Fix commit
ba110ce01f9a104e7d533b24bafe57956976f2a9

Description

Score-based products are premium products that require a signature. However, during product creation through the register_product method, the public key requirement is not enforced. This allows users to register score-based products without a public key, enabling deposits without any signature verification.

jars:register_product:contract/src/product/api.rs
fn register_product(&mut self, command: ProductDto) {                self.assert_manager();                assert_one_yocto();                 assert!(self.products.get(&command.id).is_none(), "Product already exists");                 let product: Product = command.into();                 product.assert_fee_amount();                 self.products.insert(&product.id, &product);                 emit(EventKind::RegisterProduct(product));            }

Recommendation

Consider enforcing public key validation in register_product when registering score-based products.

Remediation

The Sweat Foundation team has fixed the issue by asseting the public key existence on score based products in this commit:

ba110ce01f9a104e7d533b24bafe57956976f2a9

GUV-4: Suboptimal Rounding Direction

Severity
Low
Impact
Negligible
Likelihood
Possible
Remediation Complexity
Simple
Status
Fixed
Fix commit
9bd95fc264d2dd286082f7d704270accef54dcca

Description

In the restake_all method's withdrawal fee calculation, the withdrawal fee is rounded down. This rounding behavior results in a cumulative loss of withdrawal fees for the protocol, though the impact is minimal.

jars:restake_all:contract/src/restake/api/api.rs
if withdrawal_amount > 0 {                        let withdrawal_fee = total_fee * withdrawal_amount / total_mature_balance;                        request.withdrawal = WithdrawalDto {                            amount: withdrawal_amount,                            fee: withdrawal_fee,                        };                         self.transfer_remainder(request, event)

Recommendation

To prevent cumulative losses and potential system exploitation, fees should always be rounded against the user. This can be achieved by using div_ceil to round up the withdrawal_fee.

Remediation

The Sweat Foundation team has fixed the issue by utilizing div_ceil in this commit: 9bd95fc264d2dd286082f7d704270accef54dcca

GUV-5: Outdated Gas Measurements

Severity
Informational
Impact
Negligible
Likelihood
Rare
Remediation Complexity
Simple
Status
Fixed

Description

After a recent refactor, the gas measurements for the restake_all operation have become outdated. This could lead to incorrect assumptions about gas consumption.

jars:measure_restake_all:integration-tests/src/measure/restake_all.rs
async fn measure_restake_all() -> Result<()> {                set_integration_logs_enabled(false);                 let product = RegisterProductCommand::Locked5Minutes60000Percents;                let mut context = prepare_contract(None, [product]).await?;                let alice = context.alice().await?;                 for _ in 0..200 {                    add_jar(&context, &alice, product, 10_000).await?;                }                 context.fast_forward_minutes(6).await?;                 context.sweat_jar().claim_total(None).with_user(&alice).await?;                 let gas = context                    .sweat_jar()                    .restake_all(product.get().id, None)                    .with_user(&alice)                    .result()                    .await?                    .total_gas_burnt;                dbg!(pretty_gas_string(gas));                 //   1  jar -  6 TGas 225 GGas total:  6225437862976                // 100 jars - 50 TGas 709 GGas total: 50709431315947                // 200 jars - 86 TGas 607 GGas total: 86607517267105...}

Recommendation

Consider re-running the measurements and updating comments

Remediation

The Sweat Foundation team has fixed the issue by re-running the measurements.

GUV-6: Redundant State Access

Severity
Informational
Impact
Negligible
Likelihood
Rare
Remediation Complexity
Simple
Status
Fixed
Fix commit
d409a301b5900353245d68f113aac27925b52a07

Description

The code contains a redundant call to get_or_create_account_mut.

jars:measure_restake_all:integration-tests/src/measure/restake_all.rs
let account = self.get_or_create_account_mut(&account_id);                   if signature.is_some() {                      account.nonce += 1;                  }                   if matches!(product.terms, Terms::ScoreBased(_)) {                      account.try_set_timezone(ticket.timezone);                  }                   let account = self.get_or_create_account_mut(&account_id);                   account.deposit(product_id, amount, None);                   emit(Deposit((product_id.clone(), amount.into())));

Recommendation

Remove the second get_or_create_account_mut call since the account reference is already available.

Remediation

The Sweat Foundation team has fixed the issue by removing the redundant call in this commit: d409a301b5900353245d68f113aac27925b52a07

The web version and the PDF (18 pages) are generated from the same source. The report as first delivered is kept in the public reports archive.

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