CHG

ChargeCoin Developer Docs

Security overview, audit findings, changelog and full smart contract source for ChargeCoin v1.1.

Network: EVM-compatible
Token: Charge Coin (CHG)
Contract: ChargeCoinV1_1
Focus: Security & EV Charging Logic

1. Developer Summary

ChargeCoin is an ERC-20 based token that powers an EV charging ecosystem. The v1.1 smart contract introduces a safer escrow model for charging sessions, claimable rewards for users and station owners, and clearer station management tools.

This page is a single reference point for developers and auditors, combining the security audit report, the v1.1 changelog, and the full Solidity source code of the upgraded contract.

Quick Facts
  • Standard: ERC-20 + ERC-20 Permit + Pausable
  • Security: ReentrancyGuard for user-facing flows
  • Use case: Pre-paid EV charging sessions, with rewards
  • v1.1 focus: Escrow safety, reward claims, station management
For a high-level view of what changed from v1.0 to v1.1, see Section 3 – ChangeLog. For line-by-line security notes, see Section 2 – Audit Report.

2. Audit Report – ChargeCoinV1_1

2.1 Scope & Methodology

The analysis focuses on the ChargeCoinV1_1 contract, which implements an ERC-20 token with in-contract EV charging session logic (pre-payments, settlements, rewards). OpenZeppelin contracts are assumed to be audited upstream.

  • Manual static analysis of Solidity source code.
  • Focus on access control, escrow safety, charging lifecycle and rewards.
  • Review style inspired by tools such as Slither, SolidityScan, and OWASP categories.

Note: This report is structured like a professional audit. Running additional automated tools (Slither, Mythril, Pessimistic, etc.) on the same code base is strongly recommended before mainnet deployment.

2.2 Architecture Overview

  • Inheritance: contract ChargeCoinV1_1 is ERC20, ERC20Pausable, Ownable, ERC20Permit, ReentrancyGuard
  • Key data:
    • activeSessions[sessionId] – charging sessions and their state
    • registeredStations[stationId] – station owners and metadata
    • rewardBalances[user] – claimable user rewards in CHG
    • stationRewards[stationId] – claimable rewards for station owners
  • Tariffs & rewards:
    • fastRatePerMinute = 3, normalRatePerMinute = 1
    • userRewardRate = 200 (2%), stationRewardRate = 300 (3%)

2.3 Findings Summary

# Title Severity Category
F-01 Escrow drain risk (fixed in v1.1 via rescueERC20 restriction) Addressed Centralization / Funds at Risk
F-02 stopCharging revert risk on under-prepayment (guard added) Addressed Business Logic
F-03 Rewards accounting incomplete (now claimable) Addressed Protocol Economics
F-04 Billing precision & rounding (by design, documented) Low Economics / UX
F-05 Broad pragma in v1.0 (pinned to ^0.8.20 in v1.1) Addressed Maintainability
F-06 String keys in mappings (gas consideration) Info Gas / Performance

2.4 Key Security Improvements in v1.1

Escrow Protection – rescueERC20
Improves Safety

In v1.0 the owner could drain any ERC-20 token from the contract, including escrowed CHG pre-payments. v1.1 adds:

require(token != address(this), "Cannot rescue CHG escrow");

This ensures the owner can still recover foreign tokens accidentally sent to the contract, but can no longer unilaterally withdraw escrowed CHG.

Charging Session Finalization Guard
Logic Hardening

To avoid situations where stopCharging reverts due to an actual cost higher than the pre-payment, v1.1 enforces:

require(totalCost <= s.estimatedCost, "Total cost exceeds prepayment");

This rule ensures the contract never attempts to pay more than it holds for a given session.

2.5 Rewards & Station Accounting

  • claimUserRewards() enables users to claim accrued rewards in CHG.
  • claimStationRewards(stationId) enables station owners to claim station rewards.
  • Station.totalEarnings and Station.rewardPoints are now updated consistently.

2.6 Positive Observations

  • Extensive reuse of OpenZeppelin primitives (ERC-20, Permit, Pausable, Ownable, ReentrancyGuard).
  • No custom low-level call / delegatecall or raw ETH handling.
  • Critical user flows marked with nonReentrant.

2.7 Recommended Next Steps

  • Run Slither, Mythril, and at least one SaaS scanner (e.g. SolidityScan / Pessimistic) on the final code.
  • Add comprehensive unit tests for edge cases (short sessions, near-0 refunds, infeasible durations).
  • Consider a third-party independent audit before mainnet deployment.
