ChargeCoin Smart Contract Audit
Security and risk review for the ChargeCoin (CHG) smart contract.
1. Scope & Overview
This document summarizes a manual security and logic review of the
ChargeCoin smart contract, which implements an ERC-20 token
with additional logic for EV charging sessions, pre-payments, station
rewards and user rewards.
The review focuses on:
- Access control, privilege boundaries and owner capabilities
- Correctness of charging session lifecycle and pre-payment handling
- Security of reward accounting and escrowed balances
- General Solidity best practices and upgrade risks
Off-chain systems (backend, mobile apps, dashboards) are explicitly out of scope.
2. Methodology
The analysis is based on a manual static review of the Solidity source code. The style of the review follows common patterns used by tools and approaches such as:
- Slither-style static analysis (reentrancy, access control, unsafe patterns)
- SolidityScan / Pessimistic / Sherlock AI style token checks
- OWASP-style categories: access control, data validation, trust boundaries, and error handling
Note: In this environment, automated tools (Slither, Mythril, SolidityScan, etc.) were not executed directly. All findings below are produced by careful manual reasoning and are formatted like a professional audit report.
3. Architecture Overview
3.1 Inheritance
contract ChargeCoin is ERC20, ERC20Pausable, Ownable, ERC20Permit, ReentrancyGuard
Key implications:
- ERC20 β standard fungible token functionality.
- ERC20Pausable β ability to pause token transfers.
- Ownable β single privileged owner address.
- ERC20Permit β EIP-2612 gasless approvals.
- ReentrancyGuard β protection for functions marked
nonReentrant.
3.2 State & Storage
Main storage elements:
mapping(string => ChargingSession) public activeSessions;mapping(address => uint256) public stakedBalances;(currently unused)mapping(string => Station) public registeredStations;mapping(address => uint256) public rewardBalances;mapping(string => uint256) public stationRewards;
Constants for pricing and rewards:
fastRatePerMinute = 3;normalRatePerMinute = 1;userRewardRate = 200;(2% in basis points)stationRewardRate = 300;(3% in basis points)
3.3 Key Functions
-
Constructor:
constructor(address recipient, address initialOwner) ERC20("Charge Coin", "CHG") Ownable(initialOwner) ERC20Permit("Charge Coin") { _mint(recipient, 1_000_000_000 * 10 ** decimals()); } startCharging(...)β opens a charging session and takes a pre-payment.stopCharging(...)β closes a session, computes actual cost, and settles payment.calculateEstimatedCost(...)β read-only cost estimate for a planned duration.calculateActualCost(...)β internal calculation for actual duration and station type.distributePayment(...)β internal transfer to station owner.distributeRewards(...)β internal accounting for user and station reward balances.initiateRewardCalculation(...)β currently empty.rescueERC20(address token, address to)β allows owner to withdraw any ERC-20 from the contract.
4. Findings Summary
Severity legend:
- High β critical impact, potential loss of funds.
- Medium β serious logic or UX issue, funds can be stuck or mis-accounted.
- Low β minor issue, gas / precision / non-critical behavior.
- Info β best practices and design notes.
| # | Title | Severity | Category |
|---|---|---|---|
| F-01 | Owner can drain escrowed funds via rescueERC20 |
High | Centralization / Funds at Risk |
| F-02 | stopCharging may revert if actual cost > pre-payment |
Medium | Business Logic / UX |
| F-03 | Rewards and station balances are unclaimable / incomplete | Medium | Business Logic / Incomplete Features |
| F-04 | Billing precision & rounding behavior | Low | Economic / Precision |
| F-05 | Overly broad Solidity pragma version | Low | Maintainability |
| F-06 | Use of string keys in mappings |
Info | Gas / Performance |
5. Detailed Findings
rescueERC20Location
function rescueERC20(address token, address to) external onlyOwner {
require(to != address(0), "Zero");
uint256 balance = IERC20(token).balanceOf(address(this));
IERC20(token).transfer(to, balance);
}
Issue
The contract uses address(this) to hold user pre-payments for charging sessions.
The rescueERC20 function allows the owner to transfer the full balance of any ERC-20
token from the contract to an arbitrary address, including the native CHG token.
Impact
- All escrowed CHG pre-payments and reward balances can be drained by the owner.
- If the owner key is compromised, an attacker can also empty the contract.
- From a user perspective this represents a centralization and potential rug-pull risk.
Recommendation
- Disallow rescuing the native token used for escrow:
require(token != address(this), "Cannot rescue escrowed CHG");
- Limit rescue to foreign tokens accidentally sent to the contract.
- Consider adding a timelock or multisig for all rescue operations.
- Clearly document any remaining privileged behavior in public documentation.
stopCharging may revert if actual cost exceeds pre-paymentIssue
Users prepay for a session using calculateEstimatedCost, and the contract transfers
that amount from the user to address(this). On stopCharging the contract
computes the actual cost based on elapsed time. If the actual cost becomes greater than the
pre-paid amount, the contract may not have enough balance to pay the station owner, causing
an internal transfer to revert.
Impact
stopChargingcan revert and the session cannot be closed.- User pre-payment can effectively be stuck until an external intervention.
- Station and reward accounting for that session never finalizes.
Recommendation
- Add a defensive check such as:
require(totalCost <= session.estimatedCost, "Total cost exceeds prepayment");
- Define a clear business rule for over-usage (e.g. hard cap or post-payment for extra minutes).
- Emit detailed events so off-chain systems can handle exceptional cases gracefully.
Issue
The contract tracks user rewards in rewardBalances and station rewards in
stationRewards, but there is no function to claim or withdraw these balances.
Additionally, stakedBalances, Station.totalEarnings and
Station.rewardPoints exist but are not fully integrated with a public API.
Impact
- Users and stations may see on-chain balances but have no way to realize them.
- This creates a mismatch between expectations and actual behavior.
- Future changes to add claiming logic may introduce new security risks if rushed.
Recommendation
- Either implement safe claim functions for users and stations, or remove unused fields.
- Protect any future management functions (e.g. registering stations, setting rates) with proper access control.
- Document the current status of the reward system clearly until it is fully implemented.
Issue
The contract measures duration in seconds and then converts to minutes using integer division:
duration / 60. Any partial minute is truncated. Depending on the business rules,
this may undercharge short sessions or cause small discrepancies compared to frontend
expectations.
Recommendation
- Define a precise billing unit (seconds, tenths of a minute, etc.).
- Consider formulas like
duration * rate / 60for higher precision if desired. - Align UI display with on-chain rounding rules to avoid confusion.
Issue
The pragma currently allows compilation with any version >= 0.4.16. The contract relies on modern OpenZeppelin contracts which are designed for 0.8.x, so compiling with older versions would be unsafe or impossible.
Recommendation
- Pin to the intended compiler range, e.g.
pragma solidity ^0.8.20;. - Keep a single, explicit compiler version in deployment tooling and CI.
string as mapping keysIssue
Mappings like mapping(string => ChargingSession) and
mapping(string => Station) are convenient for readability, but using
strings as keys is more gas-intensive than using bytes32 or uint256.
Recommendation
- Consider using fixed-size identifiers (e.g. hashes of human-readable IDs) as mapping keys.
- Convert between human-readable IDs and hashed IDs off-chain.
6. Positive Observations
- Leverages audited OpenZeppelin contracts (ERC-20, Ownable, Pausable, Permit, ReentrancyGuard).
- Critical public functions interacting with user funds are protected with
nonReentrant. - No usage of low-level
call,delegatecall, orselfdestruct. - No direct handling of raw ETH in this contract, which simplifies the attack surface.
7. Recommended Next Steps
To move toward production readiness and possibly a third-party audit, the following steps are recommended:
- Address high and medium-severity issues (F-01, F-02, F-03) in a new contract version.
- Finalize business rules for over-usage, rewards and station accounting.
- Run automated tools (Slither, Mythril, SolidityScan, Pessimistic, Sherlock AI) on the updated code.
- Add comprehensive unit tests for charging flows, refunds and reward accrual.
- Consider an external independent audit once the logic is stable.
8. Conclusion
The current version of the ChargeCoin contract does not exhibit classic low-level
vulnerabilities such as reentrancy via external calls or arithmetic overflows (assuming a
Solidity 0.8.x compiler). However, there are important risks around owner privileges and
incomplete business logic that should be addressed before mainnet deployment.
Fixing the highlighted findings, tightening the escrow and rescue mechanisms, and completing the reward / station flows will significantly improve both the security posture and the trust model of the ChargeCoin ecosystem.