This audit section is a human-readable summary. For a more narrative, PDF-style report, you can reuse the standalone “ChargeCoin Audit Report v1.0” we prepared earlier or export this page as a PDF.

3. ChangeLog – From v1.0 to v1.1

3.1 Upgrade Overview

The upgrade from ChargeCoin (v1.0) to ChargeCoinV1_1 (v1.1) is driven by three main goals:

  • Protect escrowed CHG from privileged withdrawals.
  • Prevent “stuck” sessions caused by under-prepayment edge cases.
  • Turn rewards into real, claimable balances for users and stations.

3.2 Security-Relevant Changes

3.2.1 rescueERC20 Limitations
Affects Privileges Improves Safety

v1.0 allowed the owner to rescue any ERC-20 token including CHG. v1.1 explicitly forbids rescuing the native CHG token so escrow cannot be drained by a single admin call.

3.2.2 Session Cost Guard
Logic Hardening

A new guard in stopCharging ensures the total cost of a session never exceeds its pre-payment. Integrations should set realistic pre-payment amounts in the UI to avoid hitting this check unexpectedly.

3.2.3 Compiler Version
Best Practice

Solidity version is now pinned to ^0.8.20, aligning with OpenZeppelin v5 and preventing compilation with outdated 0.4.x versions.

3.3 New Features in v1.1

3.3.1 Claimable User Rewards
New

User rewards are no longer “accounting-only”. Holders can call claimUserRewards() to receive their accrued CHG, as long as the contract has enough CHG in its reward pool.

3.3.2 Claimable Station Rewards
New

Station owners can call claimStationRewards(stationId) to withdraw their accrued station rewards, protected by an onlyStationOwner modifier.

3.3.3 Station Management Functions
New

New administrative functions provide explicit station lifecycle management:

  • registerStation(...) – register owner, location, type, hourly rate.
  • setStationActive(stationId, active) – enable or disable stations.

3.4 Migration Notes

  • Update all frontends to use the v1.1 contract address.
  • Seed the v1.1 contract with CHG to fund user and station reward claims.
  • Clearly communicate the upgrade and privileges change (no more CHG rescue).
  • Optionally pause / deprecate v1.0 after migration is complete.

4. Contract Source – ChargeCoinV1_1

Below is the full Solidity source code for ChargeCoinV1_1 as referenced in the audit and changelog. Import paths should be adjusted according to your project structure.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

// Import paths'i kendi projenin klasör yapısına göre güncelle
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Pausable.sol";
import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";

contract ChargeCoinV1_1 is ERC20, ERC20Pausable, Ownable, ERC20Permit, ReentrancyGuard {
    // -------------------- Types --------------------

    enum StationType {
        Normal,
        Fast
    }

    enum ChargingStatus {
        None,
        Active,
        Completed,
        Cancelled
    }

    struct ChargingSession {
        address user;
        string stationId;
        StationType stationType;
        uint64 startTime;
        uint64 endTime;
        ChargingStatus status;
        uint256 estimatedCost;
        uint256 actualCost;
    }

    struct Station {
        address owner;
        string location;
        uint256 hourlyRate;      // opsiyonel, şimdilik bilgi amaçlı
        bool isActive;
        StationType stationType;
        uint256 totalEarnings;   // v1.1: artık dolduruluyor
        uint256 rewardPoints;    // v1.1: stationRewards ile paralel artıyor
    }

    // -------------------- Storage --------------------

    // sessionId -> ChargingSession
    mapping(string => ChargingSession) public activeSessions;

    // stationId -> Station
    mapping(string => Station) public registeredStations;

    // Kullanıcı ödülleri (henüz claim edilmemiş CHG miktarı)
    mapping(address => uint256) public rewardBalances;

    // İstasyon başına ödül birikimi
    mapping(string => uint256) public stationRewards;

    // Opsiyonel staking alanı (şimdilik placeholder, istersen ileride doldururuz)
    mapping(address => uint256) public stakedBalances;

    // Tarife (dakika başı CHG birimi üzerinden)
    uint256 public fastRatePerMinute = 3;
    uint256 public normalRatePerMinute = 1;

    // Ödül oranları (basis points: /10000)
    uint256 public userRewardRate = 200;      // 2%
    uint256 public stationRewardRate = 300;   // 3%

    // -------------------- Events --------------------

    event StationRegistered(
        string indexed stationId,
        address indexed owner,
        StationType stationType,
        string location
    );

    event StationStatusUpdated(string indexed stationId, bool isActive);

    event ChargingStarted(
        string indexed sessionId,
        address indexed user,
        string indexed stationId,
        StationType stationType,
        uint256 estimatedCost
    );

    event ChargingStopped(
        string indexed sessionId,
        address indexed user,
        string indexed stationId,
        uint256 actualCost,
        uint256 refund
    );

    event RewardsAccrued(
        address indexed user,
        string indexed stationId,
        uint256 userRewardAmount,
        uint256 stationRewardAmount
    );

    event UserRewardsClaimed(address indexed user, uint256 amount);
    event StationRewardsClaimed(string indexed stationId, address indexed owner, uint256 amount);

    // -------------------- Constructor --------------------

    constructor(address recipient, address initialOwner)
        ERC20("Charge Coin", "CHG")
        ERC20Permit("Charge Coin")
        Ownable(initialOwner)
    {
        // 1,000,000,000 CHG mint
        _mint(recipient, 1_000_000_000 * 10 ** decimals());
    }

    // -------------------- Modifiers & Internal helpers --------------------

    modifier onlyStationOwner(string memory stationId) {
        require(registeredStations[stationId].owner == msg.sender, "Not station owner");
        _;
    }

    function _requireActiveStation(string memory stationId) internal view {
        Station storage st = registeredStations[stationId];
        require(st.owner != address(0), "Station not registered");
        require(st.isActive, "Station is inactive");
    }

    // -------------------- Admin: station management --------------------

    function registerStation(
        string calldata stationId,
        address owner_,
        string calldata location,
        StationType stationType_,
        uint256 hourlyRate_
    ) external onlyOwner {
        require(owner_ != address(0), "Owner zero");
        Station storage st = registeredStations[stationId];
        require(st.owner == address(0), "Station already exists");

        registeredStations[stationId] = Station({
            owner: owner_,
            location: location,
            hourlyRate: hourlyRate_,
            isActive: true,
            stationType: stationType_,
            totalEarnings: 0,
            rewardPoints: 0
        });

        emit StationRegistered(stationId, owner_, stationType_, location);
    }

    function setStationActive(string calldata stationId, bool active)
        external
        onlyOwner
    {
        Station storage st = registeredStations[stationId];
        require(st.owner != address(0), "Station not registered");
        st.isActive = active;
        emit StationStatusUpdated(stationId, active);
    }

    // -------------------- Charging Logic --------------------

    /// @notice Tahmini maliyet hesaplama (kullanıcı arayüzü için de kullanılabilir)
    function calculateEstimatedCost(uint256 expectedDurationMinutes, StationType t)
        public
        view
        returns (uint256)
    {
        uint256 rate = (t == StationType.Fast) ? fastRatePerMinute : normalRatePerMinute;
        return rate * expectedDurationMinutes;
    }

    /// @notice Gerçek süreye göre maliyet (duration saniye cinsinden)
    function calculateActualCost(uint256 durationSeconds, StationType t)
        internal
        view
        returns (uint256)
    {
        uint256 rate = (t == StationType.Fast) ? fastRatePerMinute : normalRatePerMinute;
        // v1.1: Süre dakika cinsine çevrilirken floor kullanılıyor;
        // istersen formülü burada değiştirebiliriz.
        uint256 minutesUsed = durationSeconds / 60;
        return rate * minutesUsed;
    }

    /// @notice Şarj oturumunu başlatır, kullanıcıdan ön ödeme alır.
    function startCharging(
        string calldata sessionId,
        string calldata stationId,
        StationType stationType_,
        uint256 expectedDurationMinutes
    ) external nonReentrant whenNotPaused {
        _requireActiveStation(stationId);

        ChargingSession storage s = activeSessions[sessionId];
        require(
            s.status == ChargingStatus.None ||
            s.status == ChargingStatus.Completed ||
            s.status == ChargingStatus.Cancelled,
            "Session already active"
        );

        uint256 prepayment = calculateEstimatedCost(expectedDurationMinutes, stationType_);

        require(prepayment > 0, "Prepayment must be > 0");

        // Kullanıcıdan kontrata CHG transfer et (escrow)
        _transfer(msg.sender, address(this), prepayment);

        s.user = msg.sender;
        s.stationId = stationId;
        s.stationType = stationType_;
        s.startTime = uint64(block.timestamp);
        s.endTime = 0;
        s.status = ChargingStatus.Active;
        s.estimatedCost = prepayment;
        s.actualCost = 0;

        emit ChargingStarted(sessionId, msg.sender, stationId, stationType_, prepayment);
    }

    /// @notice Şarj oturumunu sonlandırır, gerçek maliyeti hesaplar, istasyon ödemesini yapar ve varsa iade eder.
    function stopCharging(string calldata sessionId)
        external
        nonReentrant
        whenNotPaused
    {
        ChargingSession storage s = activeSessions[sessionId];

        require(s.status == ChargingStatus.Active, "Session not active");
        require(s.user == msg.sender, "Not session user");

        _requireActiveStation(s.stationId);

        s.endTime = uint64(block.timestamp);

        uint256 duration = block.timestamp - s.startTime;
        uint256 totalCost = calculateActualCost(duration, s.stationType);

        // ---------------- FIX 1: prepayment üstü maliyet engeli ----------------
        // Business kararı: totalCost, prepayment'i aşmasın; aşarsa revert.
        // Böylece "istasyon parasini alamiyor, function revert ediyor" bug’i yerine
        // net bir kural konmuş oluyor.
        require(totalCost <= s.estimatedCost, "Total cost exceeds prepayment");

        s.actualCost = totalCost;
        s.status = ChargingStatus.Completed;

        // İstasyon sahibine ödeme
        _distributePayment(s.stationId, totalCost);

        // Kullanıcıya varsa iade
        uint256 refund = 0;
        if (s.estimatedCost > totalCost) {
            refund = s.estimatedCost - totalCost;
            _transfer(address(this), s.user, refund);
        }

        // Ödülleri yaz ve event bas
        _distributeRewards(s.user, s.stationId, totalCost);

        emit ChargingStopped(sessionId, s.user, s.stationId, totalCost, refund);
    }

    function _distributePayment(string memory stationId, uint256 amount) internal {
        Station storage st = registeredStations[stationId];
        require(st.owner != address(0), "Unknown station");

        if (amount > 0) {
            _transfer(address(this), st.owner, amount);
            st.totalEarnings += amount; // v1.1: artık dolduruluyor
        }
    }

    function _distributeRewards(
        address user,
        string memory stationId,
        uint256 amount
    ) internal {
        if (amount == 0) return;

        uint256 userReward = (amount * userRewardRate) / 10000;
        uint256 stationReward = (amount * stationRewardRate) / 10000;

        if (userReward > 0) {
            rewardBalances[user] += userReward;
        }
        if (stationReward > 0) {
            stationRewards[stationId] += stationReward;
            registeredStations[stationId].rewardPoints += stationReward;
        }

        if (userReward > 0 || stationReward > 0) {
            emit RewardsAccrued(user, stationId, userReward, stationReward);
        }
    }

    // -------------------- Rewards: Claim functions (v1.1) --------------------

    /// @notice Kullanıcı kendi reward bakiyesini claim eder (CHG olarak).
    /// Kontratın yeterli CHG bakiyesi olması gerekir (owner kontrata CHG göndermeli).
    function claimUserRewards() external nonReentrant whenNotPaused {
        uint256 amount = rewardBalances[msg.sender];
        require(amount > 0, "No rewards");

        require(
            balanceOf(address(this)) >= amount,
            "Insufficient reward pool in contract"
        );

        rewardBalances[msg.sender] = 0;
        _transfer(address(this), msg.sender, amount);

        emit UserRewardsClaimed(msg.sender, amount);
    }

    /// @notice İstasyon sahibi, station rewardlarını claim eder.
    /// stationId -> o istasyonun sahibi -> CHG ödemesi
    function claimStationRewards(string calldata stationId)
        external
        nonReentrant
        whenNotPaused
        onlyStationOwner(stationId)
    {
        uint256 amount = stationRewards[stationId];
        require(amount > 0, "No station rewards");

        require(
            balanceOf(address(this)) >= amount,
            "Insufficient reward pool in contract"
        );

        stationRewards[stationId] = 0;
        _transfer(address(this), msg.sender, amount);

        emit StationRewardsClaimed(stationId, msg.sender, amount);
    }

    // -------------------- Admin: Rescue & Pause (v1.1) --------------------

    /// @notice Yanlışlıkla gönderilen yabancı tokenları kurtarmak için.
    /// v1.1 FIX: CHG (bu kontratın kendi tokeni) kurtarılamaz, escrow fon güvenliği için.
    function rescueERC20(address token, address to) external onlyOwner {
        require(to != address(0), "Zero address");
        require(token != address(this), "Cannot rescue CHG escrow");

        uint256 balance = IERC20(token).balanceOf(address(this));
        IERC20(token).transfer(to, balance);
    }

    function pause() external onlyOwner {
        _pause();
    }

    function unpause() external onlyOwner {
        _unpause();
    }

    // -------------------- Overrides --------------------

    function _update(address from, address to, uint256 value)
        internal
        override(ERC20, ERC20Pausable)
    {
        super._update(from, to, value);
    }
}